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

12 KiB
Raw Blame History

质检工单 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-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 执行。