# 入库质检工单 · 协议单多样本 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:** 在细胞存储质检、科研捐赠质检两个 Tab 中,将同一协议单下多个样本质检工单合并为一张协议组卡片,组内用 Tab 切换样本,样本内保留触发来源 Tab。 **Architecture:** 前端新增 `groupInboundQcWorkOrdersByProtocol()` 与 `QcWorkOrderProtocolGroup.vue` 作为协议外层;复用 `QcWorkOrderSampleGroup` 作为样本内层(`hideContextBar`);`QcWorkOrderCardList` 在 `inbound-grouped` 布局时渲染协议组。后端 `list_work_orders` 关键词增加 `protocol_no` 匹配。两页(manage/mine)共用 CardList,mine 关键词补 `protocolNo`。 **Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy(backend);Vue 3 / TypeScript / Element Plus(web-admin) **Spec:** `docs/superpowers/specs/2026-06-25-qc-work-order-protocol-group-design.md` --- ## File Map | 文件 | 职责 | |------|------| | `web-admin/src/utils/qc-work-order-groups.ts` | 协议分组模型、`groupInboundQcWorkOrdersByProtocol`、gate 工单/进度统计 helper | | `web-admin/src/utils/qc-work-order-card-meta.ts` | 协议组头副标题、协议状态 Tag、组级 `paymentSummary` 选取 | | `web-admin/src/utils/qc-work-order-tabs.ts` | 入库 Tab 关键词 placeholder 含协议号/捐赠单号 | | `web-admin/src/utils/qc-mine-task-groups.ts` | mine 前端关键词增加 `protocolNo` | | `web-admin/src/views/.../QcWorkOrderProtocolGroup.vue` | **新建** 协议组卡片(组头 + 样本 Tab + 内嵌 SampleGroup) | | `web-admin/src/views/.../QcWorkOrderSampleGroup.vue` | `hideContextBar`、`hideProtocolFeeSummary` prop | | `web-admin/src/views/.../QcWorkOrderSingleCard.vue` | 尊重 `hideProtocolFeeSummary` | | `web-admin/src/views/.../QcWorkOrderCardList.vue` | inbound 布局改用 `QcWorkOrderProtocolGroup` | | `backend/app/crud/qc_work_order.py` | 列表关键词 OR 增加 `protocol_no.like` | **不修改:** `buildAllTabListItems`(「所有」Tab)、出库/临时 Tab、`buildMineCardListModel`(mine 走 flat rows + CardList 分组)。 --- ### Task 1: 协议分组工具函数 **Files:** - Modify: `web-admin/src/utils/qc-work-order-groups.ts` - [ ] **Step 1: 在文件末尾、`buildAllTabListItems` 之前追加类型与 helper** 在 `isGateTriggerSource` 之后、`buildAllTabListItems` 之前插入: ```typescript export function resolveInboundProtocolGroupKey(row: QcWorkOrder): string { const id = (row.collectionProtocolId || '').trim() if (id) return id const no = (row.protocolNo || '').trim() if (no) return `protocol-no:${no}` return row.id } export function resolveInboundSampleGroupKey(row: QcWorkOrder): string { return (row.sampleCode || '').trim() || row.id } /** 样本组内用于组头/Tab 状态的 gate 工单(接收优先,否则首条) */ export function resolveGateWorkOrderForSampleGroup( workOrders: QcWorkOrder[] ): QcWorkOrder | undefined { if (!workOrders.length) return undefined return ( workOrders.find((w) => isGateTriggerSource(w.triggerSource)) || workOrders[0] ) } export type QcWorkOrderProtocolGroupModel = { groupKey: string protocolId: string protocolNo: string receptionSource: 'cell_storage' | 'research_donation' sampleGroups: QcWorkOrderSampleGroupModel[] workOrders: QcWorkOrder[] } function protocolGroupSortTime(group: QcWorkOrderProtocolGroupModel): string { let max = '' for (const wo of group.workOrders) { const t = String(wo.createTime || '') if (t > max) max = t } return max } /** 入库 Tab:按协议单分组,组内再按样本分组 */ export function groupInboundQcWorkOrdersByProtocol( rows: QcWorkOrder[] ): QcWorkOrderProtocolGroupModel[] { const map = new Map() for (const row of rows) { const key = resolveInboundProtocolGroupKey(row) const list = map.get(key) if (list) list.push(row) else map.set(key, [row]) } const groups = Array.from(map.entries()).map(([groupKey, workOrders]) => { const sampleGroups = groupInboundQcWorkOrders(workOrders) const head = workOrders[0] const source = (head?.receptionSource || 'cell_storage') === 'research_donation' ? 'research_donation' : 'cell_storage' return { groupKey, protocolId: (head?.collectionProtocolId || '').trim(), protocolNo: (head?.protocolNo || '').trim(), receptionSource: source as 'cell_storage' | 'research_donation', sampleGroups, workOrders } }) return groups.sort( (a, b) => protocolGroupSortTime(b).localeCompare(protocolGroupSortTime(a)) ) } /** 协议组进度:已配置样本数 / 样本总数(按 gate 工单 workOrderStatus !== dpz) */ export function countConfiguredSamplesInProtocolGroup( group: QcWorkOrderProtocolGroupModel ): { configured: number; total: number } { const total = group.sampleGroups.length let configured = 0 for (const sg of group.sampleGroups) { const gate = resolveGateWorkOrderForSampleGroup(sg.workOrders) if (gate && gate.workOrderStatus !== 'dpz') configured += 1 } return { configured, total } } ``` - [ ] **Step 2: 本地 smoke(Node REPL 或临时 script,可选)** 在 `web-admin` 目录用两条 mock 工单(同 `collectionProtocolId`、不同 `sampleCode`)调用 `groupInboundQcWorkOrdersByProtocol`,期望返回 1 个 group、`sampleGroups.length === 2`。 - [ ] **Step 3: Commit** ```bash git add web-admin/src/utils/qc-work-order-groups.ts git commit -m "feat(web-admin): add inbound QC work order protocol grouping utils" ``` --- ### Task 2: 协议组头 card-meta helper **Files:** - Modify: `web-admin/src/utils/qc-work-order-card-meta.ts` - [ ] **Step 1: 增加 import 与 helper** 文件顶部 import 区增加: ```typescript import { countConfiguredSamplesInProtocolGroup, type QcWorkOrderProtocolGroupModel } from '@/utils/qc-work-order-groups' import type { QcProtocolPaymentSummary } from '@/api/qc-work-order' ``` 在 `buildDonationGroupContext` 之后追加: ```typescript export function buildProtocolGroupProgressLabel( group: QcWorkOrderProtocolGroupModel ): string { const { configured, total } = countConfiguredSamplesInProtocolGroup(group) if (total <= 0) return '' if (configured <= 0) return '' return `${configured}/${total} 已配置` } export function resolveProtocolGroupStatus(row: QcWorkOrder): { label: string tagType: TagType } { const status = String(row.protocolStatus || '') const source = row.receptionSource || 'cell_storage' return { label: protocolStatusLabelBySource(status, source), tagType: protocolStatusTagTypeBySource(status, source) } } /** 协议组级费用摘要:取组内首条有 paymentSummary 的 cell_storage 工单 */ export function resolveProtocolGroupPaymentSummary( group: QcWorkOrderProtocolGroupModel ): QcProtocolPaymentSummary | null { if (group.receptionSource !== 'cell_storage') return null for (const wo of group.workOrders) { if (wo.paymentSummary) return wo.paymentSummary } return null } ``` - [ ] **Step 2: Commit** ```bash git add web-admin/src/utils/qc-work-order-card-meta.ts git commit -m "feat(web-admin): add protocol group card meta helpers for inbound QC" ``` --- ### Task 3: SampleGroup / SingleCard 支持隐藏组头与卡片级协议费用 **Files:** - Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderSampleGroup.vue` - Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderSingleCard.vue` - [ ] **Step 1: `QcWorkOrderSampleGroup.vue` — props + 条件渲染** `defineProps` 增加: ```typescript hideContextBar?: boolean hideProtocolFeeSummary?: boolean ``` 模板中 `qc-sample-row__ctx` 外层加 `v-if="!hideContextBar"`。 `QcWorkOrderSingleCard` 传参增加: ```vue :hide-protocol-fee-summary="hideProtocolFeeSummary" ``` - [ ] **Step 2: `QcWorkOrderSingleCard.vue` — prop + computed** props 增加: ```typescript hideProtocolFeeSummary?: boolean ``` 修改 `showProtocolFeeSummary` computed: ```typescript const showProtocolFeeSummary = computed( () => !props.hideProtocolFeeSummary && effectiveCardMeta.value.showProtocolFeeSummary && !!props.row.paymentSummary ) ``` - [ ] **Step 3: 浏览器快速确认** 打开细胞存储 Tab,在改动 CardList 前行为应不变(默认 prop false)。 - [ ] **Step 4: Commit** ```bash git add web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderSampleGroup.vue \ web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderSingleCard.vue git commit -m "feat(web-admin): allow hiding sample context bar and card-level protocol fee" ``` --- ### Task 4: 新建 `QcWorkOrderProtocolGroup.vue` **Files:** - Create: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolGroup.vue` - [ ] **Step 1: 创建组件(完整骨架)** ```vue ``` - [ ] **Step 2: 追加 scoped SCSS** 在 `` 后追加样式(复用 `QcWorkOrderSampleGroup` 的 pill Tab 类名,协议组头对齐 `qc-sample-row__ctx`): ```scss ``` - [ ] **Step 3: Commit** ```bash git add web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderProtocolGroup.vue git commit -m "feat(web-admin): add protocol group card with sample tabs for inbound QC" ``` --- ### Task 5: `QcWorkOrderCardList` 接入协议分组 **Files:** - Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderCardList.vue` - [ ] **Step 1: import 与 groupedRows 分支** script 区: ```typescript import { groupInboundQcWorkOrders, groupInboundQcWorkOrdersByProtocol, groupOutboundQcWorkOrders, buildAllTabListItems, type QcWorkOrderAllTabListItem } from '@/utils/qc-work-order-groups' import QcWorkOrderProtocolGroup from './QcWorkOrderProtocolGroup.vue' ``` 新增 computed: ```typescript const protocolGroupedRows = computed(() => { if (!cardMetaUsesInboundGroups(props.cardMetaConfig.tabKey)) return [] return groupInboundQcWorkOrdersByProtocol(props.rows) }) ``` - [ ] **Step 2: 模板 — inbound 分支改用 ProtocolGroup** 将 `useGroupedList` 分支中: ```vue ``` 或更简洁:`groupedRows` computed 内 inbound 返回空数组,单独 `protocolGroupedRows` 仅 inbound 使用;outbound 仍用 `groupedRows`。 推荐改 `groupedRows`: ```typescript const groupedRows = computed(() => { if (cardMetaUsesOutboundGroups(props.cardMetaConfig.tabKey)) { return groupOutboundQcWorkOrders(props.rows) } return [] }) ``` inbound 专用 `protocolGroupedRows` 见 Step 1。 - [ ] **Step 3: 浏览器验证 manage 页** URL: `http://localhost:3006/#/quality-group/quality-manage/qc-work-order?tab=inbound_cell_storage` 期望:同协议多样本 → 1 张协议卡 + 样本 Tab;单样本 → 无样本 Tab 行。 - [ ] **Step 4: Commit** ```bash git add web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderCardList.vue git commit -m "feat(web-admin): render inbound QC lists as protocol groups with sample tabs" ``` --- ### Task 6: 关键词 placeholder + mine 前端过滤 **Files:** - Modify: `web-admin/src/utils/qc-work-order-tabs.ts` - Modify: `web-admin/src/utils/qc-mine-task-groups.ts` - [ ] **Step 1: `keywordPlaceholderForTab`** 将 inbound 默认 return 改为: ```typescript if (tab.key === 'inbound_research_donation') { return '工单号/捐赠单号/样本编码/入库编码' } if (tab.group === 'inbound') { return '工单号/协议号/样本编码/入库编码' } ``` (放在 `tab.group === 'outbound'` 判断之前,且 `inbound_research_donation` 先于 general inbound。) - [ ] **Step 2: `keywordMatchesWorkOrder` inbound 字段** 将 inbound 组 fields 数组改为: ```typescript : [row.workOrderNo, row.protocolNo, row.sampleCode, row.labStorageCode] ``` - [ ] **Step 3: Commit** ```bash git add web-admin/src/utils/qc-work-order-tabs.ts web-admin/src/utils/qc-mine-task-groups.ts git commit -m "feat(web-admin): support protocol number in inbound QC keyword filters" ``` --- ### Task 7: 后端列表关键词 — `protocol_no` **Files:** - Modify: `backend/app/crud/qc_work_order.py` - [ ] **Step 1: 在 `list_work_orders` 关键词 OR 中追加** 定位 `_filtered()` 内(约 L1272): ```python keyword_conditions = [ BizQcWorkOrder.work_order_no.like(like), BizQcWorkOrder.sample_code.like(like), BizQcWorkOrder.protocol_no.like(like), ] ``` - [ ] **Step 2: 手动 API 验证** ```bash cd backend && source venv/bin/activate # 假设本地服务已起;替换 TOKEN 与协议号 curl -s "http://127.0.0.1:8000/api/web/admin/qc-work-orders?page=1&pageSize=10&workOrderType=inbound&receptionSource=cell_storage&keyword=XY2026" \ -H "Authorization: Bearer " | python -m json.tool | head -40 ``` 期望:返回列表中 `protocolNo` 含关键词的工单。 - [ ] **Step 3: Commit** ```bash git add backend/app/crud/qc_work_order.py git commit -m "feat(backend): include protocol number in QC work order list keyword search" ``` --- ### Task 8: 联调回归与文档收尾 **Files:** - Modify: `docs/superpowers/specs/2026-06-25-qc-work-order-protocol-group-design.md`(状态 → 已实现) - [ ] **Step 1: 前端 lint** ```bash cd web-admin && pnpm lint ``` 期望:无新增 error。 - [ ] **Step 2: 手工回归清单(两页)** | # | 场景 | 页面 | 期望 | |---|------|------|------| | 1 | 同协议 2+ 样本 | 质检工单 · 细胞存储 | 1 协议卡,样本 Tab 可切换 | | 2 | 同协议 1 样本 | 同上 | 无样本 Tab,直接工单区 | | 3 | 同样本 接收+生产 | 同上 | 内层触发来源 Tab 正常 | | 4 | 协议费用 | 细胞存储 | 组头一次,卡片内不重复 | | 5 | 科研捐赠 | 科研捐赠 Tab | 无费用条,捐赠单号组头 | | 6 | 关键词 | 两页 | 协议号/捐赠单号可搜 | | 7 | 所有 Tab | 两页 | 仍平铺,无协议分组 | | 8 | 出库/临时 Tab | 质检工单 | 布局不变 | | 9 | 我的质检任务 | mine · 细胞存储 | 与 manage 分组一致,仅查看 | - [ ] **Step 3: 更新知识图谱** ```bash ./scripts/update-graph.sh frontend ./scripts/update-graph.sh backend ``` - [ ] **Step 4: 更新 spec 状态** `docs/superpowers/specs/2026-06-25-qc-work-order-protocol-group-design.md` 头部 `状态:已实现`。 - [ ] **Step 5: Commit** ```bash git add docs/superpowers/specs/2026-06-25-qc-work-order-protocol-group-design.md git commit -m "docs: mark inbound QC protocol group spec as implemented" ``` --- ## Spec Coverage Checklist | Spec § | Task | |--------|------| | §2 三层结构 / 分组键 | Task 1 | | §3.1 协议组头 | Task 2, 4 | | §3.2 样本 Tab | Task 4 | | §3.3 hideContextBar + 内层 SampleGroup | Task 3, 4 | | §3 协议费用组级一次 | Task 2, 3, 4 | | §4.4 所有 Tab 不变 | 不修改 `buildAllTabListItems` | | §4.5 mine 共用 + protocolNo 关键词 | Task 5, 6 | | §5.1 后端 protocol_no | Task 7 | | §5.2 placeholder | Task 6 | | §7 验收 1–9 | Task 8 | --- ## Plan complete Saved to `docs/superpowers/plans/2026-06-25-qc-work-order-protocol-group.md`. **Two execution options:** 1. **Subagent-Driven (recommended)** — 按 Task 1→8 派发子 agent,每 Task 完成后 review 2. **Inline Execution** — 本会话内用 `executing-plans` 连续实现,Task 5 完成后浏览器 checkpoint 需要哪种方式开始写代码?