From 92149f43a619476acf399763d23cba3330aa216a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 18 Jul 2026 14:16:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=201.=20=E4=BC=98=E5=8C=96=E4=BA=86?= =?UTF-8?q?=E5=AE=A1=E6=96=B9pc=E7=9A=84=E6=95=88=E6=9E=9C=202.=20?= =?UTF-8?q?=E5=B0=B1=E8=AF=8A=E4=BA=BA=E4=BF=A1=E6=81=AF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/.env.production | 3 +- .../PrescriptionExpireTime.vue | 112 ++++ .../wx-user-patient/VisitTypeTag.vue | 25 + .../wx-user-patient/WxUserPatientCell.vue | 103 +++ .../wx-user-patient/WxUserPatientDrawer.vue | 357 ++++++++++ .../src/components/wx-user-patient/api.ts | 43 ++ .../src/components/wx-user-patient/index.ts | 8 + .../business/order/product-order/api/index.ts | 5 +- .../business/order/product-order/index.vue | 259 +++++--- .../components/PrescriptionDetail.vue | 608 ++++++++++-------- .../components/AuditPrescriptionCardList.vue | 273 ++++++++ .../components/DoctorAvatarCell.vue | 45 ++ .../components/MedicineTypeTag.vue | 18 + .../components/PrescriptionInfoCell.vue | 129 ++++ .../components/TimeCell.vue | 23 + .../audit-prescription/components/modal.vue | 43 +- .../audit-prescription/config/search.ts | 145 ++++- .../audit-prescription/config/table.ts | 103 ++- .../pharmacist/audit-prescription/index.vue | 324 ++++++---- .../utils/viewModeStorage.ts | 26 + .../components/modal.vue | 20 +- .../delivery-warehouse-drug/config/form.ts | 98 ++- .../components/stock-log-drawer.vue | 153 +++-- 23 files changed, 2299 insertions(+), 624 deletions(-) create mode 100644 apps/web-antd/src/components/wx-user-patient/PrescriptionExpireTime.vue create mode 100644 apps/web-antd/src/components/wx-user-patient/VisitTypeTag.vue create mode 100644 apps/web-antd/src/components/wx-user-patient/WxUserPatientCell.vue create mode 100644 apps/web-antd/src/components/wx-user-patient/WxUserPatientDrawer.vue create mode 100644 apps/web-antd/src/components/wx-user-patient/api.ts create mode 100644 apps/web-antd/src/components/wx-user-patient/index.ts create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/components/AuditPrescriptionCardList.vue create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/components/DoctorAvatarCell.vue create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/components/MedicineTypeTag.vue create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/components/PrescriptionInfoCell.vue create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/components/TimeCell.vue create mode 100644 apps/web-antd/src/views/pharmacist/audit-prescription/utils/viewModeStorage.ts diff --git a/apps/web-antd/.env.production b/apps/web-antd/.env.production index f56882a7..4b7546c0 100644 --- a/apps/web-antd/.env.production +++ b/apps/web-antd/.env.production @@ -20,4 +20,5 @@ VITE_INJECT_APP_LOADING=true VITE_ARCHIVER=true # WebSocket 连接地址 -VITE_WS_URL=wss://api.ws.g.xiaokang88.com/ws +# VITE_WS_URL=wss://api.ws.g.xiaokang88.com/ws +VITE_WS_URL=wss://xk.ws.nailaoyun.cn/ws diff --git a/apps/web-antd/src/components/wx-user-patient/PrescriptionExpireTime.vue b/apps/web-antd/src/components/wx-user-patient/PrescriptionExpireTime.vue new file mode 100644 index 00000000..7febde19 --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/PrescriptionExpireTime.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/apps/web-antd/src/components/wx-user-patient/VisitTypeTag.vue b/apps/web-antd/src/components/wx-user-patient/VisitTypeTag.vue new file mode 100644 index 00000000..de662d27 --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/VisitTypeTag.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/web-antd/src/components/wx-user-patient/WxUserPatientCell.vue b/apps/web-antd/src/components/wx-user-patient/WxUserPatientCell.vue new file mode 100644 index 00000000..88de109a --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/WxUserPatientCell.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/web-antd/src/components/wx-user-patient/WxUserPatientDrawer.vue b/apps/web-antd/src/components/wx-user-patient/WxUserPatientDrawer.vue new file mode 100644 index 00000000..dccfd521 --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/WxUserPatientDrawer.vue @@ -0,0 +1,357 @@ + + + diff --git a/apps/web-antd/src/components/wx-user-patient/api.ts b/apps/web-antd/src/components/wx-user-patient/api.ts new file mode 100644 index 00000000..229c3263 --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/api.ts @@ -0,0 +1,43 @@ +/** + * 就诊人档案 API(跨医生视角,供药师审方等场景) + */ +import { requestClient } from '#/api/request'; + +const prefix = 'user-patient-profile/'; + +/** 小程序用户 + 就诊人基础信息 */ +export async function getUserPatientProfileDetailApi(upId: number) { + return requestClient.get(`${prefix}detail`, { + params: { up_id: upId }, + }); +} + +/** 就诊人挂号记录 */ +export async function getUserPatientRegisterListApi( + upId: number, + params?: { page?: number; pageSize?: number }, +) { + return requestClient.get(`${prefix}register-list`, { + params: { up_id: upId, ...params }, + }); +} + +/** 就诊人处方记录 */ +export async function getUserPatientPrescriptionListApi( + upId: number, + params?: { page?: number; pageSize?: number }, +) { + return requestClient.get(`${prefix}prescription-list`, { + params: { up_id: upId, ...params }, + }); +} + +/** 就诊人商品订单记录 */ +export async function getUserPatientOrderListApi( + upId: number, + params?: { page?: number; pageSize?: number }, +) { + return requestClient.get(`${prefix}order-list`, { + params: { up_id: upId, ...params }, + }); +} diff --git a/apps/web-antd/src/components/wx-user-patient/index.ts b/apps/web-antd/src/components/wx-user-patient/index.ts new file mode 100644 index 00000000..184ca52f --- /dev/null +++ b/apps/web-antd/src/components/wx-user-patient/index.ts @@ -0,0 +1,8 @@ +/** + * 微信用户/就诊人相关组件导出 + */ +export { default as WxUserPatientCell } from './WxUserPatientCell.vue'; +export { default as WxUserPatientDrawer } from './WxUserPatientDrawer.vue'; +export { default as VisitTypeTag } from './VisitTypeTag.vue'; +export { default as PrescriptionExpireTime } from './PrescriptionExpireTime.vue'; +export * from './api'; diff --git a/apps/web-antd/src/views/business/order/product-order/api/index.ts b/apps/web-antd/src/views/business/order/product-order/api/index.ts index b6ea9908..f45b2456 100644 --- a/apps/web-antd/src/views/business/order/product-order/api/index.ts +++ b/apps/web-antd/src/views/business/order/product-order/api/index.ts @@ -93,8 +93,9 @@ export async function syncLegacyShipmentApi(orderId: number) { */ export async function changeDeliveryWarehouseApi(data: { order_id: number; - from_warehouse_id: number; - to_warehouse_id: number; + from_warehouse_id?: number; + to_warehouse_id?: number; + items?: Array<{ order_item_id: number; to_warehouse_id: number }>; }) { return requestClient.post(`${prefix}change-delivery-warehouse`, data); } diff --git a/apps/web-antd/src/views/business/order/product-order/index.vue b/apps/web-antd/src/views/business/order/product-order/index.vue index 94b403fa..db5a5ce2 100644 --- a/apps/web-antd/src/views/business/order/product-order/index.vue +++ b/apps/web-antd/src/views/business/order/product-order/index.vue @@ -497,77 +497,74 @@ async function handleCancelOrder(row: Record) { } } -/** 订单页改仓弹窗状态 */ +/** 订单页改仓弹窗状态(按药分别选仓) */ const changeWhOpen = ref(false); const changeWhSubmitting = ref(false); const changeWhLoading = ref(false); const changeWhOrderId = ref(0); const changeWhFromId = ref(0); const changeWhFromName = ref(''); -const changeWhToId = ref(undefined); -const changeWhOptions = ref< - Array<{ warehouse_id: number; warehouse_name: string; quote: string }> +/** 弹窗内药品行:每药独立选 to_warehouse_id */ +const changeWhDrugRows = ref< + Array<{ + order_item_id: number; + drug_id: number; + drug_name: string; + image: string; + specification: string; + number: number; + from_warehouse_id: number; + to_warehouse_id: number; + options: Array<{ + warehouse_id: number; + warehouse_name: string; + quote: string | null; + disabled?: boolean; + }>; + }> >([]); +const changeWhPendingCount = computed( + () => + changeWhDrugRows.value.filter( + (r) => Number(r.to_warehouse_id) !== Number(r.from_warehouse_id), + ).length, +); + /** - * 改仓卡片选项:首项本仓库 + 可选配送仓 - * 当前已是本仓时本仓库卡片 disabled + * 为单药构造仓卡片:本仓库 + 该药 options(排除不可用) */ -const changeWhCardOptions = computed(() => { +function buildDrugWarehouseCards( + drugOptions: any[], + _fromWarehouseId: number, +): Array<{ + warehouse_id: number; + warehouse_name: string; + quote: string | null; + disabled?: boolean; +}> { const local = { warehouse_id: 0, warehouse_name: '萧康医药本仓库', quote: null as string | null, - disabled: changeWhFromId.value === 0, }; - return [ - local, - ...changeWhOptions.value.map((o) => ({ - ...o, - quote: o.quote as string | null, - disabled: false, - })), - ]; -}); - -/** - * 将 options-by-drugs 按仓去重(取最低 quote),供改仓下拉使用 - */ -function flattenWarehouseOptions( - map: Record | null | undefined, -): Array<{ warehouse_id: number; warehouse_name: string; quote: string }> { - const byId = new Map< - number, - { warehouse_id: number; warehouse_name: string; quote: string } - >(); - if (!map || typeof map !== 'object') { - return []; + const seen = new Set([0]); + const cards = [local]; + for (const opt of drugOptions || []) { + const id = Number(opt?.warehouse_id ?? 0); + if (id <= 0 || seen.has(id)) continue; + seen.add(id); + cards.push({ + warehouse_id: id, + warehouse_name: String(opt?.warehouse_name ?? `仓库#${id}`), + quote: String(opt?.quote ?? '0'), + }); } - for (const opts of Object.values(map)) { - if (!Array.isArray(opts)) { - continue; - } - for (const opt of opts) { - const id = Number(opt?.warehouse_id ?? 0); - if (id <= 0) { - continue; - } - const quote = String(opt?.quote ?? '0'); - const prev = byId.get(id); - if (!prev || Number(quote) < Number(prev.quote)) { - byId.set(id, { - warehouse_id: id, - warehouse_name: String(opt?.warehouse_name ?? `仓库#${id}`), - quote, - }); - } - } - } - return [...byId.values()].sort((a, b) => Number(a.quote) - Number(b.quote)); + return cards; } /** - * 点击仓库 Tag:打开改仓弹窗(首项本仓库 + 配送仓列表) + * 点击仓库 Tag:打开按药改仓弹窗 */ async function openChangeWarehouse( row: Record, @@ -576,39 +573,61 @@ async function openChangeWarehouse( changeWhOrderId.value = Number(row.id ?? 0); changeWhFromId.value = Number(wh?.id ?? 0); changeWhFromName.value = String(wh?.name ?? ''); - changeWhToId.value = undefined; - changeWhOptions.value = []; + changeWhDrugRows.value = []; changeWhOpen.value = true; changeWhLoading.value = true; try { const items = Array.isArray(row.product_order_items) ? row.product_order_items : []; - // 优先取当前仓名下药品;若无 delivery_warehouse_id 字段则退回整单药品 const fromId = changeWhFromId.value; const scoped = items.filter((it: any) => { - if (it?.delivery_warehouse_id === undefined || it?.delivery_warehouse_id === null) { + if ( + it?.delivery_warehouse_id === undefined || + it?.delivery_warehouse_id === null + ) { return true; } const wid = Number(it.delivery_warehouse_id ?? 0); return fromId > 0 ? wid === fromId : wid <= 0; }); + const list = scoped.length ? scoped : items; const drugIds = [ ...new Set( - (scoped.length ? scoped : items) + list .map((it: any) => Number(it?.drug_id ?? 0)) .filter((id: number) => id > 0), ), ]; - if (drugIds.length === 0) { + if (list.length === 0) { + message.warning('该仓库下没有可改仓的药品'); return; } - const res = await getDeliveryWarehouseOptionsByDrugs({ drug_ids: drugIds }); - const flattened = flattenWarehouseOptions(res); - // 排除当前仓,避免无意义提交 - changeWhOptions.value = flattened.filter( - (o) => o.warehouse_id !== changeWhFromId.value, - ); + const res = + drugIds.length > 0 + ? await getDeliveryWarehouseOptionsByDrugs({ drug_ids: drugIds }) + : {}; + const map = (res || {}) as Record; + changeWhDrugRows.value = list.map((it: any) => { + const drugId = Number(it?.drug_id ?? 0); + const itemId = Number(it?.id ?? 0); + const opts = map[String(drugId)] || map[drugId] || []; + const fromWid = Number(it?.delivery_warehouse_id ?? fromId ?? 0); + return { + order_item_id: itemId, + drug_id: drugId, + drug_name: String(it?.drug_name || it?.name || `药品#${drugId}`), + image: String(it?.image || it?.drug?.image || ''), + specification: String( + it?.specification || it?.drug?.specification || '', + ), + number: Number(it?.number ?? it?.select_number ?? 1), + from_warehouse_id: fromWid, + // 默认仍为当前仓,用户需主动点选目标仓 + to_warehouse_id: fromWid, + options: buildDrugWarehouseCards(opts, fromWid), + }; + }); } catch (error: any) { message.error(error?.message || '加载配送仓库失败'); } finally { @@ -616,14 +635,16 @@ async function openChangeWarehouse( } } -/** 确认改仓 */ +/** 确认按药改仓 */ async function submitChangeWarehouse() { - if (changeWhToId.value === undefined || changeWhToId.value === null) { - message.warning('请选择目标仓库'); - return; - } - if (Number(changeWhToId.value) === Number(changeWhFromId.value)) { - message.warning('目标仓库与当前仓库相同'); + const items = changeWhDrugRows.value + .filter((r) => Number(r.to_warehouse_id) !== Number(r.from_warehouse_id)) + .map((r) => ({ + order_item_id: r.order_item_id, + to_warehouse_id: Number(r.to_warehouse_id), + })); + if (items.length === 0) { + message.warning('请至少为一种药品选择新的配送仓库'); return; } changeWhSubmitting.value = true; @@ -631,9 +652,9 @@ async function submitChangeWarehouse() { await changeDeliveryWarehouseApi({ order_id: changeWhOrderId.value, from_warehouse_id: changeWhFromId.value, - to_warehouse_id: Number(changeWhToId.value), + items, }); - message.success('改仓成功'); + message.success(`改仓成功(${items.length} 种药品)`); changeWhOpen.value = false; await gridApi.query(); } catch (error: any) { @@ -1349,51 +1370,83 @@ const openOrderAmountVerify = () => { - +
当前仓库:{{ changeWhFromName || '—' }} + + · 将改仓 {{ changeWhPendingCount }} 种药品 +
加载可选仓库…
-
- +
- 暂无其它可用配送仓库 +
+ +
+ 无图 +
+
+
+ {{ row.drug_name }} +
+
+ 规格:{{ row.specification || '--' }} · 数量 {{ row.number }} +
+
+
+
+ +
+
+
+ 暂无可改仓药品
diff --git a/apps/web-antd/src/views/doctor/doctor-reception/components/PrescriptionDetail.vue b/apps/web-antd/src/views/doctor/doctor-reception/components/PrescriptionDetail.vue index f085042e..591257a3 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/components/PrescriptionDetail.vue +++ b/apps/web-antd/src/views/doctor/doctor-reception/components/PrescriptionDetail.vue @@ -1,15 +1,28 @@ @@ -322,111 +412,97 @@ function parseRecipeContent(content: string | Record) { padding: 20px; font-family: 'SimSun', serif; } - .prescription-header { border-bottom: 2px solid #000; padding-bottom: 15px; margin-bottom: 20px; } - .header-top { display: flex; justify-content: space-between; margin-bottom: 15px; } - .prescription-type { border: 1px solid #666; padding: 2px 8px; border-radius: 4px; } - .clinic-name { text-align: center; font-size: 24px; margin: 15px 0; } - -.patient-info .info-row { +.prescription-date { + text-align: right; +} +.patient-info { + margin-bottom: 20px; +} +.info-row { display: flex; justify-content: space-between; margin: 8px 0; } - .medicine-list { - margin: 20px 0 200px 0; - border-top: 1px solid #ccc; - padding-top: 15px; + min-height: 200px; + border-top: 1px dashed #666; + border-bottom: 1px dashed #666; + padding: 15px 0; } - .rp-title { - font-size: 32px; font-weight: bold; + font-size: 18px; + margin-bottom: 10px; +} +.recipe-item { margin-bottom: 15px; } - -.recipe-item { - margin-bottom: 25px; -} - .medicine-item { display: flex; justify-content: space-between; - margin: 8px 0; - padding: 4px 0; + margin: 5px 0; } -.chinese-item { - width: 28%; -} - .drug-name { - font-weight: 500; -} - -.drug-spec { - font-size: 12px; - color: #666; - margin-left: 8px; - font-weight: normal; -} - -.preparation-info { - color: #666; - margin-top: 12px; - font-size: 0.9em; -} - -.footer-section { - position: absolute; - bottom: 0; - margin-top: 30px; -} - -.medical-advice { - color: #c00; - margin-bottom: 20px; -} - -.signature-area { - display: flex; - justify-content: space-between; - margin-top: 40px; -} - -.signature { - min-width: 200px; -} - -.price-info { - margin-top: 20px; - position: absolute; - right: 0; font-weight: bold; } - -.validity { +.drug-spec { + margin-left: 6px; + font-weight: normal; color: #666; - text-align: center; - margin-top: 25px; +} +.usage-info { + width: 100%; + font-size: 12px; + color: #666; +} +.preparation-info { + margin-top: 8px; + color: #333; +} +.footer-section { + margin-top: 20px; +} +.medical-advice { + margin-bottom: 15px; +} +.signature-area { + display: flex; + flex-wrap: wrap; + gap: 12px 24px; + margin: 15px 0; +} +.signature { + display: flex; + align-items: center; + gap: 8px; +} +.price-info { + text-align: right; + font-weight: bold; + margin-top: 8px; +} +.validity { + text-align: right; + margin-top: 8px; } diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/AuditPrescriptionCardList.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/AuditPrescriptionCardList.vue new file mode 100644 index 00000000..2d497adb --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/AuditPrescriptionCardList.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/DoctorAvatarCell.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/DoctorAvatarCell.vue new file mode 100644 index 00000000..987f70ca --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/DoctorAvatarCell.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/MedicineTypeTag.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/MedicineTypeTag.vue new file mode 100644 index 00000000..f1fe8c58 --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/MedicineTypeTag.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/PrescriptionInfoCell.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/PrescriptionInfoCell.vue new file mode 100644 index 00000000..47bb0b59 --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/PrescriptionInfoCell.vue @@ -0,0 +1,129 @@ + + + diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/TimeCell.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/TimeCell.vue new file mode 100644 index 00000000..646cefaf --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/TimeCell.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/components/modal.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/components/modal.vue index d7369e92..6768d3f8 100644 --- a/apps/web-antd/src/views/pharmacist/audit-prescription/components/modal.vue +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/components/modal.vue @@ -1,4 +1,8 @@ diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/config/search.ts b/apps/web-antd/src/views/pharmacist/audit-prescription/config/search.ts index 08ec11bc..33d4dcbf 100644 --- a/apps/web-antd/src/views/pharmacist/audit-prescription/config/search.ts +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/config/search.ts @@ -1,67 +1,146 @@ import type { VbenFormProps } from '#/adapter/form'; -// import dayjs from 'dayjs'; +import { + getDoctorOptionApi, + getStoreOption, +} from '#/views/system/store/api'; +/** + * 审方页统一搜索表单(列表 / 卡片共用) + */ export const formOptions: VbenFormProps = { - // 默认展开 - collapsed: false, + collapsed: true, + compact: true, + commonConfig: { + labelWidth: 80, + componentProps: { + class: 'w-full', + }, + }, + wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4', schema: [ { component: 'VbenInput', componentProps: { - placeholder: '输入订单号', + allowClear: true, + placeholder: '处方订单号', }, - defaultValue: '', fieldName: 'prescription_no', label: '订单号', }, + { + component: 'VbenInput', + componentProps: { + allowClear: true, + placeholder: '就诊人姓名(模糊)', + }, + fieldName: 'patient_name', + label: '患者', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + showSearch: true, + filterOption: true, + afterFetch: (data: { id: number; name: string }[]) => { + return (data || []).map((item: any) => ({ + label: `${item.name}【${item.id}】`, + value: item.id, + })); + }, + api: getStoreOption, + placeholder: '请选择诊所', + }, + fieldName: 'store_id', + label: '诊所', + }, + { + component: 'ApiSelect', + componentProps: { + allowClear: true, + showSearch: true, + filterOption: true, + afterFetch: (data: { id: number; name: string }[]) => { + return (data || []).map((item: any) => ({ + label: item.name, + value: item.id, + })); + }, + api: () => Promise.resolve([]), + placeholder: '请先选择诊所', + disabled: true, + }, + fieldName: 'su_id', + label: '医生', + dependencies: { + triggerFields: ['store_id'], + componentProps: (values: Record) => { + const storeId = values?.store_id; + if (!storeId) { + return { + api: () => Promise.resolve([]), + placeholder: '请先选择诊所', + disabled: true, + }; + } + return { + api: () => getDoctorOptionApi(storeId), + placeholder: '请选择医生', + disabled: false, + }; + }, + trigger: (values: Record, formApi: any) => { + // 诊所变更时清空已选医生,避免跨店脏值 + formApi?.setFieldValue?.('su_id', undefined); + }, + }, + }, { component: 'VbenSelect', componentProps: { allowClear: true, - filterOption: true, - showSearch: true, options: [ - { - label: '中药', - value: 1, - }, - { - label: '西药', - value: 2, - }, - { - label: '中成药', - value: 3, - }, - { - label: '产品服务包', - value: 5, - }, + { label: '线下就诊', value: 0 }, + { label: '在线问诊', value: 1 }, + { label: '在线复诊', value: 2 }, ], - placeholder: '请选择', + placeholder: '请选择来源', + }, + fieldName: 'is_online', + label: '处方来源', + }, + { + component: 'VbenSelect', + componentProps: { + allowClear: true, + options: [ + { label: '中药', value: 1 }, + { label: '西药', value: 2 }, + { label: '中成药', value: 3 }, + { label: '产品服务包', value: 5 }, + ], + placeholder: '请选择类型', }, fieldName: 'prescription_type', - label: '订单类型', + label: '药品类型', }, { component: 'RangePicker', componentProps: { - format: 'YYYY-MM-DD', // 确保格式与你需要的一致 + format: 'YYYY-MM-DD', + class: 'w-full', }, - // defaultValue: [dayjs().startOf('month'), dayjs()], fieldName: 'search_time', label: '时间范围', }, ], - // 控制表单是否显示折叠按钮 showCollapseButton: true, submitButtonOptions: { content: '查询', }, - // 是否在字段值改变时提交表单 - // submitOnChange: true, - // submitOnChange: true, - // 按下回车时是否提交表单 - submitOnEnter: false, + resetButtonOptions: { + content: '重置', + }, + submitOnEnter: true, }; diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/config/table.ts b/apps/web-antd/src/views/pharmacist/audit-prescription/config/table.ts index f3411ecf..ace337e5 100644 --- a/apps/web-antd/src/views/pharmacist/audit-prescription/config/table.ts +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/config/table.ts @@ -3,33 +3,79 @@ import type { VxeGridProps } from '#/adapter/vxe-table'; import { getPrescriptionListApi } from '../api'; interface RowType { - id: string; - name: string; - logo: string; - introduce: string; + id: number; + prescription_no: string; created_at: string; + auto_expire_time: number; + is_online: number; + status: number; + prescription_type: number; } +/** + * 审方列表列:多列拆分,避免单列堆叠 + * query 由页面注入 searchValues,此处仅作默认;index 会覆盖 proxy + */ export const gridOptions: VxeGridProps = { checkboxConfig: { highlight: true, labelField: '', }, columns: [ - // { type: 'checkbox', width: 60 }, - { field: 'id', align: 'left', title: 'ID' }, - { field: 'prescription_no', align: 'left', title: '处方订单号' }, - { field: 'doctor_info.name', align: 'left', title: '开方医生' }, - { field: 'user_patient.name', align: 'left', title: '就诊人名称' }, - { field: 'store.name', align: 'left', title: '开方诊所' }, - { field: 'status', title: '状态', slots: { default: 'status' } }, - // { field: 'created_at', title: '下单时间' }, { - type: 'html', + field: 'prescription_no', + align: 'left', + title: '处方单号', + minWidth: 160, + slots: { default: 'prescription_no' }, + }, + { + field: 'store.name', + align: 'left', + title: '诊所', + minWidth: 120, + slots: { default: 'store' }, + }, + { + field: 'doctor_info', + align: 'left', + title: '开方医生', + minWidth: 140, + slots: { default: 'doctor' }, + }, + { + field: 'user_patient', + align: 'left', + title: '就诊人', + minWidth: 200, + slots: { default: 'patient' }, + }, + { + field: 'is_online', + align: 'left', + title: '处方来源', + width: 110, + slots: { default: 'is_online' }, + }, + { + field: 'created_at', + align: 'left', + title: '时间', + minWidth: 180, + slots: { default: 'time' }, + }, + { + field: 'status', + title: '状态', + width: 100, + slots: { default: 'status' }, + }, + { title: '操作', align: 'right', slots: { default: 'action' }, - width: 250, + width: 280, + fixed: 'right', }, ], keepSource: true, @@ -39,14 +85,10 @@ export const gridOptions: VxeGridProps = { }, rowConfig: { useKey: true, + isHover: true, }, - // scrollY: { - // enabled: true, - // gt: 0, - // }, proxyConfig: { ajax: { - // 请求后端接口方法 query: async ({ page }, formValues) => { return await getPrescriptionListApi({ page: page.currentPage, @@ -57,26 +99,17 @@ export const gridOptions: VxeGridProps = { }, }, height: 'auto', - border: false, + border: 'inner', toolbarConfig: { - // 是否显示搜索表单控制按钮 - // @ts-ignore 正式环境时有完整的类型声明 - search: true, - refresh: true, // 刷新 - print: false, // 打印 - export: false, // 导出 - // custom: true, // 自定义列 - zoom: true, // 最大化最小化 + // @ts-ignore + search: false, + refresh: true, + print: false, + export: false, + zoom: true, slots: { buttons: 'toolbar-buttons', }, - custom: { - // 自定义列-图标 - icon: 'vxe-icon-menu', - }, - }, - expandConfig: { - // expandAll: true, }, showOverflow: false, }; diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/index.vue b/apps/web-antd/src/views/pharmacist/audit-prescription/index.vue index c8cdd5c1..49f746ea 100644 --- a/apps/web-antd/src/views/pharmacist/audit-prescription/index.vue +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/index.vue @@ -1,46 +1,104 @@ - diff --git a/apps/web-antd/src/views/pharmacist/audit-prescription/utils/viewModeStorage.ts b/apps/web-antd/src/views/pharmacist/audit-prescription/utils/viewModeStorage.ts new file mode 100644 index 00000000..a0ed2a66 --- /dev/null +++ b/apps/web-antd/src/views/pharmacist/audit-prescription/utils/viewModeStorage.ts @@ -0,0 +1,26 @@ +/** + * 审方页列表/卡片视图偏好本地存储 + */ +export type AuditPrescriptionViewMode = 'list' | 'card'; + +const STORAGE_KEY = 'audit-prescription-view-mode'; + +/** 读取上次视图模式,默认列表 */ +export function readAuditViewMode(): AuditPrescriptionViewMode { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw === 'list' || raw === 'card') return raw; + } catch { + // ignore + } + return 'list'; +} + +/** 写入视图模式,下次打开直接进入对应视图 */ +export function writeAuditViewMode(mode: AuditPrescriptionViewMode) { + try { + localStorage.setItem(STORAGE_KEY, mode); + } catch { + // ignore + } +} diff --git a/apps/web-antd/src/views/system/delivery-warehouse-drug/components/modal.vue b/apps/web-antd/src/views/system/delivery-warehouse-drug/components/modal.vue index 655d4d4c..cbd7b38b 100644 --- a/apps/web-antd/src/views/system/delivery-warehouse-drug/components/modal.vue +++ b/apps/web-antd/src/views/system/delivery-warehouse-drug/components/modal.vue @@ -18,10 +18,13 @@ import { updateDeliveryWarehouseDrug, } from '#/views/system/delivery-warehouse-drug/api'; import { + applyAutoPromoFee, calcFeeSum, isFeeOverSalePrice, modalFormProps, registerFeeOverTipHandler, + resetAutoPromoFeeKey, + seedAutoPromoFeeKey, syncFeeSumDisplay, } from '#/views/system/delivery-warehouse-drug/config/form'; import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price'; @@ -59,7 +62,8 @@ onUnmounted(() => { }); /** - * 写入售价;新增时默认平台费 = 售价 5% + * 写入售价;新增时默认平台费 = 售价 5%,并按差额回填推广费 + * 编辑仅写售价并锁定依赖 key,不覆盖已有三费 */ async function applyCentralPrice( price: number | string | null | undefined, @@ -73,8 +77,18 @@ async function applyCentralPrice( const sale = Number(n.toFixed(4)); await formApi.setFieldValue('sale_price', sale); if (isCreate) { - await formApi.setFieldValue('platform_fee', Number((sale * 0.05).toFixed(4))); + const platform = Number((sale * 0.05).toFixed(4)); + await formApi.setFieldValue('platform_fee', platform); + const values = await formApi.getValues(); + const merged = { ...values, sale_price: sale, platform_fee: platform }; + applyAutoPromoFee(merged, formApi); + syncFeeSumDisplay(merged, formApi); + return; } + const values = await formApi.getValues(); + const merged = { ...values, sale_price: sale }; + seedAutoPromoFeeKey(merged); + syncFeeSumDisplay(merged, formApi); } /** @@ -148,6 +162,8 @@ const [Modal, modalApi] = useVbenModal({ lockDrug.value = !!data.lockDrug && !update; editDrugCard.value = null; feeOverTip.value = ''; + // 清零自动算费 key,避免沿用上一单的报价/平台费组合 + resetAutoPromoFeeKey(); const salePrice = pickSalePrice(data); // 先重置,再 await 写入,避免异步 setValues 冲掉 sale_price await formApi.resetForm(); diff --git a/apps/web-antd/src/views/system/delivery-warehouse-drug/config/form.ts b/apps/web-antd/src/views/system/delivery-warehouse-drug/config/form.ts index 755be65b..dc9312d8 100644 --- a/apps/web-antd/src/views/system/delivery-warehouse-drug/config/form.ts +++ b/apps/web-antd/src/views/system/delivery-warehouse-drug/config/form.ts @@ -10,6 +10,18 @@ export function calcFeeSum(values: Record) { return (quote + promoFee + platformFee).toFixed(4); } +/** + * 门店推广费 = 售价 − (报价 + 平台费),不低于 0 + * 有售价时用于自动回填,保证三费合计贴合售价 + */ +export function calcPromoFee(values: Record): number { + const sale = Number(values.sale_price) || 0; + if (!(sale > 0)) return Number(values.promo_fee) || 0; + const quote = Number(values.quote) || 0; + const platformFee = Number(values.platform_fee) || 0; + return Number(Math.max(0, sale - quote - platformFee).toFixed(4)); +} + /** 三费是否超过药品售价 */ export function isFeeOverSalePrice(values: Record) { const sale = Number(values.sale_price); @@ -21,23 +33,74 @@ export function isFeeOverSalePrice(values: Record) { type FeeOverTipHandler = (tip: string) => void; let feeOverTipHandler: FeeOverTipHandler | null = null; +/** + * 记录上次自动算费依赖(售价|报价|平台费) + * 仅依赖变化时覆盖 promo_fee,避免手改推广费被立刻冲掉 + */ +let lastAutoPromoKey = ''; + /** 注册/注销弹窗内的超售价文本提示处理器 */ export function registerFeeOverTipHandler(handler: FeeOverTipHandler | null) { feeOverTipHandler = handler; } +/** 弹窗打开/重置时清零,避免沿用上一次绑定的 key */ +export function resetAutoPromoFeeKey() { + lastAutoPromoKey = ''; +} + /** - * 费用合计联动:实时重算、三费 max、超售价 help + 文本提示 - * 为什么放在 trigger:输入任三费即触发,不必等提交 + * 编辑回填后锁定当前依赖 key,避免一打开就把已有推广费冲掉 + * 之后只有改报价/平台费/售价才会重新自动算 + */ +export function seedAutoPromoFeeKey(values: Record) { + const sale = Number(values.sale_price) || 0; + if (!(sale > 0)) { + lastAutoPromoKey = ''; + return; + } + lastAutoPromoKey = `${sale}|${Number(values.quote) || 0}|${Number(values.platform_fee) || 0}`; +} + +/** + * 有售价且 quote/platform/sale 变化时,自动回填门店推广费 + * @returns 回填后的 values(含最新 promo_fee),供合计展示使用 + */ +export function applyAutoPromoFee( + values: Record, + formApi: any, +): Record { + const sale = Number(values.sale_price) || 0; + if (!(sale > 0)) { + return values; + } + const key = `${sale}|${Number(values.quote) || 0}|${Number(values.platform_fee) || 0}`; + const nextPromo = calcPromoFee(values); + // 依赖未变:可能是手改推广费触发,不覆盖 + if (key === lastAutoPromoKey) { + return values; + } + lastAutoPromoKey = key; + const cur = Number(values.promo_fee); + if (!(Math.abs(cur - nextPromo) < 0.00005)) { + formApi.setFieldValue('promo_fee', nextPromo); + } + return { ...values, promo_fee: nextPromo }; +} + +/** + * 费用合计联动:先按差额回填推广费,再刷新合计 / 超售价提示 + * 为什么放在 trigger:输入报价或平台费即触发,不必等提交 * @returns 超售价提示文案(无则空串) */ export function syncFeeSumDisplay( values: Record, formApi: any, ): string { - const sum = calcFeeSum(values); + const merged = applyAutoPromoFee(values, formApi); + const sum = calcFeeSum(merged); formApi.setFieldValue('_fee_sum', sum); - const sale = Number(values.sale_price) || 0; + const sale = Number(merged.sale_price) || 0; const over = sale > 0 && Number(sum) > sale; const tip = over ? `三费合计已超过药品售价 ¥${sale.toFixed(4)},请下调报价/推广费/平台费` @@ -45,7 +108,7 @@ export function syncFeeSumDisplay( const feeHelp = over ? tip : sale > 0 - ? `报价+推广费+平台费之和不得超过药品售价 ¥${sale.toFixed(4)}` + ? `推广费自动=售价−(报价+平台费);合计不得超过 ¥${sale.toFixed(4)}` : '报价+推广费+平台费之和,保存时需大于0'; // 单项输入上限:有售价时不得超过售价;同时刷新合计 help const feeInputProps = { @@ -64,9 +127,10 @@ export function syncFeeSumDisplay( }, { fieldName: 'promo_fee', + help: sale > 0 ? '自动计算:售价 − (报价 + 平台费),可微调' : undefined, componentProps: { ...feeInputProps, - placeholder: '请输入推广费', + placeholder: sale > 0 ? '自动=售价−报价−平台费' : '请输入推广费', }, }, { @@ -174,6 +238,7 @@ export const modalFormProps: VbenFormProps = { }, fieldName: 'promo_fee', label: '推广费', + help: '有售价时自动=售价−(报价+平台费)', rules: 'required', }, { @@ -204,7 +269,26 @@ export const modalFormProps: VbenFormProps = { triggerFields: ['quote', 'promo_fee', 'platform_fee', 'sale_price'], }, }, - // 绑定创建时库存固定为 0,后续在「库存管理」入库/出库,禁止表单直改数量 + // 创建时可填初始库存,提交后走入库流水(编辑时不展示) + { + component: 'InputNumber', + componentProps: { + placeholder: '选填,默认 0', + min: 0, + precision: 0, + class: 'w-full', + }, + fieldName: 'initial_stock', + label: '初始库存', + help: '大于 0 时绑定成功后自动入库并记流水', + defaultValue: 0, + dependencies: { + show(values) { + return !values?.id; + }, + triggerFields: ['id'], + }, + }, { component: 'RadioGroup', componentProps: { diff --git a/apps/web-antd/src/views/system/delivery-warehouse-stock/components/stock-log-drawer.vue b/apps/web-antd/src/views/system/delivery-warehouse-stock/components/stock-log-drawer.vue index fd07735b..2c374b28 100644 --- a/apps/web-antd/src/views/system/delivery-warehouse-stock/components/stock-log-drawer.vue +++ b/apps/web-antd/src/views/system/delivery-warehouse-stock/components/stock-log-drawer.vue @@ -1,9 +1,13 @@