Compare commits
9 Commits
db8621ec5c
...
f876ee7bbe
Author | SHA1 | Date | |
---|---|---|---|
|
f876ee7bbe | ||
07c3808122 | |||
d0dd83451c | |||
|
7733f88dae | ||
|
a506b4dcc1 | ||
97f05d2c5c | |||
d6782d867c | |||
8d73e0d48b | |||
f9d02d67a6 |
@ -686,13 +686,18 @@ const fileTypeAvatar = (fileType) => {
|
||||
|
||||
const previewPDF = (item) => {
|
||||
console.log(item)
|
||||
if (typeof plus !== 'undefined') {
|
||||
downloadAndOpenFile(item)
|
||||
} else {
|
||||
document.addEventListener('plusready', () => {
|
||||
downloadAndOpenFile(item)
|
||||
})
|
||||
}
|
||||
// if (typeof plus !== 'undefined') {
|
||||
// downloadAndOpenFile(item)
|
||||
// } else {
|
||||
// document.addEventListener('plusready', () => {
|
||||
// downloadAndOpenFile(item)
|
||||
// })
|
||||
// }
|
||||
window.open(
|
||||
`${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`,
|
||||
'_blank',
|
||||
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
|
||||
)
|
||||
}
|
||||
|
||||
const downloadAndOpenFile = (item) => {
|
||||
@ -926,7 +931,6 @@ body:deep(.round-3) {
|
||||
}
|
||||
.condition-each-resultList {
|
||||
.condition-each-resultList-each {
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
.condition-each-result-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -942,9 +946,16 @@ body:deep(.round-3) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 14px 0;
|
||||
padding: 14px 20px;
|
||||
// background-color: #f3f3f3;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(70, 41, 157, 0.1)
|
||||
}
|
||||
|
||||
.attachment-avatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -1126,6 +1137,10 @@ body:deep(.round-3) {
|
||||
.image-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid #46299d;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-image) {
|
||||
|
@ -69,7 +69,43 @@
|
||||
class="text-[12px] font-regular"
|
||||
:text="resultDetail"
|
||||
:searchText="props.searchText"
|
||||
v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 6"
|
||||
/>
|
||||
<div class="message-component-wrapper" v-if="props.searchItem?.msg_type === 3" @click.stop>
|
||||
<component
|
||||
:is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'"
|
||||
:extra="resultDetail"
|
||||
:data="props?.searchItem"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-message-wrapper" v-if="props.searchItem?.msg_type === 6" @click.stop>
|
||||
<div class="condition-each-result-attachments" @click="previewPDF(resultDetail.path)">
|
||||
<div class="attachment-avatar">
|
||||
<img :src="resultDetail?.file_avatar" />
|
||||
</div>
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-info-title">
|
||||
<span class="text-[14px] font-regular">
|
||||
{{ resultDetail?.name }}
|
||||
</span>
|
||||
<span
|
||||
class="text-[14px] font-regular"
|
||||
style="color: #999999; flex-shrink: 0; margin: 0 0 0 20px;"
|
||||
>
|
||||
{{ resultDetail?.dateTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="attachment-sub-info">
|
||||
<span class="text-[12px] font-regular">
|
||||
{{ resultDetail?.typeText }}
|
||||
</span>
|
||||
<span class="text-[12px] font-regular" style="flex-shrink: 0; margin: 0 0 0 20px;">
|
||||
{{ resultDetail?.fileSize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchRecordDetail-fastLocal" v-if="searchRecordDetail">
|
||||
<span>定位到聊天位置</span>
|
||||
</div>
|
||||
@ -85,7 +121,7 @@ import avatarModule from '@/components/avatar-module/index.vue'
|
||||
import { ref, watch, computed, onMounted, onUnmounted, reactive, defineProps } from 'vue'
|
||||
import HighlightText from './highLightText.vue'
|
||||
import { beautifyTime } from '@/utils/datetime'
|
||||
import { ChatMsgTypeMapping } from '@/constant/message'
|
||||
import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message'
|
||||
const props = defineProps({
|
||||
searchItem: Object | Number,
|
||||
searchResultKey: {
|
||||
@ -255,6 +291,8 @@ const resultDetail = computed(() => {
|
||||
result_detail =
|
||||
props.searchItem?.msg_type === 1
|
||||
? props.searchItem?.extra?.content
|
||||
: props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 6
|
||||
? props.searchItem?.extra
|
||||
: ChatMsgTypeMapping[props.searchItem?.msg_type]
|
||||
break
|
||||
default:
|
||||
@ -262,6 +300,22 @@ const resultDetail = computed(() => {
|
||||
}
|
||||
return result_detail
|
||||
})
|
||||
|
||||
const previewPDF = (item) => {
|
||||
console.log(item)
|
||||
// if (typeof plus !== 'undefined') {
|
||||
// downloadAndOpenFile(item)
|
||||
// } else {
|
||||
// document.addEventListener('plusready', () => {
|
||||
// downloadAndOpenFile(item)
|
||||
// })
|
||||
// }
|
||||
window.open(
|
||||
`${import.meta.env.VITE_PAGE_URL}/office?url=${item}`,
|
||||
'_blank',
|
||||
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-item {
|
||||
@ -321,6 +375,69 @@ const resultDetail = computed(() => {
|
||||
color: #999999;
|
||||
line-height: 20px;
|
||||
}
|
||||
.file-message-wrapper {
|
||||
.condition-each-result-attachments {
|
||||
width: 289px;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 12px 15px;
|
||||
background-color: #f3f3f3;
|
||||
border-radius: 4px;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
box-sizing: border-box;
|
||||
|
||||
.attachment-avatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
.attachment-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: 0 0 0 11px;
|
||||
width: calc(100% - 38px - 11px);
|
||||
.attachment-info-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
span {
|
||||
line-height: 20px;
|
||||
color: #191919;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.attachment-sub-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
span {
|
||||
line-height: 17px;
|
||||
color: #999999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.info-detail-searchRecordDetail {
|
||||
display: flex;
|
||||
@ -380,4 +497,31 @@ const resultDetail = computed(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-component-wrapper {
|
||||
width: 154px;
|
||||
height: 100px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.im-message-video,
|
||||
.im-message-image,
|
||||
.image-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.n-image) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(img),
|
||||
:deep(video) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,12 +16,16 @@ const props = defineProps({
|
||||
createdAt: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
modalTitle: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
})
|
||||
const isShow=defineModel<boolean>('show')
|
||||
const { showUserInfoModal } = useInject()
|
||||
const items = ref<ITalkRecord[]>([])
|
||||
const title = ref('会话记录')
|
||||
const title = ref(props?.modalTitle || '会话记录')
|
||||
|
||||
const onMaskClick = () => {
|
||||
isShow.value=false
|
||||
@ -30,7 +34,7 @@ const onMaskClick = () => {
|
||||
const onLoadData = () => {
|
||||
ServeGetForwardRecords({
|
||||
msg_id: props.msgId,
|
||||
biz_date: parseTime(new Date(props.createdAt), '{y}{m}')
|
||||
biz_date: parseTime(new Date(props.createdAt || ''), '{y}{m}')
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
items.value = res.data.items || []
|
||||
|
@ -12,7 +12,13 @@ const props = defineProps<{
|
||||
const isShowRecord = ref(false)
|
||||
|
||||
const title = computed(() => {
|
||||
return [...new Set(props.extra.records.map((v) => v.nickname))].join('、')
|
||||
const uniqueNames = [...new Set(props.extra.records.map(v => v.nickname))];
|
||||
if (uniqueNames.length <= 2) {
|
||||
return uniqueNames.join('和');
|
||||
} else {
|
||||
return uniqueNames.slice(0, 2).join('和') + '等';
|
||||
}
|
||||
// return [...new Set(props.extra.records.map((v) => v.nickname))].join('和')
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
@ -21,7 +27,7 @@ const onClick = () => {
|
||||
</script>
|
||||
<template>
|
||||
<section class="im-message-forward pointer" @click="onClick">
|
||||
<div class="title">{{ title }} 的会话记录</div>
|
||||
<div class="title">{{ extra.forward_name || title}}的会话记录</div>
|
||||
<div class="list" v-for="(record, index) in extra.records" :key="index">
|
||||
<p>
|
||||
<span>{{ record.nickname }}: </span>
|
||||
@ -33,7 +39,7 @@ const onClick = () => {
|
||||
<span>转发:聊天会话记录 ({{ extra.msg_ids.length }}条)</span>
|
||||
</div>
|
||||
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at"/>
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at" :modalTitle="(extra.forward_name || title) + '的会话记录'"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -41,19 +47,21 @@ const onClick = () => {
|
||||
.im-message-forward {
|
||||
width: 250px;
|
||||
min-height: 95px;
|
||||
max-height: 150px;
|
||||
max-height: 190px;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--im-message-border-color);
|
||||
user-select: none;
|
||||
|
||||
.title {
|
||||
height: 30px;
|
||||
max-height: 60px;
|
||||
line-height: 30px;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
@ -11,12 +11,20 @@ defineProps<{
|
||||
let show = ref(false)
|
||||
</script>
|
||||
<template>
|
||||
<section class="im-message-group-notice pointer" @click="show = !show">
|
||||
<section
|
||||
class="im-message-group-notice pointer"
|
||||
@click="show = !show"
|
||||
:class="{
|
||||
left: data.float === 'left',
|
||||
right: data.float === 'right'
|
||||
}"
|
||||
>
|
||||
<div class="title">
|
||||
<n-tag :bordered="false" size="small" type="primary"> 群公告 </n-tag>
|
||||
《{{ extra.title }}》
|
||||
<!-- <n-tag :bordered="false" size="small" type="primary"> 群公告 </n-tag>
|
||||
《{{ extra.title }}》 -->
|
||||
<text>群公告</text>
|
||||
</div>
|
||||
<div class="content" :class="{ ellipsis: !show }">
|
||||
<div class="title" :class="{ ellipsis: !show }">
|
||||
{{ extra.content }}
|
||||
</div>
|
||||
</section>
|
||||
@ -30,14 +38,14 @@ let show = ref(false)
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--im-message-border-color);
|
||||
user-select: none;
|
||||
background-color: #fff;
|
||||
|
||||
.title {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 44rpx;
|
||||
font-size: 32rpx;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
@ -56,5 +64,18 @@ let show = ref(false)
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
background-color: #fff;
|
||||
border-radius: 0 16rpx 16rpx 16rpx;
|
||||
}
|
||||
|
||||
&.right {
|
||||
background-color: #46299d;
|
||||
border-radius: 16rpx 0 16rpx 16rpx;
|
||||
.title {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -27,6 +27,16 @@ const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
revokeInfo: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@ -42,16 +52,92 @@ const onRevoke = () => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="im-message-revoke">
|
||||
<div class="content">
|
||||
<div v-if="login_uid === user_id">
|
||||
<span> 你撤回了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<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>
|
||||
</div>
|
||||
<span v-else-if="talk_type == 1"> 对方撤回了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<span v-else>
|
||||
"{{ nickname }}" 撤回了一条消息 |
|
||||
<div class="content" v-if="JSON.stringify(revokeInfo) !== '{}'">
|
||||
<span v-if="talk_type === 1 && login_uid === revokeInfo.withdraw_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 1 && login_uid !== revokeInfo.withdraw_id">
|
||||
{{ revokeInfo.withdraw_name }}撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid === revokeInfo.withdraw_id &&
|
||||
login_uid === revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
你撤回了一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid === revokeInfo.withdraw_id &&
|
||||
login_uid !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
你撤回了{{ revokeInfo.retracted_name }}一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
revokeInfo.withdraw_id === revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
login_uid === revokeInfo.retracted_id &&
|
||||
revokeInfo.withdraw_id !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了你一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
login_uid !== revokeInfo.retracted_id &&
|
||||
revokeInfo.withdraw_id !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了{{ revokeInfo.retracted_name }}一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<!-- <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> A撤回B了一条消息 | {{ formatTime(datetime) }} </span> -->
|
||||
</div>
|
||||
<div class="content" v-if="JSON.stringify(revokeInfo) === '{}'">
|
||||
<span v-if="talk_type === 1 && login_uid === user_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 1 && login_uid !== user_id">
|
||||
{{ nickname }}撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 2 && !extra && login_uid === user_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 2 && !extra && login_uid !== user_id">
|
||||
{{ nickname }}撤回了一条消息 | {{ 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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -70,6 +70,12 @@ class Revoke extends Base {
|
||||
useTalkStore().updateItem({
|
||||
index_name: this.getIndexName(),
|
||||
msg_text: this.resource.text,
|
||||
revokeInfo: {
|
||||
retracted_id: this.resource.retracted_id,
|
||||
retracted_name: this.resource.retracted_name,
|
||||
withdraw_id: this.resource.withdraw_id,
|
||||
withdraw_name: this.resource.withdraw_name,
|
||||
},
|
||||
updated_at: parseTime(new Date())
|
||||
})
|
||||
|
||||
@ -80,6 +86,12 @@ class Revoke extends Base {
|
||||
|
||||
useDialogueStore().updateDialogueRecord({
|
||||
msg_id: this.msg_id,
|
||||
revokeInfo: {
|
||||
retracted_id: this.resource.retracted_id,
|
||||
retracted_name: this.resource.retracted_name,
|
||||
withdraw_id: this.resource.withdraw_id,
|
||||
withdraw_name: this.resource.withdraw_name,
|
||||
},
|
||||
is_revoke: 1
|
||||
})
|
||||
}
|
||||
|
@ -83,9 +83,37 @@ export const useTalkRecord = (uid: number) => {
|
||||
location.msgid = ''
|
||||
location.num = 0
|
||||
|
||||
element?.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
// 使用更精确的滚动定位方式
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (el && element) {
|
||||
// 计算目标元素相对于容器的偏移量
|
||||
const targetOffsetTop = element.offsetTop
|
||||
const containerHeight = el.clientHeight
|
||||
const targetHeight = element.offsetHeight
|
||||
|
||||
// 计算理想的滚动位置:让目标元素居中显示
|
||||
let scrollTo = targetOffsetTop - containerHeight / 2 + targetHeight / 2
|
||||
|
||||
// 边界检查:确保目标元素完全在可视区域内
|
||||
const minScrollTop = 0
|
||||
const maxScrollTop = el.scrollHeight - containerHeight
|
||||
|
||||
// 如果计算出的位置超出边界,调整到边界内
|
||||
if (scrollTo < minScrollTop) {
|
||||
scrollTo = minScrollTop
|
||||
} else if (scrollTo > maxScrollTop) {
|
||||
scrollTo = maxScrollTop
|
||||
}
|
||||
|
||||
// 执行滚动
|
||||
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
|
||||
} else {
|
||||
// 降级方案:使用 scrollIntoView
|
||||
element?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
}
|
||||
|
||||
addClass(element, 'border')
|
||||
|
||||
@ -143,7 +171,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
if (el) {
|
||||
if (request.cursor == 0) {
|
||||
// el.scrollTop = el.scrollHeight
|
||||
|
||||
|
||||
// setTimeout(() => {
|
||||
// el.scrollTop = el.scrollHeight + 1000
|
||||
// }, 500)
|
||||
@ -233,8 +261,10 @@ export const useTalkRecord = (uid: number) => {
|
||||
// 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById('imChatPanel')
|
||||
const target = document.getElementById(options.specifiedMsg?.msg_id || '')
|
||||
if (el && target) {
|
||||
const msgId = options.specifiedMsg?.msg_id
|
||||
const target = msgId ? document.getElementById(msgId) : null
|
||||
|
||||
if (el) {
|
||||
// 如果是向上加载更多,保持原有内容位置
|
||||
if (contextParams.direction === 'up') {
|
||||
el.scrollTop = el.scrollHeight - scrollHeight
|
||||
@ -246,21 +276,54 @@ export const useTalkRecord = (uid: number) => {
|
||||
el.scrollTop = scrollHeight - el.clientHeight
|
||||
}
|
||||
})
|
||||
} else {
|
||||
} else if (target && msgId) {
|
||||
// 只有在有目标元素且有 msg_id 时才执行定位逻辑
|
||||
// 如果是定位到特定消息,计算并滚动到目标位置
|
||||
const containerRect = el.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const offset = targetRect.top - containerRect.top
|
||||
loadConfig.isLocatingMessage = true
|
||||
// 居中
|
||||
const scrollTo = el.scrollTop + offset - el.clientHeight / 2 + target.clientHeight / 2
|
||||
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
|
||||
addClass(target, 'border')
|
||||
setTimeout(() => removeClass(target, 'border'), 3000)
|
||||
// 使用 nextTick 确保 DOM 完全渲染后再计算位置
|
||||
nextTick(() => {
|
||||
const el = document.getElementById('imChatPanel')
|
||||
const target = document.getElementById(msgId)
|
||||
|
||||
if (el && target) {
|
||||
loadConfig.isLocatingMessage = true
|
||||
|
||||
// 计算目标元素相对于容器的偏移量
|
||||
const targetOffsetTop = target.offsetTop
|
||||
const containerHeight = el.clientHeight
|
||||
const targetHeight = target.offsetHeight
|
||||
|
||||
// 计算理想的滚动位置:让目标元素居中显示
|
||||
let scrollTo = targetOffsetTop - containerHeight / 2 + targetHeight / 2
|
||||
|
||||
// 边界检查:确保目标元素完全在可视区域内
|
||||
const minScrollTop = 0
|
||||
const maxScrollTop = el.scrollHeight - containerHeight
|
||||
|
||||
// 如果计算出的位置超出边界,调整到边界内
|
||||
if (scrollTo < minScrollTop) {
|
||||
scrollTo = minScrollTop
|
||||
} else if (scrollTo > maxScrollTop) {
|
||||
scrollTo = maxScrollTop
|
||||
}
|
||||
|
||||
// 执行滚动
|
||||
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
|
||||
|
||||
// 添加高亮边框
|
||||
addClass(target, 'border')
|
||||
setTimeout(() => removeClass(target, 'border'), 3000)
|
||||
|
||||
// 清除 dialogueStore 中的 specifiedMsg,避免重复使用
|
||||
if (dialogueStore.specifiedMsg) {
|
||||
dialogueStore.specifiedMsg = ''
|
||||
dialogueStore.noRefreshRecords = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 其他情况滚动到底部
|
||||
scrollToBottom()
|
||||
}
|
||||
} else if (el) {
|
||||
// el.scrollTop = el.scrollHeight
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -285,6 +348,7 @@ export const useTalkRecord = (uid: number) => {
|
||||
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
|
||||
) {
|
||||
// 特殊参数模式下,direction: 'up',cursor: 当前最小 sequence
|
||||
// 注意:向上加载更多时,不传递 msg_id,避免触发定位逻辑
|
||||
onLoad(
|
||||
{
|
||||
receiver_id: loadConfig.receiver_id,
|
||||
@ -297,10 +361,8 @@ export const useTalkRecord = (uid: number) => {
|
||||
direction: 'up',
|
||||
sort_sequence: '',
|
||||
cursor: getMinSequence(),
|
||||
msg_id:
|
||||
records.value.find((item) =>
|
||||
item.sequence === getMinSequence() ? item.msg_id : ''
|
||||
)?.msg_id || ''
|
||||
// 向上加载更多时不传递 msg_id,保持原有滚动位置
|
||||
msg_id: undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -37,6 +37,9 @@ export const useDialogueStore = defineStore('dialogue', {
|
||||
// 查询指定消息上下文的消息信息
|
||||
specifiedMsg: '',
|
||||
|
||||
// 是否不刷新聊天记录
|
||||
noRefreshRecords: false,
|
||||
|
||||
// 是否是手动切换会话
|
||||
isManualSwitch: false,
|
||||
|
||||
|
@ -51,7 +51,8 @@ export interface ITalkRecord {
|
||||
float: string,
|
||||
is_convert_text?:number//语音记录的 是否是在展示转文本状态 1:是 0:否,
|
||||
erp_user_id:number,
|
||||
read_total_num:number
|
||||
read_total_num:number,
|
||||
revokeInfo?: any
|
||||
}
|
||||
|
||||
export interface ITalkRecordExtraText {
|
||||
@ -81,6 +82,7 @@ export interface ITalkRecordExtraForward {
|
||||
}[]
|
||||
talk_type: number
|
||||
user_id: number
|
||||
forward_name?: any
|
||||
}
|
||||
|
||||
export interface ITalkRecordExtraGroupNotice {
|
||||
|
@ -37,6 +37,7 @@ const labelColor=[
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="nickname">{{ username }}</span>
|
||||
<span v-if="data.talk_type == 2">({{data.group_member_num}})</span>
|
||||
<!-- <span class="badge top" v-show="data.is_top">顶</span>
|
||||
<span class="badge roboot" v-show="data.is_robot">助</span>
|
||||
<span class="badge group" v-show="data.talk_type == 2">群</span> -->
|
||||
|
@ -77,7 +77,9 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid)
|
||||
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(
|
||||
props.uid
|
||||
)
|
||||
const uploadsStore = useUploadsStore()
|
||||
const { useMessage } = useUtil()
|
||||
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
|
||||
@ -299,7 +301,6 @@ const onContextMenu = (e: any, item: ITalkRecord) => {
|
||||
}
|
||||
|
||||
const onConvertText = async (data: ITalkRecord) => {
|
||||
|
||||
data.is_convert_text = 1
|
||||
const res = await voiceToText({ msgId: data.msg_id, voiceUrl: data.extra.url })
|
||||
if (res.code == 200) {
|
||||
@ -340,12 +341,29 @@ const onRowClick = (item: ITalkRecord) => {
|
||||
}
|
||||
|
||||
const lastParams = ref('')
|
||||
|
||||
// 添加会话标识,用于检测会话切换
|
||||
const currentSessionKey = ref('')
|
||||
// 添加防重复刷新的定时器引用
|
||||
let noRefreshTimer: number | null = null
|
||||
// 监听整个 props 对象的变化
|
||||
watch(
|
||||
() => props,
|
||||
async (newProps) => {
|
||||
await nextTick()
|
||||
// 生成当前会话的唯一标识
|
||||
const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}`
|
||||
// 检测是否是会话切换
|
||||
const isSessionChanged = currentSessionKey.value !== newSessionKey
|
||||
// 如果是会话切换,立即重置 noRefreshRecords 并清除定时器
|
||||
if (isSessionChanged) {
|
||||
dialogueStore.noRefreshRecords = false
|
||||
if (noRefreshTimer) {
|
||||
clearTimeout(noRefreshTimer)
|
||||
noRefreshTimer = null
|
||||
}
|
||||
currentSessionKey.value = newSessionKey
|
||||
}
|
||||
|
||||
let specialParams = undefined
|
||||
if (newProps.specifiedMsg) {
|
||||
try {
|
||||
@ -359,6 +377,21 @@ watch(
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 检查是否需要阻止刷新
|
||||
if (dialogueStore.noRefreshRecords) {
|
||||
// 清除之前的定时器(如果存在)
|
||||
if (noRefreshTimer) {
|
||||
clearTimeout(noRefreshTimer)
|
||||
}
|
||||
// 设置新的定时器
|
||||
noRefreshTimer = setTimeout(() => {
|
||||
dialogueStore.noRefreshRecords = false
|
||||
noRefreshTimer = null
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
|
||||
onLoad(
|
||||
{
|
||||
receiver_id: newProps.receiver_id,
|
||||
@ -767,6 +800,8 @@ const onCustomSkipBottomEvent = () => {
|
||||
:nickname="item.nickname"
|
||||
:talk_type="item.talk_type"
|
||||
:datetime="item.created_at"
|
||||
:revokeInfo="item.revokeInfo"
|
||||
:extra="item.extra"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -877,11 +912,20 @@ const onCustomSkipBottomEvent = () => {
|
||||
<!-- 已读回执 -->
|
||||
<div class="talk_read_num" v-if="item.user_id === props.uid">
|
||||
<span v-if="props.talk_type === 1">{{
|
||||
item.read_total_num > 0 ? '已读' : '未读'
|
||||
}}</span>
|
||||
<n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;" v-if="props.talk_type === 2">
|
||||
item.read_total_num > 0 ? '已读' : '未读'
|
||||
}}</span>
|
||||
<n-popover
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
style="height: 382px; padding: 0;"
|
||||
v-if="props.talk_type === 2"
|
||||
>
|
||||
<template #trigger>
|
||||
<span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)" style="cursor: pointer;">
|
||||
<span
|
||||
v-if="props.talk_type === 2"
|
||||
@click="toShowMessageReadDetail(item)"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
已读 ({{ item?.read_total_num || 0 }}/{{
|
||||
props.num - 1 > 0 ? props.num - 1 : 0
|
||||
}})
|
||||
@ -949,7 +993,11 @@ const onCustomSkipBottomEvent = () => {
|
||||
</div>
|
||||
|
||||
<!-- 置底按钮 -->
|
||||
<SkipBottom v-model="skipBottom" :customSkipBottomEvent="true" @customSkipBottomEvent="onCustomSkipBottomEvent"/>
|
||||
<SkipBottom
|
||||
v-model="skipBottom"
|
||||
:customSkipBottomEvent="true"
|
||||
@customSkipBottomEvent="onCustomSkipBottomEvent"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
|
@ -147,6 +147,7 @@ const onSetMenu = () => {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user