|
|
@@ -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 上绘制二维码,导出临时文件路径供 <image> 展示。
|
|
|
+ * @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<string>} 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
|
|
|
+<template>
|
|
|
+ <view v-if="show" class="pound-qr-mask" @click="close">
|
|
|
+ <view class="pound-qr-panel" @click.stop>
|
|
|
+ <view class="pound-qr-panel__title">{{ title }}</view>
|
|
|
+ <view v-if="loading" class="pound-qr-panel__loading">加载中...</view>
|
|
|
+ <view v-else-if="row" class="pound-slip-capture-inline">
|
|
|
+ <view class="capture-card">
|
|
|
+ <view class="capture-row capture-row-plate">
|
|
|
+ <view class="vehicle-plate">
|
|
|
+ <view class="vehicle-plate-dot vehicle-plate-dot-lt" />
|
|
|
+ <view class="vehicle-plate-dot vehicle-plate-dot-lb" />
|
|
|
+ <text class="vehicle-plate-text">{{ row.vehicleNo || '未填车牌' }}</text>
|
|
|
+ <view class="vehicle-plate-dot vehicle-plate-dot-rt" />
|
|
|
+ <view class="vehicle-plate-dot vehicle-plate-dot-rb" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="capture-row capture-row-large">
|
|
|
+ <text class="label">往来单位:</text>
|
|
|
+ <text class="value">{{ row.partnerCompany || '--' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="capture-row capture-row-large">
|
|
|
+ <text class="label">物料名称:</text>
|
|
|
+ <text class="value">{{ productLine }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="remarksText" class="capture-row">
|
|
|
+ <text class="label">备注:</text>
|
|
|
+ <text class="value">{{ remarksText }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="capture-card capture-qrcode">
|
|
|
+ <view class="capture-id">
|
|
|
+ {{ row.date || '' }}
|
|
|
+ {{ row.businessType === '出库' ? '出库单ID:' : '入库单ID:' }}{{ row.id }}
|
|
|
+ </view>
|
|
|
+ <view class="capture-qr-wrap">
|
|
|
+ <image v-if="qrPath" class="capture-qr-img" :src="qrPath" mode="aspectFit" />
|
|
|
+ </view>
|
|
|
+ <view class="capture-desc">出入库单标识(扫码核验)</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- 隐藏 canvas,供 uqrcodejs 绘制 -->
|
|
|
+ <canvas
|
|
|
+ canvas-id="poundQrCanvas"
|
|
|
+ id="poundQrCanvas"
|
|
|
+ class="pound-qr-canvas"
|
|
|
+ :style="{ width: canvasSize + 'px', height: canvasSize + 'px' }"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, getCurrentInstance, ref, watch } from 'vue'
|
|
|
+import { inventoryApi } from '@/pages/db/api/db'
|
|
|
+import {
|
|
|
+ formatPoundQrProductLine,
|
|
|
+ normalizePoundQrRemarks,
|
|
|
+ poundQrDialogTitle
|
|
|
+} from '@/utils/dbPoundQr'
|
|
|
+import { makeQrTempFile } from '@/utils/makeQrTempFile'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ show: Boolean,
|
|
|
+ inventoryId: { type: String, default: '' },
|
|
|
+ /** 列表行快照(可选);仍会按需 get 补全 */
|
|
|
+ snapshot: { type: Object, default: null }
|
|
|
+})
|
|
|
+const emit = defineEmits(['close', 'update:show'])
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const row = ref(null)
|
|
|
+const qrPath = ref('')
|
|
|
+const canvasSize = 200
|
|
|
+const instance = getCurrentInstance()
|
|
|
+
|
|
|
+const title = computed(() => poundQrDialogTitle(row.value?.businessType))
|
|
|
+const productLine = computed(() =>
|
|
|
+ row.value ? formatPoundQrProductLine(row.value, '吨') : ''
|
|
|
+)
|
|
|
+const remarksText = computed(() => (row.value ? normalizePoundQrRemarks(row.value) : ''))
|
|
|
+
|
|
|
+function close() {
|
|
|
+ emit('update:show', false)
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+function reset() {
|
|
|
+ row.value = null
|
|
|
+ qrPath.value = ''
|
|
|
+ loading.value = false
|
|
|
+}
|
|
|
+
|
|
|
+async function load() {
|
|
|
+ const id = String(props.inventoryId || '').trim()
|
|
|
+ if (!id) {
|
|
|
+ uni.showToast({ title: '缺少单据ID', icon: 'none' })
|
|
|
+ close()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loading.value = true
|
|
|
+ row.value = null
|
|
|
+ qrPath.value = ''
|
|
|
+ try {
|
|
|
+ let data = props.snapshot && props.snapshot.id === id ? { ...props.snapshot } : null
|
|
|
+ if (!data || data.quantity == null || !data.partnerCompany) {
|
|
|
+ const res = await inventoryApi.get(id)
|
|
|
+ data = res?.data ?? res
|
|
|
+ }
|
|
|
+ if (!data || !data.id) throw new Error('单据不存在')
|
|
|
+ row.value = data
|
|
|
+ await new Promise((r) => setTimeout(r, 50))
|
|
|
+ const proxy = instance?.proxy
|
|
|
+ qrPath.value = await makeQrTempFile({
|
|
|
+ text: data.id,
|
|
|
+ canvasId: 'poundQrCanvas',
|
|
|
+ componentInstance: proxy,
|
|
|
+ size: canvasSize
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e?.message || '加载二维码失败', icon: 'none' })
|
|
|
+ close()
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [props.show, props.inventoryId],
|
|
|
+ ([show]) => {
|
|
|
+ if (show) load()
|
|
|
+ else reset()
|
|
|
+ }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.pound-qr-mask {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 1100;
|
|
|
+ background: rgba(0, 0, 0, 0.92);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 24px 16px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.pound-qr-panel {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 360px;
|
|
|
+ max-height: 90vh;
|
|
|
+ overflow: auto;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 12px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.pound-qr-panel__title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1e293b;
|
|
|
+ padding: 14px 12px 6px;
|
|
|
+}
|
|
|
+.pound-qr-panel__loading {
|
|
|
+ text-align: center;
|
|
|
+ color: #9ca3af;
|
|
|
+ padding: 40px 0;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.pound-qr-canvas {
|
|
|
+ position: fixed;
|
|
|
+ left: -9999px;
|
|
|
+ top: 0;
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.pound-slip-capture-inline {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 4px 8px 12px;
|
|
|
+ margin: 0 6px 10px;
|
|
|
+}
|
|
|
+.capture-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 10px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.capture-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.capture-row-large {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.capture-row .label {
|
|
|
+ color: #1e293b;
|
|
|
+ min-width: 82px;
|
|
|
+}
|
|
|
+.capture-row .value {
|
|
|
+ color: #1e293b;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.capture-row-plate {
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.vehicle-plate {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px 32px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background: #f5d000;
|
|
|
+ border: 3px solid #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 0 0 2px #fff;
|
|
|
+}
|
|
|
+.vehicle-plate-text {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #000;
|
|
|
+ letter-spacing: 1px;
|
|
|
+}
|
|
|
+.vehicle-plate-dot {
|
|
|
+ position: absolute;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.vehicle-plate-dot-lt {
|
|
|
+ left: 6px;
|
|
|
+ top: 8px;
|
|
|
+}
|
|
|
+.vehicle-plate-dot-lb {
|
|
|
+ left: 6px;
|
|
|
+ bottom: 8px;
|
|
|
+}
|
|
|
+.vehicle-plate-dot-rt {
|
|
|
+ right: 6px;
|
|
|
+ top: 8px;
|
|
|
+}
|
|
|
+.vehicle-plate-dot-rb {
|
|
|
+ right: 6px;
|
|
|
+ bottom: 8px;
|
|
|
+}
|
|
|
+.capture-qrcode {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.capture-id {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1e293b;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ word-break: break-all;
|
|
|
+ padding: 0 4px;
|
|
|
+}
|
|
|
+.capture-qr-wrap {
|
|
|
+ padding: 8px 0;
|
|
|
+ min-height: 200px;
|
|
|
+}
|
|
|
+.capture-qr-img {
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+.capture-desc {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #64748b;
|
|
|
+ margin-top: 4px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **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
|
|
|
+ <view class="db-card__foot" @click.stop>
|
|
|
+ <db-btn size="small" type="primary" plain @click="viewRelated(item)">关联</db-btn>
|
|
|
+ <db-btn size="small" type="primary" @click="genWeighing(item)">生成地磅单</db-btn>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <view class="db-card__foot" @click.stop>
|
|
|
+ <db-btn size="small" type="primary" plain @click="viewRelated(item)">关联</db-btn>
|
|
|
+ <db-btn size="small" type="primary" @click="openPoundQr(item)">磅单二维码</db-btn>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 在关联弹窗后增加磅单弹窗**
|
|
|
+
|
|
|
+在 `</db-popup>`(关联弹窗)之后增加:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <db-pound-qrcode-popup
|
|
|
+ :show="poundQrOpen"
|
|
|
+ :inventory-id="poundQrId"
|
|
|
+ :snapshot="poundQrSnapshot"
|
|
|
+ @close="poundQrOpen = false"
|
|
|
+ @update:show="(v) => (poundQrOpen = v)"
|
|
|
+ />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **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」占位。
|