diff --git a/package.json b/package.json
index f0a1f0a..8118e39 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
+ "@floating-ui/dom": "^1.7.2",
"@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/ion": "^1.2.3",
"@kangc/v-md-editor": "^2.3.18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ded82e..d6511c8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.17(typescript@5.2.2))
+ '@floating-ui/dom':
+ specifier: ^1.7.2
+ version: 1.7.2
'@highlightjs/vue-plugin':
specifier: ^2.1.0
version: 2.1.0(highlight.js@11.11.1)(vue@3.5.17(typescript@5.2.2))
@@ -568,6 +571,15 @@ packages:
cpu: [x64]
os: [win32]
+ '@floating-ui/core@1.7.2':
+ resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
+
+ '@floating-ui/dom@1.7.2':
+ resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
'@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -4280,6 +4292,17 @@ snapshots:
'@esbuild/win32-x64@0.25.5':
optional: true
+ '@floating-ui/core@1.7.2':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.2':
+ dependencies:
+ '@floating-ui/core': 1.7.2
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/utils@0.2.10': {}
+
'@hapi/hoek@9.3.0': {}
'@hapi/topo@5.1.0':
diff --git a/src/components/editor/MentionList.vue b/src/components/editor/MentionList.vue
new file mode 100644
index 0000000..5062e0b
--- /dev/null
+++ b/src/components/editor/MentionList.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
\ 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'
}
/**