diff --git a/docs/superpowers/plans/2026-07-27-pl-plan-batch-print-merge.md b/docs/superpowers/plans/2026-07-27-pl-plan-batch-print-merge.md new file mode 100644 index 000000000..6a5e43886 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-pl-plan-batch-print-merge.md @@ -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`→`
` | +| `web-admin/src/utils/pl-delivery-slip-template.ts` | 同上(送货) | +| `web-admin/src/utils/pl-subpack-slip-template.ts` | 备注 `\n`→`
`(序号已按合并 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` 若把备注还原为占位符,保持现逻辑即可,勿把 `
` 写回污染。 + +- [ ] **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 + + + 打印单据 + + + + +``` + +- [ ] **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;步骤含具体代码与命令。