Files
shanxia2.0/docs/superpowers/plans/2026-08-22-pl-plan-blank-slip-print.md
T
2026-08-22 11:21:47 +08:00

17 KiB
Raw Blame History

运输计划空白单据打印 — 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: 三类 pl-*-slip-template.ts 各导出 renderBlank*SlipHtml()(默认模板 + 全空业务字段 + 空产品行);三打印弹窗 defineExpose({ open, openBlank });列表工具栏「打印单据」始终可点并增加三个空白菜单项。不改后端。

Tech Stack: Vue 3 + TypeScript + Element Plus;现有 pl-*-slip-template.ts / 打印弹窗;自检用 pnpm exec tsx

Spec: docs/superpowers/specs/2026-08-22-pl-plan-blank-slip-print-design.md


文件结构

文件 职责
web-admin/src/utils/pl-inbound-slip-template.ts BLANK_SLIP_PRODUCT_MIN_ROWS;buildProductRows 空白空行;renderBlankInboundSlipHtml()
web-admin/src/utils/pl-delivery-slip-template.ts 同上;空白时 {{companySeal}} 为空
web-admin/src/utils/pl-subpack-slip-template.ts renderBlankSubpackSlipHtml():单号/日期/人员等全空,产品用现有最小空行
web-admin/src/components/business/pl-inbound-slip-print-dialog/index.vue openBlank();空白态隐藏保存模板
web-admin/src/components/business/pl-delivery-slip-print-dialog/index.vue 同上;空白默认关章
web-admin/src/components/business/pl-subpack-slip-print-dialog/index.vue 同上;不调 ensureSubpackSlipNo
web-admin/src/views/pl/plan/index.vue 工具栏按钮始终可点;下拉加空白三项;handler 分流

Task 1: 入库单 / 送(提)货单空白 HTML 纯函数

Files:

  • Modify: web-admin/src/utils/pl-inbound-slip-template.ts

  • Modify: web-admin/src/utils/pl-delivery-slip-template.ts

  • Step 1: 在入库模板中增加空行常量与 buildProductRows 空白分支

在 web-admin/src/utils/pl-inbound-slip-template.ts 靠近 buildProductRows 处增加:

/** 空白单据物料空行数(与分装单最小行数对齐) */
export const BLANK_SLIP_PRODUCT_MIN_ROWS = 5

将 buildProductRows 签名改为支持空白模式(在文件内其它调用处不传第三参后的 options,行为不变):

