From 9503fbe78a5a90038ffb5916a5c1506cdde264d1 Mon Sep 17 00:00:00 2001
From: wangyifeng <812766448@qq.com>
Date: Mon, 23 Jun 2025 17:07:48 +0800
Subject: [PATCH 01/22] =?UTF-8?q?=E5=A4=84=E7=90=86=E8=A7=A3=E6=95=A3?=
=?UTF-8?q?=E7=BE=A4=E8=81=8A=E5=92=8C=E8=A2=AB=E7=A7=BB=E5=87=BA=E7=BE=A4?=
=?UTF-8?q?=E8=81=8A=E5=8F=B3=E4=B8=8A=E8=A7=92=E7=BE=A4=E8=81=8A=E8=AE=BE?=
=?UTF-8?q?=E7=BD=AE=E6=8C=89=E9=92=AE=E7=9A=84=E7=83=AD=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/event/talk.js | 13 +++++++++++++
src/views/message/inner/panel/PanelHeader.vue | 7 ++++++-
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/event/talk.js b/src/event/talk.js
index 8a1db18..1c79ab6 100644
--- a/src/event/talk.js
+++ b/src/event/talk.js
@@ -186,7 +186,14 @@ class Talk extends Base {
//群解散时,需要更新群成员权限
if ([1106].includes(record.msg_type)) {
+ //更新会话信息
useDialogueStore().updateDismiss(true)
+ //更新会话列表中的会话信息
+ useTalkStore().updateItem({
+ index_name: this.getIndexName(),
+ is_dismiss: 1,
+ group_member_num: 0
+ })
}
//群成员被移出时,需要更新群成员权限
@@ -197,6 +204,12 @@ class Talk extends Base {
)
if (isMeQuit) {
useDialogueStore().updateQuit(true)
+ //更新会话列表中的会话信息
+ useTalkStore().updateItem({
+ index_name: this.getIndexName(),
+ is_quit: 1,
+ group_member_num: 0
+ })
}
}
}
diff --git a/src/views/message/inner/panel/PanelHeader.vue b/src/views/message/inner/panel/PanelHeader.vue
index 2cf915d..12ac0a7 100644
--- a/src/views/message/inner/panel/PanelHeader.vue
+++ b/src/views/message/inner/panel/PanelHeader.vue
@@ -1,4 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/utils/auth.js b/src/utils/auth.js
index 9683ec7..ea2b8c1 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'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b897a4f2416a772eacd03215226020e2e551cdac98368e42541ee3082dc07317d4ecc6a5dfbbe2a28f8c48ccfae7bc6046c3b9b79c0eb3a1ec4c25f5d766a2f8f01f64da8f70f7dbf63e124ffcf72398d86'
+ return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22f227a3ad4383a2ddef0a2f43b855f869560968fd2dae0412498273e591b78554b2373a17017cdaae7c9ec427325b7d078d54cb00b001c9894c8e1f990747c8db3b62b17eb8ed39e2b2c2b6d63ce26756'
}
/**
diff --git a/src/views/message/inner/panel/PanelFooter.vue b/src/views/message/inner/panel/PanelFooter.vue
index 6ba23fa..7170135 100644
--- a/src/views/message/inner/panel/PanelFooter.vue
+++ b/src/views/message/inner/panel/PanelFooter.vue
@@ -13,6 +13,7 @@ import { ServePublishMessage, ServeSendVote } from '@/api/chat'
import { throttle, getVideoImage } from '@/utils/common'
import { parseTime } from '@/utils/datetime'
import Editor from '@/components/editor/Editor.vue'
+import TiptapEditor from '@/components/editor/TiptapEditor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import {scrollToBottom} from '@/utils/dom.ts'
@@ -294,9 +295,9 @@ onMounted(() => {
{
vueJsx({}),
compressPlugin(),
UnoCSS(),
- vueDevTools({
- launchEditor: 'trae',
- })
+ // vueDevTools({
+ // launchEditor: 'trae',
+ // })
],
define: {
__APP_ENV__: env.APP_ENV
From dd170cb50d0243bfef8351f18502af388409d9b5 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Wed, 2 Jul 2025 13:15:16 +0800
Subject: [PATCH 04/22] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0blockqu?=
=?UTF-8?q?ote=E5=92=8Cemoji=E6=89=A9=E5=B1=95=E5=B9=B6=E9=87=8D=E6=9E=84?=
=?UTF-8?q?=E5=BC=95=E7=94=A8=E5=8D=A1=E7=89=87=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加@tiptap/extension-blockquote和@tiptap/extension-emoji依赖
- 将自定义Quote扩展从Extension改为Node实现
- 简化引用卡片的HTML渲染逻辑
- 改进引用卡片的键盘删除行为
- 优化引用内容的插入位置和格式
---
package.json | 2 +
pnpm-lock.yaml | 49 ++++++++
src/components/editor/TiptapEditor.vue | 155 ++++++++++---------------
3 files changed, 115 insertions(+), 91 deletions(-)
diff --git a/package.json b/package.json
index 92879b1..f0a1f0a 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,8 @@
"@kangc/v-md-editor": "^2.3.18",
"@onlyoffice/document-editor-vue": "^1.5.0",
"@tiptap/core": "^2.23.1",
+ "@tiptap/extension-blockquote": "^2.23.1",
+ "@tiptap/extension-emoji": "^2.23.1",
"@tiptap/extension-image": "^2.23.1",
"@tiptap/extension-link": "^2.23.1",
"@tiptap/extension-mention": "^2.23.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 21901b2..3ded82e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,12 @@ importers:
'@tiptap/core':
specifier: ^2.23.1
version: 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/extension-blockquote':
+ specifier: ^2.23.1
+ version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
+ '@tiptap/extension-emoji':
+ specifier: ^2.23.1
+ version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(@tiptap/suggestion@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(emojibase@16.0.0)
'@tiptap/extension-image':
specifier: ^2.23.1
version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
@@ -928,6 +934,13 @@ packages:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
+ '@tiptap/extension-emoji@2.23.1':
+ resolution: {integrity: sha512-bqTn+hbq0bDIcrPIIjVq3GndJ/PYQfReMDlyTv0mUCtRbP7zReJ1oFx02d25RmwgS6XL3U8WW4kEFomhliwWSQ==}
+ peerDependencies:
+ '@tiptap/core': ^2.7.0
+ '@tiptap/pm': ^2.7.0
+ '@tiptap/suggestion': ^2.7.0
+
'@tiptap/extension-floating-menu@2.23.1':
resolution: {integrity: sha512-GMWkpH+p/OUOk1Y5UGOnKuHSDEVBN7DhYIJiWt5g9LK/mpPeuqoCmQg3RQDgjtZXb74SlxLK2pS/3YcAnemdfQ==}
peerDependencies:
@@ -2013,9 +2026,21 @@ packages:
elkjs@0.9.3:
resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==}
+ emoji-regex@10.4.0:
+ resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ emojibase-data@15.3.2:
+ resolution: {integrity: sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==}
+ peerDependencies:
+ emojibase: '*'
+
+ emojibase@16.0.0:
+ resolution: {integrity: sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ==}
+ engines: {node: '>=18.12.0'}
+
enhanced-resolve@5.18.2:
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
engines: {node: '>=10.13.0'}
@@ -2454,6 +2479,9 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
+ is-emoji-supported@0.0.5:
+ resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==}
+
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
@@ -4548,6 +4576,17 @@ snapshots:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/pm': 2.23.1
+ '@tiptap/extension-emoji@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(@tiptap/suggestion@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(emojibase@16.0.0)':
+ dependencies:
+ '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
+ '@tiptap/pm': 2.23.1
+ '@tiptap/suggestion': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
+ emoji-regex: 10.4.0
+ emojibase-data: 15.3.2(emojibase@16.0.0)
+ is-emoji-supported: 0.0.5
+ transitivePeerDependencies:
+ - emojibase
+
'@tiptap/extension-floating-menu@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
@@ -5877,8 +5916,16 @@ snapshots:
elkjs@0.9.3: {}
+ emoji-regex@10.4.0: {}
+
emoji-regex@8.0.0: {}
+ emojibase-data@15.3.2(emojibase@16.0.0):
+ dependencies:
+ emojibase: 16.0.0
+
+ emojibase@16.0.0: {}
+
enhanced-resolve@5.18.2:
dependencies:
graceful-fs: 4.2.11
@@ -6353,6 +6400,8 @@ snapshots:
is-docker@3.0.0: {}
+ is-emoji-supported@0.0.5: {}
+
is-extendable@0.1.1: {}
is-extendable@1.0.1:
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index eaa0742..0604af1 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -121,110 +121,74 @@ const Emoji = Node.create({
})
// 自定义Quote扩展
-const Quote = Extension.create({
+const Quote = Node.create({
name: 'quote',
-
+ group: 'block',
+ atom: true,
+ draggable: true,
+
addAttributes() {
return {
- id: {
- default: null,
- },
- title: {
- default: null,
- },
- describe: {
- default: null,
- },
- image: {
- default: '',
- },
+ id: { default: null },
+ title: { default: null },
+ describe: { default: null },
+ image: { default: '' }
}
},
-
+
parseHTML() {
- return [
- {
- tag: 'div.quote-card',
- },
- ]
+ return [{ tag: 'div.quote-card' }]
},
-
+
renderHTML({ HTMLAttributes }) {
const { id, title, describe, image } = HTMLAttributes
-
- // 创建引用卡片的HTML结构
- const quoteCardContent = document.createElement('span')
- quoteCardContent.classList.add('quote-card-content')
-
- const close = document.createElement('span')
- close.classList.add('quote-card-remove')
- close.textContent = '×'
- close.addEventListener('click', (e) => {
- e.stopPropagation()
- // 移除引用
- if (editor.value) {
- editor.value.commands.deleteNode('quote')
- }
- })
-
- const quoteCardTitle = document.createElement('span')
- quoteCardTitle.classList.add('quote-card-title')
- quoteCardTitle.textContent = title
- quoteCardTitle.appendChild(close)
-
- quoteCardContent.appendChild(quoteCardTitle)
-
- if (!image || image.length === 0) {
- const quoteCardMeta = document.createElement('span')
- quoteCardMeta.classList.add('quote-card-meta')
- quoteCardMeta.textContent = describe
- quoteCardContent.appendChild(quoteCardMeta)
- } else {
- const iconImg = document.createElement('img')
- iconImg.setAttribute('src', image)
- iconImg.setAttribute('style', 'width:30px;height:30px;margin-right:10px;')
- quoteCardContent.appendChild(iconImg)
+
+ const titleEl = ['span', { class: 'quote-card-title' }, title || '']
+ let contentChildren = [titleEl]
+
+ if (image && image.length > 0) {
+ contentChildren.push(['img', { src: image, style: 'width:30px;height:30px;margin-right:10px;' }])
+ } else if (describe) {
+ contentChildren.push(['span', { class: 'quote-card-meta' }, describe])
}
-
- const node = document.createElement('div')
- node.classList.add('quote-card')
- node.setAttribute('data-id', id)
- node.setAttribute('data-title', title)
- node.setAttribute('data-describe', describe)
- node.setAttribute('data-image', image || '')
- node.setAttribute('contenteditable', 'false')
- node.appendChild(quoteCardContent)
-
- return ['div', { class: 'quote-card-wrapper' }, node]
+
+ const cardContent = ['span', { class: 'quote-card-content' }, ...contentChildren]
+
+ return [
+ 'div',
+ {
+ class: 'quote-card',
+ 'data-id': id,
+ 'data-title': title,
+ 'data-describe': describe,
+ 'data-image': image || '',
+ contenteditable: 'false'
+ },
+ cardContent
+ ]
},
-
+
addKeyboardShortcuts() {
return {
- 'Backspace': () => {
+ Backspace: () => {
const { selection } = this.editor.state
- const { empty, anchor } = selection
-
+ const { $from, empty } = selection
+
if (!empty) {
return false
}
-
- const isAtStart = anchor === 0
-
- if (!isAtStart) {
- return false
+
+ if ($from.parent.isTextblock && $from.parentOffset === 0) {
+ const nodeBefore = $from.nodeBefore
+ if (nodeBefore && nodeBefore.type.name === this.name) {
+ return this.editor.commands.deleteNode(this.name)
+ }
}
-
- // 检查是否有引用节点
- const quoteNode = this.editor.state.doc.firstChild
- if (quoteNode && quoteNode.type.name === 'quote') {
- this.editor.commands.deleteNode('quote')
- return true
- }
-
+
return false
- },
+ }
}
- },
+ }
})
// 创建自定义键盘处理插件,处理Enter键发送消息
@@ -706,18 +670,27 @@ function onSubscribeMention(data) {
*/
function onSubscribeQuote(data) {
if (!editor.value) return
-
+
// 检查是否已有引用内容
const json = editor.value.getJSON()
- if (json.content?.some((node) => node.type === 'quote')) {
- return // 已有引用则不再添加
+ if (json.content?.some(node => node.type === 'quote')) {
+ return // 已有引用则不再添加
}
// 在编辑器开头插入引用
- editor.value.chain().focus().insertContent({
- type: 'quote',
- attrs: data,
- }).run()
+ editor.value
+ .chain()
+ .focus()
+ .insertContentAt(0, [
+ {
+ type: 'quote',
+ attrs: data
+ },
+ {
+ type: 'paragraph'
+ }
+ ])
+ .run()
}
/**
From 8be8afc6751e980ca769ffb0275faa8832420a9c Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Wed, 2 Jul 2025 13:34:23 +0800
Subject: [PATCH 05/22] =?UTF-8?q?refactor(TiptapEditor):=20=E9=87=8D?=
=?UTF-8?q?=E6=9E=84=E6=B6=88=E6=81=AF=E8=BD=AC=E6=8D=A2=E9=80=BB=E8=BE=91?=
=?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E5=A4=9A=E6=B6=88=E6=81=AF=E5=88=86?=
=?UTF-8?q?=E6=AE=B5=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重构 tiptapToMessage 函数,将单条消息处理改为支持多条消息分段处理
优化消息内容处理流程,添加文本缓冲区和图片单独处理逻辑
简化消息发送逻辑,移除 msgType 判断改为直接处理不同类型消息
清理已注释的导航功能代码
---
src/components/editor/TiptapEditor.vue | 261 ++++++++++++-------------
1 file changed, 121 insertions(+), 140 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 0604af1..e19da1e 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -457,71 +457,106 @@ function onRecorderEvent(file) {
// 将Tiptap内容转换为消息格式
function tiptapToMessage() {
- if (!editor.value) return { items: [], mentions: [], mentionUids: [], quoteId: '', msgType: 1 }
-
+ if (!editor.value) return []
+
const json = editor.value.getJSON()
- const resp = {
- items: [],
- mentions: [],
- mentionUids: [],
- quoteId: '',
- msgType: 1
- }
-
- // 处理引用
- const quoteNode = json.content?.find((node) => node.type === 'quote')
- if (quoteNode) {
- resp.quoteId = quoteNode.attrs.id
- }
-
- // 处理内容
- let textContent = ''
- let hasImage = false
-
- const processNode = (node) => {
- if (node.type === 'text') {
- textContent += node.text
- } else if (node.type === 'mention') {
- textContent += ` @${node.attrs.label} `
- resp.mentions.push({
- name: `@${node.attrs.label}`,
- atid: parseInt(node.attrs.id)
- })
- } else if (node.type === 'emoji') {
- textContent += node.attrs.alt
- } else if (node.type === 'image') {
- hasImage = true
- resp.items.push({
- type: 3,
- content: node.attrs.src
- })
- } else if (node.content) {
- node.content.forEach(processNode)
+ const messages = []
+ let quoteId = null
+ let currentTextBuffer = ''
+ let currentMentions = []
+ let currentMentionUids = new Set()
+
+ const flushTextBuffer = () => {
+ const content = currentTextBuffer.trim()
+ if (content) {
+ const data = {
+ items: [{ type: 1, content: content }],
+ mentions: [...currentMentions],
+ mentionUids: Array.from(currentMentionUids)
+ }
+ if (quoteId) {
+ data.quoteId = quoteId
+ quoteId = null
+ }
+ messages.push({ type: 'text', data })
}
+ currentTextBuffer = ''
+ currentMentions = []
+ currentMentionUids.clear()
}
-
- if (json.content) {
- json.content.forEach(processNode)
- }
-
- // 如果有文本内容,添加到items
- if (textContent.trim()) {
- resp.items.unshift({
- type: 1,
- content: textContent.trim()
+
+ const processInlines = nodes => {
+ nodes.forEach(node => {
+ if (node.type === 'text') {
+ currentTextBuffer += node.text
+ } else if (node.type === 'mention') {
+ currentTextBuffer += `@${node.attrs.label} `
+ const uid = parseInt(node.attrs.id)
+ if (!currentMentionUids.has(uid)) {
+ currentMentionUids.add(uid)
+ currentMentions.push({ name: `@${node.attrs.label}`, atid: uid })
+ }
+ } else if (node.type === 'emoji') {
+ currentTextBuffer += node.attrs.alt
+ } else if (node.type === 'hardBreak') {
+ currentTextBuffer += '\n'
+ } else if (node.type === 'image') {
+ // 处理段落内的图片
+ flushTextBuffer()
+ const data = {
+ ...getImageInfo(node.attrs.src),
+ url: node.attrs.src
+ }
+ if (quoteId) {
+ data.quoteId = quoteId
+ quoteId = null
+ }
+ messages.push({ type: 'image', data })
+ }
})
}
-
- // 设置消息类型
- if (resp.items.length > 1) {
- resp.msgType = 12 // 混合消息
- } else if (resp.items.length === 1) {
- resp.msgType = resp.items[0].type
+
+ if (json.content) {
+ const quoteIndex = json.content.findIndex(node => node.type === 'quote')
+ if (quoteIndex > -1) {
+ quoteId = json.content[quoteIndex].attrs.id
+ json.content.splice(quoteIndex, 1)
+ }
+
+ json.content.forEach(node => {
+ if (node.type === 'paragraph') {
+ if (node.content) {
+ processInlines(node.content)
+ }
+ currentTextBuffer += '\n' // Add newline after each paragraph
+ } else if (node.type === 'image') {
+ flushTextBuffer()
+ const data = {
+ ...getImageInfo(node.attrs.src),
+ url: node.attrs.src
+ }
+ if (quoteId) {
+ data.quoteId = quoteId
+ quoteId = null
+ }
+ messages.push({ type: 'image', data })
+ }
+ })
}
-
- resp.mentionUids = resp.mentions.map((item) => item.atid)
-
- return resp
+
+ flushTextBuffer()
+
+ if (messages.length > 0) {
+ const lastMessage = messages[messages.length - 1]
+ if (lastMessage.type === 'text') {
+ lastMessage.data.items[0].content = lastMessage.data.items[0].content.trim()
+ if (!lastMessage.data.items[0].content) {
+ messages.pop()
+ }
+ }
+ }
+
+ return messages
}
// 将Tiptap内容转换为纯文本
@@ -550,52 +585,36 @@ function isEditorEmpty() {
* 根据编辑器内容类型发送不同类型的消息
*/
function onSendMessage() {
- if (!editor.value) return
-
- if (isEditorEmpty()) return
-
- const data = tiptapToMessage()
-
- if (data.items.length === 0 || (data.items[0].type === 1 && !data.items[0].content.trim())) {
- return // 没有内容不发送
+ if (!editor.value || isEditorEmpty()) return
+
+ const messages = tiptapToMessage()
+
+ if (messages.length === 0) {
+ return
}
- switch (data.msgType) {
- case 1: // 文字消息
- if (data.items[0].content.length > 1024) {
- return window['$message'].info('发送内容超长,请分条发送')
+ let canClear = true
+ messages.forEach(msg => {
+ if (msg.type === 'text') {
+ if (msg.data.items[0].content.length > 1024) {
+ window['$message'].info('发送内容超长,请分条发送')
+ canClear = false
+ return
}
+ emit('editor-event', emitCall('text_event', msg.data))
+ } else if (msg.type === 'image') {
+ const data = {
+ height: 0,
+ width: 0,
+ size: 10000,
+ url: msg.data.url,
+ }
+ emit('editor-event', emitCall('image_event', data))
+ }
+ })
- // 发送文本消息
- emit(
- 'editor-event',
- emitCall('text_event', data, (ok) => {
- ok && editor.value?.commands.clearContent(true) // 成功发送后清空编辑器
- })
- )
- break
- case 3: // 图片消息
- // 发送图片消息
- emit(
- 'editor-event',
- emitCall(
- 'image_event',
- { ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
- (ok) => {
- ok && editor.value?.commands.clearContent(true) // 成功发送后清空编辑器
- }
- )
- )
- break
- case 12: // 图文混合消息
- // 发送混合消息
- emit(
- 'editor-event',
- emitCall('mixed_event', data, (ok) => {
- ok && editor.value?.commands.clearContent(true) // 成功发送后清空编辑器
- })
- )
- break
+ if (canClear) {
+ editor.value?.commands.clearContent(true)
}
}
@@ -728,45 +747,7 @@ const navs = reactive([
uploadFileRef.value.click() // 触发文件上传
}
},
- // 以下功能已被注释掉,但保留代码
- // {
- // title: '代码',
- // icon: markRaw(SourceCode),
- // show: true,
- // click: () => {
- // isShowEditorCode.value = true
- // }
- // },
- // {
- // title: '语音消息',
- // icon: markRaw(IconVoice),
- // show: true,
- // click: () => {
- // isShowEditorRecorder.value = true
- // }
- // },
- // {
- // title: '地理位置',
- // icon: markRaw(Local),
- // show: true,
- // click: () => {}
- // },
- // {
- // title: '群投票',
- // icon: markRaw(Ranking),
- // show: computed(() => props.vote),
- // click: () => {
- // isShowEditorVote.value = true
- // }
- // },
- // {
- // title: '历史记录',
- // icon: markRaw(History),
- // show: true,
- // click: () => {
- // emit('editor-event', emitCall('history_event'))
- // }
- // }
+
])
// 监听聊天索引变化,切换聊天时加载对应草稿
From 0f161de28f1ddfa1f85327b47256a28236e34edf Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Wed, 2 Jul 2025 15:57:03 +0800
Subject: [PATCH 06/22] =?UTF-8?q?feat(editor):=20=E9=87=8D=E6=9E=84?=
=?UTF-8?q?=E5=BC=95=E7=94=A8=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除旧的Quote节点扩展,改为使用quoteData状态管理引用消息
- 添加图片上传状态跟踪和加载指示器
- 优化提及列表的交互和关闭行为
- 支持粘贴图片自动上传功能
- 完善编辑器草稿保存机制,包含引用数据
---
src/components/editor/TiptapEditor.vue | 476 +++++++++++++++++--------
1 file changed, 319 insertions(+), 157 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index e19da1e..77a8996 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -12,7 +12,7 @@ import { Plugin, PluginKey } from '@tiptap/pm/state'
// 引入Vue核心功能
import { reactive, watch, ref, markRaw, computed, onMounted, onUnmounted, shallowRef } from 'vue'
// 引入Naive UI的弹出框组件
-import { NPopover } from 'naive-ui'
+import { NPopover, NIcon } from 'naive-ui'
// 引入图标组件
import {
Voice as IconVoice, // 语音图标
@@ -22,7 +22,8 @@ import {
Pic, // 图片图标
FolderUpload, // 文件上传图标
Ranking, // 排名图标(用于投票)
- History // 历史记录图标
+ History, // 历史记录图标
+ Close // 关闭图标
} from '@icon-park/vue-next'
// 引入状态管理
@@ -70,6 +71,7 @@ const isShowEditorVote = ref(false)
const isShowEditorCode = ref(false)
// 控制是否显示录音界面
const isShowEditorRecorder = ref(false)
+const uploadingImages = ref(new Map())
// 图片文件上传DOM引用
const fileImageRef = ref()
// 文件上传DOM引用
@@ -78,6 +80,8 @@ const uploadFileRef = ref()
const emoticonRef = ref()
// 表情面板显示状态
const showEmoticon = ref(false)
+// 引用消息数据
+const quoteData = ref(null)
// 自定义Emoji扩展
const Emoji = Node.create({
@@ -120,76 +124,7 @@ const Emoji = Node.create({
},
})
-// 自定义Quote扩展
-const Quote = Node.create({
- name: 'quote',
- group: 'block',
- atom: true,
- draggable: true,
- addAttributes() {
- return {
- id: { default: null },
- title: { default: null },
- describe: { default: null },
- image: { default: '' }
- }
- },
-
- parseHTML() {
- return [{ tag: 'div.quote-card' }]
- },
-
- renderHTML({ HTMLAttributes }) {
- const { id, title, describe, image } = HTMLAttributes
-
- const titleEl = ['span', { class: 'quote-card-title' }, title || '']
- let contentChildren = [titleEl]
-
- if (image && image.length > 0) {
- contentChildren.push(['img', { src: image, style: 'width:30px;height:30px;margin-right:10px;' }])
- } else if (describe) {
- contentChildren.push(['span', { class: 'quote-card-meta' }, describe])
- }
-
- const cardContent = ['span', { class: 'quote-card-content' }, ...contentChildren]
-
- return [
- 'div',
- {
- class: 'quote-card',
- 'data-id': id,
- 'data-title': title,
- 'data-describe': describe,
- 'data-image': image || '',
- contenteditable: 'false'
- },
- cardContent
- ]
- },
-
- addKeyboardShortcuts() {
- return {
- Backspace: () => {
- const { selection } = this.editor.state
- const { $from, empty } = selection
-
- if (!empty) {
- return false
- }
-
- if ($from.parent.isTextblock && $from.parentOffset === 0) {
- const nodeBefore = $from.nodeBefore
- if (nodeBefore && nodeBefore.type.name === this.name) {
- return this.editor.commands.deleteNode(this.name)
- }
- }
-
- return false
- }
- }
- }
-})
// 创建自定义键盘处理插件,处理Enter键发送消息
const EnterKeyPlugin = new Plugin({
@@ -222,7 +157,43 @@ const CustomKeyboard = Extension.create({
const editor = useEditor({
extensions: [
StarterKit,
- Image.configure({
+ Image.extend({
+ addNodeView() {
+ return ({ node, getPos, editor }) => {
+ const container = document.createElement('span')
+ container.style.position = 'relative'
+ container.style.display = 'inline-block'
+
+ const img = document.createElement('img')
+ img.setAttribute('src', node.attrs.src)
+ img.style.maxWidth = '100px'
+ img.style.borderRadius = '3px'
+ img.style.backgroundColor = '#48484d'
+ img.style.margin = '0px 2px'
+
+ container.appendChild(img)
+
+ if (uploadingImages.value.has(node.attrs.src)) {
+ container.classList.add('image-upload-loading')
+ }
+
+ const stopWatch = watch(uploadingImages, () => {
+ if (uploadingImages.value.has(node.attrs.src)) {
+ container.classList.add('image-upload-loading')
+ } else {
+ container.classList.remove('image-upload-loading')
+ }
+ }, { deep: true })
+
+ return {
+ dom: container,
+ destroy() {
+ stopWatch()
+ }
+ }
+ }
+ }
+ }).configure({
inline: true,
allowBase64: true,
}),
@@ -234,6 +205,10 @@ const editor = useEditor({
class: 'mention',
},
suggestion: {
+ allowedPrefixes: null,
+ hideOnClickOutside: true,
+ hideOnKeyDown: true,
+ emptyQueryClass: 'is-empty-query',
items: ({ query }) => {
if (!props.members.length) {
return []
@@ -245,13 +220,21 @@ const editor = useEditor({
list.unshift({ id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' })
}
- return list.filter(
+ const filteredItems = list.filter(
(item) => item.nickname.toLowerCase().includes(query.toLowerCase())
)
+
+ // 如果没有匹配项,返回空数组以关闭弹窗
+ if (filteredItems.length === 0) {
+ return []
+ }
+
+ return filteredItems
},
render: () => {
let component
let popup
+ let handleClickOutside
return {
onStart: (props) => {
@@ -260,6 +243,18 @@ const editor = useEditor({
popup.classList.add('ql-mention-list-container', 'me-scrollbar', 'me-scrollbar-thumb')
document.body.appendChild(popup)
+ // 添加全局点击事件监听器,点击弹窗外部时关闭弹窗
+ handleClickOutside = (event) => {
+ if (popup && !popup.contains(event.target)) {
+ popup.remove()
+ document.removeEventListener('click', handleClickOutside)
+ }
+ }
+ // 使用setTimeout确保事件不会立即触发
+ setTimeout(() => {
+ document.addEventListener('click', handleClickOutside)
+ }, 100)
+
// 渲染提及列表
props.items.forEach((item, index) => {
const mentionItem = document.createElement('div')
@@ -297,18 +292,28 @@ const editor = useEditor({
onKeyDown: (props) => {
// 处理键盘事件
+ // Escape键关闭弹窗
if (props.event.key === 'Escape') {
popup.remove()
return true
}
+ // 空格键、回车键或其他非导航键也关闭弹窗
+ const navigationKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab']
+ if (!navigationKeys.includes(props.event.key) && props.items.length === 0) {
+ popup.remove()
+ return false
+ }
+
return false
},
onExit: () => {
- // 清理
+ // 清理弹窗和事件监听器
if (popup) {
popup.remove()
+ // 移除所有可能的点击事件监听器
+ document.removeEventListener('click', handleClickOutside)
}
},
}
@@ -317,7 +322,6 @@ const editor = useEditor({
}),
Link,
Emoji,
- Quote,
CustomKeyboard,
],
content: '',
@@ -327,6 +331,57 @@ const editor = useEditor({
onUpdate: () => {
onEditorChange()
},
+ editorProps: {
+ handlePaste: (view, event) => {
+ const items = (event.clipboardData || event.originalEvent.clipboardData).items
+ for (const item of items) {
+ if (item.type.indexOf('image') === 0) {
+ event.preventDefault()
+ const file = item.getAsFile()
+ if (!file) continue
+
+ const tempUrl = URL.createObjectURL(file)
+ const { state, dispatch } = view
+ const { tr } = state
+ const node = state.schema.nodes.image.create({ src: tempUrl })
+ dispatch(tr.replaceSelectionWith(node))
+
+ const form = new FormData()
+ form.append('file', file)
+ form.append('source', 'fonchain-chat')
+
+ uploadingImages.value.set(tempUrl, true)
+
+ uploadImg(form)
+ .then(({ code, data, message }) => {
+ if (code === 0 && data.ori_url) {
+ const pos = findImagePos(tempUrl)
+ if (pos !== -1) {
+ const { tr } = view.state
+ view.dispatch(
+ tr.setNodeMarkup(pos, null, { src: data.ori_url })
+ )
+ }
+ } else {
+ window['$message'].error(message || '图片上传失败')
+ removeImage(tempUrl)
+ }
+ })
+ .catch(() => {
+ window['$message'].error('图片上传失败')
+ removeImage(tempUrl)
+ })
+ .finally(() => {
+ uploadingImages.value.delete(tempUrl)
+ URL.revokeObjectURL(tempUrl)
+ })
+
+ return true
+ }
+ }
+ return false
+ }
+ }
})
/**
@@ -334,6 +389,28 @@ const editor = useEditor({
* @param file 文件对象
* @returns Promise,成功时返回图片URL
*/
+function findImagePos(url) {
+ if (!editor.value) return -1
+ let pos = -1
+ editor.value.state.doc.descendants((node, p) => {
+ if (node.type.name === 'image' && node.attrs.src === url) {
+ pos = p
+ return false
+ }
+ return true
+ })
+ return pos
+}
+
+function removeImage(url) {
+ if (!editor.value) return
+ const pos = findImagePos(url)
+ if (pos !== -1) {
+ const { tr } = editor.value.state
+ editor.value.view.dispatch(tr.delete(pos, pos + 1))
+ }
+}
+
function onUploadImage(file) {
return new Promise((resolve) => {
let image = new Image()
@@ -461,7 +538,6 @@ function tiptapToMessage() {
const json = editor.value.getJSON()
const messages = []
- let quoteId = null
let currentTextBuffer = ''
let currentMentions = []
let currentMentionUids = new Set()
@@ -474,10 +550,6 @@ function tiptapToMessage() {
mentions: [...currentMentions],
mentionUids: Array.from(currentMentionUids)
}
- if (quoteId) {
- data.quoteId = quoteId
- quoteId = null
- }
messages.push({ type: 'text', data })
}
currentTextBuffer = ''
@@ -507,22 +579,12 @@ function tiptapToMessage() {
...getImageInfo(node.attrs.src),
url: node.attrs.src
}
- if (quoteId) {
- data.quoteId = quoteId
- quoteId = null
- }
messages.push({ type: 'image', data })
}
})
}
if (json.content) {
- const quoteIndex = json.content.findIndex(node => node.type === 'quote')
- if (quoteIndex > -1) {
- quoteId = json.content[quoteIndex].attrs.id
- json.content.splice(quoteIndex, 1)
- }
-
json.content.forEach(node => {
if (node.type === 'paragraph') {
if (node.content) {
@@ -535,10 +597,6 @@ function tiptapToMessage() {
...getImageInfo(node.attrs.src),
url: node.attrs.src
}
- if (quoteId) {
- data.quoteId = quoteId
- quoteId = null
- }
messages.push({ type: 'image', data })
}
})
@@ -585,11 +643,14 @@ function isEditorEmpty() {
* 根据编辑器内容类型发送不同类型的消息
*/
function onSendMessage() {
- if (!editor.value || isEditorEmpty()) return
+ if (uploadingImages.value.size > 0) {
+ return window['$message'].info('正在上传图片,请稍后再发')
+ }
+ if (!editor.value || (isEditorEmpty() && !quoteData.value)) return
const messages = tiptapToMessage()
- if (messages.length === 0) {
+ if (messages.length === 0 && !quoteData.value) {
return
}
@@ -601,6 +662,13 @@ function onSendMessage() {
canClear = false
return
}
+
+ // 添加引用消息参数
+ if (quoteData.value) {
+ msg.data.quoteId = quoteData.value.id
+ msg.data.quote = { ...quoteData.value }
+ }
+
emit('editor-event', emitCall('text_event', msg.data))
} else if (msg.type === 'image') {
const data = {
@@ -609,12 +677,33 @@ function onSendMessage() {
size: 10000,
url: msg.data.url,
}
+
+ // 添加引用消息参数
+ if (quoteData.value) {
+ data.quoteId = quoteData.value.id
+ data.quote = { ...quoteData.value }
+ }
+
emit('editor-event', emitCall('image_event', data))
}
})
+ // 如果只有引用消息但没有内容,也发送一条空文本消息带引用
+ if (messages.length === 0 && quoteData.value) {
+ const emptyData = {
+ items: [{ type: 1, content: '' }],
+ mentions: [],
+ mentionUids: [],
+ quoteId: quoteData.value.id,
+ quote: { ...quoteData.value }
+ }
+ emit('editor-event', emitCall('text_event', emptyData))
+ }
+
if (canClear) {
editor.value?.commands.clearContent(true)
+ // 清空引用数据
+ quoteData.value = null
}
}
@@ -627,11 +716,12 @@ function onEditorChange() {
const text = tiptapToString()
- if (!isEditorEmpty()) {
+ if (!isEditorEmpty() || quoteData.value) {
// 保存草稿到store
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
text: text,
- content: editor.value.getJSON()
+ content: editor.value.getJSON(),
+ quoteData: quoteData.value
})
} else {
// 编辑器为空时删除对应草稿
@@ -649,6 +739,10 @@ function onEditorChange() {
function loadEditorDraftText() {
if (!editor.value) return
+ // 保存当前引用数据
+ const currentQuoteData = quoteData.value
+ quoteData.value = null
+
// 从缓存中加载编辑器草稿
let draft = editorDraftStore.items[indexName.value || '']
if (draft) {
@@ -658,10 +752,20 @@ function loadEditorDraftText() {
} else if (parsed.text) {
editor.value.commands.setContent(parsed.text)
}
+
+ // 如果草稿中有引用数据,恢复它
+ if (parsed.quoteData) {
+ quoteData.value = parsed.quoteData
+ }
} else {
editor.value.commands.clearContent(true) // 没有草稿则清空编辑器
}
+ // 如果有当前引用数据,优先使用它
+ if (currentQuoteData) {
+ quoteData.value = currentQuoteData
+ }
+
// 设置光标位置到末尾
editor.value.commands.focus('end')
}
@@ -689,27 +793,9 @@ function onSubscribeMention(data) {
*/
function onSubscribeQuote(data) {
if (!editor.value) return
-
- // 检查是否已有引用内容
- const json = editor.value.getJSON()
- if (json.content?.some(node => node.type === 'quote')) {
- return // 已有引用则不再添加
- }
-
- // 在编辑器开头插入引用
- editor.value
- .chain()
- .focus()
- .insertContentAt(0, [
- {
- type: 'quote',
- attrs: data
- },
- {
- type: 'paragraph'
- }
- ])
- .run()
+
+ // 保存引用数据
+ quoteData.value = data
}
/**
@@ -770,6 +856,8 @@ useEventBus([
+
+
-
+
+
+
+
+ {{ quoteData.title || ' ' }}
+
+
+
+
![引用图片]()
+
+
+ {{ quoteData.describe }}
+
+
+
@@ -838,10 +940,64 @@ useEventBus([
@@ -903,6 +1081,25 @@ html[theme-mode='dark'] {
overflow: auto;
padding: 8px;
outline: none;
+
+ .image-upload-loading {
+ position: relative;
+ display: inline-block;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5) url('data:image/svg+xml;charset=UTF-8,');
+ background-size: 30px 30px;
+ background-position: center center;
+ background-repeat: no-repeat;
+ border-radius: 5px;
+ }
+ }
/* 滚动条样式 */
&::-webkit-scrollbar {
@@ -959,45 +1156,10 @@ html[theme-mode='dark'] {
}
/* 引用卡片样式 */
- .quote-card-wrapper {
+ .quote-card {
margin-bottom: 8px;
}
-
- .quote-card-content {
- display: flex;
- background-color: #f6f6f6;
- flex-direction: column;
- padding: 8px;
- margin-bottom: 5px;
- cursor: pointer;
- user-select: none;
- .quote-card-title {
- height: 22px;
- line-height: 22px;
- font-size: 12px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- justify-content: space-between;
-
- .quote-card-remove {
- margin-right: 15px;
- font-size: 18px;
- }
- }
-
- .quote-card-meta {
- margin-top: 4px;
- font-size: 12px;
- line-height: 20px;
- color: #999;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
}
/* 提及列表样式 */
From a405a3bd906ea53ab410563229518bd290e99f5e Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Wed, 2 Jul 2025 16:04:41 +0800
Subject: [PATCH 07/22] =?UTF-8?q?fix(editor):=20=E5=88=87=E6=8D=A2?=
=?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=97=B6=E6=B8=85=E7=A9=BA=E5=BC=95=E7=94=A8?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=80=8C=E9=9D=9E=E4=BF=9D=E7=95=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修改编辑器行为,在切换会话时主动清空引用数据而不是保留之前的引用。这避免了不同会话间引用数据的混淆问题。
---
src/components/editor/TiptapEditor.vue | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 77a8996..fa7b4a4 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -739,8 +739,7 @@ function onEditorChange() {
function loadEditorDraftText() {
if (!editor.value) return
- // 保存当前引用数据
- const currentQuoteData = quoteData.value
+ // 切换会话时清空引用数据,不保存当前引用数据
quoteData.value = null
// 从缓存中加载编辑器草稿
@@ -761,11 +760,6 @@ function loadEditorDraftText() {
editor.value.commands.clearContent(true) // 没有草稿则清空编辑器
}
- // 如果有当前引用数据,优先使用它
- if (currentQuoteData) {
- quoteData.value = currentQuoteData
- }
-
// 设置光标位置到末尾
editor.value.commands.focus('end')
}
From 0b634e8cdd291048e30c5c4919f165525a37860a Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Wed, 2 Jul 2025 16:22:42 +0800
Subject: [PATCH 08/22] =?UTF-8?q?fix(=E7=BC=96=E8=BE=91=E5=99=A8):=20?=
=?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=85=E7=A9=BA=E5=BC=95=E7=94=A8=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E6=97=B6=E6=9C=AA=E6=9B=B4=E6=96=B0=E8=8D=89=E7=A8=BF?=
=?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
在清空引用数据时调用clearQuoteData方法,确保同时更新草稿状态。修改了引用卡片关闭按钮的点击事件处理逻辑,使用新方法替代直接赋值null。
---
src/components/editor/TiptapEditor.vue | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index fa7b4a4..7ecedc9 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -704,6 +704,8 @@ function onSendMessage() {
editor.value?.commands.clearContent(true)
// 清空引用数据
quoteData.value = null
+ // 更新草稿
+ onEditorChange()
}
}
@@ -790,6 +792,17 @@ function onSubscribeQuote(data) {
// 保存引用数据
quoteData.value = data
+ // 更新草稿
+ onEditorChange()
+}
+
+/**
+ * 清空引用数据并更新草稿
+ */
+function clearQuoteData() {
+ quoteData.value = null
+ // 更新草稿
+ onEditorChange()
}
/**
@@ -894,7 +907,7 @@ useEventBus([
{{ quoteData.title || ' ' }}
-
+
![引用图片]()
From 4153a936a67845d43bc28fd19965cf0e86b6c702 Mon Sep 17 00:00:00 2001
From: wangyifeng <812766448@qq.com>
Date: Thu, 3 Jul 2025 09:14:19 +0800
Subject: [PATCH 09/22] =?UTF-8?q?=E5=B0=86ES=E6=8E=A5=E5=8F=A3=E6=8D=A2?=
=?UTF-8?q?=E5=9B=9E=E9=9D=9EV2=E7=89=88=EF=BC=8C=E7=AD=89=E5=BE=85SAAS?=
=?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E9=80=9A=E7=9F=A5?=
=?UTF-8?q?=E4=B8=8A=E7=BA=BF=E5=86=8D=E8=A1=8C=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/search.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/api/search.js b/src/api/search.js
index 4addc0e..65e7c16 100644
--- a/src/api/search.js
+++ b/src/api/search.js
@@ -2,12 +2,14 @@ import { post, get, upload } from '@/utils/request'
//ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览
export const ServeSeachQueryAll = (data = {}) => {
- return post('/api/v1/elasticsearch/query-all/v2', data)
+ return post('/api/v1/elasticsearch/query-all', data)
+ // return post('/api/v1/elasticsearch/query-all/v2', data)
}
// ES搜索用户数据
export const ServeQueryUser = (data) => {
- return post('/api/v1/elasticsearch/query-user/v2', data)
+ return post('/api/v1/elasticsearch/query-user', data)
+ // return post('/api/v1/elasticsearch/query-user/v2', data)
}
// ES搜索群组数据
From c64a562913a62c8793665032d4b08af6deedf350 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Thu, 3 Jul 2025 10:33:29 +0800
Subject: [PATCH 10/22] =?UTF-8?q?refactor(db):=20=E9=87=8D=E6=9E=84?=
=?UTF-8?q?=E4=BC=9A=E8=AF=9D=E8=A1=A8=E4=B8=BB=E9=94=AE=E7=BB=93=E6=9E=84?=
=?UTF-8?q?=E5=B9=B6=E6=B8=85=E7=90=86=E6=97=A7=E6=95=B0=E6=8D=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将会话表主键从自增id改为index_name
- 添加数据库版本升级逻辑清理旧数据
- 更新所有相关操作方法使用新主键
- 添加详细的版本变更注释
---
src/store/modules/talk.ts | 2 +-
src/utils/auth.js | 2 +-
src/utils/db.js | 69 ++++++++++++++++++++++++++++++---------
3 files changed, 55 insertions(+), 18 deletions(-)
diff --git a/src/store/modules/talk.ts b/src/store/modules/talk.ts
index e79abd4..7f9350c 100644
--- a/src/store/modules/talk.ts
+++ b/src/store/modules/talk.ts
@@ -181,7 +181,7 @@ export const useTalkStore = defineStore('talk', {
// 更新状态和本地数据库
this.items = serverItems
-
+ console.log('serverItems',serverItems)
// 将最新的会话列表保存到本地数据库
for (const item of serverItems) {
await addOrUpdateConversation(item)
diff --git a/src/utils/auth.js b/src/utils/auth.js
index ea2b8c1..c218336 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'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22f227a3ad4383a2ddef0a2f43b855f869560968fd2dae0412498273e591b78554b2373a17017cdaae7c9ec427325b7d078d54cb00b001c9894c8e1f990747c8db3b62b17eb8ed39e2b2c2b6d63ce26756'
+ return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22eec7a138bb20774ef183e109945229d43e1f63fb01cdee46f5f663037f4ed946a0c04441b1f642c945d218180e84e91d272dc621be157602785ef226dd21b9b6c92c292bc73be90fad0320bad0812e11'
}
/**
diff --git a/src/utils/db.js b/src/utils/db.js
index 8bec6a8..c4c56d1 100644
--- a/src/utils/db.js
+++ b/src/utils/db.js
@@ -1,11 +1,30 @@
import Dexie from 'dexie';
+/**
+ * 聊天历史数据库
+ * 版本5-6: 修复会话表主键问题
+ * - 版本5: 删除旧的会话表结构
+ * - 版本6: 使用index_name作为主键重新创建会话表
+ *
+ * 注意: Dexie不支持直接更改主键,必须通过删除并重建表的方式实现
+ */
export const db = new Dexie('chatHistory');
// 定义数据库表结构和索引
-// 版本3:优化了索引,提高了查询和排序性能
-db.version(4).stores({
+// 版本6:修复主键更改问题
+// 版本5:删除旧的会话表
+db.version(5).stores({
+ conversations: null
+}).upgrade(function(trans) {
+ // 确保物理删除表
+ if (trans.idbtrans.db.objectStoreNames.contains('conversations')) {
+ trans.idbtrans.db.deleteObjectStore('conversations');
+ }
+});
+
+// 版本6:使用新的主键结构重新创建会话表
+db.version(6).stores({
/**
* 聊天记录表
* - msg_id: 消息唯一ID (主键)
@@ -18,12 +37,20 @@ db.version(4).stores({
/**
* 会话表
- * - ++id: 自增主键
- * - &index_name: 唯一索引 (talk_type + '_' + receiver_id)
+ * - index_name: 主键 (talk_type + '_' + receiver_id)
* - updated_at: 索引,用于排序
* - is_top: 索引,用于置顶排序
*/
- conversations: 'id, &index_name, talk_type, receiver_id, updated_at, unread_num, is_top',
+ conversations: 'index_name, talk_type, receiver_id, updated_at, unread_num, is_top',
+});
+
+// 清理旧版本数据
+db.on('versionchange', function(event) {
+ if (event.oldVersion < 6 && event.newVersion >= 6) {
+ console.log('数据库版本升级到6,清理旧数据');
+ db.conversations.clear();
+ console.log('会话表数据已清理,主键结构已更新');
+ }
});
db.on('ready', () => {
@@ -219,14 +246,24 @@ export async function deleteMessage(msgId) {
/**
* 添加或更新会话
* @param {object} conversation - 会话对象
- * @returns {Promise
} 会话ID
+ * @returns {Promise} 会话索引名称
*/
export async function addOrUpdateConversation(conversation) {
try {
- // put 方法会根据唯一索引 index_name 自动判断是添加还是更新
+ // 确保 index_name 存在,这是会话表的主键
+ if (!conversation.index_name && conversation.talk_type && conversation.receiver_id) {
+ conversation.index_name = `${conversation.talk_type}_${conversation.receiver_id}`;
+ }
+
+ if (!conversation.index_name) {
+ throw new Error('无法添加会话:缺少必要的index_name或无法生成');
+ }
+
+ // 使用 put 方法,如果主键已存在则更新,否则添加
return await db.conversations.put(conversation);
} catch (error) {
console.error('添加或更新会话失败:', error);
+ console.error('错误详情:', error.message, error.stack);
throw error;
}
}
@@ -274,7 +311,7 @@ export async function getConversations(includeEmpty = false) {
export async function getConversation(talkType, receiverId) {
try {
const indexName = `${talkType}_${receiverId}`;
- return await db.conversations.get({ index_name: indexName });
+ return await db.conversations.get(indexName);
} catch (error) {
console.error('获取会话失败:', error);
throw error;
@@ -291,11 +328,11 @@ export async function getConversation(talkType, receiverId) {
export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) {
try {
const indexName = `${talkType}_${receiverId}`;
- const conversation = await db.conversations.get({ index_name: indexName });
+ const conversation = await db.conversations.get(indexName);
if (conversation) {
const newUnreadNum = unreadNum === null ? (conversation.unread_num || 0) + 1 : unreadNum;
- return await db.conversations.update(conversation.id, { unread_num: newUnreadNum });
+ return await db.conversations.update(indexName, { unread_num: newUnreadNum });
}
return 0;
} catch (error) {
@@ -316,18 +353,18 @@ export function clearConversationUnreadNum(talkType, receiverId) {
/**
* 删除会话及其相关的消息
- * @param {number} conversationId - 会话ID
+ * @param {string} indexName - 会话索引名称
* @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录
* @returns {Promise}
*/
-export async function deleteConversation(conversationId, deleteMessages = false) {
+export async function deleteConversation(indexName, deleteMessages = false) {
try {
await db.transaction('rw', db.conversations, db.messages, async () => {
- const conversation = await db.conversations.get(conversationId);
+ const conversation = await db.conversations.get(indexName);
if (!conversation) return;
// 删除会话
- await db.conversations.delete(conversationId);
+ await db.conversations.delete(indexName);
// 如果需要,删除关联的消息
if (deleteMessages) {
@@ -352,7 +389,7 @@ export async function updateConversationLastMessage(message) {
const targetReceiverId = talk_type === TalkType.PRIVATE ? (user_id === receiver_id ? user_id : receiver_id) : receiver_id;
const indexName = `${talk_type}_${targetReceiverId}`;
- const conversation = await db.conversations.get({ index_name: indexName });
+ const conversation = await db.conversations.get(indexName);
if (!conversation) return 0;
let msgText = '';
@@ -367,7 +404,7 @@ export async function updateConversationLastMessage(message) {
default: msgText = '[未知消息]';
}
- return await db.conversations.update(conversation.id, {
+ return await db.conversations.update(indexName, {
msg_text: msgText,
content: message.content || '',
updated_at: message.created_at,
From a0b28b19efd84f9e0c44c962fa1c27b343cbe82b Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Thu, 3 Jul 2025 10:47:10 +0800
Subject: [PATCH 11/22] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E9=AB=98=E5=BA=A6=E9=97=AE=E9=A2=98?=
=?UTF-8?q?=E5=92=8C=E7=A7=BB=E9=99=A4=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
调整Tiptap编辑器高度为100%以正确填充容器
移除消息面板中已读回执的调试日志输出
---
src/components/editor/TiptapEditor.vue | 4 +++-
src/views/message/inner/panel/PanelContent.vue | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 7ecedc9..12e0ffd 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -1088,7 +1088,9 @@ html[theme-mode='dark'] {
overflow: auto;
padding: 8px;
outline: none;
-
+ .tiptap.ProseMirror{
+ height: 100%;
+ }
.image-upload-loading {
position: relative;
display: inline-block;
diff --git a/src/views/message/inner/panel/PanelContent.vue b/src/views/message/inner/panel/PanelContent.vue
index 3fc8b63..e99c05d 100644
--- a/src/views/message/inner/panel/PanelContent.vue
+++ b/src/views/message/inner/panel/PanelContent.vue
@@ -511,7 +511,7 @@ const checkVisibleElements = () => {
prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id
)
if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) {
- console.error('====发送了新版已读回执=====', doReadItem)
+ // console.error('====发送了新版已读回执=====', doReadItem)
ws.emit('im.message.new.read', doReadItem)
}
})
From 0b8de6f5c253b5f577ac07a8ca909b88939b1ff8 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Thu, 3 Jul 2025 13:22:01 +0800
Subject: [PATCH 12/22] =?UTF-8?q?=E8=AE=A1=E7=AE=97=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/hooks/useTalkRecord.ts | 380 +++++++++++++++++++++++++++++--------
src/utils/db.js | 131 ++++++++++---
2 files changed, 410 insertions(+), 101 deletions(-)
diff --git a/src/hooks/useTalkRecord.ts b/src/hooks/useTalkRecord.ts
index efebd85..7b36c2b 100644
--- a/src/hooks/useTalkRecord.ts
+++ b/src/hooks/useTalkRecord.ts
@@ -124,48 +124,72 @@ export const useTalkRecord = (uid: number) => {
// 加载数据列表
const load = async (params: Params) => {
+ // 使用性能标记测量加载时间
+ const startTime = performance.now()
+
const request = {
talk_type: params.talk_type,
receiver_id: params.receiver_id,
cursor: loadConfig.cursor,
limit: 30
}
+
// 如果不是从本地数据库加载的,则设置加载状态为0(加载中)
if (loadConfig.status !== 2 && loadConfig.status !== 3) {
loadConfig.status = 0
}
+ // 记录当前滚动高度,用于后续保持滚动位置
let scrollHeight = 0
const el = document.getElementById('imChatPanel')
if (el) {
scrollHeight = el.scrollHeight
}
+
+ // 发起网络请求获取服务器数据
const { data, code } = await ServeTalkRecords(request)
+
+ // 处理请求失败的情况
if (code != 200) {
- return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态
+ // 如果已经从本地加载了数据,保持原状态
+ loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1
+ return
}
+
// 防止对话切换过快,数据渲染错误
- if (
- request.talk_type != loadConfig.talk_type ||
- request.receiver_id != loadConfig.receiver_id
- ) {
- return (location.msgid = '')
+ if (request.talk_type != loadConfig.talk_type || request.receiver_id != loadConfig.receiver_id) {
+ location.msgid = ''
+ return
}
- const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
-
- // 同步到本地数据库
- try {
- const { batchAddOrUpdateMessages } = await import('@/utils/db')
- await batchAddOrUpdateMessages(data.items || [], params.talk_type, params.receiver_id, true, 'sequence')
- console.log('聊天记录已同步到本地数据库')
- } catch (error) {
- console.error('同步聊天记录到本地数据库失败:', error)
+ // 优化:使用批量处理而不是map,减少内存分配
+ const serverItems = data.items || []
+ const items = new Array(serverItems.length)
+ for (let i = 0; i < serverItems.length; i++) {
+ items[i] = formatTalkRecord(uid, serverItems[i])
}
+ // 同步到本地数据库(异步操作,不阻塞UI更新)
+ const syncToLocalDB = async () => {
+ try {
+ const syncStartTime = performance.now()
+ const { batchAddOrUpdateMessages } = await import('@/utils/db')
+ await batchAddOrUpdateMessages(serverItems, params.talk_type, params.receiver_id, true, 'sequence')
+ const syncEndTime = performance.now()
+ console.log(`聊天记录已同步到本地数据库,耗时: ${(syncEndTime - syncStartTime).toFixed(2)}ms`)
+ } catch (error) {
+ console.error('同步聊天记录到本地数据库失败:', error)
+ }
+ }
+
+ // 启动异步同步过程
+ syncToLocalDB()
+
// 如果是从本地数据库加载的数据,且服务器返回的数据与本地数据相同,则不需要更新UI
if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) {
try {
+ const compareStartTime = performance.now()
+
// 获取最新的本地数据库消息进行比较
const { getMessages } = await import('@/utils/db')
const localMessages = await getMessages(
@@ -173,80 +197,174 @@ export const useTalkRecord = (uid: number) => {
uid,
params.receiver_id,
items.length || 30, // 获取与服务器返回数量相同的消息
- 0 // 从第一页开始
+ 0, // 从第一页开始
+ 'sequence' // 明确指定排序字段
)
- // 格式化本地消息,确保与服务器消息结构一致
- const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
-
-
- // 改进比较逻辑:检查消息数量和所有消息的ID是否匹配
- if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 0) {
- // 创建消息ID映射,用于快速查找
+ // 快速路径:如果本地消息数量与服务器不同,直接更新UI
+ if (localMessages.length !== items.length) {
+ console.log('本地数据与服务器数据数量不一致,更新UI')
+ } else if (items.length > 0) {
+ // 优化:使用位图标记需要更新的消息,减少内存使用
+ const needsUpdate = new Uint8Array(items.length)
+ let updateCount = 0
+
+ // 优化:使用哈希表存储消息ID到索引的映射,加速查找
const serverMsgMap = new Map()
- items.forEach(item => serverMsgMap.set(item.msg_id, item))
-
- // 检查每条本地消息是否与服务器消息匹配
- const allMatch = formattedLocalMessages.every(localMsg => {
- const serverMsg = serverMsgMap.get(localMsg.msg_id)
- // 检查消息是否存在且关键状态是否一致(考虑撤回、已读等状态变化)
- return serverMsg &&
- serverMsg.is_revoke === localMsg.is_revoke &&
- serverMsg.is_read === localMsg.is_read &&
- (serverMsg.send_status === localMsg.send_status ||
- (!serverMsg.send_status && !localMsg.send_status)) &&
- serverMsg.content === localMsg.content
- })
-
- if (allMatch) {
- console.log('本地数据与服务器数据一致,无需更新UI')
- return
+ for (let i = 0; i < items.length; i++) {
+ serverMsgMap.set(items[i].msg_id, i)
}
+
+ // 优化:首先检查首尾消息,如果它们匹配,再使用抽样检查中间消息
+ const firstLocalMsg = localMessages[0]
+ const lastLocalMsg = localMessages[localMessages.length - 1]
+
+ const firstServerIdx = serverMsgMap.get(firstLocalMsg.msg_id)
+ const lastServerIdx = serverMsgMap.get(lastLocalMsg.msg_id)
+
+ // 如果首尾消息ID存在于服务器数据中,进行详细比较
+ if (firstServerIdx !== undefined && lastServerIdx !== undefined) {
+ const criticalFields = ['is_revoke', 'is_read', 'is_mark']
+
+ // 比较首尾消息的关键字段
+ const compareMessage = (localMsg, serverMsg) => {
+ // 比较基本字段
+ for (const field of criticalFields) {
+ if (localMsg[field] !== serverMsg[field]) {
+ return false
+ }
+ }
+
+ // 特殊处理content字段,它在extra对象中
+ const localContent = localMsg.extra?.content
+ const serverContent = serverMsg.extra?.content
+
+ if (localContent !== serverContent) {
+ return false
+ }
+
+ return true
+ }
+
+ const firstMatch = compareMessage(firstLocalMsg, items[firstServerIdx])
+ const lastMatch = compareMessage(lastLocalMsg, items[lastServerIdx])
+
+ // 如果首尾消息匹配,使用抽样检查中间消息
+ if (firstMatch && lastMatch) {
+ // 智能抽样检查策略
+ // 1. 检查首尾消息(已完成)
+ // 2. 检查中间点消息
+ // 3. 检查最近修改的消息(通常是最新的几条)
+ // 4. 随机抽样检查
+
+ let allMatch = true
+
+ // 中间点检查
+ const midIndex = Math.floor(localMessages.length / 2)
+ const midMsg = localMessages[midIndex]
+ const midServerIdx = serverMsgMap.get(midMsg.msg_id)
+
+ if (midServerIdx === undefined || !compareMessage(midMsg, items[midServerIdx])) {
+ allMatch = false
+ }
+
+ // 最近消息检查(检查最新的3条消息,通常是最可能被修改的)
+ if (allMatch && localMessages.length >= 4) {
+ for (let i = 1; i <= 3; i++) {
+ const recentMsg = localMessages[localMessages.length - i]
+ const recentServerIdx = serverMsgMap.get(recentMsg.msg_id)
+
+ if (recentServerIdx === undefined || !compareMessage(recentMsg, items[recentServerIdx])) {
+ allMatch = false
+ break
+ }
+ }
+ }
+
+ // 随机抽样检查(如果前面的检查都通过)
+ if (allMatch && localMessages.length > 10) {
+ // 随机选择5%的消息或至少2条进行检查
+ const sampleSize = Math.max(2, Math.floor(localMessages.length * 0.05))
+ const usedIndices = new Set([0, midIndex, localMessages.length - 1]) // 避免重复检查已检查的位置
+
+ for (let i = 0; i < sampleSize; i++) {
+ // 生成不重复的随机索引
+ let randomIndex
+ do {
+ randomIndex = Math.floor(Math.random() * localMessages.length)
+ } while (usedIndices.has(randomIndex))
+
+ usedIndices.add(randomIndex)
+
+ const randomMsg = localMessages[randomIndex]
+ const randomServerIdx = serverMsgMap.get(randomMsg.msg_id)
+
+ if (randomServerIdx === undefined || !compareMessage(randomMsg, items[randomServerIdx])) {
+ allMatch = false
+ break
+ }
+ }
+ }
+
+ if (allMatch) {
+ const compareEndTime = performance.now()
+ console.log(`本地数据与服务器数据一致(抽样检查),无需更新UI,比较耗时: ${(compareEndTime - compareStartTime).toFixed(2)}ms`)
+ return
+ }
+ }
+ }
+
+ console.log('本地数据与服务器数据不一致,更新UI')
}
-
- // 数据不一致,需要更新UI
- console.log('本地数据与服务器数据不一致,更新UI')
} catch (error) {
console.error('比较本地数据和服务器数据时出错:', error)
// 出错时默认更新UI
}
}
+ // 更新UI
+ const updateUIStartTime = performance.now()
+
if (request.cursor == 0) {
// 判断是否是初次加载
dialogueStore.clearDialogueRecord()
}
+ // 反转消息顺序并添加到对话记录
dialogueStore.unshiftDialogueRecord(items.reverse())
+ // 更新加载状态
loadConfig.status = items.length >= request.limit ? 1 : 2
-
loadConfig.cursor = data.cursor
- nextTick(() => {
+ // 使用requestAnimationFrame代替nextTick,提高滚动性能
+ requestAnimationFrame(() => {
const el = document.getElementById('imChatPanel')
if (el) {
if (request.cursor == 0) {
- // el.scrollTop = el.scrollHeight
-
- // setTimeout(() => {
- // el.scrollTop = el.scrollHeight + 1000
- // }, 500)
console.log('滚动到底部')
// 在初次加载完成后恢复上传任务
- // 确保在所有聊天记录加载完成后再恢复上传任务
dialogueStore.restoreUploadTasks()
+ // 使用优化的滚动函数
scrollToBottom()
} else {
+ // 保持滚动位置
el.scrollTop = el.scrollHeight - scrollHeight
}
}
+ // 如果有需要定位的消息ID,执行定位
if (location.msgid) {
onJumpMessage(location.msgid)
}
+
+ const updateUIEndTime = performance.now()
+ const totalEndTime = performance.now()
+
+ console.log(`UI更新耗时: ${(updateUIEndTime - updateUIStartTime).toFixed(2)}ms`)
+ console.log(`load函数总耗时: ${(totalEndTime - startTime).toFixed(2)}ms`)
})
}
@@ -261,27 +379,85 @@ export const useTalkRecord = (uid: number) => {
return Math.max(...records.value.map((item) => item.sequence))
}
+ // 本地数据库加载缓存,用于优化短时间内的重复加载
+ const localDBCache = {
+ key: '', // 缓存键:talk_type-receiver_id
+ data: null, // 缓存的消息数据
+ timestamp: 0, // 缓存时间戳
+ ttl: 2000 // 缓存有效期(毫秒)
+ }
+
// 从本地数据库加载聊天记录
const loadFromLocalDB = async (params: Params) => {
try {
+ // 使用性能标记测量加载时间
+ const startTime = performance.now()
+
+ // 生成缓存键
+ const cacheKey = `${params.talk_type}-${params.receiver_id}`
+
+ // 检查缓存是否有效
+ const now = Date.now()
+ if (localDBCache.key === cacheKey &&
+ localDBCache.data &&
+ now - localDBCache.timestamp < localDBCache.ttl) {
+ console.log('使用缓存的本地数据库消息')
+
+ // 清空现有记录
+ dialogueStore.clearDialogueRecord()
+
+ // 直接使用缓存数据
+ dialogueStore.unshiftDialogueRecord([...localDBCache.data]) // 创建副本避免引用问题
+
+ // 设置加载状态为完成(3表示从本地数据库加载完成)
+ loadConfig.status = 3
+
+ // 恢复上传任务
+ dialogueStore.restoreUploadTasks()
+
+ // 使用requestAnimationFrame优化滚动性能
+ requestAnimationFrame(() => {
+ scrollToBottom()
+ })
+
+ const endTime = performance.now()
+ console.log(`从缓存加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localDBCache.data.length}条记录`)
+
+ return true
+ }
+
// 导入 getMessages 函数
const { getMessages } = await import('@/utils/db')
- // 从本地数据库获取聊天记录
+
+ // 从本地数据库获取聊天记录,使用sequence作为排序字段以提高性能
const localMessages = await getMessages(
params.talk_type,
uid,
params.receiver_id,
params.limit || 30,
- 0 // 从第一页开始
- // 不传入 maxSequence 参数,获取最新的消息
+ 0, // 从第一页开始
+ 'sequence' // 明确指定排序字段
)
+
// 如果有本地数据
if (localMessages && localMessages.length > 0) {
// 清空现有记录
dialogueStore.clearDialogueRecord()
- // 格式化并添加记录
- const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
+ // 优化:预分配数组大小,减少内存重分配
+ const formattedMessages = new Array(localMessages.length)
+
+ // 优化:使用批量处理而不是map,减少内存分配和GC压力
+ for (let i = 0; i < localMessages.length; i++) {
+ formattedMessages[i] = formatTalkRecord(uid, localMessages[i])
+ }
+
+ // 更新缓存
+ localDBCache.key = cacheKey
+ localDBCache.data = formattedMessages
+ localDBCache.timestamp = now
+
+ // 批量添加记录
dialogueStore.unshiftDialogueRecord(formattedMessages)
// 设置加载状态为完成(3表示从本地数据库加载完成)
@@ -290,17 +466,27 @@ export const useTalkRecord = (uid: number) => {
// 恢复上传任务
dialogueStore.restoreUploadTasks()
- // 滚动到底部
- nextTick(() => {
+ // 使用requestAnimationFrame优化滚动性能
+ requestAnimationFrame(() => {
scrollToBottom()
})
+ const endTime = performance.now()
+ console.log(`从本地数据库加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localMessages.length}条记录`)
+
return true
}
+ // 无数据时清除缓存
+ localDBCache.key = ''
+ localDBCache.data = null
+
return false
} catch (error) {
console.error('从本地数据库加载聊天记录失败:', error)
+ // 出错时清除缓存
+ localDBCache.key = ''
+ localDBCache.data = null
return false
}
}
@@ -311,6 +497,10 @@ export const useTalkRecord = (uid: number) => {
* @param options 可选,{ specifiedMsg } 指定消息对象
*/
const onLoad = async (params: Params, options?: LoadOptions) => {
+ // 使用性能标记测量加载时间
+ const startTime = performance.now()
+
+ // 检查会话是否变更,如果变更则重置配置
if (
params.talk_type !== loadConfig.talk_type ||
params.receiver_id !== loadConfig.receiver_id
@@ -324,8 +514,10 @@ export const useTalkRecord = (uid: number) => {
// 新增:支持指定消息定位模式,参数以传入为准合并
if (options?.specifiedMsg?.cursor !== undefined) {
+ // 特殊消息定位模式
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
loadConfig.status = 0 // 复用主流程 loading 状态
+
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
const contextParams = {
...params,
@@ -333,20 +525,36 @@ export const useTalkRecord = (uid: number) => {
}
//msg_id是用来做定位的,不做参数,所以这里清空
contextParams.msg_id = ''
- ServeTalkRecords(contextParams).then(({ data, code }) => {
- console.log('data',data)
+
+ // 使用Promise.all并行处理数据库操作和网络请求
+ const serverDataPromise = ServeTalkRecords(contextParams)
+
+ // 记录当前滚动高度
+ const el = document.getElementById('imChatPanel')
+ const scrollHeight = el?.scrollHeight || 0
+
+ try {
+ // 等待服务器响应
+ const { data, code } = await serverDataPromise
+
if (code !== 200) {
loadConfig.status = 2
return
}
- // 记录当前滚动高度
- const el = document.getElementById('imChatPanel')
- const scrollHeight = el?.scrollHeight || 0
-
+
+ console.log('data', data)
+
+ // 优化:使用批量处理而不是map,减少内存分配
+ const items = new Array(data.items?.length || 0)
+ for (let i = 0; i < (data.items?.length || 0); i++) {
+ items[i] = formatTalkRecord(uid, data.items[i])
+ }
+
+ // 根据方向和类型处理数据
if (contextParams.direction === 'down' && !contextParams.type) {
dialogueStore.clearDialogueRecord()
}
- const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
+
if (contextParams.type && contextParams.type === 'loadMore') {
dialogueStore.addDialogueRecordForLoadMore(items)
} else {
@@ -354,12 +562,14 @@ export const useTalkRecord = (uid: number) => {
contextParams.direction === 'down' ? items : items.reverse()
)
}
+
if (
contextParams.direction === 'up' ||
(contextParams.direction === 'down' && !contextParams.type)
) {
- loadConfig.status = items[0].sequence == 1 || data.length === 0 ? 2 : 1
+ loadConfig.status = items[0]?.sequence == 1 || data.length === 0 ? 2 : 1
}
+
loadConfig.cursor = data.cursor
// 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
@@ -375,7 +585,7 @@ export const useTalkRecord = (uid: number) => {
} else if (contextParams.type && contextParams.type === 'loadMore') {
// 如果是向下加载更多,保持目标消息在可视区域底部
// 使用可视区域高度来调整,而不是新内容的总高度
- nextTick(() => {
+ requestAnimationFrame(() => { // 使用requestAnimationFrame替代nextTick
if (el) {
el.scrollTop = scrollHeight - el.clientHeight
}
@@ -383,8 +593,8 @@ export const useTalkRecord = (uid: number) => {
} else if (target && msgId) {
// 只有在有目标元素且有 msg_id 时才执行定位逻辑
// 如果是定位到特定消息,计算并滚动到目标位置
- // 使用 nextTick 确保 DOM 完全渲染后再计算位置
- nextTick(() => {
+ // 使用 requestAnimationFrame 确保 DOM 完全渲染后再计算位置
+ requestAnimationFrame(() => {
const el = document.getElementById('imChatPanel')
const target = document.getElementById(msgId)
@@ -431,23 +641,39 @@ export const useTalkRecord = (uid: number) => {
scrollToBottom()
}
}
+
+ const endTime = performance.now()
+ console.log(`特殊消息定位模式加载耗时: ${(endTime - startTime).toFixed(2)}ms`)
})
- })
+ } catch (error) {
+ console.error('特殊消息定位模式加载失败:', error)
+ loadConfig.status = 2
+ }
+
return
}
+ // 普通模式
loadConfig.specialParams = undefined // 普通模式清空
// 设置初始加载状态为0(加载中)
loadConfig.status = 0
- // 先从本地数据库加载数据
- const hasLocalData = await loadFromLocalDB(params)
-
- // 无论是否有本地数据,都从服务器获取最新数据
- // 原有逻辑
- console.log('onLoad()执行load')
- load(params)
+ // 使用Promise.all并行处理本地数据库加载和网络请求准备
+ try {
+ // 先从本地数据库加载数据
+ const hasLocalData = await loadFromLocalDB(params)
+
+ // 无论是否有本地数据,都从服务器获取最新数据
+ console.log('onLoad()执行load')
+ await load(params)
+
+ const endTime = performance.now()
+ console.log(`普通模式加载总耗时: ${(endTime - startTime).toFixed(2)}ms`)
+ } catch (error) {
+ console.error('加载聊天记录失败:', error)
+ loadConfig.status = 2
+ }
}
// 向上加载更多(兼容特殊参数模式)
diff --git a/src/utils/db.js b/src/utils/db.js
index c4c56d1..49807e6 100644
--- a/src/utils/db.js
+++ b/src/utils/db.js
@@ -114,31 +114,71 @@ export async function addMessage(message) {
/**
* 批量添加或更新聊天记录
* @param {Array
@@ -305,6 +305,7 @@ import { parseTime } from '@/utils/datetime'
import { fileFormatSize, fileSuffix } from '@/utils/strings'
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
import { MessageComponents } from '@/constant/message'
+import { checkFileCanPreview } from '@/utils/helper/form'
const emits = defineEmits([
'clearSearchMemberByAlphabet',
@@ -693,11 +694,15 @@ const previewPDF = (item) => {
// downloadAndOpenFile(item)
// })
// }
- window.open(
- `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`,
- '_blank',
- 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
- )
+ if(checkFileCanPreview(item?.extra?.path || '')){
+ window.open(
+ `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`,
+ '_blank',
+ 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
+ )
+ } else {
+ toDialogueByMember(item)
+ }
}
const downloadAndOpenFile = (item) => {
diff --git a/src/components/search/searchItem.vue b/src/components/search/searchItem.vue
index 964a6f3..1822f5b 100644
--- a/src/components/search/searchItem.vue
+++ b/src/components/search/searchItem.vue
@@ -69,9 +69,9 @@
class="text-[12px] font-regular"
:text="resultDetail"
:searchText="props.searchText"
- v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 6"
+ v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 5 && props.searchItem?.msg_type !== 6"
/>
-
+
{
result_detail =
props.searchItem?.msg_type === 1
? props.searchItem?.extra?.content
- : props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 6
+ : props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 5 || props.searchItem?.msg_type === 6
? props.searchItem?.extra
: ChatMsgTypeMapping[props.searchItem?.msg_type]
break
@@ -310,11 +311,15 @@ const previewPDF = (item) => {
// downloadAndOpenFile(item)
// })
// }
- window.open(
- `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`,
- '_blank',
- 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
- )
+ if(checkFileCanPreview(item?.extra?.path || '')){
+ window.open(
+ `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`,
+ '_blank',
+ 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
+ )
+ } else {
+ //由于聊天记录本身有跳转到指定位置的逻辑,所以这里不需要再做跳转
+ }
}
\ No newline at end of file
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 12e0ffd..910ae5b 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -5,6 +5,7 @@ import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import Placeholder from '@tiptap/extension-placeholder'
import Mention from '@tiptap/extension-mention'
+import { computePosition, flip, shift } from '@floating-ui/dom'
import Link from '@tiptap/extension-link'
import { Extension, Node } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
@@ -36,6 +37,8 @@ import { EditorConst } from '@/constant/event-bus'
import { emitCall } from '@/utils/common'
// 引入默认头像常量
import { defAvatar } from '@/constant/default'
+// 引入提及建议功能
+import suggestion from './suggestion.js'
// 引入编辑器各子组件
import MeEditorVote from './MeEditorVote.vue' // 投票组件
import MeEditorEmoticon from './MeEditorEmoticon.vue' // 表情组件
@@ -205,118 +208,15 @@ const editor = useEditor({
class: 'mention',
},
suggestion: {
- allowedPrefixes: null,
- hideOnClickOutside: true,
- hideOnKeyDown: true,
- emptyQueryClass: 'is-empty-query',
+ ...suggestion,
items: ({ query }) => {
- if (!props.members.length) {
- return []
- }
-
- let list = [...props.members]
-
- if ((dialogueStore.groupInfo).is_manager) {
- list.unshift({ id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' })
- }
-
- const filteredItems = list.filter(
- (item) => item.nickname.toLowerCase().includes(query.toLowerCase())
- )
-
- // 如果没有匹配项,返回空数组以关闭弹窗
- if (filteredItems.length === 0) {
- return []
- }
-
- return filteredItems
- },
- render: () => {
- let component
- let popup
- let handleClickOutside
-
- return {
- onStart: (props) => {
- // 创建提及列表容器
- popup = document.createElement('div')
- popup.classList.add('ql-mention-list-container', 'me-scrollbar', 'me-scrollbar-thumb')
- document.body.appendChild(popup)
-
- // 添加全局点击事件监听器,点击弹窗外部时关闭弹窗
- handleClickOutside = (event) => {
- if (popup && !popup.contains(event.target)) {
- popup.remove()
- document.removeEventListener('click', handleClickOutside)
- }
- }
- // 使用setTimeout确保事件不会立即触发
- setTimeout(() => {
- document.addEventListener('click', handleClickOutside)
- }, 100)
-
- // 渲染提及列表
- props.items.forEach((item, index) => {
- const mentionItem = document.createElement('div')
- mentionItem.classList.add('ed-member-item')
- mentionItem.innerHTML = `
${item.nickname}`
- mentionItem.addEventListener('click', () => {
- props.command({ id: item.id, label: item.nickname })
- })
-
- if (index === props.selectedIndex) {
- mentionItem.classList.add('selected')
- }
-
- popup.appendChild(mentionItem)
- })
-
- // 定位提及列表
- const coords = props.clientRect()
- popup.style.position = 'fixed'
- popup.style.top = `${coords.top + window.scrollY}px`
- popup.style.left = `${coords.left + window.scrollX}px`
- },
-
- onUpdate: (props) => {
- // 更新选中项
- const items = popup.querySelectorAll('.ed-member-item')
- items.forEach((item, index) => {
- if (index === props.selectedIndex) {
- item.classList.add('selected')
- } else {
- item.classList.remove('selected')
- }
- })
- },
-
- onKeyDown: (props) => {
- // 处理键盘事件
- // Escape键关闭弹窗
- if (props.event.key === 'Escape') {
- popup.remove()
- return true
- }
-
- // 空格键、回车键或其他非导航键也关闭弹窗
- const navigationKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab']
- if (!navigationKeys.includes(props.event.key) && props.items.length === 0) {
- popup.remove()
- return false
- }
-
- return false
- },
-
- onExit: () => {
- // 清理弹窗和事件监听器
- if (popup) {
- popup.remove()
- // 移除所有可能的点击事件监听器
- document.removeEventListener('click', handleClickOutside)
- }
- },
- }
+ return suggestion.items({
+ query,
+ props: {
+ members: props.members,
+ isGroupManager: (dialogueStore.groupInfo).is_manager
+ }
+ })
},
},
}),
@@ -1171,39 +1071,6 @@ html[theme-mode='dark'] {
}
-/* 提及列表样式 */
-.ql-mention-list-container {
- width: 270px;
- max-height: 200px;
- background-color: #fff;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- border-radius: 4px;
- overflow-y: auto;
- z-index: 10000;
-
- .ed-member-item {
- display: flex;
- align-items: center;
- padding: 5px 10px;
- cursor: pointer;
-
- &:hover, &.selected {
- background-color: #f5f7fa;
- }
-
- .avator {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- margin-right: 8px;
- }
-
- .nickname {
- font-size: 14px;
- }
- }
-}
-
/* 暗色模式下的样式调整 */
html[theme-mode='dark'] {
.tiptap-editor {
@@ -1215,20 +1082,5 @@ html[theme-mode='dark'] {
background-color: var(--im-message-bg-color);
}
}
-
- .ql-mention-list-container {
- background-color: #1e1e1e;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
-
- .ed-member-item {
- &:hover, &.selected {
- background-color: #2c2c2c;
- }
-
- .nickname {
- color: #e0e0e0;
- }
- }
- }
}
\ No newline at end of file
diff --git a/src/components/editor/suggestion.js b/src/components/editor/suggestion.js
new file mode 100644
index 0000000..2c43b77
--- /dev/null
+++ b/src/components/editor/suggestion.js
@@ -0,0 +1,111 @@
+import { computePosition, flip, shift } from '@floating-ui/dom'
+import { posToDOMRect, VueRenderer } from '@tiptap/vue-3'
+
+import MentionList from './MentionList.vue'
+import { defAvatar } from '@/constant/default'
+
+const updatePosition = (editor, element) => {
+ const virtualElement = {
+ getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
+ }
+
+ computePosition(virtualElement, element, {
+ placement: 'bottom-start',
+ strategy: 'absolute',
+ middleware: [shift(), flip()],
+ }).then(({ x, y, strategy }) => {
+ element.style.position = strategy
+ if (window.__POWERED_BY_WUJIE__) {
+ element.style.left = `${x + 200}px`
+ element.style.top = `${y + 100}px`
+ } else {
+ element.style.left = `${x}px`
+ element.style.top = `${y}px`
+ }
+
+
+ })
+}
+
+export default {
+ items: ({ query, editor, props }) => {
+ if (!props.members || !props.members.length) {
+ return []
+ }
+
+ let list = [...props.members]
+
+ // 如果是群组管理员,添加"所有人"选项
+ if (props.isGroupManager) {
+ list.unshift({ id: 0, nickname: '所有人', avatar: defAvatar })
+ }
+
+ const filteredItems = list.filter(
+ (item) => item.nickname.toLowerCase().includes(query.toLowerCase())
+ )
+
+ // 如果没有匹配项,返回空数组以关闭弹窗
+ if (filteredItems.length === 0) {
+ return []
+ }
+
+ return filteredItems
+ },
+
+ render: () => {
+ let component
+
+ return {
+ onStart: props => {
+ // 如果没有匹配项,不创建弹窗
+ if (!props.items || props.items.length === 0) {
+ return
+ }
+
+ component = new VueRenderer(MentionList, {
+ // Vue 3 props格式
+ props,
+ editor: props.editor,
+ })
+
+ if (!props.clientRect) {
+ return
+ }
+
+ component.element.style.position = 'absolute'
+
+ document.body.appendChild(component.element)
+
+ updatePosition(props.editor, component.element)
+ },
+
+ onUpdate(props) {
+ component.updateProps(props)
+
+ if (props.items.length === 0) {
+ this.onExit()
+ return
+ }
+
+ if (!props.clientRect) {
+ return
+ }
+
+ updatePosition(props.editor, component.element)
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === 'Escape') {
+ this.onExit()
+ return true
+ }
+ return component.ref.onKeyDown(props)
+ },
+
+ onExit() {
+ component.element.remove()
+ component.destroy()
+ },
+ }
+ },
+}
\ No newline at end of file
diff --git a/src/utils/auth.js b/src/utils/auth.js
index c218336..a9815a7 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'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22eec7a138bb20774ef183e109945229d43e1f63fb01cdee46f5f663037f4ed946a0c04441b1f642c945d218180e84e91d272dc621be157602785ef226dd21b9b6c92c292bc73be90fad0320bad0812e11'
+ return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d220365eb2ca93ef31880576e2aa3ca8c45a705b447d40e300a54644829e2da528ea463bd2581a396336ed74880960d35716f5f7594e5b8cbb597027c6133b97b12df23427ca728fd2625977a0658ab470d'
}
/**
From 825ca5ec3c150c262c2fbaa0ac5fb6c133e0f37e Mon Sep 17 00:00:00 2001
From: wangyifeng <812766448@qq.com>
Date: Mon, 7 Jul 2025 09:32:47 +0800
Subject: [PATCH 17/22] =?UTF-8?q?=E5=A4=84=E7=90=86=E8=81=8A=E5=A4=A9?=
=?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=96=87=E4=BB=B6=E6=A0=BC=E5=BC=8F=E5=88=A4?=
=?UTF-8?q?=E6=96=AD=E8=A7=84=E5=88=99=E3=80=81=E8=B7=B3=E8=BD=AC=E6=8F=90?=
=?UTF-8?q?=E7=A4=BA=E7=AD=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/search/searchByCondition.vue | 24 ++++++++++++++-------
src/components/search/searchItem.vue | 19 ++++++++++++----
2 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/src/components/search/searchByCondition.vue b/src/components/search/searchByCondition.vue
index 8088731..6376bc8 100644
--- a/src/components/search/searchByCondition.vue
+++ b/src/components/search/searchByCondition.vue
@@ -668,15 +668,23 @@ const queryAllSearch = () => {
//文件类型图标
const fileTypeAvatar = (fileType) => {
+ //PDF文件扩展名映射
+ const PDF_EXTENSIONS = ['PDF', 'pdf']
+ // Excel文件扩展名映射
+ const EXCEL_EXTENSIONS = ['XLS', 'XLSX', 'CSV', 'xls', 'xlsx', 'csv']
+ // Word文件扩展名映射
+ const WORD_EXTENSIONS = ['DOC', 'DOCX', 'RTF', 'DOT', 'DOTX', 'doc', 'docx', 'rtf', 'dot', 'dotx']
+ // PPT文件扩展名映射
+ const PPT_EXTENSIONS = ['PPT', 'PPTX', 'PPS', 'PPSX', 'ppt', 'pptx', 'pps', 'ppsx']
let file_type_avatar = fileType_Files
if (fileType) {
- if (fileType === 'ppt' || fileType === 'pptx') {
+ if (PPT_EXTENSIONS.includes(fileType)) {
file_type_avatar = fileType_PPT
- } else if (fileType === 'pdf') {
+ } else if (PDF_EXTENSIONS.includes(fileType)) {
file_type_avatar = fileType_PDF
- } else if (fileType === 'doc' || fileType === 'docx') {
+ } else if (WORD_EXTENSIONS.includes(fileType)) {
file_type_avatar = fileType_WORD
- } else if (fileType === 'xls' || fileType === 'xlsx') {
+ } else if (EXCEL_EXTENSIONS.includes(fileType)) {
file_type_avatar = fileType_EXCEL
} else {
file_type_avatar = fileType_Files
@@ -694,7 +702,7 @@ const previewPDF = (item) => {
// downloadAndOpenFile(item)
// })
// }
- if(checkFileCanPreview(item?.extra?.path || '')){
+ if (checkFileCanPreview(item?.extra?.path || '')) {
window.open(
`${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`,
'_blank',
@@ -956,11 +964,11 @@ body:deep(.round-3) {
border-radius: 4px;
cursor: pointer;
border-bottom: 1px solid #f8f8f8;
-
+
&:hover {
- background-color: rgba(70, 41, 157, 0.1)
+ background-color: rgba(70, 41, 157, 0.1);
}
-
+
.attachment-avatar {
display: flex;
flex-direction: row;
diff --git a/src/components/search/searchItem.vue b/src/components/search/searchItem.vue
index 846ff05..e44e747 100644
--- a/src/components/search/searchItem.vue
+++ b/src/components/search/searchItem.vue
@@ -69,9 +69,17 @@
class="text-[12px] font-regular"
:text="resultDetail"
:searchText="props.searchText"
- v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 5 && props.searchItem?.msg_type !== 6"
+ v-if="
+ props.searchItem?.msg_type !== 3 &&
+ props.searchItem?.msg_type !== 5 &&
+ props.searchItem?.msg_type !== 6
+ "
/>
-
+
{
result_detail =
props.searchItem?.msg_type === 1
? props.searchItem?.extra?.content
- : props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 5 || props.searchItem?.msg_type === 6
+ : props.searchItem?.msg_type === 3 ||
+ props.searchItem?.msg_type === 5 ||
+ props.searchItem?.msg_type === 6
? props.searchItem?.extra
: ChatMsgTypeMapping[props.searchItem?.msg_type]
break
@@ -311,7 +321,7 @@ const previewPDF = (item) => {
// downloadAndOpenFile(item)
// })
// }
- if(checkFileCanPreview(item || '')){
+ if (checkFileCanPreview(item || '')) {
window.open(
`${import.meta.env.VITE_PAGE_URL}/office?url=${item}`,
'_blank',
@@ -319,6 +329,7 @@ const previewPDF = (item) => {
)
} else {
//由于聊天记录本身有跳转到指定位置的逻辑,所以这里不需要再做跳转
+ window['$message'].warning('暂不支持在线预览该类型文件')
}
}
From a05d637bd27c00d5e5876e1afb05ea12377f70ff Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Mon, 7 Jul 2025 09:43:43 +0800
Subject: [PATCH 18/22] =?UTF-8?q?style(=E7=BC=96=E8=BE=91=E5=99=A8):=20?=
=?UTF-8?q?=E8=B0=83=E6=95=B4=E6=8F=90=E5=8F=8A=E6=A0=B7=E5=BC=8F=E9=A2=9C?=
=?UTF-8?q?=E8=89=B2=E5=92=8C=E8=83=8C=E6=99=AF=E4=BB=A5=E9=80=82=E9=85=8D?=
=?UTF-8?q?=E6=B7=B1=E8=89=B2=E4=B8=BB=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
统一编辑器与消息组件中的提及样式,使用主题变量并增加内边距
---
src/components/editor/TiptapEditor.vue | 6 +++---
src/components/talk/message/TextMessage.vue | 2 +-
src/utils/strings.js | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 910ae5b..7b757d7 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -1058,10 +1058,10 @@ html[theme-mode='dark'] {
/* 提及样式 */
.mention {
- color: #0366d6;
- background-color: rgba(3, 102, 214, 0.1);
+ color: #fff;
+ background-color: var(--im-primary-color);
border-radius: 2px;
- padding: 0 2px;
+ padding: 0 5px;
}
/* 引用卡片样式 */
diff --git a/src/components/talk/message/TextMessage.vue b/src/components/talk/message/TextMessage.vue
index 68087dc..eba6792 100644
--- a/src/components/talk/message/TextMessage.vue
+++ b/src/components/talk/message/TextMessage.vue
@@ -17,7 +17,7 @@ let textContent = props.extra?.content || ''
textContent = textReplaceLink(textContent)
if (props.data.talk_type == 2) {
- textContent = textReplaceMention(textContent, float==='right'?'#fff':'#462AA0')
+ textContent = textReplaceMention(textContent, float==='right'?'#462AA0':'#fff',float==='right'?'#fff':'#462AA0')
}
textContent = textReplaceEmoji(textContent)
diff --git a/src/utils/strings.js b/src/utils/strings.js
index bba4767..9e0a9b5 100644
--- a/src/utils/strings.js
+++ b/src/utils/strings.js
@@ -42,9 +42,9 @@ export function textReplaceLink(text, color = '#409eff') {
* @param {String} text 文本
* @param {String} color 超链接颜色
*/
-export function textReplaceMention(text, color = '#2196F3') {
+export function textReplaceMention(text, color = '#2196F3',bg) {
return text.replace(new RegExp(/@\S+/, 'g'), ($0, $1) => {
- return `${$0}`
+ return `${$0}`
})
}
From 3f89777bf8895cd4298ee4e1e2ff99c7fe9e50f7 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Mon, 7 Jul 2025 09:44:39 +0800
Subject: [PATCH 19/22] =?UTF-8?q?fix(TextMessage):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E6=8F=90=E5=8F=8A=E6=96=87=E6=9C=AC=E9=A2=9C=E8=89=B2=E5=9C=A8?=
=?UTF-8?q?=E5=8F=B3=E4=BE=A7=E6=B5=AE=E5=8A=A8=E6=97=B6=E7=9A=84=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/talk/message/TextMessage.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/talk/message/TextMessage.vue b/src/components/talk/message/TextMessage.vue
index eba6792..e31677c 100644
--- a/src/components/talk/message/TextMessage.vue
+++ b/src/components/talk/message/TextMessage.vue
@@ -17,7 +17,7 @@ let textContent = props.extra?.content || ''
textContent = textReplaceLink(textContent)
if (props.data.talk_type == 2) {
- textContent = textReplaceMention(textContent, float==='right'?'#462AA0':'#fff',float==='right'?'#fff':'#462AA0')
+ textContent = textReplaceMention(textContent, float==='right'?'#462AA0':'#fff',float==='right'?'#EEE9F9':'#462AA0')
}
textContent = textReplaceEmoji(textContent)
From b18a1b2604f4883a136314e10907ef18a021e8f5 Mon Sep 17 00:00:00 2001
From: Phoenix <64720302+Concur-max@users.noreply.github.com>
Date: Mon, 7 Jul 2025 10:26:09 +0800
Subject: [PATCH 20/22] =?UTF-8?q?refactor(TiptapEditor):=20=E6=B8=85?=
=?UTF-8?q?=E7=90=86=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A=E5=92=8C=E6=A0=BC?=
=?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/editor/TiptapEditor.vue | 277 ++++++++++---------------
1 file changed, 108 insertions(+), 169 deletions(-)
diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue
index 7b757d7..7969b2a 100644
--- a/src/components/editor/TiptapEditor.vue
+++ b/src/components/editor/TiptapEditor.vue
@@ -1,5 +1,5 @@
-