From b40c7402aa9f6d2ffc1dd5f7508c6ea08d4804ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Thu, 4 Jun 2026 16:05:02 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=8E=A8=E5=B9=BF=E5=91=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drug-search-select/drug-search-select.vue | 6 + .../form/components/promoter-picker.vue | 243 ++++++++++++++++++ .../web-antd/src/router/routes/modules/sys.ts | 6 + apps/web-antd/src/store/prescription.ts | 73 +++++- .../src/utils/chinesePrescriptionMargin.ts | 60 +++++ .../chat/components/PrescriptionModal.vue | 16 +- .../business/clinic-salesperson/api/index.ts | 23 ++ .../clinic-salesperson/commission.vue | 86 +++++++ .../business/clinic-salesperson/earnings.vue | 92 +++++++ .../business/clinic-salesperson/index.vue | 235 +++++++++++++++++ .../business/clinic-salesperson/leads.vue | 64 +++++ .../clinic-salesperson/settlement.vue | 114 ++++++++ .../src/views/business/order/api/order-ops.ts | 28 ++ .../components/china-erp-sync-log-drawer.vue | 224 ++++++++++++++++ .../order/product-order/config/table.ts | 64 ++--- .../business/order/product-order/index.vue | 200 ++++++++++++-- .../business/store/settings/api/index.ts | 30 +++ .../settings/components/SalespersonCard.vue | 175 ++++++++----- .../SalespersonCommissionDrawer.vue | 89 ++++++- .../components/SalespersonCreateModal.vue | 73 +++++- .../components/SalespersonUserBindDrawer.vue | 77 ++++++ .../business/store/settings/config/form.ts | 46 +++- .../views/business/store/settings/index.vue | 2 +- .../doctor/doctor-reception/api/index.ts | 9 +- .../views/doctor/doctor-reception/index.vue | 80 +++++- .../views/doctor/doctor/components/modal.vue | 1 + .../views/system/admin/_shared/role-meta.ts | 1 + .../system/admin/clinic-salesperson/index.vue | 6 + .../views/system/doctor-input/api/index.ts | 27 ++ .../src/views/system/doctor-input/audit.vue | 128 +++++++++ .../doctor-input/components/AuditModal.vue | 86 +++++++ .../doctor-input/components/DetailModal.vue | 146 +++++++++++ .../system/doctor-input/components/modal.vue | 78 ++++++ .../views/system/doctor-input/config/form.ts | 228 ++++++++++++++++ .../system/doctor-input/config/search.ts | 55 ++++ .../views/system/doctor-input/config/table.ts | 170 ++++++++++++ .../src/views/system/doctor-input/index.vue | 145 +++++++++++ .../system/pharmacy/components/modal.vue | 1 + .../src/views/system/pharmacy/config/table.ts | 7 + .../src/views/system/pharmacy/index.vue | 20 ++ .../views/system/process/components/modal.vue | 96 +++++-- .../process/components/volume-modal.vue | 79 ++++++ .../src/views/system/process/config/form.ts | 150 +++++++---- .../src/views/system/process/config/table.ts | 80 +++++- .../src/views/system/process/index.vue | 235 +++++++++++------ .../src/views/system/store-input/api/index.ts | 15 ++ .../src/views/system/store-input/audit.vue | 25 +- .../store-input/components/AuditModal.vue | 166 ++++++++++-- .../store-input/components/DetailModal.vue | 243 ++++++++++++------ .../components/DoctorInputCardGrid.vue | 196 ++++++++++++++ .../views/system/store-input/config/form.ts | 29 ++- .../views/system/store-input/config/search.ts | 12 +- .../views/system/store-input/config/table.ts | 20 +- .../src/views/system/store-input/index.vue | 36 ++- .../store-input/utils/doctorInputDisplay.ts | 83 ++++++ .../system/store-input/utils/inputUser.ts | 31 +++ .../src/views/system/store/api/index.ts | 8 + .../src/views/system/store/api/salesperson.ts | 4 + .../SalespersonCommissionDrawer.vue | 90 ++++++- .../views/system/store/components/modal.vue | 1 + .../src/views/system/store/config/table.ts | 7 + .../web-antd/src/views/system/store/index.vue | 20 ++ 62 files changed, 4373 insertions(+), 467 deletions(-) create mode 100644 apps/web-antd/src/components/form/components/promoter-picker.vue create mode 100644 apps/web-antd/src/utils/chinesePrescriptionMargin.ts create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/api/index.ts create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/commission.vue create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/earnings.vue create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/index.vue create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/leads.vue create mode 100644 apps/web-antd/src/views/business/clinic-salesperson/settlement.vue create mode 100644 apps/web-antd/src/views/business/order/components/china-erp-sync-log-drawer.vue create mode 100644 apps/web-antd/src/views/business/store/settings/components/SalespersonUserBindDrawer.vue create mode 100644 apps/web-antd/src/views/system/admin/clinic-salesperson/index.vue create mode 100644 apps/web-antd/src/views/system/doctor-input/api/index.ts create mode 100644 apps/web-antd/src/views/system/doctor-input/audit.vue create mode 100644 apps/web-antd/src/views/system/doctor-input/components/AuditModal.vue create mode 100644 apps/web-antd/src/views/system/doctor-input/components/DetailModal.vue create mode 100644 apps/web-antd/src/views/system/doctor-input/components/modal.vue create mode 100644 apps/web-antd/src/views/system/doctor-input/config/form.ts create mode 100644 apps/web-antd/src/views/system/doctor-input/config/search.ts create mode 100644 apps/web-antd/src/views/system/doctor-input/config/table.ts create mode 100644 apps/web-antd/src/views/system/doctor-input/index.vue create mode 100644 apps/web-antd/src/views/system/process/components/volume-modal.vue create mode 100644 apps/web-antd/src/views/system/store-input/components/DoctorInputCardGrid.vue create mode 100644 apps/web-antd/src/views/system/store-input/utils/doctorInputDisplay.ts create mode 100644 apps/web-antd/src/views/system/store-input/utils/inputUser.ts 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 02d13d36..e1c59c86 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 @@ -37,6 +37,10 @@ interface Props { * 诊所ID */ storeId?: number; + /** + * 挂号ID(用于取价门店与毛利率权限) + */ + registerId?: number; /** * 是否禁用 */ @@ -47,6 +51,7 @@ const props = withDefaults(defineProps(), { type: 1, placeholder: '输入药品名称搜索', storeId: 2, + registerId: undefined, disabled: false, }); @@ -120,6 +125,7 @@ const searchDrugs = debounce(async (keyword: string) => { name: keyword, type: props.type, store_id: props.storeId, + ...(props.registerId ? { register_id: props.registerId } : {}), }); // 处理返回数据 diff --git a/apps/web-antd/src/components/form/components/promoter-picker.vue b/apps/web-antd/src/components/form/components/promoter-picker.vue new file mode 100644 index 00000000..ad6ebff2 --- /dev/null +++ b/apps/web-antd/src/components/form/components/promoter-picker.vue @@ -0,0 +1,243 @@ + + + + + + + diff --git a/apps/web-antd/src/router/routes/modules/sys.ts b/apps/web-antd/src/router/routes/modules/sys.ts index 798d1b64..23ee646d 100644 --- a/apps/web-antd/src/router/routes/modules/sys.ts +++ b/apps/web-antd/src/router/routes/modules/sys.ts @@ -69,6 +69,12 @@ const adminRoleRoutes: RouteRecordRaw[] = [ path: '/system/admin/pharmacist', component: () => import('#/views/system/admin/pharmacist/index.vue'), }, + { + meta: { title: '诊所推广员' }, + name: 'SystemAdminClinicSalesperson', + path: '/system/admin/clinic-salesperson', + component: () => import('#/views/system/admin/clinic-salesperson/index.vue'), + }, ]; const routes: RouteRecordRaw[] = [ diff --git a/apps/web-antd/src/store/prescription.ts b/apps/web-antd/src/store/prescription.ts index efd2daca..25f1bf4a 100644 --- a/apps/web-antd/src/store/prescription.ts +++ b/apps/web-antd/src/store/prescription.ts @@ -1,4 +1,4 @@ -import { computed, nextTick, ref } from 'vue'; +import { computed, nextTick, ref, watch } from 'vue'; import { message, notification } from 'ant-design-vue'; import { debounce } from 'lodash-es'; @@ -10,12 +10,18 @@ import { sendMessage } from '#/views/business/chat/utils/request'; import { addWestPrescription, checkChineseMedicineConflictApi, + getCurrentStoreTypeApi, getDrugUseList, getMyStoreListApi, getPatientItem, getProcessRuleList, getProductListDoctorReception, } from '#/views/doctor/doctor-reception/api'; +import { + calcItemMarginPercent, + calcTotalMarginPercent, + isSeeRateEnabled, +} from '#/utils/chinesePrescriptionMargin'; import { getRegisterStoreInfo as getRegisterStoreInfoPharmacy } from '#/views/doctor/online-consultation/api'; import { getRegisterStoreInfo as getRegisterStoreInfoClinic } from '#/views/doctor/online-consultation-clinic/api'; import traditionalJson from '#/views/business/chat/config/traditional.json'; @@ -89,6 +95,9 @@ export const usePrescriptionStore = defineStore('prescription', () => { // 存储前缀(用于区分在线复诊-药店和在线复诊-诊所) const storagePrefix = ref('onlineConsultation-'); // 默认前缀 + /** 当前取价门店是否允许查看毛利率(0/1,来自 xk-api) */ + const seeRate = ref(0); + // 获取localStorage key(按类型分开存储) const getStorageKey = (category?: number) => { const cat = category ?? activeCategory.value; @@ -143,6 +152,56 @@ export const usePrescriptionStore = defineStore('prescription', () => { return totalProductCost.value + processingFee.value; }); + const resolvePrescribingStoreId = () => { + if (sendMode.value === 1 && selectedStoreId.value) { + return selectedStoreId.value; + } + return myStoreId.value; + }; + + const fetchStoreSeeRate = async () => { + try { + const params: { register_id?: number; store_id?: number } = {}; + const sid = resolvePrescribingStoreId(); + if (sid) { + params.store_id = sid; + } + if (currentRegisterId.value) { + params.register_id = Number(currentRegisterId.value); + } + const res = await getCurrentStoreTypeApi(params); + seeRate.value = Number(res?.see_rate ?? 0); + } catch (error) { + console.error('获取毛利率权限失败:', error); + } + }; + + watch( + [myStoreId, selectedStoreId, sendMode, currentRegisterId], + () => { + if (myStoreId.value) { + fetchStoreSeeRate(); + } + }, + ); + + const chineseGrossMarginPercent = computed(() => { + if (!isSeeRateEnabled(seeRate.value) || activeCategory.value !== 1) { + return null; + } + return calcTotalMarginPercent(currentDrugs.value, dosage.value); + }); + + const getChineseItemMargin = (drug: { + price?: number | string; + buy_price?: number | string; + }) => { + if (!isSeeRateEnabled(seeRate.value)) { + return null; + } + return calcItemMarginPercent(drug.price, drug.buy_price); + }; + // localStorage同步方法 const syncToLocalStorage = () => { try { @@ -240,6 +299,8 @@ export const usePrescriptionStore = defineStore('prescription', () => { await initializeBasicData(); } + await fetchStoreSeeRate(); + // 获取患者信息(只在需要时调用,如 PrescriptionModal) if (shouldFetchPatientInfo) { if ( @@ -377,6 +438,7 @@ export const usePrescriptionStore = defineStore('prescription', () => { sendMode.value = 1; } console.log('获取挂号诊所信息成功:', res); + await fetchStoreSeeRate(); } catch (error) { console.error('获取挂号诊所信息失败:', error); } @@ -435,6 +497,9 @@ export const usePrescriptionStore = defineStore('prescription', () => { store_id: myStoreId.value, type: activeCategory.value, name: searchText, + ...(currentRegisterId.value + ? { register_id: Number(currentRegisterId.value) } + : {}), }); drugList.value = res.map((item) => { @@ -476,6 +541,7 @@ export const usePrescriptionStore = defineStore('prescription', () => { ), unit: drugUnit.value.find((item) => item.id === data.drug.unit_id), price: data.price, + buy_price: data.buy_price, way_id: data.drug?.way_id, use_ways: drugUseWay.value.find((item) => item.id === data.drug.way_id), time_id: data.drug.time_id, @@ -605,6 +671,7 @@ export const usePrescriptionStore = defineStore('prescription', () => { ), unit: drugUnit.value.find((item) => item.id === data.drug.unit_id), price: data.price, + buy_price: data.buy_price, way_id: data.drug?.way_id, use_ways: drugUseWay.value.find((item) => item.id === data.drug.way_id), time_id: data.drug.time_id, @@ -985,6 +1052,10 @@ export const usePrescriptionStore = defineStore('prescription', () => { totalProductCost, processingFee, totalCost, + seeRate, + chineseGrossMarginPercent, + getChineseItemMargin, + fetchStoreSeeRate, // 方法 initializePrescription, diff --git a/apps/web-antd/src/utils/chinesePrescriptionMargin.ts b/apps/web-antd/src/utils/chinesePrescriptionMargin.ts new file mode 100644 index 00000000..2bae9423 --- /dev/null +++ b/apps/web-antd/src/utils/chinesePrescriptionMargin.ts @@ -0,0 +1,60 @@ +/** + * 中药处方毛利率计算(仅商品计费,不含诊疗费/加工费) + */ + +export function isSeeRateEnabled(seeRate: unknown): boolean { + return Number(seeRate) === 1; +} + +function isValidBuyPrice(value: unknown): boolean { + if (value === null || value === undefined || value === '') { + return false; + } + const n = Number(value); + return !Number.isNaN(n); +} + +export function calcItemMarginPercent( + price: number | string | undefined, + buyPrice: number | string | undefined, +): string { + const p = Number(price); + if (!p || p <= 0 || !isValidBuyPrice(buyPrice)) { + return '--'; + } + const b = Number(buyPrice); + return (((p - b) / p) * 100).toFixed(2); +} + +export interface ChineseDrugMarginRow { + price?: number | string; + buy_price?: number | string; + number?: number | string; + select_number?: number | string; +} + +export function calcTotalMarginPercent( + drugs: ChineseDrugMarginRow[], + dosage: number, +): string { + if (!drugs?.length || dosage <= 0) { + return '0.00'; + } + let saleTotal = 0; + let buyTotal = 0; + for (const drug of drugs) { + if (!isValidBuyPrice(drug.buy_price)) { + return '--'; + } + const qty = Number(drug.number ?? drug.select_number ?? 1); + const price = Number(drug.price || 0); + const buyPrice = Number(drug.buy_price); + saleTotal += price * qty * dosage; + buyTotal += buyPrice * qty * dosage; + } + if (saleTotal <= 0) { + return '0.00'; + } + return (((saleTotal - buyTotal) / saleTotal) * 100).toFixed(2); +} + \ No newline at end of file diff --git a/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue b/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue index eb806ec6..4dd4ac71 100644 --- a/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue +++ b/apps/web-antd/src/views/business/chat/components/PrescriptionModal.vue @@ -472,6 +472,7 @@ async function handleSelectCommonPrescription(data: any, type: number) { drug_name: recipe.drug_name || recipe.name, number: recipe.number || 1, price: recipe.price || 0, + buy_price: recipe.buy_price, way_id: recipe.way_id || 0, select_number: 1, }; @@ -976,6 +977,12 @@ const cancelSaveCommonPrescription = () => {

{{ (drug.price * drug.number).toFixed(2) }} + + 毛利率 {{ prescriptionStore.getChineseItemMargin(drug) }}% +