9.5 KiB
司机运输计划单「复制发群」文案 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签名与行为保持不变,避免列表/详情改动