This commit is contained in:
yinkang 2025-07-07 14:29:43 +08:00
commit 7999499702
22 changed files with 2702 additions and 187 deletions

View File

@ -15,10 +15,21 @@
},
"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",
"@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",
"@tiptap/extension-placeholder": "^2.23.1",
"@tiptap/pm": "^2.23.1",
"@tiptap/starter-kit": "^2.23.1",
"@tiptap/vue-3": "^2.23.1",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",

View File

@ -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))
@ -23,6 +26,36 @@ importers:
'@onlyoffice/document-editor-vue':
specifier: ^1.5.0
version: 1.5.0(vue@3.5.17(typescript@5.2.2))
'@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))
'@tiptap/extension-link':
specifier: ^2.23.1
version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-mention':
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))
'@tiptap/extension-placeholder':
specifier: ^2.23.1
version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/pm':
specifier: ^2.23.1
version: 2.23.1
'@tiptap/starter-kit':
specifier: ^2.23.1
version: 2.23.1
'@tiptap/vue-3':
specifier: ^2.23.1
version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(vue@3.5.17(typescript@5.2.2))
'@vicons/fluent':
specifier: ^0.13.0
version: 0.13.0
@ -538,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==}
@ -711,6 +753,12 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rollup/pluginutils@5.2.0':
resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
engines: {node: '>=14.0.0'}
@ -850,6 +898,173 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@tiptap/core@2.23.1':
resolution: {integrity: sha512-EURGKGsEPrwxvOPi9gA+BsczvsECJNV+xgTAGWHmEtU4YJ0AulYrCX3b7FK+aiduVhThIHDoG/Mmvmb/HPLRhQ==}
peerDependencies:
'@tiptap/pm': ^2.7.0
'@tiptap/extension-blockquote@2.23.1':
resolution: {integrity: sha512-GI3s+uFU88LWRaDG20Z9yIu2av3Usn8kw2lkm2ntwX1K6/mQBS/zkGhWr/FSwWOlMtTzYFxF4Ttb0e+hn67A/A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bold@2.23.1':
resolution: {integrity: sha512-OM4RxuZeOqpYRN1G/YpXSE8tZ3sVtT2XlO3qKa74qf+htWz8W3x4X0oQCrHrRTDSAA1wbmeZU3QghAIHnbvP/A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bubble-menu@2.23.1':
resolution: {integrity: sha512-tupuvrlZMTziVZXJuCVjLwllUnux/an9BtTYHpoRyLX9Hg0v7Kh39k9x58zJaoW8Q/Oc/qxPhbJpyOqhE1rLeg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-bullet-list@2.23.1':
resolution: {integrity: sha512-0g9U42m+boLJZP3x9KoJHDCp9WD5abaVdqNbTg9sFPDNsepb7Zaeu8AEB+yZLP/fuTI1I4ko6qkdr3UaaIYcmA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-code-block@2.23.1':
resolution: {integrity: sha512-eYzJVUR13BhSE/TYAMZihGBId+XiwhnTPqGcSFo+zx89It/vxwDLvAUn0PReMNI7ULKPTw8orUt2fVKSarb2DQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.23.1':
resolution: {integrity: sha512-3IOdE40m0UTR2+UXui69o/apLtutAbtzfgmMxD6q0qlRvVqz99QEfk9RPHDNlUqJtYCL4TD+sj7UclBsDdgVXA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.23.1':
resolution: {integrity: sha512-2nkIkGVsaMJkpd024E6vXK+5XNz8VOVWp/pM6bbXpuv0HnGPrfLdh4ruuFc+xTQ3WPOmpSu8ygtujt4I1o9/6g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.23.1':
resolution: {integrity: sha512-GyVp+o/RVrKlLdrQvtIpJGphFGogiPjcPCkAFcrfY1vDY1EYxfVZELC96gG1mUT1BO8FUD3hmbpkWi9l8/6O4A==}
peerDependencies:
'@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:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.23.1':
resolution: {integrity: sha512-iP+TiFIGZEbOvYAs04pI14mLI4xqbt64Da91TgMF1FNZUrG+9eWKjqbcHLQREuK3Qnjn5f0DI4nOBv61FlnPmA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.23.1':
resolution: {integrity: sha512-YF66EVxnBxt1bHPx6fUUSSXK1Vg+/9baJ0AfJ12hCSPCgSjUclRuNmWIH5ikVfByOmPV1xlrN9wryLoSEBcNRQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-heading@2.23.1':
resolution: {integrity: sha512-5BPoli9wudiAOgSyK8309jyRhFyu5vd02lNChfpHwxUudzIJ/L+0E6FcwrDcw+yXh23cx7F5SSjtFQ7AobxlDQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.23.1':
resolution: {integrity: sha512-1rp2CRjM+P58oGEgeUUDSk0ch67ngIGbGJOOjiBGKU9GIVhI2j4uSwsYTAa9qYMjMUI6IyH1xJpsY2hLKcBOtg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-horizontal-rule@2.23.1':
resolution: {integrity: sha512-uHEF0jpmhtgAxjKw8/s5ipEeTnu99f9RVMGAlmcthJ5Fx9TzH0MvtH4dtBNEu5MXC7+0bNsnncWo125AAbCohg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-image@2.23.1':
resolution: {integrity: sha512-8j2FLBWKq6j27aQcOAlmyKixxHflW8j5FhxLgPPS6hithgFQVET4OYH+1c6r7Qd/T4YoAqt/0PmNZ/gYWI9gzg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-italic@2.23.1':
resolution: {integrity: sha512-a+cPzffaC/1AKMmZ1Ka6l81xmTgcalf8NXfBuFCUTf5r7uI9NIgXnLo9hg+jR9F4K+bwhC4/UbMvQQzAjh0c0A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.23.1':
resolution: {integrity: sha512-zMD0V8djkvwRYACzd8EvFXXNLQH5poJt6aHC9/8uest6njRhlrRjSjwG5oa+xHW4A76XfAH0A5BPj6ZxZnAUQg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-list-item@2.23.1':
resolution: {integrity: sha512-wVrRp6KAiyjFVFGmn+ojisP64Bsd+ZPdqQBYVbebBx1skZeW0uhG60d7vUkWHi0gCgxHZDfvDbXpfnOD0INRWw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-mention@2.23.1':
resolution: {integrity: sha512-YaHg2ZqNb2Vur5hBoswzu8pJ3kV54PPonR8gHgelD83T1s8vBqdl03WL2NUVciovEbENnirePLDfhKaFjRXSzQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/suggestion': ^2.7.0
'@tiptap/extension-ordered-list@2.23.1':
resolution: {integrity: sha512-Zp+qognyNgoaJ9bxkBwIuWJEnQ67RdsHXzv3YOdeGRbkUhd8LT6OL7P0mAuNbMBU8MwHxyJ7C7NsyzwzuVbFzA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-paragraph@2.23.1':
resolution: {integrity: sha512-LLEPizt1ALE7Ek6prlJ1uhoUCT8C/a3PdZpCh3DshM1L3Kv9TENlaJL2GhFl8SVUCwHmWHvXg30+4tIRFBedaQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-placeholder@2.23.1':
resolution: {integrity: sha512-zSkCljVpxJh3GHW7ppFNYhHPjYKmS3Tw0e74BOGzb5TqP57GRvnPgDGg4fr6kDsSWMo9minnNM3PDA7kNT+PRQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-strike@2.23.1':
resolution: {integrity: sha512-hAT9peYkKezRGp/EcPQKtyYQT+2XGUbb26toTr9XIBQIeQCuCpT+FirPrDMrMVWPwcJt7Rv+AzoVjDuBs9wE0A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-style@2.23.1':
resolution: {integrity: sha512-fZn1GePlL27pUFfKXKoRZo4L4pZP9dUjNNiS/eltLpbi/SenJ15UKhAoHtN1KQvNGJsWkYN49FjnnltU8qvQ+Q==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text@2.23.1':
resolution: {integrity: sha512-XK0D/eyS1Vm5yUrCtkS0AfgyKLJqpi8nJivCOux/JLhhC4x87R1+mI8NoFDYZJ5ic/afREPSBB8jORqOi0qIHg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm@2.23.1':
resolution: {integrity: sha512-iAx4rP0k4Xd9Ywh+Gpaz5IWfY2CYRpiwVXWekTHLlNRFtrVIWVpMxaQr2mvRU2g0Ca6rz5w3KzkHBMqrI3dIBA==}
'@tiptap/starter-kit@2.23.1':
resolution: {integrity: sha512-rrImwzJbKSHoFa+WdNU4I0evXcMiQ4yRm737sxvNJwYItT6fXIxrbRT7nJDmtYu2TflcfT1KklEnSrzz1hhYRw==}
'@tiptap/suggestion@2.23.1':
resolution: {integrity: sha512-ryAsYhXmehwQLJnGFw9VHfQMANeWgFzDQUfgg0vcjbHJAa0t3qBsSOUP2RKOuErQmCvlgY/cgxO/6Q0l5IzsIA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/vue-3@2.23.1':
resolution: {integrity: sha512-wBF9LxI96qMXE0Z+3C8GU4mmDRbCapcnjb/euQ1NeihqBwK6qdd5g8IIgGm+rQCSY8hCJBUTWJtfn7vJAasSDQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
vue: ^3.0.0
'@tsconfig/node18@18.2.4':
resolution: {integrity: sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ==}
@ -883,15 +1098,24 @@ packages:
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.18':
resolution: {integrity: sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdast@3.0.15':
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
@ -1485,6 +1709,9 @@ packages:
cose-base@1.0.3:
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@ -1811,9 +2038,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'}
@ -1881,6 +2120,10 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
@ -2248,6 +2491,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'}
@ -2442,6 +2688,12 @@ packages:
linkify-it@3.0.3:
resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.1:
resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==}
loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@ -2516,6 +2768,10 @@ packages:
resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==}
hasBin: true
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
markdown-it@8.4.2:
resolution: {integrity: sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==}
hasBin: true
@ -2536,6 +2792,9 @@ packages:
mdurl@1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
@ -2766,6 +3025,9 @@ packages:
resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==}
engines: {node: '>=18'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
@ -2885,12 +3147,74 @@ packages:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
prosemirror-changeset@2.3.1:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
prosemirror-history@1.4.1:
resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
prosemirror-inputrules@1.5.0:
resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-markdown@1.13.2:
resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
prosemirror-menu@1.2.5:
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
prosemirror-model@1.25.1:
resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
prosemirror-schema-basic@1.2.4:
resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
prosemirror-tables@1.7.1:
resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==}
prosemirror-trailing-node@3.0.0:
resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
peerDependencies:
prosemirror-model: ^1.22.1
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
prosemirror-transform@1.10.4:
resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
prosemirror-view@1.40.0:
resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
@ -2980,6 +3304,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
run-applescript@7.0.0:
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
engines: {node: '>=18'}
@ -3223,6 +3550,9 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'}
@ -3274,6 +3604,9 @@ packages:
uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@ -3538,6 +3871,9 @@ packages:
peerDependencies:
vue: ^3.0.11
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
wait-on@6.0.1:
resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==}
engines: {node: '>=10.0.0'}
@ -3956,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':
@ -4123,6 +4470,10 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@popperjs/core@2.11.8': {}
'@remirror/core-constants@3.0.0': {}
'@rollup/pluginutils@5.2.0(rollup@4.44.0)':
dependencies:
'@types/estree': 1.0.8
@ -4208,6 +4559,193 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@tiptap/core@2.23.1(@tiptap/pm@2.23.1)':
dependencies:
'@tiptap/pm': 2.23.1
'@tiptap/extension-blockquote@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-bold@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-bubble-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)
'@tiptap/pm': 2.23.1
tippy.js: 6.3.7
'@tiptap/extension-bullet-list@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-code-block@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)
'@tiptap/pm': 2.23.1
'@tiptap/extension-code@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-document@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-dropcursor@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)
'@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)
'@tiptap/pm': 2.23.1
tippy.js: 6.3.7
'@tiptap/extension-gapcursor@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)
'@tiptap/pm': 2.23.1
'@tiptap/extension-hard-break@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-heading@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-history@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)
'@tiptap/pm': 2.23.1
'@tiptap/extension-horizontal-rule@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)
'@tiptap/pm': 2.23.1
'@tiptap/extension-image@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-italic@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-link@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)
'@tiptap/pm': 2.23.1
linkifyjs: 4.3.1
'@tiptap/extension-list-item@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-mention@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))':
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)
'@tiptap/extension-ordered-list@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-paragraph@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-placeholder@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)
'@tiptap/pm': 2.23.1
'@tiptap/extension-strike@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-text-style@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-text@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/pm@2.23.1':
dependencies:
prosemirror-changeset: 2.3.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.4.1
prosemirror-inputrules: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-markdown: 1.13.2
prosemirror-menu: 1.2.5
prosemirror-model: 1.25.1
prosemirror-schema-basic: 1.2.4
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.3
prosemirror-tables: 1.7.1
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/starter-kit@2.23.1':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-blockquote': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-bold': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-bullet-list': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-code': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-code-block': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-document': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-dropcursor': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-gapcursor': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-hard-break': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-heading': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-history': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-horizontal-rule': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-italic': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-list-item': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-ordered-list': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-paragraph': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-strike': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-text': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))
'@tiptap/extension-text-style': 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)':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/pm': 2.23.1
'@tiptap/vue-3@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(vue@3.5.17(typescript@5.2.2))':
dependencies:
'@tiptap/core': 2.23.1(@tiptap/pm@2.23.1)
'@tiptap/extension-bubble-menu': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/extension-floating-menu': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)
'@tiptap/pm': 2.23.1
vue: 3.5.17(typescript@5.2.2)
'@tsconfig/node18@18.2.4': {}
'@types/d3-scale-chromatic@3.1.0': {}
@ -4243,16 +4781,25 @@ snapshots:
'@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.18
'@types/lodash@4.17.18': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdast@3.0.15':
dependencies:
'@types/unist': 2.0.11
'@types/mdurl@2.0.0': {}
'@types/minimatch@5.1.2': {}
'@types/ms@2.1.0': {}
@ -5058,6 +5605,8 @@ snapshots:
dependencies:
layout-base: 1.0.2
crelt@1.0.6: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@ -5390,8 +5939,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
@ -5479,6 +6036,8 @@ snapshots:
escape-string-regexp@1.0.5: {}
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
eslint-scope@5.1.1:
@ -5864,6 +6423,8 @@ snapshots:
is-docker@3.0.0: {}
is-emoji-supported@0.0.5: {}
is-extendable@0.1.1: {}
is-extendable@1.0.1:
@ -6033,6 +6594,12 @@ snapshots:
dependencies:
uc.micro: 1.0.6
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.1: {}
loader-runner@4.3.0: {}
local-pkg@1.1.1:
@ -6100,6 +6667,15 @@ snapshots:
mdurl: 1.0.1
uc.micro: 1.0.6
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-it@8.4.2:
dependencies:
argparse: 1.0.10
@ -6135,6 +6711,8 @@ snapshots:
mdurl@1.0.1: {}
mdurl@2.0.0: {}
memorystream@0.3.1: {}
merge-stream@2.0.0: {}
@ -6489,6 +7067,8 @@ snapshots:
is-inside-container: 1.0.0
is-wsl: 3.1.0
orderedmap@2.1.1: {}
package-manager-detector@1.3.0: {}
parchment@1.1.4: {}
@ -6575,11 +7155,116 @@ snapshots:
prismjs@1.30.0: {}
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.10.4
prosemirror-collab@1.3.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-gapcursor@1.3.2:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-history@1.4.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.0:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
prosemirror-markdown@1.13.2:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
prosemirror-model: 1.25.1
prosemirror-menu@1.2.5:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.7.1
prosemirror-history: 1.4.1
prosemirror-state: 1.4.3
prosemirror-model@1.25.1:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-state@1.4.3:
dependencies:
prosemirror-model: 1.25.1
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-tables@1.7.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-transform@1.10.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-view@1.40.0:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
proxy-from-env@1.1.0: {}
prr@1.0.1:
optional: true
punycode.js@2.3.1: {}
quansync@0.2.10: {}
queue-microtask@1.2.3: {}
@ -6688,6 +7373,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.44.0
fsevents: 2.3.3
rope-sequence@1.3.4: {}
run-applescript@7.0.0: {}
run-parallel@1.2.0:
@ -6929,6 +7616,10 @@ snapshots:
fdir: 6.4.6(picomatch@4.0.2)
picomatch: 4.0.2
tippy.js@6.3.7:
dependencies:
'@popperjs/core': 2.11.8
to-object-path@0.3.0:
dependencies:
kind-of: 3.2.2
@ -6969,6 +7660,8 @@ snapshots:
uc.micro@1.0.6: {}
uc.micro@2.1.0: {}
ufo@1.6.1: {}
unconfig@0.3.13:
@ -7270,6 +7963,8 @@ snapshots:
vooks: 0.2.12(vue@3.5.17(typescript@5.2.2))
vue: 3.5.17(typescript@5.2.2)
w3c-keyname@2.2.8: {}
wait-on@6.0.1:
dependencies:
axios: 0.25.0

View File

@ -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搜索群组数据

View File

@ -33,7 +33,6 @@ const props = defineProps({
const emit = defineEmits(['editor-event'])
const userStore = useUserStore()
const dialogueStore = useDialogueStore()
console.log('dialogueStore', dialogueStore.talk.talk_type)
const editorDraftStore = useEditorDraftStore()
const editorRef = ref(null)
const content = ref('')
@ -172,7 +171,6 @@ const updateMentionPosition = (range) => {
}
}
const insertMention = (member, clonedRange) => {
console.log('插入mention', member);
const selection = window.getSelection();
if (!clonedRange || !selection || !editorRef.value) return;
const range = clonedRange;
@ -565,20 +563,20 @@ const parseEditorContent = () => {
currentTextBuffer += '\n';
break;
case 'IMG':
flushTextBufferIfNeeded();
const src = node.getAttribute('src');
const alt = node.getAttribute('alt');
const isEmojiPic = node.classList.contains('editor-emoji');
const isTextEmojiPlaceholder = node.classList.contains('emoji');
if (isTextEmojiPlaceholder && alt) {
if ((isEmojiPic || isTextEmojiPlaceholder) && alt) {
currentTextBuffer += alt;
} else if (src) {
flushTextBufferIfNeeded();
items.push({
type: 3,
content: src,
isEmoji: isEmojiPic,
width: node.getAttribute('data-original-width') || node.width || null,
height: node.getAttribute('data-original-height') || node.height || null,
isEmoji: false,
width: node.getAttribute('data-original-width') || node.width,
height: node.getAttribute('data-original-height') || node.height
});
}
break;
@ -887,12 +885,8 @@ const insertImageEmoji = (imgSrc, altText) => {
img.className = 'editor-emoji';
img.setAttribute('data-role', 'emoji');
range.insertNode(img);
const spaceNode = document.createTextNode('\u00A0');
range.setStartAfter(img);
range.collapse(true);
range.insertNode(spaceNode);
range.setStartAfter(spaceNode);
range.collapse(true);
if (selection) {
selection.removeAllRanges();
selection.addRange(range);

View File

@ -0,0 +1,161 @@
<template>
<div class="dropdown-menu">
<n-virtual-list
ref="virtualListRef"
style="max-height: 240px"
:item-size="50"
:items="props.items"
>
<template #default="{ item }">
<button
:class="{ 'is-selected': props.items[selectedIndex] === item }"
@click="selectItem(item)"
>
<img :src="item.avatar" class="avatar" />
<span class="nickname">{{ item.nickname }}</span>
</button>
</template>
</n-virtual-list>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineExpose } from 'vue'
const props = defineProps({
items: {
type: Array,
required: true
},
command: {
type: Function,
required: true
}
})
const selectedIndex = ref(0)
const virtualListRef = ref(null)
watch(
() => props.items,
() => {
selectedIndex.value = 0
}
)
const onKeyDown = ({ event }) => {
console.log('event',event)
if (event.key === 'ArrowUp') {
upHandler()
return true
}
if (event.key === 'ArrowDown') {
downHandler()
return true
}
if (event.key === 'Enter') {
enterHandler()
return true
}
return false
}
const upHandler = () => {
selectedIndex.value =
(selectedIndex.value + props.items.length - 1) % props.items.length
virtualListRef.value?.scrollTo({ index: selectedIndex.value })
}
const downHandler = () => {
selectedIndex.value = (selectedIndex.value + 1) % props.items.length
virtualListRef.value?.scrollTo({ index: selectedIndex.value })
}
const enterHandler = () => {
selectItem(props.items[selectedIndex.value])
}
const selectItem = item => {
if (item) {
props.command({ id: item.id, label: item.nickname })
}
}
defineExpose({
onKeyDown
})
</script>
<style lang="scss">
.dropdown-menu {
background: var(--white, #fff);
border: 1px solid var(--gray-1, #e0e0e0);
border-radius: 0.7rem;
box-shadow: var(--shadow, 0 2px 12px 0 rgba(0, 0, 0, 0.1));
display: flex;
flex-direction: column;
gap: 0.1rem;
overflow: auto;
padding: 0.4rem;
position: relative;
max-height: 200px;
width: 200px;
button {
align-items: center;
background-color: transparent;
display: flex;
gap: 0.25rem;
text-align: left;
width: 100%;
padding: 5px 10px;
border: none;
cursor: pointer;
&:hover,
&:hover.is-selected {
background-color: var(--gray-3, #f5f7fa);
}
&.is-selected {
background-color: var(--gray-2, #f0f0f0);
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 8px;
}
.nickname {
font-size: 14px;
}
}
}
/* 暗色模式下的样式调整 */
html[theme-mode='dark'] {
.dropdown-menu {
background-color: #1e1e1e;
border-color: #333;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
button {
&:hover,
&:hover.is-selected {
background-color: #2c2c2c;
}
&.is-selected {
background-color: #333;
}
.nickname {
color: #e0e0e0;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
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
}
if(!component?.props.items?.length){
return false
}
return component.ref.onKeyDown(props)
},
onExit() {
console.log('component.element',component.element)
component.element.remove()
component.destroy()
},
}
},
}

View File

@ -1,70 +1,172 @@
<template>
<span>
<template v-for="(part, index) in parts" :key="index">
<span v-if="part.highlighted" :class="highlightClass">
{{ part.text }}
<template v-if="isHtml">
<span v-html="highlightedHtml" />
</template>
<template v-else>
<span class="text-content">
<template v-for="(part, index) in parts" :key="index">
<span
v-if="part.highlighted"
:class="highlightClass"
v-html="textReplaceEmoji(part.text)"
/>
<span v-else v-html="textReplaceEmoji(part.text)" />
</template>
</span>
<span v-else>{{ part.text }}</span>
</template>
</span>
</template>
<script setup>
import { computed } from 'vue'
import { textReplaceEmoji } from '@/utils/emojis'
const props = defineProps({
text: {
type: String,
required: true,
required: true
},
searchText: {
type: String,
default: '',
default: ''
},
highlightClass: {
type: String,
default: 'highlight',
},
default: 'highlight'
}
})
// HTML
const isHtml = computed(() => {
return /<[^>]*>/g.test(props.text)
})
const escapedSearchText = computed(() =>
String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
)
const pattern = computed(() => new RegExp(escapedSearchText.value, 'gi'))
const parts = computed(() => {
if (!props.searchText || !props.text)
return [{ text: props.text, highlighted: false }];
if (!props.searchText || !props.text) return [{ text: props.text, highlighted: false }]
const result = [];
let currentIndex = 0;
const escapedSearchTextValue = escapedSearchText.value;
const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi');
const result = []
let currentIndex = 0
const escapedSearchTextValue = escapedSearchText.value
const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi')
props.text.replace(searchPattern, (match, p1, offset) => {
//
if (currentIndex < offset) {
result.push({ text: props.text.slice(currentIndex, offset), highlighted: false });
result.push({ text: props.text.slice(currentIndex, offset), highlighted: false })
}
//
result.push({ text: p1, highlighted: true });
result.push({ text: p1, highlighted: true })
//
currentIndex = offset + p1.length;
return p1; // replace
});
currentIndex = offset + p1.length
return p1 // replace
})
//
if (currentIndex < props.text.length) {
result.push({ text: props.text.slice(currentIndex), highlighted: false });
result.push({ text: props.text.slice(currentIndex), highlighted: false })
}
return result;
});
return result
})
//
const processSpecialChars = (text) => {
return (
text
//
.replace(/\n/g, '<br>')
//
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
// &nbsp;
.replace(/ {2,}/g, (match) => {
return '&nbsp;'.repeat(match.length)
})
//
.replace(/[\u200B-\u200D\uFEFF]/g, '')
)
}
// HTML - 使
const highlightedHtml = computed(() => {
if (!props.searchText || !props.text) {
//
return textReplaceEmoji(processSpecialChars(props.text))
}
// 使
// HTML
// HTML
const parts = []
let lastIndex = 0
const tagRegex = /<[^>]*>/g
let tagMatch
// lastIndex
tagRegex.lastIndex = 0
while ((tagMatch = tagRegex.exec(props.text)) !== null) {
//
if (tagMatch.index > lastIndex) {
const textBeforeTag = props.text.slice(lastIndex, tagMatch.index)
if (textBeforeTag) {
//
const processedText = processSpecialChars(textBeforeTag)
const searchPattern = new RegExp(`(${escapedSearchText.value})`, 'gi')
const highlightedText = processedText.replace(
searchPattern,
`<span class="${props.highlightClass}">$1</span>`
)
parts.push(highlightedText)
}
}
//
parts.push(tagMatch[0])
lastIndex = tagMatch.index + tagMatch[0].length
}
//
if (lastIndex < props.text.length) {
const textAfterLastTag = props.text.slice(lastIndex)
if (textAfterLastTag) {
//
const processedText = processSpecialChars(textAfterLastTag)
const searchPattern = new RegExp(`(${escapedSearchText.value})`, 'gi')
const highlightedText = processedText.replace(
searchPattern,
`<span class="${props.highlightClass}">$1</span>`
)
parts.push(highlightedText)
}
}
let html = parts.join('')
//
return textReplaceEmoji(html)
})
</script>
<style scoped>
.highlight {
color: #7a58de;
}
.text-content {
white-space: pre-wrap;
word-break: break-word;
:deep(.emoji) {
vertical-align: text-bottom!important;
margin: 0 5px !important;
width: 22px !important;
height: 22px !important;
}
}
</style>

View File

@ -137,7 +137,7 @@
@click="toDialogueByMember(item)"
:searchResultKey="'search_by_member_condition'"
:searchItem="item"
:searchText="state.searchText"
:searchText="props?.searchRecordByConditionText"
:searchRecordDetail="true"
></searchItem>
</div>
@ -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',
@ -667,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
@ -693,11 +702,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) => {
@ -951,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;

View File

@ -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 !== 6"
v-if="
props.searchItem?.msg_type !== 3 &&
props.searchItem?.msg_type !== 5 &&
props.searchItem?.msg_type !== 6
"
/>
<div class="message-component-wrapper" v-if="props.searchItem?.msg_type === 3" @click.stop>
<div
class="message-component-wrapper"
v-if="props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 5"
@click.stop
>
<component
:is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'"
:extra="resultDetail"
@ -122,6 +130,7 @@ import { ref, watch, computed, onMounted, onUnmounted, reactive, defineProps } f
import HighlightText from './highLightText.vue'
import { beautifyTime } from '@/utils/datetime'
import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message'
import { checkFileCanPreview } from '@/utils/helper/form'
const props = defineProps({
searchItem: Object | Number,
searchResultKey: {
@ -291,7 +300,9 @@ const resultDetail = computed(() => {
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 +321,16 @@ 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 || '')) {
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 {
//
window['$message'].warning('暂不支持在线预览该类型文件')
}
}
</script>
<style lang="scss" scoped>
@ -377,7 +393,8 @@ const previewPDF = (item) => {
}
.file-message-wrapper {
.condition-each-result-attachments {
width: 289px;
min-width: 289px;
max-width: 660px;
height: 62px;
display: flex;
flex-direction: row;
@ -447,6 +464,7 @@ const previewPDF = (item) => {
span {
color: #191919;
word-break: break-all;
max-width: 660px;
}
.searchRecordDetail-fastLocal {
display: none;

View File

@ -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'?'#EEE9F9':'#462AA0')
}
textContent = textReplaceEmoji(textContent)

View File

@ -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
})
}
}
}

View File

@ -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,121 @@ 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) {
// 根据用户建议只比较msg_id和is_revoke字段
// 因为消息ID是唯一的内容变化主要是由撤回操作引起的
const compareMessage = (localMsg, serverMsg) => {
// 消息ID已在外部比较过这里只需检查is_revoke状态
return localMsg.is_revoke === serverMsg.is_revoke
}
const firstMatch = compareMessage(firstLocalMsg, items[firstServerIdx])
const lastMatch = compareMessage(lastLocalMsg, items[lastServerIdx])
// 如果首尾消息匹配,进行全量检查所有消息
if (firstMatch && lastMatch) {
// 全量检查策略:检查所有消息
// 由于一次只有30条消息全量检查不会带来太大的性能负担
let allMatch = true
// 遍历所有本地消息,与服务器消息进行比较
for (let i = 0; i < localMessages.length; i++) {
const localMsg = localMessages[i]
const serverIdx = serverMsgMap.get(localMsg.msg_id)
// 如果消息ID不存在于服务器数据中或者消息内容不匹配
if (serverIdx === undefined || !compareMessage(localMsg, items[serverIdx])) {
allMatch = false
console.log(`消息不匹配,索引: ${i}, 消息ID: ${localMsg.msg_id}`)
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 +326,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 +413,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 +444,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 +461,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 +472,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 +509,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 +532,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 +540,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 +588,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
}
}
// 向上加载更多(兼容特殊参数模式)

View File

@ -181,7 +181,7 @@ export const useTalkStore = defineStore('talk', {
// 更新状态和本地数据库
this.items = serverItems
console.log('serverItems',serverItems)
// 将最新的会话列表保存到本地数据库
for (const item of serverItems) {
await addOrUpdateConversation(item)

View File

@ -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'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d220365eb2ca93ef31880576e2aa3ca8c45a705b447d40e300a54644829e2da528ea463bd2581a396336ed74880960d35716f5f7594e5b8cbb597027c6133b97b12df23427ca728fd2625977a0658ab470d'
}
/**

View File

@ -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', () => {
@ -87,31 +114,71 @@ export async function addMessage(message) {
/**
* 批量添加或更新聊天记录
* @param {Array<object>} messages - 消息对象数组
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @param {boolean} [updateConversation=true] - 是否更新会话信息
* @param {string} [sortField='created_at'] - 排序字段
* @returns {Promise<void>}
*/
export async function batchAddOrUpdateMessages(messages) {
export async function batchAddOrUpdateMessages(messages, talkType, receiverId, updateConversation = true, sortField = 'created_at') {
try {
if (!Array.isArray(messages) || messages.length === 0) {
return;
}
const messagesToStore = messages.map(message => {
if (!message.msg_id) {
message.msg_id = generateUUID();
// 使用批处理优化性能
return await db.transaction('rw', db.messages, db.conversations, async () => {
// 预处理消息数据,避免在循环中多次创建对象
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
// 使用for循环替代map减少内存分配
const messagesToStore = new Array(messages.length);
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
// 确保必要字段存在
if (!message.msg_id) {
message.msg_id = generateUUID();
}
if (!message.created_at) {
message.created_at = now;
}
// 确保talk_type和receiver_id字段存在
if (talkType && !message.talk_type) {
message.talk_type = talkType;
}
if (receiverId && !message.receiver_id) {
message.receiver_id = receiverId;
}
messagesToStore[i] = message;
}
if (!message.created_at) {
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
// 使用bulkPut批量插入/更新,提高性能
await db.messages.bulkPut(messagesToStore);
// 只有在需要时才更新会话信息
if (updateConversation && messagesToStore.length > 0) {
// 根据排序字段找出最新消息
let latestMessage;
if (sortField === 'sequence') {
// 按sequence排序找出最大的
latestMessage = messagesToStore.reduce((max, current) => {
return (current.sequence > (max.sequence || 0)) ? current : max;
}, messagesToStore[0]);
} else {
// 默认按created_at排序
latestMessage = messagesToStore.reduce((latest, current) => {
if (!latest.created_at) return current;
if (!current.created_at) return latest;
return new Date(current.created_at) > new Date(latest.created_at) ? current : latest;
}, messagesToStore[0]);
}
// 异步更新会话最后消息,不阻塞主流程
updateConversationLastMessage(latestMessage).catch(err => {
console.error('更新会话最后消息失败:', err);
});
}
return message;
});
await db.messages.bulkPut(messagesToStore);
// 更新最后一条消息到会话
const latestMessage = messagesToStore[messagesToStore.length - 1];
if (latestMessage) {
await updateConversationLastMessage(latestMessage);
}
} catch (error) {
console.error('批量添加或更新消息失败:', error);
throw error;
@ -125,35 +192,78 @@ export async function batchAddOrUpdateMessages(messages) {
* @param {number} receiverId - 接收者ID (私聊为对方用户ID群聊为群ID)
* @param {number} [limit=30] - 限制返回的记录数量
* @param {number|null} [maxSequence=null] - 最大sequence值用于分页加载更早的消息
* @param {string} [sortField='sequence'] - 排序字段默认按sequence排序
* @returns {Promise<Array<object>>} 消息列表 (按sequence升序排列)
*/
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) {
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null, sortField = 'sequence') {
try {
// 使用缓存优化重复查询
const cacheKey = `${talkType}_${receiverId}_${limit}_${maxSequence}_${sortField}`;
const cachedResult = messageCache.get(cacheKey);
// 如果缓存存在且未过期,直接返回缓存结果
if (cachedResult && (Date.now() - cachedResult.timestamp < 2000)) { // 2秒缓存
return cachedResult.data;
}
let collection;
// 优化查询策略
if (maxSequence !== null) {
// 加载更多:查询 sequence 小于 maxSequence 的消息
// 使用复合索引优化查询
collection = db.messages
.where('[talk_type+receiver_id+sequence]')
.between([talkType, receiverId, 0], [talkType, receiverId, maxSequence], true, false);
} else {
// 首次加载:查询指定会话的所有消息
collection = db.messages.where({ '[talk_type+receiver_id]': [talkType, receiverId] });
// 使用复合索引优化查询
collection = db.messages.where('[talk_type+receiver_id]').equals([talkType, receiverId]);
}
// 1. reverse() - 利用索引倒序排列,获取最新的消息
// 2. limit() - 限制数量,实现分页
// 3. toArray() - 执行查询
const messages = await collection.reverse().limit(limit).toArray();
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
return messages.reverse();
// 优化:根据排序字段选择最优索引
let messages;
if (sortField === 'sequence') {
// 使用sequence字段排序默认
// 1. reverse() - 利用索引倒序排列,获取最新的消息
// 2. limit() - 限制数量,实现分页
// 3. toArray() - 执行查询一次性获取所有数据减少IO操作
messages = await collection.reverse().limit(limit).toArray();
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
messages = messages.reverse();
} else if (sortField === 'created_at') {
// 使用created_at字段排序
messages = await collection.toArray();
// 在内存中排序,避免数据库排序开销
messages.sort((a, b) => {
const dateA = new Date(a.created_at || 0);
const dateB = new Date(b.created_at || 0);
return dateA - dateB; // 升序排列
});
// 限制返回数量
messages = messages.slice(-limit);
} else {
// 默认排序逻辑
messages = await collection.reverse().limit(limit).toArray();
messages = messages.reverse();
}
// 缓存查询结果
messageCache.set(cacheKey, {
data: messages,
timestamp: Date.now()
});
return messages;
} catch (error) {
console.error('获取消息失败:', error);
throw error;
}
}
// 简单的内存缓存实现
const messageCache = new Map();
/**
* 标记指定会话的所有消息为已读
* @param {number} talkType - 会话类型
@ -219,14 +329,24 @@ export async function deleteMessage(msgId) {
/**
* 添加或更新会话
* @param {object} conversation - 会话对象
* @returns {Promise<number>} 会话ID
* @returns {Promise<string>} 会话索引名称
*/
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 +394,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 +411,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 +436,18 @@ export function clearConversationUnreadNum(talkType, receiverId) {
/**
* 删除会话及其相关的消息
* @param {number} conversationId - 会话ID
* @param {string} indexName - 会话索引名称
* @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录
* @returns {Promise<void>}
*/
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 +472,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 +487,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,

View File

@ -345,3 +345,29 @@ export const formatNumberWithCommas = (num) => {
}
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
// 判断文件是否可以预览
export const checkFileCanPreview = (path) => {
if (!path) {
return false
}
//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']
// 获取文件扩展名
function getFileExtension(filepath) {
const parts = filepath?.split('.')
return parts?.length > 1 ? parts?.pop()?.toUpperCase() : ''
}
const extension = getFileExtension(path)
return PDF_EXTENSIONS.includes(extension) ||
EXCEL_EXTENSIONS.includes(extension) ||
WORD_EXTENSIONS.includes(extension) ||
PPT_EXTENSIONS.includes(extension)
}

View File

@ -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 `<span style="color:${color};">${$0}</span>`
return `<span style="color:${color};background:${bg};border-radius:2px;padding:0 5px">${$0}</span>`
})
}

View File

@ -518,7 +518,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)
}
})

View File

@ -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(() => {
<template>
<footer class="el-footer">
<MultiSelectFooter v-if="dialogueStore.isOpenMultiSelect" />
<!-- <Editor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" /> -->
<CustomEditor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" />
<!-- <CustomEditor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" /> -->
<TiptapEditor v-else @editor-event="onEditorEvent" :vote="talk_type == 2" :members="members" />
</footer>
<HistoryRecord

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { reactive, computed } from 'vue'
import { Peoples, Announcement, MenuUnfoldOne, MenuFoldOne } from '@icon-park/vue-next'
import { useDialogueStore } from '@/store'
@ -30,6 +31,10 @@ defineProps({
})
const dialogueStore = useDialogueStore()
const dialogueParams = reactive({
isDismiss: computed(() => dialogueStore.isDismiss),
isQuit: computed(() => dialogueStore.isQuit)
})
const emit = defineEmits(['evnet'])
const onSetMenu = () => {
@ -83,7 +88,7 @@ const onSetMenu = () => {
:size="18"
class="icon"
@click="emit('evnet', 'group')"
v-show="!dialogueStore.isDismiss && !dialogueStore.isQuit"
v-show="!dialogueParams.isDismiss && !dialogueParams.isQuit"
>
<img
style="width: 20px; height: 20px;"

View File

@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
vueJsx({}),
compressPlugin(),
UnoCSS(),
vueDevTools({
launchEditor: 'trae',
})
// vueDevTools({
// launchEditor: 'trae',
// })
],
define: {
__APP_ENV__: env.APP_ENV