# 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 ``` - [ ] **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 与目标设备,必须在交付说明中列为未验证。