更新组件声明,新增NDropdown支持;调整ForwardRecord.vue中的模态框逻辑,优化文件点击事件处理,修复ContactModal.vue中的数据提交逻辑,更新路由配置以支持新页面。

This commit is contained in:
Phoenix 2025-05-21 13:18:41 +08:00
parent b65f38f02e
commit 579fed2e69
11 changed files with 203 additions and 25 deletions

1
components.d.ts vendored
View File

@ -52,6 +52,7 @@ declare module 'vue' {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NDropdown: typeof import('naive-ui')['NDropdown']
NEmpty: typeof import('naive-ui')['NEmpty']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']

View File

@ -21,7 +21,7 @@ html {
// message
--im-message-bg-color: #f7f7f7;
--im-message-border-color: #efeff5;
--im-message-left-bg-color: #F4F4FC;
--im-message-left-bg-color: #fff;
--im-message-left-text-color: #333;
--im-message-right-bg-color: #46299D;
--im-message-right-text-color: #fff;

View File

@ -5,22 +5,21 @@ import { ServeGetForwardRecords } from '@/api/chat'
import { MessageComponents } from '@/constant/message'
import { ITalkRecord } from '@/types/chat'
import { useInject } from '@/hooks'
const emit = defineEmits(['close'])
import customModal from '@/components/common/customModal.vue'
import { voiceToText } from '@/api/chat.js'
const props = defineProps({
msgId: {
type: String,
required: true
}
})
const isShow=defineModel<boolean>('show')
const { showUserInfoModal } = useInject()
const isShow = ref(true)
const items = ref<ITalkRecord[]>([])
const title = ref('会话记录')
const onMaskClick = () => {
emit('close')
isShow.value=false
}
const onLoadData = () => {
@ -30,18 +29,92 @@ const onLoadData = () => {
if (res.code == 200) {
items.value = res.data.items || []
title.value = `会话记录(${items.value.length})`
// title.value = `(${items.value.length})`
}
})
}
const dropdown=ref({
show:false,
x:'',
y:'',
options:[] as any,
item:{} as ITalkRecord,
})
const onConvertText =async (data: ITalkRecord) => {
data.is_convert_text = 1
const res = await voiceToText({msgId:data.msg_id,voiceUrl:data.extra.url})
if(res.code == 200){
data.extra.content = res.data.convText
}
}
const onloseConvertText=(data: ITalkRecord)=>{
data.is_convert_text = 0
}
const evnets = {
convertText: onConvertText,
closeConvertText:onloseConvertText
}
const onContextMenuHandle=(key:string)=>{
evnets[key] && evnets[key](dropdown.value.item)
closeDropdownMenu()
}
const closeDropdownMenu=()=>{
dropdown.value.show=false
}
onMounted(() => {
onLoadData()
})
const onContextMenu = (e:any,item: ITalkRecord) => {
dropdown.value.show=true
dropdown.value.x=e.clientX
dropdown.value.y=e.clientY
if(item.is_convert_text === 1){
dropdown.value.options=[{ label: '关闭转文字', key: 'closeConvertText' }]
}else{
dropdown.value.options=[{ label: '转文字', key: 'convertText' }]
}
dropdown.value.item=item
}
</script>
<template>
<n-modal
<customModal :closable="false" customCloseBtn v-model:show="isShow" :title="title" style="width: 997px;background-color: #F9F9FD;" :on-after-leave="onMaskClick">
<template #content>
<div class="main-box bg-#fff me-scrollbar me-scrollbar-thumb">
<Loading v-if="items.length === 0" />
<div v-for="item in items" :key="item.msg_id" class="message-item">
<div class="left-box pointer" @click="showUserInfoModal(item.user_id)">
<im-avatar :src="item.avatar" :size="38" :username="item.nickname" />
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">{{ item.nickname }}</span>
<span class="time"> {{ item.created_at }}</span>
</div>
<component
@contextmenu.prevent="onContextMenu($event,item)"
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
</div>
</div>
<!-- 右键菜单 -->
<n-dropdown :show="dropdown.show" :x="dropdown.x" :y="dropdown.y" style="width: 142px;" :options="dropdown.options"
@select="onContextMenuHandle" @clickoutside="closeDropdownMenu" />
</template>
</customModal>
<!-- <n-modal
v-model:show="isShow"
preset="card"
:title="title"
@ -80,7 +153,7 @@ onMounted(() => {
</div>
</div>
</div>
</n-modal>
</n-modal> -->
</template>
<style lang="less" scoped>
@ -94,10 +167,10 @@ onMounted(() => {
min-height: 38px;
display: flex;
margin-bottom: 10px;
padding: 5px 15px;
padding: 24px 42px;
.left-box {
width: 30px;
width: 38px;
display: flex;
user-select: none;
padding-top: 8px;

View File

@ -86,12 +86,10 @@ const strokeDashoffset = computed(() =>
//
const handleClick = () => {
console.log('props.extra', props.extra);
window.open(
`http://localhost:5500/?url=${props.extra.path}`,
`${window.location.origin}/office?url=${props.extra.path}`,
'_blank',
'width=800,height=600,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'
);
}
</script>

View File

@ -33,7 +33,7 @@ const onClick = () => {
<span>转发聊天会话记录 ({{ extra.msg_ids.length }})</span>
</div>
<ForwardRecord v-if="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
</section>
</template>

View File

@ -13,6 +13,7 @@ const { showUserInfoModal } = useInject()
<template>
<div class="im-message-sys-text">
<div class="sys-text">
<a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }}
</a>

View File

@ -127,11 +127,12 @@ const onCancel = () => {
const onSubmit = () => {
let data = checkedFilter.value.map((item: any) => {
return {
id: item.id,
type: item.type
receiver_id: item.receiver_id,
talk_type: item.talk_type
}
})
console.log('data', data);
console.log('checkedFilter.value', checkedFilter.value);
emit('on-submit', data)
}

View File

@ -40,6 +40,11 @@ const routes = [
path: '/:pathMatch(.*)*',
name: '404 NotFound',
component: () => import('@/views/other/not-found.vue')
},
{
path: '/office',
name: 'office',
component: () => import('@/views/office/index.vue')
}
]

View File

@ -18,7 +18,7 @@ export function isLoggedIn() {
*/
export function getAccessToken() {
// return storage.get(AccessToken) || ''
return JSON.parse(localStorage.getItem('token'))||'6e0b0a00c35d0e1613d8f9ce2303067e46a0c2d9941c959848ccf7c91e7eb0fe14690f485ae6b5e932196267c2af8a6089bda35a715c44ad565de15114eb2c82fddcba958dd35b80cd0980f7d6fa4dde04e3c5f1407e39ac1073432c4e9dc0031afbe520e8a6528eeba7e79cf2c97f4c6e07e4a466460ee50fc28b2f05ca5215db9ea522a4d76911dbdd5d863c107f1cb073da0c94091ce81c59c9bd2faba1de9552a38a8c9ac69bb4a8d562b41b127a1e92468d4b8a5749adf7c899a65c748940eda0d6c8834cdd8995b527ab6f56cf9a6ede9de78ce7938190030708b576238d62f018abb0d363ee4cb4c1a8235487c20938760bece6caaf5f3573f888d15a0f9e79c4b09bd5214c704c135be67de9b475e24addc92d662768128eef05ddc67d68d8c0f16b5293888508c2f7f87bd0766fa7609726c18f814f04551f6f54d7'
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22201760cb2a37a6703ccf06c9d5a5752e0c3798b552c606065aa0079caa2737c431030be4871e931d3dcb27c239d70b4ba5f2e533a36a0b9b784388409ccd3ad8c62c1de535f4fd865b4f3bf37d260ccc'
}
/**

View File

@ -39,20 +39,20 @@ const onMultiDelete = () => {
dialogueStore.ApiDeleteRecord(msgIds)
}
const onContactModal = (data: { id: number; type: number }[]) => {
const onContactModal = (data: { receiver_id: number; talk_type: number }[]) => {
let msg_ids = dialogueStore.selectItems.map((item: any) => item.msg_id)
let user_ids: number[] = []
let group_ids: number[] = []
for (let o of data) {
if (o.type == 1) {
user_ids.push(o.id)
if (o.talk_type == 1) {
user_ids.push(o.receiver_id)
} else {
group_ids.push(o.id)
group_ids.push(o.receiver_id)
}
}
console.log('user_ids',user_ids)
dialogueStore.ApiForwardRecord({
mode: forwardMode.value,
message_ids: msg_ids,

View File

@ -0,0 +1,99 @@
<template>
<div id="office-div" ></div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// OnlyOffice API
function loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve()
return
}
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = src
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}
//
function getDocumentTypes(url) {
const extension = url.split('.').pop().toLowerCase()
const types = {
'docx': { fileType: 'docx', documentType: 'word' },
'doc': { fileType: 'doc', documentType: 'word' },
'xlsx': { fileType: 'xlsx', documentType: 'cell' },
'xls': { fileType: 'xls', documentType: 'cell' },
'pptx': { fileType: 'pptx', documentType: 'slide' },
'ppt': { fileType: 'ppt', documentType: 'slide' },
'pdf': { fileType: 'pdf', documentType: 'word' }
}
return types[extension] || { fileType: 'docx', documentType: 'word' }
}
// CSP meta
function insertCSPMeta() {
if (!document.querySelector('meta[http-equiv="Content-Security-Policy"]')) {
const meta = document.createElement('meta')
meta.httpEquiv = 'Content-Security-Policy'
meta.content = 'upgrade-insecure-requests'
document.head.appendChild(meta)
}
}
onMounted(async () => {
insertCSPMeta()
await loadScript('https://onlyoffice.fontree.cn/web-apps/apps/api/documents/api.js')
const url = route.query.url
if (!url) {
alert('请提供文档 URL 参数')
return
}
const fileName = url.split('/').pop()
const { fileType, documentType } = getDocumentTypes(url)
// eslint-disable-next-line no-undef
new window.DocsAPI.DocEditor('office-div', {
document: {
fileType,
key: `doc_${fileName}_${Date.now()}`,
title: fileName,
url
},
documentType,
editorConfig: {
mode: 'view',
lang: 'zh-CN',
user: {
id: 'user_' + Date.now(),
name: '访客用户'
},
customization: {
chat: false,
commentAuthorOnly: false,
compactToolbar: true,
hideRightMenu: false,
compatibility: true,
showReviewChanges: false
}
}
})
})
</script>
<style>
iframe[name="frameEditor"] {
width: 100% !important;
height: 100vh !important;
min-height: 100vh !important;
border: none !important;
display: block;
}
</style>