368 lines
12 KiB
Markdown
368 lines
12 KiB
Markdown
# 质检工单 Tab 拆分与分型展示 Implementation Plan
|
||
|
||
> **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:** 将质检工单与我的质检任务从 3 Tab 拆为 6 Tab,服务端强制按子类型过滤;Tab 子页面拷贝再改;六份独立查看弹窗;CardList 共用 + cardMetaConfig props。
|
||
|
||
**Architecture:** `tabs/*` 六份子页面从现有入库/出库/临时区块拷贝;`view-dialogs/*` 六份独立查看界面(不共用 Host);`QcWorkOrderTaskSubList` 与配置检测弹窗共用;`QcWorkOrderCardList` + SampleGroup 共用,props 传 `cardMetaConfig`;检测任务 sections 抽 `useQcWorkOrderTaskViewSections` 供六弹窗 import。后端 `list_work_orders` / `list_mine_tasks` 增加 `receptionSource` 过滤。
|
||
|
||
**Tech Stack:** Vue 3 + Element Plus(web-admin)、FastAPI + SQLAlchemy(backend)、TypeScript strict:false
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-06-16-qc-work-order-tab-split-design.md`
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `web-admin/src/utils/qc-work-order-tabs.ts` | **新建** Tab 常量、`toListQueryParams` |
|
||
| `web-admin/src/utils/qc-work-order-card-meta.ts` | **新建** 六 Tab 的 `cardMetaConfig`(CardList props) |
|
||
| `web-admin/src/views/.../qc-work-order/tabs/*.vue` | **新建×6** 从 index 拷贝的 Tab 子页面 |
|
||
| `web-admin/src/views/.../qc-work-order/view-dialogs/*.vue` | **新建×6** 独立查看弹窗 |
|
||
| `web-admin/src/views/.../qc-work-order/composables/useQcWorkOrderTaskViewSections.ts` | **新建** 检测任务 sections(六弹窗共用) |
|
||
| `backend/app/crud/qc_work_order.py` | `list_work_orders` + `reception_source` |
|
||
| `backend/app/crud/qc_task.py` | `list_mine_tasks` + `reception_source` / `biz_type` |
|
||
| `QcWorkOrderCardList.vue` / `SingleCard.vue` / `SampleGroup.vue` | 接收 `cardMetaConfig` props |
|
||
| `QcWorkOrderViewDialogHost.vue` | **删除**(逻辑迁入六弹窗) |
|
||
| `OutboundQcViewDialog.vue` | **删除**(拷贝为三个 outbound 弹窗后删) |
|
||
| `useQcWorkOrderViewDialog.ts` | **删除**(拆入弹窗 + task composable) |
|
||
| `qc-work-order/index.vue` | Tab 壳 + 共用配置检测弹窗 |
|
||
| `my-qc-task/index.vue` | Tab 壳,import 同一套 `tabs/*` |
|
||
|
||
---
|
||
|
||
### Task 1: Tab 配置模块(前端基础)
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/utils/qc-work-order-tabs.ts`
|
||
|
||
- [ ] **Step 1: 创建 Tab 配置**
|
||
|
||
```typescript
|
||
// web-admin/src/utils/qc-work-order-tabs.ts
|
||
export type QcWorkOrderTabKey =
|
||
| 'inbound_cell_storage'
|
||
| 'inbound_research_donation'
|
||
| 'outbound_pilot_service'
|
||
| 'outbound_basic_experiment'
|
||
| 'outbound_clinical_application'
|
||
| 'temporary'
|
||
|
||
export type QcWorkOrderTabConfig = {
|
||
key: QcWorkOrderTabKey
|
||
label: string
|
||
group: 'inbound' | 'outbound' | 'temporary'
|
||
workOrderType: 'inbound' | 'outbound' | 'temporary'
|
||
receptionSource?: 'cell_storage' | 'research_donation'
|
||
bizType?: 'pilot_service' | 'basic_experiment' | 'clinical_application'
|
||
listLayout: 'inbound-grouped' | 'standalone'
|
||
}
|
||
|
||
export const QC_WORK_ORDER_TABS: QcWorkOrderTabConfig[] = [
|
||
{
|
||
key: 'inbound_cell_storage',
|
||
label: '采集入库',
|
||
group: 'inbound',
|
||
workOrderType: 'inbound',
|
||
receptionSource: 'cell_storage',
|
||
listLayout: 'inbound-grouped'
|
||
},
|
||
{
|
||
key: 'inbound_research_donation',
|
||
label: '捐赠入库',
|
||
group: 'inbound',
|
||
workOrderType: 'inbound',
|
||
receptionSource: 'research_donation',
|
||
listLayout: 'inbound-grouped'
|
||
},
|
||
{
|
||
key: 'outbound_pilot_service',
|
||
label: '中试',
|
||
group: 'outbound',
|
||
workOrderType: 'outbound',
|
||
bizType: 'pilot_service',
|
||
listLayout: 'standalone'
|
||
},
|
||
{
|
||
key: 'outbound_basic_experiment',
|
||
label: '基础实验',
|
||
group: 'outbound',
|
||
workOrderType: 'outbound',
|
||
bizType: 'basic_experiment',
|
||
listLayout: 'standalone'
|
||
},
|
||
{
|
||
key: 'outbound_clinical_application',
|
||
label: '临床应用',
|
||
group: 'outbound',
|
||
workOrderType: 'outbound',
|
||
bizType: 'clinical_application',
|
||
listLayout: 'standalone'
|
||
},
|
||
{
|
||
key: 'temporary',
|
||
label: '临时',
|
||
group: 'temporary',
|
||
workOrderType: 'temporary',
|
||
listLayout: 'standalone'
|
||
}
|
||
]
|
||
|
||
export const DEFAULT_QC_WORK_ORDER_TAB: QcWorkOrderTabKey = 'inbound_cell_storage'
|
||
|
||
export function getQcWorkOrderTab(key: string): QcWorkOrderTabConfig | undefined {
|
||
return QC_WORK_ORDER_TABS.find((t) => t.key === key)
|
||
}
|
||
|
||
export function parseQcWorkOrderTabKey(raw?: string | null): QcWorkOrderTabKey {
|
||
const k = (raw || '').trim()
|
||
if (getQcWorkOrderTab(k)) return k as QcWorkOrderTabKey
|
||
// 兼容旧 query:inbound / outbound / temporary
|
||
if (k === 'inbound') return 'inbound_cell_storage'
|
||
if (k === 'outbound') return 'outbound_pilot_service'
|
||
if (k === 'temporary') return 'temporary'
|
||
return DEFAULT_QC_WORK_ORDER_TAB
|
||
}
|
||
|
||
export function toWorkOrderListQuery(tab: QcWorkOrderTabConfig) {
|
||
return {
|
||
workOrderType: tab.workOrderType,
|
||
receptionSource: tab.receptionSource,
|
||
bizType: tab.bizType
|
||
}
|
||
}
|
||
|
||
export function resolveTabKeyFromWorkOrder(row: {
|
||
workOrderType?: string
|
||
receptionSource?: string
|
||
bizType?: string
|
||
}): QcWorkOrderTabKey {
|
||
const wt = row.workOrderType || 'inbound'
|
||
if (wt === 'temporary') return 'temporary'
|
||
if (wt === 'outbound') {
|
||
const bt = row.bizType || 'pilot_service'
|
||
if (bt === 'basic_experiment') return 'outbound_basic_experiment'
|
||
if (bt === 'clinical_application') return 'outbound_clinical_application'
|
||
return 'outbound_pilot_service'
|
||
}
|
||
return row.receptionSource === 'research_donation'
|
||
? 'inbound_research_donation'
|
||
: 'inbound_cell_storage'
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 本地类型检查**
|
||
|
||
Run: `cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -20`
|
||
Expected: 无新增错误(新文件未被引用时可能无输出)
|
||
|
||
---
|
||
|
||
### Task 2: 后端列表 API — `receptionSource` 过滤
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/qc_work_order.py`(`list_work_orders`)
|
||
- Modify: `backend/app/api/qc.py`(`GET /qc-work-orders`)
|
||
|
||
- [ ] **Step 1: CRUD 增加参数与过滤**
|
||
|
||
在 `list_work_orders` 签名增加 `reception_source: Optional[str] = None`,在 `biz_type` 过滤之后:
|
||
|
||
```python
|
||
rs = (reception_source or "").strip()
|
||
if rs:
|
||
q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
|
||
```
|
||
|
||
- [ ] **Step 2: API 路由暴露参数**
|
||
|
||
```python
|
||
receptionSource: Optional[str] = Query(None, alias="receptionSource"),
|
||
# ...
|
||
reception_source=receptionSource,
|
||
```
|
||
|
||
- [ ] **Step 3: 手动验证**
|
||
|
||
启动 backend 后请求(需有效 token):
|
||
|
||
```bash
|
||
curl -s 'http://127.0.0.1:8000/api/admin/qc-work-orders?workOrderType=inbound&receptionSource=cell_storage&page=1&pageSize=5' -H "Authorization: Bearer <token>"
|
||
```
|
||
|
||
Expected: 返回列表中每条 `receptionSource` 均为 `cell_storage`(或空默认为 cell_storage 的工单)
|
||
|
||
---
|
||
|
||
### Task 3: 后端我的任务 API — `receptionSource` / `bizType` 过滤
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/qc_task.py`(`list_mine_tasks`)
|
||
- Modify: `backend/app/api/qc.py`(`GET /qc-tasks/mine`)
|
||
|
||
- [ ] **Step 1: CRUD 增加过滤**
|
||
|
||
`list_mine_tasks` 签名增加 `reception_source`、`biz_type`;在 `qc_biz_type` 过滤后:
|
||
|
||
```python
|
||
rs = (reception_source or "").strip()
|
||
if rs:
|
||
q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
|
||
bt_out = (biz_type or "").strip()
|
||
if bt_out:
|
||
q0 = q0.filter(BizQcWorkOrder.biz_type == bt_out)
|
||
```
|
||
|
||
- [ ] **Step 2: API 暴露 Query 参数并传入**
|
||
|
||
---
|
||
|
||
### Task 4: 前端 API 类型
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/api/qc-work-order.ts`
|
||
- Modify: `web-admin/src/api/qc-task.ts`
|
||
|
||
- [ ] **Step 1: 列表查询参数增加 `receptionSource?: string`**
|
||
|
||
- [ ] **Step 2: `fetchMyQcTaskList` 参数增加 `receptionSource?: string`、`bizType?: string`**
|
||
|
||
---
|
||
|
||
### Task 5: `cardMetaConfig`(CardList props)
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/utils/qc-work-order-card-meta.ts`
|
||
|
||
- [ ] **Step 1: 导出六份 `QcWorkOrderCardMetaConfig`**
|
||
|
||
含:`groupContextBuilder`(入库组头)、`metaFields`(卡片 dl)、`showProtocolFeeSummary`、`showOutboundFeeSummary`、`listLayout`。
|
||
|
||
- [ ] **Step 2: 出库 meta 不得含 `receptionSource`**
|
||
|
||
---
|
||
|
||
### Task 6–8: CardList / SingleCard / SampleGroup 接 props
|
||
|
||
**Files:**
|
||
- Modify: `QcWorkOrderSingleCard.vue`、`QcWorkOrderSampleGroup.vue`、`QcWorkOrderCardList.vue`
|
||
|
||
- [ ] **Step 1: 新增 prop `cardMetaConfig: QcWorkOrderCardMetaConfig`**
|
||
|
||
- [ ] **Step 2: 删除 `activeTypeTab === 'outbound'` 等分支,改为渲染 `cardMetaConfig.metaFields`**
|
||
|
||
- [ ] **Step 3: CardList 按 `cardMetaConfig.listLayout` 决定是否样本分组**
|
||
|
||
---
|
||
|
||
### Task 9: 检测任务 composable(六弹窗共用)
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTaskViewSections.ts`
|
||
|
||
- [ ] **Step 1: 从 `useQcWorkOrderViewDialog.ts` 抽出 `buildTaskTypeSections`、附件 slot、物料 slot 逻辑**
|
||
|
||
- [ ] **Step 2: 导出 `useQcWorkOrderTaskViewSections(tasks)` 供六弹窗 import**
|
||
|
||
---
|
||
|
||
### Task 10: 六份独立查看弹窗(拷贝再改)
|
||
|
||
**Files:**
|
||
- Create: `view-dialogs/InboundCellStorageViewDialog.vue` ← 拷 `QcWorkOrderViewDialogHost` + `useQcWorkOrderViewDialog`
|
||
- Create: `view-dialogs/InboundResearchDonationViewDialog.vue` ← 拷上,删协议缴费
|
||
- Create: `view-dialogs/OutboundPilotViewDialog.vue` ← 拷 `OutboundQcViewDialog`,仅中试 sections
|
||
- Create: `view-dialogs/OutboundBasicExperimentViewDialog.vue` ← 拷出库弹窗,删患者/输注
|
||
- Create: `view-dialogs/OutboundClinicalViewDialog.vue` ← 拷出库弹窗,临床 + 缴费
|
||
- Create: `view-dialogs/TemporaryQcViewDialog.vue` ← 拷入库弹窗,仅产品 + 任务
|
||
|
||
- [ ] **Step 1: 先落地 `InboundCellStorageViewDialog` + `OutboundPilotViewDialog`,行为与现网一致**
|
||
|
||
- [ ] **Step 2: 拷贝出其余四份并按 Spec §6.1 删减区块**
|
||
|
||
- [ ] **Step 3: 各弹窗内 import Task 9 composable**
|
||
|
||
---
|
||
|
||
### Task 11: 六 Tab 子页面(拷贝再改)
|
||
|
||
**Files:**
|
||
- Create: `tabs/InboundCellStorageTab.vue` ← 从 `index.vue` 剪出原入库 Tab(筛选+列表+本 Tab 弹窗)
|
||
- Create: `tabs/InboundResearchDonationTab.vue` ← 拷贝,改 API 参数与 cardMeta
|
||
- Create: `tabs/OutboundPilotTab.vue` ← 剪出原出库 Tab
|
||
- Create: `tabs/OutboundBasicExperimentTab.vue`、`tabs/OutboundClinicalTab.vue` ← 拷贝出库 Tab
|
||
- Create: `tabs/TemporaryTab.vue` ← 剪出原临时 Tab
|
||
|
||
每个 Tab 文件内:
|
||
- 写死 `toWorkOrderListQuery(tabConfig)`
|
||
- 传入对应 `cardMetaConfig`
|
||
- 挂载对应 `view-dialogs/*ViewDialog.vue`
|
||
- prop `pageMode?: 'manage' | 'mine'`
|
||
|
||
- [ ] **Step 1: 先完成 `InboundCellStorageTab` + `OutboundPilotTab` + `TemporaryTab`**
|
||
|
||
- [ ] **Step 2: 拷贝完成捐赠 + 两个 outbound 变体**
|
||
|
||
---
|
||
|
||
### Task 12: `index.vue` / `my-qc-task/index.vue` Tab 壳
|
||
|
||
**Files:**
|
||
- Modify: `qc-work-order/index.vue` — 6 Tab 按钮 + `<component :is="tabComponent" />` + **共用**配置检测弹窗
|
||
- Modify: `my-qc-task/index.vue` — 同上,传 `page-mode="mine"`
|
||
|
||
- [ ] **Step 1: `index.vue` 移除原筛选/列表大段,改为动态加载 `tabs/*`**
|
||
|
||
- [ ] **Step 2: `my-qc-task` import 同一套 `tabs/*`,不重复维护**
|
||
|
||
- [ ] **Step 3: 删除 `QcWorkOrderViewDialogHost.vue`、`OutboundQcViewDialog.vue`、`useQcWorkOrderViewDialog.ts`**
|
||
|
||
- [ ] **Step 4: 更新 `qc-mine-task-groups.ts` TabKey 与 API 过滤**
|
||
|
||
---
|
||
|
||
### Task 13: 列表 enrich 字段(若缺)
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/qc_work_order.py`
|
||
|
||
- [ ] **Step 1: 确认 `basic_experiment` 列表含 `projectTitle`、`customerName`**
|
||
|
||
---
|
||
|
||
### Task 14: 知识图谱与验收
|
||
|
||
- [ ] **Step 1: 六 Tab 数据隔离 + 六弹窗独立打开验收**
|
||
|
||
- [ ] **Step 2: `./scripts/update-graph.sh frontend`**
|
||
|
||
---
|
||
|
||
## Spec Coverage Self-Review
|
||
|
||
| Spec § | Task |
|
||
|--------|------|
|
||
| §2 Tab 定义 | Task 1, 11, 12 |
|
||
| §2.2 数据隔离 | Task 2, 3, 11 |
|
||
| §4 组件边界 | Task 5–12 |
|
||
| §5 卡片矩阵 | Task 5–8, 11 |
|
||
| §6 查看弹窗 | Task 9, 10, 12 |
|
||
| §7 Tab 子页面 | Task 11, 12 |
|
||
| §8 我的任务 | Task 12 |
|
||
| §9 后端 API | Task 2, 3, 13 |
|
||
| §11 验收 | Task 14 |
|
||
|
||
---
|
||
|
||
## Execution Handoff
|
||
|
||
Plan complete and saved to `docs/superpowers/plans/2026-06-16-qc-work-order-tab-split.md`.
|
||
|
||
**两种执行方式:**
|
||
|
||
1. **Subagent-Driven(推荐)** — 按 Task 逐条派发子 agent,每步完成后 review
|
||
2. **Inline Execution** — 本会话按 Task 1→13 连续实现,关键节点暂停确认
|
||
|
||
请告知选择哪种方式;若直接开始实现,将从 Task 1 执行。
|