diff --git a/apps/web-antd/src/components/loading/PrescriptionLoading.vue b/apps/web-antd/src/components/loading/PrescriptionLoading.vue new file mode 100644 index 00000000..4996776f --- /dev/null +++ b/apps/web-antd/src/components/loading/PrescriptionLoading.vue @@ -0,0 +1,714 @@ + + + + + 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 97edd82c..54d0785f 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 @@ -428,3 +428,41 @@ export async function saveGranularCommonPrescriptionApi(data: { data, ); } + +/** AI 给处方 */ +export async function aiGeneratePrescriptionApi(data: { + register_id: number; + store_id?: number; + prescription_type: number; + /** 前端确认过的主诉(可编辑) */ + chief_complaint?: string; + /** 当前病历表单快照(可未保存),供 AI 综合参考 */ + medical_record?: Record; +}) { + return requestClient.post(`${prefix}ai-generate-prescription`, data); +} + +/** 本挂号 AI 生成历史(轻量列表) */ +export async function aiListGenerationsApi(data: { + register_id: number; + scene?: string; + limit?: number; +}) { + return requestClient.get(`${prefix}ai-list-generations`, { params: data }); +} + +/** AI 生成详情(点选历史) */ +export async function aiGenerationDetailApi(data: { id: number }) { + return requestClient.get(`${prefix}ai-generation-detail`, { params: data }); +} + +/** AI 处方药名模糊对照 */ +export async function aiMatchPrescriptionDrugsApi(data: { + generation_id?: number; + register_id?: number; + store_id?: number; + prescription_type?: number; + items?: Array>; +}) { + return requestClient.post(`${prefix}ai-match-prescription-drugs`, data); +} diff --git a/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue b/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue new file mode 100644 index 00000000..4c122128 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor-reception/components/AiPrescriptionDrawer.vue @@ -0,0 +1,710 @@ + + + + + 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 85b7bc96..d4efeee5 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/index.vue +++ b/apps/web-antd/src/views/doctor/doctor-reception/index.vue @@ -14,6 +14,7 @@ import { MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined, + RobotOutlined, SaveOutlined, StopOutlined, SwapOutlined, @@ -85,6 +86,7 @@ import RefusalOfTreatmentModal from "#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue"; // 常用方选择弹窗组件 import CommonPrescriptionModal from './components/CommonPrescriptionModal.vue'; +import AiPrescriptionDrawer from './components/AiPrescriptionDrawer.vue'; // 确认模态框组件 import ConfirmModal from './components/ConfirmModal.vue'; // 信息提示模态框组件 @@ -1264,6 +1266,177 @@ function openCommonPrescriptionModal() { CommonPrescriptionModalApi.open(); } +const aiPrescriptionDrawerRef = ref | null>(null); + +/** 打开 AI 给处方抽屉:先确认患者与主诉,再生成预览 */ +function openAiPrescriptionDrawer() { + if (guardSpecialPrescriptionCartEdit()) return; + const registerId = Number.parseInt( + localStorage.getItem(`doctorReception-id`) || '0', + ); + if (!registerId) { + message.warning('请先选择患者'); + return; + } + if (!activeCategory.value) { + message.warning('请先选择处方类型'); + return; + } + const payload = medicalRecordPanelRef.value?.getPayload?.() || {}; + aiPrescriptionDrawerRef.value?.open({ + registerId, + storeId: Number(myStoreId.value || 0) || undefined, + prescriptionType: Number(activeCategory.value), + patientName: String(activePatient.value?.name || ''), + patientSex: Number(activePatient.value?.sex || 0), + patientAge: Number(activePatient.value?.age || 0), + chiefComplaint: String(payload.chief_complaint || ''), + // 带入当前病历表单(含未保存内容),出方时综合参考而非只看主诉 + medicalRecord: { ...payload }, + }); +} + +/** + * AI 出方预览里改病历字段时,回写到当前病历面板(不强制保存) + */ +function handleSyncAiMedicalRecord(partial: Record) { + medicalRecordPanelRef.value?.patchFields?.(partial || {}); +} + +/** + * 将 AI 对照结果覆盖导入当前处方(非追加) + * 中药会同步剂数/每日次数,并按 usage 匹配先煎后下 way_id + */ +function handleImportAiPrescription(payload: { + rows?: any[]; + dosage?: number; + day_dosage?: number; +}) { + if (guardSpecialPrescriptionCartEdit()) return; + const list = Array.isArray(payload?.rows) ? payload.rows : []; + if (!list.length) { + message.warning('没有可导入的药品'); + return; + } + const resolveWayId = (usage: string) => { + const u = String(usage || '').trim(); + if (!u) return 0; + const found = (drugUseWay.value || []).find( + (w: any) => + String(w.name || '') === u || String(w.name || '').includes(u), + ); + return found ? Number(found.id || 0) : 0; + }; + const doOverwrite = () => { + clearAppliedSpecialPrescription(); + const next: any[] = []; + for (const row of list) { + const c = row?.candidate || {}; + const drugId = Number(c.drug_id || 0); + if (!drugId) continue; + if (next.find((item: any) => Number(item.id) === drugId)) continue; + const doseNum = parseFloat(row.dose); + const qty = !Number.isNaN(doseNum) && doseNum > 0 ? doseNum : 1; + const drug = c.drug || {}; + if (activeCategory.value === 1) { + const wayId = resolveWayId(row.usage || ''); + next.push({ + index_id: c.id || drugId, + id: drugId, + drug_name: c.drug_name || row.ai_name, + number: qty, + price: parseFloat(c.price) || 0, + way_id: wayId, + use_ways: drugUseWay.value.find((item: any) => item.id === wayId), + select_number: 1, + unit: c.unit || { id: 0, name: row.unit || 'g' }, + }); + continue; + } + if (activeCategory.value === 2) { + next.push({ + index_id: c.id || drugId, + id: drugId, + drug_name: c.drug_name || drug.drug_name || row.ai_name, + number: qty, + select_number: qty, + price: parseFloat(c.price) || 0, + image: c.image || drug.image || '', + instruction: drug.instruction || '', + time_id: c.time_id || drug.time_id || 0, + type_id: c.type_id || drug.type_id || 0, + frequency_id: c.frequency_id || drug.frequency_id || 0, + unit_id: c.unit_id || drug.unit_id || 0, + use_num: drugTime.value.find( + (item) => item.id === (c.time_id || drug.time_id || 0), + ), + use_type: drugUseType.value.find( + (item) => item.id === (c.type_id || drug.type_id || 0), + ), + use_frequency: drugUseFrequency.value.find( + (item) => item.id === (c.frequency_id || drug.frequency_id || 0), + ), + unit: + c.unit || + drugUnit.value.find( + (item) => item.id === (c.unit_id || drug.unit_id || 0), + ), + type: 2, + }); + continue; + } + next.push({ + index_id: c.id || drugId, + id: drugId, + drug_name: c.drug_name || row.ai_name, + number: 1, + select_number: 1, + price: parseFloat(c.price) || 0, + image: c.image || '', + type: activeCategory.value, + }); + } + if (!next.length) { + message.warning('没有可导入的药品'); + return; + } + currentDrugs.value = next; + if (activeCategory.value === 1) { + if (payload.dosage != null && Number(payload.dosage) > 0) { + dosage.value = Number(payload.dosage); + } + if (payload.day_dosage != null && Number(payload.day_dosage) > 0) { + dayDosage.value = Number(payload.day_dosage); + } + } + updateLocalStorage(); + message.success(`已覆盖导入 ${next.length} 个药品`); + }; + if (currentDrugs.value.length > 0) { + AntModal.confirm({ + title: '覆盖当前处方', + content: '导入将清空并覆盖现有处方药品,是否继续?', + okText: '继续覆盖', + cancelText: '取消', + onOk: () => doOverwrite(), + }); + return; + } + doOverwrite(); +} + +/** 未对照药名:提示手动搜索 */ +function handleAiSearchUnmatchedDrug(keyword: string) { + const key = String(keyword || ''); + if (activeCategory.value === 2) { + openWesternModal(); + return; + } + if (key) { + message.info(`请手动搜索:${key}`); + } +} + /** * 处理选择常用方 * @param data 常用方数据,包含prescription和recipes @@ -2947,6 +3120,15 @@ function onStoreSelectOpenChange(open: boolean) { 选择常用方 + + +