feat(editor): 添加Ionicons4图标并优化编辑器功能
- 新增@vicons/ionicons4依赖用于编辑器发送按钮 - 优化提及列表滚动行为,保持选中项可见 - 支持Ctrl+Enter/Shift+Enter换行功能 - 添加发送按钮和编辑器placeholder提示 - 修复引用消息id字段不一致问题
This commit is contained in:
parent
1ff26564c7
commit
7067c42b2b
@ -20,6 +20,7 @@
|
|||||||
"@kangc/v-md-editor": "^2.3.18",
|
"@kangc/v-md-editor": "^2.3.18",
|
||||||
"@onlyoffice/document-editor-vue": "^1.5.0",
|
"@onlyoffice/document-editor-vue": "^1.5.0",
|
||||||
"@vicons/fluent": "^0.13.0",
|
"@vicons/fluent": "^0.13.0",
|
||||||
|
"@vicons/ionicons4": "^0.13.0",
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"@vueuse/core": "^10.7.0",
|
"@vueuse/core": "^10.7.0",
|
||||||
|
@ -26,6 +26,9 @@ importers:
|
|||||||
'@vicons/fluent':
|
'@vicons/fluent':
|
||||||
specifier: ^0.13.0
|
specifier: ^0.13.0
|
||||||
version: 0.13.0
|
version: 0.13.0
|
||||||
|
'@vicons/ionicons4':
|
||||||
|
specifier: ^0.13.0
|
||||||
|
version: 0.13.0
|
||||||
'@vicons/ionicons5':
|
'@vicons/ionicons5':
|
||||||
specifier: ^0.13.0
|
specifier: ^0.13.0
|
||||||
version: 0.13.0
|
version: 0.13.0
|
||||||
@ -1002,6 +1005,9 @@ packages:
|
|||||||
'@vicons/fluent@0.13.0':
|
'@vicons/fluent@0.13.0':
|
||||||
resolution: {integrity: sha512-bYGZsOE3qzvm3Cm43e7tybgGlr5ZUpYqtRZq0g0Tfupe8jIzLolpvQLNUt1zS8Mgt6goTbUk5YH7Fkv16jkykg==}
|
resolution: {integrity: sha512-bYGZsOE3qzvm3Cm43e7tybgGlr5ZUpYqtRZq0g0Tfupe8jIzLolpvQLNUt1zS8Mgt6goTbUk5YH7Fkv16jkykg==}
|
||||||
|
|
||||||
|
'@vicons/ionicons4@0.13.0':
|
||||||
|
resolution: {integrity: sha512-5WHIl/4R5a4i9GONa+hIQWxg/WczrbsCdqxawHZvdd3drsEr+Q3yzlfS+NNRO4WS3uDW2uWLCwoW+yp5TgcKeQ==}
|
||||||
|
|
||||||
'@vicons/ionicons5@0.13.0':
|
'@vicons/ionicons5@0.13.0':
|
||||||
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
|
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
|
||||||
|
|
||||||
@ -4411,6 +4417,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vicons/fluent@0.13.0': {}
|
'@vicons/fluent@0.13.0': {}
|
||||||
|
|
||||||
|
'@vicons/ionicons4@0.13.0': {}
|
||||||
|
|
||||||
'@vicons/ionicons5@0.13.0': {}
|
'@vicons/ionicons5@0.13.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@3.1.0(vite@6.3.5(@types/node@18.19.99)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.2))(vue@3.5.13(typescript@5.2.2))':
|
'@vitejs/plugin-vue-jsx@3.1.0(vite@6.3.5(@types/node@18.19.99)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.2))(vue@3.5.13(typescript@5.2.2))':
|
||||||
|
@ -15,7 +15,7 @@ import { uploadImg } from '@/api/upload'
|
|||||||
import { defAvatar } from '@/constant/default'
|
import { defAvatar } from '@/constant/default'
|
||||||
import { getImageInfo } from '@/utils/functions'
|
import { getImageInfo } from '@/utils/functions'
|
||||||
import MeEditorEmoticon from './MeEditorEmoticon.vue'
|
import MeEditorEmoticon from './MeEditorEmoticon.vue'
|
||||||
|
import {IosSend} from '@vicons/ionicons4'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vote: {
|
vote: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -24,6 +24,10 @@ const props = defineProps({
|
|||||||
members: {
|
members: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Enter-发送消息 [Ctrl+Enter/Shift+Enter]-换行'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -284,10 +288,34 @@ const handleKeydown = (event) => {
|
|||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
selectedMentionIndex.value = Math.max(0, selectedMentionIndex.value - 1)
|
selectedMentionIndex.value = Math.max(0, selectedMentionIndex.value - 1)
|
||||||
|
// 确保选中项可见 - 向上滚动
|
||||||
|
nextTick(() => {
|
||||||
|
const mentionList = document.querySelector('.mention-list ul')
|
||||||
|
const selectedItem = mentionList?.children[selectedMentionIndex.value]
|
||||||
|
if (mentionList && selectedItem) {
|
||||||
|
// 如果选中项在可视区域上方,滚动到选中项
|
||||||
|
if (selectedItem.offsetTop < mentionList.scrollTop) {
|
||||||
|
mentionList.scrollTop = selectedItem.offsetTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
selectedMentionIndex.value = Math.min(mentionList.value.length - 1, selectedMentionIndex.value + 1)
|
selectedMentionIndex.value = Math.min(mentionList.value.length - 1, selectedMentionIndex.value + 1)
|
||||||
|
// 确保选中项可见 - 向下滚动
|
||||||
|
nextTick(() => {
|
||||||
|
const mentionList = document.querySelector('.mention-list ul')
|
||||||
|
const selectedItem = mentionList?.children[selectedMentionIndex.value]
|
||||||
|
if (mentionList && selectedItem) {
|
||||||
|
// 如果选中项在可视区域下方,滚动到选中项
|
||||||
|
const itemBottom = selectedItem.offsetTop + selectedItem.offsetHeight
|
||||||
|
const listBottom = mentionList.scrollTop + mentionList.clientHeight
|
||||||
|
if (itemBottom > listBottom) {
|
||||||
|
mentionList.scrollTop = itemBottom - mentionList.clientHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
@ -302,14 +330,44 @@ const handleKeydown = (event) => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('键盘事件:', event.key, 'Ctrl:', event.ctrlKey, 'Meta:', event.metaKey);
|
console.log('键盘事件:', event.key, 'Ctrl:', event.ctrlKey, 'Meta:', event.metaKey, 'Shift:', event.shiftKey);
|
||||||
|
|
||||||
// 处理Enter键发送消息(只有在没有按Ctrl/Cmd时才发送)
|
// 处理Ctrl+Enter或Shift+Enter换行
|
||||||
if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey) {
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey || event.shiftKey)) {
|
||||||
|
console.log('Ctrl+Enter或Shift+Enter换行');
|
||||||
|
// 不阻止默认行为,允许插入换行符
|
||||||
|
// 手动插入换行符
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const br = document.createElement('br');
|
||||||
|
range.deleteContents();
|
||||||
|
range.insertNode(br);
|
||||||
|
|
||||||
|
// 在换行符后添加一个空文本节点,并将光标移动到这个节点
|
||||||
|
const textNode = document.createTextNode('');
|
||||||
|
range.setStartAfter(br);
|
||||||
|
range.insertNode(textNode);
|
||||||
|
range.setStartAfter(textNode);
|
||||||
|
range.collapse(true);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
// 触发输入事件更新编辑器内容
|
||||||
|
handleInput({ target: editorRef.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阻止默认行为,防止触发表单提交
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理Enter键发送消息(只有在没有按Ctrl/Cmd/Shift时才发送)
|
||||||
|
if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||||
console.log('Enter发送消息');
|
console.log('Enter发送消息');
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
console.log('editorContent.value', editorContent.value)
|
console.log('editorContent.value', editorContent.value);
|
||||||
console.log('editorHtml.value', editorHtml.value)
|
console.log('editorHtml.value', editorHtml.value);
|
||||||
// 确保编辑器内容不为空(文本、图片、文件或表情)
|
// 确保编辑器内容不为空(文本、图片、文件或表情)
|
||||||
// 由于我们已经在 handleInput 中处理了表情文本,editorContent.value 应该包含表情文本
|
// 由于我们已经在 handleInput 中处理了表情文本,editorContent.value 应该包含表情文本
|
||||||
// if (editorContent.value.trim()) {
|
// if (editorContent.value.trim()) {
|
||||||
@ -356,7 +414,7 @@ const sendMessage = () => {
|
|||||||
mentionUids: messageData.mentionUids,
|
mentionUids: messageData.mentionUids,
|
||||||
quoteId: messageData.quoteId,
|
quoteId: messageData.quoteId,
|
||||||
quoteData: quoteData.value ? {
|
quoteData: quoteData.value ? {
|
||||||
msg_id: quoteData.value.msg_id,
|
id: quoteData.value.id,
|
||||||
title: quoteData.value.title,
|
title: quoteData.value.title,
|
||||||
describe: quoteData.value.describe,
|
describe: quoteData.value.describe,
|
||||||
image: quoteData.value.image
|
image: quoteData.value.image
|
||||||
@ -405,6 +463,7 @@ const sendMessage = () => {
|
|||||||
}else if(item.type === 4){
|
}else if(item.type === 4){
|
||||||
|
|
||||||
}
|
}
|
||||||
|
clearEditor()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -424,7 +483,7 @@ const parseEditorContent = () => {
|
|||||||
let quoteInfo = null
|
let quoteInfo = null
|
||||||
if (quoteElements.length > 0 && quoteData.value) {
|
if (quoteElements.length > 0 && quoteData.value) {
|
||||||
quoteInfo = {
|
quoteInfo = {
|
||||||
msg_id: quoteData.value.msg_id,
|
id: quoteData.value.id,
|
||||||
title: quoteData.value.title,
|
title: quoteData.value.title,
|
||||||
describe: quoteData.value.describe,
|
describe: quoteData.value.describe,
|
||||||
image: quoteData.value.image
|
image: quoteData.value.image
|
||||||
@ -1241,7 +1300,7 @@ const handleEditorClick = (event) => {
|
|||||||
包含各种编辑工具按钮
|
包含各种编辑工具按钮
|
||||||
-->
|
-->
|
||||||
<header class="el-header toolbar bdr-t">
|
<header class="el-header toolbar bdr-t">
|
||||||
<div class="tools">
|
<div class="tools pr-30px">
|
||||||
<!--
|
<!--
|
||||||
表情选择器弹出框
|
表情选择器弹出框
|
||||||
使用n-popover组件创建悬浮的表情选择面板
|
使用n-popover组件创建悬浮的表情选择面板
|
||||||
@ -1292,6 +1351,14 @@ const handleEditorClick = (event) => {
|
|||||||
<n-icon size="18" class="icon" :component="nav.icon" />
|
<n-icon size="18" class="icon" :component="nav.icon" />
|
||||||
<p class="tip-title">{{ nav.title }}</p>
|
<p class="tip-title">{{ nav.title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<n-button class="w-80px h-30px ml-auto" type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<IosSend />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
发送
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -1325,37 +1392,25 @@ const handleEditorClick = (event) => {
|
|||||||
通过v-if="showMention"控制显示/隐藏
|
通过v-if="showMention"控制显示/隐藏
|
||||||
使用动态样式定位列表位置
|
使用动态样式定位列表位置
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showMention"
|
v-if="showMention"
|
||||||
class="mention-list py-5px"
|
class="mention-list py-5px"
|
||||||
:style="{ top: mentionPosition.top + 'px', left: mentionPosition.left + 'px' }"
|
:style="{ top: mentionPosition.top + 'px', left: mentionPosition.left + 'px' }"
|
||||||
>
|
>
|
||||||
<n-virtual-list style="max-height: 140px;width: 163px;" :item-size="42" :items="mentionList">
|
<ul class="max-h-140px w-163px overflow-auto hide-scrollbar">
|
||||||
<template #default="{ item }">
|
<li
|
||||||
<div :key="item.key" class="cursor-pointer px-14px h-42px hover:bg-#EEE9F9" @mousedown.prevent="insertMention(item)">
|
|
||||||
<div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 flex items-center h-full ">
|
|
||||||
<img class="w-26px h-26px rounded-50% mr-11px" :src="item.avatar" alt="">
|
|
||||||
<span> {{ item.nickname }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-virtual-list>
|
|
||||||
<ul>
|
|
||||||
<!--
|
|
||||||
提及列表项
|
|
||||||
循环渲染mentionList数组中的用户
|
|
||||||
通过:class="{ selected: index === selectedMentionIndex }"高亮当前选中项
|
|
||||||
@mousedown.prevent防止失去焦点
|
|
||||||
-->
|
|
||||||
<!-- <li
|
|
||||||
v-for="(member, index) in mentionList"
|
v-for="(member, index) in mentionList"
|
||||||
:key="member.user_id"
|
:key="member.user_id || member.id"
|
||||||
:class="{ selected: index === selectedMentionIndex }"
|
class="cursor-pointer px-14px h-42px"
|
||||||
|
:class="{ 'bg-#EEE9F9': index === selectedMentionIndex }"
|
||||||
@mousedown.prevent="insertMention(member)"
|
@mousedown.prevent="insertMention(member)"
|
||||||
|
@mouseover="selectedMentionIndex = index"
|
||||||
>
|
>
|
||||||
{{ member.nickname }}
|
<div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full">
|
||||||
</li> -->
|
<img class="w-26px h-26px rounded-50% mr-11px" :src="member.avatar" alt="">
|
||||||
|
<span>{{ member.nickname }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@ -1399,9 +1454,10 @@ const handleEditorClick = (event) => {
|
|||||||
* 使用flex布局排列工具按钮
|
* 使用flex布局排列工具按钮
|
||||||
*/
|
*/
|
||||||
.tools {
|
.tools {
|
||||||
height: 100%;
|
height: 40px;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个工具按钮样式
|
* 单个工具按钮样式
|
||||||
@ -1615,6 +1671,13 @@ const handleEditorClick = (event) => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* 添加placeholder样式 */
|
||||||
|
&:empty:before {
|
||||||
|
content: attr(placeholder);
|
||||||
|
color: #999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义滚动条样式 - 轨道
|
* 自定义滚动条样式 - 轨道
|
||||||
* 使用::-webkit-scrollbar伪元素自定义滚动条轨道
|
* 使用::-webkit-scrollbar伪元素自定义滚动条轨道
|
||||||
@ -1829,4 +1892,22 @@ html[theme-mode='dark'] {
|
|||||||
--tip-bg-color: #48484d;
|
--tip-bg-color: #48484d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏滚动条样式
|
||||||
|
* 保留滚动功能但隐藏滚动条的视觉显示
|
||||||
|
*/
|
||||||
|
.hide-scrollbar {
|
||||||
|
/* Chrome, Safari, Edge */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
/* IE */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
vueJsx({}),
|
vueJsx({}),
|
||||||
compressPlugin(),
|
compressPlugin(),
|
||||||
UnoCSS(),
|
UnoCSS(),
|
||||||
vueDevTools({
|
// vueDevTools({
|
||||||
launchEditor: 'trae',
|
// launchEditor: 'trae',
|
||||||
})
|
// })
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
__APP_ENV__: env.APP_ENV
|
__APP_ENV__: env.APP_ENV
|
||||||
|
Loading…
Reference in New Issue
Block a user