Compare commits

...

22 Commits

Author SHA1 Message Date
Phoenix
4b5c160e94 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-10 15:03:31 +08:00
Phoenix
ebd567a757 fix(消息面板): 修复消息菜单和撤回消息按钮的显示逻辑
修复消息菜单中缺少的is_self_action属性设置,确保撤回消息按钮仅在自身操作时显示
调整编辑器内容处理逻辑,优化草稿保存的数据结构
2025-06-10 15:03:29 +08:00
18871db6b6 Merge branch 'main' into dev 2025-06-10 14:56:10 +08:00
5f4dd80b2a 解决es搜索时按人搜索和按群搜索的跳转不正确问题;解决按日期搜索后切换其他条件或取消没有正确重置日期选择问题 2025-06-10 14:50:15 +08:00
Phoenix
1ae317dbb3 Merge branch 'xingyy' into dev 2025-06-10 13:39:29 +08:00
Phoenix
3b6d998ce1 Merge branch 'xingyy' into dev 2025-06-09 14:46:59 +08:00
Phoenix
45eec2ff22 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-09 13:57:38 +08:00
Phoenix
9c34066128 Merge branch 'xingyy' into dev 2025-06-09 13:57:32 +08:00
92fce58429 Merge branch 'main' into dev 2025-06-09 13:33:02 +08:00
d55616e2e7 Merge branch 'wyfMain-dev' 2025-06-09 13:32:31 +08:00
031411ba49 更换转发、es搜索接口为/v2;处理搜索判断是否还有更多数据的逻辑;解决聊天记录中视频类型点击播放会全屏关不掉问题;更换鼠标悬浮背景色;新增列表中省略内容悬浮显示完整功能;修改回到聊天底部功能实现 2025-06-09 13:29:06 +08:00
Phoenix
c0f4248385 Merge branch 'main' of http://172.16.100.91:3000/scout666/chat-pc 2025-06-09 11:52:12 +08:00
Phoenix
2e998a1174 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-09 11:51:48 +08:00
Phoenix
60a2fb996b Merge branch 'xingyy' into dev 2025-06-09 11:51:38 +08:00
b282562cdd Merge branch 'main' into dev 2025-06-06 18:54:59 +08:00
642992640f 解决1、清空群公告提示文字和UI不一致问题;2、去除聊天记录搜索选中类别后,输入框的输入提示;3、解决点击关闭按钮,关闭通讯录弹窗的时候没有重置窗口内条件选择问题 2025-06-06 18:52:40 +08:00
d0abf7d8ab Merge branch 'main' into dev 2025-06-06 09:05:56 +08:00
5b4ee3c677 处理代码冲突——恢复已读未读功能 2025-06-05 16:32:31 +08:00
Phoenix
409af72039 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-05 14:14:47 +08:00
Phoenix
799599bd83 Merge branch 'xingyy' into dev 2025-06-05 14:14:38 +08:00
ec18d85546 Merge branch 'wyfMain-dev' into dev 2025-06-05 09:20:27 +08:00
Phoenix
a97f293a6c Merge branch 'xingyy' into dev
# Conflicts:
#	src/views/message/inner/panel/PanelFooter.vue   resolved by xingyy version
2025-06-04 16:32:24 +08:00
15 changed files with 733 additions and 102 deletions

View File

