From bfe50a8d67bf35f0ff695df3791fa7fb1a01593a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Tue, 29 Sep 2026 10:28:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BE=85=E6=96=99=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E3=80=81=E5=BA=B7=E6=AD=A3=E6=BA=90=E6=8B=89=E5=8F=96=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chinese-drug-name-bubble.vue | 92 +- .../chinese-drug-way-bubble.vue | 413 +++++ .../chinese-drug-way-bubble/index.ts | 5 + .../drug-search-select/drug-search-select.vue | 146 +- .../EntrustedProcessPicker.vue | 242 +++ .../components/loading/OcrImageLoading.vue | 872 +++++++++++ .../OcrOriginImageFloat.vue | 311 ++++ .../ocr-origin-image-float/index.ts | 4 + .../OcrOriginImagePanel.vue | 306 ++++ .../ocr-origin-image-panel/index.ts | 4 + apps/web-antd/src/utils/imageTransferQuote.ts | 67 + .../order/image-transfer-order/api/index.ts | 38 + .../components/detail-drawer.vue | 695 +++++++++ .../components/order-card.vue | 139 ++ .../order/image-transfer-order/index.vue | 145 ++ .../order/product-order/components/detail.vue | 21 +- .../order/product-order/config/constants.ts | 1 + .../business/order/product-order/index.vue | 1 + .../product/china-medicine/config/form.ts | 4 +- .../business/product/excipient/api/index.ts | 105 ++ .../components/DrugNumberBatchModal.vue | 489 ++++++ .../excipient/components/ExcelUpload.vue | 169 ++ .../product/excipient/components/modal.vue | 79 + .../business/product/excipient/config/form.ts | 167 ++ .../product/excipient/config/search.ts | 37 + .../product/excipient/config/table.ts | 81 + .../business/product/excipient/index.vue | 321 ++++ .../excipient/utils/parseDrugNumberExcel.ts | 214 +++ .../product/health-food/config/form.ts | 5 +- .../product/medical-device/config/form.ts | 4 +- .../business/product/non-drug/config/form.ts | 4 +- .../product/service-pack/config/form.ts | 4 +- .../product/western-medicine/config/form.ts | 4 +- .../shared/useWarehouseDrugTypeRoute.ts | 2 +- .../image-transfer-prescription/api/index.ts | 62 + .../image-transfer-prescription/index.vue | 13 + .../doctor/doctor-reception/api/index.ts | 35 + .../components/CreateUserPatientModal.vue | 81 +- .../components/ImageOcrDrawer.vue | 1153 ++++++++++++++ .../components/PrescriptionDetail.vue | 40 + .../views/doctor/doctor-reception/index.vue | 284 +++- .../utils/alignChineseDrugPrice.ts | 171 +++ .../components/FullscreenImageUpload.vue | 320 ++++ .../image-transfer-prescription/index.vue | 1353 +++++++++++++++++ .../finance/fund-water/api/account-change.ts | 13 + .../config/account-change-search.ts | 63 + .../fund-water/config/account-change-table.ts | 77 + .../treatment-fee-cash-backfill/api/index.ts | 52 + .../components/detail-modal.vue | 670 ++++++++ .../config/search.ts | 51 + .../config/table.ts | 94 ++ .../treatment-fee-cash-backfill/index.vue | 351 +++++ .../components/withdrawal-rules-modal.vue | 254 ++++ .../views/finance/withdrawal/config/rules.ts | 96 ++ .../views/log/mes-open-api-log/api/index.ts | 12 + .../components/detail-modal.vue | 79 + .../log/mes-open-api-log/config/search.ts | 43 + .../log/mes-open-api-log/config/table.ts | 40 + .../src/views/log/mes-open-api-log/index.vue | 54 + .../ai/generation/components/detail-modal.vue | 78 +- .../ai/image-ocr-record/config/constants.ts | 18 + .../ai/image-ocr-record/config/search.ts | 104 ++ .../ai/image-ocr-record/config/table.ts | 104 ++ .../system/ai/image-ocr-record/index.vue | 89 ++ .../delivery-warehouse-product/config/form.ts | 4 +- .../views/system/mes-open-app/api/index.ts | 32 + .../system/mes-open-app/components/modal.vue | 87 ++ .../mes-open-app/components/secret-modal.vue | 99 ++ .../system/mes-open-app/config/constants.ts | 6 + .../views/system/mes-open-app/config/form.ts | 46 + .../src/views/system/mes-open-app/index.vue | 232 +++ .../store/components/LedgerSplitModal.vue | 119 ++ .../system/store/components/StoreNavModal.vue | 168 ++ .../views/system/store/config/constants.ts | 22 + .../excipient-price-config-panel.vue | 89 ++ .../image-transfer-config-panel.vue | 181 +++ .../withdrawal-rules-config-panel.vue | 379 +++++ .../src/views/system/system-config/index.vue | 6 + 78 files changed, 12397 insertions(+), 118 deletions(-) create mode 100644 apps/web-antd/src/components/chinese-drug-way-bubble/chinese-drug-way-bubble.vue create mode 100644 apps/web-antd/src/components/chinese-drug-way-bubble/index.ts create mode 100644 apps/web-antd/src/components/entrusted-process-picker/EntrustedProcessPicker.vue create mode 100644 apps/web-antd/src/components/loading/OcrImageLoading.vue create mode 100644 apps/web-antd/src/components/ocr-origin-image-float/OcrOriginImageFloat.vue create mode 100644 apps/web-antd/src/components/ocr-origin-image-float/index.ts create mode 100644 apps/web-antd/src/components/ocr-origin-image-panel/OcrOriginImagePanel.vue create mode 100644 apps/web-antd/src/components/ocr-origin-image-panel/index.ts create mode 100644 apps/web-antd/src/utils/imageTransferQuote.ts create mode 100644 apps/web-antd/src/views/business/order/image-transfer-order/api/index.ts create mode 100644 apps/web-antd/src/views/business/order/image-transfer-order/components/detail-drawer.vue create mode 100644 apps/web-antd/src/views/business/order/image-transfer-order/components/order-card.vue create mode 100644 apps/web-antd/src/views/business/order/image-transfer-order/index.vue create mode 100644 apps/web-antd/src/views/business/product/excipient/api/index.ts create mode 100644 apps/web-antd/src/views/business/product/excipient/components/DrugNumberBatchModal.vue create mode 100644 apps/web-antd/src/views/business/product/excipient/components/ExcelUpload.vue create mode 100644 apps/web-antd/src/views/business/product/excipient/components/modal.vue create mode 100644 apps/web-antd/src/views/business/product/excipient/config/form.ts create mode 100644 apps/web-antd/src/views/business/product/excipient/config/search.ts create mode 100644 apps/web-antd/src/views/business/product/excipient/config/table.ts create mode 100644 apps/web-antd/src/views/business/product/excipient/index.vue create mode 100644 apps/web-antd/src/views/business/product/excipient/utils/parseDrugNumberExcel.ts create mode 100644 apps/web-antd/src/views/clinic/image-transfer-prescription/api/index.ts create mode 100644 apps/web-antd/src/views/clinic/image-transfer-prescription/index.vue create mode 100644 apps/web-antd/src/views/doctor/doctor-reception/components/ImageOcrDrawer.vue create mode 100644 apps/web-antd/src/views/doctor/doctor-reception/utils/alignChineseDrugPrice.ts create mode 100644 apps/web-antd/src/views/doctor/image-transfer-prescription/components/FullscreenImageUpload.vue create mode 100644 apps/web-antd/src/views/doctor/image-transfer-prescription/index.vue create mode 100644 apps/web-antd/src/views/finance/fund-water/api/account-change.ts create mode 100644 apps/web-antd/src/views/finance/fund-water/config/account-change-search.ts create mode 100644 apps/web-antd/src/views/finance/fund-water/config/account-change-table.ts create mode 100644 apps/web-antd/src/views/finance/treatment-fee-cash-backfill/api/index.ts create mode 100644 apps/web-antd/src/views/finance/treatment-fee-cash-backfill/components/detail-modal.vue create mode 100644 apps/web-antd/src/views/finance/treatment-fee-cash-backfill/config/search.ts create mode 100644 apps/web-antd/src/views/finance/treatment-fee-cash-backfill/config/table.ts create mode 100644 apps/web-antd/src/views/finance/treatment-fee-cash-backfill/index.vue create mode 100644 apps/web-antd/src/views/finance/withdrawal/components/withdrawal-rules-modal.vue create mode 100644 apps/web-antd/src/views/finance/withdrawal/config/rules.ts create mode 100644 apps/web-antd/src/views/log/mes-open-api-log/api/index.ts create mode 100644 apps/web-antd/src/views/log/mes-open-api-log/components/detail-modal.vue create mode 100644 apps/web-antd/src/views/log/mes-open-api-log/config/search.ts create mode 100644 apps/web-antd/src/views/log/mes-open-api-log/config/table.ts create mode 100644 apps/web-antd/src/views/log/mes-open-api-log/index.vue create mode 100644 apps/web-antd/src/views/system/ai/image-ocr-record/config/constants.ts create mode 100644 apps/web-antd/src/views/system/ai/image-ocr-record/config/search.ts create mode 100644 apps/web-antd/src/views/system/ai/image-ocr-record/config/table.ts create mode 100644 apps/web-antd/src/views/system/ai/image-ocr-record/index.vue create mode 100644 apps/web-antd/src/views/system/mes-open-app/api/index.ts create mode 100644 apps/web-antd/src/views/system/mes-open-app/components/modal.vue create mode 100644 apps/web-antd/src/views/system/mes-open-app/components/secret-modal.vue create mode 100644 apps/web-antd/src/views/system/mes-open-app/config/constants.ts create mode 100644 apps/web-antd/src/views/system/mes-open-app/config/form.ts create mode 100644 apps/web-antd/src/views/system/mes-open-app/index.vue create mode 100644 apps/web-antd/src/views/system/store/components/LedgerSplitModal.vue create mode 100644 apps/web-antd/src/views/system/store/components/StoreNavModal.vue create mode 100644 apps/web-antd/src/views/system/store/config/constants.ts create mode 100644 apps/web-antd/src/views/system/system-config/components/excipient-price-config-panel.vue create mode 100644 apps/web-antd/src/views/system/system-config/components/image-transfer-config-panel.vue create mode 100644 apps/web-antd/src/views/system/system-config/components/withdrawal-rules-config-panel.vue diff --git a/apps/web-antd/src/components/chinese-drug-name-bubble/chinese-drug-name-bubble.vue b/apps/web-antd/src/components/chinese-drug-name-bubble/chinese-drug-name-bubble.vue index 02c7a87a..39b825cb 100644 --- a/apps/web-antd/src/components/chinese-drug-name-bubble/chinese-drug-name-bubble.vue +++ b/apps/web-antd/src/components/chinese-drug-name-bubble/chinese-drug-name-bubble.vue @@ -24,7 +24,10 @@ import { splitHighlight, type HighlightPart, } from '#/utils/dictSearchRank'; -import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api'; +import { + getProductListDoctorReception, + getUsedExcipientListDoctorReception, +} from '#/views/doctor/doctor-reception/api'; export type ChineseDrugBubbleItem = { id: number; @@ -83,6 +86,8 @@ const open = ref(false); const searching = ref(false); const highlightIndex = ref(0); const options = ref([]); +/** 当前列表是否「用过辅料」空搜结果(用于提示文案) */ +const showingUsedExcipients = ref(false); const scrollTop = ref(0); const listViewportH = ref(200); const panelStyle = ref>({}); @@ -217,24 +222,39 @@ function updatePanelPlacement() { } /** - * 远程搜药;空关键词不请求(与原中药 Select 一致) + * 远程搜药;空关键词时:辅料(type=8)拉「用过」列表,中药仍不请求 */ async function fetchDrugs(kw: string) { const name = String(kw || '').trim(); - if (!name) { + // 非辅料空词不请求(与原中药 Select 一致) + if (!name && Number(props.type) !== 8) { options.value = []; + showingUsedExcipients.value = false; loading.value = false; return; } loading.value = true; try { - const res = await getProductListDoctorReception({ - name, - type: props.type, - store_id: props.storeId || 0, - ...(props.registerId > 0 ? { register_id: props.registerId } : {}), - }); - const list = Array.isArray(res) ? res : []; + let list: any[] = []; + if (!name && Number(props.type) === 8) { + // 聚焦空词:展示该医生云端「用过辅料」 + const res = await getUsedExcipientListDoctorReception({ + store_id: props.storeId || 0, + ...(props.registerId > 0 ? { register_id: props.registerId } : {}), + limit: 30, + }); + list = Array.isArray(res) ? res : []; + showingUsedExcipients.value = true; + } else { + const res = await getProductListDoctorReception({ + name, + type: props.type, + store_id: props.storeId || 0, + ...(props.registerId > 0 ? { register_id: props.registerId } : {}), + }); + list = Array.isArray(res) ? res : []; + showingUsedExcipients.value = false; + } const mapped: ChineseDrugBubbleItem[] = list.map((item: any) => { const drug = item.drug || {}; return { @@ -249,30 +269,33 @@ async function fetchDrugs(kw: string) { }, }; }); - options.value = rankByMatchScore( - mapped, - name, - (it) => drugNameOf(it), - (it) => - [ - it.drug?.pinyin_simple, - it.drug?.pinyin_full, - it.drug?.pinyin_display, - it.drug?.drug_alias, - it.matched_alias || it.drug?.matched_alias, - it.matched_alias_pinyin || it.drug?.matched_alias_pinyin, - it.drug?.search_digits, - it.drug?.specification, - ] - .map((v) => String(v || '')) - .join(' '), - ); + options.value = name + ? rankByMatchScore( + mapped, + name, + (it) => drugNameOf(it), + (it) => + [ + it.drug?.pinyin_simple, + it.drug?.pinyin_full, + it.drug?.pinyin_display, + it.drug?.drug_alias, + it.matched_alias || it.drug?.matched_alias, + it.matched_alias_pinyin || it.drug?.matched_alias_pinyin, + it.drug?.search_digits, + it.drug?.specification, + ] + .map((v) => String(v || '')) + .join(' '), + ) + : mapped; highlightIndex.value = options.value.length > 0 ? 0 : -1; scrollTop.value = 0; if (listRef.value) listRef.value.scrollTop = 0; } catch (e) { console.error('中药药名搜索失败:', e); options.value = []; + showingUsedExcipients.value = false; } finally { loading.value = false; nextTick(() => updatePanelPlacement()); @@ -289,11 +312,8 @@ function openPanel() { searching.value = true; nextTick(() => { updatePanelPlacement(); - if (keyword.value.trim()) { - debouncedFetch(keyword.value); - } else { - options.value = []; - } + // 有关键词走搜索;空词时辅料拉「用过」,中药保持空列表 + debouncedFetch(keyword.value); }); } @@ -301,6 +321,7 @@ function closePanel(restoreDisplay = true) { open.value = false; searching.value = false; options.value = []; + showingUsedExcipients.value = false; highlightIndex.value = 0; if (restoreDisplay) { keyword.value = String(props.displayName || ''); @@ -463,6 +484,7 @@ defineExpose({ ↑↓←→ 循环切换 · Enter 选用 · Esc 关闭 + @@ -473,7 +495,9 @@ defineExpose({ :description=" keyword.trim() ? '未找到匹配药材' - : '请输入药名/别名/拼音/数字搜索' + : Number(type) === 8 + ? '暂无用过的辅料,请输入名称搜索' + : '请输入药名/别名/拼音/数字搜索' " class="py-4" /> diff --git a/apps/web-antd/src/components/chinese-drug-way-bubble/chinese-drug-way-bubble.vue b/apps/web-antd/src/components/chinese-drug-way-bubble/chinese-drug-way-bubble.vue new file mode 100644 index 00000000..3383bec0 --- /dev/null +++ b/apps/web-antd/src/components/chinese-drug-way-bubble/chinese-drug-way-bubble.vue @@ -0,0 +1,413 @@ + + + + + + + diff --git a/apps/web-antd/src/components/chinese-drug-way-bubble/index.ts b/apps/web-antd/src/components/chinese-drug-way-bubble/index.ts new file mode 100644 index 00000000..139be647 --- /dev/null +++ b/apps/web-antd/src/components/chinese-drug-way-bubble/index.ts @@ -0,0 +1,5 @@ +/** + * 中药煎法/用法气泡选择器 + */ +export { default as ChineseDrugWayBubble } from './chinese-drug-way-bubble.vue'; +export type { DrugWayOption } from './chinese-drug-way-bubble.vue'; diff --git a/apps/web-antd/src/components/drug-search-select/drug-search-select.vue b/apps/web-antd/src/components/drug-search-select/drug-search-select.vue index 5b133505..40c2ba73 100644 --- a/apps/web-antd/src/components/drug-search-select/drug-search-select.vue +++ b/apps/web-antd/src/components/drug-search-select/drug-search-select.vue @@ -16,7 +16,7 @@ import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue'; import { Button, Input, Spin } from 'ant-design-vue'; import { debounce } from 'lodash-es'; -import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api'; +import { getProductListDoctorReception, getUsedExcipientListDoctorReception } from '#/views/doctor/doctor-reception/api'; /** 无图时占位(中药/西药等统一兜底,避免仓绑药下拉只剩文字) */ const DRUG_PLACEHOLDER_IMAGE = @@ -27,7 +27,7 @@ const DRUG_PLACEHOLDER_IMAGE = interface Props { /** * 药品类型 - * 1-中药,2-西药 + * 1-中药,2-西药,8-辅料 */ type?: number; /** @@ -100,6 +100,11 @@ const searchResults = ref([]); */ const showDropdown = ref(false); +/** + * 当前列表是否「用过辅料」空搜结果(用于提示) + */ +const showingUsedExcipients = ref(false); + /** * 组件容器引用 */ @@ -127,18 +132,59 @@ function resolveDropdownImage(item: any): string { return item._image || DRUG_PLACEHOLDER_IMAGE; } +/** + * 是否辅料单类型模式(空搜拉「用过」) + */ +function isExcipientSingleType(): boolean { + return props.types.length === 0 && Number(props.type) === 8; +} + +/** + * 把门店/主库商品行打平成下拉展示字段 + */ +function mapProductRows(res: any): any[] { + if (!Array.isArray(res)) return []; + return res.map((item: any) => { + const drug = item.drug || {}; + return { + ...item, + _id: item.id, + _drugId: item.drug_id || drug.id || item.id, + _drugName: drug.drug_name || item.drug_name || '', + _image: drug.image || item.image || '', + _specification: drug.specification || item.specification || '', + // 辅料等按药配置的开方快捷数量(逗号串),供开方侧解析成按钮 + _quickNumbers: drug.quick_numbers || item.quick_numbers || '', + _supplier: drug.supplier?.name || item.supplier?.name || '', + _price: Number(item.price ?? 0), + // 已绑定配送仓:列表展示「多仓」标识 + _hasDeliveryWarehouse: Number(item.has_delivery_warehouse ?? 0) === 1, + _timeId: drug.time_id || 0, + _typeId: drug.type_id || 0, + _frequencyId: drug.frequency_id || 0, + _unitId: drug.unit_id || 0, + _number: drug.number || 1, + _useNum: drug.useNum, + _useType: drug.useType, + _useFrequency: drug.useFrequency, + }; + }); +} + // ==================== 方法定义 ==================== /** * 搜索药品 * @param keyword 搜索关键词 - * @description 调用后端API搜索药品,支持按名称和拼音搜索; + * @description 调用后端API搜索药品;辅料(type=8)空关键词拉「用过」列表并展开; * 传入 types 时允许空关键词拉取该类型全量列表(用于下拉展开) */ const searchDrugs = debounce(async (keyword: string) => { - // 单类型模式:空关键词清空;多类型模式:空关键词仍请求后端拉全量 - if ((!keyword || keyword.length < 1) && props.types.length === 0) { + const kw = String(keyword || '').trim(); + // 单类型非辅料:空关键词清空并收起 + if (!kw && props.types.length === 0 && !isExcipientSingleType()) { searchResults.value = []; + showingUsedExcipients.value = false; showDropdown.value = false; return; } @@ -147,49 +193,35 @@ const searchDrugs = debounce(async (keyword: string) => { showDropdown.value = true; try { - // types 优先:有多类型时只传 types,不传 type,避免后端被单 type 覆盖 - const typeParams = - props.types.length > 0 - ? { types: props.types } - : { type: props.type }; - const res = await getProductListDoctorReception({ - name: keyword || '', - ...typeParams, - store_id: props.storeId, - ...(props.registerId ? { register_id: props.registerId } : {}), - }); - - // 处理返回数据(兼容门店 relation 与平台主库打平结构) - if (Array.isArray(res)) { - searchResults.value = res.map((item: any) => { - const drug = item.drug || {}; - return { - ...item, - _id: item.id, - _drugId: item.drug_id || drug.id || item.id, - _drugName: drug.drug_name || item.drug_name || '', - _image: drug.image || item.image || '', - _specification: drug.specification || item.specification || '', - _supplier: drug.supplier?.name || item.supplier?.name || '', - _price: Number(item.price ?? 0), - // 已绑定配送仓:列表展示「多仓」标识 - _hasDeliveryWarehouse: Number(item.has_delivery_warehouse ?? 0) === 1, - _timeId: drug.time_id || 0, - _typeId: drug.type_id || 0, - _frequencyId: drug.frequency_id || 0, - _unitId: drug.unit_id || 0, - _number: drug.number || 1, - _useNum: drug.useNum, - _useType: drug.useType, - _useFrequency: drug.useFrequency, - }; + let res: any = []; + if (!kw && isExcipientSingleType()) { + // 辅料空搜:医生云端「用过辅料」 + res = await getUsedExcipientListDoctorReception({ + store_id: props.storeId, + ...(props.registerId ? { register_id: props.registerId } : {}), + limit: 30, }); + showingUsedExcipients.value = true; } else { - searchResults.value = []; + showingUsedExcipients.value = false; + // types 优先:有多类型时只传 types,不传 type,避免后端被单 type 覆盖 + const typeParams = + props.types.length > 0 + ? { types: props.types } + : { type: props.type }; + res = await getProductListDoctorReception({ + name: kw || '', + ...typeParams, + store_id: props.storeId, + ...(props.registerId ? { register_id: props.registerId } : {}), + }); } + + searchResults.value = mapProductRows(res); } catch (error) { console.error('搜索药品失败:', error); searchResults.value = []; + showingUsedExcipients.value = false; } finally { isSearching.value = false; } @@ -208,7 +240,7 @@ function handleInput(e: Event) { /** * 处理输入框获得焦点 - * @description 多类型模式聚焦即拉取列表并展开;单类型模式仅在已有结果时展示下拉 + * @description 多类型 / 辅料:聚焦即拉取列表并展开;其它单类型仅在已有结果时展示下拉 */ function handleFocus() { if (props.types.length > 0) { @@ -216,6 +248,12 @@ function handleFocus() { showDropdown.value = true; return; } + // 辅料:聚焦空词直接拉「用过」并展开气泡 + if (isExcipientSingleType()) { + searchDrugs(searchKeyword.value || ''); + showDropdown.value = true; + return; + } if (searchResults.value.length > 0) { showDropdown.value = true; } @@ -231,6 +269,7 @@ function handleSelectDrug(drug: any) { searchKeyword.value = ''; searchResults.value = []; showDropdown.value = false; + showingUsedExcipients.value = false; highlightIndex.value = -1; if (props.isCard) { selectedDrug.value = drug; @@ -319,6 +358,7 @@ watch( () => { searchResults.value = []; showDropdown.value = false; + showingUsedExcipients.value = false; searchKeyword.value = ''; selectedDrug.value = null; }, @@ -392,7 +432,7 @@ watch(
- 搜索中... + {{ showingUsedExcipients ? '加载用过辅料...' : '搜索中...' }}
@@ -400,11 +440,21 @@ watch( v-else-if="searchResults.length === 0" class="drug-dropdown__empty" > - 暂无匹配药品 + {{ + showingUsedExcipients + ? '暂无用过的辅料,请输入名称搜索' + : '暂无匹配药品' + }}
+
+ 最近用过的辅料 +
+/** + * 识图传方 / 审方共用的委托调剂(代配)编辑器 + * 直接复用接诊的制剂方式、制剂要求、煎法、备注,避免再写一套只读文案 + */ +import { onMounted, ref, watch } from 'vue'; + +import ChineseMedicineConfig, { + type ChineseMedicineConfigChange, +} from '#/views/doctor/doctor-reception/components/ChineseMedicineConfig.vue'; +import { getProcessRuleList } from '#/views/doctor/doctor-reception/api'; +import { + applySingleChildProcessRuleAutoSelect, + resolveProcessRuleLoadedLevel, +} from '#/utils/processRuleCascade'; + +const props = withDefaults( + defineProps<{ + processRuleId?: number | null; + childProcessRuleId?: number | null; + processRuleNoteId?: number | null; + dosage?: number; + dayDosage?: number; + /** 1 自制剂,2 委托调剂;不传则有规则 ID 时当代配 */ + ruleType?: number; + packageMethodId?: number | null; + disabled?: boolean; + /** 未填字段名,透传给 ChineseMedicineConfig 标红 */ + errorKeys?: string[]; + }>(), + { + processRuleId: 0, + childProcessRuleId: 0, + processRuleNoteId: 0, + dosage: 7, + dayDosage: 2, + ruleType: 0, + packageMethodId: 2, + disabled: false, + errorKeys: () => [], + }, +); + +const emit = defineEmits<{ + change: [payload: ChineseMedicineConfigChange]; +}>(); + +/** 传方默认就是委托调剂,否则制剂/煎法列表不会出来 */ +const ruleType = ref(2); +const packageMethodId = ref(2); +const processRuleId = ref(null); +const childProcessRuleId = ref(null); +const processRuleNoteId = ref(null); +const dosage = ref(7); +const dayDosage = ref(2); +const processRuleList = ref>([]); +const childProcessRuleList = ref>([]); +const processRuleNoteList = ref>([]); + +function toId(value: number | null | undefined) { + const n = Number(value || 0); + return n > 0 ? n : null; +} + +/** 把当前选择一次性抛给父页面,提交和审方都读这一份 */ +function emitCurrent(extra: ChineseMedicineConfigChange = {}) { + emit('change', { + ruleType: ruleType.value, + packageMethodId: ruleType.value === 1 ? packageMethodId.value : null, + processRuleId: ruleType.value === 2 ? processRuleId.value : null, + childProcessRuleId: ruleType.value === 2 ? childProcessRuleId.value : null, + processRuleNoteId: ruleType.value === 2 ? processRuleNoteId.value : null, + dosage: dosage.value, + dayDosage: dayDosage.value, + ...extra, + }); +} + +async function loadProcessRuleData(pid = 0, ruleId = 0) { + const data = ruleId === 0 ? { pid } : { rule_id: ruleId }; + const value = await getProcessRuleList(data); + const list = Array.isArray(value) ? value : []; + if (ruleId !== 0) { + processRuleNoteList.value = list; + } else if (pid === 0) { + processRuleList.value = list; + } else { + childProcessRuleList.value = list; + } + await applySingleChildProcessRuleAutoSelect( + resolveProcessRuleLoadedLevel(pid, ruleId), + { + ruleType: ruleType.value, + disabled: props.disabled, + processRuleId: processRuleId.value, + childProcessRuleId: childProcessRuleId.value, + processRuleNoteId: processRuleNoteId.value, + processRuleList: processRuleList.value, + childProcessRuleList: childProcessRuleList.value, + processRuleNoteList: processRuleNoteList.value, + load: loadProcessRuleData, + setProcessRuleId: (id) => { + processRuleId.value = id; + }, + setChildProcessRuleId: (id) => { + childProcessRuleId.value = id; + }, + setProcessRuleNoteId: (id) => { + processRuleNoteId.value = id; + }, + clearBelowProcessRule: () => { + childProcessRuleId.value = null; + processRuleNoteId.value = null; + childProcessRuleList.value = []; + processRuleNoteList.value = []; + }, + clearBelowChild: () => { + processRuleNoteId.value = null; + processRuleNoteList.value = []; + }, + }, + ); +} + +/** 打开已有订单时按已选规则把煎法和备注列表补出来 */ +async function hydrate() { + processRuleId.value = toId(props.processRuleId); + childProcessRuleId.value = toId(props.childProcessRuleId); + processRuleNoteId.value = toId(props.processRuleNoteId); + dosage.value = Number(props.dosage || 7); + dayDosage.value = Number(props.dayDosage || 2); + if (props.ruleType === 1 || props.ruleType === 2) { + ruleType.value = props.ruleType; + } else if (processRuleId.value) { + ruleType.value = 2; + } + if (Number(props.packageMethodId || 0) > 0) { + packageMethodId.value = Number(props.packageMethodId); + } + if (processRuleList.value.length === 0) { + await loadProcessRuleData(); + } + if (processRuleId.value) { + await loadProcessRuleData(processRuleId.value); + } + if (childProcessRuleId.value) { + await loadProcessRuleData(0, childProcessRuleId.value); + } + const changed = + Number(processRuleId.value || 0) !== Number(props.processRuleId || 0) || + Number(childProcessRuleId.value || 0) !== Number(props.childProcessRuleId || 0) || + Number(processRuleNoteId.value || 0) !== Number(props.processRuleNoteId || 0); + if (changed) { + emitCurrent(); + } +} + +async function onChange(payload: ChineseMedicineConfigChange) { + if (props.disabled) return; + const oldRuleType = ruleType.value; + const oldProcessRuleId = processRuleId.value; + const oldChildProcessRuleId = childProcessRuleId.value; + if (payload.ruleType === 2 && payload.ruleType !== oldRuleType && processRuleList.value.length === 0) { + await loadProcessRuleData(); + } + if (payload.ruleType === 1 && oldRuleType === 2) { + processRuleId.value = null; + childProcessRuleId.value = null; + processRuleNoteId.value = null; + childProcessRuleList.value = []; + processRuleNoteList.value = []; + } + if (payload.processRuleId != null && payload.processRuleId !== oldProcessRuleId) { + childProcessRuleId.value = null; + processRuleNoteId.value = null; + childProcessRuleList.value = []; + processRuleNoteList.value = []; + } + if (payload.childProcessRuleId != null && payload.childProcessRuleId !== oldChildProcessRuleId) { + processRuleNoteId.value = null; + processRuleNoteList.value = []; + } + if (payload.ruleType != null) ruleType.value = payload.ruleType; + if (payload.packageMethodId !== undefined) packageMethodId.value = payload.packageMethodId ?? 2; + if (payload.processRuleId !== undefined) processRuleId.value = payload.processRuleId; + if (payload.childProcessRuleId !== undefined) childProcessRuleId.value = payload.childProcessRuleId; + if (payload.processRuleNoteId !== undefined) processRuleNoteId.value = payload.processRuleNoteId; + if (payload.dosage != null) dosage.value = payload.dosage; + if (payload.dayDosage != null) dayDosage.value = payload.dayDosage; + if (payload.processRuleId != null && payload.processRuleId !== oldProcessRuleId) { + await loadProcessRuleData(payload.processRuleId); + } + if (payload.childProcessRuleId != null && payload.childProcessRuleId !== oldChildProcessRuleId) { + await loadProcessRuleData(0, payload.childProcessRuleId); + } + emitCurrent(); +} + +watch( + () => [ + props.processRuleId, + props.childProcessRuleId, + props.processRuleNoteId, + props.dosage, + props.dayDosage, + props.ruleType, + ], + () => { + const same = + toId(props.processRuleId) === processRuleId.value && + toId(props.childProcessRuleId) === childProcessRuleId.value && + toId(props.processRuleNoteId) === processRuleNoteId.value && + Number(props.dosage || 7) === dosage.value && + Number(props.dayDosage || 2) === dayDosage.value && + (props.ruleType !== 1 && props.ruleType !== 2 || props.ruleType === ruleType.value); + if (same) return; + void hydrate(); + }, +); + +onMounted(() => { + void hydrate(); +}); + + + diff --git a/apps/web-antd/src/components/loading/OcrImageLoading.vue b/apps/web-antd/src/components/loading/OcrImageLoading.vue new file mode 100644 index 00000000..c856fab4 --- /dev/null +++ b/apps/web-antd/src/components/loading/OcrImageLoading.vue @@ -0,0 +1,872 @@ + + + + + diff --git a/apps/web-antd/src/components/ocr-origin-image-float/OcrOriginImageFloat.vue b/apps/web-antd/src/components/ocr-origin-image-float/OcrOriginImageFloat.vue new file mode 100644 index 00000000..32759c1e --- /dev/null +++ b/apps/web-antd/src/components/ocr-origin-image-float/OcrOriginImageFloat.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/apps/web-antd/src/components/ocr-origin-image-float/index.ts b/apps/web-antd/src/components/ocr-origin-image-float/index.ts new file mode 100644 index 00000000..9a76d23c --- /dev/null +++ b/apps/web-antd/src/components/ocr-origin-image-float/index.ts @@ -0,0 +1,4 @@ +/** + * 处方原图独立预览浮窗 + */ +export { default as OcrOriginImageFloat } from './OcrOriginImageFloat.vue'; diff --git a/apps/web-antd/src/components/ocr-origin-image-panel/OcrOriginImagePanel.vue b/apps/web-antd/src/components/ocr-origin-image-panel/OcrOriginImagePanel.vue new file mode 100644 index 00000000..05facb5a --- /dev/null +++ b/apps/web-antd/src/components/ocr-origin-image-panel/OcrOriginImagePanel.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/apps/web-antd/src/components/ocr-origin-image-panel/index.ts b/apps/web-antd/src/components/ocr-origin-image-panel/index.ts new file mode 100644 index 00000000..3f1e5b0b --- /dev/null +++ b/apps/web-antd/src/components/ocr-origin-image-panel/index.ts @@ -0,0 +1,4 @@ +/** + * 处方原图对照模块(内嵌布局栏,非浮窗) + */ +export { default as OcrOriginImagePanel } from './OcrOriginImagePanel.vue'; diff --git a/apps/web-antd/src/utils/imageTransferQuote.ts b/apps/web-antd/src/utils/imageTransferQuote.ts new file mode 100644 index 00000000..2707d490 --- /dev/null +++ b/apps/web-antd/src/utils/imageTransferQuote.ts @@ -0,0 +1,67 @@ +/** + * 识图传方报价:请求侧增量组装 + * 首次全量 drugs;同味数下只传 quote_token + drugs_patch + */ + +export type QuoteDrugRow = { + drug_id: number; + name?: string; + number?: number; + price?: number; + way_id?: number; + [key: string]: unknown; +}; + +/** 比较两味报价药品是否等价(用于算 patch) */ +export function isSameQuoteDrug( + a?: QuoteDrugRow | null, + b?: QuoteDrugRow | null, +): boolean { + if (!a || !b) return false; + return ( + Number(a.drug_id || 0) === Number(b.drug_id || 0) && + Number(a.number || 0) === Number(b.number || 0) && + Number(a.price || 0) === Number(b.price || 0) && + String(a.name || '') === String(b.name || '') && + Number(a.way_id || 0) === Number(b.way_id || 0) + ); +} + +/** + * 组装增量报价入参 + * 为什么:点选改一味时不必每次 POST 整方 drugs,服务端用 token 缓存整方再 merge patch + */ +export function buildIncrementalQuotePayload(opts: { + scalars: Record; + drugs: QuoteDrugRow[]; + quoteToken?: string; + lastQuoteDrugs?: QuoteDrugRow[] | null; +}): { payload: Record; nextLastDrugs: QuoteDrugRow[] } { + const scalars = opts.scalars || {}; + const drugs = Array.isArray(opts.drugs) ? opts.drugs : []; + const quoteToken = String(opts.quoteToken || '').trim(); + const prev = Array.isArray(opts.lastQuoteDrugs) ? opts.lastQuoteDrugs : null; + const payload: Record = { ...scalars }; + if (quoteToken) { + payload.quote_token = quoteToken; + } + if (!quoteToken || !prev || prev.length !== drugs.length) { + payload.drugs = drugs; + return { payload, nextLastDrugs: drugs }; + } + const patches: Array<{ index: number; drug: QuoteDrugRow }> = []; + for (let i = 0; i < drugs.length; i++) { + if (!isSameQuoteDrug(prev[i], drugs[i])) { + patches.push({ index: i, drug: drugs[i] }); + } + } + if (patches.length === 0) { + return { payload, nextLastDrugs: prev }; + } + if (patches.length >= drugs.length) { + payload.drugs = drugs; + } else { + payload.drugs_patch = patches; + } + return { payload, nextLastDrugs: drugs }; +} diff --git a/apps/web-antd/src/views/business/order/image-transfer-order/api/index.ts b/apps/web-antd/src/views/business/order/image-transfer-order/api/index.ts new file mode 100644 index 00000000..28f3e794 --- /dev/null +++ b/apps/web-antd/src/views/business/order/image-transfer-order/api/index.ts @@ -0,0 +1,38 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'image-transfer-order/'; + +/** 传方订单列表 */ +export async function getImageTransferOrderList(params: Record) { + return requestClient.get(`${prefix}list`, { params }); +} + +/** 传方订单详情,审方前看原图、处方和发货 */ +export async function getImageTransferOrderDetail(params: { id: number }) { + return requestClient.get(`${prefix}detail`, { params }); +} + +/** 审核通过并推 MES,会带上审方改过的患者信息和药品 */ +export async function approveImageTransferOrder(data: Record) { + return requestClient.post(`${prefix}approve`, data); +} + +/** 驳回,原因必填 */ +export async function rejectImageTransferOrder(data: { id: number; reason: string }) { + return requestClient.post(`${prefix}reject`, data); +} + +/** 推送失败后重推,同样带上改过的处方 */ +export async function retryImageTransferOrder(data: Record) { + return requestClient.post(`${prefix}retry`, data); +} + +/** 审方端报价预览:改地址/代煎/药品后即时重算,不落库 */ +export async function quoteImageTransferOrder(data: Record) { + return requestClient.post(`${prefix}quote`, data); +} + +/** 医生/诊所:待审或驳回修改后再交 */ +export async function resubmitImageTransferOrder(data: Record) { + return requestClient.post(`${prefix}resubmit`, data); +} diff --git a/apps/web-antd/src/views/business/order/image-transfer-order/components/detail-drawer.vue b/apps/web-antd/src/views/business/order/image-transfer-order/components/detail-drawer.vue new file mode 100644 index 00000000..5b3a57a4 --- /dev/null +++ b/apps/web-antd/src/views/business/order/image-transfer-order/components/detail-drawer.vue @@ -0,0 +1,695 @@ + + + + + diff --git a/apps/web-antd/src/views/business/order/image-transfer-order/components/order-card.vue b/apps/web-antd/src/views/business/order/image-transfer-order/components/order-card.vue new file mode 100644 index 00000000..386db822 --- /dev/null +++ b/apps/web-antd/src/views/business/order/image-transfer-order/components/order-card.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/apps/web-antd/src/views/business/order/image-transfer-order/index.vue b/apps/web-antd/src/views/business/order/image-transfer-order/index.vue new file mode 100644 index 00000000..cf3b9a81 --- /dev/null +++ b/apps/web-antd/src/views/business/order/image-transfer-order/index.vue @@ -0,0 +1,145 @@ + + + diff --git a/apps/web-antd/src/views/business/order/product-order/components/detail.vue b/apps/web-antd/src/views/business/order/product-order/components/detail.vue index 5c4d9871..cbb2d78e 100644 --- a/apps/web-antd/src/views/business/order/product-order/components/detail.vue +++ b/apps/web-antd/src/views/business/order/product-order/components/detail.vue @@ -502,7 +502,9 @@ function prescriptionStatusColor() { - {{ data.address?.name }} + + {{ data.address?.name || data.express_name || '—' }} + {{ data.express_name || data.patient?.name || data.cancel_remark @@ -513,12 +515,24 @@ function prescriptionStatusColor() { {{ - `${data.address?.province || ''} ${data.address?.region || ''} ${data.address?.detail_address || ''}` + ( + [ + data.address?.province, + data.address?.region || data.express_region, + data.address?.detail_address || data.express_address, + ] + .filter(Boolean) + .join(' ') || + [data.express_region, data.express_address] + .filter(Boolean) + .join(' ') || + '—' + ) }} @@ -540,6 +554,7 @@ function prescriptionStatusColor() { class="tcm-detail-chip w-[220px] max-w-full flex-shrink-0 rounded-lg border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-600 dark:bg-gray-900" >
+ 辅料 【{{ item.drug?.drug_number }}】{{ item.drug_name }}
diff --git a/apps/web-antd/src/views/business/order/product-order/config/constants.ts b/apps/web-antd/src/views/business/order/product-order/config/constants.ts index 878d347b..a3db5cf5 100644 --- a/apps/web-antd/src/views/business/order/product-order/config/constants.ts +++ b/apps/web-antd/src/views/business/order/product-order/config/constants.ts @@ -24,6 +24,7 @@ export const PRESCRIPTION_TYPE_MAP: Record = { 5: { label: '产品服务包', color: 'pink' }, 6: { label: '非药品', color: 'cyan' }, 7: { label: '医疗器械', color: 'geekblue' }, + 8: { label: '辅料', color: 'lime' }, }; /** 订单状态(status)映射,与列表原 if-else 链颜色语义一致 */ 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 b65bcec2..69a8e662 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 @@ -1503,6 +1503,7 @@ function buildRowActions(row: Record): { class="tcm-drug-chip w-[200px] max-w-full flex-shrink-0 rounded-lg border border-gray-200 bg-white p-3 text-sm dark:border-gray-600 dark:bg-gray-900" >
+ 辅料 【{{ item.drug?.drug_number }}】{{ item.display_drug_name || item.drug_name }}
diff --git a/apps/web-antd/src/views/business/product/china-medicine/config/form.ts b/apps/web-antd/src/views/business/product/china-medicine/config/form.ts index 6ec744fb..452996e6 100644 --- a/apps/web-antd/src/views/business/product/china-medicine/config/form.ts +++ b/apps/web-antd/src/views/business/product/china-medicine/config/form.ts @@ -44,14 +44,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', }, { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入药品别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '药品别名', - rules: 'required', }, { component: 'VbenInput', diff --git a/apps/web-antd/src/views/business/product/excipient/api/index.ts b/apps/web-antd/src/views/business/product/excipient/api/index.ts new file mode 100644 index 00000000..e10181f2 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/api/index.ts @@ -0,0 +1,105 @@ +import { requestClient } from '#/api/request'; + +const prefix = 'excipient/'; + +/** + * 分页查询辅料列表 + * @param data + */ +export async function getExcipientList(data: any) { + return requestClient.get(`${prefix}list`, { params: data }); +} + +/** + * 辅料下拉选项 + * @param data + */ +export async function getExcipientOption(data: any) { + return requestClient.get(`${prefix}option`, { params: data }); +} + +/** + * 获取辅料详情 + * @param id + */ +export async function getExcipientInfo(id: number) { + return requestClient.get(`${prefix}detail`, { params: { id } }); +} + +/** + * 导出辅料药品 + */ +export async function exportExcipientApi() { + return requestClient.download(`${prefix}export`); +} + +/** + * 新增辅料 + * @param data + */ +export async function createExcipient(data: Record) { + return requestClient.post(`${prefix}create`, data); +} + +/** + * 编辑辅料 + * @param data + */ +export async function updateExcipient(data: Record) { + return requestClient.post(`${prefix}update`, data); +} + +/** + * 删除辅料 + * @param data + */ +export async function deleteExcipient(data: Record) { + return requestClient.post(`${prefix}delete`, data); +} + +/** + * 导入辅料 + * @param data + */ +export async function importExcipientApi(data: Record) { + return requestClient.upload(`${prefix}import`, data); +} + +/** + * 预览批量更新药品编码(只读) + */ +export async function previewBatchDrugNumberApi(data: { + items: { drug_name: string; drug_number: string }[]; +}) { + return requestClient.post(`${prefix}preview-batch-drug-number`, data); +} + +/** + * 批量更新编码:全量辅料 id/名称/编号(卡片参考) + */ +export async function getAllDrugNumbersForBatchApi() { + return requestClient.get<{ + items: { + id: number; + drug_name: string; + drug_number: null | string; + pinyin_simple: string; + }[]; + }>(`${prefix}list-all-drug-numbers-for-batch`); +} + +/** + * 确认批量更新药品编码 + */ +export async function applyBatchDrugNumberApi(data: { + items: { id: number; drug_number: string }[]; +}) { + return requestClient.post(`${prefix}apply-batch-drug-number`, data); +} + +/** + * 同步空拼音(首拼/全拼/数字串) + */ +export async function syncExcipientPinyin() { + return requestClient.post(`${prefix}sync-pinyin`, {}); +} diff --git a/apps/web-antd/src/views/business/product/excipient/components/DrugNumberBatchModal.vue b/apps/web-antd/src/views/business/product/excipient/components/DrugNumberBatchModal.vue new file mode 100644 index 00000000..2e0e987f --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/components/DrugNumberBatchModal.vue @@ -0,0 +1,489 @@ + + + + + diff --git a/apps/web-antd/src/views/business/product/excipient/components/ExcelUpload.vue b/apps/web-antd/src/views/business/product/excipient/components/ExcelUpload.vue new file mode 100644 index 00000000..2b093d4e --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/components/ExcelUpload.vue @@ -0,0 +1,169 @@ + + diff --git a/apps/web-antd/src/views/business/product/excipient/components/modal.vue b/apps/web-antd/src/views/business/product/excipient/components/modal.vue new file mode 100644 index 00000000..31c8ae76 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/components/modal.vue @@ -0,0 +1,79 @@ + + diff --git a/apps/web-antd/src/views/business/product/excipient/config/form.ts b/apps/web-antd/src/views/business/product/excipient/config/form.ts new file mode 100644 index 00000000..8d88aba5 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/config/form.ts @@ -0,0 +1,167 @@ +import type {VbenFormProps} from '#/adapter/form'; + + +export const modalFormProps: VbenFormProps = { + wrapperClass: 'grid-cols-12', // 24栅格, + commonConfig: { + formItemClass: 'col-span-12', + // 所有表单项 + componentProps: { + class: 'w-full', + }, + }, + // handleSubmit: onSubmit, + layout: 'horizontal', + schema: [ + { + component: 'VbenInput', + fieldName: 'id', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['id'], + }, + }, + { + component: 'VbenInput', + fieldName: 'showSelect', + label: 'ID', + formItemClass: 'col-span-6', + dependencies: { + show: false, + triggerFields: ['id'], + }, + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入药品昵称', + }, + fieldName: 'drug_name', + formItemClass: 'col-span-6', + label: '药品昵称', + rules: 'required', + }, + { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 + component: 'VbenInput', + componentProps: { + placeholder: '选填,多个可用顿号/逗号分隔', + }, + fieldName: 'drug_alias', + formItemClass: 'col-span-6', + label: '药品别名', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '可留空,保存时按药名自动生成', + }, + fieldName: 'pinyin_simple', + formItemClass: 'col-span-6', + label: '拼音首拼', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '可留空,保存时按药名自动生成', + }, + fieldName: 'pinyin_full', + formItemClass: 'col-span-6', + label: '拼音全拼', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '可留空,保存时从药名提取数字', + }, + fieldName: 'search_digits', + formItemClass: 'col-span-6', + label: '检索数字', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '选填,默认 -1', + }, + fieldName: 'drug_number', + formItemClass: 'col-span-6', + label: '药品编号', + defaultValue: '-1', + }, + { + component: 'VbenInput', + componentProps: { + placeholder: '请输入规格', + }, + fieldName: 'specification', + formItemClass: 'col-span-6', + label: '规格', + rules: 'required', + }, + { + // 按药配置开方快捷数量:逗号分隔,空则开方侧不显示快捷按钮 + component: 'VbenInput', + componentProps: { + placeholder: '选填,如 5,10,15(逗号分隔)', + }, + fieldName: 'quick_numbers', + formItemClass: 'col-span-6', + label: '快捷数量', + }, + // 辅料不需要中药那套:使用时间/方法/单位/频率;开方侧单位缺省按 g + { + component: 'RadioGroup', + componentProps: { + placeholder: '请选择', + options: [ + { + label: '草稿', + value: 1, + }, + { + label: '下架', + value: 2, + }, + { + label: '上架', + value: 3, + }, + ], + }, + defaultValue: 1, + formItemClass: 'col-span-12', + fieldName: 'status', + label: '商品状态', + rules: 'required', + }, + ], + showDefaultActions: false, +}; +export const uploadExcelProps: VbenFormProps = { + wrapperClass: 'grid-cols-12', // 24栅格, + commonConfig: { + formItemClass: 'col-span-12', + // 所有表单项 + componentProps: { + class: 'w-full', + }, + }, + // handleSubmit: onSubmit, + layout: 'horizontal', + schema: [ + { + component: 'Upload', + componentProps: { + placeholder: '请选择', + multiple: true, + }, + fieldName: 'frequency_id', + formItemClass: 'col-span-12', + label: '导入excel', + rules: 'file', + }, + ], + showDefaultActions: false, +}; diff --git a/apps/web-antd/src/views/business/product/excipient/config/search.ts b/apps/web-antd/src/views/business/product/excipient/config/search.ts new file mode 100644 index 00000000..8f592104 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/config/search.ts @@ -0,0 +1,37 @@ +import type { VbenFormProps } from '#/adapter/form'; + +// import dayjs from 'dayjs'; + +export const formOptions: VbenFormProps = { + // 默认展开 + collapsed: false, + schema: [ + { + component: 'VbenInput', + componentProps: { + placeholder: '输入名称', + }, + defaultValue: '', + fieldName: 'name', + label: '辅料名称', + }, + // { + // component: 'RangePicker', + // componentProps: { + // format: 'YYYY-MM-DD', // 确保格式与你需要的一致 + // }, + // // defaultValue: [dayjs().startOf('month'), dayjs()], + // fieldName: 'search_time', + // label: '时间范围', + // }, + ], + // 控制表单是否显示折叠按钮 + showCollapseButton: true, + submitButtonOptions: { + content: '查询', + }, + // 是否在字段值改变时提交表单 + submitOnChange: true, + // 按下回车时是否提交表单 + submitOnEnter: false, +}; diff --git a/apps/web-antd/src/views/business/product/excipient/config/table.ts b/apps/web-antd/src/views/business/product/excipient/config/table.ts new file mode 100644 index 00000000..5b30f804 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/config/table.ts @@ -0,0 +1,81 @@ +import type { VxeGridProps } from '#/adapter/vxe-table'; + +import { getExcipientList } from '../api'; + +interface RowType { + id: string; + name: string; + logo: string; + introduce: string; + created_at: string; +} + +export const gridOptions: VxeGridProps = { + checkboxConfig: { + highlight: true, + labelField: '', + }, + columnConfig: { + useKey: true, + }, + rowConfig: { + useKey: true, + }, + columns: [ + { type: 'checkbox', width: 60 }, + { field: 'id', align: 'left', title: 'ID', width: 100 }, + { field: 'drug_name', align: 'left', title: '药品名称', slots: { default: 'drug_name' } }, + { field: 'drug_number', align: 'left', title: '药品编码(erp)' }, + { field: 'specification', align: 'left', title: '规格', minWidth: 120 }, + { field: 'quick_numbers', align: 'left', title: '快捷数量', minWidth: 120 }, + { + field: 'central_price', + title: '总仓售价', + width: 110, + slots: { default: 'central_price' }, + }, + { field: 'pinyin_simple', title: '首拼', minWidth: 100, slots: { default: 'pinyin_simple' } }, + { field: 'pinyin_full', title: '全拼', minWidth: 140 }, + { field: 'search_digits', title: '数字', width: 90 }, + { field: 'status', title: '状态', slots: { default: 'status' } }, + { field: 'created_at', title: '发布时间' }, + { type: 'html', title: '操作', slots: { default: 'action' } }, + ], + keepSource: true, + pagerConfig: {}, + proxyConfig: { + ajax: { + // 请求后端接口方法 + query: async ({ page }, formValues) => { + return await getExcipientList({ + page: page.currentPage, + pageSize: page.pageSize, + ...formValues, + }); + }, + }, + }, + // exportConfig: { + // api: passApplicationApi, + // }, + height: 'auto', + border: false, + toolbarConfig: { + // 是否显示搜索表单控制按钮 + // @ts-ignore 正式环境时有完整的类型声明 + search: true, + refresh: true, // 刷新 + print: false, // 打印 + export: false, // 导出 + // custom: true, // 自定义列 + zoom: true, // 最大化最小化 + slots: { + buttons: 'toolbar-actions', + }, + custom: { + // 自定义列-图标 + icon: 'vxe-icon-menu', + }, + }, + showOverflow: false, +}; diff --git a/apps/web-antd/src/views/business/product/excipient/index.vue b/apps/web-antd/src/views/business/product/excipient/index.vue new file mode 100644 index 00000000..95e3c5e7 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/index.vue @@ -0,0 +1,321 @@ + + + + diff --git a/apps/web-antd/src/views/business/product/excipient/utils/parseDrugNumberExcel.ts b/apps/web-antd/src/views/business/product/excipient/utils/parseDrugNumberExcel.ts new file mode 100644 index 00000000..0825f046 --- /dev/null +++ b/apps/web-antd/src/views/business/product/excipient/utils/parseDrugNumberExcel.ts @@ -0,0 +1,214 @@ +import * as XLSX from 'xlsx'; + +const MAX_ROWS = 5000; + +export type ParsedDrugNumberRow = { + drug_name: string; + drug_number: string; +}; + +function normalizeCell(value: unknown): string { + if (value === null || value === undefined) { + return ''; + } + if (typeof value === 'number') { + return Number.isInteger(value) ? String(value) : String(value).trim(); + } + return String(value).trim(); +} + +/** 表头含空白或全角空格时仍能匹配 */ +function pickByHeaderAliases( + row: Record, + aliases: string[], +): string { + for (const key of Object.keys(row)) { + const k = key.replace(/\s+/g, '').replace(/\u3000/g, ''); + for (const alias of aliases) { + if (k === alias.replace(/\s+/g, '').replace(/\u3000/g, '')) { + return normalizeCell(row[key]); + } + } + } + return ''; +} + +function parseRowsByObjectKeys( + rows: Record[], +): { items: ParsedDrugNumberRow[]; invalidCount: number } { + const items: ParsedDrugNumberRow[] = []; + let invalidCount = 0; + const n = Math.min(rows.length, MAX_ROWS); + + const numberAliases = [ + '药品编号', + '药品编码', + '编号', + 'drug_number', + '编码', + ]; + const nameAliases = ['药品名称', '药名', 'drug_name', '名称']; + + for (let i = 0; i < n; i++) { + const row = rows[i]; + if (!row || typeof row !== 'object') { + invalidCount++; + continue; + } + + let drugNumber = + pickByHeaderAliases(row, numberAliases) || + normalizeCell(row['药品编号']) || + normalizeCell(row['药品编码']) || + normalizeCell(row['编号']) || + normalizeCell(row['drug_number']); + + let drugName = + pickByHeaderAliases(row, nameAliases) || + normalizeCell(row['药品名称']) || + normalizeCell(row['药名']) || + normalizeCell(row['drug_name']); + + if (!drugName || !drugNumber) { + const empty = !Object.values(row).some( + (v) => normalizeCell(v) !== '', + ); + if (empty) { + continue; + } + invalidCount++; + continue; + } + + items.push({ + drug_name: drugName, + drug_number: drugNumber, + }); + } + + return { items, invalidCount }; +} + +/** + * 标准「药品编码」表:第 1 列为编号、第 2 列为名称(表头行自动跳过) + */ +function parseRowsByPosition(sheet: XLSX.WorkSheet): { + items: ParsedDrugNumberRow[]; + invalidCount: number; + rowCount: number; +} { + const aoa = XLSX.utils.sheet_to_json(sheet, { + header: 1, + defval: '', + }) as unknown[][]; + + const items: ParsedDrugNumberRow[] = []; + let invalidCount = 0; + if (aoa.length === 0) { + return { items: [], invalidCount: 0, rowCount: 0 }; + } + + let start = 0; + const h0 = normalizeCell(aoa[0]?.[0]); + const h1 = normalizeCell(aoa[0]?.[1]); + if ( + h0 && + (h0.includes('编号') || + h0.includes('编码') || + h0.includes('代码') || + /药品/.test(h0)) + ) { + start = 1; + } + if ( + h1 && + (h1.includes('名称') || h1.includes('药名') || h1.includes('品名')) + ) { + start = 1; + } + + const limit = Math.min(aoa.length, start + MAX_ROWS); + for (let i = start; i < limit; i++) { + const row = aoa[i]; + if (!row?.length) { + continue; + } + const drugNumber = normalizeCell(row[0]); + const drugName = normalizeCell(row[1]); + if (!drugName || !drugNumber) { + const anyCell = row.some((c) => normalizeCell(c) !== ''); + if (anyCell) { + invalidCount++; + } + continue; + } + items.push({ drug_name: drugName, drug_number: drugNumber }); + } + + return { items, invalidCount, rowCount: aoa.length }; +} + +/** + * 从第一张工作表解析「药品编号」「药品名称」列(兼容表头别名 + 列位置回退) + */ +export function parseDrugNumberExcelBuffer(buffer: ArrayBuffer): { + items: ParsedDrugNumberRow[]; + invalidCount: number; + rowCount: number; +} { + const workbook = XLSX.read(buffer, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + if (!sheetName) { + return { items: [], invalidCount: 0, rowCount: 0 }; + } + const sheet = workbook.Sheets[sheetName]; + if (!sheet) { + return { items: [], invalidCount: 0, rowCount: 0 }; + } + + const rows = XLSX.utils.sheet_to_json>(sheet, { + defval: '', + }); + + let { items, invalidCount } = parseRowsByObjectKeys(rows); + let rowCount = rows.length; + + if (items.length === 0) { + const pos = parseRowsByPosition(sheet); + items = pos.items; + invalidCount = pos.invalidCount; + rowCount = pos.rowCount; + } + + return { + items, + invalidCount, + rowCount, + }; +} + +/** + * 与后端 `normalizeDrugNumberForBatch` 对齐,用于对比是否变更 + */ +export function normalizeDrugNumberForCompare(value: unknown): string { + if (value === null || value === undefined || value === '') { + return ''; + } + if (typeof value === 'boolean') { + return ''; + } + if (typeof value === 'number') { + if (Number.isInteger(value) || Math.floor(value) === value) { + return String(Math.trunc(value)); + } + return String(value).trim(); + } + return String(value).trim(); +} + +export function isDrugNumberChanged( + oldVal: unknown, + newVal: unknown, +): boolean { + return normalizeDrugNumberForCompare(oldVal) !== normalizeDrugNumberForCompare(newVal); +} diff --git a/apps/web-antd/src/views/business/product/health-food/config/form.ts b/apps/web-antd/src/views/business/product/health-food/config/form.ts index aa0e6440..c882ca87 100644 --- a/apps/web-antd/src/views/business/product/health-food/config/form.ts +++ b/apps/web-antd/src/views/business/product/health-food/config/form.ts @@ -145,15 +145,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', // 必填验证 }, { - // 保健食品别名字段:必填,保健食品的其他名称 + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入保健食品别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '保健食品别名', - rules: 'required', // 必填验证 }, { // 保健食品编号字段:必填,保健食品的唯一编号 diff --git a/apps/web-antd/src/views/business/product/medical-device/config/form.ts b/apps/web-antd/src/views/business/product/medical-device/config/form.ts index d8a7ff03..6cef76a3 100644 --- a/apps/web-antd/src/views/business/product/medical-device/config/form.ts +++ b/apps/web-antd/src/views/business/product/medical-device/config/form.ts @@ -121,14 +121,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', }, { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入器械别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '器械别名', - rules: 'required', }, { component: 'VbenInput', diff --git a/apps/web-antd/src/views/business/product/non-drug/config/form.ts b/apps/web-antd/src/views/business/product/non-drug/config/form.ts index e55d5cc8..dc5bb579 100644 --- a/apps/web-antd/src/views/business/product/non-drug/config/form.ts +++ b/apps/web-antd/src/views/business/product/non-drug/config/form.ts @@ -121,14 +121,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', }, { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入产品别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '产品别名', - rules: 'required', }, { component: 'VbenInput', diff --git a/apps/web-antd/src/views/business/product/service-pack/config/form.ts b/apps/web-antd/src/views/business/product/service-pack/config/form.ts index b189ad30..dd846802 100644 --- a/apps/web-antd/src/views/business/product/service-pack/config/form.ts +++ b/apps/web-antd/src/views/business/product/service-pack/config/form.ts @@ -134,14 +134,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', }, { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入服务包别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '服务包别名', - rules: 'required', }, { component: 'VbenInput', diff --git a/apps/web-antd/src/views/business/product/western-medicine/config/form.ts b/apps/web-antd/src/views/business/product/western-medicine/config/form.ts index 43139628..ef99ff2c 100644 --- a/apps/web-antd/src/views/business/product/western-medicine/config/form.ts +++ b/apps/web-antd/src/views/business/product/western-medicine/config/form.ts @@ -125,14 +125,14 @@ export const modalFormProps: VbenFormProps = { rules: 'required', }, { + // 别名选填:无别名也可保存,后端 syncAliases 空串会清空别名表 component: 'VbenInput', componentProps: { - placeholder: '请输入药品别名', + placeholder: '选填,多个可用顿号/逗号分隔', }, fieldName: 'drug_alias', formItemClass: 'col-span-6', label: '药品别名', - rules: 'required', }, { component: 'VbenInput', diff --git a/apps/web-antd/src/views/business/warehouse-drug-management/shared/useWarehouseDrugTypeRoute.ts b/apps/web-antd/src/views/business/warehouse-drug-management/shared/useWarehouseDrugTypeRoute.ts index d66fd574..f4c54071 100644 --- a/apps/web-antd/src/views/business/warehouse-drug-management/shared/useWarehouseDrugTypeRoute.ts +++ b/apps/web-antd/src/views/business/warehouse-drug-management/shared/useWarehouseDrugTypeRoute.ts @@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'; import { getWarehouseProductTypeOptions } from '../admin/api'; -const VALID_TYPES = [1, 2, 3, 5, 6, 7]; +const VALID_TYPES = [1, 2, 3, 5, 6, 7, 8]; function resolveProductType(route: RouteLocationNormalizedLoaded): number { const rawParam = route.params.type; diff --git a/apps/web-antd/src/views/clinic/image-transfer-prescription/api/index.ts b/apps/web-antd/src/views/clinic/image-transfer-prescription/api/index.ts new file mode 100644 index 00000000..41ae0140 --- /dev/null +++ b/apps/web-antd/src/views/clinic/image-transfer-prescription/api/index.ts @@ -0,0 +1,62 @@ +/** + * 诊所 PC 识图传方 API(xk-api clinic-image-transfer) + * 本店范围,提交须带 doctor_id + */ +import { requestClient } from '#/api/request'; + +const prefix = 'clinic-image-transfer/'; + +/** 处方图 OCR */ +export async function clinicOcrPrescriptionImageApi(data: { + image_urls: string[]; +}) { + return requestClient.post(`${prefix}ocr-prescription-image`, data); +} + +/** 确认提交传方订单 */ +export async function clinicConfirmImageTransferApi(data: Record) { + return requestClient.post(`${prefix}confirm`, data); +} + +/** 本店识图生成记录 */ +export async function clinicAiListGenerationsApi(params?: { limit?: number }) { + return requestClient.get(`${prefix}ai-list-generations`, { params }); +} + +/** 本店识图记录详情 */ +export async function clinicAiGenerationDetailApi(params: { id: number }) { + return requestClient.get(`${prefix}ai-generation-detail`, { params }); +} + +/** 匹配就诊人 */ +export async function clinicMatchTransferPatientApi(data: { + patient_name?: string; + patient_mobile?: string; + name?: string; + mobile?: string; +}) { + return requestClient.post(`${prefix}match-transfer-patient`, data); +} + +/** 报价预览 */ +export async function clinicQuoteTransferImageApi(data: Record) { + return requestClient.post(`${prefix}quote`, data); +} + +/** 本店医生下拉(必选) */ +export async function clinicTransferDoctorOptionApi() { + return requestClient.get(`${prefix}doctor-option`); +} + +/** 本店药库搜索 */ +export async function clinicProductListApi(params: Record) { + return requestClient.get(`${prefix}product-list`, { params }); +} + +/** 加工规则列表 */ +export async function clinicProcessRuleListApi(params?: { + pid?: number; + rule_id?: number; +}) { + return requestClient.get(`${prefix}process-rule-list`, { params }); +} diff --git a/apps/web-antd/src/views/clinic/image-transfer-prescription/index.vue b/apps/web-antd/src/views/clinic/image-transfer-prescription/index.vue new file mode 100644 index 00000000..1d2d280a --- /dev/null +++ b/apps/web-antd/src/views/clinic/image-transfer-prescription/index.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/web-antd/src/views/doctor/doctor-reception/api/index.ts b/apps/web-antd/src/views/doctor/doctor-reception/api/index.ts index 0d5748d8..61d14029 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/api/index.ts +++ b/apps/web-antd/src/views/doctor/doctor-reception/api/index.ts @@ -45,6 +45,18 @@ export async function getProductListDoctorReception(data: any) { return requestClient.get(`${prefix}product-list`, { params: data }); } +/** + * 医生用过的辅料(发方成功云端沉淀;气泡空搜优先展示) + * xk-api GET doctor-reception/used-excipient-list + */ +export async function getUsedExcipientListDoctorReception(data: { + store_id?: number; + register_id?: number; + limit?: number; +}) { + return requestClient.get(`${prefix}used-excipient-list`, { params: data }); +} + /** * 复用/常用方填方前对比中药快照价与当前门店价 * 为什么:供货比例变更后关系表已更新,快照价不一致时要让医生确认是否按最新价计算 @@ -702,3 +714,26 @@ export async function confirmTransferImageOcrApi(data: Record) { patient_action: string; }>(`${prefix}confirm-transfer-image-ocr`, data); } + +/** 识图传方主动匹配就诊人 */ +export async function matchTransferPatientApi(data: { + patient_name?: string; + patient_mobile?: string; + name?: string; + mobile?: string; +}) { + return requestClient.post<{ + matched: boolean; + user_patient_id: number; + name: string; + mobile: string; + sex: number; + age: number; + address: string; + }>(`${prefix}match-transfer-patient`, data); +} + +/** 医生识图传方报价预览 */ +export async function quoteTransferImageApi(data: Record) { + return requestClient.post(`${prefix}quote-image-transfer`, data); +} diff --git a/apps/web-antd/src/views/doctor/doctor-reception/components/CreateUserPatientModal.vue b/apps/web-antd/src/views/doctor/doctor-reception/components/CreateUserPatientModal.vue index 64b9726a..2287460d 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/components/CreateUserPatientModal.vue +++ b/apps/web-antd/src/views/doctor/doctor-reception/components/CreateUserPatientModal.vue @@ -36,6 +36,12 @@ import { PERSONAL_HISTORY_OPTIONS, PRESENT_HISTORY_OPTIONS, } from '#/views/doctor/doctor-reception/config/health-history'; +import RegionAddressPicker from '#/components/form/components/region-address-picker.vue'; +import { + formatAddressDisplay, + resolveAddressValue, + type AddressValue, +} from '#/util/address-index'; /** 创建成功回传:含 register_id / user_patient_id / is_pay,供接诊页分情况引导 */ export type CreateUserPatientResult = { @@ -109,6 +115,11 @@ function buildEmptyForm() { guardian_id_card: '', guardian_mobile: '', guardian_relation: 0, + // 收货地址选填:填了才建 yii_address 并绑 default_address_id + ship_region: undefined as AddressValue | undefined, + ship_detail_address: '', + ship_name: '', + ship_mobile: '', }; } @@ -224,13 +235,35 @@ function setHistoryTags(historyField: keyof ReturnType, t /** * 组装提交体:不展示的月经/婚育强制清零;history 仅在 status=有 时带出 + * 收货地址:有详细地址才带省市区字段,后端据此建 yii_address */ function buildSubmitPayload() { const f = form.value; const menstrualOk = showMenstrualHistory.value; const maritalOk = showMaritalHistory.value; + const resolved = resolveAddressValue(f.ship_region); + const detail = String(f.ship_detail_address || '').trim(); + const shipPayload = + detail && (resolved.province || resolved.city || resolved.district) + ? { + ship_province: resolved.province?.label || '', + ship_city: resolved.city?.label || '', + ship_area: resolved.district?.label || '', + ship_region: formatAddressDisplay(f.ship_region), + ship_detail_address: detail, + ship_name: String(f.ship_name || f.name || '').trim(), + ship_mobile: String(f.ship_mobile || f.mobile || '').trim(), + } + : {}; + const { + ship_region: _shipRegion, + ship_detail_address: _shipDetail, + ship_name: _shipName, + ship_mobile: _shipMobile, + ...rest + } = f; return { - ...f, + ...rest, waive_register_fee: f.waive_register_fee ? 1 : 0, menstrual_status: menstrualOk ? f.menstrual_status : 0, menstrual_history: menstrualOk && f.menstrual_status === 1 ? f.menstrual_history : [], @@ -242,6 +275,7 @@ function buildSubmitPayload() { family_history: f.family_status === 1 ? f.family_history : [], epidemic_history: f.epidemic_status === 1 ? f.epidemic_history : [], personal_history: f.personal_status === 1 ? f.personal_history : [], + ...shipPayload, }; } @@ -276,6 +310,15 @@ const [Modal, modalApi] = useVbenModal({ return; } } + // 填了收货地址时校验收件人:最多 10 个字(含默认用姓名) + const shipDetail = String(form.value.ship_detail_address || '').trim(); + if (shipDetail) { + const shipName = String(form.value.ship_name || form.value.name || '').trim(); + if (shipName.length > 10) { + message.error('收件人最多10个字'); + return; + } + } submitting.value = true; modalApi.setState({ confirmLoading: true }); try { @@ -393,6 +436,42 @@ function onBirthdayChange(v: Dayjs | string | null) { + +
+ 收货地址 + 选填,填写后开方默认使用 +
+ + + + + + + + + + + + + + + + + + + + + +
健康信息
diff --git a/apps/web-antd/src/views/doctor/doctor-reception/components/ImageOcrDrawer.vue b/apps/web-antd/src/views/doctor/doctor-reception/components/ImageOcrDrawer.vue new file mode 100644 index 00000000..05222779 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor-reception/components/ImageOcrDrawer.vue @@ -0,0 +1,1153 @@ + + + + + + + + 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 6aeb992c..f8bcb485 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 @@ -264,6 +264,22 @@ function parseRecipeContent(content: Record | string) { } return content; } + +/** + * 解析辅料列表:优先 recipe.excipients,其次处方 content.excipients + * 旧单无辅料字段时返回空数组,不影响药味展示 + */ +function resolveExcipients(prescription: Record, recipe: Record) { + const fromRecipe = recipe?.excipients; + if (Array.isArray(fromRecipe) && fromRecipe.length > 0) { + return fromRecipe; + } + const fromContent = prescription?.content?.excipients; + if (Array.isArray(fromContent) && fromContent.length > 0) { + return fromContent; + } + return []; +}