chat-pc/src/views/message/inner/panel/PanelContent.vue

605 lines
15 KiB
Vue
Raw Normal View History

2024-12-24 08:14:21 +00:00
<script lang="ts" setup>
import { watch, onMounted, ref } from 'vue'
import { NDropdown, NCheckbox } from 'naive-ui'
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
import { bus } from '@/utils/event-bus'
import { useDialogueStore } from '@/store'
import { formatTime, parseTime } from '@/utils/datetime'
import { clipboard, htmlDecode, clipboardImage } from '@/utils/common'
import { downloadImage } from '@/utils/functions'
import { MessageComponents, ForwardableMessageType } from '@/constant/message'
import { useMenu } from './menu'
import SkipBottom from './SkipBottom.vue'
import { ITalkRecord } from '@/types/chat'
import { EditorConst } from '@/constant/event-bus'
import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue';
import { useUserStore } from '@/store'
import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
2024-12-24 08:14:21 +00:00
const props = defineProps({
uid: {
type: Number,
default: 0
},
talk_type: {
type: Number,
default: 0
},
receiver_id: {
type: Number,
default: 0
},
index_name: {
type: String,
default: ''
}
})
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid)
const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
const { showUserInfoModal } = useInject()
const dialogueStore = useDialogueStore()
const userStore = useUserStore()
// const showUserInfoModal = (uid: number) => {
// userStore.getUserInfo(uid)
// }
2024-12-24 08:14:21 +00:00
watch(() => records, (newValue, oldValue) => {
console.log(newValue);
}, { deep: true, immediate: true })
2024-12-24 08:14:21 +00:00
// 置底按钮
const skipBottom = ref(false)
setTimeout(() => {
console.log(records.value, 'records.value');
}, 1000)
2024-12-24 08:14:21 +00:00
// 是否显示消息时间
const isShowTalkTime = (index: number, datetime: string) => {
if (datetime == undefined) {
return false
}
if (records.value[index].is_revoke == 1) {
return false
}
datetime = datetime.replace(/-/g, '/')
let time = Math.floor(Date.parse(datetime) / 1000)
let currTime = Math.floor(new Date().getTime() / 1000)
// 当前时间5分钟内时间不显示
if (currTime - time < 300) return false
// 判断是否是最后一条消息,最后一条消息默认显示时间
if (index == records.value.length - 1) {
return true
}
let nextDate = records.value[index + 1].created_at.replace(/-/g, '/')
return !(
parseTime(new Date(datetime), '{y}-{m}-{d} {h}:{i}') ==
parseTime(new Date(nextDate), '{y}-{m}-{d} {h}:{i}')
)
}
// 窗口滚动事件
const onPanelScroll = (e: any) => {
if (e.target.scrollTop == 0) {
onRefreshLoad()
}
const height = e.target.scrollTop + e.target.clientHeight
skipBottom.value = height < e.target.scrollHeight - 200
if (!skipBottom.value && dialogueStore.unreadBubble) {
dialogueStore.setUnreadBubble(0)
}
// 检测是否到达底部
if (skipBottom.value == false) {
let len = dialogueStore.records.length
if (len > 100) {
// 为了优化数据过多页面卡顿页面最多只显示100条数据
// 目前没有用虚拟列表只能这么干
dialogueStore.records.splice(0, len - 100)
let minSequence = 0
dialogueStore.records.forEach((item: ITalkRecord) => {
if (minSequence == 0 || item.sequence < minSequence) {
minSequence = item.sequence
}
})
loadConfig.cursor = minSequence
loadConfig.status = 1
}
}
}
// 复制文本信息
const onCopyText = (data: ITalkRecord) => {
if (data.msg_type == 1) {
if (data.extra.content && data.extra.content.length > 0) {
return clipboard(htmlDecode(data.extra.content), () => useMessage.success('复制成功'))
}
}
if (data.extra?.url) {
return clipboardImage(data.extra.url, () => {
useMessage.success('复制成功')
})
}
}
// 删除对话消息
const onDeleteTalk = (data: ITalkRecord) => {
dialogueStore.ApiDeleteRecord([data.msg_id])
}
// 撤销对话消息
const onRevokeTalk = (data: ITalkRecord) => {
dialogueStore.ApiRevokeRecord(data.msg_id)
}
// 多选事件
const onMultiSelect = (data: ITalkRecord) => {
dialogueStore.updateDialogueRecord({
msg_id: data.msg_id,
isCheck: true
})
dialogueStore.isOpenMultiSelect = true
}
const onDownloadFile = (data: ITalkRecord) => {
if (data.msg_type == 3) {
return downloadImage(data.extra.url, `${data.msg_id}.${data.extra.suffix}`)
}
if (data.msg_type == 4) {
return useMessage.info('音频暂不支持下载!')
}
return useMessage.info('视频暂不支持下载!')
}
const onQuoteMessage = (data: ITalkRecord) => {
let item = {
id: data.msg_id,
title: `${data.nickname} ${data.created_at}`,
describe: '',
image: ''
}
switch (data.msg_type) {
case 1:
item.describe = data?.extra?.content
break // 文本消息
case 2:
item.describe = '[代码消息]'
break // 代码消息
case 3:
item.image = data.extra.url
break // 图片文件
case 4:
item.describe = '[语音文件]'
break // 语音文件
case 5:
item.describe = '[视频文件]'
break // 视频文件
case 6:
item.describe = '[其它文件]'
break // 其它文件
case 7:
item.describe = '[位置消息]'
break // 位置消息
case 8:
item.describe = '[名片消息]'
break // 名片消息
case 9:
item.describe = '[转发消息]'
break // 转发消息
case 10:
item.describe = '[登录消息]'
break // 登录消息
case 11:
item.describe = '[投票消息]'
break // 投票消息
case 12:
item.describe = '[图文消息]'
break // 图文消息
}
bus.emit('editor:quote', item)
}
const onCollectImage = (data: ITalkRecord) => {
if (data.msg_type == 3) {
dialogueStore.ApiCollectImage({
msg_id: data.msg_id
})
}
}
const onClickNickname = (data: ITalkRecord) => {
bus.emit(EditorConst.Mention, {
id: data.user_id,
value: data.nickname
})
}
// 会话列表右键显示菜单
const onContextMenu = (e: any, item: ITalkRecord) => {
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
return e.preventDefault()
}
showDropdownMenu(e, props.uid, item)
e.preventDefault()
}
const evnets = {
copy: onCopyText,
revoke: onRevokeTalk,
delete: onDeleteTalk,
multiSelect: onMultiSelect,
download: onDownloadFile,
quote: onQuoteMessage,
collect: onCollectImage
}
// 会话列表右键菜单回调事件
const onContextMenuHandle = (key: string) => {
// 触发事件
evnets[key] && evnets[key](dropdown.item)
closeDropdownMenu()
}
const onRowClick = (item: ITalkRecord) => {
if (dialogueStore.isOpenMultiSelect) {
if (ForwardableMessageType.includes(item.msg_type)) {
item.isCheck = !item.isCheck
} else {
useMessage.info('此类消息不支持转发')
}
}
}
watch(props, () => {
onLoad({ ...props, limit: 30 })
})
onMounted(() => {
onLoad({ ...props, limit: 30 })
})
</script>
<template>
<section class="section">
<div id="imChatPanel" class="me-scrollbar me-scrollbar-thumb talk-container" @scroll="onPanelScroll($event)">
2024-12-24 08:14:21 +00:00
<!-- 数据加载状态栏 -->
<div class="load-toolbar pointer">
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
<span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span>
<span v-else class="no-more"> 没有更多消息了 </span>
</div>
<div class="message-item" v-for="(item, index) in records" :key="item.msg_id" :id="item.msg_id">
2024-12-24 08:14:21 +00:00
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" />
2024-12-24 08:14:21 +00:00
</div>
<!-- 撤回消息 -->
<div v-else-if="item.is_revoke == 1" class="message-box">
<revoke-message :login_uid="uid" :data="item" :user_id="item.user_id" :nickname="item.nickname" :talk_type="item.talk_type"
:datetime="item.created_at" />
2024-12-24 08:14:21 +00:00
</div>
<div v-else class="message-box record-box" :class="{
'direction-rt': item.float == 'right',
'multi-select': dialogueStore.isOpenMultiSelect,
'multi-select-check': item.isCheck
}">
2024-12-24 08:14:21 +00:00
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
2024-12-24 08:14:21 +00:00
</aside>
<!-- 头像信息 -->
2024-12-24 08:14:21 +00:00
<aside class="avatar-column">
<im-avatar class="pointer" :src="item.avatar" :size="42" :username="item.nickname"
@click="showUserInfoModal(item.erp_user_id)" />
2024-12-24 08:14:21 +00:00
</aside>
<!-- 主体信息 -->
<main class="main-column">
<div class="talk-title">
<span class="nickname pointer" v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)">
2024-12-24 08:14:21 +00:00
<span class="at">@</span>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
2024-12-24 08:14:21 +00:00
</div>
<div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }" @click="onRowClick(item)">
2024-12-24 08:14:21 +00:00
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item"
:max-width="true" :source="'panel'" @contextmenu.prevent="onContextMenu($event, item)" />
<div v-if="item.float==='right'&&item.extra.percentage===-1&&item.extra.is_uploading" class="mr-10px"> <n-button text style="font-size: 20px">
<n-icon color="#CF3050">
<ExclamationCircleFilled />
</n-icon>
</n-button></div>
<!-- <div class="talk-tools">
2024-12-24 08:14:21 +00:00
<template v-if="talk_type == 1 && item.float == 'right'">
<loading
theme="outline"
size="19"
fill="#000"
:strokeWidth="1"
class="icon-rotate"
v-show="item.send_status == 1"
/>
<span v-show="item.send_status == 1"> 正在发送... </span>
<span v-show="item.send_status != 1"> 已送达 </span>
2024-12-24 08:14:21 +00:00
</template>
<n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" />
</div> -->
2024-12-24 08:14:21 +00:00
</div>
<div v-if="item.extra.reply" class="talk-reply pointer" @click="onJumpMessage(item.extra?.reply?.msg_id)">
2024-12-24 08:14:21 +00:00
<n-icon :component="ToTop" size="14" class="icon-top" />
<span class="ellipsis">
回复 {{ item.extra?.reply?.nickname }}:
{{ item.extra?.reply?.content }}
</span>
</div>
</main>
</div>
<div class="datetime" v-show="isShowTalkTime(index, item.created_at)">
{{ formatTime(item.created_at) }}
</div>
</div>
</div>
<!-- 置底按钮 -->
<SkipBottom v-model="skipBottom" />
</section>
<!-- 右键菜单 -->
<n-dropdown :show="dropdown.show" :x="dropdown.x" :y="dropdown.y" style="width: 142px;" :options="dropdown.options"
@select="onContextMenuHandle" @clickoutside="closeDropdownMenu" />
2024-12-24 08:14:21 +00:00
</template>
<style lang="less" scoped>
.section {
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
}
.talk-container {
height: 100%;
width: 100%;
box-sizing: border-box;
padding: 10px 15px 30px;
overflow-y: auto;
overflow-x: hidden;
.load-toolbar {
height: 38px;
color: #409eff;
text-align: center;
line-height: 38px;
font-size: 13px;
2024-12-24 08:14:21 +00:00
.no-more {
color: #b9b3b3;
}
}
.message-item {
&.border {
border-radius: 10px;
border: 1px solid var(--im-primary-color);
}
}
.message-box {
width: 100%;
min-height: 30px;
margin-bottom: 5px;
}
.datetime {
height: 30px;
line-height: 30px;
color: #ccc9c9;
font-size: 12px;
text-align: center;
margin: 5px 0;
}
.record-box {
display: flex;
flex-direction: row;
.checkbox-column {
display: flex;
justify-content: center;
width: 35px;
order: 1;
user-select: none;
padding-top: 12px;
}
.avatar-column {
width: 47px;
2024-12-24 08:14:21 +00:00
display: flex;
align-items: center;
order: 2;
user-select: none;
padding-top: 10px;
flex-direction: column;
}
.main-column {
flex: 1 auto;
order: 3;
position: relative;
box-sizing: border-box;
padding: 5px;
overflow: hidden;
min-height: 30px;
.talk-title {
display: flex;
align-items: center;
height: 24px;
margin-bottom: 2px;
font-size: 12px;
user-select: none;
color: #a7a0a0;
opacity: 1;
&.show {
opacity: 1;
}
.nickname {
color: var(--im-text-color);
margin-right: 5px;
font-size: 12px;
2024-12-24 08:14:21 +00:00
.at {
display: none;
}
&:hover {
color: var(--im-primary-color);
.at {
display: inline-block;
}
}
}
span {
transform: scale(0.88);
transform-origin: left center;
}
}
.talk-content {
display: flex;
justify-content: flex-start;
align-items: flex-end;
box-sizing: border-box;
width: 100%;
.talk-tools {
display: flex;
margin: 0 8px;
color: #a79e9e;
font-size: 12px;
user-select: none;
align-items: center;
justify-content: space-around;
.more-tools {
visibility: hidden;
margin-left: 5px;
}
}
}
.talk-reply {
display: flex;
align-items: flex-start;
align-items: center;
width: fit-content;
padding: 4px;
margin-top: 3px;
margin-right: auto;
font-size: 12px;
color: #8f8f8f;
word-break: break-all;
background-color: var(--im-message-left-bg-color);
border-radius: 5px;
max-width: 300px;
overflow: hidden;
user-select: none;
.icon-top {
margin-right: 3px;
}
.ellipsis {
display: -webkit-inline-box;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}
&:hover {
.talk-title {
opacity: 1;
}
.more-tools {
visibility: visible !important;
}
}
}
&.direction-rt {
.avatar-column {
order: 3;
}
.main-column {
order: 2;
.talk-title {
justify-content: flex-end;
span {
transform-origin: right center;
}
}
.talk-content {
flex-direction: row-reverse;
}
.talk-reply {
margin-right: 0;
margin-left: auto;
}
}
}
&.multi-select {
border-radius: 5px;
&:hover,
&.multi-select-check {
background-color: var(--im-active-bg-color);
}
}
}
}
</style>