16 KiB
Sample Freight Menu 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: 在「细胞存储 → 样本信息管理」下新增「运费信息」菜单页,以合并单元格列表管理运费批次(含细胞存储/科研捐赠样本),支持配置/取消关联/编辑;协议单快捷入口保持不变。
Architecture: 复用 biz_sample_freight 与现有 CRUD/API;扩展列表 includeItems 与筛选;新增样本 options 接口;前端标准列表 + el-table span-method 渲染批次 rowspan。前端固定菜单模式须同步 sqxp.ts 与 backend/app/api/menu.py。
Tech Stack: FastAPI、SQLAlchemy、Vue 3、Element Plus、MySQL 菜单 SQL。
Spec: docs/superpowers/specs/2026-10-08-sample-freight-menu-design.md
Demo: docs/demos/2026-10-08-sample-freight-menu-demo.html
File map
| 文件 | 职责 |
|---|---|
docs/sql/migrations/2026-10-08-sample-freight-menu.sql |
sys_menu + 角色授权(DB 菜单模式) |
backend/app/api/menu.py |
前端菜单树插入运费信息 |
web-admin/src/router/modules/sqxp.ts |
前端路由 |
backend/app/crud/sample_freight.py |
list 增强、items enrichment、sample_options |
backend/app/api/sample_freight.py |
查询参数 + sample-options 路由 |
backend/tests/test_sample_freight.py |
list filters / span helper 单测 |
web-admin/src/api/sample-freight.ts |
客户端类型与 API |
web-admin/src/views/cell-storage/sample-freight/index.vue |
列表页 |
web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue |
配置运费弹窗 |
web-admin/src/views/cell-storage/sample-freight/FreightEditDialog.vue |
编辑方式/金额 |
web-admin/src/App.vue |
标题竖条规则(若需显式注册「运费信息」) |
Task 1: 菜单 SQL + 前后端路由骨架
Files:
-
Create:
docs/sql/migrations/2026-10-08-sample-freight-menu.sql -
Modify:
backend/app/api/menu.py(CellStorageSampleInfo节点后插入) -
Modify:
web-admin/src/router/modules/sqxp.ts(同上) -
Create:
web-admin/src/views/cell-storage/sample-freight/index.vue(空壳列表) -
Step 1: 写菜单迁移 SQL
菜单 id 使用:50000000000000000000000000000021
parent:CEL00000000000000000000000000001
perms:cell-storage:sample-freight:list
path:sample-freight
component:/cell-storage/sample-freight/index
order_num:取样本信息菜单 50000000000000000000000000000013 的 order_num + 1(或固定写在样本信息之后的数值,实现时查库确认;幂等 INSERT)。
授权:已有 cell-storage:sample-info:list(menu id 50000000000000000000000000000013)的角色补 sys_role_menu(对照 2026-10-05-presign-order-menu.sql)。
- Step 2:
menu.py在 SampleInfo 后插入
{
"id": "50000000000000000000000000000021",
"name": "CellStorageSampleFreight",
"path": "sample-freight",
"component": "/cell-storage/sample-freight/index",
"meta": {
"title": "运费信息",
"keepAlive": True,
"menuCode": "cell-storage:sample-freight:list",
},
},
-
Step 3:
sqxp.ts同步插入同结构路由(CellStorageSampleInfo与CellStoragePaymentConfirm之间) -
Step 4: 空壳页面
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">运费信息</h2>
</div>
<div class="filter-block"><!-- Task 5 填充 --></div>
<div class="list-toolbar"><!-- Task 5 --></div>
<div class="list-body list-wrap">
<el-empty description="运费列表加载中…" />
</div>
</section>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'CellStorageSampleFreight' })
</script>
- Step 5: Commit
git add docs/sql/migrations/2026-10-08-sample-freight-menu.sql \
backend/app/api/menu.py \
web-admin/src/router/modules/sqxp.ts \
web-admin/src/views/cell-storage/sample-freight/index.vue
git commit -m "feat: add sample freight menu route and page shell"
Task 2: 后端 list 增强(includeItems + 筛选)— TDD
Files:
-
Modify:
backend/app/crud/sample_freight.py -
Modify:
backend/app/api/sample_freight.py -
Modify:
backend/tests/test_sample_freight.py -
Step 1: 写失败测试 — enrich item 字段形状与 filter helper
在 test_sample_freight.py 追加:
class TestFreightListHelpers(unittest.TestCase):
def test_item_dict_has_enrichment_keys(self):
from app.crud.sample_freight import _item_to_dict_enriched
class I:
id = "i1"
freight_id = "f1"
sample_source = "cell_storage"
sample_id = "s1"
sample_code = "CODE1"
protocol_id = "p1"
d = _item_to_dict_enriched(
I(),
protocol_no="CP001",
sample_type_name="脐带血",
)
self.assertEqual(d["protocolNo"], "CP001")
self.assertEqual(d["sampleTypeName"], "脐带血")
self.assertEqual(d["sampleSource"], "cell_storage")
- Step 2: 跑测确认 FAIL
cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_sample_freight.py::TestFreightListHelpers -v
Expected: FAIL(_item_to_dict_enriched 不存在)
- Step 3: 实现 enrichment + 扩展
list_freights
签名改为:
def list_freights(
db,
*,
tenant_no: str,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
transport_mode: Optional[str] = None,
sample_source: Optional[str] = None, # cell_storage | research_donation
create_time_from: Optional[str] = None, # YYYY-MM-DD or datetime
create_time_to: Optional[str] = None,
include_items: bool = False,
) -> Tuple[List[Dict[str, Any]], int]:
行为要点:
transport_mode精确匹配BizSampleFreight.transport_mode(若传)。sample_source:仅保留「存在该 source 的 active item」的 freight(exists子查询)。keyword:扩展为 OR — 运输方式 value/label、item.sample_code、协议单号(细胞存储BizCollectionProtocol.protocol_no、科研捐赠对应单号字段,查research_donationmodel 确认字段名,常见donation_no/protocol_no)。- 时间范围过滤
create_time。 include_items=True:对本页 freight_ids 批量查 items,批量解析 protocolNo / sampleTypeName,写入每条items。
_item_to_dict_enriched 在 _item_to_dict 基础上增加 protocolNo、sampleTypeName。
- Step 4: API 层透传参数
GET /sample-freights 增加 Query:
includeItems: bool = FalsetransportMode: Optional[str]sampleSource: Optional[str]createTimeFrom/createTimeTo
注意:/sample-freights/sample-options 须注册在 /{freight_id} 之前,避免路径被当成 id。
- Step 5: 跑测 PASS + Commit
cd backend && PYTHONPATH=. pytest tests/test_sample_freight.py -v
git add backend/app/crud/sample_freight.py backend/app/api/sample_freight.py backend/tests/test_sample_freight.py
git commit -m "feat(backend): enrich sample freight list with items and filters"
Task 3: 样本 options API
Files:
-
Modify:
backend/app/crud/sample_freight.py(list_sample_options) -
Modify:
backend/app/api/sample_freight.py -
Modify:
backend/tests/test_sample_freight.py(可选:纯函数规范化) -
Step 1: 实现
list_sample_options
def list_sample_options(
db,
*,
tenant_no: str,
page: int = 1,
page_size: int = 20,
keyword: Optional[str] = None,
sample_source: Optional[str] = None, # None = both
) -> Tuple[List[Dict[str, Any]], int]:
"""
返回:
{
sampleSource, sampleId, sampleCode, protocolId, protocolNo,
sampleTypeName, freightId # 若已挂运费则带上,供前端提示覆盖
}
"""
实现策略(最小可用):
-
若
sample_source in (None, cell_storage):查BizProtocolSamplejoin 协议主表(del_flag=0),关键字匹配 sample_code / protocol_no。 -
若
sample_source in (None, research_donation):查捐赠样本 join 捐赠主表。 -
合并结果按
create_time DESC内存分页可接受首版;若数据量大改为 UNION SQL。 -
左连
biz_sample_freight_item取当前freightId。 -
Step 2: 路由
@router.get("/sample-freights/sample-options")
async def list_sample_freight_options(...):
...
放在 /{freight_id} 路由之前。
- Step 3: Commit
git add backend/app/crud/sample_freight.py backend/app/api/sample_freight.py
git commit -m "feat(backend): add sample options for freight config dialog"
Task 4: 前端 API 扩展
Files:
-
Modify:
web-admin/src/api/sample-freight.ts -
Step 1: 扩展类型与方法
export type SampleFreightItem = {
sampleSource: 'cell_storage' | 'research_donation'
sampleId: string
protocolId?: string
sampleCode?: string
protocolNo?: string
sampleTypeName?: string
}
export function fetchSampleFreightList(params?: {
page?: number
pageSize?: number
keyword?: string
transportMode?: string
sampleSource?: string
createTimeFrom?: string
createTimeTo?: string
includeItems?: boolean
}) { ... }
export function fetchSampleFreightSampleOptions(params?: {
page?: number
pageSize?: number
keyword?: string
sampleSource?: string
}) {
return request.get<PageList<SampleFreightItem & { freightId?: string }>>({
url: '/web/admin/sample-freights/sample-options',
params
})
}
- Step 2: Commit
git add web-admin/src/api/sample-freight.ts
git commit -m "feat(web-admin): extend sample-freight API client for menu page"
Task 5: 列表页(筛选 + 合并单元格表格)
Files:
-
Modify:
web-admin/src/views/cell-storage/sample-freight/index.vue -
Reference:
docs/demos/2026-10-08-sample-freight-menu-demo.html、web-admin/src/views/system/unit/index.vue -
Step 1: 扁平行模型 + span-method
type FlatRow = {
freightId: string
rowspan: number // 该批次第一行 = items.length || 1;其余 0
isFirst: boolean
transportMode: string
transportModeLabel: string
freightAmount: number
linkedSampleCount: number
createTime: string
sampleSource?: string
protocolNo?: string
sampleCode?: string
sampleTypeName?: string
sampleId?: string
protocolId?: string
}
function flattenFreights(list: SampleFreight[]): FlatRow[] {
const rows: FlatRow[] = []
for (const f of list) {
const items = f.items?.length ? f.items : [null]
items.forEach((it, idx) => {
rows.push({
freightId: f.id,
rowspan: idx === 0 ? items.length : 0,
isFirst: idx === 0,
transportMode: f.transportMode,
transportModeLabel: f.transportModeLabel || f.transportMode,
freightAmount: f.freightAmount,
linkedSampleCount: f.linkedSampleCount ?? (f.items?.length || 0),
createTime: f.createTime || '',
sampleSource: it?.sampleSource,
protocolNo: it?.protocolNo,
sampleCode: it?.sampleCode,
sampleTypeName: it?.sampleTypeName,
sampleId: it?.sampleId,
protocolId: it?.protocolId
})
})
}
return rows
}
function spanMethod({ column, row }: any) {
const mergeProps = [
'selection',
'transportMode',
'freightAmount',
'linkedSampleCount',
'createTime',
'actions'
]
if (!mergeProps.includes(column.property) && column.type !== 'selection') {
return { rowspan: 1, colspan: 1 }
}
if (column.type === 'selection' || mergeProps.includes(column.property)) {
if (row.rowspan > 0) return { rowspan: row.rowspan, colspan: 1 }
return { rowspan: 0, colspan: 0 }
}
return { rowspan: 1, colspan: 1 }
}
注意:为 selection 列设置 property 或在 span-method 用 columnIndex 判断(实现时以 Element Plus 实际 column 对象为准,对照 Demo)。
- Step 2: 筛选区 + 加载
调用 fetchSampleFreightList({ includeItems: true, page, pageSize, ...filters }),字典运输方式用 fetchDictDataOptions('sample_transport_mode')。
- Step 3: 工具栏
已选 N 项(按 freightId 去重)→ 配置运费 / 取消关联 / 编辑运费;仅第一个主按钮 type="primary";均带 icon。
- Step 4: 表格列
运输方式 Tag、运费金额、关联数、来源 Tag、协议单号、样本编码、样本类型、创建时间、操作(编辑 / 取消关联)。操作列 fixed="right" min-width=260。
- Step 5: Commit
git add web-admin/src/views/cell-storage/sample-freight/index.vue
git commit -m "feat(web-admin): render sample freight list with rowspan cells"
Task 6: 配置 / 编辑 / 取消关联弹窗
Files:
-
Create:
web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue -
Create:
web-admin/src/views/cell-storage/sample-freight/FreightEditDialog.vue -
Modify:
web-admin/src/views/cell-storage/sample-freight/index.vue -
Step 1: FreightConfigDialog
-
dialog-form-layout、label-position="top"、modal-class="protocol-dialog-mask" -
Tabs:新建 | 关联已有
-
新建:运输方式 select
filterableallow-create;金额AmountInput;样本多选(远程fetchSampleFreightSampleOptions,来源筛选) -
关联已有:
fetchSampleFreightList单选 + 样本多选 -
确定:create 或 link;若样本已有
freightId且不同,确认「将替换原运费关联」 -
Step 2: FreightEditDialog
运输方式 + 金额 → fetchUpdateSampleFreight;提示同步 N 个样本。
-
Step 3: 取消关联
-
工具栏:对每个选中 freightId
fetchSampleFreightDetail取 items 全量 unlink(或后端扩展 unlink-all;首版前端传全部 items) -
行内:弹窗勾选该批次 items 再 unlink
-
Step 4: Commit
git add web-admin/src/views/cell-storage/sample-freight/
git commit -m "feat(web-admin): add freight configure/edit/unlink dialogs on menu page"
Task 7: 标题竖条 + 收尾验证
Files:
-
Modify:
web-admin/src/App.vue(若「运费信息」未自动命中竖条,在panelTitleIconByText/ 等价规则中增加精确匹配「运费信息」) -
graphify
-
Step 1: 注册面板标题「运费信息」以显示主色竖条(对照现有「样本信息管理」写法)
-
Step 2: 跑后端测试
cd backend && PYTHONPATH=. pytest tests/test_sample_freight.py -v
Expected: PASS
- Step 3: 手测清单
- 前端模式侧栏出现「运费信息」在样本信息管理下
- 执行菜单 SQL(测试库)后 DB 模式亦可见(可选)
- 列表合并单元格;混源样本展示
- 配置/关联/取消/编辑与协议单数据一致
- 合同金额不受影响
- Step 4: 更新图谱
./scripts/update-graph.sh all
- Step 5: Commit
git add web-admin/src/App.vue backend/graphify-out web-admin/graphify-out
git commit -m "chore: register freight panel title and refresh graphify"
Spec coverage
| Spec | Task |
|---|---|
| 菜单位置 / 路由 / 权限 | 1, 7 |
| 合并单元格列表 | 5 |
| 筛选(方式/来源/关键字/时间) | 2, 5 |
| includeItems + protocolNo/typeName | 2 |
| 配置新建/关联 + 混选样本 | 3, 6 |
| 取消关联 / 编辑 | 6 |
| 协议单入口保留 | 无改协议单(验收手测) |
| Demo 视觉对齐 | 5(对照 HTML) |
Self-review notes
- 无 TBD;科研捐赠协议单号字段名实现时在 model 中确认后写入
list_freightskeyword 查询。 sample-options路由顺序:必须在/{freight_id}之前。- 前端固定菜单:
menu.py+sqxp.ts双写,不可只改 SQL。