586 lines
25 KiB
Markdown
586 lines
25 KiB
Markdown
# 中试服务申请单 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<string, string> = { dks: '待开始', jxz: '进行中', ywc: '已完成', yzf: '已作废' }
|
||
return map[status] || status || '-'
|
||
}
|
||
export function applicationStatusTagType(status: string): '' | 'success' | 'warning' | 'danger' | 'info' {
|
||
const map: Record<string, '' | 'success' | 'warning' | 'danger' | 'info'> = {
|
||
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 函数参数一致
|