Files
zkxw/docs/superpowers/plans/2026-06-25-qc-work-order-protocol-group.md
T
2026-06-26 00:50:04 +08:00

778 lines
24 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 分组 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<string, QcWorkOrder[]>()
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
<!-- 质检工单 · 同协议多样本分组(协议组头 + 样本 Tab + SampleGroup) -->
<template>
<article class="qc-protocol-row">
<div class="qc-protocol-row__ctx">
<span class="qc-protocol-row__idx">{{ groupIndex }}</span>
<span class="qc-protocol-row__code">{{ group.protocolNo || '—' }}</span>
<el-tag v-if="protocolStatus.label" :type="protocolStatus.tagType" size="small">
{{ protocolStatus.label }}
</el-tag>
<span class="qc-protocol-row__sub">
{{ sampleCountLabel }}
<template v-if="progressLabel"> · {{ progressLabel }}</template>
</span>
<span v-if="group.workOrders.length > 1" class="qc-protocol-row__badge">
{{ group.workOrders.length }} 条质检
</span>
</div>
<QcWorkOrderProtocolPaymentPanel
v-if="protocolPaymentSummary"
:summary="protocolPaymentSummary"
compact
class="qc-protocol-row__fee"
/>
<div
v-if="group.sampleGroups.length > 1"
class="qc-protocol-sample-tabs"
>
<div class="qc-protocol-sample-tabs__row" role="tablist" aria-label="同协议样本">
<button
v-for="(sampleGroup, tabIndex) in group.sampleGroups"
:key="sampleTabKey(sampleGroup)"
type="button"
role="tab"
class="qc-wo-indep-tabs__btn"
:class="{ 'is-active': sampleTabKey(sampleGroup) === activeSampleKey }"
:aria-selected="sampleTabKey(sampleGroup) === activeSampleKey"
@click="activeSampleKey = sampleTabKey(sampleGroup)"
>
<span class="qc-wo-tab__index">{{ tabIndex + 1 }}</span>
<span class="qc-wo-tab__no">{{ sampleGroup.sampleCode || '—' }}</span>
<span v-if="sampleTypeLabel(sampleGroup)" class="qc-wo-tab__source qc-wo-tab__source--reception">
{{ sampleTypeLabel(sampleGroup) }}
</span>
<el-tag
v-if="gateWorkOrder(sampleGroup)"
:type="workOrderStatusTagType(gateWorkOrder(sampleGroup)!.workOrderStatus)"
size="small"
class="qc-wo-tab__status"
>
{{ workOrderStatusLabel(gateWorkOrder(sampleGroup)!.workOrderStatus) }}
</el-tag>
</button>
</div>
</div>
<QcWorkOrderSampleGroup
v-if="activeSampleGroup"
:group="activeSampleGroup"
:group-index="groupIndex"
:card-meta-config="cardMetaConfig"
:task-refresh-token="taskRefreshToken"
:show-work-order-ops="showWorkOrderOps"
:show-work-order-view-only="showWorkOrderViewOnly"
:task-list-mode="taskListMode"
:preset-tasks-by-work-order-id="presetTasksByWorkOrderId"
hide-context-bar
:hide-protocol-fee-summary="!!protocolPaymentSummary"
@configure="(row) => emit('configure', row)"
@view="(row) => emit('view', row)"
@command="(cmd, row) => emit('command', cmd, row)"
@tasks-changed="emit('tasks-changed')"
/>
</article>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { QcWorkOrder } from '@/api/qc-work-order'
import type { QcTask } from '@/api/qc-task'
import { workOrderStatusLabel } from '@/utils/qc-labels'
import type { TagType } from '@/utils/status-tag-registry'
import {
type QcWorkOrderProtocolGroupModel,
type QcWorkOrderSampleGroupModel,
resolveGateWorkOrderForSampleGroup,
resolveInboundSampleGroupKey
} from '@/utils/qc-work-order-groups'
import type { QcWorkOrderCardMetaConfig } from '@/utils/qc-work-order-card-meta'
import {
buildProtocolGroupProgressLabel,
resolveProtocolGroupPaymentSummary,
resolveProtocolGroupStatus
} from '@/utils/qc-work-order-card-meta'
import QcWorkOrderSampleGroup from './QcWorkOrderSampleGroup.vue'
import QcWorkOrderProtocolPaymentPanel from './QcWorkOrderProtocolPaymentPanel.vue'
defineOptions({ name: 'QcWorkOrderProtocolGroup' })
const props = withDefaults(
defineProps<{
group: QcWorkOrderProtocolGroupModel
groupIndex: number
cardMetaConfig: QcWorkOrderCardMetaConfig
taskRefreshToken: number
showWorkOrderOps?: boolean
showWorkOrderViewOnly?: boolean
taskListMode?: 'workOrder' | 'mine'
presetTasksByWorkOrderId?: Record<string, QcTask[]>
}>(),
{
showWorkOrderOps: true,
showWorkOrderViewOnly: false,
taskListMode: 'workOrder'
}
)
const emit = defineEmits<{
configure: [row: QcWorkOrder]
view: [row: QcWorkOrder]
command: [cmd: string, row: QcWorkOrder]
'tasks-changed': []
}>()
function sampleTabKey(sampleGroup: QcWorkOrderSampleGroupModel) {
return sampleGroup.groupKey || resolveInboundSampleGroupKey(sampleGroup.workOrders[0])
}
const activeSampleKey = ref(
props.group.sampleGroups[0] ? sampleTabKey(props.group.sampleGroups[0]) : ''
)
watch(
() => props.group.sampleGroups.map((g) => sampleTabKey(g)).join(','),
() => {
const keys = props.group.sampleGroups.map((g) => sampleTabKey(g))
if (!keys.includes(activeSampleKey.value)) {
activeSampleKey.value = keys[0] || ''
}
}
)
const activeSampleGroup = computed(() =>
props.group.sampleGroups.find((g) => sampleTabKey(g) === activeSampleKey.value)
)
const gateRow = computed(() =>
resolveGateWorkOrderForSampleGroup(
props.group.sampleGroups[0]?.workOrders || props.group.workOrders
)
)
const protocolStatus = computed(() =>
gateRow.value
? resolveProtocolGroupStatus(gateRow.value)
: { label: '', tagType: 'info' as TagType }
)
const sampleCountLabel = computed(() => `${props.group.sampleGroups.length} 个样本`)
const progressLabel = computed(() => buildProtocolGroupProgressLabel(props.group))
const protocolPaymentSummary = computed(() =>
resolveProtocolGroupPaymentSummary(props.group)
)
function sampleTypeLabel(sampleGroup: QcWorkOrderSampleGroupModel) {
const gate = resolveGateWorkOrderForSampleGroup(sampleGroup.workOrders)
return (gate?.sampleTypeName || sampleGroup.workOrders[0]?.sampleTypeName || '').trim()
}
function gateWorkOrder(sampleGroup: QcWorkOrderSampleGroupModel) {
return resolveGateWorkOrderForSampleGroup(sampleGroup.workOrders)
}
function workOrderStatusTagType(status: string): TagType {
if (status === 'ywc') return 'success'
if (status === 'ypz') return 'warning'
if (status === 'yzf') return 'danger'
return 'info'
}
</script>
```
- [ ] **Step 2: 追加 scoped SCSS**
在 `</script>` 后追加样式(复用 `QcWorkOrderSampleGroup` 的 pill Tab 类名,协议组头对齐 `qc-sample-row__ctx`):
```scss
<style scoped lang="scss">
.qc-protocol-row {
display: flex;
flex-direction: column;
min-width: 0;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
.qc-protocol-row__ctx {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
padding: 12px 16px;
background: linear-gradient(180deg, var(--primary-bg, #ecfdf5) 0%, #fff 100%);
border-bottom: 1px solid #d1fae5;
}
.qc-protocol-row__idx {
flex-shrink: 0;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
min-width: 20px;
}
.qc-protocol-row__code {
font-size: 14px;
font-weight: 600;
color: #1e293b;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.qc-protocol-row__sub {
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.qc-protocol-row__badge {
flex-shrink: 0;
font-size: 11px;
font-weight: 600;
padding: 2px 10px;
border-radius: 999px;
background: var(--primary-bg, #ecfdf5);
color: #047857;
border: 1px solid #d1fae5;
}
.qc-protocol-row__fee {
border-bottom: 1px solid #e2e8f0;
}
.qc-protocol-sample-tabs {
padding: 12px 16px;
border-bottom: 1px solid #e2e8f0;
background: #f8fafc;
}
.qc-protocol-sample-tabs__row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 24px;
max-width: 100%;
}
/* 将 QcWorkOrderSampleGroup.vue 中 .qc-wo-indep-tabs__btn 及相关 pill Tab 样式复制到此处 */
</style>
```
- [ ] **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
<QcWorkOrderSampleGroup
v-for="(group, idx) in groupedRows"
```
替换为:
```vue
<QcWorkOrderProtocolGroup
v-if="cardMetaUsesInboundGroups(cardMetaConfig.tabKey)"
v-for="(group, idx) in protocolGroupedRows"
:key="group.groupKey"
:group="group"
:group-index="cardIndex(idx)"
...
/>
<QcWorkOrderSampleGroup
v-else
v-for="(group, idx) in groupedRows"
...
/>
```
或更简洁:`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 <token>" | 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
需要哪种方式开始写代码?