新增OnlyOffice文档编辑器和Fluent图标库依赖,优化文件消息组件的下载功能,调整文本消息背景色,改进会话菜单逻辑,优化会话列表排序,修复部分样式问题。

This commit is contained in:
Phoenix 2025-05-21 19:57:07 +08:00
parent 91107e2f85
commit 0fe1119789
13 changed files with 152 additions and 91 deletions

View File

@ -18,6 +18,8 @@
"@highlightjs/vue-plugin": "^2.1.0", "@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/ion": "^1.2.3", "@iconify-json/ion": "^1.2.3",
"@kangc/v-md-editor": "^2.3.18", "@kangc/v-md-editor": "^2.3.18",
"@onlyoffice/document-editor-vue": "^1.5.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons5": "^0.13.0", "@vicons/ionicons5": "^0.13.0",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",

View File

@ -20,6 +20,12 @@ importers:
'@kangc/v-md-editor': '@kangc/v-md-editor':
specifier: ^2.3.18 specifier: ^2.3.18
version: 2.3.18(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.2.2)) version: 2.3.18(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.2.2))
'@onlyoffice/document-editor-vue':
specifier: ^1.5.0
version: 1.5.0(vue@3.5.13(typescript@5.2.2))
'@vicons/fluent':
specifier: ^0.13.0
version: 0.13.0
'@vicons/ionicons5': '@vicons/ionicons5':
specifier: ^0.13.0 specifier: ^0.13.0
version: 0.13.0 version: 0.13.0
@ -594,6 +600,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@onlyoffice/document-editor-vue@1.5.0':
resolution: {integrity: sha512-HZEebUhBloP4LomspI5BddgoQdhtPq91h57yA9K/Lk70MMc1vgOTQ4Wq+N5TZYXNxdDTv+TSsEVFLnBCl1Y71A==}
peerDependencies:
vue: ^3.0.0
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -988,6 +999,9 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.0.0 vue: ^3.0.0
'@vicons/fluent@0.13.0':
resolution: {integrity: sha512-bYGZsOE3qzvm3Cm43e7tybgGlr5ZUpYqtRZq0g0Tfupe8jIzLolpvQLNUt1zS8Mgt6goTbUk5YH7Fkv16jkykg==}
'@vicons/ionicons5@0.13.0': '@vicons/ionicons5@0.13.0':
resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==} resolution: {integrity: sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==}
@ -4012,6 +4026,11 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 fastq: 1.19.1
'@onlyoffice/document-editor-vue@1.5.0(vue@3.5.13(typescript@5.2.2))':
dependencies:
lodash: 4.17.21
vue: 3.5.13(typescript@5.2.2)
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@ -4390,6 +4409,8 @@ snapshots:
dependencies: dependencies:
vue: 3.5.13(typescript@5.2.2) vue: 3.5.13(typescript@5.2.2)
'@vicons/fluent@0.13.0': {}
'@vicons/ionicons5@0.13.0': {} '@vicons/ionicons5@0.13.0': {}
'@vitejs/plugin-vue-jsx@3.1.0(vite@6.3.5(@types/node@18.19.99)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.2))(vue@3.5.13(typescript@5.2.2))': '@vitejs/plugin-vue-jsx@3.1.0(vite@6.3.5(@types/node@18.19.99)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.2))(vue@3.5.13(typescript@5.2.2))':

View File

