<template> <div class="dialog-page"> <ZPaging use-chat-record-mode use-virtual-list cell-height-mode="dynamic" :refresher-enabled="true" :show-scrollbar="false" :loading-more-enabled="true" :hide-empty-view="true" height="100%" ref="zpagingRef" v-model="virtualList" :loading-more-custom-style="{ display: 'none', height: '0' }" @scrolltolower="onScrollToLower" @scrolltoupper="onScrollToUpper" > <template #top> <customNavbar :title="talkParams.username" id="navBarArea"> <template #subTitle v-if="talkStore?.findItem(talkParams.index_name)?.group_type === 4" > <div class="text-[24rpx] text-[#999999]">公司群</div> </template> <template #right> <div class="mr-[36rpx] toChatSetting_btn"> <tm-icon color="rgb(51, 51, 51)" :font-size="36" name="tmicon-gengduo" @click="toChatSettingsPage" ></tm-icon> </div> </template> </customNavbar> </template> <!-- <template #top> <div class="load-toolbar pointer"> <span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span> <span v-else-if="loadConfig.status == 1" @click="onScrollToLower"> 查看更多消息 ... </span> <span v-else class="no-more"> 没有更多消息了 </span> </div> </template> --> <!-- 数据加载状态栏 --> <div class="dialog-list" @touchstart="handleHidePanel"> <div class="message-item" v-for="item in virtualList" :id="`zp-id-${item.msg_id}`" :key="item.zp_index" style="transform: scaleY(-1);" > <!-- 系统消息 --> <div v-if="item.msg_type >= 1000" class="message-box"> <component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" /> </div> <!-- 撤回消息 --> <div v-else-if="item.is_revoke == 1" class="message-box"> <revoke-message :login_uid="userStore.uid" :user_id="item.user_id" :nickname="item.nickname" :talk_type="item.talk_type" :datetime="item.created_at" :msg_id="item.msg_id" > <template v-if="canEditRevokedMessage(item) && item.user_id === userStore.uid"> <span class="edit-revoked-message" @click="restoreRevokedMessage(item)">重新编辑</span> </template> </revoke-message> </div> <div v-else class="message-box record-box" :class="{ 'direction-rt': item.float == 'right', 'multi-select': dialogueStore.isOpenMultiSelect, 'multi-select-check': item.isCheck, }" > <!-- 多选按钮 --> <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column" > <!-- <n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" /> --> <tm-checkbox :round="10" :defaultChecked="item.isCheck" @update:modelValue="item.isCheck = !item.isCheck" :size="42" color="#46299D" ></tm-checkbox> </aside> <!-- 头像信息 --> <aside class="avatar-column" @click="toUserDetailPage(item)"> <im-avatar class="pointer" :src="item.avatar" :size="80" :username="item.nickname" @click="showUserInfoModal(item.user_id)" /> </aside> <!-- 主体信息 --> <main class="main-column"> <div class="talk-title"> <span class="nickname pointer" v-show="talkParams.type == 2 && item.float == 'left'" > <span class="at">@</span> {{ item.nickname }} </span> <span> {{ parseTime(item.created_at, '{m}/{d} {h}:{i}') }} </span> </div> <div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }" > <deepBubble @clickMenu="(menuType) => onContextMenu(menuType, item)" :isShowCopy="isShowCopy(item)" :isShowWithdraw="isRevoke(talkParams.uid, item)" > <component class="component-content" :key="item.zp_index" :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" :max-width="true" :source="'panel'" /> </deepBubble> <!-- <div class="talk-tools"> <template v-if="talkParams.type == 1 && item.float == 'right'"> <loading theme="outline" size="19" fill="#000" :sxtrokeWidth="1" class="icon-rotate" v-show="item.send_status == 1" /> <span v-show="item.send_status == 1"> 正在发送... </span> <span v-show="item.send_status != 1"> 已送达 </span> </template> </div> --> </div> <div v-if="item.extra.reply" class="talk-reply pointer" @click="onJumpMessage(item.extra?.reply?.msg_id)" > <!-- <n-icon :component="ToTop" size="14" class="icon-top" /> --> <span class="ellipsis"> 回复 {{ item.extra?.reply?.nickname }}: {{ item.extra?.reply?.content }} </span> </div> </main> </div> </div> <div class="load-toolbar pointer" style="transform: scaleY(-1);"> <span v-if="loadConfig.status == 0">正在加载数据中 ...</span> <span v-else-if="loadConfig.status == 1" @click="onScrollToLower"> 查看更多消息 ... </span> <span v-else-if=" loadConfig.status != 0 && loadConfig.status != 1 && state.localPageLoadDone " class="no-more" > 没有更多消息了 </span> </div> </div> <template #bottom> <div class="footBox" id="footBoxArea"> <div v-if="!dialogueStore.isOpenMultiSelect"> <div class="pt-[16rpx] ml-[32rpx] mr-[32rpx] flex items-start justify-between" > <div class="flex-1 quillBox"> <QuillEditor ref="editor" id="editor" :options="editorOption" @editorChange="onEditorChange" style="width: 100%; flex: 1; height: 100%; border: none;" @click="onEditorClick" /> <!-- <tm-input type=textarea autoHeight focusColor="#F9F9F9" color="#F9F9F9" :inputPadding="[12]" placeholder=""></tm-input> --> <div class="quote-area" v-if="state?.quoteInfo"> <span v-if="state?.quoteInfo?.msg_type === 1" class="text-[28rpx] text-[#999]" > {{ state?.quoteInfo?.nickname + ':' + state?.quoteInfo?.extra?.content }} </span> <span v-if="state?.quoteInfo" class="text-[28rpx] text-[#999]" > {{ state?.quoteInfo?.nickname + ':' + ChatMsgTypeMapping[state?.quoteInfo?.msg_type] }} </span> <img @click="clearQuoteInfo" style="width: 30rpx; height: 30rpx;" src="/src/static/image/login/check-circle-filled@3x.png" /> </div> </div> <div class="flex items-center justify-end h-[72rpx]"> <tm-image :margin="[10, 0]" @click="handleEmojiPanel" :width="52" :height="52" :round="12" :src="state.isOpenEmojiPanel ? keyboard : smile" ></tm-image> <tm-image @click="handleFilePanel" :margin="[10, 0]" :width="52" :height="52" :round="12" :src="addCircleGray" ></tm-image> <tm-button @click="onSendMessageClick" :margin="[0, 0]" :padding="[0, 30]" color="#46299D" :fontSize="28" size="mini" :shadow="0" label="发送" ></tm-button> </div> </div> <div v-if="state.isOpenEmojiPanel" class="mt-[50rpx]"> <emojiPanel @on-select="onEmoticonEvent" /> </div> <div v-if="state.isOpenFilePanel" class="mt-[16rpx]"> <filePanel @selectImg="handleSelectImg" :talkParams="talkParams" /> </div> </div> <div v-else class="h-[232rpx]"> <div class="flex items-center justify-center mt-[12rpx] text-[24rpx] text-[#747474] leading-[44rpx]" > <div class="mr-[8rpx]">已选中:</div> <div>{{ selectedMessage.length }}条消息</div> </div> <div class="flex items-center justify-around pl-[128rpx] pr-[128rpx] mt-[18rpx] text-[20rpx] text-[#737373]" > <div @click="handleMergeForward" class="flex flex-col items-center justify-center" > <tm-image :width="68" :height="68" :src="zu6050"></tm-image> <div class="mt-[6rpx]">合并转发</div> </div> <div @click="handleSingleForward" class="flex flex-col items-center justify-center" > <tm-image :width="68" :height="68" :src="zu6051"></tm-image> <div class="mt-[6rpx]">逐条转发</div> </div> <div @click="handleWechatForward" class="flex flex-col items-center justify-center" > <tm-image :width="68" :height="68" :src="zu6052"></tm-image> <div class="mt-[6rpx]">微信</div> </div> <div @click="handleDelete" class="flex flex-col items-center justify-center" > <tm-image :width="68" :height="68" :src="zu6053"></tm-image> <div class="mt-[6rpx]">删除</div> </div> </div> </div> <!--底部安全区--> <div class="content-placeholder"></div> <tm-drawer placement="bottom" v-model:show="state.showWin" :hideHeader="true" :height="416" :round="6" > <div class="w-full h-full flex flex-col items-center"> <div class="mt-[46rpx] mb-[44rpx] leading-[48rpx] text-[#747474] text-[24rpx]" > 撤回该条消息? </div> <div class="divider"></div> <div @click="withdrawerConfirm" class="mt-[32rpx] mb-[32rpx] text-[32rpx] text-[#CF3050] leading-[48rpx]" > 撤回 </div> <div class="divider"></div> <div @click="state.showWin = false" class="mt-[32rpx] mb-[32rpx] text-[32rpx] text-[#000000] leading-[48rpx]" > 取消 </div> </div> </tm-drawer> </div> </template> </ZPaging> <tm-drawer placement="bottom" v-model:show="state.isShowMentionSelect" :hideHeader="true" :round="5" :height="state.mentionSelectHeight" > <div class="mention-select-drawer flex flex-row flex-1 flex-row flex-row-center-between" > <div class="cancel-btns flex-row flex flex-row-center-start" style="width: 210rpx;" > <div class="hide-btn" v-if="!state.mentionIsMulSelect" @click="hideMentionSelect" > <img style="width: 40rpx; height: 40rpx;" src="/src/static/image/chatList/mention_select_hide_bg.png" /> <img style=" position: absolute; top: 50%; left: 50%; margin-left: -9rpx; margin-top: -5rpx; " src="/src/static/image/chatList/mention_select_hide_icon.png" /> </div> <span style="flex-shrink: 0; display: block;" class="text-[32rpx] font-regular text-[#191919]" v-if="state.mentionIsMulSelect" @click="changeMentionSelectMul(false)" > {{ $t('cancel') }} </span> </div> <div class="flex flex-row-center-center flex-col" style="padding: 6rpx 0;" > <text>{{ $t('chat.mention.select') }}</text> </div> <div class="flex-row flex flex-row-center-end" style="width: 210rpx;"> <div class="mention-edit-btn" v-if="!state.mentionIsMulSelect" @click="changeMentionSelectMul(true)" > <span class="text-[32rpx] font-regular text-[#191919]"> {{ $t('button.multiple.choice') }} </span> </div> <div class="mention-done-btn" :class=" state?.selectedMembersNum > 0 ? 'mention-done-btn-can-do' : '' " v-if="state.mentionIsMulSelect" @click="confirmMentionSelect" > <span class="text-[32rpx] font-regular text-[#191919]"> {{ $t('button.text.done') }} </span> <span class="text-[32rpx] font-regular text-[#191919]" v-if="state?.selectedMembersNum > 0" > {{ '(' + state?.selectedMembersNum + ')' }} </span> </div> </div> </div> <selectMemberByAlphabet :manageType="'mention'" ref="selectMemberByAlphabetRef" :selectAreaHeight="state.selectAreaHeight" @updateSelectedMembersNum="updateSelectedMembersNum" :isMulSelect="state.mentionIsMulSelect" @getSelectResult="getSelectResult" @getMentionSelectLists="getMentionSelectLists" ></selectMemberByAlphabet> </tm-drawer> </div> </template> <script setup> import selectMemberByAlphabet from '../chatSettings/components/selectMemberByAlphabet.vue' import { ref, reactive, watch, computed, onMounted, onUnmounted, nextTick, } from 'vue' import { QuillEditor, Quill } from '@vueup/vue-quill' import EmojiBlot from './formats/emoji' import { useChatList } from '@/store/chatList/index.js' import { useAuth } from '@/store/auth' import { useUserStore, useDialogueStore, useUploadsStore, useEditorDraftStore, useTalkStore, useSettingsStore, useDialogueListStore, } from '@/store' import addCircleGray from '@/static/image/chatList/addCircleGray.png' import { MessageComponents, ForwardableMessageType, ChatMsgTypeMapping, } from '@/constant/message' import { formatTime, parseTime } from '@/utils/datetime' import { deltaToMessage, deltaToString, isEmptyDelta } from './util' import smile from '@/static/image/chatList/smile@2x.png' import keyboard from '@/static/image/chatList/keyboard@2x.png' import { useInject, useTalkRecord } from '@/hooks' import { emitCall } from '@/utils/common' import ZPaging from '@/uni_modules/z-paging/components/z-paging/z-paging.vue' import useZPaging from '@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js' import emojiPanel from './components/emojiPanel.vue' import filePanel from './components/filePanel.vue' import lodash from 'lodash' import { ServePublishMessage,detailGetRecordsContext } from '@/api/chat' import copy07 from '@/static/image/chatList/copy07@2x.png' import multipleChoices from '@/static/image/chatList/multipleChoices@2x.png' import cite from '@/static/image/chatList/cite@2x.png' import withdraw from '@/static/image/chatList/withdraw@2x.png' import delete07 from '@/static/image/chatList/delete@2x.png' import zu6050 from '@/static/image/chatList/zu6050@2x.png' import zu6051 from '@/static/image/chatList/zu6051@2x.png' import zu6052 from '@/static/image/chatList/zu6052@2x.png' import zu6053 from '@/static/image/chatList/zu6053@2x.png' import deepBubble from '@/components/deep-bubble/deep-bubble.vue' import { isRevoke } from './menu' import useConfirm from '@/components/x-confirm/useConfirm.js' import { onLoad as uniOnload } from '@dcloudio/uni-app' Quill.register('formats/emoji', EmojiBlot) const selectMemberByAlphabetRef = ref(null) const { getDialogueList, updateZpagingRef, virtualList, } = useDialogueListStore() const talkStore = useTalkStore() const { showConfirm } = useConfirm() const settingsStore = useSettingsStore() const userStore = useUserStore() const dialogueStore = useDialogueStore() const editorDraftStore = useEditorDraftStore() const editor = ref() const zpagingRef = ref() useZPaging(zpagingRef) const indexName = computed(() => dialogueStore.index_name) const talkParams = reactive({ uid: computed(() => userStore.uid), index_name: computed(() => dialogueStore.index_name), type: computed(() => dialogueStore.talk.talk_type), receiver_id: computed(() => dialogueStore.talk.receiver_id), username: computed(() => dialogueStore.talk.username), online: computed(() => dialogueStore.online), keyboard: computed(() => dialogueStore.keyboard), num: computed(() => dialogueStore.members.length), }) const state = ref({ isOpenEmojiPanel: false, isOpenFilePanel: false, showWin: false, onfocusItem: null, sessionId: '', localPageLoadDone: true, //分页加载缓存中的聊天记录是否完毕 quoteInfo: null, //引用信息 mentionIsMulSelect: false, //是否是多选提醒的人 selectedMembersNum: 0, //选中的要提醒的人数 mentionSelectHeight: 0, //选择要提醒人的区域高度 selectAreaHeight: 0, //选择要提醒人的可选人员列表区域高度 isShowMentionSelect: false, //是否显示要提醒人的选择区域 useCustomLoadMore: false, //是否使用自定义加载更多事件(下拉刷新、上拉加载) }) uniOnload((options) => { if (options.sessionId) { state.value.sessionId = options.sessionId } if (options.msgInfo) { const msgInfo = JSON.parse(decodeURIComponent(options.msgInfo)) queryRecordsByMsgInfo(msgInfo) state.value.useCustomLoadMore = true return } initData() }) const handleEmojiPanel = () => { state.value.isOpenFilePanel = false state.value.isOpenEmojiPanel = !state.value.isOpenEmojiPanel } const handleFilePanel = () => { state.value.isOpenEmojiPanel = false state.value.isOpenFilePanel = !state.value.isOpenFilePanel } //点击隐藏表情/文件上传 面板 const handleHidePanel = () => { state.value.isOpenFilePanel = false state.value.isOpenEmojiPanel = false } //点击编辑区聚焦输入框 const onEditorClick = () => { handleHidePanel() } const onSendMessage = (data = {}, callBack) => { let message = { ...data, receiver: { receiver_id: talkParams.receiver_id, talk_type: talkParams.type, }, } ServePublishMessage(message) .then(({ code, message }) => { if (code == 200) { if (callBack) { callBack(true) } } else { message.warning(message) } }) .catch(() => { message.warning('网络繁忙,请稍后重试!') }) } const onSendMessageClick = () => { let delta = getQuill().getContents() if (state.value.quoteInfo) { delta.ops.unshift({ insert: { quote: { id: state.value.quoteInfo.msg_id, }, }, }) } let data = deltaToMessage(delta) if (data.items.length === 0) { return } switch (data.msgType) { case 1: // 文字消息 if (data.items[0].content.length > 1024) { return message.info('发送内容超长,请分条发送') } onSendTextEvent({ data, callBack: (ok) => { if (!ok) return getQuill().setContents([], Quill.sources.USER) if (state.value.quoteInfo) { state.value.quoteInfo = null } }, }) break } } // 发送文本消息 const onSendTextEvent = lodash.throttle((value) => { let { data, callBack } = value let message = { type: 'text', content: data.items[0].content, quote_id: data.quoteId, mentions: data.mentionUids, } onSendMessage(message, callBack) }, 1000) // 编辑器输入事件 const onInputEvent = ({ data }) => { talkStore.updateItem({ index_name: indexName.value, draft_text: data, }) // 判断对方是否在线和是否需要推送 // 3秒时间内推送一次 if (settingsStore.isKeyboard && props.online) { onKeyboardPush() } } // 发送表情消息 const onSendEmoticonEvent = ({ data }) => { onSendMessage({ type: 'emoticon', emoticon_id: data }) } // 注册事件 const evnets = { text_event: onSendTextEvent, input_event: onInputEvent, emoticon_event: onSendEmoticonEvent, history_event: () => { isShowHistory.value = true }, } const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, } = useTalkRecord(talkParams.uid) const getQuill = () => { return editor.value?.getQuill() } const isShowCopy = (item) => { switch (item.msg_type) { case 1: return true case 3: return true case 5: return true case 6: return true default: return false } } const selectedMessage = computed(() => { return virtualList.value.filter((item) => item.isCheck) }) // 编辑器事件 const onEditorEvent = (msg) => { evnets[msg.event] && evnets[msg.event](msg) } const getQuillSelectionIndex = () => { let quill = getQuill() return (quill.getSelection() || {}).index } const onEmoticonEvent = (data) => { if (data.type == 1) { const quill = getQuill() let index = getQuillSelectionIndex() if (index == 1 && quill.getLength() == 1 && quill.getText(0, 1) == '\n') { quill.deleteText(0, 1) index = 0 } if (data.img) { quill.insertEmbed(index, 'emoji', { alt: data.value, src: data.img, }) } else { quill.insertText(index, data.value) } quill.setSelection(index + 1, 0, 'user') } else { let fn = emitCall('emoticon_event', data.value, () => {}) emit('editor-event', fn) } } const onEditorChange = () => { let delta = getQuill().getContents() let text = deltaToString(delta) if ( text.length > 0 && text.slice(-2).trim() === '@' && talkParams.type === 2 ) { state.value.isShowMentionSelect = true } if (!isEmptyDelta(delta)) { editorDraftStore.items[indexName.value || ''] = JSON.stringify({ text: text, ops: delta.ops, }) } else { // 删除 editorDraftStore.items 下的元素 delete editorDraftStore.items[indexName.value || ''] } onEditorEvent(emitCall('input_event', text)) // emit('editor-event', emitCall('input_event', text)) // 清理过期的撤回消息(超过5分钟) const now = new Date().getTime() Object.keys(state.value.revokedMessages).forEach(msgId => { if ((now - state.value.revokedMessages[msgId].revokeTime) > 5 * 60 * 1000) { delete state.value.revokedMessages[msgId] } }) } const onClipboardMatcher = (node, Delta) => { const ops = [] Delta.ops.forEach((op) => { // 如果粘贴了图片,这里会是一个对象,所以可以这样处理 if (op.insert && typeof op.insert === 'string') { ops.push({ insert: op.insert, // 文字内容 attributes: {}, //文字样式(包括背景色和文字颜色等) }) } else { ops.push(op) } }) Delta.ops = ops return Delta } const editorOption = { debug: false, modules: { toolbar: false, clipboard: { // 粘贴版,处理粘贴时候的自带样式 matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]], }, keyboard: { bindings: { enter: { key: 13, handler: onSendMessageClick, }, }, }, // imageUploader: { // upload: onEditorUpload // }, // mention: { // allowedChars: /^[\u4e00-\u9fa5]*$/, // mentionDenotationChars: ['@'], // positioningStrategy: 'fixed', // renderItem: (data) => { // const el = document.createElement('div') // el.className = 'ed-member-item' // el.innerHTML = `<img src="${data.avatar}" class="avator"/>` // el.innerHTML += `<span class="nickname">${data.nickname}</span>` // return el // }, // source: function (searchTerm, renderList) { // if (!props.members.length) { // return renderList([]) // } // let list = [ // { id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' }, // ...props.members // ] // const items = list.filter( // (item) => item.nickname.toLowerCase().indexOf(searchTerm) !== -1 // ) // renderList(items) // }, // mentionContainerClass: 'ql-mention-list-container me-scrollbar me-scrollbar-thumb' // } }, placeholder: '', } const handleSelectImg = (data) => { onSendMessage({ ...data }) } const virtualListChange = (vList) => { virtualList.value = vList } const onContextMenu = (menuType, item) => { console.log(menuType, item, 'item') switch (menuType) { case 'actionCopy': actionCopy(item) break case 'multipleChoose': multipleChoose(item) break case 'actionCite': actionCite(item) break case 'actionWithdraw': actionWithdraw(item) break case 'actionDelete': actionDelete(item) break default: break } } const actionCopy = (item) => { console.log('复制') let content = '' switch (item.msg_type) { case 1: content = item.extra.content break case 3: content = item.extra.url break case 5: content = item.extra.url break default: break } uni.setClipboardData({ data: content, }) } const multipleChoose = (item) => { item.isCheck = true dialogueStore.setMultiSelect(true) } const actionCite = (item) => { console.log('引用') state.value.quoteInfo = item } //清除引用信息 const clearQuoteInfo = () => { state.value.quoteInfo = null } const actionWithdraw = (item) => { console.log('撤回') state.value.onfocusItem = item state.value.showWin = true } const withdrawerConfirm = () => { dialogueStore.ApiRevokeRecord(state.value.onfocusItem.msg_id) state.value.onfocusItem = null state.value.showWin = false } // 添加检查是否可以重新编辑撤回消息的函数 const canEditRevokedMessage = (item) => { console.log( item) if(item.is_revoke === 1 && item.msg_type === 1) { const now = new Date().getTime(); const revokeTime = new Date(item.created_at).getTime(); console.log(now) // 检查是否在5分钟内 return (now - revokeTime) <= 5 * 60 * 1000 } return false } // 添加恢复撤回消息到输入框的函数 const restoreRevokedMessage = async (item) => { // 接口拿数据,之后把查询的内容给输入框 const res = await detailGetRecordsContext({ msgId: item.msg_id }) console.log(res) if(res.code == 200) { const content = res.data.item?.extra?.content; const quill = getQuill() quill.setText(content) // 将光标设置到文本末尾 quill.setSelection(content.length, 0) quill.focus() } /* const revokedMsg = state.value.revokedMessages[msgId] // 根据消息类型处理 if (revokedMsg.msgType === 1) { // 文本消息 const quill = getQuill() quill.setText(revokedMsg.content) quill.focus() } */ // 可以根据需要添加其他类型消息的处理 } const actionDelete = (item) => { console.log('删除') item.isCheck = true handleDelete() } const handleMergeForward = () => { if (selectedMessage.value.length == 0) { return message.warning('未选择消息') } console.log('合并转发') dialogueStore.setForwardType(2) dialogueStore.setForwardMessages(selectedMessage.value) uni.navigateTo({ url: '/pages/chooseChat/index', success: function (res) { clearMultiSelect() }, }) } const handleSingleForward = () => { if (selectedMessage.value.length == 0) { return message.warning('未选择消息') } console.log('逐条转发') dialogueStore.setForwardType(1) dialogueStore.setForwardMessages(selectedMessage.value) uni.navigateTo({ url: '/pages/chooseChat/index', success: function (res) { clearMultiSelect() }, }) } const handleWechatForward = () => { if (selectedMessage.value.length == 0) { return message.warning('未选择消息') } console.log('微信转发') } const handleDelete = () => { if (selectedMessage.value.length == 0) { return message.warning('未选择消息') } console.log('删除') showConfirm({ content: '确定删除聊天记录', confirmText: '删除', confirmColor: '#CF3050', onConfirm: async () => { const msgIds = selectedMessage.value.map((item) => item.msg_id) virtualList.value = virtualList.value.filter( (item) => !msgIds.includes(item.msg_id), ) dialogueStore.ApiDeleteRecord(msgIds) clearMultiSelect() }, onCancel: () => {}, }) } //更新选中的要提醒的人数 const updateSelectedMembersNum = (numChange) => { state.value.selectedMembersNum = state.value.selectedMembersNum + numChange } watch( () => zpagingRef.value, (newValue, oldValue) => { if (newValue) { updateZpagingRef(newValue) } }, ) watch( () => virtualList.value, (newValue, oldValue) => { if (newValue) { const dialogueList = getDialogueList(talkParams.index_name) // console.log(newValue[newValue.length - 1]?.sequence, dialogueList?.records?.[0]?.sequence) if ( newValue[newValue.length - 1]?.sequence === dialogueList.records?.[0]?.sequence ) { //相同意味着分页加载缓存中的聊天记录完毕 state.value.localPageLoadDone = true } else { state.value.localPageLoadDone = false } } }, { deep: true, }, ) const onScrollToLower = () => { if (state.value.useCustomLoadMore) { const tempVirtualList = lodash.cloneDeep(virtualList.value).reverse() const dialogueList = getDialogueList(talkParams.index_name) const recordIndex = dialogueList.records.findIndex( (record) => record.msg_id === tempVirtualList[0].msg_id, ) if (recordIndex === -1) { } else { if (tempVirtualList[0].sequence > dialogueList.records[0].sequence) { virtualList.value = dialogueList.records .slice(0, recordIndex) .concat(tempVirtualList) .reverse() // zpagingRef.value.setLocalPaging( // dialogueList.records // .slice(0, recordIndex) // .concat(tempVirtualList) // .reverse(), // // zpagingRef.value.scrollIntoViewById('zp-id-' + virtualList.value[virtualList.value.length - 1].msg_id) // ) console.log(virtualList.value) } } return } if (state.value.localPageLoadDone) { //本地缓存的聊天记录分页加载完之后,才可以请求接口 onRefreshLoad() } } //本来的下拉刷新——列表倒置后为上拉加载 const onScrollToUpper = () => { if (state.value.useCustomLoadMore) { const tempVirtualList = lodash.cloneDeep(virtualList.value).reverse() const dialogueList = getDialogueList(talkParams.index_name) const recordIndex = dialogueList.records.findIndex( (record) => record.msg_id === tempVirtualList[tempVirtualList.length - 1].msg_id, ) if (recordIndex === -1) { } else { if ( tempVirtualList[tempVirtualList.length - 1].sequence < dialogueList.records[dialogueList.records.length - 1].sequence ) { virtualList.value = tempVirtualList .concat( dialogueList.records.slice( recordIndex + 1, dialogueList.records.length, ), ) .reverse() // zpagingRef.value.setLocalPaging( // tempVirtualList // .concat( // dialogueList.records.slice( // recordIndex + 1, // dialogueList.records.length, // ), // ) // .reverse(), // // zpagingRef.value.scrollIntoViewById('zp-id-' + virtualList.value[virtualList.value.length - 1].msg_id) // ) console.log(virtualList.value) } } } } const clearMultiSelect = () => { dialogueStore.setMultiSelect(false) virtualList.value.forEach((item) => { item.isCheck = false }) } const initData = async () => { const dialogueList = getDialogueList(talkParams.index_name) let objT = { uid: talkParams.uid, talk_type: talkParams.type, receiver_id: talkParams.receiver_id, index_name: talkParams.index_name, direction: dialogueList ? 'down' : 'up', no_limit: dialogueList ? 1 : 0, } await onLoad({ ...objT }) zpagingRef.value?.setLocalPaging(records.value) } //点击跳转到聊天设置页面 const toChatSettingsPage = () => { uni.navigateTo({ url: '/pages/chatSettings/index?groupId=' + talkParams?.receiver_id + '&sessionId=' + state.value.sessionId, }) } //点击跳转到用户详情页面 const toUserDetailPage = (userItem) => { uni.navigateTo({ url: '/pages/dialog/dialogDetail/userDetail?erpUserId=' + userItem.erp_user_id, }) } //切换提醒的人选择弹窗多选状态 const changeMentionSelectMul = (status) => { state.value.mentionIsMulSelect = status } //隐藏要提醒人的选择 const hideMentionSelect = () => { state.value.isShowMentionSelect = false } //确认要提醒人的选择 const confirmMentionSelect = () => { if (state?.value.selectedMembersNum > 0) { if (selectMemberByAlphabetRef.value) { selectMemberByAlphabetRef.value.confirmSelectMembers() } hideMentionSelect() } } //获取选择的结果 const getSelectResult = (mentionSelect) => { console.log(mentionSelect) getMentionSelectLists(mentionSelect) } //处理要提醒人的消息样式 const getMentionSelectLists = (mentionSelectList) => { console.log(mentionSelectList) let mentionUserIds = [] let mentionUsers = getQuill().getContents().ops //先读出来之前的信息内容 mentionUsers[0].insert = mentionUsers[0].insert.slice(0, -2) + mentionUsers[0].insert.slice(-1) console.log(mentionUsers[0].insert) mentionSelectList.forEach((mentionSelectItem) => { mentionUserIds.push(mentionSelectItem.id) mentionUsers.push({ insert: '@' + mentionSelectItem.nickname + ' ', attributes: { // mention: { // id: mentionSelectItem.id, // }, color: '#1890ff', }, }) }) getQuill().setContents(mentionUsers) hideMentionSelect() } //根据msg信息找到对应的聊天记录,并根据sequence等查看上下文 const queryRecordsByMsgInfo = (msgInfo) => { console.log(msgInfo) const dialogueList = getDialogueList(talkParams.index_name) const recordIndex = dialogueList.records.findIndex( (record) => record.msg_id === msgInfo.msg_id, ) if (recordIndex === -1) { } else { // console.log(recordIndex) const startRecordIndex = Math.max(0, recordIndex - 10) const endRecordIndex = Math.max(0, recordIndex + 10) // console.log(dialogueList.records.slice(startRecordIndex, endRecordIndex)) // console.log(recordIndex-startRecordIndex) const recordsList = dialogueList.records.slice( startRecordIndex, endRecordIndex, ) nextTick(() => { zpagingRef.value.complete(recordsList.reverse()) loadConfig.status = dialogueList.records?.[0]?.sequence > 1 ? 1 : 2 nextTick(() => { let offset = uni.getSystemInfoSync().windowHeight const navBarAreaQuery = uni.createSelectorQuery() navBarAreaQuery .select('#navBarArea') .boundingClientRect((res) => { if (res) { // console.log('元素高度:', res.height) offset = offset - res.height } }) .exec() const footBoxAreaQuery = uni.createSelectorQuery() footBoxAreaQuery .select('#footBoxArea') .boundingClientRect((res) => { if (res) { // console.log('元素高度:', res.height) offset = offset - res.height } }) .exec() setTimeout(() => { zpagingRef.value.scrollIntoViewById( 'zp-id-' + msgInfo.msg_id, offset - 60, ) }, 1000) }) }) } } onMounted(async () => { nextTick(() => { state.value.mentionSelectHeight = pxTorPx( uni.getSystemInfoSync().windowHeight * 0.86, ) state.value.selectAreaHeight = rpxToPx(state.value.mentionSelectHeight) - rpxToPx(90) + 'px' }) }) const pxTorPx = (px) => { const sysInfo = uni.getSystemInfoSync() const rpx = px / (sysInfo.screenWidth / 750) return rpx } const rpxToPx = (rpx) => { const sysInfo = uni.getSystemInfoSync() const px = (sysInfo.screenWidth / 750) * rpx return px } onUnmounted(() => { dialogueStore.setDialogue({}) clearMultiSelect() }) </script> <style scoped lang="less"> .dialog-page { flex: 1; background-image: url('@/static/image/clockIn/z3280@3x.png'); background-size: cover; background-position: bottom center; background-attachment: fixed; width: 100%; .dialog-list { padding: 20rpx 32rpx; } .toChatSetting_btn { ::v-deep .tmicon-gengduo { line-height: unset !important; } } } .edit-revoked-message { margin-left: 10rpx; color: #46299D; cursor: pointer; font-size: 24rpx; &:hover { text-decoration: underline; } } .searchRoot { background-color: #fff; padding: 22rpx 18rpx; } .contentRoot { margin-top: 20rpx; background-color: #fff; } .footBox { min-height: 162rpx; background-color: #fff; .quote-area { margin: 4rpx 0 0 0; display: flex; flex-direction: row; align-items: center; justify-content: space-between; overflow: hidden; width: 100%; span { display: -webkit-inline-box; text-overflow: ellipsis; -webkit-line-clamp: 2; -webkit-box-orient: vertical; width: 100%; } img { margin: 0 0 0 30rpx; flex-shrink: 0; } } } .load-toolbar { height: 50rpx; color: #409eff; text-align: center; line-height: 50rpx; font-size: 24rpx; .no-more { color: #b9b3b3; } } .message-item { &.border { border-radius: 16rpx; } } .message-box { width: 100%; min-height: 30rpx; margin-bottom: 5rpx; } .record-box { display: flex; flex-direction: row; align-items: flex-start; .checkbox-column { display: flex; justify-content: center; width: 42rpx; order: 1; user-select: none; margin-top: 20rpx; margin-right: 20rpx; } .avatar-column { width: 80rpx; display: flex; align-items: center; order: 2; user-select: none; margin-top: 16rpx; flex-direction: column; } .main-column { flex: 1 auto; order: 3; position: relative; box-sizing: border-box; padding: 16rpx 20rpx 14rpx 20rpx; // overflow: hidden; min-height: 30px; .talk-title { display: flex; align-items: center; margin-bottom: 6rpx; font-size: 24rpx; user-select: none; color: #bababa; opacity: 1; &.show { opacity: 1; } .nickname { color: var(--im-text-color); margin-right: 5rpx; .at { display: none; } &:hover { color: var(--im-primary-color); .at { display: inline-block; } } } span { transform: scale(0.88); transform-origin: left center; } } .talk-content { display: flex; justify-content: flex-start; align-items: flex-end; box-sizing: border-box; width: 100%; .talk-tools { display: flex; margin: 0 16rpx; color: #a79e9e; font-size: 24rpx; user-select: none; align-items: center; justify-content: space-around; .more-tools { visibility: hidden; margin-left: 5rpx; } } } .talk-reply { display: flex; align-items: flex-start; align-items: center; width: fit-content; padding: 8rpx; margin-top: 6rpx; margin-right: auto; font-size: 24rpx; color: #8f8f8f; word-break: break-all; background-color: var(--im-message-left-bg-color); border-radius: 10rpx; max-width: 450rpx; overflow: hidden; user-select: none; .icon-top { margin-right: 6rpx; } .ellipsis { display: -webkit-inline-box; text-overflow: ellipsis; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } } &:hover { .talk-title { opacity: 1; } .more-tools { visibility: visible !important; } } } &.direction-rt { .avatar-column { order: 3; } .main-column { order: 2; .talk-title { justify-content: flex-end; span { transform-origin: right center; } } .talk-content { flex-direction: row-reverse; } .talk-reply { margin-right: 0; margin-left: auto; } } } &.multi-select { border-radius: 5px; &:hover, &.multi-select-check { background-color: var(--im-active-bg-color); } } } .content-placeholder { height: 58rpx; } .quillBox { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; :deep(.ql-clipboard) { position: relative; opacity: 0; height: 1rpx; overflow: auto; } :deep(.ql-editor) { padding: 14rpx 22rpx; background-color: #f9f9f9; border-radius: 8rpx; outline: none !important; max-height: 294rpx; overflow: auto; line-height: 44rpx; font-size: 32rpx; p { display: inline-flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; white-space: normal; word-break: break-all; .ed-emoji { width: 44rpx; height: 44rpx; display: inline-block; } span { user-select: all; } } } } :deep(.wd-action-sheet) { background-color: #8b8b8b !important; } :deep(.wd-action-sheet__panel-title) { color: #fff !important; } .component-content { position: relative; z-index: 1; /* 确保 z-index 低于 deepBubble */ } .divider { width: 100%; height: 1rpx; background-color: #e7e7e7; } .mention-select-drawer { display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 36rpx 32rpx 0; .cancel-btns { flex-shrink: 0; display: flex; flex-direction: row; align-items: center; justify-content: center; .hide-btn { display: flex; flex-direction: row; align-items: center; justify-content: center; position: relative; text { } img { width: 18rpx; height: 10rpx; } } } .mention-done-btn { display: flex; flex-direction: row; align-items: center; justify-content: center; padding: 6rpx 24rpx; background-color: #f3f3f3; border-radius: 8rpx; flex-shrink: 0; span { color: #bababa; line-height: 40rpx; flex-shrink: 0; } } .mention-done-btn-can-do { background-color: #46299d; span { color: #fff; } } .mention-edit-btn { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; flex-shrink: 0; span { flex-shrink: 0; } } } </style>