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,95 @@
# Admin 导航分组与静态交互设计
## 目标
将 admin 当前全部平铺的一级菜单改为可扩展的二级分组导航,同时保留现有静态页面和 Pinia 交互。Java 接口及响应结构尚未确定,本阶段不提前设计领域 API 契约。
## 范围
本次只处理 `apps/admin-web`:
- 调整侧边栏信息架构与折叠交互。
- 保留现有页面、路由地址和模拟数据行为。
- 增加 admin 项目内部测试,不在仓库根目录增加 Node 配置或依赖。
不处理 client、小程序、APP、PDA 或 Java 服务;不猜测最终后端 URL、响应信封、业务错误码或分页格式。
## 导航结构
采用两级分组,工作台保留为直接入口:
```text
工作台
业务管理
├── 客户管理
└── 头程订单
系统管理
├── 内部用户
├── 角色管理
└── 菜单权限
```
侧边栏使用 Element Plus 的菜单和子菜单能力,以获得稳定的展开、折叠、当前路由高亮和折叠态浮层。页面路由地址保持不变。
菜单配置放在 `src/navigation/adminNavigation.ts`,使用 admin 本地的 `AdminNavigationNode` 类型,避免修改共享包中的单层 `NavigationItem` 并影响 client。节点包含唯一标识、标题、图标、路径、权限编码和可选子节点。图标跟随节点配置,不再依赖 `iconMap[index]` 的数组位置。
当前静态阶段显示全部菜单,不根据模拟角色执行权限过滤,因为现有登录用户没有后端返回的权限编码集合。节点保留权限编码字段;后端权限契约确定后,再以叶子菜单为单位过滤,并只显示至少包含一个可访问子菜单的分组。
移动端继续使用现有抽屉式侧栏:选择页面后自动关闭,点击右侧遮罩关闭。桌面折叠时由 Element Plus 展示子菜单浮层,避免二级入口丢失。
## 静态数据与交互
客户、订单、内部用户、角色和权限页面继续使用 `useAdminDataStore`:
- 查询、筛选和分页在前端执行。
- 新增、编辑、审核、启停和状态流转直接修改 Pinia 内存数据。
- 刷新页面后恢复初始模拟数据,不增加持久化。
- 页面不发起真实业务网络请求。
该策略用于先验收页面结构和操作流程,不伪装成已完成后端联调。
## API 当前边界
保留现有 `src/api/http.ts` 作为 Axios 基础实例,仅包含基础地址、超时和 admin Token 注入。当前不新增客户、订单、用户、角色或权限领域 API,不定义响应信封解包,不实现业务错误转换,也不在 401 拦截器中直接引用 Router 或 Pinia Store。
后端提供接口文档后,再单独确定以下内容:
- URL 前缀和版本规则。
- 统一响应信封及成功业务码。
- 分页参数和分页结果结构。
- 业务错误码与用户提示映射。
- 401 处理方式;若需要全局退出,使用应用启动层注册的 `onUnauthorized` 回调,避免请求层与 Router、Store 循环依赖。
- 按认证、客户、头程订单、内部用户、角色和权限拆分领域模块。
真实接口接入后,由 Store 调用领域 API,并负责加载状态、错误状态和数据缓存。页面继续读取 Store,不直接调用 Axios。
## 测试与验收
在 `apps/admin-web` 内增加 Vitest,不影响其他项目。测试覆盖:
- 导航只有工作台是直接一级入口,业务管理和系统管理拥有预期子菜单。
- 每个叶子菜单的路径和权限编码唯一。
- 菜单图标由节点自身定义,不依赖数组下标。
- 静态阶段不因权限字段存在而隐藏任何菜单。
完成后执行:
```bash
cd apps/admin-web
pnpm test
pnpm typecheck
pnpm build
```
浏览器验收桌面展开态、桌面折叠态和 390px 移动端,确认二级菜单可访问、当前页面正确高亮、移动侧栏可关闭,并且原有业务交互不回归。
## 完成标准
- admin 菜单不再全部平铺在一级。
- 新增业务模块时可以放入既有分组或新增分组,不需要改写侧栏组件。
- 当前六个页面入口全部可达且路由不变。
- 静态页面的筛选、表单、详情和状态操作继续可用。
- 页面不会请求不存在的 Java 业务接口,也不会固化未经确认的响应结构。
- admin 的测试、类型检查和生产构建全部通过。
@@ -0,0 +1,100 @@
# Admin 权限树分组同步设计
## 目标
让权限管理和角色授权中的资源层级与已完成的 admin 侧边栏一致,并支持后续继续增加更深层级,而不需要修改页面的树构建逻辑。
## 方案选择
有三种可选方式:
1. 在权限页面和角色页面分别编写递归逻辑。改动直接,但会产生两份容易漂移的实现。
2. 使用共享的权限树构建器。两个页面消费同一棵树,筛选、排序和层级行为一致,作为本次采用方案。
3. 直接从侧边栏导航生成权限树。可以保证菜单一致,但导航不包含按钮和接口权限,且未来后端动态权限尚未确定,不适合当前阶段。
## 权限层级
静态权限数据调整为:
```text
工作台
业务管理
├── 客户管理
│ ├── 新增客户
│ └── 审核客户
└── 头程订单
├── 创建订单
└── 更新状态
系统管理
├── 内部用户
│ └── 维护用户
├── 角色管理
│ └── 角色授权
└── 菜单权限
└── 维护权限
```
“业务管理”和“系统管理”是目录;“工作台、客户管理、头程订单、内部用户、角色管理、菜单权限”是菜单;具体操作是按钮权限。菜单本身使用现有路由权限编码,例如 `customer:list` 和 `system:role:list`,不再额外保留同义的“查看客户”“查看角色”节点。
接口权限待 Java 接口契约确定后再补充。本次不根据静态 URL 猜测接口资源;现有按钮权限上的临时 `/api/...` 路径和 HTTP 方法一并清空,按钮只保留稳定的权限编码。
## 共享树构建器
在 `src/utils/permissionTree.ts` 提供纯函数:
- 根据 `parentId` 递归构建任意深度的权限树。
- 同级按 `sort` 排序。
- 接收可选匹配函数;子节点匹配时保留完整祖先链。
- 防止异常循环引用造成无限递归。
- 提供后代 ID 查询,编辑上级资源时排除自身及全部后代,避免创建循环。
权限管理页面使用完整 `Permission` 节点,以便编辑、启停和添加下级。角色页面将同一结果映射为 Element Plus 树需要的 `id`、`label`、`code` 和 `children`。
## 静态角色授权
同步更新五个模拟角色的 `permissionIds`:
- 超级管理员拥有全部资源。
- 运营主管拥有工作台、业务管理、客户审核、订单查看和状态处理。
- 操作员拥有工作台及头程订单创建、处理权限。
- 财务拥有工作台及头程订单查看权限。
- 销售拥有工作台、客户维护及头程订单查看权限。
分组目录 ID 一并保留在角色授权中,以确保回显和父子半选状态明确。
## 页面行为
权限管理:
- 默认展开完整三级树。
- 搜索命中按钮时仍显示所属菜单及目录。
- 新增下级、编辑、启停继续使用现有静态交互。
- 编辑上级资源时不能选择自身或自己的后代。
角色管理:
- 授权抽屉展示相同的目录、菜单和按钮层级。
- 保存时继续合并已选和半选节点。
- 权限摘要按菜单与操作分别计数,不把目录计入菜单数量。
## 测试与验收
使用 admin 现有 Vitest:
- 先写失败测试固定三级结构和同级排序。
- 验证搜索命中深层节点时保留祖先链。
- 验证循环引用不会导致递归失控。
- 验证后代 ID 查询能阻止把节点移动到自己的后代下。
- 验证静态权限编码与导航叶子权限编码一致。
最后执行 `pnpm test`、`pnpm typecheck`、`pnpm build`,并在浏览器检查权限管理页面和角色授权抽屉的桌面及移动端层级展示。UI 改动完成后运行一次 Impeccable 检测。
## 完成标准
- 权限管理和角色授权不再显示五个独立一级业务目录。
- 两个页面使用同一个递归树构建器。
- 搜索、排序、授权回显和静态编辑交互保持可用。
- 不新增真实 API 调用或猜测后端响应格式。
- 测试、类型检查、构建及浏览器验收通过。
@@ -0,0 +1,168 @@
# Client Web 导航分级与移动侧栏设计
## 背景
`apps/client-web` 当前把业务总览、企业资料、企业成员、角色管理、菜单权限和头程订单全部渲染为同级侧栏链接。权威原型 `docs/悦/头程下单/柏悦客户端.html` 采用“一级业务域 + 二级具体任务”的导航结构,现有实现没有延续这一信息架构,也无法承接后续业务扩展。
Impeccable 验收还发现一个阻断性问题:720px 以下侧栏被隐藏,但页面上的菜单按钮没有打开移动导航的行为,导致移动端无法切换模块。
## 目标
- 将客户工作台的平铺菜单改为可扩展的两级分组导航。
- 保留现有路由、页面、文案、品牌颜色与 Element Plus 技术体系。
- 修复移动端导航不可用的问题。
- 让菜单节点显式携带图标与权限信息,避免依赖数组下标。
- 为后续真实权限集合接入保留清晰边界,不在本次伪造授权数据。
## 非目标
- 不新增原型中尚未实现的一键导入、快速下单、海外仓或财务结算入口。
- 不改变现有路由地址,不拆分当前 `ModuleView.vue` 页面。
- 不照搬原型的紫色渐变视觉,不重做登录、注册或业务内容页。
- 不实现服务端权限校验,也不把前端菜单隐藏当作安全控制。
- 不新增测试框架;当前项目尚未声明测试脚本。
## 导航信息架构
采用两级分组,业务总览保留直接入口:
```text
业务总览 /
头程业务 可展开分组
└── 头程订单 /orders/first-leg
企业管理 可展开分组
├── 企业资料 /company/profile
├── 企业成员 /organization/members
├── 角色管理 /organization/roles
└── 菜单权限 /organization/permissions
```
分组遵循原型的结构原则,但只呈现当前真实存在的客户端路由。后续功能只有在页面与路由实现后,才加入对应业务分组。
## 组件与数据结构
### 导航配置
新增客户端本地导航配置 `src/navigation/clientNavigation.ts`。定义客户端专用的 `ClientNavigationNode`,包含:
- 稳定 `key`;
- 用户可见 `title`;
- Lucide 图标组件;
- 可选 `path`;
- 可选 `permission`;
- 可选 `children`。
共享包中的 `NavigationItem` 仍保持单层公共模型,避免为了单个应用的壳层结构扩大跨项目契约。
### 侧栏菜单组件
新增独立的客户端侧栏菜单组件,使用 Element Plus 的 `el-menu`、`el-sub-menu` 与 `el-menu-item`:
- `active-path` 由当前路由提供;
- 父分组可展开和收起;
- 当前路由所在分组默认展开;
- 叶子菜单负责实际路由跳转;
- 图标来自节点配置,不依赖渲染顺序;
- 桌面与移动端复用同一组件和同一份导航数据。
本轮不额外实现展开状态持久化;Element Plus 在当前会话中维护用户开合状态,且当前路由所在分组始终可识别。持久化不是修复平铺结构与移动阻断所必需的能力。
### 应用壳层
`AppLayout.vue` 继续负责品牌、租户、用户和页面标题,同时增加:
- `mobileMenuOpen` 状态;
- 移动菜单按钮的 `aria-expanded`;
- 遮罩关闭;
- 路由切换后自动关闭;
- Escape 键关闭;
- 打开时阻止背景页面滚动;
- 退出确认取消时不产生未处理异常。
桌面侧栏保持固定宽度和现有绿色服务型视觉,不引入后台的深色侧栏,也不增加桌面折叠模式。
## 交互与状态流
### 桌面端
1. 路由加载后,菜单以 `route.path` 高亮对应叶子。
2. “业务总览”为直接入口。
3. “头程业务”和“企业管理”为分组触发器。
4. 当前页面属于某分组时,该分组保持展开并显示父级激活语义。
5. 用户可以独立展开或收起其他分组。
### 移动端
1. 720px 以下侧栏移出视口,而不是使用 `display: none` 永久移除。
2. 点击页面标题前的菜单按钮打开抽屉并显示遮罩。
3. 点击遮罩、按 Escape 或选择任一叶子路由后关闭抽屉。
4. 抽屉打开时禁止背景滚动,关闭或组件卸载时恢复。
5. 抽屉沿用完整租户导航,不维护第二份移动菜单。
### 权限
导航节点继续携带权限编码。当前模拟用户没有可靠的权限集合,因此静态阶段显示全部已实现菜单。真实认证接口返回权限集合后,再增加叶子过滤;父分组没有可访问子项时隐藏。服务端必须继续执行租户与权限校验。
## 视觉与可访问性
- 保留现有 `--primary`、`--navy`、`--border` 和 `--muted` 设计令牌。
- 一级分组与二级叶子通过缩进、字号、颜色和激活背景形成清晰层级。
- 分组触发器与移动菜单按钮使用真实可聚焦控件。
- 保持可见 `:focus-visible` 状态,不只依赖 hover 或颜色表达当前状态。
- 菜单触控目标不低于约 44px。
- 二级菜单文本允许单行省略,避免长租户业务名称破坏侧栏宽度。
- 侧栏导航区域允许纵向滚动;底部帮助区保持可达。
- 320px、720px 与桌面宽度均不能依赖水平滚动完成导航。
## 错误与边界处理
- 用户取消退出确认时保持当前页面,不产生控制台未处理异常。
- 移动抽屉在快速路由切换、浏览器后退和组件卸载后不得残留锁定滚动状态。
- 不存在或被移除的路由继续走现有兜底重定向。
- 当前不虚构客户经理电话、微信或帮助中心链接;帮助区保留为信息提示,后续有真实渠道后再做可执行入口。
- 权限为空或数据尚未加载时,本轮不提前隐藏菜单,避免模拟阶段出现无法恢复的空侧栏。
## 实施范围
预计修改或新增:
- `apps/client-web/src/navigation/clientNavigation.ts`
- `apps/client-web/src/components/ClientSidebarMenu.vue`
- `apps/client-web/src/layouts/AppLayout.vue`
- `apps/client-web/src/router/index.ts`
- `apps/client-web/src/styles/index.css`
仅当实现需要声明自动导入组件时,依赖现有插件自动生成声明,不手工编辑 `auto-imports.d.ts` 或 `components.d.ts`。
## 验收与验证
### 自动验证
在 `apps/client-web` 执行:
```bash
pnpm typecheck
pnpm build
```
项目当前没有 test 或 lint 脚本,因此不声称相应检查通过,也不为本次导航改动引入整套测试设施。
### Impeccable 验收
- 运行一次机械检测,目标覆盖本次变更的 Vue 文件。
- 桌面检查:一级分组、二级缩进、当前路由高亮、可见焦点和侧栏滚动。
- 720px 检查:断点切换时导航仍可达。
- 320px 检查:抽屉、遮罩、触控目标与页面内容不发生水平溢出。
- 浏览器可用时检查菜单打开、选择路由、遮罩关闭和 Escape 关闭;若浏览器不可用,明确记录回退证据,不伪装成已完成视觉实测。
## 完成标准
- 侧栏不再把全部功能放在一级菜单。
- 当前六个页面入口全部可达,路由地址不变。
- 桌面端当前页面与父分组状态清晰。
- 720px 以下菜单按钮能够打开和关闭真实导航抽屉。
- 导航图标不会因菜单调序或新增分组而错位。
- 未实现的原型功能不会作为无效入口出现。
- 类型检查、生产构建与 Impeccable 检测无未解释失败。
@@ -0,0 +1,105 @@
# PDA 拣货静态页面设计
## 目标
在 `apps/pda-uniapp` 中将 `docs/悦/PDA拣货/pda-picking.html` 转换为可运行在 Android PDA 上的 uni-app 页面。原型是唯一视觉和交互依据;实现保留原型文案、布局、颜色、页面密度、演示数据和本地状态流。
本次不接后端、不实现真实鉴权、不接厂商扫描 SDK、不实现持久化或离线队列。
## 实现方案
采用单页面作业流:登录态与拣货态均由 `pages/index/index.vue` 承载,使用 Vue 本地响应式状态切换,而不拆成多个路由。原因如下:
- 原型本身是连续的单页状态机,登录后立即进入同一作业上下文。
- 本阶段没有后端、深链、权限路由或跨页面数据恢复需求。
- 单页面可避免箱号、货品状态和弹层状态在路由之间同步,保持改动小而聚焦。
为避免页面文件承担过多重复结构,可将货品卡片和通用弹层拆成局部业务组件;演示数据与状态转换仍由页面统一管理。当前不引入状态管理库或新依赖。
## 页面结构
### 登录态
- 蓝色渐变背景、系统标识、`柏悦物流 · PDA拣货` 标题和副标题。
- 工号、密码、作业仓库三个字段。
- 普通登录按钮和三个演示账号快捷登录按钮。
- 空工号、空密码和账号密码错误使用原型文案提示。
- 登录成功后显示欢迎提示并进入拣货态;退出后清空登录表单与作业状态。
### 拣货态
- 顶部蓝色作业栏:页面标题、仓库、退出、当前箱号、订单号和人员身份。
- `pages.json` 将首页设置为 `navigationStyle: "custom"`,不显示当前标题为 `uni-app` 的原生导航栏,避免与原型作业栏重复;页面自行处理状态栏安全区。
- 箱号选择器:显示全部演示箱号与订单号,切换时重置当前箱的未提交本地操作。
- 扫码区:输入或扫描 SKU 后回车确认;匹配当前箱中待拣 SKU 时标记已拣,错误码显示失败提示。登录进入拣货态、成功或失败扫码、确认拣出、撤销以及关闭任一弹层后,都应把焦点恢复到扫码输入框;打开需要输入数量的部分拣货弹层时,焦点临时交给数量输入框。成功扫码后清空输入值,无效码和重复扫描已处理 SKU 时保留明确错误反馈,并在反馈后继续接收下一次扫描。
- 箱号与订单信息卡。
- 拣货进度、货品列表、集装箱完成/待拣统计和底部提交栏。
- 底部提交栏固定在可视区域底部,并避让 Android 导航栏安全区。
- 提交按钮初始禁用;当所有货品均已处理时启用并显示“提交拣货完成”;当至少存在一件已拣或部分拣货、同时仍有待拣货品时启用并显示“提交拣货完成(N件待拣将视为未拣)”;仅发生部分跳拣且仍有待拣货品时保持禁用。
### 货品状态
每个货品使用以下本地状态之一:
- `pending`:待拣,提供确认拣出、部分拣货、跳拣。
- `picked`:已拣,显示完成标签与撤销入口。
- `partial`:部分拣货,显示实际数量、目标数量与撤销入口。
- `skipped`:跳拣或提交时自动未拣,显示原因与撤销入口。
待拣货品排在已处理货品之前。进度按已拣、部分拣和跳拣的已处理数量计算。
## 弹层与反馈
- 跳拣弹层:单选原型中的五个原因,未选择时禁止确认并提示。
- 部分拣货弹层:显示 SKU、描述、总量、单位、快捷比例和数量输入;数量必须大于 0 且小于需求总量。
- 集装箱列表弹层:分别展示已完成与待拣箱,允许从待拣列表切换箱号。
- 完成弹层:展示已拣、部分拣、跳拣/未拣统计,并提供返回查看和下一箱。
- 短反馈沿用原型顶部提示条及成功、警告、错误配色。
Android 系统返回键按以下优先级处理:关闭当前弹层,关闭箱号下拉;若处于拣货态且无覆盖层,则调用与“退出”按钮相同的重置流程返回登录态;登录态交由系统退出页面。
## 本地数据与状态
保留原型中的三个箱号、订单号、货品、库位、单位、演示账号和跳拣原因。页面维护:
- 当前用户与仓库;
- 当前箱号索引;
- 当前箱内各 SKU 状态;
- 部分拣货数量;
- 已完成箱号集合;
- 当前下拉、弹层、目标货品和提示条状态;
- 扫码输入值。
提交时,未处理货品按原型标记为“未拣出(提交时自动标记)”,保存当前箱的最终演示状态并更新集装箱统计。刷新、退出或重启应用后允许数据重置,因为本阶段没有持久化要求。
## 视觉与响应式
- 颜色遵循原型:主色 `#1a73e8`、成功 `#0d9488`、危险 `#dc2626`、警告 `#f59e0b`、页面背景 `#f5f7fa`。
- 保留 12px 圆角、浅阴影、蓝色当前货品边线、状态色和卡片密度。
- 使用 uni-app 支持的 `rpx`、百分比和安全区变量适配 Android PDA,以 `360 × 640 dp` 为主要检查尺寸,兼容约 `320–480 dp` 宽度。
- 不引入新字体、图片生成素材、第三方图标库或新的视觉语言。
- 图标优先使用简单 CSS/文本符号或现有静态资源,避免依赖网页专属 SVG 注入方式。
- 本阶段不使用网络、震动、相机或存储能力,因此实现时移除 `manifest.json` 中手工声明的 `VIBRATE` 和 `ACCESS_NETWORK_STATE` 权限;未来接入对应能力时再按明确用途恢复最小权限。
## 边界状态
- 登录加载态:当前纯本地操作无需异步加载,不添加虚假等待动画。
- 空态:保留“该箱号暂无拣货任务”“暂无已拣完成的集装箱”“所有集装箱已拣完成”等原型语义。
- 错误态:覆盖登录校验、空扫码、无效 SKU、未选跳拣原因、无效部分数量和最后一箱提示。
- 重复操作:已处理货品不再匹配扫码;撤销后恢复为待拣。
- 权限态:本阶段没有服务端权限;人员角色仅作演示展示,不据此隐藏功能。
- 软键盘:输入区与部分拣货输入不得被键盘永久遮挡,关闭弹层后恢复扫码输入上下文。
## 验证
仓库当前没有项目根级依赖清单、可核实的构建脚本或自动化测试设施,因此不新增或编造测试命令。实现后进行以下可核实检查:
1. 静态检查 `pages.json`、Vue 模板、脚本和样式是否符合现有 uni-app 结构。
2. 若当前环境可用 HBuilderX/uni-app 运行能力,则以 Android App 方式运行;否则明确记录未执行构建。
3. 在常见 PDA 尺寸核对登录、主作业、弹层、底部固定栏和软键盘遮挡。
4. 手工走通快捷登录、箱号切换、有效/无效/重复扫码、确认拣出、部分拣货、跳拣、撤销、提前提交、完整提交和下一箱,并核对每次关键操作后的扫码焦点。
5. 检查提交按钮在初始、仅部分跳拣、存在已拣且仍有待拣、全部处理四种状态下的启用状态和文案。
6. 检查系统返回对弹层、下拉、拣货态和登录态的处理,并确认返回登录态时执行完整状态重置。
7. 检查首页未出现重复原生导航栏,并确认 Android 配置不再手工声明当前未使用的网络与震动权限。
真实扫描速度、连续扫描、低电量、断网、前后台切换、厂商扫描头和正式打包发布不属于本次静态实现的可验证范围,需在设备与工具链确定后补测。
@@ -0,0 +1,217 @@
# Admin 静态 RBAC 权限预览设计
## 背景
管理后台已经具备内部用户、角色、权限资源和分组菜单的静态交互,但角色授权结果尚未作用到登录后的菜单、路由和操作按钮。路由中的 `meta.permission` 目前只是元数据,无法演示角色权限是否真正生效。
本阶段建立一个可操作的静态 RBAC 演示闭环,用于前端原型验收。它不代表真实服务端鉴权,也不定义 Java API、Token 内容、权限响应结构或错误码。
## 目标
- 在管理后台顶部切换初始模拟数据中的五个启用角色,并继续支持后续新增或停用角色后的动态列表。
- 根据角色授权实时过滤侧边栏菜单。
- 阻止静态原型访问无权限路由,并提供清晰的无权限页面。
- 对已经存在稳定权限编码的操作按钮进行显示控制。
- 角色授权数据发生变化时,权限预览同步更新。
- 保留现有登录、页面交互、响应式布局和独立前端工程结构。
## 不在本次范围
- 不调用真实登录、用户、角色、菜单或权限接口。
- 不修改 `src/api/http.ts` 的请求或响应约定。
- 不新增或猜测 Java API 路径、响应包络、分页结构和错误码。
- 不把前端菜单隐藏或路由跳转描述为安全边界;服务端仍需在后续接口阶段执行最终鉴权。
- 不为当前静态权限模型中没有定义的操作擅自增加权限编码。
- 不模拟角色的 `dataScope` 数据范围,不因切换“全部数据”“本部门数据”或“本人数据”角色而过滤客户、订单等模拟记录。
- 不处理企业客户端、官网、小程序、APP 或 PDA 的权限体系。
## 方案选择
### 采用方案:顶部切换演示角色
在后台顶栏提供“演示角色”切换入口,角色变化后菜单、路由和明确映射的操作按钮立即更新。该方式不要求反复退出登录,适合静态原型展示和权限配置验收。
### 未采用方案
- 使用不同账号重新登录:更接近真实登录流程,但重复退出登录会降低原型验收效率,且当前没有服务端身份契约。
- 只控制菜单和路由:改动较少,但无法验证现有按钮权限是否产生可见效果。
## 架构边界
### 权限预览状态
新增 admin 专属 Pinia 状态层,职责仅限静态权限预览:
- 保存当前演示角色 ID。
- 从 `useAdminDataStore` 中读取当前启用角色和权限资源。
- 根据角色的 `permissionIds` 计算权限编码集合,但只纳入 `status === '启用'` 的权限资源;角色仍保留已停用权限 ID 时,不授予对应访问权。
- 提供 `can(permissionCode)` 判断。
- 提供当前角色、可选角色及第一个可访问菜单。
- 提供登录成功后的初始化、退出时重置和两阶段角色切换动作;页面和布局不直接写入持久化角色 ID。
认证状态仍由 `src/stores/auth.ts` 管理。权限预览状态不写入 Token,不修改 `CurrentUser` 的稳定共享模型,也不在页面中复制认证逻辑。
演示角色 ID 使用 admin 项目专属的 `localStorage` 键保存,以便刷新后保留预览上下文。保存值失效、角色被删除或角色被停用时,回退到启用的超级管理员;如果超级管理员不可用,则回退到第一个启用角色。此回退只服务静态原型,不代表后端策略。
预览状态与认证状态采用明确的生命周期边界:
- 页面刷新保留当前演示角色。
- 退出操作先调用权限预览 Store 的重置动作,清除内存状态与持久化演示角色 ID,再调用 `auth.logout()`;认证 Store 不承担预览状态职责。
- 登录成功后由登录流程调用权限预览 Store 的初始化动作,默认选择启用的超级管理员;超级管理员不可用时选择第一个启用角色。未登录阶段不自动初始化或重新持久化默认角色。
- 顶栏固定区分两个来源:`登录身份:{auth.user.name}({auth.user.account})` 与 `权限预览:{previewRole.name}`。认证角色 `auth.user.roles` 不作为无标签副标题展示,也不被预览状态读取或改写。
- `localStorage` 在同源标签页之间共享,但本期不监听 `storage` 事件做实时跨标签同步;其他标签页刷新后读取最新持久化角色。
### 权限数据流
```text
adminData.roles + adminData.permissions
↓
静态权限预览 Store
↓ ↓ ↓
侧边栏 路由决策 页面操作
↓
未保存内容保护
```
角色授权编辑保存到 `adminData.roles` 后,权限编码集合通过计算属性立即更新,不复制第二份角色或权限数据。
## 菜单与路由行为
### 菜单过滤
在现有 `adminNavigation` 基础上增加纯函数过滤:
- 叶子菜单只在其 `permission` 存在于当前权限编码集合时保留。
- 目录仅作为导航组织节点,其自身启停状态不级联授予或撤销后代权限;叶子菜单只判断自身对应的启用权限资源。
- 任意层级目录在递归过滤后没有任何子菜单时隐藏。
- 菜单原始配置保持不可变,过滤函数返回新节点。
- 工作台继续使用 `dashboard:view`,与其他菜单采用相同规则。
- “第一个可访问菜单”固定为过滤后 `adminNavigation` 配置顺序中的第一个叶子,采用深度优先遍历,不使用权限资源的 `sort` 决定回退目标。
工作台路由必须显式声明 `meta.permission: 'dashboard:view'`。所有受导航权限控制的叶子页面都应同时具有对应路由权限元数据,不能只过滤菜单而遗漏直接地址访问。
`AdminSidebarMenu` 接收过滤后的导航数据,不直接读取 Pinia,并通过递归菜单节点支持后续新增更深层级;权限来源与具体渲染层级不耦合。
### 路由守卫
登录校验继续先于权限校验执行:
1. 未登录访问受保护页面时跳转登录页。
2. 已登录且目标路由声明 `meta.permission` 时,使用静态权限预览判断访问权。
3. 无权限的直接访问跳转到静态 `/403` 页面,使用 `query.from` 保留经过校验的站内来源路径用于纯文本提示;只接受以单个 `/` 开头的内部路径,不把该值作为外部跳转地址。
4. `/403` 本身需要登录,但不要求业务权限,避免重定向循环。
5. 404 重定向保持现状。
为便于单元测试,路由权限决策下沉为不依赖浏览器导航的纯函数;Vue Router 守卫只负责读取状态并执行决策结果。
除导航守卫外,登录态布局还需要响应式监听当前有效权限集合、当前演示角色有效性和当前路由。当角色授权被修改、当前角色被停用或删除、权限资源被停用等变化导致当前页面失去访问权时,立即重新校验当前路由:优先跳转到回退后角色的第一个可访问叶子菜单;没有可访问菜单时进入 `/403`。
导航守卫负责首次进入和显式导航,登录态布局只负责已渲染页面上的响应式权限变化;两者调用同一个纯路由决策函数。布局执行修正前比较目标与当前 `fullPath`,使用单次导航锁避免并发修正,并统一调用 `router.replace`,使浏览器后退不会再次回到刚被拒绝的地址。目标已经相同或当前已处于无菜单可恢复的 `/403` 时不重复导航。
### 主动切换角色
用户从顶栏切换角色采用“请求—确认—提交”两阶段流程,不在下拉选择事件中直接覆盖当前角色:
- 先根据候选角色计算候选权限、当前页面可访问性和回退目标。
- 当前页面的新增/编辑抽屉或对话框存在已修改但未保存的内容,且切换会离开当前路由或撤销该表单保存所需的操作权限时,显示确认框,明确候选角色和将离开的页面;取消后角色、路由和表单内容均保持不变。
- 各业务页面通过 admin 专属的轻量离开保护协调器登记 `{ dirty, requiredPermission, label }`;只在字段实际变化后标记 dirty,关闭或成功保存时注销,不把业务表单数据复制到全局 Store。
- 用户确认或没有受影响的未保存内容后,才提交演示角色 ID;需要修正路由时统一使用 `router.replace`。
- 顶栏随后显示新的演示角色名称,并显示 `已切换至“{角色名}”,当前仅为静态权限预览`。
- 当前页面仍可访问时保持原路由。
- 当前页面不再可访问时,跳转到该角色第一个可访问菜单。
- 如果切换角色时当前页面是 `/403`,只要新角色存在可访问菜单,就始终跳转到第一个可访问叶子菜单,不把 `/403` 视为应继续停留的业务页面。
- 如果角色没有任何菜单权限,进入 `/403`,显示“当前演示角色暂无可访问菜单”。
- 自动跳转完成后将焦点移到新页面主标题,并通过 `aria-live="polite"` 播报当前预览角色与目标页面;仅停留原页面时也播报角色变化。
## 操作权限映射
本次只控制现有静态模型中已有明确编码的操作:
| 页面 | 操作 | 权限编码 |
|---|---|---|
| 客户管理 | 新增客户 | `customer:create` |
| 客户管理 | 审核客户、详情中的通过审核 | `customer:audit` |
| 头程订单 | 创建订单、创建并提交、保存新草稿 | `first-leg:create` |
| 头程订单 | 单笔处理、批量更新、更新状态 | `first-leg:status` |
| 内部用户 | 新增、编辑、重置密码、启停、保存 | `system:user:write` |
| 角色管理 | 新增、配置、启停、删除、保存 | `system:role:grant` |
| 菜单权限 | 新增、编辑、添加下级、启停、保存 | `system:permission:write` |
详情、查询、筛选和已有页面级导出继续由对应菜单权限覆盖。客户编辑/启停、订单编辑等没有独立稳定操作编码的功能暂不额外限制;待 Java 权限契约确定后再细分,避免静态原型创造错误的长期编码。
页面通过同一个权限预览 Store 的 `can` 方法控制明确映射的操作,不创建页面私有权限判断逻辑。无权限操作不渲染,避免用户误以为按钮可用后才收到拒绝。
## 顶栏交互
- 在现有用户信息和退出按钮之间增加“演示角色”下拉入口。
- 使用 Lucide 图标和 Element Plus 现有组件,不引入新依赖。
- 下拉只展示启用角色,并标记当前角色。
- 桌面端持续显示 `权限预览:{角色名}`,并提供“仅预览菜单、页面和操作;数据范围未模拟”的可聚焦说明;登录用户区域显示 `登录身份:{姓名}({账号})`,两者不共用“角色”副标题。
- 窄屏至少保留可识别图标、`权限预览:{角色名},数据范围未模拟` 的可访问名称和 44px 触控区域;长角色名称使用省略与完整 tooltip,不挤压退出按钮。
- 角色下拉支持 Tab 聚焦、Enter/Space 打开、方向键移动、Enter 选择和 Escape 关闭,并保持可见焦点。
- 入口和反馈文案始终包含“演示”或“预览”,避免被理解为真实用户换号或数据范围切换。
## 无权限页面
新增轻量静态 403 页面:
- 说明当前演示角色无权访问目标页面。
- 显示当前演示角色名称。
- 来源路径只作为经过校验、可截断换行的纯文本显示,不生成“返回来源地址”链接。
- 当前角色存在可访问菜单时,提供“返回可访问首页”操作。
- 当前角色没有任何可访问菜单时,提示用户使用顶栏入口切换演示角色。
- 不使用夸张插画或与现有后台不一致的视觉语言。
- 页面作为登录态布局的子路由呈现,使顶栏角色切换始终可用。
## 状态与边界处理
- 角色切换只修改本地预览状态,不修改内部用户的真实 `roleIds`。
- 停用或删除当前演示角色时自动执行回退,不保留悬空角色 ID。
- 停用权限资源会立即从当前角色的有效权限编码集合中移除,即使该权限 ID 仍存在于角色授权数据中。
- 停用目录资源只改变该目录资源自身的有效编码,不级联改变启用后代的菜单、路由或操作权限;权限管理页的停用确认明确说明这一点。
- 超级管理员通过角色现有的全部 `permissionIds` 获得权限,不在代码中使用绕过所有检查的特殊布尔值。
- 角色拥有目录权限但没有任何叶子菜单权限时,不显示空目录。
- 如果异常模拟数据中不存在任何启用角色,当前角色为空、有效权限集合为空并进入 `/403`;正常页面继续保护内置超级管理员,避免产生该状态。
- 多角色合并不在本次预览范围;预览入口一次选择一个角色,真实用户多角色合并规则等待后端契约。
- 静态权限预览不静默回退到远程数据,也不发起网络请求。
## 测试策略
使用 admin 现有 Vitest,按测试驱动方式实现:
- 权限预览 Store:两阶段角色选择、权限编码派生、停用权限资源不授予访问权、失效角色回退、零启用角色、角色授权变更实时生效、退出登录重置且刷新保留、未登录时不重新初始化默认角色。
- 导航过滤:隐藏无权限叶子、目录停用不级联后代、递归移除空目录、保持原配置不可变、按 `adminNavigation` 深度优先顺序确定第一个可访问菜单。
- 路由配置与决策:工作台显式声明 `dashboard:view`;覆盖未登录、已登录有权限、已登录无权限、403 免业务权限四种分支。
- 当前路由再校验:角色授权减少、角色停用或删除、权限资源停用后,当前无权限页面通过 `router.replace` 跳转到第一个可访问叶子或 `/403`;覆盖目标相同、导航锁和浏览器后退不返回被拒页面。
- 侧边栏组件:只渲染传入的过滤结果,并覆盖三层导航数据以证明递归渲染不受当前两层结构限制。
- 顶栏角色切换:当前角色回显、准确的登录身份/权限预览标签、窄屏可访问名称、键盘操作、长角色名,以及从 `/403` 切换到有权限角色后恢复到第一个可访问菜单。
- 未保存内容保护:覆盖无修改直接切换、有修改但权限不受影响、有修改且将离开或失去保存权限、取消保持全部状态、确认后提交角色并 replace 跳转。
- 无障碍动态状态:覆盖成功切换和自动跳转的 polite 播报、跳转后主标题焦点与 Escape 关闭角色菜单。
- 状态来源与生命周期:用户姓名/账号来自认证 Store,演示角色来自权限预览 Store;退出清除预览角色,重新登录使用默认启用角色。
- 页面操作:至少覆盖每个操作权限域的一项代表按钮,确认无权限时不渲染。
- 数据范围边界:角色切换不改变模拟记录集合,界面持续说明 `dataScope` 未模拟。
交付前运行:
```bash
pnpm test
pnpm typecheck
pnpm build
```
浏览器验收覆盖桌面、390px 和项目约定的 320px 最小宽度,并检查 200% 浏览器缩放:角色切换、菜单变化、未保存内容确认/取消、自动跳转、直接访问 403、代表性按钮显示与隐藏、键盘完整操作、可见焦点、动态播报、顶栏不重叠或裁切以及控制台错误。UI 修改完成后运行一次 Impeccable 检测。
## 完成标准
- 初始模拟数据中的五个启用角色可在顶栏切换,后续角色新增、停用或删除后列表与回退实时更新。
- 菜单、路由和已映射操作与当前角色的启用权限资源一致,停用权限不产生访问权。
- 权限预览明确不包含 `dataScope`,角色切换不改变模拟记录,桌面和窄屏均持续提供这一说明。
- 无权限直接访问有明确 403 反馈,不出现循环跳转。
- 角色授权、角色有效性或权限资源状态变化能即时影响当前预览;当前路由失去权限时立即修正到第一个可访问叶子或 `/403`。
- 目录停用语义固定为不级联后代;递归菜单过滤与渲染支持后续增加更深菜单层级。
- 角色切换不会静默丢失已修改表单;取消保持原角色、路由和内容,确认后使用 replace 完成必要跳转。
- 从 `/403` 切换到具有菜单权限的角色后能恢复到可访问页面。
- 刷新保留演示角色,退出登录清除预览状态,重新登录采用默认启用角色;`登录身份` 与 `权限预览` 的标签、数据来源和视觉层级清晰分离。
- 键盘、读屏播报、焦点管理、320px/390px、200% 缩放与长角色名称均通过验收。
- 不产生任何真实 API 请求或新的后端契约假设。
- admin 项目测试、类型检查、构建、桌面/移动端浏览器验收及 Impeccable 检测通过。