11 KiB
细胞产品库 — 来源链路展示设计
日期:2026-05-24 状态:待审核 方案:A(自定义 Vue 组件 + CSS 绘制连线) Demo:
docs/demo-source-chain-graph.html
1. 需求概述
细胞产品库列表页(/#/production/cell-product)当前仅展示产品基础信息,缺少来源追溯能力。需要:
- 列表新增来源信息列:来源类型、来源协议号、来源工单号、来源任务号
- 列表新增来源类型筛选:下拉筛选(全部/生产入库/手动录入)
- 查看弹窗新增链路图:横向 DAG 可视化,展示从样本到当前产品的祖先链 + 当前产品到子代产品的后代链,双向递归到顶点/终点
- 链路图连线标注:连线上方显示工单号、任务号
数据模型
溯源链路涉及 5 张核心表:
biz_collection_protocol (采集协议单)
↓ collection_protocol_id
biz_protocol_sample (协议样本)
↓ protocol_sample_id + collection_protocol_id
biz_sample_reception (样本接收记录)
↓ reception_id + protocol_sample_id + collection_protocol_id
biz_production_work_order (生产工单) ← 冗余存储 protocol_no, sample_code
↓ work_order_id
biz_production_task (生产任务)
↓ task_id
biz_cell_product (细胞产品) ← production_task_id 关联到任务
后代链路通过投入物/产出物表关联:
父细胞产品 (biz_cell_product.id=A)
↓ biz_production_task_input(input_type='cell_product', input_id=A, task_id=T)
biz_production_task (id=T)
↓ biz_production_task_output(task_id=T, cell_product_id=B, C, ...)
子细胞产品们 (biz_cell_product.id=B, C, ...)
2. 后端改造
2.1 列表 API 增强
接口:GET /web/admin/cell-products(已有)
改造文件:backend/app/crud/cell_product.py
2.1.1 _enrich_product_dicts() 增加来源富化
在现有富化逻辑(productTypeName, specName, storageSlotName)之后,增加来源信息的批量富化查询:
- 收集所有非空
production_task_id - 一次查询
biz_production_task,获取task_no和work_order_id - 一次查询
biz_production_work_order,获取work_order_no、protocol_no、reception_source - 映射回每条产品记录
新增返回字段:
| 字段名 | 类型 | 来源路径 | 说明 |
|---|---|---|---|
sourceTypeLabel |
str | source_type 枚举映射 | 来源类型中文 |
sourceProtocolNo |
str | task → work_order → protocol_no | 来源协议号 |
sourceWorkOrderNo |
str | task → work_order → work_order_no | 来源工单号 |
sourceTaskNo |
str | task → task_no | 来源生产任务号 |
sourceReceptionSource |
str | work_order → reception_source | cell_storage/research_donation |
source_type 枚举映射:
production→ "生产入库"manual→ "手动录入"
2.1.2 list_cell_products() 增加筛选参数
入参新增 source_type: Optional[str] = None,在查询条件中增加:
if source_type:
query = query.filter(BizCellProduct.source_type == source_type)
2.1.3 Schema 更新
CellProductListQuery 新增 source_type: Optional[str] = None。
API 路由层 production.py 从 query params 读取 source_type 传入 service。
2.2 新增链路 API
接口:GET /web/admin/cell-products/{product_id}/chain
新文件:无(在 backend/app/crud/cell_product.py 中新增函数,在 backend/app/api/production.py 中新增路由)
2.2.1 返回数据结构
{
"current": {
"id": "uuid",
"productCode": "CP001",
"productName": "脐带间充质干细胞",
"nodeType": "cell_product",
"productStatus": "frozen",
"passage": "P2",
"isCurrent": true
},
"ancestors": [
{
"node": {
"id": "uuid",
"productCode": "CP000",
"productName": "...",
"nodeType": "cell_product",
"productStatus": "frozen",
"passage": "P1",
"isCurrent": false
},
"edge": {
"workOrderNo": "WO001",
"workOrderType": "cell_storage",
"taskNo": "T001",
"taskName": "制备任务"
},
"parent": null
}
],
"descendants": [
{
"node": {
"id": "uuid",
"productCode": "CP002",
"productName": "...",
"nodeType": "cell_product",
"productStatus": "frozen",
"passage": "P3",
"isCurrent": false
},
"edge": {
"workOrderNo": "WO002",
"workOrderType": "outbound",
"taskNo": "T002",
"taskName": "出库生产"
},
"children": []
}
]
}
当追溯链路中遇到样本节点(非细胞产品)时,nodeType = "sample",包含 sampleCode 和 sampleType 字段。
2.2.2 递归查询逻辑
祖先方向(从当前产品向上追溯):
- 从
biz_cell_product.production_task_id获取任务 - 从
biz_production_task.work_order_id获取工单(含 protocol_no, sample_code) - 检查该任务的投入物:
biz_production_task_input(input_type='cell_product')是否有父产品 - 如有父产品,递归继续(将父产品作为新的起点)
- 如投入物是样本(
input_type='sample'),创建样本节点,终止递归 - 如
production_task_id为空或source_type='manual',终止递归
子代方向(从当前产品向下追溯):
- 查询
biz_production_task_input(input_type='cell_product', input_id=当前产品id)找到所有引用此产品的任务 - 对每个任务,查询
biz_production_task_output获取产出的细胞产品列表 - 对每个产出产品,递归继续(1对多分支)
- 如无任务引用此产品,终止递归
性能保护:最大递归深度 10 层。
2.2.3 路由注册
在 backend/app/api/production.py Section 11(细胞产品库)中新增路由,放在 GET /{product_id} 之后。
3. 前端改造
3.1 列表页
文件:web-admin/src/views/production/cell-product/index.vue
3.1.1 新增表格列
在 productStatus(状态)列之后,新增 4 列:
| prop | label | min-width | 渲染 |
|---|---|---|---|
sourceTypeLabel |
来源类型 | 120 | <el-tag>:生产入库=success、手动录入=info |
sourceProtocolNo |
来源协议号 | 160 | 文本,无值显示 "—" |
sourceWorkOrderNo |
来源工单号 | 140 | 文本,无值显示 "—" |
sourceTaskNo |
来源任务号 | 140 | 文本,无值显示 "—" |
3.1.2 新增搜索筛选
搜索栏增加「来源类型」下拉,位于 productStatus 筛选之后:
<el-select v-model="searchForm.sourceType" placeholder="来源类型" clearable>
<el-option label="生产入库" value="production" />
<el-option label="手动录入" value="manual" />
</el-select>
3.1.3 查看弹窗基础信息
在现有 viewSections 的「基本信息」section 中追加 2 个 ViewField:
| field | label | 渲染 |
|---|---|---|
sourceTypeLabel |
来源类型 | tagType 映射 |
sourceProtocolNo |
来源协议号 | 文本 |
3.2 链路图组件
新文件:web-admin/src/views/production/cell-product/SourceChainGraph.vue
3.2.1 组件职责
- 接收
productIdprop - 调用
GET /web/admin/cell-products/{id}/chain获取链路数据 - 渲染横向 DAG:祖先 → 当前 → 子代
- 支持节点点击查看详情(Element Plus Popover)
3.2.2 组件 Props
| prop | type | 说明 |
|---|---|---|
productId |
string | 当前细胞产品 ID |
visible |
boolean | 是否显示(由父组件控制) |
3.2.3 布局结构
SourceChainGraph
├── 祖先区域(水平 flex,从左到右:最早祖先 → ... → 当前节点)
│ ├── ChainNode(递归渲染祖先链)
│ └── ChainConnector(连线 + 工单/任务标签)
├── 当前节点(高亮样式,角标「当前」)
└── 子代区域(支持分支,水平 flex-wrap)
├── ChainNode(递归渲染子代链,1对多用 flex-wrap)
└── ChainConnector(连线 + 工单/任务标签)
3.2.4 节点样式
| 节点类型 | 背景 | 边框 | 内容 |
|---|---|---|---|
| 样本 | #eff6ff(浅蓝) |
#93c5fd |
编码、类型 |
| 细胞产品 | #ecfdf5(浅绿) |
#a7f3d0 |
编码、名称、代次 |
| 当前产品 | #ecfdf5 |
#10b981(2px 加粗) |
编码、名称、代次 + 角标「当前」 |
节点尺寸:148×60px,内容过长用 text-overflow: ellipsis 截断。
3.2.5 连线样式
- 水平线:2px border,颜色
#d1d5db - 箭头:CSS border 三角形
- 连线上方标签:绝对定位
<span>,显示工单号和任务号,字号 10px - 分叉线:从父节点右侧引出竖线 + 横线到各子节点
3.2.6 查看弹窗集成
在 index.vue 的 SystemViewDialog 中新增第三个 section「来源链路」,使用 template #sourceChain 自定义插槽:
<SystemViewDialog ...>
<!-- 基本信息 section -->
<!-- 产品图片 section -->
<!-- 来源链路 section (新增) -->
<template #sourceChain>
<SourceChainGraph :product-id="currentRow.id" :visible="viewDialogVisible" />
</template>
</SystemViewDialog>
SystemViewDialog 需支持第三个 section(通过 viewSections 配置中增加 slot: 'sourceChain')。
3.3 API 层
文件:web-admin/src/api/cell-product.ts
新增接口函数:
export function fetchCellProductChain(productId: string) {
return request.get({ url: `/web/admin/cell-products/${productId}/chain` })
}
fetchCellProductList 的参数类型新增 sourceType?: string。
4. 影响范围
后端文件
| 文件 | 改动类型 |
|---|---|
backend/app/crud/cell_product.py |
修改:_enrich_product_dicts 增加来源富化,新增 get_product_chain() 递归函数 |
backend/app/schemas/cell_product.py |
修改:CellProductListQuery 增加 source_type 参数 |
backend/app/api/production.py |
修改:列表路由读取 source_type 参数,新增 chain 路由 |
前端文件
| 文件 | 改动类型 |
|---|---|
web-admin/src/views/production/cell-product/index.vue |
修改:新增表格列、搜索筛选、查看弹窗字段 |
web-admin/src/views/production/cell-product/SourceChainGraph.vue |
新增:链路图组件 |
web-admin/src/api/cell-product.ts |
修改:新增 fetchCellProductChain,fetchCellProductList 参数增加 sourceType |
不涉及的文件
- 数据库无表结构变更(无新表、无新字段)
- 不涉及其他模块(订单、协议单、质检等)
5. 约束与边界
- 手动录入产品(起源节点):无
production_task_id,所以祖先链为空。但后续如果被用于生产,子代链路仍然会展示。只有既无祖先又无子代的孤立产品才显示空状态提示「该产品暂无来源及衍生链路信息」 - 最大递归深度:10 层,防止异常数据导致无限递归
- 性能:链路 API 单次查询,预估节点数 ≤ 50,响应时间 < 500ms
- 科研捐赠来源:通过
reception_source='research_donation'标识,链路追溯路径相同 - 子代追踪:依赖
biz_production_task_input(input_type='cell_product')记录,如投入物未正确录入则无法追踪子代 - 配色:严格使用 Fresh Mint 色系(
#10b981、#ecfdf5),样本节点使用蓝色系(#eff6ff、#93c5fd)做区分