chat-pc/src/views/message/inner/panel/PanelContent.vue
2024-12-24 16:14:21 +08:00

649 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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'
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()
watch(() => records, (newValue, oldValue) => {
console.log(newValue);
},{deep:true,immediate:true})
// 置底按钮
const skipBottom = ref(false)
// 是否显示消息时间
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)"
>
<!-- 数据加载状态栏 -->
<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"
>
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
<!-- 撤回消息 -->
<div v-else-if="item.is_revoke == 1" class="message-box">
<revoke-message
:login_uid="uid"
:user_id="item.user_id"
:nickname="item.nickname"
:talk_type="item.talk_type"
:datetime="item.created_at"
/>
</div>
<div
v-else
class="message-box record-box"
:class="{
'direction-rt': item.float == 'right',
'multi-select': dialogueStore.isOpenMultiSelect,
'multi-select-check': item.isCheck
}"
>
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column">
<n-checkbox
size="small"
:checked="item.isCheck"
@update:checked="item.isCheck = !item.isCheck"
/>
</aside>
<!-- 头像信息 -->
<aside class="avatar-column">
<im-avatar
class="pointer"
:src="item.avatar"
:size="30"
:username="item.nickname"
@click="showUserInfoModal(item.user_id)"
/>
</aside>
<!-- 主体信息 -->
<main class="main-column">
<div class="talk-title">
<span
class="nickname pointer"
v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)"
>
<span class="at">@</span>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{m}/{d} {h}:{i}') }}</span>
</div>
<div
class="talk-content"
:class="{ pointer: dialogueStore.isOpenMultiSelect }"
@click="onRowClick(item)"
>
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
:max-width="true"
:source="'panel'"
@contextmenu.prevent="onContextMenu($event, item)"
/>
<div class="talk-tools">
<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> -->
</template>
<n-icon
class="more-tools pointer"
:component="MoreThree"
@click="onContextMenu($event, item)"
/>
</div>
</div>
<div
v-if="item.extra.reply"
class="talk-reply pointer"
@click="onJumpMessage(item.extra?.reply?.msg_id)"
>
<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"
:options="dropdown.options"
@select="onContextMenuHandle"
@clickoutside="closeDropdownMenu"
/>
</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;
.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: 35px;
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;
.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>