Files
shanxia2.0/docs/superpowers/plans/2026-10-12-mini-pl-driver-plan-copy-text-enrich.md
T
2026-10-15 08:50:25 +08:00

9.5 KiB
Raw Blame History

司机运输计划单「复制发群」文案 enrichment 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: 仅重写 miniprogram/utils/plDriverPlanCopy.js 中的 buildDriverPlanCopyText。列表与详情入口、copyDriverPlanToClipboard、ensureCopySeq 流程保持不变。不改后端、不加永久测试文件(规格 D1);用一次性 Node 断言脚本验证后删除。

Tech Stack: uni-app 小程序工具 JS(ES module)、Node 临时校验。

Spec: docs/superpowers/specs/2026-10-12-mini-pl-driver-plan-copy-text-enrich-design.md


File map

文件 动作 职责
miniprogram/utils/plDriverPlanCopy.js Modify 新文案拼接;导出签名不变
miniprogram/pages/pl/driver-plan/list.vue 不改 已调用 copyDriverPlanToClipboard
miniprogram/pages/pl/driver-plan/detail.vue 不改 同上
/tmp 临时校验脚本 Create→Delete Node 断言定稿示例

Task 1: 重写 buildDriverPlanCopyText

Files:

  • Modify: miniprogram/utils/plDriverPlanCopy.js

  • Step 1: 用下列完整内容替换 miniprogram/utils/plDriverPlanCopy.js

/**
 * 司机运输计划单「复制发群」纯文本(不含附件)
 * 规格:docs/superpowers/specs/2026-10-12-mini-pl-driver-plan-copy-text-enrich-design.md
 */

const DASH = '—'
const FIELD_GAP = '  '

function stopTypeOf(stop) {
  return String(stop?.stopType || stop?.stop_type || '').trim()
}

function customerNameOf(stop) {
  const name = String(stop?.customerName || stop?.customer_name || '').trim()
  return name || DASH
}

function textOrDash(value) {
  const s = String(value ?? '').trim()
  return s || DASH
}

function formatQtyWithUnit(raw, unit) {
  if (raw == null || raw === '') return DASH
  const qty = String(raw).trim()
  if (!qty) return DASH
  const u = String(unit || '').trim()
  return u ? `${qty} ${u}` : qty
}

function planQtyOf(product) {
  if (product?.planQty != null && product.planQty !== '') return product.planQty
  if (product?.plan_qty != null && product.plan_qty !== '') return product.plan_qty
  if (product?.quantity != null && product.quantity !== '') return product.quantity
  return null
}

function actualQtyOf(product) {
  if (product?.actualQty != null && product.actualQty !== '') return product.actualQty
  if (product?.actual_qty != null && product.actual_qty !== '') return product.actual_qty
  return null
}

/** 产品紧凑行:- 名称  型号  包装  计划数量:…  实际数量:… */
function formatProductLine(product) {
  const name = textOrDash(product?.productName ?? product?.product_name)
  const model = textOrDash(product?.model)
  const pack = textOrDash(product?.packageSpec ?? product?.package_spec)
  const unit = product?.unit
  const planPart = formatQtyWithUnit(planQtyOf(product), unit)
  const actualPart = formatQtyWithUnit(actualQtyOf(product), unit)
  return (
    `- ${name}${FIELD_GAP}${model}${FIELD_GAP}${pack}` +
    `${FIELD_GAP}计划数量:${planPart}${FIELD_GAP}实际数量:${actualPart}`
  )
}

/**
 * 追加「送货信息:」或「装货信息:」区块。
 * 无站点 → 标题下单独一行 —
 * 有站点无产品 → 客户名:下一行 - —
 */
function appendInfoSection(lines, title, stops) {
  lines.push(title)
  if (!stops.length) {
    lines.push(DASH)
    return
  }
  stops.forEach((stop) => {
    lines.push(`${customerNameOf(stop)}:`)
    const products = Array.isArray(stop?.products) ? stop.products : []
    if (!products.length) {
      lines.push(`- ${DASH}`)
      return
    }
    products.forEach((product) => {
      lines.push(formatProductLine(product))
    })
  })
}

/**
 * @param {object} page driverPlanApi.get 详情(可含 copySeqNo)
 * @returns {string}
 */
export function buildDriverPlanCopyText(page = {}) {
  const stops = Array.isArray(page?.stops) ? page.stops : []
  const loadStops = stops.filter((s) => stopTypeOf(s) === 'load')
  const deliveryStops = stops.filter((s) => stopTypeOf(s) === 'delivery')

  const rawSeq = page?.copySeqNo ?? page?.copy_seq_no
  const seqText = rawSeq != null && rawSeq !== '' ? String(rawSeq) : DASH

  const lines = [
    `序号:${seqText}`,
    `计划编号:${textOrDash(page?.planNo ?? page?.plan_no)}`,
    `车辆:${textOrDash(page?.licensePlate ?? page?.license_plate)}`
  ]

  appendInfoSection(lines, '送货信息:', deliveryStops)
  appendInfoSection(lines, '装货信息:', loadStops)

  return lines.join('\n')
}

