docs(pl): add batch print merge implementation plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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;步骤含具体代码与命令。
|
||||
Reference in new issue
Block a user