更新
This commit is contained in:
@@ -14,6 +14,28 @@
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<view class="message-container">
|
||||
<!-- 医生卡片(视频通话开始或结束时自动隐藏) -->
|
||||
<view v-if="doctorInfo && showDoctorCard" class="doctor-card" @click="goDoctorDetail">
|
||||
<view class="doctor-card-main">
|
||||
<view class="doctor-card-avatar-wrap">
|
||||
<image :src="doctorInfo.avatar || '/static/user/user.png'" class="doctor-card-avatar" mode="aspectFill" />
|
||||
<view class="doctor-card-status-dot"></view>
|
||||
</view>
|
||||
<view class="doctor-card-info">
|
||||
<text class="doctor-card-name">{{ doctorInfo.name || '医生' }}</text>
|
||||
<text class="doctor-card-title">{{ doctorInfo.title || '主任医师' }}</text>
|
||||
<text class="doctor-card-hospital">{{ doctorInfo.hospital || '甄养堂医院' }}</text>
|
||||
</view>
|
||||
<view class="doctor-card-timer">
|
||||
<!-- <text class="doctor-card-timer-value">{{ formatDoctorWaitTime(doctorWaitSeconds) }}</text> -->
|
||||
<text class="doctor-card-timer-label">等待中</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="doctor-card-tip">
|
||||
<text class="doctor-card-tip-icon">⏳</text>
|
||||
<text class="doctor-card-tip-text">请等待医生,医生即将进入诊室</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(msg, index) in messages" :key="index" class="message-wrapper" :class="msg.flow === 'out' ? 'message-wrapper-out' : 'message-wrapper-in'">
|
||||
<!-- 头像 -->
|
||||
<!-- 头像 -->
|
||||
@@ -84,16 +106,16 @@
|
||||
<text class="tool-label">表情</text>
|
||||
</view> -->
|
||||
<view class="tool-item tool-item-left" @click="toggleInputMode">
|
||||
<uni-icons :type="inputMode === 'voice' ? 'compose' : 'mic'" size="38" color="#576b95"></uni-icons>
|
||||
<uni-icons :type="inputMode === 'voice' ? 'compose' : 'mic'" size="28" color="#576b95"></uni-icons>
|
||||
<text class="tool-label" v-if="inputMode === 'voice'">文字</text>
|
||||
<text class="tool-label" v-else>语音</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="chooseImage">
|
||||
<uni-icons type="image " size="40" color="#576b95"></uni-icons>
|
||||
<uni-icons type="image " size="30" color="#576b95"></uni-icons>
|
||||
<text class="tool-label">图片</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="chooseFile">
|
||||
<uni-icons type="paperclip " size="40" color="#576b95"></uni-icons>
|
||||
<uni-icons type="paperclip " size="30" color="#576b95"></uni-icons>
|
||||
<text class="tool-label">文件</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -181,20 +203,27 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||||
import { ref, nextTick, getCurrentInstance } from 'vue';
|
||||
import { onLoad, onShow, onUnload, onHide } from '@dcloudio/uni-app';
|
||||
import TencentCloudChat from '@tencentcloud/lite-chat/basic';
|
||||
import conversationPlugin from '@tencentcloud/lite-chat/plugins/conversation';
|
||||
import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer';
|
||||
import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message';
|
||||
import groupPlugin from '@tencentcloud/lite-chat/plugins/group';
|
||||
|
||||
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
|
||||
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
|
||||
uni.CallManager = new CallManager();
|
||||
const { proxy } = getCurrentInstance()
|
||||
const messages = ref([]);
|
||||
const inputText = ref('');
|
||||
const scrollTop = ref(0);
|
||||
const showEmoji = ref(false);
|
||||
const keyboardHeight = ref(0);
|
||||
const userAvatar = ref(''); // 当前用户头像
|
||||
const doctorInfo = ref(null); // 医生信息(对方为医生时展示)
|
||||
const showDoctorCard = ref(true); // 是否显示医生卡片(通话开始/结束时隐藏)
|
||||
const doctorWaitSeconds = ref(0); // 等待医生计时(秒)
|
||||
let doctorWaitTimer = null; // 等待计时器
|
||||
const inputMode = ref('text'); // 'text' | 'voice'
|
||||
const isRecording = ref(false);
|
||||
const recordingDuration = ref(0);
|
||||
@@ -212,6 +241,12 @@
|
||||
let currentUserID = ''; // 当前登录用户ID
|
||||
let scrollTimer = null; // 滚动定时器
|
||||
|
||||
const CONFIRMATION_MESSAGE = `我已确认以下信息:
|
||||
1、本人确认已在线下确诊;
|
||||
2、本人确认问诊单信息无误;
|
||||
3、本人确认已知晓病情评估风险告知;
|
||||
4、本人确认已阅读并同意《互联网诊疗知情同意书》。`;
|
||||
|
||||
const emojiList = [
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
|
||||
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
|
||||
@@ -463,17 +498,51 @@
|
||||
innerAudioContext = null;
|
||||
uni.showToast({ title: '播放失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
const video =async (userid)=>{
|
||||
|
||||
const uid=userid.replace('patient_', '')
|
||||
console.log('userid',userid,uid)
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/getPatientSignature',
|
||||
method: 'GET',
|
||||
data: {
|
||||
patient_id: uid
|
||||
},
|
||||
}, false)
|
||||
|
||||
const {userId } = res.data;
|
||||
|
||||
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
|
||||
userID:userId,
|
||||
});
|
||||
|
||||
getApp().globalData.userID = userId;
|
||||
getApp().globalData.userSig = userSig;
|
||||
getApp().globalData.SDKAppID = SDKAppID;
|
||||
const cc={
|
||||
sdkAppID: SDKAppID, // 替换为用户自己的 sdkAppID
|
||||
userID: userId, // 替换为用户自己的 userID
|
||||
userSig: userSig, // 替换为用户自己的 userSig
|
||||
globalCallPagePath: "TUICallKit/src/Components/TUICallKit", // 替换为步骤一里注册的全局监听页面
|
||||
}
|
||||
uni.setStorageSync('CallManager', userId)
|
||||
await uni.CallManager.init(cc);
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
try {
|
||||
console.log('进入onMounted:', options);
|
||||
const app = getApp();
|
||||
const userID = app.globalData.userID || options.userID;
|
||||
const userSig = app.globalData.userSig || options.userSig;
|
||||
const SDKAppID = app.globalData.SDKAppID || options.SDKAppID;
|
||||
const userID = options.userID|| app.globalData.userID;
|
||||
if(!userID.includes('patient')){
|
||||
userID='patient_'+userID
|
||||
}
|
||||
await video(userID)
|
||||
const userSig = options.userSig||app.globalData.userSig;
|
||||
const SDKAppID = options.SDKAppID|| app.globalData.SDKAppID ;
|
||||
targetUserID = options.targetUserID;
|
||||
currentUserID = userID; // 保存当前用户ID
|
||||
console.log('-------',userID,userSig,SDKAppID)
|
||||
console.log('currentUserID',currentUserID)
|
||||
// 获取用户头像
|
||||
const userData = uni.getStorageSync('userData');
|
||||
if (userData && userData.avatar) {
|
||||
@@ -487,8 +556,22 @@
|
||||
}
|
||||
|
||||
conversationID = `C2C${targetUserID}`;
|
||||
console.log('当前用户:', currentUserID, '对方用户:', targetUserID,'conversationID'+conversationID);
|
||||
|
||||
console.log('currentUserID:', currentUserID, '对方用户:', targetUserID,'conversationID'+conversationID);
|
||||
|
||||
console.log('onMounted',userID,userSig,SDKAppID,conversationID)
|
||||
// 切换账户前先登出,避免 IM 登录用户不匹配
|
||||
const prevUserID = app.globalData.imChatUserID;
|
||||
if (app.globalData.imChat && prevUserID !== userID) {
|
||||
try {
|
||||
app.globalData.imChat.off?.(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||||
await app.globalData.imChat.logout();
|
||||
await app.globalData.imChat.destroy?.();
|
||||
} catch (e) {
|
||||
console.warn('IM logout error:', e);
|
||||
}
|
||||
app.globalData.imChat = null;
|
||||
app.globalData.imChatUserID = null;
|
||||
}
|
||||
if (!app.globalData.imChat) {
|
||||
chat = TencentCloudChat.create({
|
||||
SDKAppID: Number(SDKAppID),
|
||||
@@ -500,18 +583,30 @@
|
||||
chat.use(richMediaMessagePlugin);
|
||||
chat.use(groupPlugin);
|
||||
|
||||
await chat.login({ userID, userSig });
|
||||
let promise = chat.updateMyProfile({
|
||||
nick: userData.nickname,
|
||||
avatar: userData.avatar,
|
||||
gender: TencentCloudChat.TYPES.GENDER_MALE,
|
||||
})
|
||||
promise.then(function(imResponse) {
|
||||
console.log('sssssssssssssssssssss',imResponse.data,userData.avatar); // 更新资料成功
|
||||
}).catch(function(imError) {
|
||||
console.warn('updateMyProfile error:', imError); // 更新资料失败的相关信息
|
||||
});
|
||||
try {
|
||||
await chat.login({ userID, userSig });
|
||||
} catch (loginErr) {
|
||||
const code = loginErr?.code ?? loginErr?.error?.code;
|
||||
const msg = String(loginErr?.message ?? loginErr?.error?.message ?? '');
|
||||
const isDuplicateLogin = code === 2025 || msg.includes('重复登录');
|
||||
if (isDuplicateLogin) {
|
||||
// 重复登录:SDK 可能已在内部处理,等待后继续;若仍不可用,后续 updateMyProfile/loadHistoryMessages 会静默失败
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
} else {
|
||||
throw loginErr;
|
||||
}
|
||||
}
|
||||
// updateMyProfile 非必需,失败不阻塞聊天(如 2024 用户未登录时可忽略)
|
||||
try {
|
||||
await chat.updateMyProfile({
|
||||
avatar: userData?.avatar,
|
||||
gender: TencentCloudChat.TYPES.GENDER_MALE,
|
||||
});
|
||||
} catch (profileErr) {
|
||||
console.warn('updateMyProfile error:', profileErr);
|
||||
}
|
||||
app.globalData.imChat = chat;
|
||||
app.globalData.imChatUserID = userID;
|
||||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||||
} else {
|
||||
chat = app.globalData.imChat;
|
||||
@@ -519,6 +614,14 @@
|
||||
}
|
||||
|
||||
await loadHistoryMessages();
|
||||
|
||||
if(options.msg==1){
|
||||
console.log('msg我进来了',options.msg)
|
||||
await sendConfirmationMessage(); // 进入聊天时自动发送知情确认
|
||||
}
|
||||
loadDoctorInfo(); // 加载医生卡片信息(对方为医生时)
|
||||
uni.$on('TUICall:callStart', onVideoCallStart);
|
||||
uni.$on('TUICall:callEnd', onVideoCallEnd);
|
||||
//uni.showToast({ title: '聊天已就绪', icon: 'success' });
|
||||
|
||||
// 监听键盘高度变化
|
||||
@@ -540,7 +643,25 @@
|
||||
}
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
const pages = getCurrentPages();
|
||||
const top = pages[pages.length - 1];
|
||||
if (top?.route?.includes('TUICallKit')) {
|
||||
getApp().globalData._chatFromCallPage = true;
|
||||
}
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (getApp().globalData._chatFromCallPage) {
|
||||
getApp().globalData._chatFromCallPage = false;
|
||||
hideDoctorCardByCall();
|
||||
}
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
uni.$off('TUICall:callStart', onVideoCallStart);
|
||||
uni.$off('TUICall:callEnd', onVideoCallEnd);
|
||||
stopDoctorWaitTimer();
|
||||
if (chat) chat.off(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||||
if (scrollTimer) clearTimeout(scrollTimer);
|
||||
if (innerAudioContext) {
|
||||
@@ -555,6 +676,7 @@
|
||||
async function loadHistoryMessages() {
|
||||
try {
|
||||
const res = await chat.getMessageList({ conversationID, count: 15 });
|
||||
console.log('消息',conversationID);
|
||||
if (res.data && res.data.messageList) {
|
||||
// SDK返回的消息是从新到旧,reverse后变成从旧到新,最新消息在底部
|
||||
const parsedMessages = res.data.messageList
|
||||
@@ -700,6 +822,24 @@
|
||||
uni.showToast({ title: '发送失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
async function sendConfirmationMessage() {
|
||||
const alreadySent = messages.value.some(m => m.type === 'text' && m.text === CONFIRMATION_MESSAGE);
|
||||
if (alreadySent) return;
|
||||
try {
|
||||
const message = chat.createTextMessage({
|
||||
to: targetUserID,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
payload: { text: CONFIRMATION_MESSAGE }
|
||||
});
|
||||
await chat.sendMessage(message);
|
||||
messages.value.push({ type: 'text', text: CONFIRMATION_MESSAGE, time: Date.now(), flow: 'out', avatar: userAvatar.value });
|
||||
scrollToBottom();
|
||||
setTimeout(() => scrollToBottom(), 150);
|
||||
} catch (error) {
|
||||
console.error('发送确认信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseImage() {
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -876,6 +1016,7 @@
|
||||
|
||||
async function makeVideoCall() {
|
||||
try {
|
||||
hideDoctorCardByCall();
|
||||
if (!uni.CallManager) {
|
||||
uni.showToast({ title: '通话功能未初始化', icon: 'none' });
|
||||
return;
|
||||
@@ -887,6 +1028,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
function startVideoCall() {
|
||||
makeVideoCall();
|
||||
}
|
||||
|
||||
async function loadDoctorInfo() {
|
||||
if (!targetUserID || !targetUserID.startsWith('doctor_')) return;
|
||||
const doctorId = targetUserID.replace('doctor_', '');
|
||||
try {
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/doctor/detail',
|
||||
method: 'GET',
|
||||
data: { id: doctorId }
|
||||
}, false);
|
||||
if (res && res.code === 1 && res.data) {
|
||||
doctorInfo.value = res.data;
|
||||
startDoctorWaitTimer();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载医生信息失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDoctorWaitTime(seconds) {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function startDoctorWaitTimer() {
|
||||
stopDoctorWaitTimer();
|
||||
doctorWaitSeconds.value = 0;
|
||||
doctorWaitTimer = setInterval(() => {
|
||||
doctorWaitSeconds.value++;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopDoctorWaitTimer() {
|
||||
if (doctorWaitTimer) {
|
||||
clearInterval(doctorWaitTimer);
|
||||
doctorWaitTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function goDoctorDetail() {
|
||||
if (!doctorInfo.value?.id) return;
|
||||
uni.navigateTo({ url: `/doctor/pages/doctor/doctor?id=${doctorInfo.value.id}` });
|
||||
}
|
||||
|
||||
function hideDoctorCardByCall() {
|
||||
showDoctorCard.value = false;
|
||||
stopDoctorWaitTimer();
|
||||
}
|
||||
|
||||
function onVideoCallStart() {
|
||||
hideDoctorCardByCall();
|
||||
}
|
||||
|
||||
function onVideoCallEnd() {
|
||||
hideDoctorCardByCall();
|
||||
uni.showToast({ title: '视频通话已结束', icon: 'success' });
|
||||
}
|
||||
|
||||
|
||||
function onInputFocus() {
|
||||
console.log('输入框获得焦点');
|
||||
showEmoji.value = false;
|
||||
@@ -1024,6 +1228,106 @@
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 医生卡片 */
|
||||
.doctor-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 32rpx;
|
||||
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid #e8ecf0;
|
||||
box-shadow: 0 4rpx 20rpx rgba(24, 144, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.doctor-card-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
.doctor-card-avatar-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.doctor-card-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #e8ecf0;
|
||||
}
|
||||
.doctor-card-status-dot {
|
||||
position: absolute;
|
||||
right: 4rpx;
|
||||
bottom: 4rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #52c41a;
|
||||
border: 4rpx solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: doctorStatusPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes doctorStatusPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
.doctor-card-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.doctor-card-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.doctor-card-title {
|
||||
font-size: 26rpx;
|
||||
color: #576b95;
|
||||
}
|
||||
.doctor-card-hospital {
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.doctor-card-timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 16rpx 28rpx;
|
||||
background: linear-gradient(135deg, rgba(24, 144, 255, 0.12) 0%, rgba(24, 144, 255, 0.06) 100%);
|
||||
border-radius: 20rpx;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
.doctor-card-timer-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.doctor-card-timer-label {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.doctor-card-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: linear-gradient(90deg, rgba(24, 144, 255, 0.08) 0%, rgba(24, 144, 255, 0.04) 100%);
|
||||
border-top: 2rpx dashed rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
.doctor-card-tip-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.doctor-card-tip-text {
|
||||
font-size: 28rpx;
|
||||
color: #576b95;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
margin-bottom: 32rpx;
|
||||
@@ -1095,7 +1399,7 @@
|
||||
|
||||
/* 文本消息 - 大字号 */
|
||||
.message-text {
|
||||
font-size: 38rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.6;
|
||||
color: #1a1a1a;
|
||||
word-break: break-all;
|
||||
|
||||
@@ -175,9 +175,17 @@ export default class EngineEventHandler {
|
||||
TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, { text: 'answered', duration: 2000 });
|
||||
await this._callService.openMicrophone();
|
||||
console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`);
|
||||
// 通知聊天页:视频/语音通话已接通,可关闭等待计时器
|
||||
try {
|
||||
uni.$emit('TUICall:callStart', event);
|
||||
} catch (_) {}
|
||||
}
|
||||
private async _handleUserEnter(event: any): Promise<void> {
|
||||
const { userID: userId, data } = analyzeEventData(event);
|
||||
// 对方进入房间,通话已接通,通知聊天页关闭等待计时器
|
||||
try {
|
||||
uni.$emit('TUICall:callStart', event);
|
||||
} catch (_) {}
|
||||
|
||||
if (userId && TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE === CallRole.CALLEE)) {
|
||||
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CALLING) {
|
||||
@@ -232,6 +240,10 @@ export default class EngineEventHandler {
|
||||
private _handleCallingEnd(event: any): void {
|
||||
console.log(`${NAME.PREFIX}callEnd event data: ${JSON.stringify(event)}.`);
|
||||
this._callService?._resetCallStore();
|
||||
// 通知聊天页等:视频通话已结束,可关闭等待计时器等
|
||||
try {
|
||||
uni.$emit('TUICall:callEnd', event);
|
||||
} catch (_) {}
|
||||
}
|
||||
private _handleKickedOut(event: any): void {
|
||||
console.log(`${NAME.PREFIX}kickOut event data: ${JSON.stringify(event)}.`);
|
||||
|
||||
Reference in New Issue
Block a user