Compare commits
22 Commits
e4354d42cd
...
4b5c160e94
Author | SHA1 | Date | |
---|---|---|---|
|
4b5c160e94 | ||
|
ebd567a757 | ||
18871db6b6 | |||
5f4dd80b2a | |||
|
1ae317dbb3 | ||
|
3b6d998ce1 | ||
|
45eec2ff22 | ||
|
9c34066128 | ||
92fce58429 | |||
d55616e2e7 | |||
031411ba49 | |||
|
c0f4248385 | ||
|
2e998a1174 | ||
|
60a2fb996b | ||
b282562cdd | |||
642992640f | |||
d0abf7d8ab | |||
5b4ee3c677 | |||
|
409af72039 | ||
|
799599bd83 | ||
ec18d85546 | |||
|
a97f293a6c |
@ -35,7 +35,7 @@ export const ServeTalkRecords = (data = {}) => {
|
||||
|
||||
// 获取转发会话记录详情列表服务接口
|
||||
export const ServeGetForwardRecords = (data = {}) => {
|
||||
return get('/api/v1/talk/records/forward', data)
|
||||
return get('/api/v1/talk/records/forward/v2', data)
|
||||
}
|
||||
|
||||
// 对话列表置顶服务接口
|
||||
|
@ -2,12 +2,12 @@ import { post, get, upload } from '@/utils/request'
|
||||
|
||||
//ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览
|
||||
export const ServeSeachQueryAll = (data = {}) => {
|
||||
return post('/api/v1/elasticsearch/query-all', data)
|
||||
return post('/api/v1/elasticsearch/query-all/v2', data)
|
||||
}
|
||||
|
||||
// ES搜索用户数据
|
||||
export const ServeQueryUser = (data) => {
|
||||
return post('/api/v1/elasticsearch/query-user', data)
|
||||
return post('/api/v1/elasticsearch/query-user/v2', data)
|
||||
}
|
||||
|
||||
// ES搜索群组数据
|
||||
|
@ -132,14 +132,22 @@ const handleInput = (event) => {
|
||||
|
||||
// 更新HTML内容
|
||||
editorHtml.value = target.innerHTML || ''
|
||||
|
||||
const currentEditor= parseEditorContent().items
|
||||
// 后续操作
|
||||
checkMention(target)
|
||||
saveDraft()
|
||||
|
||||
emit('editor-event', {
|
||||
event: 'input_event',
|
||||
data: editorContent.value
|
||||
data: currentEditor.map(x=>{
|
||||
let text=''
|
||||
if(x.type===3){
|
||||
text='[图片]'
|
||||
}else if(x.type===1){
|
||||
text=x.content
|
||||
}
|
||||
return text
|
||||
})?.join('')
|
||||
})
|
||||
}
|
||||
|
||||
@ -1244,17 +1252,34 @@ const saveDraft = () => {
|
||||
// 获取不包含引用的内容
|
||||
const contentToSave = tempDiv.textContent || ''
|
||||
const htmlToSave = tempDiv.innerHTML || ''
|
||||
|
||||
const currentEditor= parseEditorContent().items
|
||||
// 检查是否有实际内容(不包括引用)
|
||||
const hasContent = contentToSave.trim().length > 0 ||
|
||||
htmlToSave.includes('<img') ||
|
||||
htmlToSave.includes('editor-file')
|
||||
|
||||
// 根据内容状态保存或删除草稿
|
||||
if (hasContent) {
|
||||
if (currentEditor.length>0) {
|
||||
console.log('保存到草稿',currentEditor.map(x=>{
|
||||
let text=''
|
||||
if(x.type===3){
|
||||
text='[图片]'
|
||||
}else if(x.type===1){
|
||||
text=x.content
|
||||
}
|
||||
return text
|
||||
})?.join(''))
|
||||
// 保存草稿到store,不包括引用数据
|
||||
editorDraftStore.items[indexName.value] = JSON.stringify({
|
||||
content: contentToSave,
|
||||
content: currentEditor.map(x=>{
|
||||
let text=''
|
||||
if(x.type===3){
|
||||
text='[图片]'
|
||||
}else if(x.type===1){
|
||||
text=x.content
|
||||
}
|
||||
return text
|
||||
})?.join(''),
|
||||
html: htmlToSave
|
||||
})
|
||||
} else {
|
||||
|
@ -168,35 +168,13 @@
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<div class="condition-result-imgAndVideo-area" v-if="item?.extra?.url">
|
||||
<template v-if="item?.msg_type === 3">
|
||||
<n-image
|
||||
:src="item?.extra?.url"
|
||||
:lazy="true"
|
||||
:preview-src="item?.extra?.url"
|
||||
:width="131"
|
||||
:height="131"
|
||||
object-fit="cover"
|
||||
></n-image>
|
||||
</template>
|
||||
<template v-else-if="item?.msg_type === 5">
|
||||
<div class="video-preview" @click="onPlay(item?.extra?.url)">
|
||||
<video :src="item?.extra?.url" :controls="false"></video>
|
||||
<!-- <n-image
|
||||
:src="
|
||||
item?.extra?.url
|
||||
? item?.extra?.url + '#t=0.001'
|
||||
: item?.extra?.cover
|
||||
"
|
||||
:width="131"
|
||||
:height="131"
|
||||
object-fit="cover"
|
||||
></n-image> -->
|
||||
<div class="btn-video">
|
||||
<!-- <img :src="playCircle" /> -->
|
||||
<n-icon :component="Play" size="40" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="message-component-wrapper">
|
||||
<component
|
||||
:is="MessageComponents[item.msg_type] || 'unknown-message'"
|
||||
:extra="item.extra"
|
||||
:data="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div
|
||||
@ -326,8 +304,14 @@ import { ServeTalkDate, ServeGetSessionId } from '@/api/search.js'
|
||||
import { parseTime } from '@/utils/datetime'
|
||||
import { fileFormatSize, fileSuffix } from '@/utils/strings'
|
||||
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
|
||||
import { MessageComponents } from '@/constant/message'
|
||||
|
||||
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray', 'hideSearchResultModal'])
|
||||
const emits = defineEmits([
|
||||
'clearSearchMemberByAlphabet',
|
||||
'getDisabledDateArray',
|
||||
'hideSearchResultModal',
|
||||
'clearSelectedDateTime'
|
||||
])
|
||||
|
||||
const dialogueStore = useDialogueStore()
|
||||
// 当前对话参数
|
||||
@ -770,6 +754,10 @@ const resetSearchConditions = (newVal) => {
|
||||
state.group_member_id = 0
|
||||
emits('clearSearchMemberByAlphabet')
|
||||
}
|
||||
if (newVal !== 'date') {
|
||||
state.selectedDateTime = null
|
||||
emits('clearSelectedDateTime')
|
||||
}
|
||||
}
|
||||
|
||||
//触底加载更多数据
|
||||
@ -851,7 +839,6 @@ watch(
|
||||
queryAllSearch()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
@ -1126,4 +1113,31 @@ body:deep(.round-3) {
|
||||
margin: 13px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-component-wrapper {
|
||||
width: 131px;
|
||||
height: 131px;
|
||||
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>
|
||||
|
@ -372,7 +372,7 @@ const resultDetail = computed(() => {
|
||||
border: 0;
|
||||
}
|
||||
.search-item:hover {
|
||||
background-color: #f8f8f8;
|
||||
background-color: rgba(70, 41, 157, 0.1);
|
||||
|
||||
.info-detail-searchRecordDetail {
|
||||
.searchRecordDetail-fastLocal {
|
||||
|
@ -41,13 +41,13 @@
|
||||
>
|
||||
<searchItem
|
||||
@click="clickSearchItem(searchResultKey, item)"
|
||||
v-if="(
|
||||
searchResultKey === 'user_infos'
|
||||
? (state.userInfosShowAll || (props.listLimit && index < 3))
|
||||
v-if="
|
||||
(searchResultKey === 'user_infos'
|
||||
? state.userInfosShowAll || (props.listLimit && index < 3)
|
||||
: searchResultKey === 'combinedGroup'
|
||||
? (state.groupInfosShowAll || (props.listLimit && index < 3))
|
||||
: (props.listLimit && index < 3)
|
||||
) || !props.listLimit"
|
||||
? state.groupInfosShowAll || (props.listLimit && index < 3)
|
||||
: props.listLimit && index < 3) || !props.listLimit
|
||||
"
|
||||
:searchResultKey="searchResultKey"
|
||||
:searchItem="item"
|
||||
:searchText="state.searchText"
|
||||
@ -403,11 +403,57 @@ const queryAllSearch = (doClearSearchResult) => {
|
||||
} else if (state?.first_talk_record_infos?.talk_type === 2) {
|
||||
total = data.group_record_count
|
||||
}
|
||||
}
|
||||
if (total < props.searchResultPageSize) {
|
||||
state.hasMore = false
|
||||
let noMoreSearchResultRecord = true
|
||||
if (
|
||||
Object.keys(data).includes('talk_record_infos') &&
|
||||
state.searchResult['talk_record_infos']?.length > 0
|
||||
) {
|
||||
//搜聊天记录详情
|
||||
if (state.searchResult['talk_record_infos']?.length < total) {
|
||||
noMoreSearchResultRecord = false
|
||||
}
|
||||
}
|
||||
if (noMoreSearchResultRecord) {
|
||||
state.hasMore = false
|
||||
} else {
|
||||
state.hasMore = true
|
||||
}
|
||||
} else {
|
||||
state.hasMore = true
|
||||
let noMoreSearchResultUser = true
|
||||
let noMoreSearchResultGroup = true
|
||||
let noMoreSearchResultGeneral = true
|
||||
if (
|
||||
Object.keys(data).includes('user_infos') &&
|
||||
state.searchResult['user_infos']?.length > 0
|
||||
) {
|
||||
//搜人
|
||||
if (state.searchResult['user_infos']?.length < total) {
|
||||
noMoreSearchResultUser = false
|
||||
}
|
||||
}
|
||||
if (
|
||||
Object.keys(data).includes('group_member_infos' || 'group_infos') &&
|
||||
state.searchResult['combinedGroup']?.length > 0
|
||||
) {
|
||||
//搜群
|
||||
if (state.searchResult['combinedGroup']?.length < total) {
|
||||
noMoreSearchResultGroup = false
|
||||
}
|
||||
}
|
||||
if (
|
||||
Object.keys(data).includes('general_infos') &&
|
||||
state.searchResult['general_infos']?.length > 0
|
||||
) {
|
||||
//搜聊天记录
|
||||
if (state.searchResult['general_infos']?.length < total) {
|
||||
noMoreSearchResultGeneral = false
|
||||
}
|
||||
}
|
||||
if (noMoreSearchResultUser && noMoreSearchResultGroup && noMoreSearchResultGeneral) {
|
||||
state.hasMore = false
|
||||
} else {
|
||||
state.hasMore = true
|
||||
}
|
||||
}
|
||||
emits('resultTotalCount', total)
|
||||
// zPaging.value?.completeByTotal([data], total)
|
||||
@ -673,18 +719,21 @@ async function loadMoreGroupInfos() {
|
||||
const resp = await ServeQueryGroup(params)
|
||||
if (resp.code === 200) {
|
||||
const groupInfos = Array.isArray(resp.data.group_infos) ? resp.data.group_infos : []
|
||||
const groupMemberInfos = Array.isArray(resp.data.group_member_infos) ? resp.data.group_member_infos : []
|
||||
const groupMemberInfos = Array.isArray(resp.data.group_member_infos)
|
||||
? resp.data.group_member_infos
|
||||
: []
|
||||
|
||||
// 给新数据加上 groupTempType
|
||||
groupInfos.forEach(item => {
|
||||
groupInfos.forEach((item) => {
|
||||
item.groupTempType = 'group_infos'
|
||||
item.group_type = item.type // 保持一致性
|
||||
})
|
||||
groupMemberInfos.forEach(item => {
|
||||
groupMemberInfos.forEach((item) => {
|
||||
item.groupTempType = 'group_member_infos'
|
||||
})
|
||||
|
||||
const isFirstLoad = (!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
|
||||
const isFirstLoad =
|
||||
(!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
|
||||
(state.groupInfosLastGroupId === 0 && state.groupInfosLastMemberId === 0)
|
||||
if (isFirstLoad) {
|
||||
// 第一次加载,直接替换
|
||||
@ -697,7 +746,9 @@ async function loadMoreGroupInfos() {
|
||||
} else {
|
||||
// 后续加载,追加
|
||||
const allGroupInfos = (state.searchResult.group_infos || []).concat(groupInfos)
|
||||
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(groupMemberInfos)
|
||||
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(
|
||||
groupMemberInfos
|
||||
)
|
||||
state.searchResult = {
|
||||
...state.searchResult,
|
||||
group_infos: allGroupInfos,
|
||||
@ -708,10 +759,9 @@ async function loadMoreGroupInfos() {
|
||||
state.groupInfosLastGroupId = resp.data.last_group_id
|
||||
state.groupInfosLastMemberId = resp.data.last_member_id
|
||||
// 判断是否全部加载完
|
||||
const noMoreData = (
|
||||
const noMoreData =
|
||||
(!groupInfos.length && !groupMemberInfos.length) ||
|
||||
(resp.data.last_group_id === 0 && resp.data.last_member_id === 0)
|
||||
)
|
||||
if (noMoreData) {
|
||||
state.groupInfosExpand = true
|
||||
}
|
||||
@ -774,7 +824,7 @@ async function loadMoreGroupInfos() {
|
||||
}
|
||||
}
|
||||
.result-has-more:hover {
|
||||
background-color: #f8f8f8;
|
||||
background-color: rgba(70, 41, 157, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,15 @@ import { ITalkRecord } from '@/types/chat'
|
||||
import { useInject } from '@/hooks'
|
||||
import customModal from '@/components/common/customModal.vue'
|
||||
import { voiceToText } from '@/api/chat.js'
|
||||
import { parseTime } from '@/utils/datetime'
|
||||
const props = defineProps({
|
||||
msgId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
const isShow=defineModel<boolean>('show')
|
||||
@ -24,7 +29,8 @@ const onMaskClick = () => {
|
||||
|
||||
const onLoadData = () => {
|
||||
ServeGetForwardRecords({
|
||||
msg_id: props.msgId
|
||||
msg_id: props.msgId,
|
||||
biz_date: parseTime(new Date(props.createdAt), '{y}{m}')
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
items.value = res.data.items || []
|
||||
|
@ -33,7 +33,7 @@ const onClick = () => {
|
||||
<span>转发:聊天会话记录 ({{ extra.msg_ids.length }}条)</span>
|
||||
</div>
|
||||
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -45,7 +45,7 @@ const onRevoke = () => {
|
||||
<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" text class="text-#46299D text-11px">重新编辑</n-button>
|
||||
<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>
|
||||
|
@ -18,7 +18,7 @@ export function isLoggedIn() {
|
||||
*/
|
||||
export function getAccessToken() {
|
||||
// return storage.get(AccessToken) || ''
|
||||
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22c9c2f9b60a57573e8b08cdf47105e1ba85550c21fa55526e8a00bf316c623eb67abf749622c48beab908d61d3db7b22ed3eb6aa8a08c77680ad4d8a3458c1e72f97ba2b8480674df77f0501a34e82b58'
|
||||
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b891a491a664540c3af42964b31bedf8b1c93e8a754bb71e4b95d53ad8e6b16ac1575f536a9e7a062e44f3bb48a367623d38bd875a10afa3a53e79374ffda424138ed9ad4cab0d972432567ae7149b2bf3c'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,7 +189,7 @@ const handleGroupNoticeModalConfirm = () => {
|
||||
state.isShowNoticeHintModal = true
|
||||
if (state?.groupNoticeInfo?.id && !state.groupNoticeInEdit) {
|
||||
//如果是在编辑中,但是没有输入内容,此时点击完成即为删除群公告
|
||||
state.noticeHintModalContent = '确定清空群公告吗?'
|
||||
state.noticeHintModalContent = '确定清空群公告'
|
||||
state.noticeHintModalActionBtns = {
|
||||
confirmBtn: {
|
||||
text: '清空',
|
||||
@ -485,6 +485,11 @@ const hideSearchResultModal = () => {
|
||||
handleSearchRecordByConditionModalClose()
|
||||
state.isShowGroupAside = false
|
||||
}
|
||||
|
||||
// 清空日期选择器
|
||||
const clearSelectedDateTime = () => {
|
||||
onDatePickClear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -594,7 +599,7 @@ const hideSearchResultModal = () => {
|
||||
<n-input
|
||||
type="text"
|
||||
v-model:value="state.searchRecordByConditionText"
|
||||
placeholder="请输入"
|
||||
:placeholder="state.conditionTag && state.conditionTag !== 'all'?'':'请输入'"
|
||||
clearable
|
||||
>
|
||||
<template #clear-icon>
|
||||
@ -712,6 +717,7 @@ const hideSearchResultModal = () => {
|
||||
:selectedDateTime="state.selectedDateTime"
|
||||
:nowDateTime="state.nowDateTime"
|
||||
@hideSearchResultModal="hideSearchResultModal"
|
||||
@clearSelectedDateTime="clearSelectedDateTime"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
|
@ -89,6 +89,12 @@ const renderChatAppSearch = () => {
|
||||
state.searchRecordText = searchText
|
||||
state.selectItemInList = res
|
||||
} else {
|
||||
if(searchResultKey === 'user_infos'){
|
||||
talk_type = 1
|
||||
}
|
||||
if(searchResultKey === 'combinedGroup'){
|
||||
talk_type = 2
|
||||
}
|
||||
talkStore.toTalk(talk_type, receiver_id, router)
|
||||
}
|
||||
state.showSearchDropdown = false
|
||||
@ -171,6 +177,9 @@ const state = reactive({
|
||||
title: '姓名 【工号】',
|
||||
field: 'nickName',
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render(row, index) {
|
||||
return row.nickName + '【' + row.jobNum + '】'
|
||||
}
|
||||
@ -179,7 +188,9 @@ const state = reactive({
|
||||
title: '岗位名称',
|
||||
field: 'positionName',
|
||||
width: 400,
|
||||
ellipsis: true,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render(row, index) {
|
||||
let positionNames = Array.isArray(row.depPositions)
|
||||
? row.depPositions.flatMap((dep) =>
|
||||
@ -192,7 +203,7 @@ const state = reactive({
|
||||
{
|
||||
title: '操作',
|
||||
field: 'action',
|
||||
width: 200,
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row, index) {
|
||||
@ -213,7 +224,10 @@ const state = reactive({
|
||||
{
|
||||
title: '群聊名称',
|
||||
field: 'groupName',
|
||||
width: 200,
|
||||
width: 400,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render(row, index) {
|
||||
return row.group_name
|
||||
}
|
||||
@ -221,7 +235,7 @@ const state = reactive({
|
||||
{
|
||||
title: '群类型',
|
||||
field: 'groupType',
|
||||
width: 400,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render(row, index) {
|
||||
let groupType = row.group_type
|
||||
@ -239,7 +253,7 @@ const state = reactive({
|
||||
{
|
||||
title: '操作',
|
||||
field: 'action',
|
||||
width: 200,
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render(row, index) {
|
||||
@ -448,6 +462,11 @@ onMounted(() => {
|
||||
const showAddressBookModal = () => {
|
||||
state.isShowAddressBookModal = true
|
||||
}
|
||||
// 点击关闭通讯录模态框
|
||||
const closeAddressBookModal = () => {
|
||||
state.isShowAddressBookModal = false
|
||||
resetAddressBookModal()
|
||||
}
|
||||
const handleTreeClick = ({ selectedKey, tree }) => {
|
||||
// console.log(tree)
|
||||
state.clickKey = tree.key
|
||||
@ -840,6 +859,8 @@ const handleEnterSearchResultChat = () => {
|
||||
:style="state.customModalStyle"
|
||||
:customCloseBtn="true"
|
||||
:closable="false"
|
||||
:customCloseEvent="true"
|
||||
@customCloseModal="closeAddressBookModal"
|
||||
>
|
||||
<template #content>
|
||||
<div class="custom-modal-content">
|
||||
@ -1170,6 +1191,7 @@ html[theme-mode='dark'] {
|
||||
background-color: #46299d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.groupChatList-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -1254,4 +1276,7 @@ html[theme-mode='dark'] {
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.n-data-table .n-data-table-tr:not(.n-data-table-tr--summary):hover > .n-data-table-td) {
|
||||
background-color: rgba(70, 41, 157, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted, ref, nextTick } from 'vue'
|
||||
import { NDropdown, NCheckbox } from 'naive-ui'
|
||||
import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
|
||||
import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
|
||||
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
|
||||
import { bus } from '@/utils/event-bus'
|
||||
import { useDialogueStore } from '@/store'
|
||||
@ -14,10 +14,42 @@ 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 ,useUploadsStore} from '@/store'
|
||||
import { useUserStore, useUploadsStore } from '@/store'
|
||||
import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
|
||||
import { voiceToText } from '@/api/chat.js'
|
||||
import {confirmBox} from '@/components/confirm-box/service.js'
|
||||
import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
|
||||
import { confirmBox } from '@/components/confirm-box/service.js'
|
||||
import ws from '@/connect'
|
||||
import avatarModule from '@/components/avatar-module/index.vue'
|
||||
|
||||
// 定义消息已读状态接口
|
||||
interface ReadStatus {
|
||||
msg_ids: string[]
|
||||
talk_type: number
|
||||
receiver_id: number
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
// 定义状态接口
|
||||
interface State {
|
||||
visibleElements: Set<HTMLElement>
|
||||
visibleOutElements: Set<HTMLElement>
|
||||
tempWaitDoRead: ReadStatus[]
|
||||
tempWaitDoCheck: ReadStatus[]
|
||||
setMessageReadInterval: number | null
|
||||
setOutMessageReadInterval: number | null
|
||||
lastUpdateTime: number
|
||||
isScrolling: boolean
|
||||
scrollTimer: number | null
|
||||
lastTriggerTime: number
|
||||
talkReadListDetail: any[]
|
||||
readDetailIsUnread: number
|
||||
currentMsgReadDetail: any | null
|
||||
currentReadDetailPage: number
|
||||
hasMoreReadListDetail: boolean
|
||||
loadingReadListDetail: boolean
|
||||
lastScrollTop: number
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
uid: {
|
||||
type: Number,
|
||||
@ -38,10 +70,14 @@ const props = defineProps({
|
||||
specifiedMsg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
num: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid)
|
||||
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid)
|
||||
const uploadsStore = useUploadsStore()
|
||||
const { useMessage } = useUtil()
|
||||
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
|
||||
@ -90,11 +126,33 @@ const onPanelScroll = (e: any) => {
|
||||
}
|
||||
|
||||
const height = e.target.scrollTop + e.target.clientHeight
|
||||
const scrollHeight = e.target.scrollHeight
|
||||
const isScrollingDown = e.target.scrollTop > state.value.lastScrollTop
|
||||
state.value.lastScrollTop = e.target.scrollTop
|
||||
|
||||
skipBottom.value = height < e.target.scrollHeight - 200
|
||||
// 触底且必须是向下滚动
|
||||
if (height === scrollHeight && isScrollingDown) {
|
||||
onLoadMoreDown()
|
||||
}
|
||||
|
||||
skipBottom.value = height < scrollHeight - 200
|
||||
if (!skipBottom.value && dialogueStore.unreadBubble) {
|
||||
dialogueStore.setUnreadBubble(0)
|
||||
}
|
||||
|
||||
// 设置滚动状态
|
||||
state.value.isScrolling = true
|
||||
if (state.value.scrollTimer) {
|
||||
clearTimeout(state.value.scrollTimer)
|
||||
}
|
||||
state.value.scrollTimer = window.setTimeout(() => {
|
||||
state.value.isScrolling = false
|
||||
// 滚动停止,强制触发一次
|
||||
checkVisibleOutElements()
|
||||
// 重置节流时间戳,保证下一次变化能立刻触发
|
||||
lastVisibleOutTriggerTime = Date.now()
|
||||
}, 300) // 300ms 的防抖时间
|
||||
|
||||
// 检测是否到达底部
|
||||
if (skipBottom.value == false) {
|
||||
let len = dialogueStore.records.length
|
||||
@ -231,7 +289,6 @@ const onClickNickname = (data: ITalkRecord) => {
|
||||
|
||||
// 会话列表右键显示菜单
|
||||
const onContextMenu = (e: any, item: ITalkRecord) => {
|
||||
|
||||
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
|
||||
return e.preventDefault()
|
||||
}
|
||||
@ -293,7 +350,10 @@ watch(
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
|
||||
// 只有会话id和参数都匹配才进入特殊模式
|
||||
if (parsed.talk_type === newProps.talk_type && parsed.receiver_id === newProps.receiver_id) {
|
||||
if (
|
||||
parsed.talk_type === newProps.talk_type &&
|
||||
parsed.receiver_id === newProps.receiver_id
|
||||
) {
|
||||
specialParams = parsed
|
||||
}
|
||||
} catch (e) {}
|
||||
@ -311,29 +371,356 @@ watch(
|
||||
)
|
||||
|
||||
// onMounted(() => {
|
||||
// onLoad({ ...props, limit: 30 })
|
||||
// onLoad({ ...props, limit: 30 })
|
||||
// })
|
||||
const retry=(item:any)=>{
|
||||
const retry = (item: any) => {
|
||||
confirmBox({
|
||||
content:'确定重发吗'
|
||||
}).then(()=>{
|
||||
uploadsStore.retryCommonUpload(item.extra.upload_id)
|
||||
content: '确定重发吗'
|
||||
}).then(() => {
|
||||
uploadsStore.retryCommonUpload(item.extra.upload_id)
|
||||
})
|
||||
}
|
||||
|
||||
const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
if(item.talk_type!==1){
|
||||
e.preventDefault()
|
||||
if(item.float!=='right'){
|
||||
bus.emit(EditorConst.Mention, {
|
||||
id: item.user_id,
|
||||
value: item.nickname
|
||||
})
|
||||
}
|
||||
const onContextMenuAvatar = (e: any, item: any) => {
|
||||
e.preventDefault()
|
||||
if (item.float !== 'right') {
|
||||
bus.emit(EditorConst.Mention, {
|
||||
id: item.user_id,
|
||||
value: item.nickname
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const state = ref<State>({
|
||||
visibleElements: new Set(),
|
||||
visibleOutElements: new Set(),
|
||||
tempWaitDoRead: [],
|
||||
tempWaitDoCheck: [],
|
||||
setMessageReadInterval: null,
|
||||
setOutMessageReadInterval: null,
|
||||
lastUpdateTime: 0,
|
||||
isScrolling: false,
|
||||
scrollTimer: null,
|
||||
lastTriggerTime: 0,
|
||||
talkReadListDetail: [],
|
||||
readDetailIsUnread: 1,
|
||||
currentMsgReadDetail: null,
|
||||
currentReadDetailPage: 1,
|
||||
hasMoreReadListDetail: true,
|
||||
loadingReadListDetail: false,
|
||||
lastScrollTop: 0
|
||||
})
|
||||
|
||||
// 定义观察者变量
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
// 检查需要发送已读回执的元素
|
||||
const checkVisibleElements = () => {
|
||||
if (state.value.visibleElements.size > 0) {
|
||||
let waitDoRead: ReadStatus[] = []
|
||||
state.value.visibleElements.forEach((el: HTMLElement) => {
|
||||
const msgId = el.dataset.msgid
|
||||
const talkType = Number(el.dataset.talktype)
|
||||
const receiverId = Number(el.dataset.receiverid)
|
||||
|
||||
if (!msgId) return
|
||||
|
||||
if (waitDoRead.length === 0) {
|
||||
waitDoRead.push({
|
||||
msg_ids: [msgId],
|
||||
talk_type: talkType,
|
||||
receiver_id: receiverId
|
||||
})
|
||||
} else {
|
||||
const existingItem = waitDoRead.find(
|
||||
(item) => item.talk_type === talkType && item.receiver_id === receiverId
|
||||
)
|
||||
if (existingItem) {
|
||||
existingItem.msg_ids.push(msgId)
|
||||
} else {
|
||||
waitDoRead.push({
|
||||
msg_ids: [msgId],
|
||||
talk_type: talkType,
|
||||
receiver_id: receiverId
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
if (waitDoRead.length > 0) {
|
||||
waitDoRead.forEach((doReadItem) => {
|
||||
const prevItem = state.value.tempWaitDoRead.find(
|
||||
(prev) =>
|
||||
prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id
|
||||
)
|
||||
if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) {
|
||||
console.error('====发送了新版已读回执=====', doReadItem)
|
||||
ws.emit('im.message.new.read', doReadItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
state.value.tempWaitDoRead = JSON.parse(JSON.stringify(waitDoRead))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查需要获取别人发送的已读回执列表的元素
|
||||
const checkVisibleOutElements = () => {
|
||||
if (state.value.visibleOutElements.size > 0) {
|
||||
let waitDoCheck: ReadStatus[] = []
|
||||
state.value.visibleOutElements.forEach((el: HTMLElement) => {
|
||||
const msgId = el.dataset.msgid
|
||||
const talkType = Number(el.dataset.talktype)
|
||||
const receiverId = Number(el.dataset.receiverid)
|
||||
|
||||
if (!msgId) return
|
||||
|
||||
if (waitDoCheck.length === 0) {
|
||||
waitDoCheck.push({
|
||||
msg_ids: [msgId],
|
||||
talk_type: talkType,
|
||||
receiver_id: receiverId,
|
||||
user_id: props.uid
|
||||
})
|
||||
} else {
|
||||
const existingItem = waitDoCheck.find(
|
||||
(item) => item.talk_type === talkType && item.receiver_id === receiverId
|
||||
)
|
||||
if (existingItem) {
|
||||
existingItem.msg_ids.push(msgId)
|
||||
} else {
|
||||
waitDoCheck.push({
|
||||
msg_ids: [msgId],
|
||||
talk_type: talkType,
|
||||
receiver_id: receiverId,
|
||||
user_id: props.uid
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
if (waitDoCheck.length > 0) {
|
||||
waitDoCheck.forEach((doCheckItem) => {
|
||||
console.error('====组装了新版已读回执参数,需要发送socket=====', doCheckItem)
|
||||
ws.emit('im.message.listen.read', doCheckItem)
|
||||
})
|
||||
}
|
||||
state.value.tempWaitDoCheck = JSON.parse(JSON.stringify(waitDoCheck))
|
||||
}
|
||||
}
|
||||
|
||||
// 定义节流时间戳
|
||||
let lastVisibleOutTriggerTime = 0
|
||||
|
||||
//新版采用socket监听已读回执,不轮询接口
|
||||
watch(
|
||||
() => state.value.visibleOutElements,
|
||||
(newVal) => {
|
||||
const now = Date.now()
|
||||
if (now - lastVisibleOutTriggerTime < 1000) {
|
||||
return
|
||||
}
|
||||
lastVisibleOutTriggerTime = now
|
||||
checkVisibleOutElements()
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 观察者函数
|
||||
const handleIntersection = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
||||
let elData = entry.target.dataset
|
||||
const msgType = elData.msgtype
|
||||
const userId = elData.userid
|
||||
if (Number(msgType) < 1000 && Number(userId) !== Number(props.uid)) {
|
||||
//我读别人发的消息,需要发送已读回执
|
||||
state.value.visibleElements.add(entry.target)
|
||||
}
|
||||
if (Number(msgType) < 1000 && Number(userId) === Number(props.uid)) {
|
||||
//我发的消息,需要获取别人发送的已读回执列表
|
||||
state.value.visibleOutElements.add(entry.target)
|
||||
}
|
||||
} else {
|
||||
// 元素离开视口,从集合中移除
|
||||
state.value.visibleElements.delete(entry.target)
|
||||
state.value.visibleOutElements.delete(entry.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听消息列表变化
|
||||
watch(
|
||||
() => records.value,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
// 断开旧的观察者
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
|
||||
// 重新初始化观察者
|
||||
const options = {
|
||||
root: null,
|
||||
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
rootMargin: '50px 0px'
|
||||
}
|
||||
observer = new IntersectionObserver(handleIntersection, options)
|
||||
|
||||
// 重新观察所有消息元素
|
||||
const messageElements = document.querySelectorAll('.message-item')
|
||||
messageElements.forEach((el) => {
|
||||
if (observer) {
|
||||
observer.observe(el)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 事件总线防抖处理
|
||||
let eventBusDebounceTimer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// 事件总线监听
|
||||
bus.subscribe('check-visible-out-elements', (type) => {
|
||||
if (eventBusDebounceTimer) {
|
||||
clearTimeout(eventBusDebounceTimer)
|
||||
}
|
||||
eventBusDebounceTimer = window.setTimeout(() => {
|
||||
checkVisibleOutElements()
|
||||
eventBusDebounceTimer = null
|
||||
}, 500)
|
||||
})
|
||||
//设置观察者前设置定时器
|
||||
if (state.value.setMessageReadInterval) {
|
||||
clearInterval(state.value.setMessageReadInterval)
|
||||
state.value.setMessageReadInterval = null
|
||||
}
|
||||
state.value.setMessageReadInterval = setInterval(() => {
|
||||
checkVisibleElements()
|
||||
}, 2000)
|
||||
|
||||
if (state.value.setOutMessageReadInterval) {
|
||||
clearInterval(state.value.setOutMessageReadInterval)
|
||||
state.value.setOutMessageReadInterval = null
|
||||
}
|
||||
// 旧版采用定时器来轮询已读回执,新版采用socket监听已读回执
|
||||
// state.value.setOutMessageReadInterval = setInterval(() => {
|
||||
// checkVisibleOutElements()
|
||||
// }, 2000)
|
||||
|
||||
//初始化设置观察者
|
||||
const options = {
|
||||
root: null,
|
||||
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||
rootMargin: '50px 0px'
|
||||
}
|
||||
observer = new IntersectionObserver(handleIntersection, options)
|
||||
|
||||
// 观察所有消息元素
|
||||
nextTick(() => {
|
||||
const messageElements = document.querySelectorAll('.message-item')
|
||||
messageElements.forEach((el) => {
|
||||
if (observer) {
|
||||
observer.observe(el)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
if (state.value.setMessageReadInterval) {
|
||||
clearInterval(state.value.setMessageReadInterval)
|
||||
state.value.setMessageReadInterval = null
|
||||
checkVisibleElements()
|
||||
}
|
||||
if (state.value.setOutMessageReadInterval) {
|
||||
clearInterval(state.value.setOutMessageReadInterval)
|
||||
state.value.setOutMessageReadInterval = null
|
||||
checkVisibleOutElements()
|
||||
}
|
||||
// 清理事件总线防抖定时器
|
||||
if (eventBusDebounceTimer) {
|
||||
clearTimeout(eventBusDebounceTimer)
|
||||
eventBusDebounceTimer = null
|
||||
}
|
||||
// 事件总线移除监听
|
||||
bus.unsubscribe('check-visible-out-elements', checkVisibleOutElements)
|
||||
})
|
||||
|
||||
//点击显示对应消息的已读回执详情
|
||||
const toShowMessageReadDetail = async (item?: ITalkRecord) => {
|
||||
if (item) {
|
||||
state.value.currentMsgReadDetail = item
|
||||
onReadTabChange('unread-tab')
|
||||
return
|
||||
}
|
||||
let params = {
|
||||
page: state.value.currentReadDetailPage,
|
||||
pageSize: 10,
|
||||
type: 'detail', //list是列表,detail是详情
|
||||
talkType: state?.value?.currentMsgReadDetail?.talk_type, //1私聊2群聊
|
||||
receiverId: state?.value?.currentMsgReadDetail?.receiver_id, //私聊的时候是对方用户id,群聊的时候是对方群id
|
||||
msgId: state?.value?.currentMsgReadDetail?.msg_id,
|
||||
isUnread: state?.value?.readDetailIsUnread //不送或者送0代表看已读,送1看未读
|
||||
}
|
||||
console.log(params)
|
||||
const resp = await ServeMessageReadDetail(params)
|
||||
console.log(resp)
|
||||
if (resp.code === 200) {
|
||||
console.log(resp?.data?.data?.length)
|
||||
if (resp?.data?.data?.length > 0) {
|
||||
state.value.hasMoreReadListDetail = true
|
||||
if (state.value.currentReadDetailPage === 1) {
|
||||
state.value.talkReadListDetail = resp.data.data
|
||||
} else {
|
||||
state.value.talkReadListDetail = [...state.value.talkReadListDetail, ...resp.data.data]
|
||||
}
|
||||
} else {
|
||||
if (state.value.currentReadDetailPage === 1) {
|
||||
state.value.talkReadListDetail = []
|
||||
}
|
||||
state.value.hasMoreReadListDetail = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//已读回执tab切换
|
||||
const onReadTabChange = (value) => {
|
||||
if (value === 'unread-tab') {
|
||||
//未读
|
||||
state.value.readDetailIsUnread = 1
|
||||
} else if (value === 'read-tab') {
|
||||
//已读
|
||||
state.value.readDetailIsUnread = 0
|
||||
}
|
||||
state.value.currentReadDetailPage = 1
|
||||
toShowMessageReadDetail()
|
||||
}
|
||||
|
||||
//加载更多已读回执
|
||||
const loadMoreReadListDetail = () => {
|
||||
console.log('loadMoreReadListDetail')
|
||||
if (!state.value.hasMoreReadListDetail || state.value.loadingReadListDetail) {
|
||||
return
|
||||
}
|
||||
state.value.loadingReadListDetail = true
|
||||
state.value.currentReadDetailPage++
|
||||
toShowMessageReadDetail().finally(() => {
|
||||
state.value.loadingReadListDetail = false
|
||||
})
|
||||
}
|
||||
|
||||
const onCustomSkipBottomEvent = () => {
|
||||
console.log('onCustomSkipBottomEvent')
|
||||
onLoad({ ...props, limit: 30 })
|
||||
// scrollToBottom()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -355,6 +742,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
v-for="(item, index) in records"
|
||||
:key="item.msg_id"
|
||||
:id="item.msg_id"
|
||||
:data-msgid="item.msg_id"
|
||||
:data-msgtype="item.msg_type"
|
||||
:data-userid="item.user_id"
|
||||
:data-talktype="props?.talk_type"
|
||||
:data-receiverid="props?.receiver_id"
|
||||
>
|
||||
<!-- 系统消息 -->
|
||||
<div v-if="item.msg_type >= 1000" class="message-box">
|
||||
@ -388,7 +780,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
>
|
||||
<!-- 多选按钮 -->
|
||||
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
|
||||
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
|
||||
<n-checkbox
|
||||
size="small"
|
||||
:checked="item.isCheck"
|
||||
@update:checked="item.isCheck = !item.isCheck"
|
||||
/>
|
||||
</aside>
|
||||
<!-- 头像信息 -->
|
||||
|
||||
@ -416,10 +812,8 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
|
||||
</div> -->
|
||||
<div class="talk-title">
|
||||
<span class="mr-7px"
|
||||
v-show="talk_type == 2 && item.float == 'left'"
|
||||
|
||||
>{{ item.nickname }}
|
||||
<span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'"
|
||||
>{{ item.nickname }}
|
||||
</span>
|
||||
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
|
||||
</div>
|
||||
@ -478,6 +872,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
{{ item.extra?.reply?.content }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 已读回执 -->
|
||||
<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">
|
||||
<template #trigger>
|
||||
<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
|
||||
}})
|
||||
</span>
|
||||
</template>
|
||||
<div class="talk-read-list-detail">
|
||||
<n-tabs
|
||||
type="line"
|
||||
animated
|
||||
justify-content="space-around"
|
||||
@update:value="onReadTabChange"
|
||||
>
|
||||
<n-tab name="unread-tab">
|
||||
{{ `未读(${props.num - 1 - (item.read_total_num || 0) || 0})` }}
|
||||
</n-tab>
|
||||
<n-tab name="read-tab">
|
||||
{{ `已读(${item.read_total_num || 0})` }}
|
||||
</n-tab>
|
||||
</n-tabs>
|
||||
<div class="talk-read-list">
|
||||
<n-infinite-scroll style="height: 340px;" @load="loadMoreReadListDetail">
|
||||
<div
|
||||
class="talk-read-list-item"
|
||||
v-for="(talkReadDetailItem,
|
||||
talkReadDetailIndex) in state.talkReadListDetail"
|
||||
:key="talkReadDetailIndex"
|
||||
>
|
||||
<avatarModule
|
||||
:mode="1"
|
||||
:avatar="talkReadDetailItem.avatar"
|
||||
:userName="talkReadDetailItem.nickName"
|
||||
:groupType="0"
|
||||
:customStyle="{
|
||||
width: '36px',
|
||||
height: '36px'
|
||||
}"
|
||||
:customTextStyle="{
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
lineHeight: '17px'
|
||||
}"
|
||||
></avatarModule>
|
||||
<div class="talk-read-list-item-info">
|
||||
<span style="font-size: 12px; font-weight: 600; line-height: 17px;">{{
|
||||
talkReadDetailItem.nickName
|
||||
}}</span>
|
||||
<span style="font-size: 12px; color: #999; line-height: 14px;">{{
|
||||
talkReadDetailItem.jobNum
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-infinite-scroll>
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -488,7 +948,7 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
</div>
|
||||
|
||||
<!-- 置底按钮 -->
|
||||
<SkipBottom v-model="skipBottom" />
|
||||
<SkipBottom v-model="skipBottom" :customSkipBottomEvent="true" @customSkipBottomEvent="onCustomSkipBottomEvent"/>
|
||||
</section>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
@ -676,6 +1136,17 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
}
|
||||
}
|
||||
|
||||
.talk_read_num {
|
||||
text-align: right;
|
||||
color: #7a58de;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
margin: 5px 0 0;
|
||||
span {
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.talk-title {
|
||||
opacity: 1;
|
||||
@ -724,4 +1195,31 @@ const onContextMenuAvatar=(e:any,item:any)=>{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.talk-read-list-detail {
|
||||
width: 341px;
|
||||
padding: 0 14px;
|
||||
|
||||
.talk-read-list {
|
||||
.talk-read-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
|
||||
.talk-read-list-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -3,20 +3,26 @@ import { useDialogueStore } from '@/store'
|
||||
import { DoubleDown } from '@icon-park/vue-next'
|
||||
import { scrollToBottom } from '@/utils/dom'
|
||||
|
||||
defineProps(['modelValue'])
|
||||
const props = defineProps(['modelValue', 'customSkipBottomEvent'])
|
||||
|
||||
const emit = defineEmits(['customSkipBottomEvent'])
|
||||
|
||||
const dialogueStore = useDialogueStore()
|
||||
|
||||
// 聊天版本滚动到底部
|
||||
const onSkipBottom = () => {
|
||||
console.log('onSkipBottom')
|
||||
scrollToBottom()
|
||||
if(props?.customSkipBottomEvent){
|
||||
emit('customSkipBottomEvent')
|
||||
}else{
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 置底按钮 -->
|
||||
<div class="skip-bottom pointer" :class="{ show: modelValue }" @click="onSkipBottom">
|
||||
<div class="skip-bottom pointer" :class="{ show: props?.modelValue }" @click="onSkipBottom">
|
||||
<span v-if="dialogueStore.unreadBubble">{{ dialogueStore.unreadBubble }} 条未读消息</span>
|
||||
<span v-else>回到底部</span>
|
||||
<n-icon size="14" color="#fff" :component="DoubleDown" />
|
||||
|
@ -33,6 +33,7 @@ export function useMenu() {
|
||||
const showDropdownMenu = (e: any, uid: number, item: any) => {
|
||||
// dropdown.item = Object.assign({}, item)
|
||||
dropdown.item = item
|
||||
dropdown.item.is_self_action = true
|
||||
dropdown.options = []
|
||||
if ([4].includes(item.msg_type)) {
|
||||
if(item.is_convert_text === 1){
|
||||
|
Loading…
Reference in New Issue
Block a user