Просмотр исходного кода

feat(@six/smart-pharmacy): 智慧药事系统第二版-处方点评静态页面新增

cmj 2 недель назад
Родитель
Сommit
d37a4da699
18 измененных файлов с 4200 добавлено и 26 удалено
  1. 40 0
      apps/smart-pharmacy/public/database/menu.json
  2. 1456 23
      apps/smart-pharmacy/src/api/method/prescription-review.ts
  3. 5 1
      apps/smart-pharmacy/src/api/model/menu.ts
  4. 1 1
      apps/smart-pharmacy/src/views/patient-evaluation/efficacy/data.ts
  5. 1 1
      apps/smart-pharmacy/src/views/patient-evaluation/satisfaction/data.ts
  6. 247 0
      apps/smart-pharmacy/src/views/prescription-review/task/data.ts
  7. 232 0
      apps/smart-pharmacy/src/views/prescription-review/task/list.vue
  8. 217 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/auxiliary-review-modal.vue
  9. 93 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/confirm-modal.vue
  10. 116 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/expert-group-modal.vue
  11. 122 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/sampling-modal.vue
  12. 112 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/sampling-rules-modal.vue
  13. 145 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/cost-report.vue
  14. 97 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/prescription-table.vue
  15. 124 0
      apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/result-summary.vue
  16. 683 0
      apps/smart-pharmacy/src/views/prescription-review/task/review.vue
  17. 397 0
      apps/smart-pharmacy/src/views/prescription-review/task/statistics-detail.vue
  18. 112 0
      apps/smart-pharmacy/src/views/prescription-review/task/statistics.vue

+ 40 - 0
apps/smart-pharmacy/public/database/menu.json

@@ -123,6 +123,16 @@
     "name": "PrescriptionReview",
     "name": "PrescriptionReview",
     "path": "/prescription-review",
     "path": "/prescription-review",
     "children": [
     "children": [
+      {
+        "id": "2503",
+        "path": "/prescription-review/task",
+        "name": "PrescriptionReviewTask",
+        "meta": {
+          "icon": "mdi:clipboard-text-search",
+          "title": "处方点评"
+        },
+        "component": "/prescription-review/task/list"
+      },
       {
       {
         "id": "2501",
         "id": "2501",
         "path": "/prescription-review/expert",
         "path": "/prescription-review/expert",
@@ -142,6 +152,36 @@
           "title": "点评指标库"
           "title": "点评指标库"
         },
         },
         "component": "/prescription-review/indicator-library/list"
         "component": "/prescription-review/indicator-library/list"
+      },
+      {
+        "id": "2504",
+        "path": "/prescription-review/task/review/:id",
+        "name": "PrescriptionReviewTaskReview",
+        "meta": {
+          "title": "处方点评",
+          "hideInMenu": true
+        },
+        "component": "/prescription-review/task/review"
+      },
+      {
+        "id": "2505",
+        "path": "/prescription-review/task/statistics/:id",
+        "name": "PrescriptionReviewTaskStatistics",
+        "meta": {
+          "title": "统计分析",
+          "hideInMenu": true
+        },
+        "component": "/prescription-review/task/statistics"
+      },
+      {
+        "id": "2506",
+        "path": "/prescription-review/task/statistics/:id/detail/:prescriptionId",
+        "name": "PrescriptionReviewTaskStatisticsDetail",
+        "meta": {
+          "title": "点评详情",
+          "hideInMenu": true
+        },
+        "component": "/prescription-review/task/statistics-detail"
       }
       }
     ]
     ]
   },
   },

+ 1456 - 23
apps/smart-pharmacy/src/api/method/prescription-review.ts

@@ -35,6 +35,163 @@ export namespace PrescriptionReviewModel {
     /** 0 启用,1 禁用 */
     /** 0 启用,1 禁用 */
     status: 0 | 1;
     status: 0 | 1;
   }
   }
