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

9.4 KiB
Raw Permalink Blame History

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。

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],
  })
})

同时创建页面契约测试,锁定自定义导航、最小权限和原型工作流入口:

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: 实现原型状态计算

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,并导入状态模块:

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: 实现登录态和拣货态模板

模板必须包含以下层级和原型文案:

<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: 完整迁移原型样式

样式使用原型色值和密度:

$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: 使用自定义导航并统一页面背景

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