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