feat: 诊所问题优化

This commit is contained in:
李琦
2026-07-28 14:22:56 +08:00
parent 77a690a071
commit 5629955dd3
6 changed files with 424 additions and 26 deletions

View File

@@ -111,6 +111,30 @@
</scroll-view>
<slot v-if="customFooterActive && $slots.footer" name="footer"></slot>
<view
v-else-if="showQuestionButtons && currentQuestion && !isSubmitting && !showWaiting && answerMode === 'multi'"
class="action-footer multi-footer safe-area-inset-bottom"
>
<!-- 多选适用症点选 toggling确认后再提交 -->
<view class="multi-options">
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
:class="['multi-option-btn', { selected: !!multiSelectedMap[index] }]"
@click="toggleMultiAnswer(index)"
>
{{ answer }}
</button>
</view>
<button
class="action-btn yes-btn confirm-btn"
:disabled="multiSelectedCount === 0"
:class="{ disabled: multiSelectedCount === 0 }"
@click="confirmMultiAnswer"
>
确认
</button>
</view>
<view
v-else-if="showQuestionButtons && currentQuestion && !isSubmitting && !showWaiting"
class="action-footer safe-area-inset-bottom"
@@ -224,6 +248,29 @@ export default {
type: Boolean,
default: false,
},
/**
* 答题模式single 点一下即提交multi 可多选后点确认
* 诊所复诊按药选适用症用 multi
*/
answerMode: {
type: String,
default: 'single',
},
},
data() {
return {
/** 多选模式下已选中的选项下标映射,避免模板里写 indexOf */
multiSelectedMap: {},
};
},
watch: {
// 题目切换时清空多选,避免上一题选中态残留
currentQuestion() {
this.multiSelectedMap = {};
},
answerMode() {
this.multiSelectedMap = {};
},
},
computed: {
agreementPopupVisible: {
@@ -234,6 +281,9 @@ export default {
this.$emit('update:showAgreement', val)
},
},
multiSelectedCount() {
return Object.keys(this.multiSelectedMap).filter((k) => this.multiSelectedMap[k]).length;
},
},
methods: {
handleHeaderBack() {
@@ -256,6 +306,28 @@ export default {
if (index === 0) return 'yes-btn'
return 'no-btn'
},
/** 多选:切换某个适用症的选中状态 */
toggleMultiAnswer(index) {
if (this.multiSelectedMap[index]) {
this.$delete(this.multiSelectedMap, index);
} else {
this.$set(this.multiSelectedMap, index, true);
}
},
/** 多选确认:把选中标签用顿号拼接后交给父组件 */
confirmMultiAnswer() {
const indexes = Object.keys(this.multiSelectedMap)
.filter((k) => this.multiSelectedMap[k])
.map((k) => Number(k))
.sort((a, b) => a - b);
if (!indexes.length) return;
const labels = indexes
.map((i) => this.currentQuestionAnswers[i])
.filter(Boolean);
if (!labels.length) return;
this.$emit('answer', labels.join('、'));
this.multiSelectedMap = {};
},
},
}
</script>
@@ -450,12 +522,15 @@ export default {
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
min-height: 88rpx;
height: auto;
line-height: 1.35;
padding: 20rpx 16rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
white-space: normal;
&::after {
border: none;
@@ -471,6 +546,48 @@ export default {
color: #333;
border: 2rpx solid #ddd;
}
&.disabled,
&[disabled] {
opacity: 0.45;
}
}
&.multi-footer {
flex-direction: column;
gap: 16rpx;
.multi-options {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.multi-option-btn {
min-width: 140rpx;
padding: 0 28rpx;
height: 64rpx;
line-height: 64rpx;
border-radius: 32rpx;
font-size: 26rpx;
background: rgba(10, 132, 255, 0.08);
color: #475569;
border: 2rpx solid transparent;
&::after {
border: none;
}
&.selected {
background: #0a84ff;
color: #fff;
}
}
.confirm-btn {
width: 100%;
flex: none;
}
}
}

View File

@@ -184,6 +184,14 @@
<text class="label">您的选择</text>
<text class="value">{{ followUpAnsweredLabel }}</text>
</view>
<view class="text-block" v-if="parsedContent.illness_info">
<text class="block-label">适用症/症状</text>
<text class="block-content">{{ parsedContent.illness_info }}</text>
</view>
<view class="text-block" v-if="parsedContent.supplement">
<text class="block-label">补充信息</text>
<text class="block-content">{{ parsedContent.supplement }}</text>
</view>
<view v-if="parsedContent.answer_status === 'pending'" class="follow-up-pending-readonly">
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text>
</view>
@@ -221,7 +229,7 @@
</view>
<view class="card-footer split">
<text class="link-btn" @click="$emit('viewPrescription', parsedContent.id, parsedContent.order_no)">详情</text>
<button class="action-btn primary" @click="$emit('goToOrderMedicine', parsedContent.order_id, parsedContent.order_no)">一键购药</button>
<button class="action-btn primary" @click="$emit('goToOrderMedicine', parsedContent.order_id, parsedContent.order_no)">立即支付</button>
</view>
</block>

View File

@@ -260,6 +260,9 @@ export default {
id,
drug_name: meta.drug_name || '',
quantity: q,
// 适用症随选药带入,复诊助手按药出题用
indications: meta.indications || '',
indications_array: Array.isArray(meta.indications_array) ? meta.indications_array : [],
});
}
}

