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) }}% +