@ -206,7 +206,7 @@ textarea {
border-radius: 2px; border-radius: 2px;
cursor: default; cursor: default;
user-select: none; user-select: none;
background-color: #dee0e3;
transform: scale(0.84); transform: scale(0.84);
transform-origin: left; transform-origin: left;
flex-shrink: 0; flex-shrink: 0;

BIN
src/assets/image/dofd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

View File

@ -7,7 +7,8 @@ import excelText from '@/assets/image/excel-text.png'
import wordText from '@/assets/image/word-text.png' import wordText from '@/assets/image/word-text.png'
import pdfText from '@/assets/image/pdf-text.png' import pdfText from '@/assets/image/pdf-text.png'
import fileText from '@/assets/image/file-text.png' import fileText from '@/assets/image/file-text.png'
import { ArrowDownload16Filled } from '@vicons/fluent'
import { download } from '@/utils/functions.js'
// //
const props = defineProps({ const props = defineProps({
// //
@ -92,6 +93,20 @@ const handleClick = () => {
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
); );
} }
//
const handleDownload = () => {
download(props.data.msg_id)
// const url = props.extra.path;
// if (!url) return;
// const a = document.createElement('a');
// a.href = url;
// a.download = url.split('/').pop() || 'download';
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
console.log('data',props.data)
}
</script> </script>
<template> <template>
@ -145,7 +160,14 @@ const handleClick = () => {
</div> </div>
</div> </div>
<!-- 文件大小信息 --> <!-- 文件大小信息 -->
<div class="file-size">{{ fileFormatSize(extra.size) }}</div> <div class="flex justify-between items-center">
<div class="file-size">{{ fileFormatSize(extra.size) }}</div>
<div class="flex items-center">
<div class="flex items-center" @click.stop="handleDownload"> <img class="w-11.7px h-11.74px mr-7px" src="@/assets/image/dofd.png" alt=""> <span class="text-12px text-#46299D">下载</span></div>
</div>
</div>
</div> </div>
</template> </template>

View File

@ -43,7 +43,7 @@ textContent = textReplaceEmoji(textContent)
min-height: 30px; min-height: 30px;
padding: 3px; padding: 3px;
color: var(--im-message-left-text-color); color: var(--im-message-left-text-color);
background: var(--im-message-left-bg-color); background: #F4F4FC;
border-radius: 0px 10px 10px 10px; border-radius: 0px 10px 10px 10px;
font-size: 14px; font-size: 14px;
&.right { &.right {

View File

@ -1,17 +1,5 @@
import { reactive, nextTick, computed, h, inject } from 'vue' import { reactive, nextTick, computed, h, inject } from 'vue'
import { ISession } from '@/types/chat' import { ISession } from '@/types/chat'
import { renderIcon } from '@/utils/util'
import {
ArrowUp,
ArrowDown,
Logout,
Delete,
Clear,
Remind,
CloseRemind,
EditTwo,
IdCard
} from '@icon-park/vue-next'
import { ServeTopTalkList, ServeDeleteTalkList, ServeSetNotDisturb } from '@/api/chat' import { ServeTopTalkList, ServeDeleteTalkList, ServeSetNotDisturb } from '@/api/chat'
import { useDialogueStore, useTalkStore } from '@/store' import { useDialogueStore, useTalkStore } from '@/store'
import { ServeSecedeGroup } from '@/api/group' import { ServeSecedeGroup } from '@/api/group'
@ -52,45 +40,45 @@ export function useSessionMenu() {
if (item.talk_type == 1) { if (item.talk_type == 1) {
options.push({ options.push({
icon: renderIcon(IdCard),
label: '好友信息', label: '好友信息',
key: 'info' key: 'info'
}) })
options.push({ options.push({
icon: renderIcon(EditTwo),
label: '修改备注', label: '修改备注',
key: 'remark' key: 'remark'
}) })
} }
options.push({ options.push({
icon: renderIcon(item.is_top ? ArrowDown : ArrowUp),
label: item.is_top ? '取消置顶' : '会话置顶', label: item.is_top ? '取消置顶' : '会话置顶',
key: 'top' key: 'top'
}) })
options.push({ options.push({
icon: renderIcon(item.is_disturb ? Remind : CloseRemind),
label: item.is_disturb ? '关闭免打扰' : '开启免打扰', label: item.is_disturb ? '关闭免打扰' : '开启免打扰',
key: 'disturb' key: 'disturb'
}) })
options.push({ options.push({
icon: renderIcon(Clear),
label: '移除会话', label: '移除会话',
key: 'remove' key: 'remove'
}) })
if (item.talk_type == 1) { if (item.talk_type == 1) {
options.push({ options.push({
icon: renderIcon(Delete),
label: '删除好友', label: '删除好友',
key: 'delete_contact' key: 'delete_contact'
}) })
} else { } else {
options.push({ options.push({
icon: renderIcon(Logout),
label: '退出群聊', label: '退出群聊',
key: 'signout_group' key: 'signout_group'
}) })

View File

@ -93,6 +93,7 @@ export const useTalkStore = defineStore('talk', {
resp.then(({ code, data }) => { resp.then(({ code, data }) => {
if (code == 200) { if (code == 200) {
this.items = data.items.map((item: any) => { this.items = data.items.map((item: any) => {
const value = formatTalkItem(item) const value = formatTalkItem(item)
@ -104,7 +105,6 @@ export const useTalkStore = defineStore('talk', {
if (value.is_robot == 1) { if (value.is_robot == 1) {
value.is_online = 1 value.is_online = 1
} }
return value return value
}) })

View File

@ -229,15 +229,22 @@ const state = reactive({
}) })
const items = computed((): ISession[] => { const items = computed((): ISession[] => {
if (searchKeyword.value.length === 0) { let filtered = talkStore.talkItems
return talkStore.talkItems
if (searchKeyword.value.length > 0) {
filtered = filtered.filter((item: ISession) => {
let keyword = item.remark || item.name
return keyword.toLowerCase().indexOf(searchKeyword.value.toLowerCase()) != -1
})
} }
return talkStore.talkItems.filter((item: ISession) => { //
let keyword = item.remark || item.name const topItems = filtered
.filter(item => item.is_top === 1)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
const normalItems = filtered.filter(item => item.is_top !== 1)
return keyword.toLowerCase().indexOf(searchKeyword.value.toLowerCase()) != -1 return [...topItems, ...normalItems]
})
}) })
watch( watch(
() => talkStore, () => talkStore,

View File

@ -19,7 +19,7 @@ const labelColor=[
</script> </script>
<template> <template>
<div class="talk pointer" :class="{ actived: active }" @click="emit('tab-talk', data)"> <div :class="`talk pointer ${data.is_top === 1 ? 'bg-#F3F3F3' : ''} ${active ? 'actived' : ''}`" @click="emit('tab-talk', data)">
<div class="avatar-box relative"> <div class="avatar-box relative">
<avatarModule showGroupType :mode="data?.group_type === 0 ? 1 : 2" <avatarModule showGroupType :mode="data?.group_type === 0 ? 1 : 2"
@ -57,11 +57,13 @@ const labelColor=[
</div> </div>
<div class="tip"> <div class="tip">
<div v-if="data.is_disturb" class="disturb"> <div v-if="data.is_disturb" class="disturb flex justify-center items-center">
<!-- <n-icon :component="CloseRemind" /> --> <!-- <n-icon :component="CloseRemind" /> -->
<span class="badge"> <span class="badge w-50px">
{{ data.unread_num > 99 ? '99+' : data.unread_num }} <!-- {{ data.unread_num > 99 ? '99+' : data.unread_num }} -->
<img src="@/assets/image/xxxx@2x.png" class="w-11.1px h-13px mr-6px" alt="">
<span v-if="data.unread_num>0" class="w-10px h-10px bg-#D03050 rounded-50%"></span>
</span> </span>
</div> </div>
@ -193,7 +195,6 @@ const labelColor=[
display: flex; display: flex;
padding-left: 5px; padding-left: 5px;
align-items: center; align-items: center;
.unread { .unread {
color: #8f959e; color: #8f959e;
font-size: 12px; font-size: 12px;
@ -216,7 +217,7 @@ const labelColor=[
} }
} }
--actived-bg: #ececec; --actived-bg: #EEE9F8;
&:hover, &:hover,
&.actived { &.actived {

View File

@ -313,7 +313,7 @@ onMounted(() => {
'multi-select-check': item.isCheck 'multi-select-check': item.isCheck
}"> }">
<!-- 多选按钮 --> <!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column"> <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" /> <n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
</aside> </aside>
<!-- 头像信息 --> <!-- 头像信息 -->

View File

@ -1,23 +1,28 @@
<template> <template>
<div id="office-div" ></div> <DocumentEditor
id="docEditor"
:documentServerUrl="documentServerUrl"
:config="config"
:events_onDocumentReady="onDocumentReady"
:onLoadComponentError="onLoadComponentError"
/>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { DocumentEditor } from "@onlyoffice/document-editor-vue"
const placeholderRef = ref(null) const documentServerUrl = 'https://onlyoffice.fontree.cn'
// OnlyOffice API onMounted(() => {
function loadScript(src) { // Content-Security-Policy meta
return new Promise((resolve, reject) => { if (!document.querySelector('meta[http-equiv="Content-Security-Policy"]')) {
const script = document.createElement('script') const meta = document.createElement('meta')
script.type = 'text/javascript' meta.httpEquiv = 'Content-Security-Policy'
script.src = src meta.content = 'upgrade-insecure-requests'
script.onload = resolve document.head.appendChild(meta)
script.onerror = reject }
document.head.appendChild(script) })
})
}
// URL // URL
function getUrlParameter(name) { function getUrlParameter(name) {
@ -42,52 +47,67 @@ function getDocumentTypes(url) {
return types[extension] || { fileType: 'docx', documentType: 'word' } return types[extension] || { fileType: 'docx', documentType: 'word' }
} }
onMounted(async () => { const url = getUrlParameter('url')
// Content-Security-Policy meta if (!url) {
if (!document.querySelector('meta[http-equiv="Content-Security-Policy"]')) { alert('请提供文档 URL 参数')
const meta = document.createElement('meta') }
meta.httpEquiv = 'Content-Security-Policy' const fileName = url ? url.split('/').pop() : ''
meta.content = 'upgrade-insecure-requests' const { fileType, documentType } = getDocumentTypes(url || '')
document.head.appendChild(meta)
} const config = {
await loadScript('https://onlyoffice.fontree.cn/web-apps/apps/api/documents/api.js') document: {
const url = getUrlParameter('url') fileType,
if (!url) { key: 'doc_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
alert('请提供文档 URL 参数') title: fileName,
return url
} },
const fileName = url.split('/').pop() documentType,
const { fileType, documentType } = getDocumentTypes(url) editorConfig: {
// eslint-disable-next-line no-undef mode: 'view',
new window.DocsAPI.DocEditor('office-div', { lang: 'zh-CN',
document: { user: {
fileType, id: 'user_' + new Date().getTime(),
key: 'doc_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), name: '访客用户'
title: fileName,
url
}, },
documentType, customization: {
editorConfig: { chat: false,
mode: 'view', commentAuthorOnly: false,
lang: 'zh-CN', compactToolbar: true,
user: { hideRightMenu: false,
id: 'user_' + new Date().getTime(), compatibility: true,
name: '访客用户' showReviewChanges: false,
}, loaderLogo: '', // logo
customization: { logo: {
chat: false, image: '', //
commentAuthorOnly: false, imageDark: '', //
compactToolbar: true, url: '', //
hideRightMenu: false, visible: false // false logo
compatibility: true,
showReviewChanges: false
} }
} }
}) }
}) }
const onDocumentReady = () => {
console.log("文档加载完成")
}
const onLoadComponentError = (errorCode, errorDescription) => {
switch(errorCode) {
case -1: //
console.log(errorDescription)
break
case -2: // DocsAPI
console.log(errorDescription)
break
case -3: // DocsAPI
console.log(errorDescription)
break
}
}
</script> </script>
<style> <style>
iframe[name="frameEditor"] { iframe[name="frameEditor"] {
width: 100% !important; width: 100% !important;
height: 100vh !important; height: 100vh !important;
min-height: 100vh !important; min-height: 100vh !important;