@ -35,7 +35,7 @@ export const ServeTalkRecords = (data = {}) => {
// 获取转发会话记录详情列表服务接口
export const ServeGetForwardRecords = (data = {}) => {
return get('/api/v1/talk/records/forward', data)
return get('/api/v1/talk/records/forward/v2', data)
}
// 对话列表置顶服务接口

View File

@ -2,12 +2,12 @@ import { post, get, upload } from '@/utils/request'
//ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览
export const ServeSeachQueryAll = (data = {}) => {
return post('/api/v1/elasticsearch/query-all', data)
return post('/api/v1/elasticsearch/query-all/v2', data)
}
// ES搜索用户数据
export const ServeQueryUser = (data) => {
return post('/api/v1/elasticsearch/query-user', data)
return post('/api/v1/elasticsearch/query-user/v2', data)
}
// ES搜索群组数据

View File

@ -132,14 +132,22 @@ const handleInput = (event) => {
// HTML
editorHtml.value = target.innerHTML || ''
const currentEditor= parseEditorContent().items
//
checkMention(target)
saveDraft()
emit('editor-event', {
event: 'input_event',
data: editorContent.value
data: currentEditor.map(x=>{
let text=''
if(x.type===3){
text='[图片]'
}else if(x.type===1){
text=x.content
}
return text
})?.join('')
})
}
@ -1244,17 +1252,34 @@ const saveDraft = () => {
//
const contentToSave = tempDiv.textContent || ''
const htmlToSave = tempDiv.innerHTML || ''
const currentEditor= parseEditorContent().items
//
const hasContent = contentToSave.trim().length > 0 ||
htmlToSave.includes('<img') ||
htmlToSave.includes('editor-file')
// 稿
if (hasContent) {
if (currentEditor.length>0) {
console.log('保存到草稿',currentEditor.map(x=>{
let text=''
if(x.type===3){
text='[图片]'
}else if(x.type===1){
text=x.content
}
return text
})?.join(''))
// 稿store
editorDraftStore.items[indexName.value] = JSON.stringify({
content: contentToSave,
content: currentEditor.map(x=>{
let text=''
if(x.type===3){
text='[图片]'
}else if(x.type===1){
text=x.content
}
return text
})?.join(''),
html: htmlToSave
})
} else {

View File

@ -168,35 +168,13 @@
</n-scrollbar>
</div>
<div class="condition-result-imgAndVideo-area" v-if="item?.extra?.url">
<template v-if="item?.msg_type === 3">
<n-image
:src="item?.extra?.url"
:lazy="true"
:preview-src="item?.extra?.url"
:width="131"
:height="131"
object-fit="cover"
></n-image>
</template>
<template v-else-if="item?.msg_type === 5">
<div class="video-preview" @click="onPlay(item?.extra?.url)">
<video :src="item?.extra?.url" :controls="false"></video>
<!-- <n-image
:src="
item?.extra?.url
? item?.extra?.url + '#t=0.001'
: item?.extra?.cover
"
:width="131"
:height="131"
object-fit="cover"
></n-image> -->
<div class="btn-video">
<!-- <img :src="playCircle" /> -->
<n-icon :component="Play" size="40" />
</div>
</div>
</template>
<div class="message-component-wrapper">
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
</div>
</div>
<!-- <div
@ -326,8 +304,14 @@ import { ServeTalkDate, ServeGetSessionId } from '@/api/search.js'
import { parseTime } from '@/utils/datetime'
import { fileFormatSize, fileSuffix } from '@/utils/strings'
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
import { MessageComponents } from '@/constant/message'
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray', 'hideSearchResultModal'])
const emits = defineEmits([
'clearSearchMemberByAlphabet',
'getDisabledDateArray',
'hideSearchResultModal',
'clearSelectedDateTime'
])
const dialogueStore = useDialogueStore()
//
@ -770,6 +754,10 @@ const resetSearchConditions = (newVal) => {
state.group_member_id = 0
emits('clearSearchMemberByAlphabet')
}
if (newVal !== 'date') {
state.selectedDateTime = null
emits('clearSelectedDateTime')
}
}
//
@ -851,7 +839,6 @@ watch(
queryAllSearch()
},
{
immediate: true,
deep: true
}
)
@ -1126,4 +1113,31 @@ body:deep(.round-3) {
margin: 13px 0 0;
}
}
.message-component-wrapper {
width: 131px;
height: 131px;
display: inline-block;
overflow: hidden;
position: relative;
.im-message-video,
.im-message-image,
.image-container {
width: 100% !important;
height: 100% !important;
}
:deep(.n-image) {
width: 100% !important;
height: 100% !important;
}
:deep(img),
:deep(video) {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
}
</style>

View File

@ -372,7 +372,7 @@ const resultDetail = computed(() => {
border: 0;
}
.search-item:hover {
background-color: #f8f8f8;
background-color: rgba(70, 41, 157, 0.1);
.info-detail-searchRecordDetail {
.searchRecordDetail-fastLocal {

View File

@ -41,13 +41,13 @@
>
<searchItem
@click="clickSearchItem(searchResultKey, item)"
v-if="(
searchResultKey === 'user_infos'
? (state.userInfosShowAll || (props.listLimit && index < 3))
v-if="
(searchResultKey === 'user_infos'
? state.userInfosShowAll || (props.listLimit && index < 3)
: searchResultKey === 'combinedGroup'
? (state.groupInfosShowAll || (props.listLimit && index < 3))
: (props.listLimit && index < 3)
) || !props.listLimit"
? state.groupInfosShowAll || (props.listLimit && index < 3)
: props.listLimit && index < 3) || !props.listLimit
"
:searchResultKey="searchResultKey"
:searchItem="item"
:searchText="state.searchText"
@ -403,11 +403,57 @@ const queryAllSearch = (doClearSearchResult) => {
} else if (state?.first_talk_record_infos?.talk_type === 2) {
total = data.group_record_count
}
}
if (total < props.searchResultPageSize) {
state.hasMore = false
let noMoreSearchResultRecord = true
if (
Object.keys(data).includes('talk_record_infos') &&
state.searchResult['talk_record_infos']?.length > 0
) {
//
if (state.searchResult['talk_record_infos']?.length < total) {
noMoreSearchResultRecord = false
}
}
if (noMoreSearchResultRecord) {
state.hasMore = false
} else {
state.hasMore = true
}
} else {
state.hasMore = true
let noMoreSearchResultUser = true
let noMoreSearchResultGroup = true
let noMoreSearchResultGeneral = true
if (
Object.keys(data).includes('user_infos') &&
state.searchResult['user_infos']?.length > 0
) {
//
if (state.searchResult['user_infos']?.length < total) {
noMoreSearchResultUser = false
}
}
if (
Object.keys(data).includes('group_member_infos' || 'group_infos') &&
state.searchResult['combinedGroup']?.length > 0
) {
//
if (state.searchResult['combinedGroup']?.length < total) {
noMoreSearchResultGroup = false
}
}
if (
Object.keys(data).includes('general_infos') &&
state.searchResult['general_infos']?.length > 0
) {
//
if (state.searchResult['general_infos']?.length < total) {
noMoreSearchResultGeneral = false
}
}
if (noMoreSearchResultUser && noMoreSearchResultGroup && noMoreSearchResultGeneral) {
state.hasMore = false
} else {
state.hasMore = true
}
}
emits('resultTotalCount', total)
// zPaging.value?.completeByTotal([data], total)
@ -673,18 +719,21 @@ async function loadMoreGroupInfos() {
const resp = await ServeQueryGroup(params)
if (resp.code === 200) {
const groupInfos = Array.isArray(resp.data.group_infos) ? resp.data.group_infos : []
const groupMemberInfos = Array.isArray(resp.data.group_member_infos) ? resp.data.group_member_infos : []
const groupMemberInfos = Array.isArray(resp.data.group_member_infos)
? resp.data.group_member_infos
: []
// groupTempType
groupInfos.forEach(item => {
groupInfos.forEach((item) => {
item.groupTempType = 'group_infos'
item.group_type = item.type //
})
groupMemberInfos.forEach(item => {
groupMemberInfos.forEach((item) => {
item.groupTempType = 'group_member_infos'
})
const isFirstLoad = (!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
const isFirstLoad =
(!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
(state.groupInfosLastGroupId === 0 && state.groupInfosLastMemberId === 0)
if (isFirstLoad) {
//
@ -697,7 +746,9 @@ async function loadMoreGroupInfos() {
} else {
//
const allGroupInfos = (state.searchResult.group_infos || []).concat(groupInfos)
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(groupMemberInfos)
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(
groupMemberInfos
)
state.searchResult = {
...state.searchResult,
group_infos: allGroupInfos,
@ -708,10 +759,9 @@ async function loadMoreGroupInfos() {
state.groupInfosLastGroupId = resp.data.last_group_id
state.groupInfosLastMemberId = resp.data.last_member_id
//
const noMoreData = (
const noMoreData =
(!groupInfos.length && !groupMemberInfos.length) ||
(resp.data.last_group_id === 0 && resp.data.last_member_id === 0)
)
if (noMoreData) {
state.groupInfosExpand = true
}
@ -774,7 +824,7 @@ async function loadMoreGroupInfos() {
}
}
.result-has-more:hover {
background-color: #f8f8f8;
background-color: rgba(70, 41, 157, 0.1);
}
}
}

View File

@ -7,10 +7,15 @@ import { ITalkRecord } from '@/types/chat'
import { useInject } from '@/hooks'
import customModal from '@/components/common/customModal.vue'
import { voiceToText } from '@/api/chat.js'
import { parseTime } from '@/utils/datetime'
const props = defineProps({
msgId: {
type: String,
required: true
},
createdAt: {
type: String,
required: false
}
})
const isShow=defineModel<boolean>('show')
@ -24,7 +29,8 @@ const onMaskClick = () => {
const onLoadData = () => {
ServeGetForwardRecords({
msg_id: props.msgId
msg_id: props.msgId,
biz_date: parseTime(new Date(props.createdAt), '{y}{m}')
}).then((res) => {
if (res.code == 200) {
items.value = res.data.items || []

View File

@ -33,7 +33,7 @@ const onClick = () => {
<span>转发聊天会话记录 ({{ extra.msg_ids.length }})</span>
</div>
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" />
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at"/>
</section>
</template>

View File

@ -45,7 +45,7 @@ const onRevoke = () => {
<div class="content">
<div v-if="login_uid === user_id">
<span> 你撤回了一条消息 | {{ formatTime(datetime) }} </span>
<n-button @click="onRevoke" v-if="data.msg_type === 1&&data.extra?.content" text class="text-#46299D text-11px">重新编辑</n-button>
<n-button @click="onRevoke" v-if="data.msg_type === 1&&data.extra?.content&&data.is_self_action" text class="text-#46299D text-11px">重新编辑</n-button>
</div>
<span v-else-if="talk_type == 1"> 对方撤回了一条消息 | {{ formatTime(datetime) }} </span>
<span v-else>

View File

@ -18,7 +18,7 @@ export function isLoggedIn() {
*/
export function getAccessToken() {
// return storage.get(AccessToken) || ''
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22c9c2f9b60a57573e8b08cdf47105e1ba85550c21fa55526e8a00bf316c623eb67abf749622c48beab908d61d3db7b22ed3eb6aa8a08c77680ad4d8a3458c1e72f97ba2b8480674df77f0501a34e82b58'
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b891a491a664540c3af42964b31bedf8b1c93e8a754bb71e4b95d53ad8e6b16ac1575f536a9e7a062e44f3bb48a367623d38bd875a10afa3a53e79374ffda424138ed9ad4cab0d972432567ae7149b2bf3c'
}
/**

View File

@ -189,7 +189,7 @@ const handleGroupNoticeModalConfirm = () => {
state.isShowNoticeHintModal = true
if (state?.groupNoticeInfo?.id && !state.groupNoticeInEdit) {
//
state.noticeHintModalContent = '确定清空群公告吗?'
state.noticeHintModalContent = '确定清空群公告'
state.noticeHintModalActionBtns = {
confirmBtn: {
text: '清空',
@ -485,6 +485,11 @@ const hideSearchResultModal = () => {
handleSearchRecordByConditionModalClose()
state.isShowGroupAside = false
}
//
const clearSelectedDateTime = () => {
onDatePickClear()
}
</script>
<template>
@ -594,7 +599,7 @@ const hideSearchResultModal = () => {
<n-input
type="text"
v-model:value="state.searchRecordByConditionText"
placeholder="请输入"
:placeholder="state.conditionTag && state.conditionTag !== 'all'?'':'请输入'"
clearable
>
<template #clear-icon>
@ -712,6 +717,7 @@ const hideSearchResultModal = () => {
:selectedDateTime="state.selectedDateTime"
:nowDateTime="state.nowDateTime"
@hideSearchResultModal="hideSearchResultModal"
@clearSelectedDateTime="clearSelectedDateTime"
/>
</div>
</n-card>

View File

@ -89,6 +89,12 @@ const renderChatAppSearch = () => {
state.searchRecordText = searchText
state.selectItemInList = res
} else {
if(searchResultKey === 'user_infos'){
talk_type = 1
}
if(searchResultKey === 'combinedGroup'){
talk_type = 2
}
talkStore.toTalk(talk_type, receiver_id, router)
}
state.showSearchDropdown = false
@ -171,6 +177,9 @@ const state = reactive({
title: '姓名 【工号】',
field: 'nickName',
width: 200,
ellipsis: {
tooltip: true
},
render(row, index) {
return row.nickName + '【' + row.jobNum + '】'
}
@ -179,7 +188,9 @@ const state = reactive({
title: '岗位名称',
field: 'positionName',
width: 400,
ellipsis: true,
ellipsis: {
tooltip: true
},
render(row, index) {
let positionNames = Array.isArray(row.depPositions)
? row.depPositions.flatMap((dep) =>
@ -192,7 +203,7 @@ const state = reactive({
{
title: '操作',
field: 'action',
width: 200,
width: 180,
align: 'center',
fixed: 'right',
render(row, index) {
@ -213,7 +224,10 @@ const state = reactive({
{
title: '群聊名称',
field: 'groupName',
width: 200,
width: 400,
ellipsis: {
tooltip: true
},
render(row, index) {
return row.group_name
}
@ -221,7 +235,7 @@ const state = reactive({
{
title: '群类型',
field: 'groupType',
width: 400,
width: 200,
ellipsis: true,
render(row, index) {
let groupType = row.group_type
@ -239,7 +253,7 @@ const state = reactive({
{
title: '操作',
field: 'action',
width: 200,
width: 180,
align: 'center',
fixed: 'right',
render(row, index) {
@ -448,6 +462,11 @@ onMounted(() => {
const showAddressBookModal = () => {
state.isShowAddressBookModal = true
}
//
const closeAddressBookModal = () => {
state.isShowAddressBookModal = false
resetAddressBookModal()
}
const handleTreeClick = ({ selectedKey, tree }) => {
// console.log(tree)
state.clickKey = tree.key
@ -840,6 +859,8 @@ const handleEnterSearchResultChat = () => {
:style="state.customModalStyle"
:customCloseBtn="true"
:closable="false"
:customCloseEvent="true"
@customCloseModal="closeAddressBookModal"
>
<template #content>
<div class="custom-modal-content">
@ -1170,6 +1191,7 @@ html[theme-mode='dark'] {
background-color: #46299d;
color: #fff;
}
.groupChatList-pagination {
display: flex;
justify-content: flex-end;
@ -1254,4 +1276,7 @@ html[theme-mode='dark'] {
}
}
}
:deep(.n-data-table .n-data-table-tr:not(.n-data-table-tr--summary):hover > .n-data-table-td) {
background-color: rgba(70, 41, 157, 0.1) !important;
}
</style>

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()
}
@ -293,7 +350,10 @@ watch(
try {
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
// id
if (parsed.talk_type === newProps.talk_type && parsed.receiver_id === newProps.receiver_id) {
if (
parsed.talk_type === newProps.talk_type &&
parsed.receiver_id === newProps.receiver_id
) {
specialParams = parsed
}
} catch (e) {}
@ -311,29 +371,356 @@ watch(
)
// onMounted(() => {
// onLoad({ ...props, limit: 30 })
// onLoad({ ...props, limit: 30 })
// })
const retry=(item:any)=>{
const retry = (item: any) => {
confirmBox({
content:'确定重发吗'
}).then(()=>{
uploadsStore.retryCommonUpload(item.extra.upload_id)
content: '确定重发吗'
}).then(() => {
uploadsStore.retryCommonUpload(item.extra.upload_id)
})
}
const onContextMenuAvatar=(e:any,item:any)=>{
if(item.talk_type!==1){
e.preventDefault()
if(item.float!=='right'){
bus.emit(EditorConst.Mention, {
id: item.user_id,
value: item.nickname
})
}
const onContextMenuAvatar = (e: any, item: any) => {
e.preventDefault()
if (item.float !== 'right') {
bus.emit(EditorConst.Mention, {
id: item.user_id,
value: item.nickname
})
}
}
const state = ref<State>({
visibleElements: new Set(),
visibleOutElements: new Set(),
tempWaitDoRead: [],
tempWaitDoCheck: [],
setMessageReadInterval: null,
setOutMessageReadInterval: null,
lastUpdateTime: 0,
isScrolling: false,
scrollTimer: null,
lastTriggerTime: 0,
talkReadListDetail: [],
readDetailIsUnread: 1,
currentMsgReadDetail: null,
currentReadDetailPage: 1,
hasMoreReadListDetail: true,
loadingReadListDetail: false,
lastScrollTop: 0
})
//
let observer: IntersectionObserver | null = null
//
const checkVisibleElements = () => {
if (state.value.visibleElements.size > 0) {
let waitDoRead: ReadStatus[] = []
state.value.visibleElements.forEach((el: HTMLElement) => {
const msgId = el.dataset.msgid
const talkType = Number(el.dataset.talktype)
const receiverId = Number(el.dataset.receiverid)
if (!msgId) return
if (waitDoRead.length === 0) {
waitDoRead.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId
})
} else {
const existingItem = waitDoRead.find(
(item) => item.talk_type === talkType && item.receiver_id === receiverId
)
if (existingItem) {
existingItem.msg_ids.push(msgId)
} else {
waitDoRead.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId
})
}
}
})
if (waitDoRead.length > 0) {
waitDoRead.forEach((doReadItem) => {
const prevItem = state.value.tempWaitDoRead.find(
(prev) =>
prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id
)
if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) {
console.error('====发送了新版已读回执=====', doReadItem)
ws.emit('im.message.new.read', doReadItem)
}
})
}
state.value.tempWaitDoRead = JSON.parse(JSON.stringify(waitDoRead))
}
}
//
const checkVisibleOutElements = () => {
if (state.value.visibleOutElements.size > 0) {
let waitDoCheck: ReadStatus[] = []
state.value.visibleOutElements.forEach((el: HTMLElement) => {
const msgId = el.dataset.msgid
const talkType = Number(el.dataset.talktype)
const receiverId = Number(el.dataset.receiverid)
if (!msgId) return
if (waitDoCheck.length === 0) {
waitDoCheck.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId,
user_id: props.uid
})
} else {
const existingItem = waitDoCheck.find(
(item) => item.talk_type === talkType && item.receiver_id === receiverId
)
if (existingItem) {
existingItem.msg_ids.push(msgId)
} else {
waitDoCheck.push({
msg_ids: [msgId],
talk_type: talkType,
receiver_id: receiverId,
user_id: props.uid
})
}
}
})
if (waitDoCheck.length > 0) {
waitDoCheck.forEach((doCheckItem) => {
console.error('====组装了新版已读回执参数需要发送socket=====', doCheckItem)
ws.emit('im.message.listen.read', doCheckItem)
})
}
state.value.tempWaitDoCheck = JSON.parse(JSON.stringify(waitDoCheck))
}
}
//
let lastVisibleOutTriggerTime = 0
//socket
watch(
() => state.value.visibleOutElements,
(newVal) => {
const now = Date.now()
if (now - lastVisibleOutTriggerTime < 1000) {
return
}
lastVisibleOutTriggerTime = now
checkVisibleOutElements()
},
{
deep: true,
immediate: true
}
)
//
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
let elData = entry.target.dataset
const msgType = elData.msgtype
const userId = elData.userid
if (Number(msgType) < 1000 && Number(userId) !== Number(props.uid)) {
//
state.value.visibleElements.add(entry.target)
}
if (Number(msgType) < 1000 && Number(userId) === Number(props.uid)) {
//
state.value.visibleOutElements.add(entry.target)
}
} else {
//
state.value.visibleElements.delete(entry.target)
state.value.visibleOutElements.delete(entry.target)
}
})
}
//
watch(
() => records.value,
() => {
nextTick(() => {
//
if (observer) {
observer.disconnect()
}
//
const options = {
root: null,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
rootMargin: '50px 0px'
}
observer = new IntersectionObserver(handleIntersection, options)
//
const messageElements = document.querySelectorAll('.message-item')
messageElements.forEach((el) => {
if (observer) {
observer.observe(el)
}
})
})
},
{ deep: true }
)
// 线
let eventBusDebounceTimer: number | null = null
onMounted(() => {
// 线
bus.subscribe('check-visible-out-elements', (type) => {
if (eventBusDebounceTimer) {
clearTimeout(eventBusDebounceTimer)
}
eventBusDebounceTimer = window.setTimeout(() => {
checkVisibleOutElements()
eventBusDebounceTimer = null
}, 500)
})
//
if (state.value.setMessageReadInterval) {
clearInterval(state.value.setMessageReadInterval)
state.value.setMessageReadInterval = null
}
state.value.setMessageReadInterval = setInterval(() => {
checkVisibleElements()
}, 2000)
if (state.value.setOutMessageReadInterval) {
clearInterval(state.value.setOutMessageReadInterval)
state.value.setOutMessageReadInterval = null
}
// socket
// state.value.setOutMessageReadInterval = setInterval(() => {
// checkVisibleOutElements()
// }, 2000)
//
const options = {
root: null,
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
rootMargin: '50px 0px'
}
observer = new IntersectionObserver(handleIntersection, options)
//
nextTick(() => {
const messageElements = document.querySelectorAll('.message-item')
messageElements.forEach((el) => {
if (observer) {
observer.observe(el)
}
})
})
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
if (state.value.setMessageReadInterval) {
clearInterval(state.value.setMessageReadInterval)
state.value.setMessageReadInterval = null
checkVisibleElements()
}
if (state.value.setOutMessageReadInterval) {
clearInterval(state.value.setOutMessageReadInterval)
state.value.setOutMessageReadInterval = null
checkVisibleOutElements()
}
// 线
if (eventBusDebounceTimer) {
clearTimeout(eventBusDebounceTimer)
eventBusDebounceTimer = null
}
// 线
bus.unsubscribe('check-visible-out-elements', checkVisibleOutElements)
})
//
const toShowMessageReadDetail = async (item?: ITalkRecord) => {
if (item) {
state.value.currentMsgReadDetail = item
onReadTabChange('unread-tab')
return
}
let params = {
page: state.value.currentReadDetailPage,
pageSize: 10,
type: 'detail', //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
})
}
const onCustomSkipBottomEvent = () => {
console.log('onCustomSkipBottomEvent')
onLoad({ ...props, limit: 30 })
// scrollToBottom()
}
</script>
<template>
@ -355,6 +742,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
v-for="(item, index) in records"
:key="item.msg_id"
:id="item.msg_id"
:data-msgid="item.msg_id"
:data-msgtype="item.msg_type"
:data-userid="item.user_id"
:data-talktype="props?.talk_type"
:data-receiverid="props?.receiver_id"
>
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
@ -388,7 +780,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
>
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
<n-checkbox
size="small"
:checked="item.isCheck"
@update:checked="item.isCheck = !item.isCheck"
/>
</aside>
<!-- 头像信息 -->
@ -416,10 +812,8 @@ const onContextMenuAvatar=(e:any,item:any)=>{
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div> -->
<div class="talk-title">
<span class="mr-7px"
v-show="talk_type == 2 && item.float == 'left'"
>{{ item.nickname }}
<span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'"
>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
</div>
@ -478,6 +872,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{
{{ item.extra?.reply?.content }}
</span>
</div>
<!-- 已读回执 -->
<div class="talk_read_num" v-if="item.user_id === props.uid">
<span v-if="props.talk_type === 1">{{
item.read_total_num > 0 ? '已读' : '未读'
}}</span>
<n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;" v-if="props.talk_type === 2">
<template #trigger>
<span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)" style="cursor: pointer;">
已读 ({{ item?.read_total_num || 0 }}/{{
props.num - 1 > 0 ? props.num - 1 : 0
}})
</span>
</template>
<div class="talk-read-list-detail">
<n-tabs
type="line"
animated
justify-content="space-around"
@update:value="onReadTabChange"
>
<n-tab name="unread-tab">
{{ `未读(${props.num - 1 - (item.read_total_num || 0) || 0})` }}
</n-tab>
<n-tab name="read-tab">
{{ `已读(${item.read_total_num || 0})` }}
</n-tab>
</n-tabs>
<div class="talk-read-list">
<n-infinite-scroll style="height: 340px;" @load="loadMoreReadListDetail">
<div
class="talk-read-list-item"
v-for="(talkReadDetailItem,
talkReadDetailIndex) in state.talkReadListDetail"
:key="talkReadDetailIndex"
>
<avatarModule
:mode="1"
:avatar="talkReadDetailItem.avatar"
:userName="talkReadDetailItem.nickName"
:groupType="0"
:customStyle="{
width: '36px',
height: '36px'
}"
:customTextStyle="{
fontSize: '12px',
fontWeight: 'bold',
color: '#fff',
lineHeight: '17px'
}"
></avatarModule>
<div class="talk-read-list-item-info">
<span style="font-size: 12px; font-weight: 600; line-height: 17px;">{{
talkReadDetailItem.nickName
}}</span>
<span style="font-size: 12px; color: #999; line-height: 14px;">{{
talkReadDetailItem.jobNum
}}</span>
</div>
</div>
</n-infinite-scroll>
</div>
</div>
</n-popover>
</div>
</main>
</div>
@ -488,7 +948,7 @@ const onContextMenuAvatar=(e:any,item:any)=>{
</div>
<!-- 置底按钮 -->
<SkipBottom v-model="skipBottom" />
<SkipBottom v-model="skipBottom" :customSkipBottomEvent="true" @customSkipBottomEvent="onCustomSkipBottomEvent"/>
</section>
<!-- 右键菜单 -->
@ -676,6 +1136,17 @@ const onContextMenuAvatar=(e:any,item:any)=>{
}
}
.talk_read_num {
text-align: right;
color: #7a58de;
font-size: 12px;
font-weight: 400;
line-height: 17px;
margin: 5px 0 0;
span {
}
}
&:hover {
.talk-title {
opacity: 1;
@ -724,4 +1195,31 @@ const onContextMenuAvatar=(e:any,item:any)=>{
}
}
}
.talk-read-list-detail {
width: 341px;
padding: 0 14px;
.talk-read-list {
.talk-read-list-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #f1f1f1;
.talk-read-list-item-info {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
span {
}
}
}
}
}
</style>

View File

@ -3,20 +3,26 @@ import { useDialogueStore } from '@/store'
import { DoubleDown } from '@icon-park/vue-next'
import { scrollToBottom } from '@/utils/dom'
defineProps(['modelValue'])
const props = defineProps(['modelValue', 'customSkipBottomEvent'])
const emit = defineEmits(['customSkipBottomEvent'])
const dialogueStore = useDialogueStore()
//
const onSkipBottom = () => {
console.log('onSkipBottom')
scrollToBottom()
if(props?.customSkipBottomEvent){
emit('customSkipBottomEvent')
}else{
scrollToBottom()
}
}
</script>
<template>
<!-- 置底按钮 -->
<div class="skip-bottom pointer" :class="{ show: modelValue }" @click="onSkipBottom">
<div class="skip-bottom pointer" :class="{ show: props?.modelValue }" @click="onSkipBottom">
<span v-if="dialogueStore.unreadBubble">{{ dialogueStore.unreadBubble }} 条未读消息</span>
<span v-else>回到底部</span>
<n-icon size="14" color="#fff" :component="DoubleDown" />

View File

@ -33,6 +33,7 @@ export function useMenu() {
const showDropdownMenu = (e: any, uid: number, item: any) => {
// dropdown.item = Object.assign({}, item)
dropdown.item = item
dropdown.item.is_self_action = true
dropdown.options = []
if ([4].includes(item.msg_type)) {
if(item.is_convert_text === 1){