在线复诊选择药品、说明主诉

This commit is contained in:
李琦
2026-05-06 16:41:19 +08:00
parent 1693b0e236
commit cd369ddf09
12 changed files with 2351 additions and 877 deletions

View File

@@ -0,0 +1,541 @@
<template>
<view class="consultation-container safe-area-inset-bottom">
<view class="consultation-header">
<view class="header-back" @click="$emit('back')">
<text class="iconfont icon-arrow-left"></text>
</view>
<view class="header-title">{{ headerTitle }}</view>
<view class="header-placeholder"></view>
</view>
<view class="notice-box" v-if="showNotice">
<view class="notice-text">
<text v-for="(part, index) in parsedNoticeParts" :key="index">
<text v-if="part.type === 'text'">{{ part.text }}</text>
<text v-else-if="part.type === 'agreement'" class="notice-link" @click="$emit('viewAgreement', part.agreementId)">
{{ part.text }}
</text>
</text>
</view>
</view>
<u-popup
v-model="agreementPopupVisible"
:safe-area-inset-bottom="true"
mode="bottom"
height="88%"
border-radius="20"
:closeable="true"
z-index="20000"
>
<view class="agreement-popup">
<view class="agreement-header">
<text class="agreement-title">{{ currentAgreementName || '协议详情' }}</text>
</view>
<view class="agreement-content">
<u-parse :html="agreementContent"></u-parse>
</view>
</view>
</u-popup>
<scroll-view class="messages-scroll" scroll-y="true" :scroll-into-view="scrollIntoView" scroll-with-animation>
<view class="messages-container">
<view v-for="message in displayedMessages" :key="message.id">
<view v-if="message.type === 'welcome'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>您好! 我是医生助理, 已收到您的用药申请:{{ prescriptionInfo }}, 为了帮助医生判断准确, 请按照您的真实情况回答以下问题</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'question'" class="message-item other" :id="'question-' + message.questionId">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'answer'" class="message-item mine">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'waiting'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>请稍等, 正在为您转接医生进行复诊开方...</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'hint'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
</view>
<view v-if="isTyping" class="message-item other typing-indicator">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble typing-bubble">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
<view :id="bottomAnchorId" style="height: 1px;"></view>
</view>
</scroll-view>
<slot v-if="customFooterActive && $slots.footer" name="footer"></slot>
<view
v-else-if="showQuestionButtons && currentQuestion && !isSubmitting && !showWaiting"
class="action-footer safe-area-inset-bottom"
>
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
:class="['action-btn', answerButtonClass(answer, index)]"
@click="$emit('answer', answer)"
>
{{ answer }}
</button>
</view>
<view class="submitting-overlay" v-if="isSubmitting">
<view class="submitting-content">
<view class="loading-spinner"></view>
<text class="submitting-text">{{ submittingText }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'AssistantConsultationShell',
props: {
headerTitle: {
type: String,
default: '转诊咨询',
},
showNotice: {
type: Boolean,
default: true,
},
parsedNoticeParts: {
type: Array,
default: () => [],
},
showAgreement: {
type: Boolean,
default: false,
},
agreementContent: {
type: String,
default: '',
},
currentAgreementName: {
type: String,
default: '',
},
displayedMessages: {
type: Array,
default: () => [],
},
assistantAvatar: {
type: String,
default: '/static/images/assistant-avatar.png',
},
assistantName: {
type: String,
default: '医生助理',
},
userAvatar: {
type: String,
default: '/static/images/user-avatar.png',
},
prescriptionInfo: {
type: String,
default: '',
},
isTyping: {
type: Boolean,
default: false,
},
scrollIntoView: {
type: String,
default: '',
},
bottomAnchorId: {
type: String,
default: 'bottom-anchor',
},
/** 为 true 时展示底部「是/否」等选项(与转诊一致) */
showQuestionButtons: {
type: Boolean,
default: true,
},
currentQuestion: {
type: Object,
default: null,
},
currentQuestionAnswers: {
type: Array,
default: () => ['是', '否'],
},
isSubmitting: {
type: Boolean,
default: false,
},
showWaiting: {
type: Boolean,
default: false,
},
submittingText: {
type: String,
default: '正在提交...',
},
/** 为 true 时使用 #footer 插槽,避免父组件始终占位导致无法显示默认答题按钮 */
customFooterActive: {
type: Boolean,
default: false,
},
},
computed: {
agreementPopupVisible: {
get() {
return this.showAgreement
},
set(val) {
this.$emit('update:showAgreement', val)
},
},
},
methods: {
answerButtonClass(answer, index) {
const answers = this.currentQuestionAnswers
const totalAnswers = answers.length
if (totalAnswers === 2) {
if (answer === '是') return 'yes-btn'
if (answer === '否') return 'no-btn'
}
if (index === 0) return 'yes-btn'
return 'no-btn'
},
},
}
</script>
<style lang="scss" scoped>
.consultation-container {
width: 100%;
height: 100vh;
background-color: #f8fafc;
display: flex;
flex-direction: column;
}
.consultation-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
flex-shrink: 0;
.header-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #333;
}
}
.header-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.header-placeholder {
width: 60rpx;
}
}
.notice-box {
margin: 20rpx 30rpx;
padding: 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #666;
flex-shrink: 0;
.notice-text {
display: block;
word-break: break-all;
}
.notice-link {
color: #0a84ff;
text-decoration: underline;
}
}
.messages-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
}
.messages-container {
padding: 30rpx;
min-height: 100%;
}
.message-item {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 10rpx;
flex-shrink: 0;
background: #f2f2f2;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
margin-left: 16rpx;
.assistant-label {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.message-bubble {
padding: 18rpx 24rpx;
font-size: 30rpx;
line-height: 1.5;
border-radius: 4rpx 12rpx 12rpx 12rpx;
background: #fff;
color: #333;
word-break: break-all;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
&.mine {
flex-direction: row-reverse;
.message-content {
margin-left: 0;
margin-right: 16rpx;
align-items: flex-end;
.message-bubble {
background: #0a84ff;
color: #fff;
border-radius: 12rpx 4rpx 12rpx 12rpx;
}
}
}
&.typing-indicator {
.typing-bubble {
padding: 18rpx 24rpx;
min-width: 80rpx;
}
.typing-dots {
display: flex;
align-items: center;
gap: 8rpx;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
@keyframes typing {
0%,
60%,
100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
.action-footer {
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
justify-content: space-between;
flex-shrink: 0;
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
&::after {
border: none;
}
&.yes-btn {
background: #0a84ff;
color: #fff;
}
&.no-btn {
background: #fff;
color: #333;
border: 2rpx solid #ddd;
}
}
}
.submitting-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.submitting-content {
background: #fff;
padding: 60rpx 80rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #0a84ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.submitting-text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.agreement-popup {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.agreement-header {
padding: 30rpx 50rpx 20rpx;
border-bottom: 1rpx solid #e5e7eb;
background: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.agreement-title {
font-size: 32rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.5;
}
.agreement-content {
flex: 1;
padding: 30rpx 50rpx 40rpx;
overflow-y: auto;
min-height: 0;
}
</style>

View File

@@ -5,7 +5,8 @@
// npm安装方式
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
// 消息通知组件
"^MessageNotification": "@/components/MessageNotification/MessageNotification.vue"
"^MessageNotification": "@/components/MessageNotification/MessageNotification.vue",
"^FollowUpDrugDrawer$": "@/subPackages/doctor/components/FollowUpDrugDrawer.vue"
},
"pages": [
{
@@ -185,6 +186,13 @@
"navigationBarTitleText": "商品详情",
"enablePullDownRefresh": false
}
},
{
"path": "follow-up/medication-info",
"style": {
"navigationBarTitleText": "用药信息",
"enablePullDownRefresh": false
}
}
]
},

View File

@@ -1,6 +1,20 @@
import {get, post, uploadFile} from './http'
const prefix = '/order';
/**
* 在线复诊:创建就诊信息(挂号前)
*/
export async function createRegisterInfoApi(params) {
return await post(`${prefix}/create-register-info`, params, 3)
}
/**
* 在线复诊:可选西药列表(双端上架)
*/
export async function getWesternDrugsForFollowUpApi(params) {
return await get(`${prefix}/western-drugs-for-follow-up`, params, 3)
}
/**
* 获取扫码诊所的商品详情
* @param params
@@ -26,6 +40,11 @@ export async function saveRegisterInfoApi(params) {
return await post(`${prefix}/save-register-info`, params, 3)
}
/** 诊所在线复诊:确认是否曾用过所选西药 */
export async function confirmFollowUpDrugUseApi(params) {
return await post(`${prefix}/confirm-follow-up-drug-use`, params, 3)
}
/**
* 查询订单的处方状态
* @param params

View File

@@ -102,6 +102,7 @@
@viewPrescription="viewPrescription"
@goToOrderMedicine="goToOrderMedicine"
@viewOrder="viewOrder"
@followUpDrugAnswered="onFollowUpDrugAnswered"
/>
</view>
@@ -403,6 +404,25 @@ export default {
return avatar;
}
},
onFollowUpDrugAnswered({ messageId, answerStatus }) {
const roomId = this.doctorUserInfo.room_id;
const idx = this.messages.findIndex((m) => m.id === messageId);
if (idx === -1) return;
const row = this.messages[idx];
try {
const o = typeof row.message_content === 'string' ? JSON.parse(row.message_content) : { ...row.message_content };
o.answer_status = answerStatus;
const updated = { ...row, message_content: JSON.stringify(o) };
this.$set(this.messages, idx, updated);
if (typeof ChatManager !== 'undefined' && roomId && ChatManager.roomMessages && ChatManager.roomMessages[roomId]) {
const ri = ChatManager.roomMessages[roomId].findIndex((m) => m.id === messageId);
if (ri !== -1) this.$set(ChatManager.roomMessages[roomId], ri, updated);
}
} catch (e) {
console.error('onFollowUpDrugAnswered', e);
}
},
// 加载助理配置信息
async loadAssistantConfig() {
@@ -1011,7 +1031,7 @@ export default {
getRegisterStatusText(status) { const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' }; return map[status] || '未知状态'; },
viewPrescription(id, orderNo) { uni.navigateTo({ url: "/subPackages/my/myrecord-detail?id=" + id + '&no=' + orderNo }); },
goToOrderMedicine(id, orderNo) { uni.navigateTo({ url: "/subPackages/my/my-drug/drug-info?id=" + id + '&order_type=prescription' }); },
viewRegister(id, orderNo) { uni.navigateTo({ url: "/subPackages/register/register-detail?id=" + id + '&no=' + orderNo }); },
viewRegister(id, orderNo) { uni.navigateTo({ url: `/subPackages/register/register-info?id=${id}&type=1` }); },
viewOrder(orderId, orderNo) { uni.navigateTo({ url: "/subPackages/my/my-drug/drug-info?id=" + orderId }); },
// 处理返回操作(自定义返回逻辑)

View File

@@ -12,7 +12,7 @@
>
<!-- 0. 文本 -->
<text v-if="msg.message_type === 0" class="text-content" user-select>{{ parsedContent }}</text>
<text v-if="msg.message_type === 0" class="text-content" user-select="true">{{ parsedContent }}</text>
<!-- 1. 图片 -->
<image v-else-if="msg.message_type === 1"
@@ -142,10 +142,45 @@
<view class="info-row"><text class="label">订单号</text><text class="value order-font">{{ parsedContent.order_no }}</text></view>
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (parsedContent.user_patient && parsedContent.user_patient.name) || '未知' }}</text></view>
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ parsedContent.price }}</text></view>
<view class="text-block" v-if="parsedContent.chief_complaint">
<text class="block-label">主诉</text>
<text class="block-content">{{ parsedContent.chief_complaint }}</text>
</view>
<view class="info-row" v-if="registerCardDrugNamesLine">
<text class="label">所选药品</text>
<text class="value">{{ registerCardDrugNamesLine }}</text>
</view>
<view class="info-row" v-if="parsedContent.number != null && parsedContent.number !== ''">
<text class="label">数量</text>
<text class="value"> {{ parsedContent.number }} </text>
</view>
</view>
<view class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></view>
</block>
<!-- 类型11: 复诊用药确认扩展 flow与药店购药 type11 互斥展示 -->
<block v-else-if="msg.message_type === 11 && parsedContent && parsedContent.flow === 'follow_up_drug'">
<view class="card-header bg-gray">
<view class="title-group"><u-icon name="question-circle-fill" size="16" color="#64748B"></u-icon><text class="card-title">医生助理</text></view>
</view>
<view class="card-body">
<view class="text-block" v-if="parsedContent.question">
<text class="block-content">{{ parsedContent.question }}</text>
</view>
<view class="info-row" v-for="(row, idx) in (parsedContent.drugs || [])" :key="'fd-' + idx">
<text class="label">{{ row.name || '药品' }}</text>
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
</view>
<view v-if="followUpAnsweredLabel" class="info-row">
<text class="label">您的选择</text>
<text class="value">{{ followUpAnsweredLabel }}</text>
</view>
<view v-if="parsedContent.answer_status === 'pending'" class="follow-up-pending-readonly">
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text>
</view>
</view>
</block>
<!-- 类型11: 患者就诊经历 -->
<block v-else-if="msg.message_type === 11">
<view class="card-header bg-gray">
@@ -231,7 +266,6 @@ import {
agreeTransferPrescriptionApi,
rejectTransferPrescriptionApi
} from '@/request/api/transferPrescription'
export default {
name: "MessageBubble",
components: {
@@ -279,6 +313,26 @@ export default {
}
}
return message_content;
},
/** 挂号卡片:多选西药名称串联,无则单药 drug.name */
registerCardDrugNamesLine() {
if (this.msg.message_type !== 10) return '';
const pc = this.parsedContent;
if (!pc || typeof pc !== 'object') return '';
const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d) => d && d.name).filter(Boolean).join('、');
}
if (pc.drug && pc.drug.name) return pc.drug.name;
return '';
},
followUpAnsweredLabel() {
const pc = this.parsedContent;
if (!pc || pc.flow !== 'follow_up_drug') return '';
const st = pc.answer_status;
if (st === 'yes' || st === '1') return '是,曾使用过';
if (st === 'no' || st === '0') return '否,未使用过';
return '';
}
},
methods: {
@@ -572,6 +626,18 @@ $success-color: #059669;
}
&.split { display: flex; justify-content: space-between; align-items: center; .link-btn { font-size: 24rpx; color: $primary; padding: 10rpx; } .action-btn { width: 160rpx; } }
}
.follow-up-pending-readonly {
margin-top: 12rpx;
padding: 16rpx 20rpx;
background: #f7f8fa;
border-radius: 12rpx;
.follow-up-pending-tip {
font-size: 24rpx;
color: #909399;
line-height: 1.5;
}
}
}
// ========== 原生风格服务通知 (Service Notify Card) ==========

