1916 lines
54 KiB
Vue
1916 lines
54 KiB
Vue
<template>
|
||
<view class="chat-container">
|
||
<u-navbar class="chat-navbar" :is-back="true" :title="navbarTitle" title-color="#000"></u-navbar>
|
||
|
||
<view class="chat-header">
|
||
<view class="header-main" @click="detailOpen = !detailOpen">
|
||
<view class="patient-info-group">
|
||
<text class="patient-name">{{ navbarTitle }}</text>
|
||
<view class="patient-badges">
|
||
<view class="status-dot"></view>
|
||
<text class="sub-text">{{ statusLine }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="header-action">
|
||
<u-icon :name="detailOpen ? 'arrow-up' : 'arrow-down'" size="18" color="#6ACDBB"></u-icon>
|
||
</view>
|
||
</view>
|
||
<PatientDetailPanel v-if="detailOpen && patientInfo" :info="patientInfo" />
|
||
</view>
|
||
|
||
<view v-if="isTransferRegister && transferCard" class="transfer-strip">
|
||
<text class="transfer-t">转诊处方:{{ transferCard.prescription_no || '关联处方' }}</text>
|
||
<view class="transfer-strip-actions">
|
||
<view class="transfer-btn transfer-btn--ghost" @click.stop="openTransferDetail">详情</view>
|
||
<view v-if="canImportTransferRxStrip" class="transfer-btn transfer-btn--ghost" @click.stop="onTransferImportToPrescription">添加到处方</view>
|
||
<view class="transfer-btn" @click="goPrescriptionV2">去开方</view>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view
|
||
class="chat-messages"
|
||
:scroll-top="scrollTop"
|
||
:scroll-into-view="scrollIntoView"
|
||
scroll-y
|
||
scroll-with-animation
|
||
upper-threshold="80"
|
||
@scrolltoupper="onScrollToUpper"
|
||
>
|
||
<view class="message-list-container">
|
||
<view v-if="!hasMore && messages.length > 0" class="empty-state empty-state--top">
|
||
<text class="empty-text">没有更多消息了</text>
|
||
</view>
|
||
<view v-if="historyLoading && page > 1" class="loading-spinner">
|
||
<text class="spinner-text">历史记录加载中...</text>
|
||
</view>
|
||
|
||
<view v-for="(msg, index) in messages" :key="msg._wxKey" class="message-wrapper">
|
||
<view v-if="shouldShowTime(msg, index)" class="time-divider">
|
||
<text class="time-text">{{ timeDividerText(msg) }}</text>
|
||
</view>
|
||
<MessageBubble
|
||
:msg="msg"
|
||
:is-mine="isMsgMine(msg)"
|
||
:avatar="getMsgAvatar(msg)"
|
||
:is-playing="isPlaying(msg)"
|
||
:can-add-experience-to-rx="canAddExperienceDrugFromChat"
|
||
@previewImage="previewImage"
|
||
@playAudio="playAudio"
|
||
@viewPrescription="onBubbleViewPrescription"
|
||
@addExperienceToRx="onAddExperienceToPrescription"
|
||
/>
|
||
</view>
|
||
|
||
<view id="chat-bottom-anchor" class="chat-bottom-anchor"></view>
|
||
|
||
<view v-if="messages.length === 0 && !bootstrapLoading" class="empty-state">
|
||
<text class="empty-text">暂无消息</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<ReceptionActionBar
|
||
:register-status="Number(patientInfo && patientInfo.status) || 0"
|
||
:room-ended="roomEnded"
|
||
@accept="onAccept"
|
||
@refuse="onRefuseTap"
|
||
@prescription="goPrescriptionV2"
|
||
@end="onEndDiagnosis"
|
||
/>
|
||
|
||
<view class="chat-footer" :style="{ bottom: keyboardHeight + 'px' }">
|
||
<view v-if="inputLocked" class="consultation-ended-notice">
|
||
<u-icon name="info-circle" size="16" color="#999"></u-icon>
|
||
<text>{{ lockReason }}</text>
|
||
</view>
|
||
<view v-else class="input-bar">
|
||
<view class="icon-btn" @click="toggleVoiceInput">
|
||
<u-icon :name="voiceInputActive ? 'keyboard' : 'mic'" size="24" color="#666"></u-icon>
|
||
</view>
|
||
<view class="input-field-wrapper">
|
||
<input
|
||
v-if="!voiceInputActive"
|
||
v-model="newMessage"
|
||
class="main-input"
|
||
placeholder="输入消息..."
|
||
confirm-type="send"
|
||
@confirm="sendTextMessage"
|
||
@focus="onInputFocus"
|
||
@blur="onInputBlur"
|
||
/>
|
||
<view
|
||
v-else
|
||
class="voice-btn"
|
||
:class="{ recording: recording }"
|
||
@touchstart="startVoiceRecording"
|
||
@touchend="stopVoiceRecording"
|
||
@touchcancel="stopVoiceRecording"
|
||
>
|
||
{{ recording ? '录音中...' : '按住说话' }}
|
||
</view>
|
||
</view>
|
||
<view class="right-action">
|
||
<view v-if="newMessage.trim()" class="send-btn" @click="sendTextMessage">发送</view>
|
||
<view class="icon-btn plus-wrap" @click="toggleToolPanel">
|
||
<text class="plus-char" :class="{ 'plus-char--open': showToolPanel }">+</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="showToolPanel && !inputLocked" class="tool-panel-wrap">
|
||
<swiper
|
||
class="tool-panel-swiper"
|
||
:indicator-dots="true"
|
||
indicator-color="rgba(0,0,0,0.15)"
|
||
indicator-active-color="#6ACDBB"
|
||
:current="toolPanelSwiperCurrent"
|
||
@change="onToolPanelSwiperChange"
|
||
>
|
||
<swiper-item>
|
||
<view class="tool-grid">
|
||
<view class="tool-cell" @click="onMorePickImage">
|
||
<view class="tool-cell-icon"><u-icon name="photo" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">图片</text>
|
||
</view>
|
||
<view class="tool-cell" @click="onMoreQuickReply">
|
||
<view class="tool-cell-icon"><u-icon name="list" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">常用回复</text>
|
||
</view>
|
||
<view class="tool-cell" @click="onMorePatientInfo">
|
||
<view class="tool-cell-icon"><u-icon name="account" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">患者信息</text>
|
||
</view>
|
||
<view class="tool-cell" @click="onMoreOrdersPopup">
|
||
<view class="tool-cell-icon"><u-icon name="order" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">挂号订单</text>
|
||
</view>
|
||
<view class="tool-cell" :class="{ 'is-disabled': transferToolDisabled }" @click="onMoreTransferDetail">
|
||
<view class="tool-cell-icon"><u-icon name="share" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">转诊详情</text>
|
||
</view>
|
||
<view class="tool-cell" :class="{ 'is-disabled': goRxToolDisabled }" @click="onMoreGoPrescription">
|
||
<view class="tool-cell-icon"><u-icon name="edit-pen" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">去开方</text>
|
||
</view>
|
||
<view class="tool-cell" :class="{ 'is-disabled': experienceToolDisabled }" @click="onMoreExperienceToRx">
|
||
<view class="tool-cell-icon"><u-icon name="file-text" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">诊历带药</text>
|
||
</view>
|
||
<view class="tool-cell" :class="{ 'is-disabled': endToolDisabled }" @click="onMoreEndDiagnosis">
|
||
<view class="tool-cell-icon"><u-icon name="checkmark-circle" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">结束问诊</text>
|
||
</view>
|
||
</view>
|
||
</swiper-item>
|
||
<swiper-item>
|
||
<view class="tool-grid">
|
||
<view class="tool-cell" :class="{ 'is-disabled': refuseToolDisabled }" @click="onMoreRefuse">
|
||
<view class="tool-cell-icon"><u-icon name="close-circle" size="30" color="#6ACDBB"></u-icon></view>
|
||
<text class="tool-cell-label">拒诊</text>
|
||
</view>
|
||
<view v-for="ph in toolPlaceholderCells" :key="ph._wxKey" class="tool-cell tool-cell--placeholder"></view>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showQuickReplySheet" class="quick-reply-mask" @click="closeQuickReplySheet">
|
||
<view class="quick-reply-sheet" @click.stop>
|
||
<view class="quick-reply-head">
|
||
<text class="quick-reply-title">常用回复</text>
|
||
<text class="quick-reply-close" @click="closeQuickReplySheet">关闭</text>
|
||
</view>
|
||
<scroll-view v-if="!quickReplyLoading && quickReplyList.length" class="quick-reply-scroll" scroll-y>
|
||
<view
|
||
v-for="item in quickReplyList"
|
||
:key="item._wxKey"
|
||
class="quick-reply-row"
|
||
@click="selectQuickReply(item)"
|
||
>
|
||
<text class="quick-reply-text">{{ item.content }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view v-else-if="quickReplyLoading" class="quick-reply-empty">加载中...</view>
|
||
<view v-else class="quick-reply-empty">暂无常用回复</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showRecordingModal" class="recording-overlay">
|
||
<view class="recording-box">
|
||
<text class="timer">{{ formatRecordingTime(recordingTime) }}</text>
|
||
<text class="tip">松开结束录音</text>
|
||
</view>
|
||
</view>
|
||
|
||
<RefuseReceptionModal ref="refuseModal" @confirm="onRefuseConfirm" />
|
||
|
||
<u-popup v-model="ordersPopupOpen" mode="bottom" border-radius="20" length="70%" :closeable="true" :safe-area-inset-bottom="true">
|
||
<view class="orders-popup" v-if="patientInfo">
|
||
<text class="orders-popup-title">挂号与订单</text>
|
||
<scroll-view scroll-y class="orders-popup-scroll">
|
||
<view v-if="registerOrderBlock" class="orders-block">
|
||
<view v-if="registerOrderBlock.order_no" class="orders-row">
|
||
<text class="orders-k">挂号单号</text>
|
||
<text class="orders-v order-font">{{ registerOrderBlock.order_no }}</text>
|
||
</view>
|
||
<view v-if="registerOrderBlock.price != null && registerOrderBlock.price !== ''" class="orders-row">
|
||
<text class="orders-k">费用</text>
|
||
<text class="orders-v price">¥{{ registerOrderBlock.price }}</text>
|
||
</view>
|
||
<view v-if="registerOrderBlock.status != null && registerOrderBlock.status !== ''" class="orders-row">
|
||
<text class="orders-k">状态</text>
|
||
<text class="orders-v">{{ registerOrderStatusText(registerOrderBlock.status) }}</text>
|
||
</view>
|
||
<view
|
||
v-for="sub in childOrdersWithKey"
|
||
:key="sub._wxKey"
|
||
class="orders-sub"
|
||
>
|
||
<text class="orders-sub-label">子订单</text>
|
||
<text class="orders-sub-no">{{ sub.order_no || sub.no || '—' }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-else class="orders-hint">暂无挂号单扩展信息</view>
|
||
<text class="orders-section-title">本次挂号处方</text>
|
||
<view v-if="ordersRxLoading" class="orders-hint">加载中…</view>
|
||
<view v-else-if="!ordersRxList.length" class="orders-hint">暂无处方</view>
|
||
<view
|
||
v-for="rx in ordersRxList"
|
||
:key="rx._wxKey"
|
||
class="orders-rx-row"
|
||
:data-wx-key="rx._wxKey"
|
||
@click="onOrdersRxTap"
|
||
>
|
||
<text class="orders-rx-no">{{ rx.order_no || rx.prescription_no || '处方' }}</text>
|
||
<text v-if="rx.total_pay_price != null" class="orders-rx-price">¥{{ rx.total_pay_price }}</text>
|
||
<text class="orders-rx-link">详情</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="orders-popup-foot">
|
||
<view class="orders-popup-close" @click="ordersPopupOpen = false">关闭</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
|
||
<u-popup v-model="transferDetailOpen" mode="bottom" border-radius="20" length="65%" :closeable="true" :safe-area-inset-bottom="true">
|
||
<view class="transfer-detail-popup" v-if="isTransferRegister && transferCard">
|
||
<text class="td-title">转诊信息</text>
|
||
<scroll-view scroll-y class="td-scroll">
|
||
<view v-if="transferCard.prescription_no" class="td-row">
|
||
<text class="td-k">处方编号</text>
|
||
<text class="td-v">{{ transferCard.prescription_no }}</text>
|
||
</view>
|
||
<view v-if="transferCard.transfer_store && transferCard.transfer_store.name" class="td-row">
|
||
<text class="td-k">转诊诊所</text>
|
||
<text class="td-v">{{ transferCard.transfer_store.name }}</text>
|
||
</view>
|
||
<view v-if="transferCard.delegate_store && transferCard.delegate_store.name" class="td-row">
|
||
<text class="td-k">委托诊所</text>
|
||
<text class="td-v">{{ transferCard.delegate_store.name }}</text>
|
||
</view>
|
||
<view v-if="transferCard.transfer_reason" class="td-row">
|
||
<text class="td-k">转诊原因</text>
|
||
<text class="td-v">{{ transferCard.transfer_reason }}</text>
|
||
</view>
|
||
<view v-if="transferCard.status != null" class="td-row">
|
||
<text class="td-k">状态</text>
|
||
<text class="td-v">{{ transferStatusText(transferCard.status) }}</text>
|
||
</view>
|
||
<view v-if="transferCard.questions && transferCard.questions.length" class="td-qa">
|
||
<text class="td-qa-title">咨询问题</text>
|
||
<view v-for="(qa, qix) in transferQuestionsWithKey" :key="qa._wxKey" class="td-qa-item">
|
||
<text class="td-qa-q">{{ qix + 1 }}. {{ qa.question }}</text>
|
||
<text class="td-qa-a">{{ qa.answer }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="td-foot td-foot--row">
|
||
<view v-if="canImportTransferRxStrip" class="transfer-btn transfer-btn--ghost" @click="onTransferImportToPrescription">添加到处方</view>
|
||
<view class="transfer-btn" @click="transferDetailOpen = false">关闭</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { chatRoomManager } from '@/utils/chat/chatRoomManager.js';
|
||
import { chatConfig } from '@/config/chat.js';
|
||
import {
|
||
getPatientItem,
|
||
getQuickReplyListApi,
|
||
getRegisterPrescriptionList,
|
||
receptionApi,
|
||
refuseReceptionApi,
|
||
endOfDiagnosisApi,
|
||
getTransferPrescriptionByRegisterApi,
|
||
getDrugsByRegisterIdApi,
|
||
getCurrentStoreTypeApi
|
||
} from '@/api/reception.js';
|
||
import {
|
||
parseTransferPrescriptionContent,
|
||
transferDrugToChineseRow,
|
||
canImportTransferPrescription,
|
||
normalizeTransferCardPayload
|
||
} from '../utils/transferPrescriptionImport.js';
|
||
import {
|
||
getChatRoomByRegisterApi,
|
||
getRoomStatusApi,
|
||
uploadChatFileApi,
|
||
sendToUserHttpApi,
|
||
isSendToUserResponseOk,
|
||
sendToUserFailMessage,
|
||
extractSendToUserMessageId
|
||
} from '@/api/chat.js';
|
||
import { parseMessageContent } from '../utils/messageParse.js';
|
||
import PatientDetailPanel from '../components/PatientDetailPanel.vue';
|
||
import MessageBubble from '../components/MessageBubble.vue';
|
||
import ReceptionActionBar from '../components/ReceptionActionBar.vue';
|
||
import RefuseReceptionModal from '../components/RefuseReceptionModal.vue';
|
||
import { withWxKey, ensureWxKey } from '@/utils/wxListKey.js';
|
||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||
|
||
const PENDING_RX_FROM_CHAT = 'xk_pending_rx_from_chat';
|
||
|
||
const TOOL_PLACEHOLDER_CELLS = Array.from({ length: 7 }, (_, i) => ({ _wxKey: `ph${i}` }));
|
||
|
||
export default {
|
||
components: { PatientDetailPanel, MessageBubble, ReceptionActionBar, RefuseReceptionModal },
|
||
data() {
|
||
return {
|
||
registerId: '',
|
||
roomId: '',
|
||
patientName: '',
|
||
userPatientId: '',
|
||
patientInfo: null,
|
||
patientUserId: '',
|
||
messages: [],
|
||
newMessage: '',
|
||
voiceInputActive: false,
|
||
recording: false,
|
||
recordingTime: 0,
|
||
recordingTimer: null,
|
||
showRecordingModal: false,
|
||
currentPlayingAudio: null,
|
||
innerAudioContext: null,
|
||
scrollTop: 0,
|
||
scrollIntoView: '',
|
||
oldScrollViewHeight: 0,
|
||
page: 1,
|
||
hasMore: true,
|
||
bootstrapLoading: false,
|
||
historyLoading: false,
|
||
isConsultationEnded: false,
|
||
keyboardHeight: 0,
|
||
currentUserId: '',
|
||
detailOpen: false,
|
||
roomChatStatus: 0,
|
||
transferCard: null,
|
||
recorderManager: null,
|
||
showToolPanel: false,
|
||
showQuickReplySheet: false,
|
||
quickReplyList: [],
|
||
quickReplyLoading: false,
|
||
transferDetailOpen: false,
|
||
ordersPopupOpen: false,
|
||
ordersRxList: [],
|
||
ordersRxLoading: false,
|
||
toolPanelSwiperCurrent: 0,
|
||
toolPlaceholderCells: TOOL_PLACEHOLDER_CELLS
|
||
};
|
||
},
|
||
computed: {
|
||
navbarTitle() {
|
||
return this.patientName || '在线问诊';
|
||
},
|
||
patientAvatar() {
|
||
return this.patientInfo?.user_patient?.avatar || '';
|
||
},
|
||
doctorAvatar() {
|
||
return uni.getStorageSync('doctor_avatar') || '';
|
||
},
|
||
roomEnded() {
|
||
return this.roomChatStatus === 1 || this.isConsultationEnded;
|
||
},
|
||
inputLocked() {
|
||
if (this.roomEnded) return true;
|
||
const st = Number(this.patientInfo && this.patientInfo.status);
|
||
if (!st) return false;
|
||
if (st === 1 || st === 2) return false;
|
||
return true;
|
||
},
|
||
lockReason() {
|
||
if (this.roomChatStatus === 1 || this.isConsultationEnded) return '问诊已结束';
|
||
const st = Number(this.patientInfo && this.patientInfo.status);
|
||
if (st === 7 || st === 8) return '已拒诊';
|
||
if (st >= 3) return '当前状态不可发送消息';
|
||
return '不可发送';
|
||
},
|
||
statusLine() {
|
||
const st = Number(this.patientInfo && this.patientInfo.status);
|
||
const m = {
|
||
0: '待支付',
|
||
1: '待接诊',
|
||
2: '接诊中',
|
||
3: '已结束',
|
||
4: '已取消',
|
||
7: '已拒诊',
|
||
8: '已拒诊'
|
||
};
|
||
return m[st] || '问诊中';
|
||
},
|
||
canAddExperienceDrugFromChat() {
|
||
const st = Number(this.patientInfo && this.patientInfo.status);
|
||
return st === 2 && !this.inputLocked && !!this.registerId;
|
||
},
|
||
registerOrderBlock() {
|
||
const i = this.patientInfo;
|
||
return i && i.register_order ? i.register_order : null;
|
||
},
|
||
childOrdersBlock() {
|
||
const ro = this.registerOrderBlock;
|
||
if (!ro) return [];
|
||
const raw = ro.child_orders || ro.sub_orders || ro.orders;
|
||
return Array.isArray(raw) ? raw : [];
|
||
},
|
||
childOrdersWithKey() {
|
||
return withWxKey(this.childOrdersBlock, 'id', 'osub');
|
||
},
|
||
transferQuestionsWithKey() {
|
||
const qs = (this.transferCard && this.transferCard.questions) || [];
|
||
return withWxKey(qs, 'id', 'tdq');
|
||
},
|
||
refuseToolDisabled() {
|
||
return Number(this.patientInfo && this.patientInfo.status) !== 1;
|
||
},
|
||
endToolDisabled() {
|
||
if (this.roomEnded) return true;
|
||
return Number(this.patientInfo && this.patientInfo.status) !== 2;
|
||
},
|
||
transferToolDisabled() {
|
||
return !this.isTransferRegister || !this.transferCard;
|
||
},
|
||
experienceToolDisabled() {
|
||
return !this.canAddExperienceDrugFromChat;
|
||
},
|
||
goRxToolDisabled() {
|
||
return Number(this.patientInfo && this.patientInfo.status) !== 2;
|
||
},
|
||
canImportTransferRxStrip() {
|
||
return this.isTransferRegister && canImportTransferPrescription(this.transferCard);
|
||
},
|
||
isTransferRegister() {
|
||
const p = this.patientInfo;
|
||
if (!p) return false;
|
||
if (Number(p.is_from_transfer) === 1) return true;
|
||
const ro = p.register_order;
|
||
if (ro && Number(ro.is_from_transfer) === 1) return true;
|
||
const subs = p.child_orders || p.sub_orders || p.orders;
|
||
if (Array.isArray(subs)) {
|
||
return subs.some((c) => Number(c.is_from_transfer) === 1);
|
||
}
|
||
return false;
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.registerId = options.register_id || '';
|
||
let rid = options.room_id || '';
|
||
try {
|
||
rid = rid ? decodeURIComponent(rid) : '';
|
||
} catch (e) {
|
||
rid = options.room_id || '';
|
||
}
|
||
this.roomId = rid;
|
||
try {
|
||
this.patientName = options.patient_name ? decodeURIComponent(options.patient_name) : '';
|
||
} catch (e) {
|
||
this.patientName = options.patient_name || '';
|
||
}
|
||
this.userPatientId = options.user_patient_id || '';
|
||
|
||
this.currentUserId = this.getHttpSenderUserId();
|
||
|
||
chatRoomManager.onMessage(this.handleChatMessage);
|
||
uni.onKeyboardHeightChange((res) => {
|
||
this.keyboardHeight = res.height || 0;
|
||
if (res.height > 0) this.scrollToBottom();
|
||
});
|
||
uni.$on('message-updated', this.onMessageUpdated);
|
||
|
||
this.bootstrap(options);
|
||
},
|
||
onShow() {
|
||
if (this.roomId) chatRoomManager.enterRoom(this.roomId);
|
||
this.refreshLatestChatMessages();
|
||
},
|
||
onHide() {
|
||
this.stopAudio();
|
||
},
|
||
onUnload() {
|
||
if (this.innerAudioContext) this.innerAudioContext.destroy();
|
||
chatRoomManager.leaveRoom();
|
||
chatRoomManager.offMessage(this.handleChatMessage);
|
||
uni.$off('message-updated', this.onMessageUpdated);
|
||
},
|
||
mounted() {
|
||
this.recorderManager = uni.getRecorderManager();
|
||
this.initRecorder();
|
||
},
|
||
methods: {
|
||
unwrap(res) {
|
||
if (res == null) return null;
|
||
if (res.result !== undefined && res.result !== null) return res.result;
|
||
if (res.data && res.data.result !== undefined) return res.data.result;
|
||
return res;
|
||
},
|
||
async bootstrap() {
|
||
if (!this.registerId) {
|
||
uni.showToast({ title: '缺少挂号信息', icon: 'none' });
|
||
return;
|
||
}
|
||
this.bootstrapLoading = true;
|
||
try {
|
||
await this.loadPatientDetail();
|
||
if (!this.roomId) {
|
||
const roomRes = await getChatRoomByRegisterApi(this.registerId);
|
||
const roomPayload = this.unwrap(roomRes) || {};
|
||
this.roomId = roomPayload.room_id || '';
|
||
if (roomPayload.receiver_user_id) this.patientUserId = roomPayload.receiver_user_id;
|
||
}
|
||
if (!this.patientUserId && this.patientInfo) {
|
||
const uid = this.patientInfo.user_id || this.patientInfo.user_patient?.user_id;
|
||
if (uid) this.patientUserId = `user-${uid}`;
|
||
else if (this.userPatientId) this.patientUserId = `user-${this.userPatientId}`;
|
||
}
|
||
if (this.roomId) {
|
||
chatRoomManager.enterRoom(this.roomId);
|
||
await this.refreshRoomStatus();
|
||
this.page = 1;
|
||
this.hasMore = true;
|
||
await this.loadHistory();
|
||
}
|
||
await this.loadTransferCard();
|
||
} catch (e) {
|
||
console.error(e);
|
||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||
} finally {
|
||
this.bootstrapLoading = false;
|
||
}
|
||
},
|
||
async refreshLatestChatMessages() {
|
||
if (!this.roomId || this.bootstrapLoading || this.historyLoading) return;
|
||
try {
|
||
this.page = 1;
|
||
this.hasMore = true;
|
||
await this.loadHistory();
|
||
} catch (e) {
|
||
console.warn('refreshLatestChatMessages', e);
|
||
}
|
||
},
|
||
async loadPatientDetail() {
|
||
const res = await getPatientItem(this.registerId);
|
||
const data = this.unwrap(res);
|
||
this.patientInfo = data && typeof data === 'object' ? data : null;
|
||
if (this.patientInfo) {
|
||
if (!this.patientName && this.patientInfo.user_patient) {
|
||
this.patientName = this.patientInfo.user_patient.name || '';
|
||
}
|
||
const uid = this.patientInfo.user_id || this.patientInfo.user_patient?.user_id;
|
||
if (uid) this.patientUserId = `user-${uid}`;
|
||
}
|
||
},
|
||
async refreshRoomStatus() {
|
||
if (!this.roomId) return;
|
||
try {
|
||
const res = await getRoomStatusApi({ room_id: this.roomId });
|
||
const raw = this.unwrap(res) || {};
|
||
this.roomChatStatus = Number(raw.status) || 0;
|
||
if (this.roomChatStatus === 1) this.isConsultationEnded = true;
|
||
} catch (e) {
|
||
console.warn(e);
|
||
}
|
||
},
|
||
async loadTransferCard() {
|
||
try {
|
||
const res = await getTransferPrescriptionByRegisterApi(this.registerId);
|
||
const data = this.unwrap(res);
|
||
this.transferCard = normalizeTransferCardPayload(data);
|
||
} catch (e) {
|
||
this.transferCard = null;
|
||
}
|
||
},
|
||
syncMessagesFromRoom() {
|
||
const raw = chatRoomManager.getRoomMessages(this.roomId) || [];
|
||
this.messages = withWxKey(raw, 'id', 'm');
|
||
},
|
||
onMessageUpdated({ roomId }) {
|
||
if (roomId === this.roomId) {
|
||
this.syncMessagesFromRoom();
|
||
this.$forceUpdate();
|
||
}
|
||
},
|
||
handleChatMessage(message, roomId) {
|
||
if (roomId === this.roomId) {
|
||
this.addMessage(message);
|
||
this.scrollToBottom();
|
||
}
|
||
},
|
||
onScrollToUpper() {
|
||
if (this.bootstrapLoading) return;
|
||
this.loadHistory();
|
||
},
|
||
async loadHistory() {
|
||
if (!this.hasMore || this.historyLoading || !this.roomId) return;
|
||
const wasFirstPage = this.page === 1;
|
||
if (!wasFirstPage) {
|
||
await new Promise((resolve) => {
|
||
this.$nextTick(() => {
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query
|
||
.select('.message-list-container')
|
||
.boundingClientRect((res) => {
|
||
this.oldScrollViewHeight = res && res.height ? res.height : 0;
|
||
resolve();
|
||
})
|
||
.exec();
|
||
});
|
||
});
|
||
}
|
||
this.historyLoading = true;
|
||
try {
|
||
const lastMessageId = wasFirstPage ? 0 : chatRoomManager.lastMessageId;
|
||
const batch = await chatRoomManager.loadRoomMessages(this.roomId, lastMessageId);
|
||
if (batch && batch.length > 0) {
|
||
this.syncMessagesFromRoom();
|
||
this.page++;
|
||
} else {
|
||
this.hasMore = false;
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.hasMore = false;
|
||
} finally {
|
||
this.historyLoading = false;
|
||
this.$nextTick(() => {
|
||
if (wasFirstPage && this.messages.length > 0) {
|
||
this.scrollToBottom();
|
||
} else if (!wasFirstPage) {
|
||
this.scrollToOldLastMessage();
|
||
}
|
||
});
|
||
}
|
||
},
|
||
addMessage(message) {
|
||
if (!message.message_content && message.content) {
|
||
message.message_content = message.content;
|
||
}
|
||
if (message.message_type === undefined && message.type !== undefined) {
|
||
message.message_type = chatConfig.getMessageType(message.type);
|
||
}
|
||
const normalizeContent = (c) => {
|
||
if (c == null || c === '') return '';
|
||
if (typeof c === 'string') {
|
||
try {
|
||
return JSON.stringify(JSON.parse(c));
|
||
} catch (e) {
|
||
return c;
|
||
}
|
||
}
|
||
if (typeof c === 'object') return JSON.stringify(c);
|
||
return String(c);
|
||
};
|
||
if (!message.isTemporary) {
|
||
const tempIndex = this.messages.findIndex((m) => {
|
||
if (!m.isTemporary) return false;
|
||
if ((m.sender_user_id || '') !== (message.sender_user_id || '')) return false;
|
||
if (normalizeContent(m.message_content) !== normalizeContent(message.message_content)) return false;
|
||
const t1 = Number(m.created_at || m.timestamp || 0);
|
||
const t2 = Number(message.created_at || message.timestamp || 0);
|
||
const a = t1 > 1e12 ? t1 : t1 * 1000;
|
||
const b = t2 > 1e12 ? t2 : t2 * 1000;
|
||
return Math.abs(a - b) < 60000;
|
||
});
|
||
if (tempIndex !== -1) {
|
||
const patched = ensureWxKey({ ...message, isTemporary: false }, 'id', `m${tempIndex}`);
|
||
this.$set(this.messages, tempIndex, patched);
|
||
return;
|
||
}
|
||
}
|
||
const existingIndex = this.messages.findIndex((m) => m.id === message.id);
|
||
if (existingIndex !== -1) {
|
||
this.$set(this.messages, existingIndex, ensureWxKey(message, 'id', `m${existingIndex}`));
|
||
return;
|
||
}
|
||
this.messages.push(ensureWxKey(message, 'id', `m${this.messages.length}`));
|
||
if (message.message_type === chatConfig.messageTypes['end-consultation']) {
|
||
this.isConsultationEnded = true;
|
||
}
|
||
},
|
||
isMsgMine(msg) {
|
||
const senderId = msg.sender_user_id || '';
|
||
return senderId === this.currentUserId;
|
||
},
|
||
getMsgAvatar(msg) {
|
||
const sid = msg.sender_user_id || '';
|
||
const fallback = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg';
|
||
if (sid === 'doctor_assistant') return fallback;
|
||
if (this.isMsgMine(msg)) {
|
||
return this.doctorAvatar || fallback;
|
||
}
|
||
return this.patientAvatar || fallback;
|
||
},
|
||
toMessageMs(t) {
|
||
const n = Number(t) || 0;
|
||
return n > 1e12 ? n : n * 1000;
|
||
},
|
||
shouldShowTime(msg, index) {
|
||
if (index === 0) return true;
|
||
const prev = this.messages[index - 1];
|
||
const currMs = this.toMessageMs(msg.created_at || msg.timestamp || 0);
|
||
const prevMs = this.toMessageMs(prev.created_at || prev.timestamp || 0);
|
||
return currMs - prevMs > 300000;
|
||
},
|
||
timeDividerText(msg) {
|
||
if (msg.created_at_text) return msg.created_at_text;
|
||
return this.formatTimeDivider(msg.created_at || msg.timestamp);
|
||
},
|
||
formatTimeDivider(ts) {
|
||
if (!ts && ts !== 0) return '';
|
||
const ms = Number(ts) > 1e12 ? Number(ts) : Number(ts) * 1000;
|
||
const d = new Date(ms);
|
||
const h = d.getHours().toString().padStart(2, '0');
|
||
const m = d.getMinutes().toString().padStart(2, '0');
|
||
return `${h}:${m}`;
|
||
},
|
||
formatClockTime(date) {
|
||
const hours = date.getHours().toString().padStart(2, '0');
|
||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||
return `${hours}:${minutes}`;
|
||
},
|
||
formatRecordingTime(seconds) {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
},
|
||
scrollToBottom() {
|
||
if (!this.messages.length) return;
|
||
this.scrollIntoView = '';
|
||
this.$nextTick(() => {
|
||
setTimeout(() => {
|
||
this.scrollIntoView = 'chat-bottom-anchor';
|
||
setTimeout(() => {
|
||
if (this.scrollIntoView !== 'chat-bottom-anchor') {
|
||
this.scrollIntoView = 'chat-bottom-anchor';
|
||
}
|
||
}, 100);
|
||
}, 300);
|
||
});
|
||
},
|
||
scrollToOldLastMessage() {
|
||
this.scrollIntoView = '';
|
||
this.$nextTick(() => {
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query
|
||
.select('.message-list-container')
|
||
.boundingClientRect((rect) => {
|
||
if (!rect) return;
|
||
const heightDiff = rect.height - this.oldScrollViewHeight;
|
||
if (heightDiff > 0) {
|
||
const next = heightDiff;
|
||
this.scrollTop = this.scrollTop === next ? next + 0.01 : next;
|
||
this.oldScrollViewHeight = rect.height;
|
||
}
|
||
})
|
||
.exec();
|
||
});
|
||
},
|
||
onInputFocus() {
|
||
this.scrollToBottom();
|
||
},
|
||
onInputBlur() {},
|
||
toggleToolPanel() {
|
||
this.showToolPanel = !this.showToolPanel;
|
||
if (!this.showToolPanel) {
|
||
this.closeQuickReplySheet();
|
||
this.toolPanelSwiperCurrent = 0;
|
||
}
|
||
},
|
||
onToolPanelSwiperChange(e) {
|
||
const c = e && e.detail && e.detail.current;
|
||
this.toolPanelSwiperCurrent = typeof c === 'number' ? c : 0;
|
||
},
|
||
onMorePickImage() {
|
||
this.onToolPickImage();
|
||
},
|
||
onMoreQuickReply() {
|
||
this.onToolQuickReply();
|
||
},
|
||
onMorePatientInfo() {
|
||
this.showToolPanel = false;
|
||
if (this.detailOpen) {
|
||
uni.showToast({ title: '患者信息已在顶部展开', icon: 'none' });
|
||
return;
|
||
}
|
||
this.detailOpen = true;
|
||
},
|
||
async onMoreOrdersPopup() {
|
||
this.showToolPanel = false;
|
||
if (!this.patientInfo) {
|
||
uni.showToast({ title: '暂无患者信息', icon: 'none' });
|
||
return;
|
||
}
|
||
this.ordersPopupOpen = true;
|
||
await this.loadOrdersRxListForPopup();
|
||
},
|
||
async loadOrdersRxListForPopup() {
|
||
if (!this.registerId) {
|
||
this.ordersRxList = [];
|
||
return;
|
||
}
|
||
this.ordersRxLoading = true;
|
||
try {
|
||
const res = await getRegisterPrescriptionList({
|
||
register_id: this.registerId,
|
||
page: 1,
|
||
page_size: 30
|
||
});
|
||
const payload = this.unwrap(res) || {};
|
||
this.ordersRxList = withWxKey(Array.isArray(payload.list) ? payload.list : [], 'id', 'orx');
|
||
} catch (e) {
|
||
console.warn(e);
|
||
this.ordersRxList = [];
|
||
} finally {
|
||
this.ordersRxLoading = false;
|
||
}
|
||
},
|
||
registerOrderStatusText(status) {
|
||
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
|
||
const n = Number(status);
|
||
return map[n] != null ? map[n] : String(status);
|
||
},
|
||
onOrdersRxTap(e) {
|
||
const wxKey = e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.wxKey;
|
||
const rx = this.ordersRxList.find((r) => r._wxKey === wxKey);
|
||
const id = rx && (rx.id || rx.prescription_id);
|
||
if (!id) return;
|
||
let url = `/subPackages/sub_my/prescriptionDetail?id=${id}`;
|
||
const no = rx.order_no || rx.prescription_no;
|
||
if (no) url += `&no=${encodeURIComponent(no)}`;
|
||
this.ordersPopupOpen = false;
|
||
uni.navigateTo({ url });
|
||
},
|
||
onMoreTransferDetail() {
|
||
if (this.transferToolDisabled) {
|
||
uni.showToast({ title: this.isTransferRegister ? '暂无转诊信息' : '当前挂号无转诊处方', icon: 'none' });
|
||
return;
|
||
}
|
||
this.showToolPanel = false;
|
||
this.openTransferDetail();
|
||
},
|
||
onMoreGoPrescription() {
|
||
this.showToolPanel = false;
|
||
if (this.goRxToolDisabled) {
|
||
uni.showToast({ title: '请先接诊', icon: 'none' });
|
||
return;
|
||
}
|
||
this.goPrescriptionV2();
|
||
},
|
||
onMoreExperienceToRx() {
|
||
if (this.experienceToolDisabled) {
|
||
uni.showToast({ title: '请先接诊后再开方', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.showModal({
|
||
title: '诊历带药',
|
||
content: '此操作会覆盖当前的处方,是否确认?',
|
||
confirmText: '确认',
|
||
cancelText: '取消',
|
||
success: (modalRes) => {
|
||
if (!modalRes.confirm) return;
|
||
this.showToolPanel = false;
|
||
this.onRegisterDrugsBulkToPrescription();
|
||
}
|
||
});
|
||
},
|
||
async onRegisterDrugsBulkToPrescription() {
|
||
if (!this.canAddExperienceDrugFromChat) {
|
||
uni.showToast({ title: '请先接诊后再开方', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.showLoading({ title: '加载药品…', mask: true });
|
||
try {
|
||
const rows = await this.fetchRegisterExperienceDrugRows();
|
||
if (!rows.length) {
|
||
uni.showToast({ title: '未找到药品信息', icon: 'none' });
|
||
return;
|
||
}
|
||
try {
|
||
uni.setStorageSync(
|
||
PENDING_RX_FROM_CHAT,
|
||
JSON.stringify({
|
||
source: 'register_drugs',
|
||
register_id: String(this.registerId),
|
||
drugs: rows
|
||
})
|
||
);
|
||
} catch (err) {
|
||
console.error(err);
|
||
uni.showToast({ title: '缓存失败', icon: 'none' });
|
||
return;
|
||
}
|
||
this.goPrescriptionV2();
|
||
} catch (e) {
|
||
console.error(e);
|
||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
},
|
||
onTransferImportToPrescription() {
|
||
if (!this.isTransferRegister) {
|
||
uni.showToast({ title: '当前挂号无转诊处方', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!this.transferCard || !canImportTransferPrescription(this.transferCard)) {
|
||
uni.showToast({ title: '当前不可导入', icon: 'none' });
|
||
return;
|
||
}
|
||
const parsed = parseTransferPrescriptionContent(this.transferCard);
|
||
if (!parsed || !parsed.drugList || !parsed.drugList.length) {
|
||
uni.showToast({ title: '处方内容为空', icon: 'none' });
|
||
return;
|
||
}
|
||
const rows = parsed.drugList.map((d) => transferDrugToChineseRow(d, parsed.prescription_type));
|
||
try {
|
||
uni.setStorageSync(
|
||
PENDING_RX_FROM_CHAT,
|
||
JSON.stringify({
|
||
source: 'transfer_import',
|
||
register_id: String(this.registerId),
|
||
drugs: rows,
|
||
diagnosisText: parsed.clinical_diagnose || '',
|
||
medicalAdvice: parsed.doctor_order || ''
|
||
})
|
||
);
|
||
} catch (err) {
|
||
console.error(err);
|
||
uni.showToast({ title: '缓存失败', icon: 'none' });
|
||
return;
|
||
}
|
||
this.transferDetailOpen = false;
|
||
this.goPrescriptionV2();
|
||
},
|
||
onMoreRefuse() {
|
||
if (this.refuseToolDisabled) return;
|
||
this.showToolPanel = false;
|
||
this.onRefuseTap();
|
||
},
|
||
onMoreEndDiagnosis() {
|
||
if (this.endToolDisabled) {
|
||
uni.showToast({ title: '当前不可结束问诊', icon: 'none' });
|
||
return;
|
||
}
|
||
this.showToolPanel = false;
|
||
this.onEndDiagnosis();
|
||
},
|
||
onToolPickImage() {
|
||
this.showToolPanel = false;
|
||
this.chooseImage();
|
||
},
|
||
async onToolQuickReply() {
|
||
this.showToolPanel = false;
|
||
this.showQuickReplySheet = true;
|
||
this.quickReplyList = [];
|
||
this.quickReplyLoading = true;
|
||
try {
|
||
const res = await getQuickReplyListApi();
|
||
const raw = this.unwrap(res);
|
||
const list = Array.isArray(raw) ? raw : raw && Array.isArray(raw.list) ? raw.list : [];
|
||
this.quickReplyList = withWxKey(list, 'id', 'qr');
|
||
} catch (e) {
|
||
console.error(e);
|
||
uni.showToast({ title: '加载常用回复失败', icon: 'none' });
|
||
this.quickReplyList = [];
|
||
} finally {
|
||
this.quickReplyLoading = false;
|
||
}
|
||
},
|
||
closeQuickReplySheet() {
|
||
this.showQuickReplySheet = false;
|
||
},
|
||
selectQuickReply(item) {
|
||
const t = item && (item.content != null ? String(item.content) : '');
|
||
this.newMessage = t;
|
||
this.closeQuickReplySheet();
|
||
this.showToolPanel = false;
|
||
},
|
||
toggleVoiceInput() {
|
||
this.voiceInputActive = !this.voiceInputActive;
|
||
},
|
||
sendTextMessage() {
|
||
if (!this.newMessage.trim() || !this.roomId || this.inputLocked) return;
|
||
this.sendMessage({ type: 'text', content: this.newMessage });
|
||
this.newMessage = '';
|
||
this.showToolPanel = false;
|
||
},
|
||
sendMessage(message) {
|
||
if (!this.roomId || !this.patientUserId || this.inputLocked) return;
|
||
const now = new Date();
|
||
const messageType = chatConfig.getMessageType(message.type || 'text');
|
||
const tempId = `tmp_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||
const fullMessage = {
|
||
...message,
|
||
id: tempId,
|
||
sender_user_id: this.getHttpSenderUserId(),
|
||
receiver_user_id: this.patientUserId,
|
||
room_id: this.roomId,
|
||
created_at_text: this.formatClockTime(now),
|
||
created_at: now.getTime(),
|
||
timestamp: now.getTime(),
|
||
message_content: message.message_content || message.content || '',
|
||
message_type: messageType,
|
||
isTemporary: true
|
||
};
|
||
this.addMessage(fullMessage);
|
||
this.scrollToBottom();
|
||
this.postSendMessageHttp(fullMessage, tempId, messageType, message.duration || 0);
|
||
},
|
||
getHttpSenderUserId() {
|
||
const raw = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||
const n = parseInt(raw, 10);
|
||
if (!Number.isNaN(n) && n > 0) return `doctor-${n}`;
|
||
return raw && String(raw).startsWith('doctor-') ? String(raw) : `doctor-${raw}`;
|
||
},
|
||
async postSendMessageHttp(fullMessage, tempId, messageType, duration) {
|
||
const payload = {
|
||
room_id: this.roomId,
|
||
sender_user_id: this.getHttpSenderUserId(),
|
||
receiver_user_id: this.patientUserId,
|
||
message_type: messageType,
|
||
message_content: fullMessage.message_content || '',
|
||
duration: duration || 0,
|
||
isFromHttp: true
|
||
};
|
||
try {
|
||
const res = await sendToUserHttpApi(payload);
|
||
if (!isSendToUserResponseOk(res)) {
|
||
throw { res };
|
||
}
|
||
const mid = extractSendToUserMessageId(res);
|
||
if (mid != null && mid !== '') {
|
||
const idx = this.messages.findIndex((m) => m.id === tempId);
|
||
if (idx !== -1) {
|
||
this.$set(this.messages[idx], 'id', mid);
|
||
this.$set(this.messages[idx], '_wxKey', String(mid));
|
||
this.$set(this.messages[idx], 'isTemporary', false);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
const res = e && e.res != null ? e.res : e && e.data != null ? e.data : e;
|
||
const idx = this.messages.findIndex((m) => m.id === tempId);
|
||
if (idx !== -1) this.messages.splice(idx, 1);
|
||
uni.showToast({ title: sendToUserFailMessage(res, e), icon: 'none' });
|
||
}
|
||
},
|
||
chooseImage() {
|
||
if (this.inputLocked) return;
|
||
uni.chooseImage({
|
||
count: 1,
|
||
success: async (res) => {
|
||
const tempFile = res.tempFiles && res.tempFiles[0];
|
||
const filePath = tempFile ? tempFile.path : res.tempFilePaths[0];
|
||
let fileSize = tempFile ? tempFile.size : 0;
|
||
|
||
if (!fileSize && filePath) {
|
||
try {
|
||
const info = await new Promise((resolve, reject) => {
|
||
uni.getFileInfo({
|
||
filePath,
|
||
success: resolve,
|
||
fail: reject,
|
||
});
|
||
});
|
||
fileSize = info.size;
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
const prepared = await prepareImagePath({
|
||
path: filePath,
|
||
size: fileSize,
|
||
});
|
||
if (!prepared) return;
|
||
this.uploadMedia(prepared.path, 'image');
|
||
},
|
||
});
|
||
},
|
||
async uploadMedia(filePath, kind) {
|
||
uni.showLoading({ title: '上传中...', mask: true });
|
||
try {
|
||
const res = await uploadChatFileApi({ filePath, name: 'file' });
|
||
const inner = this.unwrap(res);
|
||
const url = inner && (inner.url || inner.path);
|
||
if (!url) {
|
||
uni.showToast({ title: '上传失败', icon: 'none' });
|
||
return;
|
||
}
|
||
if (kind === 'image') {
|
||
this.sendMessage({ type: 'image', content: url });
|
||
} else if (kind === 'audio') {
|
||
const d = this._pendingAudioDuration || 1;
|
||
this.sendMessage({ type: 'audio', content: url, duration: d });
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
uni.showToast({ title: '上传失败', icon: 'none' });
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
},
|
||
initRecorder() {
|
||
this.recorderManager.onStop((res) => {
|
||
if (res.duration < 1000) {
|
||
uni.showToast({ title: '录音时间太短', icon: 'none' });
|
||
this._finishRecordingUi();
|
||
return;
|
||
}
|
||
const sec = Math.floor(res.duration / 1000);
|
||
this._pendingAudioDuration = sec;
|
||
this.uploadMedia(res.tempFilePath, 'audio');
|
||
this._finishRecordingUi();
|
||
});
|
||
this.recorderManager.onError(() => {
|
||
this._finishRecordingUi();
|
||
});
|
||
},
|
||
_finishRecordingUi() {
|
||
this.recording = false;
|
||
this.showRecordingModal = false;
|
||
if (this.recordingTimer) clearInterval(this.recordingTimer);
|
||
this.recordingTimer = null;
|
||
},
|
||
startVoiceRecording() {
|
||
if (this.inputLocked || this.recording) return;
|
||
uni.getSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.record']) {
|
||
this._startRecording();
|
||
} else {
|
||
uni.authorize({
|
||
scope: 'scope.record',
|
||
success: () => this._startRecording(),
|
||
fail: () => uni.showToast({ title: '需要录音权限', icon: 'none' })
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
_startRecording() {
|
||
this.recording = true;
|
||
this.showRecordingModal = true;
|
||
this.recordingTime = 0;
|
||
this.recordingTimer = setInterval(() => {
|
||
this.recordingTime++;
|
||
}, 1000);
|
||
this.recorderManager.start({
|
||
duration: 60000,
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 96000,
|
||
format: 'mp3'
|
||
});
|
||
},
|
||
stopVoiceRecording() {
|
||
if (!this.recording) return;
|
||
this.recorderManager.stop();
|
||
},
|
||
playAudio(msg) {
|
||
const parsed = parseMessageContent(msg.message_type, msg.message_content);
|
||
const finalUrl = typeof parsed === 'string' ? parsed : (parsed && parsed.url) || '';
|
||
if (!finalUrl) {
|
||
uni.showToast({ title: '无效的音频地址', icon: 'none' });
|
||
return;
|
||
}
|
||
if (this.currentPlayingAudio === msg.id) {
|
||
this.stopAudio();
|
||
return;
|
||
}
|
||
this.stopAudio();
|
||
if (!this.innerAudioContext) {
|
||
this.innerAudioContext = uni.createInnerAudioContext();
|
||
this.innerAudioContext.onEnded(() => {
|
||
this.currentPlayingAudio = null;
|
||
});
|
||
this.innerAudioContext.onError(() => {
|
||
this.currentPlayingAudio = null;
|
||
uni.showToast({ title: '播放失败', icon: 'none' });
|
||
});
|
||
}
|
||
this.innerAudioContext.src = finalUrl;
|
||
this.innerAudioContext.play();
|
||
this.currentPlayingAudio = msg.id;
|
||
},
|
||
stopAudio() {
|
||
if (this.innerAudioContext) this.innerAudioContext.stop();
|
||
this.currentPlayingAudio = null;
|
||
},
|
||
isPlaying(msg) {
|
||
return this.currentPlayingAudio === msg.id;
|
||
},
|
||
previewImage(url) {
|
||
uni.previewImage({ urls: [url], current: url });
|
||
},
|
||
viewPrescription(content) {
|
||
const id = content && content.id;
|
||
if (id) {
|
||
uni.navigateTo({ url: `/subPackages/sub_my/prescriptionDetail?id=${id}` });
|
||
}
|
||
},
|
||
onBubbleViewPrescription(idOrContent, orderNo) {
|
||
if (idOrContent != null && typeof idOrContent === 'object') {
|
||
this.viewPrescription(idOrContent);
|
||
return;
|
||
}
|
||
const id = idOrContent;
|
||
if (id) {
|
||
let url = `/subPackages/sub_my/prescriptionDetail?id=${id}`;
|
||
if (orderNo) url += `&no=${encodeURIComponent(orderNo)}`;
|
||
uni.navigateTo({ url });
|
||
}
|
||
},
|
||
openTransferDetail() {
|
||
if (!this.isTransferRegister || !this.transferCard) return;
|
||
this.transferDetailOpen = true;
|
||
},
|
||
transferStatusText(st) {
|
||
const n = Number(st);
|
||
const m = { 0: '待处理', 1: '已同意', 3: '已拒绝' };
|
||
return m[n] != null ? m[n] : `状态${st}`;
|
||
},
|
||
async resolveStoreIdForRegisterDrugs() {
|
||
try {
|
||
const res = await getCurrentStoreTypeApi({ register_id: this.registerId });
|
||
const raw = this.unwrap(res) || {};
|
||
const sid =
|
||
Number(raw.is_from_transfer) === 1 && raw.delegate_store_id
|
||
? Number(raw.delegate_store_id)
|
||
: Number(raw.store_id) || 0;
|
||
return sid > 0 ? sid : 0;
|
||
} catch (e) {
|
||
console.warn(e);
|
||
return 0;
|
||
}
|
||
},
|
||
async fetchRegisterExperienceDrugRows() {
|
||
const storeId = await this.resolveStoreIdForRegisterDrugs();
|
||
if (!storeId) {
|
||
uni.showToast({ title: '无法获取门店,请稍后重试', icon: 'none' });
|
||
return [];
|
||
}
|
||
const res = await getDrugsByRegisterIdApi({
|
||
register_id: this.registerId,
|
||
store_id: storeId
|
||
});
|
||
const list = this.unwrap(res);
|
||
return Array.isArray(list) ? list : [];
|
||
},
|
||
onAddExperienceToPrescription(payload) {
|
||
if (!this.canAddExperienceDrugFromChat) {
|
||
uni.showToast({ title: '请先接诊后再开方', icon: 'none' });
|
||
return;
|
||
}
|
||
const drugName = payload && (payload.drug_name || payload.name);
|
||
if (!drugName) {
|
||
uni.showToast({ title: '无药品名称', icon: 'none' });
|
||
return;
|
||
}
|
||
this._applyExperienceDrugWithOptionalFilter(payload);
|
||
},
|
||
async _applyExperienceDrugWithOptionalFilter(payload) {
|
||
uni.showLoading({ title: '加载药品…', mask: true });
|
||
try {
|
||
const rows = await this.fetchRegisterExperienceDrugRows();
|
||
if (rows.length) {
|
||
const wantId = payload && payload.drug_id != null && String(payload.drug_id).trim() !== '' ? String(payload.drug_id) : '';
|
||
const wantName = String(payload.drug_name || payload.name || '').trim();
|
||
let picked = rows;
|
||
if (wantId) {
|
||
picked = rows.filter((r) => {
|
||
const did = r.drug_id != null ? String(r.drug_id) : r.drug && r.drug.id != null ? String(r.drug.id) : '';
|
||
return did === wantId;
|
||
});
|
||
}
|
||
if (!picked.length && wantName) {
|
||
picked = rows.filter((r) => {
|
||
const dn = (r.drug && r.drug.drug_name) || r.drug_name || '';
|
||
return String(dn).trim() === wantName;
|
||
});
|
||
}
|
||
if (picked.length) {
|
||
try {
|
||
uni.setStorageSync(
|
||
PENDING_RX_FROM_CHAT,
|
||
JSON.stringify({
|
||
source: 'register_drugs',
|
||
register_id: String(this.registerId),
|
||
drugs: picked
|
||
})
|
||
);
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
uni.hideLoading();
|
||
this.goPrescriptionV2();
|
||
return;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn(e);
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
try {
|
||
uni.setStorageSync(
|
||
PENDING_RX_FROM_CHAT,
|
||
JSON.stringify({
|
||
register_id: String(this.registerId),
|
||
drug_name: String(payload.drug_name || payload.name || ''),
|
||
drug_id: payload && payload.drug_id != null ? String(payload.drug_id) : ''
|
||
})
|
||
);
|
||
} catch (err2) {
|
||
console.error(err2);
|
||
}
|
||
this.goPrescriptionV2();
|
||
},
|
||
goPrescriptionV2() {
|
||
const st = Number(this.patientInfo && this.patientInfo.status);
|
||
if (st !== 2) {
|
||
uni.showToast({ title: '请先接诊', icon: 'none' });
|
||
return;
|
||
}
|
||
const pid = this.userPatientId || this.patientInfo?.user_patient_id || this.patientInfo?.user_patient?.id || '';
|
||
let url = `/subPackages/sub_workbench/prescription_v2/index?register_id=${this.registerId}`;
|
||
if (pid) url += `&patient_id=${pid}`;
|
||
uni.navigateTo({ url });
|
||
},
|
||
async onAccept() {
|
||
try {
|
||
await receptionApi(this.registerId);
|
||
uni.showToast({ title: '接诊成功', icon: 'none' });
|
||
await this.loadPatientDetail();
|
||
} catch (e) {
|
||
uni.showToast({ title: (e && e.msg) || '接诊失败', icon: 'none' });
|
||
}
|
||
},
|
||
onRefuseTap() {
|
||
this.$refs.refuseModal && this.$refs.refuseModal.open();
|
||
},
|
||
async onRefuseConfirm(reason) {
|
||
try {
|
||
await refuseReceptionApi({ register_id: this.registerId, refuse_reason: reason });
|
||
uni.showToast({ title: '已拒诊', icon: 'none' });
|
||
await this.loadPatientDetail();
|
||
} catch (e) {
|
||
uni.showToast({ title: (e && e.msg) || '操作失败', icon: 'none' });
|
||
}
|
||
},
|
||
async onEndDiagnosis() {
|
||
uni.showModal({
|
||
title: '结束诊断',
|
||
content: '确认结束本次诊断?',
|
||
success: async (r) => {
|
||
if (!r.confirm) return;
|
||
try {
|
||
await endOfDiagnosisApi(this.registerId);
|
||
uni.showToast({ title: '已结束', icon: 'none' });
|
||
await this.loadPatientDetail();
|
||
await this.refreshRoomStatus();
|
||
} catch (e) {
|
||
uni.showToast({ title: (e && e.msg) || '操作失败', icon: 'none' });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
$primary-color: #6acdbb;
|
||
$bg-page: #f4f6f9;
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: $bg-page;
|
||
}
|
||
.chat-navbar {
|
||
flex-shrink: 0;
|
||
}
|
||
.chat-header {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
.header-main {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx 32rpx;
|
||
}
|
||
.patient-name {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
.patient-badges {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 8rpx;
|
||
}
|
||
.status-dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
background: $primary-color;
|
||
margin-right: 8rpx;
|
||
}
|
||
.sub-text {
|
||
font-size: 24rpx;
|
||
color: #888;
|
||
}
|
||
.transfer-strip {
|
||
margin: 16rpx 24rpx;
|
||
padding: 20rpx 24rpx;
|
||
background: #fff7e6;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 26rpx;
|
||
}
|
||
.transfer-strip .transfer-t {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-right: 16rpx;
|
||
}
|
||
.transfer-strip-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
flex-shrink: 0;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
max-width: 52%;
|
||
}
|
||
.transfer-btn {
|
||
color: $primary-color;
|
||
font-weight: 500;
|
||
}
|
||
.transfer-btn--ghost {
|
||
color: #666;
|
||
font-weight: 400;
|
||
font-size: 26rpx;
|
||
}
|
||
.transfer-detail-popup {
|
||
padding: 28rpx 32rpx 24rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
}
|
||
.td-title {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.td-scroll {
|
||
max-height: 52vh;
|
||
}
|
||
.td-row {
|
||
display: flex;
|
||
margin-bottom: 16rpx;
|
||
font-size: 28rpx;
|
||
line-height: 1.45;
|
||
}
|
||
.td-k {
|
||
color: #888;
|
||
width: 160rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.td-v {
|
||
flex: 1;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
.td-qa {
|
||
margin-top: 8rpx;
|
||
padding: 16rpx;
|
||
background: #fafafa;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #eee;
|
||
}
|
||
.td-qa-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.td-qa-item {
|
||
margin-bottom: 12rpx;
|
||
padding: 12rpx;
|
||
background: #fff;
|
||
border-radius: 8rpx;
|
||
}
|
||
.td-qa-q {
|
||
display: block;
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.td-qa-a {
|
||
font-size: 24rpx;
|
||
color: $primary-color;
|
||
}
|
||
.td-foot {
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.td-foot--row {
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
}
|
||
.chat-messages {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding-bottom: 8rpx;
|
||
}
|
||
.message-list-container {
|
||
padding: 16rpx 0 120rpx;
|
||
}
|
||
.message-wrapper {
|
||
width: 100%;
|
||
}
|
||
.chat-bottom-anchor {
|
||
height: 1px;
|
||
width: 100%;
|
||
}
|
||
.empty-state--top {
|
||
padding: 12rpx 0 8rpx;
|
||
}
|
||
.empty-state--top .empty-text {
|
||
font-size: 24rpx;
|
||
color: #bbb;
|
||
}
|
||
.time-divider {
|
||
text-align: center;
|
||
margin: 16rpx 0;
|
||
}
|
||
.time-text {
|
||
font-size: 22rpx;
|
||
color: #999;
|
||
}
|
||
.loading-spinner {
|
||
text-align: center;
|
||
padding: 16rpx;
|
||
}
|
||
.spinner-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
.empty-state {
|
||
padding: 80rpx;
|
||
text-align: center;
|
||
}
|
||
.empty-text {
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
.chat-footer {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
background: #fff;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
.consultation-ended-notice {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
padding: 20rpx;
|
||
color: #999;
|
||
font-size: 26rpx;
|
||
}
|
||
.input-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx 24rpx;
|
||
border-top: 1rpx solid #eee;
|
||
}
|
||
.icon-btn {
|
||
padding: 12rpx;
|
||
}
|
||
.input-field-wrapper {
|
||
flex: 1;
|
||
margin: 0 16rpx;
|
||
}
|
||
.main-input {
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 36rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
.voice-btn {
|
||
text-align: center;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 36rpx;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
.voice-btn.recording {
|
||
background: #ffe0e0;
|
||
color: #c00;
|
||
}
|
||
.right-action {
|
||
min-width: 100rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
.send-btn {
|
||
padding: 12rpx 28rpx;
|
||
background: $primary-color;
|
||
color: #fff;
|
||
border-radius: 32rpx;
|
||
font-size: 28rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
.plus-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.plus-char {
|
||
font-size: 48rpx;
|
||
font-weight: 300;
|
||
color: #666;
|
||
line-height: 1;
|
||
}
|
||
.plus-char--open {
|
||
color: $primary-color;
|
||
}
|
||
.tool-panel-wrap {
|
||
background: #fff;
|
||
border-radius: 28rpx 28rpx 0 0;
|
||
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.06);
|
||
overflow: hidden;
|
||
border-top: 1rpx solid #eef2f0;
|
||
}
|
||
.tool-panel-swiper {
|
||
height: 360rpx;
|
||
width: 100%;
|
||
}
|
||
.tool-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
padding: 28rpx 12rpx 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.tool-cell {
|
||
width: 25%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding: 12rpx 0 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.tool-cell-icon {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 24rpx;
|
||
background: linear-gradient(145deg, #e8f6f4 0%, #f0faf8 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4rpx 12rpx rgba(106, 205, 187, 0.15);
|
||
}
|
||
.tool-cell-label {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
color: #333;
|
||
text-align: center;
|
||
line-height: 1.25;
|
||
max-width: 95%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
.tool-cell.is-disabled .tool-cell-icon {
|
||
opacity: 0.35;
|
||
}
|
||
.tool-cell.is-disabled .tool-cell-label {
|
||
color: #bbb;
|
||
}
|
||
.tool-cell--placeholder {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.orders-popup {
|
||
padding: 28rpx 32rpx 24rpx;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
}
|
||
.orders-popup-title {
|
||
display: block;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.orders-popup-scroll {
|
||
max-height: 56vh;
|
||
}
|
||
.orders-block {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
.orders-row {
|
||
display: flex;
|
||
margin-bottom: 14rpx;
|
||
font-size: 28rpx;
|
||
line-height: 1.4;
|
||
}
|
||
.orders-k {
|
||
color: #888;
|
||
width: 160rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.orders-v {
|
||
flex: 1;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
.orders-v.order-font {
|
||
font-family: Courier, monospace;
|
||
font-size: 24rpx;
|
||
}
|
||
.orders-v.price {
|
||
color: #ef4444;
|
||
font-weight: 600;
|
||
}
|
||
.orders-sub {
|
||
padding: 12rpx 16rpx;
|
||
background: #f8fafc;
|
||
border-radius: 10rpx;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
.orders-sub-label {
|
||
color: #999;
|
||
margin-right: 8rpx;
|
||
}
|
||
.orders-sub-no {
|
||
color: #333;
|
||
}
|
||
.orders-hint {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
padding: 12rpx 0;
|
||
}
|
||
.orders-section-title {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 20rpx 0 12rpx;
|
||
}
|
||
.orders-rx-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20rpx 16rpx;
|
||
margin-bottom: 10rpx;
|
||
background: #f8fafc;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #eef2f7;
|
||
}
|
||
.orders-rx-no {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
.orders-rx-price {
|
||
font-size: 26rpx;
|
||
color: #ef4444;
|
||
font-weight: 600;
|
||
margin-right: 12rpx;
|
||
}
|
||
.orders-rx-link {
|
||
font-size: 24rpx;
|
||
color: #6acdbb;
|
||
flex-shrink: 0;
|
||
}
|
||
.orders-popup-foot {
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.orders-popup-close {
|
||
padding: 16rpx 48rpx;
|
||
font-size: 28rpx;
|
||
color: #6acdbb;
|
||
font-weight: 500;
|
||
}
|
||
.quick-reply-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
}
|
||
.quick-reply-sheet {
|
||
width: 100%;
|
||
max-height: 60vh;
|
||
background: #fff;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
.quick-reply-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 28rpx 32rpx;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
.quick-reply-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
.quick-reply-close {
|
||
font-size: 28rpx;
|
||
color: $primary-color;
|
||
}
|
||
.quick-reply-scroll {
|
||
max-height: 50vh;
|
||
}
|
||
.quick-reply-row {
|
||
padding: 28rpx 32rpx;
|
||
border-bottom: 1rpx solid #f5f5f5;
|
||
}
|
||
.quick-reply-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.5;
|
||
}
|
||
.quick-reply-empty {
|
||
padding: 48rpx;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
.recording-overlay {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.recording-box {
|
||
background: #fff;
|
||
padding: 48rpx 64rpx;
|
||
border-radius: 16rpx;
|
||
text-align: center;
|
||
}
|
||
.timer {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
}
|
||
.tip {
|
||
display: block;
|
||
margin-top: 16rpx;
|
||
color: #666;
|
||
font-size: 26rpx;
|
||
}
|
||
</style>
|