Compare commits

...

2 Commits
xingyy ... main

3 changed files with 530 additions and 31 deletions

View File

@ -191,7 +191,7 @@ const handleGroupNoticeModalConfirm = () => {
state.isShowNoticeHintModal = true
if (state?.groupNoticeInfo?.id && !state.groupNoticeInEdit) {
//
state.noticeHintModalContent = '确定清空群公告吗?'
state.noticeHintModalContent = '确定清空群公告'
state.noticeHintModalActionBtns = {
confirmBtn: {
text: '清空',
@ -596,7 +596,7 @@ const hideSearchResultModal = () => {
<n-input
type="text"
v-model:value="state.searchRecordByConditionText"
placeholder="请输入"
:placeholder="state.conditionTag && state.conditionTag !== 'all'?'':'请输入'"
clearable
>
<template #clear-icon>

View File

@ -448,6 +448,11 @@ onMounted(() => {
const showAddressBookModal = () => {
state.isShowAddressBookModal = true
}
//
const closeAddressBookModal = () => {
state.isShowAddressBookModal = false
resetAddressBookModal()
}
const handleTreeClick = ({ selectedKey, tree }) => {
// console.log(tree)
state.clickKey = tree.key
@ -840,6 +845,8 @@ const handleEnterSearchResultChat = () => {
:style="state.customModalStyle"
:customCloseBtn="true"
:closable="false"
:customCloseEvent="true"
@customCloseModal="closeAddressBookModal"
>
<template #content>
<div class="custom-modal-content">

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { watch, onMounted, ref, nextTick } from 'vue'
import { NDropdown, NCheckbox } from 'naive-ui'
import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
import { bus } from '@/utils/event-bus'
import { useDialogueStore } from '@/store'
@ -14,10 +14,42 @@ import { ITalkRecord } from '@/types/chat'
import { EditorConst } from '@/constant/event-bus'
import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue'
import { useUserStore ,useUploadsStore} from '@/store'
import { useUserStore, useUploadsStore } from '@/store'
import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
import { voiceToText } from '@/api/chat.js'
import {confirmBox} from '@/components/confirm-box/service.js'
import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
import { confirmBox } from '@/components/confirm-box/service.js'
import ws from '@/connect'
import avatarModule from '@/components/avatar-module/index.vue'
//
interface ReadStatus {
msg_ids: string[]
talk_type: number
receiver_id: number
user_id?: number
}
//
interface State {
visibleElements: Set<HTMLElement>
visibleOutElements: Set<HTMLElement>
tempWaitDoRead: ReadStatus[]
tempWaitDoCheck: ReadStatus[]
setMessageReadInterval: number | null
setOutMessageReadInterval: number | null
lastUpdateTime: number
isScrolling: boolean
scrollTimer: number | null
lastTriggerTime: number
talkReadListDetail: any[]
readDetailIsUnread: number
currentMsgReadDetail: any | null
currentReadDetailPage: number
hasMoreReadListDetail: boolean
loadingReadListDetail: boolean
lastScrollTop: number
}
const props = defineProps({
uid: {
type: Number,
@ -38,10 +70,14 @@ const props = defineProps({
specifiedMsg: {
type: String,
default: ''
},
num: {
type: Number,
default: 0
}
})
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid)
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid)
const uploadsStore = useUploadsStore()
const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
@ -90,11 +126,33 @@ const onPanelScroll = (e: any) => {
}
const height = e.target.scrollTop + e.target.clientHeight
const scrollHeight = e.target.scrollHeight
const isScrollingDown = e.target.scrollTop > state.value.lastScrollTop
state.value.lastScrollTop = e.target.scrollTop
skipBottom.value = height < e.target.scrollHeight - 200
//
if (height === scrollHeight && isScrollingDown) {
onLoadMoreDown()
}
skipBottom.value = height < scrollHeight - 200
if (!skipBottom.value && dialogueStore.unreadBubble) {
dialogueStore.setUnreadBubble(0)
}
//
state.value.isScrolling = true
if (state.value.scrollTimer) {
clearTimeout(state.value.scrollTimer)
}
state.value.scrollTimer = window.setTimeout(() => {
state.value.isScrolling = false
//
checkVisibleOutElements()
//
lastVisibleOutTriggerTime = Date.now()
}, 300) // 300ms
//
if (skipBottom.value == false) {
let len = dialogueStore.records.length
@ -231,7 +289,6 @@ const onClickNickname = (data: ITalkRecord) => {
//
const onContextMenu = (e: any, item: ITalkRecord) => {
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
return e.preventDefault()
}
@ -294,7 +351,10 @@ watch(
try {
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
// id
if (parsed.talk_type === newProps.talk_type && parsed.receiver_id === newProps.receiver_id) {
if (
parsed.talk_type === newProps.talk_type &&
parsed.receiver_id === newProps.receiver_id
) {
specialParams = parsed
}
} catch (e) {}
@ -312,29 +372,350 @@ watch(
)
// onMounted(() => {
// onLoad({ ...props, limit: 30 })
// onLoad({ ...props, limit: 30 })
// })
const retry=(item:any)=>{
const retry = (item: any) => {
confirmBox({
content:'确定重发吗'
}).then(()=>{
uploadsStore.retryCommonUpload(item.extra.upload_id)
content: '确定重发吗'
}).then(() => {
uploadsStore.retryCommonUpload(item.extra.upload_id)
})
}
const onContextMenuAvatar=(e:any,item:any)=>{
if(item.talk_type!==1){
e.preventDefault()
if(item.float!=='right'){
bus.emit(EditorConst.Mention, {
id: item.user_id,
value: item.nickname
})
}
const onContextMenuAvatar = (e: any, item: any) => {
e.preventDefault()
if (item.float !== 'right') {
bus.emit(EditorConst.Mention, {
id: item.user_id,
value: item.nickname
})
}
}
const state = ref<State>({
visibleElements: new Set(),
visibleOutElements: new Set(),
tempWaitDoRead: [],
tempWaitDoCheck: [],
setMessageReadInterval: null,
setOutMessageReadInterval: null,
lastUpdateTime: 0,
isScrolling: false,
scrollTimer: null,
lastTriggerTime: 0,
talkReadListDetail: [],
readDetailIsUnread: 1,
currentMsgReadDetail: null,
currentReadDetailPage: 1,
hasMoreReadListDetail: true,
loadingReadListDetail: false,
lastScrollTop: 0
})
//
let observer: IntersectionObserver | null = null
//
const checkVisibleElements = () => {
if (state.value.visibleElements.size > 0) {
let waitDoRead: ReadStatus[] = []
state.value.visibleElements.forEach((el: HTMLElement) => {
const msgId = el.dataset.msgid
const talkType = Number(el.dataset.talktype)
const receiverId = Number(el.dataset.receiverid)
if (!msgId) return
if (waitDoRead.length === 0) {
waitDoRead.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId
})
} else {
const existingItem = waitDoRead.find(
(item) => item.talk_type === talkType && item.receiver_id === receiverId
)
if (existingItem) {
existingItem.msg_ids.push(msgId)
} else {
waitDoRead.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId
})
}
}
})
if (waitDoRead.length > 0) {
waitDoRead.forEach((doReadItem) => {
const prevItem = state.value.tempWaitDoRead.find(
(prev) =>
prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id
)
if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) {
console.error('====发送了新版已读回执=====', doReadItem)
ws.emit('im.message.new.read', doReadItem)
}
})
}
state.value.tempWaitDoRead = JSON.parse(JSON.stringify(waitDoRead))
}
}
//
const checkVisibleOutElements = () => {
if (state.value.visibleOutElements.size > 0) {
let waitDoCheck: ReadStatus[] = []
state.value.visibleOutElements.forEach((el: HTMLElement) => {
const msgId = el.dataset.msgid
const talkType = Number(el.dataset.talktype)
const receiverId = Number(el.dataset.receiverid)
if (!msgId) return
if (waitDoCheck.length === 0) {
waitDoCheck.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId,
user_id: props.uid
})
} else {
const existingItem = waitDoCheck.find(
(item) => item.talk_type === talkType && item.receiver_id === receiverId
)
if (existingItem) {
existingItem.msg_ids.push(msgId)
} else {
waitDoCheck.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId,
user_id: props.uid
})
}
}
})
if (waitDoCheck.length > 0) {
waitDoCheck.forEach((doCheckItem) => {
console.error('====组装了新版已读回执参数需要发送socket=====', doCheckItem)
ws.emit('im.message.listen.read', doCheckItem)
})
}
state.value.tempWaitDoCheck = JSON.parse(JSON.stringify(waitDoCheck))
}
}
//
let lastVisibleOutTriggerTime = 0
//socket
watch(
() => state.value.visibleOutElements,
(newVal) => {
const now = Date.now()
if (now - lastVisibleOutTriggerTime < 1000) {
return
}
lastVisibleOutTriggerTime = now
checkVisibleOutElements()
},
{
deep: true,
immediate: true
}
)
//
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
let elData = entry.target.dataset
const msgType = elData.msgtype
const userId = elData.userid
if (Number(msgType) < 1000 && Number(userId) !== Number(props.uid)) {
//
state.value.visibleElements.add(entry.target)
}
if (Number(msgType) < 1000 && Number(userId) === Number(props.uid)) {
//
state.value.visibleOutElements.add(entry.target)
}
} else {
//
state.value.visibleElements.delete(entry.target)
state.value.visibleOutElements.delete(entry.target)
}
})
}
//
watch(
() => records.value,
() => {
nextTick(() => {
//
if (observer) {
observer.disconnect()
}
//
const options = {
root: null,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
rootMargin: '50px 0px'
}
observer = new IntersectionObserver(handleIntersection, options)
//
const messageElements = document.querySelectorAll('.message-item')
messageElements.forEach((el) => {
if (observer) {
observer.observe(el)
}
})
})
},
{ deep: true }
)
// 线
let eventBusDebounceTimer: number | null = null
onMounted(() => {
// 线
bus.subscribe('check-visible-out-elements', (type) => {
if (eventBusDebounceTimer) {
clearTimeout(eventBusDebounceTimer)
}
eventBusDebounceTimer = window.setTimeout(() => {
checkVisibleOutElements()
eventBusDebounceTimer = null
}, 500)
})
//
if (state.value.setMessageReadInterval) {
clearInterval(state.value.setMessageReadInterval)
state.value.setMessageReadInterval = null
}
state.value.setMessageReadInterval = setInterval(() => {
checkVisibleElements()
}, 2000)
if (state.value.setOutMessageReadInterval) {
clearInterval(state.value.setOutMessageReadInterval)
state.value.setOutMessageReadInterval = null
}
// socket
// state.value.setOutMessageReadInterval = setInterval(() => {
// checkVisibleOutElements()
// }, 2000)
//
const options = {
root: null,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
rootMargin: '50px 0px'
}
observer = new IntersectionObserver(handleIntersection, options)
//
nextTick(() => {
const messageElements = document.querySelectorAll('.message-item')
messageElements.forEach((el) => {
if (observer) {
observer.observe(el)
}
})
})
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
if (state.value.setMessageReadInterval) {
clearInterval(state.value.setMessageReadInterval)
state.value.setMessageReadInterval = null
checkVisibleElements()
}
if (state.value.setOutMessageReadInterval) {
clearInterval(state.value.setOutMessageReadInterval)
state.value.setOutMessageReadInterval = null
checkVisibleOutElements()
}
// 线
if (eventBusDebounceTimer) {
clearTimeout(eventBusDebounceTimer)
eventBusDebounceTimer = null
}
// 线
bus.unsubscribe('check-visible-out-elements', checkVisibleOutElements)
})
//
const toShowMessageReadDetail = async (item?: ITalkRecord) => {
if (item) {
state.value.currentMsgReadDetail = item
onReadTabChange('unread-tab')
return
}
let params = {
page: state.value.currentReadDetailPage,
pageSize: 10,
type: 'detail', //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>
<template>
@ -356,6 +737,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
v-for="(item, index) in records"
:key="item.msg_id"
:id="item.msg_id"
:data-msgid="item.msg_id"
:data-msgtype="item.msg_type"
:data-userid="item.user_id"
:data-talktype="props?.talk_type"
:data-receiverid="props?.receiver_id"
>
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
@ -389,7 +775,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
>
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
<n-checkbox
size="small"
:checked="item.isCheck"
@update:checked="item.isCheck = !item.isCheck"
/>
</aside>
<!-- 头像信息 -->
@ -417,10 +807,8 @@ const onContextMenuAvatar=(e:any,item:any)=>{
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div> -->
<div class="talk-title">
<span class="mr-7px"
v-show="talk_type == 2 && item.float == 'left'"
>{{ item.nickname }}
<span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'"
>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div>
@ -479,6 +867,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{
{{ item.extra?.reply?.content }}
</span>
</div>
<!-- 已读回执 -->
<div class="talk_read_num" v-if="item.user_id === props.uid">
<span v-if="props.talk_type === 1">{{
item.read_total_num > 0 ? '已读' : '未读'
}}</span>
<n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;" v-if="props.talk_type === 2">
<template #trigger>
<span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)" style="cursor: pointer;">
已读 ({{ item?.read_total_num || 0 }}/{{
props.num - 1 > 0 ? props.num - 1 : 0
}})
</span>
</template>
<div class="talk-read-list-detail">
<n-tabs
type="line"
animated
justify-content="space-around"
@update:value="onReadTabChange"
>
<n-tab name="unread-tab">
{{ `未读(${props.num - 1 - (item.read_total_num || 0) || 0})` }}
</n-tab>
<n-tab name="read-tab">
{{ `已读(${item.read_total_num || 0})` }}
</n-tab>
</n-tabs>
<div class="talk-read-list">
<n-infinite-scroll style="height: 340px;" @load="loadMoreReadListDetail">
<div
class="talk-read-list-item"
v-for="(talkReadDetailItem,
talkReadDetailIndex) in state.talkReadListDetail"
:key="talkReadDetailIndex"
>
<avatarModule
:mode="1"
:avatar="talkReadDetailItem.avatar"
:userName="talkReadDetailItem.nickName"
:groupType="0"
:customStyle="{
width: '36px',
height: '36px'
}"
:customTextStyle="{
fontSize: '12px',
fontWeight: 'bold',
color: '#fff',
lineHeight: '17px'
}"
></avatarModule>
<div class="talk-read-list-item-info">
<span style="font-size: 12px; font-weight: 600; line-height: 17px;">{{
talkReadDetailItem.nickName
}}</span>
<span style="font-size: 12px; color: #999; line-height: 14px;">{{
talkReadDetailItem.jobNum
}}</span>
</div>
</div>
</n-infinite-scroll>
</div>
</div>
</n-popover>
</div>
</main>
</div>
@ -677,6 +1131,17 @@ const onContextMenuAvatar=(e:any,item:any)=>{
}
}
.talk_read_num {
text-align: right;
color: #7a58de;
font-size: 12px;
font-weight: 400;
line-height: 17px;
margin: 5px 0 0;
span {
}
}
&:hover {
.talk-title {
opacity: 1;
@ -725,4 +1190,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>