Files
zkxw/docs/superpowers/specs/2026-06-16-qc-work-order-tab-split-design.md
T
2026-06-16 12:36:29 +08:00

353 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 质检工单 Tab 拆分与分型展示设计
> 日期:2026-06-16
> 状态:已确认(2026-06-16 组件边界已确认)
> 范围:`web-admin` 质检工单 + 我的质检任务;后端列表 API 补充分型过滤
> 关联:`2026-06-12-qc-work-order-fee-summary-design.md`、`outbound-qc-pilot-view-sections.ts`、`2026-06-10-basic-experiment-design.md`
---
## 1. 背景与目标
### 1.1 问题
【质检工单】【我的质检任务】原有三 Tab(入库 / 出库 / 临时),同一 Tab 内混用字段模板,导致:
- 出库卡片出现入库字段(如「来源类型」`receptionSource`)
- 基础实验与中试/临床共用卡片 meta,关键业务字段缺失或错位
- 入库采集与捐赠在组头、查看弹窗混用「协议」语义
### 1.2 目标
将业务线拆为 **6 个 Tab**,每个 Tab **仅展示、仅查询** 对应子类型的工单与任务;列表卡片、查看弹窗、筛选区与 Tab 一一对应。
### 1.3 非目标
- 不新增工单创建入口(捐赠入库仍由样本接收自动触发)
- 不改动质检任务执行、配置检测、物料使用逻辑
- 不在本期做「全部工单」汇总 Tab
- 不改动生产工单模块 Tab 结构
---
## 2. Tab 定义
### 2.1 Tab 配置表(权威)
| Tab Key | 展示标签 | `workOrderType` | `receptionSource` | `bizType` | 列表布局 |
|---------|----------|-----------------|-------------------|-----------|----------|
| `inbound_cell_storage` | 采集入库 | `inbound` | `cell_storage` | — | 样本分组 `inbound-grouped` |
| `inbound_research_donation` | 捐赠入库 | `inbound` | `research_donation` | — | 样本分组 `inbound-grouped` |
| `outbound_pilot_service` | 中试 | `outbound` | — | `pilot_service` | 单列 `standalone` |
| `outbound_basic_experiment` | 基础实验 | `outbound` | — | `basic_experiment` | 单列 `standalone` |
| `outbound_clinical_application` | 临床应用 | `outbound` | — | `clinical_application` | 单列 `standalone` |
| `temporary` | 临时 | `temporary` | — | — | 单列 `standalone` |
**Tab 栏视觉分组(可选 CSS):** 入库 2 | 出库 3 | 临时 1,标签使用短文案。
**默认 Tab:** `inbound_cell_storage`(替换原 `inbound`)。
**路由记忆:** `?tab=<tabKey>`,刷新/分享链接保持当前 Tab。
### 2.2 数据隔离规则(强约束)
每个 Tab 的列表 API 请求 **必须同时携带** 上表中的过滤参数,禁止仅传 `workOrderType` 后在客户端再筛:
```
采集入库 → workOrderType=inbound AND receptionSource=cell_storage
捐赠入库 → workOrderType=inbound AND receptionSource=research_donation
中试 → workOrderType=outbound AND bizType=pilot_service
基础实验 → workOrderType=outbound AND bizType=basic_experiment
临床应用 → workOrderType=outbound AND bizType=clinical_application
临时 → workOrderType=temporary
```
**验收:** 在任一 Tab 下,列表中不得出现其它子类型的工单;切换 Tab 后分页重置为第 1 页。
---
## 3. 筛选区(按 Tab)
移除原「业务类型」「来源类型」下拉(已由 Tab 固定)。
| Tab | 关键词 placeholder | 时间筛选标签 | 时间字段 | 额外筛选 |
|-----|-------------------|-------------|----------|----------|
| 采集入库 | 工单号/样本编码/入库编码 | 创建时间 | `createTime` | 触发来源(样本接收 / 生产过程) |
| 捐赠入库 | 工单号/样本编码/入库编码 | 创建时间 | `createTime` | 触发来源 |
| 中试 | 工单号/申请单号/申请单名称/患者 | 输注时间 | `infusionTime` | — |
| 基础实验 | 工单号/申请单号/课题名称/客户 | 交付时间 | `infusionTime`(schedule 提货时间) | — |
| 临床应用 | 工单号/申请单号/申请单名称/患者 | 输注时间 | `infusionTime` | — |
| 临时 | 工单号 | 创建时间 | `createTime` | — |
默认时间范围:近 30 天(出库类按输注/交付时间,入库/临时按创建时间)。
---
## 4. 组件边界(已确认)
| 模块 | 策略 | 说明 |
|------|------|------|
| `QcWorkOrderTaskSubList` | **共用** | 任务执行、填结果、物料与 Tab 无关 |
| 配置检测 / 调整检测弹窗 | **共用** | 三种工单类型逻辑相同 |
| `QcWorkOrderCardList` + `QcWorkOrderSampleGroup` + `QcWorkOrderSingleCard` | **共用** | 各 Tab 通过 props 传入**卡片 meta 字段模板**(组头文案、meta 列、费用条开关) |
| 查看详情弹窗 | **不共用** | 六 Tab 各一份**独立查看界面**;信息区块差异大,禁止再用 Host + variant 分支 |
| Tab 页内容(筛选 + 工具栏 + 列表) | **拷贝再改** | 从现有入库/出库/临时 Tab **剪出/拷贝**为 6 个子页面;`index.vue` 仅 Tab 壳 |
**我的质检任务:** 与质检工单 **共用同一套 6 个 Tab 子组件**(`tabs/*`),**已确认**。差异仅:
- prop `pageMode='mine'`
- 卡片操作:仅「查看」,无配置检测 / 新增 / 删除等
- 任务区 `taskListMode='mine'`
- 数据:`GET /qc-tasks/mine` + 与 Tab 一致的 `receptionSource` / `bizType` 过滤
**维护成本:** 6 个 Tab 文件 + 2 个壳页面(`qc-work-order/index.vue`、`my-qc-task/index.vue`),**不是** 12 份拷贝。
---
## 5. 列表卡片展示矩阵
共用 `QcWorkOrderCardList` / `QcWorkOrderSampleGroup` / `QcWorkOrderSingleCard`;各 Tab 子页面传入不同的 **cardMetaConfig**(字段 label、取值、Tag、费用条),禁止在卡片内用 `activeTypeTab === 'outbound'` 大包大揽。
### 5.1 入库 · 采集(`inbound_cell_storage`)
**样本组头(`QcWorkOrderSampleGroup`)**
- 样本编码、样本类型、**采集协议号**、**协议状态**、接收时间、经手人
**组内费用条(组级,已有逻辑扩展)**
- 组内存在 `cell_storage` 工单且 `paymentSummary` 时展示 `QcWorkOrderProtocolPaymentPanel`
**卡片 meta**
- 质检对象、触发来源 Tag、关联生产工单(仅 `triggerSource=production`)、实验室入库编码、检测方式、创建时间、备注
**卡片费用条**
- `paymentSummary`(`QcWorkOrderProtocolPaymentPanel` compact)
**工具栏**
- 「新增入库质检工单」(仅本 Tab;`receptionSource` 固定 `cell_storage`)
### 5.2 入库 · 捐赠(`inbound_research_donation`)
**样本组头**
- 样本编码、样本类型、**捐赠单号**(`protocolNo` 字段存捐赠编号)、**捐赠状态**、接收时间、经手人
**卡片 meta**
- 同采集入库卡片结构;**不展示**协议状态、费用相关字段
**工具栏**
- 无新增按钮
### 5.3 出库 · 中试(`outbound_pilot_service`)
**卡片 meta**
- 申请单号、申请单名称、渠道名称、**患者姓名**、第 N 次 / 共 M 次、输注时间、检测方式、创建时间、备注
**禁止展示**
- `receptionSource`、业务类型 Tag(Tab 已表达)、基础实验专属字段
**费用条**
- 不展示
### 5.4 出库 · 基础实验(`outbound_basic_experiment`)
**卡片 meta**
- 申请单号、**课题名称**、**客户名称**、**交付时间**、售卖金额(可选,来自 `feeSummary.saleAmount`)、检测方式、创建时间、备注
**费用条**
- `QcWorkOrderFeeSummaryBar`(`feeSummary`)
### 5.5 出库 · 临床应用(`outbound_clinical_application`)
**卡片 meta**
- 申请单号、申请单名称、渠道名称、患者姓名、第 N 次 / 共 M 次、输注时间、检测方式、创建时间、备注
**费用条**
- `QcWorkOrderFeeSummaryBar`(与 `2026-06-13-production-work-order-fee-summary-design.md` D1=B 一致)
### 5.6 临时(`temporary`)
保持现状:产品编码/名称/类型、检测方式、创建时间;工具栏「新增临时工单」。
---
## 6. 查看弹窗(六份独立界面)
**不共用** `QcWorkOrderViewDialogHost`、`OutboundQcViewDialog` 及其中 `variant` 分支。自现有弹窗**拷贝**后按 Tab 删减字段;检测任务区块(实验室/第三方/参数/附件/物料)可抽 **`useQcWorkOrderTaskViewSections.ts`** 供六弹窗 import,避免任务展示逻辑拷六份。
### 6.1 Tab → 弹窗组件(一一对应)
| Tab Key | 新建组件 | 拷贝来源 | 主要差异 |
|---------|----------|----------|----------|
| `inbound_cell_storage` | `InboundCellStorageViewDialog.vue` | 现 `QcWorkOrderViewDialogHost` + `useQcWorkOrderViewDialog` | 协议状态、协议缴费、运输、样本接收 |
| `inbound_research_donation` | `InboundResearchDonationViewDialog.vue` | 上者拷贝 | **无**协议缴费;捐赠状态;组头/基本信息用捐赠文案 |
| `outbound_pilot_service` | `OutboundPilotViewDialog.vue` | 现 `OutboundQcViewDialog` | 仅中试:申请单、患者、输注方案/计划、产品、检测任务;**无**缴费区块 |
| `outbound_basic_experiment` | `OutboundBasicExperimentViewDialog.vue` | 出库弹窗拷贝 | 课题/客户/交付计划/产品/缴费;**无**患者、输注方案 |
| `outbound_clinical_application` | `OutboundClinicalViewDialog.vue` | 出库弹窗拷贝 | 同中试结构 + 临床应用缴费信息(如有) |
| `temporary` | `TemporaryQcViewDialog.vue` | 入库弹窗拷贝 | 仅产品信息 + 检测任务;**无**接收/运输/协议 |
### 6.2 挂载方式
- 各 **Tab 子页面** 内直接 `import` 对应弹窗,`:visible` + `@view` 打开;**不再**使用统一 `openView(row)` 按类型分流。
- 旧文件 `QcWorkOrderViewDialogHost.vue`、`OutboundQcViewDialog.vue` 在六弹窗落地并切换引用后**删除**(或首版标记 `@deprecated` 再删)。
### 6.3 可共用的底层(非 UI 壳)
| 共用项 | 说明 |
|--------|------|
| `SystemViewDialog` | 布局壳 |
| `ProtocolAttachmentReadonly` | 附件只读 |
| `QcWorkOrderProtocolPaymentPanel` | 仅采集入库弹窗使用 |
| `fetchQcWorkOrderDetail` / `fetchOutboundQcDetail` | API |
| `useQcWorkOrderTaskViewSections`(新建) | 检测任务 sections + 附件 slot + 物料 slot |
---
## 7. Tab 子页面结构(拷贝再改)
```
web-admin/src/views/quality-inspection/qc-work-order/
index.vue # 6 Tab 壳 + ?tab= 路由
tabs/
InboundCellStorageTab.vue # 从原 index 入库区块剪出
InboundResearchDonationTab.vue # 拷贝 InboundCellStorageTab 再改
OutboundPilotTab.vue # 从原出库区块剪出
OutboundBasicExperimentTab.vue # 拷贝 OutboundPilotTab 再改
OutboundClinicalTab.vue # 拷贝 OutboundPilotTab 再改
TemporaryTab.vue # 从原临时区块剪出
view-dialogs/
InboundCellStorageViewDialog.vue
InboundResearchDonationViewDialog.vue
OutboundPilotViewDialog.vue
OutboundBasicExperimentViewDialog.vue
OutboundClinicalViewDialog.vue
TemporaryQcViewDialog.vue
```
`my-qc-task/index.vue` 同样 6 Tab 壳,**import 上述 `tabs/*` 组件**,传 `page-mode="mine"`。
---
## 8. 我的质检任务
与质检工单 **共用** `tabs/*` 子组件与 `cardMetaConfig`;差异:
- `pageMode='mine'`:卡片仅「查看」
- 任务列表 `taskListMode='mine'`
- 数据流:先按 Tab 参数请求 `GET /qc-tasks/mine`,再聚合为卡片
**后端扩展 `list_mine_tasks`:** 增加 `receptionSource`、`bizType` 过滤,与工单列表对齐,避免客户端从全量任务中筛类型。
**`qc-mine-task-groups.ts`:**
- `QcWorkOrderTypeTab` 替换为 `QcWorkOrderTabKey`
- `resolveWorkOrderTabKey(task)` 由 `workOrderType` + `receptionSource` / `bizType` 解析
- `filterMineEntriesByWorkOrderSearch` 按 `tabKey` 固定过滤,移除 `bizType`/`receptionSource` 筛选项
---
## 9. 后端 API 变更
### 7.1 `GET /qc-work-orders`(列表)
新增 Query 参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `receptionSource` | string | 入库分型:`cell_storage` / `research_donation` |
`list_work_orders()` 在 `work_order_type=inbound` 且 `reception_source` 有值时:
```python
q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
```
出库 Tab 继续传 `bizType`(已有)。
### 7.2 `GET /qc-tasks/mine`
新增 Query 参数:`receptionSource`、`bizType`,在 join `BizQcWorkOrder` 后过滤。
### 7.3 列表 enrich
确认基础实验列表项含 `projectTitle`(课题名称)、`customerName`;缺则于 `_enrich_outbound_list_fields` 补充 join。
---
## 10. 前端模块结构(摘要)
```
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/ # 6 Tab 子页面(拷贝再改)
web-admin/src/views/.../qc-work-order/view-dialogs/ # 6 独立查看弹窗
web-admin/src/views/.../qc-work-order/composables/useQcWorkOrderTaskViewSections.ts # 检测任务区块共用
```
**共用不改业务:** `QcWorkOrderTaskSubList`、配置检测弹窗(仍在 `index.vue` 或 composable)。
**删除/替换:** `QcWorkOrderViewDialogHost.vue`、`OutboundQcViewDialog.vue`、`useQcWorkOrderViewDialog.ts`(逻辑拆入六弹窗 + task composable)。
---
## 11. 验收清单
### 11.1 数据隔离
- [ ] 采集入库 Tab 仅 `receptionSource=cell_storage` 的 inbound 工单
- [ ] 捐赠入库 Tab 仅 `research_donation` 的 inbound 工单
- [ ] 中试 / 基础实验 / 临床 Tab 各自仅对应 `bizType` 的 outbound 工单
- [ ] 临时 Tab 仅 `workOrderType=temporary`
- [ ] 我的质检任务六 Tab 与上相同(任务所属工单类型一致)
### 11.2 展示
- [ ] 出库卡片无 `receptionSource` 字段
- [ ] 基础实验卡片无患者/输注疗程字段,有课题/客户/交付时间
- [ ] 捐赠组头为捐赠单号/捐赠状态,非协议文案
- [ ] 采集入库查看弹窗有协议缴费;捐赠无
- [ ] 六 Tab 各打开对应**独立**查看弹窗,无交叉引用 Host/variant
- [ ] 基础实验查看弹窗无患者/输注方案;中试/临床无课题专属区块
### 11.3 操作
- [ ] 仅采集入库 Tab 有「新增入库质检工单」
- [ ] 仅临时 Tab 有「新增临时工单」
- [ ] 配置检测 / 调整检测 / 编辑备注行为与改版前一致
### 11.4 回归
- [ ] Tab 切换重置分页与筛选(保留 Tab 专属默认时间范围)
- [ ] URL `?tab=` 可深链到指定 Tab
- [ ] 费用摘要条规则与 `2026-06-12-qc-work-order-fee-summary-design.md` 一致
---
## 12. 决策记录
| 项 | 决策 |
|----|------|
| Tab 数量 | 6(入库 2 + 出库 3 + 临时 1) |
| Tab 标签 | 短标签:采集入库 / 捐赠入库 / 中试 / 基础实验 / 临床应用 / 临时 |
| 实现策略 | **Tab 子页面拷贝再改**;`index.vue` 仅 Tab 壳 |
| 数据过滤 | **服务端** 强制分型,禁止仅靠前端过滤 |
| TaskSubList | **共用** |
| 配置检测弹窗 | **共用** |
| CardList + SampleGroup | **共用**,props 传 `cardMetaConfig` |
| 查看弹窗 | **六份独立界面**,不共用 Host / OutboundQcViewDialog |
| 检测任务展示 | composable 共用,避免六份弹窗重复任务 UI 逻辑 |
| 我的质检任务 | **已确认** 共用 6 个 `tabs/*`,`pageMode='mine'` |