From 335b3535d52293c78665cc5e2c4246ecb5bed39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 8 Aug 2026 18:36:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=91=E6=96=B9=E7=9B=B8=E5=85=B3=20?= =?UTF-8?q?feat:=20=E6=82=A3=E8=80=85=E4=BF=A1=E6=81=AF=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E8=AF=81=E6=94=B9=E6=88=90=E9=9D=9E=E5=BF=85=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/_shared/drug-detail-modal.vue | 204 +++++++++------ .../product/_shared/drug-warehouse-panel.vue | 220 ++++++++++++++++ .../admin/api/index.ts | 31 +++ .../delivery-fee-reallocate-modal.vue | 216 +++++++++++++++ .../admin/components/modal.vue | 213 ++++++++------- .../warehouse-drug-management/admin/index.vue | 39 +++ .../medical-record/MedicalRecordPanel.vue | 61 +++-- .../components/AiMedicalRecordModal.vue | 191 ++++++++++++-- .../api/index.ts | 10 +- .../components/audit-modal.vue | 246 ++++++++++++++++-- .../index.vue | 36 +-- 11 files changed, 1184 insertions(+), 283 deletions(-) create mode 100644 apps/web-antd/src/views/business/product/_shared/drug-warehouse-panel.vue create mode 100644 apps/web-antd/src/views/business/warehouse-drug-management/admin/components/delivery-fee-reallocate-modal.vue diff --git a/apps/web-antd/src/views/business/product/_shared/drug-detail-modal.vue b/apps/web-antd/src/views/business/product/_shared/drug-detail-modal.vue index e7db50ed..8c5dfb1a 100644 --- a/apps/web-antd/src/views/business/product/_shared/drug-detail-modal.vue +++ b/apps/web-antd/src/views/business/product/_shared/drug-detail-modal.vue @@ -1,6 +1,7 @@ + + diff --git a/apps/web-antd/src/views/business/warehouse-drug-management/admin/api/index.ts b/apps/web-antd/src/views/business/warehouse-drug-management/admin/api/index.ts index 3f3a2ef2..7c8216a4 100644 --- a/apps/web-antd/src/views/business/warehouse-drug-management/admin/api/index.ts +++ b/apps/web-antd/src/views/business/warehouse-drug-management/admin/api/index.ts @@ -161,3 +161,34 @@ export async function applyBatchPriceApi(data: { new_count: number; }>(`${prefix}apply-batch-price`, data); } + +/** 药品仓库信息:总仓 + 门店价 + 配送仓绑定 */ +export async function getDrugWarehouseInfo(drugId: number) { + return requestClient.get(`${prefix}drug-warehouse-info`, { + params: { drug_id: drugId }, + }); +} + +/** 按药品批量改门店供货价/售价 */ +export async function updateStorePricesByDrug(data: { + drug_id: number; + items: Array<{ id: number; price: number; buy_price: number }>; +}) { + return requestClient.post(`${prefix}update-store-prices-by-drug`, data); +} + +/** 总仓改价并重分配配送仓三费 */ +export async function updateWarehouseWithDeliveryFees(data: { + id: number; + drug_id?: number; + market_price: number; + price: number; + delivery_binds: Array<{ + id: number; + quote: number; + promo_fee: number; + platform_fee: number; + }>; +}) { + return requestClient.post(`${prefix}update-with-delivery-fees`, data); +} diff --git a/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/delivery-fee-reallocate-modal.vue b/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/delivery-fee-reallocate-modal.vue new file mode 100644 index 00000000..f2870c71 --- /dev/null +++ b/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/delivery-fee-reallocate-modal.vue @@ -0,0 +1,216 @@ + + + diff --git a/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/modal.vue b/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/modal.vue index 0b4bb395..a71cab8d 100644 --- a/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/modal.vue +++ b/apps/web-antd/src/views/business/warehouse-drug-management/admin/components/modal.vue @@ -1,4 +1,8 @@ + diff --git a/apps/web-antd/src/views/doctor/medical-record/components/AiMedicalRecordModal.vue b/apps/web-antd/src/views/doctor/medical-record/components/AiMedicalRecordModal.vue index c6f294a0..ffa1f29e 100644 --- a/apps/web-antd/src/views/doctor/medical-record/components/AiMedicalRecordModal.vue +++ b/apps/web-antd/src/views/doctor/medical-record/components/AiMedicalRecordModal.vue @@ -16,9 +16,14 @@ import { aiListGenerations, } from '../api'; import { MEDICAL_RECORD_FIELD_OPTIONS } from '../config/constants'; +import TextareaExpandModal from './TextareaExpandModal.vue'; import AiDisclaimerBanner from '#/views/doctor/components/AiDisclaimerBanner.vue'; import { ensureAiFeatureConsent } from '#/views/doctor/utils/aiFeatureGate'; import PrescriptionLoading from '#/components/loading/PrescriptionLoading.vue'; +import { getSystemConfigByKeys } from '#/views/system/system-config/api'; + +/** 常用|我的展示位置(与病历面板一致,供放大弹窗 chips/气泡) */ +type MrCommonDisplayMode = 'tags' | 'bubble' | 'both'; /** 本地「正在生成中」占位 id,不与真实 generation_id 冲突 */ const PENDING_GEN_ID = -1; @@ -93,6 +98,8 @@ const emit = defineEmits<{ } | null; }, ): void; + /** 生成弹窗里改字段时即时回写病历面板(与 AI 出方 sync-medical-record 同协议) */ + (e: 'sync-medical-record', payload: Record): void; }>(); const registerId = ref(0); @@ -136,6 +143,33 @@ const previewRows = computed(() => })).filter((r) => r.value !== ''), ); +/** + * 二级「生成病历」弹窗行:主诉始终展示 + 选填字段(空也展示,可点改) + * UI 与 AI 出方 GenModal 一致:Descriptions 浏览态 / 点击进入 TextArea + */ +const genPreviewRows = computed(() => { + const rows: Array<{ key: string; label: string }> = [ + { key: 'chief_complaint', label: '主诉' }, + ]; + for (const f of GEN_OPTIONAL_FIELDS) { + rows.push({ key: f.value, label: f.label }); + } + return rows; +}); + +/** 二级弹窗:当前正在编辑的字段 key,空表示浏览态 */ +const editingKey = ref(''); +/** 单击延迟定时器:与双击放大区分开,避免双击先误进行内编辑 */ +let genFieldClickTimer: ReturnType | null = null; +const commonDisplayMode = ref('tags'); +const showCommonTags = computed( + () => commonDisplayMode.value === 'tags' || commonDisplayMode.value === 'both', +); +const showCommonInBubble = computed( + () => + commonDisplayMode.value === 'bubble' || commonDisplayMode.value === 'both', +); + const [Drawer, drawerApi] = useVbenDrawer({ title: 'AI写病历', class: 'w-[860px]', @@ -188,6 +222,7 @@ const [Drawer, drawerApi] = useVbenDrawer({ drawerApi.close(); return; } + void loadCommonDisplayMode(); void bootstrapHistory(); }, }); @@ -201,6 +236,11 @@ const [GenModal, genModalApi] = useVbenModal({ onConfirm: async () => confirmGenerateAndClose(), }); +/** 与病历面板同一套双击放大编辑 */ +const [ExpandModal, expandModalApi] = useVbenModal({ + connectedComponent: TextareaExpandModal, +}); + function resetState() { histLoading.value = false; generating.value = false; @@ -213,9 +253,30 @@ function resetState() { chiefComplaint.value = ''; genContext.value = emptyGenContext(); disclaimerText.value = ''; + editingKey.value = ''; + if (genFieldClickTimer) { + clearTimeout(genFieldClickTimer); + genFieldClickTimer = null; + } + expandModalApi.close(); genModalApi.close(); } +/** 拉取常用词条展示模式,放大弹窗 chips/气泡与病历页一致 */ +async function loadCommonDisplayMode() { + try { + const res = await getSystemConfigByKeys(['mr_common_display_mode']); + const mode = String( + res?.mr_common_display_mode ?? res?.data?.mr_common_display_mode ?? 'tags', + ) as MrCommonDisplayMode; + if (mode === 'tags' || mode === 'bubble' || mode === 'both') { + commonDisplayMode.value = mode; + } + } catch { + commonDisplayMode.value = 'tags'; + } +} + function formatTime(ts: number) { const n = Number(ts || 0); if (!n) return '—'; @@ -262,9 +323,93 @@ async function bootstrapHistory() { } function openGenerateModal() { + editingKey.value = ''; genModalApi.open(); } +/** 点字段进入行内编辑(与 AI 出方二级弹窗同一交互) */ +function startEdit(key: string) { + editingKey.value = key; +} + +function fieldValue(key: string) { + if (key === 'chief_complaint') return chiefComplaint.value; + return String(genContext.value?.[key] ?? ''); +} + +/** + * 生成弹窗字段编辑:主诉写 chiefComplaint,其余写 genContext + * 同时 emit 回写病历面板,避免只改弹窗本地态、父页表单仍是旧值 + */ +function onFieldInput(key: string, val: string) { + if (key === 'chief_complaint') { + chiefComplaint.value = val; + } else { + genContext.value = { + ...genContext.value, + [key]: val, + }; + } + emit('sync-medical-record', { [key]: val }); +} + +function endEdit() { + editingKey.value = ''; +} + +/** + * 单击延迟进项内编辑:若随后是双击则取消,改为打开放大弹窗 + * (否则双击的第一次 click 会先切到 TextArea,导致 dblclick 丢事件) + */ +function onGenFieldClick(key: string) { + if (genFieldClickTimer) clearTimeout(genFieldClickTimer); + genFieldClickTimer = setTimeout(() => { + genFieldClickTimer = null; + startEdit(key); + }, 250); +} + +function onGenFieldDblClick(key: string, label: string) { + if (genFieldClickTimer) { + clearTimeout(genFieldClickTimer); + genFieldClickTimer = null; + } + openGenFieldExpand(key, label); +} + +/** + * 行内 TextArea 用 mousedown.detail===2 捕获双击 + * 为什么不用 dblclick:blur 会先卸载 TextArea,导致 dblclick 丢事件 + */ +function onGenTextareaMouseDown(e: MouseEvent, key: string, label: string) { + if (e.detail === 2) { + e.preventDefault(); + openGenFieldExpand(key, label); + } +} + +/** + * 双击字段:打开与病历页同一套 TextareaExpandModal(词条搜索 + 常用) + * joinMode/source 规则对齐 MedicalRecordPanel.openFieldExpand + */ +function openGenFieldExpand(key: string, label: string) { + editingKey.value = ''; + const joinMode = + key === 'tongue' || key === 'pulse' ? 'replace' : 'newline'; + expandModalApi.setData({ + title: label, + value: fieldValue(key) || '', + onConfirm: (v: string) => onFieldInput(key, v), + storeId: storeId.value, + source: 'entry', + fieldCode: key, + showChips: showCommonTags.value, + showCommonInBubble: showCommonInBubble.value, + joinMode, + }); + expandModalApi.open(); +} + async function onSelectHistory(row: any) { if (!row?.id || Number(row.id) === PENDING_GEN_ID || row._pending) return; activeId.value = Number(row.id); @@ -544,13 +689,13 @@ defineExpose({ - +
{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || '—' }}岁 · - 主诉必填;下方选填有内容会作为 AI 参考(若该字段在「病历配置」中需生成则保留原文) + 主诉必填;单击编辑,双击放大(同病历页);选填有内容会作为 AI 参考
- - - +
+ {{ fieldValue(row.key) || '(空,单击填写 / 双击放大)' }} +
-
- 点击「开始生成」后将关闭本弹窗,请在抽屉中等待结果。 -
+