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: 抽出 flattenSampleFreights 工具与 SampleFreightPickTable 选批表格组件;两处弹窗改为 includeItems: true 加载并嵌入该组件;按 freightId 单选,不改 create/link API。
Tech Stack: Vue 3 + Element Plus + TypeScript(web-admin);既有 fetchSampleFreightList。
Spec: docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md
| 文件 | 职责 |
|---|---|
Create: web-admin/src/utils/sample-freight-flatten.ts |
flatten + spanMethod |
Create: web-admin/src/components/business/sample-freight-pick-table/index.vue |
关联已有选批表 |
Modify: web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue |
嵌入选批表、加宽、keyword |
Modify: web-admin/src/components/business/sample-transport-form-section/index.vue |
嵌入选批表、加宽、includeItems |
Modify(可选): web-admin/src/views/cell-storage/sample-freight/index.vue |
改用公共 flatten(减少重复) |
Files:
Create: web-admin/src/utils/sample-freight-flatten.ts
[x] Step 1: 新增工具文件
import type { SampleFreight } from '@/api/sample-freight'
export type SampleFreightFlatRow = {
rowKey: string
freightId: string
rowspan: number
isFirst: boolean
transportMode: string
transportModeLabel: string
freightAmount: number
linkedSampleCount: number
createBy?: string
createByName?: string
createTime: string
sampleSource?: string
protocolNo?: string
sampleCode?: string
sampleTypeName?: string
sampleId?: string
protocolId?: string
/** 原始批次,选中时回传 */
freight: SampleFreight
}
export function flattenSampleFreights(list: SampleFreight[]): SampleFreightFlatRow[] {
const rows: SampleFreightFlatRow[] = []
for (const f of list) {
const items = f.items?.length ? f.items : [null]
items.forEach((it, idx) => {
rows.push({
rowKey: `${f.id}|${it?.sampleId || idx}`,
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),
createBy: f.createBy || '',
createByName: f.createByName || f.createBy || '',
createTime: f.createTime || '',
sampleSource: it?.sampleSource,
protocolNo: it?.protocolNo,
sampleCode: it?.sampleCode,
sampleTypeName: it?.sampleTypeName,
sampleId: it?.sampleId,
protocolId: it?.protocolId,
freight: f
})
})
}
return rows
}
const MERGE_PROPS = new Set([
'selection',
'radio',
'transportMode',
'freightAmount',
'linkedSampleCount',
'createByName',
'createTime'
])
export function sampleFreightSpanMethod({
column,
row
}: {
column: { type?: string; property?: string }
row: SampleFreightFlatRow
}): { rowspan: number; colspan: number } {
const prop = column.type === 'selection' ? 'selection' : column.property || ''
if (MERGE_PROPS.has(prop)) {
if (row.rowspan > 0) return { rowspan: row.rowspan, colspan: 1 }
return { rowspan: 0, colspan: 0 }
}
return { rowspan: 1, colspan: 1 }
}
Files:
Create: web-admin/src/components/business/sample-freight-pick-table/index.vue
[x] Step 1: 实现选批表格
Props:
list: SampleFreight[]loading?: booleanmodelValue: string(选中的 freightId)maxHeight?: number | string(默认 280)keyword?: string(可选,仅展示由父级控制检索时可不内置)Emits:
update:modelValueselect → SampleFreight | null行为:
flattenSampleFreights + sampleFreightSpanMethodprop="radio"),仅 isFirst 行显示;点击任意行 setCurrentRow / 设置 modelValue 为该行 freightIdformatMoneyDisplay@row-click 选中整批模板要点(radio 列):
<el-table-column prop="radio" label="" min-width="48" align="center" header-align="center">
<template #default="{ row }">
<el-radio
v-if="row.isFirst"
:model-value="modelValue"
:value="row.freightId"
@change="emitSelect(row.freight)"
/>
</template>
</el-table-column>
@row-click="(row) => emitSelect(row.freight)"。
Files:
Modify: web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue
[x] Step 1: 弹窗 width 改为 1020px
[x] Step 2: 「关联已有」区替换为 keyword + SampleFreightPickTable
<div v-else class="link-existing-block" style="margin-bottom: 12px">
<div class="sample-pick-toolbar" style="margin-bottom: 10px">
<el-input
v-model="existingKeyword"
placeholder="运输方式/协议单号/样本编码"
clearable
style="width: 260px"
@keyup.enter="loadExistingFreights"
/>
<el-button class="btn-ghost" @click="loadExistingFreights">
<el-icon><Search /></el-icon>
搜索
</el-button>
</div>
<SampleFreightPickTable
v-model="selectedFreightId"
:list="existingFreightList"
:loading="existingLoading"
@select="onPickFreight"
/>
</div>
loadExistingFreights 传 includeItems: true 与 keywordconst res = await fetchSampleFreightList({
page: 1,
pageSize: 50,
includeItems: true,
keyword: existingKeyword.value || undefined
})
[x] Step 4: 用 selectedFreightId + selectedExisting 同步;open/closed 时清空 keyword 与选中
[x] Step 5: 提交时仍用 selectedExisting.value.id(或由 id 从 list 找回)
Files:
Modify: web-admin/src/components/business/sample-transport-form-section/index.vue
[x] Step 1: 配置运费弹窗 width 改为 1020px
[x] Step 2: 「关联已有」旧 el-table 换成 SampleFreightPickTable + 可选 keyword(与 Task 3 一致)
[x] Step 3: openFreightDialog 中 list 请求加 includeItems: true
[x] Step 4: selectedExistingFreight 由 pick 的 @select / v-model 更新;confirmFreight 的 link 分支不变
Files:
Modify: web-admin/src/views/cell-storage/sample-freight/index.vue
[x] Step 1: 删除本地 flattenFreights / 调整 spanMethod,改 import flattenSampleFreights + sampleFreightSpanMethod
[x] Step 2: FlatRow 可复用 SampleFreightFlatRow(操作列仍本地;span 合并 props 若含 actions,在页面包装一层把 actions 加入 merge 集合,或扩展 util 支持 extraMergeProps)
若扩展成本高可跳过本 Task,验收不依赖此项。
./scripts/update-graph.sh frontend| Spec 要求 | Task |
|---|---|
| 对齐运费信息列(含创建人) | 2 |
| 两入口一致 | 3, 4 |
| includeItems=true | 3, 4 |
| 按 freightId 单选 | 2 |
| 弹窗加宽 | 3, 4 |
| 轻量 keyword | 3, 4 |
| 不改 create/link 规则 | 3, 4 保持现有提交 |
| 创建人后端已有 | 前置已完成 |
SampleFreightFlatRow.freight 保证选中后无需再查 map。prop="radio" 须在 MERGE_PROPS 中,与 selection 同行为。