diff --git a/apps/web-antd/src/utils/dictSearchRank.ts b/apps/web-antd/src/utils/dictSearchRank.ts index a731be4b..61caf7f2 100644 --- a/apps/web-antd/src/utils/dictSearchRank.ts +++ b/apps/web-antd/src/utils/dictSearchRank.ts @@ -92,6 +92,8 @@ export function splitHighlight( } let cachedRankMode: DictSearchRankMode | null = null; +/** 多气泡实例共享同一请求,避免进页 N 次 get-by-keys */ +let rankModeFetchPromise: Promise | null = null; /** 写入缓存(从系统配置或接口响应读取后调用) */ export function setDictSearchRankMode(mode: string | undefined | null) { @@ -102,6 +104,32 @@ export function getDictSearchRankMode(): DictSearchRankMode { return cachedRankMode || 'backend'; } +export function isDictSearchRankModeLoaded(): boolean { + return cachedRankMode != null; +} + +/** + * 确保已加载匹配度模式:已缓存则跳过;并发调用共享同一 Promise + * @param loader 真正打网取配置的函数,返回 frontend|backend + */ +export async function ensureDictSearchRankMode( + loader: () => Promise, +): Promise { + if (cachedRankMode != null) return cachedRankMode; + if (!rankModeFetchPromise) { + rankModeFetchPromise = (async () => { + try { + const mode = await loader(); + setDictSearchRankMode(mode); + } catch { + setDictSearchRankMode('backend'); + } + return cachedRankMode || 'backend'; + })(); + } + return rankModeFetchPromise; +} + /** * 按当前模式决定是否前端重排;有关键字时保证按匹配度降序,并尽量带上可展示的 match_score */ diff --git a/apps/web-antd/src/views/doctor/doctor-reception/index.vue b/apps/web-antd/src/views/doctor/doctor-reception/index.vue index d4efeee5..f5c74442 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/index.vue +++ b/apps/web-antd/src/views/doctor/doctor-reception/index.vue @@ -177,6 +177,10 @@ const tabType = ref(0); const rxMrTab = ref<'rx' | 'mr'>('rx'); const medicalRecordPanelRef = ref | null>(null); const canUseMedicalRecord = computed(() => hasVipPermission('medical_record')); +/** AI 辅助出方 VIP */ +const canUseAiPrescription = computed(() => hasVipPermission('ai_prescription')); +/** AI 写病历 VIP */ +const canUseAiMedicalRecord = computed(() => hasVipPermission('ai_medical_record')); watch(rxMrTab, (tab) => { const rid = getRegisterId(); @@ -225,9 +229,20 @@ const prescriptionTypeLoading = ref(false); const prescriptionTypeAllowed = ref(true); const prescriptionTypeDenyMessage = ref(''); +/** 同参处方类型请求去重(setup + selectPatient 易双打) */ +let prescriptionTypeInflight: { + key: string; + promise: Promise; +} | null = null; + /** 拉取处方类型列表与默认选中;无权限时清空并返回 false */ async function loadPrescriptionTypeOptions(registerId?: number) { + const key = `${Number(registerId || 0)}_${Number(myStoreId.value || 0)}`; + if (prescriptionTypeInflight?.key === key) { + return prescriptionTypeInflight.promise; + } prescriptionTypeLoading.value = true; + const run = (async () => { try { const params: { register_id?: number; store_id?: number } = {}; if (registerId) params.register_id = registerId; @@ -260,7 +275,13 @@ async function loadPrescriptionTypeOptions(registerId?: number) { return false; } finally { prescriptionTypeLoading.value = false; + if (prescriptionTypeInflight?.key === key) { + prescriptionTypeInflight = null; + } } + })(); + prescriptionTypeInflight = { key, promise: run }; + return run; } /** 将当前分类 Tab 滚入可视区 */ @@ -307,6 +328,28 @@ const doctorReceptionRegisterId = computed(() => { ); }); +/** 门店类型接口短时缓存,供毛利率 / 转诊提示共用,避免进页双打 */ +let storeTypeCache: { key: string; data: any; at: number } | null = null; +const STORE_TYPE_CACHE_TTL = 30_000; + +async function getCurrentStoreTypeCached(params: { + register_id?: number; + store_id?: number; +}) { + const key = `${Number(params.store_id || 0)}_${Number(params.register_id || 0)}`; + const now = Date.now(); + if ( + storeTypeCache && + storeTypeCache.key === key && + now - storeTypeCache.at < STORE_TYPE_CACHE_TTL + ) { + return storeTypeCache.data; + } + const data = await getCurrentStoreTypeApi(params); + storeTypeCache = { key, data, at: now }; + return data; +} + async function fetchStoreSeeRate() { try { const registerId = Number.parseInt( @@ -319,7 +362,7 @@ async function fetchStoreSeeRate() { if (registerId > 0) { params.register_id = registerId; } - const res = await getCurrentStoreTypeApi(params); + const res = await getCurrentStoreTypeCached(params); seeRate.value = Number(res?.see_rate ?? 0); allowInsuranceCategory.value = Number(res?.allow_insurance_category ?? 0); priceAdjustEnabled.value = Number(res?.enable_order_price_percent_adjust ?? 0) === 1; @@ -1270,6 +1313,10 @@ const aiPrescriptionDrawerRef = ref | /** 打开 AI 给处方抽屉:先确认患者与主诉,再生成预览 */ function openAiPrescriptionDrawer() { + if (!canUseAiPrescription.value) { + message.warning('当前门店未开通AI辅助出方VIP功能'); + return; + } if (guardSpecialPrescriptionCartEdit()) return; const registerId = Number.parseInt( localStorage.getItem(`doctorReception-id`) || '0', @@ -1781,16 +1828,14 @@ async function checkAndShowTransferTip() { localStorage.getItem('doctorReception-id') || '0', 10, ); - const storeInfo = await getCurrentStoreTypeApi({ + const storeInfo = await getCurrentStoreTypeCached({ store_id: myStoreId.value, ...(registerId > 0 ? { register_id: registerId } : {}), }); - // const storeInfo = res; if (!storeInfo) { return; } - console.log(storeInfo, 'aaaaaaaaaaaaaaaaa') // 检测是否为西医诊所(clinic_type === 1) if (storeInfo.clinic_type === 1 && storeInfo.delegate_store) { @@ -3121,7 +3166,7 @@ function onStoreSelectOpenChange(open: boolean) { 选择常用方 - diff --git a/apps/web-antd/src/views/doctor/medical-record/components/CommonDxOrderChips.vue b/apps/web-antd/src/views/doctor/medical-record/components/CommonDxOrderChips.vue index fabdd1f6..f5d2ddf8 100644 --- a/apps/web-antd/src/views/doctor/medical-record/components/CommonDxOrderChips.vue +++ b/apps/web-antd/src/views/doctor/medical-record/components/CommonDxOrderChips.vue @@ -9,10 +9,11 @@ import { computed, onMounted, ref, watch } from 'vue'; import { Tag } from 'ant-design-vue'; import { - getMyMedicalRecordEntryList, -} from '#/views/doctor/medical-record/api'; -import { getDoctorOrderList } from '#/views/doctor/doctor-reception/api'; -import { getDoctorMyDiseaseListApi } from '#/views/doctor/settings/api'; + fetchDoctorOrderListCached, + fetchMyDiseaseListCached, + fetchMyEntriesByField, + invalidateCommonChipsCache, +} from '../utils/commonChipsCache'; const props = withDefaults( defineProps<{ @@ -63,13 +64,13 @@ const activeSet = computed(() => { }); /** - * 拉取常用项 + * 拉取常用项(诊断/医嘱/词条均走模块缓存,进页多实例只打一次网) */ async function loadChips() { loading.value = true; try { if (props.source === 'diagnosis') { - const list = await getDoctorMyDiseaseListApi().catch(() => []); + const list = await fetchMyDiseaseListCached(); chips.value = (list || []) .map((item: any) => { const d = item?.disease || item; @@ -85,10 +86,8 @@ async function loadChips() { chips.value = []; return; } - const list = await getMyMedicalRecordEntryList({ - field_code: props.fieldCode, - limit: props.limit, - }).catch(() => []); + // 全量 my-entry-list 一次,再按 field_code 本地切片 + const list = await fetchMyEntriesByField(props.fieldCode, props.limit); chips.value = (list || []) .map((item: any) => { const text = String(item?.content || item?.title || '').trim(); @@ -100,7 +99,7 @@ async function loadChips() { .slice(0, props.limit) as ChipItem[]; return; } - const res = await getDoctorOrderList().catch(() => ({ my: [], common: [] })); + const res = await fetchDoctorOrderListCached(); const my = (res?.my || []) .map((item: any) => { const text = String(item?.content || '').trim(); @@ -123,6 +122,12 @@ async function loadChips() { } } +/** 强制刷新:清缓存后重拉(新增/删除常用后用) */ +async function reloadChips() { + invalidateCommonChipsCache(props.source); + await loadChips(); +} + function onPick(item: ChipItem) { if (props.disabled || !item?.text) return; emit('select', item.text); @@ -145,7 +150,7 @@ onMounted(() => { loadChips(); }); -defineExpose({ reload: loadChips }); +defineExpose({ reload: reloadChips });