Ver Fonte

feat(@six/smart-pharmacy): 智慧药事系统第一版处方管理接口对接

cmj há 1 mês atrás
pai
commit
22d09d4178

+ 1 - 0
apps/smart-pharmacy/src/api/index.ts

@@ -11,6 +11,7 @@ export * from './method/access';
 export * from './method/business';
 export * from './method/common';
 export * from './method/dict';
+export * from './method/prescription';
 export * from './method/system';
 
 export const http = createRequestClient({

+ 15 - 0
apps/smart-pharmacy/src/api/method/dict.ts

@@ -43,3 +43,18 @@ export function listDeptTypeDictMethod() {
 export function listEnterpriseTypeDictMethod() {
   return listDictByCodeMethod('enterprise_type');
 }
+
+/** 流程状态/流程节点字典(proccess_node) */
+export function listProcessNodeDictMethod() {
+  return listDictByCodeMethod('proccess_node');
+}
+
+/** 配送方式字典(delivery_method) */
+export function listDeliveryMethodDictMethod() {
+  return listDictByCodeMethod('delivery_method');
+}
+
+/** 物流公司字典(logistics_company) */
+export function listLogisticsCompanyDictMethod() {
+  return listDictByCodeMethod('logistics_company');
+}

+ 53 - 0
apps/smart-pharmacy/src/api/method/prescription.ts

@@ -0,0 +1,53 @@
+import type { TransformData, TransformList } from '#/api';
+
+import { http } from '#/api';
+import { fromPrescriptionDetail } from '#/api/model/prescription-detail';
+import {
+  fromPrescription,
+  type PrescriptionModel,
+  toPrescriptionQuery,
+} from '#/api/model/prescription';
+
+export type { PrescriptionModel };
+
+/** 处方分页列表 */
+export function listPrescriptionsMethod(
+  page = 1,
+  size = 20,
+  query?: PrescriptionModel.ListQuery,
+) {
+  const result = toPrescriptionQuery(query);
+
+  return http.get<
+    TransformList<PrescriptionModel.Prescription>,
+    TransformList
+  >(`/manager/tcmp-pc/prescription/list`, {
+    params: {
+      pageNum: page,
+      pageSize: size,
+      ...result,
+    },
+    cacheFor: 0,
+    transform({ items, ...data }) {
+      const rows = items ?? [];
+      return {
+        ...data,
+        items: rows.map((item) => fromPrescription(item as TransformData)),
+      };
+    },
+  });
+}
+
+/** 处方详情 */
+export function getPrescriptionDetailMethod(id: string | number) {
+  return http.get<PrescriptionModel.Detail, TransformData>(
+    `/manager/tcmp-pc/prescription/detailById`,
+    {
+      params: { id },
+      cacheFor: 0,
+      transform(data) {
+        return fromPrescriptionDetail(data);
+      },
+    },
+  );
+}

+ 110 - 0
apps/smart-pharmacy/src/api/method/system.ts

@@ -126,6 +126,91 @@ export namespace SystemModel {
     children?: Menu[];
   }
 }
+function normalizeDeptList(data: unknown): TransformData[] {
+  if (Array.isArray(data)) return data;
+  if (data && typeof data === 'object') {
+    const record = data as TransformData;
+    if (Array.isArray(record.rows)) return record.rows;
+    if (Array.isArray(record.list)) return record.list;
+    if (Array.isArray(record.items)) return record.items;
+  }
+  return [];
+}
+
+/** 获取医疗机构列表(deptType=3,用于处方筛选等) */
+export function listMedicalInstitutionsMethod() {
+  return http.get<SystemModel.Organization[], TransformData[]>(
+    `/manager/tcmp-pc/dept/allList`,
+    {
+      params: { deptType: 3 },
+      cacheFor: 60_000,
+      transform(data) {
+        return normalizeDeptList(data).map((item) => fromOrganization(item));
+      },
+    },
+  );
+}
+
+/** 根据医疗机构 deptId 获取院区列表(deptType=1) */
+export function listCampusByParentMethod(parentId?: string | number) {
+  if (parentId === undefined || parentId === null || parentId === '') {
+    return Promise.resolve([] as SystemModel.Organization[]);
+  }
+  return http.get<SystemModel.Organization[], TransformData[]>(
+    `/manager/tcmp-pc/dept/allList`,
+    {
+      params: { parentId, deptType: 1 },
+      cacheFor: 0,
+      transform(data) {
+        return normalizeDeptList(data).map((item) => fromOrganization(item));
+      },
+    },
+  );
+}
+
+function normalizeParentIds(
+  parentIds?: Array<string | number> | string | number,
+): string[] {
+  if (parentIds === undefined || parentIds === null || parentIds === '') {
+    return [];
+  }
+  const list = Array.isArray(parentIds) ? parentIds : [parentIds];
+  return list
+    .filter((id) => id !== undefined && id !== null && id !== '')
+    .map(String);
+}
+
+/** ApiSelect 用:按医疗机构 deptId(可多选)加载院区 */
+export async function listCampusByParentApiMethod(params?: {
+  parentId?: string | number;
+  parentIds?: Array<string | number>;
+  /** 逗号分隔的 deptId,用于多选医疗机构时稳定传参 */
+  parentKey?: string;
+}) {
+  const ids = params?.parentKey
+    ? params.parentKey.split(',').filter(Boolean)
+    : normalizeParentIds(params?.parentIds ?? params?.parentId);
+  if (!ids.length) {
+    return [] as SystemModel.Organization[];
+  }
+
+  const results = await Promise.allSettled(
+    ids.map((parentId) => listCampusByParentMethod(parentId)),
+  );
+
+  const map = new Map<string, SystemModel.Organization>();
+  for (const result of results) {
+    if (result.status !== 'fulfilled') continue;
+    for (const item of result.value) {
+      const optionKey = item.code || item.id;
+      if (optionKey) {
+        map.set(optionKey, item);
+      }
+    }
+  }
+  return Array.from(map.values());
+}
+
 // 获取机构(部门)列表
 export function listOrganizationsMethod(
   page = 1,
@@ -366,6 +451,31 @@ export function deleteOrganizationsMethod(
   });
 }
 
+function normalizeEnterpriseList(data: unknown): TransformData[] {
+  if (Array.isArray(data)) return data;
+  if (data && typeof data === 'object') {
+    const record = data as TransformData;
+    if (Array.isArray(record.rows)) return record.rows;
+    if (Array.isArray(record.list)) return record.list;
+    if (Array.isArray(record.items)) return record.items;
+  }
+  return [];
+}
+
+/** 获取煎药中心列表(enterprise/allList?enterpriseType=1,用于处方筛选等) */
+export function listDecoctionCentersAllMethod() {
+  return http.get<SystemModel.Enterprise[], TransformData[]>(
+    `/manager/tcmp-pc/enterprise/allList`,
+    {
+      params: { enterpriseType: 1 },
+      cacheFor: 60_000,
+      transform(data) {
+        return normalizeEnterpriseList(data).map((item) => fromEnterprise(item));
+      },
+    },
+  );
+}
+
 /** 获取企业分页列表 */
 export function listEnterprisesMethod(
   page = 1,

+ 85 - 0
apps/smart-pharmacy/src/api/model/dict-label-map.ts

@@ -0,0 +1,85 @@
+import type { DictItem } from '#/api/model/dict';
+
+/** 构建 dictValue -> dictName 映射 */
+export function buildDictLabelMap(items: DictItem[]): Map<string, string> {
+  const map = new Map<string, string>();
+  for (const item of items) {
+    if (
+      item.dictValue === undefined ||
+      item.dictValue === null ||
+      item.dictValue === ''
+    ) {
+      continue;
+    }
+    map.set(String(item.dictValue), item.dictName);
+  }
+  return map;
+}
+
+/** 根据字典编码解析展示名称 */
+export function resolveDictLabel(
+  value: unknown,
+  map: Map<string, string>,
+  fallback = '-',
+): string {
+  if (value === undefined || value === null || value === '') return fallback;
+  const key = String(value);
+  return map.get(key) ?? key;
+}
+
+/** 根据字典编码解析展示名称(忽略大小写) */
+export function resolveDictLabelIgnoreCase(
+  value: unknown,
+  map: Map<string, string>,
+  fallback = '-',
+): string {
+  if (value === undefined || value === null || value === '') return fallback;
+  const key = String(value);
+  return (
+    map.get(key) ??
+    map.get(key.toUpperCase()) ??
+    map.get(key.toLowerCase()) ??
+    key
+  );
+}
+
+/** 构建字典映射(同时写入大小写 key,便于编码匹配) */
+export function buildDictLabelMapIgnoreCase(
+  items: DictItem[],
+): Map<string, string> {
+  const map = new Map<string, string>();
+  for (const item of items) {
+    if (
+      item.dictValue === undefined ||
+      item.dictValue === null ||
+      item.dictValue === ''
+    ) {
+      continue;
+    }
+    const value = String(item.dictValue);
+    const label = item.dictName;
+    map.set(value, label);
+    map.set(value.toUpperCase(), label);
+    map.set(value.toLowerCase(), label);
+  }
+  return map;
+}
+
+/** 创建带缓存的字典映射获取器 */
+export function createDictLabelMapGetter(
+  loader: () => Promise<DictItem[]>,
+): () => Promise<Map<string, string>> {
+  let cachedLabelMap: Map<string, string> | null = null;
+  let loadingPromise: Promise<Map<string, string>> | null = null;
+
+  return async () => {
+    if (cachedLabelMap) return cachedLabelMap;
+    if (!loadingPromise) {
+      loadingPromise = loader().then((items) => {
+        cachedLabelMap = buildDictLabelMap(items);
+        return cachedLabelMap;
+      });
+    }
+    return loadingPromise;
+  };
+}

+ 4 - 0
apps/smart-pharmacy/src/api/model/index.ts

@@ -5,6 +5,10 @@ export * from './dict';
 export * from './doctor';
 export * from './enterprise';
 export * from './organization';
+export * from './prescription';
+export * from './prescription-detail';
+export * from './dict-label-map';
+export * from './process-node-dict';
 export * from './project';
 export * from './role';
 export * from './tisane';

+ 6 - 1
apps/smart-pharmacy/src/api/model/organization.ts

@@ -36,7 +36,12 @@ export function fromOrganization(
     }),
     id,
     name: data?.deptName ?? data?.name,
-    code: data?.deptCode ?? data?.code,
+    code:
+      data?.deptCode ??
+      data?.code ??
+      (data?.deptId === undefined || data?.deptId === null
+        ? ''
+        : String(data.deptId)),
     deptType,
     type: typeLabel !== '-' ? typeLabel : data?.type,
     parentInstitutionId:

+ 350 - 0
apps/smart-pharmacy/src/api/model/prescription-detail.ts

@@ -0,0 +1,350 @@
+import type { TransformData } from '#/api';
+
+import type { PrescriptionModel } from '#/api/model/prescription';
+
+const BELONG_MAP: Record<string, string> = {
+  '1': '门诊',
+  '2': '住院',
+};
+
+function pickValue(data: TransformData | undefined, ...keys: string[]): unknown {
+  if (!data) return undefined;
+  for (const key of keys) {
+    const value = data[key];
+    if (value !== undefined && value !== null && value !== '') {
+      return value;
+    }
+  }
+  return undefined;
+}
+
+function pickList(data: TransformData | undefined, ...keys: string[]): TransformData[] {
+  if (!data) return [];
+  for (const key of keys) {
+    const value = data[key];
+    if (Array.isArray(value)) return value as TransformData[];
+  }
+  return [];
+}
+
+function toDisplay(value: unknown, fallback = '-'): string {
+  if (value === undefined || value === null || value === '') return fallback;
+  return String(value);
+}
+
+function toNumber(value: unknown, fallback = 0): number {
+  const num = Number(value);
+  return Number.isFinite(num) ? num : fallback;
+}
+
+function resolveYesNo(value: unknown): string {
+  if (value === 1 || value === '1') return '是';
+  if (value === 2 || value === '2') return '否';
+  if (value === 0 || value === '0') return '否';
+  return toDisplay(value, '-');
+}
+
+function resolveDecoctionLabel(data: TransformData): string {
+  const isBehalf = pickValue(data, 'isBehalf');
+  if (isBehalf === 1 || isBehalf === '1') return '代煎';
+  if (isBehalf === 2 || isBehalf === '2') return '自煎/代配';
+  return toDisplay(isBehalf, '-');
+}
+
+function resolveGender(data: TransformData): string {
+  const sex = pickValue(data, 'sex', 'gender');
+  if (sex === 1 || sex === '1') return '男';
+  if (sex === 2 || sex === '2') return '女';
+  return toDisplay(sex, '-');
+}
+
+function resolveAge(data: TransformData): string {
+  const age = pickValue(data, 'age');
+  if (age === undefined || age === null || age === '') return '-';
+  const text = String(age);
+  return text.includes('岁') ? text : `${text}岁`;
+}
+
+function resolveBelong(value: unknown): string {
+  const key = String(value ?? '');
+  return BELONG_MAP[key] ?? toDisplay(value, '-');
+}
+
+function formatDateText(value: unknown): string {
+  const text = toDisplay(value, '');
+  if (!text) return '-';
+  return text.length >= 10 ? text.slice(0, 10) : text;
+}
+
+function joinAddress(data: TransformData): string {
+  const province = pickValue(data, 'provinceName');
+  const city = pickValue(data, 'cityName');
+  const region = pickValue(data, 'regionName', 'districtName');
+  const combined = [province, city, region].filter(Boolean).join('');
+  return combined || '-';
+}
+
+function sortByScanTime(list: TransformData[]): TransformData[] {
+  return [...list].sort((a, b) => {
+    const ta = new Date(String(a.scanTime ?? 0)).getTime();
+    const tb = new Date(String(b.scanTime ?? 0)).getTime();
+    return tb - ta;
+  });
+}
+
+function fromMedicineDetail(item: TransformData): PrescriptionModel.HerbalItem {
+  return {
+    name: toDisplay(pickValue(item, 'standardName'), ''),
+    nationalCode: toDisplay(pickValue(item, 'standardCode'), '-'),
+    hospitalName: toDisplay(pickValue(item, 'yljgMedicineName'), '-'),
+    spec: toDisplay(pickValue(item, 'specification'), '-'),
+    origin: toDisplay(pickValue(item, 'matOriginName'), '-'),
+    dosage: toNumber(pickValue(item, 'matDose')),
+    unit: toDisplay(pickValue(item, 'unit'), '-'),
+    specialMethod: toDisplay(pickValue(item, 'matUsageName'), '-'),
+    price: toNumber(pickValue(item, 'price')),
+    doctorSignature: resolveYesNo(pickValue(item, 'doubleSignature')),
+  };
+}
+
+function fromTraceability(item: TransformData): PrescriptionModel.HerbalTraceItem {
+  return {
+    name: toDisplay(pickValue(item, 'standardName'), ''),
+    specCode: toDisplay(pickValue(item, 'standardCode'), '-'),
+    batchNo: toDisplay(pickValue(item, 'batchNumber'), '-'),
+    supplier: toDisplay(pickValue(item, 'supplierName'), '-'),
+    manufacturer: toDisplay(pickValue(item, 'productionOrg'), '-'),
+    origin: toDisplay(pickValue(item, 'matOriginName'), '-'),
+    productionDate: toDisplay(pickValue(item, 'productionDate'), '-'),
+    expiryDate: toDisplay(pickValue(item, 'effectiveDate'), '-'),
+    reportUrl: toDisplay(pickValue(item, 'qualityReportFileUrl'), ''),
+  };
+}
+
+function fromProcessNode(item: TransformData): PrescriptionModel.ProcessFlowItem {
+  const actualWeight = pickValue(item, 'actualWeight');
+  const weightText =
+    actualWeight === undefined || actualWeight === null || actualWeight === ''
+      ? ''
+      : `${actualWeight}g`;
+
+  const photoUrl = toDisplay(pickValue(item, 'picturePathUrl'), '');
+
+  return {
+    status: toDisplay(pickValue(item, 'operateType'), ''),
+    operator: toDisplay(pickValue(item, 'operatorName'), '-'),
+    operationTime: toDisplay(pickValue(item, 'scanTime'), '-'),
+    note: toDisplay(pickValue(item, 'remark'), ''),
+    water: '',
+    weightCheck: '',
+    weight: weightText,
+    hasPhoto: Boolean(photoUrl),
+    photoUrl,
+    deviceName: toDisplay(pickValue(item, 'deviceName'), ''),
+  };
+}
+
+function fromExpressNode(item: TransformData): PrescriptionModel.ExpressNodeItem {
+  return {
+    status: toDisplay(pickValue(item, 'operateType'), ''),
+    operator: toDisplay(pickValue(item, 'operatorName'), '-'),
+    operationTime: toDisplay(pickValue(item, 'scanTime'), '-'),
+    location: toDisplay(pickValue(item, 'location'), '-'),
+    note: toDisplay(pickValue(item, 'remark'), ''),
+    trackingNumber: toDisplay(pickValue(item, 'trackingNumber'), '-'),
+  };
+}
+
+export function createEmptyPrescriptionDetail(): PrescriptionModel.Detail {
+  return {
+    header: {
+      id: '',
+      number: '',
+      date: '',
+      status: '',
+      medicalInstitution: '',
+      campus: '',
+      decoctionCenter: '',
+      decoctionCode: '',
+      patientName: '',
+      gender: '',
+      age: '',
+      phone: '',
+    },
+    prescriptionDetails: {
+      chineseMedicineType: '',
+      chineseMedicineForm: '',
+      prescriptionCount: 0,
+      eachPackageCount: 0,
+      totalCount: 0,
+      decoctionMark: '',
+      decoctionTime: '',
+      packageType: '',
+      decoctionTimeMark: 0,
+      decoctionTimeValue: 0,
+    },
+    deliveryInfo: {
+      deliveryMethod: '',
+      logisticsCompany: '',
+      recipientName: '',
+      recipientPhone: '',
+      provinceCity: '',
+      detailAddress: '',
+    },
+    usageInfo: {
+      dailyCount: 0,
+      usageRoute: '',
+      usageMethod: '',
+      usageFrequency: '',
+      takeTime: '',
+      prescriptionMedicineCount: 0,
+      totalAmount: 0,
+      decoctionFee: 0,
+      deliveryFee: 0,
+      prescriptionTotalAmount: 0,
+      medicalInsuranceAmount: 0,
+      selfPayAmount: 0,
+      pharmacistRemark: '',
+    },
+    doctorInfo: {
+      outpatientType: '',
+      outpatientNo: '',
+      department: '',
+      doctorName: '',
+      chineseMedicineName: '',
+      chineseMedicineCode: '',
+      diagnosisMethod: '',
+      pregnancyFlag: '',
+      lactationFlag: '',
+      doctorNote: '',
+    },
+    herbalDetails: [],
+    herbalTraceDetails: [],
+    processFlowDetails: [],
+    logisticsInfo: {
+      company: '',
+      trackingNo: '',
+      status: '',
+      statusTime: '',
+      progress: '',
+    },
+    logisticsNodes: [],
+  };
+}
+
+export function fromPrescriptionDetail(
+  data?: TransformData,
+): PrescriptionModel.Detail {
+  const root = (data ?? {}) as TransformData;
+  const info = (root.prescriptionInfo ?? root) as TransformData;
+  const empty = createEmptyPrescriptionDetail();
+
+  const header: PrescriptionModel.DetailHeader = {
+    id: toDisplay(pickValue(info, 'id'), ''),
+    number: toDisplay(pickValue(info, 'recipeCode'), ''),
+    date: formatDateText(pickValue(info, 'dealDate', 'createTime')),
+    status: toDisplay(pickValue(info, 'processState'), '-'),
+    medicalInstitution: toDisplay(pickValue(info, 'hospitalName'), '-'),
+    campus: toDisplay(pickValue(info, 'yardName'), '-'),
+    decoctionCenter: toDisplay(pickValue(info, 'decoctionCenterName'), '-'),
+    decoctionCode: resolveDecoctionLabel(info),
+    patientName: toDisplay(pickValue(info, 'recipientName'), '-'),
+    gender: resolveGender(info),
+    age: resolveAge(info),
+    phone: toDisplay(pickValue(info, 'recipientTel'), '-'),
+  };
+
+  const prescriptionDetails: PrescriptionModel.DetailPrescriptionInfo = {
+    chineseMedicineType: toDisplay(pickValue(info, 'zhongyaoType'), '-'),
+    chineseMedicineForm: toDisplay(pickValue(info, 'packagingType', 'zyjx'), '-'),
+    prescriptionCount: toNumber(pickValue(info, 'quantity')),
+    eachPackageCount: toNumber(pickValue(info, 'packagePaste')),
+    totalCount: toNumber(pickValue(info, 'totalPackagePaste')),
+    decoctionMark: resolveYesNo(pickValue(info, 'thick')),
+    decoctionTime: toDisplay(pickValue(info, 'packType'), '-'),
+    packageType: toDisplay(pickValue(info, 'packingName'), '-'),
+    decoctionTimeMark: toNumber(pickValue(info, 'soakTime')),
+    decoctionTimeValue: toNumber(pickValue(info, 'boilTime')),
+  };
+
+  const deliveryInfo: PrescriptionModel.DetailDeliveryInfo = {
+    deliveryMethod: toDisplay(pickValue(info, 'deliveryMethod'), '-'),
+    logisticsCompany: toDisplay(pickValue(info, 'logisticsCompany'), '-'),
+    recipientName: toDisplay(pickValue(info, 'consignee', 'recipientName'), '-'),
+    recipientPhone: toDisplay(pickValue(info, 'consigneeTel'), '-'),
+    provinceCity: joinAddress(info),
+    detailAddress: toDisplay(pickValue(info, 'recipientAddress'), '-'),
+  };
+
+  const usageInfo: PrescriptionModel.DetailUsageInfo = {
+    dailyCount: toNumber(pickValue(info, 'dailyDosage')),
+    usageRoute: toDisplay(pickValue(info, 'zyRoute'), '-'),
+    usageMethod: toDisplay(pickValue(info, 'prescriptionUsageCode'), '-'),
+    usageFrequency: toDisplay(pickValue(info, 'frequency'), '-'),
+    takeTime: toDisplay(pickValue(info, 'medicationTime'), '-'),
+    prescriptionMedicineCount: toNumber(pickValue(info, 'medicinalNumber')),
+    totalAmount: toNumber(pickValue(info, 'preMonry')),
+    decoctionFee: toNumber(pickValue(info, 'daijianCost')),
+    deliveryFee: toNumber(pickValue(info, 'distributionCost')),
+    prescriptionTotalAmount: toNumber(pickValue(info, 'prescriptionSum')),
+    medicalInsuranceAmount: toNumber(pickValue(info, 'preMonryYb')),
+    selfPayAmount: toNumber(pickValue(info, 'preMonryZf')),
+    pharmacistRemark: toDisplay(pickValue(info, 'pharmacistRemark'), '-'),
+  };
+
+  const doctorInfo: PrescriptionModel.DetailDoctorInfo = {
+    outpatientType: resolveBelong(pickValue(info, 'belong')),
+    outpatientNo: toDisplay(pickValue(info, 'outpatientNum'), '-'),
+    department: toDisplay(pickValue(info, 'department'), '-'),
+    doctorName: toDisplay(pickValue(info, 'doctorName'), '-'),
+    chineseMedicineName: toDisplay(pickValue(info, 'disName'), '-'),
+    chineseMedicineCode: toDisplay(pickValue(info, 'symName'), '-'),
+    diagnosisMethod: toDisplay(pickValue(info, 'therapeuticName'), '-'),
+    pregnancyFlag: resolveYesNo(pickValue(info, 'pregnancy')),
+    lactationFlag: resolveYesNo(pickValue(info, 'lactation')),
+    doctorNote: toDisplay(pickValue(info, 'doctorRemark'), '-'),
+  };
+
+  const herbalDetails = pickList(root, 'medicineDetails').map(fromMedicineDetail);
+  const herbalTraceDetails = pickList(root, 'traceabilities').map(fromTraceability);
+  const processFlowDetails = sortByScanTime(pickList(root, 'processNodes')).map(
+    fromProcessNode,
+  );
+  const logisticsNodes = sortByScanTime(pickList(root, 'expressNodes')).map(
+    fromExpressNode,
+  );
+
+  const latestExpress = logisticsNodes[0];
+  const logisticsInfo: PrescriptionModel.DetailLogisticsInfo = {
+    company: toDisplay(pickValue(info, 'logisticsCompany'), '-'),
+    trackingNo: toDisplay(pickValue(info, 'trackingNumber'), '-'),
+    status: latestExpress?.status ?? '-',
+    statusTime: latestExpress?.operationTime ?? '-',
+    progress: logisticsNodes
+      .map((node) => {
+        const parts = [
+          node.operationTime,
+          node.status,
+          node.location !== '-' ? node.location : '',
+          node.note,
+        ].filter((part) => part && part !== '-');
+        return parts.join(' ');
+      })
+      .filter(Boolean)
+      .join(';'),
+  };
+
+  return {
+    ...empty,
+    header,
+    prescriptionDetails,
+    deliveryInfo,
+    usageInfo,
+    doctorInfo,
+    herbalDetails,
+    herbalTraceDetails,
+    processFlowDetails,
+    logisticsInfo,
+    logisticsNodes,
+  };
+}

+ 349 - 0
apps/smart-pharmacy/src/api/model/prescription.ts

@@ -0,0 +1,349 @@
+import type { TransformData, TransformRecord } from '#/api';
+
+import { fromRow } from '#/api/model';
+
+export namespace PrescriptionModel {
+  export interface Prescription extends TransformRecord {
+    id: string;
+    date: string;
+    number: string;
+    medicalInstitution: string;
+    campus: string;
+    patientName: string;
+    decoctionMethod: string;
+    decoctionEnterprise: string;
+    decoctionCenter: string;
+    deliveryMethod: string;
+    logisticsCompany: string;
+    processStatus: string;
+  }
+
+  export interface DetailHeader {
+    id: string;
+    number: string;
+    date: string;
+    status: string;
+    medicalInstitution: string;
+    campus: string;
+    decoctionCenter: string;
+    decoctionCode: string;
+    patientName: string;
+    gender: string;
+    age: string;
+    phone: string;
+  }
+
+  export interface DetailPrescriptionInfo {
+    chineseMedicineType: string;
+    chineseMedicineForm: string;
+    prescriptionCount: number;
+    eachPackageCount: number;
+    totalCount: number;
+    decoctionMark: string;
+    decoctionTime: string;
+    packageType: string;
+    decoctionTimeMark: number;
+    decoctionTimeValue: number;
+  }
+
+  export interface DetailDeliveryInfo {
+    deliveryMethod: string;
+    logisticsCompany: string;
+    recipientName: string;
+    recipientPhone: string;
+    provinceCity: string;
+    detailAddress: string;
+  }
+
+  export interface DetailUsageInfo {
+    dailyCount: number;
+    usageRoute: string;
+    usageMethod: string;
+    usageFrequency: string;
+    takeTime: string;
+    prescriptionMedicineCount: number;
+    totalAmount: number;
+    decoctionFee: number;
+    deliveryFee: number;
+    prescriptionTotalAmount: number;
+    medicalInsuranceAmount: number;
+    selfPayAmount: number;
+    pharmacistRemark: string;
+  }
+
+  export interface DetailDoctorInfo {
+    outpatientType: string;
+    outpatientNo: string;
+    department: string;
+    doctorName: string;
+    chineseMedicineName: string;
+    chineseMedicineCode: string;
+    diagnosisMethod: string;
+    pregnancyFlag: string;
+    lactationFlag: string;
+    doctorNote: string;
+  }
+
+  export interface HerbalItem {
+    name: string;
+    nationalCode: string;
+    hospitalName: string;
+    spec: string;
+    origin: string;
+    dosage: number;
+    unit: string;
+    specialMethod: string;
+    price: number;
+    doctorSignature: string;
+  }
+
+  export interface HerbalTraceItem {
+    name: string;
+    specCode: string;
+    batchNo: string;
+    supplier: string;
+    manufacturer: string;
+    origin: string;
+    productionDate: string;
+    expiryDate: string;
+    reportUrl?: string;
+  }
+
+  export interface ProcessFlowItem {
+    status: string;
+    operator: string;
+    operationTime: string;
+    note: string;
+    water: string;
+    weightCheck: string;
+    weight: string;
+    hasPhoto: boolean;
+    photoUrl?: string;
+    deviceName?: string;
+  }
+
+  export interface DetailLogisticsInfo {
+    company: string;
+    trackingNo: string;
+    status: string;
+    statusTime: string;
+    progress: string;
+  }
+
+  export interface ExpressNodeItem {
+    status: string;
+    operator: string;
+    operationTime: string;
+    location: string;
+    note: string;
+    trackingNumber: string;
+  }
+
+  export interface Detail {
+    header: DetailHeader;
+    prescriptionDetails: DetailPrescriptionInfo;
+    deliveryInfo: DetailDeliveryInfo;
+    usageInfo: DetailUsageInfo;
+    doctorInfo: DetailDoctorInfo;
+    herbalDetails: HerbalItem[];
+    herbalTraceDetails: HerbalTraceItem[];
+    processFlowDetails: ProcessFlowItem[];
+    logisticsInfo: DetailLogisticsInfo;
+    logisticsNodes: ExpressNodeItem[];
+  }
+
+  export interface ListQuery {
+    id?: number | string;
+    hospitalCodes?: Array<number | string> | number | string;
+    hospitalName?: string;
+    yardCodes?: Array<number | string> | number | string;
+    yardName?: string;
+    medCenterIds?: Array<number | string> | number | string;
+    medCenterName?: string;
+    decoctionCenterCodes?: Array<number | string> | number | string;
+    decoctionCenterName?: string;
+    startTime?: string;
+    endTime?: string;
+    processStates?: Array<number | string> | number | string;
+    recipeCode?: string;
+    recipientName?: string;
+    isBehalf?: Array<number | string> | number | string;
+    operateTimeRange?: [string, string];
+    /** 搜索表单别名 */
+    itemName?: string;
+    status?: Array<number | string> | number | string;
+    medicalInstitution?: Array<number | string> | number | string;
+    campus?: Array<number | string> | number | string;
+    decoctionCenter?: Array<number | string> | number | string;
+    patientName?: string;
+    decoctionMethod?: Array<number | string> | number | string;
+    decoctionEnterprise?: Array<number | string> | number | string;
+  }
+}
+
+function hasMultiValue(value: unknown): boolean {
+  if (value === undefined || value === null || value === '') return false;
+  if (Array.isArray(value)) {
+    return value.some(
+      (item) => item !== undefined && item !== null && item !== '',
+    );
+  }
+  return true;
+}
+
+function joinMultiValue(value?: Array<number | string> | number | string): string {
+  if (!hasMultiValue(value)) return '';
+  if (Array.isArray(value)) {
+    return value
+      .filter((item) => item !== undefined && item !== null && item !== '')
+      .map(String)
+      .join(',');
+  }
+  return String(value);
+}
+
+function formatQueryDateTime(date?: string, endOfDay = false): string {
+  if (!date) return '';
+  if (date.includes(' ')) return date;
+  return endOfDay ? `${date} 23:59:59` : `${date} 00:00:00`;
+}
+
+function resolveDecoctionMethod(isBehalf?: number | string): string {
+  if (isBehalf === 1 || isBehalf === '1') return '代煎';
+  if (isBehalf === 2 || isBehalf === '2') return '自煎/代配';
+  return '';
+}
+
+export function fromPrescription(
+  data?: TransformData,
+): PrescriptionModel.Prescription {
+  const id =
+    data?.id === undefined || data?.id === null ? '' : String(data.id);
+  const decoctionMethod =
+    data?.decoctionMethod ??
+    data?.isBehalfName ??
+    resolveDecoctionMethod(data?.isBehalf);
+
+  return {
+    ...fromRow({
+      ...data,
+      id,
+      createTime: data?.createTime ?? data?.recipeDate,
+    }),
+    id,
+    date:
+      data?.recipeDate ??
+      data?.prescriptionDate ??
+      data?.createTime ??
+      data?.date ??
+      '',
+    number: data?.recipeCode ?? data?.number ?? '',
+    medicalInstitution: data?.hospitalName ?? data?.medicalInstitution ?? '',
+    campus: data?.yardName ?? data?.campus ?? '',
+    patientName: data?.recipientName ?? data?.patientName ?? '',
+    decoctionMethod,
+    decoctionEnterprise:
+      data?.medCenterName ?? data?.decoctionEnterprise ?? '',
+    decoctionCenter:
+      data?.decoctionCenterName ?? data?.decoctionCenter ?? '',
+    deliveryMethod: String(
+      data?.deliveryMethod ?? data?.deliveryType ?? '',
+    ),
+    logisticsCompany: String(data?.logisticsCompany ?? ''),
+    processStatus: String(
+      data?.processState ??
+        data?.processStates ??
+        data?.processStateName ??
+        data?.processStatus ??
+        '',
+    ),
+  };
+}
+
+/** 列表查询条件 -> PrescriptionDTO */
+export function toPrescriptionQuery(
+  data?: PrescriptionModel.ListQuery | Record<string, unknown>,
+): TransformData {
+  if (!data) return {};
+
+  const query: TransformData = {};
+
+  if (data.id !== undefined && data.id !== null && data.id !== '') {
+    query.id = data.id;
+  }
+
+  const recipeCode = data.recipeCode ?? data.itemName;
+  if (recipeCode) query.recipeCode = String(recipeCode);
+
+  const recipientName = data.recipientName ?? data.patientName;
+  if (recipientName) query.recipientName = String(recipientName);
+
+  const hospitalCodes = data.hospitalCodes ?? data.medicalInstitution;
+  if (hasMultiValue(hospitalCodes)) {
+    query.hospitalCodes = joinMultiValue(
+      hospitalCodes as Array<number | string> | number | string,
+    );
+  }
+
+  const yardCodes = data.yardCodes ?? data.campus;
+  if (hasMultiValue(yardCodes)) {
+    query.yardCodes = joinMultiValue(
+      yardCodes as Array<number | string> | number | string,
+    );
+  }
+
+  const decoctionCenterCodes =
+    data.decoctionCenterCodes ?? data.decoctionCenter;
+  if (hasMultiValue(decoctionCenterCodes)) {
+    query.decoctionCenterCodes = joinMultiValue(
+      decoctionCenterCodes as Array<number | string> | number | string,
+    );
+  }
+
+  const medCenterIds = data.medCenterIds ?? data.decoctionEnterprise;
+  if (hasMultiValue(medCenterIds)) {
+    query.medCenterIds = joinMultiValue(
+      medCenterIds as Array<number | string> | number | string,
+    );
+  }
+
+  const processStates = data.processStates ?? data.status;
+  if (hasMultiValue(processStates)) {
+    query.processStates = joinMultiValue(
+      processStates as Array<number | string> | number | string,
+    );
+  }
+
+  const isBehalf = data.isBehalf ?? data.decoctionMethod;
+  if (hasMultiValue(isBehalf)) {
+    query.isBehalf = joinMultiValue(
+      isBehalf as Array<number | string> | number | string,
+    );
+  }
+
+  if (data.hospitalName) query.hospitalName = String(data.hospitalName);
+  if (data.yardName) query.yardName = String(data.yardName);
+  if (data.medCenterName) query.medCenterName = String(data.medCenterName);
+  if (data.decoctionCenterName) {
+    query.decoctionCenterName = String(data.decoctionCenterName);
+  }
+
+  if (
+    data.operateTimeRange &&
+    Array.isArray(data.operateTimeRange) &&
+    data.operateTimeRange.length >= 2
+  ) {
+    const [start, end] = data.operateTimeRange;
+    query.startTime = formatQueryDateTime(start, false);
+    query.endTime = formatQueryDateTime(end, true);
+  } else {
+    if (data.startTime) {
+      query.startTime = formatQueryDateTime(String(data.startTime), false);
+    }
+    if (data.endTime) {
+      query.endTime = formatQueryDateTime(String(data.endTime), true);
+    }
+  }
+
+  return query;
+}

+ 47 - 0
apps/smart-pharmacy/src/api/model/process-node-dict.ts

@@ -0,0 +1,47 @@
+import {
+  listDeliveryMethodDictMethod,
+  listLogisticsCompanyDictMethod,
+  listProcessNodeDictMethod,
+} from '#/api/method/dict';
+
+import {
+  buildDictLabelMap,
+  buildDictLabelMapIgnoreCase,
+  createDictLabelMapGetter,
+  resolveDictLabel,
+  resolveDictLabelIgnoreCase,
+} from '#/api/model/dict-label-map';
+
+export { buildDictLabelMap as buildProcessNodeLabelMap };
+export { resolveDictLabel as resolveProcessNodeLabel };
+export { resolveDictLabel as resolveDeliveryMethodLabel };
+export { resolveDictLabelIgnoreCase as resolveLogisticsCompanyLabel };
+
+/** 获取流程节点字典映射(带缓存) */
+export const getProcessNodeLabelMap = createDictLabelMapGetter(
+  listProcessNodeDictMethod,
+);
+
+/** 获取配送方式字典映射(带缓存) */
+export const getDeliveryMethodLabelMap = createDictLabelMapGetter(
+  listDeliveryMethodDictMethod,
+);
+
+let cachedLogisticsCompanyMap: Map<string, string> | null = null;
+let loadingLogisticsCompanyPromise: Promise<Map<string, string>> | null = null;
+
+/** 获取物流公司字典映射(带缓存,忽略大小写) */
+export async function getLogisticsCompanyLabelMap(): Promise<
+  Map<string, string>
+> {
+  if (cachedLogisticsCompanyMap) return cachedLogisticsCompanyMap;
+  if (!loadingLogisticsCompanyPromise) {
+    loadingLogisticsCompanyPromise = listLogisticsCompanyDictMethod().then(
+      (items) => {
+        cachedLogisticsCompanyMap = buildDictLabelMapIgnoreCase(items);
+        return cachedLogisticsCompanyMap;
+      },
+    );
+  }
+  return loadingLogisticsCompanyPromise;
+}

+ 127 - 56
apps/smart-pharmacy/src/views/prescription/management/data.ts

@@ -2,10 +2,68 @@ import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
 
 import type { VbenFormSchema } from '#/adapter/form';
 import type { OnActionClickFn } from '#/adapter/vxe-table';
-import type { OperateModel } from '#/api/method/operate';
+import type { PrescriptionModel } from '#/api';
 
+import {
+  listCampusByParentApiMethod,
+  listMedicalInstitutionsMethod,
+  listDecoctionCentersAllMethod,
+  listProcessNodeDictMethod,
+} from '#/api';
 import { $t } from '#/locales';
 
+/** 仅单选医疗机构时允许选择院区 */
+function canSelectCampus(values?: Record<string, unknown>): boolean {
+  return getMedicalInstitutionIds(values).length === 1;
+}
+
+/** 解析医疗机构多选值(兼容单选、逗号分隔字符串、labelInValue 对象) */
+function getMedicalInstitutionIds(values?: Record<string, unknown>): string[] {
+  const raw = values?.medicalInstitution;
+  if (raw === undefined || raw === null || raw === '') {
+    return [];
+  }
+
+  const list: unknown[] = [];
+  const append = (item: unknown) => {
+    if (item === undefined || item === null || item === '') return;
+    if (Array.isArray(item)) {
+      item.forEach(append);
+      return;
+    }
+    if (typeof item === 'string' && item.includes(',')) {
+      item
+        .split(',')
+        .map((part) => part.trim())
+        .filter(Boolean)
+        .forEach(append);
+      return;
+    }
+    list.push(item);
+  };
+
+  append(raw);
+
+  const ids: string[] = [];
+  for (const item of list) {
+    if (typeof item === 'object' && item !== null) {
+      const record = item as Record<string, unknown>;
+      const value = record.value ?? record.id ?? record.key;
+      if (value !== undefined && value !== null && value !== '') {
+        ids.push(String(value));
+      }
+      continue;
+    }
+    ids.push(String(item));
+  }
+
+  return [...new Set(ids)];
+}
+
+function buildCampusParentKey(parentIds: string[]): string {
+  return [...parentIds].sort().join(',');
+}
+
 export function useUserSearchFormSchema(): VbenFormSchema[] {
   return [
     {
@@ -23,24 +81,17 @@ export function useUserSearchFormSchema(): VbenFormSchema[] {
       },
     },
     {
-      component: 'Select',
-      fieldName: 'status',
+      component: 'ApiSelect',
+      fieldName: 'processStates',
       label: $t('prescription.list.processStatus'),
       componentProps: {
-        options: [
-          {
-            label: '已浸泡',
-            value: 1,
-          },
-          {
-            label: '已调配',
-            value: 0,
-          },
-          {
-            label: '已打包',
-            value: 2,
-          },
-        ],
+        allowClear: true,
+        api: listProcessNodeDictMethod,
+        class: 'w-full',
+        labelField: 'dictName',
+        valueField: 'dictValue',
+        mode: 'multiple',
+        maxTagCount: 'responsive',
       },
     },
     {
@@ -49,65 +100,85 @@ export function useUserSearchFormSchema(): VbenFormSchema[] {
       label: $t('prescription.list.number'),
     },
     {
-      component: 'Select',
+      component: 'ApiSelect',
       fieldName: 'medicalInstitution',
       label: $t('prescription.list.medicalInstitution'),
       componentProps: {
-        options: [
-          {
-            label: '同仁堂',
-            value: 1,
-          },
-          {
-            label: '浙江省中医院',
-            value: 0,
-          },
-          {
-            label: '蒋村社区卫生服务中心',
-            value: 2,
-          },
-        ],
+        allowClear: true,
+        api: listMedicalInstitutionsMethod,
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'id',
+        mode: 'multiple',
+        maxTagCount: 'responsive',
+        numberToString: true,
       },
     },
     {
-      component: 'Select',
+      component: 'ApiSelect',
       fieldName: 'campus',
       label: $t('prescription.list.campus'),
       componentProps: {
-        options: [
-          {
-            label: '萧山馆',
-            value: 1,
-          },
-          {
-            label: '湖滨院区',
-            value: 0,
-          },
-        ],
+        allowClear: true,
+        api: listCampusByParentApiMethod,
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'code',
+        mode: 'multiple',
+        maxTagCount: 'responsive',
+        numberToString: true,
+        immediate: false,
+      },
+      dependencies: {
+        triggerFields: ['medicalInstitution'],
+        disabled: (values) => !canSelectCampus(values),
+        trigger: (_values, formApi) => {
+          formApi.setFieldValue('campus', undefined);
+        },
+        componentProps: (values) => {
+          const parentIds = getMedicalInstitutionIds(values);
+          const enabled = canSelectCampus(values);
+          const parentKey = enabled ? parentIds[0]! : '';
+          return {
+            key: `campus-${parentKey || 'empty'}`,
+            allowClear: true,
+            api: listCampusByParentApiMethod,
+            class: 'w-full',
+            labelField: 'name',
+            valueField: 'code',
+            mode: 'multiple',
+            maxTagCount: 'responsive',
+            numberToString: true,
+            immediate: enabled,
+            placeholder: enabled
+              ? '请选择院区'
+              : parentIds.length > 1
+                ? '多选医疗机构时不可选择院区'
+                : '请先选择医疗机构',
+            // 单选时只传一个 parentKey
+            params: enabled ? { parentKey } : { parentKey: '' },
+          };
+        },
       },
     },
     {
-      component: 'Select',
+      component: 'ApiSelect',
       fieldName: 'decoctionCenter',
       label: $t('prescription.list.decoctionCenter'),
       componentProps: {
-        options: [
-          {
-            label: '重煎药中心华东区',
-            value: 1,
-          },
-          {
-            label: '杭州煎药中心',
-            value: 0,
-          },
-        ],
+        allowClear: true,
+        api: listDecoctionCentersAllMethod,
+        class: 'w-full',
+        labelField: 'name',
+        valueField: 'code',
+        mode: 'multiple',
+        maxTagCount: 'responsive',
       },
     },
-    
   ];
 }
 
-export function useUserTableColumns<T = OperateModel.Record>(
+export function useUserTableColumns<T = PrescriptionModel.Prescription>(
   onActionClick?: OnActionClickFn<T>,
 ): VxeTableGridOptions<T>['columns'] {
   return [

+ 562 - 356
apps/smart-pharmacy/src/views/prescription/management/detail.vue

@@ -1,121 +1,123 @@
 <script lang="ts" setup>
-import { computed, ref } from 'vue';
+import type { PrescriptionModel } from '#/api';
+
+import { computed, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { Page } from '@vben/common-ui';
-import { ArrowLeft, MapPin, Phone, Building2, User, Calendar, FileText, Pill, Package, Truck, Clock, CreditCard, Stethoscope } from 'lucide-vue-next';
-import { Tabs, Descriptions, Breadcrumb } from 'ant-design-vue';
-
-const route = useRoute();
-const activeTab = ref('prescription');
 
-const prescriptionId = computed(() => route.params.id);
+import { Breadcrumb, Descriptions, Empty, Spin, Tabs } from 'ant-design-vue';
 
-const mockPrescription = {
-  id: prescriptionId.value || '20230923238475',
-  date: '2023-09-23',
-  status: '打包中',
-  medicalInstitution: '蒋村社区卫生服务中心',
-  campus: '-',
-  decoctionCenter: '重煎药中心华东区',
-  decoctionCode: '代煎',
-  patientName: '唐*理',
-  gender: '女',
-  age: '48岁',
-  phone: '138****1233',
-};
+import { createEmptyPrescriptionDetail } from '#/api/model/prescription-detail';
+import {
+  getDeliveryMethodLabelMap,
+  getLogisticsCompanyLabelMap,
+  getProcessNodeLabelMap,
+  resolveDeliveryMethodLabel,
+  resolveLogisticsCompanyLabel,
+  resolveProcessNodeLabel,
+} from '#/api/model/process-node-dict';
+import { getPrescriptionDetailMethod } from '#/api';
 
-const prescriptionDetails = {
-  chineseMedicineType: '饮片(散装)',
-  chineseMedicineForm: '汤剂',
-  prescriptionCount: 7,
-  eachPackageCount: 2,
-  totalCount: 14,
-  decoctionMark: '否',
-  decoctionTime: '200ml',
-  packageType: '袋装',
-  decoctionTimeMark: 30,
-  decoctionTimeValue: 40,
-  decoctionTimeSecond: 30,
-  decoctionTimeSecondValue: 40,
-};
+const route = useRoute();
+const activeTab = ref('prescription');
+const loading = ref(false);
 
-const deliveryInfo = {
-  deliveryMethod: '配送到家',
-  logisticsCompany: '顺丰速运',
-  recipientName: '唐*理',
-  recipientPhone: '138****2374',
-  provinceCity: '浙江省杭州市西湖区',
-  detailAddress: '蒋村街道**********',
-};
+const prescriptionId = computed(() => {
+  const id = route.params.id;
+  return Array.isArray(id) ? id[0] : id;
+});
 
-const usageInfo = {
-  dailyCount: 1,
-  usageRoute: '内服',
-  usageMethod: '内服',
-  usageFrequency: '1日2次',
-  takeTime: '饭后服',
-  prescriptionMedicineCount: 18,
-  totalAmount: 234.00,
-  decoctionFee: 21.00,
-  deliveryFee: 0.00,
-  prescriptionTotalAmount: 245.00,
-  medicalInsuranceAmount: 231.90,
-  selfPayAmount: 13.10,
-  dispensingFee: 0.00,
-  prescriptionTotalAmountSecond: 245.00,
-  medicalInsuranceAmountSecond: 231.90,
-  selfPayAmountSecond: 13.10,
-};
+const detail = ref<PrescriptionModel.Detail>(createEmptyPrescriptionDetail());
 
-const doctorInfo = {
-  outpatientType: '住院',
-  outpatientNo: '93859496',
-  department: '中医科(20号)',
-  doctorName: '王卫国',
-  chineseMedicineName: '腰痛',
-  chineseMedicineCode: '肝肾亏虚',
-  diagnosisMethod: '补益肝肾',
-  chineseMedicineType: '腰痛',
-  chineseMedicineCodeSecond: '否',
-  diagnosisMethodSecond: '否',
-  doctorNote: '',
-};
+const header = computed(() => detail.value.header);
+const prescriptionDetails = computed(() => detail.value.prescriptionDetails);
+const deliveryInfo = computed(() => detail.value.deliveryInfo);
+const usageInfo = computed(() => detail.value.usageInfo);
+const doctorInfo = computed(() => detail.value.doctorInfo);
+const herbalDetails = computed(() => detail.value.herbalDetails);
+const herbalTraceDetails = computed(() => detail.value.herbalTraceDetails);
+const processFlowDetails = computed(() => detail.value.processFlowDetails);
+const logisticsInfo = computed(() => detail.value.logisticsInfo);
+const logisticsNodes = computed(() => detail.value.logisticsNodes);
 
-const herbalDetails = [
-  { name: '麸炒苍术', nationalCode: '06130203958365938', hospitalName: '麸炒苍术', spec: '1kg', origin: '山东', dosage: 9.0000, unit: 'g', specialMethod: '先煎', price: 3, doctorSignature: '否' },
-];
+function formatMoney(value?: number): string {
+  const num = Number(value);
+  if (!Number.isFinite(num)) return '0.00';
+  return num.toFixed(2);
+}
 
-const herbalTraceDetails = [
-  { name: '黄芪', specCode: 'HQ-001', batchNo: '20230901', supplier: '亳州中药材市场', manufacturer: '亳州药业有限公司', origin: '安徽亳州', productionDate: '2023-09-01', expiryDate: '2025-09-01' },
-  { name: '当归', specCode: 'DG-002', batchNo: '20230815', supplier: '甘肃中药材市场', manufacturer: '陇西药业有限公司', origin: '甘肃陇西', productionDate: '2023-08-15', expiryDate: '2025-08-15' },
-  { name: '白芍', specCode: 'BS-003', batchNo: '20230820', supplier: '浙江中药材市场', manufacturer: '杭白芍业有限公司', origin: '浙江杭州', productionDate: '2023-08-20', expiryDate: '2025-08-20' },
-  { name: '熟地', specCode: 'SD-004', batchNo: '20230905', supplier: '河南中药材市场', manufacturer: '焦作药业有限公司', origin: '河南焦作', productionDate: '2023-09-05', expiryDate: '2025-09-05' },
-  { name: '川芎', specCode: 'CX-005', batchNo: '20230825', supplier: '四川中药材市场', manufacturer: '彭州药业有限公司', origin: '四川彭州', productionDate: '2023-08-25', expiryDate: '2025-08-25' },
-  { name: '杜仲', specCode: 'DZ-006', batchNo: '20230910', supplier: '陕西中药材市场', manufacturer: '汉中药业有限公司', origin: '陕西汉中', productionDate: '2023-09-10', expiryDate: '2025-09-10' },
-  { name: '续断', specCode: 'XD-007', batchNo: '20230818', supplier: '湖北中药材市场', manufacturer: '恩施药业有限公司', origin: '湖北恩施', productionDate: '2023-08-18', expiryDate: '2025-08-18' },
-  { name: '牛膝', specCode: 'NX-008', batchNo: '20230908', supplier: '河南中药材市场', manufacturer: '怀庆药业有限公司', origin: '河南焦作', productionDate: '2023-09-08', expiryDate: '2025-09-08' },
-];
+function applyDictLabels(
+  data: PrescriptionModel.Detail,
+  processNodeMap: Map<string, string>,
+  deliveryMethodMap: Map<string, string>,
+  logisticsCompanyMap: Map<string, string>,
+): PrescriptionModel.Detail {
+  return {
+    ...data,
+    header: {
+      ...data.header,
+      status: resolveProcessNodeLabel(data.header.status, processNodeMap),
+    },
+    deliveryInfo: {
+      ...data.deliveryInfo,
+      deliveryMethod: resolveDeliveryMethodLabel(
+        data.deliveryInfo.deliveryMethod,
+        deliveryMethodMap,
+      ),
+      logisticsCompany: resolveLogisticsCompanyLabel(
+        data.deliveryInfo.logisticsCompany,
+        logisticsCompanyMap,
+      ),
+    },
+    processFlowDetails: data.processFlowDetails.map((item) => ({
+      ...item,
+      status: resolveProcessNodeLabel(item.status, processNodeMap),
+    })),
+    logisticsNodes: data.logisticsNodes.map((item) => ({
+      ...item,
+      status: resolveProcessNodeLabel(item.status, processNodeMap),
+    })),
+    logisticsInfo: {
+      ...data.logisticsInfo,
+      company: resolveLogisticsCompanyLabel(
+        data.logisticsInfo.company,
+        logisticsCompanyMap,
+      ),
+      status: resolveProcessNodeLabel(data.logisticsInfo.status, processNodeMap),
+    },
+  };
+}
 
-const processFlowDetails = [
-  { status: '已开始浓缩', operator: '李*霞', operationTime: '2023-09-23 10:25:38', note: '备注1', water: '2100ml', hasPhoto: true },
-  { status: '已开始煎煮', operator: '李*霞', operationTime: '2023-09-23 10:24:38', note: '备注2', water: '3500ml', hasPhoto: true },
-  { status: '已复核', operator: '王*霞', operationTime: '2023-09-23 10:23:38', note: '备注3', weightCheck: '1949.50g', hasPhoto: true },
-  { status: '已调配', operator: '吴*', operationTime: '2023-09-23 10:22:38', note: '备注4', weight: '1948.31g', hasPhoto: true },
-  { status: '药房审核已通过', operator: '崔*红', operationTime: '2023-09-23 09:21:38', note: '', water: '', hasPhoto: true },
-];
+async function loadDetail() {
+  const id = prescriptionId.value;
+  if (!id) return;
 
-const logisticsInfo = {
-  company: '顺丰速运',
-  trackingNo: 'SF73648596038958987',
-  status: '已揽件',
-  statusTime: '2023-09-23 09:25:38',
-  progress: '返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容',
-};
+  loading.value = true;
+  try {
+    const [data, processNodeMap, deliveryMethodMap, logisticsCompanyMap] =
+      await Promise.all([
+        getPrescriptionDetailMethod(id),
+        getProcessNodeLabelMap(),
+        getDeliveryMethodLabelMap(),
+        getLogisticsCompanyLabelMap(),
+      ]);
+    detail.value = applyDictLabels(
+      data,
+      processNodeMap,
+      deliveryMethodMap,
+      logisticsCompanyMap,
+    );
+  } finally {
+    loading.value = false;
+  }
+}
 
 function goBack() {
   window.history.back();
 }
+
+watch(prescriptionId, loadDetail, { immediate: true });
 </script>
 
 <template>
@@ -124,11 +126,11 @@ function goBack() {
       <div class="flex items-center justify-between">
         <div class="flex items-center gap-3">
           <button
-            class="flex items-center gap-1 text-gray-600 hover:text-gray-900 transition-colors"
+            class="flex items-center gap-1 text-gray-600 transition-colors hover:text-gray-900"
+            type="button"
             @click="goBack"
           >
-            <ArrowLeft class="size-5" />
-            <span>返回</span>
+            <span>← 返回</span>
           </button>
           <Breadcrumb>
             <Breadcrumb.Item>处方管理</Breadcrumb.Item>
@@ -139,284 +141,488 @@ function goBack() {
       </div>
     </template>
 
-    <div class="bg-white rounded-lg shadow-sm p-6">
-      <div class="flex items-center justify-between mb-6 pb-4 border-b">
-        <div class="flex items-center gap-4">
-          <div class="flex items-center gap-2">
-            <FileText class="size-6 text-blue-500" />
+    <Spin :spinning="loading">
+      <div class="rounded-lg bg-white p-6 shadow-sm">
+        <div class="mb-6 flex items-center justify-between border-b pb-4">
+          <div class="flex items-center gap-4">
             <span class="text-lg font-semibold">煎药中心处方</span>
+            <span class="text-gray-400">|</span>
+            <span class="text-gray-600">
+              处方号:{{ header.number || header.id || '-' }}
+            </span>
+          </div>
+          <div class="flex items-center gap-2 text-gray-600">
+            <span>{{ header.date }}</span>
+            <span class="text-gray-400">|</span>
+            <span
+              class="rounded-full bg-yellow-100 px-3 py-1 text-sm text-yellow-700"
+            >
+              {{ header.status }}
+            </span>
           </div>
-          <span class="text-gray-400">|</span>
-          <span class="text-gray-600">处方号:{{ mockPrescription.id }}</span>
-        </div>
-        <div class="flex items-center gap-2">
-          <Calendar class="size-5 text-gray-400" />
-          <span class="text-gray-600">{{ mockPrescription.date }}</span>
-          <span class="text-gray-400">|</span>
-          <span class="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-sm">{{ mockPrescription.status }}</span>
         </div>
-      </div>
 
-      <Descriptions :column="4" bordered :size="'small'" class="mb-6">
-        <Descriptions.Item label="医疗机构" :labelStyle="{ width: '120px' }">
-          <div class="flex items-center gap-2">
-            <Building2 class="size-4 text-gray-400" />
-            {{ mockPrescription.medicalInstitution }}
-          </div>
-        </Descriptions.Item>
-        <Descriptions.Item label="院区" :labelStyle="{ width: '120px' }">
-          {{ mockPrescription.campus }}
-        </Descriptions.Item>
-        <Descriptions.Item label="代煎中心" :labelStyle="{ width: '120px' }">
-          {{ mockPrescription.decoctionCenter }}
-        </Descriptions.Item>
-        <Descriptions.Item label="代煎代码" :labelStyle="{ width: '120px' }">
-          <span class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-sm">{{ mockPrescription.decoctionCode }}</span>
-        </Descriptions.Item>
-        <Descriptions.Item label="患者姓名" :labelStyle="{ width: '120px' }">
-          <div class="flex items-center gap-2">
-            <User class="size-4 text-gray-400" />
-            {{ mockPrescription.patientName }}
-          </div>
-        </Descriptions.Item>
-        <Descriptions.Item label="性别" :labelStyle="{ width: '120px' }">
-          {{ mockPrescription.gender }}
-        </Descriptions.Item>
-        <Descriptions.Item label="年龄" :labelStyle="{ width: '120px' }">
-          {{ mockPrescription.age }}
-        </Descriptions.Item>
-        <Descriptions.Item label="患者电话" :labelStyle="{ width: '120px' }">
-          <div class="flex items-center gap-2">
-            <Phone class="size-4 text-gray-400" />
-            {{ mockPrescription.phone }}
-          </div>
-        </Descriptions.Item>
-      </Descriptions>
+        <Descriptions :column="4" bordered :size="'small'" class="mb-6">
+          <Descriptions.Item label="医疗机构" :label-style="{ width: '120px' }">
+            {{ header.medicalInstitution }}
+          </Descriptions.Item>
+          <Descriptions.Item label="院区" :label-style="{ width: '120px' }">
+            {{ header.campus }}
+          </Descriptions.Item>
+          <Descriptions.Item label="代煎中心" :label-style="{ width: '120px' }">
+            {{ header.decoctionCenter }}
+          </Descriptions.Item>
+          <Descriptions.Item label="代煎代码" :label-style="{ width: '120px' }">
+            <span
+              class="rounded bg-blue-100 px-2 py-0.5 text-sm text-blue-700"
+            >
+              {{ header.decoctionCode }}
+            </span>
+          </Descriptions.Item>
+          <Descriptions.Item label="患者姓名" :label-style="{ width: '120px' }">
+            {{ header.patientName }}
+          </Descriptions.Item>
+          <Descriptions.Item label="性别" :label-style="{ width: '120px' }">
+            {{ header.gender }}
+          </Descriptions.Item>
+          <Descriptions.Item label="年龄" :label-style="{ width: '120px' }">
+            {{ header.age }}
+          </Descriptions.Item>
+          <Descriptions.Item label="患者电话" :label-style="{ width: '120px' }">
+            {{ header.phone }}
+          </Descriptions.Item>
+        </Descriptions>
 
-      <Tabs v-model:activeKey="activeTab" type="card">
-        <Tabs.TabPane key="prescription" tab="处方详情">
-          <div class="space-y-6">
-            <div class="bg-gray-50 rounded-lg p-4">
-              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
-                <Pill class="size-4 text-green-500" />
-                煎药要求
-              </h3>
-              <Descriptions :column="4" :size="'small'">
-                <Descriptions.Item label="中药类型">
-                  <span class="px-2 py-0.5 bg-green-100 text-green-700 rounded text-sm">饮片(散装)</span>
-                </Descriptions.Item>
-                <Descriptions.Item label="中药剂型">
-                  <span class="px-2 py-0.5 bg-green-100 text-green-700 rounded text-sm">汤剂</span>
-                </Descriptions.Item>
-                <Descriptions.Item label="处方剂数">{{ prescriptionDetails.prescriptionCount }} 剂</Descriptions.Item>
-                <Descriptions.Item label="每剂包数">{{ prescriptionDetails.eachPackageCount }} 包</Descriptions.Item>
-                <Descriptions.Item label="总包数">{{ prescriptionDetails.totalCount }} 包</Descriptions.Item>
-                <Descriptions.Item label="煎药标记">
-                  <span class="px-2 py-0.5 bg-red-100 text-red-700 rounded text-sm">{{ prescriptionDetails.decoctionMark }}</span>
-                </Descriptions.Item>
-                <Descriptions.Item label="煎药时间">{{ prescriptionDetails.decoctionTime }}</Descriptions.Item>
-                <Descriptions.Item label="包装类型">
-                  <span class="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-sm">{{ prescriptionDetails.packageType }}</span>
-                </Descriptions.Item>
-                <Descriptions.Item label="浸泡时间">{{ prescriptionDetails.decoctionTimeMark }}分钟</Descriptions.Item>
-                <Descriptions.Item label="煎煮时间">{{ prescriptionDetails.decoctionTimeValue }}分钟</Descriptions.Item>
-              </Descriptions>
-            </div>
+        <Tabs v-model:active-key="activeTab" type="card">
+          <Tabs.TabPane key="prescription" tab="处方详情">
+            <div class="space-y-6">
+              <div class="rounded-lg bg-gray-50 p-4">
+                <h3 class="mb-4 text-sm font-medium text-gray-700">煎药要求</h3>
+                <Descriptions :column="4" :size="'small'">
+                  <Descriptions.Item label="中药类型">
+                    {{ prescriptionDetails.chineseMedicineType }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="中药剂型">
+                    {{ prescriptionDetails.chineseMedicineForm }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="处方剂数">
+                    {{ prescriptionDetails.prescriptionCount }} 剂
+                  </Descriptions.Item>
+                  <Descriptions.Item label="每剂包数">
+                    {{ prescriptionDetails.eachPackageCount }} 包
+                  </Descriptions.Item>
+                  <Descriptions.Item label="总包数">
+                    {{ prescriptionDetails.totalCount }} 包
+                  </Descriptions.Item>
+                  <Descriptions.Item label="煎药标记">
+                    {{ prescriptionDetails.decoctionMark }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="煎药时间">
+                    {{ prescriptionDetails.decoctionTime }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="包装类型">
+                    {{ prescriptionDetails.packageType }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="浸泡时间">
+                    {{ prescriptionDetails.decoctionTimeMark }} 分钟
+                  </Descriptions.Item>
+                  <Descriptions.Item label="煎煮时间">
+                    {{ prescriptionDetails.decoctionTimeValue }} 分钟
+                  </Descriptions.Item>
+                </Descriptions>
+              </div>
 
-            <div class="bg-gray-50 rounded-lg p-4">
-              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
-                <Truck class="size-4 text-orange-500" />
-                配送信息
-              </h3>
-              <Descriptions :column="4" :size="'small'">
-                <Descriptions.Item label="配送方式">{{ deliveryInfo.deliveryMethod }}</Descriptions.Item>
-                <Descriptions.Item label="物流公司">{{ deliveryInfo.logisticsCompany }}</Descriptions.Item>
-                <Descriptions.Item label="收件人">{{ deliveryInfo.recipientName }}</Descriptions.Item>
-                <Descriptions.Item label="收件电话">{{ deliveryInfo.recipientPhone }}</Descriptions.Item>
-                <Descriptions.Item label="收件省市" :span="2">
-                  <div class="flex items-center gap-2">
-                    <MapPin class="size-4 text-gray-400" />
+              <div class="rounded-lg bg-gray-50 p-4">
+                <h3 class="mb-4 text-sm font-medium text-gray-700">配送信息</h3>
+                <Descriptions :column="4" :size="'small'">
+                  <Descriptions.Item label="配送方式">
+                    {{ deliveryInfo.deliveryMethod }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="物流公司">
+                    {{ deliveryInfo.logisticsCompany }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="收件人">
+                    {{ deliveryInfo.recipientName }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="收件电话">
+                    {{ deliveryInfo.recipientPhone }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="收件省市" :span="2">
                     {{ deliveryInfo.provinceCity }}
-                  </div>
-                </Descriptions.Item>
-                <Descriptions.Item label="详细地址" :span="2">
-                  <div class="flex items-center gap-2">
-                    <MapPin class="size-4 text-gray-400" />
+                  </Descriptions.Item>
+                  <Descriptions.Item label="详细地址" :span="2">
                     {{ deliveryInfo.detailAddress }}
-                  </div>
-                </Descriptions.Item>
-              </Descriptions>
-            </div>
+                  </Descriptions.Item>
+                </Descriptions>
+              </div>
 
-            <div class="bg-gray-50 rounded-lg p-4">
-              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
-                <Clock class="size-4 text-blue-500" />
-                用药信息和金额
-              </h3>
-              <Descriptions :column="4" :size="'small'">
-                <Descriptions.Item label="每日剂数">{{ usageInfo.dailyCount }} 剂</Descriptions.Item>
-                <Descriptions.Item label="用药途径">{{ usageInfo.usageRoute }}</Descriptions.Item>
-                <Descriptions.Item label="用药方法">{{ usageInfo.usageMethod }}</Descriptions.Item>
-                <Descriptions.Item label="使用频次">{{ usageInfo.usageFrequency }}</Descriptions.Item>
-                <Descriptions.Item label="服药时间">{{ usageInfo.takeTime }}</Descriptions.Item>
-                <Descriptions.Item label="处方药味数">{{ usageInfo.prescriptionMedicineCount }} 味</Descriptions.Item>
-                <Descriptions.Item label="药品总金额">¥{{ usageInfo.totalAmount.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="代煎或加工费金额">¥{{ usageInfo.decoctionFee.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="配送金额">¥{{ usageInfo.deliveryFee.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="处方总金额">¥{{ usageInfo.prescriptionTotalAmount.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="医保报销金额">¥{{ usageInfo.medicalInsuranceAmount.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="自付金额">¥{{ usageInfo.selfPayAmount.toFixed(2) }}</Descriptions.Item>
-                <Descriptions.Item label="药师备注">¥{{ usageInfo.dispensingFee.toFixed(2) }}</Descriptions.Item>
-              </Descriptions>
-            </div>
+              <div class="rounded-lg bg-gray-50 p-4">
+                <h3 class="mb-4 text-sm font-medium text-gray-700">
+                  用药信息和金额
+                </h3>
+                <Descriptions :column="4" :size="'small'">
+                  <Descriptions.Item label="每日剂数">
+                    {{ usageInfo.dailyCount }} 剂
+                  </Descriptions.Item>
+                  <Descriptions.Item label="用药途径">
+                    {{ usageInfo.usageRoute }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="用药方法">
+                    {{ usageInfo.usageMethod }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="使用频次">
+                    {{ usageInfo.usageFrequency }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="服药时间">
+                    {{ usageInfo.takeTime }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="处方药味数">
+                    {{ usageInfo.prescriptionMedicineCount }} 味
+                  </Descriptions.Item>
+                  <Descriptions.Item label="药品总金额">
+                    ¥{{ formatMoney(usageInfo.totalAmount) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="代煎或加工费金额">
+                    ¥{{ formatMoney(usageInfo.decoctionFee) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="配送金额">
+                    ¥{{ formatMoney(usageInfo.deliveryFee) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="处方总金额">
+                    ¥{{ formatMoney(usageInfo.prescriptionTotalAmount) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="医保报销金额">
+                    ¥{{ formatMoney(usageInfo.medicalInsuranceAmount) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="自付金额">
+                    ¥{{ formatMoney(usageInfo.selfPayAmount) }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="药师备注" :span="2">
+                    {{ usageInfo.pharmacistRemark }}
+                  </Descriptions.Item>
+                </Descriptions>
+              </div>
 
-            <div class="bg-gray-50 rounded-lg p-4">
-              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
-                <CreditCard class="size-4 text-purple-500" />
-                就诊信息
-              </h3>
-              <Descriptions :column="4" :size="'small'">
-                <Descriptions.Item label="门诊/住院">{{ doctorInfo.outpatientType }}</Descriptions.Item>
-                <Descriptions.Item label="门诊/住院号">{{ doctorInfo.outpatientNo }}</Descriptions.Item>
-                <Descriptions.Item label="科室/病区">{{ doctorInfo.department }}</Descriptions.Item>
-                <Descriptions.Item label="医生姓名">
-                  <div class="flex items-center gap-2">
-                    <Stethoscope class="size-4 text-gray-400" />
+              <div class="rounded-lg bg-gray-50 p-4">
+                <h3 class="mb-4 text-sm font-medium text-gray-700">就诊信息</h3>
+                <Descriptions :column="4" :size="'small'">
+                  <Descriptions.Item label="门诊/住院">
+                    {{ doctorInfo.outpatientType }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="门诊/住院号">
+                    {{ doctorInfo.outpatientNo }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="科室/病区">
+                    {{ doctorInfo.department }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="医生姓名">
                     {{ doctorInfo.doctorName }}
-                  </div>
-                </Descriptions.Item>
-                <Descriptions.Item label="中医病名">{{ doctorInfo.chineseMedicineName }}</Descriptions.Item>
-                <Descriptions.Item label="中医证型">{{ doctorInfo.chineseMedicineCode }}</Descriptions.Item>
-                <Descriptions.Item label="治则治法">{{ doctorInfo.diagnosisMethod }}</Descriptions.Item>
-                <Descriptions.Item label="妊娠标识">{{ doctorInfo.chineseMedicineCodeSecond }}</Descriptions.Item>
-                <Descriptions.Item label="哺乳标识">{{ doctorInfo.diagnosisMethodSecond }}</Descriptions.Item>
-                <Descriptions.Item label="医生备注" :span="2">{{ doctorInfo.doctorNote || '-' }}</Descriptions.Item>
-              </Descriptions>
+                  </Descriptions.Item>
+                  <Descriptions.Item label="中医病名">
+                    {{ doctorInfo.chineseMedicineName }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="中医证型">
+                    {{ doctorInfo.chineseMedicineCode }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="治则治法">
+                    {{ doctorInfo.diagnosisMethod }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="妊娠标识">
+                    {{ doctorInfo.pregnancyFlag }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="哺乳标识">
+                    {{ doctorInfo.lactationFlag }}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="医生备注" :span="2">
+                    {{ doctorInfo.doctorNote || '-' }}
+                  </Descriptions.Item>
+                </Descriptions>
+              </div>
             </div>
-          </div>
-        </Tabs.TabPane>
+          </Tabs.TabPane>
 
-        <Tabs.TabPane key="herbal" tab="饮片明细">
-          <div class="bg-gray-50 rounded-lg p-4">
-            <div class="overflow-x-auto">
-              <table class="w-full text-sm">
-                <thead>
-                  <tr class="bg-white">
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品名称</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">国标编码</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">医疗机构药品名称</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">规格</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">产地</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">剂量</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品单位</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">中药特殊煎法</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品单价(元)</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">医生签名</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr v-for="(item, index) in herbalDetails" :key="index" class="bg-white hover:bg-gray-50">
-                    <td class="px-4 py-3 border-b">{{ item.name }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.nationalCode }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.hospitalName }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.spec }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.origin }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.dosage }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.unit }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.specialMethod }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.price || '-' }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.doctorSignature }}</td>
-                  </tr>
-                </tbody>
-              </table>
+          <Tabs.TabPane key="herbal" tab="饮片明细">
+            <div class="rounded-lg bg-gray-50 p-4">
+              <div v-if="herbalDetails.length" class="overflow-x-auto">
+                <table class="w-full text-sm">
+                  <thead>
+                    <tr class="bg-white">
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        药品名称
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        国标编码
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        医疗机构药品名称
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        规格
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        产地
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        剂量
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        药品单位
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        中药特殊煎法
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        药品单价(元)
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        医生签名
+                      </th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr
+                      v-for="(item, index) in herbalDetails"
+                      :key="index"
+                      class="bg-white hover:bg-gray-50"
+                    >
+                      <td class="border-b px-4 py-3">{{ item.name }}</td>
+                      <td class="border-b px-4 py-3">{{ item.nationalCode }}</td>
+                      <td class="border-b px-4 py-3">{{ item.hospitalName }}</td>
+                      <td class="border-b px-4 py-3">{{ item.spec }}</td>
+                      <td class="border-b px-4 py-3">{{ item.origin }}</td>
+                      <td class="border-b px-4 py-3">{{ item.dosage }}</td>
+                      <td class="border-b px-4 py-3">{{ item.unit }}</td>
+                      <td class="border-b px-4 py-3">{{ item.specialMethod }}</td>
+                      <td class="border-b px-4 py-3">
+                        {{ item.price || '-' }}
+                      </td>
+                      <td class="border-b px-4 py-3">{{ item.doctorSignature }}</td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              <Empty v-else description="暂无饮片明细" />
             </div>
-          </div>
-        </Tabs.TabPane>
+          </Tabs.TabPane>
 
-        <Tabs.TabPane key="flow" tab="流程物流追溯">
-          <div class="bg-gray-50 rounded-lg p-4">
-            <div class="grid grid-cols-2 gap-4">
-              <div class="bg-white rounded-lg p-4">
-                <h3 class="text-sm font-medium text-gray-700 mb-4">处方流转流程</h3>
-                <div class="relative pl-6 border-l-2 border-gray-200">
-                  <div v-for="(item, index) in processFlowDetails" :key="index" class="relative pb-4 last:pb-0">
-                    <div class="absolute -left-[9px] w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
-                    <div class="ml-2">
-                      <div style="display: flex;align-items: center;justify-content: space-between;padding-right: 100px;">
-                        <div class="text-sm font-medium text-gray-800">{{ item.status }}</div>
-                        <div class="text-xs text-gray-500">操作时间: {{ item.operationTime }}</div>
-                      </div>
-                      <div style="display: flex;align-items: center;justify-content: space-between;padding-right: 100px;">
-                        <div class="text-xs text-gray-500 mt-1">操作人: {{ item.operator }}</div>
-                        <div class="mt-1">
-                          <span class="text-xs text-gray-500 mt-1">环节照片/视频:</span><a href="#" class="text-xs text-blue-500 hover:text-blue-700"> 查看照片</a>
+          <Tabs.TabPane key="flow" tab="流程物流追溯">
+            <div class="rounded-lg bg-gray-50 p-4">
+              <div class="grid grid-cols-2 gap-4">
+                <div class="rounded-lg bg-white p-4">
+                  <h3 class="mb-4 text-sm font-medium text-gray-700">
+                    处方流转流程
+                  </h3>
+                  <div
+                    v-if="processFlowDetails.length"
+                    class="relative border-l-2 border-gray-200 pl-6"
+                  >
+                    <div
+                      v-for="(item, index) in processFlowDetails"
+                      :key="index"
+                      class="relative pb-4 last:pb-0"
+                    >
+                      <div
+                        class="absolute -left-[9px] h-4 w-4 rounded-full border-2 border-white bg-green-500"
+                      />
+                      <div class="ml-2">
+                        <div
+                          class="flex items-center justify-between pr-24 text-sm font-medium text-gray-800"
+                        >
+                          <span>{{ item.status }}</span>
+                          <span class="text-xs text-gray-500">
+                            操作时间: {{ item.operationTime }}
+                          </span>
+                        </div>
+                        <div
+                          class="mt-1 flex items-center justify-between pr-24 text-xs text-gray-500"
+                        >
+                          <span>操作人: {{ item.operator }}</span>
+                          <span v-if="item.hasPhoto">
+                            环节照片/视频:
+                            <a
+                              v-if="item.photoUrl"
+                              :href="item.photoUrl"
+                              class="text-blue-500 hover:text-blue-700"
+                              rel="noopener noreferrer"
+                              target="_blank"
+                            >
+                              查看照片
+                            </a>
+                            <span v-else>有</span>
+                          </span>
+                        </div>
+                        <div v-if="item.water" class="text-xs text-gray-500">
+                          加水量: {{ item.water }}
+                        </div>
+                        <div v-if="item.weightCheck" class="text-xs text-gray-500">
+                          复核重量: {{ item.weightCheck }}
+                        </div>
+                        <div v-if="item.weight" class="text-xs text-gray-500">
+                          实际重量: {{ item.weight }}
+                        </div>
+                        <div v-if="item.deviceName" class="text-xs text-gray-500">
+                          设备: {{ item.deviceName }}
+                        </div>
+                        <div v-if="item.note" class="mt-1 text-xs text-gray-500">
+                          备注: {{ item.note }}
                         </div>
                       </div>
-                      <div v-if="item.water" class="text-xs text-gray-500">加水量: {{ item.water }}</div>
-                      <div v-if="item.weightCheck" class="text-xs text-gray-500">复核重量: {{ item.weightCheck }}</div>
-                      <div v-if="item.weight" class="text-xs text-gray-500">调配重量: {{ item.weight }}</div>
-                      <div v-if="item.note" class="text-xs text-gray-500 mt-1">备注: {{ item.note }}</div>
                     </div>
                   </div>
+                  <Empty v-else description="暂无流转流程" />
                 </div>
-              </div>
 
-              <div class="bg-white rounded-lg p-4">
-                <h3 class="text-sm font-medium text-gray-700 mb-4">物流跟踪</h3>
-                <div class="flex items-center justify-between mb-4">
-                  <span class="text-sm text-gray-500">{{ logisticsInfo.company }} {{ logisticsInfo.trackingNo }}</span>
-                  <span class="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs">{{ logisticsInfo.status }}</span>
-                </div>
-                <div class="relative pl-6 border-l-2 border-gray-200">
-                  <div class="absolute -left-[9px] w-4 h-4 bg-yellow-500 rounded-full border-2 border-white"></div>
-                  <div class="ml-2">
-                    <div class="text-sm font-medium text-gray-800">{{ logisticsInfo.status }}</div>
-                    <div class="text-xs text-gray-500 mt-1">{{ logisticsInfo.statusTime }}</div>
-                    <div class="text-xs text-gray-500 mt-1">{{ logisticsInfo.progress }}</div>
-                  </div>
+                <div class="rounded-lg bg-white p-4">
+                  <h3 class="mb-4 text-sm font-medium text-gray-700">物流跟踪</h3>
+                  <template
+                    v-if="
+                      logisticsInfo.company ||
+                      logisticsInfo.trackingNo ||
+                      logisticsNodes.length
+                    "
+                  >
+                    <div class="mb-4 flex items-center justify-between">
+                      <span class="text-sm text-gray-500">
+                        {{ logisticsInfo.company }}
+                        {{ logisticsInfo.trackingNo }}
+                      </span>
+                      <span
+                        v-if="logisticsInfo.status !== '-'"
+                        class="rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-700"
+                      >
+                        {{ logisticsInfo.status }}
+                      </span>
+                    </div>
+                    <div
+                      v-if="logisticsNodes.length"
+                      class="relative border-l-2 border-gray-200 pl-6"
+                    >
+                      <div
+                        v-for="(item, index) in logisticsNodes"
+                        :key="index"
+                        class="relative pb-4 last:pb-0"
+                      >
+                        <div
+                          class="absolute -left-[9px] h-4 w-4 rounded-full border-2 border-white bg-yellow-500"
+                        />
+                        <div class="ml-2">
+                          <div
+                            class="flex items-center justify-between pr-8 text-sm font-medium text-gray-800"
+                          >
+                            <span>{{ item.status }}</span>
+                            <span class="text-xs text-gray-500">
+                              {{ item.operationTime }}
+                            </span>
+                          </div>
+                          <div class="mt-1 text-xs text-gray-500">
+                            操作人: {{ item.operator }}
+                          </div>
+                          <div
+                            v-if="item.location && item.location !== '-'"
+                            class="text-xs text-gray-500"
+                          >
+                            地点: {{ item.location }}
+                          </div>
+                          <div
+                            v-if="item.trackingNumber && item.trackingNumber !== '-'"
+                            class="text-xs text-gray-500"
+                          >
+                            运单号: {{ item.trackingNumber }}
+                          </div>
+                          <div
+                            v-if="item.note && item.note !== '-'"
+                            class="mt-1 text-xs text-gray-500"
+                          >
+                            备注: {{ item.note }}
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                    <div
+                      v-else-if="logisticsInfo.progress"
+                      class="text-xs text-gray-500"
+                    >
+                      {{ logisticsInfo.progress }}
+                    </div>
+                  </template>
+                  <Empty v-else description="暂无物流信息" />
                 </div>
               </div>
             </div>
-          </div>
-        </Tabs.TabPane>
+          </Tabs.TabPane>
 
-        <Tabs.TabPane key="trace" tab="饮片追溯">
-          <div class="bg-gray-50 rounded-lg p-4">
-            <div class="overflow-x-auto">
-              <table class="w-full text-sm">
-                <thead>
-                  <tr class="bg-white">
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片名称</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">规格编码</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片批号</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">供应商</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">生产厂家</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片产地</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">生产日期</th>
-                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">有效期至</th>
-                    <th class="px-4 py-3 text-center font-medium text-gray-700 border-b">检验报告</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr v-for="(item, index) in herbalTraceDetails" :key="index" class="bg-white hover:bg-gray-50">
-                    <td class="px-4 py-3 border-b">{{ item.name }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.specCode }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.batchNo }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.supplier }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.manufacturer }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.origin }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.productionDate }}</td>
-                    <td class="px-4 py-3 border-b">{{ item.expiryDate }}</td>
-                    <td class="px-4 py-3 border-b text-center">
-                      <a href="#" class="text-blue-500 hover:text-blue-700 underline">查看</a>
-                    </td>
-                  </tr>
-                </tbody>
-              </table>
+          <Tabs.TabPane key="trace" tab="饮片追溯">
+            <div class="rounded-lg bg-gray-50 p-4">
+              <div v-if="herbalTraceDetails.length" class="overflow-x-auto">
+                <table class="w-full text-sm">
+                  <thead>
+                    <tr class="bg-white">
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        饮片名称
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        规格编码
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        饮片批号
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        供应商
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        生产厂家
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        饮片产地
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        生产日期
+                      </th>
+                      <th class="border-b px-4 py-3 text-left font-medium text-gray-700">
+                        有效期至
+                      </th>
+                      <th
+                        class="border-b px-4 py-3 text-center font-medium text-gray-700"
+                      >
+                        检验报告
+                      </th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr
+                      v-for="(item, index) in herbalTraceDetails"
+                      :key="index"
+                      class="bg-white hover:bg-gray-50"
+                    >
+                      <td class="border-b px-4 py-3">{{ item.name }}</td>
+                      <td class="border-b px-4 py-3">{{ item.specCode }}</td>
+                      <td class="border-b px-4 py-3">{{ item.batchNo }}</td>
+                      <td class="border-b px-4 py-3">{{ item.supplier }}</td>
+                      <td class="border-b px-4 py-3">{{ item.manufacturer }}</td>
+                      <td class="border-b px-4 py-3">{{ item.origin }}</td>
+                      <td class="border-b px-4 py-3">{{ item.productionDate }}</td>
+                      <td class="border-b px-4 py-3">{{ item.expiryDate }}</td>
+                      <td class="border-b px-4 py-3 text-center">
+                        <a
+                          v-if="item.reportUrl"
+                          :href="item.reportUrl"
+                          class="text-blue-500 underline hover:text-blue-700"
+                          rel="noopener noreferrer"
+                          target="_blank"
+                        >
+                          查看
+                        </a>
+                        <span v-else>-</span>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              <Empty v-else description="暂无饮片追溯数据" />
             </div>
-          </div>
-        </Tabs.TabPane>
-      </Tabs>
-    </div>
+          </Tabs.TabPane>
+        </Tabs>
+      </div>
+    </Spin>
   </Page>
 </template>

+ 47 - 124
apps/smart-pharmacy/src/views/prescription/management/list.vue

@@ -3,99 +3,28 @@ import type {
   OnActionClickParams,
   VxeTableGridOptions,
 } from '#/adapter/vxe-table';
-import type { SystemModel } from '#/api';
+import type { PrescriptionModel } from '#/api';
 
 import { useRouter } from 'vue-router';
 
 import { Page } from '@vben/common-ui';
 
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { listPrescriptionsMethod } from '#/api';
+import {
+  getDeliveryMethodLabelMap,
+  getLogisticsCompanyLabelMap,
+  getProcessNodeLabelMap,
+  resolveDeliveryMethodLabel,
+  resolveLogisticsCompanyLabel,
+  resolveProcessNodeLabel,
+} from '#/api/model/process-node-dict';
 
 import { useUserSearchFormSchema, useUserTableColumns } from './data';
 
-const mockData = {
-  TotalRecordCount: 5,
-  TotalPageCount: 1,
-  PageSize: 20,
-  ButtonRight: '',
-  CurrentPageSize: 14,
-  Items: [
-    {
-      id: '111',
-      date: '2026-03-09',
-      number: '2023092323945',
-      medicalInstitution: '蒋村社区卫生服务中心',
-      campus: '萧山馆',
-      patientName: '张*行',
-      decoctionMethod: '代煎',
-      decoctionEnterprise: '重煎药中心',
-      decoctionCenter: '重煎药中心华东区',
-      deliveryMethod: '配送到医院',
-      logisticsCompany: '顺丰速运',
-      processStatus: '已打包',
-    },
-    {
-      id: '222',
-      date: '2026-03-11',
-      number: '2023092312934',
-      medicalInstitution: '浙江省中医院',
-      campus: '萧山馆',
-      patientName: '赵*方',
-      decoctionMethod: '代煎',
-      decoctionEnterprise: '重煎药中心',
-      decoctionCenter: '重煎药中心华东区',
-      deliveryMethod: '配送到医院',
-      logisticsCompany: '邮政快递',
-      processStatus: '已调配',
-    },
-    {
-      id: '333',
-      date: '2026-03-12',
-      number: '2023092329384',
-      medicalInstitution: '同仁堂',
-      campus: '湖滨院区',
-      patientName: '李*当',
-      decoctionMethod: '代煎',
-      decoctionEnterprise: '杭州药中心',
-      decoctionCenter: '杭州煎药中心',
-      deliveryMethod: '配送到家',
-      logisticsCompany: '邮政快递',
-      processStatus: '已打包',
-    },
-    {
-      id: '444',
-      date: '2026-03-13',
-      number: '2023092329384',
-      medicalInstitution: '同仁堂',
-      campus: '',
-      patientName: '王*冰',
-      decoctionMethod: '自煎/代配',
-      decoctionEnterprise: '',
-      decoctionCenter: '',
-      deliveryMethod: '不配送',
-      logisticsCompany: '',
-      processStatus: '已调配',
-    },
-    {
-      id: '555',
-      date: '2026-03-13',
-      number: '2023092392834',
-      medicalInstitution: '浙江省中医院',
-      campus: '湖滨院区',
-      patientName: '刘*鑫',
-      decoctionMethod: '代煎',
-      decoctionEnterprise: '杭州药中心',
-      decoctionCenter: '杭州煎药中心',
-      deliveryMethod: '配送到家',
-      logisticsCompany: '顺丰速运',
-      processStatus: '已浸泡',
-    },
-  ],
-  PageIndex: 1,
-};
 const router = useRouter();
 
-const [Grid, gridApi] = useVbenVxeGrid({
+const [Grid] = useVbenVxeGrid({
   formOptions: {
     schema: useUserSearchFormSchema(),
     submitOnChange: true,
@@ -104,51 +33,50 @@ const [Grid, gridApi] = useVbenVxeGrid({
     columns: useUserTableColumns(onActionClick),
     height: 'auto',
     keepSource: true,
-    // proxyConfig: {
-    //   ajax: {
-    //     query({ page }, formValues) {
-    //       // 处理时间范围字段分离
-    //       const processedValues = { ...formValues };
-    //       if (
-    //         processedValues.operateTimeRange &&
-    //         Array.isArray(processedValues.operateTimeRange)
-    //       ) {
-    //         const [startTime, endTime] = processedValues.operateTimeRange;
-    //         processedValues.startTime = startTime;
-    //         processedValues.endTime = endTime;
-    //         delete processedValues.operateTimeRange; // 删除原始字段
-    //       }
-    //       return listRecordsMethod(
-    //         page.currentPage,
-    //         page.pageSize,
-    //         processedValues,
-    //       );
-    //     },
-    //   },
-    // },
     proxyConfig: {
-      response: {
-        result: 'Data.Items',
-        total: 'Data.TotalRecordCount',
-      },
-
       ajax: {
-        query() {
-          return Promise.resolve({
-            Data: mockData,
-            ResultInfo: '操作成功',
-            ResultCode: 0,
-          });
+        async query({ page }, formValues) {
+          const [result, processNodeMap, deliveryMethodMap, logisticsCompanyMap] =
+            await Promise.all([
+              listPrescriptionsMethod(
+                page.currentPage,
+                page.pageSize,
+                formValues,
+              ),
+              getProcessNodeLabelMap(),
+              getDeliveryMethodLabelMap(),
+              getLogisticsCompanyLabelMap(),
+            ]);
+          return {
+            ...result,
+            items: result.items.map((item) => ({
+              ...item,
+              processStatus: resolveProcessNodeLabel(
+                item.processStatus,
+                processNodeMap,
+              ),
+              deliveryMethod: resolveDeliveryMethodLabel(
+                item.deliveryMethod,
+                deliveryMethodMap,
+              ),
+              logisticsCompany: resolveLogisticsCompanyLabel(
+                item.logisticsCompany,
+                logisticsCompanyMap,
+              ),
+            })),
+          };
         },
       },
     },
     rowConfig: {
       keyField: 'id',
     },
-  } as VxeTableGridOptions<SystemModel.User>,
+  } as VxeTableGridOptions<PrescriptionModel.Prescription>,
 });
-function onActionClick(e: OnActionClickParams<SystemModel.User>) {
-  console.log('onActionClick---查看---', e);
+
+function onActionClick(
+  e: OnActionClickParams<PrescriptionModel.Prescription>,
+) {
   switch (e.code) {
     case 'view': {
       onViewHandle(e.row);
@@ -156,18 +84,13 @@ function onActionClick(e: OnActionClickParams<SystemModel.User>) {
     }
   }
 }
-// 刷新
-function onRefresh() {
-  gridApi.query();
-}
 
-function onViewHandle(row: SystemModel.User) {
+function onViewHandle(row: PrescriptionModel.Prescription) {
   router.push(`/prescription/detail/${row.id}`);
 }
 </script>
 <template>
   <Page auto-content-height>
-    <FormModal @success="onRefresh" />
     <Grid />
   </Page>
 </template>