diff --git a/docs/superpowers/plans/2026-09-24-mp-inventory-pound-qrcode-popup.md b/docs/superpowers/plans/2026-09-24-mp-inventory-pound-qrcode-popup.md new file mode 100644 index 000000000..2f4778116 --- /dev/null +++ b/docs/superpowers/plans/2026-09-24-mp-inventory-pound-qrcode-popup.md @@ -0,0 +1,677 @@ +# 小程序出入库列表 · 磅单二维码弹窗 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:** 出入库管理列表去掉「生成地磅单」,改为主色「磅单二维码」按钮,弹出与运输计划一致的居中磅单二维码卡片(本地生成 QR,内容为出入库单 `id`)。 + +**Architecture:** 抽取纯函数工具(标题/物料行/备注归一)便于单测;新建居中弹窗组件 `db-pound-qrcode-popup`(样式对齐 `PoundSlipQrcodeView`);列表页只负责开/关与传 `inventoryId`。二维码用 `uqrcodejs` 画到隐藏 canvas 再导出临时路径展示,不依赖外网 QR 图床。 + +**Tech Stack:** uni-app Vue3 小程序、`uqrcodejs`、现有 `inventoryApi` / `db-btn` + +**Spec:** `docs/superpowers/specs/2026-09-24-mp-inventory-pound-qrcode-popup-design.md` +**视觉参考:** `web-admin/src/components/business/db/PoundSlipQrcodeView.vue`、`web-admin/src/styles/db-module.scss`(`.pound-slip-capture-inline`)、运输计划 webview `DbPoundSlipQrcodeDialog` + +--- + +## 文件结构 + +| 路径 | 职责 | +|------|------| +| `miniprogram/utils/dbPoundQr.js` | 纯函数:弹窗标题、物料行文案、备注字段归一 | +| `miniprogram/scripts/test_db_pound_qr.mjs` | Node 断言跑上述纯函数(小程序无 jest) | +| `miniprogram/utils/makeQrTempFile.js` | 用 `uqrcodejs` + canvas 生成临时图片路径 | +| `miniprogram/components/db-ui/db-pound-qrcode-popup.vue` | 居中弹窗 UI + 拉详情 + 展示 QR | +| `miniprogram/pages/db/inventory/list.vue` | 按钮替换、挂载弹窗、删除 `genWeighing` | +| `miniprogram/package.json` | 增加依赖 `uqrcodejs` | + +--- + +### Task 1: 纯函数工具 + 单测 + +**Files:** +- Create: `miniprogram/utils/dbPoundQr.js` +- Create: `miniprogram/scripts/test_db_pound_qr.mjs` + +- [ ] **Step 1: 写失败用例脚本(先测再实现)** + +`miniprogram/scripts/test_db_pound_qr.mjs`: + +```js +import assert from 'node:assert/strict' +import { + poundQrDialogTitle, + formatPoundQrProductLine, + normalizePoundQrRemarks +} from '../utils/dbPoundQr.js' + +assert.equal(poundQrDialogTitle('出库'), '出库磅单二维码') +assert.equal(poundQrDialogTitle('入库'), '入库磅单二维码') +assert.equal(poundQrDialogTitle(''), '入库磅单二维码') + +assert.equal(formatPoundQrProductLine({ product: '尿素', specification: '', quantity: 32 }, '吨'), '尿素(32 吨)') +assert.equal( + formatPoundQrProductLine({ product: '尿素颗粒', specification: '颗粒', quantity: 10 }, '吨'), + '尿素 颗粒(10 吨)' +) +assert.equal( + formatPoundQrProductLine({ product: '尿素', specification: '46%', quantity: 1 }, '吨'), + '尿素 46%(1 吨)' +) + +assert.equal(normalizePoundQrRemarks({ remarks: '急单' }), '急单') +assert.equal(normalizePoundQrRemarks({ remark: '备货' }), '备货') +assert.equal(normalizePoundQrRemarks({ remarks: ' ', remark: '' }), '') + +console.log('test_db_pound_qr: ok') +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: + +```bash +cd miniprogram && node scripts/test_db_pound_qr.mjs +``` + +Expected: `ERR_MODULE_NOT_FOUND` 或类似(`dbPoundQr.js` 尚不存在)。 + +- [ ] **Step 3: 实现纯函数** + +`miniprogram/utils/dbPoundQr.js`: + +```js +/** + * 磅单二维码弹窗文案工具(与 web-admin PoundSlipQrcodeView / DbPoundSlipQrcodeDialog 对齐) + */ + +export function poundQrDialogTitle(businessType) { + return String(businessType || '').trim() === '出库' ? '出库磅单二维码' : '入库磅单二维码' +} + +/** 与 DbPoundSlipQrcodeDialog.formatQrcodeProductLabel 一致 */ +export function formatQrcodeProductLabel(row) { + const product = String(row?.product || '').trim() + const specification = String(row?.specification || '').trim() + if (!specification) return product + if (product.endsWith(specification)) { + const base = product.slice(0, -specification.length).trimEnd() + return base ? `${base} ${specification}` : specification + } + return `${product} ${specification}`.trim() +} + +export function formatPoundQrProductLine(row, quantityUnit = '吨') { + const unit = String(quantityUnit || '').trim() || '吨' + const label = formatQrcodeProductLabel(row) + const qty = row?.quantity + const qtyText = qty === 0 || qty ? String(qty) : '--' + return `${label}(${qtyText} ${unit})` +} + +export function normalizePoundQrRemarks(row) { + const a = String(row?.remarks ?? '').trim() + if (a) return a + return String(row?.remark ?? '').trim() +} +``` + +- [ ] **Step 4: 再跑测试确认通过** + +```bash +cd miniprogram && node scripts/test_db_pound_qr.mjs +``` + +Expected: `test_db_pound_qr: ok` + +- [ ] **Step 5: Commit** + +```bash +git add miniprogram/utils/dbPoundQr.js miniprogram/scripts/test_db_pound_qr.mjs +git commit -m "$(cat <<'EOF' +feat(miniprogram): add pound QR label helpers and node checks. + +EOF +)" +``` + +--- + +### Task 2: 安装 uqrcodejs + 生成临时二维码图 + +**Files:** +- Modify: `miniprogram/package.json` +- Create: `miniprogram/utils/makeQrTempFile.js` + +- [ ] **Step 1: 安装依赖** + +在 `miniprogram` 目录: + +```bash +cd miniprogram && npm install uqrcodejs --save +``` + +确认 `package.json` 的 `dependencies` 含 `"uqrcodejs"`。 + +- [ ] **Step 2: 实现生成工具** + +`miniprogram/utils/makeQrTempFile.js`: + +```js +import UQRCode from 'uqrcodejs' + +/** + * 在组件内隐藏 canvas 上绘制二维码,导出临时文件路径供 展示。 + * @param {object} opts + * @param {string} opts.text 二维码内容(出入库单 id) + * @param {string} opts.canvasId canvas-id + * @param {object} opts.componentInstance 组件实例(getCurrentInstance().proxy) + * @param {number} [opts.size=200] + * @returns {Promise} tempFilePath + */ +export function makeQrTempFile({ text, canvasId, componentInstance, size = 200 }) { + const content = String(text || '').trim() + if (!content) { + return Promise.reject(new Error('二维码内容为空')) + } + if (!componentInstance) { + return Promise.reject(new Error('缺少组件实例')) + } + + return new Promise((resolve, reject) => { + try { + const qr = new UQRCode() + qr.data = content + qr.size = size + qr.margin = 10 + qr.make() + const ctx = uni.createCanvasContext(canvasId, componentInstance) + qr.canvasContext = ctx + qr.drawCanvas() + // drawCanvas 内部会 ctx.draw;稍延迟再导出 + setTimeout(() => { + uni.canvasToTempFilePath( + { + canvasId, + width: size, + height: size, + destWidth: size, + destHeight: size, + success: (res) => { + if (res.tempFilePath) resolve(res.tempFilePath) + else reject(new Error('二维码导出失败')) + }, + fail: (err) => reject(err || new Error('二维码导出失败')) + }, + componentInstance + ) + }, 120) + } catch (e) { + reject(e) + } + }) +} +``` + +说明:若真机 `drawCanvas` 与导出时序不稳,实现时可改为 `uQRCode` 文档中的 `make({ canvasId, componentInstance, text, size, success })` 成功回调拿 path/base64,但**禁止**改回外网 `api.qrserver.com`。 + +- [ ] **Step 3: Commit** + +```bash +git add miniprogram/package.json miniprogram/package-lock.json miniprogram/utils/makeQrTempFile.js +git commit -m "$(cat <<'EOF' +feat(miniprogram): add local QR temp-file helper via uqrcodejs. + +EOF +)" +``` + +(若无 lock 文件则只 add 实际存在的依赖清单文件。) + +--- + +### Task 3: 新建 `db-pound-qrcode-popup` 组件 + +**Files:** +- Create: `miniprogram/components/db-ui/db-pound-qrcode-popup.vue` +- Test: 手工打开弹窗(Task 5);纯函数已在 Task 1 覆盖 + +- [ ] **Step 1: 创建组件** + +`miniprogram/components/db-ui/db-pound-qrcode-popup.vue`(完整实现): + +```vue + + + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add miniprogram/components/db-ui/db-pound-qrcode-popup.vue +git commit -m "$(cat <<'EOF' +feat(miniprogram): add centered pound QR popup component. + +EOF +)" +``` + +--- + +### Task 4: 改造出入库列表页 + +**Files:** +- Modify: `miniprogram/pages/db/inventory/list.vue` + +- [ ] **Step 1: 替换卡片底部按钮** + +将: + +```vue + + 关联 + 生成地磅单 + +``` + +改为: + +```vue + + 关联 + 磅单二维码 + +``` + +- [ ] **Step 2: 在关联弹窗后增加磅单弹窗** + +在 ``(关联弹窗)之后增加: + +```vue + +``` + +- [ ] **Step 3: script 调整** + +1. 增加 import: + +```js +import DbPoundQrcodePopup from '@/components/db-ui/db-pound-qrcode-popup.vue' +``` + +2. 增加状态: + +```js +const poundQrOpen = ref(false) +const poundQrId = ref('') +const poundQrSnapshot = ref(null) +``` + +3. 增加方法: + +```js +function openPoundQr(item) { + poundQrId.value = item?.id || '' + poundQrSnapshot.value = item || null + poundQrOpen.value = true +} +``` + +4. **删除**整个 `genWeighing` 函数(约 263–277 行)及对 `PAGE_PATHS.dbWeighingDetail` 的仅服务于该函数的用法(若 `PAGE_PATHS` 仍被 `goBack`/`viewItem` 使用则保留 import)。 + +- [ ] **Step 4: Commit** + +```bash +git add miniprogram/pages/db/inventory/list.vue +git commit -m "$(cat <<'EOF' +feat(miniprogram): replace inventory list weigh-slip create with pound QR. + +EOF +)" +``` + +--- + +### Task 5: 验收与图谱更新 + +**Files:** +- 无强制代码变更;若改了文件则跑 graphify + +- [ ] **Step 1: 再跑纯函数测试** + +```bash +cd miniprogram && node scripts/test_db_pound_qr.mjs +``` + +Expected: `test_db_pound_qr: ok` + +- [ ] **Step 2: 微信开发者工具手工验收** + +1. 打开「出入库管理」列表:卡片底部仅「关联」「磅单二维码」。 +2. 点「磅单二维码」:深色遮罩居中弹窗,黄牌车牌、往来单位、物料(含吨)、有备注则显示、日期+单 ID、二维码、「出入库单标识(扫码核验)」;标题为入/出库磅单二维码。 +3. 扫码内容 = 该行出入库单 `id`。 +4. 「关联」、点卡片进详情行为不变。 +5. 断网或屏蔽外网图床时仍能出二维码(本地 canvas)。 + +- [ ] **Step 3: 更新知识图谱(若本会话改了代码)** + +```bash +./scripts/update-graph.sh frontend +``` + +或按仓库惯例对 miniprogram 目录执行对应 graphify 更新。 + +- [ ] **Step 4: 若 Step 3 有产物,单独 commit** + +```bash +git add miniprogram/graphify-out graphify-out +git commit -m "$(cat <<'EOF' +chore: update graphify after inventory pound QR popup. + +EOF +)" +``` + +(仅 add 实际变更的图谱路径。) + +--- + +## Spec 覆盖自检 + +| Spec 要求 | 任务 | +|-----------|------| +| 去掉「生成地磅单」 | Task 4 | +| 「关联」+「磅单二维码」主色 | Task 4 | +| 居中深色弹窗、视觉对齐 PoundSlip | Task 3 | +| QR 内容 = inventory id | Task 2 + 3 | +| 本地生成、禁外网 QR 服务 | Task 2 | +| 列表快照 + get 补全 | Task 3 | +| 失败 toast 关弹窗 | Task 3 | +| 不改后端 | 全计划无 backend 文件 | + +无 TBD / 无「类似 Task N」占位。