日期:2026-05-24 状态:待审核 方案:A(自定义 Vue 组件 + CSS 绘制连线) Demo:
docs/demo-source-chain-graph.html
细胞产品库列表页(/#/production/cell-product)当前仅展示产品基础信息,缺少来源追溯能力。需要:
溯源链路涉及 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, ...)
接口:GET /web/admin/cell-products(已有)
改造文件:backend/app/crud/cell_product.py
_enrich_product_dicts() 增加来源富化在现有富化逻辑(productTypeName, specName, storageSlotName)之后,增加来源信息的批量富化查询:
production_task_idbiz_production_task,获取 task_no 和 work_order_idbiz_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 → "手动录入"list_cell_products() 增加筛选参数入参新增 source_type: Optional[str] = None,在查询条件中增加:
if source_type:
query = query.filter(BizCellProduct.source_type == source_type)
CellProductListQuery 新增 source_type: Optional[str] = None。
API 路由层 production.py 从 query params 读取 source_type 传入 service。
接口:GET /web/admin/cell-products/{product_id}/chain
新文件:无(在 backend/app/crud/cell_product.py 中新增函数,在 backend/app/api/production.py 中新增路由)
{
"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 字段。
祖先方向(从当前产品向上追溯):
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 获取产出的细胞产品列表性能保护:最大递归深度 10 层。
在 backend/app/api/production.py Section 11(细胞产品库)中新增路由,放在 GET /{product_id} 之后。
文件:web-admin/src/views/production/cell-product/index.vue
在 productStatus(状态)列之后,新增 4 列:
| prop | label | min-width | 渲染 |
|---|---|---|---|
sourceTypeLabel |
来源类型 | 120 | <el-tag>:生产入库=success、手动录入=info |
sourceProtocolNo |
来源协议号 | 160 | 文本,无值显示 "—" |
sourceWorkOrderNo |
来源工单号 | 140 | 文本,无值显示 "—" |
sourceTaskNo |
来源任务号 | 140 | 文本,无值显示 "—" |
搜索栏增加「来源类型」下拉,位于 productStatus 筛选之后:
<el-select v-model="searchForm.sourceType" placeholder="来源类型" clearable>
<el-option label="生产入库" value="production" />
<el-option label="手动录入" value="manual" />
</el-select>
在现有 viewSections 的「基本信息」section 中追加 2 个 ViewField:
| field | label | 渲染 |
|---|---|---|
sourceTypeLabel |
来源类型 | tagType 映射 |
sourceProtocolNo |
来源协议号 | 文本 |
新文件:web-admin/src/views/production/cell-product/SourceChainGraph.vue
productId propGET /web/admin/cell-products/{id}/chain 获取链路数据| prop | type | 说明 |
|---|---|---|
productId |
string | 当前细胞产品 ID |
visible |
boolean | 是否显示(由父组件控制) |
SourceChainGraph
├── 祖先区域(水平 flex,从左到右:最早祖先 → ... → 当前节点)
│ ├── ChainNode(递归渲染祖先链)
│ └── ChainConnector(连线 + 工单/任务标签)
├── 当前节点(高亮样式,角标「当前」)
└── 子代区域(支持分支,水平 flex-wrap)
├── ChainNode(递归渲染子代链,1对多用 flex-wrap)
└── ChainConnector(连线 + 工单/任务标签)
| 节点类型 | 背景 | 边框 | 内容 |
|---|---|---|---|
| 样本 | #eff6ff(浅蓝) |
#93c5fd |
编码、类型 |
| 细胞产品 | #ecfdf5(浅绿) |
#a7f3d0 |
编码、名称、代次 |
| 当前产品 | #ecfdf5 |
#10b981(2px 加粗) |
编码、名称、代次 + 角标「当前」 |
节点尺寸:148×60px,内容过长用 text-overflow: ellipsis 截断。
#d1d5db<span>,显示工单号和任务号,字号 10px在 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')。
文件: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。
| 文件 | 改动类型 |
|---|---|
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 |
production_task_id,所以祖先链为空。但后续如果被用于生产,子代链路仍然会展示。只有既无祖先又无子代的孤立产品才显示空状态提示「该产品暂无来源及衍生链路信息」reception_source='research_donation' 标识,链路追溯路径相同biz_production_task_input(input_type='cell_product') 记录,如投入物未正确录入则无法追踪子代#10b981、#ecfdf5),样本节点使用蓝色系(#eff6ff、#93c5fd)做区分