From c89056d7f1f881a3ca610b53a13495d9ea182896 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Thu, 5 Jun 2025 16:21:39 +0800
Subject: [PATCH 1/9] edit
---
src/components/editor/CustomEditor.vue | 1670 +++++++++++++++++
src/views/message/inner/panel/PanelFooter.vue | 4 +-
vite.config.ts | 6 +-
3 files changed, 1676 insertions(+), 4 deletions(-)
create mode 100644 src/components/editor/CustomEditor.vue
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
new file mode 100644
index 0000000..27ddf87
--- /dev/null
+++ b/src/components/editor/CustomEditor.vue
@@ -0,0 +1,1670 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
{{ item.nickname }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/message/inner/panel/PanelFooter.vue b/src/views/message/inner/panel/PanelFooter.vue
index dc5f8ad..b80e130 100644
--- a/src/views/message/inner/panel/PanelFooter.vue
+++ b/src/views/message/inner/panel/PanelFooter.vue
@@ -10,6 +10,7 @@ import {
} from '@/store'
import ws from '@/connect'
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
+import CustomEditor from '@/components/editor/CustomEditor.vue'
import { throttle, getVideoImage } from '@/utils/common'
import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
@@ -342,7 +343,8 @@ onMounted(() => {
{
vueJsx({}),
compressPlugin(),
UnoCSS(),
- vueDevTools({
- launchEditor: 'trae',
- })
+ // vueDevTools({
+ // launchEditor: 'trae',
+ // })
],
define: {
__APP_ENV__: env.APP_ENV
From f279248a51639a39e7f4b64a6410191f3d1f0d4c Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Fri, 6 Jun 2025 10:44:17 +0800
Subject: [PATCH 2/9] =?UTF-8?q?feat(=E7=BC=96=E8=BE=91=E5=99=A8):=20?=
=?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=BC=96=E8=BE=91=E5=99=A8=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加对粘贴图片的支持,自动触发上传流程
- 优化图片插入逻辑,保留原始尺寸信息并改进显示效果
- 重构消息内容解析逻辑,完善数据结构
- 移除冗余的文件插入功能,专注于图片处理优化
- 调整编辑器样式,改进图片显示效果
---
src/components/editor/CustomEditor.vue | 202 +++++++++++++++++--------
src/utils/auth.js | 2 +-
2 files changed, 143 insertions(+), 61 deletions(-)
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 27ddf87..1672b92 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -240,6 +240,22 @@ const insertMention = (member) => {
const handlePaste = (event) => {
event.preventDefault()
+ // 检查是否有图片
+ const items = event.clipboardData?.items
+ if (items) {
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].type.indexOf('image') !== -1) {
+ // 获取粘贴的图片文件
+ const file = items[i].getAsFile()
+ if (file) {
+ // 使用现有的上传图片功能处理
+ onUploadFile([file])
+ return
+ }
+ }
+ }
+ }
+
// 获取粘贴的纯文本内容
const text = event.clipboardData?.getData('text/plain') || ''
@@ -295,10 +311,19 @@ const handleKeydown = (event) => {
console.log('editorContent.value', editorContent.value)
console.log('editorHtml.value', editorHtml.value)
// 确保编辑器内容不为空(文本、图片、文件或表情)
- if (editorContent.value.trim() ||
- editorHtml.value.includes('
item.type))
+ console.log('提及用户IDs:', messageData.mentionUids)
+ console.log('引用消息ID:', messageData.quoteId)
+
+ // 继续发送消息
sendMessage()
}
}
@@ -309,16 +334,26 @@ const handleKeydown = (event) => {
// 发送消息
const sendMessage = () => {
console.log('发送消息');
- // 检查编辑器是否有内容:文本、图片、文件或表情
- if (!editorContent.value.trim() &&
- !editorHtml.value.includes('
{
const tempDiv = document.createElement('div')
tempDiv.innerHTML = editorHtml.value
+ // 检查是否有引用元素
+ const quoteElements = tempDiv.querySelectorAll('.editor-quote')
+ let quoteInfo = null
+ if (quoteElements.length > 0 && quoteData.value) {
+ quoteInfo = {
+ msg_id: quoteData.value.msg_id,
+ title: quoteData.value.title,
+ describe: quoteData.value.describe,
+ image: quoteData.value.image
+ }
+ }
+
let textContent = ''
const nodes = Array.from(tempDiv.childNodes)
@@ -368,6 +415,11 @@ const parseEditorContent = () => {
if (node.nodeType === Node.TEXT_NODE) {
textContent += node.textContent
} else if (node.nodeType === Node.ELEMENT_NODE) {
+ // 跳过引用元素的处理,因为我们已经单独处理了
+ if (node.classList.contains('editor-quote')) {
+ return
+ }
+
if (node.classList.contains('mention')) {
const userId = node.getAttribute('data-user-id')
if (userId) {
@@ -377,8 +429,8 @@ const parseEditorContent = () => {
} else if (node.tagName === 'IMG') {
// 处理图片
const src = node.getAttribute('src')
- const width = node.getAttribute('width') || ''
- const height = node.getAttribute('height') || ''
+ const width = node.getAttribute('data-original-width') || node.getAttribute('width') || ''
+ const height = node.getAttribute('data-original-height') || node.getAttribute('height') || ''
const isEmoji = node.classList.contains('editor-emoji')
if (textContent.trim()) {
@@ -407,7 +459,9 @@ const parseEditorContent = () => {
// 处理普通图片
items.push({
type: 3,
- content: src + (width && height ? `?width=${width}&height=${height}` : '')
+ content: src + (width && height ? `?width=${width}&height=${height}` : ''),
+ width: width,
+ height: height
})
}
} else if (node.classList.contains('emoji')) {
@@ -447,11 +501,19 @@ const parseEditorContent = () => {
})
}
- return {
+ // 构建完整的消息数据结构
+ const result = {
items: items.length > 0 ? items : [{ type: 1, content: '' }],
mentionUids,
quoteId: quoteData.value?.msg_id || 0
}
+
+ // 如果有引用信息,添加到结果中
+ if (quoteInfo) {
+ result.quoteInfo = quoteInfo
+ }
+
+ return result
}
// 清空编辑器
@@ -479,17 +541,24 @@ const clearEditor = () => {
// 插入图片
-const insertImage = (url, width, height) => {
+const insertImage = (src, width, height) => {
const selection = window.getSelection()
if (!selection.rangeCount) return
const range = selection.getRangeAt(0)
+
+ // 创建图片元素
const img = document.createElement('img')
- img.src = url
- img.style.maxWidth = '200px'
+ img.src = src
+ img.className = 'editor-image'
+ img.alt = '图片'
img.style.maxHeight = '200px'
- if (width) img.setAttribute('width', width)
- if (height) img.setAttribute('height', height)
+ img.style.maxWidth = '100%'
+ img.style.objectFit = 'contain' // 保持原始比例
+
+ // 存储原始尺寸信息,但不直接设置宽高属性
+ if (width) img.setAttribute('data-original-width', width)
+ if (height) img.setAttribute('data-original-height', height)
range.deleteContents()
range.insertNode(img)
@@ -502,35 +571,6 @@ const insertImage = (url, width, height) => {
handleInput({ target: editorRef.value })
}
-// 插入文件
-const insertFile = (url, fileName, fileSize) => {
- const selection = window.getSelection()
- if (!selection.rangeCount) return
-
- const range = selection.getRangeAt(0)
-
- // 创建文件链接元素
- const fileLink = document.createElement('a')
- fileLink.href = url
- fileLink.target = '_blank'
- fileLink.className = 'editor-file'
- fileLink.textContent = fileName
- fileLink.setAttribute('data-size', formatFileSize(fileSize))
- fileLink.setAttribute('data-url', url) // 添加URL属性用于解析
- fileLink.setAttribute('data-name', fileName) // 添加文件名属性用于解析
- fileLink.setAttribute('data-size-raw', fileSize) // 添加原始文件大小属性用于解析
-
- range.deleteContents()
- range.insertNode(fileLink)
- range.setStartAfter(fileLink)
- range.collapse(true)
- selection.removeAllRanges()
- selection.addRange(range)
-
- editorRef.value.focus()
- handleInput({ target: editorRef.value })
-}
-
// 格式化文件大小
const formatFileSize = (size) => {
if (size < 1024) {
@@ -547,21 +587,61 @@ const formatFileSize = (size) => {
/**
* 文件上传处理
- * @param e 上传事件对象
+ * @param e 上传事件对象或文件数组
*/
- async function onUploadFile(e) {
- let file = e.target.files[0]
-
- e.target.value = null // 清空input,允许再次选择相同文件
-
+// 文件上传处理
+async function onUploadFile(e) {
+ let files;
+
+ // 判断参数类型
+ if (Array.isArray(e)) {
+ // 直接传入的文件数组
+ files = e;
+ } else {
+ // 传入的是事件对象
+ files = e.target.files;
+ e.target.value = null; // 清空input,允许再次选择相同文件
+ }
+
+ // 确保有文件
+ if (!files || files.length === 0) return;
+
+ // 处理第一个文件
+ const file = files[0];
+
console.log("文件类型"+file.type)
if (file.type.indexOf('image/') === 0) {
console.log("进入图片")
- // 处理图片文件 - 立即显示临时消息,然后上传
- let fn = emitCall('image_event', file, () => {})
- emit('editor-event', fn)
-
- return
+ // 创建临时URL
+ const tempUrl = URL.createObjectURL(file);
+
+ // 创建图片对象以获取尺寸
+ const image = new Image();
+ image.src = tempUrl;
+
+ image.onload = () => {
+ // 上传图片到服务器
+ const form = new FormData();
+ form.append('file', file);
+ form.append("source", "fonchain-chat"); // 图片来源标识
+ form.append("urlParam", `width=${image.width}&height=${image.height}`);
+
+ // 先将临时图片插入编辑器,不直接设置宽高,而是传递原始尺寸信息
+ insertImage(tempUrl, image.width, image.height);
+
+ // 上传图片并获取永久URL
+ uploadImg(form).then(({ code, data, message }) => {
+ if (code == 0) {
+ // 上传成功后,可以将临时URL替换为永久URL
+ // 但这里我们不做替换,因为临时URL在当前会话中已足够使用
+ console.log('图片上传成功:', data.ori_url);
+ } else {
+ window['$message'].error(message);
+ }
+ });
+ };
+
+ return;
}
if (file.type.indexOf('video/') === 0) {
@@ -1536,12 +1616,14 @@ const onVoteSubmit = (data) => {
* 添加圆角和鼠标指针样式
*/
.editor-image {
- max-width: 100px;
- max-height: 100px;
+ max-width: 300px;
+ max-height: 200px;
border-radius: 3px;
background-color: #48484d;
margin: 0px 2px;
cursor: pointer;
+ object-fit: contain; /* 保持原始比例 */
+ display: inline-block; /* 确保图片正确显示 */
}
/**
diff --git a/src/utils/auth.js b/src/utils/auth.js
index f4f176c..0383a0a 100644
--- a/src/utils/auth.js
+++ b/src/utils/auth.js
@@ -18,7 +18,7 @@ export function isLoggedIn() {
*/
export function getAccessToken() {
// return storage.get(AccessToken) || ''
- return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22726726265e9af0db370a54ea5ee002b43662d571b84c8468ac15330f79503a5cd5e72282d8bee92749b1a3c1b7fd87ae70b64b90e437e84c1b558c64a35e181b2ecf5db3007680c3607eac1edee7f59d'
+ return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22403363066ad3c046839f7b2cf8a6186da017388f197c0c3b219b1c04e7d986e9774b72664a22a6075cee77da3584b7a2131365913796a5fcabc8f4594284e480a592a84a40a9aa7f5f27c951a53a369c'
}
/**
From 17c136834675da6da785a1ea44ca3127862f0dc2 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Fri, 6 Jun 2025 11:52:55 +0800
Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=B6=88?=
=?UTF-8?q?=E6=81=AF=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91=E5=92=8C=E7=BC=96?=
=?UTF-8?q?=E8=BE=91=E5=99=A8=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在vite配置中启用vueDevTools工具
- 重构PanelFooter.vue中的图片消息发送逻辑,改为直接调用onSendMessage
- 修改CustomEditor.vue的消息发送逻辑,支持分类型处理消息内容
- 增加编辑器引用元素的检查逻辑,避免无效引用
- 优化图片上传后的URL替换逻辑,确保编辑器内容更新
---
src/components/editor/CustomEditor.vue | 104 ++++++++++++------
src/views/message/inner/panel/PanelFooter.vue | 62 +----------
vite.config.ts | 6 +-
3 files changed, 76 insertions(+), 96 deletions(-)
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 1672b92..73b0ff4 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -312,7 +312,16 @@ const handleKeydown = (event) => {
console.log('editorHtml.value', editorHtml.value)
// 确保编辑器内容不为空(文本、图片、文件或表情)
// 由于我们已经在 handleInput 中处理了表情文本,editorContent.value 应该包含表情文本
- if (editorContent.value.trim()) {
+ // if (editorContent.value.trim()) {
+ if (true) {
+ // 检查引用元素是否存在,如果不存在但 quoteData 有值,则清除 quoteData
+ const editor = editorRef.value
+ const quoteElement = editor?.querySelector('.editor-quote')
+ if (!quoteElement && quoteData.value) {
+ console.log('引用元素已被删除,但 quoteData 仍有值,清除 quoteData')
+ quoteData.value = null
+ }
+
// 解析并输出编辑器内容
const messageData = parseEditorContent()
console.log('编辑器内容解析结果:', JSON.stringify(messageData, null, 2))
@@ -335,9 +344,9 @@ const handleKeydown = (event) => {
const sendMessage = () => {
console.log('发送消息');
// 检查编辑器是否有内容:文本内容(包括表情文本)
- if (!editorContent.value.trim()) {
- return
- }
+ // if (!editorContent.value.trim()) {
+ // return
+ // }
console.log('发送消息1');
const messageData = parseEditorContent()
@@ -353,38 +362,51 @@ const sendMessage = () => {
image: quoteData.value.image
} : null
})
-
- if (editingMessage.value) {
- // 编辑消息
- emit('editor-event', {
- event: 'edit_message',
- data: {
- ...messageData,
- msg_id: editingMessage.value.msg_id
- },
- callBack: (success) => {
- if (success) {
- clearEditor()
- editingMessage.value = null
- }
+ messageData.items.forEach(item => {
+ // 处理文本内容
+ if (item.type === 1) {
+ const data={
+ items:[{
+ content:item.content,
+ type:1
+ }],
+ mentionUids:messageData.mentionUids,
+ mentions:[],
+ quoteId:messageData.quoteId,
}
- })
- } else {
- // 发送新消息
- const eventType = messageData.items.length > 1 ? 'mixed_event' :
- messageData.items[0].type === 1 ? 'text_event' :
- messageData.items[0].type + '_event'
- console.log('发送消息2',eventType);
- emit('editor-event', {
- event: eventType,
- data: messageData,
- callBack: (success) => {
- if (success) {
- clearEditor()
+ console.log('发送前',data)
+ emit(
+ 'editor-event',
+ emitCall('text_event', data,(ok)=>{
+ console.log('发送后',ok)
+ })
+ )
+ }else if(item.type === 2){
+ //图片消息
+ }else if(item.type === 3){
+ console.log('发送图片消息')
+ const data={
+ height:0,
+ width:0,
+ size:10000,
+ url:item.content,
}
+ emit(
+ 'editor-event',
+ emitCall(
+ 'image_event',
+ data,
+ (ok) => {
+ // 成功发送后清空编辑器
+ }
+ )
+ )
+ }else if(item.type === 4){
+
}
- })
- }
+ })
+
+
}
// 解析编辑器内容
@@ -505,7 +527,7 @@ const parseEditorContent = () => {
const result = {
items: items.length > 0 ? items : [{ type: 1, content: '' }],
mentionUids,
- quoteId: quoteData.value?.msg_id || 0
+ quoteId: quoteElements.length > 0 && quoteData.value ? quoteData.value.msg_id ||'' : ''
}
// 如果有引用信息,添加到结果中
@@ -632,9 +654,19 @@ async function onUploadFile(e) {
// 上传图片并获取永久URL
uploadImg(form).then(({ code, data, message }) => {
if (code == 0) {
- // 上传成功后,可以将临时URL替换为永久URL
- // 但这里我们不做替换,因为临时URL在当前会话中已足够使用
+ // 上传成功后,将临时URL替换为永久URL
console.log('图片上传成功:', data.ori_url);
+
+ // 查找编辑器中刚插入的图片元素并替换其src为永久URL
+ const editorImages = editorRef.value.querySelectorAll('img.editor-image');
+ // 查找最后插入的图片(通常是最近添加的那个)
+ const lastImage = editorImages[editorImages.length - 1];
+ if (lastImage && lastImage.src === tempUrl) {
+ // 替换为永久URL
+ lastImage.src = data.ori_url;
+ // 触发输入事件更新编辑器内容
+ handleInput({ target: editorRef.value });
+ }
} else {
window['$message'].error(message);
}
diff --git a/src/views/message/inner/panel/PanelFooter.vue b/src/views/message/inner/panel/PanelFooter.vue
index b80e130..bea5429 100644
--- a/src/views/message/inner/panel/PanelFooter.vue
+++ b/src/views/message/inner/panel/PanelFooter.vue
@@ -10,13 +10,13 @@ import {
} from '@/store'
import ws from '@/connect'
import { ServePublishMessage, ServeSendVote } from '@/api/chat'
-import CustomEditor from '@/components/editor/CustomEditor.vue'
import { throttle, getVideoImage } from '@/utils/common'
import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import {scrollToBottom} from '@/utils/dom.ts'
+import CustomEditor from '@/components/editor/CustomEditor.vue'
const userStore = useUserStore()
const talkStore = useTalkStore()
const editorStore = useEditorStore()
@@ -61,11 +61,11 @@ const onSendMessage = (data = {}, callBack: any) => {
}
ServePublishMessage(message)
- .then(({ code, message }) => {
+ .then(({ code, message, msg }) => {
if (code == 200) {
callBack(true)
} else {
- window['$message'].warning(message)
+ window['$message'].warning(message || msg)
}
})
.catch(() => {
@@ -95,60 +95,8 @@ const onSendTextEvent = throttle((value: any) => {
}, 1000)
// 发送图片消息
-const onSendImageEvent = ({ data }) => {
- console.log('onSendImageEvent')
-
- // 先创建一个带有上传ID的临时消息对象,用于显示进度
- const uploadId = `image-${Date.now()}-${Math.floor(Math.random() * 1000)}`
-
- // 创建本地预览URL
- const previewUrl = URL.createObjectURL(data)
-
- // 创建临时消息记录
- const tempMessage = {
- msg_id: uploadId,
- sequence: Date.now(),
- talk_type: props.talk_type,
- msg_type: 3, // 图片消息类型
- user_id: props.uid,
- receiver_id: props.receiver_id,
- is_revoke: 0,
- is_mark: 0,
- is_read: 1,
- content: '',
- created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
- extra: {
- url: previewUrl, // 使用本地预览URL
- size: data.size,
- is_uploading: true,
- upload_id: uploadId,
- percentage: 0
- },
- isCheck: false,
- send_status: 1,
- float: 'right' // 我发送的消息显示在右侧
- }
-
- // 直接添加到对话记录中
- dialogueStore.addDialogueRecord(tempMessage)
- nextTick(()=>{
- scrollToBottom()
- })
- uploadsStore.initUploadFile(
- data,
- props.talk_type,
- props.receiver_id,
- uploadId,
- async (percentage) => {
- dialogueStore.updateUploadProgress(uploadId, percentage)
- },
- async () => {
- // 清理本地预览URL
- URL.revokeObjectURL(previewUrl)
- dialogueStore.batchDelDialogueRecord([uploadId])
-
- }
- )
+const onSendImageEvent = ({ data, callBack }) => {
+ onSendMessage({ type: 'image', ...data }, callBack)
}
// 发送视频消息
diff --git a/vite.config.ts b/vite.config.ts
index 715bc77..83a792e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
vueJsx({}),
compressPlugin(),
UnoCSS(),
- // vueDevTools({
- // launchEditor: 'trae',
- // })
+ vueDevTools({
+ launchEditor: 'trae',
+ })
],
define: {
__APP_ENV__: env.APP_ENV
From b18a6e5432330bd604812e5c8071a93a39711b97 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Fri, 6 Jun 2025 12:00:12 +0800
Subject: [PATCH 4/9] =?UTF-8?q?feat(=E7=BC=96=E8=BE=91=E5=99=A8):=20?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B8=85=E9=99=A4=E4=BA=8B=E4=BB=B6=E5=B8=B8?=
=?UTF-8?q?=E9=87=8F=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8F=90=E5=8F=8A=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
在事件总线常量中添加 editor:clear 事件类型
优化提及功能,确保编辑器获得焦点后光标位置正确
---
src/components/editor/CustomEditor.vue | 14 ++++++++++++++
src/constant/event-bus.ts | 3 ++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 73b0ff4..754f90f 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -783,6 +783,20 @@ const onSubscribeMention = (data) => {
// 确保编辑器获得焦点
editorRef.value?.focus()
+ // 如果编辑器为空或者光标不在编辑器内,将光标移动到编辑器末尾
+ const selection = window.getSelection()
+ if (!selection.rangeCount || !editorRef.value.contains(selection.anchorNode)) {
+ const range = document.createRange()
+ if (editorRef.value.lastChild) {
+ range.setStartAfter(editorRef.value.lastChild)
+ } else {
+ range.setStart(editorRef.value, 0)
+ }
+ range.collapse(true)
+ selection.removeAllRanges()
+ selection.addRange(range)
+ }
+
// 插入@提及
insertMention(data)
}
diff --git a/src/constant/event-bus.ts b/src/constant/event-bus.ts
index a437264..3abbb99 100644
--- a/src/constant/event-bus.ts
+++ b/src/constant/event-bus.ts
@@ -5,5 +5,6 @@ export const enum ContactConst {
export const enum EditorConst {
Mention = 'editor:mention',
Quote = 'editor:quote',
- Edit = 'editor:edit'
+ Edit = 'editor:edit',
+ Clear = 'editor:clear'
}
From 1ff26564c781e8125cdab2cf981bcc60de4dedab Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Fri, 6 Jun 2025 13:43:39 +0800
Subject: [PATCH 5/9] =?UTF-8?q?refactor(editor):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E5=BC=95=E7=94=A8=E6=B6=88=E6=81=AF=E7=9A=84=E7=82=B9=E5=87=BB?=
=?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
使用事件委托统一处理引用消息的点击事件,包括关闭按钮点击和光标定位
移除重复的事件监听器,简化代码结构
修复引用消息ID字段从msg_id改为id的匹配问题
---
src/components/editor/CustomEditor.vue | 62 ++++++++++++++++++++------
1 file changed, 48 insertions(+), 14 deletions(-)
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 754f90f..335f632 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -375,6 +375,7 @@ const sendMessage = () => {
quoteId:messageData.quoteId,
}
console.log('发送前',data)
+ console.log('quoteData',quoteData.value)
emit(
'editor-event',
emitCall('text_event', data,(ok)=>{
@@ -527,7 +528,7 @@ const parseEditorContent = () => {
const result = {
items: items.length > 0 ? items : [{ type: 1, content: '' }],
mentionUids,
- quoteId: quoteElements.length > 0 && quoteData.value ? quoteData.value.msg_id ||'' : ''
+ quoteId: quoteElements.length > 0 && quoteData.value ? quoteData.value.id ||'' : ''
}
// 如果有引用信息,添加到结果中
@@ -840,10 +841,15 @@ const onSubscribeQuote = (data) => {
editor.appendChild(quoteElement)
}
- // 添加关闭按钮点击事件
- const closeBtn = quoteElement.querySelector('.quote-close')
- if (closeBtn) {
- closeBtn.addEventListener('click', () => {
+ // 使用事件委托处理引用元素的所有点击事件
+ quoteElement.addEventListener('click', (e) => {
+ console.log('执行删除',e)
+ // 检查点击的是否是关闭按钮或其内部元素
+ const closeButton = e.target.classList?.contains('quote-close') ? e.target : e.target.closest('.quote-close')
+ if (closeButton) {
+ // 阻止事件冒泡
+ e.stopPropagation()
+
// 移除引用元素
quoteElement.remove()
@@ -859,15 +865,8 @@ const onSubscribeQuote = (data) => {
setTimeout(() => {
editor.focus()
}, 0)
- })
- }
-
- // 注意:不调用saveDraft(),确保引用内容不会被保存到草稿中
-
- // 添加点击整个引用卡片的事件
- quoteElement.addEventListener('click', (e) => {
- // 如果不是点击关闭按钮,则设置光标到引用卡片后面
- if (!e.target.classList.contains('quote-close')) {
+ } else {
+ // 如果不是点击关闭按钮,则设置光标到引用卡片后面
const selection = window.getSelection()
const range = document.createRange()
range.setStartAfter(quoteElement)
@@ -880,6 +879,13 @@ const onSubscribeQuote = (data) => {
}
})
+ // 注意:不调用saveDraft(),确保引用内容不会被保存到草稿中
+
+ // 在同一个事件监听器中处理引用卡片的点击
+ // 已经在上面的事件处理中添加了关闭按钮的处理逻辑
+ // 这里只需要处理非关闭按钮的点击
+ // 注意:由于事件委托的方式,不需要额外添加点击事件监听器
+
// 监听键盘事件,处理删除操作
// 监听键盘事件,处理删除操作
const handleDeleteQuote = function(e) {
@@ -1108,6 +1114,11 @@ onMounted(() => {
bus.subscribe(EditorConst.Quote, onSubscribeQuote)
bus.subscribe(EditorConst.Edit, onSubscribeEdit)
bus.subscribe(EditorConst.Clear, onSubscribeClear)
+
+ // 为编辑器添加点击事件监听器,用于处理引用消息关闭等
+ if (editorRef.value) {
+ editorRef.value.addEventListener('click', handleEditorClick);
+ }
// 点击外部隐藏mention
document.addEventListener('click', (event) => {
@@ -1133,6 +1144,11 @@ onBeforeUnmount(() => {
bus.unsubscribe(EditorConst.Quote, onSubscribeQuote)
bus.unsubscribe(EditorConst.Edit, onSubscribeEdit)
bus.unsubscribe(EditorConst.Clear, onSubscribeClear)
+
+ // 移除编辑器点击事件监听器
+ if (editorRef.value) {
+ editorRef.value.removeEventListener('click', handleEditorClick);
+ }
// 清理DOM事件监听器
const editor = editorRef.value
@@ -1191,6 +1207,24 @@ const onVoteSubmit = (data) => {
})
isShowVote.value = false
}
+
+// 处理编辑器内部点击事件(用于关闭引用等)
+const handleEditorClick = (event) => {
+ const closeButton = event.target.closest('.quote-close');
+
+ if (closeButton) {
+ const quoteElement = event.target.closest('.editor-quote');
+ if (quoteElement) {
+ quoteElement.remove();
+ quoteData.value = null;
+
+ handleInput({ target: editorRef.value });
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+};
From 7067c42b2b5922ce4c1c39547323ec6831c5fc41 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Fri, 6 Jun 2025 14:49:38 +0800
Subject: [PATCH 6/9] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0Ionicons4?=
=?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=B9=B6=E4=BC=98=E5=8C=96=E7=BC=96=E8=BE=91?=
=?UTF-8?q?=E5=99=A8=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增@vicons/ionicons4依赖用于编辑器发送按钮
- 优化提及列表滚动行为,保持选中项可见
- 支持Ctrl+Enter/Shift+Enter换行功能
- 添加发送按钮和编辑器placeholder提示
- 修复引用消息id字段不一致问题
---
package.json | 1 +
pnpm-lock.yaml | 8 ++
src/components/editor/CustomEditor.vue | 153 +++++++++++++++++++------
vite.config.ts | 6 +-
4 files changed, 129 insertions(+), 39 deletions(-)
diff --git a/package.json b/package.json
index 8c5366b..0b8bf58 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@kangc/v-md-editor": "^2.3.18",
"@onlyoffice/document-editor-vue": "^1.5.0",
"@vicons/fluent": "^0.13.0",
+ "@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^10.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 168cebb..e186607 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
'@vicons/fluent':
specifier: ^0.13.0
version: 0.13.0
+ '@vicons/ionicons4':
+ specifier: ^0.13.0
+ version: 0.13.0
'@vicons/ionicons5':
specifier: ^0.13.0
version: 0.13.0
@@ -1002,6 +1005,9 @@ packages:
'@vicons/fluent@0.13.0':
resolution: {integrity: sha512-bYGZsOE3qzvm3Cm43e7tybgGlr5ZUpYqtRZq0g0Tfupe8jIzLolpvQLNUt1zS8Mgt6goTbUk5YH7Fkv16jkykg==}
+ '@vicons/ionicons4@0.13.0':
+ resolution: {integrity: sha512-5WHIl/4R5a4i9GONa+hIQWxg/WczrbsCdqxawHZvdd3drsEr+Q3yzlfS+NNRO4WS3uDW2uWLCwoW+yp5TgcKeQ==}
+
'@vicons/ionicons5@0.13.0':
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
@@ -4411,6 +4417,8 @@ snapshots:
'@vicons/fluent@0.13.0': {}
+ '@vicons/ionicons4@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))':
diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue
index 335f632..fe75735 100644
--- a/src/components/editor/CustomEditor.vue
+++ b/src/components/editor/CustomEditor.vue
@@ -15,7 +15,7 @@ import { uploadImg } from '@/api/upload'
import { defAvatar } from '@/constant/default'
import { getImageInfo } from '@/utils/functions'
import MeEditorEmoticon from './MeEditorEmoticon.vue'
-
+import {IosSend} from '@vicons/ionicons4'
const props = defineProps({
vote: {
type: Boolean,
@@ -24,6 +24,10 @@ const props = defineProps({
members: {
type: Array,
default: () => []
+ },
+ placeholder: {
+ type: String,
+ default: 'Enter-发送消息 [Ctrl+Enter/Shift+Enter]-换行'
}
})
@@ -284,10 +288,34 @@ const handleKeydown = (event) => {
case 'ArrowUp':
event.preventDefault()
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
case 'ArrowDown':
event.preventDefault()
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
case 'Enter':
case 'Tab':
@@ -302,14 +330,44 @@ const handleKeydown = (event) => {
}
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时才发送)
- if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey) {
+ // 处理Ctrl+Enter或Shift+Enter换行
+ 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发送消息');
- event.preventDefault()
- console.log('editorContent.value', editorContent.value)
- console.log('editorHtml.value', editorHtml.value)
+ event.preventDefault();
+ console.log('editorContent.value', editorContent.value);
+ console.log('editorHtml.value', editorHtml.value);
// 确保编辑器内容不为空(文本、图片、文件或表情)
// 由于我们已经在 handleInput 中处理了表情文本,editorContent.value 应该包含表情文本
// if (editorContent.value.trim()) {
@@ -356,7 +414,7 @@ const sendMessage = () => {
mentionUids: messageData.mentionUids,
quoteId: messageData.quoteId,
quoteData: quoteData.value ? {
- msg_id: quoteData.value.msg_id,
+ id: quoteData.value.id,
title: quoteData.value.title,
describe: quoteData.value.describe,
image: quoteData.value.image
@@ -405,6 +463,7 @@ const sendMessage = () => {
}else if(item.type === 4){
}
+ clearEditor()
})
@@ -424,7 +483,7 @@ const parseEditorContent = () => {
let quoteInfo = null
if (quoteElements.length > 0 && quoteData.value) {
quoteInfo = {
- msg_id: quoteData.value.msg_id,
+ id: quoteData.value.id,
title: quoteData.value.title,
describe: quoteData.value.describe,
image: quoteData.value.image
@@ -1241,7 +1300,7 @@ const handleEditorClick = (event) => {
包含各种编辑工具按钮
-->