/** 复制到剪贴板;成功返回 true */
export function copyDriverPlanToClipboard(page) {
  const text = buildDriverPlanCopyText(page)
  if (!text) {
    uni.showToast({ title: '暂无可复制内容', icon: 'none' })
    return Promise.resolve(false)
  }
  return new Promise((resolve) => {
    uni.setClipboardData({
      data: text,
      success: () => {
        uni.showToast({ title: '已复制', icon: 'success' })
        resolve(true)
      },
      fail: () => {
        uni.showToast({ title: '复制失败', icon: 'none' })
        resolve(false)
      }
    })
  })
}
  • Step 2: 写临时校验脚本并运行(不入库)

在仓库根目录执行:

cat > /tmp/check-driver-plan-copy.mjs << 'EOF'
import { buildDriverPlanCopyText } from './miniprogram/utils/plDriverPlanCopy.js'

const page = {
  copySeqNo: 128,
  planNo: 'YSJH20260409001',
  licensePlate: '沪A12345',
  stops: [
    {
      stopType: 'load',
      customerName: '某某化工厂',
      products: [
        { productName: '工业酒精', model: '95%', packageSpec: '吨桶', planQty: 20, actualQty: 19.8, unit: '吨' },
        { productName: '甲醇', model: '99.9%', packageSpec: '吨桶', planQty: 2, actualQty: 2, unit: '吨' }
      ]
    },
    {
      stopType: 'delivery',
      customerName: '甲客户',
      products: [
        { productName: '工业酒精', model: '95%', packageSpec: '吨桶', planQty: 12, actualQty: 11.9, unit: '吨' }
      ]
    },
    {
      stopType: 'delivery',
      customerName: '乙客户',
      products: [
        { productName: '工业酒精', model: '95%', packageSpec: '吨桶', planQty: 8, actualQty: 7.9, unit: '吨' },
        { productName: '甲醇', model: '99.9%', packageSpec: '吨桶', planQty: 2, actualQty: 2, unit: '吨' }
      ]
    }
  ]
}

const expected = [
  '序号:128',
  '计划编号:YSJH20260409001',
  '车辆:沪A12345',
  '送货信息:',
  '甲客户:',
  '- 工业酒精  95%  吨桶  计划数量:12 吨  实际数量:11.9 吨',
  '乙客户:',
  '- 工业酒精  95%  吨桶  计划数量:8 吨  实际数量:7.9 吨',
  '- 甲醇  99.9%  吨桶  计划数量:2 吨  实际数量:2 吨',
  '装货信息:',
  '某某化工厂:',
  '- 工业酒精  95%  吨桶  计划数量:20 吨  实际数量:19.8 吨',
  '- 甲醇  99.9%  吨桶  计划数量:2 吨  实际数量:2 吨'
].join('\n')

const actual = buildDriverPlanCopyText(page)
if (actual !== expected) {
  console.error('MISMATCH\n--- actual ---\n' + actual + '\n--- expected ---\n' + expected)
  process.exit(1)
}

const emptyish = buildDriverPlanCopyText({ planNo: 'X', stops: [] })
const emptyExpected = [
  '序号:—',
  '计划编号:X',
  '车辆:—',
  '送货信息:',
  '—',
  '装货信息:',
  '—'
].join('\n')
if (emptyish !== emptyExpected) {
  console.error('EMPTY MISMATCH\n' + emptyish)
  process.exit(1)
}

console.log('OK')
EOF
node /tmp/check-driver-plan-copy.mjs
rm /tmp/check-driver-plan-copy.mjs

Expected stdout: OK
若路径报错,改为从仓库根 node --input-type=module 且 import 使用绝对路径:
import { buildDriverPlanCopyText } from 'file:///Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plDriverPlanCopy.js'

  • Step 3: Commit
git add miniprogram/utils/plDriverPlanCopy.js
git commit -m "$(cat <<'EOF'
feat(miniprogram): enrich driver plan copy-to-group text

Show vehicle plate and compact product lines under delivery/load
sections segmented by customer, per the 2026-10-12 design.
EOF
)"

Task 2: 手动冒烟(可选但推荐)

Files: 无代码改动

  • Step 1: 小程序打开「司机运输计划单」,对一装两送计划点「复制发群」,粘贴到备忘录,对照 spec §4 定稿。(需真机/开发者工具手动验证)
  • Step 2: 打开回执保存后弹窗「复制发群」,确认含刚填的实际数量,且与列表复制结构一致。
  • Step 3: 确认剪贴板无附件 URL。

Spec coverage checklist

Spec 项 Task
D1 只改工具函数 Task 1
D2 头信息序号/计划编号/车辆 Task 1 Step 1
D3 送货信息在前 Task 1 appendInfoSection 顺序
D4 按客户分段 Task 1
D5 产品紧凑行 formatProductLine
D6 空值 — textOrDash / formatQtyWithUnit
D7 不含附件 未引用 attachment
D8 详情字段 映射见 helpers
边界无站/无产品 appendInfoSection + Task 1 Step 2 empty case
列表/详情入口不变 文件 map「不改」

Self-review notes

  • 无 TBD / placeholder 步骤
  • 规格明确不加永久测试文件 → 用 /tmp 脚本验证后删除
  • copyDriverPlanToClipboard 签名与行为保持不变,避免列表/详情改动