chat-app/src/pages/dialog/index.vue

2350 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dialog-page">
<ZPaging
use-chat-record-mode
use-virtual-list
:inside-more="true"
:hide-no-more-inside="true"
cell-height-mode="fixed"
: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"
:hideBack="dialogueStore.isOpenMultiSelect"
>
<template #left v-if="dialogueStore.isOpenMultiSelect">
<text
class="ml-[36rpx]"
@click="dialogueStore.isOpenMultiSelect = false"
>
取消
</text>
</template>
<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"
v-if="!talkParams.isDismiss && !talkParams.isQuit"
>
<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"
:revokeInfo="item.revokeInfo"
:extra="item.extra"
>
<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.user_id === talkParams.uid,
'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)"
@touchstart="() => handleAvatarTouchStart(item)"
@touchend="handleAvatarTouchEnd"
>
<!-- <im-avatar
class="pointer"
:src="item.avatar"
:size="80"
:username="item.nickname"
@click="showUserInfoModal(item.user_id)"
/> -->
<avatarModule
:mode="1"
:avatar="item.avatar"
:groupType="0"
:userName="item.nickname"
:customStyle="{ width: '80rpx', height: '80rpx' }"
:customTextStyle="{
fontSize: '32rpx',
fontWeight: 'bold',
color: '#fff',
lineHeight: '44rpx',
}"
></avatarModule>
</aside>
<!-- 主体信息 -->
<main class="main-column">
<div class="talk-title">
<span
class="nickname pointer"
v-show="
talkParams.type == 2 && item.user_id !== talkParams.uid
"
>
<!-- <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) || isLeader"
>
<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-if="
loadConfig.status == 1 ||
(loadConfig.status == 2 && !state.localPageLoadDone)
"
@click="onScrollToLower"
>
查看更多消息 ...
</span>
<span
v-if="loadConfig.status == 2 && state.localPageLoadDone"
class="no-more"
>
没有更多消息了
</span>
</div>
</div>
<template #bottom>
<div class="footBox" id="footBoxArea">
<span
class="flex items-center justify-center text-[24rpx] text-[#999999]"
style="background-color: #e5e5e5; padding: 12rpx 24rpx;"
v-if="uploadsParams.isUploading"
>
{{
'正在发送中,剩余' +
uploadsParams.uploadingNum +
'个...请不要离开哦~'
}}
</span>
<div v-if="!dialogueStore.isOpenMultiSelect">
<div
class="pt-[16rpx] ml-[32rpx] mr-[32rpx] flex items-start justify-between"
>
<div class="flex-1 quillBox" style="">
<QuillEditor
ref="editor"
id="editor"
:options="editorOption"
@editorChange="onEditorChange"
style="width: 100%; flex: 1; height: 100%;"
@click="onEditorClick"
v-if="state.canUseQuillEditor && !state.isUseSpeech"
/>
<tm-input
type="textarea"
autoHeight
focusColor="#F9F9F9"
color="#F9F9F9"
:inputPadding="[12]"
placeholder=""
v-if="!state.canUseQuillEditor && !state.isUseSpeech"
@update:modelValue="onTextAreaChange"
v-model="state.textAreaValue"
@input="onTextAreaInput"
></tm-input>
<allSpeech
@startRecord="startRecord"
@endRecord="endRecord"
@cancelRecord="cancelRecord"
popupTitle=""
popupDefaultTips=""
:btnDefaultText="$t('hold_to') + ' ' + $t('speak')"
lineStartColor="#fff"
lineEndColor="#fff"
:chatInputHeight="chatInputHeight"
v-if="state.isUseSpeech"
></allSpeech>
<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?.msg_type !== 1"
class="text-[28rpx] text-[#999]"
>
{{
state?.quoteInfo?.nickname +
'' +
ChatMsgTypeMapping[state?.quoteInfo?.msg_type]
}}
</span>
<img
@click="clearQuoteInfo"
style="width: 30rpx; height: 30rpx;"
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="发送"
:loading="state.isLoading"
:width="120"
v-if="!state.isUseSpeech"
></tm-button>
</div>
</div>
<div v-if="state.isOpenEmojiPanel" class="mt-[50rpx]">
<emojiPanel
@on-select="
(data) => {
if (!state.canUseQuillEditor) {
onTextAreaEmoticon(data)
} else {
onEmoticonEvent(data)
}
}
"
/>
</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] w-full text-center"
>
撤回
</div>
<div class="divider"></div>
<div
@click="state.showWin = false"
class="mt-[32rpx] mb-[32rpx] text-[32rpx] text-[#000000] leading-[48rpx] w-full text-center"
>
取消
</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="@/static/image/chatList/mention_select_hide_bg.png"
/>
<img
style="
position: absolute;
top: 50%;
left: 50%;
margin-left: -9rpx;
margin-top: -5rpx;
"
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 allSpeech from '@/uni_modules/all-speech/components/all-speech/all-speech.vue'
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,
ServeClearTalkUnreadNum,
ServeTalkRecords,
} 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, onUnload as uniOnUnload } from '@dcloudio/uni-app'
Quill.register('formats/emoji', EmojiBlot)
import 'quill-mention'
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 uploadsStore = useUploadsStore()
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),
isDismiss: computed(() => dialogueStore.isDismiss),
isQuit: computed(() => dialogueStore.isQuit),
adminList: computed(() => dialogueStore.getAdminList),
unReadNum: computed(() => dialogueStore.unreadNum),
})
const uploadsParams = reactive({
isUploading: computed(() => uploadsStore.isUploading),
uploadingNum: computed(() => uploadsStore.uploadingNum),
})
const state = ref({
isOpenEmojiPanel: false,
isOpenFilePanel: false,
showWin: false,
onfocusItem: null,
sessionId: '', //会话Id
localPageLoadDone: true, //分页加载缓存中的聊天记录是否完毕
quoteInfo: null, //引用信息
mentionIsMulSelect: false, //是否是多选提醒的人
selectedMembersNum: 0, //选中的要提醒的人数
mentionSelectHeight: 0, //选择要提醒人的区域高度
selectAreaHeight: 0, //选择要提醒人的可选人员列表区域高度
isShowMentionSelect: false, //是否显示要提醒人的选择区域
useCustomLoadMore: false, //是否使用自定义加载更多事件(下拉刷新、上拉加载)
recordDate: '', //按日期查询聊天记录的开始日期
serveFindRecord: [], //调用接口查找到的聊天记录
middleMsg: {}, //缓存中没有时,调用接口初次使用的依据记录
keepDialogInfo: false, //是否保存会话信息
lastCursorIndex: undefined, //在失焦前保存光标位置
mentionUserIds: [], //存储所有被@的用户ID
isInsertingMention: false, //是否正在插入mention
showMentionSelectTimer: null, //显示提醒选择框的定时器
lastMentionText: '', // 记录上一次触发@时的完整文本内容
lastMentionTriggered: false, // 记录当前@是否已经触发过
lastMentionPosition: -1, // 添加新的状态来记录上一次@的位置
isLoading: false, //发送按钮loading
lastSelection: 0,
canUseQuillEditor: true, //是否可以使用quill编辑器如果版本不支持则使用普通输入框
textAreaValue: '', //普通输入框的值
isUseSpeech: true, //是否使用语音输入
})
uniOnload(async (options) => {
console.log('onLoad' + JSON.stringify(options))
if (options.sessionId) {
state.value.sessionId = options.sessionId
}
if (options.keepDialogInfo) {
state.value.keepDialogInfo = options.keepDialogInfo === '1' ? true : false
}
if (options.msgInfo) {
const msgInfo = JSON.parse(decodeURIComponent(options.msgInfo))
queryRecordsByMsgInfo(msgInfo)
state.value.useCustomLoadMore = true
return
}
if (options.recordDate) {
state.value.recordDate = options.recordDate
const msgInfo = await findTalkRecords(options.recordDate, true)
queryRecordsByMsgInfo(msgInfo)
state.value.useCustomLoadMore = true
return
}
initData()
})
uniOnUnload(() => {
console.log('onUnload')
ServeClearTalkUnreadNum(
{
talk_type: Number(talkParams.type),
receiver_id: Number(talkParams.receiver_id),
},
talkParams.unReadNum,
).then(() => {
talkStore.updateItem({
index_name: talkParams.index_name,
unread_num: 0,
})
dialogueStore.clearUnreadNum()
})
})
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 quill = getQuill();
// if (quill.getText().endsWith('@\n')) {
// showMentionSelectDebounced(quill)
// }
// quill.focus()
}
const onSendMessage = (data = {}, callBack, showLoading = false) => {
if (showLoading) {
state.value.isLoading = true
}
let message = {
...data,
receiver: {
receiver_id: talkParams.receiver_id,
talk_type: talkParams.type,
},
}
ServePublishMessage(message)
.then(({ code, message }) => {
state.value.isLoading = false
if (code == 200) {
uploadsStore.updateUploadStatus(false)
if (callBack) {
callBack(true)
}
} else {
uploadsStore.updateUploadStatus(false)
message.warning(message)
}
})
.catch(() => {
state.value.isLoading = false
uploadsStore.updateUploadStatus(false)
message.warning('网络繁忙,请稍后重试!')
})
}
const onTextAreaChange = (value) => {
state.value.textAreaValue = value
// 更新mentionUserIds数组确保与输入框中的@内容一致
const atPattern = /@([^@\s]+)(?:\s|$)/g
const matches = Array.from(value.matchAll(atPattern))
const mentionedUsers = matches.map((match) => match[1].trim().toLowerCase())
// 获取当前对话中的所有成员信息
const members = dialogueStore.members || []
// 根据@的用户名找到对应的user_id
const newMentionUserIds = mentionedUsers
.map((username) => {
if (username === '所有人') {
return 0 // 如果是@所有人返回0
}
const member = members.find(
(m) => m.nickname.trim().toLowerCase() === username,
)
return member ? member.id : null
})
.filter((id) => id !== null)
// 只有在输入框中有@内容时才更新mentionUserIds
if (value.includes('@')) {
state.value.mentionUserIds = newMentionUserIds
} else {
state.value.mentionUserIds = []
}
}
// 处理输入框输入事件
const onTextAreaInput = (value) => {
console.log(value, 'value')
if (value.length > 0) {
if (value[value.length - 1] === '@') {
if (talkParams.type === 1) {
return
}
state.value.isShowMentionSelect = true
}
}
}
const onSendMessageClick = () => {
if (!state.value.canUseQuillEditor) {
// 处理普通输入框的发送消息
if (state.value.textAreaValue.trim() === '') {
return
}
if (state.value.textAreaValue.length > 1024) {
return message.warning('发送内容超长,请分条发送')
}
let message = {
type: 'text',
content: state.value.textAreaValue,
quote_id: state.value.quoteInfo ? state.value.quoteInfo.msg_id : null, // 添加quote_id
mentions: state.value.mentionUserIds, // 使用最终的用户ID数组
}
console.log(message)
onSendMessage(
message,
(ok) => {
if (!ok) return
state.value.textAreaValue = ''
state.value.quoteInfo = null // 发送后清除引用信息
},
true,
)
return
}
// 发送前确保 mentionUserIds 是最新的
updateMentionUserIds()
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.trim() === '') {
return
}
if (data.items[0].content.length > 1024) {
return message.warning('发送内容超长,请分条发送')
}
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: state.value.mentionUserIds, // 使用最终的用户ID数组
}
console.log(message)
onSendMessage(message, callBack, true)
}, 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)
}
}
let calcDelta = false
const onEditorChange = () => {
if(calcDelta){
calcDelta = false
return
}
// 以下逻辑是 光标与@之间存在其他内容时 不触发选人弹窗
const qtext = getQuill().getText()
let selectIdx = getQuill().getSelection().index
const textBeforeCursor = qtext.substring(0, selectIdx)
if (textBeforeCursor[0]?.charCodeAt(0) === 10) {
const delta = getQuill().getContents()
const ops = delta.ops || []
if (ops[0].insert === '\n') {
ops.splice(0, 1)
getQuill().setContents(delta)
getQuill().setSelection(getQuill().getText().length, 0)
}
// for (let i = 0; i < ops.length; i++) {
// if (ops[i].insert === '\n') {
// console.error('有空格')
// ops.splice(i, 1)
// calcDelta = true
// }
// }
// getQuill().setContents(delta)
// getQuill().setSelection(getQuill().getText().length, 0)
}
let endWithAt = false
if (
textBeforeCursor[textBeforeCursor.length - 1] === '@' ||
textBeforeCursor[textBeforeCursor.length - 2]?.charCodeAt(0) === 64
) {
endWithAt = true
}
if (endWithAt) {
state.value.cursorMention = true
} else {
state.value.cursorMention = false
}
if (getQuill().getText() !== state.value.lastMentionText) {
state.value.lastMentionTriggered = false
}
let delta = getQuill().getContents()
let text = deltaToString(delta)
// 如果正在插入mention不进行mention的检查和更新
if (!state.value.isInsertingMention) {
updateMentionUserIds()
}
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 += `<span class="nickname">${data.nickname}</span>`
return el
},
source: function (searchTerm, renderList, mentionChar) {
if (talkParams.type === 1) {
return
}
// 在失焦前保存光标位置
const quill = getQuill()
const range = quill.getSelection()
if (range) {
state.value.lastCursorIndex = range.index
}
// 使用防抖函数
showMentionSelectDebounced(quill)
},
mentionContainerClass: '',
},
},
placeholder: '',
}
const handleSelectImg = (data, file_num) => {
if (Array.isArray(data)) {
// 批量发送图片
data.forEach((item) => {
onSendMessage({ ...item, file_num: item.file_num })
})
} else {
// 单张图片发送
onSendMessage({ ...data, file_num })
}
}
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 (!dialogueList || dialogueList?.length === 0) {
state.value.localPageLoadDone = true
return
}
// 只在正常加载模式下检查是否加载完缓存数据
if (!state.value.useCustomLoadMore) {
if (
newValue[newValue.length - 1]?.sequence ===
dialogueList?.records?.[0]?.sequence
) {
if (
dialogueList?.records?.[0]?.sequence !== 1 &&
!state.value.localPageLoadDone
) {
loadConfig.status = 1
}
//相同意味着分页加载缓存中的聊天记录完毕
state.value.localPageLoadDone = true
} else {
state.value.localPageLoadDone = false
}
}
}
},
{
deep: true,
},
)
const onScrollToLower = async () => {
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 || recordIndex === -1) {
const moreRecords = await findTalkRecords(
'',
false,
tempVirtualList[0].sequence,
{
direction: 'up',
sort_sequence: '',
},
)
console.log(moreRecords)
// 格式化新加载的消息
const formattedMoreRecords = moreRecords.map((item) => ({
...item,
float: item.user_id === talkParams.uid ? 'right' : 'left',
}))
virtualList.value = formattedMoreRecords.concat(tempVirtualList).reverse()
loadConfig.status =
dialogueList?.records?.[0]?.sequence > 1 && moreRecords.length > 0
? 1
: 2
} else {
if (tempVirtualList[0].sequence > dialogueList.records[0].sequence) {
virtualList.value = dialogueList.records
.slice(Math.max(0, recordIndex - 10), 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 = async () => {
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,
)
console.log(recordIndex)
if (!recordIndex || recordIndex === -1) {
// 记住加载更多前消息的ID
const currentMsgId = tempVirtualList[tempVirtualList.length - 1].msg_id
const moreRecords = await findTalkRecords(
'',
false,
tempVirtualList[tempVirtualList.length - 1].sequence,
)
console.log(moreRecords)
// 格式化新加载的消息
const formattedMoreRecords = moreRecords.map((item) => ({
...item,
float: item.user_id === talkParams.uid ? 'right' : 'left',
}))
virtualList.value = tempVirtualList
.concat(formattedMoreRecords.reverse())
.reverse()
console.log(virtualList.value)
// 数据更新后,滚动到之前的位置
nextTick(() => {
zpagingRef.value?.scrollIntoViewById('zp-id-' + currentMsgId)
})
} else {
if (
tempVirtualList[tempVirtualList.length - 1].sequence <
dialogueList.records[dialogueList.records.length - 1].sequence
) {
// 记住加载更多前消息的ID
const currentMsgId = tempVirtualList[tempVirtualList.length - 1].msg_id
virtualList.value = tempVirtualList
.concat(
dialogueList.records.slice(
recordIndex + 1,
Math.min(recordIndex + 11, dialogueList.records.length),
),
)
.reverse()
// 数据更新后,滚动到之前的位置
nextTick(() => {
zpagingRef.value?.scrollIntoViewById('zp-id-' + currentMsgId)
})
}
}
}
}
const clearMultiSelect = () => {
dialogueStore.setMultiSelect(false)
virtualList.value.forEach((item) => {
item.isCheck = false
})
}
const initData = async () => {
const dialogueList = getDialogueList(talkParams.index_name)
let doLocalPaging = false
if(dialogueList?.records?.length > 0){
doLocalPaging = true
}
console.error('dialogueList', dialogueList)
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 })
if(doLocalPaging){
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
// 清除保存的光标位置
state.value.lastCursorIndex = undefined
// 获得焦点
const quill = getQuill()
// quill.focus()
}
//确认要提醒人的选择
const confirmMentionSelect = () => {
if (state?.value.selectedMembersNum > 0) {
if (selectMemberByAlphabetRef.value) {
selectMemberByAlphabetRef.value.confirmSelectMembers()
}
hideMentionSelect()
// 重置选中人数和多选状态
state.value.selectedMembersNum = 0
state.value.mentionIsMulSelect = false
}
}
//获取选择的结果
const getSelectResult = (mentionSelect) => {
console.log(mentionSelect)
getMentionSelectLists(mentionSelect)
}
//处理要提醒人的消息样式
const getMentionSelectLists = (mentionSelectList) => {
if (!state.value.canUseQuillEditor) {
if (mentionSelectList.length > 0) {
mentionSelectList.forEach((item) => {
onTextAreaMention(item)
})
}
return
}
const quill = getQuill()
const mention = quill.getModule('mention')
// 使用之前保存的光标位置
if (state.value.lastCursorIndex !== undefined) {
// 删除光标前的 @ 符号
quill.deleteText(state.value.lastCursorIndex - 1, 1)
}
// 设置正在插入mention的标记
state.value.isInsertingMention = true
// 逐个插入mention
mentionSelectList.forEach((mentionSelectItem) => {
// 如果是@所有人,确保 id 为 0
const id =
mentionSelectItem?.nickname === '所有人' ? 0 : mentionSelectItem?.id
mention.insertItem(
{
id: id,
denotationChar: '@',
value: mentionSelectItem?.nickname + ' ',
},
true,
)
})
// 清除保存的光标位置
state.value.lastCursorIndex = undefined
// 延迟清除插入mention的标记并手动触发一次内容更新
setTimeout(() => {
state.value.isInsertingMention = false
// 手动触发一次内容更新,确保 mentionUserIds 是最新的
updateMentionUserIds()
}, 100)
hideMentionSelect()
}
// 新增一个更新 mentionUserIds 的函数
const updateMentionUserIds = () => {
const delta = getQuill().getContents()
const ops = delta.ops || []
const currentMentions = new Set()
// 收集当前所有@的用户ID并转换为number类型
ops.forEach((op) => {
if (op.insert && op.insert.mention) {
const id = Number(op.insert.mention.id)
// 过滤掉 null 值
if (!isNaN(id)) {
currentMentions.add(id)
}
}
})
// 如果当前有@所有人则添加0
if (currentMentions.has(0)) {
state.value.mentionUserIds = [0]
} else {
// 如果没有@任何人,保持空数组
state.value.mentionUserIds = Array.from(currentMentions)
}
}
//根据msg信息找到对应的聊天记录并根据sequence等查看上下文
const queryRecordsByMsgInfo = async (msgInfo) => {
console.log(msgInfo)
state.value.middleMsg = msgInfo
const dialogueList = getDialogueList(talkParams.index_name)
const recordIndex = dialogueList?.records?.findIndex(
(record) => record.msg_id === msgInfo.msg_id,
)
let recordsList = []
console.log(recordIndex)
if (!recordIndex || recordIndex === -1) {
recordsList = await findTalkRecords('', true, msgInfo.sequence - 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)
recordsList = dialogueList.records.slice(startRecordIndex, endRecordIndex)
}
// 格式化消息,确保每条消息都有正确的 float
recordsList = recordsList.map((item) => {
return {
...item,
float: item.user_id === talkParams.uid ? 'right' : 'left',
}
})
nextTick(() => {
zpagingRef.value.complete(recordsList.reverse())
loadConfig.status =
dialogueList?.records?.[0]?.sequence > 1 && recordsList.length > 0 ? 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)
})
})
}
//查找聊天记录
const findTalkRecords = (record, isMiddle, sequence, appointParams) => {
return new Promise((resolve, reject) => {
let params = {
talk_type: talkParams.type, //1私聊2群聊
receiver_id: talkParams.receiver_id, //目标用户id或群聊id
no_limit: '', //1不限制
file_name: '',
msg_type: 0, //消息类型0:全部;2:代码;3:图片;4:音频;5:视频;6:文件;7:位置;9:会话;11群投票;12图文混合
cursor: sequence || 0, //上次查询的游标
limit: 10, //数据行数
direction: 'down', //down向下查最新up向上查老数据
start_time: '',
end_time: '',
group_member_user_id: 0, //群成员id当查询群历史消息的时候需要指定群成员的时候送
sort_sequence: 'asc',
create_time: state.value.middleMsg.created_at,
}
if (record) {
params = Object.assign({}, params, {
start_time: record,
end_time: record,
limit: 1,
direction: '',
})
}
if (appointParams) {
params = Object.assign({}, params, appointParams)
}
console.log(params)
const resp = ServeTalkRecords(params)
console.log(resp)
resp.then(({ code, data }) => {
console.log(data)
if (code == 200) {
if (data?.items.length > 0) {
if (record) {
resolve(data?.items[0])
return
}
if (isMiddle) {
state.value.serveFindRecord = JSON.parse(
JSON.stringify(data?.items),
)
return findTalkRecords('', false, sequence + 1, {
direction: 'up',
sort_sequence: '',
}).then((upResult) => {
const finalResult = [...upResult, ...state.value.serveFindRecord]
console.log(finalResult)
resolve(finalResult)
})
} else {
state.value.serveFindRecord = data?.items
.reverse()
.concat(state.value.serveFindRecord)
resolve(JSON.parse(JSON.stringify(state.value.serveFindRecord)))
state.value.serveFindRecord = []
}
} else {
resolve([])
}
} else {
resolve([])
}
})
resp.catch(() => {})
})
}
//是否是管理员
const isLeader = computed(() => {
if (talkParams.adminList.length > 0) {
return (
talkParams.adminList.filter(
(adminItem) => adminItem.erp_user_id === useAuth()?.userInfo?.value?.ID,
).length > 0
)
}
return false
})
//长按头像@用户
const doMentionUser = (mentionSelect) => {
console.log(mentionSelect)
if (talkParams.type === 1) {
return
}
// 构造正确的 mention 对象
const mentionObj = {
id: mentionSelect.user_id, // 使用 user_id 而不是 erp_user_id
nickname: mentionSelect.nickname,
}
getMentionSelectLists([mentionObj])
}
let avatarPressTimer = null
let currentPressItem = null
const handleAvatarTouchStart = (item) => {
currentPressItem = item
avatarPressTimer = setTimeout(() => {
if (!state.value.canUseQuillEditor) {
onTextAreaMention(item)
} else {
doMentionUser(item)
}
}, 500)
}
const handleAvatarTouchEnd = () => {
if (avatarPressTimer) {
clearTimeout(avatarPressTimer)
avatarPressTimer = null
}
currentPressItem = null
}
onMounted(async () => {
if (uni.getSystemInfoSync().osName === 'ios') {
let versions = uni.getSystemInfoSync().osVersion.split('.')
if (Number(versions[0]) < 17) {
console.error('ios版本低于17')
state.value.canUseQuillEditor = false
}
}
if (typeof plus !== 'undefined') {
const webview = plus.webview.currentWebview()
webview.setStyle({
bottom: 0,
})
} else {
document.addEventListener('plusready', () => {
const webview = plus.webview.currentWebview()
webview.setStyle({
bottom: 0,
})
})
}
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(() => {
if (!state.value.keepDialogInfo) {
dialogueStore.setDialogue({})
}
clearMultiSelect()
if (avatarPressTimer) {
clearTimeout(avatarPressTimer)
}
// 清理防抖定时器
if (state.value.showMentionSelectTimer) {
clearTimeout(state.value.showMentionSelectTimer)
}
if (uploadsStore.isUploading) {
uploadsStore.clearUpload()
}
})
// 修改防抖函数的实现
const showMentionSelectDebounced = (quill) => {
const text = quill.getText()
// 光标不在@后第一位时,不触发选人弹窗
if (!state.value.cursorMention) {
return
}
// 以下逻辑是 记录触发@时,用户的输入框内容,下次一样内容得@不会再次触发
if (text !== state.value.lastMentionText) {
state.value.lastMentionTriggered = false
}
// 如果已经触发过,则不再触发
if (state.value.lastMentionTriggered) {
console.log('return')
return
}
// 记录当前文本内容和触发状态
state.value.lastMentionText = text
state.value.lastMentionTriggered = true
state.value.isShowMentionSelect = true
// quill.blur()
}
// 处理普通输入框的表情插入
const onTextAreaEmoticon = (data) => {
if (data.type == 1) {
if (data.img) {
// 如果是图片表情,插入对应的文本标记
state.value.textAreaValue += `${data.value}`
} else {
// 如果是文本表情,直接插入
state.value.textAreaValue += data.value
}
}
}
// 处理普通输入框的长按@功能
const onTextAreaMention = (user) => {
if (talkParams.type === 1) {
return
}
if (state.value.textAreaValue.length > 0) {
if (
state.value.textAreaValue[state.value.textAreaValue.length - 1] === '@'
) {
state.value.textAreaValue = state.value.textAreaValue.slice(0, -1)
}
}
state.value.textAreaValue += `@${user.nickname} `
// 先创建新数组,再赋值
const newMentionUserIds = state.value.mentionUserIds
? [...state.value.mentionUserIds]
: []
newMentionUserIds.push(user.nickname === '所有人' ? 0 : user.id)
state.value.mentionUserIds = newMentionUserIds
if (state.value.isShowMentionSelect) {
state.value.isShowMentionSelect = false
}
}
</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: scroll;
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%;
word-break: break-all;
}
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 {
:deep(.ql-clipboard) {
position: relative;
opacity: 0;
height: 1rpx;
overflow: auto;
display: none;
}
: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;
.ed-emoji {
width: 44rpx;
height: 44rpx;
display: inline-block;
}
}
:deep(.round-3) {
max-height: 320rpx;
overflow-y: scroll;
}
}
: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;
}
}
}
:deep(.mention) {
color: #1890ff;
}
</style>