This commit is contained in:
Your Name
2026-03-20 13:56:40 +08:00
parent 7147c8e148
commit d765517b74
258 changed files with 2481 additions and 364 deletions
+329 -25
View File
@@ -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)}.`);