docs(pl): add batch print merge implementation plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-27 20:45:00 +08:00
1 parent 8d76c6bec6
commit 92736acced
1 file changed
+352
@@ -0,0 +1,352 @@
# 运输计划多选合打打印 — 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:** 前端纯函数校验与合打视图拼装;三个打印弹窗 `open` 支持 `string | string[]`;列表工具栏 N≥1 启用打印并按规则禁用菜单项。不改后端、不改模版 HTML 结构。
**Tech Stack:** Vue 3 + TypeScript + Element Plus;现有 `pl-*-slip-template.ts` / 打印弹窗
**Spec:** `docs/superpowers/specs/2026-07-27-pl-plan-batch-print-merge-design.md`
---
## 文件结构
| 文件 | 职责 |
|------|------|
| `web-admin/src/utils/pl-batch-print.ts` | `canBatchPrintSlip`、备注合并、合打 Plan 视图、清空车辆字段 |
| `web-admin/src/utils/pl-inbound-slip-template.ts` | `buildProductRows` 支持行级 `slipPlanNo`;备注 `\n`→`<br />` |
| `web-admin/src/utils/pl-delivery-slip-template.ts` | 同上(送货) |
| `web-admin/src/utils/pl-subpack-slip-template.ts` | 备注 `\n`→`<br />`(序号已按合并 products 递增) |
| `web-admin/src/components/business/pl-inbound-slip-print-dialog/index.vue` | `open(ids)` 多计划拉取+合打 |
| `web-admin/src/components/business/pl-delivery-slip-print-dialog/index.vue` | 同上 |
| `web-admin/src/components/business/pl-subpack-slip-print-dialog/index.vue` | 同上 |
| `web-admin/src/views/pl/plan/index.vue` | 工具栏多选打印入口 |
---
### Task 1: 合打纯函数 + 自检
**Files:**
- Create: `web-admin/src/utils/pl-batch-print.ts`
- Test via: `cd web-admin && pnpm exec tsx -e "…"` 或自检脚本
- [ ] **Step 1: 先写 `pl-batch-print.ts`(见 Step 2 完整代码),再用 tsx 跑断言**
因仓库无 vitest,用 `tsx` 直接测正式模块:
```bash
cd web-admin && pnpm exec tsx <<'TS'
import assert from 'node:assert/strict'
import {
canBatchPrintSlip,
mergeBatchPrintRemarks,
buildBatchPrintPlanView
} from './src/utils/pl-batch-print.ts'
const same = [
{ id: '1', planNo: 'A', planType: 'inbound', customerName: '云栋', planDate: '2026-07-27', remark: '早到', hasFreight: 0, totalAmount: 0, freightAmount: 0, createTime: '' },
{ id: '2', planNo: 'B', planType: 'inbound', customerName: '云栋', planDate: '2026-07-27', remark: '质检', hasFreight: 0, totalAmount: 0, freightAmount: 0, createTime: '' }
] as any
assert.equal(canBatchPrintSlip(same, 'inbound').ok, true)
assert.equal(canBatchPrintSlip([{ ...same[0], customerName: '其他' }, same[1]], 'inbound').ok, false)
assert.equal(
canBatchPrintSlip(
[
{ planNo: 'O1', planType: 'outbound', customerName: '甲', planDate: '2026-07-01' },
{ planNo: 'O2', planType: 'outbound', customerName: '乙', planDate: '2026-07-02' }
] as any,
'subpack'
).ok,
true
)
assert.equal(mergeBatchPrintRemarks(same), 'A:早到\nB:质检')
const view = buildBatchPrintPlanView([
{ ...same[0], products: [{ productId: 'p1', productNo: 'n1', productName: '酸', unit: '吨', quantity: 1 }] },
{ ...same[1], products: [{ productId: 'p2', productNo: 'n2', productName: '碱', unit: '吨', quantity: 2 }] }
] as any)
assert.equal(view.products?.length, 2)
assert.equal((view.products as any)[0].slipPlanNo, 'A')
assert.equal(view.remark, 'A:早到\nB:质检')
console.log('pl-batch-print: ok')
TS
```
Expected: `pl-batch-print: ok`
(实现文件内容见原 Step 2 代码块;可删去 `pl-batch-print.selftest.mjs` 双写方案,只保留 `.ts`。)
- [ ] **Step 2: 实现 `web-admin/src/utils/pl-batch-print.ts`**
(完整代码同上一版 Step 2 中的 TypeScript 模块。)
- [ ] **Step 3: 再跑 Step 1 命令,确认 PASS**
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/utils/pl-batch-print.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add batch print merge helpers for transport plans
EOF
)"
```
---
### Task 2: 模版 buildProductRows 支持行级计划号 + 备注换行
**Files:**
- Modify: `web-admin/src/utils/pl-inbound-slip-template.ts`(`buildProductRows`、`replaceInboundSlipPlaceholders` 中 `{{remark}}`)
- Modify: `web-admin/src/utils/pl-delivery-slip-template.ts`(同上)
- Modify: `web-admin/src/utils/pl-subpack-slip-template.ts`(`{{remark}}` 替换)
- [ ] **Step 1: 入库单**
在 `buildProductRows` 内,将单行 `planNoText` 改为:
```ts
import type { SlipPlanProduct } from '@/utils/pl-batch-print'
import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
// 循环内:
const rowPlanNo = (product as SlipPlanProduct).slipPlanNo || planNo
const planNoText = formatInboundSlipProductGridCellContent(rowPlanNo)
```
`replaceInboundSlipPlaceholders`(及 restore 对称处若有写死 remark)中:
```ts
'{{remark}}': formatRemarkForSlipHtml(plan.remark),
```
注意:`restoreInboundSlipPlaceholders` 若把备注还原为占位符,保持现逻辑即可,勿把 `<br />` 写回污染。
- [ ] **Step 2: 送货单**
对 `pl-delivery-slip-template.ts` 的 `buildProductRows` 做同样 `slipPlanNo` 处理;`{{remark}}` 用 `formatRemarkForSlipHtml`。`{{receiveContact}}` 保持 `''`(已有则不动)。
- [ ] **Step 3: 分装单**
仅改备注替换为 `formatRemarkForSlipHtml(plan.remark)`;产品行已按 `products` 长度递增序号,合打展开 products 后自然正确。
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/utils/pl-inbound-slip-template.ts \
web-admin/src/utils/pl-delivery-slip-template.ts \
web-admin/src/utils/pl-subpack-slip-template.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): support per-row planNo and multiline remarks on slip templates
EOF
)"
```
---
### Task 3: 三个打印弹窗支持 `open(string | string[])`
**Files:**
- Modify: `pl-inbound-slip-print-dialog/index.vue`
- Modify: `pl-delivery-slip-print-dialog/index.vue`
- Modify: `pl-subpack-slip-print-dialog/index.vue`
对各弹窗统一模式(以入库为例):
- [ ] **Step 1: 改 `open`**
```ts
import {
buildBatchPrintPlanView,
normalizePlanIds
} from '@/utils/pl-batch-print'
async function open(planIdOrIds: string | string[]) {
const ids = normalizePlanIds(planIdOrIds)
if (!ids.length) {
ElMessage.warning('无法获取计划ID')
return
}
currentPlanId.value = ids[0] // 模板读写仍落在第一条
isPreviewMode.value = false
showRedSeal.value = true
try {
ElMessage.info({ message: ids.length > 1 ? '正在生成合打入库单...' : '正在生成入库单...', duration: 800 })
const plans = await Promise.all(
ids.map(async (id) => {
const res = await planApi.get(id)
return (res.data ?? res) as Plan
})
)
const plan = ids.length === 1 ? plans[0] : buildBatchPrintPlanView(plans)
currentPlan.value = plan
let htmlContent = ''
try {
const templateRes = await planApi.getInboundSlipTemplate(ids[0])
const saved = (templateRes.data ?? templateRes) as string
htmlContent = saved?.trim() ? saved : generateInboundSlipTemplate(plan)
} catch {
htmlContent = generateInboundSlipTemplate(plan)
}
const renderCtx = await buildSlipRenderContext(plan)
slipContent.value = replaceInboundSlipPlaceholders(htmlContent, plan, renderCtx)
visible.value = true
} catch (error) {
console.error('打开入库单打印失败', error)
ElMessage.error('加载入库单失败')
}
}
```
送货 / 分装:同样改 `open`,文案改为「合打送(提)货单」「合打分装单」;分装取号仍用 `ids[0]` 走现网逻辑。
`defineExpose({ open })` 签名自然兼容旧调用 `open(id)`。
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/components/business/pl-inbound-slip-print-dialog/index.vue \
web-admin/src/components/business/pl-delivery-slip-print-dialog/index.vue \
web-admin/src/components/business/pl-subpack-slip-print-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): allow multi planIds in transport slip print dialogs
EOF
)"
```
---
### Task 4: 列表工具栏多选合打入口
**Files:**
- Modify: `web-admin/src/views/pl/plan/index.vue`
- [ ] **Step 1: 导入与计算属性**
```ts
import { canBatchPrintSlip } from '@/utils/pl-batch-print'
```
模板中打印下拉:
```vue
<el-dropdown
trigger="click"
:disabled="selectedRows.length < 1"
@command="handleToolbarPrintSlip"
>
<el-button class="btn-ghost" :disabled="selectedRows.length < 1">
<el-icon><Printer /></el-icon>打印单据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="inbound"
:disabled="!canBatchPrintSlip(selectedRows, 'inbound').ok"
>
入库单
</el-dropdown-item>
<el-dropdown-item
command="delivery"
:disabled="!canBatchPrintSlip(selectedRows, 'delivery').ok"
>
送(提)货单
</el-dropdown-item>
<el-dropdown-item
command="subpack"
:disabled="!canBatchPrintSlip(selectedRows, 'subpack').ok"
>
分装单
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
```
- [ ] **Step 2: 改 `handleToolbarPrintSlip`**
```ts
function handleToolbarPrintSlip(command: string) {
const cmd = command as PlPrintSlipCommand
const plans = selectedRows.value
const check = canBatchPrintSlip(plans, cmd)
if (!check.ok) {
ElMessage.warning(check.reason || '无法打印')
return
}
const ids = plans.map((p) => p.id)
if (cmd === 'inbound') {
inboundSlipPrintRef.value?.open(ids)
return
}
if (cmd === 'delivery') {
deliverySlipPrintRef.value?.open(ids)
return
}
subpackSlipPrintRef.value?.open(ids)
}
```
保留 `handlePrintSlip(command, row)` 给查看弹窗单条使用,不改。
- [ ] **Step 3: 手工验收**
1. 选 1 条入库 → 入库单,与改前一致。
2. 选同客户同日 2 条入库 → 合打物料多行、单据编号各异、备注 `计划号:…` 换行。
3. 跨客户勾选 → 入库/送货菜单禁用。
4. 跨客户出库多选 → 分装可开,入库/送货不可。
5. 查看弹窗打印仍单条。
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/views/pl/plan/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): enable multi-select batch slip print on plan list toolbar
EOF
)"
```
---
### Task 5: graphify 更新
- [ ] **Step 1:** `./scripts/update-graph.sh frontend`(或 `all`)
- [ ] **Step 2:** 提交图谱变更(若有)
```bash
git add web-admin/graphify-out/GRAPH_REPORT.md web-admin/graphify-out/graph.json
git commit -m "$(cat <<'EOF'
chore: update graphify after pl batch print merge
EOF
)" || true
```
---
## Spec 覆盖自检
| Spec 项 | Task |
|---------|------|
| D1 前端合打 | 3–4 |
| D2–D4 校验 | 1, 4 |
| D5 分行 + 单据编号 | 1–2 |
| D6 备注格式 | 1–2 |
| D7–D8 联系方式空/无车辆 | 1(`buildBatchPrintPlanView`)+ delivery 既有 receiveContact |
| D9 模版结构不变 | 2(仅数据) |
| D10 仅列表工具栏 | 4 |
| D11 单选兼容 | 3–4 |
## Placeholder 扫描
无 TBD;步骤含具体代码与命令。