# 中试服务申请单 Phase 1 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现中试服务申请单的完整 CRUD、患者信息管理、输注方案/计划管理,含后端 API + 前端列表页+详情Tab页。 **Architecture:** 独立后端模块方案(方案A)。新建 `crud/pilot_application.py` 和 `api/pilot_application.py`,在 `models.py` 中新增 4 个 Model。前端新增列表页+详情页,路由追加到 `sqxp.ts`。 **Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 / Vue 3 / TypeScript / Element Plus / Tailwind CSS 4 **Spec:** `docs/superpowers/specs/2026-05-24-pilot-application-phase1-design.md` --- ## Task 1: SQL 迁移文件 + Backend Models **Files:** - Create: `docs/sql/pilot-application-migration.sql` - Modify: `backend/app/models/models.py` - [ ] **Step 1: 创建 SQL 迁移文件** 创建 `docs/sql/pilot-application-migration.sql`,内容为 ALTER `biz_pilot_application` 增加 4 个字段 + 唯一索引: ```sql -- 中试服务申请单 Phase 1 迁移 -- 为 biz_pilot_application 增加申请单号、状态、渠道缓存字段 ALTER TABLE biz_pilot_application ADD COLUMN application_no VARCHAR(64) DEFAULT NULL COMMENT '申请单号(自动生成,前缀ZSSQ)' AFTER id, ADD COLUMN application_status VARCHAR(16) NOT NULL DEFAULT 'dks' COMMENT 'dks待开始 jxz进行中 ywc已完成 yzf已作废' AFTER application_no, ADD COLUMN channel_id VARCHAR(64) DEFAULT NULL COMMENT '渠道ID(缓存自服务订单)' AFTER channel_service_order_id, ADD COLUMN channel_name VARCHAR(200) DEFAULT NULL COMMENT '渠道名称(缓存)' AFTER channel_id; ALTER TABLE biz_pilot_application ADD UNIQUE KEY uk_application_no (application_no, del_flag); ``` - [ ] **Step 2: 在 models.py 末尾(BizChannelServiceOrderTxn 之后)新增 4 个 Model** 遵循项目已有 Model 模式(参考 `BizChannelServiceOrder` 在 L1761 的写法): - 字段顺序:id → 业务字段 → public 字段(tenant_no / order_num / remark / create_by / create_dept_id / create_time / update_by / update_time / del_flag) - 类型映射:VARCHAR→String, INT→Integer, TINYINT→SmallInteger, DATETIME→DateTime, DECIMAL→Numeric - 添加 `__tablename__` 和中文注释 新增的 4 个 Model: **BizPilotApplication** → `biz_pilot_application` - id, application_no, application_status(default='dks'), channel_service_order_id, channel_id, channel_name - + public fields **BizPilotPatient** → `biz_pilot_patient` - id, pilot_application_id, customer_id, patient_name, sex, age, id_card_no, phone_no, emergency_contact, emergency_phone, clinical_diagnosis, diagnosis_attachments, need_checkup(default=1) - + public fields **BizPilotInfusionPlan** → `biz_pilot_infusion_plan` - id, pilot_application_id, cell_type, spec_text, total_course_count, course_interval, infusion_route, infusion_start_date - + public fields **BizPilotInfusionSchedule** → `biz_pilot_infusion_schedule` - id, infusion_plan_id, cell_type, spec_text, infusion_time, schedule_status(default=''), attachments - + public fields - [ ] **Step 3: 验证** Run: `cd backend && source venv/bin/activate && python -c "from app.models.models import BizPilotApplication, BizPilotPatient, BizPilotInfusionPlan, BizPilotInfusionSchedule; print('Models imported OK')"` Expected: `Models imported OK` - [ ] **Step 4: 运行 LSP 诊断** Run `lsp_diagnostics` on `backend/app/models/models.py` — expect 0 errors. --- ## Task 2: Backend CRUD **Files:** - Create: `backend/app/crud/pilot_application.py` - [ ] **Step 1: 创建 CRUD 文件** 创建 `backend/app/crud/pilot_application.py`,遵循 `crud/pilot_service.py` 的模式(`_row_to_dict_*` + 分页列表 + CRUD 函数)。 **import 部分:** ```python import uuid import json from datetime import datetime, date from typing import Dict, List, Optional, Any from sqlalchemy.orm import Session from sqlalchemy import and_ from app.models.models import ( BizPilotApplication, BizPilotPatient, BizPilotInfusionPlan, BizPilotInfusionSchedule, BizChannelServiceOrder, BizChannelServiceOrderProduct, BizCustomer ) from app.core.database import get_db ``` **_row_to_dict 函数(4 个):** 1. `_row_to_dict_application(row, db=None, tenant_no=None)`: - 基础字段:id, applicationNo, applicationStatus, channelServiceOrderId, channelId, channelName, remark, createTime, updateTime - 扩展字段(需 db 查询): - patientName: 从 BizPilotPatient 查询(WHERE pilot_application_id=row.id AND del_flag=0) - infusionPlanCount: 从 BizPilotInfusionPlan COUNT(WHERE pilot_application_id=row.id AND del_flag=0) - serviceOrderNo: 从 BizChannelServiceOrder 查询(WHERE id=row.channel_service_order_id)取其关联的订单信息 - orderType: 同上,服务订单的 order_type 2. `_row_to_dict_patient(row)`: - id, pilotApplicationId, customerId, patientName, sex, age, idCardNo, phoneNo, emergencyContact, emergencyPhone, clinicalDiagnosis, diagnosisAttachments, needCheckup, remark, createTime, updateTime 3. `_row_to_dict_infusion_plan(row, db=None, tenant_no=None)`: - id, pilotApplicationId, cellType, specText, totalCourseCount, courseInterval, infusionRoute, infusionStartDate, remark, createTime, updateTime - 扩展:scheduleCount (COUNT schedules), completedCount (COUNT WHERE schedule_status='ywc') 4. `_row_to_dict_infusion_schedule(row)`: - id, infusionPlanId, cellType, specText, infusionTime, scheduleStatus, attachments, remark, createTime, updateTime **CRUD 函数(16 个):** **Application(8 个):** - `list_applications(db, tenant_no, page, page_size, channel_id=None, application_status=None, channel_service_order_id=None)` — 分页列表,按 create_time DESC 排序 - `get_application(db, app_id, tenant_no)` — 获取详情 - `create_application(db, data, tenant_no, user_id, dept_id)` — 校验服务订单存在且 status='sxz',从服务订单缓存 channel_id/channel_name,生成 application_no(ZSSQ+YYYYMMDD+4位序列) - `update_application(db, app_id, data, tenant_no)` — 仅 dks 状态可编辑 - `soft_delete_application(db, app_id, tenant_no)` — 仅 dks 状态 - `start_application(db, app_id, tenant_no, user_id)` — dks→jxz - `complete_application(db, app_id, tenant_no, user_id)` — jxz→ywc - `void_application(db, app_id, tenant_no, user_id)` — dks或jxz→yzf **Patient(2 个):** - `get_patient_by_application(db, app_id, tenant_no)` - `save_patient(db, app_id, data, tenant_no, user_id, dept_id)` — 如果有 customer_id 则更新 biz_customer;如果无则新建 biz_customer 并回填 customer_id。然后用 upsert 逻辑保存 biz_pilot_patient(先查再更新或新建)。 **InfusionPlan(4 个):** - `list_infusion_plans(db, app_id, tenant_no)` — 按 order_num / create_time ASC - `create_infusion_plan(db, app_id, data, tenant_no, user_id, dept_id)` - `update_infusion_plan(db, plan_id, data, tenant_no)` - `soft_delete_infusion_plan(db, plan_id, tenant_no)` — 级联软删除其下所有 schedule **InfusionSchedule(6 个):** - `list_schedules_by_plan(db, plan_id, tenant_no)` — 按 order_num ASC - `auto_generate_schedules(db, plan_id, tenant_no, user_id, dept_id)` — 读取 plan.total_course_count,批量插入 N 条 schedule - `create_schedule(db, plan_id, data, tenant_no, user_id, dept_id)` — 手动添加单条 - `update_schedule(db, schedule_id, data, tenant_no)` - `soft_delete_schedule(db, schedule_id, tenant_no)` - `mark_schedule_complete(db, schedule_id, tenant_no, user_id)` — schedule_status='ywc' **application_no 生成逻辑:** ```python def _generate_application_no(db, tenant_no): today_str = datetime.now().strftime('%Y%m%d') prefix = f'ZSSQ{today_str}' # 查今天已有的最大序号 existing = db.query(BizPilotApplication.application_no).filter( BizPilotApplication.tenant_no == tenant_no, BizPilotApplication.application_no.like(f'{prefix}%'), BizPilotApplication.del_flag == 0 ).order_by(BizPilotApplication.application_no.desc()).first() seq = 1 if existing and existing[0]: seq = int(existing[0][-4:]) + 1 return f'{prefix}{seq:04d}' ``` - [ ] **Step 2: 验证导入** Run: `cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import list_applications; print('CRUD imported OK')"` Expected: `CRUD imported OK` - [ ] **Step 3: 运行 LSP 诊断** Run `lsp_diagnostics` on `backend/app/crud/pilot_application.py` — expect 0 errors. --- ## Task 3: Backend API Routes + Router Registration **Files:** - Create: `backend/app/api/pilot_application.py` - Modify: `backend/app/api/__init__.py` - Modify: `backend/app/main.py` - [ ] **Step 1: 创建 API 路由文件** 创建 `backend/app/api/pilot_application.py`,遵循 `api/pilot_service.py` 的模式(Pydantic schemas 内联 + 路由函数)。 **import 部分:** ```python from fastapi import APIRouter, Depends, Query, Request from sqlalchemy.orm import Session from pydantic import BaseModel, Field from typing import Optional, List from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.crud import pilot_application ``` **Pydantic Schemas(6 个):** ```python class ApplicationCreateBody(BaseModel): channel_service_order_id: str remark: Optional[str] = None class ApplicationUpdateBody(BaseModel): remark: Optional[str] = None class PatientSaveBody(BaseModel): customer_id: Optional[str] = None patient_name: str sex: Optional[str] = None age: Optional[int] = None id_card_no: Optional[str] = None phone_no: Optional[str] = None emergency_contact: Optional[str] = None emergency_phone: Optional[str] = None clinical_diagnosis: Optional[str] = None diagnosis_attachments: Optional[str] = None need_checkup: int = 1 class InfusionPlanCreateBody(BaseModel): pilot_application_id: str cell_type: str spec_text: Optional[str] = None total_course_count: int course_interval: Optional[str] = None infusion_route: Optional[str] = None infusion_start_date: Optional[str] = None class InfusionPlanUpdateBody(BaseModel): cell_type: Optional[str] = None spec_text: Optional[str] = None total_course_count: Optional[int] = None course_interval: Optional[str] = None infusion_route: Optional[str] = None infusion_start_date: Optional[str] = None class InfusionScheduleUpdateBody(BaseModel): infusion_time: Optional[str] = None remark: Optional[str] = None attachments: Optional[str] = None ``` **Router:** ```python router = APIRouter(prefix="/web/admin", tags=["中试服务申请单管理"]) ``` **API 路由(19 个):** | 方法 | 路径 | 函数名 | 依赖 | 说明 | |------|------|--------|------|------| | GET | `/pilot-application/list` | api_list_applications | require_admin | 分页列表,query params: page/page_size/channel_id/application_status/channel_service_order_id | | GET | `/pilot-application/detail` | api_get_application | require_admin | query param: id | | POST | `/pilot-application/create` | api_create_application | require_admin | body: ApplicationCreateBody | | PUT | `/pilot-application/update` | api_update_application | require_admin | body: ApplicationUpdateBody + id | | DELETE | `/pilot-application/delete` | api_delete_application | require_admin | body: {id} | | POST | `/pilot-application/start` | api_start_application | require_admin | body: {id} | | POST | `/pilot-application/complete` | api_complete_application | require_admin | body: {id} | | POST | `/pilot-application/void` | api_void_application | require_admin | body: {id} | | GET | `/pilot-patient/detail` | api_get_patient | require_admin | query param: application_id | | POST | `/pilot-patient/save` | api_save_patient | require_admin | body: PatientSaveBody + application_id | | GET | `/pilot-infusion-plan/list` | api_list_plans | require_admin | query param: application_id | | POST | `/pilot-infusion-plan/create` | api_create_plan | require_admin | body: InfusionPlanCreateBody | | PUT | `/pilot-infusion-plan/update` | api_update_plan | require_admin | body: InfusionPlanUpdateBody + id | | DELETE | `/pilot-infusion-plan/delete` | api_delete_plan | require_admin | body: {id} | | POST | `/pilot-infusion-schedule/auto-generate` | api_auto_generate | require_admin | body: {plan_id} | | POST | `/pilot-infusion-schedule/create` | api_create_schedule | require_admin | body: {plan_id, infusion_time?, remark?} | | PUT | `/pilot-infusion-schedule/update` | api_update_schedule | require_admin | body: InfusionScheduleUpdateBody + id | | DELETE | `/pilot-infusion-schedule/delete` | api_delete_schedule | require_admin | body: {id} | | POST | `/pilot-infusion-schedule/mark-complete` | api_mark_complete | require_admin | body: {id} | 每个路由函数结构: ```python @router.get("/pilot-application/list") def api_list_applications( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), channel_id: Optional[str] = Query(None), application_status: Optional[str] = Query(None), channel_service_order_id: Optional[str] = Query(None), _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _user["tenant_no"] result = pilot_application.list_applications(db, tenant_no, page, page_size, channel_id, application_status, channel_service_order_id) return {"code": 0, "data": result} ``` - [ ] **Step 2: 注册路由** 在 `backend/app/api/__init__.py` 中追加: ```python from app.api.pilot_application import router as pilot_application_router ``` 并在 `routers` 列表中追加 `pilot_application_router`。 在 `backend/app/main.py` 中追加路由 include(参考已有的 `pilot_service_router` 注册方式)。 - [ ] **Step 3: 验证** Run: `cd backend && source venv/bin/activate && python -c "from app.api.pilot_application import router; print(f'Routes: {len(router.routes)}')"` Expected: `Routes: 19` - [ ] **Step 4: 运行 LSP 诊断** Run `lsp_diagnostics` on `backend/app/api/pilot_application.py`, `backend/app/api/__init__.py`, `backend/app/main.py` — expect 0 errors on all. --- ## Task 4: Frontend API 函数 + 状态标签 + 路由 **Files:** - Modify: `web-admin/src/api/admin.ts` - Modify: `web-admin/src/utils/status-tag-registry.ts` - Modify: `web-admin/src/router/modules/sqxp.ts` - [ ] **Step 1: 在 admin.ts 末尾追加 19 个 API 函数** 遵循文件中已有函数模式(参考 `fetchPilotApplicationList` 风格),追加以下函数: ```typescript // ==================== 中试服务申请单 ==================== export function fetchPilotApplicationList(params: any) { return request({ url: '/pilot-application/list', method: 'get', params }) } export function fetchPilotApplicationDetail(params: any) { return request({ url: '/pilot-application/detail', method: 'get', params }) } export function createPilotApplication(data: any) { return request({ url: '/pilot-application/create', method: 'post', data }) } export function updatePilotApplication(data: any) { return request({ url: '/pilot-application/update', method: 'put', data }) } export function deletePilotApplication(data: any) { return request({ url: '/pilot-application/delete', method: 'delete', data }) } export function startPilotApplication(data: any) { return request({ url: '/pilot-application/start', method: 'post', data }) } export function completePilotApplication(data: any) { return request({ url: '/pilot-application/complete', method: 'post', data }) } export function voidPilotApplication(data: any) { return request({ url: '/pilot-application/void', method: 'post', data }) } // 患者信息 export function fetchPilotPatient(params: any) { return request({ url: '/pilot-patient/detail', method: 'get', params }) } export function savePilotPatient(data: any) { return request({ url: '/pilot-patient/save', method: 'post', data }) } // 输注方案 export function fetchPilotInfusionPlanList(params: any) { return request({ url: '/pilot-infusion-plan/list', method: 'get', params }) } export function createPilotInfusionPlan(data: any) { return request({ url: '/pilot-infusion-plan/create', method: 'post', data }) } export function updatePilotInfusionPlan(data: any) { return request({ url: '/pilot-infusion-plan/update', method: 'put', data }) } export function deletePilotInfusionPlan(data: any) { return request({ url: '/pilot-infusion-plan/delete', method: 'delete', data }) } // 输注计划 export function fetchPilotInfusionScheduleList(params: any) { return request({ url: '/pilot-infusion-schedule/list', method: 'get', params }) } export function autoGenerateSchedules(data: any) { return request({ url: '/pilot-infusion-schedule/auto-generate', method: 'post', data }) } export function updatePilotInfusionSchedule(data: any) { return request({ url: '/pilot-infusion-schedule/update', method: 'put', data }) } export function deletePilotInfusionSchedule(data: any) { return request({ url: '/pilot-infusion-schedule/delete', method: 'delete', data }) } export function markScheduleComplete(data: any) { return request({ url: '/pilot-infusion-schedule/mark-complete', method: 'post', data }) } ``` - [ ] **Step 2: 在 status-tag-registry.ts 追加申请单状态标签函数** ```typescript /** 中试申请单状态 */ export function applicationStatusLabel(status: string): string { const map: Record = { dks: '待开始', jxz: '进行中', ywc: '已完成', yzf: '已作废' } return map[status] || status || '-' } export function applicationStatusTagType(status: string): '' | 'success' | 'warning' | 'danger' | 'info' { const map: Record = { dks: 'info', jxz: '', ywc: 'success', yzf: 'danger' } return map[status] || 'info' } ``` - [ ] **Step 3: 在 sqxp.ts 追加 2 个路由** 在 PilotService 的 children 数组末尾追加: ```typescript { path: 'pilot-application', name: 'PilotApplication', component: () => import('@views/pilot-service/pilot-application/index.vue'), meta: { title: '中试服务申请单', icon: 'i-ep:document' } }, { path: 'pilot-application/detail', name: 'PilotApplicationDetail', component: () => import('@views/pilot-service/pilot-application/ApplicationDetail.vue'), meta: { title: '申请单详情', hidden: true } }, ``` 注意:路由 ID 需要根据现有 PilotService 的 ID 段确定,查看 sqxp.ts 中已有的路由 ID 模式。中试服务模块 ID 段为 8000...,检查当前已使用的最大 ID 后递增。 - [ ] **Step 4: 运行 LSP 诊断** Run `lsp_diagnostics` on all 3 modified files — expect 0 errors. --- ## Task 5: 前端列表页 **Files:** - Create: `web-admin/src/views/pilot-service/pilot-application/index.vue` - [ ] **Step 1: 创建列表页** 创建目录 `web-admin/src/views/pilot-service/pilot-application/` 和 `index.vue`。 **参考模板页:** `src/views/system/order/index.vue`(复杂列表页) **页面结构:** - 筛选区:申请单号(el-input)、渠道(el-select,数据源 fetchChannelList)、服务订单(el-select,联动渠道)、状态(el-radio-group 或 tag 按钮) - 操作栏:新增按钮(el-button type="primary") - 表格(el-table):列如 spec 中定义 - 分页(el-pagination) **关键交互:** - 新增:弹窗(el-dialog),选择渠道 → 联动加载该渠道的服务订单(调用已有的 fetchServiceOrderList,传入 channel_id 筛选)→ 选择服务订单 → 填写备注 → 提交 → 成功后 router.push 到详情页 - 状态操作:根据 applicationStatus 动态显示按钮 - dks:显示"开始"(primary) + "作废"(danger) - jxz:显示"完成"(primary) + "作废"(danger) - ywc/yzf:无按钮 - 编辑/删除:仅 dks 状态可操作 - 行点击:router.push({ name: 'PilotApplicationDetail', query: { id: row.id } }) - 状态列用 el-tag + applicationStatusLabel/applicationStatusTagType **数据加载:** onMounted 调用 fetchPilotApplicationList,筛选变更时重新加载。 **样式:** 遵循项目 Fresh Mint 色系(--primary #10b981),使用已有的 SCSS 变量和 mixin。 - [ ] **Step 2: 运行 LSP 诊断** Run `lsp_diagnostics` on `web-admin/src/views/pilot-service/pilot-application/index.vue` — expect 0 errors. --- ## Task 6: 前端详情页 — 基本信息 Tab + 输注方案 Tab **Files:** - Create: `web-admin/src/views/pilot-service/pilot-application/ApplicationDetail.vue` - [ ] **Step 1: 创建详情页** **参考模板页:** `src/views/system/product/index.vue`(含详情展示的复杂页面) **页面结构:** - 顶部:返回按钮 + 申请单号 + 状态标签 + 操作按钮(开始/完成/作废,根据状态显示) - el-tabs:4 个 tab(基本信息 / 输注方案 / 体检信息-locked / 交付信息-locked) **基本信息 Tab:** - 上半区:el-descriptions 展示申请单信息(只读) - 申请单号、渠道名称、关联服务订单、状态、创建时间、备注 - 下半区:患者信息面板(el-card) - "选择客户"按钮 → el-dialog 弹窗展示客户列表(调用已有 API 获取 biz_customer 列表,支持姓名/手机搜索) - 选中后自动填充表单字段 - 表单字段:patientName, sex, age, idCardNo, phoneNo, emergencyContact, emergencyPhone, clinicalDiagnosis, diagnosisAttachments(附件上传), needCheckup(el-switch) - "保存患者信息"按钮 **输注方案 Tab:** - 工具栏:"新增输注方案"按钮 - 方案列表用 el-table,每行显示方案的完整信息 - 最后一列加 expand 展开行,展开后显示该方案的输注计划列表 - 展开行内的计划列表(el-table): - 列:序号、细胞类型、规格、输注时间(可编辑)、状态、操作 - 操作:编辑(弹窗修改时间/备注)、删除、标记完成 - "手动添加计划"按钮在展开行内 **新增/编辑输注方案弹窗(el-dialog):** - 表单字段:cell_type(el-select,数据源从细胞产品类型 API)、spec_text、total_course_count(el-input-number)、course_interval、infusion_route、infusion_start_date(el-date-picker) - label-position="top",遵循 .form-grid-2 两列布局 - 保存成功后 ElMessageBox.confirm("是否立即生成 N 条输注计划?"),确认则调用 autoGenerateSchedules **数据加载:** onMounted 根据 route.query.id 调用 fetchPilotApplicationDetail 获取详情(含患者+方案),同时 fetchPilotInfusionPlanList 获取方案列表,每个方案再 fetchPilotInfusionScheduleList 获取计划。 **样式约定:** - Dialog forms: `label-position="top"`, `.dialog-form-layout`, `.form-grid-2` - 无 backdrop blur: `backdrop-filter: none` - Panel heading: `.title-bar-heading` + `::before` 竖线 - 状态 Tag: el-tag + semantic colors - [ ] **Step 2: 运行 LSP 诊断** Run `lsp_diagnostics` on `web-admin/src/views/pilot-service/pilot-application/ApplicationDetail.vue` — expect 0 errors. --- ## Task 7: 集成验证 **Files:** 无新文件 - [ ] **Step 1: 后端启动验证** Run: `cd backend && source venv/bin/activate && python -c " from app.main import app routes = [r.path for r in app.routes if hasattr(r, 'path')] pilot_routes = [r for r in routes if 'pilot-application' in r or 'pilot-patient' in r or 'pilot-infusion' in r] print(f'Pilot application routes: {len(pilot_routes)}') for r in sorted(pilot_routes): print(f' {r}') "` Expected: 19 routes listed - [ ] **Step 2: 前端构建验证** Run: `cd web-admin && pnpm build` Expected: Build succeeds with exit code 0 - [ ] **Step 3: 前端 LSP 全量诊断** Run `lsp_diagnostics` on directory `web-admin/src/views/pilot-service/pilot-application/` — expect 0 errors. - [ ] **Step 4: 更新知识图谱** Run: `./scripts/update-graph.sh backend frontend` Expected: 知识图谱更新完成 --- ## 自审检查 ### Spec Coverage | Spec 需求 | 对应 Task | |-----------|----------| | ALTER biz_pilot_application | Task 1 | | 4 个 Model 类 | Task 1 | | Application CRUD 8 个函数 | Task 2 | | Patient save/get 2 个函数 | Task 2 | | InfusionPlan CRUD 4 个函数 | Task 2 | | InfusionSchedule 6 个函数 | Task 2 | | API routes 19 个 | Task 3 | | Router registration | Task 3 | | 19 个前端 API 函数 | Task 4 | | 状态标签函数 | Task 4 | | 路由追加 | Task 4 | | 列表页 | Task 5 | | 详情页(基本信息+输注方案) | Task 6 | | 集成验证 | Task 7 | ### Placeholder 扫描 无 TBD/TODO/占位符。所有步骤含具体代码或明确指令。 ### Type 一致性 - Model 字段名与 CRUD _row_to_dict 输出的 camelCase key 一致 - API 路径与前端 request URL 一致 - Pydantic schema 字段与 CRUD 函数参数一致