Merge branch 'wyfMain-dev'

This commit is contained in:
wangyifeng 2025-05-29 18:10:46 +08:00
commit a86cdbf94a
15 changed files with 1056 additions and 111 deletions

View File

@ -94,3 +94,8 @@ export const ServeConfirmVoteHandle = (data = {}) => {
export const ServeEmptyMessage = (data) => { export const ServeEmptyMessage = (data) => {
return post('/api/v1/talk/message/empty', data) return post('/api/v1/talk/message/empty', data)
} }
//获取消息已读未读详情
export const ServeMessageReadDetail = (data) => {
return post('/api/v1/talk/my-records/read/condition', data)
}

View File

@ -5,6 +5,16 @@ export const ServeSeachQueryAll = (data = {}) => {
return post('/api/v1/elasticsearch/query-all', data) return post('/api/v1/elasticsearch/query-all', data)
} }
// ES搜索用户数据
export const ServeQueryUser = (data) => {
return post('/api/v1/elasticsearch/query-user', data)
}
// ES搜索群组数据
export const ServeQueryGroup = (data) => {
return post('/api/v1/elasticsearch/query-group', data)
}
//ES搜索聊天记录-主页搜索什么都有、聊天记录 //ES搜索聊天记录-主页搜索什么都有、聊天记录
export const ServeQueryTalkRecord = (data = {}) => { export const ServeQueryTalkRecord = (data = {}) => {
return post('/api/v1/elasticsearch/query-talk-record', data) return post('/api/v1/elasticsearch/query-talk-record', data)

View File

@ -11,13 +11,14 @@ import {
ServeSecedeGroup, ServeSecedeGroup,
ServeUpdateGroupCard, ServeUpdateGroupCard,
ServeGetGroupNotices, ServeGetGroupNotices,
ServeEditGroup ServeEditGroup,
ServeDismissGroup
} from '@/api/group' } from '@/api/group'
import { useInject } from '@/hooks' import { useInject } from '@/hooks'
import customModal from '@/components/common/customModal.vue' import customModal from '@/components/common/customModal.vue'
import avatarModule from '@/components/avatar-module/index.vue' import avatarModule from '@/components/avatar-module/index.vue'
import UserCardModal from '@/components/user/UserCardModal.vue' import UserCardModal from '@/components/user/UserCardModal.vue'
import { ServeEmptyMessage } from '@/api/chat' import { ServeEmptyMessage, ServeTopTalkList, ServeSetNotDisturb } from '@/api/chat'
import { parseTime } from '@/utils/datetime' import { parseTime } from '@/utils/datetime'
const userStore = useUserStore() const userStore = useUserStore()
@ -48,6 +49,12 @@ const props = defineProps({
} }
}) })
const talkParams = reactive({
isTop: computed(() => talkStore.findItem(props.talkType + '_' + props.gid)?.is_top),
isDisturb: computed(() => talkStore.findItem(props.talkType + '_' + props.gid)?.is_disturb),
sessionId: computed(() => talkStore.findItem(props.talkType + '_' + props.gid)?.id)
})
watch(props, () => { watch(props, () => {
if (props.talkType === 2) { if (props.talkType === 2) {
loadDetail() loadDetail()
@ -98,7 +105,8 @@ const state = reactive({
}, // }, //
editGroupName: false, // editGroupName: false, //
editGroupNameValue: '', // editGroupNameValue: '', //
chatSettingOperateType: '' // chatSettingOperateType: '', //
isLastAdmin: false //
}) })
const members = ref<any[]>([]) const members = ref<any[]>([])
@ -177,17 +185,36 @@ const onClose = () => {
emit('close') emit('close')
} }
const onSignOut = () => { const onSignOut = (closeLoading) => {
ServeSecedeGroup({ ServeSecedeGroup({
group_id: props.gid group_id: props.gid
}).then((res) => { })
.then((res) => {
closeLoading()
if (res.code == 200) { if (res.code == 200) {
window['$message'].success('已退出群聊') window['$message'].success('已退出群聊')
state.isShowModal = false
onClose() onClose()
} else { } else {
window['$message'].error(res.message) window['$message'].error(res.message || res.msg)
} }
}) })
.catch((err) => {
closeLoading()
window['$message'].error(err.message)
})
}
const onDismiss = async (closeLoading) => {
const { code, message } = await ServeDismissGroup({ group_id: props.gid })
closeLoading()
if (code === 200) {
onClose()
state.isShowModal = false
window['$message'].success('群聊已解散')
} else {
window['$message'].info(message)
}
} }
const onChangeRemark = () => { const onChangeRemark = () => {
@ -238,6 +265,16 @@ const handleModalConfirm = (closeLoading) => {
}) })
} else if (state.chatSettingOperateType == 'quit') { } else if (state.chatSettingOperateType == 'quit') {
//退 //退
if (state.isLastAdmin) {
//退
onDismiss(closeLoading)
} else {
//退
onSignOut(closeLoading)
}
} else if (state.chatSettingOperateType == 'dismiss') {
//
onDismiss(closeLoading)
} }
} }
@ -256,12 +293,15 @@ const showChatSettingOperateModal = (type: string) => {
break break
case 'quit': case 'quit':
state.chatSettingOperateHint = '确定退出群聊' state.chatSettingOperateHint = '确定退出群聊'
const findAdmin = groupMemberList.value.find((item) => item.leader === 2 || item.leader === 1) const findOtherAdmin = groupMemberList.value.find(
const isLastAdmin = findAdmin && findAdmin.user_id === userStore.uid (item) => (item.leader === 2 || item.leader === 1) && item.user_id !== userStore.uid
if (isLastAdmin) { )
state.chatSettingOperateSubHint = '退出后,本群将被解散' if (findOtherAdmin) {
} else { state.isLastAdmin = false
state.chatSettingOperateSubHint = '退出后,聊天记录将被清空' state.chatSettingOperateSubHint = '退出后,聊天记录将被清空'
} else {
state.isLastAdmin = true
state.chatSettingOperateSubHint = '退出后,本群将被解散'
} }
break break
} }
@ -380,6 +420,42 @@ const handleEditGroupNameConfirm = () => {
} }
}) })
} }
//
const onTopChange = (value: boolean) => {
ServeTopTalkList({
list_id: talkParams.sessionId,
type: value ? 1 : 2
}).then(({ code, message }) => {
if (code == 200) {
talkStore.updateItem({
index_name: props.talkType + '_' + props.gid,
is_top: talkParams.isTop == 0 ? 1 : 0
})
} else {
window['$message'].error(message)
}
})
}
//
const onDisturbChange = (value: boolean) => {
ServeSetNotDisturb({
talk_type: props.talkType,
receiver_id: props.gid,
is_disturb: value ? 1 : 0
}).then(({ code, message }) => {
if (code == 200) {
window['$message'].success('设置成功!')
talkStore.updateItem({
index_name: props.talkType + '_' + props.gid,
is_disturb: value ? 1 : 0
})
} else {
window['$message'].error(message)
}
})
}
</script> </script>
<template> <template>
<section class="el-container is-vertical section"> <section class="el-container is-vertical section">
@ -602,14 +678,14 @@ const handleEditGroupNameConfirm = () => {
<div class="b-box" style="margin: 16px 0 32px;"> <div class="b-box" style="margin: 16px 0 32px;">
<div class="block"> <div class="block">
<div class="title">置顶会话</div> <div class="title">置顶会话</div>
<n-switch /> <n-switch :value="talkParams.isTop === 1" @update:value="onTopChange" />
</div> </div>
</div> </div>
<div class="b-box" style="margin: 32px 0 20px;"> <div class="b-box" style="margin: 32px 0 20px;">
<div class="block"> <div class="block">
<div class="title">消息免打扰</div> <div class="title">消息免打扰</div>
<n-switch /> <n-switch :value="talkParams.isDisturb === 1" @update:value="onDisturbChange" />
</div> </div>
</div> </div>
</div> </div>
@ -733,9 +809,11 @@ const handleEditGroupNameConfirm = () => {
<template #content> <template #content>
<div class="custom-modal-content"> <div class="custom-modal-content">
<text>{{ state.chatSettingOperateHint }}</text> <text>{{ state.chatSettingOperateHint }}</text>
<text style="font-size: 16px; color: #999999; margin: 0; line-height: 22px;">{{ <text
state.chatSettingOperateSubHint style="font-size: 16px; color: #999999; margin: 0; line-height: 22px;"
}}</text> :style="{ color: state.isLastAdmin ? '#CF3050' : '' }"
>{{ state.chatSettingOperateSubHint }}</text
>
</div> </div>
</template> </template>
</customModal> </customModal>

View File

@ -327,7 +327,7 @@ import { parseTime } from '@/utils/datetime'
import { fileFormatSize, fileSuffix } from '@/utils/strings' import { fileFormatSize, fileSuffix } from '@/utils/strings'
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui' import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray']) const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray', 'hideSearchResultModal'])
const dialogueStore = useDialogueStore() const dialogueStore = useDialogueStore()
// //
@ -736,15 +736,29 @@ const downloadAndOpenFile = (item) => {
// //
const toDialogueByMember = async (msgInfo) => { const toDialogueByMember = async (msgInfo) => {
const sessionId = await getSessionId(dialogueParams.talk_type, dialogueParams.receiver_id) console.error('====跳转到对应的记录位置====', msgInfo)
uni.navigateTo({ // , sequence
url: dialogueStore.specifiedMsg = encodeURIComponent(
'/pages/dialog/index?sessionId=' + JSON.stringify({
sessionId + talk_type: msgInfo.talk_type,
'&keepDialogInfo=1' + receiver_id: msgInfo.receiver_id,
'&msgInfo=' + msg_id: msgInfo.msg_id,
encodeURIComponent(JSON.stringify(msgInfo)) cursor: msgInfo.sequence - 15 > 0 ? msgInfo.sequence - 15 : 0,
direction: 'down',
sort_sequence: 'asc',
create_time: msgInfo.created_at
}) })
)
emits('hideSearchResultModal')
// const sessionId = await getSessionId(dialogueParams.talk_type, dialogueParams.receiver_id)
// uni.navigateTo({
// url:
// '/pages/dialog/index?sessionId=' +
// sessionId +
// '&keepDialogInfo=1' +
// '&msgInfo=' +
// encodeURIComponent(JSON.stringify(msgInfo))
// })
} }
// //

View File

@ -41,7 +41,13 @@
> >
<searchItem <searchItem
@click="clickSearchItem(searchResultKey, item)" @click="clickSearchItem(searchResultKey, item)"
v-if="(props.listLimit && index < 3) || !props.listLimit" 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"
:searchResultKey="searchResultKey" :searchResultKey="searchResultKey"
:searchItem="item" :searchItem="item"
:searchText="state.searchText" :searchText="state.searchText"
@ -56,8 +62,14 @@
</div> </div>
<div <div
class="result-has-more" class="result-has-more"
v-if="getHasMoreResult(searchResultKey)" v-if="
@click="toMoreResultPage(searchResultKey)" getHasMoreResult(searchResultKey) &&
!(
(searchResultKey === 'user_infos' && state.userInfosExpand) ||
(searchResultKey === 'combinedGroup' && state.groupInfosExpand)
)
"
@click="onMoreResultClick(searchResultKey)"
> >
<span class="text-[14px] font-regular"> <span class="text-[14px] font-regular">
{{ getHasMoreResult(searchResultKey) }} {{ getHasMoreResult(searchResultKey) }}
@ -147,6 +159,7 @@
import { NInfiniteScroll } from 'naive-ui' import { NInfiniteScroll } from 'naive-ui'
import searchItem from './searchItem.vue' import searchItem from './searchItem.vue'
import { ref, reactive, defineEmits, defineProps, onMounted, watch } from 'vue' import { ref, reactive, defineEmits, defineProps, onMounted, watch } from 'vue'
import { ServeQueryUser, ServeQueryGroup } from '@/api/search'
const emits = defineEmits([ const emits = defineEmits([
'toMoreResultPage', 'toMoreResultPage',
@ -164,7 +177,16 @@ const state = reactive({
uid: 12303, //id uid: 12303, //id
clickStayItem: '', //item clickStayItem: '', //item
hasMore: true, // hasMore: true, //
loading: false // loading: false, //
userInfosExpand: false, //
userInfosLoading: false, //
userInfosLastId: undefined, // last_id
userInfosShowAll: false, // "" true
groupInfosExpand: false, //
groupInfosLoading: false, //
groupInfosLastGroupId: 0, // last_group_id
groupInfosLastMemberId: 0, // last_member_id
groupInfosShowAll: false // "" true
}) })
const props = defineProps({ const props = defineProps({
@ -214,7 +236,11 @@ const props = defineProps({
useCustomTitle: { useCustomTitle: {
type: Boolean, type: Boolean,
default: false default: false
} //使 }, //使
selectItemInList: {
type: String,
default: ''
} //
}) })
onMounted(() => { onMounted(() => {
@ -247,6 +273,13 @@ watch(
emits('clickStayItemChange', state.clickStayItem) emits('clickStayItemChange', state.clickStayItem)
// //
emits('lastIdChange', 0, 0, 0, '', '') emits('lastIdChange', 0, 0, 0, '', '')
state.userInfosExpand = false
state.userInfosShowAll = false
state.userInfosLastId = undefined
state.groupInfosExpand = false
state.groupInfosShowAll = false
state.groupInfosLastGroupId = 0
state.groupInfosLastMemberId = 0
queryAllSearch() queryAllSearch()
} }
) )
@ -384,6 +417,12 @@ const queryAllSearch = (doClearSearchResult) => {
} }
} }
state.pageNum = state.pageNum + 1 state.pageNum = state.pageNum + 1
// userInfosLastId
if (typeof data.last_id !== 'undefined') {
state.userInfosLastId = data.last_id
} else {
state.userInfosLastId = undefined
}
} else { } else {
if (state.pageNum === 1) { if (state.pageNum === 1) {
// //
@ -538,6 +577,13 @@ const clickSearchItem = (searchResultKey, searchItem) => {
// //
const doLoadMore = (doClearSearchResult) => { const doLoadMore = (doClearSearchResult) => {
if (
state.userInfosLoading ||
state.userInfosShowAll ||
state.groupInfosShowAll // queryAllSearch
) {
return
}
if (!state.hasMore || state.loading) { if (!state.hasMore || state.loading) {
return return
} }
@ -546,6 +592,134 @@ const doLoadMore = (doClearSearchResult) => {
state.loading = false state.loading = false
}) })
} }
watch(
() => props.selectItemInList,
(newVal, oldVal) => {
if (newVal) {
const selectedItem = JSON.parse(decodeURIComponent(newVal))
clickSearchItem('general_infos', selectedItem)
}
},
{
deep: true,
immediate: true
}
)
// last_id
async function loadMoreUserInfos() {
if (state.userInfosLoading) return
state.userInfosLoading = true
try {
let params = {
key: state.searchText,
last_id: state.userInfosLastId,
size: 10
}
const resp = await ServeQueryUser(params)
if (resp.code === 200 && Array.isArray(resp.data.user_infos)) {
if (!state.userInfosLastId) {
//
state.searchResult = {
...state.searchResult,
user_infos: resp.data.user_infos
}
} else {
//
state.searchResult = {
...state.searchResult,
user_infos: (state.searchResult.user_infos || []).concat(resp.data.user_infos)
}
}
state.userInfosLastId = resp.data.last_id
//
if (
!resp.data.last_id ||
(Array.isArray(resp.data.user_infos) && resp.data.user_infos.length < 10)
) {
state.userInfosExpand = true
}
}
} finally {
state.userInfosLoading = false
}
}
// "" ""
function onMoreResultClick(searchResultKey) {
if (searchResultKey === 'user_infos') {
state.userInfosShowAll = true
loadMoreUserInfos()
} else if (searchResultKey === 'combinedGroup') {
state.groupInfosShowAll = true
loadMoreGroupInfos()
} else {
emits('toMoreResultPage', searchResultKey, state.searchText)
}
}
// last_id
async function loadMoreGroupInfos() {
if (state.groupInfosLoading) return
state.groupInfosLoading = true
try {
let params = {
key: state.searchText,
last_group_id: state.groupInfosLastGroupId,
last_member_id: state.groupInfosLastMemberId,
size: 10
}
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 : []
// groupTempType
groupInfos.forEach(item => {
item.groupTempType = 'group_infos'
item.group_type = item.type //
})
groupMemberInfos.forEach(item => {
item.groupTempType = 'group_member_infos'
})
const isFirstLoad = (!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
(state.groupInfosLastGroupId === 0 && state.groupInfosLastMemberId === 0)
if (isFirstLoad) {
//
state.searchResult = {
...state.searchResult,
group_infos: groupInfos,
group_member_infos: groupMemberInfos,
combinedGroup: groupInfos.concat(groupMemberInfos)
}
} else {
//
const allGroupInfos = (state.searchResult.group_infos || []).concat(groupInfos)
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(groupMemberInfos)
state.searchResult = {
...state.searchResult,
group_infos: allGroupInfos,
group_member_infos: allGroupMemberInfos,
combinedGroup: allGroupInfos.concat(allGroupMemberInfos)
}
}
state.groupInfosLastGroupId = resp.data.last_group_id
state.groupInfosLastMemberId = resp.data.last_member_id
//
const noMoreData = (
(!groupInfos.length && !groupMemberInfos.length) ||
(resp.data.last_group_id === 0 && resp.data.last_member_id === 0)
)
if (noMoreData) {
state.groupInfosExpand = true
}
}
} finally {
state.groupInfosLoading = false
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-list { .search-list {

View File

@ -24,7 +24,7 @@ const { showUserInfoModal } = useInject()
<em v-show="index < extra.members.length - 1"></em> <em v-show="index < extra.members.length - 1"></em>
</template> </template>
<span>出群聊</span> <span>出群聊</span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -7,6 +7,7 @@ import EventTalk from './event/talk'
import EventKeyboard from './event/keyboard' import EventKeyboard from './event/keyboard'
import EventLogin from './event/login' import EventLogin from './event/login'
import EventRevoke from './event/revoke' import EventRevoke from './event/revoke'
import EventRead from './event/read'
import { getAccessToken, isLoggedIn } from './utils/auth' import { getAccessToken, isLoggedIn } from './utils/auth'
const urlCallback = () => { const urlCallback = () => {
@ -85,6 +86,8 @@ class Connect {
this.onImContactStatus() this.onImContactStatus()
this.onImMessageRevoke() this.onImMessageRevoke()
this.onImMessageKeyboard() this.onImMessageKeyboard()
this.onImMessageListenRead()
this.onImMessageListenReadIncr()
} }
onPing() { onPing() {
@ -131,6 +134,16 @@ class Connect {
this.conn.on('im.message.revoke', (data: any) => new EventRevoke(data)) this.conn.on('im.message.revoke', (data: any) => new EventRevoke(data))
} }
onImMessageListenRead() {
// 消息已读回执监听事件(全量)
this.conn.on('im.message.listen.read', (data: any) => new EventRead(data, 'total'))
}
onImMessageListenReadIncr() {
// 消息已读回执监听事件(增量)
this.conn.on('im.message.listen.read.incr', (data: any) => new EventRead(data, 'incr'))
}
onImContactApply() { onImContactApply() {
// 好友申请事件 // 好友申请事件
this.conn.on('im.contact.apply', (data: any) => { this.conn.on('im.contact.apply', (data: any) => {

54
src/event/read.js Normal file
View File

@ -0,0 +1,54 @@
import Base from './base'
import { useTalkStore, useDialogueStore } from '@/store'
import ws from '@/connect'
import { bus } from '@/utils/event-bus'
/**
* 已读回执事件
*/
class Read extends Base {
/**
* @var resource 资源
*/
resource
/**
* 场景类型
*/
type
/**
* 初始化构造方法
*
* @param {Object} resource Socket消息
*/
constructor(resource, type) {
super()
this.resource = resource
this.type = type
this.handle()
}
handle() {
if (this.type == 'total') {
console.error('====接收到了新版已读回执全量=====', this.resource)
const readList = this.resource.result
if (readList.length > 0) {
readList.forEach((item) => {
useDialogueStore().updateDialogueRecord({
msg_id: item.msg_id,
read_total_num: item.read_total_num
})
})
}
} else if (this.type == 'incr') {
console.error('====接收到了新版已读回执增量=====', this.resource)
// 由于直接使用增量的数值,会导致消息列表的已读回执数量不准确,可能多可能少
// 所以收到增量消息后,直接手动触发一次查询全量
bus.emit('check-visible-out-elements', 'incr')
}
}
}
export default Read

View File

@ -14,8 +14,9 @@ interface Params {
interface SpecialParams extends Params { interface SpecialParams extends Params {
msg_id?: string msg_id?: string
cursor?: number cursor?: number
direction?: 'up' | 'down', direction?: 'up' | 'down'
sort_sequence?: string sort_sequence?: string
type?: 'loadMore'
} }
interface LoadOptions { interface LoadOptions {
@ -38,7 +39,11 @@ export const useTalkRecord = (uid: number) => {
talk_type: 0, talk_type: 0,
status: 0, status: 0,
cursor: 0, cursor: 0,
specialParams: undefined as SpecialParams | undefined specialParams: undefined as SpecialParams | undefined,
isLocatingMessage: false,
isLoadingMore: false,
lastLoadMoreTime: 0,
targetMessagePosition: 0
}) })
// 重置 loadConfig // 重置 loadConfig
@ -153,13 +158,13 @@ export const useTalkRecord = (uid: number) => {
const getMinSequence = () => { const getMinSequence = () => {
console.error('records.value', records.value) console.error('records.value', records.value)
if (!records.value.length) return 0 if (!records.value.length) return 0
console.error(Math.min(...records.value.map(item => item.sequence))) console.error(Math.min(...records.value.map((item) => item.sequence)))
return Math.min(...records.value.map(item => item.sequence)) return Math.min(...records.value.map((item) => item.sequence))
} }
// 获取当前消息的最大 sequence // 获取当前消息的最大 sequence
const getMaxSequence = () => { const getMaxSequence = () => {
if (!records.value.length) return 0 if (!records.value.length) return 0
return Math.max(...records.value.map(item => item.sequence)) return Math.max(...records.value.map((item) => item.sequence))
} }
/** /**
@ -169,7 +174,10 @@ export const useTalkRecord = (uid: number) => {
*/ */
const onLoad = (params: Params, options?: LoadOptions) => { const onLoad = (params: Params, options?: LoadOptions) => {
// 如果会话切换,重置所有状态 // 如果会话切换,重置所有状态
if (params.talk_type !== loadConfig.talk_type || params.receiver_id !== loadConfig.receiver_id) { if (
params.talk_type !== loadConfig.talk_type ||
params.receiver_id !== loadConfig.receiver_id
) {
resetLoadConfig() resetLoadConfig()
} }
@ -189,34 +197,69 @@ export const useTalkRecord = (uid: number) => {
...params, ...params,
...options.specifiedMsg ...options.specifiedMsg
} }
//msg_id是用来做定位的不做参数所以这里清空
contextParams.msg_id = ''
ServeTalkRecords(contextParams).then(({ data, code }) => { ServeTalkRecords(contextParams).then(({ data, code }) => {
if (code !== 200) { if (code !== 200) {
loadConfig.status = 2 loadConfig.status = 2
return return
} }
// dialogueStore.clearDialogueRecord() // 记录当前滚动高度
const el = document.getElementById('imChatPanel')
const scrollHeight = el?.scrollHeight || 0
if (contextParams.direction === 'down' && !contextParams.type) {
dialogueStore.clearDialogueRecord()
}
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item)) const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
dialogueStore.unshiftDialogueRecord(contextParams.direction === 'down' ? items : items.reverse()) if (contextParams.type && contextParams.type === 'loadMore') {
loadConfig.status = items.length >= contextParams.limit ? 1 : 2 dialogueStore.addDialogueRecordForLoadMore(items)
} else {
dialogueStore.unshiftDialogueRecord(
contextParams.direction === 'down' ? items : items.reverse()
)
}
if (
contextParams.direction === 'up' ||
(contextParams.direction === 'down' && !contextParams.type)
) {
loadConfig.status = items[0].sequence == 1 || data.length === 0 ? 2 : 1
}
loadConfig.cursor = data.cursor loadConfig.cursor = data.cursor
nextTick(() => {
setTimeout(() => { // 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
requestAnimationFrame(() => {
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
const target = document.getElementById(options.specifiedMsg?.msg_id || '') const target = document.getElementById(options.specifiedMsg?.msg_id || '')
if (el && target) { if (el && target) {
// 如果是向上加载更多,保持原有内容位置
if (contextParams.direction === 'up') {
el.scrollTop = el.scrollHeight - scrollHeight
} else if (contextParams.type && contextParams.type === 'loadMore') {
// 如果是向下加载更多,保持目标消息在可视区域底部
// 使用可视区域高度来调整,而不是新内容的总高度
nextTick(() => {
if (el) {
el.scrollTop = scrollHeight - el.clientHeight
}
})
} else {
// 如果是定位到特定消息,计算并滚动到目标位置
const containerRect = el.getBoundingClientRect() const containerRect = el.getBoundingClientRect()
const targetRect = target.getBoundingClientRect() const targetRect = target.getBoundingClientRect()
const offset = targetRect.top - containerRect.top const offset = targetRect.top - containerRect.top
loadConfig.isLocatingMessage = true
// 居中 // 居中
const scrollTo = el.scrollTop + offset - el.clientHeight / 2 + target.clientHeight / 2 const scrollTo = el.scrollTop + offset - el.clientHeight / 2 + target.clientHeight / 2
el.scrollTo({ top: scrollTo, behavior: 'smooth' }) el.scrollTo({ top: scrollTo, behavior: 'smooth' })
addClass(target, 'border') addClass(target, 'border')
setTimeout(() => removeClass(target, 'border'), 3000) setTimeout(() => removeClass(target, 'border'), 3000)
} else if (el) { } else if (el) {
scrollToBottom() scrollToBottom()
} }
}, 50) } else if (el) {
el.scrollTop = el.scrollHeight
}
}) })
}) })
return return
@ -235,8 +278,10 @@ export const useTalkRecord = (uid: number) => {
// 判断是否是特殊参数模式 // 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') { if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
// 检查特殊参数是否与当前会话匹配 // 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type && if (
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) { loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) {
// 特殊参数模式下direction: 'up'cursor: 当前最小 sequence // 特殊参数模式下direction: 'up'cursor: 当前最小 sequence
onLoad( onLoad(
{ {
@ -249,7 +294,11 @@ export const useTalkRecord = (uid: number) => {
...loadConfig.specialParams, ...loadConfig.specialParams,
direction: 'up', direction: 'up',
sort_sequence: '', sort_sequence: '',
cursor: getMinSequence() cursor: getMinSequence(),
msg_id:
records.value.find((item) =>
item.sequence === getMinSequence() ? item.msg_id : ''
)?.msg_id || ''
} }
} }
) )
@ -273,13 +322,33 @@ export const useTalkRecord = (uid: number) => {
} }
} }
// 向下加载更多(兼容特殊参数模式) // 向下加载更多(特殊参数模式才生效,普通模式无效,因为普通模式的数据就是从最新开始加载历史的,所以不需要加载更新的数据
const onLoadMoreDown = () => { const onLoadMoreDown = () => {
// 判断是否是特殊参数模式 // 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') { if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
if (loadConfig.isLocatingMessage) {
loadConfig.isLocatingMessage = false
return
}
// 添加时间间隔限制,至少间隔 500ms 才能触发下一次加载
const now = Date.now()
if (now - loadConfig.lastLoadMoreTime < 500) {
return
}
loadConfig.lastLoadMoreTime = now
// 记录当前目标消息的位置
const el = document.getElementById('imChatPanel')
if (el) {
loadConfig.targetMessagePosition = el.scrollHeight - el.scrollTop
}
console.log('onLoadMoreDown 特殊模式触底了')
// 检查特殊参数是否与当前会话匹配 // 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type && if (
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) { loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) {
onLoad( onLoad(
{ {
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
@ -290,27 +359,23 @@ export const useTalkRecord = (uid: number) => {
specifiedMsg: { specifiedMsg: {
...loadConfig.specialParams, ...loadConfig.specialParams,
direction: 'down', direction: 'down',
cursor: getMaxSequence() sort_sequence: 'asc',
cursor: getMaxSequence(),
type: 'loadMore'
} }
} }
) )
} else {
// 如果不匹配,重置为普通模式
resetLoadConfig()
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
} }
} else {
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
} }
} }
return { loadConfig, records, onLoad, onRefreshLoad, onLoadMoreDown, onJumpMessage, resetLoadConfig } return {
loadConfig,
records,
onLoad,
onRefreshLoad,
onLoadMoreDown,
onJumpMessage,
resetLoadConfig
}
} }

View File

@ -151,6 +151,10 @@ export const useDialogueStore = defineStore('dialogue', {
unshiftDialogueRecord(records) { unshiftDialogueRecord(records) {
this.records.unshift(...records) this.records.unshift(...records)
}, },
//数组尾部加入更多对话记录
addDialogueRecordForLoadMore(records){
this.records.push(...records)
},
async getGroupInfo(){ async getGroupInfo(){
const { code, data } = await ServeGroupDetail({ const { code, data } = await ServeGroupDetail({
group_id: this.talk.receiver_id group_id: this.talk.receiver_id

View File

@ -49,8 +49,8 @@ export interface ITalkRecord {
send_status: number send_status: number
float: string, float: string,
is_convert_text?:number//语音记录的 是否是在展示转文本状态 1是 0否, is_convert_text?:number//语音记录的 是否是在展示转文本状态 1是 0否,
erp_user_id:number erp_user_id:number,
read_total_num:number
} }
export interface ITalkRecordExtraText { export interface ITalkRecordExtraText {

View File

@ -481,6 +481,12 @@ const onDatePickShow = (show) => {
// state.nowDateTime = new Date() // state.nowDateTime = new Date()
} }
} }
//
const hideSearchResultModal = () => {
handleSearchRecordByConditionModalClose()
state.isShowGroupAside = false
}
</script> </script>
<template> <template>
@ -506,6 +512,7 @@ const onDatePickShow = (show) => {
:receiver_id="talkParams.receiver_id" :receiver_id="talkParams.receiver_id"
:index_name="talkParams.index_name" :index_name="talkParams.index_name"
:specifiedMsg="talkParams.specifiedMsg" :specifiedMsg="talkParams.specifiedMsg"
:num="talkParams.num"
/> />
</main> </main>
@ -706,6 +713,7 @@ const onDatePickShow = (show) => {
@getDisabledDateArray="getDisabledDateArray" @getDisabledDateArray="getDisabledDateArray"
:selectedDateTime="state.selectedDateTime" :selectedDateTime="state.selectedDateTime"
:nowDateTime="state.nowDateTime" :nowDateTime="state.nowDateTime"
@hideSearchResultModal="hideSearchResultModal"
/> />
</div> </div>
</n-card> </n-card>

View File

@ -74,6 +74,24 @@ const renderChatAppSearch = () => {
console.log(searchText, searchResultKey, talk_type, receiver_id) console.log(searchText, searchResultKey, talk_type, receiver_id)
const result = JSON.parse(decodeURIComponent(res)) const result = JSON.parse(decodeURIComponent(res))
console.log(result) console.log(result)
if (searchResultKey === 'general_infos') {
state.ServeQueryTalkRecordParams = encodeURIComponent(
JSON.stringify({
talk_type: 0, //12
receiver_id: 0, //
last_group_id: 0, //id
last_member_id: 0, //id
last_receiver_user_name: '', //
last_receiver_group_name: '' //
})
)
state.isShowSearchRecordModal = true
state.searchRecordText = searchText
state.selectItemInList = res
} else {
talkStore.toTalk(talk_type, receiver_id, router)
}
state.showSearchDropdown = false
}, },
onToMoreResultPage: (searchResultKey, searchText) => { onToMoreResultPage: (searchResultKey, searchText) => {
if (searchResultKey === 'general_infos') { if (searchResultKey === 'general_infos') {
@ -277,7 +295,9 @@ const state = reactive({
apiParams: '', apiParams: '',
lastId: undefined as any, lastId: undefined as any,
total: 0 total: 0
} },
showSearchDropdown: false, //
selectItemInList: '' //
}) })
const items = computed((): ISession[] => { const items = computed((): ISession[] => {
@ -292,9 +312,9 @@ const items = computed((): ISession[] => {
// //
const topItems = filtered const topItems = filtered
.filter(item => item.is_top === 1) .filter((item) => item.is_top === 1)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
const normalItems = filtered.filter(item => item.is_top !== 1) const normalItems = filtered.filter((item) => item.is_top !== 1)
return [...topItems, ...normalItems] return [...topItems, ...normalItems]
}) })
@ -589,8 +609,8 @@ const handleGroupChatListPaginationSize = (value) => {
// //
const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_id, res) => { const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_id, res) => {
console.log(searchText, searchResultKey, talk_type, receiver_id) console.log(searchText, searchResultKey, talk_type, receiver_id)
const result = JSON.parse(decodeURIComponent(res)) // const result = JSON.parse(decodeURIComponent(res))
console.log(result) // console.log(result)
if (searchResultKey === 'general_infos') { if (searchResultKey === 'general_infos') {
// //
state.isShowSearchRecordDetailInfo = false state.isShowSearchRecordDetailInfo = false
@ -722,6 +742,8 @@ const handleEnterSearchResultChat = () => {
trigger="click" trigger="click"
:options="state.chatSearchOptions" :options="state.chatSearchOptions"
style="width: 248px; height: 677px;" style="width: 248px; height: 677px;"
:show="state.showSearchDropdown"
@clickoutside="state.showSearchDropdown = false"
> >
<n-input <n-input
placeholder="搜索好友 / 群聊" placeholder="搜索好友 / 群聊"
@ -729,6 +751,7 @@ const handleEnterSearchResultChat = () => {
round round
clearable clearable
style="width: 78%;" style="width: 78%;"
@click="state.showSearchDropdown = true"
> >
<template #prefix> <template #prefix>
<n-icon :component="Search" /> <n-icon :component="Search" />
@ -950,6 +973,7 @@ const handleEnterSearchResultChat = () => {
@clickStayItemChange="handleClickStayItemChange" @clickStayItemChange="handleClickStayItemChange"
@lastIdChange="handleSearchListLastIdChange" @lastIdChange="handleSearchListLastIdChange"
:searchResultMaxHeight="'517px'" :searchResultMaxHeight="'517px'"
:selectItemInList="state.selectItemInList"
></chatAppSearchList> ></chatAppSearchList>
</div> </div>
<div class="search-record-detail"> <div class="search-record-detail">

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { watch, onMounted, ref, nextTick } from 'vue' import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
import { NDropdown, NCheckbox } from 'naive-ui' import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next' import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
import { bus } from '@/utils/event-bus' import { bus } from '@/utils/event-bus'
import { useDialogueStore } from '@/store' import { useDialogueStore } from '@/store'
@ -16,8 +16,40 @@ import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue' 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 RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
import { voiceToText } from '@/api/chat.js' import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
import { confirmBox } from '@/components/confirm-box/service.js' import { confirmBox } from '@/components/confirm-box/service.js'
import ws from '@/connect'
import 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({ const props = defineProps({
uid: { uid: {
type: Number, type: Number,
@ -38,10 +70,14 @@ const props = defineProps({
specifiedMsg: { specifiedMsg: {
type: String, type: String,
default: '' 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 uploadsStore = useUploadsStore()
const { useMessage } = useUtil() const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu() const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
@ -90,11 +126,33 @@ const onPanelScroll = (e: any) => {
} }
const height = e.target.scrollTop + e.target.clientHeight 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) { if (!skipBottom.value && dialogueStore.unreadBubble) {
dialogueStore.setUnreadBubble(0) 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) { if (skipBottom.value == false) {
let len = dialogueStore.records.length let len = dialogueStore.records.length
@ -231,7 +289,6 @@ const onClickNickname = (data: ITalkRecord) => {
// //
const onContextMenu = (e: any, item: ITalkRecord) => { const onContextMenu = (e: any, item: ITalkRecord) => {
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) { if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
return e.preventDefault() return e.preventDefault()
} }
@ -294,7 +351,10 @@ watch(
try { try {
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg)) const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
// id // 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 specialParams = parsed
} }
} catch (e) {} } catch (e) {}
@ -332,6 +392,330 @@ const onContextMenuAvatar=(e:any,item:any)=>{
} }
} }
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', //listdetail
talkType: state?.value?.currentMsgReadDetail?.talk_type, //12
receiverId: state?.value?.currentMsgReadDetail?.receiver_id, //idid
msgId: state?.value?.currentMsgReadDetail?.msg_id,
isUnread: state?.value?.readDetailIsUnread //01
}
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
})
}
</script> </script>
<template> <template>
@ -353,6 +737,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
v-for="(item, index) in records" v-for="(item, index) in records"
:key="item.msg_id" :key="item.msg_id"
:id="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"> <div v-if="item.msg_type >= 1000" class="message-box">
@ -386,7 +775,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
> >
<!-- 多选按钮 --> <!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0"> <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> </aside>
<!-- 头像信息 --> <!-- 头像信息 -->
@ -414,9 +807,7 @@ const onContextMenuAvatar=(e:any,item:any)=>{
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div> --> </div> -->
<div class="talk-title"> <div class="talk-title">
<span class="mr-7px" <span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'"
v-show="talk_type == 2 && item.float == 'left'"
>{{ item.nickname }} >{{ item.nickname }}
</span> </span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
@ -476,6 +867,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{
{{ item.extra?.reply?.content }} {{ item.extra?.reply?.content }}
</span> </span>
</div> </div>
<!-- 已读回执 -->
<div class="talk_read_num" v-if="item.user_id === props.uid">
<n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;">
<template #trigger>
<span v-if="props.talk_type === 1">{{
item.read_total_num > 0 ? '已读' : '未读'
}}</span>
<span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)">
已读 ({{ 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> </main>
</div> </div>
@ -674,6 +1131,18 @@ 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 {
cursor: pointer;
}
}
&:hover { &:hover {
.talk-title { .talk-title {
opacity: 1; opacity: 1;
@ -722,4 +1191,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> </style>

View File

@ -61,11 +61,11 @@ const onSendMessage = (data = {}, callBack: any) => {
} }
ServePublishMessage(message) ServePublishMessage(message)
.then(({ code, message }) => { .then(({ code, message, msg }) => {
if (code == 200) { if (code == 200) {
callBack(true) callBack(true)
} else { } else {
window['$message'].warning(message) window['$message'].warning(message || msg)
} }
}) })
.catch(() => { .catch(() => {