260 lines
9.4 KiB
Markdown
260 lines
9.4 KiB
Markdown
# 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 与目标设备,必须在交付说明中列为未验证。
|