View File

@@ -0,0 +1,300 @@
<template>
<view class="follow-up-drug-drawer">
<view class="trigger" @click="openPopup">
<view class="trigger-main">
<text v-if="!displaySummary" class="placeholder">请选择药品</text>
<text v-else class="summary">{{ displaySummary }}</text>
</view>
<u-icon name="arrow-right" color="#94A3B8" size="28"></u-icon>
</view>
<u-popup
v-model="popupVisible"
mode="bottom"
border-radius="24"
height="66%"
:safe-area-inset-bottom="true"
:closeable="true"
@close="onPopupClose"
>
<view class="drawer-inner">
<view class="drawer-title">选择相关西药</view>
<view v-if="!drugList.length" class="drawer-empty">暂无可选西药</view>
<scroll-view v-else scroll-y class="drawer-scroll">
<view
v-for="d in drugList"
:key="d.id"
class="drawer-row"
>
<view class="drug-check" :class="{ on: getQty(d.id) > 0 }" @click="toggleRow(d)">
<text v-if="getQty(d.id) > 0" class="check-mark"></text>
</view>
<image class="drug-thumb" :src="drugImage(d)" mode="aspectFill" @click="toggleRow(d)" />
<view class="drug-meta" @click="toggleRow(d)">
<text class="dn">{{ d.drug_name }}</text>
<text class="ds" v-if="d.specification">{{ d.specification }}</text>
</view>
<view class="drawer-row-actions">
<u-number-box
:value="getQty(d.id)"
:min="0"
:max="99"
:size="22"
:index="d.id"
@input="onQtyInput(d, $event)"
/>
</view>
</view>
</scroll-view>
<view class="drawer-footer safe-area-inset-bottom">
<view class="btn-text" @click="clearDraft">清空</view>
<view class="btn-primary" @click="confirm">确定</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
name: 'FollowUpDrugDrawer',
props: {
drugList: {
type: Array,
default: () => [],
},
value: {
type: Array,
default: () => [],
},
},
data() {
return {
popupVisible: false,
draftQty: {},
};
},
computed: {
displaySummary() {
const v = this.value || [];
if (!v.length) return '';
return v.map((i) => `${i.drug_name} * ${i.quantity}`).join('');
},
},
methods: {
drugImage(d) {
const u = d && d.image;
return u && String(u).trim() ? u : '/static/mine/avatar_1.png';
},
getQty(id) {
const q = this.draftQty[id];
return q != null && q > 0 ? Number(q) : 0;
},
initDraftFromValue() {
const next = {};
for (const it of this.value || []) {
const id = it.id;
const q = Number(it.quantity);
if (id != null && q > 0) {
next[id] = q;
}
}
this.draftQty = next;
},
openPopup() {
this.initDraftFromValue();
this.popupVisible = true;
},
onPopupClose() {
this.popupVisible = false;
},
toggleRow(d) {
const id = d.id;
if (this.getQty(id) > 0) {
this.$delete(this.draftQty, id);
} else {
this.$set(this.draftQty, id, 1);
}
},
onQtyInput(d, val) {
const n = Number(val);
if (!n || n < 1) {
this.$delete(this.draftQty, d.id);
} else {
this.$set(this.draftQty, d.id, Math.min(99, n));
}
},
clearDraft() {
this.draftQty = {};
},
confirm() {
const out = [];
for (const d of this.drugList) {
const q = this.getQty(d.id);
if (q > 0) {
out.push({
id: d.id,
drug_name: d.drug_name || '',
quantity: q,
});
}
}
this.$emit('input', out);
this.$emit('confirm', out);
this.popupVisible = false;
},
},
};
</script>
<style lang="scss" scoped>
.follow-up-drug-drawer {
width: 100%;
}
.trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
margin-top: 12rpx;
background: #f8fafc;
border-radius: 12rpx;
border: 1rpx solid #e2e8f0;
}
.trigger-main {
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
.placeholder {
font-size: 28rpx;
color: #94a3b8;
}
.summary {
font-size: 28rpx;
color: #0f172a;
line-height: 1.5;
}
.drawer-inner {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
padding: 24rpx 24rpx 0;
}
.drawer-title {
font-size: 32rpx;
font-weight: 600;
color: #0f172a;
text-align: center;
padding-bottom: 20rpx;
}
.drawer-empty {
flex: 1;
padding: 48rpx 0;
text-align: center;
color: #94a3b8;
font-size: 28rpx;
}
.drawer-scroll {
flex: 1;
max-height: 55vh;
min-height: 200rpx;
}
.drawer-row {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f1f5f9;
}
.drug-check {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #cbd5e1;
border-radius: 8rpx;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.on {
background: #4175ee;
border-color: #4175ee;
}
.check-mark {
color: #fff;
font-size: 24rpx;
line-height: 1;
}
}
.drug-thumb {
width: 96rpx;
height: 96rpx;
border-radius: 12rpx;
margin-right: 16rpx;
flex-shrink: 0;
background: #f1f5f9;
}
.drug-meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
.dn {
font-size: 28rpx;
color: #0f172a;
}
.ds {
font-size: 24rpx;
color: #64748b;
margin-top: 6rpx;
}
}
.drawer-row-actions {
flex-shrink: 0;
margin-left: 12rpx;
}
.drawer-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0 32rpx;
gap: 24rpx;
}
.btn-text {
flex: 1;
text-align: center;
font-size: 30rpx;
color: #64748b;
padding: 22rpx 0;
}
.btn-primary {
flex: 2;
text-align: center;
font-size: 30rpx;
color: #fff;
background: #4175ee;
border-radius: 12rpx;
padding: 22rpx 0;
}
</style>

View File

