|
|
@@ -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;步骤含具体代码与命令。
|