View File

@@ -17,12 +17,32 @@
:show-question-buttons="postRegisterStep === 'await_answer'"
:current-question="postRegisterCurrentQuestion"
:current-question-answers="postRegisterQuestionAnswers"
:answer-mode="postRegisterAnswerMode"
:custom-footer-active="postRegisterStep === 'await_supplement'"
:is-submitting="postRegisterSubmitting"
:show-waiting="postRegisterWaiting"
@back="handlePostRegisterBack"
@answer="onPostRegisterShellAnswer"
@view-agreement="viewPostRegisterAgreement"
/>
>
<!-- 有补充底部输入框填完再转接医生 -->
<view slot="footer" class="supplement-footer safe-area-inset-bottom">
<textarea
class="supplement-input"
v-model="postRegisterSupplementText"
placeholder="请补充您的病情或用药相关信息"
:maxlength="500"
:disabled="postRegisterSubmitting"
/>
<button
class="supplement-submit"
:disabled="postRegisterSubmitting || !postRegisterSupplementCanSubmit"
@click="onPostRegisterSupplementSubmit"
>
提交补充
</button>
</view>
</AssistantConsultationShell>
<u-toast ref="uToast" />
</view>
@@ -116,7 +136,7 @@
<script>
import { getStoreDrugDetailApi, checkOnlineConsultationConfigApi } from '@/request/api/product';
import request from '@/request/api/request';
import { confirmFollowUpDrugUseApi } from '@/request/api/order';
import { confirmFollowUpDrugUseApi, getWesternDrugsForFollowUpApi } from '@/request/api/order';
import {
CLINIC_POST_REGISTER_STORAGE_KEY,
markFollowUpAssistSent,
@@ -132,6 +152,12 @@ import {
const FOLLOWUP_PAYLOAD_KEY = '__medication_followup_payload';
/** 收尾确认题:确诊/无禁忌后是否还有补充 */
const POST_REGISTER_CONFIRM_QUESTION =
'您已确诊过此疾病,并使用过药,且无相关禁忌症和不良反应。请问您是否还有信息需要补充?如无,我将依据病情为您开具处方。';
const POST_REGISTER_CONFIRM_NO_MORE = '无需补充,立即开方';
const POST_REGISTER_CONFIRM_HAS_MORE = '有补充';
export default {
components: { AssistantConsultationShell },
data() {
@@ -175,10 +201,14 @@ export default {
postRegisterQuestionList: [],
postRegisterQuestionIndex: -1,
postRegisterUserAnswers: [],
/** 按药收集的适用症:{ drug_id, name, symptoms }[] */
postRegisterSymptomAnswers: [],
postRegisterSupplementText: '',
postRegisterFinalSupplement: '',
postRegisterReplyDelay: 0.65,
postRegisterIsTyping: false,
postRegisterShowWelcome: true,
/** idle | await_answer | typing_after | submitting */
/** idle | await_answer | await_supplement | typing_after | submitting */
postRegisterStep: 'idle',
postRegisterScrollIntoView: '',
postRegisterSubmitting: false,
@@ -233,6 +263,15 @@ export default {
});
}
}
// 有补充输入提交后追加一条补充内容气泡
if (this.postRegisterFinalSupplement) {
messages.push({
type: 'answer',
id: 'answer-supplement',
content: this.postRegisterFinalSupplement,
timestamp: messages.length,
});
}
if (this.postRegisterWaiting) {
messages.push({
type: 'waiting',
@@ -244,11 +283,24 @@ export default {
},
postRegisterCurrentQuestion() {
if (this.postRegisterStep !== 'await_answer') return null;
if (!this.postRegisterQuestionList.length) return null;
return this.postRegisterQuestionList[0];
const qs = this.postRegisterQuestionList;
const idx = this.postRegisterQuestionIndex;
if (idx < 0 || idx >= qs.length) return null;
return qs[idx];
},
postRegisterQuestionAnswers() {
return ['是', '否'];
const q = this.postRegisterCurrentQuestion;
if (!q || !Array.isArray(q.answers)) return ['是', '否'];
return q.answers;
},
/** 适用症题用 multi其余单选 */
postRegisterAnswerMode() {
const q = this.postRegisterCurrentQuestion;
return q && q.type === 'symptom' ? 'multi' : 'single';
},
/** 补充文案是否可提交(避免模板里调用 trim */
postRegisterSupplementCanSubmit() {
return !!(this.postRegisterSupplementText && String(this.postRegisterSupplementText).trim());
},
productPrice() {
const rel = this.productDetail.drug_store_relations;
@@ -289,16 +341,18 @@ export default {
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());
// 先补齐适用症再出题,避免选药 payload 缺 indications 时跳过症状题
this.enrichClinicDrugsIndications()
.then(() => {
this.buildPostRegisterQuestionList();
this.$nextTick(() => this.beginPostRegisterAssistantFlow());
})
.catch(() => {
this.buildPostRegisterQuestionList();
this.$nextTick(() => this.beginPostRegisterAssistantFlow());
});
}
return;
}
@@ -361,10 +415,103 @@ export default {
this.postRegisterScrollIntoView = '';
}, 120);
},
/**
* 构建诊所复诊助手题目:
* 1) 是否用过药 2) 每个有适用症的药一题 3) 是否还需补充
*/
buildPostRegisterQuestionList() {
const list = [
{
id: 'clinic-post-reg-used',
type: 'used_drug',
question: this.clinicQuestion || '您是否曾使用过此类药品?',
answers: ['是', '否'],
},
];
(this.clinicDrugs || []).forEach((drug, idx) => {
const answers = this.normalizeDrugIndicationLabels(drug);
if (!answers.length) return;
const name = drug.name || drug.drug_name || '药品';
list.push({
id: `clinic-post-reg-symptom-${drug.drug_id || idx}`,
type: 'symptom',
drug_id: Number(drug.drug_id || drug.id) || 0,
drug_name: name,
question: `请选择「${name}」本次复诊的适用症`,
answers,
});
});
list.push({
id: 'clinic-post-reg-confirm',
type: 'confirm',
question: POST_REGISTER_CONFIRM_QUESTION,
answers: [POST_REGISTER_CONFIRM_NO_MORE, POST_REGISTER_CONFIRM_HAS_MORE],
});
this.postRegisterQuestionList = list;
},
/**
* 若选药 payload 未带适用症,用门店西药列表接口按 id 回填
* 原因:直接读 yii_drug.indications避开商品详情对处方药清空适用症
*/
async enrichClinicDrugsIndications() {
const drugs = this.clinicDrugs || [];
const need = drugs.some((d) => !this.normalizeDrugIndicationLabels(d).length);
if (!need || !drugs.length) return;
const storeId = uni.getStorageSync('store_id');
if (!storeId) return;
try {
const res = await getWesternDrugsForFollowUpApi({ store_id: storeId });
const list = Array.isArray(res.data && res.data.result) ? res.data.result : [];
if (!list.length) return;
const byId = {};
list.forEach((row) => {
if (row && row.id) byId[Number(row.id)] = row;
});
this.clinicDrugs = drugs.map((d) => {
const id = Number(d.drug_id || d.id);
if (this.normalizeDrugIndicationLabels(d).length) return d;
const meta = byId[id];
if (!meta) return d;
return {
...d,
indications: meta.indications || '',
indications_array: Array.isArray(meta.indications_array) ? meta.indications_array : [],
};
});
} catch (e) {
console.warn('enrichClinicDrugsIndications', e);
}
},
/** 把药品 indications / indications_array 规范成可选标签文案数组 */
normalizeDrugIndicationLabels(drug) {
if (!drug) return [];
let arr = Array.isArray(drug.indications_array) ? drug.indications_array : [];
if (!arr.length && drug.indications) {
arr = String(drug.indications)
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => ({ value: s, label: s }));
}
return arr
.map((item) => (typeof item === 'string' ? item : item.label || item.value || ''))
.map((s) => String(s).trim())
.filter(Boolean);
},
/** 聚合「药名:症状1、症状2」写入 illnessInfo */
buildPostRegisterIllnessInfo() {
return (this.postRegisterSymptomAnswers || [])
.filter((row) => row && row.symptoms)
.map((row) => `${row.name || '药品'}:${row.symptoms}`)
.join('');
},
beginPostRegisterAssistantFlow() {
this.postRegisterStep = 'idle';
this.postRegisterShowWelcome = true;
this.postRegisterUserAnswers = [];
this.postRegisterSymptomAnswers = [];
this.postRegisterSupplementText = '';
this.postRegisterFinalSupplement = '';
this.postRegisterWaiting = false;
this.postRegisterQuestionIndex = -1;
this.postRegisterIsTyping = true;
@@ -376,10 +523,72 @@ export default {
this.$nextTick(() => this.postRegisterScrollToBottom());
}, delayMs);
},
/**
* 推进到下一题或结束提交typing 延迟与转诊一致
*/
advancePostRegisterAfterAnswer() {
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;
const nextIdx = this.postRegisterQuestionIndex + 1;
if (nextIdx < this.postRegisterQuestionList.length) {
this.postRegisterQuestionIndex = nextIdx;
this.postRegisterStep = 'await_answer';
this.$nextTick(() => this.postRegisterScrollToBottom());
return;
}
// 全部答完 → 提交转接
this.$nextTick(() => this.postRegisterScrollToBottom());
setTimeout(() => {
this.submitClinicPostRegister();
}, 500);
}, typingMs);
},
onPostRegisterShellAnswer(answer) {
if (this.postRegisterStep !== 'await_answer' || this.submitted) return;
this.postRegisterUserAnswers = [answer];
this.clinicForm.hasUsedDrug = answer === '是' ? '1' : '0';
const q = this.postRegisterCurrentQuestion;
if (!q) return;
this.postRegisterUserAnswers = this.postRegisterUserAnswers.concat([answer]);
if (q.type === 'used_drug') {
this.clinicForm.hasUsedDrug = answer === '是' ? '1' : '0';
this.advancePostRegisterAfterAnswer();
return;
}
if (q.type === 'symptom') {
this.postRegisterSymptomAnswers = this.postRegisterSymptomAnswers.concat([
{
drug_id: q.drug_id,
name: q.drug_name,
symptoms: answer,
},
]);
this.advancePostRegisterAfterAnswer();
return;
}
if (q.type === 'confirm') {
if (answer === POST_REGISTER_CONFIRM_HAS_MORE) {
// 进入补充输入,不立刻提交
this.postRegisterStep = 'await_supplement';
this.postRegisterSupplementText = '';
this.$nextTick(() => this.postRegisterScrollToBottom());
return;
}
this.postRegisterFinalSupplement = '';
this.advancePostRegisterAfterAnswer();
}
},
/** 有补充:校验非空后写入气泡并提交 */
onPostRegisterSupplementSubmit() {
if (this.postRegisterStep !== 'await_supplement' || this.submitted) return;
const text = (this.postRegisterSupplementText || '').trim();
if (!text) {
uni.showToast({ title: '请填写补充信息', icon: 'none' });
return;
}
this.postRegisterFinalSupplement = text;
this.postRegisterStep = 'typing_after';
this.postRegisterIsTyping = true;
this.$nextTick(() => this.postRegisterScrollToBottom());
@@ -387,7 +596,6 @@ export default {
setTimeout(() => {
this.postRegisterIsTyping = false;
this.$nextTick(() => this.postRegisterScrollToBottom());
// 与转诊一致:最后一题答完后短延迟再自动提交
setTimeout(() => {
this.submitClinicPostRegister();
}, 500);
@@ -396,11 +604,11 @@ export default {
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' ? '是' : '否'];
// 失败后回到最后一题,方便用户重试
if (this.postRegisterFinalSupplement) {
this.postRegisterStep = 'await_supplement';
} else {
this.postRegisterUserAnswers = [];
this.postRegisterStep = 'await_answer';
}
this.$nextTick(() => this.postRegisterScrollToBottom());
},
@@ -608,9 +816,13 @@ export default {
this.$nextTick(() => this.postRegisterScrollToBottom());
const val = this.clinicForm.hasUsedDrug;
const answerStatus = val === '1' ? 'yes' : 'no';
const illnessInfo = this.buildPostRegisterIllnessInfo();
const supplement = (this.postRegisterFinalSupplement || '').trim();
confirmFollowUpDrugUseApi({
register_info_id: this.registerInfoId,
has_used_drug: val,
illnessInfo,
supplement,
})
.then(async (res) => {
const d = res.data || {};
@@ -630,6 +842,8 @@ export default {
drugsPayload: this.clinicDrugs,
question: this.clinicQuestion,
answerStatus,
illnessInfo,
supplement,
});
markFollowUpAssistSent(this.postRegisterRegisterId);
}
@@ -930,5 +1144,42 @@ export default {
.safe-area-bottom {
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
}
/* 有补充时底部输入区 */
.supplement-footer {
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.supplement-input {
width: 100%;
min-height: 160rpx;
padding: 20rpx;
box-sizing: border-box;
background: #f8fafc;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.supplement-submit {
margin-top: 20rpx;
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
background: #0a84ff;
color: #fff;
border: none;
}
.supplement-submit[disabled] {
opacity: 0.45;
}
.supplement-submit::after {
border: none;
}
</style>

View File

@@ -332,6 +332,9 @@ export default {
drug_id: Number(d.id),
name: d.drug_name || d.name || '',
quantity: Math.min(99, Math.max(1, Number(d.quantity) || 1)),
// 适用症透传到挂号后助手提问页(一个药一题)
indications: d.indications || '',
indications_array: Array.isArray(d.indications_array) ? d.indications_array : [],
}))
.filter((d) => d.drug_id > 0);
const followUpNumber = followUpDrugs.reduce((s, r) => s + (Number(r.quantity) || 1), 0) || 1;

View File

@@ -5,7 +5,7 @@ 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}> }}
* @returns {{ registerInfoId: string, drugsPayload: Array<{drug_id:number,name:string,quantity:number,indications?:string,indications_array?:Array}> }}
*/
export function resolveFollowUpDrugsFromRInfo(rInfo) {
const empty = { registerInfoId: '', drugsPayload: [] };
@@ -29,10 +29,21 @@ export function resolveFollowUpDrugsFromRInfo(rInfo) {
const row = Number(d.quantity ?? d.number);
const quantity =
Number.isFinite(row) && row >= 1 ? Math.min(99, row) : fallbackQty;
// 规范化适用症数组,兼容字符串 indications
let indicationsArray = Array.isArray(d.indications_array) ? d.indications_array : [];
if (!indicationsArray.length && d.indications) {
indicationsArray = String(d.indications)
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => ({ value: s, label: s }));
}
return {
drug_id: Number(d.drug_id || d.id),
name: d.name || d.drug_name || '药品',
quantity,
indications: d.indications || '',
indications_array: indicationsArray,
};
});
return { registerInfoId: String(infoId), drugsPayload };
@@ -50,6 +61,8 @@ export async function sendFollowUpDrugAssistantMessage({
drugsPayload,
question,
answerStatus,
illnessInfo,
supplement,
}) {
const payload = {
flow: 'follow_up_drug',
@@ -58,6 +71,9 @@ export async function sendFollowUpDrugAssistantMessage({
drugs: drugsPayload,
question: question || '您是否曾使用过以上药品?',
answer_status: answerStatus,
// 症状摘要给医生聊天侧展示
illness_info: illnessInfo || '',
supplement: supplement || '',
};
await sendToUserApi({
room_id: roomId,