@@ -1,11 +1,18 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 标题 -->
<view class="title">
您需要为谁{{ registerType === '3'? '购药' : '挂号' }}
<text>请选择患者信息以便医生给出更准确的诊疗建议患者信息仅医生可见</text>
<!-- 优化1标题与挂号类型合并区域 -->
<view class="header-section">
<view class="title-row">
<text class="main-title">您需要为谁{{ registerType === '3' || registerType === '2' ? '复诊' : '挂号' }}</text>
<view class="register-type-tag">
<text class="type-label">{{ registerTypeLabel }}</text>
</view>
</view>
<text class="sub-title">请选择患者信息以便医生给出更准确的诊疗建议患者信息仅医生可见</text>
</view>
<!-- 就诊人 -->
<view class="detail">
<!-- relation -->
@@ -19,22 +26,50 @@
<image src="@/static/choose/choosed.png" mode=""></image>
</view>
<!-- 添加就诊人 -->
<view class="other" @click="goAdd(0)">
<text>+</text>
</view>
</view>
<!-- 挂号类型展示只读 -->
<view class="card" style="padding-bottom: 0">
<view class="card_title">
挂号类型
<!-- 优化2现代化的复诊表单 UI主诉 + 西药多选 -->
<view class="card follow-up-card modern-card" v-if="showFollowUpForm">
<!-- 患者主诉 -->
<view class="section-header">
<text class="title-text">患者主诉</text>
<text class="req-tag">* 必填</text>
</view>
<view class="card_names">
<view class="register-type-display">
<text class="type-label">{{ registerTypeLabel }}</text>
</view>
<view class="input-wrapper">
<textarea
class="chief-textarea"
v-model="chiefComplaint"
placeholder="请简要描述症状、持续时间等..."
placeholder-style="color:#94A3B8; font-size: 28rpx;"
maxlength="2000"
auto-height
/>
</view>
<view class="section-divider"></view>
<!-- 选择西药 -->
<view class="section-header">
<text class="title-text">选择相关西药</text>
<text class="req-tag">* 必选</text>
</view>
<view class="hint-box">
<text class="sub-hint">请至少选择一种药品仅展示本门店与总后台均已上架的西药数量仅作展示提交按药品种类关联</text>
</view>
<view class="drug-container">
<view v-if="followUpDrugList.length === 0" class="empty-drugs">暂无可选西药</view>
<FollowUpDrugDrawer
v-else
v-model="selectedFollowUpDrugs"
:drug-list="followUpDrugList"
/>
</view>
</view>
@@ -126,7 +161,7 @@ import {
registe
} from '@/request/api/api.js'
import {checkDev} from "@/utils/utils";
import {genOrderApi, saveRegisterInfoApi} from "@/request/api/order";
import {createRegisterInfoApi, getWesternDrugsForFollowUpApi, genOrderApi, saveRegisterInfoApi} from "@/request/api/order";
import request from "@/request/api/request";
export default {
data() {
@@ -182,6 +217,9 @@ export default {
rInfo: [], // 健康信息
registerType: "0", // 新增挂号类型0线下/1线上
delegateStoreId: '', // 委托诊所ID购药时使用
chiefComplaint: '',
followUpDrugList: [],
selectedFollowUpDrugs: [],
}
},
onLoad(e) {
@@ -196,7 +234,7 @@ export default {
this.rInfo = JSON.parse(decodeURIComponent(e.r_info))
} else {
// 如果已经是对象,直接使用
this.rInfo = e.r_info
this.rInfo = e.r_info
}
} catch (error) {
console.error('解析 r_info 失败:', error)
@@ -225,17 +263,112 @@ export default {
},
// 挂号类型标签
registerTypeLabel() {
// 与 Yii RegisterForm 一致2=诊所在线复诊3=药店购药/委托复诊
const typeMap = {
'0': '线下就诊',
'1': '在线问诊',
'2': '预约购药',
'2': '在线复诊',
'3': '在线复诊'
};
return typeMap[this.registerType] || '未知类型';
},
/** symptoms 等流程已 create 过就诊信息,不再展示主诉/选药 */
hasPrefilledFollowUpInfo() {
const r = this.rInfo;
return !!(r && typeof r === 'object' && !Array.isArray(r) && r.info_id);
},
/** 诊所在线复诊(2) 与 药店/症状复诊(3) 共用主诉与多选西药 */
showFollowUpForm() {
const t = this.registerType;
return (t === '2' || t === '3') && !this.hasPrefilledFollowUpInfo;
}
},
methods: {
checkDev,
resolveRegisterStoreId() {
return (this.registerType !== '0' && this.delegateStoreId)
? this.delegateStoreId
: (uni.getStorageSync('store_id') || '11001');
},
async loadFollowUpWesternDrugs() {
if (!this.showFollowUpForm) return;
const storeId = this.resolveRegisterStoreId();
try {
const res = await getWesternDrugsForFollowUpApi({ store_id: storeId });
const list = res.data?.result;
this.followUpDrugList = Array.isArray(list) ? list : [];
} catch (e) {
console.error('加载复诊西药列表失败', e);
this.followUpDrugList = [];
}
},
followUpDrugIdsForApi() {
const ids = [];
const seen = new Set();
for (const it of this.selectedFollowUpDrugs || []) {
const id = Number(it.id);
if (id > 0 && !seen.has(id)) {
seen.add(id);
ids.push(id);
}
}
return ids;
},
/** 在线复诊主诉 + 选药非空(无预填 info_id 时) */
validateFollowUpFields() {
const chief = String(this.chiefComplaint || '').trim();
if (!chief) return '请填写患者主诉';
if (this.followUpDrugIdsForApi().length === 0) return '请至少选择一种药品';
return '';
},
async prepareFollowUpRegisterInfo() {
let params = typeof this.rInfo === 'string' ? JSON.parse(this.rInfo) : { ...(this.rInfo || {}) };
if (params.info_id) {
return params;
}
const err = this.validateFollowUpFields();
if (err) {
throw new Error(err);
}
const chief = String(this.chiefComplaint || '').trim();
const storeId = this.resolveRegisterStoreId();
const westernDrugQty = {};
let qtySum = 0;
for (const d of this.selectedFollowUpDrugs || []) {
const id = Number(d.id);
const q = Math.min(99, Math.max(1, Number(d.quantity) || 1));
if (id > 0) {
westernDrugQty[String(id)] = q;
qtySum += q;
}
}
const createRes = await createRegisterInfoApi({
doctor_id: Number(this.Did),
drug_id: '',
has_visited: '',
has_used_drug: '',
illnessInfo: '',
type: 2,
number: qtySum >= 1 ? qtySum : 1,
store_id: Number(storeId) || storeId,
chief_complaint: chief,
drug_ids: this.followUpDrugIdsForApi(),
western_drug_qty: JSON.stringify(westernDrugQty),
});
const infoId = createRes.data?.result?.info_id;
if (!infoId) {
throw new Error('创建就诊信息失败');
}
const followUpDrugs = (this.selectedFollowUpDrugs || [])
.map((d) => ({
drug_id: Number(d.id),
name: d.drug_name || d.name || '',
quantity: Math.min(99, Math.max(1, Number(d.quantity) || 1)),
}))
.filter((d) => d.drug_id > 0);
const followUpNumber = followUpDrugs.reduce((s, r) => s + (Number(r.quantity) || 1), 0) || 1;
return { ...params, info_id: infoId, follow_up_drugs: followUpDrugs, follow_up_number: followUpNumber, number: followUpNumber };
},
// 处理挂号类型切换
handleTypeChange(type) {
this.registerType = type;
@@ -336,15 +469,28 @@ export default {
},
// 挂号
getAsk() {
async getAsk() {
uni.showLoading({
title: "加载中...",
mask: true
});
// 如果是购药类型(register_type=3)且有委托诊所ID则使用委托诊所ID否则使用当前门店ID
const storeId = (this.registerType != '0' && this.delegateStoreId)
? this.delegateStoreId
: (uni.getStorageSync('store_id') || '11001');
const storeId = this.resolveRegisterStoreId();
let followUpRInfo = this.rInfo;
if (this.showFollowUpForm) {
try {
followUpRInfo = await this.prepareFollowUpRegisterInfo();
this.rInfo = followUpRInfo;
} catch (e) {
uni.hideLoading();
this.$refs.uToast.show({
title: e.message || '提交失败',
type: 'default',
icon: false
});
return;
}
}
// const storeId = ((this.registerType === '3' || this.registerType == '2') && this.delegateStoreId)
// ? this.delegateStoreId
// : (uni.getStorageSync('store_id') || '11001');
@@ -362,10 +508,9 @@ export default {
if (res.data.errcode == 0) {
this.register_id = res.data.data.register_id
// if (this.registerType === "3" || this.registerType === "2") {
if (this.registerType === "3") {
// this.rInfo 在 onLoad 中已经解析为对象,直接使用
let params = typeof this.rInfo === 'string' ? JSON.parse(this.rInfo) : this.rInfo;
if (this.registerType === "3" || this.registerType === "2") {
// this.rInfo 在 onLoad 中已经解析为对象;纯复诊流程可能已在 getAsk 中写入 info_id
let params = typeof followUpRInfo === 'string' ? JSON.parse(followUpRInfo) : followUpRInfo;
// 如果有 info_id使用新接口更新关联否则用旧接口保存
if (params.info_id) {
@@ -378,7 +523,7 @@ export default {
}
}).then(() => {
// 跳转到挂号页面,让用户确认挂号并支付
const rInfoStr = typeof this.rInfo === 'string' ? this.rInfo : JSON.stringify(this.rInfo);
const rInfoStr = typeof followUpRInfo === 'string' ? followUpRInfo : JSON.stringify(followUpRInfo);
uni.navigateTo({
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${encodeURIComponent(rInfoStr)}`
});
@@ -386,7 +531,7 @@ export default {
console.error('更新就诊信息关联失败:', error);
// 即使更新失败,也跳转到挂号页面
uni.navigateTo({
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${encodeURIComponent(typeof this.rInfo === 'string' ? this.rInfo : JSON.stringify(this.rInfo))}`
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${encodeURIComponent(typeof followUpRInfo === 'string' ? followUpRInfo : JSON.stringify(followUpRInfo))}`
});
});
} else {
@@ -403,13 +548,13 @@ export default {
store_id: uni.getStorageSync('store_id') || '11001',
}).then(() => {
// 跳转到挂号页面,让用户确认挂号并支付
const rInfoStr = typeof this.rInfo === 'string' ? this.rInfo : JSON.stringify(this.rInfo);
const rInfoStr = typeof followUpRInfo === 'string' ? followUpRInfo : JSON.stringify(followUpRInfo);
uni.navigateTo({
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${encodeURIComponent(rInfoStr)}`
});
}).catch((error) => {
console.error('保存患者就诊信息失败:', error);
const rInfoStr = typeof this.rInfo === 'string' ? this.rInfo : JSON.stringify(this.rInfo);
const rInfoStr = typeof followUpRInfo === 'string' ? followUpRInfo : JSON.stringify(followUpRInfo);
uni.navigateTo({
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${encodeURIComponent(rInfoStr)}`
});
@@ -419,7 +564,7 @@ export default {
}
uni.setStorageSync('register_id', this.register_id)
const rInfoEncoded = encodeURIComponent(typeof this.rInfo === 'string' ? this.rInfo : JSON.stringify(this.rInfo || {}));
const rInfoEncoded = encodeURIComponent(typeof followUpRInfo === 'string' ? followUpRInfo : JSON.stringify(followUpRInfo || {}));
if (res.data.data.isHas == 0) {
uni.navigateTo({
url: `/subPackages/register/register?id=${this.register_id}&doctor_id=${this.Did}&r_info=${rInfoEncoded}`,
@@ -481,6 +626,19 @@ export default {
},
//订阅消息
subScriptionMessage() {
// 优化:在此处前置执行表单非空校验,阻止无效信息的订阅授权弹窗
if (this.showFollowUpForm) {
const err = this.validateFollowUpFields();
if (err) {
this.$refs.uToast.show({
title: err,
type: 'default',
icon: false
});
return;
}
}
uni.requestSubscribeMessage({
tmplIds: ['O5P8ZZB6T01V5Z66tkbhmzlobSaSRoEfdoCLATuP2Mw',
'ddT3v-m_Sr4kXwGCBLf0O4P89mOS5q10LFMZeEuQm10'
@@ -494,9 +652,11 @@ export default {
},
mounted() {
this.getInfolist()
this.loadFollowUpWesternDrugs()
},
onShow() {
this.getInfolist()
this.loadFollowUpWesternDrugs()
}
}
</script>
@@ -506,25 +666,53 @@ export default {
width: 750rpx;
max-height: 100%;
min-height: 100vh;
box-sizing: border-box;
background-color: #F8FAFC; /* 增加浅灰色底层背景,让白色卡片更具呼吸感 */
padding-bottom: 200rpx;
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
.title {
width: 686rpx;
height: 120rpx;
font-size: 36rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #1E293B;
margin: 32rpx 32rpx 0 32rpx;
/* 顶部重构区域 */
.header-section {
padding: 32rpx 32rpx 0;
text {
display: inline-block;
.title-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.main-title {
font-size: 38rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 600;
color: #0F172A;
margin-right: 16rpx;
}
.register-type-tag {
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
padding: 4rpx 16rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(65, 117, 238, 0.2);
.type-label {
color: #FFFFFF;
font-size: 24rpx;
font-weight: 500;
}
}
}
.sub-title {
display: block;
font-size: 24rpx;
font-weight: 400;
color: #6C7380;
margin-top: 16rpx;
color: #64748B;
line-height: 1.5;
}
}
/* 原有就诊人卡片区 */
.detail {
width: 710rpx;
height: 280rpx;
@@ -540,6 +728,7 @@ export default {
margin: 0 16rpx 16rpx 0;
border: 2rpx solid #CBD5E1;
position: relative;
background: #FFFFFF;
.name {
margin: 16rpx 0 8rpx 32rpx;
@@ -576,14 +765,11 @@ export default {
bottom: -1rpx;
display: none;
}
}
.active {
border: 2rpx solid #536DFE;
background: rgba(83, 109, 254, 0.04);
image {
display: block;
}
@@ -595,36 +781,34 @@ export default {
line-height: 104rpx;
text-align: center;
border-radius: 8rpx;
border: 2rpx solid #CBD5E1; /* 修复移除多余的b字符 */
border: 2rpx dashed #CBD5E1;
background: #FFFFFF;
margin-top: -10rpx;
text {
font-size: 68rpx;
color: #CBD5E1;
color: #94A3B8;
}
}
}
/* 基础卡片样式 */
.card {
width: 710rpx;
background: #FFFFFF;
border-radius: 8rpx;
border-radius: 16rpx; /* 增加圆角,提升现代感 */
margin: 20rpx auto 40rpx;
padding: 0 32rpx 190rpx;
padding: 0 32rpx 40rpx;
font-family: PingFang SC-Regular, PingFang SC;
box-shadow: 0 4rpx 24rpx rgba(15, 23, 42, 0.02); /* 添加极弱阴影 */
.card_title {
height: 114rpx;
line-height: 114rpx;
font-size: 36rpx;
font-weight: 500;
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
border-bottom: 1rpx solid #E2E8F0;
}
.card_name:last-child {
border-bottom: 0;
border-bottom: 1rpx solid #F1F5F9;
}
.card_names {
@@ -634,38 +818,19 @@ export default {
align-items: center;
justify-content: space-between;
font-size: 30rpx;
border-bottom: 1rpx solid #E2E8F0;
border-bottom: 1rpx solid #F1F5F9;
text {
font-weight: 400;
color: #1E293B;
}
::v-deep .u-input {
width: 380rpx;
}
.register-type-display {
display: flex;
align-items: center;
.type-label {
display: inline-block;
padding: 8rpx 24rpx;
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 500;
border-radius: 8rpx;
}
}
}
.card_history {
width: 646rpx;
padding: 34rpx 0 32rpx 0;
font-size: 30rpx;
border-bottom: 1rpx solid #E2E8F0;
border-bottom: 1rpx solid #F1F5F9;
.name {
display: flex;
@@ -703,7 +868,6 @@ export default {
border-radius: 50%;
}
}
}
.tabs {
@@ -725,152 +889,146 @@ export default {
padding: 4rpx 8rpx;
}
}
}
.card_history:last-child {
border-bottom: 0;
}
.card_relation {
width: 646rpx;
height: 106rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
border-bottom: 1rpx solid #E2E8F0;
text {
font-weight: 400;
color: #1E293B;
}
.u-icon {
vertical-align: middle;
margin-top: 2rpx;
}
::v-deep .u-input {
margin-left: 130rpx;
}
::v-deep .u-input__right-icon ::before {
content: ""
}
.choose {
font-weight: 400;
color: #94A3B8;
}
}
.card_telephone {
width: 646rpx;
height: 106rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
text {
font-weight: 400;
color: #1E293B;
}
}
}
.floor {
z-index: 99;
/* 现代化的复诊卡片重构 */
.modern-card {
padding: 32rpx;
border: none;
.floor_item {
padding: 32rpx;
font-size: 32rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #0F172A;
position: relative;
padding-left: 18rpx;
/* 使用左侧主题色强调线 */
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 28rpx;
background: #4175EE;
border-radius: 4rpx;
}
}
.req-tag {
color: #EF4444;
font-size: 24rpx;
background: rgba(239, 68, 68, 0.08);
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
}
.section-divider {
height: 1rpx;
background-color: #F1F5F9;
margin: 40rpx 0 32rpx;
}
.input-wrapper {
background: #F8FAFC;
border-radius: 12rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
&:focus-within {
border-color: #CBD5E1;
background: #FFFFFF;
}
.chief-textarea {
width: 100%;
min-height: 160rpx;
padding: 24rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #334155;
line-height: 1.5;
background: transparent;
}
}
.hint-box {
background: #F1F5F9;
border-radius: 8rpx;
padding: 16rpx 24rpx;
margin-bottom: 24rpx;
.sub-hint {
font-size: 24rpx;
color: #64748B;
line-height: 1.6;
display: block;
}
}
.empty-drugs {
padding: 32rpx 0;
text-align: center;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
.title {
height: 66rpx;
width: 620rpx;
font-size: 36rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #1E293B;
}
.u_btn {
margin: 40rpx 0;
}
.u_inp {
margin: 40rpx 0;
::v-deep .u-input {
background: #F1F5F9;
}
}
.info {
width: 620rpx;
margin: 40rpx 0;
.addinfo {
display: flex;
justify-content: space-between;
align-items: center;
width: 620rpx;
.words {
height: 42rpx;
font-size: 30rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #1E293B;
margin: 20rpx 0 10rpx;
}
.radios {}
}
}
color: #94A3B8;
font-size: 28rpx;
background: #F8FAFC;
border-radius: 12rpx;
}
}
/* 底部悬浮按钮 */
.footer {
width: 750rpx;
height: 104rpx;
min-height: 104rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2rpx solid #F2F3F5;
border-top: 2rpx solid #F1F5F9;
position: fixed;
left: 0;
bottom: env(safe-area-inset-bottom);
background-color: #fff;
right: 0;
bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background-color: #FFFFFF;
box-sizing: border-box;
z-index: 100;
.infos {
width: 686rpx;
height: 90rpx;
line-height: 90rpx;
display: flex;
justify-content: space-around;
justify-content: center;
align-items: center;
margin: 16rpx 0;
font-size: 32rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
text-align: center;
height: 86rpx;
background: #4175EE;
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
box-shadow: 0 8rpx 20rpx rgba(65, 117, 238, 0.3);
border-radius: 198rpx;
color: #FFFFFF;
margin-left: 32rpx;
transition: opacity 0.3s;
&:active {
opacity: 0.8;
}
}
}
}

View File

@@ -0,0 +1,863 @@
<template>
<view v-if="mode === 'clinic_post_register'">
<AssistantConsultationShell
header-title="复诊用药确认"
:show-notice="showPostRegisterNotice"
:parsed-notice-parts="parsedPostRegisterNoticeParts"
:show-agreement.sync="showPostRegisterAgreement"
:agreement-content="postRegisterAgreementContent"
:current-agreement-name="postRegisterCurrentAgreementName"
:displayed-messages="postRegisterDisplayedMessages"
:assistant-avatar="postRegAssistantAvatar"
:assistant-name="postRegAssistantName"
user-avatar="/static/images/user-avatar.png"
:prescription-info="postRegisterPrescriptionInfo"
:is-typing="postRegisterIsTyping"
:scroll-into-view="postRegisterScrollIntoView"
:show-question-buttons="postRegisterStep === 'await_answer'"
:current-question="postRegisterCurrentQuestion"
:current-question-answers="postRegisterQuestionAnswers"
:is-submitting="postRegisterSubmitting"
:show-waiting="postRegisterWaiting"
@back="handlePostRegisterBack"
@answer="onPostRegisterShellAnswer"
@view-agreement="viewPostRegisterAgreement"
/>
<u-toast ref="uToast" />
</view>
<view v-else class="page">
<view class="page-inner safe-area-bottom">
<view class="page-title">{{ pageTitle }}</view>
<!-- 药店商品摘要 -->
<block v-if="mode === 'pharmacy'">
<view class="product-info-row">
<image class="p-img" :src="productDetail.image" mode="aspectFill"></image>
<view class="p-info">
<view class="p-name">{{ productDetail.drug_name }}</view>
<view class="p-spec">{{ productDetail.specification || '暂无规格' }}</view>
<view class="p-price">¥{{ productPrice }}</view>
</view>
</view>
<view class="quantity-row">
<text class="label">购买数量</text>
<u-number-box v-model="prescriptionQuantity" :min="1" :max="999" integer :step="1"></u-number-box>
</view>
<u-form :model="prescriptionFormData" ref="prescriptionForm">
<u-form-item label="是否就诊过" prop="hasVisited" label-width="200">
<u-radio-group v-model="prescriptionFormData.hasVisited">
<u-radio name="1" active-color="#2B85E4" style="margin-right: 40rpx;"></u-radio>
<u-radio name="0" active-color="#2B85E4"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="是否用过此药" prop="hasUsedDrug" label-width="200">
<u-radio-group v-model="prescriptionFormData.hasUsedDrug">
<u-radio name="1" active-color="#2B85E4" style="margin-right: 40rpx;"></u-radio>
<u-radio name="0" active-color="#2B85E4"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="症状" prop="illnessInfo" label-width="200">
<u-input
v-model="prescriptionFormData.illnessInfo"
type="select"
placeholder="请选择症状"
@click="showIllnessPicker = true"
disabled
/>
</u-form-item>
</u-form>
</block>
<!-- 诊所复诊用药确认仅聊天内入口 -->
<block v-else-if="mode === 'clinic_followup'">
<view v-if="clinicQuestion" class="clinic-question">{{ clinicQuestion }}</view>
<view v-if="clinicDrugs.length" class="clinic-drugs">
<view class="sub-title">涉及药品</view>
<view v-for="(row, idx) in clinicDrugs" :key="'d-' + idx" class="drug-line">
<text class="n">{{ row.name || '药品' }}</text>
<text class="q">×{{ row.quantity != null ? row.quantity : 1 }}</text>
</view>
</view>
<u-form :model="clinicForm" ref="clinicFormRef">
<u-form-item label="是否用过此类药" prop="hasUsedDrug" label-width="220">
<u-radio-group v-model="clinicForm.hasUsedDrug">
<u-radio name="1" active-color="#2B85E4" style="margin-right: 40rpx;"></u-radio>
<u-radio name="0" active-color="#2B85E4"></u-radio>
</u-radio-group>
</u-form-item>
</u-form>
</block>
<view class="popup-actions">
<button class="btn-cancel" @click="goBack">取消</button>
<button class="btn-confirm" @click="onSubmit">{{ submitButtonText }}</button>
</view>
</view>
<u-picker
v-model="showIllnessPicker"
mode="selector"
:range="illnessOptions"
range-key="label"
@confirm="onIllnessConfirm"
:mask-close-able="true"
confirm-color="#2B85E4"
></u-picker>
<u-toast ref="uToast" />
</view>
</template>
<script>
import { getStoreDrugDetailApi, checkOnlineConsultationConfigApi } from '@/request/api/product';
import request from '@/request/api/request';
import { confirmFollowUpDrugUseApi } from '@/request/api/order';
import {
CLINIC_POST_REGISTER_STORAGE_KEY,
markFollowUpAssistSent,
sendFollowUpDrugAssistantMessage,
wasFollowUpAssistSent,
} from '@/utils/clinicFollowUpAssistantIm';
import AssistantConsultationShell from '@/components/AssistantConsultationShell.vue';
import {
getTransferAssistantConfigApi,
getTransferConsultationAgreementInfoApi,
getAgreementDetailApi,
} from '@/request/api/transferPrescription.js';
const FOLLOWUP_PAYLOAD_KEY = '__medication_followup_payload';
export default {
components: { AssistantConsultationShell },
data() {
return {
mode: 'pharmacy',
productDetail: {
id: '',
type: 2,
drug_name: '',
image: '',
price: 0,
specification: '',
indications_array: [],
},
prescriptionQuantity: 1,
prescriptionFormData: {
hasVisited: '1',
hasUsedDrug: '1',
illnessInfo: '',
},
illnessOptions: [],
showIllnessPicker: false,
delegateStoreId: '',
doctorIdFromQuery: '',
submitted: false,
// clinic
registerInfoId: '',
messageId: '',
clinicQuestion: '',
clinicDrugs: [],
clinicForm: {
hasUsedDrug: '1',
},
postRegisterRoomId: '',
postRegisterDoctorId: '',
postRegisterPatientId: '',
postRegisterDoctorPrefixed: '',
postRegisterRegisterId: '',
// clinic_post_register与转诊咨询壳共用 UI
postRegisterQuestionList: [],
postRegisterQuestionIndex: -1,
postRegisterUserAnswers: [],
postRegisterReplyDelay: 0.65,
postRegisterIsTyping: false,
postRegisterShowWelcome: true,
/** idle | await_answer | typing_after | submitting */
postRegisterStep: 'idle',
postRegisterScrollIntoView: '',
postRegisterSubmitting: false,
postRegisterWaiting: false,
postRegAssistantAvatar: '/static/images/assistant-avatar.png',
postRegAssistantName: '医生助理',
showPostRegisterNotice: true,
parsedPostRegisterNoticeParts: [],
postRegisterAgreements: [],
postRegisterNoticeText: '',
showPostRegisterAgreement: false,
postRegisterAgreementContent: '',
postRegisterCurrentAgreementName: '',
};
},
computed: {
pageTitle() {
if (this.mode === 'clinic_followup') return '请填写用药信息';
return '请填写用药信息';
},
submitButtonText() {
return '提交';
},
postRegisterPrescriptionInfo() {
if (!this.clinicDrugs || !this.clinicDrugs.length) return '相关药品';
return this.clinicDrugs
.map((row) => `${row.name || '药品'}×${row.quantity != null ? row.quantity : 1}`)
.join('、');
},
postRegisterDisplayedMessages() {
const messages = [];
if (this.postRegisterShowWelcome) {
messages.push({ type: 'welcome', id: 'welcome-msg', timestamp: 0 });
}
const qs = this.postRegisterQuestionList;
const idx = this.postRegisterQuestionIndex;
for (let i = 0; i <= idx && i < qs.length; i++) {
const q = qs[i];
messages.push({
type: 'question',
id: `question-${q.id}`,
questionId: q.id,
content: q.question,
timestamp: i * 2 + 1,
});
if (i < this.postRegisterUserAnswers.length) {
messages.push({
type: 'answer',
id: `answer-${i}`,
content: this.postRegisterUserAnswers[i],
timestamp: i * 2 + 2,
});
}
}
if (this.postRegisterWaiting) {
messages.push({
type: 'waiting',
id: 'waiting-msg',
timestamp: messages.length,
});
}
return messages;
},
postRegisterCurrentQuestion() {
if (this.postRegisterStep !== 'await_answer') return null;
if (!this.postRegisterQuestionList.length) return null;
return this.postRegisterQuestionList[0];
},
postRegisterQuestionAnswers() {
return ['是', '否'];
},
productPrice() {
if (this.productDetail.drug_store_drug && this.productDetail.drug_store_drug.price) {
return parseFloat(this.productDetail.drug_store_drug.price);
}
return parseFloat(this.productDetail.price || 0);
},
},
onLoad(options) {
if (options.mode === 'clinic_post_register') {
this.mode = 'clinic_post_register';
try {
const raw = uni.getStorageSync(CLINIC_POST_REGISTER_STORAGE_KEY);
if (raw) {
const o = typeof raw === 'string' ? JSON.parse(raw) : raw;
this.registerInfoId = String(o.register_info_id || '');
this.clinicQuestion = o.question || '您是否曾使用过此类药品?';
this.clinicDrugs = Array.isArray(o.drugs) ? o.drugs : [];
this.postRegisterRoomId = o.room_id || '';
this.postRegisterDoctorId = o.doctor_id != null ? String(o.doctor_id) : '';
this.postRegisterPatientId = String(o.patient_id || '');
this.postRegisterDoctorPrefixed = o.doctor_prefixed || '';
this.postRegisterRegisterId = String(o.register_id || '');
}
} catch (e) {
console.error('medication-info clinic_post_register', e);
}
uni.removeStorageSync(CLINIC_POST_REGISTER_STORAGE_KEY);
if (!this.registerInfoId || !this.postRegisterRoomId || !this.postRegisterDoctorId || !this.postRegisterRegisterId) {
uni.showToast({ title: '参数无效', icon: 'none' });
} else {
this.postRegisterQuestionList = [
{
id: 'clinic-post-reg-1',
question: this.clinicQuestion || '您是否曾使用过此类药品?',
answers: ['是', '否'],
},
];
this.loadPostRegisterAssistantConfig();
this.loadPostRegisterAgreementInfo();
this.$nextTick(() => this.beginPostRegisterAssistantFlow());
}
return;
}
this.mode = options.mode === 'clinic_followup' ? 'clinic_followup' : 'pharmacy';
if (this.mode === 'clinic_followup') {
this.messageId = options.message_id || '';
try {
const raw = uni.getStorageSync(FOLLOWUP_PAYLOAD_KEY);
if (raw) {
const o = typeof raw === 'string' ? JSON.parse(raw) : raw;
this.registerInfoId = String(o.register_info_id || '');
this.clinicQuestion = o.question || '';
this.clinicDrugs = Array.isArray(o.drugs) ? o.drugs : [];
}
} catch (e) {
console.error('medication-info clinic payload', e);
}
uni.removeStorageSync(FOLLOWUP_PAYLOAD_KEY);
if (!this.registerInfoId) {
uni.showToast({ title: '参数无效', icon: 'none' });
}
return;
}
this.productDetail.id = options.id || '';
this.doctorIdFromQuery = options.doctor_id || '';
this.delegateStoreId = options.delegate_store_id || '';
if (options.drug_name) {
try {
this.productDetail.drug_name = decodeURIComponent(options.drug_name);
} catch {
this.productDetail.drug_name = options.drug_name;
}
}
if (options.image) {
try {
this.productDetail.image = decodeURIComponent(options.image);
} catch {
this.productDetail.image = options.image;
}
}
if (options.price) this.productDetail.price = parseFloat(options.price);
if (options.type) this.productDetail.type = parseInt(options.type, 10) || 2;
if (this.productDetail.id) {
this.getDetail();
} else {
uni.showToast({ title: '缺少商品信息', icon: 'none' });
}
},
methods: {
postRegisterScrollToBottom() {
this.postRegisterScrollIntoView = 'bottom-anchor';
setTimeout(() => {
this.postRegisterScrollIntoView = '';
}, 120);
},
beginPostRegisterAssistantFlow() {
this.postRegisterStep = 'idle';
this.postRegisterShowWelcome = true;
this.postRegisterUserAnswers = [];
this.postRegisterWaiting = false;
this.postRegisterQuestionIndex = -1;
this.postRegisterIsTyping = true;
const delayMs = Math.max(200, (this.postRegisterReplyDelay || 0.65) * 1000);
setTimeout(() => {
this.postRegisterIsTyping = false;
this.postRegisterQuestionIndex = 0;
this.postRegisterStep = 'await_answer';
this.$nextTick(() => this.postRegisterScrollToBottom());
}, delayMs);
},
onPostRegisterShellAnswer(answer) {
if (this.postRegisterStep !== 'await_answer' || this.submitted) return;
this.postRegisterUserAnswers = [answer];
this.clinicForm.hasUsedDrug = answer === '是' ? '1' : '0';
this.postRegisterStep = 'typing_after';
this.postRegisterIsTyping = true;
this.$nextTick(() => this.postRegisterScrollToBottom());
const typingMs = Math.max(200, (this.postRegisterReplyDelay || 0.65) * 1000);
setTimeout(() => {
this.postRegisterIsTyping = false;
this.$nextTick(() => this.postRegisterScrollToBottom());
// 与转诊一致:最后一题答完后短延迟再自动提交
setTimeout(() => {
this.submitClinicPostRegister();
}, 500);
}, typingMs);
},
resetPostRegisterAfterSubmitFailure() {
this.submitted = false;
this.postRegisterWaiting = false;
this.postRegisterStep = 'await_answer';
if (this.clinicForm.hasUsedDrug === '0' || this.clinicForm.hasUsedDrug === '1') {
this.postRegisterUserAnswers = [this.clinicForm.hasUsedDrug === '1' ? '是' : '否'];
} else {
this.postRegisterUserAnswers = [];
}
this.$nextTick(() => this.postRegisterScrollToBottom());
},
async loadPostRegisterAssistantConfig() {
try {
const res = await getTransferAssistantConfigApi();
if (res.data && res.data.code === 0) {
const config = res.data.result || {};
this.postRegAssistantName = config.name || '医生助理';
this.postRegAssistantAvatar = config.avatar || '/static/images/assistant-avatar.png';
const d = parseFloat(config.reply_delay);
if (!Number.isNaN(d) && d > 0) {
this.postRegisterReplyDelay = d;
}
}
} catch (e) {
console.warn('loadPostRegisterAssistantConfig', e);
}
},
async loadPostRegisterAgreementInfo() {
try {
const res = await getTransferConsultationAgreementInfoApi();
if (res.data && res.data.code === 0) {
const result = res.data.result || {};
this.postRegisterNoticeText = result.notice_text || '';
this.postRegisterAgreements = result.agreements || [];
this.parsePostRegisterNoticeText();
this.showPostRegisterNotice =
this.parsedPostRegisterNoticeParts.length > 0 || !!this.postRegisterNoticeText;
}
} catch (e) {
console.warn('loadPostRegisterAgreementInfo', e);
this.postRegisterNoticeText =
'根据国家互联网医院管理办法要求,平台仅为复诊患者提供服务。为了保障您的用药安全,请根据真实情况回答,并请仔细阅读《互联网医疗风险告知及知情同意书》,继续咨询即表示您已知悉相关规则与风险并同意相关条款。';
this.parsePostRegisterNoticeText();
this.showPostRegisterNotice =
this.parsedPostRegisterNoticeParts.length > 0 || !!this.postRegisterNoticeText;
}
},
parsePostRegisterNoticeText() {
if (!this.postRegisterNoticeText) {
this.parsedPostRegisterNoticeParts = [];
return;
}
const parts = [];
const regex = /《([^》]+)》/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(this.postRegisterNoticeText)) !== null) {
if (match.index > lastIndex) {
parts.push({
type: 'text',
text: this.postRegisterNoticeText.substring(lastIndex, match.index),
});
}
const agreementName = match[1];
const agreement = this.postRegisterAgreements.find(
(ag) => ag.name === agreementName || (ag.name && ag.name.includes(agreementName)),
);
if (agreement) {
parts.push({
type: 'agreement',
text: agreementName,
agreementId: agreement.id,
});
} else {
parts.push({ type: 'text', text: match[0] });
}
lastIndex = regex.lastIndex;
}
if (lastIndex < this.postRegisterNoticeText.length) {
parts.push({
type: 'text',
text: this.postRegisterNoticeText.substring(lastIndex),
});
}
this.parsedPostRegisterNoticeParts = parts;
},
async viewPostRegisterAgreement(agreementId) {
if (!agreementId) {
uni.showToast({ title: '协议ID不能为空', icon: 'none' });
return;
}
const agreement = this.postRegisterAgreements.find((ag) => ag.id === agreementId);
this.postRegisterCurrentAgreementName = agreement ? agreement.name : '协议详情';
this.showPostRegisterAgreement = true;
try {
const res = await getAgreementDetailApi(agreementId);
if (res.data && res.data.code === 0) {
const result = res.data.result || {};
this.postRegisterAgreementContent = result.content || '';
if (result.name) {
this.postRegisterCurrentAgreementName = result.name;
}
} else {
throw new Error((res.data && res.data.message) || '获取协议详情失败');
}
} catch (error) {
console.error('viewPostRegisterAgreement', error);
uni.showToast({
title: (error && error.message) || '获取协议详情失败',
icon: 'none',
});
this.showPostRegisterAgreement = false;
}
},
handlePostRegisterBack() {
uni.showModal({
title: '提示',
content: '确定返回首页吗?未完成确认将无法同步给医生。',
success: (res) => {
if (res.confirm) {
uni.switchTab({ url: '/pages/home/home' });
}
},
});
},
goBack() {
if (this.mode === 'clinic_post_register') {
uni.switchTab({ url: '/pages/home/home' });
return;
}
uni.navigateBack({ delta: 1 });
},
getDetail() {
getStoreDrugDetailApi({ id: this.productDetail.id }).then((res) => {
if (res.data.code === 0) {
const data = res.data.result;
Object.assign(this.productDetail, data);
if (data.indications_array && data.indications_array.length > 0) {
this.illnessOptions = data.indications_array.map((item) => ({
value: item.label || item.value,
label: item.label || item.value,
}));
}
}
});
},
onIllnessConfirm(e) {
const index = e[0];
this.prescriptionFormData.illnessInfo = this.illnessOptions[index].value;
this.showIllnessPicker = false;
},
async getDefaultDoctorId() {
try {
const storeId = uni.getStorageSync('store_id');
if (!storeId) {
return { data: { result: { doctor_id: null } } };
}
const res = await request('/xkApi/online-consultation/get-default-doctor-id', {
method: 'GET',
data: { store_id: storeId },
}, 0);
return res;
} catch (error) {
console.error('获取默认医生ID失败:', error);
return { data: { result: { doctor_id: null } } };
}
},
onSubmit() {
if (this.mode === 'clinic_followup') {
this.submitClinic();
} else {
this.submitPharmacy();
}
},
submitClinicPostRegister() {
if (!this.registerInfoId || !this.postRegisterRoomId || !this.postRegisterDoctorId || !this.postRegisterRegisterId) {
uni.showToast({ title: '参数缺失', icon: 'none' });
return;
}
if (this.clinicForm.hasUsedDrug !== '0' && this.clinicForm.hasUsedDrug !== '1') {
uni.showToast({ title: '请先选择是否用过药', icon: 'none' });
return;
}
if (this.submitted) return;
this.submitted = true;
this.postRegisterSubmitting = true;
this.postRegisterWaiting = true;
this.postRegisterStep = 'submitting';
this.$nextTick(() => this.postRegisterScrollToBottom());
const val = this.clinicForm.hasUsedDrug;
const answerStatus = val === '1' ? 'yes' : 'no';
confirmFollowUpDrugUseApi({
register_info_id: this.registerInfoId,
has_used_drug: val,
})
.then(async (res) => {
const d = res.data || {};
const ok = d.errcode === 0 || d.code === 0 || d.status === true;
if (!ok) {
this.resetPostRegisterAfterSubmitFailure();
uni.showToast({ title: d.msg || d.message || '提交失败', icon: 'none' });
return;
}
try {
if (!wasFollowUpAssistSent(this.postRegisterRegisterId)) {
await sendFollowUpDrugAssistantMessage({
roomId: this.postRegisterRoomId,
doctorId: this.postRegisterDoctorId,
registerId: this.postRegisterRegisterId,
registerInfoId: this.registerInfoId,
drugsPayload: this.clinicDrugs,
question: this.clinicQuestion,
answerStatus,
});
markFollowUpAssistSent(this.postRegisterRegisterId);
}
} catch (e) {
console.error('sendFollowUpDrugAssistantMessage', e);
this.resetPostRegisterAfterSubmitFailure();
uni.showToast({ title: '发送消息失败,请重试', icon: 'none' });
return;
}
const encUser = encodeURIComponent(
this.postRegisterDoctorPrefixed || `doctor-${this.postRegisterDoctorId}`,
);
const chatUrl = `/subPackages/chat/chat?user_id=${encUser}&room_id=${this.postRegisterRoomId}&register_id=${this.postRegisterRegisterId}`;
uni.redirectTo({ url: chatUrl });
})
.catch(() => {
this.resetPostRegisterAfterSubmitFailure();
uni.showToast({ title: '提交失败', icon: 'none' });
})
.finally(() => {
this.postRegisterSubmitting = false;
this.postRegisterWaiting = false;
});
},
submitClinic() {
if (!this.registerInfoId) {
uni.showToast({ title: '缺少登记信息', icon: 'none' });
return;
}
if (this.clinicForm.hasUsedDrug !== '0' && this.clinicForm.hasUsedDrug !== '1') {
uni.showToast({ title: '请选择是否用过药', icon: 'none' });
return;
}
uni.showLoading({ title: '提交中', mask: true });
confirmFollowUpDrugUseApi({
register_info_id: this.registerInfoId,
has_used_drug: this.clinicForm.hasUsedDrug,
})
.then((res) => {
const d = res.data || {};
const ok = d.errcode === 0 || d.code === 0 || d.status === true;
if (ok) {
const val = this.clinicForm.hasUsedDrug;
const answerStatus = val === '1' ? 'yes' : 'no';
try {
const ec = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (ec && ec.emit) {
ec.emit('followUpDrugCompleted', {
messageId: this.messageId,
answerStatus,
raw: val,
});
}
} catch (e) {
console.warn('eventChannel', e);
}
uni.showToast({ title: '已记录', icon: 'success' });
setTimeout(() => uni.navigateBack({ delta: 1 }), 400);
} else {
uni.showToast({ title: d.msg || d.message || '提交失败', icon: 'none' });
}
})
.catch(() => {
uni.showToast({ title: '提交失败', icon: 'none' });
})
.finally(() => {
uni.hideLoading();
});
},
async submitPharmacy() {
if (!this.prescriptionFormData.hasVisited || !this.prescriptionFormData.hasUsedDrug || !this.prescriptionFormData.illnessInfo) {
uni.showToast({ title: '请填写完整信息', icon: 'none' });
return;
}
if (this.submitted) return;
const formData = { ...this.prescriptionFormData };
const quantity = this.prescriptionQuantity;
this.submitted = true;
try {
const storeId = uni.getStorageSync('store_id');
let delegateStoreId = this.delegateStoreId;
let doctorId = this.doctorIdFromQuery || null;
if (!delegateStoreId) {
const configRes = await checkOnlineConsultationConfigApi({ store_id: storeId });
if (!configRes.data?.result?.can_use) {
uni.showToast({
title: configRes.data?.result?.message || '该门店暂不支持在线复诊功能',
icon: 'none',
duration: 2000,
});
this.submitted = false;
return;
}
delegateStoreId = configRes.data.result.delegate_store_id || storeId;
doctorId = configRes.data.result.doctor_id;
} else if (!doctorId) {
const doctorRes = await this.getDefaultDoctorId();
doctorId = doctorRes.data.result?.doctor_id;
}
if (!doctorId) {
uni.showToast({ title: '该门店暂不支持在线复诊', icon: 'none', duration: 2000 });
this.submitted = false;
return;
}
const createRes = await request('/xkApi/order/create-register-info', {
method: 'POST',
data: {
doctor_id: doctorId,
drug_id: this.productDetail.id,
has_visited: formData.hasVisited,
has_used_drug: formData.hasUsedDrug,
illnessInfo: formData.illnessInfo,
type: this.productDetail.type || 2,
number: quantity,
},
}, 1);
const infoId = createRes.data?.result?.info_id || createRes.data?.info_id;
if (!infoId) {
throw new Error('创建就诊信息失败');
}
const rInfo = {
info_id: infoId,
doctor_id: doctorId,
has_visited: formData.hasVisited,
has_used_drug: formData.hasUsedDrug,
illness_info: formData.illnessInfo,
drug_name: this.productDetail.drug_name,
drug_id: this.productDetail.id,
number: quantity,
type: this.productDetail.type || 2,
created_at: new Date().toLocaleString('zh-CN'),
};
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${doctorId}&r_type=3&r_info=${encodeURIComponent(JSON.stringify(rInfo))}&delegate_store_id=${delegateStoreId}`,
});
} catch (error) {
console.error('提交处方药信息失败:', error);
uni.showToast({ title: '提交失败,请重试', icon: 'none' });
this.submitted = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f7fa;
}
.page-inner {
padding: 30rpx;
background: #fff;
min-height: 100vh;
}
.page-title {
text-align: center;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 30rpx;
}
.product-info-row {
display: flex;
margin-bottom: 30rpx;
.p-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f8f8f8;
margin-right: 20rpx;
}
.p-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.p-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.p-spec {
font-size: 24rpx;
color: #999;
}
.p-price {
font-size: 36rpx;
color: #ff4d4f;
font-weight: bold;
}
}
}
.quantity-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-top: 1rpx solid #f5f5f5;
border-bottom: 1rpx solid #f5f5f5;
margin-bottom: 40rpx;
.label {
font-size: 28rpx;
color: #333;
}
}
.popup-actions {
display: flex;
gap: 30rpx;
margin-top: 40rpx;
&.post-register-actions {
flex-direction: column;
}
button {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
border: none;
&.btn-cancel {
background: #f5f7fa;
color: #606266;
}
&.btn-confirm {
background: #2b85e4;
color: #fff;
}
&.btn-confirm-block {
flex: none;
width: 100%;
}
}
}
.post-register-exit {
text-align: center;
margin-top: 28rpx;
font-size: 26rpx;
color: #909399;
padding: 16rpx 0;
}
.clinic-question {
font-size: 28rpx;
color: #333;
line-height: 1.6;
margin-bottom: 24rpx;
padding: 20rpx;
background: #f7f8fa;
border-radius: 12rpx;
}
.clinic-drugs {
margin-bottom: 24rpx;
.sub-title {
font-size: 26rpx;
color: #909399;
margin-bottom: 12rpx;
}
.drug-line {
display: flex;
justify-content: space-between;
font-size: 28rpx;
padding: 12rpx 0;
border-bottom: 1rpx solid #eee;
.n {
color: #333;
}
.q {
color: #666;
}
}
}
.safe-area-bottom {
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
}
</style>

View File

@@ -232,85 +232,15 @@
</view>
</u-popup>
<!-- 处方药购买表单弹窗 -->
<u-popup
v-model="showPrescriptionFormPopup"
mode="bottom"
border-radius="24"
:closeable="true"
@close="closePrescriptionFormPopup"
>
<view class="popup-wrapper safe-area-bottom">
<view class="popup-header-title">请填写用药信息</view>
<view class="product-info-row">
<image class="p-img" :src="productDetail.image" mode="aspectFill"></image>
<view class="p-info">
<view class="p-name">{{ productDetail.drug_name }}</view>
<view class="p-spec">{{ productDetail.specification || '暂无规格' }}</view>
<view class="p-price">¥{{ productPrice }}</view>
</view>
</view>
<view class="quantity-row">
<text class="label">购买数量</text>
<u-number-box v-model="prescriptionQuantity" :min="1" :max="999" integer :step="1"></u-number-box>
</view>
<u-form :model="prescriptionFormData" ref="prescriptionForm">
<u-form-item label="是否就诊过" prop="hasVisited" label-width="200">
<u-radio-group v-model="prescriptionFormData.hasVisited">
<u-radio name="1" active-color="#2B85E4" style="margin-right: 40rpx;"></u-radio>
<u-radio name="0" active-color="#2B85E4"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="是否用过此药" prop="hasUsedDrug" label-width="200">
<u-radio-group v-model="prescriptionFormData.hasUsedDrug">
<u-radio name="1" active-color="#2B85E4" style="margin-right: 40rpx;"></u-radio>
<u-radio name="0" active-color="#2B85E4"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="症状" prop="illnessInfo" label-width="200">
<u-input
v-model="prescriptionFormData.illnessInfo"
type="select"
placeholder="请选择症状"
@click="showIllnessPicker = true"
disabled
/>
</u-form-item>
</u-form>
<view class="popup-actions" style="margin-top:40rpx;">
<button class="btn-cancel" @click="closePrescriptionFormPopup">取消</button>
<button class="btn-confirm" @click="submitPrescriptionForm">提交信息</button>
</view>
</view>
</u-popup>
<!-- 患病信息选择器 -->
<u-picker
v-model="showIllnessPicker"
mode="selector"
:range="illnessOptions"
range-key="label"
@confirm="onIllnessConfirm"
:mask-close-able="true"
confirm-color="#2B85E4"
></u-picker>
<u-toast ref="uToast" />
</view>
</template>
<script>
// 完整引入原有API
import { getOrderTextApi, getStoreDrugDetailApi, checkOnlineConsultationConfigApi } from "@/request/api/product";
import { getOrderTextApi, getStoreDrugDetailApi } from "@/request/api/product";
import { genOrderApi } from "@/request/api/order";
import { getCartCountsApi, saveCartApi } from "@/request/api/cart";
import { sendToUserApi } from "@/request/api/im";
import request from "@/request/api/request";
export default {
data() {
return {
@@ -349,20 +279,10 @@ export default {
productImages: [],
isFavorite: false,
cartCount: 0,
submitted: false,
showCartPopup: false,
cartQuantity: 1,
showBuyPopup: false,
buyQuantity: 1,
showPrescriptionFormPopup: false,
prescriptionQuantity: 1,
prescriptionFormData: {
hasVisited: '1',
hasUsedDrug: '1',
illnessInfo: '',
},
illnessOptions: [],
showIllnessPicker: false,
orderText: '本次商品物流配送,由萧康医药提供服务。',
introductionImages: [], // 商品介绍图数组
delegateStoreId: '', // 委托诊所ID
@@ -428,12 +348,6 @@ export default {
const data = res.data.result;
Object.assign(this.productDetail, data);
this.productImages = data.image ? [data.image] : [];
if (data.indications_array && data.indications_array.length > 0) {
this.illnessOptions = data.indications_array.map(item => ({
value: item.label || item.value,
label: item.label || item.value
}));
}
if (!this.productDetail.indications && data.function) {
this.productDetail.indications = data.function;
}
@@ -510,7 +424,19 @@ export default {
},
handleBuyNow() {
if (this.productDetail.is_otc === 0 && this.productDetail.type == 2) {
this.showPrescriptionFormPopup = true;
const q = [
`mode=pharmacy`,
`id=${encodeURIComponent(this.productDetail.id)}`,
`doctor_id=${encodeURIComponent(this.formData.doctor_id || '')}`,
`delegate_store_id=${encodeURIComponent(this.delegateStoreId || '')}`,
`type=${encodeURIComponent(String(this.productDetail.type || 2))}`,
`image=${encodeURIComponent(this.productDetail.image || '')}`,
`price=${encodeURIComponent(String(this.productPrice))}`,
`drug_name=${encodeURIComponent(this.productDetail.drug_name || '')}`,
].join('&');
uni.navigateTo({
url: `/subPackages/follow-up/medication-info?${q}`,
});
} else {
this.showBuyPopup = true;
}
@@ -527,129 +453,6 @@ export default {
});
})
},
async submitPrescriptionForm() {
if (!this.prescriptionFormData.hasVisited || !this.prescriptionFormData.hasUsedDrug || !this.prescriptionFormData.illnessInfo) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
// 先保存表单数据到局部变量(因为 closePrescriptionFormPopup 会清空表单)
const formData = {
hasVisited: this.prescriptionFormData.hasVisited,
hasUsedDrug: this.prescriptionFormData.hasUsedDrug,
illnessInfo: this.prescriptionFormData.illnessInfo,
};
const quantity = this.prescriptionQuantity;
this.submitted = true;
this.closePrescriptionFormPopup();
try {
const storeId = uni.getStorageSync('store_id');
let delegateStoreId = this.delegateStoreId;
let doctorId = null;
// 如果没有传入委托诊所ID需要先检查在线复诊配置
if (!delegateStoreId) {
const configRes = await checkOnlineConsultationConfigApi({ store_id: storeId });
if (!configRes.data?.result?.can_use) {
uni.showToast({
title: configRes.data?.result?.message || '该门店暂不支持在线复诊功能',
icon: 'none',
duration: 2000
});
this.submitted = false;
return;
}
// 获取委托诊所ID和医生ID
delegateStoreId = configRes.data.result.delegate_store_id || storeId;
doctorId = configRes.data.result.doctor_id;
} else {
// 如果已有委托诊所ID获取默认医生ID
const doctorRes = await this.getDefaultDoctorId();
doctorId = doctorRes.data.result?.doctor_id;
}
// 检查是否配置了在线复诊负责人
if (!doctorId) {
uni.showToast({
title: '该门店暂不支持在线复诊',
icon: 'none',
duration: 2000
});
this.submitted = false;
return;
}
// 2. 先调用后端API保存就诊信息获取info_id
const createRes = await request('/xkApi/order/create-register-info', {
method: 'POST',
data: {
doctor_id: doctorId,
drug_id: this.productDetail.id,
has_visited: formData.hasVisited,
has_used_drug: formData.hasUsedDrug,
illnessInfo: formData.illnessInfo,
type: this.productDetail.type || 2,
number: quantity,
}
}, 1);
const infoId = createRes.data?.result?.info_id || createRes.data?.info_id;
if (!infoId) {
throw new Error('创建就诊信息失败');
}
// 3. 准备患者就诊信息包含info_id用于后续更新register_id
const rInfo = {
info_id: infoId, // 新增:用于挂号后更新关联
doctor_id: doctorId,
has_visited: formData.hasVisited,
has_used_drug: formData.hasUsedDrug,
illness_info: formData.illnessInfo,
drug_name: this.productDetail.drug_name,
drug_id: this.productDetail.id,
number: quantity,
type: this.productDetail.type || 2, // 2=西药3=保健食品
created_at: new Date().toLocaleString('zh-CN')
};
// 4. 跳转到就诊人选择页面,传递 register_type=3、患者就诊信息和委托诊所ID
// 用户选择就诊人后,会创建挂号记录并跳转到 register.vue
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${doctorId}&r_type=3&r_info=${encodeURIComponent(JSON.stringify(rInfo))}&delegate_store_id=${delegateStoreId}`
});
} catch (error) {
console.error('提交处方药信息失败:', error);
uni.showToast({
title: '提交失败,请重试',
icon: 'none'
});
}
},
// 获取默认医生ID根据门店配置
async getDefaultDoctorId() {
try {
const storeId = uni.getStorageSync('store_id');
if (!storeId) {
console.warn('未找到store_id无法获取默认医生');
return { data: { result: { doctor_id: null, message: '未找到门店信息' } } };
}
const res = await request('/xkApi/online-consultation/get-default-doctor-id', {
method: 'GET',
data: { store_id: storeId }
}, 0);
return res;
} catch (error) {
console.error('获取默认医生ID失败:', error);
// 如果接口失败,返回空值
return { data: { result: { doctor_id: null, message: '获取失败' } } };
}
},
goToCart() {
uni.navigateTo({
url: '/subPackages/shop/shop-cate'
@@ -666,11 +469,6 @@ export default {
url: '/pages/index/platform-info?store_id=0'
});
},
onIllnessConfirm(e) {
const index = e[0];
this.prescriptionFormData.illnessInfo = this.illnessOptions[index].value;
this.showIllnessPicker = false;
},
closeCartPopup() {
this.showCartPopup = false;
this.cartQuantity = 1;
@@ -679,15 +477,6 @@ export default {
this.showBuyPopup = false;
this.buyQuantity = 1;
},
closePrescriptionFormPopup() {
this.showPrescriptionFormPopup = false;
this.prescriptionQuantity = 1;
this.prescriptionFormData = {
hasVisited: '1',
hasUsedDrug: '1',
illnessInfo: '',
};
}
}
}
</script>

View File

@@ -141,6 +141,10 @@
import {checkDev} from "@/utils/utils";
import {genOrderApi, saveRegisterInfoApi} from "@/request/api/order";
import request from "@/request/api/request";
import {
CLINIC_POST_REGISTER_STORAGE_KEY,
resolveFollowUpDrugsFromRInfo,
} from '@/utils/clinicFollowUpAssistantIm';
export default {
data() {
return {
@@ -270,6 +274,12 @@
console.error('waitForRegisterDetail', err);
}
},
/** 诊所复诊(2)且存在复诊药:支付后先走独立用药确认页,再进聊天 */
needsClinicPostRegisterDrugPage() {
if (!this.registList || this.registList.register_type !== 2) return false;
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(this.rInfo);
return !!(registerInfoId && drugsPayload.length);
},
// 挂号信息
getInfo() {
const that = this;
@@ -369,22 +379,42 @@
duration: 0
});
}
// 6. 生成订单(如果需要)
// 6. 生成订单(如果需要);诊所复诊有药时先 redirect 用药确认页,助理卡改在页内提交后发
const chatUrl = `/subPackages/chat/chat?user_id=${encodeURIComponent(doctorPrefixed)}&room_id=${roomId}&register_id=${that.register_id}`;
const goAfterGenOrder = () => {
if (that.needsClinicPostRegisterDrugPage()) {
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(that.rInfo);
uni.setStorageSync(
CLINIC_POST_REGISTER_STORAGE_KEY,
JSON.stringify({
register_info_id: registerInfoId,
register_id: that.register_id,
drugs: drugsPayload,
question: '您是否曾使用过此类药品?',
room_id: roomId,
doctor_id: doctorId,
patient_id: patientId,
doctor_prefixed: doctorPrefixed,
}),
);
uni.hideLoading();
uni.redirectTo({
url: '/subPackages/follow-up/medication-info?mode=clinic_post_register',
});
return;
}
uni.hideLoading();
uni.navigateTo({ url: chatUrl });
};
genOrderApi({
register_id: that.register_id,
register_info: typeof that.rInfo === 'string' ? that.rInfo : JSON.stringify(that.rInfo),
}).then((resOrder) => {
uni.hideLoading();
uni.navigateTo({
url: `/subPackages/chat/chat?user_id=${encodeURIComponent(doctorPrefixed)}&room_id=${roomId}&register_id=${that.register_id}`
});
}).then(() => {
goAfterGenOrder();
}).catch((error) => {
console.error('生成订单失败:', error);
uni.hideLoading();
uni.navigateTo({
url: `/subPackages/chat/chat?user_id=${encodeURIComponent(doctorPrefixed)}&room_id=${roomId}&register_id=${that.register_id}`
});
goAfterGenOrder();
});
} catch (error) {
uni.hideLoading();

View File

@@ -1,131 +1,31 @@
<template>
<view class="consultation-container safe-area-inset-bottom">
<!-- 顶部导航栏 -->
<view class="consultation-header">
<view class="header-back" @click="handleBack">
<text class="iconfont icon-arrow-left"></text>
</view>
<view class="header-title">转诊咨询</view>
<view class="header-placeholder"></view>
</view>
<!-- 提示信息类似图片中的灰色提示框 -->
<view class="notice-box" v-if="showNotice">
<view class="notice-text">
<text v-for="(part, index) in parsedNoticeParts" :key="index">
<text v-if="part.type === 'text'">{{ part.text }}</text>
<text v-else-if="part.type === 'agreement'" class="notice-link" @click="viewAgreement(part.agreementId)">
{{ part.text }}
</text>
</text>
</view>
</view>
<!-- 协议抽屉 -->
<u-popup v-model="showAgreement" :safe-area-inset-bottom="true" mode="bottom" height="88%" border-radius="20"
:closeable="true" z-index="20000">
<view class="agreement-popup">
<view class="agreement-header">
<text class="agreement-title">{{ currentAgreementName || '协议详情' }}</text>
</view>
<view class="agreement-content">
<u-parse :html="agreementContent"></u-parse>
</view>
</view>
</u-popup>
<!-- 聊天消息区域 -->
<scroll-view class="messages-scroll" scroll-y="true" :scroll-into-view="scrollIntoView" scroll-with-animation>
<view class="messages-container">
<!-- 按时间顺序渲染消息 -->
<view v-for="(message, index) in displayedMessages" :key="message.id">
<!-- 欢迎消息 -->
<view v-if="message.type === 'welcome'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>您好! 我是医生助理, 已收到您的用药申请:{{ prescriptionInfo }}, 为了帮助医生判断准确, 请按照您的真实情况回答以下问题</text>
</view>
</view>
</view>
<!-- 问题消息 -->
<view v-else-if="message.type === 'question'" class="message-item other" :id="'question-' + message.questionId">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<!-- 用户回答消息 -->
<view v-else-if="message.type === 'answer'" class="message-item mine">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<!-- 等待转接消息 -->
<view v-else-if="message.type === 'waiting'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>请稍等, 正在为您转接医生进行复诊开方...</text>
</view>
</view>
</view>
</view>
<!-- "正在回复"提示气泡 -->
<view v-if="isTyping" class="message-item other typing-indicator">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble typing-bubble">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
<!-- 底部锚点 -->
<view id="bottom-anchor" style="height: 1px;"></view>
</view>
</scroll-view>
<!-- 底部操作按钮根据问题的answers动态生成 -->
<view class="action-footer safe-area-inset-bottom" v-if="currentQuestion && !isSubmitting && !showWaiting">
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
:class="['action-btn', getAnswerButtonClass(answer, index)]"
@click="handleAnswer(answer)"
>
{{ answer }}
</button>
</view>
<!-- 提交中提示 -->
<view class="submitting-overlay" v-if="isSubmitting">
<view class="submitting-content">
<view class="loading-spinner"></view>
<text class="submitting-text">正在提交...</text>
</view>
</view>
</view>
<AssistantConsultationShell
header-title="转诊咨询"
:show-notice="showNotice"
:parsed-notice-parts="parsedNoticeParts"
:show-agreement.sync="showAgreement"
:agreement-content="agreementContent"
:current-agreement-name="currentAgreementName"
:displayed-messages="displayedMessages"
:assistant-avatar="assistantAvatar"
:assistant-name="assistantName"
:user-avatar="userAvatar"
:prescription-info="prescriptionInfo"
:is-typing="isTyping"
:scroll-into-view="scrollIntoView"
:show-question-buttons="true"
:current-question="currentQuestion"
:current-question-answers="currentQuestionAnswers"
:is-submitting="isSubmitting"
:show-waiting="showWaiting"
@back="handleBack"
@answer="handleAnswer"
@view-agreement="viewAgreement"
/>
</template>
<script>
import AssistantConsultationShell from '@/components/AssistantConsultationShell.vue'
import {
getTransferPrescriptionDetailApi,
getTransferQuestionsApi,
@@ -137,6 +37,7 @@ import {
export default {
name: 'TransferConsultation',
components: { AssistantConsultationShell },
data() {
return {
transferId: 0, // 转诊ID
@@ -176,10 +77,10 @@ export default {
if (!this.currentQuestion) {
return ['是', '否'] // 默认答案
}
// 获取问题的answers字段
let answers = this.currentQuestion.answers
// 如果answers是字符串JSON格式解析为数组
if (typeof answers === 'string') {
try {
@@ -189,19 +90,19 @@ export default {
answers = ['是', '否']
}
}
// 如果answers不是数组或为空使用默认值
if (!Array.isArray(answers) || answers.length === 0) {
answers = ['是', '否']
}
// 过滤空值
return answers.filter(answer => answer && answer.trim().length > 0)
},
// 按时间顺序显示的消息列表
displayedMessages() {
const messages = []
// 1. 欢迎消息
if (this.showWelcome) {
messages.push({
@@ -210,7 +111,7 @@ export default {
timestamp: 0
})
}
// 2. 问题和答案交替显示
for (let i = 0; i <= this.currentQuestionIndex && i < this.questions.length; i++) {
const question = this.questions[i]
@@ -222,7 +123,7 @@ export default {
content: question.question,
timestamp: i * 2 + 1
})
// 如果有对应的答案,添加答案
if (i < this.userAnswers.length) {
messages.push({
@@ -233,7 +134,7 @@ export default {
})
}
}
// 3. 等待转接消息
if (this.showWaiting) {
messages.push({
@@ -242,7 +143,7 @@ export default {
timestamp: messages.length
})
}
return messages
}
},
@@ -307,8 +208,8 @@ export default {
this.prescriptionInfo = transferData.drug_name_text
} else if (transferData.content) {
// 如果后端没有返回drug_name_text前端自己解析
const content = typeof transferData.content === 'string'
? JSON.parse(transferData.content)
const content = typeof transferData.content === 'string'
? JSON.parse(transferData.content)
: transferData.content
this.prescriptionInfo = this.extractDrugName(content, transferData.prescription_type || 1)
}
@@ -321,9 +222,9 @@ export default {
// 从处方内容中提取药品名称(前端备用方法)
extractDrugName(content, prescriptionType) {
if (!content) return '未知药品'
const drugNames = []
if (prescriptionType === 1) {
// 中药处方
if (content.repice && Array.isArray(content.repice)) {
@@ -369,7 +270,7 @@ export default {
})
}
}
if (drugNames.length === 0) return '未知药品'
if (drugNames.length === 1) return drugNames[0]
return drugNames[0] + '等共' + drugNames.length + '味'
@@ -402,7 +303,7 @@ export default {
}
return a.id - b.id
})
if (this.questions.length === 0) {
uni.showToast({
title: '暂无咨询问题',
@@ -455,7 +356,7 @@ export default {
if (this.currentQuestionIndex < this.questions.length - 1) {
// 显示"正在回复"提示
this.isTyping = true
// 延迟后显示下一个问题
setTimeout(() => {
this.isTyping = false
@@ -514,7 +415,7 @@ export default {
const doctorInfo = result.doctor_info || {}
const doctorName = doctorInfo.name || '医生'
const doctorAvatar = doctorInfo.avatar || ''
// 构建跳转URL包含所有必要参数
let chatUrl = `/subPackages/chat/chat?user_id=${result.doctor_id}&room_id=${result.room_id}&nick_name=${encodeURIComponent(doctorName)}&room_status=0`
if (registerId) {
@@ -523,7 +424,7 @@ export default {
if (doctorAvatar) {
chatUrl += `&avatar=${encodeURIComponent(doctorAvatar)}`
}
uni.navigateTo({
url: chatUrl
})
@@ -545,27 +446,6 @@ export default {
})
}
},
// 获取答案按钮的样式类
getAnswerButtonClass(answer, index) {
const answers = this.currentQuestionAnswers
const totalAnswers = answers.length
// 如果只有两个答案,使用原来的样式(是/否)
if (totalAnswers === 2) {
if (answer === '是') {
return 'yes-btn'
} else if (answer === '否') {
return 'no-btn'
}
}
// 多个答案时,第一个按钮使用主色调,其他使用次要样式
if (index === 0) {
return 'yes-btn' // 第一个按钮使用主色调
} else {
return 'no-btn' // 其他按钮使用次要样式
}
},
// 返回
handleBack() {
uni.showModal({
@@ -594,7 +474,7 @@ export default {
const result = res.data.result || {}
this.noticeText = result.notice_text || ''
this.agreements = result.agreements || []
// 解析文案,提取协议链接
this.parseNoticeText()
}
@@ -630,7 +510,7 @@ export default {
// 查找匹配的协议
const agreementName = match[1]
const agreement = this.agreements.find(ag => ag.name === agreementName || ag.name.includes(agreementName))
if (agreement) {
// 找到匹配的协议,添加为可点击的链接
parts.push({
@@ -699,283 +579,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.consultation-container {
width: 100%;
height: 100vh;
background-color: #F8FAFC;
display: flex;
flex-direction: column;
}
.consultation-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
.header-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #333;
}
}
.header-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.header-placeholder {
width: 60rpx;
}
}
.notice-box {
margin: 20rpx 30rpx;
padding: 24rpx;
background: #F5F5F5;
border-radius: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #666;
.notice-text {
display: block;
word-break: break-all;
}
.notice-link {
color: #0A84FF;
text-decoration: underline;
}
}
.messages-scroll {
flex: 1;
overflow: hidden;
}
.messages-container {
padding: 30rpx;
min-height: 100%;
}
.message-item {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 10rpx;
flex-shrink: 0;
background: #f2f2f2;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
margin-left: 16rpx;
.assistant-label {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.message-bubble {
padding: 18rpx 24rpx;
font-size: 30rpx;
line-height: 1.5;
border-radius: 4rpx 12rpx 12rpx 12rpx;
background: #fff;
color: #333;
word-break: break-all;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
&.mine {
flex-direction: row-reverse;
.message-content {
margin-left: 0;
margin-right: 16rpx;
align-items: flex-end;
.message-bubble {
background: #0A84FF;
color: #fff;
border-radius: 12rpx 4rpx 12rpx 12rpx;
}
}
}
&.typing-indicator {
.typing-bubble {
padding: 18rpx 24rpx;
min-width: 80rpx;
}
.typing-dots {
display: flex;
align-items: center;
gap: 8rpx;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
.action-footer {
padding: 20rpx 30rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
justify-content: space-between;
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
&::after {
border: none;
}
&.yes-btn {
background: #0A84FF;
color: #fff;
}
&.no-btn {
background: #fff;
color: #333;
border: 2rpx solid #ddd;
}
}
}
.submitting-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.submitting-content {
background: #fff;
padding: 60rpx 80rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #0A84FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.submitting-text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.agreement-popup {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.agreement-header {
padding: 30rpx 50rpx 20rpx;
border-bottom: 1rpx solid #e5e7eb;
background: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.agreement-title {
font-size: 32rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.5;
}
.agreement-content {
flex: 1;
padding: 30rpx 50rpx 40rpx;
overflow-y: auto;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,80 @@
import { sendToUserApi } from '@/request/api/im';
/** 挂号支付后进「复诊用药确认」页时写入,与聊天内打开的 payload 分离 */
export const CLINIC_POST_REGISTER_STORAGE_KEY = '__clinic_post_register_payload';
/**
* 从 r_info 解析复诊西药列表(与 register.vue 原 sendClinicFollowUpAssistantMessage 一致)
* @returns {{ registerInfoId: string, drugsPayload: Array<{drug_id:number,name:string,quantity:number}> }}
*/
export function resolveFollowUpDrugsFromRInfo(rInfo) {
const empty = { registerInfoId: '', drugsPayload: [] };
if (!rInfo || typeof rInfo !== 'object') return empty;
const infoId = rInfo.info_id;
if (!infoId) return empty;
let drugs = Array.isArray(rInfo.follow_up_drugs) ? rInfo.follow_up_drugs : [];
if (!drugs.length && rInfo.drug_ids) {
try {
const ids = typeof rInfo.drug_ids === 'string' ? JSON.parse(rInfo.drug_ids) : rInfo.drug_ids;
if (Array.isArray(ids)) {
drugs = ids.map((id) => ({ drug_id: Number(id), name: '' })).filter((d) => d.drug_id > 0);
}
} catch (e) {
drugs = [];
}
}
if (!drugs.length) return empty;
const fallbackQty = Math.min(99, Math.max(1, Number(rInfo.follow_up_number || rInfo.number || 1) || 1));
const drugsPayload = drugs.map((d) => {
const row = Number(d.quantity ?? d.number);
const quantity =
Number.isFinite(row) && row >= 1 ? Math.min(99, row) : fallbackQty;
return {
drug_id: Number(d.drug_id || d.id),
name: d.name || d.drug_name || '药品',
quantity,
};
});
return { registerInfoId: String(infoId), drugsPayload };
}
/**
* 助理号发送 type11 follow_up_drug 卡片
* @param {string} answerStatus 'pending' | 'yes' | 'no'
*/
export async function sendFollowUpDrugAssistantMessage({
roomId,
doctorId,
registerId,
registerInfoId,
drugsPayload,
question,
answerStatus,
}) {
const payload = {
flow: 'follow_up_drug',
register_info_id: registerInfoId,
register_id: registerId,
drugs: drugsPayload,
question: question || '您是否曾使用过以上药品?',
answer_status: answerStatus,
};
await sendToUserApi({
room_id: roomId,
sender_user_id: 'doctor_assistant',
receiver_user_id: `doctor-${doctorId}`,
message_type: 11,
message_content: JSON.stringify(payload),
duration: 0,
});
}
export function markFollowUpAssistSent(registerId) {
if (registerId) {
uni.setStorageSync(`followup_assist_sent_${registerId}`, '1');
}
}
export function wasFollowUpAssistSent(registerId) {
return !!(registerId && uni.getStorageSync(`followup_assist_sent_${registerId}`));
}