Files
docs/superpowers/plans/2026-07-27-pda-picking-static-pages.md
T

260 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PDA 拣货静态页面 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:** 将 PDA 拣货 HTML 原型完整迁移为 Android PDA 可用的 uni-app 本地交互页面。
**Architecture:** 首页承载登录态和拣货态,业务数据与 UI 状态保留在页面中;可独立验证的状态计算放在 `utils/picking-state.js`。不引入后端、状态管理库、厂商 SDK 或新 npm 依赖。
**Tech Stack:** uni-app、Vue Options API、SCSS、Node.js 内置测试运行器
---
当前目录没有 Git 元数据,因此计划不包含提交步骤;本机也没有 HBuilderX 且项目没有 `package.json`,不得声称 App 构建已通过。
### Task 1: 用失败测试锁定拣货状态规则
**Files:**
- Create: `apps/pda-uniapp/tests/picking-state.test.mjs`
- Create: `apps/pda-uniapp/tests/page-contract.test.mjs`
- Create later: `apps/pda-uniapp/utils/picking-state.js`
- [ ] **Step 1: 编写状态测试**
测试通过 `data:` URL 加载 ES module,使源文件继续使用 uni-app 兼容的 `export` 语法且无需新增 `package.json`。
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../utils/picking-state.js', import.meta.url), 'utf8')
const state = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`)
test('createItemStates initializes every item as pending', () => {
assert.deepEqual(state.createItemStates([{ id: 1 }, { id: 2 }]), { 1: 'pending', 2: 'pending' })
})
test('getPickSummary reproduces prototype submit rules', () => {
assert.equal(state.getPickSummary([{ id: 1 }, { id: 2 }], { 1: 'pending', 2: 'pending' }).canSubmit, false)
assert.equal(state.getPickSummary([{ id: 1 }, { id: 2 }], { 1: 'picked', 2: 'pending' }).buttonText,
'提交拣货完成(1件待拣将视为未拣)')
})
test('submitStates turns pending items into skipped', () => {
assert.deepEqual(state.submitStates([{ id: 1 }, { id: 2 }], { 1: 'picked', 2: 'pending' }), {
states: { 1: 'picked', 2: 'skipped' },
autoSkippedIds: [2],
})
})
```
同时创建页面契约测试,锁定自定义导航、最小权限和原型工作流入口:
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
test('page configuration uses the custom prototype header', async () => {
const pages = JSON.parse(await readFile(new URL('../pages.json', import.meta.url), 'utf8'))
assert.equal(pages.pages[0].style.navigationStyle, 'custom')
})
test('static prototype does not request unused Android permissions', async () => {
const manifest = await readFile(new URL('../manifest.json', import.meta.url), 'utf8')
assert.doesNotMatch(manifest, /android\.permission\.(VIBRATE|ACCESS_NETWORK_STATE)/)
})
test('page contains every required workflow entry', async () => {
const page = await readFile(new URL('../pages/index/index.vue', import.meta.url), 'utf8')
for (const text of ['登录', '确认拣出', '部分拣货', '跳拣', '提交拣货完成', '下一箱']) {
assert.match(page, new RegExp(text))
}
})
```
- [ ] **Step 2: 运行测试并确认正确失败**
Run: `node --test tests/*.test.mjs`(工作目录 `apps/pda-uniapp`)
Expected: FAIL;状态测试因 `utils/picking-state.js` 尚不存在,页面契约测试因当前仍是官方示例页、原生导航和未使用权限而失败。
### Task 2: 实现纯状态模块
**Files:**
- Create: `apps/pda-uniapp/utils/picking-state.js`
- Test: `apps/pda-uniapp/tests/picking-state.test.mjs`
- [ ] **Step 1: 实现原型状态计算**
```js
export const ITEM_STATUS = Object.freeze({
PENDING: 'pending',
PICKED: 'picked',
PARTIAL: 'partial',
SKIPPED: 'skipped',
})
export function createItemStates(items) {
return items.reduce((states, item) => {
states[item.id] = ITEM_STATUS.PENDING
return states
}, {})
}
export function getPickSummary(items, states) {
const summary = { total: items.length, picked: 0, partial: 0, skipped: 0, pending: 0 }
items.forEach((item) => { summary[states[item.id] || ITEM_STATUS.PENDING] += 1 })
summary.processed = summary.picked + summary.partial + summary.skipped
summary.progress = summary.total ? Math.round(summary.processed / summary.total * 100) : 0
summary.canSubmit = summary.total > 0 && (summary.pending === 0 || summary.picked > 0 || summary.partial > 0)
summary.buttonText = summary.pending > 0 && (summary.picked > 0 || summary.partial > 0)
? `提交拣货完成(${summary.pending}件待拣将视为未拣)`
: '提交拣货完成'
return summary
}
export function submitStates(items, states) {
const nextStates = { ...states }
const autoSkippedIds = []
items.forEach((item) => {
if ((nextStates[item.id] || ITEM_STATUS.PENDING) === ITEM_STATUS.PENDING) {
nextStates[item.id] = ITEM_STATUS.SKIPPED
autoSkippedIds.push(item.id)
}
})
return { states: nextStates, autoSkippedIds }
}
```
- [ ] **Step 2: 运行状态测试**
Run: `node --test tests/picking-state.test.mjs`
Expected: 3 tests passed, 0 failed。
### Task 3: 迁移原型页面与本地交互
**Files:**
- Modify: `apps/pda-uniapp/pages/index/index.vue`
- Import: `apps/pda-uniapp/utils/picking-state.js`
- [ ] **Step 1: 建立页面状态和演示数据**
页面使用 Options API,并导入状态模块:
```js
import { ITEM_STATUS, createItemStates, getPickSummary, submitStates } from '@/utils/picking-state.js'
export default {
data() {
return {
currentUser: null,
currentBoxIndex: 0,
itemStates: {},
partialQuantities: {},
completedBoxes: {},
scanValue: '',
activeOverlay: '',
boxDropdownOpen: false,
toast: { visible: false, message: '', type: '' },
}
},
}
```
演示账号、三组箱号、货品和五个跳拣原因逐项复制自 `docs/悦/PDA拣货/pda-picking.html`。
- [ ] **Step 2: 实现登录态和拣货态模板**
模板必须包含以下层级和原型文案:
```vue
<template>
<view class="page-shell">
<view v-if="!currentUser" class="login-page"><!-- 登录表单与快捷账号 --></view>
<view v-else class="work-page"><!-- 自定义头部、箱号选择、扫码、任务列表、统计与提交栏 --></view>
<view v-if="activeOverlay" class="modal-overlay"><!-- 当前业务弹层 --></view>
<view v-if="toast.visible" class="app-toast">{{ toast.message }}</view>
</view>
</template>
```
- [ ] **Step 3: 实现原型交互方法**
实现并绑定 `login`、`quickLogin`、`logout`、`selectBox`、`handleScan`、`pickItem`、`undoItem`、`openPartial`、`confirmPartial`、`openSkip`、`confirmSkip`、`openBoxList`、`submitPicking`、`nextBox`、`closeOverlay`、`restoreScanFocus` 和 `onBackPress`。
扫码输入使用 `:focus="scanFocused"` 配合短暂置 false/true 恢复焦点;部分数量输入打开时接管焦点。系统返回依次关闭弹层、下拉并执行统一退出逻辑。
- [ ] **Step 4: 完整迁移原型样式**
样式使用原型色值和密度:
```scss
$primary: #1a73e8;
$success: #0d9488;
$danger: #dc2626;
$warning: #f59e0b;
$page-bg: #f5f7fa;
$text: #1f2937;
$text-secondary: #6b7280;
$border: #e5e7eb;
```
覆盖 `320–480px` 常见 PDA 宽度、状态栏安全区、底部导航栏安全区、固定提交栏和软键盘场景。不得引入新视觉方向。
### Task 4: 修正 Android 页面配置与权限
**Files:**
- Modify: `apps/pda-uniapp/pages.json`
- Modify: `apps/pda-uniapp/manifest.json`
- Modify: `apps/pda-uniapp/App.vue`
- Modify: `apps/pda-uniapp/README.md`
- [ ] **Step 1: 使用自定义导航并统一页面背景**
```json
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "集装箱拣货"
}
}
```
`App.vue` 移除官方示例日志与 `.example-info`,全局页面背景改为 `#f5f7fa`。
- [ ] **Step 2: 移除当前未使用的手工权限**
`manifest.json` 的 Android `permissions` 改为空数组;本阶段不使用网络、震动、相机或存储。
- [ ] **Step 3: 记录可执行验证命令和剩余风险**
README 记录 `node --test tests/picking-state.test.mjs`,并说明本机无 HBuilderX,Android App 构建与真机扫描头尚未验证。
### Task 5: 自动检查与原型验收
**Files:**
- Verify: `apps/pda-uniapp/tests/page-contract.test.mjs`
- Verify: `apps/pda-uniapp/pages/index/index.vue`
- Verify: `apps/pda-uniapp/pages.json`
- Verify: `apps/pda-uniapp/manifest.json`
- [ ] **Step 1: 运行全部自动检查**
Run: `node --test tests/*.test.mjs`
Expected: 所有测试通过,0 failed。
- [ ] **Step 2: 对照原型逐项验收**
检查登录、快捷账号、仓库选择、三个箱号、有效/无效/重复扫码、确认拣出、部分数量校验、跳拣原因、撤销、提交启用规则、完成统计、返回查看、下一箱、弹层关闭和系统返回。
- [ ] **Step 3: 运行机械 UI 检测并记录限制**
Run: `node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json apps/pda-uniapp/pages/index/index.vue apps/pda-uniapp/App.vue`
Expected: 不存在阻塞级设计规则问题。Android 构建、真机截图、扫描头、弱网和前后台切换因当前环境缺少 HBuilderX 与目标设备,必须在交付说明中列为未验证。