12 KiB
质检工单 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 配置
// 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 过滤之后:
rs = (reception_source or "").strip()
if rs:
q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
- Step 2: API 路由暴露参数
receptionSource: Optional[str] = Query(None, alias="receptionSource"),
# ...
reception_source=receptionSource,
- Step 3: 手动验证
启动 backend 后请求(需有效 token):
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 过滤后:
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-taskimport 同一套tabs/*,不重复维护 -
Step 3: 删除
QcWorkOrderViewDialogHost.vue、OutboundQcViewDialog.vue、useQcWorkOrderViewDialog.ts -
Step 4: 更新
qc-mine-task-groups.tsTabKey 与 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.
两种执行方式:
- Subagent-Driven(推荐) — 按 Task 逐条派发子 agent,每步完成后 review
- Inline Execution — 本会话按 Task 1→13 连续实现,关键节点暂停确认
请告知选择哪种方式;若直接开始实现,将从 Task 1 执行。