uniapp-app/pages_chat/chat/message/message-input.vue
2025-09-24 11:37:39 +08:00

1546 lines
42 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="input-root">
<div class="msg-input-wrapper">
<!-- 当回复他人消息时输入框上方需要展示被回复消息相关内容 -->
<div v-if="isReplyMsg" class="reply-message-wrapper">
<div class="reply-message-close" @tap="removeReplyMsg">
<Icon
color="#929299"
:iconStyle="{ fontWeight: '200' }"
:size="13"
type="icon-guanbi1"
/>
</div>
<div class="reply-line"></div>
<div class="reply-title">{{ t('replyText') }}</div>
<div class="reply-to">
<Appellation
:account="replyMsg && replyMsg.senderId"
:team-id="
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
color="#929299"
:fontSize="13"
>
</Appellation>
</div>
<div class="reply-to-colon">:</div>
<div
v-if="replyMsg && replyMsg.messageClientId === 'noFind'"
class="reply-noFind"
>
{{ t('replyNotFindText') }}
</div>
<div class="reply-message" v-else>
<message-one-line
v-if="
replyMsg &&
replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
:text="replyMsg && replyMsg.text"
></message-one-line>
<div v-else>
{{
replyMsg
? '[' + REPLY_MSG_TYPE_MAP[replyMsg.messageType] + ']'
: ''
}}
</div>
</div>
</div>
<!-- 输入框上按钮组 -->
<div class="msg-button-group">
<div
@tap="handleAudioVisible"
v-if="!(isWeb || isHarmonyOs || isGroupSend)"
class="msg-input-button"
>
<Icon
v-if="audioPanelVisible"
:size="20"
type="audio-btn-selected"
key="audio-btn-selected"
/>
<Icon v-else :size="20" type="icon-audio" key="icon-audio" />
</div>
<div class="msg-input-button">
<Icon @tap="handleEmojiVisible" :size="20" type="icon-biaoqing" />
</div>
<div class="msg-input-button">
<div>
<Icon @tap="handleSendImageMsg" :size="20" type="icon-tupian" />
</div>
</div>
<div class="msg-input-button">
<Icon @tap="handleSendMoreVisible" type="send-more" :size="20" />
</div>
<!-- <div class="msg-input-button">
<Icon @tap="handleSetting" type="icon-shezhi" :size="20" />
</div> -->
</div>
<div v-if="inputVisible" class="msg-input">
<!-- 当从表情面板切换到文字输入时直接唤起键盘会导致input框滚动消失故此处需要用EmojiInput兼容下保证先隐藏表情面板再弹出键盘 -->
<div
v-show="showEmojiInput"
@click="onClickEmojiInput"
class="input-emoji"
>
<div v-if="inputText" class="input-text">{{ inputText }}</div>
<div v-else class="input-placeholder">
{{
isTeamMute ? t('teamMutePlaceholder') : t('chatInputPlaceHolder')
}}
</div>
</div>
<input
v-show="!showEmojiInput"
:focus="isFocus"
class="msg-input-input"
:maxlength="-1"
:placeholder="
isTeamMute ? t('teamMutePlaceholder') : t('chatInputPlaceHolder')
"
v-model="inputText"
type="text"
:disabled="isTeamMute"
:confirm-hold="true"
cursor-spacing="20"
adjust-position="true"
confirm-type="send"
@confirm="handleSendTextMsg"
@blur="handleInputBlur"
@input="handleInput"
id="msg-input"
/>
</div>
<!-- 表情面板 -->
<div v-if="emojiVisible" class="msg-emoji-panel" @click.stop="() => {}">
<Face
@emojiClick="handleEmoji"
@emojiDelete="handleEmojiDelete"
@emojiSend="handleSendTextMsg"
/>
</div>
<!-- 发送语音消息面板 -->
<div
v-if="audioPanelVisible"
class="msg-audio-panel"
@click.stop="() => {}"
>
<VoicePanel @handleSendAudioMsg="handleSendAudioMsg"></VoicePanel>
</div>
<!-- 发送更多面板 -->
<div
v-if="sendMoreVisible"
class="send-more-panel"
@click.stop="() => {}"
>
<div class="send-more-panel-item-wrapper" v-if="!(isGroupSend)">
<div
class="send-more-panel-item"
@tap="(event:any) => handleSendVideoMsg('camera', event)"
>
<Icon type="icon-paishe" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('shootText') }}</div>
</div>
<div class="send-more-panel-item-wrapper" v-if="!(isGroupSend)">
<div
class="send-more-panel-item"
@tap="(event: any) => handleSendVideoMsg('album', event)"
>
<Icon type="icon-shipin2" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('albumText') }}</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('reply', event)"
>
<image :src="quickImg" mode="widthFix"></image>
</div>
<div class="icon-text">快捷回复</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('hj', event)"
>
<image :src="quickImg" mode="widthFix"></image>
</div>
<div class="icon-text">患教</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('outpatient', event)"
>
<image :src="chuzhenImg" mode="widthFix"></image>
</div>
<div class="icon-text">出/停诊公告</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('mall', event)"
>
<image :src="mallImg" mode="widthFix"></image>
</div>
<div class="icon-text">商城</div>
</div>
<!-- <div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('hospital', event)"
>
<image :src="hospitalImg" mode="widthFix"></image>
</div>
<div class="icon-text">互联网医院</div>
</div> -->
<!-- 音频呼叫 -->
<!-- <div
class="send-more-panel-item-wrapper"
v-if="
isAndroidOrIosApp &&
props.conversationType !==
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
"
>
<div class="send-more-panel-item" @tap="handleCall(1)">
<Icon type="icon-audio-call" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('voiceCallText') }}</div>
</div> -->
<!-- 视频呼叫 -->
<!-- <div
class="send-more-panel-item-wrapper"
v-if="
isAndroidOrIosApp &&
props.conversationType !==
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
"
>
<div class="send-more-panel-item" @tap="() => handleCall(2)">
<Icon type="icon-video-call" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('videoCallText') }}</div>
</div> -->
<!-- <div
v-if="isWeb"
class="send-more-panel-item-wrapper"
@tap="handleSendFileMsg"
>
<div class="send-more-panel-item">
<Icon type="icon-file" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('fileText') }}</div>
</div> -->
</div>
</div>
<!-- @消息相关 popup -->
<UniPopup
ref="popupRef"
background-color="#ffffff"
type="bottom"
mask-background-color="rgba(0,0,0,0.4)"
@change="onPopupChange"
>
<MentionMemberList :team-id="to"></MentionMemberList>
</UniPopup>
<!-- 复制自 groupSend 的遮罩与弹窗 -->
<view v-if="showModal" class="mask" @tap="closeModal"></view>
<view v-if="showModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="onSelectSince('article')">
<text>图文科普</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="onSelectSince('video')">
<text>视频科普</text>
</view>
</view>
<!-- 商城弹窗 -->
<view v-if="showMallModal" class="mask" @tap="closeMallModal"></view>
<view v-if="showMallModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="sendMallMsg">
<text>纽娃复合营养素固体饮料</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" >
<text>更多商品正在准备中</text>
</view>
</view>
</div>
</template>
<script lang="ts" setup>
/** 消息页面输入框组件 */
import docUrl from '@/utils/docUrl'
import Face from './face.vue'
import VoicePanel from './voice-panel.vue'
import Icon from '@/components/Icon.vue'
import { ref, getCurrentInstance, computed, onUnmounted, onMounted } from 'vue'
import { ALLOW_AT, events, REPLY_MSG_TYPE_MAP } from '@/utils/im/constants'
import { emojiMap } from '@/utils/im/emoji'
import { t } from '@/utils/im/i18n'
import { handleNoPermission } from '@/utils/im/permission'
import { customNavigateTo } from '@/utils/im/customNavigate'
import MessageOneLine from '@/components/MessageOneLine.vue'
import {
isAndroidApp,
stopAllAudio,
isIosWeb,
isWeb,
isWxApp,
startCall,
isAndroidOrIosApp,
isHarmonyOs,
} from '@/utils/im/index'
// @ts-ignore
import UniPopup from '@/components/uni-components/uni-popup/components/uni-popup/uni-popup.vue'
// @ts-ignore
import MentionMemberList from './mention-member-list.vue'
import Appellation from '@/components/Appellation.vue'
import { AT_ALL_ACCOUNT } from '@/utils/im/constants'
import { replaceEmoji } from '@/utils/im/index'
import { autorun } from 'mobx'
import quickImg from '@/static/quck_message.png'
import chuzhenImg from '@/static/outpatient_true.png'
import mallImg from '@/static/ytx_chattingfooter_shopping.png'
import hospitalImg from '@/static/ytx_chatting_hospital.png'
import {onShow,onUnload} from '@dcloudio/uni-app'
import {
V2NIMTeam,
V2NIMTeamChatBannedMode,
V2NIMTeamMember,
} from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMTeamService'
import {
V2NIMMessageForUI,
YxServerExt,
YxAitMsg,
} from '@xkit-yx/im-store-v2/dist/types/types'
//@ts-ignore
import { V2NIMConst } from '@/utils/im/nim'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
export type MentionedMember = { accountId: string; appellation: string }
import navTo from '@/utils/navTo'
import api from '@/api/api.js'
let userInfo=uni.getStorageSync('userInfo');
let expert_uuid=userInfo.uuid;
let expert_name=userInfo.realName;
let patient_uuid='';
const articleInfo=ref({});
const videoInfo=ref({});
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
//patientList.value = res.data;
for (let i = 0; i < res.data.length; i++) {
if (res.data[i].uuid.toLowerCase() === props.to) {
patient_uuid = res.data[i].uuid;
break;
}
}
}
};
const props = withDefaults(
defineProps<{
conversationType: V2NIMConst.V2NIMConversationType
to: string
isGroupSend?: boolean
replyMsgsMap?: {
[key: string]: V2NIMMessageForUI
}
}>(),
{}
)
const emits = defineEmits(['send'])
/** 会话ID */
const conversationId =
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
? uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(props.to)
: uni.$UIKitNIM.V2NIMConversationIdUtil.teamConversationId(props.to)
/** 输入框内容 */
const inputText = ref('')
const extVisible = ref(false)
/** 群发选择弹窗 */
const showModal = ref(false)
/** 商城弹窗 */
const showMallModal = ref(false)
/** 音频面板flag */
const audioPanelVisible = ref(false)
/** 发送更多面板flag */
const sendMoreVisible = ref(false)
/** 表情面板flag */
const emojiVisible = ref(false)
/** input框flag */
const inputVisible = computed(() => {
if (audioPanelVisible.value || sendMoreVisible.value) {
return false
} else {
return true
}
})
/** 发起呼叫type: 1 音频呼叫2 视频呼叫 */
const handleCall = (type: number) => {
const myAccount = uni.$UIKitStore.userStore.myUserInfo.accountId
const remoteShowName = uni.$UIKitStore.uiStore.getAppellation({
account: props.to,
})
if (myAccount) {
startCall({
remoteUserAccid: props.to,
currentUserAccid: myAccount,
type: type,
remoteShowName: remoteShowName,
})
} else {
uni.showToast({
title: t('callFailedText'),
icon: 'none',
})
}
}
const outPatientList = ref([]);
const getListOutPatient = async () => {
const res = await api.listOutPatient({
page:1,
});
if(res.code == 200){
outPatientList.value = res.data.list.list;
}
}
const stopOutPatientList = ref([]);
const getStopOutPatientList = async () => {
const res = await api.stopOutPatientList();
if(res.code == 200){
stopOutPatientList.value = res.data;
}
}
onShow(() => {
uni.$on('articelItem', (article) => {
console.log('article', article);
articleInfo.value = article;
senCustomMsg('article');
//handleSendTextMsg('article', article);
});
uni.$on('videoItem', (video) => {
console.log('video', video);
videoInfo.value = video;
senCustomMsg('video');
//handleSendTextMsg('video', video);
});
uni.$on('quickReply', (reply) => {
inputText.value= reply;
uni.$emit(events.CLOSE_PANEL)
});
getListOutPatient();
getStopOutPatientList();
});
onUnload(() => {
uni.$off('articelItem');
uni.$off('videoItem');
uni.$off('quickReply');
});
/** 商城弹窗 */
let rawStr="{\"gdxz_content\":\"我已入驻肝胆相照互联网医院,复诊购药一站式服务,快来看看吧\",\"gdxz_ext_data\":\"[互联网医院]\",\"gdxz_id\":\"1681174885629431808\",\"gdxz_img\":\"https://img.applets.igandanyiyuan.com/applet/admin/avatar/2023071813261420200708181049.png\",\"gdxz_title\":\"肝胆相照互联网医院\",\"gdxz_type\":\"[互联网医院]\",\"gdxz_url\":\"\"}";
const sendMallMsg = () => {
showMallModal.value = false;
//handleSendTextMsg('mall');
senCustomMsg('mall');
}
/** 商城弹窗 */
const closeMallModal = () => {
showMallModal.value = false
}
/** 用于解决表情面板和键盘冲突,导致输入框滚动消失问题 */
const showEmojiInput = ref(false)
/** 回复消息相关 */
const isReplyMsg = ref(false)
const isFocus = ref(false)
const replyMsg = ref<V2NIMMessageForUI>()
/** @ 消息相关 */
const ctx = getCurrentInstance()
const popupRef = ref(null)
/** @ 成员列表 */
const selectedAtMembers = ref<MentionedMember[]>([])
/** 群相关 */
const team = ref<V2NIMTeam>()
/** 群成员 */
const teamMembers = ref<V2NIMTeamMember[]>([])
/** 群禁言模式 */
const teamMute = ref<V2NIMTeamChatBannedMode>(
V2NIMConst.V2NIMTeamChatBannedMode.V2NIM_TEAM_CHAT_BANNED_MODE_UNBAN
)
/** 是否是群主 */
const isGroupOwner = ref(false)
/** 是否是群管理员 */
const isGroupManager = ref(false)
/** 群是否禁言 */
const isTeamMute = ref(false)
/** 是否允许@ 所有人 */
const allowAtAll = computed(() => {
let ext: YxServerExt = {}
try {
ext = JSON.parse((team.value || {}).serverExtension || '{}')
} catch (error) {
//
}
if (ext[ALLOW_AT] === 'manager') {
return isGroupOwner.value || isGroupManager.value
}
return true
})
/** 更新群禁言 */
const updateTeamMute = () => {
if (
teamMute.value ===
V2NIMConst.V2NIMTeamChatBannedMode.V2NIM_TEAM_CHAT_BANNED_MODE_UNBAN
) {
isTeamMute.value = false
return
}
/** 群主或者群管理员在群禁言时,可以发送消息 */
if (isGroupOwner.value || isGroupManager.value) {
isTeamMute.value = false
return
}
isTeamMute.value = true
return
}
/** 弹窗*/
const onPopupChange = (e: any) => {
uni.$emit(events.HANDLE_MOVE_THROUGH, e.value)
}
/** 打开/关闭群发选择弹窗(与 groupSend 保持一致) */
const openModal = () => {
showModal.value = true;
}
const closeModal = () => {
showModal.value = false
}
const toggleModal = () => {
showModal.value = !showModal.value
}
const onSelectSince = (type: string) => {
showModal.value = false
if (type === 'article') {
navTo({ url: '/pages_chat/article/article' })
} else if (type === 'video') {
navTo({ url: '/pages_chat/video/video' })
}
}
/** 点击@ 群成员 */
const handleMentionItemClick = (member: MentionedMember) => {
//@ts-ignore
ctx.refs.popupRef.close()
uni.$emit(events.HANDLE_MOVE_THROUGH, false)
const nickInTeam = member.appellation
selectedAtMembers.value = [
...selectedAtMembers.value.filter(
(item) => item.accountId !== member.accountId
),
member,
]
const newInputText = inputText.value + nickInTeam + ' '
// 更新input框的内容
inputText.value = newInputText
}
/** 关闭popup */
const closePopup = () => {
//@ts-ignore
ctx.refs.popupRef.close()
}
/** 点击表情输入框,隐藏表情面板,显示键盘 */
const onClickEmojiInput = () => {
showEmojiInput.value = false
extVisible.value = false
emojiVisible.value = false
if (isIosWeb) {
showKeyboard()
} else if (!isHarmonyOs) {
const timeout = setTimeout(() => {
showKeyboard()
clearTimeout(timeout)
}, 500)
}
}
/** 输入框失焦 */
const handleInputBlur = () => {
isFocus.value = false
}
/** 滚动到底部 */
const scrollBottom = () => {
if (!isWeb) {
setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
}, 300)
} else {
uni.$emit(events.ON_SCROLL_BOTTOM)
}
}
/** 输入框输入事件 */
const handleInput = (event: any) => {
const text = event?.detail?.value
const isAit = text.endsWith('@') || text.endsWith('@\n')
if (
props.conversationType ==
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
if (isAit) {
/** 当前输入的是@ */
uni.hideKeyboard()
// @ts-ignore
ctx.refs.popupRef.open('bottom')
isFocus.value = false
uni.$emit(events.HANDLE_MOVE_THROUGH, true)
}
}
}
/** 发送文本消息 */
const handleSendTextMsg = (type:string) => {
if (inputText.value.trim() === '') return
const ext = onAtMembersExtHandler()
let mallText='纽娃复合营养素固体饮料主要成分是:蜂花粉、乳清蛋白粉、灰树花粉、低聚木糖、蚕蛹氨基酸、麦芽粉、薏苡仁粉、烟酸、磷脂,以及其他调味品、辅助原料。科学配比制成,含有丰富的蛋白质、氨基酸、维生素、微量元素及其他营养元素,点击链接了解详情。'
let text = type==='mall'?mallText:replaceEmoji(inputText.value)
const textMsg = uni.$UIKitNIM.V2NIMMessageCreator.createTextMessage(text)
//let serverExtension={"gdxz_nickName":"测试","gdxz_sessionType":"general"};
if(props.isGroupSend){
emits('send',{
content:text,
msg_type:1,
})
inputText.value = ''
isReplyMsg.value = false
replyMsg.value = undefined
selectedAtMembers.value = [];
}else{
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: textMsg as unknown as V2NIMMessage,
conversationId,
serverExtension:selectedAtMembers.value.length && (ext as any),
sendBefore: () => {
scrollBottom();
},
})
.catch(() => {
uni.showToast({
icon: 'error',
title: t('sendMsgFailedText'),
})
})
.finally(() => {
scrollBottom()
})
inputText.value = ''
isReplyMsg.value = false
replyMsg.value = undefined
selectedAtMembers.value = [];
// if(type=='mall'){
// senCustomMsg('mall');
// }
}
}
/** 发送文件消息 */
const handleSendFileMsg = () => {
uni.chooseFile({
count: 1,
type: 'all',
success: (res) => {
const filePath = res?.tempFilePaths?.[0]
// @ts-ignore
const fileName = res?.tempFiles?.[0]?.name
if (filePath && fileName) {
const fileMsg = uni.$UIKitNIM.V2NIMMessageCreator.createFileMessage(
filePath,
fileName
)
uni.$UIKitStore.msgStore.sendMessageActive({
msg: fileMsg as unknown as V2NIMMessage,
conversationId,
sendBefore: () => {
scrollBottom()
},
})
}
},
fail: () => {
uni.showToast({
title: t('sendFileFailedText'),
icon: 'none',
})
},
})
}
/** 移除回复消息 */
const removeReplyMsg = () => {
uni.$UIKitStore.msgStore.removeReplyMsgActive(
replyMsg?.value?.conversationId as string
)
isReplyMsg.value = false
}
/** 显示表情面板 */
const handleEmojiVisible = () => {
if (isTeamMute.value) return
if (isWxApp || isAndroidApp || isHarmonyOs) {
const timeout = setTimeout(() => {
emojiVisible.value = true
extVisible.value = true
uni.$emit(events.ON_SCROLL_BOTTOM)
clearTimeout(timeout)
}, 300)
} else {
emojiVisible.value = true
extVisible.value = true
}
showEmojiInput.value = true
audioPanelVisible.value = false
sendMoreVisible.value = false
uni.$emit(events.ON_SCROLL_BOTTOM)
}
/** 显示发送更多"+"面板 */
const handleSendMoreVisible = () => {
if (isTeamMute.value) return
audioPanelVisible.value = false
emojiVisible.value = false
sendMoreVisible.value = !sendMoreVisible.value
setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
}, 300)
}
/** 点击表情 */
const handleEmoji = (emoji: { key: string; type: string }) => {
inputText.value += emoji.key
}
/** 删除表情 */
const handleEmojiDelete = () => {
let target = ''
const isEmojiEnd = Object.keys(emojiMap).reduce((prev, cur) => {
const isEnd = inputText.value.endsWith(cur)
if (isEnd) {
target = cur
}
return prev || isEnd
}, false)
if (isEmojiEnd && target) {
inputText.value = inputText.value.replace(target, '')
} else {
inputText.value = inputText.value.slice(0, -1)
}
}
/** 显示语音面板 */
const handleAudioVisible = () => {
if (isTeamMute.value) return
audioPanelVisible.value = !audioPanelVisible.value
emojiVisible.value = false
setTimeout(() => {
uni.$emit(events.ON_SCROLL_BOTTOM)
}, 300)
}
/** 发送图片消息 */
const handleSendImageMsg = () => {
if (isTeamMute.value) return
stopAllAudio()
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
success: (res) => {
const imgMsg = uni.$UIKitNIM.V2NIMMessageCreator.createImageMessage(
res.tempFilePaths[0]
)
if(props.isGroupSend){
const fileManager = uni.getFileSystemManager();
fileManager.readFile({
filePath: res.tempFilePaths[0],
encoding: 'base64',
success: (res) => {
emits('send',{
content:res.data,
msg_type:2,
})
},
fail: (error) => {
console.log('chooseImage', error)
}
})
}else{
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: imgMsg as unknown as V2NIMMessage,
conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
.then(() => {
scrollBottom()
})
.catch(() => {
scrollBottom()
uni.showToast({
icon: 'error',
title: t('sendImageFailedText'),
})
})
}
},
/** uniapp 提供的chooseImage api 在鸿蒙上表现为不支持选择gif在其他端支持 */
fail: (error) => {
console.log('chooseImage', error)
},
/** 没有开启权限时,提示开启权限 */
complete: handleNoPermission,
})
}
const senCustomMsg = (type:string) => {
let rawStr="";
let text="";
let msg_type=0;
let msg_content="";
let doctor_id="";
if(type == 'hospital'){
doctor_id='';
msg_type=8;
msg_content="https://img.applets.igandanyiyuan.com/applet/admin/avatar/2023071813261420200708181049.png";
text="[互联网医院]";
rawStr=`{\"gdxz_content\":\"我已入驻肝胆相照互联网医院,复诊购药一站式服务,快来看看吧\",\"gdxz_ext_data\":\"[互联网医院]\",\"gdxz_id\":\"1681174885629431808\",\"gdxz_img\":\"https://img.applets.igandanyiyuan.com/applet/admin/avatar/2023071813261420200708181049.png\",\"gdxz_title\":\"肝胆相照互联网医院\",\"gdxz_type\":\"[互联网医院]\",\"gdxz_url\":\"\"}`;
}else if(type == 'mall'){
msg_type=6;
msg_content="";
text="[肝胆商城]";
rawStr=`{\"gdxz_content\":\"肝胆相照®肝胆病在线公共服务平台\",\"gdxz_ext_data\":\"[肝胆商城]\",\"gdxz_id\":\"\",\"gdxz_img\":\"\",\"gdxz_title\":\"纽娃复合营养素固体饮料\",\"gdxz_type\":\"[肝胆商城]\",\"gdxz_url\":\"https://wx.igandan.com/shop_notify/setInfo?patient_uuid=${patient_uuid}&expert_uuid=${expert_uuid}\"}`;
}else if(type == 'outpatient'){
msg_type=5;
msg_content="";
text="[门诊公告]";
rawStr=`{\"gdxz_content\":\"门诊详情\",\"gdxz_ext_data\":\"[门诊公告]\",\"gdxz_id\":\"\",\"gdxz_img\":\"\",\"gdxz_title\":\"${expert_name}医生门诊详情\",\"gdxz_type\":\"[门诊公告]\",\"gdxz_url\":\"https://dev-wx.igandan.com/wxPatient/index.htm#/outPatient?link=share&expertUuid=${expert_uuid}\"}`
}else if(type == 'article'){
msg_type=3;
msg_content=articleInfo.value.uuid;
text="[图文科普]";
let content='"'+articleInfo.value.summary+'"';
let id='"'+articleInfo.value.uuid+'"';
let title='"'+articleInfo.value.title+'"';
let path='"'+docUrl+articleInfo.value.path+'"';
rawStr=`{\"gdxz_content\":${content},\"gdxz_ext_data\":\"[图文科普]\",\"gdxz_id\":${id},\"gdxz_img\":\"http://doc.igandan.org/app/book/pdf/2019/20190613152617.png\",\"gdxz_title\":${title},\"gdxz_type\":\"[图文科普]\",\"gdxz_url\":${path}}`
}else if(type == 'video'){
msg_type=4;
msg_content=videoInfo.value.uuid;
let content='"'+videoInfo.value.note+'"';
let id='"'+videoInfo.value.uuid+'"';
let title='"'+videoInfo.value.name+'"';
let path='"'+docUrl+videoInfo.value.path+'"';
text="[视频科普]";
rawStr=`{\"gdxz_content\":${content},\"gdxz_ext_data\":\"[视频科普]\",\"gdxz_id\":${id},\"gdxz_img\":\"http://doc.igandan.org/app/book/pdf/2019/20190613152617.png\",\"gdxz_title\":${title},\"gdxz_type\":\"[视频科普]\",\"gdxz_url\":${path}}`
}
const customMsg = uni.$UIKitNIM.V2NIMMessageCreator.createCustomMessage(text,rawStr)
if(props.isGroupSend){
emits('send',{
content:msg_content,
msg_type:msg_type,
})
}else{
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: customMsg as unknown as V2NIMMessage,
conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
.then(() => {
scrollBottom();
})
.catch(() => {
scrollBottom()
uni.showToast({
icon: 'error',
title: '发送失败',
})
})
}
}
const handleCustom = (type: string, event: any) => {
if (isTeamMute.value) return
if(type == 'reply'){
navTo({
url: '/pages_chat/quickReply/quickReply',
})
}else if(type == 'hj'){
showModal.value = true
}else if(type == 'mall'){
showMallModal.value = true
patientListByGBK();
}else if(type == 'outpatient'){
// navTo({
// url: '/pages_chat/outpatient/outpatient',
// })
if( outPatientList.value.length==0 && stopOutPatientList.value.length==0 ){
navTo({
url: '/pages_chat/outpatient/outpatient',
})
}else{
senCustomMsg('outpatient');
}
}else if(type == 'hospital'){
senCustomMsg('hospital');
}
}
/** 发送视频消息(使用相机或者从相册选择) */
const handleSendVideoMsg = (type: string, event: any) => {
if (isTeamMute.value) return
stopAllAudio()
// 这里做一层拦截的原因是微信小程序在input聚焦的时候点击+号按钮会触发此函数执行阻止冒泡也无法解决该问题疑为uniapp编译问题
if (isWxApp && event?.type == 'blur') {
return
}
uni.chooseVideo({
sourceType: [type],
compressed: true,
maxDuration: 60,
success: (res) => {
const videoMsg = uni.$UIKitNIM.V2NIMMessageCreator.createVideoMessage(
res.tempFilePath
)
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: videoMsg as unknown as V2NIMMessage,
conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
.then(() => {
scrollBottom()
})
.catch(() => {
scrollBottom()
uni.showToast({
icon: 'error',
title: t('sendVideoFailedText'),
})
})
},
/** 没有开启权限时,提示开启权限 */
complete: handleNoPermission,
})
}
/** 发送语音消息 */
const handleSendAudioMsg = (filePath: string, duration: number) => {
const audioMsg =
uni.$UIKitNIM.V2NIMMessageCreator.createAudioMessage(filePath)
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: audioMsg as unknown as V2NIMMessage,
conversationId,
progress: () => true,
sendBefore: (msg) => {
scrollBottom()
uni.$UIKitStore.msgStore.addMsg(msg.conversationId, [
{
...msg,
//@ts-ignore
attachment: {
duration: duration,
},
},
])
},
})
.then(() => {
scrollBottom()
})
.catch(() => {
uni.showToast({
icon: 'error',
title: t('sendAudioFailedText'),
})
scrollBottom()
})
}
/** 跳转设置页 */
const handleSetting = () => {
uni.hideKeyboard()
if (
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
customNavigateTo({
url: `/pages/Chat/message/p2p-set?id=${props.to}`,
})
} else if (
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
customNavigateTo({
url: `/pages/Team/team-set/index?id=${props.to}`,
})
}
}
let teamWatch = () => {}
/** 编译到鸿蒙上时通过isFocus来唤起键盘会导致input框滚动消失故鸿蒙不使用isFocus来唤起键盘由用户手动唤起 */
const showKeyboard = () => {
isFocus.value = true
}
onMounted(() => {
/** 群监听 */
teamWatch = autorun(() => {
if (
props.conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
//@ts-expect-error
const _team: V2NIMTeam = uni.$UIKitStore.teamStore.teams.get(props.to)
teamMembers.value = uni.$UIKitStore.teamMemberStore.getTeamMember(
props.to
)
const myUser = uni.$UIKitStore.userStore.myUserInfo
isGroupOwner.value = _team?.ownerAccountId == myUser.accountId
isGroupManager.value = teamMembers.value
.filter(
(item) =>
item.memberRole ===
V2NIMConst.V2NIMTeamMemberRole.V2NIM_TEAM_MEMBER_ROLE_MANAGER
)
.some((member) => member.accountId === (myUser ? myUser.accountId : ''))
team.value = _team
if (_team) {
teamMute.value = _team.chatBannedMode
}
updateTeamMute()
}
})
/** 撤回后,重新编辑消息 */
uni.$on(events.ON_REEDIT_MSG, (msg: V2NIMMessageForUI) => {
const _replyMsg = props.replyMsgsMap?.[msg.messageClientId]
// 为了解决 1.撤回回复消息A 2.再撤回普通文本消息B 3.重新编辑消息A 4.再重新编辑消息B后 输入框显示A的引用内容发送后显示A的引用内容的问题
if (msg.conversationId) {
uni.$UIKitStore.msgStore.removeReplyMsgActive(msg.conversationId)
isReplyMsg.value = false
}
/** 如果重新编辑的是回复消息,则需要将回复消息展示在输入框上方 */
if (_replyMsg?.messageClientId) {
_replyMsg && uni.$UIKitStore.msgStore.replyMsgActive(_replyMsg)
replyMsg.value = _replyMsg
isReplyMsg.value = true
}
/** 如果重新编辑的是@消息,则需要将被@ 的成员重新加入selectedAtMembers */
if (msg.serverExtension) {
const extObj = JSON.parse(msg.serverExtension)
const yxAitMsg = extObj.yxAitMsg
if (yxAitMsg) {
const _mentionedMembers: MentionedMember[] = []
Object.keys(yxAitMsg).forEach((key) => {
if (key == AT_ALL_ACCOUNT) {
_mentionedMembers.push({
accountId: key,
appellation: '所有人',
})
} else {
_mentionedMembers.push({
accountId: key,
appellation: uni.$UIKitStore.uiStore.getAppellation({
account: key,
teamId: props.to,
ignoreAlias: true,
}),
})
}
})
selectedAtMembers.value = _mentionedMembers
}
}
inputText.value = msg?.oldText || ''
if (!isHarmonyOs) {
showKeyboard()
}
})
uni.$on(events.REPLY_MSG, (msg: V2NIMMessageForUI) => {
isReplyMsg.value = true
replyMsg.value = msg
if (!isHarmonyOs) {
showKeyboard()
}
})
uni.$on(events.AIT_TEAM_MEMBER, (member: MentionedMember) => {
selectedAtMembers.value = [
...selectedAtMembers.value.filter(
(item) => item.accountId !== member.accountId
),
member,
]
const newInputText = inputText.value + '@' + member.appellation + ' '
/** 更新input框的内容 */
inputText.value = newInputText
})
/** 关闭表情、语音、发送更多面板 */
uni.$on(events.CLOSE_PANEL, () => {
emojiVisible.value = false
extVisible.value = false
audioPanelVisible.value = false
sendMoreVisible.value = false
})
/** @ 消息 @ 群成员 */
uni.$on(events.HANDLE_AIT_MEMBER, (member: MentionedMember) => {
handleMentionItemClick(member)
})
/** 关闭@ 群成员面板 */
uni.$on(events.CLOSE_AIT_POPUP, () => {
closePopup()
})
/** 表情点击 */
uni.$on(events.EMOJI_CLICK, (emoji) => {
handleEmoji(emoji)
})
/** 表情删除 */
uni.$on(events.EMOJI_DELETE, () => {
handleEmojiDelete()
})
/** 表情发送 */
uni.$on(events.EMOJI_SEND, () => {
emojiVisible.value = false
extVisible.value = false
handleSendTextMsg()
})
if (uni.onKeyboardHeightChange) {
uni.onKeyboardHeightChange((res) => {
const isAndroidWxapp =
uni.getSystemInfoSync().platform == 'android' && isWxApp
// 此处是为了点击安卓键盘上的收起按钮时,表情面板需要隐藏
if (
(res.height === 0 && isAndroidApp) ||
(res.height === 0 && isAndroidWxapp)
) {
emojiVisible.value = false
extVisible.value = false
}
})
}
})
/** 处理选中的@ 成员 */
const onAtMembersExtHandler = () => {
let ext: YxServerExt
if (selectedAtMembers.value.length) {
selectedAtMembers.value
.filter((member) => {
if (!allowAtAll.value && member.accountId === AT_ALL_ACCOUNT) {
return false
}
return true
})
.forEach((member) => {
const substr = `@${member.appellation}`
const positions: number[] = []
let pos = inputText.value?.indexOf(substr)
while (pos !== -1) {
positions.push(pos)
pos = inputText.value?.indexOf(substr, pos + 1)
}
if (positions.length) {
if (!ext) {
ext = {
yxAitMsg: {
[member.accountId]: {
text: substr,
segments: [],
},
},
}
} else {
;(ext.yxAitMsg as YxAitMsg)[member.accountId] = {
text: substr,
segments: [],
}
}
positions.forEach((position) => {
const start = position
;(ext?.yxAitMsg as YxAitMsg)[member.accountId].segments.push({
start,
end: start + substr.length,
broken: false,
})
})
}
})
}
// @ts-ignore
return ext
}
onUnmounted(() => {
uni.$off(events.REPLY_MSG)
uni.$off(events.ON_REEDIT_MSG)
uni.$off(events.REPLY_MSG)
uni.$off(events.AIT_TEAM_MEMBER)
// 关闭表情面板
uni.$off(events.CLOSE_PANEL)
// @消息 @群成员
uni.$off(events.HANDLE_AIT_MEMBER)
// 关闭@群成员面板
uni.$off(events.CLOSE_AIT_POPUP)
// 表情点击
uni.$off(events.EMOJI_CLICK)
// 表情删除
uni.$off(events.EMOJI_DELETE)
// 表情发送
uni.$off(events.EMOJI_SEND)
removeReplyMsg()
teamWatch()
})
</script>
<style scoped lang="scss">
@import '@/styles/common.scss';
.input-root {
width: 100%;
display: flex;
flex-direction: column;
height: auto;
max-height: 300px;
}
.input-root-h5 {
height: auto;
position: relative;
order: 1;
}
.msg-input-wrapper {
width: 100%;
height: 100%;
background-color: #eff1f3;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
z-index: 999;
}
.msg-input {
overflow-x: hidden;
padding: 7px;
background-color: #eff1f3;
&-input {
background-color: #fff;
height: 40px;
font-size: 16px;
padding: 0 12px;
border-radius: 6px;
margin-bottom: 5px;
&::placeholder {
padding: 0 12px;
}
}
}
.msg-button-group {
padding: 12px 20px 2px 20px;
display: flex;
flex-direction: row;
align-items: center;
}
.msg-input-button {
flex: 1;
// &:not(:last-child) {
// margin-right: 60px;
// }
&.msg-input-loading {
animation: loadingCircle 1s infinite linear;
z-index: 1;
width: 20px;
height: 20px;
margin-top: 4px;
.loading {
width: 100%;
height: 100%;
}
}
}
.msg-ext {
overflow-y: auto;
width: 100%;
height: 300px;
background-color: #eff1f3;
z-index: 1;
}
.msg-emoji-panel {
overflow-y: auto;
width: 100%;
height: 246px;
background-color: #eff1f3;
z-index: 1;
}
.msg-audio-panel {
overflow-y: hidden;
width: 100%;
height: 300px;
background-color: #eff1f3;
z-index: 1;
}
.send-more-panel {
padding: 15px;
overflow-y: hidden;
width: 100%;
height: 300px;
background-color: #eff1f3;
z-index: 1;
flex-wrap: wrap;
box-sizing: border-box;
}
.send-more-panel-item-wrapper {
display: flex;
flex-direction: column;
align-items: center;
display: inline-block;
margin-bottom: 10px;
.send-more-panel-item {
background-color: #fff;
border-radius: 8px;
width: 100rpx;
height: 100rpx;
display: flex;
overflow: hidden;
align-items: center;
margin: 0 15px;
justify-content: center;
image{
width: 100rpx;
height: 100rpx;
border-radius: 8px;
}
}
.icon-text {
font-size: 12px;
color: #747475;
margin-top: 8px;
text-align: center;
}
}
.reply-message-wrapper {
display: flex;
font-size: 13px;
background-color: #eff1f2;
height: 25px;
padding-top: 6px;
align-items: center;
color: #929299;
.reply-noFind {
width: fit-content;
}
.reply-to-colon {
flex-basis: 3px;
margin-right: 2px;
}
.reply-message-close {
flex-basis: 14px;
margin-left: 10px;
display: flex;
align-items: center;
}
.reply-message {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
message-one-line {
flex: 1;
font-size: 13px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.reply-title {
flex-basis: 30px;
white-space: nowrap;
margin-right: 5px;
}
.reply-to {
max-width: 120px;
flex: 0 0 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
}
.input-emoji {
background-color: #fff;
height: 40px;
line-height: 40px;
font-size: 16px;
padding: 0 12px;
border-radius: 6px;
}
.input-text {
white-space: nowrap;
}
.input-placeholder {
background-color: #fff;
height: 40px;
line-height: 40px;
font-size: 16px;
padding: 0 12px;
border-radius: 6px;
color: gray;
}
.file-picker-wrapper {
position: absolute;
width: 60px;
height: 60px;
z-index: 1;
.files-button {
width: 60px;
height: 60px;
}
}
/* 复制自 groupSend 的遮罩与弹窗样式 */
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
}
.center-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 650rpx;
background: #ffffff;
border-radius: 16rpx;
z-index: 1001;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
}
.modal-title {
text-align: center;
font-size: 34rpx;
color: #d32f2f;
padding: 28rpx 20rpx;
}
.modal-item {
padding: 36rpx 28rpx;
font-size: 30rpx;
color: #333333;
background: #ffffff;
}
.modal-divider {
height: 2rpx;
background: #eeeeee;
}
</style>