+
+  /** 点评任务状态 */
+  export type ReviewTaskStatus =
+    | 'archived'
+    | 'completed'
+    | 'in_progress'
+    | 'pending';
+
+  /** 处方点评任务(抽样批次) */
+  export interface ReviewTask extends TransformRecord {
+    name: string;
+    sampleTime: string;
+    sampleCount: number;
+    prescriptionDateStart: string;
+    prescriptionDateEnd: string;
+    status: ReviewTaskStatus;
+    reviewedCount: number;
+    archiveTime?: string;
+    passRate: number;
+    /** 抽样规则快照 */
+    samplingRule?: ReviewSamplingRule;
+    expertIds?: string[];
+  }
+
+  /** 抽样规则 */
+  export interface ReviewSamplingRule {
+    name: string;
+    institutions?: string[];
+    prescriptionDateStart?: string;
+    prescriptionDateEnd?: string;
+    herbCountMin?: number;
+    herbCountMax?: number;
+    amountMin?: number;
+    amountMax?: number;
+    sampleCount?: number;
+    sampleRatio?: number;
+    excludeReviewed?: boolean;
+  }
+
+  /** 抽样机构树节点(医疗机构 / 科室 / 医生) */
+  export interface ReviewInstitutionTreeNode {
+    id: string;
+    pid: string;
+    name: string;
+    children?: ReviewInstitutionTreeNode[];
+  }
+
+  /** 点评任务中的处方记录 */
+  export interface ReviewPrescriptionRecord extends TransformRecord {
+    taskId: string;
+    prescriptionNo: string;
+    institutionName: string;
+    departmentName: string;
+    doctorName: string;
+    status: 'pending' | 'qualified' | 'unqualified';
+    reviewExpert?: string;
+    /** 点评详情 */
+    reviewResult?: ReviewPrescriptionResult;
+    /** 智能辅助点评结果 */
+    auxiliaryReviewResult?: ReviewPrescriptionResult;
+    /** 处方详情 */
+    patientName?: string;
+    patientGender?: string;
+    patientAge?: number;
+    pregnancy?: boolean;
+    lactation?: boolean;
+    tcmDisease?: string;
+    tcmSyndrome?: string;
+    treatmentPrinciple?: string;
+    administrationMethod?: string;
+    herbCount?: number;
+    doseCount?: number;
+    totalAmount?: number;
+    unitDoseAmount?: number;
+    herbs?: string[];
+  }
+
+  export interface ReviewPrescriptionResult {
+    qualified: boolean;
+    indicatorIds?: string[];
+    herbIndicatorMap?: Record<string, string[]>;
+    comment?: string;
+  }
+
+  /** 统计分析 - 处方点评表行 */
+  export interface ReviewStatisticsPrescriptionRow extends TransformRecord {
+    prescriptionNo: string;
+    institutionName: string;
+    doctorName: string;
+    pharmacistName?: string;
+    result: 'qualified' | 'unqualified';
+    unqualifiedDetail?: string;
+    comment?: string;
+    unitDoseAmount: number;
+  }
+
+  /** 统计分析 - 处方点评详情 */
+  export interface ReviewStatisticsPrescriptionDetail
+    extends ReviewStatisticsPrescriptionRow {
+    departmentName: string;
+    patientName: string;
+    patientGender: string;
+    patientAge: number;
+    pregnancy: boolean;
+    lactation: boolean;
+    tcmDisease: string;
+    tcmSyndrome: string;
+    treatmentPrinciple: string;
+    administrationMethod: string;
+    herbCount: number;
+    doseCount: number;
+    totalAmount: number;
+    herbs: string[];
+    reviewExpert?: string;
+    reviewTime?: string;
+    reviewResult?: ReviewPrescriptionResult;
+    /** 问题饮片旁标注文案,如 { 秦艽: '唐熙' } */
+    herbIssueLabels?: Record<string, string>;
+  }
+
+  /** 统计分析 - 均剂费用汇报层级 */
+  export type ReviewStatisticsCostLevel =
+    | 'department'
+    | 'doctor'
+    | 'institution'
+    | 'prescription';
+
+  /** 统计分析 - 均剂费用汇报行(树形:机构 → 科室 → 医生 → 处方号) */
+  export interface ReviewStatisticsCostRow extends TransformRecord {
+    name: string;
+    level: ReviewStatisticsCostLevel;
+    prescriptionCount: number;
+    totalDoseCount: number;
+    totalAmount: number;
+    avgDoseCost: number;
+    exceedLimit?: boolean;
+    children?: ReviewStatisticsCostRow[];
+  }
+
+  /** 统计分析 - 点评结果汇报 */
+  export interface ReviewStatisticsSummary {
+    sampledPrescriptionCount: number;
+    sampledInstitutionCount: number;
+    qualifiedCount: number;
+    qualifiedRate: number;
+    avgDoseCost: number;
+    exceedLimitInstitutionCount: number;
+    sampleRatio: number;
+    sampledDoctorCount: number;
+    unqualifiedCount: number;
+    unqualifiedRate: number;
+    exceedLimitDoseCount: number;
+    exceedLimitDoseRate: number;
+    exceedLimitDoctorCount: number;
+    topUnqualifiedReasons: string[];
+    topExcessDosageHerbs: string[];
+  }
 }
 }
 
 
 const MOCK_EXPERTS: PrescriptionReviewModel.ReviewExpert[] = [
 const MOCK_EXPERTS: PrescriptionReviewModel.ReviewExpert[] = [
@@ -195,8 +352,9 @@ const MOCK_INDICATOR_CATEGORIES: PrescriptionReviewModel.ReviewIndicatorCategory
     { id: 'cat-1', name: '适应症', source: 'system' },
     { id: 'cat-1', name: '适应症', source: 'system' },
     { id: 'cat-2', name: '用法用量', source: 'system' },
     { id: 'cat-2', name: '用法用量', source: 'system' },
     { id: 'cat-3', name: '配伍禁忌', source: 'system' },
     { id: 'cat-3', name: '配伍禁忌', source: 'system' },
-    { id: 'cat-4', name: '特殊人群禁忌', source: 'system' },
+    { id: 'cat-4', name: '特殊人群禁忌', source: 'system' },
     { id: 'cat-5', name: '其他禁忌', source: 'system' },
     { id: 'cat-5', name: '其他禁忌', source: 'system' },
+    { id: 'cat-6', name: '超均贴限价', source: 'system' },
   ];
   ];
 
 
 const MOCK_INDICATORS: PrescriptionReviewModel.ReviewIndicator[] = [
 const MOCK_INDICATORS: PrescriptionReviewModel.ReviewIndicator[] = [
@@ -236,24 +394,6 @@ const MOCK_INDICATORS: PrescriptionReviewModel.ReviewIndicator[] = [
     associatedChineseMedicine: false,
     associatedChineseMedicine: false,
     status: 1,
     status: 1,
   },
   },
-  {
-    id: 'ind-5',
-    categoryId: 'cat-2',
-    categoryName: '用法用量',
-    name: '超剂量用药',
-    source: 'system',
-    associatedChineseMedicine: false,
-    status: 1,
-  },
-  {
-    id: 'ind-6',
-    categoryId: 'cat-3',
-    categoryName: '配伍禁忌',
-    name: '十八反十九畏',
-    source: 'system',
-    associatedChineseMedicine: true,
-    status: 0,
-  },
   {
   {
     id: 'ind-7',
     id: 'ind-7',
     categoryId: 'cat-1',
     categoryId: 'cat-1',
@@ -276,6 +416,159 @@ const MOCK_INDICATORS: PrescriptionReviewModel.ReviewIndicator[] = [
     createTime: '2023-09-23 15:29:38',
     createTime: '2023-09-23 15:29:38',
     status: 0,
     status: 0,
   },
   },
+  {
+    id: 'ind-dose-exceed',
+    categoryId: 'cat-2',
+    categoryName: '用法用量',
+    name: '剂量超标',
+    source: 'system',
+    associatedChineseMedicine: true,
+    status: 1,
+  },
+  {
+    id: 'ind-dose-low',
+    categoryId: 'cat-2',
+    categoryName: '用法用量',
+    name: '剂量过低',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-admin-wrong',
+    categoryId: 'cat-2',
+    categoryName: '用法用量',
+    name: '用法不正确',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-herb-count',
+    categoryId: 'cat-2',
+    categoryName: '用法用量',
+    name: '药味数超标',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-long-term',
+    categoryId: 'cat-2',
+    categoryName: '用法用量',
+    name: '长期服用',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-18fan',
+    categoryId: 'cat-3',
+    categoryName: '配伍禁忌',
+    name: '十八反',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-19wei',
+    categoryId: 'cat-3',
+    categoryName: '配伍禁忌',
+    name: '十九畏',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-incompatible',
+    categoryId: 'cat-3',
+    categoryName: '配伍禁忌',
+    name: '不宜同用',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-child',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '儿童',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-elder',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '老年人',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-childbearing',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '育龄期妇女',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-pregnant-caution',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '孕妇慎用',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-pregnant-forbidden',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '孕妇禁用',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-pregnant-avoid',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '孕妇忌用',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-lactation',
+    categoryId: 'cat-4',
+    categoryName: '特殊人群慎禁忌',
+    name: '哺乳期妇女禁忌',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-other',
+    categoryId: 'cat-5',
+    categoryName: '其他禁忌',
+    name: '其他禁忌',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
+  {
+    id: 'ind-price-limit',
+    categoryId: 'cat-6',
+    categoryName: '超均贴限价',
+    name: '超均贴限价',
+    source: 'system',
+    associatedChineseMedicine: false,
+    status: 1,
+  },
 ];
 ];
 
 
 function getCategoryIndicatorCount(categoryId: string) {
 function getCategoryIndicatorCount(categoryId: string) {
@@ -430,14 +723,19 @@ export function deleteReviewIndicatorCategoryMethod(categoryId: string) {
 }
 }
 
 
 /** 按分类获取点评指标列表(当前为本地 mock) */
 /** 按分类获取点评指标列表(当前为本地 mock) */
-export function listReviewIndicatorsByCategoryMethod(categoryId: string) {
+export function listReviewIndicatorsByCategoryMethod(
+  categoryId: string,
+  options?: { enabledOnly?: boolean },
+) {
   const category = MOCK_INDICATOR_CATEGORIES.find((item) => item.id === categoryId);
   const category = MOCK_INDICATOR_CATEGORIES.find((item) => item.id === categoryId);
   if (!category) {
   if (!category) {
     return Promise.reject(new Error('分类不存在'));
     return Promise.reject(new Error('分类不存在'));
   }
   }
-  return Promise.resolve(
-    MOCK_INDICATORS.filter((item) => item.categoryId === categoryId),
-  );
+  let items = MOCK_INDICATORS.filter((item) => item.categoryId === categoryId);
+  if (options?.enabledOnly) {
+    items = items.filter((item) => item.status === 1);
+  }
+  return Promise.resolve(items);
 }
 }
 
 
 /** 点评指标排序(当前为本地 mock,按分类内顺序) */
 /** 点评指标排序(当前为本地 mock,按分类内顺序) */
@@ -569,3 +867,1138 @@ export function updateReviewIndicatorStatusMethod(
   indicator.status = status;
   indicator.status = status;
   return Promise.resolve(true);
   return Promise.resolve(true);
 }
 }
+
+const REVIEW_TASK_STATUS_LABEL: Record<
+  PrescriptionReviewModel.ReviewTaskStatus,
+  string
+> = {
+  pending: '待点评',
+  in_progress: '点评进行中',
+  completed: '点评完成',
+  archived: '已归档',
+};
+
+function buildReviewInstitutionTreeNode(
+  id: string,
+  name: string,
+  children?: PrescriptionReviewModel.ReviewInstitutionTreeNode[],
+): PrescriptionReviewModel.ReviewInstitutionTreeNode {
+  return {
+    id,
+    pid: id,
+    name,
+    ...(children?.length ? { children } : {}),
+  };
+}
+
+/** 抽样弹窗 - 医疗机构/科室/医生树(当前为本地 mock) */
+const MOCK_REVIEW_SAMPLING_INSTITUTION_TREE: PrescriptionReviewModel.ReviewInstitutionTreeNode[] =
+  [
+    buildReviewInstitutionTreeNode('hospital-1', '智慧药事医院', [
+      buildReviewInstitutionTreeNode('dept-lab', '检验科', [
+        buildReviewInstitutionTreeNode('doc-lab-1', '张检验'),
+        buildReviewInstitutionTreeNode('doc-lab-2', '李检验'),
+        buildReviewInstitutionTreeNode('doc-lab-3', '王检验'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-emergency', '应急物资库'),
+      buildReviewInstitutionTreeNode('dept-pharmacy', '药库', [
+        buildReviewInstitutionTreeNode('dept-tcm', '中药库', [
+          buildReviewInstitutionTreeNode('dept-tcm-room', '中药房'),
+          buildReviewInstitutionTreeNode('doc-tcm-1', '陈药师'),
+          buildReviewInstitutionTreeNode('doc-tcm-2', '林药师'),
+        ]),
+        buildReviewInstitutionTreeNode('dept-wm', '西药库', [
+          buildReviewInstitutionTreeNode('dept-wm-room', '西药房'),
+          buildReviewInstitutionTreeNode('doc-wm-1', '赵药师'),
+          buildReviewInstitutionTreeNode('doc-wm-2', '钱药师'),
+        ]),
+        buildReviewInstitutionTreeNode('dept-vaccine', '疫苗库', [
+          buildReviewInstitutionTreeNode('dept-vaccine-room', '疫苗仓储'),
+        ]),
+      ]),
+      buildReviewInstitutionTreeNode('dept-rehab', '康复科', [
+        buildReviewInstitutionTreeNode('doc-rehab-1', '刘医生'),
+        buildReviewInstitutionTreeNode('doc-rehab-2', '孙医生'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-checkup', '体检科室', [
+        buildReviewInstitutionTreeNode('doc-checkup-1', '周体检'),
+        buildReviewInstitutionTreeNode('doc-checkup-2', '吴体检'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-office', '办公室'),
+      buildReviewInstitutionTreeNode('dept-station', '服务站', [
+        buildReviewInstitutionTreeNode('station-east', '城东服务站'),
+        buildReviewInstitutionTreeNode('station-west', '城西服务站'),
+        buildReviewInstitutionTreeNode('station-south', '城南服务站'),
+        buildReviewInstitutionTreeNode('station-north', '城北服务站'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-internal', '内科', [
+        buildReviewInstitutionTreeNode('doc-int-1', '张明'),
+        buildReviewInstitutionTreeNode('doc-int-2', '李华'),
+        buildReviewInstitutionTreeNode('doc-int-3', '王芳'),
+        buildReviewInstitutionTreeNode('doc-int-4', '赵敏'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-surgery', '外科', [
+        buildReviewInstitutionTreeNode('doc-sur-1', '王强'),
+        buildReviewInstitutionTreeNode('doc-sur-2', '赵磊'),
+        buildReviewInstitutionTreeNode('doc-sur-3', '孙伟'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-tcm-clinic', '中医科', [
+        buildReviewInstitutionTreeNode('doc-tcm-clinic-1', '周中医'),
+        buildReviewInstitutionTreeNode('doc-tcm-clinic-2', '吴中医'),
+        buildReviewInstitutionTreeNode('doc-tcm-clinic-3', '郑中医'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-pediatric', '儿科', [
+        buildReviewInstitutionTreeNode('doc-ped-1', '杨儿科'),
+        buildReviewInstitutionTreeNode('doc-ped-2', '何儿科'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-gynecology', '妇科', [
+        buildReviewInstitutionTreeNode('doc-gyn-1', '徐妇科'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-orthopedics', '骨科', [
+        buildReviewInstitutionTreeNode('doc-orth-1', '冯骨科'),
+        buildReviewInstitutionTreeNode('doc-orth-2', '沈骨科'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-dermatology', '皮肤科'),
+      buildReviewInstitutionTreeNode('dept-ophthalmology', '眼科', [
+        buildReviewInstitutionTreeNode('doc-eye-1', '韩眼科'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-ent', '耳鼻喉科'),
+      buildReviewInstitutionTreeNode('dept-emergency-room', '急诊科', [
+        buildReviewInstitutionTreeNode('doc-er-1', '马急诊'),
+        buildReviewInstitutionTreeNode('doc-er-2', '朱急诊'),
+      ]),
+    ]),
+    buildReviewInstitutionTreeNode('hospital-2', '蒋村社区卫生服务中心', [
+      buildReviewInstitutionTreeNode('dept-jc-tcm', '中医科', [
+        buildReviewInstitutionTreeNode('doc-jc-tcm-1', '唐熙'),
+      ]),
+      buildReviewInstitutionTreeNode('dept-jc-internal', '全科', [
+        buildReviewInstitutionTreeNode('doc-jc-int-1', '郭可新'),
+      ]),
+    ]),
+    buildReviewInstitutionTreeNode('hospital-3', '浙江省中医院', [
+      buildReviewInstitutionTreeNode('dept-zj-tcm', '中药房', [
+        buildReviewInstitutionTreeNode('doc-zj-1', '沈慧'),
+      ]),
+    ]),
+  ];
+
+/** 获取抽样机构树(当前为本地 mock,后期对接后端接口) */
+export function listReviewSamplingInstitutionTreeMethod() {
+  return Promise.resolve(
+    structuredClone(MOCK_REVIEW_SAMPLING_INSTITUTION_TREE),
+  );
+}
+
+const MOCK_REVIEW_TASKS: PrescriptionReviewModel.ReviewTask[] = [
+  {
+    id: 'task-1',
+    name: '2023年月度',
+    sampleTime: '2023-09-23 14:00:39',
+    sampleCount: 200,
+    prescriptionDateStart: '2023.01.01',
+    prescriptionDateEnd: '2023.01.30',
+    status: 'pending',
+    reviewedCount: 0,
+    passRate: 100,
+    samplingRule: {
+      name: '2023年月度',
+      institutions: ['蒋村社区卫生服务中心'],
+      prescriptionDateStart: '2023.01.01',
+      prescriptionDateEnd: '2023.01.30',
+      sampleCount: 200,
+      sampleRatio: 50,
+      excludeReviewed: true,
+    },
+    expertIds: ['1', '2'],
+  },
+  {
+    id: 'task-2',
+    name: '2022年度多药味处方点评',
+    sampleTime: '2023-09-23 14:00:39',
+    sampleCount: 56,
+    prescriptionDateStart: '2022.01.01',
+    prescriptionDateEnd: '2022.12.31',
+    status: 'in_progress',
+    reviewedCount: 20,
+    passRate: 10,
+    samplingRule: {
+      name: '2022年度多药味处方点评',
+      prescriptionDateStart: '2022.01.01',
+      prescriptionDateEnd: '2022.12.31',
+      herbCountMin: 15,
+      sampleCount: 56,
+      excludeReviewed: true,
+    },
+    expertIds: ['1'],
+  },
+  {
+    id: 'task-3',
+    name: '2022年度处方点评',
+    sampleTime: '2023-09-23 14:00:39',
+    sampleCount: 90,
+    prescriptionDateStart: '2022.01.01',
+    prescriptionDateEnd: '2022.12.31',
+    status: 'completed',
+    reviewedCount: 90,
+    passRate: 89,
+    samplingRule: {
+      name: '2022年度处方点评',
+      institutions: ['蒋村社区卫生服务中心', '同仁堂', '浙江省中医院'],
+      prescriptionDateStart: '2022.01.01',
+      prescriptionDateEnd: '2022.12.31',
+      sampleCount: 90,
+      excludeReviewed: true,
+    },
+    expertIds: ['1', '2', '3'],
+  },
+  {
+    id: 'task-4',
+    name: '2022年度处方点评归档',
+    sampleTime: '2023-09-23 14:00:39',
+    sampleCount: 167,
+    prescriptionDateStart: '2022.01.01',
+    prescriptionDateEnd: '2022.12.31',
+    status: 'archived',
+    reviewedCount: 167,
+    archiveTime: '2023-09-24 10:38:12',
+    passRate: 30,
+    samplingRule: {
+      name: '2022年度处方点评',
+      institutions: ['机构1', '机构2', '机构3'],
+      prescriptionDateStart: '2022.01.01',
+      prescriptionDateEnd: '2022.12.31',
+      amountMin: 300,
+      sampleCount: 167,
+      sampleRatio: 99,
+      excludeReviewed: true,
+    },
+    expertIds: ['1', '2'],
+  },
+];
+
+const MOCK_REVIEW_PRESCRIPTIONS: PrescriptionReviewModel.ReviewPrescriptionRecord[] =
+  [
+    {
+      id: 'rp-1',
+      taskId: 'task-3',
+      prescriptionNo: '202309238475',
+      institutionName: '蒋村社区卫生服务中心',
+      departmentName: '中医科',
+      doctorName: '张医生',
+      status: 'unqualified',
+      reviewExpert: '专家1',
+      patientName: '唐熙',
+      patientGender: '女',
+      patientAge: 48,
+      pregnancy: false,
+      lactation: false,
+      tcmDisease: '腰痛',
+      tcmSyndrome: '肝肾亏虚证',
+      treatmentPrinciple: '补益肝肾',
+      administrationMethod: '内服',
+      herbCount: 18,
+      doseCount: 7,
+      totalAmount: 234.01,
+      unitDoseAmount: 33.43,
+      herbs: [
+        '熟地黄 15g',
+        '山茱萸 15g',
+        '山药 15g',
+        '茯苓 15g',
+        '泽泻 10g',
+        '牡丹皮 10g',
+        '秦艽 15g',
+        '杜仲 15g',
+        '续断 15g',
+        '牛膝 15g',
+        '当归 12g',
+        '白芍 12g',
+        '川芎 10g',
+        '甘草 6g',
+        '陈皮 10g',
+        '半夏 10g',
+        '生姜 3片',
+        '大枣 3枚',
+      ],
+      reviewResult: {
+        qualified: false,
+        indicatorIds: [
+          'ind-1',
+          'ind-dose-exceed',
+          'ind-18fan',
+          'ind-child',
+          'ind-other',
+        ],
+        herbIndicatorMap: { 'ind-dose-exceed': ['秦艽'] },
+      },
+      auxiliaryReviewResult: {
+        qualified: false,
+        indicatorIds: ['ind-1', 'ind-2', 'ind-dose-exceed', 'ind-18fan'],
+        herbIndicatorMap: { 'ind-dose-exceed': ['秦艽'] },
+      },
+    },
+    {
+      id: 'rp-2',
+      taskId: 'task-3',
+      prescriptionNo: '202309238476',
+      institutionName: '同仁堂',
+      departmentName: '内科',
+      doctorName: '李医生',
+      status: 'pending',
+      patientName: '王明',
+      patientGender: '男',
+      patientAge: 55,
+      herbCount: 12,
+      doseCount: 5,
+      totalAmount: 156.5,
+      unitDoseAmount: 31.3,
+      herbs: ['黄芪 30g', '党参 15g', '白术 15g', '茯苓 15g'],
+    },
+    {
+      id: 'rp-3',
+      taskId: 'task-3',
+      prescriptionNo: '202309238477',
+      institutionName: '浙江省中医院',
+      departmentName: '针灸科',
+      doctorName: '王医生',
+      status: 'qualified',
+      reviewExpert: '专家2',
+      patientName: '赵丽',
+      patientGender: '女',
+      patientAge: 32,
+      herbCount: 10,
+      doseCount: 7,
+      totalAmount: 98.0,
+      unitDoseAmount: 14.0,
+      herbs: ['当归 10g', '川芎 10g', '白芍 10g'],
+      reviewResult: { qualified: true },
+    },
+  ];
+
+function filterReviewTasks(
+  items: PrescriptionReviewModel.ReviewTask[],
+  query?: {
+    name?: string;
+    sampleTimeEnd?: string;
+    sampleTimeStart?: string;
+    status?: PrescriptionReviewModel.ReviewTaskStatus;
+  },
+) {
+  let result = [...items];
+  if (query?.status) {
+    result = result.filter((item) => item.status === query.status);
+  }
+  if (query?.name?.trim()) {
+    const keyword = query.name.trim();
+    result = result.filter((item) => item.name.includes(keyword));
+  }
+  if (query?.sampleTimeStart) {
+    result = result.filter(
+      (item) => item.sampleTime >= query.sampleTimeStart!,
+    );
+  }
+  if (query?.sampleTimeEnd) {
+    result = result.filter((item) => item.sampleTime <= `${query.sampleTimeEnd} 23:59:59`);
+  }
+  return result;
+}
+
+export function getReviewTaskStatusLabel(
+  status: PrescriptionReviewModel.ReviewTaskStatus,
+) {
+  return REVIEW_TASK_STATUS_LABEL[status] ?? status;
+}
+
+/** 处方点评任务列表(当前为本地 mock) */
+export function listReviewTasksMethod(
+  page = 1,
+  size = 10,
+  query?: Parameters<typeof filterReviewTasks>[1],
+): Promise<TransformList<PrescriptionReviewModel.ReviewTask>> {
+  const filtered = filterReviewTasks(MOCK_REVIEW_TASKS, query);
+  const start = (page - 1) * size;
+  const items = filtered.slice(start, start + size);
+  return Promise.resolve({
+    items,
+    total: filtered.length,
+    data: { page, size, total: filtered.length },
+  });
+}
+
+/** 获取点评任务详情(当前为本地 mock) */
+export function getReviewTaskMethod(taskId: string) {
+  const task = MOCK_REVIEW_TASKS.find((item) => item.id === taskId);
+  if (!task) return Promise.reject(new Error('点评任务不存在'));
+  return Promise.resolve(task);
+}
+
+/** 创建抽样任务(当前为本地 mock) */
+export function createReviewTaskMethod(
+  data: PrescriptionReviewModel.ReviewSamplingRule,
+) {
+  const task: PrescriptionReviewModel.ReviewTask = {
+    id: `task-${Date.now()}`,
+    name: data.name,
+    sampleTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
+    sampleCount: data.sampleCount ?? 0,
+    prescriptionDateStart: data.prescriptionDateStart ?? '',
+    prescriptionDateEnd: data.prescriptionDateEnd ?? '',
+    status: 'pending',
+    reviewedCount: 0,
+    passRate: 100,
+    samplingRule: data,
+    expertIds: [],
+  };
+  MOCK_REVIEW_TASKS.unshift(task);
+  return Promise.resolve(task);
+}
+
+/** 归档点评任务(当前为本地 mock) */
+export function archiveReviewTaskMethod(taskId: string) {
+  const task = MOCK_REVIEW_TASKS.find((item) => item.id === taskId);
+  if (!task) return Promise.reject(new Error('点评任务不存在'));
+  if (task.status !== 'completed') {
+    return Promise.reject(new Error('仅点评完成的任务可归档'));
+  }
+  task.status = 'archived';
+  task.archiveTime = new Date().toISOString().slice(0, 19).replace('T', ' ');
+  return Promise.resolve(task);
+}
+
+/** 删除点评任务(当前为本地 mock) */
+export function deleteReviewTaskMethod(taskId: string) {
+  const index = MOCK_REVIEW_TASKS.findIndex((item) => item.id === taskId);
+  if (index === -1) return Promise.reject(new Error('点评任务不存在'));
+  MOCK_REVIEW_TASKS.splice(index, 1);
+  MOCK_REVIEW_PRESCRIPTIONS.splice(
+    0,
+    MOCK_REVIEW_PRESCRIPTIONS.length,
+    ...MOCK_REVIEW_PRESCRIPTIONS.filter((item) => item.taskId !== taskId),
+  );
+  return Promise.resolve(true);
+}
+
+/** 更新点评专家组(当前为本地 mock) */
+export function updateReviewTaskExpertsMethod(
+  taskId: string,
+  expertIds: string[],
+) {
+  const task = MOCK_REVIEW_TASKS.find((item) => item.id === taskId);
+  if (!task) return Promise.reject(new Error('点评任务不存在'));
+  task.expertIds = expertIds;
+  return Promise.resolve(task);
+}
+
+/** 启动智能辅助点评(当前为本地 mock) */
+export function startIntelligentReviewMethod(taskId: string) {
+  const task = MOCK_REVIEW_TASKS.find((item) => item.id === taskId);
+  if (!task) return Promise.reject(new Error('点评任务不存在'));
+  if (task.status === 'archived') {
+    return Promise.reject(new Error('已归档任务不可操作'));
+  }
+  task.status = 'in_progress';
+  return Promise.resolve(true);
+}
+
+/** 点评任务处方列表(当前为本地 mock) */
+export function listReviewPrescriptionsMethod(
+  taskId: string,
+  page = 1,
+  size = 10,
+  query?: {
+    departmentName?: string;
+    doctorName?: string;
+    institutionName?: string;
+    status?: string;
+  },
+) {
+  let items = MOCK_REVIEW_PRESCRIPTIONS.filter(
+    (item) => item.taskId === taskId,
+  );
+  if (query?.status) {
+    items = items.filter((item) => item.status === query.status);
+  }
+  if (query?.institutionName) {
+    items = items.filter(
+      (item) => item.institutionName === query.institutionName,
+    );
+  }
+  if (query?.departmentName) {
+    items = items.filter(
+      (item) => item.departmentName === query.departmentName,
+    );
+  }
+  if (query?.doctorName) {
+    items = items.filter((item) => item.doctorName === query.doctorName);
+  }
+  const start = (page - 1) * size;
+  return Promise.resolve({
+    items: items.slice(start, start + size),
+    total: items.length,
+    data: { page, size, total: items.length },
+  });
+}
+
+/** 获取处方点评详情(当前为本地 mock) */
+export function getReviewPrescriptionMethod(prescriptionId: string) {
+  const record = MOCK_REVIEW_PRESCRIPTIONS.find(
+    (item) => item.id === prescriptionId,
+  );
+  if (!record) return Promise.reject(new Error('处方不存在'));
+  return Promise.resolve(record);
+}
+
+/** 获取处方辅助点评结果(当前为本地 mock) */
+export function getAuxiliaryReviewResultMethod(prescriptionId: string) {
+  const record = MOCK_REVIEW_PRESCRIPTIONS.find(
+    (item) => item.id === prescriptionId,
+  );
+  if (!record) return Promise.reject(new Error('处方不存在'));
+  if (!record.auxiliaryReviewResult) {
+    return Promise.reject(new Error('暂无辅助点评结果'));
+  }
+  return Promise.resolve(record.auxiliaryReviewResult);
+}
+
+/** 保存处方点评结果(当前为本地 mock) */
+export function saveReviewPrescriptionResultMethod(
+  prescriptionId: string,
+  result: PrescriptionReviewModel.ReviewPrescriptionResult,
+) {
+  const record = MOCK_REVIEW_PRESCRIPTIONS.find(
+    (item) => item.id === prescriptionId,
+  );
+  if (!record) return Promise.reject(new Error('处方不存在'));
+  const task = MOCK_REVIEW_TASKS.find((item) => item.id === record.taskId);
+  if (task?.status === 'archived') {
+    return Promise.reject(new Error('已归档任务不可修改'));
+  }
+  const wasReviewed = record.status !== 'pending';
+  record.reviewResult = result;
+  record.status = result.qualified ? 'qualified' : 'unqualified';
+  if (task && !wasReviewed) {
+    task.reviewedCount += 1;
+    if (task.reviewedCount >= task.sampleCount) {
+      task.status = 'completed';
+    } else if (task.status === 'pending') {
+      task.status = 'in_progress';
+    }
+    const taskRecords = MOCK_REVIEW_PRESCRIPTIONS.filter(
+      (item) => item.taskId === task.id && item.status !== 'pending',
+    );
+    const qualified = taskRecords.filter(
+      (item) => item.status === 'qualified',
+    ).length;
+    task.passRate =
+      taskRecords.length > 0
+        ? Math.round((qualified / taskRecords.length) * 100)
+        : 100;
+  }
+  return Promise.resolve(record);
+}
+
+const MOCK_STATISTICS_PRESCRIPTION_DETAILS: PrescriptionReviewModel.ReviewStatisticsPrescriptionDetail[] =
+  [
+    {
+      id: 'stat-1',
+      prescriptionNo: '202309238475',
+      institutionName: '蒋村社区卫生服务中心',
+      departmentName: '全科医疗科',
+      doctorName: '唐熙',
+      pharmacistName: '崔红',
+      result: 'unqualified',
+      unqualifiedDetail: '剂量超标',
+      comment:
+        '处方整体用药基本合理,但秦艽用量超出常规范围,建议复核剂量。\n请结合患者实际情况调整用药方案。',
+      unitDoseAmount: 33.43,
+      patientName: '唐熙',
+      patientGender: '女',
+      patientAge: 48,
+      pregnancy: false,
+      lactation: false,
+      tcmDisease: '腰痛',
+      tcmSyndrome: '肝肾亏虚证',
+      treatmentPrinciple: '补益肝肾',
+      administrationMethod: '内服',
+      herbCount: 18,
+      doseCount: 7,
+      totalAmount: 234.01,
+      herbs: [
+        '熟地黄 15g',
+        '山茱萸 15g',
+        '山药 15g',
+        '茯苓 15g',
+        '泽泻 10g',
+        '牡丹皮 10g',
+        '秦艽 20g',
+        '杜仲 15g',
+        '续断 15g',
+        '牛膝 15g',
+        '当归 12g',
+        '白芍 12g',
+        '川芎 10g',
+        '甘草 6g',
+        '陈皮 10g',
+        '半夏 10g',
+        '生姜 3片',
+        '大枣 3枚',
+      ],
+      reviewExpert: '于沐',
+      reviewTime: '2023-09-23 14:00:39',
+      reviewResult: {
+        qualified: false,
+        indicatorIds: ['ind-dose-exceed'],
+        herbIndicatorMap: { 'ind-dose-exceed': ['秦艽'] },
+        comment:
+          '处方整体用药基本合理,但秦艽用量超出常规范围,建议复核剂量。\n请结合患者实际情况调整用药方案。',
+      },
+      herbIssueLabels: { 秦艽: '唐熙' },
+    },
+    {
+      id: 'stat-2',
+      prescriptionNo: '2023092323946',
+      institutionName: '同仁堂',
+      departmentName: '内科',
+      doctorName: '郭可新',
+      pharmacistName: '',
+      result: 'unqualified',
+      unqualifiedDetail: '病证禁忌',
+      comment: '',
+      unitDoseAmount: 14,
+      patientName: '王明',
+      patientGender: '男',
+      patientAge: 55,
+      pregnancy: false,
+      lactation: false,
+      tcmDisease: '感冒',
+      tcmSyndrome: '风寒束表证',
+      treatmentPrinciple: '辛温解表',
+      administrationMethod: '内服',
+      herbCount: 12,
+      doseCount: 5,
+      totalAmount: 70,
+      herbs: ['黄芪 30g', '党参 15g', '白术 15g', '茯苓 15g'],
+      reviewExpert: '于沐',
+      reviewTime: '2023-09-23 14:05:12',
+      reviewResult: {
+        qualified: false,
+        indicatorIds: ['ind-1'],
+        comment: '',
+      },
+    },
+    {
+      id: 'stat-3',
+      prescriptionNo: '2023092323947',
+      institutionName: '浙江省中医院',
+      departmentName: '针灸科',
+      doctorName: '沈慧',
+      pharmacistName: '李药师',
+      result: 'unqualified',
+      unqualifiedDetail: '超均贴限价',
+      comment: '单剂金额偏高,建议优化处方结构。',
+      unitDoseAmount: 45,
+      patientName: '赵丽',
+      patientGender: '女',
+      patientAge: 32,
+      pregnancy: false,
+      lactation: false,
+      tcmDisease: '头痛',
+      tcmSyndrome: '肝阳上亢证',
+      treatmentPrinciple: '平肝潜阳',
+      administrationMethod: '内服',
+      herbCount: 10,
+      doseCount: 7,
+      totalAmount: 315,
+      herbs: ['当归 10g', '川芎 10g', '白芍 10g', '天麻 10g'],
+      reviewExpert: '专家2',
+      reviewTime: '2023-09-23 15:20:00',
+      reviewResult: {
+        qualified: false,
+        indicatorIds: ['ind-price-limit'],
+        comment: '单剂金额偏高,建议优化处方结构。',
+      },
+    },
+  ];
+
+const MOCK_STATISTICS_PRESCRIPTIONS: PrescriptionReviewModel.ReviewStatisticsPrescriptionRow[] =
+  MOCK_STATISTICS_PRESCRIPTION_DETAILS.map(
+    ({
+      administrationMethod,
+      departmentName,
+      doseCount,
+      herbCount,
+      herbIssueLabels,
+      herbs,
+      lactation,
+      patientAge,
+      patientGender,
+      patientName,
+      pregnancy,
+      reviewExpert,
+      reviewResult,
+      reviewTime,
+      tcmDisease,
+      tcmSyndrome,
+      totalAmount,
+      treatmentPrinciple,
+      ...row
+    }) => row,
+  );
+
+const MOCK_STATISTICS_DETAIL_INDICATORS: PrescriptionReviewModel.ReviewIndicator[] =
+  [
+    {
+      id: 'ind-1',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '病证禁忌',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-2',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '慎用',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-3',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '禁用',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-4',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '忌用',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-7',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '点评项1',
+      source: 'custom',
+      associatedChineseMedicine: true,
+      createUser: '陆长林',
+      createTime: '2023-09-23 15:29:38',
+      status: 0,
+    },
+    {
+      id: 'ind-8',
+      categoryId: 'cat-1',
+      categoryName: '适应症',
+      name: '点评项2',
+      source: 'custom',
+      associatedChineseMedicine: true,
+      createUser: '陆长林',
+      createTime: '2023-09-23 15:29:38',
+      status: 0,
+    },
+    {
+      id: 'ind-dose-low',
+      categoryId: 'cat-2',
+      categoryName: '用法用量',
+      name: '剂量不足',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-dose-exceed',
+      categoryId: 'cat-2',
+      categoryName: '用法用量',
+      name: '剂量超标',
+      source: 'system',
+      associatedChineseMedicine: true,
+      status: 1,
+    },
+    {
+      id: 'ind-admin-wrong',
+      categoryId: 'cat-2',
+      categoryName: '用法用量',
+      name: '服法错误',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-decoct',
+      categoryId: 'cat-2',
+      categoryName: '用法用量',
+      name: '特殊煎法',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-18fan',
+      categoryId: 'cat-3',
+      categoryName: '配伍禁忌',
+      name: '十八反',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-19wei',
+      categoryId: 'cat-3',
+      categoryName: '配伍禁忌',
+      name: '十九畏',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-incompatible',
+      categoryId: 'cat-3',
+      categoryName: '配伍禁忌',
+      name: '不宜同用',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-food',
+      categoryId: 'cat-3',
+      categoryName: '配伍禁忌',
+      name: '忌用慎用的食物',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-child',
+      categoryId: 'cat-4',
+      categoryName: '特殊人群慎禁忌',
+      name: '儿童',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-elder',
+      categoryId: 'cat-4',
+      categoryName: '特殊人群慎禁忌',
+      name: '老年人',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-pregnant',
+      categoryId: 'cat-4',
+      categoryName: '特殊人群慎禁忌',
+      name: '孕妇',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-lactation',
+      categoryId: 'cat-4',
+      categoryName: '特殊人群慎禁忌',
+      name: '哺乳期妇女',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-other',
+      categoryId: 'cat-5',
+      categoryName: '其他禁忌',
+      name: '其他禁忌',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+    {
+      id: 'ind-price-limit',
+      categoryId: 'cat-6',
+      categoryName: '超均贴限价',
+      name: '超均贴限价',
+      source: 'system',
+      associatedChineseMedicine: false,
+      status: 1,
+    },
+  ];
+
+function buildReviewStatisticsCostTree(): PrescriptionReviewModel.ReviewStatisticsCostRow[] {
+  return [
+    {
+      id: 'cost-1',
+      name: '蒋村社区卫生服务中心',
+      level: 'institution',
+      prescriptionCount: 233,
+      totalDoseCount: 1398,
+      totalAmount: 47392.2,
+      avgDoseCost: 33.9,
+      exceedLimit: true,
+      children: [
+        {
+          id: 'cost-1-dept-1',
+          name: '中医科',
+          level: 'department',
+          prescriptionCount: 120,
+          totalDoseCount: 720,
+          totalAmount: 24336,
+          avgDoseCost: 33.8,
+          exceedLimit: true,
+          children: [
+            {
+              id: 'cost-1-dept-1-doc-1',
+              name: '康毛理',
+              level: 'doctor',
+              prescriptionCount: 68,
+              totalDoseCount: 408,
+              totalAmount: 13872,
+              avgDoseCost: 34,
+              exceedLimit: true,
+              children: [
+                {
+                  id: 'cost-1-dept-1-doc-1-rx-1',
+                  name: '2023092323945',
+                  level: 'prescription',
+                  prescriptionCount: 1,
+                  totalDoseCount: 7,
+                  totalAmount: 208.6,
+                  avgDoseCost: 29.8,
+                },
+                {
+                  id: 'cost-1-dept-1-doc-1-rx-2',
+                  name: '2023092323948',
+                  level: 'prescription',
+                  prescriptionCount: 1,
+                  totalDoseCount: 6,
+                  totalAmount: 210,
+                  avgDoseCost: 35,
+                  exceedLimit: true,
+                },
+              ],
+            },
+            {
+              id: 'cost-1-dept-1-doc-2',
+              name: '张医生',
+              level: 'doctor',
+              prescriptionCount: 52,
+              totalDoseCount: 312,
+              totalAmount: 10464,
+              avgDoseCost: 33.5,
+              exceedLimit: true,
+            },
+          ],
+        },
+        {
+          id: 'cost-1-dept-2',
+          name: '全科',
+          level: 'department',
+          prescriptionCount: 113,
+          totalDoseCount: 678,
+          totalAmount: 23056.2,
+          avgDoseCost: 34,
+          exceedLimit: true,
+        },
+      ],
+    },
+    {
+      id: 'cost-2',
+      name: '同仁堂',
+      level: 'institution',
+      prescriptionCount: 455,
+      totalDoseCount: 3185,
+      totalAmount: 103512.5,
+      avgDoseCost: 32.5,
+      exceedLimit: true,
+      children: [
+        {
+          id: 'cost-2-dept-1',
+          name: '内科',
+          level: 'department',
+          prescriptionCount: 255,
+          totalDoseCount: 1785,
+          totalAmount: 58012.5,
+          avgDoseCost: 32.5,
+          exceedLimit: true,
+          children: [
+            {
+              id: 'cost-2-dept-1-doc-1',
+              name: '郭可新',
+              level: 'doctor',
+              prescriptionCount: 130,
+              totalDoseCount: 910,
+              totalAmount: 29575,
+              avgDoseCost: 32.5,
+              exceedLimit: true,
+              children: [
+                {
+                  id: 'cost-2-dept-1-doc-1-rx-1',
+                  name: '2023092323946',
+                  level: 'prescription',
+                  prescriptionCount: 1,
+                  totalDoseCount: 5,
+                  totalAmount: 70,
+                  avgDoseCost: 14,
+                },
+              ],
+            },
+          ],
+        },
+        {
+          id: 'cost-2-dept-2',
+          name: '中医科',
+          level: 'department',
+          prescriptionCount: 200,
+          totalDoseCount: 1400,
+          totalAmount: 45500,
+          avgDoseCost: 32.5,
+          exceedLimit: true,
+        },
+      ],
+    },
+    {
+      id: 'cost-3',
+      name: '浙江省中医院',
+      level: 'institution',
+      prescriptionCount: 3454,
+      totalDoseCount: 22451,
+      totalAmount: 713941.8,
+      avgDoseCost: 31.8,
+      exceedLimit: true,
+      children: [
+        {
+          id: 'cost-3-dept-1',
+          name: '针灸科',
+          level: 'department',
+          prescriptionCount: 1680,
+          totalDoseCount: 10920,
+          totalAmount: 347256,
+          avgDoseCost: 31.8,
+          exceedLimit: true,
+          children: [
+            {
+              id: 'cost-3-dept-1-doc-1',
+              name: '沈慧',
+              level: 'doctor',
+              prescriptionCount: 820,
+              totalDoseCount: 5330,
+              totalAmount: 169494,
+              avgDoseCost: 31.8,
+              exceedLimit: true,
+              children: [
+                {
+                  id: 'cost-3-dept-1-doc-1-rx-1',
+                  name: '2023092323947',
+                  level: 'prescription',
+                  prescriptionCount: 1,
+                  totalDoseCount: 7,
+                  totalAmount: 315,
+                  avgDoseCost: 45,
+                  exceedLimit: true,
+                },
+              ],
+            },
+          ],
+        },
+        {
+          id: 'cost-3-dept-2',
+          name: '骨伤科',
+          level: 'department',
+          prescriptionCount: 1774,
+          totalDoseCount: 11531,
+          totalAmount: 366685.8,
+          avgDoseCost: 31.8,
+          exceedLimit: true,
+        },
+      ],
+    },
+  ];
+}
+
+/** 统计分析 - 处方点评表(当前为本地 mock) */
+export function listReviewStatisticsPrescriptionsMethod(taskId: string) {
+  void taskId;
+  return Promise.resolve([...MOCK_STATISTICS_PRESCRIPTIONS]);
+}
+
+/** 统计分析 - 处方点评详情(当前为本地 mock) */
+export function getReviewStatisticsPrescriptionDetailMethod(
+  taskId: string,
+  prescriptionId: string,
+) {
+  void taskId;
+  const detail = MOCK_STATISTICS_PRESCRIPTION_DETAILS.find(
+    (item) => item.id === prescriptionId,
+  );
+  if (!detail) {
+    return Promise.reject(new Error('处方不存在'));
+  }
+  return Promise.resolve(detail);
+}
+
+/** 统计分析 - 点评详情指标分类(当前为本地 mock) */
+export function listReviewStatisticsDetailIndicatorCategoriesMethod() {
+  const categoryIds = [
+    ...new Set(MOCK_STATISTICS_DETAIL_INDICATORS.map((item) => item.categoryId)),
+  ];
+  return Promise.resolve(
+    MOCK_INDICATOR_CATEGORIES.filter((item) => categoryIds.includes(item.id)),
+  );
+}
+
+/** 统计分析 - 点评详情指标列表(当前为本地 mock) */
+export function listReviewStatisticsDetailIndicatorsByCategoryMethod(
+  categoryId: string,
+) {
+  return Promise.resolve(
+    MOCK_STATISTICS_DETAIL_INDICATORS.filter(
+      (item) => item.categoryId === categoryId,
+    ),
+  );
+}
+
+/** 统计分析 - 均剂费用汇报(当前为本地 mock,树形:机构 → 科室 → 医生 → 处方号) */
+export function listReviewStatisticsCostMethod(taskId: string) {
+  void taskId;
+  return Promise.resolve(buildReviewStatisticsCostTree());
+}
+
+/** 统计分析 - 点评结果汇报(当前为本地 mock) */
+export function getReviewStatisticsSummaryMethod(
+  taskId: string,
+): Promise<PrescriptionReviewModel.ReviewStatisticsSummary> {
+  void taskId;
+  return Promise.resolve({
+    sampledPrescriptionCount: 234,
+    sampledInstitutionCount: 5,
+    qualifiedCount: 204,
+    qualifiedRate: 87.18,
+    avgDoseCost: 19.28,
+    exceedLimitInstitutionCount: 2,
+    sampleRatio: 26,
+    sampledDoctorCount: 25,
+    unqualifiedCount: 30,
+    unqualifiedRate: 12.82,
+    exceedLimitDoseCount: 35,
+    exceedLimitDoseRate: 5.18,
+    exceedLimitDoctorCount: 2,
+    topUnqualifiedReasons: [
+      '病证禁忌',
+      '剂量超标',
+      '药味数超标',
+      '不宜同用',
+      '超均贴限价',
+    ],
+    topExcessDosageHerbs: ['秦艽', '知柏', '甘草', '黄芩', '当归'],
+  });
+}

+ 5 - 1
apps/smart-pharmacy/src/api/model/menu.ts

@@ -48,6 +48,10 @@ export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
     id: '2500',
     id: '2500',
     label: '处方点评',
     label: '处方点评',
     children: [
     children: [
+      { id: '2503', label: '处方点评' },
+      { id: '2504', label: '处方点评详情' },
+      { id: '2505', label: '统计分析' },
+      { id: '2506', label: '点评详情' },
       { id: '2501', label: '点评专家' },
       { id: '2501', label: '点评专家' },
       { id: '2502', label: '点评指标库' },
       { id: '2502', label: '点评指标库' },
     ],
     ],
@@ -63,7 +67,7 @@ export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
 ];
 ];
 
 
 const HARDCODED_MENU_ROOT_IDS = ['2500', '2600'];
 const HARDCODED_MENU_ROOT_IDS = ['2500', '2600'];
-const HARDCODED_MENU_LEAF_IDS = ['2415', '2416'];
+const HARDCODED_MENU_LEAF_IDS = ['2415', '2416', '2504', '2505', '2506'];
 
 
 /** 将本地写死菜单合并进后端 treeselect 结果 */
 /** 将本地写死菜单合并进后端 treeselect 结果 */
 export function mergeHardcodedMenuTree(
 export function mergeHardcodedMenuTree(

+ 1 - 1
apps/smart-pharmacy/src/views/patient-evaluation/efficacy/data.ts

@@ -14,7 +14,7 @@ export function useEfficacySearchFormSchema(): VbenFormSchema[] {
       label: '日期范围',
       label: '日期范围',
       componentProps: {
       componentProps: {
         valueFormat: 'YYYY-MM-DD',
         valueFormat: 'YYYY-MM-DD',
-        style: { width: '100%' },
+        style: { width: '30%' },
         placeholder: ['开始日期', '结束日期'],
         placeholder: ['开始日期', '结束日期'],
         inputReadOnly: true,
         inputReadOnly: true,
       },
       },

+ 1 - 1
apps/smart-pharmacy/src/views/patient-evaluation/satisfaction/data.ts

@@ -14,7 +14,7 @@ export function useSatisfactionSearchFormSchema(): VbenFormSchema[] {
       label: '日期范围',
       label: '日期范围',
       componentProps: {
       componentProps: {
         valueFormat: 'YYYY-MM-DD',
         valueFormat: 'YYYY-MM-DD',
-        style: { width: '100%' },
+        style: { width: '30%' },
         placeholder: ['开始日期', '结束日期'],
         placeholder: ['开始日期', '结束日期'],
         inputReadOnly: true,
         inputReadOnly: true,
       },
       },

+ 247 - 0
apps/smart-pharmacy/src/views/prescription-review/task/data.ts

@@ -0,0 +1,247 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { PrescriptionReviewModel } from '#/api/method/prescription-review';
+
+import { getReviewTaskStatusLabel } from '#/api';
+import { listReviewSamplingInstitutionTreeMethod } from '#/api/method/prescription-review';
+
+const REVIEW_STATUS_OPTIONS: {
+  label: string;
+  value: PrescriptionReviewModel.ReviewTaskStatus;
+}[] = [
+  { label: '待点评', value: 'pending' },
+  { label: '点评进行中', value: 'in_progress' },
+  { label: '点评完成', value: 'completed' },
+  { label: '已归档', value: 'archived' },
+];
+
+export function useReviewTaskSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'RangePicker',
+      componentProps: {
+        class: 'w-full',
+        placeholder: ['开始日期', '结束日期'],
+        valueFormat: 'YYYY-MM-DD',
+      },
+      fieldName: 'sampleTimeRange',
+      label: '抽样时间',
+    },
+    {
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        class: 'w-full',
+        options: REVIEW_STATUS_OPTIONS,
+        placeholder: '请选择',
+      },
+      fieldName: 'status',
+      label: '点评状态',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'name',
+      label: '抽样名称',
+    },
+  ];
+}
+
+export function useReviewTaskTableColumns(): VxeTableGridOptions<PrescriptionReviewModel.ReviewTask>['columns'] {
+  return [
+    {
+      type: 'seq',
+      title: '序号',
+      width: 70,
+    },
+    {
+      field: 'name',
+      minWidth: 180,
+      title: '抽样名称',
+    },
+    {
+      field: 'sampleTime',
+      minWidth: 160,
+      sortable: true,
+      title: '抽样时间',
+    },
+    {
+      field: 'sampleCount',
+      minWidth: 90,
+      title: '抽样数',
+    },
+    {
+      field: 'prescriptionDateRange',
+      minWidth: 200,
+      slots: {
+        default: ({ row }) =>
+          row.prescriptionDateStart && row.prescriptionDateEnd
+            ? `${row.prescriptionDateStart} - ${row.prescriptionDateEnd}`
+            : '/',
+      },
+      title: '处方日期区间',
+    },
+    {
+      field: 'status',
+      minWidth: 110,
+      slots: {
+        default: ({ row }) => getReviewTaskStatusLabel(row.status),
+      },
+      title: '点评状态',
+    },
+    {
+      field: 'reviewProgress',
+      minWidth: 150,
+      slots: {
+        default: ({ row }) => `${row.reviewedCount}/${row.sampleCount}`,
+      },
+      title: '点评进度(已完成/抽样数)',
+    },
+    {
+      field: 'archiveTime',
+      minWidth: 160,
+      slots: {
+        default: ({ row }) => row.archiveTime ?? '/',
+      },
+      title: '归档时间',
+    },
+    {
+      field: 'passRate',
+      minWidth: 90,
+      slots: {
+        default: ({ row }) => `${row.passRate}%`,
+      },
+      title: '合格率',
+    },
+    {
+      align: 'center',
+      field: 'operation',
+      fixed: 'right',
+      slots: { default: 'operation' },
+      title: '操作',
+      width: 150,
+    },
+  ];
+}
+
+export function useReviewSamplingFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'name',
+      label: '抽样名称',
+      rules: 'required',
+    },
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        allowClear: true,
+        api: listReviewSamplingInstitutionTreeMethod,
+        childrenField: 'children',
+        class: 'w-full',
+        dropdownStyle: { maxHeight: 400, overflow: 'auto' },
+        labelField: 'name',
+        maxTagCount: 'responsive',
+        multiple: true,
+        placeholder: '请选择',
+        showCheckedStrategy: 'SHOW_PARENT',
+        showSearch: true,
+        treeCheckable: true,
+        treeDefaultExpandAll: true,
+        treeNodeFilterProp: 'label',
+        valueField: 'pid',
+      },
+      fieldName: 'institutionIds',
+      help: '未选择默认全部机构',
+      label: '医疗机构/科室/医生',
+    },
+    {
+      component: 'RangePicker',
+      componentProps: {
+        class: 'w-full',
+        placeholder: ['开始日期', '结束日期'],
+        valueFormat: 'YYYY-MM-DD',
+      },
+      fieldName: 'prescriptionDateRange',
+      help: '未选择默认全部日期的处方',
+      label: '处方日期',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        placeholder: '请输入下限',
+      },
+      fieldName: 'herbCountMin',
+      label: '处方药味数',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        placeholder: '请输入上限',
+      },
+      fieldName: 'herbCountMax',
+      label: ' ',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        placeholder: '请输入下限',
+      },
+      fieldName: 'amountMin',
+      label: '药品金额范围',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 0,
+        placeholder: '请输入上限',
+      },
+      fieldName: 'amountMax',
+      label: ' ',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        class: 'w-full',
+        min: 1,
+        placeholder: '请输入抽取的处方数量',
+      },
+      fieldName: 'sampleCount',
+      label: '抽取处方数',
+    },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '%',
+        class: 'w-full',
+        max: 100,
+        min: 0,
+        placeholder: '请输入0-100的数值',
+      },
+      fieldName: 'sampleRatio',
+      help: '抽取处方数和抽取处方比例,至少输入一项。当两项同时填写时,以最终结果少的处方数量抽取',
+      label: '抽取处方比例',
+    },
+    {
+      component: 'Checkbox',
+      defaultValue: true,
+      fieldName: 'excludeReviewed',
+      label: ' ',
+      renderComponentContent: () => ({
+        default: () => '不抽取已点评处方',
+      }),
+    },
+  ];
+}

+ 232 - 0
apps/smart-pharmacy/src/views/prescription-review/task/list.vue

@@ -0,0 +1,232 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { PrescriptionReviewModel } from '#/api';
+
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Button, Dropdown, Menu, message } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  archiveReviewTaskMethod,
+  deleteReviewTaskMethod,
+  listReviewTasksMethod,
+  startIntelligentReviewMethod,
+} from '#/api';
+
+import {
+  useReviewTaskSearchFormSchema,
+  useReviewTaskTableColumns,
+} from './data';
+import ConfirmModal from './modules/confirm-modal.vue';
+import ExpertGroupModal from './modules/expert-group-modal.vue';
+import SamplingModal from './modules/sampling-modal.vue';
+import SamplingRulesModal from './modules/sampling-rules-modal.vue';
+
+const router = useRouter();
+
+const [ConfirmModalComp, confirmModalApi] = useVbenModal({
+  connectedComponent: ConfirmModal,
+  destroyOnClose: true,
+});
+
+const [ExpertGroupModalComp, expertGroupModalApi] = useVbenModal({
+  connectedComponent: ExpertGroupModal,
+  destroyOnClose: true,
+});
+
+const [SamplingRulesModalComp, samplingRulesModalApi] = useVbenModal({
+  connectedComponent: SamplingRulesModal,
+  destroyOnClose: true,
+});
+
+const [SamplingModalComp, samplingModalApi] = useVbenModal({
+  connectedComponent: SamplingModal,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useReviewTaskSearchFormSchema(),
+    submitOnChange: false,
+    wrapperClass: 'grid-cols-3 review-task-search-form',
+  },
+  gridOptions: {
+    columns: useReviewTaskTableColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query({ page }, formValues) {
+          const [sampleTimeStart, sampleTimeEnd] =
+            formValues.sampleTimeRange ?? [];
+          return listReviewTasksMethod(page.currentPage, page.pageSize, {
+            name: formValues.name,
+            sampleTimeEnd,
+            sampleTimeStart,
+            status: formValues.status,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+    },
+    stripe: true,
+  } as VxeTableGridOptions<PrescriptionReviewModel.ReviewTask>,
+});
+
+function onRefresh() {
+  gridApi.query();
+}
+
+function onReview(row: PrescriptionReviewModel.ReviewTask) {
+  router.push(`/prescription-review/task/review/${row.id}`);
+}
+
+function onStatistics(row: PrescriptionReviewModel.ReviewTask) {
+  router.push(`/prescription-review/task/statistics/${row.id}`);
+}
+
+function onExpertGroup(row: PrescriptionReviewModel.ReviewTask) {
+  expertGroupModalApi.setData(row).open();
+}
+
+function onSamplingRules(row: PrescriptionReviewModel.ReviewTask) {
+  samplingRulesModalApi.setData(row).open();
+}
+
+function onArchive(row: PrescriptionReviewModel.ReviewTask) {
+  confirmModalApi
+    .setData({
+      content: '归档后,点评结果不能再做修改,请确认',
+      onConfirm: async () => {
+        try {
+          await archiveReviewTaskMethod(row.id);
+          message.success('归档成功');
+          onRefresh();
+        } catch (error: any) {
+          message.error(error.message || '归档失败');
+          throw error;
+        }
+      },
+      title: '归档提醒',
+    })
+    .open();
+}
+
+function onIntelligentReview(row: PrescriptionReviewModel.ReviewTask) {
+  confirmModalApi
+    .setData({
+      content:
+        '点击"确认"后,系统将对本批次处方启动智能辅助点评\n智能辅助点评完成后,无预警项问题的处方将批量完成合格点评',
+      onConfirm: async () => {
+        try {
+          await startIntelligentReviewMethod(row.id);
+          message.success('智能辅助点评已启动');
+          onRefresh();
+        } catch (error: any) {
+          message.error(error.message || '操作失败');
+          throw error;
+        }
+      },
+      title: '智能辅助点评',
+    })
+    .open();
+}
+
+function onDelete(row: PrescriptionReviewModel.ReviewTask) {
+  confirmModalApi
+    .setData({
+      content: '删除后,本条记录将被删除,同时,删除点评结果,请确认',
+      onConfirm: async () => {
+        try {
+          await deleteReviewTaskMethod(row.id);
+          message.success('删除成功');
+          onRefresh();
+        } catch (error: any) {
+          message.error(error.message || '删除失败');
+          throw error;
+        }
+      },
+      title: '删除提醒',
+    })
+    .open();
+}
+
+function onMoreAction(
+  key: string,
+  row: PrescriptionReviewModel.ReviewTask,
+) {
+  switch (key) {
+    case 'archive':
+      onArchive(row);
+      break;
+    case 'delete':
+      onDelete(row);
+      break;
+    case 'expert':
+      onExpertGroup(row);
+      break;
+    case 'intelligent':
+      onIntelligentReview(row);
+      break;
+    case 'rules':
+      onSamplingRules(row);
+      break;
+    case 'statistics':
+      onStatistics(row);
+      break;
+  }
+}
+
+function canArchive(row: PrescriptionReviewModel.ReviewTask) {
+  return row.status === 'completed';
+}
+</script>
+
+<template>
+  <Page auto-content-height class="review-task-page">
+    <ConfirmModalComp />
+    <ExpertGroupModalComp @success="onRefresh" />
+    <SamplingRulesModalComp />
+    <SamplingModalComp @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button type="primary" @click="samplingModalApi.open()">抽样</Button>
+      </template>
+      <template #operation="{ row }">
+        <div class="flex items-center justify-center gap-1">
+          <Button size="small" type="link" @click="onReview(row)">点评</Button>
+          <Dropdown :trigger="['click']">
+            <Button size="small" type="link">
+              更多
+              <IconifyIcon class="ml-0.5 size-3" icon="mdi:chevron-down" />
+            </Button>
+            <template #overlay>
+              <Menu @click="({ key }) => onMoreAction(String(key), row)">
+                <Menu.Item :disabled="!canArchive(row)" key="archive">
+                  归档
+                </Menu.Item>
+                <Menu.Item key="statistics">统计分析</Menu.Item>
+                <Menu.Item key="expert">点评专家组</Menu.Item>
+                <Menu.Item key="rules">抽样规则</Menu.Item>
+                <Menu.Item key="intelligent">智能辅助点评</Menu.Item>
+                <Menu.Item key="delete">删除</Menu.Item>
+              </Menu>
+            </template>
+          </Dropdown>
+        </div>
+      </template>
+    </Grid>
+  </Page>
+</template>
+
+<style scoped>
+.review-task-page :deep(.review-task-search-form) {
+  margin-bottom: 0;
+}
+</style>

+ 217 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/auxiliary-review-modal.vue

@@ -0,0 +1,217 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Checkbox, Select } from 'ant-design-vue';
+
+interface CategoryGroup {
+  categoryId: string;
+  categoryName: string;
+  indicators: PrescriptionReviewModel.ReviewIndicator[];
+}
+
+export interface AuxiliaryReviewModalData {
+  indicatorGroups: CategoryGroup[];
+  result: PrescriptionReviewModel.ReviewPrescriptionResult;
+  herbOptions: { label: string; value: string }[];
+  onApply?: (result: PrescriptionReviewModel.ReviewPrescriptionResult) => void;
+}
+
+const EDITABLE_CATEGORY_IDS = new Set(['cat-1', 'cat-2', 'cat-3']);
+
+const displayGroups = ref<CategoryGroup[]>([]);
+const localIndicatorIds = ref<string[]>([]);
+const localHerbMap = ref<Record<string, string[]>>({});
+const herbOptions = ref<{ label: string; value: string }[]>([]);
+
+const [Modal, modalApi] = useVbenModal({
+  cancelText: '关闭',
+  centered: true,
+  class: 'auxiliary-review-modal w-[640px]',
+  confirmText: '应用',
+  footerClass: 'auxiliary-review-modal__footer',
+  fullscreenButton: false,
+  headerClass: 'auxiliary-review-modal__header',
+  showCancelButton: true,
+  showConfirmButton: true,
+  onOpenChange(isOpen) {
+    if (!isOpen) return;
+    const data = modalApi.getData<AuxiliaryReviewModalData>();
+    if (!data) return;
+    const auxiliaryIds = new Set(data.result.indicatorIds ?? []);
+    displayGroups.value = data.indicatorGroups
+      .map((group) => ({
+        ...group,
+        indicators: group.indicators.filter((item) => auxiliaryIds.has(item.id)),
+      }))
+      .filter((group) => group.indicators.length > 0);
+    localIndicatorIds.value = [...(data.result.indicatorIds ?? [])];
+    localHerbMap.value = { ...(data.result.herbIndicatorMap ?? {}) };
+    herbOptions.value = data.herbOptions;
+  },
+  onConfirm() {
+    const data = modalApi.getData<AuxiliaryReviewModalData>();
+    data?.onApply?.({
+      qualified: false,
+      indicatorIds: [...localIndicatorIds.value],
+      herbIndicatorMap: { ...localHerbMap.value },
+    });
+    modalApi.close();
+  },
+});
+
+function isCategoryEditable(categoryId: string) {
+  return EDITABLE_CATEGORY_IDS.has(categoryId);
+}
+
+function isIndicatorChecked(indicatorId: string) {
+  return localIndicatorIds.value.includes(indicatorId);
+}
+
+function needsHerbSelect(indicator: PrescriptionReviewModel.ReviewIndicator) {
+  return indicator.categoryId === 'cat-2' || indicator.associatedChineseMedicine;
+}
+
+function onIndicatorChange(indicatorId: string, checked: boolean) {
+  if (checked) {
+    if (!localIndicatorIds.value.includes(indicatorId)) {
+      localIndicatorIds.value.push(indicatorId);
+    }
+    return;
+  }
+  localIndicatorIds.value = localIndicatorIds.value.filter(
+    (id) => id !== indicatorId,
+  );
+  delete localHerbMap.value[indicatorId];
+}
+
+function onHerbChange(indicatorId: string, herbs: string[]) {
+  localHerbMap.value[indicatorId] = herbs;
+}
+</script>
+
+<template>
+  <Modal title="辅助点评结果">
+    <div class="auxiliary-review-modal__body">
+      <div
+        v-for="group in displayGroups"
+        :key="group.categoryId"
+        class="indicator-group"
+      >
+        <div class="group-title">{{ group.categoryName }}</div>
+        <div class="group-items">
+          <div
+            v-for="indicator in group.indicators"
+            :key="indicator.id"
+            class="indicator-item"
+          >
+            <Checkbox
+              :checked="isIndicatorChecked(indicator.id)"
+              :disabled="!isCategoryEditable(group.categoryId)"
+              @change="
+                (e) =>
+                  onIndicatorChange(indicator.id, e.target.checked ?? false)
+              "
+            >
+              {{ indicator.name }}
+            </Checkbox>
+            <Select
+              v-if="
+                isIndicatorChecked(indicator.id) && needsHerbSelect(indicator)
+              "
+              :disabled="!isCategoryEditable(group.categoryId)"
+              :options="herbOptions"
+              :value="localHerbMap[indicator.id] ?? []"
+              class="herb-select"
+              mode="multiple"
+              size="small"
+              @change="(value) => onHerbChange(indicator.id, value as string[])"
+            />
+          </div>
+        </div>
+      </div>
+      <div v-if="displayGroups.length === 0" class="empty-tip">
+        暂无辅助点评结果
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style scoped>
+.auxiliary-review-modal__body {
+  max-height: 480px;
+  padding: 16px 20px;
+  overflow-y: auto;
+}
+
+.indicator-group {
+  margin-bottom: 16px;
+}
+
+.group-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+}
+
+.group-items {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 16px;
+}
+
+.indicator-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.herb-select {
+  width: 140px;
+}
+
+.empty-tip {
+  padding: 24px;
+  text-align: center;
+  color: rgb(0 0 0 / 45%);
+}
+
+:deep(.auxiliary-review-modal__header) {
+  padding: 12px 20px;
+  color: #fff;
+  background-color: #1677ff;
+}
+
+:deep(.auxiliary-review-modal__header h2) {
+  font-size: 16px;
+  font-weight: 500;
+  color: #fff;
+}
+
+:deep(.auxiliary-review-modal__header button) {
+  color: #fff;
+  opacity: 0.85;
+}
+
+:deep(.auxiliary-review-modal__footer) {
+  gap: 12px;
+  justify-content: center;
+  padding: 16px 24px 24px;
+  border-top: none;
+}
+
+:deep(.auxiliary-review-modal__footer button:first-child) {
+  min-width: 88px;
+  color: #1677ff;
+  background: #fff;
+  border-color: #1677ff;
+}
+
+:deep(.auxiliary-review-modal__footer button:last-child) {
+  min-width: 88px;
+  background-color: #1677ff;
+  border-color: #1677ff;
+}
+</style>

+ 93 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/confirm-modal.vue

@@ -0,0 +1,93 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+export interface ConfirmModalData {
+  content?: string;
+  onConfirm?: () => Promise<void> | void;
+  title?: string;
+}
+
+const emit = defineEmits<{
+  success: [];
+}>();
+
+const [Modal, modalApi] = useVbenModal({
+  cancelText: '取消',
+  centered: true,
+  class: 'review-confirm-modal w-[480px]',
+  confirmText: '确认',
+  footerClass: 'review-confirm-modal__footer',
+  fullscreenButton: false,
+  headerClass: 'review-confirm-modal__header',
+  async onConfirm() {
+    const data = modalApi.getData<ConfirmModalData>();
+    modalApi.lock();
+    try {
+      await data?.onConfirm?.();
+      emit('success');
+      await modalApi.close();
+    } catch {
+      // 保持弹窗打开,由 onConfirm 内处理错误提示
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+
+function getModalData() {
+  return modalApi.getData<ConfirmModalData>();
+}
+</script>
+
+<template>
+  <Modal :title="getModalData()?.title ?? '提醒'">
+    <div class="review-confirm-modal__body">{{ getModalData()?.content ?? '' }}</div>
+  </Modal>
+</template>
+
+<style scoped>
+.review-confirm-modal__body {
+  padding: 48px 32px;
+  text-align: center;
+  font-size: 14px;
+  line-height: 1.8;
+  white-space: pre-line;
+}
+
+:deep(.review-confirm-modal__header) {
+  background-color: #1677ff;
+  color: #fff;
+  padding: 12px 20px;
+}
+
+:deep(.review-confirm-modal__header h2) {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+:deep(.review-confirm-modal__header button) {
+  color: #fff;
+  opacity: 0.85;
+}
+
+:deep(.review-confirm-modal__footer) {
+  justify-content: center;
+  gap: 12px;
+  padding: 16px 24px 24px;
+  border-top: none;
+}
+
+:deep(.review-confirm-modal__footer button:first-child) {
+  min-width: 88px;
+  color: #1677ff;
+  border-color: #1677ff;
+  background: #fff;
+}
+
+:deep(.review-confirm-modal__footer button:last-child) {
+  min-width: 88px;
+  background-color: #1677ff;
+  border-color: #1677ff;
+}
+</style>

+ 116 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/expert-group-modal.vue

@@ -0,0 +1,116 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import {
+  listReviewExpertsMethod,
+  updateReviewTaskExpertsMethod,
+} from '#/api';
+
+const emit = defineEmits(['success']);
+
+const expertOptions = ref<{ label: string; value: string }[]>([]);
+const selectedIds = ref<string[]>([]);
+const taskId = ref('');
+
+const [Modal, modalApi] = useVbenModal({
+  cancelText: '取消',
+  centered: true,
+  class: 'review-expert-group-modal w-[520px]',
+  confirmText: '保存',
+  footerClass: 'review-expert-group-modal__footer',
+  fullscreenButton: false,
+  headerClass: 'review-expert-group-modal__header',
+  async onConfirm() {
+    if (!taskId.value) return;
+    modalApi.lock();
+    try {
+      await updateReviewTaskExpertsMethod(taskId.value, selectedIds.value);
+      message.success('保存成功');
+      emit('success');
+      await modalApi.close();
+    } catch (error: any) {
+      message.error(error.message || '保存失败');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen) {
+    if (!isOpen) {
+      selectedIds.value = [];
+      taskId.value = '';
+      return;
+    }
+    const data = modalApi.getData<PrescriptionReviewModel.ReviewTask>();
+    taskId.value = data?.id ?? '';
+    selectedIds.value = [...(data?.expertIds ?? [])];
+    const result = await listReviewExpertsMethod(1, 100);
+    expertOptions.value = result.items.map((item) => ({
+      label: item.name,
+      value: item.id,
+    }));
+  },
+});
+</script>
+
+<template>
+  <Modal title="点评专家组">
+    <div class="review-expert-group-modal__body">
+      <Select
+        v-model:value="selectedIds"
+        :options="expertOptions"
+        class="w-full"
+        mode="multiple"
+        placeholder="请选择专家"
+      />
+    </div>
+  </Modal>
+</template>
+
+<style scoped>
+.review-expert-group-modal__body {
+  padding: 32px;
+}
+
+:deep(.review-expert-group-modal__header) {
+  background-color: #1677ff;
+  color: #fff;
+  padding: 12px 20px;
+}
+
+:deep(.review-expert-group-modal__header h2) {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+:deep(.review-expert-group-modal__header button) {
+  color: #fff;
+  opacity: 0.85;
+}
+
+:deep(.review-expert-group-modal__footer) {
+  justify-content: center;
+  gap: 12px;
+  padding: 16px 24px 24px;
+  border-top: none;
+}
+
+:deep(.review-expert-group-modal__footer button:first-child) {
+  min-width: 88px;
+  color: #1677ff;
+  border-color: #1677ff;
+  background: #fff;
+}
+
+:deep(.review-expert-group-modal__footer button:last-child) {
+  min-width: 88px;
+  background-color: #1677ff;
+  border-color: #1677ff;
+}
+</style>

+ 122 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/sampling-modal.vue

@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createReviewTaskMethod } from '#/api';
+
+import { useReviewSamplingFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    labelWidth: 150,
+  },
+  schema: useReviewSamplingFormSchema(),
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-1',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  cancelText: '取消',
+  centered: true,
+  class: 'review-sampling-modal w-[640px]',
+  confirmText: '保存',
+  footerClass: 'review-sampling-modal__footer',
+  fullscreenButton: false,
+  headerClass: 'review-sampling-modal__header',
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) return;
+    const values = await formApi.getValues();
+    if (!values.sampleCount && values.sampleRatio == null) {
+      message.warning('抽取处方数和抽取处方比例,至少输入一项');
+      return;
+    }
+    modalApi.lock();
+    try {
+      const [prescriptionDateStart, prescriptionDateEnd] =
+        values.prescriptionDateRange ?? [];
+      await createReviewTaskMethod({
+        name: values.name,
+        institutions: values.institutionIds,
+        prescriptionDateStart,
+        prescriptionDateEnd,
+        herbCountMin: values.herbCountMin,
+        herbCountMax: values.herbCountMax,
+        amountMin: values.amountMin,
+        amountMax: values.amountMax,
+        sampleCount: values.sampleCount,
+        sampleRatio: values.sampleRatio,
+        excludeReviewed: values.excludeReviewed ?? true,
+      });
+      message.success('抽样成功');
+      emit('success');
+      await modalApi.close();
+    } catch (error: any) {
+      message.error(error.message || '抽样失败');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      formApi.resetForm();
+      formApi.setValues({ excludeReviewed: true });
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="抽样">
+    <Form class="review-sampling-modal__body" />
+  </Modal>
+</template>
+
+<style scoped>
+:deep(.review-sampling-modal__body) {
+  padding: 24px 32px 8px;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+:deep(.review-sampling-modal__header) {
+  background-color: #1677ff;
+  color: #fff;
+  padding: 12px 20px;
+}
+
+:deep(.review-sampling-modal__header h2) {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+:deep(.review-sampling-modal__header button) {
+  color: #fff;
+  opacity: 0.85;
+}
+
+:deep(.review-sampling-modal__footer) {
+  justify-content: center;
+  gap: 12px;
+  padding: 16px 24px 24px;
+  border-top: none;
+}
+
+:deep(.review-sampling-modal__footer button:first-child) {
+  min-width: 88px;
+  color: #1677ff;
+  border-color: #1677ff;
+  background: #fff;
+}
+
+:deep(.review-sampling-modal__footer button:last-child) {
+  min-width: 88px;
+  background-color: #1677ff;
+  border-color: #1677ff;
+}
+</style>

+ 112 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/sampling-rules-modal.vue

@@ -0,0 +1,112 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+const [Modal, modalApi] = useVbenModal({
+  cancelText: '取消',
+  centered: true,
+  class: 'review-sampling-rules-modal w-[560px]',
+  confirmText: '关闭',
+  footerClass: 'review-sampling-rules-modal__footer',
+  fullscreenButton: false,
+  headerClass: 'review-sampling-rules-modal__header',
+  showCancelButton: false,
+  async onConfirm() {
+    await modalApi.close();
+  },
+});
+
+const rule = computed(() => {
+  const data = modalApi.getData<PrescriptionReviewModel.ReviewTask>();
+  return data?.samplingRule;
+});
+
+function formatRange(min?: number, max?: number) {
+  if (min != null && max != null) return `${min}-${max}`;
+  if (min != null) return `${min}-`;
+  if (max != null) return `-${max}`;
+  return '/';
+}
+
+function formatDateRange(start?: string, end?: string) {
+  if (start && end) return `${start} - ${end}`;
+  return '/';
+}
+</script>
+
+<template>
+  <Modal title="抽样规则">
+    <div v-if="rule" class="review-sampling-rules-modal__body">
+      <div class="rule-item">
+        <span class="rule-label">抽样名称:</span>
+        <span>{{ rule.name }}</span>
+      </div>
+      <div class="rule-item">
+        <span class="rule-label">医疗机构:</span>
+        <span>{{ rule.institutions?.join(',') || '全部' }}</span>
+      </div>
+      <div class="rule-item">
+        <span class="rule-label">处方日期:</span>
+        <span>{{
+          formatDateRange(rule.prescriptionDateStart, rule.prescriptionDateEnd)
+        }}</span>
+      </div>
+      <div class="rule-item">
+        <span class="rule-label">药品金额范围:</span>
+        <span>{{ formatRange(rule.amountMin, rule.amountMax) }}</span>
+      </div>
+      <div class="rule-item">
+        <span class="rule-label">抽取处方数:</span>
+        <span>{{ rule.sampleCount ?? '/' }}</span>
+      </div>
+      <div class="rule-item">
+        <span class="rule-label">抽取处方比例:</span>
+        <span>{{ rule.sampleRatio != null ? `${rule.sampleRatio}%` : '/' }}</span>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style scoped>
+.review-sampling-rules-modal__body {
+  padding: 24px 32px;
+}
+
+.rule-item {
+  display: flex;
+  margin-bottom: 16px;
+  line-height: 1.6;
+}
+
+.rule-label {
+  flex-shrink: 0;
+  width: 120px;
+  color: rgb(0 0 0 / 65%);
+}
+
+:deep(.review-sampling-rules-modal__header) {
+  background-color: #1677ff;
+  color: #fff;
+  padding: 12px 20px;
+}
+
+:deep(.review-sampling-rules-modal__header h2) {
+  color: #fff;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+:deep(.review-sampling-rules-modal__header button) {
+  color: #fff;
+  opacity: 0.85;
+}
+
+:deep(.review-sampling-rules-modal__footer) {
+  justify-content: center;
+  padding: 16px 24px 24px;
+  border-top: none;
+}
+</style>

+ 145 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/cost-report.vue

@@ -0,0 +1,145 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { computed, onMounted, ref } from 'vue';
+
+import { Table } from 'ant-design-vue';
+
+import { listReviewStatisticsCostMethod } from '#/api';
+
+const props = defineProps<{
+  taskId: string;
+}>();
+
+interface TableRow {
+  avgDoseCost: string;
+  children?: TableRow[];
+  exceedLimit?: boolean;
+  id: string;
+  index: string;
+  level: PrescriptionReviewModel.ReviewStatisticsCostLevel;
+  name: string;
+  prescriptionCount: number;
+  totalAmount: string;
+  totalDoseCount: number;
+}
+
+const loading = ref(false);
+const rawRows = ref<PrescriptionReviewModel.ReviewStatisticsCostRow[]>([]);
+const dataSource = ref<TableRow[]>([]);
+
+const exceedCount = computed(
+  () =>
+    rawRows.value.filter(
+      (item) => item.level === 'institution' && item.exceedLimit,
+    ).length,
+);
+
+const summaryRow = computed(() => {
+  const institutions = rawRows.value.filter(
+    (item) => item.level === 'institution',
+  );
+  const total = institutions.reduce(
+    (acc, item) => ({
+      prescriptionCount: acc.prescriptionCount + item.prescriptionCount,
+      totalDoseCount: acc.totalDoseCount + item.totalDoseCount,
+      totalAmount: acc.totalAmount + item.totalAmount,
+    }),
+    { prescriptionCount: 0, totalDoseCount: 0, totalAmount: 0 },
+  );
+  const avgDoseCost =
+    total.totalDoseCount > 0
+      ? (total.totalAmount / total.totalDoseCount).toFixed(2)
+      : '0.00';
+  return { ...total, avgDoseCost };
+});
+
+const columns = [
+  { dataIndex: 'index', title: '序号', width: 70 },
+  { dataIndex: 'name', title: '医疗机构', width: 260 },
+  { dataIndex: 'prescriptionCount', title: '处方数', width: 100 },
+  { dataIndex: 'totalDoseCount', title: '总剂数', width: 100 },
+  { dataIndex: 'totalAmount', title: '总金额', width: 120 },
+  { dataIndex: 'avgDoseCost', title: '均剂费用', width: 100 },
+];
+
+function formatCostTree(
+  rows: PrescriptionReviewModel.ReviewStatisticsCostRow[],
+  isRoot = true,
+): TableRow[] {
+  return rows.map((item, index) => ({
+    avgDoseCost: item.avgDoseCost.toFixed(2),
+    children: item.children?.length
+      ? formatCostTree(item.children, false)
+      : undefined,
+    exceedLimit: item.exceedLimit,
+    id: item.id,
+    index: isRoot ? String(index + 1).padStart(2, '0') : '',
+    level: item.level,
+    name: item.name,
+    prescriptionCount: item.prescriptionCount,
+    totalAmount: item.totalAmount.toFixed(2),
+    totalDoseCount: item.totalDoseCount,
+  }));
+}
+
+async function loadData() {
+  loading.value = true;
+  try {
+    const rows = await listReviewStatisticsCostMethod(props.taskId);
+    rawRows.value = rows;
+    dataSource.value = formatCostTree(rows);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(loadData);
+</script>
+
+<template>
+  <div>
+    <p v-if="exceedCount > 0" class="mb-3 text-red-500">
+      共有{{ exceedCount }}个机构超均剂限价
+    </p>
+    <Table
+      :columns="columns"
+      :data-source="dataSource"
+      :default-expand-all-rows="false"
+      :indent-size="20"
+      :loading="loading"
+      :pagination="false"
+      bordered
+      row-key="id"
+      size="small"
+    >
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.dataIndex === 'avgDoseCost'">
+          <span :class="{ 'text-red-500': record.exceedLimit }">
+            {{ record.avgDoseCost }}
+          </span>
+        </template>
+      </template>
+      <template #summary>
+        <Table.Summary fixed>
+          <Table.Summary.Row>
+            <Table.Summary.Cell :index="0" />
+            <Table.Summary.Cell :index="1">合计</Table.Summary.Cell>
+            <Table.Summary.Cell :index="2">
+              {{ summaryRow.prescriptionCount }}
+            </Table.Summary.Cell>
+            <Table.Summary.Cell :index="3">
+              {{ summaryRow.totalDoseCount }}
+            </Table.Summary.Cell>
+            <Table.Summary.Cell :index="4">
+              {{ summaryRow.totalAmount.toFixed(2) }}
+            </Table.Summary.Cell>
+            <Table.Summary.Cell :index="5">
+              {{ summaryRow.avgDoseCost }}
+            </Table.Summary.Cell>
+          </Table.Summary.Row>
+        </Table.Summary>
+      </template>
+    </Table>
+  </div>
+</template>

+ 97 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/prescription-table.vue

@@ -0,0 +1,97 @@
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Table } from 'ant-design-vue';
+
+import { listReviewStatisticsPrescriptionsMethod } from '#/api';
+
+const props = defineProps<{
+  taskId: string;
+}>();
+
+const router = useRouter();
+
+interface TableRow {
+  comment: string;
+  doctorName: string;
+  id: string;
+  index: string;
+  institutionName: string;
+  pharmacistName: string;
+  prescriptionNo: string;
+  result: string;
+  unqualifiedDetail: string;
+  unitDoseAmount: string;
+}
+
+const loading = ref(false);
+const dataSource = ref<TableRow[]>([]);
+
+const columns = [
+  { dataIndex: 'index', title: '序号', width: 70 },
+  { dataIndex: 'prescriptionNo', title: '处方号', width: 140 },
+  { dataIndex: 'institutionName', title: '医疗机构', width: 180 },
+  { dataIndex: 'doctorName', title: '医生', width: 100 },
+  { dataIndex: 'pharmacistName', title: '药师', width: 100 },
+  { dataIndex: 'result', title: '点评结果', width: 100 },
+  { dataIndex: 'unqualifiedDetail', title: '不合格详情', width: 120 },
+  { dataIndex: 'comment', title: '点评意见和说明', minWidth: 140 },
+  { dataIndex: 'unitDoseAmount', title: '单剂金额', width: 100 },
+  { dataIndex: 'operation', title: '操作', width: 100 },
+];
+
+function onViewDetail(record: TableRow) {
+  router.push(
+    `/prescription-review/task/statistics/${props.taskId}/detail/${record.id}`,
+  );
+}
+
+async function loadData() {
+  loading.value = true;
+  try {
+    const rows = await listReviewStatisticsPrescriptionsMethod(props.taskId);
+    dataSource.value = rows.map((item, index) => ({
+      ...item,
+      index: String(index + 1).padStart(2, '0'),
+      pharmacistName: item.pharmacistName || '',
+      result: item.result === 'qualified' ? '合格' : '不合格',
+      unqualifiedDetail: item.unqualifiedDetail || '',
+      comment: item.comment || '',
+      unitDoseAmount: item.unitDoseAmount.toFixed(2),
+    }));
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(loadData);
+</script>
+
+<template>
+  <Table
+    :columns="columns"
+    :data-source="dataSource"
+    :loading="loading"
+    :pagination="false"
+    :scroll="{ x: 1200 }"
+    bordered
+    row-key="id"
+    size="small"
+  >
+    <template #bodyCell="{ column, record }">
+      <template v-if="column.dataIndex === 'unitDoseAmount'">
+        <span
+          :class="{
+            'text-red-500': record.result === '不合格',
+          }"
+        >
+          {{ record.unitDoseAmount }}
+        </span>
+      </template>
+      <template v-if="column.dataIndex === 'operation'">
+        <a class="text-primary" @click="onViewDetail(record)">点评详情</a>
+      </template>
+    </template>
+  </Table>
+</template>

+ 124 - 0
apps/smart-pharmacy/src/views/prescription-review/task/modules/statistics/result-summary.vue

@@ -0,0 +1,124 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { onMounted, ref } from 'vue';
+
+import { Spin } from 'ant-design-vue';
+
+import { getReviewStatisticsSummaryMethod } from '#/api';
+
+const props = defineProps<{
+  taskId: string;
+}>();
+
+const loading = ref(false);
+const summary = ref<PrescriptionReviewModel.ReviewStatisticsSummary>();
+
+async function loadData() {
+  loading.value = true;
+  try {
+    summary.value = await getReviewStatisticsSummaryMethod(props.taskId);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(loadData);
+</script>
+
+<template>
+  <Spin :spinning="loading">
+  <div v-if="summary" class="result-summary">
+    <div class="summary-grid">
+      <div class="summary-item">
+        <span>抽取处方数:</span>
+        <strong>{{ summary.sampledPrescriptionCount }}张</strong>
+      </div>
+      <div class="summary-item">
+        <span>抽取机构数:</span>
+        <strong>{{ summary.sampledInstitutionCount }}家</strong>
+      </div>
+      <div class="summary-item">
+        <span>合格处方数:</span>
+        <strong
+          >{{ summary.qualifiedCount }}张({{ summary.qualifiedRate }}%)</strong
+        >
+      </div>
+      <div class="summary-item">
+        <span>均剂费用:</span>
+        <strong>{{ summary.avgDoseCost }}元</strong>
+      </div>
+      <div class="summary-item">
+        <span>超均贴限价机构数:</span>
+        <strong>{{ summary.exceedLimitInstitutionCount }}家</strong>
+      </div>
+    </div>
+    <div class="summary-grid">
+      <div class="summary-item">
+        <span>抽取处方比例:</span>
+        <strong>{{ summary.sampleRatio }}%</strong>
+      </div>
+      <div class="summary-item">
+        <span>抽取医生数:</span>
+        <strong>{{ summary.sampledDoctorCount }}位</strong>
+      </div>
+      <div class="summary-item">
+        <span>不合格处方数:</span>
+        <strong
+          >{{ summary.unqualifiedCount }}张({{ summary.unqualifiedRate }}%)</strong
+        >
+      </div>
+      <div class="summary-item">
+        <span>超均贴限价数:</span>
+        <strong
+          >{{ summary.exceedLimitDoseCount }}剂({{
+            summary.exceedLimitDoseRate
+          }}%)</strong
+        >
+      </div>
+      <div class="summary-item">
+        <span>超均贴限价医生数:</span>
+        <strong>{{ summary.exceedLimitDoctorCount }}位</strong>
+      </div>
+    </div>
+    <div class="summary-list">
+      <div class="summary-list-item">
+        <span class="list-label">不合格处方原因前5:</span>
+        <strong>{{ summary.topUnqualifiedReasons.join(';') }}</strong>
+      </div>
+      <div class="summary-list-item">
+        <span class="list-label">剂量超标中药前5:</span>
+        <strong>{{ summary.topExcessDosageHerbs.join(';') }}</strong>
+      </div>
+    </div>
+  </div>
+  </Spin>
+</template>
+
+<style scoped>
+.result-summary {
+  padding: 16px 8px;
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 16px 24px;
+  margin-bottom: 24px;
+}
+
+.summary-item span {
+  color: rgb(0 0 0 / 65%);
+}
+
+.summary-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.list-label {
+  color: rgb(0 0 0 / 45%);
+  margin-right: 8px;
+}
+</style>

+ 683 - 0
apps/smart-pharmacy/src/views/prescription-review/task/review.vue

@@ -0,0 +1,683 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { computed, onMounted, ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import {
+  Button,
+  Checkbox,
+  message,
+  Radio,
+  RadioGroup,
+  Select,
+  Spin,
+  Table,
+  Textarea,
+} from 'ant-design-vue';
+
+import {
+  getAuxiliaryReviewResultMethod,
+  getReviewPrescriptionMethod,
+  getReviewTaskMethod,
+  listReviewIndicatorCategoriesMethod,
+  listReviewIndicatorsByCategoryMethod,
+  listReviewPrescriptionsMethod,
+  saveReviewPrescriptionResultMethod,
+} from '#/api';
+
+import AuxiliaryReviewModal from './modules/auxiliary-review-modal.vue';
+
+const router = useRouter();
+const route = useRoute();
+
+const taskLoading = ref(false);
+const listLoading = ref(false);
+const detailLoading = ref(false);
+const saving = ref(false);
+const taskName = ref('');
+const prescriptions = ref<PrescriptionReviewModel.ReviewPrescriptionRecord[]>([]);
+const selectedId = ref('');
+const currentPrescription =
+  ref<PrescriptionReviewModel.ReviewPrescriptionRecord>();
+const total = ref(0);
+const currentPage = ref(1);
+const pageSize = ref(50);
+
+const filterStatus = ref<string>();
+const filterInstitution = ref<string>();
+const filterDepartment = ref<string>();
+const filterDoctor = ref<string>();
+
+const qualified = ref(true);
+const reviewComment = ref('');
+const selectedIndicators = ref<string[]>([]);
+const herbSelections = ref<Record<string, string[]>>({});
+
+interface CategoryGroup {
+  categoryId: string;
+  categoryName: string;
+  indicators: PrescriptionReviewModel.ReviewIndicator[];
+}
+
+const indicatorGroups = ref<CategoryGroup[]>([]);
+
+const [AuxiliaryReviewModalComp, auxiliaryReviewModalApi] = useVbenModal({
+  connectedComponent: AuxiliaryReviewModal,
+  destroyOnClose: true,
+});
+
+const taskId = computed(() => {
+  const id = route.params.id;
+  return Array.isArray(id) ? id[0] : id;
+});
+
+const institutionOptions = computed(() =>
+  [...new Set(prescriptions.value.map((item) => item.institutionName))].map(
+    (name) => ({ label: name, value: name }),
+  ),
+);
+
+const departmentOptions = computed(() =>
+  [...new Set(prescriptions.value.map((item) => item.departmentName))].map(
+    (name) => ({ label: name, value: name }),
+  ),
+);
+
+const doctorOptions = computed(() =>
+  [...new Set(prescriptions.value.map((item) => item.doctorName))].map(
+    (name) => ({ label: name, value: name }),
+  ),
+);
+
+const listColumns = [
+  { dataIndex: 'index', title: '序号', width: 60 },
+  { dataIndex: 'prescriptionNo', title: '处方号', width: 130 },
+  { dataIndex: 'institutionName', title: '医疗机构', width: 140 },
+  { dataIndex: 'departmentName', title: '科室', width: 90 },
+  { dataIndex: 'doctorName', title: '医生', width: 80 },
+  { dataIndex: 'statusLabel', title: '状态', width: 80 },
+  { dataIndex: 'reviewExpert', title: '点评专家', width: 90 },
+];
+
+const statusLabelMap: Record<string, string> = {
+  pending: '待点评',
+  qualified: '合格',
+  unqualified: '不合格',
+};
+
+const tableData = computed(() =>
+  prescriptions.value.map((item, index) => ({
+    ...item,
+    index: (currentPage.value - 1) * pageSize.value + index + 1,
+    statusLabel: statusLabelMap[item.status] ?? item.status,
+    reviewExpert: item.reviewExpert ?? '',
+  })),
+);
+
+function getHerbOptions() {
+  return (currentPrescription.value?.herbs ?? []).map((herb) => {
+    const name = herb.split(' ')[0] ?? herb;
+    return { label: name, value: name };
+  });
+}
+
+async function loadIndicators() {
+  const categories = await listReviewIndicatorCategoriesMethod();
+  const reviewCategories = categories.filter((item) => item.id !== 'cat-6');
+  const groups = await Promise.all(
+    reviewCategories.map(async (category) => ({
+      categoryId: category.id,
+      categoryName: category.name,
+      indicators: await listReviewIndicatorsByCategoryMethod(category.id, {
+        enabledOnly: true,
+      }),
+    })),
+  );
+  indicatorGroups.value = groups.filter((item) => item.indicators.length > 0);
+}
+
+async function loadTask() {
+  if (!taskId.value) return;
+  taskLoading.value = true;
+  try {
+    const task = await getReviewTaskMethod(taskId.value);
+    taskName.value = task.name;
+  } finally {
+    taskLoading.value = false;
+  }
+}
+
+async function loadPrescriptions() {
+  if (!taskId.value) return;
+  listLoading.value = true;
+  try {
+    const result = await listReviewPrescriptionsMethod(
+      taskId.value,
+      currentPage.value,
+      pageSize.value,
+      {
+        departmentName: filterDepartment.value,
+        doctorName: filterDoctor.value,
+        institutionName: filterInstitution.value,
+        status: filterStatus.value,
+      },
+    );
+    prescriptions.value = result.items;
+    total.value = result.total;
+    if (!selectedId.value && result.items.length > 0) {
+      const first = result.items[0];
+      if (first) selectPrescription(first.id);
+    }
+  } finally {
+    listLoading.value = false;
+  }
+}
+
+async function loadPrescriptionDetail(id: string) {
+  detailLoading.value = true;
+  try {
+    const detail = await getReviewPrescriptionMethod(id);
+    currentPrescription.value = detail;
+    qualified.value = detail.reviewResult?.qualified ?? true;
+    reviewComment.value = detail.reviewResult?.comment ?? '';
+    selectedIndicators.value = detail.reviewResult?.indicatorIds ?? [];
+    herbSelections.value = { ...(detail.reviewResult?.herbIndicatorMap ?? {}) };
+  } finally {
+    detailLoading.value = false;
+  }
+}
+
+function selectPrescription(id: string) {
+  selectedId.value = id;
+  loadPrescriptionDetail(id);
+}
+
+function onQualifiedChange() {
+  if (qualified.value) {
+    selectedIndicators.value = [];
+    herbSelections.value = {};
+  }
+}
+
+function onIndicatorChange(indicatorId: string, checked: boolean) {
+  if (checked) {
+    if (!selectedIndicators.value.includes(indicatorId)) {
+      selectedIndicators.value.push(indicatorId);
+    }
+  } else {
+    selectedIndicators.value = selectedIndicators.value.filter(
+      (id) => id !== indicatorId,
+    );
+    delete herbSelections.value[indicatorId];
+  }
+}
+
+function isIndicatorChecked(indicatorId: string) {
+  return selectedIndicators.value.includes(indicatorId);
+}
+
+function needsHerbSelect(indicator: PrescriptionReviewModel.ReviewIndicator) {
+  return indicator.categoryId === 'cat-2' || indicator.associatedChineseMedicine;
+}
+
+function getRowClassName(record: { id: string }) {
+  return record.id === selectedId.value ? 'selected-row' : '';
+}
+
+async function openAuxiliaryReviewModal() {
+  if (!selectedId.value) return;
+  try {
+    const result = await getAuxiliaryReviewResultMethod(selectedId.value);
+    auxiliaryReviewModalApi.setData({
+      indicatorGroups: indicatorGroups.value,
+      result,
+      herbOptions: getHerbOptions(),
+      onApply: (appliedResult: PrescriptionReviewModel.ReviewPrescriptionResult) => {
+        qualified.value = false;
+        selectedIndicators.value = [...(appliedResult.indicatorIds ?? [])];
+        herbSelections.value = {
+          ...(appliedResult.herbIndicatorMap ?? {}),
+        };
+      },
+    });
+    auxiliaryReviewModalApi.open();
+  } catch (error: any) {
+    message.warning(error.message || '暂无辅助点评结果');
+  }
+}
+
+async function onSave() {
+  if (!selectedId.value) return;
+  saving.value = true;
+  try {
+    await saveReviewPrescriptionResultMethod(selectedId.value, {
+      qualified: qualified.value,
+      indicatorIds: qualified.value ? [] : selectedIndicators.value,
+      herbIndicatorMap: qualified.value ? {} : herbSelections.value,
+      comment: reviewComment.value,
+    });
+    message.success('保存成功');
+    await loadPrescriptions();
+    await loadPrescriptionDetail(selectedId.value);
+  } catch (error: any) {
+    message.error(error.message || '保存失败');
+  } finally {
+    saving.value = false;
+  }
+}
+
+function goBack() {
+  router.push('/prescription-review/task');
+}
+
+function onPageChange(page: number) {
+  currentPage.value = page;
+  loadPrescriptions();
+}
+
+watch(
+  [filterStatus, filterInstitution, filterDepartment, filterDoctor],
+  () => {
+    currentPage.value = 1;
+    loadPrescriptions();
+  },
+);
+
+onMounted(async () => {
+  await Promise.all([loadTask(), loadIndicators()]);
+  await loadPrescriptions();
+});
+</script>
+
+<template>
+  <Page auto-content-height class="review-workspace-page">
+    <Spin :spinning="taskLoading">
+      <div class="workspace-header">
+        <Button type="link" @click="goBack">返回</Button>
+        <span v-if="taskName" class="workspace-title">{{ taskName }}</span>
+      </div>
+      <div class="workspace-body">
+        <div class="prescription-list-panel">
+          <div class="list-filters">
+            <Select
+              v-model:value="filterStatus"
+              :options="[
+                { label: '待点评', value: 'pending' },
+                { label: '合格', value: 'qualified' },
+                { label: '不合格', value: 'unqualified' },
+              ]"
+              allow-clear
+              class="filter-item"
+              placeholder="状态"
+            />
+            <Select
+              v-model:value="filterInstitution"
+              :options="institutionOptions"
+              allow-clear
+              class="filter-item"
+              placeholder="医疗机构"
+            />
+            <Select
+              v-model:value="filterDepartment"
+              :options="departmentOptions"
+              allow-clear
+              class="filter-item"
+              placeholder="科室"
+            />
+            <Select
+              v-model:value="filterDoctor"
+              :options="doctorOptions"
+              allow-clear
+              class="filter-item"
+              placeholder="医生"
+            />
+          </div>
+          <Table
+            :columns="listColumns"
+            :custom-row="
+              (record) => ({
+                onClick: () => selectPrescription(record.id),
+              })
+            "
+            :data-source="tableData"
+            :loading="listLoading"
+            :pagination="{
+              current: currentPage,
+              pageSize,
+              total,
+              showSizeChanger: false,
+              showTotal: (t) => `共 ${t} 条`,
+              onChange: onPageChange,
+            }"
+            :row-class-name="getRowClassName"
+            :scroll="{ y: 480 }"
+            row-key="id"
+            size="small"
+          />
+        </div>
+        <div class="review-panel">
+          <Spin :spinning="detailLoading">
+            <template v-if="currentPrescription">
+              <div class="prescription-info">
+                <div class="info-row">
+                  <span
+                    style="min-width: 160px"
+                    >处方号:<a class="text-primary">{{
+                      currentPrescription.prescriptionNo
+                    }}</a></span
+                  >
+                  <span>姓名:{{ currentPrescription.patientName }}</span>
+                  <span>性别:{{ currentPrescription.patientGender }}</span>
+                  <span>年龄:{{ currentPrescription.patientAge }}</span>
+                  <span
+                    >妊娠:{{ currentPrescription.pregnancy ? '是' : '否' }}</span
+                  >
+                  <span
+                    >哺乳:{{ currentPrescription.lactation ? '是' : '否' }}</span
+                  >
+                </div>
+                <div class="info-row">
+                  <span>中医病名:{{ currentPrescription.tcmDisease }}</span>
+                  <span>中医证候:{{ currentPrescription.tcmSyndrome }}</span>
+                  <span>治则治法:{{ currentPrescription.treatmentPrinciple }}</span>
+                  <span>服法:{{ currentPrescription.administrationMethod }}</span>
+                </div>
+                <div class="info-row">
+                  <span>药味数:{{ currentPrescription.herbCount }}</span>
+                  <span>剂数:{{ currentPrescription.doseCount }}</span>
+                  <span>总金额:{{ currentPrescription.totalAmount }}</span>
+                  <span>单剂金额:{{ currentPrescription.unitDoseAmount }}</span>
+                </div>
+              </div>
+              <div class="herb-details">
+                <div class="herb-title">明细</div>
+                <div class="herb-grid">
+                  <span
+                    v-for="(herb, idx) in currentPrescription.herbs"
+                    :key="idx"
+                    class="herb-item"
+                  >
+                    {{ herb }}
+                  </span>
+                </div>
+              </div>
+              <div class="review-form">
+                <div v-if="qualified" class="qualified-panel">
+                  <div class="review-form-header">
+                    <span class="form-title">点评结果:</span>
+                    <RadioGroup v-model:value="qualified" @change="onQualifiedChange">
+                      <Radio :value="true">合格</Radio>
+                      <Radio :value="false">不合格</Radio>
+                    </RadioGroup>
+                    <a
+                      class="auxiliary-link"
+                      @click.prevent="openAuxiliaryReviewModal"
+                    >
+                      辅助点评结果
+                    </a>
+                  </div>
+                  <div class="comment-section">
+                    <div class="comment-label">点评意见和说明</div>
+                    <Textarea
+                      v-model:value="reviewComment"
+                      :rows="6"
+                      placeholder="请输入"
+                    />
+                  </div>
+                </div>
+                <template v-else>
+                  <div class="review-form-header">
+                    <span class="form-title">点评结果:</span>
+                    <RadioGroup v-model:value="qualified" @change="onQualifiedChange">
+                      <Radio :value="true">合格</Radio>
+                      <Radio :value="false">不合格</Radio>
+                    </RadioGroup>
+                    <a
+                      class="auxiliary-link"
+                      @click.prevent="openAuxiliaryReviewModal"
+                    >
+                      辅助点评结果
+                    </a>
+                  </div>
+                  <div class="indicator-section">
+                    <div
+                      v-for="group in indicatorGroups"
+                      :key="group.categoryId"
+                      class="indicator-group"
+                    >
+                      <div class="group-title">{{ group.categoryName }}</div>
+                      <div class="group-items">
+                        <template
+                          v-for="indicator in group.indicators"
+                          :key="indicator.id"
+                        >
+                          <div class="indicator-item">
+                            <Checkbox
+                              :checked="isIndicatorChecked(indicator.id)"
+                              @change="
+                                (e) =>
+                                  onIndicatorChange(
+                                    indicator.id,
+                                    e.target.checked ?? false,
+                                  )
+                              "
+                            >
+                              {{ indicator.name }}
+                            </Checkbox>
+                            <Select
+                              v-if="
+                                isIndicatorChecked(indicator.id) &&
+                                needsHerbSelect(indicator)
+                              "
+                              v-model:value="herbSelections[indicator.id]"
+                              :options="getHerbOptions()"
+                              class="herb-select"
+                              mode="multiple"
+                              placeholder="选择中药"
+                              size="small"
+                            />
+                          </div>
+                        </template>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+              </div>
+            </template>
+            <div v-else class="empty-panel">请选择处方进行点评</div>
+          </Spin>
+        </div>
+      </div>
+    </Spin>
+    <AuxiliaryReviewModalComp />
+    <div v-if="currentPrescription" class="review-save-bar">
+      <Button :loading="saving" type="primary" @click="onSave">保存</Button>
+    </div>
+  </Page>
+</template>
+
+<style scoped>
+.workspace-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.workspace-title {
+  font-size: 15px;
+  font-weight: 500;
+}
+
+.workspace-body {
+  display: flex;
+  gap: 12px;
+  min-height: calc(100vh - 200px);
+}
+
+.prescription-list-panel {
+  flex-shrink: 0;
+  width: 52%;
+  padding: 12px;
+  background: #fff;
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+}
+
+.list-filters {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.filter-item {
+  flex: 1;
+  min-width: 0;
+}
+
+.review-panel {
+  flex: 1;
+  min-width: 0;
+  padding: 12px;
+  padding-bottom: 72px;
+  background: #fff;
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+  overflow-y: auto;
+}
+
+:deep(.ant-table-tbody > tr.selected-row > td) {
+  background-color: #e6f4ff !important;
+}
+
+:deep(.ant-table-tbody > tr.selected-row:hover > td) {
+  background-color: #e6f4ff !important;
+}
+
+:deep(.ant-table-tbody > tr) {
+  cursor: pointer;
+}
+
+.prescription-info {
+  margin-bottom: 16px;
+}
+
+.info-row {
+  display: flex;
+  gap: 8px 12px;
+  margin-bottom: 8px;
+  font-size: 13px;
+}
+
+.info-row > span {
+  flex: 1;
+  min-width: 0;
+}
+
+.herb-details {
+  margin-bottom: 16px;
+  padding: 12px;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.herb-title {
+  margin-bottom: 8px;
+  font-weight: 500;
+}
+
+.herb-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 8px;
+}
+
+.herb-item {
+  font-size: 13px;
+}
+
+.qualified-panel {
+  padding: 16px;
+  background: #f5f5f5;
+  border-radius: 4px;
+}
+
+.review-form-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.qualified-panel .review-form-header {
+  margin-bottom: 16px;
+}
+
+.form-title {
+  margin-right: 12px;
+  font-weight: 500;
+  color: #fa8c16;
+}
+
+.auxiliary-link {
+  margin-left: 16px;
+  color: #1677ff;
+  cursor: pointer;
+}
+
+.indicator-section {
+  padding: 16px;
+  background: #f5f5f5;
+  border-radius: 4px;
+}
+
+.indicator-group {
+  margin-bottom: 16px;
+}
+
+.group-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+}
+
+.group-items {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 16px;
+}
+
+.indicator-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.herb-select {
+  width: 140px;
+}
+
+.comment-section {
+  margin-bottom: 0;
+}
+
+.comment-label {
+  margin-bottom: 8px;
+  font-size: 13px;
+  color: rgb(0 0 0 / 88%);
+}
+
+.review-save-bar {
+  position: fixed;
+  right: 45px;
+  bottom: 24px;
+  z-index: 100;
+}
+
+.empty-panel {
+  padding: 48px;
+  text-align: center;
+  color: rgb(0 0 0 / 45%);
+}
+</style>

+ 397 - 0
apps/smart-pharmacy/src/views/prescription-review/task/statistics-detail.vue

@@ -0,0 +1,397 @@
+<script lang="ts" setup>
+import type { PrescriptionReviewModel } from '#/api';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import {
+  Button,
+  Checkbox,
+  Input,
+  Radio,
+  RadioGroup,
+  Spin,
+  Textarea,
+} from 'ant-design-vue';
+
+import {
+  getReviewStatisticsPrescriptionDetailMethod,
+  listReviewStatisticsDetailIndicatorCategoriesMethod,
+  listReviewStatisticsDetailIndicatorsByCategoryMethod,
+} from '#/api';
+
+interface CategoryGroup {
+  categoryId: string;
+  categoryName: string;
+  indicators: PrescriptionReviewModel.ReviewIndicator[];
+}
+
+const router = useRouter();
+const route = useRoute();
+
+const loading = ref(false);
+const detail = ref<PrescriptionReviewModel.ReviewStatisticsPrescriptionDetail>();
+const indicatorGroups = ref<CategoryGroup[]>([]);
+const qualified = ref(true);
+
+const taskId = computed(() => {
+  const id = route.params.id;
+  return Array.isArray(id) ? id[0] : id;
+});
+
+const prescriptionId = computed(() => {
+  const id = route.params.prescriptionId;
+  return Array.isArray(id) ? id[0] : id;
+});
+
+const selectedIndicatorIds = computed(
+  () => detail.value?.reviewResult?.indicatorIds ?? [],
+);
+
+const herbIndicatorMap = computed(
+  () => detail.value?.reviewResult?.herbIndicatorMap ?? {},
+);
+
+const reviewComment = computed(
+  () =>
+    detail.value?.reviewResult?.comment ??
+    detail.value?.comment ??
+    '',
+);
+
+function getHerbName(herb: string) {
+  return herb.split(' ')[0] ?? herb;
+}
+
+function getHerbIssueLabel(herb: string) {
+  const name = getHerbName(herb);
+  return detail.value?.herbIssueLabels?.[name];
+}
+
+function isIndicatorChecked(indicatorId: string) {
+  return selectedIndicatorIds.value.includes(indicatorId);
+}
+
+function needsHerbInput(indicator: PrescriptionReviewModel.ReviewIndicator) {
+  return indicator.categoryId === 'cat-2' || indicator.associatedChineseMedicine;
+}
+
+function getSelectedHerbs(indicatorId: string) {
+  return herbIndicatorMap.value[indicatorId]?.join('、') ?? '';
+}
+
+async function loadDetail() {
+  if (!taskId.value || !prescriptionId.value) return;
+  loading.value = true;
+  try {
+    const [detailData, categories] = await Promise.all([
+      getReviewStatisticsPrescriptionDetailMethod(
+        taskId.value,
+        prescriptionId.value,
+      ),
+      listReviewStatisticsDetailIndicatorCategoriesMethod(),
+    ]);
+    detail.value = detailData;
+    qualified.value =
+      detailData.reviewResult?.qualified ?? detailData.result === 'qualified';
+    const groups = await Promise.all(
+      categories.map(async (category) => ({
+        categoryId: category.id,
+        categoryName: category.name,
+        indicators: await listReviewStatisticsDetailIndicatorsByCategoryMethod(
+          category.id,
+        ),
+      })),
+    );
+    indicatorGroups.value = groups.filter((item) => item.indicators.length > 0);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function goBack() {
+  router.push(`/prescription-review/task/statistics/${taskId.value}`);
+}
+
+onMounted(loadDetail);
+</script>
+
+<template>
+  <Page auto-content-height class="statistics-detail-page">
+    <Spin :spinning="loading">
+      <div class="detail-header">
+        <Button type="link" @click="goBack">返回</Button>
+      </div>
+      <div v-if="detail" class="detail-body">
+        <div class="prescription-panel">
+          <div class="panel-title">处方详情</div>
+          <div class="info-grid">
+            <div class="info-item">
+              <span class="info-label">医疗机构:</span>
+              <span>{{ detail.institutionName }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">科室:</span>
+              <span>{{ detail.departmentName }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">医生:</span>
+              <span>{{ detail.doctorName }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">药师:</span>
+              <span>{{ detail.pharmacistName || '—' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">处方号:</span>
+              <a class="text-primary">{{ detail.prescriptionNo }}</a>
+            </div>
+            <div class="info-item">
+              <span class="info-label">姓名:</span>
+              <span>{{ detail.patientName }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">性别:</span>
+              <span>{{ detail.patientGender }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">年龄:</span>
+              <span>{{ detail.patientAge }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">中医病名:</span>
+              <span>{{ detail.tcmDisease }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">中医证候:</span>
+              <span>{{ detail.tcmSyndrome }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">治则治法:</span>
+              <span>{{ detail.treatmentPrinciple }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">服法:</span>
+              <span>{{ detail.administrationMethod }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">药味数:</span>
+              <span>{{ detail.herbCount }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">妊娠:</span>
+              <span>{{ detail.pregnancy ? '是' : '否' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">哺乳:</span>
+              <span>{{ detail.lactation ? '是' : '否' }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">剂数:</span>
+              <span>{{ detail.doseCount }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">总金额:</span>
+              <span>{{ detail.totalAmount }}</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">单剂金额:</span>
+              <span>{{ detail.unitDoseAmount }}</span>
+            </div>
+          </div>
+          <div class="herb-section">
+            <div class="herb-title">明细</div>
+            <div class="herb-grid">
+              <span
+                v-for="(herb, idx) in detail.herbs"
+                :key="idx"
+                class="herb-item"
+              >
+                {{ herb }}
+                <span v-if="getHerbIssueLabel(herb)" class="herb-issue">
+                  {{ getHerbIssueLabel(herb) }}
+                </span>
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="review-panel">
+          <div class="panel-title">点评结果</div>
+          <div class="review-status-row">
+            <RadioGroup v-model:value="qualified">
+              <Radio :value="true">合格</Radio>
+              <Radio :value="false">不合格</Radio>
+            </RadioGroup>
+          </div>
+          <div class="review-meta">
+            <span>点评专家:{{ detail.reviewExpert || '—' }}</span>
+            <span>点评时间:{{ detail.reviewTime || '—' }}</span>
+          </div>
+          <div class="comment-section">
+            <div class="comment-label">点评意见和说明</div>
+            <Textarea
+              :rows="4"
+              :value="reviewComment"
+              disabled
+              placeholder="暂无点评意见"
+            />
+          </div>
+          <div
+            v-for="group in indicatorGroups"
+            :key="group.categoryId"
+            class="indicator-group"
+          >
+            <div class="group-title">{{ group.categoryName }}</div>
+            <div class="group-items">
+              <div
+                v-for="indicator in group.indicators"
+                :key="indicator.id"
+                class="indicator-item"
+              >
+                <Checkbox
+                  :checked="isIndicatorChecked(indicator.id)"
+                  disabled
+                >
+                  {{ indicator.name }}
+                </Checkbox>
+                <Input
+                  v-if="
+                    isIndicatorChecked(indicator.id) &&
+                    needsHerbInput(indicator)
+                  "
+                  :value="getSelectedHerbs(indicator.id)"
+                  class="herb-input"
+                  disabled
+                  size="small"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </Spin>
+  </Page>
+</template>
+
+<style scoped>
+.detail-header {
+  margin-bottom: 12px;
+}
+
+.detail-body {
+  display: flex;
+  gap: 0;
+  min-height: calc(100vh - 180px);
+  background: #fff;
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+}
+
+.prescription-panel {
+  flex: 1;
+  min-width: 0;
+  padding: 16px 20px;
+  border-right: 1px solid #f0f0f0;
+}
+
+.review-panel {
+  flex: 1;
+  min-width: 0;
+  padding: 16px 20px;
+  overflow-y: auto;
+}
+
+.panel-title {
+  margin-bottom: 16px;
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 10px 24px;
+  margin-bottom: 20px;
+  font-size: 13px;
+}
+
+.info-label {
+  color: rgb(0 0 0 / 65%);
+}
+
+.herb-section {
+  padding: 12px;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.herb-title {
+  margin-bottom: 10px;
+  font-weight: 500;
+}
+
+.herb-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 8px 12px;
+}
+
+.herb-item {
+  font-size: 13px;
+}
+
+.herb-issue {
+  margin-left: 4px;
+  color: #ff4d4f;
+}
+
+.review-status-row {
+  margin-bottom: 12px;
+}
+
+.review-meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px 32px;
+  margin-bottom: 16px;
+  font-size: 13px;
+  color: rgb(0 0 0 / 65%);
+}
+
+.comment-section {
+  margin-bottom: 20px;
+}
+
+.comment-label {
+  margin-bottom: 8px;
+  font-size: 13px;
+  color: rgb(0 0 0 / 65%);
+}
+
+.indicator-group {
+  margin-bottom: 16px;
+}
+
+.group-title {
+  margin-bottom: 8px;
+  font-weight: 600;
+}
+
+.group-items {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 20px;
+}
+
+.indicator-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.herb-input {
+  width: 100px;
+}
+</style>

+ 112 - 0
apps/smart-pharmacy/src/views/prescription-review/task/statistics.vue

@@ -0,0 +1,112 @@
+<script lang="ts" setup>
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import { Button, Spin, Tabs } from 'ant-design-vue';
+
+import { getReviewTaskMethod } from '#/api';
+
+import CostReport from './modules/statistics/cost-report.vue';
+import PrescriptionTable from './modules/statistics/prescription-table.vue';
+import ResultSummary from './modules/statistics/result-summary.vue';
+
+const router = useRouter();
+const currentRoute = useRoute();
+const loading = ref(false);
+const taskName = ref('');
+const activeTab = ref('prescription-table');
+
+const taskId = computed(() => {
+  const id = currentRoute.params.id;
+  return Array.isArray(id) ? id[0] : id;
+});
+
+async function loadTask() {
+  if (!taskId.value) return;
+  loading.value = true;
+  try {
+    const task = await getReviewTaskMethod(taskId.value);
+    taskName.value = task.name;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function goBack() {
+  router.push('/prescription-review/task');
+}
+
+onMounted(loadTask);
+</script>
+
+<template>
+  <Page auto-content-height class="review-statistics-page">
+    <Spin :spinning="loading">
+      <div class="statistics-header">
+        <Button type="link" @click="goBack">返回</Button>
+        <div class="statistics-title-wrap">
+          <h2 class="statistics-title">{{ taskName || '点评任务' }}</h2>
+          <p class="statistics-subtitle">点评单结果统计</p>
+        </div>
+      </div>
+      <Tabs v-model:active-key="activeTab" class="statistics-tabs">
+        <Tabs.TabPane key="prescription-table" tab="处方点评表">
+          <PrescriptionTable v-if="taskId" :task-id="taskId" />
+        </Tabs.TabPane>
+        <Tabs.TabPane key="cost-report" tab="均剂费用汇报表">
+          <CostReport v-if="taskId" :task-id="taskId" />
+        </Tabs.TabPane>
+        <Tabs.TabPane key="result-summary" tab="点评结果汇报">
+          <ResultSummary v-if="taskId" :task-id="taskId" />
+        </Tabs.TabPane>
+      </Tabs>
+    </Spin>
+  </Page>
+</template>
+
+<style scoped>
+.statistics-header {
+  position: relative;
+  margin-bottom: 8px;
+  text-align: center;
+}
+
+.statistics-title-wrap {
+  padding: 0 80px;
+}
+
+.statistics-title {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 500;
+  line-height: 1.5;
+}
+
+.statistics-subtitle {
+  margin: 4px 0 0;
+  font-size: 14px;
+  color: rgb(0 0 0 / 65%);
+}
+
+.statistics-header :deep(.ant-btn) {
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.statistics-tabs :deep(.ant-tabs-nav) {
+  margin-bottom: 16px;
+}
+
+.statistics-tabs :deep(.ant-tabs-tab) {
+  padding: 8px 0;
+  margin-right: 32px;
+}
+
+.statistics-tabs :deep(.ant-tabs-content) {
+  padding: 0 4px;
+}
+</style>