function buildProductRows(
  products: PlanProduct[] = [],
  planNo = '',
  ctx?: InboundSlipRenderContext,
  options?: { blank?: boolean }
): string {
  if (options?.blank) {
    const rows: string[] = []
    for (let i = 0; i < BLANK_SLIP_PRODUCT_MIN_ROWS; i += 1) {
      rows.push(`<tr>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
        <td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">&nbsp;</td>
      </tr>`)
    }
    return rows.join('')
  }
  if (!products.length) {
    return `<tr><td class="inbound-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
  }
  // ... 保持现有 dataRows 逻辑不变
}

在 replaceInboundSlipPlaceholders 增加可选第四参,并在 {{productTable}} 处传入:

export function replaceInboundSlipPlaceholders(
  htmlContent: string,
  plan: Plan,
  ctx?: InboundSlipRenderContext,
  options?: { blank?: boolean }
): string {
  if (!htmlContent || !plan) return htmlContent

  const replacements: Record<string, string> = {
    '{{companyName}}': INBOUND_SLIP_DEFAULTS.companyName,
    '{{receivingUnit}}': options?.blank ? '' : buildDeliveryUnit(plan),
    '{{planNo}}': options?.blank ? '' : plan.planNo || '',
    '{{receiveContact}}': '',
    '{{driverInfo}}': '',
    '{{planDateChinese}}': options?.blank ? '' : formatPlanDateChinese(plan.planDate),
    '{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx, options),
    '{{remark}}': options?.blank ? '' : formatRemarkForSlipHtml(plan.remark),
    '{{companyAddress}}': INBOUND_SLIP_DEFAULTS.companyAddress,
    '{{companyPhone}}': INBOUND_SLIP_DEFAULTS.companyPhone,
    '{{contactPhone}}': INBOUND_SLIP_DEFAULTS.contactPhone,
    '{{companySeal}}': buildInboundSlipCompanySealHtml(),
    '{{sideLabels}}': buildInboundSlipSideLabelsHtml()
  }
  // ... 其余 normalize + replace 逻辑不变
}

文件末尾(或 replaceInboundSlipPlaceholders 之后)新增:

/** 空白入库单 HTML:默认模板 + 业务字段全空 + 空产品行(保留印章) */
export function renderBlankInboundSlipHtml(): string {
  const blankPlan = {
    id: '',
    planNo: '',
    products: [],
    customerName: '',
    remark: ''
  } as Plan
  const html = generateInboundSlipTemplate(blankPlan)
  return replaceInboundSlipPlaceholders(html, blankPlan, undefined, { blank: true })
}
  • Step 2: 对送(提)货单模板做对称改动

在 web-admin/src/utils/pl-delivery-slip-template.ts:

  1. 增加:
export const BLANK_SLIP_PRODUCT_MIN_ROWS = 5
  1. buildProductRows 增加 options?: { blank?: boolean },空白时输出 8 列 × BLANK_SLIP_PRODUCT_MIN_ROWS 空行(class 前缀为 delivery-slip-)。

  2. replaceDeliverySlipPlaceholders 增加第四参 options?: { blank?: boolean }:

'{{receivingUnit}}': options?.blank ? '' : buildReceivingUnit(plan),
'{{planNo}}': options?.blank ? '' : plan.planNo || '',
'{{planDateChinese}}': options?.blank ? '' : formatPlanDateChinese(plan.planDate),
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx, options),
'{{remark}}': options?.blank ? '' : formatRemarkForSlipHtml(plan.remark),
'{{companySeal}}': options?.blank ? '' : buildDeliverySlipCompanySealHtml(),

其余字段与现网一致(公司名等 defaults 保留)。

  1. 新增:
export function renderBlankDeliverySlipHtml(): string {
  const blankPlan = {
    id: '',
    planNo: '',
    products: [],
    customerName: '',
    remark: ''
  } as Plan
  const html = generateDeliverySlipTemplate(blankPlan)
  return replaceDeliverySlipPlaceholders(html, blankPlan, undefined, { blank: true })
}
  • Step 3: 用 tsx 自检空白 HTML

Run:

cd web-admin && pnpm exec tsx -e "
import { renderBlankInboundSlipHtml } from './src/utils/pl-inbound-slip-template.ts'
import { renderBlankDeliverySlipHtml } from './src/utils/pl-delivery-slip-template.ts'

const inbound = renderBlankInboundSlipHtml()
const delivery = renderBlankDeliverySlipHtml()

function assert(cond: boolean, msg: string) {
  if (!cond) throw new Error(msg)
}

assert(inbound.includes('入'), 'inbound title')
assert(!inbound.includes('暂无产品信息'), 'inbound no empty tip')
assert((inbound.match(/inbound-slip-product-grid-cell/g) || []).length >= 40, 'inbound empty cells')
assert(inbound.includes('alt=\"印章\"'), 'inbound blank keeps seal')

assert(delivery.includes('送') || delivery.includes('提'), 'delivery title')
assert(!delivery.includes('暂无产品信息'), 'delivery no empty tip')
assert(!delivery.includes('alt=\"印章\"'), 'delivery blank must not have seal img')
assert((delivery.match(/delivery-slip-product-grid-cell/g) || []).length >= 40, 'delivery empty cells')

console.log('blank inbound/delivery OK')
"

Expected: 打印 blank inbound/delivery OK,exit 0。

  • Step 4: Commit
git add web-admin/src/utils/pl-inbound-slip-template.ts web-admin/src/utils/pl-delivery-slip-template.ts
git commit -m "$(cat <<'EOF'
feat(pl): add blank HTML renderers for inbound and delivery slips

EOF
)"

Task 2: 分装单空白 HTML 纯函数

Files:

  • Modify: web-admin/src/utils/pl-subpack-slip-template.ts

  • Step 1: 扩展 replaceSubpackSlipPlaceholders 支持 blank

将签名改为:

export function replaceSubpackSlipPlaceholders(
  htmlContent: string,
  plan: Plan,
  ctx?: SubpackSlipRenderContext,
  options?: { blank?: boolean }
): string {
  if (!htmlContent || !plan) return htmlContent

  const blank = Boolean(options?.blank)
  const orderDate = blank ? '' : ctx?.orderDate || formatDateIso()
  const orderUser = blank ? '' : ctx?.orderUser || ''
  const preparerUser = blank ? '' : ctx?.preparerUser || orderUser

  const replacements: Record<string, string> = {
    '{{companyName}}': SUBPACK_SLIP_DEFAULTS.companyName,
    '{{subpackSlipNo}}': blank ? '' : ctx?.subpackSlipNo || '',
    '{{orderDate}}': orderDate,
    '{{deliveryDate}}': '',
    '{{orderUser}}': orderUser,
    '{{preparerUser}}': preparerUser,
    '{{handlerName}}': '',
    '{{receivingUnit}}': blank ? '' : orderDate,
    '{{planNo}}': '',
    '{{receiveContact}}': blank ? '' : orderUser,
    '{{driverInfo}}': '',
    '{{planDateChinese}}': '',
    '{{productTable}}': buildProductRows(blank ? [] : plan.products, ctx),
    '{{remark}}': blank ? '' : formatRemarkForSlipHtml(plan.remark),
    '{{remarkExtra}}': '',
    '{{companyAddress}}': '',
    '{{companyPhone}}': '',
    '{{contactPhone}}': '',
    '{{companySeal}}': '',
    '{{sideLabels}}': ''
  }
  // ... 其余 normalize + for 循环替换保持不变
  // blank 时跳过「用 ctx.subpackSlipNo 强制写回 display」那段 if (ctx?.subpackSlipNo) { ... }
}

注意:现有文件末尾若有 if (ctx?.subpackSlipNo) { result = result.replace(... display ...) },在 blank === true 时不要执行该分支。

  • Step 2: 新增 renderBlankSubpackSlipHtml
export function renderBlankSubpackSlipHtml(): string {
  const blankPlan = {
    id: '',
    planNo: '',
    products: [],
    customerName: '',
    remark: ''
  } as Plan
  const html = generateSubpackSlipTemplate(blankPlan)
  return replaceSubpackSlipPlaceholders(html, blankPlan, undefined, { blank: true })
}
  • Step 3: tsx 自检
cd web-admin && pnpm exec tsx -e "
import { renderBlankSubpackSlipHtml } from './src/utils/pl-subpack-slip-template.ts'

const html = renderBlankSubpackSlipHtml()
function assert(cond: boolean, msg: string) {
  if (!cond) throw new Error(msg)
}
assert(html.includes('分'), 'title')
assert(!/NO:SXFZ\d{8}-\d{3}/.test(html), 'no slip no')
assert(!html.includes('暂无产品信息'), 'no empty tip')
assert((html.match(/subpack-slip-product-grid-cell/g) || []).length >= 20, 'empty product cells')
console.log('blank subpack OK')
"

Expected: blank subpack OK

  • Step 4: Commit
git add web-admin/src/utils/pl-subpack-slip-template.ts
git commit -m "$(cat <<'EOF'
feat(pl): add blank HTML renderer for subpack slips

EOF
)"

Task 3: 三个打印弹窗 openBlank()

Files:

  • Modify: web-admin/src/components/business/pl-inbound-slip-print-dialog/index.vue

  • Modify: web-admin/src/components/business/pl-delivery-slip-print-dialog/index.vue

  • Modify: web-admin/src/components/business/pl-subpack-slip-print-dialog/index.vue

  • Step 1: 入库打印弹窗

在 pl-inbound-slip-print-dialog/index.vue:

  1. import 增加 renderBlankInboundSlipHtml。
  2. 增加 const isBlankMode = ref(false)。
  3. footer「保存模板」按钮加 v-if="!isBlankMode"。
  4. 实现:
function openBlank() {
  currentPlanId.value = ''
  currentPlan.value = null
  isBlankMode.value = true
  isPreviewMode.value = false
  showRedSeal.value = true
  slipContent.value = renderBlankInboundSlipHtml()
  visible.value = true
}
  1. 在现有 open 开头设 isBlankMode.value = false。
  2. handleClosed 中重置 isBlankMode.value = false。
  3. PDF 文件名:若 isBlankMode,用 空白入库单.pdf;否则保持现逻辑。
  4. defineExpose({ open, openBlank })。
  • Step 2: 送(提)货打印弹窗

对称改动,import renderBlankDeliverySlipHtml:

function openBlank() {
  currentPlanId.value = ''
  currentPlan.value = null
  isBlankMode.value = true
  isPreviewMode.value = false
  showRedSeal.value = false
  slipContent.value = renderBlankDeliverySlipHtml()
  visible.value = true
}
  • 「保存模板」v-if="!isBlankMode"。

  • 「显示/隐藏红章」按钮空白模式仍可保留(用户可临时打开章);打开空白时默认无章。

  • PDF:空白送提货单.pdf(文件名去掉非法字符)。

  • defineExpose({ open, openBlank })。

  • Step 3: 分装打印弹窗

import renderBlankSubpackSlipHtml:

function openBlank() {
  currentPlanId.value = ''
  currentPlan.value = null
  currentSubpackSlipNo.value = ''
  isBlankMode.value = true
  isPreviewMode.value = false
  showRedSeal.value = false
  slipContent.value = renderBlankSubpackSlipHtml()
  visible.value = true
}

禁止调用 planApi.ensureSubpackSlipNo / get / getSubpackSlipTemplate。

  • 「保存模板」v-if="!isBlankMode"。

  • PDF:空白分装单.pdf。

  • defineExpose({ open, openBlank })。

  • Step 4: Commit

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(pl): expose openBlank on slip print dialogs

EOF
)"

Task 4: 列表工具栏入口

Files:

  • Modify: web-admin/src/views/pl/plan/index.vue

  • Step 1: 调整工具栏「打印单据」下拉

找到工具栏中 el-dropdown(约 L129–160),改为:

  1. 去掉 dropdown / 按钮上的 :disabled="selectedRows.length < 1"(按钮始终可点)。
  2. 在三个原菜单项之后增加:
<el-dropdown-item divided command="blank-inbound">空白入库单</el-dropdown-item>
<el-dropdown-item command="blank-delivery">空白送(提)货单</el-dropdown-item>
<el-dropdown-item command="blank-subpack">空白分装单</el-dropdown-item>

原三项的 :disabled="!canBatchPrintSlip(...).ok" 保持不变(未勾选时 canBatchPrintSlip 返回 false,自然灰掉)。

  • Step 2: 扩展 handleToolbarPrintSlip
function handleToolbarPrintSlip(command: string) {
  if (command === 'blank-inbound') {
    inboundSlipPrintRef.value?.openBlank()
    return
  }
  if (command === 'blank-delivery') {
    deliverySlipPrintRef.value?.openBlank()
    return
  }
  if (command === 'blank-subpack') {
    subpackSlipPrintRef.value?.openBlank()
    return
  }

  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)
}

查看弹窗内的打印下拉:不要加空白项(spec D2)。

  • Step 3: 手工验收清单(在 pnpm dev 下勾选)
  1. 未勾选:点「打印单据」→ 原三项灰、空白三项可点。
  2. 空白入库单:无客户/日期/编号业务值;无「暂无产品信息」;有空行;有印章(若默认模板含章);保存模板按钮不可见。
  3. 空白送(提)货单:无业务值;无印章;可打印。
  4. 空白分装单:无分装单号;无下单日期/人等;可打印;网络无 subpack-slip-no 请求。
  5. 勾选一条入库计划:原「入库单」可填数打印;空白三项仍出空白且不带计划数据。
  • Step 4: Commit
git add web-admin/src/views/pl/plan/index.vue
git commit -m "$(cat <<'EOF'
feat(pl): add blank slip print commands to plan list toolbar

EOF
)"

Spec 覆盖自检

Spec 项 Task
D1 openBlank Task 3
D2 仅列表工具栏 Task 4(查看弹窗不加)
D3 未勾选可点、原三项灰 Task 4
D4 勾选时空白仍可用 Task 4
D5 空白入库全空 Task 1 + 3
D6 送提货默认无章 Task 1(seal 空)+ Task 3(showRedSeal false)
D7 分装单号空、不占号 Task 2 + 3
D8 空行非「暂无产品信息」 Task 1–2
D9 不读计划模板 Task 3 openBlank
D10 禁用保存模板 Task 3 v-if="!isBlankMode"

无后端任务。无 TBD 占位。