@@ -16,27 +16,40 @@
< view class = "message-container" >
<!-- 医生卡片 ( 视频通话开始或结束时自动隐藏 ) -- >
< view v-if = "doctorInfo && showDoctorCard" class="doctor-card" @click="goDoctorDetail" >
< view class = "doctor-card-topbar" >
< text class = "doctor-card-label" > 接诊医生 < / text >
< view class = "doctor-card-status-pill" : class = "{ 'doctor-card-status-pill--entered': doctorEnteredConsultRoom }" >
< view class = "doctor-card-status-pulse" : class = "{ 'doctor-card-status-pulse--entered': doctorEnteredConsultRoom }" > < / view >
< text class = "doctor-card-status-text" > { { doctorEnteredConsultRoom ? '已进入诊室' : '等待接诊' } } < / text >
< / view >
< / view >
< 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 class = "doctor-card-avatar-ring" >
< image : src = "doctorInfo.avatar || '/static/user/user.png'" class = "doctor-card-avatar" mode = "aspectFill" / >
< / 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 class = "doctor-card-meta" >
< text class = "doctor-card-title " > { { doctorInfo . title || '主任医师 ' } } < / text >
< text class = "doctor-card-dot" > · < / text >
< text class = "doctor-card-hospital" > { { doctorInfo . hospital || '甄养堂医院' } } < / text >
< / view >
< / view >
< view class = "doctor-card-timer" >
<!-- < text class = "doctor-card-timer-value" > { { formatDoctorWaitTime ( doctorWaitSeconds ) } } < / text > -- >
< text class = "doctor-card-timer-label" > 等待中 < / text >
< view class = "doctor-card-chevron" aria -hidden = " true " >
< text class = "doctor-card-chevron-icon" > › < / text >
< / view >
< / view >
< view class = "doctor-card-tip" >
< text class = "doctor-card-tip-icon " > ⏳ < / text >
< text class = "doctor-card-tip-text" > 请等待医生 , 医生即将进入诊室 < / text >
< view class = "doctor-card-tip-bar " > < / view >
< view class = "doctor-card-tip-inner" >
< text class = "doctor-card-tip-title" > { { doctorEnteredConsultRoom ? '面诊提示' : '温馨提示' } } < / text >
< text class = "doctor-card-tip-text" > { { doctorEnteredConsultRoom ? '医生已进入诊室,即将为您视频面诊,请保持网络畅通' : '请稍候,医生进入诊室后将与您在线沟通' } } < / text >
< / view >
< / view >
< / view >
< view v-for = "(msg, index) in messages" :key="index" class="message-wrapper" :class="msg.flow === 'out' ? 'message-wrapper-out' : 'message-wrapper-in'" >
< view v-for = "(msg, index) in messages" :key="index" class="message-wrapper" :class="[ msg.flow === 'out' ? 'message-wrapper-out' : 'message-wrapper-in', msg.type === 'confirmation' ? 'message-wrapper-confirmation' : ''] " >
< ! - - 头像 - - >
<!-- 头像 -- >
< view class = "avatar" : class = "msg.flow === 'out' ? 'avatar-out' : 'avatar-in'" >
@@ -50,7 +63,23 @@
< / view >
<!-- 消息内容 -- >
< view class = "message-bubble" : class = "msg.flow === 'out' ? 'bubble-out' : 'bubble-in'" >
< view class = "message-bubble" : class = "[ msg.flow === 'out' ? 'bubble-out' : 'bubble-in', msg.type === 'confirmation' ? 'bubble-confirmation' : ''] " >
<!-- 知情确认 : 独立卡片样式 -- >
< view v-if = "msg.type === 'confirmation'" class="confirmation-card" >
< view class = "confirmation-card-head" >
< view class = "confirmation-card-badge" >
< text class = "confirmation-card-badge-icon" > ✓ < / text >
< / view >
< view class = "confirmation-card-head-text" >
< text class = "confirmation-card-title" > 知情同意确认 < / text >
< text class = "confirmation-card-sub" > 请确认您已阅读并同意以下内容 < / text >
< / view >
< / view >
< view class = "confirmation-card-divider" > < / view >
< view class = "confirmation-card-lines" >
< text v-for = "(line, li) in getConfirmationLines(msg.text)" :key="li" class="confirmation-line" > {{ line }} < / text >
< / view >
< / view >
< ! - - 文本消息 - - >
< text v-if = "msg.type === 'text'" class="message-text" > {{ msg.text }} < / text >
@@ -211,8 +240,8 @@
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();
import { TUI CallKitAPI } from '@/TUICallKit/src/TUICallService/index' ;
// CallManager 在 App.vue 已挂载到 uni.CallManager,勿在此重复 new,否则会丢失通话状态
const { proxy } = getCurrentInstance()
const messages = ref([]);
const inputText = ref('');
@@ -222,6 +251,8 @@ const { proxy } = getCurrentInstance()
const userAvatar = ref(''); // 当前用户头像
const doctorInfo = ref(null); // 医生信息(对方为医生时展示)
const showDoctorCard = ref(true); // 是否显示医生卡片(通话开始/结束时隐藏)
/** 收到医生端「已进入诊室」信令后为 true(聊天列表不展示该条自定义消息) */
const doctorEnteredConsultRoom = ref(false);
const doctorWaitSeconds = ref(0); // 等待医生计时(秒)
let doctorWaitTimer = null; // 等待计时器
const inputMode = ref('text'); // 'text' | 'voice'
@@ -246,6 +277,16 @@ const { proxy } = getCurrentInstance()
2、本人确认问诊单信息无误;
3、本人确认已知晓病情评估风险告知;
4、本人确认已阅读并同意《互联网诊疗知情同意书》。`;
function normalizeConfirmText(t) {
return String(t || '').replace(/\r\n/g, '\n').trim();
}
function isConfirmationMessageText(text) {
return normalizeConfirmText(text) === normalizeConfirmText(CONFIRMATION_MESSAGE);
}
function getConfirmationLines(text) {
return normalizeConfirmText(text).split('\n').filter((line) => line.length > 0);
}
const emojiList = [
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
@@ -515,21 +556,52 @@ const { proxy } = getCurrentInstance()
const {userId } = res.data;
patient_data.value=res.data.data
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
const { userSig, SDKAppID,secretKey } = 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 ", // 替换为步骤一里注册的全局监听页面
}
getApp().globalData.secretKey = secretKey;
uni.setStorageSync('CallManager', userId)
await uni.CallManager.init(cc);
// TUICallKit 必须在 TIM chat 登录成功后再 init,且必须传入 tim,否则无法接收通话信令(聊天正常、视频接不到)
}
/** 与当前页 IM 实例绑定后初始化音视频(TUICallEngine 依赖 tim 收发自定义信令) */
async function initTUICallKitWithTim(imChat) {
const app = getApp();
const sdkAppID = app.globalData.SDKAppID;
const userSig = app.globalData.userSig;
// 必须与 chat.login 的 userID 一致(globalData.userId 与路由可能不同步)
const userID = currentUserID || app.globalData.userID;
if (!imChat || !sdkAppID || !userSig || !userID) {
console.warn('initTUICallKitWithTim: 缺少 im / SDKAppID / userSig / userID');
return;
}
// 非 IDLE 时 destroyed 会抛错,随后 _doInit 会因旧 engine 仍存在而直接 return,导致未绑定新 tim、接不到来电
try {
await TUICallKitAPI.handleExceptionExit();
} catch (_) {}
try {
await uni.CallManager.destroyed();
} catch (e) {
console.warn('CallManager.destroyed:', e);
try {
await TUICallKitAPI.handleExceptionExit();
} catch (_) {}
try {
await uni.CallManager.destroyed();
} catch (e2) {
console.warn('CallManager.destroyed retry:', e2);
}
}
await uni.CallManager.init({
sdkAppID: Number(sdkAppID),
userID,
userSig,
globalCallPagePath: 'TUICallKit/src/Components/TUICallKit',
tim: imChat
});
}
onLoad(async (options) => {
try {
@@ -558,6 +630,7 @@ const { proxy } = getCurrentInstance()
}
conversationID = `C2C${targetUserID}`;
doctorEnteredConsultRoom.value = false;
console.log('currentUserID:', currentUserID, '对方用户:', targetUserID,'conversationID'+conversationID);
console.log('onMounted',userID,userSig,SDKAppID,conversationID)
@@ -640,11 +713,13 @@ const { proxy } = getCurrentInstance()
chat = app . globalData . imChat ;
chat . on ( TencentCloudChat . EVENT . MESSAGE _RECEIVED , onMessageReceived ) ;
}
await initTUICallKitWithTim ( chat ) ;
await loadHistoryMessages ( ) ;
// 打开会话时通知医生(IM自定义消息 + 后端全局事件)
notifyDoctorOnChatOpen ( ) ;
await notifyDoctorOnChatOpen ( ) ;
if ( options . msg == 1 ) {
console . log ( 'msg我进来了' , options . msg )
@@ -745,11 +820,24 @@ const { proxy } = getCurrentInstance()
}
}
function applyDoctorEnteredSignalFromRawMessage ( msg ) {
if ( ! msg || msg . type !== TencentCloudChat . TYPES . MSG _CUSTOM ) return ;
try {
const d = JSON . parse ( msg . payload . data ) ;
if ( d . businessID === 'doctor_entered_consult_room' && msg . from !== currentUserID ) {
doctorEnteredConsultRoom . value = true ;
}
} catch ( _ ) { }
}
async function loadHistoryMessages ( ) {
try {
const res = await chat . getMessageList ( { conversationID , count : 15 } ) ;
console . log ( '消息' , conversationID ) ;
if ( res . data && res . data . messageList ) {
for ( const raw of res . data . messageList ) {
applyDoctorEnteredSignalFromRawMessage ( raw ) ;
}
// SDK返回的消息是从新到旧,reverse后变成从旧到新,最新消息在底部
const parsedMessages = res . data . messageList
. map ( parseMessage )
@@ -789,8 +877,13 @@ const { proxy } = getCurrentInstance()
} ;
switch ( msg . type ) {
case TencentCloudChat . TYPES . MSG _TEXT :
return { ... baseMsg , type : 'text' , text : msg . payload . text } ;
case TencentCloudChat . TYPES . MSG _TEXT : {
const text = msg . payload . text ;
if ( isConfirmationMessageText ( text ) ) {
return { ... baseMsg , type : 'confirmation' , text } ;
}
return { ... baseMsg , type : 'text' , text } ;
}
case TencentCloudChat . TYPES . MSG _IMAGE :
// 优先使用原图,如果没有则使用缩略图
@@ -827,6 +920,10 @@ const { proxy } = getCurrentInstance()
if ( customData . businessID === 'patient_opened_chat' ) {
return null ; // 返回null表示不显示此消息
}
// 医生进入诊室信令(admin 发送):仅更新顶部卡片,不在聊天列表展示
if ( customData . businessID === 'doctor_entered_consult_room' ) {
return null ;
}
// 通话消息
if ( customData . businessID === 1 || customData . businessID === 'av_call' ) {
@@ -836,7 +933,11 @@ const { proxy } = getCurrentInstance()
// 其他自定义消息,如果有text字段则显示
if ( customData . text ) {
return { ... baseMsg , type : 'text' , text : customData . text } ;
const t = customData . text ;
if ( isConfirmationMessageText ( t ) ) {
return { ... baseMsg , type : 'confirmation' , text : t } ;
}
return { ... baseMsg , type : 'text' , text : t } ;
}
// 没有可显示内容的自定义消息,不显示
@@ -868,6 +969,7 @@ const { proxy } = getCurrentInstance()
function onMessageReceived ( event ) {
event . data . forEach ( msg => {
if ( msg . conversationID === conversationID ) {
applyDoctorEnteredSignalFromRawMessage ( msg ) ;
const parsedMsg = parseMessage ( msg ) ;
// 只添加非null的消息
if ( parsedMsg !== null ) {
@@ -878,29 +980,113 @@ const { proxy } = getCurrentInstance()
}
} ) ;
}
function imGetLoginCode ( err ) {
const c = err ? . code ? ? err ? . error ? . code ;
if ( c != null ) return c ;
const msg = String ( err ? . message ? ? err ? . error ? . message ? ? '' ) ;
const m = msg . match ( /["']?code["']?\s*:\s*(\d+)/ ) ;
return m ? parseInt ( m [ 1 ] , 10 ) : null ;
}
/** 拉取最新 userSig 并重新登录 IM,必要时重建 chat 实例(与 onLoad 逻辑一致) */
async function reloginIMChat ( ) {
const app = getApp ( ) ;
await video ( currentUserID ) ;
const userSig = app . globalData . userSig ;
const SDKAppID = app . globalData . SDKAppID ;
const userID = currentUserID ;
if ( ! userSig || ! SDKAppID ) {
throw new Error ( 'IM 凭证缺失' ) ;
}
if ( ! chat ) {
throw new Error ( 'IM 未初始化' ) ;
}
try {
chat . off ( TencentCloudChat . EVENT . MESSAGE _RECEIVED , onMessageReceived ) ;
} catch ( _ ) { }
try {
await chat . logout ( ) ;
} catch ( e ) {
console . warn ( 'IM logout:' , e ) ;
}
const attachPlugins = ( instance ) => {
instance . use ( conversationPlugin ) ;
instance . use ( messageEnhancerPlugin ) ;
instance . use ( richMediaMessagePlugin ) ;
instance . use ( groupPlugin ) ;
} ;
const doLogin = async ( instance ) => instance . login ( { userID , userSig } ) ;
try {
await doLogin ( chat ) ;
} catch ( loginErr ) {
const code = imGetLoginCode ( loginErr ) ;
const msg = String ( loginErr ? . message ? ? loginErr ? . error ? . message ? ? '' ) ;
const isDuplicateLogin = code === 2025 || msg . includes ( '重复登录' ) ;
if ( ! isDuplicateLogin ) throw loginErr ;
try {
await chat . destroy ? . ( ) ;
} catch ( _ ) { }
chat = TencentCloudChat . create ( {
SDKAppID : Number ( SDKAppID ) ,
scene : 'uniapp-wx'
} ) ;
attachPlugins ( chat ) ;
await doLogin ( chat ) ;
}
app . globalData . imChat = chat ;
app . globalData . imChatUserID = userID ;
chat . on ( TencentCloudChat . EVENT . MESSAGE _RECEIVED , onMessageReceived ) ;
await initTUICallKitWithTim ( chat ) ;
}
async function sendTextMessage ( ) {
if ( ! inputText . value . trim ( ) ) return ;
try {
const text = inputText . value . trim ( ) ;
const pushSentAndClear = ( ) => {
messages . value . push ( {
type : isConfirmationMessageText ( text ) ? 'confirmation' : 'text' ,
text ,
time : Date . now ( ) ,
flow : 'out' ,
avatar : userAvatar . value
} ) ;
inputText . value = '' ;
uni . hideKeyboard ( ) ;
scrollToBottom ( ) ;
setTimeout ( ( ) => scrollToBottom ( ) , 150 ) ;
} ;
const doSend = async ( ) => {
const message = chat . createTextMessage ( {
to : targetUserID ,
conversationType : TencentCloudChat . TYPES . CONV _C2C ,
payload : { text : inputText . value }
payload : { text }
} ) ;
await chat . sendMessage ( message ) ;
messages . value . push ( { type : 'text' , text : inputText . value , time : Date . now ( ) , flow : 'out' , avatar : userAvatar . value } ) ;
inputText . value = '' ;
// 立即滚动,然后再延迟滚动一次确保成功
scrollToBottom ( ) ;
setTimeout ( ( ) => scrollToBottom ( ) , 150 ) ;
} ;
try {
await doSend ( ) ;
pushSentAndClear ( ) ;
} catch ( error ) {
console . error ( '发送消息失败:' , error ) ;
uni . showToast ( { title : '发送失败' , icon : 'none' } ) ;
try {
uni . showLoading ( { title : '重新登录中' , mask : true } ) ;
await reloginIMChat ( ) ;
await doSend ( ) ;
pushSentAndClear ( ) ;
} catch ( e2 ) {
console . error ( '重登后仍发送失败:' , e2 ) ;
uni . showToast ( { title : '发送失败' , icon : 'none' } ) ;
} finally {
uni . hideLoading ( ) ;
}
}
}
async function sendConfirmationMessage ( ) {
const alreadySent = messages . value . some ( m => m . type === 'text' && m . text === CONFIRMATION _MESSAGE ) ;
const alreadySent = messages . value . some (
( m ) => m . type === 'confirmation' || ( m . type === 'text' && isConfirmationMessageText ( m . text ) )
) ;
if ( alreadySent ) return ;
try {
const message = chat . createTextMessage ( {
@@ -909,7 +1095,13 @@ const { proxy } = getCurrentInstance()
payload : { text : CONFIRMATION _MESSAGE }
} ) ;
await chat . sendMessage ( message ) ;
messages . value . push ( { type : 'text' , text : CONFIRMATION _MESSAGE , time : Date . now ( ) , flow : 'out' , avatar : userAvatar . value } ) ;
messages . value . push ( {
type : 'confirmation' ,
text : CONFIRMATION _MESSAGE ,
time : Date . now ( ) ,
flow : 'out' ,
avatar : userAvatar . value
} ) ;
scrollToBottom ( ) ;
setTimeout ( ( ) => scrollToBottom ( ) , 150 ) ;
} catch ( error ) {
@@ -1299,109 +1491,220 @@ const { proxy } = getCurrentInstance()
}
. message - container {
padding : 28 rpx 32 rpx ;
padding : 28 rpx 22 rpx ;
padding - bottom : 320 rpx ;
min - height : 100 % ;
}
/* 医生卡片 */
/* 医生卡片:问诊场景、层次清晰 */
. doctor - card {
display : flex ;
flex - direction : column ;
margin - bottom : 32 rpx ;
background : linear - gradient ( 145 deg , # ffffff 0 % , # f8fafc 100 % ) ;
border - radius : 24 rpx ;
border : 2 rpx solid # e8ecf0 ;
box - shadow : 0 4 rpx 20 rpx rgba ( 24 , 144 , 255 , 0.06 ) ;
border - radius : 20 rpx ;
overflow : hidden ;
background : # fff ;
box - shadow :
0 2 rpx 8 rpx rgba ( 15 , 23 , 42 , 0.04 ) ,
0 12 rpx 40 rpx rgba ( 13 , 148 , 136 , 0.08 ) ;
border : 1 rpx solid rgba ( 13 , 148 , 136 , 0.12 ) ;
position : relative ;
transition : transform 0.15 s ease , box - shadow 0.15 s ease ;
}
. doctor - card : active {
transform : scale ( 0.992 ) ;
box - shadow :
0 2 rpx 6 rpx rgba ( 15 , 23 , 42 , 0.06 ) ,
0 8 rpx 28 rpx rgba ( 13 , 148 , 136 , 0.1 ) ;
}
. doctor - card : : before {
content : '' ;
position : absolute ;
left : 0 ;
top : 0 ;
right : 0 ;
height : 6 rpx ;
background : linear - gradient ( 90 deg , # 0 d9488 0 % , # 14 b8a6 45 % , # 5 eead4 100 % ) ;
}
. doctor - card - topbar {
display : flex ;
flex - direction : row ;
align - items : center ;
justify - content : space - between ;
padding : 22 rpx 28 rpx 12 rpx ;
padding - top : 28 rpx ;
}
. doctor - card - label {
font - size : 24 rpx ;
font - weight : 600 ;
color : # 64748 b ;
letter - spacing : 2 rpx ;
}
. doctor - card - status - pill {
display : flex ;
flex - direction : row ;
align - items : center ;
gap : 10 rpx ;
padding : 8 rpx 18 rpx ;
border - radius : 999 rpx ;
background : linear - gradient ( 135 deg , # fff7ed 0 % , # ffedd5 100 % ) ;
border : 1 rpx solid rgba ( 251 , 146 , 60 , 0.35 ) ;
}
. doctor - card - status - pulse {
width : 12 rpx ;
height : 12 rpx ;
border - radius : 50 % ;
background : # ea580c ;
box - shadow : 0 0 0 0 rgba ( 234 , 88 , 12 , 0.45 ) ;
animation : doctorWaitPulse 1.8 s ease - out infinite ;
}
@ keyframes doctorWaitPulse {
0 % {
box - shadow : 0 0 0 0 rgba ( 234 , 88 , 12 , 0.5 ) ;
}
70 % {
box - shadow : 0 0 0 10 rpx rgba ( 234 , 88 , 12 , 0 ) ;
}
100 % {
box - shadow : 0 0 0 0 rgba ( 234 , 88 , 12 , 0 ) ;
}
}
. doctor - card - status - text {
font - size : 22 rpx ;
font - weight : 600 ;
color : # c2410c ;
letter - spacing : 0.5 rpx ;
}
. doctor - card - status - pill -- entered {
background : linear - gradient ( 135 deg , # ecfdf5 0 % , # d1fae5 100 % ) ;
border : 1 rpx solid rgba ( 16 , 185 , 129 , 0.45 ) ;
}
. doctor - card - status - pill -- entered . doctor - card - status - text {
color : # 047857 ;
}
. doctor - card - status - pulse -- entered {
background : # 10 b981 ;
box - shadow : 0 0 0 0 rgba ( 16 , 185 , 129 , 0.45 ) ;
animation : doctorEnteredPulse 1.6 s ease - out infinite ;
}
@ keyframes doctorEnteredPulse {
0 % {
box - shadow : 0 0 0 0 rgba ( 16 , 185 , 129 , 0.55 ) ;
}
70 % {
box - shadow : 0 0 0 10 rpx rgba ( 16 , 185 , 129 , 0 ) ;
}
100 % {
box - shadow : 0 0 0 0 rgba ( 16 , 185 , 129 , 0 ) ;
}
}
. doctor - card - main {
display : flex ;
flex - direction : row ;
align - items : center ;
padding : 28 rpx 32 rpx ;
padding : 8 rpx 24 rpx 28 rpx ;
gap : 20 rpx ;
}
. doctor - card - avatar - wrap {
position : relative ;
flex - shrink : 0 ;
}
. doctor - card - avatar {
width : 96 rpx ;
height : 96 rpx ;
border - radius : 20 rpx ;
background : # e8ecf0 ;
}
. doctor - card - status - dot {
position : absolute ;
right : 4 rpx ;
bottom : 4 rpx ;
width : 20 rpx ;
height : 20 rpx ;
background : # 52 c41a ;
border : 4 rpx solid # fff ;
. doctor - card - avatar - ring {
width : 112 rpx ;
height : 112 rpx ;
padding : 4 rpx ;
border - radius : 50 % ;
animation : doctorStatusPulse 1.5 s ease - in - out infinite ;
background : linear - gradient ( 145 deg , # ccfbf1 0 % , # 99 f6e4 50 % , # 5 eead4 100 % ) ;
box - shadow : inset 0 1 rpx 2 rpx rgba ( 255 , 255 , 255 , 0.8 ) ;
}
@ keyframes doctorStatusPulse {
0 % , 100 % { opacity : 1 ; transform : scale ( 1 ) ; }
50 % { opacity : 0.7 ; transform : scale ( 1.1 ) ; }
. doctor - card - avatar {
width : 104 rpx ;
height : 104 rpx ;
border - radius : 50 % ;
display : block ;
background : # e2e8f0 ;
border : 4 rpx solid # fff ;
box - sizing : border - box ;
}
. doctor - card - info {
flex : 1 ;
margin - left : 24 rpx ;
min - width : 0 ;
display : flex ;
flex - direction : column ;
gap : 6 rpx ;
gap : 10 rpx ;
}
. doctor - card - name {
font - size : 34 rpx ;
font - weight : 600 ;
color : # 1 a1a1a ;
font - size : 36 rpx ;
font - weight : 700 ;
color : # 0 f172a ;
letter - spacing : 0.5 rpx ;
line - height : 1.3 ;
}
. doctor - card - meta {
display : flex ;
flex - direction : row ;
flex - wrap : wrap ;
align - items : center ;
gap : 6 rpx ;
}
. doctor - card - title {
font - size : 26 rpx ;
color : # 576 b95 ;
font - size : 24 rpx ;
color : # 0 d9488 ;
font - weight : 500 ;
}
. doctor - card - dot {
font - size : 24 rpx ;
color : # cbd5e1 ;
}
. doctor - card - hospital {
font - size : 24 rpx ;
color : # 94 a3b8 ;
color : # 64748 b ;
max - width : 100 % ;
}
. doctor - card - timer {
. doctor - card - chevron {
flex - shrink : 0 ;
width : 48 rpx ;
height : 48 rpx ;
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
gap : 4 rpx ;
padding : 16 rpx 28 rpx ;
background : linear - gradient ( 135 deg , rgba ( 24 , 144 , 255 , 0.12 ) 0 % , rgba ( 24 , 144 , 255 , 0.06 ) 100 % ) ;
border - radius : 20 rpx ;
min - width : 120 rpx ;
border - radius : 50 % ;
background : # f1f5f9 ;
}
. doctor - card - timer - value {
. doctor - card - chevron - icon {
font - size : 40 rpx ;
font - weight : 600 ;
color : # 1890 ff ;
font - variant - numeric : tabular - nums ;
}
. doctor - card - timer - label {
font - size : 22 rpx ;
line - height : 1 ;
color : # 94 a3b8 ;
font - weight : 300 ;
}
. doctor - card - tip {
display : flex ;
align - items : center ;
justify - content : center ;
gap : 12 rpx ;
padding : 20 rpx 24 rpx ;
background : linear - gradient ( 90 deg , rgba ( 24 , 144 , 255 , 0.08 ) 0 % , rgba ( 24 , 144 , 255 , 0.04 ) 100 % ) ;
border - top : 2 rpx dashed rgba ( 24 , 144 , 255 , 0.15 ) ;
flex - direction : row ;
align - items : stretch ;
background : linear - gradient ( 180 deg , # f8fafc 0 % , # f1f5f9 100 % ) ;
border - top : 1 rpx solid rgba ( 148 , 163 , 184 , 0.2 ) ;
}
. doctor - card - tip - icon {
font - size : 32 rpx ;
. doctor - card - tip - bar {
width : 6 rpx ;
flex - shrink : 0 ;
background : linear - gradient ( 180 deg , # 14 b8a6 0 % , # 0 d9488 100 % ) ;
}
. doctor - card - tip - inner {
flex : 1 ;
padding : 20 rpx 24 rpx 22 rpx 20 rpx ;
display : flex ;
flex - direction : column ;
gap : 8 rpx ;
}
. doctor - card - tip - title {
font - size : 22 rpx ;
font - weight : 600 ;
color : # 475569 ;
letter - spacing : 1 rpx ;
}
. doctor - card - tip - text {
font - size : 28 rpx ;
color : # 576 b95 ;
font - weight : 500 ;
font - size : 26 rpx ;
color : # 64748 b ;
line - height : 1.55 ;
}
. message - wrapper {
@@ -1425,8 +1728,8 @@ const { proxy } = getCurrentInstance()
/* 头像 - 更大 */
. avatar {
width : 100 rpx ;
height : 100 rpx ;
width : 80 rpx ;
height : 80 rpx ;
border - radius : 12 rpx ;
display : flex ;
align - items : center ;
@@ -1443,7 +1746,7 @@ const { proxy } = getCurrentInstance()
. avatar - out {
margin - left : 20 rpx ;
margin - left : 10 rpx ;
}
. avatar - text {
@@ -1473,6 +1776,95 @@ const { proxy } = getCurrentInstance()
background : # 95 ec69 ;
}
/* 知情确认:全宽略增、与绿气泡区分 */
. message - wrapper - confirmation . message - bubble {
max - width : 620 rpx ;
}
. bubble - confirmation . bubble - out {
background : linear - gradient ( 165 deg , # f0faf4 0 % , # e3f2e8 55 % , # d8ebe3 100 % ) ;
border : 1 rpx solid rgba ( 46 , 139 , 87 , 0.28 ) ;
box - shadow : 0 4 rpx 20 rpx rgba ( 34 , 100 , 70 , 0.08 ) ;
}
. bubble - confirmation . bubble - in {
background : linear - gradient ( 165 deg , # f7fbff 0 % , # eef6fc 100 % ) ;
border : 1 rpx solid rgba ( 87 , 107 , 149 , 0.22 ) ;
box - shadow : 0 4 rpx 20 rpx rgba ( 50 , 80 , 120 , 0.06 ) ;
}
. confirmation - card {
width : 100 % ;
}
. confirmation - card - head {
display : flex ;
flex - direction : row ;
align - items : center ;
gap : 20 rpx ;
}
. confirmation - card - badge {
width : 56 rpx ;
height : 56 rpx ;
border - radius : 50 % ;
background : linear - gradient ( 135 deg , # 2 e8b57 0 % , # 3 cb371 100 % ) ;
display : flex ;
align - items : center ;
justify - content : center ;
flex - shrink : 0 ;
box - shadow : 0 2 rpx 8 rpx rgba ( 46 , 139 , 87 , 0.35 ) ;
}
. confirmation - card - badge - icon {
font - size : 28 rpx ;
color : # fff ;
font - weight : 700 ;
line - height : 1 ;
}
. confirmation - card - head - text {
flex : 1 ;
display : flex ;
flex - direction : column ;
gap : 4 rpx ;
}
. confirmation - card - title {
font - size : 32 rpx ;
font - weight : 600 ;
color : # 1 a4d2e ;
letter - spacing : 0.5 rpx ;
}
. bubble - in . confirmation - card - title {
color : # 1 a3a5c ;
}
. confirmation - card - sub {
font - size : 20 rpx ;
color : rgba ( 26 , 77 , 46 , 0.45 ) ;
text - transform : uppercase ;
letter - spacing : 1 rpx ;
}
. bubble - in . confirmation - card - sub {
color : rgba ( 26 , 58 , 92 , 0.4 ) ;
}
. confirmation - card - divider {
height : 1 rpx ;
background : linear - gradient ( 90 deg , transparent , rgba ( 46 , 139 , 87 , 0.25 ) , transparent ) ;
margin : 20 rpx 0 16 rpx ;
}
. bubble - in . confirmation - card - divider {
background : linear - gradient ( 90 deg , transparent , rgba ( 87 , 107 , 149 , 0.22 ) , transparent ) ;
}
. confirmation - card - lines {
display : flex ;
flex - direction : column ;
gap : 14 rpx ;
}
. confirmation - line {
font - size : 28 rpx ;
line - height : 1.65 ;
color : # 2 d4a3a ;
display : block ;
padding - left : 4 rpx ;
}
. bubble - in . confirmation - line {
color : # 2 c3d52 ;
}
/* 文本消息 - 大字号 */
. message - text {
font - size : 30 rpx ;