Compare commits

..

No commits in common. "6a54757c6a8cb5649e8bf6c583352b325dafc448" and "9503fbe78a5a90038ffb5916a5c1506cdde264d1" have entirely different histories.

49 changed files with 11565 additions and 3510 deletions

9309
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,11 +24,8 @@
"@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",
"@vueuse/rxjs": "^13.4.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.13",
"dexie": "^4.0.11",
"highlight.js": "^11.5.0", "highlight.js": "^11.5.0",
"js-audio-recorder": "^1.0.7", "js-audio-recorder": "^1.0.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -38,7 +35,6 @@
"quill": "^1.3.7", "quill": "^1.3.7",
"quill-image-uploader": "^1.3.0", "quill-image-uploader": "^1.3.0",
"quill-mention": "^4.1.0", "quill-mention": "^4.1.0",
"rxjs": "^7.8.2",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"viewerjs": "^1.11.7", "viewerjs": "^1.11.7",
"vue": "^3.3.11", "vue": "^3.3.11",

File diff suppressed because it is too large Load Diff

View File

@ -99,23 +99,3 @@ export const ServeEmptyMessage = (data) => {
export const ServeMessageReadDetail = (data) => { export const ServeMessageReadDetail = (data) => {
return post('/api/v1/talk/my-records/read/condition', data) return post('/api/v1/talk/my-records/read/condition', data)
} }
// 主动添加好友(单向好友)
export const ServeAddFriend = (data) => {
return post('/api/v1/contact/friend/add', data)
}
// 检测是否需要加好友
export const ServeCheckFriend = (data) => {
return post('/api/v1/contact/friend/check', data)
}
// 检测是否需要加好友
export const GetContactFriendList = (data) => {
return post('/api/v1/contact/friend/list', data)
}
// 搜索好友
export const GetFriendList = (data) => {
return post('/api/v1/contact/friend/search', data)
}

View File

@ -2,14 +2,12 @@ import { post, get, upload } from '@/utils/request'
//ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览 //ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览
export const ServeSeachQueryAll = (data = {}) => { export const ServeSeachQueryAll = (data = {}) => {
return post('/api/v1/elasticsearch/query-all', data) return post('/api/v1/elasticsearch/query-all/v2', data)
// return post('/api/v1/elasticsearch/query-all/v2', data)
} }
// ES搜索用户数据 // ES搜索用户数据
export const ServeQueryUser = (data) => { export const ServeQueryUser = (data) => {
return post('/api/v1/elasticsearch/query-user', data) return post('/api/v1/elasticsearch/query-user/v2', data)
// return post('/api/v1/elasticsearch/query-user/v2', data)
} }
// ES搜索群组数据 // ES搜索群组数据

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ const onCanplay = () => {
state.duration = audioRef.value.duration state.duration = audioRef.value.duration
durationDesc.value = formatTime(parseInt(audioRef.value.duration)) durationDesc.value = formatTime(parseInt(audioRef.value.duration))
state.loading = false state.loading = false
if (props.data.is_convert_text === 1 && props.data.extra.content) { if (props.data.is_convert_text === 1 && props.data.extra.content) {
setTimeout(() => { setTimeout(() => {
state.showText = true state.showText = true
@ -72,61 +72,47 @@ const formatTime = (value: number = 0) => {
} }
</script> </script>
<template> <template>
<div class="pointer w-200px bg-#F3F4FD rounded-10px px-11px"> <div class="pointer w-200px bg-#f5f5f5 rounded-10px px-11px">
<div class="im-message-audio h-44px"> <div class="im-message-audio h-44px">
<aTrumpet :isPlay="false" color="black" :size="30"></aTrumpet> <audio
<audio ref="audioRef"
ref="audioRef" preload="auto"
preload="auto" type="audio/mp3,audio/wav"
type="audio/mp3,audio/wav" :src="extra.url"
:src="extra.url" @timeupdate="onTimeUpdate"
@timeupdate="onTimeUpdate" @ended="onPlayEnd"
@ended="onPlayEnd" @canplay="onCanplay"
@canplay="onCanplay" @error="onError"
@error="onError" />
/>
<div class="play"> <div class="play">
<div class="btn pointer" @click.stop="onPlay"> <div class="btn pointer" @click.stop="onPlay">
<!-- <n-icon :size="18" :component="state.isAudioPlay ? PauseOne : PlayOne" /> --> <n-icon :size="18" :component="state.isAudioPlay ? PauseOne : PlayOne" />
<img
v-if="!state.isAudioPlay"
src="@/assets/image/yuyin.png"
class="w-[16px] h-[16px]"
alt=""
/>
<img v-else src="@/assets/image/bofang.png" class="w-[16px] h-[16px]" alt="" />
</div>
</div> </div>
<!-- <div class="desc"> </div>
<div class="desc">
<span class="line" v-for="i in 23" :key="i"></span> <span class="line" v-for="i in 23" :key="i"></span>
<span <span
class="indicator" class="indicator"
:style="{ left: state.progress + '%' }" :style="{ left: state.progress + '%' }"
v-show="state.progress > 0" v-show="state.progress > 0"
></span> ></span>
</div> -->
<!-- <div class="time">{{ durationDesc }}</div> -->
<div>{{ durationDesc.split('"')[0] }}s</div>
</div> </div>
<div class="time">{{ durationDesc }}</div>
<transition name="expand">
<div
class="text-container py-12px border-t-1px border-t-solid border-t-#E2E2EB"
v-if="data.is_convert_text === 1"
>
<div
class="flex justify-center items-center"
v-if="data.is_convert_text === 1 && !data.extra.content"
>
<n-spin :stroke-width="3" size="small" />
</div>
<transition name="fade">
<div class="text-content" v-if="data.extra.content">{{ data.extra.content }}</div>
</transition>
</div>
</transition>
</div> </div>
<transition name="expand">
<div class="text-container py-12px border-t-2px border-t-solid border-t-#E0E0E4" v-if="data.is_convert_text===1">
<div class="flex justify-center items-center" v-if="data.is_convert_text===1&&!data.extra.content">
<n-spin :stroke-width="3" size="small" />
</div>
<transition name="fade">
<div class="text-content" v-if="data.extra.content">{{ data.extra.content }}</div>
</transition>
</div>
</transition>
</div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.im-message-audio { .im-message-audio {
@ -149,7 +135,7 @@ const formatTime = (value: number = 0) => {
.btn { .btn {
width: 26px; width: 26px;
height: 26px; height: 26px;
// background-color: var(--audio-btn-bg-color); background-color: var(--audio-btn-bg-color);
border-radius: 50%; border-radius: 50%;
color: rgb(24, 24, 24); color: rgb(24, 24, 24);
display: flex; display: flex;

View File

@ -62,15 +62,6 @@ const fileInfo = computed(() => {
return fileTypes[extension] || fileTypes.DEFAULT return fileTypes[extension] || fileTypes.DEFAULT
}) })
//
const canPreview = computed(() => {
const extension = getFileExtension(props.extra.path)
return extension === 'PDF' ||
EXCEL_EXTENSIONS.includes(extension) ||
WORD_EXTENSIONS.includes(extension) ||
PPT_EXTENSIONS.includes(extension)
})
// //
function getFileExtension(filepath) { function getFileExtension(filepath) {
const parts = filepath?.split('.') const parts = filepath?.split('.')
@ -95,19 +86,14 @@ const strokeDashoffset = computed(() =>
// //
const handleClick = () => { const handleClick = () => {
// if(!props.extra.is_uploading){
if(!props.extra.is_uploading) { window.open(
if(canPreview.value){ `${import.meta.env.VITE_PAGE_URL}/office?url=${props.extra.path}`,
window.open( '_blank',
`${import.meta.env.VITE_PAGE_URL}/office?url=${props.extra.path}`, 'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
'_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('暂不支持在线预览该类型文件')
}
} }
} }
function downloadFileWithProgress(resourceUrl, filename) { function downloadFileWithProgress(resourceUrl, filename) {
@ -128,7 +114,7 @@ const handleDownload = () => {
</script> </script>
<template> <template>
<div class="file-message flex flex-col can-preview" @click="handleClick"> <div class="file-message flex flex-col" @click="handleClick">
<!-- 文件头部信息 --> <!-- 文件头部信息 -->
<div class="file-header"> <div class="file-header">
<!-- 文件名 --> <!-- 文件名 -->
@ -198,14 +184,7 @@ const handleDownload = () => {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 14px; padding: 0 14px;
}
.can-preview {
cursor: pointer; cursor: pointer;
&:hover {
background-color: #f9f9f9;
}
} }
.file-header { .file-header {

View File

@ -38,7 +38,7 @@ const img = (src: string, width = 200) => {
<div class="image-container"> <div class="image-container">
<n-image class="h-149px" :src="extra.url" /> <n-image class="h-149px" :src="extra.url" />
<!-- 上传中的loading蒙版 --> <!-- 上传中的loading蒙版 -->
<div v-if="extra.is_uploading" class="loading-overlay"> <div v-if="props.extra.is_uploading" class="loading-overlay">
<n-spin size="large" /> <n-spin size="large" />
</div> </div>
</div> </div>
@ -53,7 +53,7 @@ const img = (src: string, width = 200) => {
height:149px; height:149px;
&.left { &.left {
background: #F4F4FC; background: var(--im-message-right-bg-color);
} }
.image-container { .image-container {

View File

@ -115,7 +115,13 @@ const onRevoke = () => {
</span> </span>
<div style="display: inline-block;" v-if="login_uid === user_id"> <div style="display: inline-block;" v-if="login_uid === user_id">
<n-button @click="onRevoke" v-if="data.msg_type === 1&&data.extra?.content&&data.is_self_action" text class="text-#46299D text-11px">重新编辑</n-button> <n-button
@click="onRevoke"
v-if="data.msg_type === 1 && data.extra?.content"
text
class="text-#46299D text-11px"
>重新编辑</n-button
>
</div> </div>
<!-- <span v-if="login_uid == user_idA"> 你撤回B了一条消息 | {{ formatTime(datetime) }} </span> <!-- <span v-if="login_uid == user_idA"> 你撤回B了一条消息 | {{ formatTime(datetime) }} </span>
<span v-else-if="login_uid == user_idB"> A撤回你了一条消息 | {{ formatTime(datetime) }} </span> <span v-else-if="login_uid == user_idB"> A撤回你了一条消息 | {{ formatTime(datetime) }} </span>
@ -136,7 +142,13 @@ const onRevoke = () => {
</span> </span>
<span v-if="talk_type === 2 && extra"> {{ extra }} | {{ formatTime(datetime) }} </span> <span v-if="talk_type === 2 && extra"> {{ extra }} | {{ formatTime(datetime) }} </span>
<div style="display: inline-block;" v-if="login_uid === user_id"> <div style="display: inline-block;" v-if="login_uid === user_id">
<n-button @click="onRevoke" v-if="data.msg_type === 1&&data.extra?.content&&data.is_self_action" text class="text-#46299D text-11px">重新编辑</n-button> <n-button
@click="onRevoke"
v-if="data.msg_type === 1 && data.extra?.content"
text
class="text-#46299D text-11px"
>重新编辑</n-button
>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import 'xgplayer/dist/index.min.css' import 'xgplayer/dist/index.min.css'
import { ref, nextTick, watch, computed } from 'vue' import { ref, nextTick, watch } from 'vue'
import { NImage, NModal, NCard, NProgress, NPopconfirm } from 'naive-ui' import { NImage, NModal, NCard, NProgress, NPopconfirm } from 'naive-ui'
import { Play, Close, Pause, Right, Attention } from '@icon-park/vue-next' import { Play, Close, Pause, Right, Attention } from '@icon-park/vue-next'
import { getImageInfo } from '@/utils/functions' import { getImageInfo } from '@/utils/functions'
@ -64,11 +64,6 @@ const updatePauseStatus = () => {
// //
updatePauseStatus() updatePauseStatus()
// URL
const videoSrc = computed(() => {
// 使URL
return props.extra.url || ''
})
// // // //
// watch(() => props.extra.percentage, (newVal: number | undefined) => { // watch(() => props.extra.percentage, (newVal: number | undefined) => {
// // UI // // UI
@ -141,7 +136,7 @@ function resumeUpload(e) {
> >
<!-- <n-image :src="extra.cover" preview-disabled /> --> <!-- <n-image :src="extra.cover" preview-disabled /> -->
<video :src="videoSrc" :controls="false"></video> <video :src="props.extra.url" :controls="false"></video>
<!-- 上传进度时的黑色半透明蒙层 --> <!-- 上传进度时的黑色半透明蒙层 -->
<div v-if="extra.is_uploading && !uploadFailed" class="upload-mask"></div> <div v-if="extra.is_uploading && !uploadFailed" class="upload-mask"></div>
<!-- 上传进度显示 --> <!-- 上传进度显示 -->
@ -257,7 +252,7 @@ function resumeUpload(e) {
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.3); /* 降低不透明度从0.45改为0.3,让视频封面能够显示 */ background: rgba(0, 0, 0, 0.45);
z-index: 1; z-index: 1;
border-radius: 5px; border-radius: 5px;
} }

View File

@ -15,7 +15,7 @@ const { showUserInfoModal } = useInject()
<div class="sys-text"> <div class="sys-text">
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a>{{ user.nickname }}</a> <a @click="showUserInfoModal(user.erp_user_id,user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>

View File

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

View File

@ -14,14 +14,14 @@ const { showUserInfoModal } = useInject()
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a > <a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }} {{ extra.owner_name }}
</a> </a>
<span>创建了群聊并邀请了</span> <span>创建了群聊并邀请了</span>
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a >{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>
</div> </div>

View File

@ -13,7 +13,7 @@ defineProps({
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a> <a @click="showUserInfoModal(data.user_id)">
<!-- {{ data.nickname }} --> <!-- {{ data.nickname }} -->
管理员 管理员
</a> </a>

View File

@ -13,14 +13,14 @@ const { showUserInfoModal } = useInject()
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a > <a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }} {{ extra.owner_name }}
</a> </a>
<span>邀请了</span> <span>邀请了</span>
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a>{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>

View File

@ -13,14 +13,14 @@ const { showUserInfoModal } = useInject()
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a > <a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }} {{ extra.owner_name }}
</a> </a>
<span>解除了</span> <span>解除了</span>
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a >{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>

View File

@ -13,14 +13,14 @@ const { showUserInfoModal } = useInject()
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a> <a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }} {{ extra.owner_name }}
</a> </a>
<span></span> <span></span>
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a>{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>

View File

@ -13,14 +13,14 @@ const { showUserInfoModal } = useInject()
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a> <a @click="showUserInfoModal(extra.owner_id)">
{{ extra.owner_name }} {{ extra.owner_name }}
</a> </a>
<span>设置了</span> <span>设置了</span>
<template v-for="(user, index) in extra.members" :key="index"> <template v-for="(user, index) in extra.members" :key="index">
<a>{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>

View File

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

View File

@ -14,7 +14,7 @@ defineProps({
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<template v-for="(user, index) in extra?.members" :key="index"> <template v-for="(user, index) in extra?.members" :key="index">
<a >{{ user.nickname }}</a> <a @click="showUserInfoModal(user.user_id)">{{ user.nickname }}</a>
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>
<span>已离开此群</span> <span>已离开此群</span>

View File

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

View File

@ -13,9 +13,9 @@ const { showUserInfoModal } = useInject()
<template> <template>
<div class="im-message-sys-text"> <div class="im-message-sys-text">
<div class="sys-text"> <div class="sys-text">
<a >{{ extra.old_owner_name }}</a> <a @click="showUserInfoModal(extra.old_owner_id)">{{ extra.old_owner_name }}</a>
<span>将群主转让给</span> <span>将群主转让给</span>
<a >{{ extra.new_owner_name }}</a> <a @click="showUserInfoModal(extra.new_owner_id)">{{ extra.new_owner_name }}</a>
</div> </div>
</div> </div>
</template> </template>

View File

@ -10,6 +10,7 @@
padding: 0 8px; padding: 0 8px;
word-wrap: break-word; word-wrap: break-word;
color: #979191; color: #979191;
user-select: none;
font-weight: 300; font-weight: 300;
display: inline-block; display: inline-block;
border-radius: 3px; border-radius: 3px;
@ -22,11 +23,13 @@
a { a {
color: #939596; color: #939596;
cursor: pointer;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
&:hover {
color: #462AA0;
}
} }
} }
} }

View File

@ -131,6 +131,8 @@ const onSubmit = () => {
talk_type: item.talk_type talk_type: item.talk_type
} }
}) })
console.log('data', data);
console.log('checkedFilter.value', checkedFilter.value);
emit('on-submit', data) emit('on-submit', data)
} }

View File

@ -8,17 +8,9 @@ import { useTalkStore } from '@/store'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import xNModal from '@/components/x-naive-ui/x-n-modal/index.vue' import xNModal from '@/components/x-naive-ui/x-n-modal/index.vue'
import { NSkeleton } from 'naive-ui' import { NSkeleton } from 'naive-ui'
import { ServeCheckFriend, ServeAddFriend } from '@/api/chat'
import { useUtil } from '@/hooks/useUtil'
const { useMessage } = useUtil()
// const isFriend = ref(false) //
// const showBtn = ref(false)
const router = useRouter() const router = useRouter()
const talkStore = useTalkStore() const talkStore = useTalkStore()
const emit = defineEmits(['update:show', 'update:uid', 'updateRemark', 'update:send']) const emit = defineEmits(['update:show', 'update:uid', 'updateRemark'])
const props = defineProps({ const props = defineProps({
show: { show: {
type: Boolean, type: Boolean,
@ -94,7 +86,6 @@ const onLoadData = () => {
const onToTalk = () => { const onToTalk = () => {
talkStore.toTalk(1, props.uid, router) talkStore.toTalk(1, props.uid, router)
emit('update:show', false) emit('update:show', false)
emit('update:send')
} }
// const onJoinContact = () => { // const onJoinContact = () => {
@ -176,85 +167,60 @@ const onToTalk = () => {
// emit('update:show', value) // emit('update:show', value)
// } // }
//
// const addFriend = () => {
// let params = {
// receiver_id: props.uid, //id
// talk_type: 1
// }
// ServeAddFriend(params).then((res) => {
// if (res?.code === 200) {
// useMessage.success('')
// isFriend.value = !isFriend.value
// }
// })
// }
const onAfterEnter = () => { const onAfterEnter = () => {
onLoadData() onLoadData()
// ServeCheckFriend({ receiver_id: props.uid, talk_type: 1 }).then((res) => {
// if (res?.code === 200) {
// showBtn.value = true
// isFriend.value = res.data?.is_friend || false
// }
// })
} }
const onAfterLeave = () => { const onAfterLeave = () => {
// loading.value = true // loading.value = true
userInfo.value = { userInfo.value = {
id: 0, id: 0,
avatar: '', avatar: '',
gender: 0, gender: 0,
mobile: '', mobile: '',
motto: '', motto: '',
nickname: '', nickname: '',
remark: '', remark: '',
email: '', email: '',
status: 1, status: 1,
text: '' text: ''
} }
} }
</script> </script>
<template> <template>
<x-n-modal <x-n-modal content-style="padding:0;" :closable="false" class="w-311px min-h-445px" style="border-radius: 10px;overflow:hidden;" :show="show" :on-after-leave="onAfterLeave" :on-after-enter="onAfterEnter">
content-style="padding:0;"
:closable="false"
class="w-311px min-h-445px"
style="border-radius: 10px; overflow: hidden"
:show="show"
:on-after-leave="onAfterLeave"
:on-after-enter="onAfterEnter"
>
<div class="section relative px-7px pt-82px pb-20px"> <div class="section relative px-7px pt-82px pb-20px">
<div class="absolute top-9px right-7px pointer z-10" @click="emit('update:show', false)"> <div class="absolute top-9px right-7px pointer z-10" @click="emit('update:show', false)">
<img class="w-20px h-20px" src="@/assets/image/close.png" alt="" /> <img class="w-20px h-20px" src="@/assets/image/close.png" alt="">
</div> </div>
<template v-if="loading"> <template v-if="loading">
<div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px"> <div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
<div class="w-59px h-59px rounded-8px mr-12px"> <div class="w-59px h-59px rounded-8px mr-12px">
<n-skeleton height="59px" width="59px" /> <n-skeleton height="59px" width="59px" />
</div> </div>
<div class="w-full"> <div class="w-full">
<n-skeleton text style="width: 80%; margin-bottom: 5px" /> <n-skeleton text style="width: 80%; margin-bottom: 5px;" />
<n-skeleton text style="width: 60%" /> <n-skeleton text style="width: 60%;" />
</div> </div>
</div> </div>
<div class="bg-#fff rounded-4px mb-20px"> <div class="bg-#fff rounded-4px mb-20px">
<div class="flex px-15px py-9px" v-for="i in 6" :key="i"> <div class="flex px-15px py-9px" v-for="i in 6" :key="i">
<n-skeleton text style="width: 30%; margin-right: 10px" /> <n-skeleton text style="width: 30%; margin-right: 10px;" />
<n-skeleton text style="width: 60%" /> <n-skeleton text style="width: 60%;" />
</div> </div>
</div> </div>
<div> <div>
<n-skeleton text style="width: 100%; height: 42px; border-radius: 4px" /> <n-skeleton text style="width: 100%; height: 42px; border-radius: 4px;" />
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px"> <div class="flex py-10px bg-#fff px-16px rounded-4px items-center mb-10px">
<div class="w-59px h-59px rounded-8px mr-12px overflow-hidden"> <div class="w-59px h-59px rounded-8px mr-12px overflow-hidden">
<n-image width="59" :src="userInfo.avatar"> </n-image> <n-image width="59" :src="userInfo.avatar" >
</n-image>
</div> </div>
<div> <div>
<div class="text-#000 text-16px mb-5px">{{ userInfo.nickname }}</div> <div class="text-#000 text-16px mb-5px">{{ userInfo.nickname }}</div>
@ -268,15 +234,11 @@ const onAfterLeave = () => {
</div> </div>
<div class="flex px-15px py-9px"> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">主管</div> <div class="text-#000 text-12px w-84px">主管</div>
<div class="text-#747474 text-12px"> <div class="text-#747474 text-12px">{{ userInfo.leaders?.map(x=>x.user_name)?.join(',') }}</div>
{{ userInfo.leaders?.map((x) => x.user_name)?.join(',') }}
</div>
</div> </div>
<div class="flex px-15px py-9px"> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">部门</div> <div class="text-#000 text-12px w-84px">部门</div>
<div class="text-#747474 text-12px"> <div class="text-#747474 text-12px">{{ userInfo.erp_dept_position?.map(x=>x.department_name)?.join(',') }}</div>
{{ userInfo.erp_dept_position?.map((x) => x.department_name)?.join(',') }}
</div>
</div> </div>
<div class="flex px-15px py-9px"> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">手机号</div> <div class="text-#000 text-12px w-84px">手机号</div>
@ -284,48 +246,21 @@ const onAfterLeave = () => {
</div> </div>
<div class="flex px-15px py-9px"> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">岗位</div> <div class="text-#000 text-12px w-84px">岗位</div>
<div class="text-#747474 text-12px"> <div class="text-#747474 text-12px">{{ userInfo.erp_dept_position?.map(x=>x.position_name)?.join(',') }}</div>
{{ userInfo.erp_dept_position?.map((x) => x.position_name)?.join(',') }}
</div>
</div> </div>
<div class="flex px-15px py-9px"> <div class="flex px-15px py-9px">
<div class="text-#000 text-12px w-84px">入职日期</div> <div class="text-#000 text-12px w-84px">入职日期</div>
<div class="text-#747474 text-12px">{{ userInfo.enter_date }}</div> <div class="text-#747474 text-12px">{{ userInfo.enter_date }}</div>
</div> </div>
</div> </div>
<div>
<n-button block color="#EEE9F8" text-color="#46299D" @click="onToTalk"> <n-button block color="#EEE9F8" text-color="#46299D" @click="onToTalk">
<div class="flex items-center justify-center py-11px"> <div class="flex items-center justify-center py-11px">
<img class="w-19.8px h-20px mr-15px" src="@/assets/image/faxi@2x.png" alt="" /> <img class="w-19.8px h-20px mr-15px" src="@/assets/image/faxi@2x.png" alt="">
<span>发送消息</span> <span>发送消息</span>
</div> </div>
</n-button>
<!-- <div v-if="showBtn">
<n-button block color="#EEE9F8" text-color="#46299D" @click="onToTalk" v-if="isFriend">
<div class="flex items-center justify-center py-11px">
<img class="w-19.8px h-20px mr-15px" src="@/assets/image/faxi@2x.png" alt="" />
<span>发送消息</span>
</div>
</n-button> </n-button>
<n-button </div>
block
type="success"
color="#46299D"
text-color="#ffffff"
@click="addFriend"
v-else
>
<div class="flex items-center justify-center py-11px">
<img
class="w-10px h-10px mr-15px"
src="@/assets/image/icon/close-btn-grey-line.png"
alt=""
style="transform: rotate(45deg)"
/>
<span>添加好友</span>
</div>
</n-button>
</div> -->
</template> </template>
</div> </div>
</x-n-modal> </x-n-modal>

View File

@ -96,7 +96,6 @@ export const MessageComponents = {
// 可转发的消息类型 // 可转发的消息类型
export const ForwardableMessageType = [ export const ForwardableMessageType = [
ChatMsgTypeForward,
ChatMsgTypeText, ChatMsgTypeText,
ChatMsgTypeCode, ChatMsgTypeCode,
ChatMsgTypeImage, ChatMsgTypeImage,

View File

@ -32,7 +32,7 @@ class Read extends Base {
handle() { handle() {
if (this.type == 'total') { if (this.type == 'total') {
console.error('====接收到了新版已读回执全量=====', this.resource)
const readList = this.resource.result const readList = this.resource.result
if (readList.length > 0) { if (readList.length > 0) {
readList.forEach((item) => { readList.forEach((item) => {

View File

@ -240,13 +240,14 @@ class Talk extends Base {
}) })
}, 1000) }, 1000)
} }
console.log('输出加载1')
// 获取聊天面板元素节点 // 获取聊天面板元素节点
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
if (!el) return if (!el) return
// 判断的滚动条是否在底部 // 判断的滚动条是否在底部
const isBottom = isScrollAtBottom(el) const isBottom = isScrollAtBottom(el)
if (isBottom || record.user_id == this.getAccountId()) { if (isBottom || record.user_id == this.getAccountId()) {
scrollToBottom() scrollToBottom()
} else { } else {

View File

@ -38,23 +38,23 @@ export function useSessionMenu() {
const options: any[] = [] const options: any[] = []
// if (item.talk_type == 1) { if (item.talk_type == 1) {
// options.push({ options.push({
// label: '好友信息', label: '好友信息',
// key: 'info' key: 'info'
// }) })
// options.push({ options.push({
// label: '修改备注', label: '修改备注',
// key: 'remark' key: 'remark'
// }) })
// } }
options.push({ options.push({
label: item.is_top ? '取消置顶' : '置顶', label: item.is_top ? '取消置顶' : '会话置顶',
key: 'top' key: 'top'
}) })
@ -66,7 +66,7 @@ export function useSessionMenu() {
options.push({ options.push({
label: '删除聊天', label: '移除会话',
key: 'remove' key: 'remove'
}) })

View File

@ -130,19 +130,19 @@ export const useTalkRecord = (uid: number) => {
cursor: loadConfig.cursor, cursor: loadConfig.cursor,
limit: 30 limit: 30
} }
// 如果不是从本地数据库加载的则设置加载状态为0加载中
if (loadConfig.status !== 2 && loadConfig.status !== 3) { loadConfig.status = 0
loadConfig.status = 0
}
let scrollHeight = 0 let scrollHeight = 0
console.log('加载数据列表load')
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
if (el) { if (el) {
scrollHeight = el.scrollHeight scrollHeight = el.scrollHeight
} }
const { data, code } = await ServeTalkRecords(request) const { data, code } = await ServeTalkRecords(request)
if (code != 200) { if (code != 200) {
return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态 return (loadConfig.status = 1)
} }
// 防止对话切换过快,数据渲染错误 // 防止对话切换过快,数据渲染错误
if ( if (
@ -154,77 +154,20 @@ export const useTalkRecord = (uid: number) => {
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item)) 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)
}
// 如果是从本地数据库加载的数据且服务器返回的数据与本地数据相同则不需要更新UI
if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) {
try {
// 获取最新的本地数据库消息进行比较
const { getMessages } = await import('@/utils/db')
const localMessages = await getMessages(
params.talk_type,
uid,
params.receiver_id,
items.length || 30, // 获取与服务器返回数量相同的消息
0 // 从第一页开始
)
// 格式化本地消息,确保与服务器消息结构一致
const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
// 改进比较逻辑检查消息数量和所有消息的ID是否匹配
if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 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
}
}
// 数据不一致需要更新UI
console.log('本地数据与服务器数据不一致更新UI')
} catch (error) {
console.error('比较本地数据和服务器数据时出错:', error)
// 出错时默认更新UI
}
}
if (request.cursor == 0) { if (request.cursor == 0) {
// 判断是否是初次加载 // 判断是否是初次加载
dialogueStore.clearDialogueRecord() dialogueStore.clearDialogueRecord()
} }
dialogueStore.unshiftDialogueRecord(items.reverse()) dialogueStore.unshiftDialogueRecord(items.reverse())
loadConfig.status = items.length >= request.limit ? 1 : 2 loadConfig.status = items.length >= request.limit ? 1 : 2
loadConfig.cursor = data.cursor loadConfig.cursor = data.cursor
nextTick(() => { nextTick(() => {
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
if (el) { if (el) {
if (request.cursor == 0) { if (request.cursor == 0) {
// el.scrollTop = el.scrollHeight // el.scrollTop = el.scrollHeight
@ -232,12 +175,6 @@ export const useTalkRecord = (uid: number) => {
// setTimeout(() => { // setTimeout(() => {
// el.scrollTop = el.scrollHeight + 1000 // el.scrollTop = el.scrollHeight + 1000
// }, 500) // }, 500)
console.log('滚动到底部')
// 在初次加载完成后恢复上传任务
// 确保在所有聊天记录加载完成后再恢复上传任务
dialogueStore.restoreUploadTasks()
scrollToBottom() scrollToBottom()
} else { } else {
el.scrollTop = el.scrollHeight - scrollHeight el.scrollTop = el.scrollHeight - scrollHeight
@ -252,7 +189,9 @@ export const useTalkRecord = (uid: number) => {
// 获取当前消息的最小 sequence // 获取当前消息的最小 sequence
const getMinSequence = () => { const getMinSequence = () => {
console.error('records.value', records.value)
if (!records.value.length) return 0 if (!records.value.length) return 0
console.error(Math.min(...records.value.map((item) => item.sequence)))
return Math.min(...records.value.map((item) => item.sequence)) return Math.min(...records.value.map((item) => item.sequence))
} }
// 获取当前消息的最大 sequence // 获取当前消息的最大 sequence
@ -261,56 +200,13 @@ export const useTalkRecord = (uid: number) => {
return Math.max(...records.value.map((item) => item.sequence)) return Math.max(...records.value.map((item) => item.sequence))
} }
// 从本地数据库加载聊天记录
const loadFromLocalDB = async (params: Params) => {
try {
// 导入 getMessages 函数
const { getMessages } = await import('@/utils/db')
// 从本地数据库获取聊天记录
const localMessages = await getMessages(
params.talk_type,
uid,
params.receiver_id,
params.limit || 30,
0 // 从第一页开始
// 不传入 maxSequence 参数,获取最新的消息
)
// 如果有本地数据
if (localMessages && localMessages.length > 0) {
// 清空现有记录
dialogueStore.clearDialogueRecord()
// 格式化并添加记录
const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item))
dialogueStore.unshiftDialogueRecord(formattedMessages)
// 设置加载状态为完成3表示从本地数据库加载完成
loadConfig.status = 3
// 恢复上传任务
dialogueStore.restoreUploadTasks()
// 滚动到底部
nextTick(() => {
scrollToBottom()
})
return true
}
return false
} catch (error) {
console.error('从本地数据库加载聊天记录失败:', error)
return false
}
}
/** /**
* *
* @param params * @param params
* @param options { specifiedMsg } * @param options { specifiedMsg }
*/ */
const onLoad = async (params: Params, options?: LoadOptions) => { const onLoad = (params: Params, options?: LoadOptions) => {
// 如果会话切换,重置所有状态
if ( if (
params.talk_type !== loadConfig.talk_type || params.talk_type !== loadConfig.talk_type ||
params.receiver_id !== loadConfig.receiver_id params.receiver_id !== loadConfig.receiver_id
@ -325,6 +221,7 @@ export const useTalkRecord = (uid: number) => {
// 新增:支持指定消息定位模式,参数以传入为准合并 // 新增:支持指定消息定位模式,参数以传入为准合并
if (options?.specifiedMsg?.cursor !== undefined) { if (options?.specifiedMsg?.cursor !== undefined) {
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用 loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
console.error('options', options)
loadConfig.status = 0 // 复用主流程 loading 状态 loadConfig.status = 0 // 复用主流程 loading 状态
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖) // 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
const contextParams = { const contextParams = {
@ -334,7 +231,6 @@ export const useTalkRecord = (uid: number) => {
//msg_id是用来做定位的不做参数所以这里清空 //msg_id是用来做定位的不做参数所以这里清空
contextParams.msg_id = '' contextParams.msg_id = ''
ServeTalkRecords(contextParams).then(({ data, code }) => { ServeTalkRecords(contextParams).then(({ data, code }) => {
console.log('data',data)
if (code !== 200) { if (code !== 200) {
loadConfig.status = 2 loadConfig.status = 2
return return
@ -426,8 +322,6 @@ export const useTalkRecord = (uid: number) => {
}) })
} else { } else {
// 其他情况滚动到底部 // 其他情况滚动到底部
// 在特殊参数模式下也需要恢复上传任务
dialogueStore.restoreUploadTasks()
scrollToBottom() scrollToBottom()
} }
} }
@ -437,22 +331,14 @@ export const useTalkRecord = (uid: number) => {
} }
loadConfig.specialParams = undefined // 普通模式清空 loadConfig.specialParams = undefined // 普通模式清空
// 设置初始加载状态为0加载中
loadConfig.status = 0
// 先从本地数据库加载数据
const hasLocalData = await loadFromLocalDB(params)
// 无论是否有本地数据,都从服务器获取最新数据
// 原有逻辑 // 原有逻辑
console.log('onLoad()执行load')
load(params) load(params)
} }
// 向上加载更多(兼容特殊参数模式) // 向上加载更多(兼容特殊参数模式)
const onRefreshLoad = () => { const onRefreshLoad = () => {
if (loadConfig.status == 1 || loadConfig.status == 3) { console.error('loadConfig.status', loadConfig.status)
if (loadConfig.status == 1) {
console.log('specialParams', loadConfig.specialParams) console.log('specialParams', loadConfig.specialParams)
// 判断是否是特殊参数模式 // 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') { if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
@ -483,7 +369,6 @@ export const useTalkRecord = (uid: number) => {
} else { } else {
// 如果不匹配,重置为普通模式 // 如果不匹配,重置为普通模式
resetLoadConfig() resetLoadConfig()
console.log('load执行2')
load({ load({
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type, talk_type: loadConfig.talk_type,
@ -492,7 +377,6 @@ export const useTalkRecord = (uid: number) => {
} }
} else { } else {
// 原有逻辑 // 原有逻辑
console.log('load执行3')
load({ load({
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type, talk_type: loadConfig.talk_type,

View File

@ -31,14 +31,14 @@ function handle() {
once = true once = true
// window['$dialog'].info({ window['$dialog'].info({
// title: '友情提示', title: '友情提示',
// content: '当前登录已失效,请重新登录?', content: '当前登录已失效,请重新登录?',
// positiveText: '立即登录?', positiveText: '立即登录?',
// maskClosable: false, maskClosable: false,
// onPositiveClick: () => { onPositiveClick: () => {
// once = false once = false
// useRouter().push('/auth/login') useRouter().push('/auth/login')
// } }
// }) })
} }

View File

@ -8,13 +8,11 @@ import router from './router'
import App from './App.vue' import App from './App.vue'
import * as plugins from './plugins' import * as plugins from './plugins'
import request from "@/api/index.js"; import request from "@/api/index.js";
if (window.__POWERED_BY_WUJIE__) { if (window.__POWERED_BY_WUJIE__) {
// eslint-disable-next-line // eslint-disable-next-line
window.__webpack_public_path__ = window.__WUJIE_PUBLIC_PATH__; window.__webpack_public_path__ = window.__WUJIE_PUBLIC_PATH__;
} }
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)

View File

@ -15,9 +15,7 @@ export const useDialogueStore = defineStore('dialogue', {
return { return {
// 对话索引(聊天对话的唯一索引) // 对话索引(聊天对话的唯一索引)
index_name: '', index_name: '',
globalUploadList:[],
// 添加一个映射,用于快速查找每个会话的上传任务
uploadTaskMap: {}, // 格式: { "talk_type_receiver_id": [task1, task2, ...] }
// 对话节点 // 对话节点
talk: { talk: {
avatar:'', avatar:'',
@ -131,10 +129,8 @@ export const useDialogueStore = defineStore('dialogue', {
if (data.talk_type == 2) { if (data.talk_type == 2) {
this.updateGroupMembers() this.updateGroupMembers()
this.getGroupInfo() this.getGroupInfo()
} }
// 注意:上传任务的恢复将在聊天记录加载完成后进行
// 在useTalkRecord.ts的onLoad方法中会在加载完聊天记录后调用restoreUploadTasks方法
}, },
// 更新提及列表 // 更新提及列表
@ -175,12 +171,10 @@ export const useDialogueStore = defineStore('dialogue', {
// 数组头部压入对话记录 // 数组头部压入对话记录
unshiftDialogueRecord(records) { unshiftDialogueRecord(records) {
console.log('unshiftDialogueRecord')
this.records.unshift(...records) this.records.unshift(...records)
}, },
//数组尾部加入更多对话记录 //数组尾部加入更多对话记录
addDialogueRecordForLoadMore(records){ addDialogueRecordForLoadMore(records){
console.log('addDialogueRecordForLoadMore')
this.records.push(...records) this.records.push(...records)
}, },
async getGroupInfo(){ async getGroupInfo(){
@ -192,55 +186,24 @@ export const useDialogueStore = defineStore('dialogue', {
} }
}, },
// 推送对话记录 // 推送对话记录
async addDialogueRecord(record) { addDialogueRecord(record) {
// TOOD 需要通过 sequence 排序,保证消息一致性 // TOOD 需要通过 sequence 排序,保证消息一致性
// this.records.splice(index, 0, record) // this.records.splice(index, 0, record)
this.records.push(record) this.records.push(record)
// 同步到本地数据库
try {
const { addMessage } = await import('@/utils/db')
await addMessage(record)
} catch (error) {
console.error('同步消息到本地数据库失败:', error)
}
}, },
// 更新对话记录 // 更新对话记录
async updateDialogueRecord(params) { updateDialogueRecord(params) {
const { msg_id = '' } = params const { msg_id = '' } = params
const item = this.records.find((item) => item.msg_id === msg_id) const item = this.records.find((item) => item.msg_id === msg_id)
if (item) { item && Object.assign(item, params)
Object.assign(item, params)
// 同步到本地数据库
try {
// 如果是撤回消息
if (params.is_revoke === 1) {
const { revokeMessage } = await import('@/utils/db')
await revokeMessage(msg_id)
}
} catch (error) {
console.error('同步消息更新到本地数据库失败:', error)
}
}
}, },
// 批量删除对话记录 // 批量删除对话记录
async batchDelDialogueRecord(msgIds = []) { batchDelDialogueRecord(msgIds = []) {
// 同步到本地数据库
try {
const { deleteMessage } = await import('@/utils/db')
for (const msgid of msgIds) {
await deleteMessage(msgid)
}
} catch (error) {
console.error('同步消息删除到本地数据库失败:', error)
}
// 从内存中删除
msgIds.forEach((msgid) => { msgIds.forEach((msgid) => {
const index = this.records.findIndex((item) => item.msg_id === msgid) const index = this.records.findIndex((item) => item.msg_id === msgid)
@ -285,6 +248,8 @@ export const useDialogueStore = defineStore('dialogue', {
}).then((res) => { }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
this.batchDelDialogueRecord(msgIds) this.batchDelDialogueRecord(msgIds)
} else {
window['$message'].warning(res.message)
} }
}) })
}, },
@ -329,16 +294,6 @@ export const useDialogueStore = defineStore('dialogue', {
// 更新视频上传进度 // 更新视频上传进度
updateUploadProgress(uploadId, percentage) { updateUploadProgress(uploadId, percentage) {
// 更新全局列表中的进度
const globalTask = this.globalUploadList.find(item =>
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
)
if (globalTask) {
globalTask.extra.percentage = percentage
}
// 更新当前会话记录中的进度
const record = this.records.find(item => const record = this.records.find(item =>
item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId item.extra && item.extra.is_uploading && item.extra.upload_id === uploadId
) )
@ -348,44 +303,6 @@ export const useDialogueStore = defineStore('dialogue', {
} }
}, },
// 添加上传任务
addUploadTask(task) {
// 添加到全局列表
this.globalUploadList.push(task)
// 添加到会话映射
const sessionKey = `${task.talk_type}_${task.receiver_id}`
if (!this.uploadTaskMap[sessionKey]) {
this.uploadTaskMap[sessionKey] = []
}
this.uploadTaskMap[sessionKey].push(task)
// 同时添加到当前会话记录
this.addDialogueRecord(task)
},
// 上传完成后移除任务
removeUploadTask(uploadId) {
// 从全局列表中找到任务
const taskIndex = this.globalUploadList.findIndex(item => item.msg_id === uploadId)
if (taskIndex >= 0) {
const task = this.globalUploadList[taskIndex]
const sessionKey = `${task.talk_type}_${task.receiver_id}`
// 从会话映射中移除
if (this.uploadTaskMap[sessionKey]) {
const mapIndex = this.uploadTaskMap[sessionKey].findIndex(item => item.msg_id === uploadId)
if (mapIndex >= 0) {
this.uploadTaskMap[sessionKey].splice(mapIndex, 1)
}
}
// 从全局列表中移除
this.globalUploadList.splice(taskIndex, 1)
}
},
// 视频上传完成后更新消息 // 视频上传完成后更新消息
completeUpload(uploadId, videoInfo) { completeUpload(uploadId, videoInfo) {
const record = this.records.find(item => const record = this.records.find(item =>
@ -402,135 +319,6 @@ export const useDialogueStore = defineStore('dialogue', {
// 更新会话信息 // 更新会话信息
updateDialogueTalk(params){ updateDialogueTalk(params){
Object.assign(this.talk, params) Object.assign(this.talk, params)
},
// 根据 insert_sequence 将任务插入到 records 数组的正确位置(使用优化的二分查找)
insertTaskAtCorrectPosition(task) {
const len = this.records.length
// 快速路径:如果数组为空或任务应该插入到末尾
if (len === 0) {
this.records.push(task)
return
}
// 快速路径:检查是否应该插入到开头或末尾(避免二分查找的开销)
if (task.insert_sequence < this.records[0].sequence) {
this.records.unshift(task)
return
}
if (task.insert_sequence >= this.records[len - 1].sequence) {
this.records.push(task)
return
}
// 使用优化的二分查找算法找到插入位置
let low = 0
let high = len - 1
// 二分查找优化:使用位运算加速计算中点
while (low <= high) {
const mid = (low + high) >>> 1 // 无符号右移代替 Math.floor((low + high) / 2)
if (this.records[mid].sequence <= task.insert_sequence) {
low = mid + 1
} else {
high = mid - 1
}
}
// 在找到的位置插入任务
this.records.splice(low, 0, task)
},
// 恢复当前会话的上传任务
restoreUploadTasks() {
// 获取当前会话的sessionKey
const sessionKey = `${this.talk.talk_type}_${this.talk.receiver_id}`
// 检查是否有需要恢复的上传任务
if (!this.uploadTaskMap[sessionKey] || this.uploadTaskMap[sessionKey].length === 0) {
return
}
// 性能优化:缓存数组长度和本地变量,减少属性查找
const tasks = this.uploadTaskMap[sessionKey]
const tasksLength = tasks.length
// 如果只有一个任务,直接处理
if (tasksLength === 1) {
this.insertTaskAtCorrectPosition(tasks[0])
return
}
// 性能优化:对于少量任务,避免创建新数组和排序开销
if (tasksLength <= 10) {
// 找出最小的 insert_sequence
let minIndex = 0
for (let i = 1; i < tasksLength; i++) {
if (tasks[i].insert_sequence < tasks[minIndex].insert_sequence) {
minIndex = i
}
}
// 按顺序插入任务
let inserted = 0
let currentMin = tasks[minIndex]
this.insertTaskAtCorrectPosition(currentMin)
inserted++
while (inserted < tasksLength) {
minIndex = -1
let minSequence = Infinity
// 找出剩余任务中 insert_sequence 最小的
for (let i = 0; i < tasksLength; i++) {
const task = tasks[i]
if (task !== currentMin && task.insert_sequence < minSequence) {
minIndex = i
minSequence = task.insert_sequence
}
}
if (minIndex !== -1) {
currentMin = tasks[minIndex]
this.insertTaskAtCorrectPosition(currentMin)
inserted++
}
}
} else {
// 对于大量任务,使用排序后批量处理
// 创建一个新数组并排序,避免修改原数组
const sortedTasks = [...tasks].sort((a, b) => a.insert_sequence - b.insert_sequence)
// 性能优化:使用 requestAnimationFrame 进行批处理,更好地配合浏览器渲染周期
const batchSize = 50 // 每批处理的任务数量
const totalBatches = Math.ceil(sortedTasks.length / batchSize)
const processBatch = (batchIndex) => {
const startIndex = batchIndex * batchSize
const endIndex = Math.min(startIndex + batchSize, sortedTasks.length)
// 处理当前批次的任务
for (let i = startIndex; i < endIndex; i++) {
this.insertTaskAtCorrectPosition(sortedTasks[i])
}
// 如果还有更多批次,安排下一个批次
if (batchIndex < totalBatches - 1) {
// 使用 requestAnimationFrame 配合浏览器渲染周期
// 如果不支持,回退到 setTimeout
if (typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(() => processBatch(batchIndex + 1))
} else {
setTimeout(() => processBatch(batchIndex + 1), 0)
}
}
}
// 开始处理第一批
processBatch(0)
}
} }
} }
}) })

View File

@ -3,7 +3,6 @@ import { ServeGetTalkList, ServeCreateTalkList } from '@/api/chat'
import { formatTalkItem, ttime, KEY_INDEX_NAME } from '@/utils/talk' import { formatTalkItem, ttime, KEY_INDEX_NAME } from '@/utils/talk'
import { useEditorDraftStore } from './editor-draft' import { useEditorDraftStore } from './editor-draft'
import { ISession } from '@/types/chat' import { ISession } from '@/types/chat'
import { getConversations, addOrUpdateConversation, deleteConversation, getConversation } from '@/utils/db'
interface TalkStoreState { interface TalkStoreState {
loadStatus: number loadStatus: number
@ -46,103 +45,56 @@ export const useTalkStore = defineStore('talk', {
}, },
// 更新对话节点 // 更新对话节点
async updateItem(params: any) { updateItem(params: any) {
const item = this.items.find((item) => item.index_name === params.index_name) const item = this.items.find((item) => item.index_name === params.index_name)
if (item) { item && Object.assign(item, params)
Object.assign(item, params)
// 同步更新本地数据库
try {
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
} catch (error) {
console.error('更新本地会话失败:', error)
}
}
}, },
// 新增对话节点 // 新增对话节点
async addItem(params: any) { addItem(params: any) {
this.items = [params, ...this.items] this.items = [params, ...this.items]
// 同步添加到本地数据库
try {
await addOrUpdateConversation(JSON.parse(JSON.stringify(params)))
} catch (error) {
console.error('添加本地会话失败:', error)
}
}, },
// 移除对话节点 // 移除对话节点
async delItem(index_name: string) { delItem(index_name: string) {
const i = this.items.findIndex((item) => item.index_name === index_name) const i = this.items.findIndex((item) => item.index_name === index_name)
if (i >= 0) { if (i >= 0) {
const item = this.items[i]
this.items.splice(i, 1) this.items.splice(i, 1)
// 同步从本地数据库删除
try {
// 从本地数据库中查找并删除会话
const [talkType, receiverId] = index_name.split('_')
const conversation = await getConversation(Number(talkType), Number(receiverId))
if (conversation && conversation.id) {
await deleteConversation(conversation.id, false) // 不删除相关消息
}
} catch (error) {
console.error('删除本地会话失败:', error)
}
} }
this.items = [...this.items] this.items = [...this.items]
}, },
// 更新对话消息 // 更新对话消息
async updateMessage(params: any) { updateMessage(params: any) {
const item = this.items.find((item) => item.index_name === params.index_name) const item = this.items.find((item) => item.index_name === params.index_name)
if (item) { if (item) {
item.unread_num++ item.unread_num++
item.msg_text = params.msg_text item.msg_text = params.msg_text
item.updated_at = params.updated_at item.updated_at = params.updated_at
// 同步更新本地数据库中的会话信息
try {
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
} catch (error) {
console.error('更新本地会话消息失败:', error)
}
} }
}, },
// 更新联系人备注 // 更新联系人备注
async setRemark(params: any) { setRemark(params: any) {
const item = this.items.find((item) => item.index_name === `1_${params.user_id}`) const item = this.items.find((item) => item.index_name === `1_${params.user_id}`)
if (item) { item && (item.remark = params.remark)
item.remark = params.remark
// 同步更新本地数据库
try {
await addOrUpdateConversation(JSON.parse(JSON.stringify(item)))
} catch (error) {
console.error('更新本地联系人备注失败:', error)
}
}
}, },
// 加载会话列表 // 加载会话列表
async loadTalkList() { loadTalkList() {
this.loadStatus = 2 this.loadStatus = 2
try { const resp = ServeGetTalkList()
// 先从本地数据库加载会话列表
const localConversations = await getConversations() resp.then(({ code, data }) => {
if (localConversations && localConversations.length > 0) { if (code == 200) {
// 将本地会话列表转换为应用所需格式
this.items = localConversations.map((item: any) => { this.items = data.items.map((item: any) => {
// 确保本地存储的会话格式与应用一致
const value = formatTalkItem(item) const value = formatTalkItem(item)
const draft = useEditorDraftStore().items[value.index_name] const draft = useEditorDraftStore().items[value.index_name]
@ -155,64 +107,23 @@ export const useTalkStore = defineStore('talk', {
} }
return value return value
}) })
// 设置为加载完成状态,因为已从本地加载了数据,不需要等待服务器数据就可以显示
this.loadStatus = 3
}
// 从服务器获取最新会话列表
const resp = await ServeGetTalkList()
if (resp.code == 200) {
// 将服务器返回的会话列表转换为应用所需格式
const serverItems = resp.data.items.map((item: any) => {
const value = formatTalkItem(item)
const draft = useEditorDraftStore().items[value.index_name]
if (draft) {
value.draft_text = JSON.parse(draft).text || ''
}
if (value.is_robot == 1) {
value.is_online = 1
}
return value
})
// 更新状态和本地数据库
this.items = serverItems
// 将最新的会话列表保存到本地数据库
for (const item of serverItems) {
await addOrUpdateConversation(item)
}
this.loadStatus = 3 this.loadStatus = 3
} else { } else {
// 如果服务器请求失败但本地有数据,保持使用本地数据
if (this.items.length === 0) {
this.loadStatus = 4
} else {
this.loadStatus = 3
}
}
} catch (error) {
console.error('加载会话列表失败:', error)
// 如果有本地数据,即使服务器请求失败也显示本地数据
if (this.items.length === 0) {
this.loadStatus = 4 this.loadStatus = 4
} else {
this.loadStatus = 3
} }
} })
resp.catch(() => {
this.loadStatus = 4
})
}, },
findTalkIndex(index_name: string) { findTalkIndex(index_name: string) {
return this.items.findIndex((item: ISession) => item.index_name === index_name) return this.items.findIndex((item: ISession) => item.index_name === index_name)
}, },
async toTalk(talk_type: number, receiver_id: number, router: any) { toTalk(talk_type: number, receiver_id: number, router: any) {
const route = { const route = {
path: '/message', path: '/message',
query: { query: {
@ -225,31 +136,13 @@ export const useTalkStore = defineStore('talk', {
return router.push(route) return router.push(route)
} }
try { ServeCreateTalkList({
// 先检查本地数据库中是否有该会话 talk_type,
const localConversation = await getConversation(talk_type, receiver_id) receiver_id
}).then(({ code, data, message }) => {
if (localConversation) {
// 如果本地有该会话,直接添加到列表中
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
this.addItem(formatTalkItem(localConversation))
}
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
return router.push(route)
}
// 如果本地没有,则从服务器创建
const { code, data, message } = await ServeCreateTalkList({
talk_type,
receiver_id
})
if (code == 200) { if (code == 200) {
const formattedItem = formatTalkItem(data)
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) { if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
await this.addItem(formattedItem) // 使用 await 确保本地数据库同步更新 this.addItem(formatTalkItem(data))
} }
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`) sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
@ -257,10 +150,7 @@ export const useTalkStore = defineStore('talk', {
} else { } else {
window['$message'].info(message) window['$message'].info(message)
} }
} catch (error) { })
console.error('创建会话失败:', error)
window['$message'].error('创建会话失败,请稍后再试')
}
} }
} }
}) })

View File

@ -1,13 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
// import { message } from 'naive-ui' import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
import { import { ServeSendTalkFile } from '@/api/chat'
ServeSendTalkFile import { uploadImg } from '@/api/upload'
} from '@/api/chat'
import {
uploadImg,
ServeFindFileSplitInfo,
ServeFileSubareaUpload
} from '@/api/upload'
import { import {
useDialogueStore useDialogueStore
} from '@/store' } from '@/store'
@ -146,12 +140,12 @@ export const useUploadsStore = defineStore('uploads', {
this.triggerUpload(upload_id, clientUploadId) this.triggerUpload(upload_id, clientUploadId)
} else { } else {
message.error(res.message) message.error(res.message)
this.handleUploadError(upload_id, clientUploadId) onProgress(-1) // 通知上传失败
} }
} catch (error) { } catch (error) {
console.error("初始化分片上传失败:", error); console.error("初始化分片上传失败:", error);
message.error("初始化上传失败,请重试") message.error("初始化上传失败,请重试")
this.handleUploadError(upload_id, clientUploadId) onProgress(-1)
} }
}, },
@ -176,14 +170,14 @@ export const useUploadsStore = defineStore('uploads', {
// 更新状态为上传中 // 更新状态为上传中
currentItem.status = 1 currentItem.status = 1
const updatedItem:any = this.findItem(uploadId)
// 上传当前分片 // 上传当前分片
try { try {
const res = await ServeFileSubareaUpload(form) const res = await ServeFileSubareaUpload(form)
// 获取最新的项目状态,确保仍然存在且没有被暂停 // 获取最新的项目状态,确保仍然存在且没有被暂停
const updatedItem:any = this.findItem(uploadId)
if (res.code == 200) { if (res.code == 200) {
// 当前分片上传成功,增加索引 // 当前分片上传成功,增加索引
updatedItem.uploadIndex++ updatedItem.uploadIndex++
@ -207,20 +201,24 @@ export const useUploadsStore = defineStore('uploads', {
this.triggerUpload(uploadId, clientUploadId) this.triggerUpload(uploadId, clientUploadId)
} }
} else { } else {
updatedItem.onProgress(-1)
// 上传失败处理 // 上传失败处理
console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`); console.error(`分片上传失败,错误码: ${res.code},错误信息: ${res.message || '未知错误'}`);
this.handleUploadError(uploadId, clientUploadId || '') updatedItem.status = 3
} }
} catch (error) { } catch (error) {
console.error("分片上传错误:", error); console.error("分片上传错误:", error);
// 获取最新的项目状态 // 获取最新的项目状态
const updatedItem = this.findItem(uploadId)
if (!updatedItem) return if (!updatedItem) return
// 如果是暂停导致的错误,不改变状态 // 如果是暂停导致的错误,不改变状态
if (updatedItem.is_paused) return if (updatedItem.is_paused) return
this.handleUploadError(uploadId, clientUploadId || '') updatedItem.status = 3
} }
}, },
@ -244,10 +242,6 @@ export const useUploadsStore = defineStore('uploads', {
talk_type: item.talk_type talk_type: item.talk_type
}) })
// 从DialogueStore中移除上传任务
const dialogueStore = useDialogueStore()
dialogueStore.removeUploadTask(clientUploadId)
if (item.onComplete) { if (item.onComplete) {
item.onComplete(item) item.onComplete(item)
} }
@ -295,21 +289,5 @@ export const useUploadsStore = defineStore('uploads', {
// 从上传列表中移除旧的上传项 // 从上传列表中移除旧的上传项
this.items = this.items.filter(i => i.client_upload_id !== clientUploadId) this.items = this.items.filter(i => i.client_upload_id !== clientUploadId)
}, },
// 上传失败处理
async handleUploadError(uploadId: string, clientUploadId: string) {
const item = this.findItem(uploadId)
if (!item) return
item.status = 3 // 设置为上传失败状态
// 从DialogueStore中移除上传任务
const dialogueStore = useDialogueStore()
dialogueStore.removeUploadTask(clientUploadId)
if (item.onProgress) {
item.onProgress(-1) // 通知上传失败
}
}
} }
}) })

View File

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

View File

@ -68,11 +68,6 @@ export function clipboard(text, callback) {
} }
export async function clipboardImage(src, callback) { export async function clipboardImage(src, callback) {
// 在wujie环境下使用主应用的clipboard
const clipboardObj = window.__POWERED_BY_WUJIE__
? window.parent.navigator.clipboard
: navigator.clipboard
const { state } = await navigator.permissions.query({ const { state } = await navigator.permissions.query({
name: 'clipboard-write' name: 'clipboard-write'
}) })
@ -85,7 +80,7 @@ export async function clipboardImage(src, callback) {
// navigator.clipboard.write 仅支持 png 图片 // navigator.clipboard.write 仅支持 png 图片
if (blob.type == 'image/png') { if (blob.type == 'image/png') {
await clipboardObj.write([ await navigator.clipboard.write([
new ClipboardItem({ new ClipboardItem({
[blob.type]: blob [blob.type]: blob
}) })
@ -104,13 +99,13 @@ export async function clipboardImage(src, callback) {
canvas.width = img.width canvas.width = img.width
canvas.height = img.height canvas.height = img.height
ctx.drawImage(img, 0, 0, canvas.width, canvas.height) ctx.drawImage(img, 0, 0)
canvas.toBlob( canvas.toBlob(
(blob) => { (blob) => {
const data = [new ClipboardItem({ [blob.type]: blob })] const data = [new ClipboardItem({ [blob.type]: blob })]
clipboardObj navigator.clipboard
.write(data) .write(data)
.then(() => { .then(() => {
callback() callback()

View File

@ -1,381 +0,0 @@
import Dexie from 'dexie';
export const db = new Dexie('chatHistory');
// 定义数据库表结构和索引
// 版本3优化了索引提高了查询和排序性能
db.version(4).stores({
/**
* 聊天记录表
* - msg_id: 消息唯一ID (主键)
* - sequence: 消息序列号用于排序
* - [talk_type+receiver_id]: 复合索引用于快速查询会话消息
* - created_at: 消息创建时间用于排序
* - [talk_type+receiver_id+sequence]: 复合索引用于高效分页查询
*/
messages: 'msg_id, sequence, [talk_type+receiver_id], created_at, [talk_type+receiver_id+sequence]',
/**
* 会话表
* - ++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',
});
db.on('ready', () => {
console.log(`数据库已就绪,版本: ${db.verno}`);
});
/** 消息类型常量 */
export const MessageType = {
TEXT: 1, // 文本消息
IMAGE: 2, // 图片消息
FILE: 3, // 文件消息
AUDIO: 4, // 语音消息
VIDEO: 5, // 视频消息
LOCATION: 6, // 位置消息
CARD: 7, // 名片消息
};
/** 会话类型常量 */
export const TalkType = {
PRIVATE: 1, // 私聊
GROUP: 2, // 群聊
};
/**
* 生成一个简单的UUID
* @returns {string} UUID
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// #region 消息操作
/**
* 添加或更新一条聊天记录
* @param {object} message - 消息对象
* @returns {Promise<string>} 消息ID
*/
export async function addMessage(message) {
try {
if (!message.msg_id) {
message.msg_id = generateUUID();
}
if (!message.created_at) {
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
}
// 使用 put 方法,如果主键已存在则更新,否则添加
await db.messages.put(message);
return message.msg_id;
} catch (error) {
console.error('添加或更新消息失败:', error);
throw error;
}
}
/**
* 批量添加或更新聊天记录
* @param {Array<object>} messages - 消息对象数组
* @returns {Promise<void>}
*/
export async function batchAddOrUpdateMessages(messages) {
try {
if (!Array.isArray(messages) || messages.length === 0) {
return;
}
const messagesToStore = messages.map(message => {
if (!message.msg_id) {
message.msg_id = generateUUID();
}
if (!message.created_at) {
message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19);
}
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;
}
}
/**
* 获取指定会话的聊天记录
* @param {number} talkType - 会话类型 (1:私聊, 2:群聊)
* @param {number} userId - 当前用户ID
* @param {number} receiverId - 接收者ID (私聊为对方用户ID群聊为群ID)
* @param {number} [limit=30] - 限制返回的记录数量
* @param {number|null} [maxSequence=null] - 最大sequence值用于分页加载更早的消息
* @returns {Promise<Array<object>>} 消息列表 (按sequence升序排列)
*/
export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) {
try {
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] });
}
// 1. reverse() - 利用索引倒序排列,获取最新的消息
// 2. limit() - 限制数量,实现分页
// 3. toArray() - 执行查询
const messages = await collection.reverse().limit(limit).toArray();
// 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示
return messages.reverse();
} catch (error) {
console.error('获取消息失败:', error);
throw error;
}
}
/**
* 标记指定会话的所有消息为已读
* @param {number} talkType - 会话类型
* @param {number} userId - 当前用户ID
* @param {number} receiverId - 接收者ID
* @returns {Promise<number>} 更新的消息数量
*/
export async function markMessagesAsRead(talkType, userId, receiverId) {
try {
let query;
if (talkType === TalkType.PRIVATE) {
// 私聊:只标记对方发给我的未读消息
query = db.messages
.where('[talk_type+receiver_id]')
.equals([talkType, userId])
.and(item => item.user_id === receiverId && item.is_read === 0);
} else {
// 群聊:标记群里所有非自己的未读消息
query = db.messages
.where('[talk_type+receiver_id]')
.equals([talkType, receiverId])
.and(item => item.user_id !== userId && item.is_read === 0);
}
return await query.modify({ is_read: 1 });
} catch (error) {
console.error('批量标记消息已读失败:', error);
throw error;
}
}
/**
* 撤回消息
* @param {string} msgId - 消息ID
* @returns {Promise<number>} 更新记录数 (1或0)
*/
export async function revokeMessage(msgId) {
try {
return await db.messages.update(msgId, { is_revoke: 1 });
} catch (error) {
console.error('撤回消息失败:', error);
throw error;
}
}
/**
* 删除消息
* @param {string} msgId - 消息ID
* @returns {Promise<void>}
*/
export async function deleteMessage(msgId) {
try {
await db.messages.delete(msgId);
} catch (error) {
console.error('删除消息失败:', error);
throw error;
}
}
// #endregion 消息操作
// #region 会话操作
/**
* 添加或更新会话
* @param {object} conversation - 会话对象
* @returns {Promise<number>} 会话ID
*/
export async function addOrUpdateConversation(conversation) {
try {
// put 方法会根据唯一索引 index_name 自动判断是添加还是更新
return await db.conversations.put(conversation);
} catch (error) {
console.error('添加或更新会话失败:', error);
throw error;
}
}
/**
* 获取所有会话列表
* @param {boolean} [includeEmpty=false] - 是否包含没有最后一条消息的会话
* @returns {Promise<Array<object>>} 会话列表 (按置顶和更新时间排序)
*/
export async function getConversations(includeEmpty = false) {
try {
const filterFn = item => !includeEmpty ? (item.msg_text && item.msg_text.length > 0) : true;
// 分别查询置顶和非置顶会话,以利用索引并优化性能
const topConversationsPromise = db.conversations
.where('is_top')
.equals(1)
.sortBy('updated_at')
.then(arr => arr.reverse().filter(filterFn));
const otherConversationsPromise = db.conversations
.where('is_top')
.notEqual(1)
.sortBy('updated_at')
.then(arr => arr.reverse().filter(filterFn));
const [topConversations, otherConversations] = await Promise.all([
topConversationsPromise,
otherConversationsPromise,
]);
return [...topConversations, ...otherConversations];
} catch (error) {
console.error('获取会话列表失败:', error);
throw error;
}
}
/**
* 获取指定会话
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @returns {Promise<object|undefined>} 会话对象
*/
export async function getConversation(talkType, receiverId) {
try {
const indexName = `${talkType}_${receiverId}`;
return await db.conversations.get({ index_name: indexName });
} catch (error) {
console.error('获取会话失败:', error);
throw error;
}
}
/**
* 更新会话的未读消息数
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @param {number|null} unreadNum - 未读消息数如果为null则自增1
* @returns {Promise<number>} 更新的记录数
*/
export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) {
try {
const indexName = `${talkType}_${receiverId}`;
const conversation = await db.conversations.get({ index_name: indexName });
if (conversation) {
const newUnreadNum = unreadNum === null ? (conversation.unread_num || 0) + 1 : unreadNum;
return await db.conversations.update(conversation.id, { unread_num: newUnreadNum });
}
return 0;
} catch (error) {
console.error('更新会话未读数失败:', error);
throw error;
}
}
/**
* 清空会话的未读消息数
* @param {number} talkType - 会话类型
* @param {number} receiverId - 接收者ID
* @returns {Promise<number>} 更新的记录数
*/
export function clearConversationUnreadNum(talkType, receiverId) {
return updateConversationUnreadNum(talkType, receiverId, 0);
}
/**
* 删除会话及其相关的消息
* @param {number} conversationId - 会话ID
* @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录
* @returns {Promise<void>}
*/
export async function deleteConversation(conversationId, deleteMessages = false) {
try {
await db.transaction('rw', db.conversations, db.messages, async () => {
const conversation = await db.conversations.get(conversationId);
if (!conversation) return;
// 删除会话
await db.conversations.delete(conversationId);
// 如果需要,删除关联的消息
if (deleteMessages) {
const { talk_type, receiver_id } = conversation;
await db.messages.where({ '[talk_type+receiver_id]': [talk_type, receiver_id] }).delete();
}
});
} catch (error) {
console.error('删除会话失败:', error);
throw error;
}
}
/**
* 更新会话的最后一条消息摘要
* @param {object} message - 消息对象
* @returns {Promise<number>} 更新的记录数
*/
export async function updateConversationLastMessage(message) {
try {
const { talk_type, user_id, receiver_id, msg_type } = 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 });
if (!conversation) return 0;
let msgText = '';
switch (msg_type) {
case MessageType.TEXT: msgText = message.content || ''; break;
case MessageType.IMAGE: msgText = '[图片]'; break;
case MessageType.FILE: msgText = '[文件]'; break;
case MessageType.AUDIO: msgText = '[语音]'; break;
case MessageType.VIDEO: msgText = '[视频]'; break;
case MessageType.LOCATION: msgText = '[位置]'; break;
case MessageType.CARD: msgText = '[名片]'; break;
default: msgText = '[未知消息]';
}
return await db.conversations.update(conversation.id, {
msg_text: msgText,
content: message.content || '',
updated_at: message.created_at,
});
} catch (error) {
console.error('更新会话最后消息失败:', error);
throw error;
}
}
// #endregion 会话操作

View File

@ -25,15 +25,15 @@ const errorHandler = (error) => {
if (!once) { if (!once) {
once = true once = true
// window['$dialog'].info({ window['$dialog'].info({
// title: '友情提示', title: '友情提示',
// content: '当前登录已失效,请重新登录?', content: '当前登录已失效,请重新登录?',
// positiveText: '立即登录?', positiveText: '立即登录?',
// maskClosable: false, maskClosable: false,
// onPositiveClick: () => { onPositiveClick: () => {
// location.reload() location.reload()
// } }
// }) })
} }
} }
} }
@ -53,12 +53,7 @@ request.interceptors.request.use((config) => {
}, errorHandler) }, errorHandler)
// 响应拦截器 // 响应拦截器
request.interceptors.response.use((response) => { request.interceptors.response.use((response) => response.data, errorHandler)
if(response.data.code !==200&&response.data.status!==0){
window['$message'].warning(response.data.msg)
}
return response.data
}, errorHandler)
/** /**
* GET 请求 * GET 请求

View File

@ -12,14 +12,12 @@ import customModal from '@/components/common/customModal.vue'
import historyRecord from '@/components/search/searchByCondition.vue' import historyRecord from '@/components/search/searchByCondition.vue'
import { ServeEditGroupNotice, ServeGetGroupNotices, ServeDeleteGroupNotice } from '@/api/group' import { ServeEditGroupNotice, ServeGetGroupNotices, ServeDeleteGroupNotice } from '@/api/group'
import avatarModule from '@/components/avatar-module/index.vue' import avatarModule from '@/components/avatar-module/index.vue'
import { ServeCheckFriend, ServeAddFriend } from '@/api/chat'
import { useUtil } from '@/hooks/useUtil'
const { useMessage } = useUtil()
const userStore = useUserStore() const userStore = useUserStore()
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
const uploadsStore = useUploadsStore() const uploadsStore = useUploadsStore()
console.log('dialogueStore', dialogueStore)
const members = computed(() => dialogueStore.members) const members = computed(() => dialogueStore.members)
const membersByAlphabet = computed(() => { const membersByAlphabet = computed(() => {
if (state.searchMemberByAlphabet) { if (state.searchMemberByAlphabet) {
@ -124,31 +122,9 @@ const events = {
} }
} }
// const isFriend = ref(true) //
// //
// const AddFriends = () => {
// let params = {
// receiver_id: talkParams.receiver_id, //id
// talk_type: 1
// }
// ServeAddFriend(params).then((res) => {
// if (res?.code === 200) {
// isFriend.value = !isFriend.value
// useMessage.success('')
// }
// })
// }
watch( watch(
() => talkParams, () => talkParams,
(newValue, oldValue) => { (newValue, oldValue) => {
//
// if (talkParams.type !== 2) {
// ServeCheckFriend({ receiver_id: newValue.receiver_id, talk_type: 1 }).then((res) => {
// if (res?.code === 200) {
// isFriend.value = res.data.is_friend
// }
// })
// }
console.log(newValue) console.log(newValue)
}, },
{ deep: true, immediate: true } { deep: true, immediate: true }
@ -534,27 +510,7 @@ const clearSelectedDateTime = () => {
</header> </header>
<!-- 聊天区域 --> <!-- 聊天区域 -->
<main class="el-main relative"> <main class="el-main">
<!-- <div
class="p-[15px] pt-[10px] w-[100%] z-99 absolute"
v-if="!isFriend && talkParams.type !== 2"
>
<div
class="bg-[#FFFFFF] w-[100%] p-[10px] text-[14px] flex justify-between"
style="box-shadow: 0 2px 6px 1px rgba(0, 0, 0, 0.2) !important; border-radius: 5px"
>
对方还不是您的好友请添加到通讯录中吧!
<n-button
@click="AddFriends"
size="tiny"
type="success"
color="#46299D"
text-color="#ffffff"
>
<span>添加好友</span>
</n-button>
</div>
</div> -->
<PanelContent <PanelContent
:uid="talkParams.uid" :uid="talkParams.uid"
:talk_type="talkParams.type" :talk_type="talkParams.type"
@ -639,13 +595,13 @@ const clearSelectedDateTime = () => {
> >
<template #content> <template #content>
<div class="search-record-modal-searchArea"> <div class="search-record-modal-searchArea">
<n-card style="padding: 0 12px"> <n-card style="padding: 0 12px;">
<div class="search-record-input"> <div class="search-record-input">
<span class="search-record-input-title">搜索</span> <span class="search-record-input-title">搜索</span>
<n-input <n-input
type="text" type="text"
v-model:value="state.searchRecordByConditionText" v-model:value="state.searchRecordByConditionText"
:placeholder="state.conditionTag && state.conditionTag !== 'all' ? '' : '请输入'" :placeholder="state.conditionTag && state.conditionTag !== 'all'?'':'请输入'"
clearable clearable
> >
<template #clear-icon> <template #clear-icon>
@ -669,7 +625,7 @@ const clearSelectedDateTime = () => {
v-model:show="state.showDateConditionPopover" v-model:show="state.showDateConditionPopover"
trigger="click" trigger="click"
placement="bottom-start" placement="bottom-start"
style="height: 312px; padding: 0" style="height: 312px; padding: 0;"
@update:show="onDatePickShow" @update:show="onDatePickShow"
> >
<template #trigger> <template #trigger>
@ -697,7 +653,7 @@ const clearSelectedDateTime = () => {
v-model:show="state.showMemberListByAlphabetPopover" v-model:show="state.showMemberListByAlphabetPopover"
trigger="click" trigger="click"
placement="bottom-start" placement="bottom-start"
style="width: 290px; height: 505px; padding: 0" style="width: 290px; height: 505px; padding: 0;"
v-if="talkParams.type === 2" v-if="talkParams.type === 2"
> >
<template #trigger> <template #trigger>
@ -706,10 +662,10 @@ const clearSelectedDateTime = () => {
<div class="member-list-by-alphabet-container"> <div class="member-list-by-alphabet-container">
<n-input <n-input
placeholder="请输入群成员" placeholder="请输入群成员"
style="margin: 0 0 17px" style="margin: 0 0 17px;"
v-model:value="state.searchMemberByAlphabet" v-model:value="state.searchMemberByAlphabet"
/> />
<n-scrollbar style="height: 430px"> <n-scrollbar style="height: 430px;">
<div <div
class="member-list-by-alphabet" class="member-list-by-alphabet"
v-for="(alphabetMembersItem, alphabetMembersIndex) in membersByAlphabet" v-for="(alphabetMembersItem, alphabetMembersIndex) in membersByAlphabet"
@ -721,8 +677,7 @@ const clearSelectedDateTime = () => {
<div class="member-list-each-alphabet"> <div class="member-list-each-alphabet">
<div <div
class="member-item-each-alphabet" class="member-item-each-alphabet"
v-for="(memberItem, memberItemIndex) in (alphabetMembersItem as any) v-for="(memberItem, memberItemIndex) in (alphabetMembersItem as any).members"
.members"
:key="memberItemIndex" :key="memberItemIndex"
@click="handleMemberItemClick(memberItem)" @click="handleMemberItemClick(memberItem)"
> >
@ -823,7 +778,7 @@ const clearSelectedDateTime = () => {
}" }"
></avatarModule> ></avatarModule>
<div class="group-notice-header-userInfo"> <div class="group-notice-header-userInfo">
<span style="color: #1b1b1b; font-weight: 600; line-height: 20px">{{ <span style="color: #1b1b1b; font-weight: 600; line-height: 20px;">{{
state.groupNoticeInfo.updater_name state.groupNoticeInfo.updater_name
}}</span> }}</span>
<span>{{ state.groupNoticeInfo.updated_at }}</span> <span>{{ state.groupNoticeInfo.updated_at }}</span>

View File

@ -1,4 +1,4 @@
<script lang="tsx" setup> <script lang="ts" setup>
import { import {
computed, computed,
ref, ref,
@ -23,10 +23,10 @@ import {
NButton, NButton,
NPagination NPagination
} from 'naive-ui' } from 'naive-ui'
import { Search, Plus, Right, AddOne, PeoplePlusOne } from '@icon-park/vue-next' import { Search, Plus, Right } from '@icon-park/vue-next'
import TalkItem from './TalkItem.vue' import TalkItem from './TalkItem.vue'
import Skeleton from './Skeleton.vue' import Skeleton from './Skeleton.vue'
import { ServeClearTalkUnreadNum, ServeAddFriend, GetFriendList } from '@/api/chat' import { ServeClearTalkUnreadNum } from '@/api/chat'
import GroupLaunch from '@/components/group/GroupLaunch.vue' import GroupLaunch from '@/components/group/GroupLaunch.vue'
import { getCacheIndexName } from '@/utils/talk' import { getCacheIndexName } from '@/utils/talk'
import { ISession } from '@/types/chat' import { ISession } from '@/types/chat'
@ -38,15 +38,9 @@ import flTree from '@/components/flnlayout/tree/flnindex.vue'
import { processError, processSuccess } from '@/utils/helper/message.js' import { processError, processSuccess } from '@/utils/helper/message.js'
import chatAppSearchList from '@/components/search/searchList.vue' import chatAppSearchList from '@/components/search/searchList.vue'
import { ServeSeachQueryAll, ServeQueryTalkRecord, ServeUserGroupChatList } from '@/api/search' import { ServeSeachQueryAll, ServeQueryTalkRecord, ServeUserGroupChatList } from '@/api/search'
import { GetContactFriendList } from '@/api/chat'
import { getUserInfoByERPUserId } from '@/api/user' import { getUserInfoByERPUserId } from '@/api/user'
import HighlightText from '@/components/search/highLightText.vue' import HighlightText from '@/components/search/highLightText.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import icon from '@/assets/image/chatList/addressBook.png'
import { useUtil } from '@/hooks/useUtil'
import UserCardModal from '@/components/user/UserCardModal.vue'
const { useMessage } = useUtil()
const router = useRouter() const router = useRouter()
const currentInstance = getCurrentInstance() const currentInstance = getCurrentInstance()
@ -66,43 +60,7 @@ const isShowGroup = ref(false)
const searchKeyword = ref('') const searchKeyword = ref('')
const topItems = computed((): ISession[] => talkStore.topItems) const topItems = computed((): ISession[] => talkStore.topItems)
const unreadNum = computed(() => talkStore.talkUnreadNum) const unreadNum = computed(() => talkStore.talkUnreadNum)
//
const handleConfirmDel = (row) => {
window['$dialog'].create({
title: '温馨提示',
content: '是否删除该好友?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
console.log('确定')
let params = {
receiver_id: row.id, //id
talk_type: 1
}
let url = '/api/v1/contact/friend/delete'
$request.HTTP.components.postDataByParams(url, params).then((res) => {
// console.log(res)
if (res?.code === 200) {
useMessage.success('删除成功')
getMyFriends()
}
})
}
})
}
const option = ref([
{
label: '添加好友',
key: 'addFriend',
icon: () => <n-icon size="20" component={PeoplePlusOne} />
},
{
label: '通讯录',
key: 'addressBook',
icon: () => <img style="width: 19px; height: 20px; cursor: pointer" src={icon} />
}
])
// //
const renderChatAppSearch = () => { const renderChatAppSearch = () => {
return h( return h(
@ -131,10 +89,10 @@ const renderChatAppSearch = () => {
state.searchRecordText = searchText state.searchRecordText = searchText
state.selectItemInList = res state.selectItemInList = res
} else { } else {
if (searchResultKey === 'user_infos') { if(searchResultKey === 'user_infos'){
talk_type = 1 talk_type = 1
} }
if (searchResultKey === 'combinedGroup') { if(searchResultKey === 'combinedGroup'){
talk_type = 2 talk_type = 2
} }
talkStore.toTalk(talk_type, receiver_id, router) talkStore.toTalk(talk_type, receiver_id, router)
@ -187,13 +145,7 @@ const renderChatAppSearch = () => {
} }
const state = reactive({ const state = reactive({
userInfo: {
isShowUserCardModal: false,
user_id: NaN,
erp_user_id: NaN
},
isShowAddressBookModal: false, // isShowAddressBookModal: false, //
isShowAddFriendModal: false, //
customModalStyle: { customModalStyle: {
width: '1288px', width: '1288px',
height: '846px', height: '846px',
@ -214,25 +166,7 @@ const state = reactive({
type: 'input', type: 'input',
valueType: 'string' valueType: 'string'
} }
], ], //
//
myFriendSearchConfig: [
{
label: '好友名称',
key: 'myFriendname',
type: 'input',
valueType: 'string'
}
],
addFriendSearchConfig: [
{
label: '姓名',
key: 'friendName',
type: 'input',
valueType: 'string'
}
], //
//
treeData: [], treeData: [],
expandedKeys: [], expandedKeys: [],
clickKey: 3, clickKey: 3,
@ -241,19 +175,18 @@ const state = reactive({
addressBookColumns: [ addressBookColumns: [
{ {
title: '姓名 【工号】', title: '姓名 【工号】',
field: 'nickname', field: 'nickName',
width: 200, width: 200,
ellipsis: { ellipsis: {
tooltip: true tooltip: true
}, },
render(row, index) { render(row, index) {
// return row.nickname + '' + row.job_num + ''
return row.nickName + '【' + row.jobNum + '】' return row.nickName + '【' + row.jobNum + '】'
} }
}, },
{ {
title: '岗位名称', title: '岗位名称',
field: 'user_position', field: 'positionName',
width: 400, width: 400,
ellipsis: { ellipsis: {
tooltip: true tooltip: true
@ -265,7 +198,6 @@ const state = reactive({
) )
: [] : []
return positionNames.join(' , ') return positionNames.join(' , ')
// return row.user_position.map((item) => item.position_name).join(' , ')
} }
}, },
{ {
@ -338,124 +270,8 @@ const state = reactive({
} }
} }
], // ], //
myFriendListColumns: [
{
title: '姓名 【工号】',
field: 'nickname',
width: 200,
ellipsis: {
tooltip: true
},
render(row, index) {
return row.nickname + '【' + row.job_num + '】'
}
},
{
title: '岗位名称',
field: 'user_position',
width: 400,
ellipsis: {
tooltip: true
},
render(row, index) {
// let positionNames = Array.isArray(row.user_position)
// ? row.depPositions.flatMap((dep) =>
// Array.isArray(dep.positions) ? dep.positions.map((pos) => pos.name) : []
// )
// : []
return row.user_position.map((item) => item.position_name).join(' , ')
}
},
{
title: '操作',
field: 'action',
width: 180,
align: 'center',
fixed: 'right',
render(row, index) {
return [
h(
NButton,
{
size: 'small',
text: true,
color: '#46299d',
onClick: () => handleEnterChat(row)
},
{ default: () => '进入聊天' }
),
h(
NButton,
{
size: 'small',
text: true,
color: '#46299d',
class: 'pl-[10px]',
onClick: () => handleConfirmDel(row)
},
{ default: () => '删除好友' }
)
]
}
}
], //
addFriendListColumns: [
{
title: '姓名 【工号】',
field: 'nickname',
width: 200,
ellipsis: {
tooltip: true
},
render(row, index) {
return row.nickname + '【' + row.job_num + '】'
}
},
{
title: '岗位名称',
field: 'user_position',
width: 400,
ellipsis: {
tooltip: true
},
render(row, index) {
// let positionNames = Array.isArray(row.user_position)
// ? row.depPositions.flatMap((dep) =>
// Array.isArray(dep.positions) ? dep.positions.map((pos) => pos.name) : []
// )
// : []
return row.user_position.map((item) => item.position_name).join(' , ')
}
},
{
title: '操作',
field: 'action',
width: 180,
align: 'center',
fixed: 'right',
render(row, index) {
return h(
NButton,
{
size: 'small',
text: true,
color: '#46299d',
onClick: () => {
state.userInfo.user_id = row.id
state.userInfo.erp_user_id = row.erp_user_id
state.userInfo.isShowUserCardModal = true
}
},
{ default: () => '查看' }
)
}
}
], //
addressBookData: [], // addressBookData: [], //
company_name: '', //
groupChatListData: [], // groupChatListData: [], //
myFriendListData: [], //
addFriendList: [], //
addressBookTableHeight: 524, // addressBookTableHeight: 524, //
addressBookTableWidth: 800, // addressBookTableWidth: 800, //
addressBookPage: 1, // addressBookPage: 1, //
@ -467,10 +283,6 @@ const state = reactive({
groupChatListPageSize: 10, // groupChatListPageSize: 10, //
groupChatListTotal: 0, // groupChatListTotal: 0, //
groupChatListSearchGroupName: '', // - groupChatListSearchGroupName: '', // -
myFriendListPage: 1, //
myFriendListPageSize: 10, //
myFriendListTotal: 0, //
myFriendListSearchName: '', // -
chatSearchOptions: [ chatSearchOptions: [
{ {
key: 'chatSearch', key: 'chatSearch',
@ -522,9 +334,6 @@ const items = computed((): ISession[] => {
return [...topItems, ...normalItems] return [...topItems, ...normalItems]
}) })
setTimeout(() => {
console.log('items', items)
}, 2000)
watch( watch(
() => state.addressBookSearchNickName, () => state.addressBookSearchNickName,
(newValue, oldValue) => { (newValue, oldValue) => {
@ -541,17 +350,6 @@ watch(
getDepPoisUser() getDepPoisUser()
} }
) )
watch(
() => state.myFriendListSearchName,
(newValue, oldValue) => {
if (newValue) {
state.myFriendListPage = 1
} else {
state.myFriendListPage = 1
}
getMyFriends()
}
)
watch( watch(
() => state.groupChatListSearchGroupName, () => state.groupChatListSearchGroupName,
(newValue, oldValue) => { (newValue, oldValue) => {
@ -599,7 +397,7 @@ const indexName = computed(() => dialogueStore.index_name)
// //
const onTabTalk = (item: ISession, follow = false) => { const onTabTalk = (item: ISession, follow = false) => {
console.log('onTabTalk') console.log('onTabTalk')
console.log('item.index_name === indexName.value', item.index_name === indexName.value)
if (item.index_name === indexName.value) return if (item.index_name === indexName.value) return
searchKeyword.value = '' searchKeyword.value = ''
@ -644,7 +442,7 @@ const onReload = () => {
// //
const onInitialize = () => { const onInitialize = () => {
let index_name = getCacheIndexName() let index_name = getCacheIndexName()
console.log('index_name', index_name)
index_name && onTabTalk(talkStore.findItem(index_name), true) index_name && onTabTalk(talkStore.findItem(index_name), true)
} }
@ -653,8 +451,6 @@ onBeforeRouteUpdate(onInitialize)
onBeforeMount(() => { onBeforeMount(() => {
getTreeData() getTreeData()
getDepPoisUser()
// getMyFriends()
getUserGroupChatList() getUserGroupChatList()
}) })
@ -666,24 +462,11 @@ onMounted(() => {
const showAddressBookModal = () => { const showAddressBookModal = () => {
state.isShowAddressBookModal = true state.isShowAddressBookModal = true
} }
//
const showAddFriendModal = () => {
state.isShowAddFriendModal = true
}
const handleSelect = (key: string | number) => {
if (key === 'addressBook') return showAddressBookModal()
showAddFriendModal()
}
// //
const closeAddressBookModal = () => { const closeAddressBookModal = () => {
state.isShowAddressBookModal = false state.isShowAddressBookModal = false
resetAddressBookModal() resetAddressBookModal()
} }
//
const closeAddFriendModal = () => {
state.isShowAddFriendModal = false
resetAddressBookModal()
}
const handleTreeClick = ({ selectedKey, tree }) => { const handleTreeClick = ({ selectedKey, tree }) => {
// console.log(tree) // console.log(tree)
state.clickKey = tree.key state.clickKey = tree.key
@ -705,7 +488,7 @@ const calcTreeData = (data) => {
delete item.sons delete item.sons
} }
} }
// - //
const getTreeData = () => { const getTreeData = () => {
let url = '/department/v2/tree/filter' let url = '/department/v2/tree/filter'
let params = {} let params = {}
@ -729,32 +512,9 @@ const getTreeData = () => {
} }
) )
} }
//
const getMyFriends = () => {
// myFriendListPage: 1, //
// myFriendListPageSize: 10, //
// myFriendListTotal: 0, //
// myFriendListSearchName: '', // -
let params = {
type: 'myFriends', //myFriends
page: state.myFriendListPage,
page_size: state.myFriendListPageSize,
name: state.myFriendListSearchName
}
// let url = '/api/v1/contact/friend/list'
GetContactFriendList(params).then((res) => {
// console.log(res)
if (res.code === 200 && Array.isArray(res.data.user_list)) {
state.myFriendListData = res.data.user_list || []
state.company_name = res.data.company_name || ''
state.myFriendListTotal = res.data.count
}
})
}
// //
const getDepPoisUser = () => { const getDepPoisUser = () => {
let url = '/user/v2/list' let url = '/user/v2/list'
// let url = '/api/v1/contact/friend/list'
let params = { let params = {
departmentId: state.addressBookSearchNickName ? undefined : state.clickKey, departmentId: state.addressBookSearchNickName ? undefined : state.clickKey,
page: state.addressBookPage, page: state.addressBookPage,
@ -769,42 +529,11 @@ const getDepPoisUser = () => {
state.addressBookTotal = res.data.count state.addressBookTotal = res.data.count
} }
}) })
// let params = {
// type: 'addressBook', //addressBook
// page: state.addressBookPage,
// page_size: state.addressBookPageSize,
// name: state.addressBookSearchNickName
// }
// GetContactFriendList(params).then((res) => {
// // console.log(res)
// if (res.code === 200 && Array.isArray(res.data.user_list)) {
// state.addressBookData = res.data.user_list || []
// state.company_name = res.data.company_name || ''
// state.addressBookTotal = res.data.count
// }
// })
} }
//
const AddFriends = (row) => {
let params = {
receiver_id: row.erp_user_id, //id
talk_type: 1
}
ServeAddFriend(params).then((res) => {
if (res?.code === 200) {
useMessage.success('添加成功')
}
})
}
// //
const handleEnterChat = async (row) => { const handleEnterChat = async (row) => {
console.log(row) console.log(row)
if ( if (state.addressBookCurrentTab === 'employeeAddressBook') {
state.addressBookCurrentTab === 'employeeAddressBook' ||
state.addressBookCurrentTab === 'myFriend'
) {
// //
await getUserInfoByERPUserId({ erp_user_id: row.ID }).then((res) => { await getUserInfoByERPUserId({ erp_user_id: row.ID }).then((res) => {
// console.log(res) // console.log(res)
@ -834,9 +563,7 @@ const resetAddressBookModal = () => {
state.groupChatListPage = 1 state.groupChatListPage = 1
state.groupChatListPageSize = 10 state.groupChatListPageSize = 10
getDepPoisUser() getDepPoisUser()
// getMyFriends()
getUserGroupChatList() getUserGroupChatList()
state.addFriendList = []
}) })
} }
// //
@ -872,32 +599,6 @@ const changeGroupChatListSearch = (value) => {
state.groupChatListSearchGroupName = value.groupName state.groupChatListSearchGroupName = value.groupName
} }
} }
//
const changeMyFriendListSearch = (value) => {
console.log(value, 'value')
if (!value.myFriendname?.trim()) {
state.myFriendListSearchName = ''
} else {
state.myFriendListSearchName = value.myFriendname
}
}
const changeAddFriendSearch = (value) => {
console.log(11)
if (value.friendName?.trim()) {
state.myFriendListSearchName = ''
//
let params = {
name: value.friendName
}
// let url = '/api/v1/contact/friend/search'
GetFriendList(params).then((res) => {
// console.log(res)
if (res.code === 200) {
state.addFriendList = res.data?.user_list || []
}
})
}
}
// //
const getUserGroupChatList = () => { const getUserGroupChatList = () => {
let params = { let params = {
@ -927,19 +628,6 @@ const handleGroupChatListPaginationSize = (value) => {
state.groupChatListPage = 1 state.groupChatListPage = 1
getUserGroupChatList() getUserGroupChatList()
} }
//
const handleMyFriendListPagination = (value) => {
console.log(value, 'value')
state.myFriendListPage = value
getMyFriends()
}
//
const handleMyFriendListPaginationSize = (value) => {
console.log(value, 'value')
state.myFriendListPageSize = value
state.myFriendListPage = 1
getMyFriends()
}
// //
const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_id, res) => { const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_id, res) => {
console.log(searchText, searchResultKey, talk_type, receiver_id) console.log(searchText, searchResultKey, talk_type, receiver_id)
@ -1078,7 +766,7 @@ const handleEnterSearchResultChat = () => {
<n-dropdown <n-dropdown
trigger="click" trigger="click"
:options="state.chatSearchOptions" :options="state.chatSearchOptions"
style="width: 248px; height: 677px" style="width: 248px; height: 677px;"
:show="state.showSearchDropdown" :show="state.showSearchDropdown"
@clickoutside="state.showSearchDropdown = false" @clickoutside="state.showSearchDropdown = false"
> >
@ -1086,7 +774,7 @@ const handleEnterSearchResultChat = () => {
placeholder="搜索好友 / 群聊" placeholder="搜索好友 / 群聊"
v-model:value.trim="searchKeyword" v-model:value.trim="searchKeyword"
clearable clearable
style="width: 78%" style="width: 78%;"
@click="state.showSearchDropdown = true" @click="state.showSearchDropdown = true"
> >
<!-- <template #prefix> <!-- <template #prefix>
@ -1100,14 +788,11 @@ const handleEnterSearchResultChat = () => {
</template> </template>
</n-button> --> </n-button> -->
<img <img
style="width: 19px; height: 20px; cursor: pointer" style="width: 19px; height: 20px; cursor: pointer;"
src="@/assets/image/chatList/addressBook.png" src="@/assets/image/chatList/addressBook.png"
alt="" alt=""
@click="showAddressBookModal" @click="showAddressBookModal"
/> />
<!-- <n-dropdown :options="option" @select="handleSelect">
<n-button> <n-icon :component="AddOne" /></n-button>
</n-dropdown> -->
</header> </header>
<!-- 置顶栏目 --> <!-- 置顶栏目 -->
@ -1151,32 +836,23 @@ const handleEnterSearchResultChat = () => {
<main id="talk-session-list" class="el-main me-scrollbar me-scrollbar-thumb"> <main id="talk-session-list" class="el-main me-scrollbar me-scrollbar-thumb">
<template v-if="loadStatus == 2"><Skeleton /></template> <template v-if="loadStatus == 2"><Skeleton /></template>
<template v-else> <template v-else>
<n-virtual-list :item-size="64" :items="items"> <TalkItem
<template #default="{ item }"> v-for="item in items"
<TalkItem :key="item.index_name"
:key="item.index_name + item.unread_num" :data="item"
:data="item" :avatar="item.avatar"
:avatar="item.avatar" :username="item.remark || item.name"
:username="item.remark || item.name" :active="item.index_name == indexName"
:active="item.index_name == indexName" @tab-talk="onTabTalk"
@tab-talk="onTabTalk" @top-talk="onToTopTalk"
@top-talk="onToTopTalk" @contextmenu.prevent="onContextMenuTalk($event, item)"
@contextmenu.prevent="onContextMenuTalk($event, item)" />
/>
</template>
</n-virtual-list>
</template> </template>
</main> </main>
</section> </section>
<GroupLaunch v-if="isShowGroup" @close="isShowGroup = false" @on-submit="onReload" /> <GroupLaunch v-if="isShowGroup" @close="isShowGroup = false" @on-submit="onReload" />
<UserCardModal
v-model:show="state.userInfo.isShowUserCardModal"
v-model:uid="(state.userInfo as any).user_id"
:euid="(state.userInfo as any).erp_user_id"
@update:send="closeAddFriendModal"
/>
<customModal <customModal
v-model:show="state.isShowAddressBookModal" v-model:show="state.isShowAddressBookModal"
title="通讯录" title="通讯录"
@ -1188,17 +864,13 @@ const handleEnterSearchResultChat = () => {
> >
<template #content> <template #content>
<div class="custom-modal-content"> <div class="custom-modal-content">
<n-card style="padding: 0 12px"> <n-card style="padding: 0 12px;">
<n-tabs <n-tabs
type="line" type="line"
@update:value="handleAddressBookTabChange" @update:value="handleAddressBookTabChange"
tab-style="font-size: 16px; font-weight: 600;color: #8B8B8B;" tab-style="font-size: 16px; font-weight: 600;color: #8B8B8B;"
> >
<!-- <n-tab name="employeeAddressBook">组织架构</n-tab>
<n-tab name="employeeAddressBook">我的好友</n-tab> -->
<!-- <n-tab name="employeeAddressBook">组织架构</n-tab> -->
<n-tab name="employeeAddressBook">员工通讯录</n-tab> <n-tab name="employeeAddressBook">员工通讯录</n-tab>
<!-- <n-tab name="myFriend">我的好友</n-tab> -->
<n-tab name="groupChatList">群聊列表</n-tab> <n-tab name="groupChatList">群聊列表</n-tab>
</n-tabs> </n-tabs>
<xSearchForm <xSearchForm
@ -1215,24 +887,10 @@ const handleEnterSearchResultChat = () => {
@change="changeGroupChatListSearch" @change="changeGroupChatListSearch"
:cols="3" :cols="3"
></xSearchForm> ></xSearchForm>
<xSearchForm
v-if="state.addressBookCurrentTab == 'myFriend'"
:search-config="state.myFriendSearchConfig"
customInputPlaceholder="请输入好友名称"
@change="changeMyFriendListSearch"
:cols="3"
></xSearchForm>
<p
v-if="state.addressBookCurrentTab === 'employeeAddressBook'"
style="transform: translateY(-10px)"
>
{{ state.company_name }}
</p>
<div <div
class="addressBook-content" class="addressBook-content"
v-if="state.addressBookCurrentTab == 'employeeAddressBook'" v-if="state.addressBookCurrentTab == 'employeeAddressBook'"
> >
<!-- 隐藏组织架构树 v-if="!state.addressBookSearchNickName && 0"-->
<div class="addressBook-tree" v-if="!state.addressBookSearchNickName"> <div class="addressBook-tree" v-if="!state.addressBookSearchNickName">
<fl-tree <fl-tree
:data="state.treeData" :data="state.treeData"
@ -1268,36 +926,6 @@ const handleEnterSearchResultChat = () => {
</div> </div>
</div> </div>
</div> </div>
<!-- 我的好友 -->
<div class="groupChatList-content" v-if="state.addressBookCurrentTab == 'myFriend'">
<div class="groupChatList-table">
<xNDataTable
:columns="state.myFriendListColumns"
:data="state.myFriendListData"
:style="{
height: '523px',
width: '1148px'
}"
flex-height
></xNDataTable>
<div class="groupChatList-pagination">
<n-pagination
v-model:page="state.myFriendListPage"
v-model:page-size="state.myFriendListPageSize"
:item-count="state.myFriendListTotal"
show-quick-jumper
show-size-picker
:page-sizes="[10, 20, 50]"
:on-update:page="handleMyFriendListPagination"
:on-update:page-size="handleMyFriendListPaginationSize"
>
<template #prefix="{ itemCount }"> {{ itemCount }} 条记录 </template>
</n-pagination>
</div>
</div>
</div>
<div class="groupChatList-content" v-if="state.addressBookCurrentTab == 'groupChatList'"> <div class="groupChatList-content" v-if="state.addressBookCurrentTab == 'groupChatList'">
<div class="groupChatList-table"> <div class="groupChatList-table">
<xNDataTable <xNDataTable
@ -1329,41 +957,6 @@ const handleEnterSearchResultChat = () => {
</div> </div>
</template> </template>
</customModal> </customModal>
<customModal
v-model:show="state.isShowAddFriendModal"
title="添加好友"
:style="state.customModalStyle"
:customCloseBtn="true"
:closable="false"
:customCloseEvent="true"
@customCloseModal="closeAddFriendModal"
>
<template #content>
<div class="custom-modal-content">
<n-card style="padding: 0 12px">
<xSearchForm
:search-config="state.addFriendSearchConfig"
customInputPlaceholder="请输入姓名"
@change="changeAddFriendSearch"
:cols="3"
></xSearchForm>
<div class="groupChatList-content">
<div class="groupChatList-table">
<xNDataTable
:columns="state.addFriendListColumns"
:data="state.addFriendList"
:style="{
height: '523px',
width: '1148px'
}"
flex-height
></xNDataTable>
</div>
</div>
</n-card>
</div>
</template>
</customModal>
<customModal <customModal
v-model:show="state.isShowSearchRecordModal" v-model:show="state.isShowSearchRecordModal"
@ -1376,7 +969,7 @@ const handleEnterSearchResultChat = () => {
> >
<template #content> <template #content>
<div class="search-record-modal-content"> <div class="search-record-modal-content">
<n-card style="padding: 0 12px"> <n-card style="padding: 0 12px;">
<div class="search-record-input"> <div class="search-record-input">
<span class="search-record-input-title">搜索</span> <span class="search-record-input-title">搜索</span>
<n-input <n-input

View File

@ -31,8 +31,7 @@ const onSingleForward = () => {
} }
const onMultiDelete = () => { const onMultiDelete = () => {
if(dialogueStore.selectItems.length>0){ confirmBox({
confirmBox({
content:'确定删除聊天记录', content:'确定删除聊天记录',
confirmText:'删除' confirmText:'删除'
}).then(()=>{ }).then(()=>{
@ -43,10 +42,6 @@ if (!msgIds.length) return
dialogueStore.ApiDeleteRecord(msgIds) dialogueStore.ApiDeleteRecord(msgIds)
}) })
}else{
window['$message'].warning('请选择聊天记录')
}
// //
} }
@ -64,6 +59,7 @@ const onContactModal = (data: { receiver_id: number; talk_type: number }[]) => {
group_ids.push(o.receiver_id) group_ids.push(o.receiver_id)
} }
} }
console.log('user_ids',user_ids)
dialogueStore.ApiForwardRecord({ dialogueStore.ApiForwardRecord({
mode: forwardMode.value, mode: forwardMode.value,
message_ids: msg_ids, message_ids: msg_ids,

View File

@ -3,7 +3,7 @@ import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui' import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next' import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
import { bus } from '@/utils/event-bus' import { bus } from '@/utils/event-bus'
import { useDialogueStore, useTalkStore } from '@/store' import { useDialogueStore } from '@/store'
import { formatTime, parseTime } from '@/utils/datetime' import { formatTime, parseTime } from '@/utils/datetime'
import { clipboard, htmlDecode, clipboardImage } from '@/utils/common' import { clipboard, htmlDecode, clipboardImage } from '@/utils/common'
import { downloadImage } from '@/utils/functions' import { downloadImage } from '@/utils/functions'
@ -19,11 +19,8 @@ import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js' import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
import { confirmBox } from '@/components/confirm-box/service.js' import { confirmBox } from '@/components/confirm-box/service.js'
import ws from '@/connect' import ws from '@/connect'
import { useRouter } from 'vue-router'
import avatarModule from '@/components/avatar-module/index.vue' import avatarModule from '@/components/avatar-module/index.vue'
const router = useRouter()
// //
interface ReadStatus { interface ReadStatus {
msg_ids: string[] msg_ids: string[]
@ -85,32 +82,15 @@ const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDow
) )
const uploadsStore = useUploadsStore() const uploadsStore = useUploadsStore()
const { useMessage } = useUtil() const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu, isOneMonthBefore } = useMenu() const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
const { showUserInfoModal } = useInject() const { showUserInfoModal } = useInject()
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
const userStore = useUserStore() const userStore = useUserStore()
const talkStore = useTalkStore()
// const showUserInfoModal = (uid: number) => { // const showUserInfoModal = (uid: number) => {
// userStore.getUserInfo(uid) // userStore.getUserInfo(uid)
// } // }
// //
const skipBottom = ref(false) const skipBottom = ref(false)
const goToMessage = (result) => {
const talk_type = props.talk_type
const receiver_id = props.receiver_id
dialogueStore.specifiedMsg = encodeURIComponent(
JSON.stringify({
talk_type,
receiver_id,
msg_id: result.msg_id,
cursor: result.sequence - 15 > 0 ? result.sequence - 15 : 0,
direction: 'down',
sort_sequence: 'asc',
create_time: result.created_at
})
)
talkStore.toTalk(talk_type, receiver_id, router)
}
// //
const isShowTalkTime = (index: number, datetime: string) => { const isShowTalkTime = (index: number, datetime: string) => {
if (datetime == undefined) { if (datetime == undefined) {
@ -204,7 +184,7 @@ const onCopyText = (data: ITalkRecord) => {
return clipboard(htmlDecode(data.extra.content), () => useMessage.success('复制成功')) return clipboard(htmlDecode(data.extra.content), () => useMessage.success('复制成功'))
} }
} }
console.log('data.extra?.url', data.extra?.url)
if (data.extra?.url) { if (data.extra?.url) {
return clipboardImage(data.extra.url, () => { return clipboardImage(data.extra.url, () => {
useMessage.success('复制成功') useMessage.success('复制成功')
@ -351,10 +331,6 @@ const onContextMenuHandle = (key: string) => {
const onRowClick = (item: ITalkRecord) => { const onRowClick = (item: ITalkRecord) => {
if (dialogueStore.isOpenMultiSelect) { if (dialogueStore.isOpenMultiSelect) {
if (!isOneMonthBefore(item.created_at.split(' ')[0])) {
return useMessage.info('只支持转发近一个月内的消息')
}
console.log('item.msg_type', item.msg_type)
if (ForwardableMessageType.includes(item.msg_type)) { if (ForwardableMessageType.includes(item.msg_type)) {
item.isCheck = !item.isCheck item.isCheck = !item.isCheck
} else { } else {
@ -372,7 +348,6 @@ let noRefreshTimer: number | null = null
watch( watch(
() => props, () => props,
async (newProps) => { async (newProps) => {
console.log('监听props',newProps)
await nextTick() await nextTick()
// //
const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}` const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}`
@ -387,7 +362,7 @@ watch(
} }
currentSessionKey.value = newSessionKey currentSessionKey.value = newSessionKey
} }
let specialParams = undefined let specialParams = undefined
if (newProps.specifiedMsg) { if (newProps.specifiedMsg) {
try { try {
@ -401,7 +376,7 @@ watch(
} }
} catch (e) {} } catch (e) {}
} }
// //
if (dialogueStore.noRefreshRecords) { if (dialogueStore.noRefreshRecords) {
// //
@ -415,7 +390,7 @@ watch(
}, 3000) }, 3000)
return return
} }
console.log('fsd付大夫')
onLoad( onLoad(
{ {
receiver_id: newProps.receiver_id, receiver_id: newProps.receiver_id,
@ -425,7 +400,7 @@ watch(
specialParams ? { specifiedMsg: specialParams } : undefined specialParams ? { specifiedMsg: specialParams } : undefined
) )
}, },
{ deep: true,immediate:true } { immediate: true, deep: true }
) )
// onMounted(() => { // onMounted(() => {
@ -556,6 +531,7 @@ const checkVisibleOutElements = () => {
}) })
if (waitDoCheck.length > 0) { if (waitDoCheck.length > 0) {
waitDoCheck.forEach((doCheckItem) => { waitDoCheck.forEach((doCheckItem) => {
console.error('====组装了新版已读回执参数需要发送socket=====', doCheckItem)
ws.emit('im.message.listen.read', doCheckItem) ws.emit('im.message.listen.read', doCheckItem)
}) })
} }
@ -615,6 +591,7 @@ watch(
if (observer) { if (observer) {
observer.disconnect() observer.disconnect()
} }
// //
const options = { const options = {
root: null, root: null,
@ -622,6 +599,7 @@ watch(
rootMargin: '50px 0px' rootMargin: '50px 0px'
} }
observer = new IntersectionObserver(handleIntersection, options) observer = new IntersectionObserver(handleIntersection, options)
// //
const messageElements = document.querySelectorAll('.message-item') const messageElements = document.querySelectorAll('.message-item')
messageElements.forEach((el) => { messageElements.forEach((el) => {
@ -789,7 +767,7 @@ const onCustomSkipBottomEvent = () => {
<div class="load-toolbar pointer"> <div class="load-toolbar pointer">
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span> <span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
<span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span> <span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span>
<span v-else-if="loadConfig.status == 2 || loadConfig.status == 3" class="no-more"> 没有更多消息了 </span> <span v-else class="no-more"> 没有更多消息了 </span>
</div> </div>
<div <div
@ -837,10 +815,8 @@ const onCustomSkipBottomEvent = () => {
> >
<!-- 多选按钮 --> <!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0"> <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<!-- 近一个月外的消息多选框禁用 {{ item }} -->
<n-checkbox <n-checkbox
size="small" size="small"
:disabled="!isOneMonthBefore(item.created_at.split(' ')[0])"
:checked="item.isCheck" :checked="item.isCheck"
@update:checked="item.isCheck = !item.isCheck" @update:checked="item.isCheck = !item.isCheck"
/> />
@ -878,14 +854,7 @@ const onCustomSkipBottomEvent = () => {
</div> </div>
<div <div
class="talk-content" class="talk-content"
:class="{ :class="{ pointer: dialogueStore.isOpenMultiSelect }"
pointer:
dialogueStore.isOpenMultiSelect &&
isOneMonthBefore(item.created_at.split(' ')[0]),
'cursor-not-allowed':
dialogueStore.isOpenMultiSelect &&
!isOneMonthBefore(item.created_at.split(' ')[0])
}"
@click="onRowClick(item)" @click="onRowClick(item)"
> >
<component <component
@ -902,7 +871,7 @@ const onCustomSkipBottomEvent = () => {
" "
class="mr-10px" class="mr-10px"
> >
<n-button text style="font-size: 20px" @click="retry(item)"> <n-button text style="font-size: 20px;" @click="retry(item)">
<n-icon color="#CF3050"> <n-icon color="#CF3050">
<ExclamationCircleFilled /> <ExclamationCircleFilled />
</n-icon> </n-icon>
@ -926,11 +895,11 @@ const onCustomSkipBottomEvent = () => {
<n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" /> <n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" />
</div> --> </div> -->
</div> </div>
<!-- @click="onJumpMessage(item.extra?.reply?.msg_id)" -->
<div <div
v-if="item.extra.reply" v-if="item.extra.reply"
class="talk-reply pointer" class="talk-reply pointer"
@click="goToMessage(item.extra?.reply)" @click="onJumpMessage(item.extra?.reply?.msg_id)"
> >
<n-icon :component="ToTop" size="14" class="icon-top" /> <n-icon :component="ToTop" size="14" class="icon-top" />
<span class="ellipsis"> <span class="ellipsis">
@ -947,14 +916,14 @@ const onCustomSkipBottomEvent = () => {
<n-popover <n-popover
trigger="click" trigger="click"
placement="bottom-end" placement="bottom-end"
style="height: 382px; padding: 0" style="height: 382px; padding: 0;"
v-if="props.talk_type === 2" v-if="props.talk_type === 2"
> >
<template #trigger> <template #trigger>
<span <span
v-if="props.talk_type === 2" v-if="props.talk_type === 2"
@click="toShowMessageReadDetail(item)" @click="toShowMessageReadDetail(item)"
style="cursor: pointer" style="cursor: pointer;"
> >
已读 ({{ item?.read_total_num || 0 }}/{{ 已读 ({{ item?.read_total_num || 0 }}/{{
props.num - 1 > 0 ? props.num - 1 : 0 props.num - 1 > 0 ? props.num - 1 : 0
@ -976,12 +945,11 @@ const onCustomSkipBottomEvent = () => {
</n-tab> </n-tab>
</n-tabs> </n-tabs>
<div class="talk-read-list"> <div class="talk-read-list">
<n-infinite-scroll style="height: 340px" @load="loadMoreReadListDetail"> <n-infinite-scroll style="height: 340px;" @load="loadMoreReadListDetail">
<div <div
class="talk-read-list-item" class="talk-read-list-item"
v-for="( v-for="(talkReadDetailItem,
talkReadDetailItem, talkReadDetailIndex talkReadDetailIndex) in state.talkReadListDetail"
) in state.talkReadListDetail"
:key="talkReadDetailIndex" :key="talkReadDetailIndex"
> >
<avatarModule <avatarModule
@ -1001,10 +969,10 @@ const onCustomSkipBottomEvent = () => {
}" }"
></avatarModule> ></avatarModule>
<div class="talk-read-list-item-info"> <div class="talk-read-list-item-info">
<span style="font-size: 12px; font-weight: 600; line-height: 17px">{{ <span style="font-size: 12px; font-weight: 600; line-height: 17px;">{{
talkReadDetailItem.nickName talkReadDetailItem.nickName
}}</span> }}</span>
<span style="font-size: 12px; color: #999; line-height: 14px">{{ <span style="font-size: 12px; color: #999; line-height: 14px;">{{
talkReadDetailItem.jobNum talkReadDetailItem.jobNum
}}</span> }}</span>
</div> </div>
@ -1036,7 +1004,7 @@ const onCustomSkipBottomEvent = () => {
:show="dropdown.show" :show="dropdown.show"
:x="dropdown.x" :x="dropdown.x"
:y="dropdown.y" :y="dropdown.y"
style="width: 142px" style="width: 142px;"
:options="dropdown.options" :options="dropdown.options"
@select="onContextMenuHandle" @select="onContextMenuHandle"
@clickoutside="closeDropdownMenu" @clickoutside="closeDropdownMenu"

View File

@ -16,7 +16,7 @@ import Editor from '@/components/editor/Editor.vue'
import MultiSelectFooter from './MultiSelectFooter.vue' import MultiSelectFooter from './MultiSelectFooter.vue'
import HistoryRecord from '@/components/talk/HistoryRecord.vue' import HistoryRecord from '@/components/talk/HistoryRecord.vue'
import {scrollToBottom} from '@/utils/dom.ts' import {scrollToBottom} from '@/utils/dom.ts'
import CustomEditor from '@/components/editor/CustomEditor.vue' import CustomEditor from '@/components/editor/CustomEditor.vue'
const userStore = useUserStore() const userStore = useUserStore()
const talkStore = useTalkStore() const talkStore = useTalkStore()
const editorStore = useEditorStore() const editorStore = useEditorStore()
@ -101,25 +101,19 @@ const onSendImageEvent = ({ data, callBack }) => {
// //
const onSendVideoEvent = async ({ data }) => { const onSendVideoEvent = async ({ data }) => {
// //
let videoPreview = null // let resp = await getVideoImage(data)
try {
videoPreview = await getVideoImage(data)
} catch (error) {
console.error('获取视频封面失败:', error)
}
// ID // ID
const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}` const uploadId = `video-${Date.now()}-${Math.floor(Math.random() * 1000)}`
// //
const tempMessage = { const tempMessage = {
msg_id: uploadId, msg_id: uploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
sequence: Date.now(), sequence: Date.now(),
talk_type: props.talk_type, talk_type: props.talk_type,
msg_type: 5, // msg_type: 5, //
user_id: props.uid, user_id: props.uid,
receiver_id: props.receiver_id, receiver_id: props.receiver_id,
@ -129,7 +123,7 @@ const onSendVideoEvent = async ({ data }) => {
content: '', content: '',
created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'), created_at: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}'),
extra: { extra: {
url: videoPreview ? URL.createObjectURL(data) : '', // 使URL url: '',
size: data.size, size: data.size,
is_uploading: true, is_uploading: true,
upload_id: uploadId, upload_id: uploadId,
@ -140,8 +134,8 @@ const onSendVideoEvent = async ({ data }) => {
float: 'right' // float: 'right' //
} }
// 使 //
dialogueStore.addUploadTask(tempMessage) dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{ nextTick(()=>{
scrollToBottom() scrollToBottom()
}) })
@ -154,7 +148,8 @@ const onSendVideoEvent = async ({ data }) => {
dialogueStore.updateUploadProgress(uploadId, percentage) dialogueStore.updateUploadProgress(uploadId, percentage)
}, },
async () => { async () => {
dialogueStore.batchDelDialogueRecord([uploadId]) dialogueStore.batchDelDialogueRecord([uploadId])
} }
) )
} }
@ -166,12 +161,13 @@ const onSendCodeEvent = ({ data, callBack }) => {
// //
const onSendFileEvent = ({ data }) => { const onSendFileEvent = ({ data }) => {
let maxsize = 200 * 1024 * 1024
if (data.size > maxsize) {
return window['$message'].warning('上传文件不能超过100M!')
}
const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}` const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}`
const tempMessage = { const tempMessage = {
msg_id: clientUploadId, msg_id: clientUploadId,
insert_sequence: dialogueStore.records.length > 0
? dialogueStore.records[dialogueStore.records.length-1].sequence
: 0,
sequence: Date.now(), sequence: Date.now(),
talk_type: props.talk_type, talk_type: props.talk_type,
msg_type: 6, msg_type: 6,
@ -193,7 +189,7 @@ const onSendFileEvent = ({ data }) => {
}, },
float: 'right' float: 'right'
} }
dialogueStore.addUploadTask(tempMessage) dialogueStore.addDialogueRecord(tempMessage)
nextTick(()=>{ nextTick(()=>{
scrollToBottom() scrollToBottom()
}) })
@ -202,8 +198,8 @@ const onSendFileEvent = ({ data }) => {
dialogueStore.updateUploadProgress(clientUploadId, percentage) dialogueStore.updateUploadProgress(clientUploadId, percentage)
}, },
async () => { async () => {
// removeUploadTask dialogueStore.batchDelDialogueRecord([clientUploadId])
// records
} }
) )
} }

View File

@ -1,5 +1,4 @@
import { reactive } from 'vue' import { reactive } from 'vue'
import dayjs from 'dayjs'
import { useDialogueStore } from '@/store/modules/dialogue.js' import { useDialogueStore } from '@/store/modules/dialogue.js'
interface IDropdown { interface IDropdown {
@ -10,34 +9,16 @@ interface IDropdown {
item: any item: any
} }
const isRevoke = (uid: number, item: any): boolean => { const isRevoke = (uid: any, item: any): boolean => {
// 不是自己发的消息不能撤回 if (uid != item.user_id) {
if (uid !== item.user_id) { return false
return false;
} }
// 检查消息是否在撤回时间限制内5分钟 const datetime = item.created_at.replace(/-/g, '/')
const messageTime = dayjs(item.created_at);
const now = dayjs(); const time = new Date().getTime() - Date.parse(datetime)
const diffInMinutes = now.diff(messageTime, 'minute');
return diffInMinutes <= 5; return Math.floor(time / 1000 / 60) <= 2
}
// 判断是否可以添加撤回选项的函数
const canAddRevokeOption = (uid: number, item: any, isManager: boolean): boolean => {
// 单聊情况:自己发的且在时间限制内
if (item.talk_type === 1) {
return isRevoke(uid, item) && item.float === 'right';
}
// 群聊情况
else if (item.talk_type === 2) {
// 管理员可以撤回任何消息
if (isManager) {
return true;
}
// 普通成员只能撤回自己的且在时间限制内的消息
return isRevoke(uid, item) && item.float === 'right';
}
return false;
} }
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
export function useMenu() { export function useMenu() {
@ -48,40 +29,32 @@ export function useMenu() {
y: 0, y: 0,
item: {} item: {}
}) })
// 判断时间是否超过一个月
function isOneMonthBefore(date) {
const oneMonthAgo = new Date()
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
const inputDate = new Date(date)
return !(inputDate <= oneMonthAgo)
}
const showDropdownMenu = (e: any, uid: number, item: any) => { const showDropdownMenu = (e: any, uid: number, item: any) => {
// dropdown.item = Object.assign({}, item) // dropdown.item = Object.assign({}, item)
dropdown.item = item dropdown.item = item
dropdown.item.is_self_action = true
dropdown.options = [] dropdown.options = []
if ([4].includes(item.msg_type)) { if ([4].includes(item.msg_type)) {
if (item.is_convert_text === 1) { if(item.is_convert_text === 1){
dropdown.options.push({ label: '关闭转文字', key: 'closeConvertText' }) dropdown.options.push({ label: '关闭转文字', key: 'closeConvertText' })
} else { }else{
dropdown.options.push({ label: '转文字', key: 'convertText' }) dropdown.options.push({ label: '转文字', key: 'convertText' })
} }
} }
if ([1, 3].includes(item.msg_type)) { if ([1, 3].includes(item.msg_type)) {
dropdown.options.push({ label: '复制', key: 'copy' }) dropdown.options.push({ label: '复制', key: 'copy' })
} }
if (isOneMonthBefore(new Date(item.created_at.split(' ')[0]))) { dropdown.options.push({ label: '多选', key: 'multiSelect' })
// 根据时间判断只有近一个月内的消息才能支持多选
dropdown.options.push({ label: '多选', key: 'multiSelect' })
}
dropdown.options.push({ label: '引用', key: 'quote' }) dropdown.options.push({ label: '引用', key: 'quote' })
if (canAddRevokeOption(uid, item, (dialogueStore.groupInfo as any).is_manager)) { if (isRevoke(uid, item)|| (dialogueStore.groupInfo as any).is_manager) {
dropdown.options.push({ label: '撤回', key: 'revoke' }); dropdown.options.push({ label: `撤回`, key: 'revoke' })
} }
dropdown.options.push({ label: '删除', key: 'delete' }) dropdown.options.push({ label: '删除', key: 'delete' })
// if ([3, 4, 5].includes(item.msg_type)) { // if ([3, 4, 5].includes(item.msg_type)) {
// dropdown.options.push({ label: '下载', key: 'download' }) // dropdown.options.push({ label: '下载', key: 'download' })
// } // }
@ -89,6 +62,7 @@ export function useMenu() {
// if ([3].includes(item.msg_type)) { // if ([3].includes(item.msg_type)) {
// dropdown.options.push({ label: '收藏', key: 'collect' }) // dropdown.options.push({ label: '收藏', key: 'collect' })
// } // }
dropdown.x = e.clientX dropdown.x = e.clientX
dropdown.y = e.clientY dropdown.y = e.clientY
@ -100,5 +74,5 @@ export function useMenu() {
dropdown.item = {} dropdown.item = {}
} }
return { dropdown, showDropdownMenu, closeDropdownMenu, isOneMonthBefore } return { dropdown, showDropdownMenu, closeDropdownMenu }
} }

View File

@ -57,6 +57,7 @@ const config = {
}, },
documentType, documentType,
editorConfig: { editorConfig: {
mode: 'view', mode: 'view',
lang: 'zh-CN', lang: 'zh-CN',
user: { user: {