-
-
+
+
+
+ onPickOnlineTcm('method', item)"
+ />
+
+
+
+ {{ methodSelectedLabel || '请通过搜索选择中医治法' }}
-
-
+
-
-
+
+
+
+ onPickOnlineTcm('syndrome', item)"
+ />
+
+
+
+ {{ syndromeSelectedLabel || '请通过搜索选择中医疾病' }}
-
-
@@ -1913,6 +2213,8 @@ const cancelSaveCommonPrescription = () => {
:patient-age="Number(prescriptionStore.activePatient?.age || 0)"
:storage-prefix="medicalRecordStoragePrefix"
:show-toolbar="false"
+ @sync-to-prescription="onMrSyncToPrescription"
+ @apply-tcm-ids="onApplyAiTcmIds"
/>
@@ -1994,6 +2296,11 @@ const cancelSaveCommonPrescription = () => {
+
diff --git a/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue b/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue
index 5714b640..522605c1 100644
--- a/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue
+++ b/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue
@@ -7,7 +7,7 @@
import { computed, ref } from 'vue';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
-import { Button, Checkbox, Descriptions, Input, Select, Spin, Switch, Tag, message } from 'ant-design-vue';
+import { Button, Checkbox, Descriptions, Input, Radio, Spin, Switch, Tag, message } from 'ant-design-vue';
import {
aiGeneratePrescriptionApi,
@@ -20,6 +20,9 @@ import AiDisclaimerBanner from '#/views/doctor/components/AiDisclaimerBanner.vue
import { ensureAiFeatureConsent } from '#/views/doctor/utils/aiFeatureGate';
import PrescriptionLoading from '#/components/loading/PrescriptionLoading.vue';
+/** 本地「正在生成中」占位 id,不与真实 generation_id 冲突 */
+const PENDING_GEN_ID = -1;
+
/** 二级弹窗通栏字段(Descriptions span=2) */
const FULL_SPAN_KEYS = new Set([
'chief_complaint',
@@ -102,11 +105,17 @@ const rxProcessRuleNoteId = ref(0);
const rxProcessRuleName = ref('');
const rxChildProcessRuleName = ref('');
const rxProcessRuleNote = ref('');
-/** 生成弹窗:中药委托调剂选项 */
+/** 生成弹窗:中药委托调剂选项(本地记忆) */
+const AI_RX_ENTRUSTED_STORAGE_KEY = 'xk_ai_rx_use_entrusted_process';
const useEntrustedProcess = ref(false);
const genProcessRuleId = ref
(undefined);
const processRuleOptions = ref>([]);
const processRuleLoading = ref(false);
+/** 委托时是否出推荐剂数/每日几次/详细制剂规则(本地记忆) */
+const AI_RX_DETAIL_STORAGE_KEY = 'xk_ai_rx_return_dosage_process_detail';
+const returnDosageProcessDetail = ref(true);
+/** 制剂要求 id 本地记忆 */
+const AI_RX_PROCESS_RULE_STORAGE_KEY = 'xk_ai_rx_process_rule_id';
/** 二级弹窗:当前正在编辑的字段 key,空表示浏览态 */
const editingKey = ref('');
/** 醒目提示文案(系统配置 / 门闸下发) */
@@ -238,7 +247,6 @@ function resetState() {
medicalRecord.value = {};
chiefComplaint.value = '';
disclaimerText.value = '';
- useEntrustedProcess.value = false;
genProcessRuleId.value = undefined;
processRuleOptions.value = [];
resetPreview();
@@ -270,8 +278,11 @@ function typeLabelOf(t: number | string) {
}
function applyRxMeta(data: any) {
- rxDosage.value = Number(data?.dosage || 0);
- rxDayDosage.value = Number(data?.day_dosage || 0);
+ // 中药剂数/每日次数缺省时用处方页默认,避免预览「—」和导入丢空
+ const dosage = Number(data?.dosage || 0);
+ const dayDosage = Number(data?.day_dosage || 0);
+ rxDosage.value = dosage > 0 ? dosage : isTcmRx.value ? 7 : 0;
+ rxDayDosage.value = dayDosage > 0 ? dayDosage : isTcmRx.value ? 2 : 0;
rxReason.value = String(data?.reason || '').trim();
rxBasis.value = String(data?.basis || '').trim();
rxPrescriptionName.value = String(data?.prescription_name || '').trim();
@@ -297,6 +308,7 @@ async function loadTopProcessRules() {
label: String(item.name || item.title || `#${item.id}`),
value: Number(item.id),
}));
+ applyProcessRulePrefFromStorage();
} catch {
processRuleOptions.value = [];
} finally {
@@ -305,14 +317,98 @@ async function loadTopProcessRules() {
}
function onEntrustedToggle(checked: boolean) {
- if (checked && !processRuleOptions.value.length) {
- void loadTopProcessRules();
- }
- if (!checked) {
+ useEntrustedProcess.value = !!checked;
+ persistUseEntrustedProcessPref(!!checked);
+ if (checked) {
+ if (!processRuleOptions.value.length) {
+ void loadTopProcessRules();
+ } else {
+ applyProcessRulePrefFromStorage();
+ }
+ } else {
genProcessRuleId.value = undefined;
}
}
+/** 标签单选制剂要求:点选写入并记本地 */
+function onProcessRuleTagClick(id: number) {
+ genProcessRuleId.value = Number(id) || undefined;
+ persistProcessRuleIdPref(genProcessRuleId.value);
+}
+
+/** 读取本地:是否勾选委托调剂(未存过默认不勾选) */
+function loadUseEntrustedProcessPref() {
+ try {
+ return localStorage.getItem(AI_RX_ENTRUSTED_STORAGE_KEY) === '1';
+ } catch {
+ return false;
+ }
+}
+
+function persistUseEntrustedProcessPref(v: boolean) {
+ try {
+ localStorage.setItem(AI_RX_ENTRUSTED_STORAGE_KEY, v ? '1' : '0');
+ } catch {
+ /* ignore */
+ }
+}
+
+/** 读取本地:制剂要求 id */
+function loadProcessRuleIdPref() {
+ try {
+ const id = Number(localStorage.getItem(AI_RX_PROCESS_RULE_STORAGE_KEY) || 0);
+ return id > 0 ? id : 0;
+ } catch {
+ return 0;
+ }
+}
+
+function persistProcessRuleIdPref(id?: number) {
+ try {
+ const n = Number(id || 0);
+ if (n > 0) {
+ localStorage.setItem(AI_RX_PROCESS_RULE_STORAGE_KEY, String(n));
+ } else {
+ localStorage.removeItem(AI_RX_PROCESS_RULE_STORAGE_KEY);
+ }
+ } catch {
+ /* ignore */
+ }
+}
+
+/** 列表加载后回填本地记忆的制剂要求(须仍在可选列表中) */
+function applyProcessRulePrefFromStorage() {
+ const prefId = loadProcessRuleIdPref();
+ if (prefId <= 0) return;
+ if (processRuleOptions.value.some((o) => Number(o.value) === prefId)) {
+ genProcessRuleId.value = prefId;
+ }
+}
+
+/** 读取本地:委托时是否出剂数/每日/详细规则 */
+function loadReturnDosageProcessDetailPref() {
+ try {
+ const raw = localStorage.getItem(AI_RX_DETAIL_STORAGE_KEY);
+ // 未存过默认「需要」;显式存 0 才为不需要
+ return raw === null || raw === '' || raw === '1';
+ } catch {
+ return true;
+ }
+}
+
+function persistReturnDosageProcessDetailPref(v: boolean) {
+ try {
+ localStorage.setItem(AI_RX_DETAIL_STORAGE_KEY, v ? '1' : '0');
+ } catch {
+ /* ignore */
+ }
+}
+
+function onReturnDosageProcessDetailChange(v: boolean) {
+ returnDosageProcessDetail.value = !!v;
+ persistReturnDosageProcessDetailPref(!!v);
+}
+
function applyConflict(conflict: any) {
const msgs = Array.isArray(conflict?.messages) ? conflict.messages : [];
conflictMessages.value = msgs
@@ -363,8 +459,16 @@ function openGenerateModal() {
return;
}
editingKey.value = '';
- if (isTcmRx.value && useEntrustedProcess.value && !processRuleOptions.value.length) {
- void loadTopProcessRules();
+ useEntrustedProcess.value = loadUseEntrustedProcessPref();
+ returnDosageProcessDetail.value = loadReturnDosageProcessDetailPref();
+ if (isTcmRx.value && useEntrustedProcess.value) {
+ if (!processRuleOptions.value.length) {
+ void loadTopProcessRules();
+ } else {
+ applyProcessRulePrefFromStorage();
+ }
+ } else {
+ genProcessRuleId.value = undefined;
}
genModalApi.open();
}
@@ -401,6 +505,40 @@ function endEdit() {
editingKey.value = '';
}
+/**
+ * 开始生成:取消旧选中、清空对照预览,历史顶部插入「正在生成中」占位
+ */
+function beginGeneratingPlaceholder() {
+ resetPreview();
+ activeId.value = PENDING_GEN_ID;
+ const rest = (historyList.value || []).filter(
+ (r) => Number(r?.id) !== PENDING_GEN_ID && !r?._pending,
+ );
+ historyList.value = [
+ {
+ id: PENDING_GEN_ID,
+ name: '正在生成中…',
+ prescription_name: '正在生成中…',
+ prescription_type: prescriptionType.value,
+ prescription_type_label: typeLabel.value,
+ item_count: 0,
+ created_at: Math.floor(Date.now() / 1000),
+ _pending: true,
+ },
+ ...rest,
+ ];
+}
+
+/** 生成结束:去掉本地占位 */
+function endGeneratingPlaceholder() {
+ historyList.value = (historyList.value || []).filter(
+ (r) => Number(r?.id) !== PENDING_GEN_ID && !r?._pending,
+ );
+ if (Number(activeId.value) === PENDING_GEN_ID) {
+ activeId.value = 0;
+ }
+}
+
/**
* 二级弹窗点「开始生成」:先关弹窗,回抽屉等待 AI
*/
@@ -430,6 +568,7 @@ async function confirmGenerateAndClose(): Promise {
}
async function runGenerateInDrawer(chief: string) {
+ beginGeneratingPlaceholder();
generating.value = true;
try {
const req: Record = {
@@ -442,24 +581,21 @@ async function runGenerateInDrawer(chief: string) {
chief_complaint: chief,
},
};
- // 中药委托调剂:传制剂要求,后端会补全煎法/规格
+ // 中药委托调剂:传制剂要求;可选是否出剂数/每日/详细规则
if (isTcmRx.value && useEntrustedProcess.value && genProcessRuleId.value) {
req.use_entrusted_process = 1;
req.process_rule_id = genProcessRuleId.value;
+ req.return_dosage_process_detail = returnDosageProcessDetail.value ? 1 : 0;
}
const data = await aiGeneratePrescriptionApi(req);
const durationText = formatDurationMs(data?.duration_ms);
// 业务软失败:HTTP 成功但 ok=false,警告提示而非 error
if (data?.ok === false) {
- matchedList.value = [];
- unmatchedList.value = [];
- conflictMessages.value = [];
- applyRxMeta({
- duration_ms: data.duration_ms,
- });
+ endGeneratingPlaceholder();
+ resetPreview();
if (data?.generation_id) {
- activeId.value = Number(data.generation_id);
await loadHistory();
+ activeId.value = Number(data.generation_id);
}
message.warning(String(data?.message || '生成未成功'));
return;
@@ -473,6 +609,8 @@ async function runGenerateInDrawer(chief: string) {
durationText ? `已生成(耗时 ${durationText})` : '已生成,请确认导入',
);
} catch (e: any) {
+ endGeneratingPlaceholder();
+ resetPreview();
message.error(e?.message || e?.msg || '生成失败');
} finally {
generating.value = false;
@@ -480,7 +618,7 @@ async function runGenerateInDrawer(chief: string) {
}
async function onSelectHistory(row: any) {
- if (!row?.id) return;
+ if (!row?.id || Number(row.id) === PENDING_GEN_ID || row._pending) return;
activeId.value = Number(row.id);
matchLoading.value = true;
try {
@@ -582,9 +720,15 @@ function onConfirmImport() {
process_rule_note_id?: number;
} = {
rows: drugs,
- dosage: rxDosage.value || undefined,
- day_dosage: rxDayDosage.value || undefined,
};
+ // 中药导入必须带剂数/每日次数,缺省用处方页默认 7/2
+ if (isTcmRx.value) {
+ importPayload.dosage = rxDosage.value > 0 ? rxDosage.value : 7;
+ importPayload.day_dosage = rxDayDosage.value > 0 ? rxDayDosage.value : 2;
+ } else {
+ if (rxDosage.value > 0) importPayload.dosage = rxDosage.value;
+ if (rxDayDosage.value > 0) importPayload.day_dosage = rxDayDosage.value;
+ }
if (rxProcessRuleId.value > 0) {
importPayload.rule_type = 2;
importPayload.process_rule_id = rxProcessRuleId.value;
@@ -647,16 +791,27 @@ defineExpose({
class="mb-1 cursor-pointer rounded-md px-2 py-1.5 text-xs transition-colors hover:bg-accent"
:class="{
'bg-primary/10 ring-1 ring-primary/20': activeId === Number(row.id),
+ 'cursor-default border border-dashed border-primary/40 bg-primary/5':
+ Number(row.id) === PENDING_GEN_ID || row._pending,
}"
@click="onSelectHistory(row)"
>
- {{ row.name || row.prescription_name || `方案 #${row.id}` }}
+ {{
+ Number(row.id) === PENDING_GEN_ID || row._pending
+ ? '正在生成中…'
+ : row.name || row.prescription_name || `方案 #${row.id}`
+ }}
- {{ row.prescription_type_label || typeLabelOf(row.prescription_type) }}
- · {{ row.item_count || 0 }}味 · {{ formatTime(row.created_at) }}
- · {{ formatDurationMs(row.duration_ms) }}
+
+ 请稍候,右侧为生成进度
+
+
+ {{ row.prescription_type_label || typeLabelOf(row.prescription_type) }}
+ · {{ row.item_count || 0 }}味 · {{ formatTime(row.created_at) }}
+ · {{ formatDurationMs(row.duration_ms) }}
+