Merge branch 'main' of http://172.16.100.91:3000/scout666/chat-pc
# Conflicts: # src/views/message/inner/panel/PanelFooter.vue resolved by main version
This commit is contained in:
commit
2518f77f9c
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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) => {
|
|
||||||
if (res.code == 200) {
|
|
||||||
window['$message'].success('已退出群聊')
|
|
||||||
onClose()
|
|
||||||
} else {
|
|
||||||
window['$message'].error(res.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.then((res) => {
|
||||||
|
closeLoading()
|
||||||
|
if (res.code == 200) {
|
||||||
|
window['$message'].success('已退出群聊')
|
||||||
|
state.isShowModal = false
|
||||||
|
onClose()
|
||||||
|
} else {
|
||||||
|
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 = () => {
|
||||||
@ -236,8 +263,18 @@ const handleModalConfirm = (closeLoading) => {
|
|||||||
closeLoading()
|
closeLoading()
|
||||||
window['$message'].error(err.message)
|
window['$message'].error(err.message)
|
||||||
})
|
})
|
||||||
} 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>
|
||||||
|
@ -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))
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
//重置部分搜索条件
|
//重置部分搜索条件
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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
54
src/event/read.js
Normal 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
|
@ -94,7 +94,7 @@ class Talk extends Base {
|
|||||||
useSettingsStore().isPromptTone && palyMusic()
|
useSettingsStore().isPromptTone && palyMusic()
|
||||||
}
|
}
|
||||||
|
|
||||||
handle() {
|
async handle() {
|
||||||
// 不是自己发送的消息则需要播放提示音
|
// 不是自己发送的消息则需要播放提示音
|
||||||
if (!this.isCurrSender()) {
|
if (!this.isCurrSender()) {
|
||||||
this.play()
|
this.play()
|
||||||
@ -102,7 +102,18 @@ class Talk extends Base {
|
|||||||
|
|
||||||
// 判断会话列表是否存在,不存在则创建
|
// 判断会话列表是否存在,不存在则创建
|
||||||
if (useTalkStore().findTalkIndex(this.getIndexName()) == -1) {
|
if (useTalkStore().findTalkIndex(this.getIndexName()) == -1) {
|
||||||
return this.addTalkItem()
|
if (this.resource.msg_type == 1102) {
|
||||||
|
//被邀请进入群聊时,需要热更新会话列表
|
||||||
|
await useTalkStore().loadTalkList()
|
||||||
|
} else if (this.resource.msg_type == 1106) {
|
||||||
|
//群解散时,需要热更新会话列表
|
||||||
|
await useTalkStore().loadTalkList()
|
||||||
|
} else if (this.resource.msg_type == 1104 || this.resource.msg_type == 1115) {
|
||||||
|
//群成员被移出时,需要热更新会话列表
|
||||||
|
await useTalkStore().loadTalkList()
|
||||||
|
} else {
|
||||||
|
return this.addTalkItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断当前是否正在和好友对话
|
// 判断当前是否正在和好友对话
|
||||||
@ -169,10 +180,42 @@ class Talk extends Base {
|
|||||||
let record = this.resource
|
let record = this.resource
|
||||||
|
|
||||||
// 群成员变化的消息,需要更新群成员列表
|
// 群成员变化的消息,需要更新群成员列表
|
||||||
if ([1102, 1103, 1104].includes(record.msg_type)) {
|
if ([1102, 1103, 1104, 1115].includes(record.msg_type)) {
|
||||||
useDialogueStore().updateGroupMembers()
|
useDialogueStore().updateGroupMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//群解散时,需要更新群成员权限
|
||||||
|
if ([1106].includes(record.msg_type)) {
|
||||||
|
useDialogueStore().updateDismiss(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
//群成员被移出时,需要更新群成员权限
|
||||||
|
if ([1104, 1115].includes(record.msg_type)) {
|
||||||
|
if (this.resource?.extra?.members?.length > 0) {
|
||||||
|
const isMeQuit = this.resource.extra.members.find(
|
||||||
|
(item) => item.user_id === this.getAccountId()
|
||||||
|
)
|
||||||
|
if (isMeQuit) {
|
||||||
|
useDialogueStore().updateQuit(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群信息变更时,需要更新群信息
|
||||||
|
if ([1116].includes(record.msg_type)) {
|
||||||
|
// 更新会话信息
|
||||||
|
useDialogueStore().updateDialogueTalk({
|
||||||
|
username: record.extra.group_name,
|
||||||
|
avatar: record.extra.group_avatar
|
||||||
|
})
|
||||||
|
//更新会话列表中的会话信息
|
||||||
|
useTalkStore().updateItem({
|
||||||
|
index_name: this.getIndexName(),
|
||||||
|
name: record.extra.group_name,
|
||||||
|
avatar: record.extra.group_avatar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useDialogueStore().addDialogueRecord(formatTalkRecord(this.getAccountId(), this.resource))
|
useDialogueStore().addDialogueRecord(formatTalkRecord(this.getAccountId(), this.resource))
|
||||||
|
|
||||||
if (!this.isCurrSender()) {
|
if (!this.isCurrSender()) {
|
||||||
@ -221,6 +264,21 @@ class Talk extends Base {
|
|||||||
msg_text: this.getTalkText(),
|
msg_text: this.getTalkText(),
|
||||||
updated_at: parseTime(new Date())
|
updated_at: parseTime(new Date())
|
||||||
})
|
})
|
||||||
|
//收到新消息时,同时判断是否有人@我
|
||||||
|
if (this.resource.msg_type === 1 && this.resource?.extra?.mentions?.length > 0) {
|
||||||
|
const findMention = this.resource?.extra?.mentions?.find(
|
||||||
|
(mention) => mention === this.getAccountId()
|
||||||
|
)
|
||||||
|
//有人@我或者@所有人,则更新会话列表
|
||||||
|
if (findMention || this.resource?.extra?.mentions?.includes(0)) {
|
||||||
|
// useTalkStore().loadTalkList()
|
||||||
|
useTalkStore().updateItem({
|
||||||
|
index_name: this.getIndexName(),
|
||||||
|
msg_text: this.getTalkText(),
|
||||||
|
atsign_num: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -137,6 +142,11 @@ export const useTalkRecord = (uid: number) => {
|
|||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
if (request.cursor == 0) {
|
if (request.cursor == 0) {
|
||||||
|
// el.scrollTop = el.scrollHeight
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// el.scrollTop = el.scrollHeight + 1000
|
||||||
|
// }, 500)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
} else {
|
} else {
|
||||||
el.scrollTop = el.scrollHeight - scrollHeight
|
el.scrollTop = el.scrollHeight - scrollHeight
|
||||||
@ -153,13 +163,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 +179,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 +202,68 @@ 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 来确保在下一帧渲染前设置滚动位置
|
||||||
const el = document.getElementById('imChatPanel')
|
requestAnimationFrame(() => {
|
||||||
const target = document.getElementById(options.specifiedMsg?.msg_id || '')
|
const el = document.getElementById('imChatPanel')
|
||||||
if (el && target) {
|
const target = document.getElementById(options.specifiedMsg?.msg_id || '')
|
||||||
|
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) {
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
}
|
||||||
}, 50)
|
} else if (el) {
|
||||||
|
// el.scrollTop = el.scrollHeight
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -235,8 +282,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 +298,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 +326,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 +363,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,12 @@ export const useDialogueStore = defineStore('dialogue', {
|
|||||||
// 是否显示编辑器
|
// 是否显示编辑器
|
||||||
isShowEditor: false,
|
isShowEditor: false,
|
||||||
|
|
||||||
|
//是否已被解散
|
||||||
|
isDismiss: false,
|
||||||
|
|
||||||
|
//是否退群/移出群
|
||||||
|
isQuit: false,
|
||||||
|
|
||||||
// 是否显示会话列表
|
// 是否显示会话列表
|
||||||
isShowSessionList: true,
|
isShowSessionList: true,
|
||||||
groupInfo: {} ,
|
groupInfo: {} ,
|
||||||
@ -80,6 +86,16 @@ export const useDialogueStore = defineStore('dialogue', {
|
|||||||
this.online = status
|
this.online = status
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 更新群解散状态
|
||||||
|
updateDismiss(isDismiss) {
|
||||||
|
this.isDismiss = isDismiss
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新群成员退出状态
|
||||||
|
updateQuit(isQuit) {
|
||||||
|
this.isQuit = isQuit
|
||||||
|
},
|
||||||
|
|
||||||
// 更新对话信息
|
// 更新对话信息
|
||||||
setDialogue(data = {}) {
|
setDialogue(data = {}) {
|
||||||
this.online = data.is_online == 1
|
this.online = data.is_online == 1
|
||||||
@ -95,6 +111,9 @@ export const useDialogueStore = defineStore('dialogue', {
|
|||||||
this.records = []
|
this.records = []
|
||||||
this.unreadBubble = 0
|
this.unreadBubble = 0
|
||||||
this.isShowEditor = data?.is_robot === 0
|
this.isShowEditor = data?.is_robot === 0
|
||||||
|
|
||||||
|
this.isDismiss = data?.is_dismiss === 1 ? true : false
|
||||||
|
this.isQuit = data?.is_quit === 1 ? true : false
|
||||||
|
|
||||||
// 只在手动切换会话时清空 specifiedMsg
|
// 只在手动切换会话时清空 specifiedMsg
|
||||||
// if (this.isManualSwitch) {
|
// if (this.isManualSwitch) {
|
||||||
@ -151,6 +170,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
|
||||||
@ -288,6 +311,11 @@ export const useDialogueStore = defineStore('dialogue', {
|
|||||||
record.extra.url = videoInfo.url
|
record.extra.url = videoInfo.url
|
||||||
// record.extra.cover = videoInfo.cover
|
// record.extra.cover = videoInfo.cover
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新会话信息
|
||||||
|
updateDialogueTalk(params){
|
||||||
|
Object.assign(this.talk, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
|
@ -39,6 +39,8 @@ export function formatTalkItem(params) {
|
|||||||
is_top: 0,
|
is_top: 0,
|
||||||
is_online: 0,
|
is_online: 0,
|
||||||
is_robot: 0,
|
is_robot: 0,
|
||||||
|
is_dismiss: 0,
|
||||||
|
is_quit: 0,
|
||||||
unread_num: 0,
|
unread_num: 0,
|
||||||
content: '......',
|
content: '......',
|
||||||
draft_text: '',
|
draft_text: '',
|
||||||
|
@ -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>
|
||||||
|
@ -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, //1私聊2群聊
|
||||||
|
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') {
|
||||||
@ -88,8 +106,10 @@ const renderChatAppSearch = () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
state.isShowSearchRecordModal = true
|
state.isShowSearchRecordModal = true
|
||||||
|
state.searchRecordText = searchText
|
||||||
}
|
}
|
||||||
console.log(searchResultKey, searchText)
|
console.log(searchResultKey, searchText)
|
||||||
|
state.showSearchDropdown = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -277,7 +297,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 +314,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]
|
||||||
})
|
})
|
||||||
@ -379,7 +401,8 @@ const onTabTalk = (item: ISession, follow = false) => {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
talkStore.updateItem({
|
talkStore.updateItem({
|
||||||
index_name: item.index_name,
|
index_name: item.index_name,
|
||||||
unread_num: 0
|
unread_num: 0,
|
||||||
|
atsign_num: 0
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -589,8 +612,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
|
||||||
@ -628,6 +651,9 @@ const handleClickSearchResultItem = (searchText, searchResultKey, talk_type, rec
|
|||||||
)
|
)
|
||||||
console.error(dialogueStore.specifiedMsg, 'dialogueStore.specifiedMsg')
|
console.error(dialogueStore.specifiedMsg, 'dialogueStore.specifiedMsg')
|
||||||
talkStore.toTalk(talk_type, receiver_id, router)
|
talkStore.toTalk(talk_type, receiver_id, router)
|
||||||
|
state.isShowSearchRecordModal = false
|
||||||
|
state.searchRecordText = ''
|
||||||
|
searchKeyword.value = ''
|
||||||
}
|
}
|
||||||
//处理点击停留item变化
|
//处理点击停留item变化
|
||||||
const handleClickStayItemChange = (item) => {
|
const handleClickStayItemChange = (item) => {
|
||||||
@ -722,24 +748,26 @@ 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="搜索好友 / 群聊"
|
||||||
v-model:value.trim="searchKeyword"
|
v-model:value.trim="searchKeyword"
|
||||||
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" />
|
||||||
</template>
|
</template> -->
|
||||||
</n-input>
|
</n-input>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
<n-button circle @click="isShowGroup = true">
|
<!-- <n-button circle @click="isShowGroup = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="Plus" />
|
<n-icon :component="Plus" />
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button> -->
|
||||||
<img
|
<img
|
||||||
style="width: 19px; height: 20px; cursor: pointer;"
|
style="width: 19px; height: 20px; cursor: pointer;"
|
||||||
src="@/assets/image/chatList/addressBook.png"
|
src="@/assets/image/chatList/addressBook.png"
|
||||||
@ -950,6 +978,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">
|
||||||
|
@ -51,6 +51,7 @@ const labelColor=[
|
|||||||
<span class="detail" v-html="data.draft_text" />
|
<span class="detail" v-html="data.draft_text" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<span class="draft" v-show="data.talk_type == 2 && data.atsign_num"> [有人@你] </span>
|
||||||
<span class="online" v-show="data.talk_type == 1 && data.is_online == 1"> [在线] </span>
|
<span class="online" v-show="data.talk_type == 1 && data.is_online == 1"> [在线] </span>
|
||||||
<span class="detail" v-html="data.msg_text" />
|
<span class="detail" v-html="data.msg_text" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -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'
|
||||||
@ -14,10 +14,42 @@ import { ITalkRecord } from '@/types/chat'
|
|||||||
import { EditorConst } from '@/constant/event-bus'
|
import { EditorConst } from '@/constant/event-bus'
|
||||||
import { useInject, useTalkRecord, useUtil } from '@/hooks'
|
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) {}
|
||||||
@ -312,26 +372,350 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// onMounted(() => {
|
// onMounted(() => {
|
||||||
// onLoad({ ...props, limit: 30 })
|
// onLoad({ ...props, limit: 30 })
|
||||||
// })
|
// })
|
||||||
const retry=(item:any)=>{
|
const retry = (item: any) => {
|
||||||
confirmBox({
|
confirmBox({
|
||||||
content:'确定重发吗'
|
content: '确定重发吗'
|
||||||
}).then(()=>{
|
}).then(() => {
|
||||||
uploadsStore.retryCommonUpload(item.extra.upload_id)
|
uploadsStore.retryCommonUpload(item.extra.upload_id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onContextMenuAvatar=(e:any,item:any)=>{
|
const onContextMenuAvatar = (e: any, item: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if(item.float!=='right'){
|
if (item.float !== 'right') {
|
||||||
bus.emit(EditorConst.Mention, {
|
bus.emit(EditorConst.Mention, {
|
||||||
id: item.user_id,
|
id: item.user_id,
|
||||||
value: item.nickname
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
</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,10 +807,8 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
@ -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>
|
||||||
|
@ -19,7 +19,7 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
avatar:{
|
avatar: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
@ -46,19 +46,22 @@ const onSetMenu = () => {
|
|||||||
/>
|
/>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<avatarModule class="mr-10px" :mode="dialogueStore.talk.talk_type"
|
<avatarModule
|
||||||
:avatar="avatar"
|
class="mr-10px"
|
||||||
:groupType="dialogueStore.talk?.group_type"
|
:mode="dialogueStore.talk.talk_type"
|
||||||
:userName="username" :customStyle="{width:'42px',height:'42px'}"></avatarModule>
|
:avatar="avatar"
|
||||||
|
:groupType="dialogueStore.talk?.group_type"
|
||||||
|
:userName="username"
|
||||||
|
:customStyle="{ width: '42px', height: '42px' }"
|
||||||
|
></avatarModule>
|
||||||
<div class="module left-module">
|
<div class="module left-module">
|
||||||
<!-- <span class="tag" :class="{ red: type == 1 }">
|
<!-- <span class="tag" :class="{ red: type == 1 }">
|
||||||
{{ type == 1 ? '好友' : '群聊' }}
|
{{ type == 1 ? '好友' : '群聊' }}
|
||||||
</span> -->
|
</span> -->
|
||||||
<span class="nickname">{{ username }}</span>
|
<span class="nickname">{{ username }}</span>
|
||||||
<span class="num" v-show="type == 2 && num">({{ num }})</span>
|
<span class="num" v-show="type == 2 && num">({{ num }})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="module center-module" v-if="type == 1">
|
<div class="module center-module" v-if="type == 1">
|
||||||
<p class="online">
|
<p class="online">
|
||||||
@ -80,8 +83,13 @@ const onSetMenu = () => {
|
|||||||
:size="18"
|
:size="18"
|
||||||
class="icon"
|
class="icon"
|
||||||
@click="emit('evnet', 'group')"
|
@click="emit('evnet', 'group')"
|
||||||
|
v-show="!dialogueStore.isDismiss && !dialogueStore.isQuit"
|
||||||
>
|
>
|
||||||
<img style="width: 20px; height: 20px;" src="@/assets/image/chatList/chat-settings.png" alt="" />
|
<img
|
||||||
|
style="width: 20px; height: 20px;"
|
||||||
|
src="@/assets/image/chatList/chat-settings.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -156,7 +164,7 @@ const onSetMenu = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&.color {
|
&.color {
|
||||||
color: #462AA0;
|
color: #462aa0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.online-status {
|
.online-status {
|
||||||
@ -168,7 +176,7 @@ const onSetMenu = () => {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #462AA0;
|
background-color: #462aa0;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
@ -177,7 +185,7 @@ const onSetMenu = () => {
|
|||||||
left: -1px;
|
left: -1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid #462AA0;
|
border: 1px solid #462aa0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-animation: antStatusProcessing 1.2s ease-in-out infinite;
|
-webkit-animation: antStatusProcessing 1.2s ease-in-out infinite;
|
||||||
animation: antStatusProcessing 1.2s ease-in-out infinite;
|
animation: antStatusProcessing 1.2s ease-in-out infinite;
|
||||||
|
Loading…
Reference in New Issue
Block a user