docs: initialize documentation repository

This commit is contained in:
2026-07-31 10:43:58 +08:00
commit 1ec28f7cfc
72 changed files with 48272 additions and 0 deletions
@@ -0,0 +1,552 @@
# Admin Grouped Navigation 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:** Replace the flat admin sidebar with maintainable two-level navigation while preserving every existing static page and interaction.
**Architecture:** Keep Vue Router responsible only for routes and move admin menu metadata into a local navigation module. Render the configuration through a focused Element Plus sidebar component, while all business pages continue to use the current Pinia mock store and make no Java API calls.
**Tech Stack:** Vue 3, TypeScript, Vue Router, Pinia, Element Plus, Lucide Vue, Vitest, Vue Test Utils, jsdom, pnpm
---
The repository is not a Git worktree. Commit steps are intentionally omitted; each green test/build checkpoint is the recovery boundary.
## File map
- Create `apps/admin-web/vitest.config.ts`: admin-only unit-test configuration.
- Create `apps/admin-web/src/navigation/adminNavigation.ts`: admin-local two-level menu types and data.
- Create `apps/admin-web/src/navigation/adminNavigation.test.ts`: navigation structure and uniqueness contract.
- Create `apps/admin-web/src/components/AdminSidebarMenu.vue`: Element Plus menu renderer.
- Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`: grouped labels, paths and collapsed-state behavior.
- Modify `apps/admin-web/src/layouts/AppLayout.vue`: delegate menu rendering to `AdminSidebarMenu`.
- Modify `apps/admin-web/src/router/index.ts`: stop exporting flat menu metadata.
- Modify `apps/admin-web/src/styles/index.css`: style Element Plus menu within the existing navy sidebar.
- Modify `apps/admin-web/package.json` and `apps/admin-web/pnpm-lock.yaml`: add admin-only test dependencies and script.
- Modify `apps/admin-web/README.md`: document grouped menu and test command.
- Modify `apps/admin-web/AGENTS.md`: add the new admin-local test quality gate.
### Task 1: Install the admin-only test harness
**Files:**
- Modify: `apps/admin-web/package.json`
- Modify: `apps/admin-web/pnpm-lock.yaml`
- Create: `apps/admin-web/vitest.config.ts`
- [ ] **Step 1: Install test dependencies inside admin-web**
Run:
```powershell
cd C:\Users\BY\Desktop\code\chongchong-global\apps\admin-web
pnpm add -D vitest @vue/test-utils jsdom
```
Expected: only `apps/admin-web/package.json`, `apps/admin-web/pnpm-lock.yaml` and `apps/admin-web/node_modules` change.
- [ ] **Step 2: Add the test script**
Add to `scripts` in `apps/admin-web/package.json`:
```json
"test": "vitest run"
```
- [ ] **Step 3: Add the Vitest configuration**
Create `apps/admin-web/vitest.config.ts`:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.ts'],
clearMocks: true,
},
})
```
- [ ] **Step 4: Verify the empty harness**
Run:
```powershell
pnpm test -- --passWithNoTests
```
Expected: PASS with no test files and no dependency-resolution errors.
### Task 2: Define the grouped navigation with TDD
**Files:**
- Create: `apps/admin-web/src/navigation/adminNavigation.test.ts`
- Create: `apps/admin-web/src/navigation/adminNavigation.ts`
- [ ] **Step 1: Write the failing navigation contract**
Create `apps/admin-web/src/navigation/adminNavigation.test.ts`:
```ts
import { describe, expect, it } from 'vitest'
import { adminNavigation, getNavigationLeaves } from './adminNavigation'
describe('adminNavigation', () => {
it('keeps only the dashboard as a direct first-level page', () => {
expect(adminNavigation.map((item) => item.title)).toEqual([
'工作台',
'业务管理',
'系统管理',
])
expect(adminNavigation[0].path).toBe('/')
expect(adminNavigation[1].children?.map((item) => item.title)).toEqual([
'客户管理',
'头程订单',
])
expect(adminNavigation[2].children?.map((item) => item.title)).toEqual([
'内部用户',
'角色管理',
'菜单权限',
])
})
it('assigns every page a unique path and permission code', () => {
const leaves = getNavigationLeaves(adminNavigation)
const paths = leaves.map((item) => item.path)
const permissions = leaves.map((item) => item.permission)
expect(paths).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
expect(new Set(paths).size).toBe(paths.length)
expect(new Set(permissions).size).toBe(permissions.length)
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
})
})
```
- [ ] **Step 2: Run the test and verify RED**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: FAIL because `src/navigation/adminNavigation.ts` does not exist.
- [ ] **Step 3: Implement the minimal local navigation model**
Create `apps/admin-web/src/navigation/adminNavigation.ts`:
```ts
import type { Component } from 'vue'
import {
BriefcaseBusiness,
Building2,
ClipboardList,
KeyRound,
LayoutDashboard,
Settings,
ShieldCheck,
Users,
} from '@lucide/vue'
export interface AdminNavigationNode {
key: string
title: string
icon: Component
path?: string
permission?: string
children?: AdminNavigationNode[]
}
export interface AdminNavigationLeaf extends AdminNavigationNode {
path: string
permission: string
children?: never
}
export const adminNavigation: AdminNavigationNode[] = [
{
key: 'dashboard',
title: '工作台',
icon: LayoutDashboard,
path: '/',
permission: 'dashboard:view',
},
{
key: 'business',
title: '业务管理',
icon: BriefcaseBusiness,
children: [
{ key: 'customers', title: '客户管理', icon: Building2, path: '/customers', permission: 'customer:list' },
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'first-leg:list' },
],
},
{
key: 'system',
title: '系统管理',
icon: Settings,
children: [
{ key: 'internal-users', title: '内部用户', icon: Users, path: '/system/users', permission: 'system:user:list' },
{ key: 'roles', title: '角色管理', icon: ShieldCheck, path: '/system/roles', permission: 'system:role:list' },
{ key: 'permissions', title: '菜单权限', icon: KeyRound, path: '/system/permissions', permission: 'system:permission:list' },
],
},
]
export function getNavigationLeaves(nodes: AdminNavigationNode[]): AdminNavigationLeaf[] {
return nodes.flatMap((node) => {
if (node.children?.length) return getNavigationLeaves(node.children)
if (!node.path || !node.permission) return []
return [node as AdminNavigationLeaf]
})
}
```
- [ ] **Step 4: Run the test and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: 2 tests PASS.
### Task 3: Render the two-level menu with TDD
**Files:**
- Create: `apps/admin-web/src/components/AdminSidebarMenu.test.ts`
- Create: `apps/admin-web/src/components/AdminSidebarMenu.vue`
- [ ] **Step 1: Write the failing sidebar component test**
Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`:
```ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import AdminSidebarMenu from './AdminSidebarMenu.vue'
const stubs = {
'el-menu': {
props: ['defaultActive', 'collapse'],
template: '<nav :data-active="defaultActive" :data-collapsed="String(collapse)"><slot /></nav>',
},
'el-sub-menu': {
props: ['index'],
template: '<section data-kind="group" :data-index="index"><header><slot name="title" /></header><slot /></section>',
},
'el-menu-item': {
props: ['index'],
template: '<a data-kind="item" :data-index="index"><slot /></a>',
},
}
describe('AdminSidebarMenu', () => {
it('renders grouped page entries and forwards the active route', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/system/users', collapsed: false },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users')
expect(wrapper.findAll('[data-kind="group"] header').map((node) => node.text())).toEqual([
'业务管理',
'系统管理',
])
expect(wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index'))).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
})
it('forwards desktop collapse state to Element Plus', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/', collapsed: true },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true')
})
})
```
- [ ] **Step 2: Run the component test and verify RED**
Run:
```powershell
pnpm test -- src/components/AdminSidebarMenu.test.ts
```
Expected: FAIL because `AdminSidebarMenu.vue` does not exist.
- [ ] **Step 3: Implement the minimal menu component**
Create `apps/admin-web/src/components/AdminSidebarMenu.vue`:
```vue
<script setup lang="ts">
import { adminNavigation } from '@/navigation/adminNavigation'
defineProps<{
activePath: string
collapsed: boolean
}>()
</script>
<template>
<el-menu
class="side-nav-menu"
:default-active="activePath"
:collapse="collapsed"
:default-openeds="['business', 'system']"
router
>
<template v-for="item in adminNavigation" :key="item.key">
<el-sub-menu v-if="item.children?.length" :index="item.key">
<template #title>
<component :is="item.icon" :size="19" :stroke-width="1.8" />
<span>{{ item.title }}</span>
</template>
<el-menu-item v-for="child in item.children" :key="child.key" :index="child.path || child.key">
<component :is="child.icon" :size="17" :stroke-width="1.8" />
<template #title>{{ child.title }}</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path || item.key">
<component :is="item.icon" :size="19" :stroke-width="1.8" />
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>
```
- [ ] **Step 4: Run both focused tests and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts src/components/AdminSidebarMenu.test.ts
```
Expected: 4 tests PASS.
### Task 4: Integrate the component into AppLayout
**Files:**
- Modify: `apps/admin-web/src/layouts/AppLayout.vue`
- Modify: `apps/admin-web/src/router/index.ts`
- [ ] **Step 1: Simplify AppLayout imports and state**
Remove the six page icon imports, `adminNavigation`, and `iconMap`. Import the focused component:
```ts
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
```
Add the computed collapse state so the mobile drawer never renders the desktop collapsed menu:
```ts
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
```
- [ ] **Step 2: Replace the flat RouterLink loop**
Replace the contents of `<nav class="side-nav" aria-label="主导航">` with:
```vue
<AdminSidebarMenu :active-path="route.path" :collapsed="menuCollapsed" />
```
Keep the existing route watcher so selecting a menu item closes the mobile drawer.
- [ ] **Step 3: Remove flat navigation metadata from the router**
Delete the `NavigationItem` import and `adminNavigation` array from `apps/admin-web/src/router/index.ts`. Do not change any route path, name, component or permission metadata.
- [ ] **Step 4: Verify all unit tests remain green**
Run:
```powershell
pnpm test
```
Expected: 4 tests PASS with no warnings.
### Task 5: Adapt the existing sidebar styling
**Files:**
- Modify: `apps/admin-web/src/styles/index.css`
- [ ] **Step 1: Replace the flat link styles**
Replace `.side-nav`, `.side-nav-item` and its hover/active rules with:
```css
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 10px; }
.side-nav-menu {
width: 100%;
border-right: 0 !important;
background: transparent !important;
--el-menu-bg-color: transparent;
--el-menu-text-color: #bdc7d7;
--el-menu-active-color: var(--cc-navy);
--el-menu-hover-bg-color: rgba(255,255,255,.08);
}
.side-nav-menu.el-menu--collapse { width: 56px; }
.side-nav-menu .el-menu-item,
.side-nav-menu .el-sub-menu__title {
height: 44px;
margin: 3px 0;
border-radius: 8px;
line-height: 44px;
}
.side-nav-menu .el-menu-item:hover,
.side-nav-menu .el-sub-menu__title:hover { color: #fff; }
.side-nav-menu .el-menu-item.is-active { background: #fff; color: var(--cc-navy); font-weight: 650; }
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 48px !important; }
.side-nav-menu .el-menu-item svg,
.side-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 12px; }
```
- [ ] **Step 2: Update the mobile override**
Remove mobile rules that reference `.side-nav-item` and add:
```css
@media (max-width: 720px) {
.side-nav-menu,
.side-nav-menu.el-menu--collapse { width: 100%; }
}
```
Do not alter the existing sidebar transform, scrim, 44px touch targets or reduced-motion behavior.
- [ ] **Step 3: Run static verification**
Run:
```powershell
pnpm test
pnpm typecheck
pnpm build
```
Expected: tests, Vue TypeScript checks and production build all PASS.
### Task 6: Browser and Impeccable verification
**Files:**
- Modify only if a verified defect is found: `apps/admin-web/src/components/AdminSidebarMenu.vue`, `apps/admin-web/src/layouts/AppLayout.vue`, `apps/admin-web/src/styles/index.css`
- [ ] **Step 1: Start the admin development server**
Run:
```powershell
pnpm dev
```
Expected: admin is available at `http://localhost:5173`.
- [ ] **Step 2: Verify desktop expanded and collapsed states in one browser pass**
Check:
- Workbench remains a direct entry.
- Business management contains customers and first-leg orders.
- System management contains users, roles and permissions.
- Active route remains highlighted after direct URL navigation.
- Collapsing the sidebar exposes grouped entries through Element Plus popovers.
- Existing customer, order, user, role and permission interactions still work.
- [ ] **Step 3: Verify the 390px mobile state in the same pass**
Check:
- The menu button opens the drawer.
- Both groups can expand and every leaf is reachable.
- Selecting a leaf closes the drawer.
- Clicking the right-side scrim closes the drawer.
- The page itself has no horizontal viewport overflow.
- [ ] **Step 4: Run the Impeccable detector once**
Run:
```powershell
node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json src/components/AdminSidebarMenu.vue src/layouts/AppLayout.vue src/styles/index.css
```
Expected: no unverified detector findings remain. Fix verified findings in one batch, then perform at most one confirmation browser pass.
### Task 7: Update project documentation and complete verification
**Files:**
- Modify: `apps/admin-web/README.md`
- Modify: `apps/admin-web/AGENTS.md`
- [ ] **Step 1: Document tests and grouped navigation**
Add:
````markdown
测试:
```bash
pnpm test
```
后台菜单按“业务管理”和“系统管理”分组;当前页面使用 Pinia 模拟数据,不请求尚未确定的 Java 业务接口。
````
Update the quality-gate section of `apps/admin-web/AGENTS.md` to include `pnpm test`, and replace the obsolete statement that the project has no automated test script with a requirement to run unit tests for navigation or other tested behavior changes.
- [ ] **Step 2: Run final verification from admin-web**
Run:
```powershell
pnpm test
pnpm typecheck
pnpm build
```
Expected: all commands exit with code 0 and `apps/admin-web/dist` is refreshed.
- [ ] **Step 3: Confirm repository isolation**
Run from the repository root:
```powershell
Get-ChildItem -Force -File | Where-Object { $_.Name -in @('package.json','pnpm-workspace.yaml','pnpm-lock.yaml') }
```
Expected: no Node package or workspace files exist at the repository root.
@@ -0,0 +1,316 @@
# Admin Permission Tree Sync 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:** Make permission management and role authorization use the same grouped, recursive static permission hierarchy as the admin sidebar while preserving all existing local interactions.
**Architecture:** Add one pure permission-tree utility that builds arbitrary-depth trees, preserves ancestors during filtering, sorts siblings, and safely traverses descendants. Keep the sidebar and permission data independent, then verify their menu permission codes agree in tests; both views consume the utility, while Pinia remains the static data source and no Java API contract is invented.
**Tech Stack:** Vue 3, TypeScript, Pinia, Element Plus, Vitest, Vue Test Utils, Vite
---
### Task 1: Lock the recursive tree contract with tests
**Files:**
- Create: `apps/admin-web/src/utils/permissionTree.test.ts`
- Create: `apps/admin-web/src/utils/permissionTree.ts`
- [ ] **Step 1: Write failing tests for recursive structure and sibling sorting**
Create fixtures with a directory, menu, and button in deliberately unsorted input order. Import the wished-for `buildPermissionTree` function and assert that it returns all three levels and sorts each sibling group by `sort`.
```ts
import { describe, expect, it } from 'vitest'
import type { Permission } from '@/types/admin'
import { buildPermissionTree } from '@/utils/permissionTree'
const permissions: Permission[] = [
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
{ id: 'menu', parentId: 'directory', name: '页面', code: 'demo:list', type: '菜单', path: '/demo', method: '-', status: '启用', sort: 10 },
{ id: 'directory', parentId: null, name: '目录', code: 'demo', type: '目录', path: '', method: '-', status: '启用', sort: 10 },
{ id: 'action-a', parentId: 'menu', name: '操作 A', code: 'demo:a', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
]
it('builds arbitrary-depth trees and sorts siblings', () => {
const tree = buildPermissionTree(permissions)
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a', 'action-b'])
})
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `pnpm test -- src/utils/permissionTree.test.ts`
Expected: FAIL because `@/utils/permissionTree` does not exist.
- [ ] **Step 3: Add failing tests for ancestor-preserving search and cycle-safe descendants**
Add assertions that a matcher for `demo:a` retains `directory -> menu -> action-a`, and that `getDescendantIds` returns each reachable child once even when malformed fixture data includes a cycle.
```ts
import { buildPermissionTree, getDescendantIds } from '@/utils/permissionTree'
it('keeps the ancestor chain when a deep descendant matches', () => {
const tree = buildPermissionTree(permissions, (item) => item.code === 'demo:a')
expect(tree.map((node) => node.id)).toEqual(['directory'])
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a'])
})
it('collects descendants once when malformed data contains a cycle', () => {
const malformed = [
...permissions,
{ id: 'cycle-a', parentId: 'cycle-b', name: 'A', code: 'cycle:a', type: '目录', path: '', method: '-', status: '启用', sort: 1 },
{ id: 'cycle-b', parentId: 'cycle-a', name: 'B', code: 'cycle:b', type: '目录', path: '', method: '-', status: '启用', sort: 2 },
] satisfies Permission[]
expect(getDescendantIds(malformed, 'cycle-a')).toEqual(['cycle-b'])
})
```
- [ ] **Step 4: Implement the minimal recursive utility**
Create `permissionTree.ts` with a `PermissionTreeNode` type, a sorted parent index, path-local cycle protection for tree construction, ancestor-preserving matching, and breadth-first descendant traversal whose visited set starts with the selected node.
```ts
import type { Permission } from '@/types/admin'
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
export type PermissionMatcher = (permission: Permission) => boolean
function groupByParent(permissions: Permission[]) {
const childrenByParent = new Map<string | null, Permission[]>()
permissions.forEach((permission) => {
const siblings = childrenByParent.get(permission.parentId) ?? []
siblings.push(permission)
childrenByParent.set(permission.parentId, siblings)
})
childrenByParent.forEach((siblings) => siblings.sort((left, right) => left.sort - right.sort))
return childrenByParent
}
export function buildPermissionTree(permissions: Permission[], matches: PermissionMatcher = () => true): PermissionTreeNode[] {
const childrenByParent = groupByParent(permissions)
const buildNode = (permission: Permission, ancestors: Set<string>): PermissionTreeNode | null => {
if (ancestors.has(permission.id)) return null
const nextAncestors = new Set(ancestors).add(permission.id)
const children = (childrenByParent.get(permission.id) ?? [])
.map((child) => buildNode(child, nextAncestors))
.filter((child): child is PermissionTreeNode => child !== null)
if (!matches(permission) && children.length === 0) return null
return { ...permission, ...(children.length ? { children } : {}) }
}
return (childrenByParent.get(null) ?? [])
.map((permission) => buildNode(permission, new Set()))
.filter((node): node is PermissionTreeNode => node !== null)
}
export function getDescendantIds(permissions: Permission[], permissionId: string): string[] {
const childrenByParent = groupByParent(permissions)
const visited = new Set([permissionId])
const descendants: string[] = []
const queue = [...(childrenByParent.get(permissionId) ?? [])]
while (queue.length) {
const current = queue.shift()
if (!current || visited.has(current.id)) continue
visited.add(current.id)
descendants.push(current.id)
queue.push(...(childrenByParent.get(current.id) ?? []))
}
return descendants
}
```
- [ ] **Step 5: Run the focused test and verify GREEN**
Run: `pnpm test -- src/utils/permissionTree.test.ts`
Expected: all permission-tree utility tests pass with no warnings.
### Task 2: Restructure the static permission and role fixtures
**Files:**
- Modify: `apps/admin-web/src/stores/adminData.ts`
- Create: `apps/admin-web/src/stores/adminData.test.ts`
- [ ] **Step 1: Write failing tests for the grouped hierarchy**
Activate a fresh Pinia instance, read `useAdminDataStore()`, build its tree, and assert the roots are `工作台`, `业务管理`, and `系统管理`; assert `业务管理` owns the customer/order pages and `系统管理` owns the three system pages.
```ts
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAdminDataStore } from '@/stores/adminData'
import { buildPermissionTree } from '@/utils/permissionTree'
beforeEach(() => setActivePinia(createPinia()))
it('groups page permissions under business and system directories', () => {
const tree = buildPermissionTree(useAdminDataStore().permissions)
expect(tree.map((node) => node.name)).toEqual(['工作台', '业务管理', '系统管理'])
expect(tree[1]?.children?.map((node) => node.name)).toEqual(['客户管理', '头程订单'])
expect(tree[2]?.children?.map((node) => node.name)).toEqual(['内部用户', '角色管理', '菜单权限'])
})
```
- [ ] **Step 2: Write a failing navigation consistency test**
Compare all `菜单` permission codes with `getNavigationLeaves(adminNavigation)` and assert both sets are equal, proving visible routes and static permission resources cannot silently drift.
```ts
it('keeps menu permission codes aligned with navigation leaves', () => {
const store = useAdminDataStore()
const menuCodes = store.permissions.filter((item) => item.type === '菜单').map((item) => item.code).sort()
const navigationCodes = getNavigationLeaves(adminNavigation).map((item) => item.permission).sort()
expect(menuCodes).toEqual(navigationCodes)
})
```
- [ ] **Step 3: Run store tests and verify RED**
Run: `pnpm test -- src/stores/adminData.test.ts`
Expected: FAIL because current permissions expose five flat roots and contain redundant view nodes.
- [ ] **Step 4: Replace the permission fixtures**
Use stable IDs and the confirmed hierarchy:
```text
P001 工作台 (菜单)
P010 业务管理 (目录)
P100 客户管理 (菜单)
P102 新增客户 (按钮)
P103 审核客户 (按钮)
P200 头程订单 (菜单)
P202 创建订单 (按钮)
P203 更新状态 (按钮)
P020 系统管理 (目录)
P300 内部用户 (菜单)
P302 维护用户 (按钮)
P400 角色管理 (菜单)
P402 角色授权 (按钮)
P500 菜单权限 (菜单)
P502 维护权限 (按钮)
```
Menu nodes keep their real route paths and route permission codes. Directory and button nodes use an empty path and `method: '-'`; do not add `接口` fixtures or `/api/...` paths before the Java contract exists.
- [ ] **Step 5: Update all five role permission ID lists**
Keep `R001` mapped to all resources. Include grouping directory IDs for the other roles and grant only the confirmed menu/action resources described by the design document.
```ts
R002: P001, P010, P100, P103, P200, P203
R003: P001, P010, P200, P202, P203
R004: P001, P010, P200
R005: P001, P010, P100, P102, P200
```
- [ ] **Step 6: Run store and full tests and verify GREEN**
Run: `pnpm test -- src/stores/adminData.test.ts`
Expected: grouped hierarchy and navigation consistency tests pass.
Run: `pnpm test`
Expected: all existing navigation/sidebar tests and new tests pass.
### Task 3: Integrate the recursive utility into both views
**Files:**
- Modify: `apps/admin-web/src/views/PermissionsView.vue`
- Modify: `apps/admin-web/src/views/RolesView.vue`
- Read before UI edits: `C:/Users/BY/.codex/skills/impeccable/reference/audit.md`
- Read before UI edits: `C:/Users/BY/.codex/skills/impeccable/reference/craft-floor.md`
- [ ] **Step 1: Replace the permission page's local two-level builder**
Import `buildPermissionTree` and `getDescendantIds`. Compute `treeRows` with `buildPermissionTree(store.permissions, matches)` so filters preserve full ancestor chains. Compute a blocked ID set from `form.id` plus all descendants and exclude those resources from `parentOptions`.
```ts
const treeRows = computed(() => buildPermissionTree(store.permissions, matches))
const blockedParentIds = computed(() => new Set(form.id ? [form.id, ...getDescendantIds(store.permissions, form.id)] : []))
const parentOptions = computed(() => store.permissions.filter((item) =>
['目录', '菜单'].includes(item.type) && !blockedParentIds.value.has(item.id),
))
```
- [ ] **Step 2: Update permission-page copy for static API scope**
Keep all existing interactions, but change copy that implies interface resources are already active. The table description should say API permissions will be added after the service contract is fixed, the fourth metric should show `0` interface permissions as reserved, and path labels/placeholders should distinguish routes from future interfaces without fabricating endpoints.
- [ ] **Step 3: Replace the role page's local two-level builder**
Import `buildPermissionTree`, map its recursive nodes to the Element Plus node shape through one recursive mapper, and keep `default-expand-all`, checked keys, half-checked keys, and save behavior unchanged.
```ts
function toRolePermissionNode(node: PermissionTreeNode): RolePermissionTreeNode {
return {
id: node.id,
label: node.name,
code: node.code,
...(node.children?.length ? { children: node.children.map(toRolePermissionNode) } : {}),
}
}
const permissionTree = computed(() => buildPermissionTree(store.permissions).map(toRolePermissionNode))
```
- [ ] **Step 4: Correct role permission summaries**
Count `菜单` items only as menus and count `按钮` plus future `接口` items as operations. Do not count `目录` nodes as menu entries.
```ts
const menuCount = store.permissions.filter((item) => row.permissionIds.includes(item.id) && item.type === '菜单').length
const actionCount = store.permissions.filter((item) => row.permissionIds.includes(item.id) && ['按钮', '接口'].includes(item.type)).length
```
- [ ] **Step 5: Run tests and type checking**
Run: `pnpm test`
Expected: all tests pass.
Run: `pnpm typecheck`
Expected: Vue and TypeScript checks exit successfully with no errors.
### Task 4: Verify production and real interactions
**Files:**
- Verify: `apps/admin-web/src/views/PermissionsView.vue`
- Verify: `apps/admin-web/src/views/RolesView.vue`
- [ ] **Step 1: Run the production build**
Run: `pnpm build`
Expected: `vue-tsc --noEmit` and `vite build` both exit with code 0; output stays within `apps/admin-web/dist`.
- [ ] **Step 2: Start or reuse the admin development server**
Run: `pnpm dev`
Expected: Vite serves the admin application on an available local port, normally `http://localhost:5173`.
- [ ] **Step 3: Perform one bounded browser inspection pass**
At desktop width, inspect `/system/permissions` and verify three roots, deep search ancestor preservation, editing, adding a child, and exclusion of self/descendants in the parent selector. Open `/system/roles`, configure a role, and verify the grouped authorization tree, checked/half-checked state, and corrected summary.
At 390px width, verify both pages have no incoherent overlap, drawers remain operable, the tree hierarchy is readable, and table horizontal scrolling remains available.
- [ ] **Step 4: Run Impeccable detector once**
Run the detector command documented by the loaded Impeccable audit reference against the modified admin UI. Fix only actionable findings caused by this change in one batch.
- [ ] **Step 5: Perform at most one confirmation pass**
Recheck only the affected routes and any fixed findings. Stop after this pass per the bounded Impeccable verification rule.
- [ ] **Step 6: Record final verification evidence**
Report exact test counts, type-check/build exit status, browser paths and viewport sizes, and any remaining risk. State explicitly that `src/api/http.ts` remains only the transport/auth wrapper and domain APIs are deferred until the Java contract is known.
> Git commits are intentionally omitted: `C:/Users/BY/Desktop/code/chongchong-global` is not a Git repository, so worktrees, commits, merges, and pull requests are unavailable.
@@ -0,0 +1,396 @@
# Client Web Navigation 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:** 将 client-web 的扁平侧栏改为参考原型的两级业务分组导航,并修复移动端抽屉导航。
**Architecture:** 在 client-web 内新增带图标、权限和 children 的导航树与独立侧栏菜单组件。`AppLayout.vue` 只管理租户壳层、当前路由和移动抽屉状态;桌面与移动端复用同一份菜单组件和配置,不扩展共享包的单层 `NavigationItem`。
**Tech Stack:** Vue 3、TypeScript、Vue Router、Element Plus、Lucide Vue、Vite。
---
### Task 1: 建立客户端导航树
**Files:**
- Create: `apps/client-web/src/navigation/clientNavigation.ts`
- Modify: `apps/client-web/src/router/index.ts:1-33`
- [ ] **Step 1: 定义导航节点类型和叶子展开工具**
创建完整文件:
```ts
import type { Component } from 'vue'
import {
Building2,
ClipboardList,
KeyRound,
LayoutGrid,
Settings,
ShieldCheck,
Truck,
Users,
} from '@lucide/vue'
export interface ClientNavigationNode {
key: string
title: string
icon: Component
path?: string
permission?: string
children?: ClientNavigationNode[]
}
export interface ClientNavigationLeaf extends ClientNavigationNode {
path: string
permission: string
children?: never
}
```
- [ ] **Step 2: 写入已确认的导航树**
在同一文件写入:
```ts
export const clientNavigation: ClientNavigationNode[] = [
{ key: 'dashboard', title: '业务总览', icon: LayoutGrid, path: '/', permission: 'dashboard:view' },
{
key: 'first-leg',
title: '头程业务',
icon: Truck,
children: [
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'order:first-leg:list' },
],
},
{
key: 'company',
title: '企业管理',
icon: Settings,
children: [
{ key: 'company-profile', title: '企业资料', icon: Building2, path: '/company/profile', permission: 'company:profile:view' },
{ key: 'company-members', title: '企业成员', icon: Users, path: '/organization/members', permission: 'company:member:list' },
{ key: 'company-roles', title: '角色管理', icon: ShieldCheck, path: '/organization/roles', permission: 'company:role:list' },
{ key: 'company-permissions', title: '菜单权限', icon: KeyRound, path: '/organization/permissions', permission: 'company:permission:list' },
],
},
]
export function getNavigationLeaves(nodes: ClientNavigationNode[]): ClientNavigationLeaf[] {
return nodes.flatMap((node) => {
if (node.children?.length) return getNavigationLeaves(node.children)
if (!node.path || !node.permission) return []
return [node as ClientNavigationLeaf]
})
}
```
不要加入原型中尚未实现的一键导入、快速下单、海外仓或财务结算。
- [ ] **Step 3: 删除 router 中的扁平 `clientNavigation` 配置并改为从本地导航模块导出**
删除 `NavigationItem` 类型导入和扁平数组,临时保留兼容导出,让尚未修改的布局仍能通过类型检查:
```ts
export { clientNavigation } from '@/navigation/clientNavigation'
```
Task 3 改造布局后删除这行兼容导出,使 router 最终只保留路由和权限元数据。
- [ ] **Step 4: 运行类型检查**
Run: `pnpm typecheck`
Expected: 成功退出,0 个 TypeScript 错误;不要引入 `any`。
### Task 2: 实现可复用客户端侧栏菜单组件
**Files:**
- Create: `apps/client-web/src/components/ClientSidebarMenu.vue`
- [ ] **Step 1: 使用 Element Plus 菜单组件实现递归一级/二级渲染**
组件脚本为:
```vue
<script setup lang="ts">
import { computed } from 'vue'
import { clientNavigation, type ClientNavigationNode } from '@/navigation/clientNavigation'
const props = defineProps<{ activePath: string }>()
const emit = defineEmits<{ navigate: [] }>()
function findParentKey(nodes: ClientNavigationNode[], path: string): string | undefined {
for (const node of nodes) {
if (node.children?.some((child) => child.path === path)) return node.key
if (node.children?.length) {
const parentKey = findParentKey(node.children, path)
if (parentKey) return parentKey
}
}
}
const defaultOpeneds = computed(() => {
const parentKey = findParentKey(clientNavigation, props.activePath)
return parentKey ? [parentKey] : []
})
function handleSelect() {
emit('navigate')
}
</script>
```
- [ ] **Step 2: 计算当前路由所在分组**
模板使用同一导航树,并把当前路由所在父分组传给 `default-openeds`:
```vue
<template>
<el-menu
class="client-nav-menu"
:default-active="activePath"
:default-openeds="defaultOpeneds"
router
@select="handleSelect"
>
<template v-for="item in clientNavigation" :key="item.key">
<el-sub-menu v-if="item.children?.length" :index="item.key">
<template #title>
<component :is="item.icon" :size="18" :stroke-width="1.9" />
<span>{{ item.title }}</span>
</template>
<el-menu-item v-for="child in item.children" :key="child.key" :index="child.path || child.key">
<component :is="child.icon" :size="17" :stroke-width="1.9" />
<template #title>{{ child.title }}</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path || item.key">
<component :is="item.icon" :size="18" :stroke-width="1.9" />
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>
```
- [ ] **Step 3: 暴露菜单选择事件**
确认 `handleSelect()` 只 emit `navigate`;路由跳转由 `el-menu router` 完成,父布局只负责关闭移动抽屉。
- [ ] **Step 4: 运行类型检查**
Run: `pnpm typecheck`
Expected: 组件 props、事件和导航节点类型全部通过严格检查。
### Task 3: 接入 AppLayout 的移动抽屉状态
**Files:**
- Modify: `apps/client-web/src/layouts/AppLayout.vue:1-75`
- [ ] **Step 1: 增加移动菜单状态和路由联动**
脚本增加以下状态与清理逻辑:
```ts
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { X } from '@lucide/vue'
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
const mobileMenuOpen = ref(false)
let previousBodyOverflow = ''
function closeMobileMenu() {
mobileMenuOpen.value = false
}
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') closeMobileMenu()
}
watch(() => route.fullPath, closeMobileMenu)
watch(mobileMenuOpen, (open) => {
if (open) {
previousBodyOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = previousBodyOverflow
}
})
onMounted(() => document.addEventListener('keydown', handleEscape))
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = previousBodyOverflow
})
```
- [ ] **Step 2: 替换内联 `v-for` 侧栏链接**
删除 `clientNavigation`、六个菜单图标和 `icons` 数组的布局导入;删除 router 中的临时兼容导出。侧栏替换为:
```vue
<aside id="client-sidebar" class="client-sidebar" aria-label="客户工作台导航">
<div class="client-sidebar-mobile-header">
<strong>功能导航</strong>
<button type="button" aria-label="关闭导航" @click="closeMobileMenu"><X :size="20" /></button>
</div>
<nav>
<ClientSidebarMenu :active-path="route.path" @navigate="closeMobileMenu" />
</nav>
<div class="sidebar-help"><strong>需要帮助?</strong><span>联系您的专属客户经理</span></div>
</aside>
```
- [ ] **Step 3: 给移动菜单按钮补齐语义和行为**
根节点、遮罩和菜单按钮使用:
```vue
<div class="client-shell" :class="{ 'mobile-menu-open': mobileMenuOpen }">
<button v-if="mobileMenuOpen" class="client-sidebar-scrim" type="button" aria-label="关闭导航" @click="closeMobileMenu" />
<!-- existing header and sidebar -->
<button type="button" aria-label="打开导航" aria-controls="client-sidebar" :aria-expanded="mobileMenuOpen" @click="mobileMenuOpen = true">
<Menu :size="20" />
</button>
</div>
```
- [ ] **Step 4: 添加遮罩和移动关闭按钮**
确认遮罩位于侧栏之前、侧栏 z-index 高于遮罩;关闭按钮只在移动断点可见。
- [ ] **Step 5: 运行类型检查**
Run: `pnpm typecheck`
Expected: 通过;退出登录确认取消时保持现有页面。
### Task 4: 调整 client-web 导航样式和响应式行为
**Files:**
- Modify: `apps/client-web/src/styles/index.css:20-75, 220-270`
- [ ] **Step 1: 将侧栏改为可滚动的分组菜单容器**
用以下菜单变量与结构样式替换原 `.client-sidebar nav a` 规则:
```css
.client-sidebar { overflow: hidden; }
.client-sidebar nav { flex: 1; min-height: 0; overflow-y: auto; }
.client-nav-menu {
border-right: 0;
--el-menu-bg-color: transparent;
--el-menu-text-color: #52616b;
--el-menu-hover-bg-color: #f2f7f6;
--el-menu-active-color: var(--primary);
}
.client-nav-menu .el-menu-item,
.client-nav-menu .el-sub-menu__title { height: 44px; margin: 3px 0; border-radius: 7px; line-height: 44px; }
.client-nav-menu .el-menu-item.is-active { background: #e9f4f2; color: var(--primary); font-weight: 650; }
.client-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 46px !important; }
.client-nav-menu .el-menu-item svg,
.client-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 11px; }
.client-nav-menu .el-menu-item span,
.client-nav-menu .el-sub-menu__title span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
```
- [ ] **Step 2: 增加焦点与触控反馈**
增加:
```css
.client-nav-menu .el-menu-item:focus-visible,
.client-nav-menu .el-sub-menu__title:focus-visible,
.client-page-heading button:focus-visible,
.client-sidebar-mobile-header button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.client-sidebar-mobile-header { display: none; }
.client-sidebar-scrim { display: none; }
```
- [ ] **Step 3: 实现移动抽屉与遮罩**
在现有 `@media (max-width: 720px)` 中删除 `.client-sidebar { display: none; }`,加入:
```css
.client-sidebar {
display: flex;
width: min(286px, 86vw);
transform: translateX(-100%);
transition: transform .2s ease;
}
.mobile-menu-open .client-sidebar { transform: translateX(0); }
.client-sidebar-scrim {
position: fixed;
inset: 68px 0 0 0;
z-index: 18;
display: block;
border: 0;
background: rgba(23, 50, 77, .46);
}
.client-sidebar-mobile-header {
display: flex;
min-height: 52px;
align-items: center;
justify-content: space-between;
padding: 0 14px;
border-bottom: 1px solid var(--border);
}
.client-sidebar-mobile-header button { display: grid; width: 44px; height: 44px; place-items: center; border: 0; background: transparent; color: var(--navy); }
```
- [ ] **Step 4: 防止移动端横向溢出**
保留 `html, body, #app { min-width: 320px; }`,并加入:
```css
.client-shell { overflow-x: hidden; }
.top-header, .client-main { max-width: 100%; }
```
在 320px 浏览器检查中确认 `document.documentElement.scrollWidth === document.documentElement.clientWidth`。
- [ ] **Step 5: 运行构建**
Run: `pnpm build`
Expected: `vue-tsc --noEmit` 与 `vite build` 均成功。
### Task 5: 完成浏览器与 Impeccable 验收
**Files:**
- Verify: `apps/client-web/src/navigation/clientNavigation.ts`
- Verify: `apps/client-web/src/components/ClientSidebarMenu.vue`
- Verify: `apps/client-web/src/layouts/AppLayout.vue`
- Verify: `apps/client-web/src/styles/index.css`
- [ ] **Step 1: 启动客户端开发服务器**
Run: `pnpm dev`
Expected: 输出 `http://localhost:5174/`;若端口被占用,记录实际端口并使用该端口验收。
- [ ] **Step 2: 验证桌面菜单**
使用预填账号 `customer / 123456` 登录,确认只有“业务总览”直接一级可达;“头程业务”和“企业管理”可展开;四个企业入口和头程订单都能到达原有路由;当前路由高亮正确。
- [ ] **Step 3: 验证移动菜单**
在 720px、390px、320px 视口确认:点击汉堡按钮打开抽屉,遮罩出现;点击二级菜单后抽屉关闭并完成路由切换;点击遮罩和按 Escape 均可关闭;页面无横向溢出。
- [ ] **Step 4: 运行 Impeccable 机械检测**
Run: `node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json src\layouts\AppLayout.vue src\components\ClientSidebarMenu.vue src\navigation\clientNavigation.ts`
Expected: 0 条未解释告警;若有告警,逐条处理或在交付说明中记录明确原因。
- [ ] **Step 5: 复跑质量门禁**
Run: `pnpm typecheck; pnpm build`
Expected: 两个命令均成功后再声称完成。浏览器不可用时必须说明采用源码与 HTTP 回退证据。
@@ -0,0 +1,259 @@
# 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 与目标设备,必须在交付说明中列为未验证。
File diff suppressed because it is too large Load Diff