Files
FastapiAdmin/frontend/audit-uxui.md
T
zhangtao 28bf232761 docs(audit): 全栈审计报告(7 份分报告 + 总报告)
由 7 名成员并行审计 + 队长线上实测复核产出,合计约 3,600 行:
- backend/audit-backend.md      后端架构与代码质量(22 条)
- backend/audit-security.md     认证/授权/敏感信息(23 条)
- backend/audit-testing.md      测试与可验证性(含真实命令输出)
- backend/audit-requirements.md 需求覆盖对照(多租户规格错位)
- frontend/audit-frontend.md    前端与移动端工程(32 条)
- frontend/audit-uxui.md        交互与视觉一致性
- docker/audit-deploy.md        容器化与部署链路
- AUDIT-REPORT.md               总报告:止血项、修复路线、待决策事项、对既有结论的修正

各报告均标注级别、文件:行号、影响、建议修法,并单列未验证项与抽样边界。
2026-10-04 04:26:11 +08:00

591 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FastapiAdmin 前端 UX/UI 审计报告(Web + 移动端)
- 审计轮次:摸底审计第 1 轮(**只读,未修改任何仓库文件**,本报告为唯一新增文件)
- 审计对象:`frontend/web`(Vue3 + Vite + TS + Element Plus 2.14.3 + Tailwind v4 + Pinia)、`frontend/app`(uni-app + wot-ui + UnoCSS presetWot)、`frontend/docs`(VitePress)
- 需求基准:`REQUIREMENTS.md`
- 审计方式:静态代码证据(grep / AST 级脚本统计 / 依赖包类型定义核对 / 后端模型注释交叉验证)
- 未执行:未启动 dev server、未安装依赖、未截图、未上真机。所有需运行期或设备才能确认的项均归入第 8 节「需人工/真机验证清单」,**不计为缺陷**。
---
## 1. 结论摘要
| 维度 | Web | 移动端 | 结论 |
|---|---|---|---|
| 状态设计完整性 | 加载/骨架/空态/二次确认**齐全**;**错误态与重试入口系统性缺失** | 骨架/空态/下拉刷新/二次确认齐全;错误态缺失,且失败被误渲染为「不存在」 | 两端共同短板是**错误态** |
| 交互一致性 | 分页+搜索+批量+导入导出 | 触底加载+搜索+筛选 Tab,无批量/导出 | 差异多为平台合理差异,但**确认机制与状态色语义**真不一致 |
| 可访问性 | 76 处可点击元素无键盘可达性;表单无 label;图标按钮无替代文本 | 0 个 aria 属性(原生端可访问性依赖系统 TalkBack/VoiceOver) | Web 侧问题可静态证明 |
| 设计体系一致性 | **236 处硬编码色值散落在 50 个业务文件**,绕过动态主题令牌 | 81 处 / 11 文件(其中 44 处集中在 3 个登录类页面);共享组件基本走令牌 | Web 偏离显著重于移动端 |
| 文案与术语一致性 | **101 个 views 文件中 85 个完全不含 i18n**;1063 处硬编码中文 | 14 个页面 **100% 走 i18n** | 两端 i18n 纪律差距最大 |
**最高优先级 4 项(P1)**:P1-1 通知状态枚举两端语义冲突、P1-2 Web i18n 近乎未落地、P1-3 列表页无错误态/重试、P1-4 移动端把接口失败渲染成「不存在」。
**先要说清的口径修正**:任务背景描述「Web 端 … + uno.config.ts」与事实不符。`uno.config.ts` 只存在于 `frontend/app/uno.config.ts`(UnoCSS + `@wot-ui/unocss-preset`);Web 端使用的是 **Tailwind v4**(`frontend/web/src/styles/tailwind.css` 的 `@theme` 块,`package.json` 依赖 `@tailwindcss/vite` + `tailwindcss@^4.3.0`,无 unocss 依赖)。因此「设计令牌」在两端是**两套独立体系**,Web 侧令牌载体是 `@theme` + `:root`/`.dark` CSS 变量,移动端是 `presetWot` + `theme.json` + `uni.scss`。
---
## 2. 可验证问题清单
级别定义:**P1 高**(用户可见的错误信息 / 无法完成的交互 / 数据语义错误);**P2 中**(一致性或可访问性缺陷,影响部分用户或造成理解成本);**P3 低**(维护性与体验打磨)。
### 2.1 P1 — 高
#### P1-1 通知公告 `status` 枚举在 Web 端与后端/移动端语义冲突(数据被错误呈现)
- **现象**:同一后端字段 `status`,三处定义不一致:
| 来源 | 取值语义 | 证据 |
|---|---|---|
| 后端模型(权威) | `0:草稿 1:已发布 2:已归档` | `backend/app/modules/system/notice/model.py:16` |
| 移动端 | `0:草稿 1:已发布 2:已归档` | `frontend/app/src/subPages/module_system/notices/index.vue:25-27`、`:31` |
| **Web** | `0:启用 1:停用`(**缺 2**) | `frontend/web/src/views/module_system/notice/index.vue:188-191`、`:254`、`:403-404` |
- **影响**:
1. 状态为 `2`(已归档)的公告,在 Web 列表里落到 `resolveTagText` 的兜底分支,渲染为**灰色标签 + 原始数字「2」**(`frontend/web/src/components/display/fa-descriptions/index.vue:64-72`);搜索栏的「状态」下拉也只有 0/1 两项(`:226`),**无法筛选已归档公告**。
2. 状态 `0/1` 的语义在 Web 被标成「启用/停用」而非「草稿/已发布」,审核流程判断会被误导。
- **建议**:以 `backend/app/modules/system/notice/model.py` 的注释为单一真值,Web 端 `STATUS_OPTIONS`/列 map/详情 map(`:188-191`、`:254`、`:403-404`)补 `2:已归档` 并改为 `0:草稿 1:已发布 2:已归档`;两端共用一份枚举常量。**这是本轮唯一发现的数据语义级错误,建议优先核对是否已有线上影响。**
#### P1-2 Web 端 i18n 近乎未落地(切英文后业务界面仍为中文)
- **现象**:
- `frontend/web/src/views/**/*.vue` 共 **101** 个文件,其中 **85 个(84%)完全不含 `$t(` / `useI18n`**。
- `<template>` 内硬编码中文字符共 **1063 处**,分布在 **87 个 .vue 文件**(统计口径见第 9 节;已剔除 HTML 注释)。TOP 文件:`views/fastlink/tutorial/index.vue`(1114 字符)、`views/module_task/storage/browse/index.vue`(480)、`views/module_generator/gencode/components/FaGenBasicStep.vue`(324)、`components/others/fa-cron/index.vue`(215)。
- 高频硬编码位置类型:文本节点 638、`label` 201、`placeholder` 89、`title` 56、`description` 22、`content` 22。
- **对照事实(说明这是纪律问题而非能力问题)**:移动端 14 个页面 **0 个**缺 i18n;模板内硬编码中文仅 `frontend/app/src/components/PrivacyPopup.vue` 1 个文件 4 字符(且为微信小程序隐私协议默认值,属合理豁免)。两端 locale 键的 zh/en **完全对齐**(Web 509/509,移动端 331/331,无缺失键),Web 的 `locales/index.ts` 也完整可用。
- **影响**:Web 端语言切换器形同虚设;英文用户在所有业务页(用户/角色/菜单/字典/工单/存储/定时任务/代码生成…)看到中文。
- **建议**:分批治理,优先共享组件与系统模块。共享组件是放大器,先修 `frontend/web/src/components/tables/fa-table/index.vue:230`、`fa-table-header-left/index.vue`、`hooks/core/useConfirm.ts`、`hooks/core/useChart.ts:495`、`modal/fa-export-dialog/index.vue`、`modal/fa-import-dialog/index.vue`、`others/fa-cron/index.vue`。
#### P1-3 列表页系统性缺失「错误态 + 重试入口」,失败被降级为「暂无数据」
- **现象(Web)**:`hooks/core/useTable.ts` 已维护 `loadingState: "idle"|"loading"|"success"|"error"` 与 `error` ref(`:203-208`、`:463-468`),但 **16 个使用 `useTable` 的页面无一读取 `error`**(`grep -rn "error," --include="*.vue" views/` 无命中)。全仓仅 1 处渲染持久错误条:`views/module_ai/chat/components/FaChatMessages.vue:12`。页面普遍写成:
```ts
} catch {
// ignore ← frontend/web/src/views/module_system/ticket/index.vue:516-518
}
```
由于 `utils/http/index.ts` 只在拦截器里弹一次 `ElMessage.error`,用户看到的是「一个会消失的 toast + 一张写着『暂无数据』的空表」,**无法区分「本来没数据」与「加载失败」**,也没有重试按钮。
- **典型受害页面**:
- `frontend/web/src/views/module_monitor/server/index.vue:214-221`:`catch { /* 静默忽略错误 */ }`,且 `server` 保持全 0 初值 → 接口失败时页面显示 **CPU 0% / 内存 0% / 磁盘表空**,看起来像「服务器空闲」,**无任何异常指示、无重试**。
- `frontend/web/src/views/module_system/params/index.vue:433-438`:`onMounted` 内 `await configStore.getConfig()`,无加载态、无失败分支;加载期间 `fieldsMap` 为空 → 左侧 Tab 全空白,用户以为页面坏了。
- `frontend/web/src/views/module_system/ticket/index.vue:877-880`:评论加载失败仅 `// ignore`,评论区显示为空,用户以为「没有评论」。
- `frontend/web/src/views/module_ai/chat/components/FaChatInput.vue:176-178`:模型列表加载 `catch { /* 静默失败 */ }`,模型下拉静默变空。
- **现象(移动端)**:`frontend/app/src/composables/useListPage.ts:31,57` 同样暴露了 `error`,但 `subPages/module_system/tickets/index.vue:82-90` 与 `notices/index.vue:33-36` 都只写了 `onError: () => toast.error(t('common.loadFailed'))`,未消费 `error` 做 UI。
- **建议**:为 `FaTable` 增加 `error`/`@retry` 两个 prop(放在 `frontend/web/src/components/tables/fa-table/index.vue`,一次修复覆盖 23 个消费页),`FaTable` 内 `v-if="error"` 渲染 `ElResult` + 重试按钮,`empty` 槽位仅在 `!error` 时启用;移动端在 `SkeletonPage` 旁增加 `ErrorState` 组件并在 `useListPage.onError` 中置位。
#### P1-4 移动端把「接口失败」渲染为「不存在 / 空资料」
- **现象**:
- `frontend/app/src/subPages/module_system/ticket-detail/index.vue:38-46` 加载失败 → `ticket` 仍为 `null`;模板 `:244` 的 `wd-empty v-else :tip="t('ticketDetail.notFound')"` 命中的是**「工单不存在或已删除」**。网络抖动会让用户以为工单被删了。
- 同文件 `:55-60`:`loadComments` 失败只 `console.error`,**连 toast 都没有**(`finally` 也不复位任何可见状态)。
- `frontend/app/src/subPages/module_system/profile/index.vue:38-45`:失败后 `userProfile` 为 `null`,而模板 `:141` 的骨架条件 `v-if="loading && !userProfile"` 不再成立 → 直接进入 `v-else`,渲染**满屏 `-` 占位**(`deptText`/`roleText` 兜底 `'-'`,`:29-33`),无错误提示、无重试。
- `frontend/app/src/pages/index/index.vue:270-286`:`Promise.allSettled` + `catch { /* silent */ }`,失败时统计卡显示 `-`,无错误态与重试。
- **建议**:`ticket-detail` 增加 `loadError` 状态区分「失败」与「不存在」;`profile` 增加错误分支(骨架条件改为 `v-if="loading"`,`v-else-if="loadError"` 渲染错误态);`index` 增加错误态与重试。
### 2.2 P2 — 中
#### P2-1 Web `module_monitor/cache` 页面完全没有加载反馈(`:loading` 是无效属性)
- **现象**:`frontend/web/src/views/module_monitor/cache/index.vue:116` 与 `:179` 写作 `<ElTable :loading="loading">` / `:loading="subLoading"`。经核对 Element Plus 2.14.3 类型定义,`TableProps` 共 49 个 prop,**不含 `loading`**(`node_modules/element-plus/es/components/table/src/table/defaults.d.ts`;该文件中的 `loading?: boolean` 属于 `TreeNode` 接口,是树形懒加载字段,与表格加载态无关)。渲染层 `table.mjs` 也不读取 `loading`。
- **佐证**:`loading`/`subLoading` 两个 ref 被完整维护(`:321,328,346,354,371,385,409,417`)却无任何消费者,属明显的「以为设了加载态」。
- **影响**:该页两个表格在请求期间**无任何反馈**,用户会重复点击「清理全部缓存」(危险操作)。
- **建议**:改为 `v-loading="loading"`,或整体换用 `FaTable`。
#### P2-2 Web 键盘可达性:76 处可点击元素既无 `tabindex` 也无 `role`
- **现象**:全仓 `@click` 挂载在 `<div>/<span>/<li>/<td>` 上、且同一标签内无 `tabindex` / `role=` / `@key*` 的共 **76 处**(`layouts/` 31、`views/` 24、`components/` 21)。而全仓实现正确模式(`role="button"` + `tabindex="0"` + `@keydown.enter`)的仅 **3 处**:`components/tables/fa-table-header/index.vue:189-196`(已封装成 `useToolButtonAttrs`)、`views/module_system/user/components/FaDeptTree.vue:21-25`、`views/module_ai/chat/components/FaWelcomeScreen.vue:14-21`。
- **确定性样例(顶级控件,非菜单项,鼠标之外无可达路径)**:
- `frontend/web/src/components/base/fa-back-to-top/index.vue:11-17` — 返回顶部按钮(`<div @click>`)。
- `frontend/web/src/layouts/fa-settings-panel/widgets/FaSettingHeader.vue:4-8` — 设置面板关闭按钮。
- `frontend/web/src/layouts/fa-header-bar/index.vue:73-79` — 顶栏全局搜索触发器。
- `frontend/web/src/layouts/fa-header-bar/index.vue:21` — Logo 回首页。
- `frontend/web/src/components/cards/fa-image-card/index.vue:3` — 整卡可点击。
- `frontend/web/src/components/forms/fa-search-bar/index.vue:143` — 展开/收起筛选开关。
- `frontend/web/src/components/tables/fa-table-select/index.vue:11`、`components/others/fa-icon-select/index.vue:6` — 弹层触发器。
- `frontend/web/src/components/banners/fa-basic-banner/index.vue:39`、`components/banners/fa-card-banner/index.vue:13,24` — Banner 按钮。
- **对比正向样例**:`frontend/web/src/views/dashboard/home/modules/quick-links.vue:33-40` 用原生 `<button type="button">` + `:aria-label` 实现卡片删除,是本仓最规范写法。
- **建议**:把 `fa-table-header/index.vue:189-196` 的 `useToolButtonAttrs` 提升为 `hooks/core/useClickable.ts` 并全仓替换;或对纯装饰容器改用原生 `<button>` / `ElButton link`。
#### P2-3 Web 登录/注册/忘记密码表单没有 label(输入框无可访问名)
- **现象**:`ElFormItem` 只写了 `prop`,**没有 `label` 也没有 `#label` 插槽**,可访问名只能依赖 `placeholder`(输入后即消失,且不是 label 的合法替代):
- `frontend/web/src/views/module_system/auth/login/components/forms/FaLoginAccountForm.vue:31`(username)、`:45`(password)
- `frontend/web/src/views/module_system/auth/login/components/panels/FaLoginRegisterPanel.vue:12,24,37,55`
- `frontend/web/src/views/module_system/auth/login/components/panels/FaLoginForgetPanel.vue:12`
- 同类还有 `views/module_generator/gencode/components/FaGenBasicStep.vue:107,128,139,157,200,219`、`views/module_system/role/components/FaPermissonDrawer.vue:35`、`components/others/fa-comment-widget/index.vue:4,12,21`、`components/modal/fa-import-dialog/index.vue:21`。
- **全仓统计**:`ElFormItem` 共 70 处,**27 处无 `label`**。
- **对照**:移动端统一使用 `wd-form-item :label="t(...)"`(如 `frontend/app/src/subPages/module_system/tickets/index.vue:259-279`),label 覆盖完整——**这是移动端做得比 Web 好的一处**。
- **建议**:登录/注册/忘记密码这类「极简设计」场景用 `label` 配 `label-position="top"` + `sr-only` 类,或至少补 `:aria-label`;`ElTooltip` 的 `content`(`:44`)不能替代表单 label。
#### P2-4 Web 图标按钮缺可访问名(含同一区域内不一致)
- **现象**:`ElButton` 内容仅为图标、且自身无 `aria-label`/`title` 的共 **12 处**(其中若干被 `el-tooltip` 包裹,见下方说明):
- `frontend/web/src/views/module_ai/chat/components/FaChatInput.vue:99-107` — **发送**按钮无任何名称;而紧邻的 `:88-95` 停止按钮写了 `title="停止生成"`。**同一行内的不自洽是最硬的证据。**
- `frontend/web/src/views/module_ai/chat/components/FaAiModelConfigPanel.vue:125`(显示/隐藏 API Key)、`:132`(复制 Key)— 无 tooltip、无 aria-label。
- `frontend/web/src/views/module_ai/chat/components/FaMessageItem.vue:16-23` — 复制消息按钮。
- `frontend/web/src/components/actions/fa-copy-button/index.vue:3` — 通用复制组件(被复用,影响面放大)。
- `frontend/web/src/views/module_task/storage/workflow/components/FaStorageNodePanel.vue:7`、`FaEdgeConfigPanel.vue:7` — 面板关闭按钮。
- `frontend/web/src/components/others/fa-ai-assistant/index.vue:5` — 悬浮球。
- **说明(避免误判)**:`views/module_task/storage/browse/index.vue:41,46,113`、`FaAiModelConfigPanel.vue:92` 等同类按钮**已被 `el-tooltip` 包裹**,EP 会为触发器生成 `aria-describedby`;但「描述」不等于「名称」,屏幕阅读器读到的是空按钮 + 描述。是否可接受需人工验证(见第 8 节)。
- **建议**:统一补 `aria-label`;`fa-copy-button` 加 `ariaLabel` prop 并默认取 `t('common.copy')`。
#### P2-5 移动端错误文案未国际化(英文语言下全是中文)
- **现象**:`frontend/app/src/http/tools/enum.ts:53-80` 的 `ShowMessage()` 对 400/401/403/404/408/500/501/502/503… 全部返回**硬编码中文**(`'请求错误(400)'`、`'未授权,请重新登录(401)'`…);`frontend/app/src/http/adapters/alova.ts:19`(`` `HTTP请求错误[${statusCode}]` ``)、`:142`、`:165`、`:176`(`'网络错误,请检查您的网络连接'`)、`:178`(`'请求超时,请重试'`)、`:180`、`:189` 同样硬编码。
- **影响**:移动端虽然页面文案 100% i18n,但**所有网络/服务端错误的提示语仍是中文**——这是最容易被漏掉的一层(它在 `.ts` 里,不在 `<template>` 里,常规 i18n 检查扫不到)。
- **Web 侧同类**:`frontend/web/src/utils/http/index.ts:273`(`"服务器连接失败,请检查后端服务是否正常运行"`)、`:277`(`"网络连接错误,请检查您的网络设置"`)为硬编码中文;其余分支已正确使用 `$t("httpMsg.*")`,且 `httpMsg` 键在 zh/en 中均存在。
- **建议**:两端 HTTP 层的错误文案统一改走 i18n(`httpMsg.status.400` 等),移动端补 10 个 `httpMsg` 键。
#### P2-6 移动端危险操作确认机制不统一(原生 `uni.showModal` vs wot `wd-dialog`)
- **现象**:同一类「确认后执行破坏性操作」,两种实现并存:
- 原生 `uni.showModal`(不可主题化;H5 下退化为浏览器原生 dialog,样式与 App 内其他弹窗割裂):`frontend/app/src/subPages/module_system/tickets/index.vue:154`、`subPages/module_ai/chat/index.vue:107`、`subPages/module_ai/ai-models/index.vue:106,123`、`composables/useSubscribeMessage.ts:116`、`components/GlobalMessage.vue:13`。
- wot `wd-dialog`(经 `useGlobalDialog`,与设计体系一致):`frontend/app/src/subPages/setting/index.vue:93`、`pages/mine/index.vue:38`。
- **建议**:统一收敛到 `globalDialog.confirm`,`useSubscribeMessage.ts` 的系统级订阅弹窗可保留原生。
#### P2-7 空态无法区分「筛选无结果」与「本来为空」
- **现象**:
- Web:`FaTable`(`frontend/web/src/components/tables/fa-table/index.vue:230`)、`FaCardList`(`components/cards/fa-card-list/index.vue:51`)、`FaCardGrid`(`components/cards/fa-card-grid/index.vue:141`)、图表(`hooks/core/useChart.ts:495`)的空态文案**固定为「暂无数据」**,无「筛选无结果」变体。除 `views/module_system/ticket/index.vue:53` 传了 `empty-text="暂无工单"` 外,无页面覆盖。
- Web locale(509 键)中**不存在任何通用空态键**(`empty`/`noData` 类仅命中 `notice.empty`、`common.delNoData`、`httpMsg.notFound`)。移动端有 **10 个**专用空态键并已区分两种语义(`common.noMore`、`tickets.empty`/`tickets.emptyWithFilter`、`notices.empty`/`emptyWithFilter`、`chat.empty`、`aiModels.empty`、`ticketDetail.emptyComments`、`ticketDetail.notFound`、`work.emptyTip`),实现见 `frontend/app/src/subPages/module_system/tickets/index.vue:215`。
- **建议**:Web 补 `table.empty` / `table.emptyFiltered`,`FaTable` 接收 `hasFilter` 或 `filtered` 标记后切换文案。
#### P2-8 同一后端枚举的**状态色**两端不一致;「启用/停用」词形也不一致
- **现象(颜色,标签文字一致、颜色不一致)**:工单 `status`,Web 用 `statusTagType`(`frontend/web/src/views/module_system/ticket/index.vue:608-613`):`0→warning(橙)`、`1→info(灰)`、`2→success(绿)`、`3→info(灰)`;移动端用 `StatusBadge`(`frontend/app/src/components/StatusBadge.vue:33-36`):`0 pending→draft(灰)`、`1 processing→primary(蓝)`、`2 completed→enabled(绿)`、`3 closed→disabled(灰)`。即**「处理中」在 Web 是灰、在移动端是蓝;「待处理」在 Web 是橙、在移动端是灰**。
- **现象(词形)**:Web 系统模块统一用「**启用/停用**」(`views/module_system/user/index.vue:457-458`、`role/index.vue:318-319`、`position/index.vue:224-225` 等,共 73 处字面量),移动端 `common.status.disabled` = 「**禁用**」(`frontend/app/src/locales/langs/zh.json`)。
- **现象(术语,同一实体三种叫法)**:同一条公告,Web 业务页叫「**公告通知**」(`views/module_system/notice/index.vue:275-278`、`:304`、`:203`),Web 顶栏通知区叫「**通知**」(locale `notice.title`),移动端叫「**通知公告**」(locale `notices.title`、`pages.json:152`)。
- **建议**:以「后端 comment 文字」为唯一词表来源,抽 `constants/status.ts` 供两端共用;状态色按语义(进行中=主色蓝、待处理=警示橙、完成=成功绿、终止=中性灰)统一。
#### P2-9 移动端主题令牌存在四份互相冲突的定义
- **现象**:同一份「主题色 → 背景色」值被声明了 4 次,且暗色值互相矛盾:
| 位置 | 内容 | 证据 |
|---|---|---|
| SCSS 变量层 | `--drop-base-a/-c` 等 6 主题色 | `frontend/app/src/styles/theme.scss:16-21,45-49,65-69,85-89,105-109,125-129` |
| Pinia store | `THEME_NAV_BG`(同 6 值) | `frontend/app/src/store/themeStore.ts:13-20` |
| 根组件 | `THEME_PAGE_BG`(同 6 值) | `frontend/app/src/App.ku.vue:9-14` |
| 原生主题文件 | `theme.json`(仅 blue + dark) | `frontend/app/src/theme.json`(经 `manifest.config.ts:74,93` 的 `themeLocation` 生效) |
逐值核对:`themeStore.THEME_NAV_BG` 的 6 个值与 `theme.scss --drop-base-a` **完全一致**;`App.ku.THEME_PAGE_BG` 的 6 个值与 `--drop-base-c` **完全一致**。
- **冲突点**:`theme.json` 的 `dark.bgColor = #000`、`dark.navBgColor = #000000`,而运行时实际使用的是 `#272B3B`(`store/themeStore.ts:76`、`App.ku.vue:23`;该值与 wot 令牌 `--wot-coolgrey-9: #272B3BFF` 一致,见 `node_modules/@wot-ui/ui/styles/theme/base/color.scss:160`、`theme/dark.scss:60`)。
- **影响**:深色模式下原生窗口背景(由 `theme.json` 决定)与 App 页面背景(`#272B3B`)不是同一个颜色 → 冷启动/下拉回弹时可能出现可见色差或闪烁。(静态可证「值不一致」;**视觉表现需真机确认**,见第 8 节。)
- **建议**:`theme.scss` 作为唯一真值源,`themeStore`/`App.ku.vue` 改读 CSS 变量或从同一常量模块导入;`theme.json` 的 dark 值改为与 `--wot-filled-content` 对齐。
#### P2-10 Web 组件与视图大量硬编码 Element Plus 默认调色板,绕过动态主题色
- **现象**:`#409eff`(EP 默认主色,非项目 `--theme-color`/`$fa-primary:#4080ff`)出现 6 次;EP 默认语义色 `#909399`(16)、`#e6a23c`(5)、`#f56c6c`(1)、`#c0c4cc`(4)、`#67c23a`(1)、`#606266`、`#f5f7fa` 等贯穿业务代码。项目明明提供了 `--el-text-color-*`/`--el-color-*`/`--fa-gray-*` 令牌(`frontend/web/src/styles/tailwind.css:215-232`)。
- **暗色模式受害样例**(这些浅色专用灰**不会**随 `.dark` 切换,属可静态证明的对比度风险):`frontend/web/src/views/module_task/storage/browse/index.vue:88,213,233,260,275,284,289,348,433,444,471,476,492,514,1685`;`views/module_generator/gencode/components/FaGenPreviewStep.vue:16,22`;`views/module_task/storage/workflow/index.vue:99`。
- **完整分布统计见第 6 节。**
#### P2-11 Web 字号/圆角绕过令牌(含不受用户设置影响的固定圆角)
- **现象**:
- Tailwind `@theme` 已定义字号 5 档 `--font-size-xs..xl` = 11/12/13/15/18px(`frontend/web/src/styles/tailwind.css:56-62`),但仍有 **33 处任意值 class**:`text-[13px]`×15、`text-[10px]`×7、`text-[14px]`×6、`text-[11px]`×6、`text-[19px]`×3、`text-[16px]`×2、`text-[18px]`、`text-[12px]`;`<style>` 内另有 `font-size: 12px`×32、`14px`×30、`13px`×25、`16px`×13、`11px`×7。
- 项目提供了 `rounded-custom-xs/sm`(`tailwind.css:66-72`)与 `--custom-radius`(用户在设置面板可调,影响 20+ 处 calc 表达式),但仍有 **17 处 `border-radius: 6px`、14 处 `8px`、13 处 `4px`** 固定值 → **用户调圆角时这些元素不变**。样例:`components/skeleton/fa-dashboard-skeleton/index.vue:229`、`layouts/fa-work-tab/index.vue:909,970`、`layouts/fa-menus/fa-sidebar-menu/index.vue:633`、`views/module_ai/chat/components/FaMessageItem.vue:138,150,201`、`views/module_task/storage/workflow/components/FaWorkflowDesignDrawer.vue:901`。
- 间距硬编码(`<style>` 内 `padding/margin/gap: Npx`,共 231 处):`8px`×61、`12px`×31、`16px`×28、`4px`×19、`6px`×18、`10px`×17。
- **建议**:`@theme` 补 `--font-size-md:14px` 等实际在用档位;`rounded-6/8` 等封装为走 `--custom-radius` 的工具类;对 `text-[Npx]` 用 stylelint 自定义规则(`declaration-property-value-disallowed-list` 已有 stylelint 基建,`frontend/web/package.json` 的 `lint:stylelint`)拦截新增。
#### P2-12 文档站 zh/en 术语与正文范围不对齐
- **现象**:VitePress 侧边栏同一份文档,中文标签为「**移动端开发指南**」,英文为「**Miniprogram Development Guide**」(`frontend/docs/.vitepress/config.mts` 的 `zhGuideSidebar` / `enGuideSidebar`);正文页标题同样中英不等(`frontend/docs/src/guide/miniprogram.md:4` = 「FastApp 移动端开发指南」 vs `frontend/docs/src/en/guide/miniprogram.md:4` = 「Miniprogram Development Guide」),而正文同时覆盖 **H5 / 微信·支付宝小程序 / iOS·Android**。英文标签把 App 端排除了。
- **正向事实**:docs 中英文件数 1:1(`guide/` 与 `en/guide/` 均为 9 个 .md),无缺页。
- **建议**:英文标签改为 "Mobile Development Guide",与 App 端 `README` 中 "mobile application" 的用词对齐。
### 2.3 P3 — 低
| ID | 现象 | 文件:行号 | 建议 |
|---|---|---|---|
| P3-1 | `useConfirm` 的确认框标题/按钮文案硬编码中文,绕过 locales | `frontend/web/src/hooks/core/useConfirm.ts:8,17,30,38` | 改走 `t()`;`common.confirm/cancel/tips` 键已存在 |
| P3-2 | 破坏性操作确认框标题词表不统一(「警告」「提示」「批量删除」「危险!」「确认清空」「同步差异预览」),且部分走 `t()` 部分硬编码 | `useConfirm.ts:9,21,30`、`views/module_task/storage/browse/index.vue:1322`、`views/module_monitor/cache/index.vue:391`、`views/module_system/ticket/index.vue:799`、`views/module_ai/chat/index.vue:328` | 统一 `useConfirm` 出口与标题键(`type` 已统一为 `warning`,无需改) |
| P3-3 | 空态「暂无数据」硬编码在 10 个位置(含 3 个共享组件),无法随语言切换 | `components/tables/fa-table/index.vue:230`、`components/cards/fa-card-list/index.vue:51`、`fa-card-grid/index.vue:141`、`hooks/core/useChart.ts:495`、`views/module_monitor/cache/index.vue:118,185`、`views/module_monitor/server/index.vue:163`、`views/module_generator/gencode/components/FaGenColumnsStep.vue:87`、`FaImportDbTableDialog.vue:49`、`views/module_system/user/components/FaDeptTree.vue:47`、`views/module_task/cronjob/job/index.vue:151` | 随 P2-7 一并治理 |
| P3-4 | `fa-table-v2` 为死代码:53 行、无任何消费方(`grep -rn "FaTableV2\|fa-table-v2"` 在组件目录外 0 命中),且未实现 loading/empty | `frontend/web/src/components/tables/fa-table-v2/index.vue` | 删除或补齐状态并与 `FaTable` 收敛 |
| P3-5 | 移动端 `GlobalLoading.vue`、`GlobalMessage.vue` 实际未用于任何页面(仅其自身 composable 引用,`grep` `useGlobalLoading(`/`useGlobalMessage(` 无业务命中) | `frontend/app/src/components/GlobalLoading.vue`、`GlobalMessage.vue` | 确认后删除,或接入使用 |
| P3-6 | 死样式:`.el-dropdown-selfdefine` 是 Element Plus 遗留类,EP 2.14.3 的 JS 在 `node_modules/element-plus/es/**` 中**不产出**该 class(仅 theme-chalk CSS 残留),本条覆盖规则永不生效 | `frontend/web/src/styles/element-plus/_overrides.scss:273-275` | 删除;焦点可见性依赖 EP 自身实现(`:465-469` 已有 `.el-tooltip__trigger:focus-visible` 正例) |
| P3-7 | 移动端工单页把表单提交 loading 与列表 loading 复用同一个 ref | `frontend/app/src/subPages/module_system/tickets/index.vue:135,151`(配合 `:212`、`:245`、`:285`) | 用独立 `submitting` ref(同仓 `ai-models/index.vue:226` 已是正确写法) |
| P3-8 | 移动端 `chat` 页内联样式 12 处硬编码 rpx/px,与同仓其它页面的 UnoCSS/wot 令牌写法不一致 | `frontend/app/src/subPages/module_ai/chat/index.vue:207,218,247,252,275,276,283` 等 | 抽成 UnoCSS class 或 `styles/` 下的具名类 |
| P3-9 | 移动端页面导航标题中文常量与 i18n 双真值源:`pages.json` 17 处 + `definePage` 13 处为中文常量,靠 `useI18nNavTitle` 在 `onShow` 覆盖 | `frontend/app/src/pages.json:23,34,45,54,63,72,80,88,102,113,123,134,143,152,161,170`;`composables/useI18nNavTitle.ts` | 覆盖率已 100%(14/14 有标题的页面均已接入),仅存在首屏闪中文的可能 → 见第 8 节 |
| P3-10 | Web `dashboard/analysis`、`dashboard/workplace` 两个首页级页面**零**加载/骨架/错误处理(101 个 views 文件中仅这两处完全没有 `loading`/`Skeleton`/`ElEmpty`) | `frontend/web/src/views/dashboard/analysis/index.vue`、`views/dashboard/workplace/index.vue` | 复用 `FaDashboardSkeleton`(`views/dashboard/home/index.vue:3` 已有正例) |
| P3-11 | 系统模块「启用/停用」字面量在同一页面重复 3~4 次(`columns` 的 tag map、详情 map、搜索项、编辑项各写一遍),共 73 处 | `views/module_system/user/index.vue:395,457,491,623`;`role/index.vue:318,390,514,584`;`position/index.vue:224,295,371,473`;`notice/index.vue:189,254,403` | 抽 `constants/status.ts` 共享 map(同时解决 P2-8) |
| P3-12 | Web `<img>` 无 `alt` 5 处(24 个图片标签中 19 个有 alt) | `components/cards/fa-image-card/index.vue:6`、`actions/fa-upload/index.vue:47`、`layouts/fa-settings-panel/widgets/FaThemeSettings.vue:11`、`FaMenuStyleSettings.vue:17`、`FaMenuLayoutSettings.vue:12` | 先判定是否纯装饰;装饰图加 `alt=""`,其余补描述性 alt |
| P3-13 | 移动端 5 个 `<image>` 均无 `alt`(H5 构建下会渲染为无替代文本的 `<img>`) | `frontend/app/src/pages/login/index.vue:248,328,336,344,352` | 品牌 logo 加 `alt`;第三方图标可 `alt=""`(装饰) |
---
## 3. 状态设计完整性(逐页抽样)
图例:✅ 有 / ❌ 缺 / ➖ 不适用(非数据页)。「空态」对 Web 指 `FaTable`/`ElEmpty`(组件内建也算 ✅)。
### 3.1 Web 抽样(23 个页面使用 `FaTable`,16 个使用 `useTable`)
| 页面 | 加载/骨架 | 空态 | **错误态** | **重试** | 危险操作二次确认 | 关键行号 |
|---|---|---|---|---|---|---|
| `views/module_system/user/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ `confirmDelete`/`confirmBatchDelete` | `:554,915-922` |
| `views/module_system/role/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:363,657` |
| `views/module_system/menu/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:825,1281` |
| `views/module_system/dept/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:272,526` |
| `views/module_system/dict/index.vue` (+`DictDataPanel`) | ✅ | ✅ | ❌ | ❌ | ✅ | `:440`、`DictDataPanel.vue:630` |
| `views/module_system/notice/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:501,556` |
| `views/module_system/position/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:514,528` |
| `views/module_system/log/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:415,459,669,713` |
| `views/module_system/version/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:490` |
| `views/module_system/ticket/index.vue` | ✅ | ✅ `暂无工单` | ❌ | ❌ | ✅ | 列表 `:516-518`;评论 `:877-880` ❌ |
| `views/module_example/demo/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:636,649` |
| `views/module_ai/memory/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:118,222` |
| `views/module_monitor/online/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ `强退所有` | `:123,256` |
| **`views/module_monitor/cache/index.vue`** | **❌ 无效 prop** | ✅ | ❌ | ⚠️ 仅 toast | ✅ | `:116,179`(见 P2-1) |
| **`views/module_monitor/server/index.vue`** | **❌** | ✅ 磁盘表 | **❌ 静默** | ❌ | ➖ | `:214-221` |
| **`views/module_system/params/index.vue`** | **❌** | ➖ | **❌** | ❌ | ➖(保存无确认,但有「已修改」标记) | `:433-438` |
| `views/module_task/cronjob/job/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ 含「关闭调度器」 | `:744,1005,1017,1085` |
| `views/module_task/cronjob/node/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:382,491` |
| `views/module_task/storage/node/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:445,962` |
| `views/module_task/storage/transfer/index.vue` | ✅ | ✅ | ⚠️ 空态文案含「加载失败」 | ❌ | ✅ | `:851,861`(取消/删除均有确认) |
| `views/module_task/storage/browse/index.vue` | ✅ | ✅ 区分「加载失败」/「该目录下没有内容」 | ⚠️ 文案级 | ❌ | ✅ | `:254`、`confirmAction:1320-1331`、`deleteRow:1333`、`batchDelete:1349` |
| `views/module_task/storage/workflow/index.vue` | ✅ | ✅ | ❌ | ❌ | ✅ | `:401,784`(含删除节点/连线确认) |
| `views/module_generator/gencode/index.vue` | ✅ | ✅ | ✅ **全仓唯一** | ✅ 保留了重试路径 | ✅ | `:667-668` |
| `views/dashboard/analysis/index.vue` / `workplace/index.vue` | ❌ | 仅图表内建空态 | ❌ | ❌ | ➖ | 见 P3-10 |
**Web 状态设计小结**:加载态 21/23 ✅、空态 22/23 ✅、**错误态 1/23 ✅**、**重试 1/23 ✅**、危险操作确认 100% ✅(含批量/关闭调度器/清理全部缓存/强退所有/删除工作流节点与连线)。
**Web 的「二次确认」是全仓质量最高的一环**,`hooks/core/useConfirm.ts` + `ElPopconfirm` 双轨覆盖到位;短板完全集中在错误态与重试。
### 3.2 移动端抽样
| 页面 | 骨架 | 空态 | **错误态** | **重试** | 二次确认 | 下拉刷新 | 触底加载 |
|---|---|---|---|---|---|---|---|
| `subPages/module_system/tickets/index.vue` | ✅ `:212` | ✅ 区分筛选 `:215` | ❌ 仅 toast `:89` | ❌ | ✅ `uni.showModal:154`(机制见 P2-6) | ✅ `:174` | ✅ `:170` |
| `subPages/module_system/notices/index.vue` | ✅ `:138` | ✅ 区分筛选 `:141` | ❌ 仅 toast `:35` | ❌ | ✅ `:101` | ✅ | ✅ `:163` |
| `subPages/module_system/ticket-detail/index.vue` | ✅ `:145` | ✅ `:220,244` | **❌ 失败→误显示「不存在」`:244`** | ❌ | ➖ | ✅ | 手动分页 `:237` |
| `subPages/module_system/profile/index.vue` | ⚠️ 仅首次 `:141` | **❌ 失败→满屏 `-`** | ❌ `:38-45` | ❌ | ➖ | ✅ | ➖ |
| `subPages/module_ai/ai-models/index.vue` | ✅ `:150` | ✅ `:153` | ❌ 仅 toast `:52` | ❌ | ✅ `uni.showModal:106,123` | ✅ | ➖(一次全量) |
| `subPages/module_ai/chat/index.vue` | ✅ `:226` | ✅ `:234` | ❌ 仅 toast `:67,101` | ❌ | ✅ `:107` | ➖ | ✅ |
| `pages/index/index.vue` | ✅ `:431` | ➖(`-` 占位) | ❌ 静默 `:270-286` | ❌ | ➖ | ✅ | ➖ |
| `subPages/setting/index.vue` | ➖ | ➖ | ➖ | ➖ | ✅ `wd-dialog:93` | ➖ | ➖ |
| `pages/mine/index.vue` | ✅ | ➖ | ➖ | ➖ | ✅ `:38`(退出登录) | ✅ | ➖ |
| `pages/work/index.vue` | ➖ | ✅ `:127` | ➖ | ➖ | ➖ | ➖ | ➖ |
| `pages/login/*`、`pages/login/register`、`pages/login/forget` | ➖ | ➖ | ✅ 滑块失败重置+toast `login/index.vue:141,147` | ⚠️ 需手动重滑 | ✅ 注册协议勾选 | ➖ | ➖ |
**移动端状态设计小结**:骨架/空态/下拉刷新/触底加载/表单校验覆盖良好,**错误态与重试同样是系统性缺口**,且比 Web 更严重的是**错误被渲染成「不存在」**(P1-4)。
### 3.3 表单校验提示(两端均 ✅)
- Web:`FaForm` + `rules`,例如 `views/module_system/user/index.vue:130`(`:rules="rules"`)配合 `:736`(`const rules = reactive({...})`);`ElForm` 上挂 `@keyup.enter` 提交(`FaLoginAccountForm.vue:11`)。
- 移动端:`wd-form :schema` / `wd-form-item` + 手动校验,例如 `subPages/module_system/tickets/index.vue:131-134`(标题必填 toast)、`pages/mine/index.vue:98-102`(两次密码一致性)。
- 差异:Web 依赖 `trigger: "blur"` 的字段级即时提示;移动端多为**提交时**集中校验(如工单标题)。属平台习惯差异,**不算缺陷**,但同一表单两端提示时机不同,需在验收标准中明确。
---
## 4. Web 与移动端交互一致性(同操作对比)
| 操作 | Web | 移动端 | 判定 |
|---|---|---|---|
| 列表分页 | 页码分页 `FaPagination`(`components/tables/fa-pagination/index.vue:5`),23 个页面复用 | 触底无限加载 `useListPage.loadNext()` + `onReachBottom`(`tickets/index.vue:170-173`) | **合理差异**(平台习惯),但两端都需保证「到底提示」:移动端有 `common.noMore`(`:246`),Web 无等价提示 |
| 列表刷新 | `FaTableHeader` 刷新按钮(`refreshData`)+ 列显隐 | 下拉刷新 `enablePullDownRefresh` + `onPullDownRefresh` | **合理差异**;移动端未提供显式刷新按钮,Web 未提供下拉刷新(Web 有 `Ctrl+Enter` 搜索,见下) |
| 搜索 | `FaSearchBar` + 展开/收起 + 重置 + **Ctrl/Cmd+Enter 快捷搜索**(`components/forms/fa-search-bar/index.vue:199-205`,提示文案 `table.searchBar.searchTooltip`) | `wd-search` + 筛选 Tab + 下拉类型筛选(`tickets/index.vue:188-209`) | 能力对齐,`@clear="onReset"` 已覆盖清空。**建议**:移动端 `onReset`(`:108-112`)重置了关键词与类型但**保留状态 Tab**,Web 的「重置查询」会清空全部筛选项(`table.searchBar.resetTooltip`=「清除所有筛选条件」)→ **语义不一致,建议统一** |
| 批量操作 | ✅ 多选 + 批量删除(`useTableSelection` + `confirmBatchDelete`),11 个页面具备 | ❌ 仅左滑单条删除(`tickets/index.vue:125-129`) | **能力缺口**(移动端通常不需要,但需在需求文档中显式声明为「不支持」而非遗漏) |
| 导入 / 导出 | ✅ `FaImportDialog` / `FaExportDialog` / `FaExcelExport` / `FaExcelImport`,按权限 `perm-export` 控制(`user/index.vue:52-53`) | ❌ 无 | **能力缺口**,建议在 `REQUIREMENTS.md` 中登记 |
| 新增/编辑 | `FaDrawer` 抽屉 + `FaForm` + `:confirm-loading` 防重复提交(`user/index.vue:85-95`) | `wd-popup` 底部弹窗 + 手动 `submitting`/`loading`(注意 P3-7) | 形式差异合理;**移动端提交中的防重复覆盖不全** |
| 危险操作确认 | `ElMessageBox.confirm`(统一 `type: warning`)+ `ElPopconfirm` | 混用 `uni.showModal` 与 `wd-dialog` | **不一致 → P2-6** |
| 退出登录 | `ElMessageBox.confirm` + i18n(`layouts/fa-header-bar/widgets/FaUserMenu.vue:151`) | `globalDialog.confirm` + i18n(`pages/mine/index.vue:38-44`) | ✅ 一致(且都用 i18n) |
| 工单状态展示 | `FaStatusTag` 颜色映射 | `StatusBadge` 颜色映射 | **颜色语义不一致 → P2-8** |
| 状态切换(启用/停用) | `handleMoreClick("enable"/"disable")` → `status/batch` | 编辑弹窗内 `wd-radio-group` 单选 | 交互路径不同但结果一致;**词形不一致(停用 vs 禁用)→ P2-8** |
| 空态文案 | 固定「暂无数据」,不区分筛选 | 区分 `empty` / `emptyWithFilter` | **不一致 → P2-7** |
---
## 5. 可访问性(静态证据 + 需人工项)
### 5.1 可用静态代码证明(计入缺陷)
| 项 | 证据 | 级别 |
|---|---|---|
| 无 label 的表单控件 | Web `ElFormItem` 70 处中 **27 处无 `label`**(第 2.3 节 P2-3);移动端 `wd-form-item` label 覆盖完整 | P2 |
| 图标按钮无可访问名 | Web **12 处**(P2-4);移动端 `wd-icon` 均配可见文字或为装饰 | P2 |
| 自定义可点击元素键盘不可达 | Web **76 处** `@click` 的 div/span/li/td 无 `tabindex`/`role=`/`@key*`,仅 3 处实现正确模式(P2-2) | P2 |
| ARIA 使用量 | Web 全仓:`aria-label` ×7、`aria-hidden` ×5、**`aria-live` ×0、`aria-describedby` ×0、`aria-expanded` ×0**;移动端 `pages/subPages/layouts/components` 下 **aria-\* = 0** | P2 |
| 图片替代文本 | Web 24 个图片标签中 5 个无 `alt`(P3-12);移动端 5 个 `<image>` 均无 `alt`(P3-13) | P3 |
| 焦点可见性 | Web 有 10 个文件定义了 `:focus-visible`/`:focus` 样式(`styles/element-plus/_overrides.scss:470-474`、`layouts/fa-work-tab/index.vue:818-823`、`views/module_ai/chat/components/FaWelcomeScreen.vue:110-113` 等),并有 2 处 `outline: none` 但**均提供了替代视觉反馈**(`styles/pages/_login.scss:411` + `:419-422` 的 focus 边框;`fa-work-tab/index.vue:819` + `:820` 的 inset 阴影)→ **未发现「移除焦点但无替代」的缺陷** | 通过 |
| 快捷键 | `Ctrl/Cmd+Enter` 提交搜索已实现并配提示文案 | 通过 |
| 语义化结构 | `layouts/index.vue:15,20` 使用 `<aside aria-label>` / `<main aria-label>`;`layouts/fa-breadcrumb/index.vue:3` 用 `<nav aria-label>` | 通过 |
### 5.2 不能用静态代码判断(**不得计为缺陷**,验证步骤见第 8 节)
- 对比度(需渲染后取实际前景/背景,尤其暗色模式下 P2-10 的硬编码灰色)。
- 屏幕阅读器实际朗读(`el-tooltip` 包裹的图标按钮是否可被读出名称;`ElMessage` toast 是否有 `aria-live` 通告——源码里没有,但 EP 内部实现需实测确认)。
- 触摸目标尺寸(移动端,需真机量取;仅能确认存在 `size="small"` 与 `w-4 h-4` 等小尺寸类,如 `views/dashboard/home/modules/quick-links.vue:35` 的 16px 删除按钮)。
- 键盘 Tab 顺序与焦点陷阱(`ElDialog`/`wd-popup` 内)。
- 移动端 H5 构建下的键盘可达性与小程序端 `aria-role` 支持。
---
## 6. 设计令牌偏离统计
### 6.1 令牌体系现状(三套并存)
| 端 | 令牌载体 | 说明 |
|---|---|---|
| Web | ① `src/styles/tailwind.css` 的 `@theme`(字号 5 档 + `--color-primary/g-100..900` 等映射到 `--fa-*`);② 同文件 `:root` / `.dark` 的 `--fa-*`、`--default-*`、`--app-bg`(约 **45 个** CSS 变量);③ `src/styles/core/_fa-tokens.scss`(SCSS 侧 6 个功能色,文件头注释明确声明「整个项目只有这一处定义 success/warning/danger/error/info 的 hex 值」);④ `--custom-radius`(用户可调) | 设计意图清晰,但 `_fa-tokens.scss:4` 的「单一来源」约定**已被违反**(见 6.3) |
| 移动端 | ① `presetWot({ baseTokens: true })`(`uno.config.ts:27-29`)产出的 `--wot-*`;② `theme.json`(uni 原生暗色);③ `uni.scss`(uni 内置 SCSS 变量);④ 本项目自建 `--drop-*`(`styles/theme.scss`);⑤ `composables/types/theme.ts` 的 `primaryShades` | 四份主题色相关定义重复(P2-9) |
| 文档站 | VitePress 默认主题,无共享令牌 | — |
### 6.2 硬编码色值统计
**Web**(`frontend/web/src`,`regex #hex{3,8}`):
| 分区 | 出现次数 | 文件数 |
|---|---|---|
| `src/styles/**`(设计体系层,**合理**) | 64 | 8 |
| `views/module_task` | 38 | 12 |
| `views/dashboard` | 37 | 9 |
| `components/`(charts 27 + 其它 38) | 65 | 17 |
| `views/module_system` | 20 | 6 |
| `config/` + `hooks/` + `router/` | 66 | 5 |
| `views/fastlink` / `module_generator` / `layouts` | 10 | 4 |
| **合计** | **300**(其中 `src/styles/**` 外 **236**) | 58 |
**`src/styles/**` 之外的 TOP 文件**:`config/index.ts` 31、`hooks/core/useChart.ts` 21、`views/module_task/storage/browse/index.vue` 19、`views/dashboard/home/modules/banner.vue` 13、`components/charts/fa-map-chart/index.vue` 13、`views/module_system/auth/login/components/backdrops/FaLoginLeftView.vue` 10、`views/module_task/storage/workflow/components/protocol.ts` 9、`config/modules/fastEnter.ts` 8、`components/forms/fa-drag-verify/index.vue` 8。
**高频值与分布**(Web 全仓):
| 值 | 次数 | 性质 | 样例 |
|---|---|---|---|
| `#fff`/`#ffffff` | 27+3 | 中性(多数合理) | `styles/**` |
| `#909399` | 16 | **EP 默认 info 灰**(绕过 `--el-text-color-secondary`) | `views/module_task/storage/browse/index.vue:88,213,233,284,289,433,444,471,476,492,514`、`views/module_generator/gencode/components/FaGenPreviewStep.vue:16,22`、`views/module_task/storage/workflow/index.vue:99` |
| `#333` | 8 | 浅色专用文本色(暗色下不可读) | `components/banners/fa-basic-banner/index.vue:156,173`、`components/charts/fa-map-chart/index.vue:188`、`views/dashboard/home/modules/banner.vue:62`、`views/dashboard/home/modules/it_banners.vue:53`、`components/forms/fa-drag-verify/index.vue:91` |
| `#409eff` | 6 | **EP 默认主色**(应为 `--el-color-primary`/`--theme-color`) | `views/fastlink/pricing/index.vue:348,377,385`、`views/module_task/storage/workflow/components/FaEdgeConfigPanel.vue:73,147`、`views/module_task/storage/workflow/components/protocol.ts:7-9` |
| `#e6a23c` | 5 | EP 默认 warning(应为 `--el-color-warning`) | `views/module_task/storage/browse/index.vue:221,260,503`、`protocol.ts:10-13`、`FaFileBrowserDialog.vue:58` |
| `#c0c4cc` | 4 | EP 默认 disabled 文本 | `views/module_task/storage/browse/index.vue:275,1685`、`FaDynamicEdge.vue:58`、`FaWorkflowDesignDrawer.vue:654` |
| `#67c23a` | 1 | EP 默认 success | `components/cards/fa-progress-card/index.vue:56` |
| `#f56c6c` | 1 | EP 默认 danger | `views/module_system/auth/login/components/forms/FaLoginAccountForm.vue:81` |
| `#606266` | 1 | EP 默认 regular 文本 | `views/module_task/storage/browse/index.vue:348` |
| `#f5f7fa` | 1 | EP 默认填充色 | `components/cards/fa-image-card/index.vue:12` |
> **判定**:`_fa-tokens.scss:1-4` 声明「整个项目只有这一处定义功能性色 hex」,但上述 EP 默认语义色在**业务代码**中被反复硬编码,**共 51 处**(`#909399`16 + `#409eff`6 + `#e6a23c`5 + `#c0c4cc`4 + `#67c23a`1 + `#f56c6c`1 + `#606266`1 + `#f5f7fa`1 + `#67C23A`1 + 其它)。这直接导致:用户切换主题色 / 切换暗色模式时这些元素**不跟随**。
**移动端**(`frontend/app/src`,排除 `uni_modules`):
| 分区 | 出现次数 |
|---|---|
| 令牌源(`composables/types/theme.ts`、`App.ku.vue`、`styles/theme.scss`、`uni.scss`、`theme.json`) | 127 |
| `pages/login/index.vue` | 26(其中 `:327,335,343,351` 为微信/QQ/Gitee/GitHub **第三方品牌色,属合理豁免**;其余为 `var(--wot-*, #fallback)` 形式的 fallback) |
| `pages/index/index.vue` | 16(ECharts 系列色 + `#FFFFFF` 文本 + `isDark` 双色) |
| `store/themeStore.ts` | 11(`THEME_NAV_BG` 与 theme.scss 重复,P2-9) |
| `pages/login/register` / `forget/index.vue` | 9 / 9(同上,fallback 形式) |
| `composables/useGlobalToast.ts` | 4(`#07c160`/`#fa5151`/`#10aeff`/`#ff9f0a`,微信语义色,**合理豁免**) |
| 其余 5 个文件 | 各 1-2 |
| **`src/styles/**` 之外的合计** | **81 / 11 文件** |
**结论**:移动端 81 处中约 **60 处**是 `var(--wot-primary-6, #1C64FD)` 这类「带 fallback 的引用」写法(引用令牌但硬编码兜底值——**严格说是重复真值源,但不是绕过令牌**),真正的绕过集中在 `pages/index/index.vue:173-174,237-238,247-248,366,437,454,471,587,681,688,712` 与 `subPages/module_system/tickets/index.vue:223`、`pages/login/index.vue:587` 等。**共享组件层(`components/SkeletonPage.vue`、`StatusBadge.vue`、`GlobalDialog.vue` 等)几乎全部使用 wot 令牌**,这是移动端明显优于 Web 的地方。
### 6.3 圆角 / 字号 / 间距偏离
| 类别 | Web 硬编码统计 | 令牌替代 |
|---|---|---|
| 圆角 | `6px`×17、`8px`×14、`4px`×13、`999px`×4、`12px`×3、`2px`×2 等(另 `50%`×18 属合理) | `rounded-custom-xs/sm`(`tailwind.css:66-72`)、`--custom-radius`、`--el-border-radius-base`(`_overrides.scss:34-37` 已映射) |
| 字号 | `<style>` 内 `12px`×32、`14px`×30、`13px`×25、`16px`×13、`11px`×7…;任意值 class **33 处** | `--font-size-xs..3xl`(11/12/13/15/18/22/28) |
| 间距 | `<style>` 内 `padding/margin/gap` 固定 px 共 **231 处**(`8px`×61 占比最高) | Tailwind 间距刻度(0.5/1/2/3/4…) |
> **注意**:14px 这个最高频字号在 `@theme` 中**没有对应档位**(`--font-size-base: 13px`、`--font-size-lg: 15px`),说明令牌刻度与实际设计稿存在缺口 → 建议补 `--font-size-md: 14px`。这条是「令牌不完整」而非「开发者不守规矩」,建议在整改时区分对待。
---
## 7. 中文/英文文案与术语一致性
### 7.1 正向结论(先说不该修的)
| 项 | 事实 |
|---|---|
| locale 键完整性 | Web `zh.json`/`en.json` 各 509 键,**键集合完全相同**(无缺失、无多余);移动端各 331 键,**完全相同** |
| 命名空间 | Web 24 个顶层命名空间(`common`、`login`、`register`、`setting`、`menus`、`table`、`httpMsg`…);移动端 17 个(`common`、`login`、`register`、`setting`、`tickets`、`notices`…) |
| 移动端页面 i18n 覆盖率 | 14/14 页面(100%),模板内硬编码中文仅 1 文件 4 字符(微信隐私协议,合理) |
| 移动端空态文案 | 10 个专用键,且区分 `empty` / `emptyWithFilter` |
| 文档站 | zh/en 文件 1:1(各 9 篇) |
### 7.2 不一致清单
| 项 | Web | 移动端 | 判定 |
|---|---|---|---|
| 顶层命名空间复用率 | 24 个 | 17 个 | **仅 4 个共享**(`common`/`login`/`register`/`setting`),**两个字面 shared 键的英文值 10 个里有 5 个措辞不同**:`login.noAccount`(`No account yet?` vs `Don't have an account?`)、`login.sliderSuccessText`(`Verification successful` vs `Verified`)、`login.sliderText`(`Please slide to verify` vs `Slide to verify`)、`register.title`(`Create account` vs `Create Account`)、`register.toLogin`(`To login` vs `Login`)→ 建议建共享词表 |
| 「启用/停用」 | 停用(`common` 侧 73 处字面量) | 禁用(`common.status.disabled`) | 不一致 |
| 同一实体「公告」 | 业务页「公告通知」/ 顶栏「通知」 | 「通知公告」 | 三选一 |
| 状态词表 | 各页面各写一份(`user/role/position/notice` 共 73 处字面量) | 集中 `common.status.*`(14 个键) | Web 缺少集中词表 |
| 错误提示语言 | 2 处硬编码中文(`utils/http/index.ts:273,277`),其余 `$t` | **全部硬编码中文**(`http/tools/enum.ts` + `http/adapters/alova.ts`,约 15 处) | 两端均有缺口,移动端更彻底 |
| 文档站移动端术语 | 中文「移动端开发指南」 | 英文「Miniprogram Development Guide」(正文含 App/H5) | 不一致(P2-12) |
---
## 8. 需人工 / 真机验证清单
> 以下项**均不得当作已确认缺陷**;每项给出可直接执行的验证步骤。本轮未启服务、未上真机,故不下结论。
### 8.1 视觉与对比度
1. **暗色模式硬编码灰的可读性**(对应 P2-10)
- 步骤:`cd frontend/web && pnpm dev` → 打开 `/module_task/storage/browse`、`/module_generator/gencode` → 右上角切「暗色模式」→ 用浏览器 DevTools 拾色器取 `text-[#909399]` 文字与其实际背景的对比度。
- 判定:WCAG 2.1 AA 正文需 ≥ 4.5:1。
2. **深色模式冷启动是否出现原生背景色差**(对应 P2-9)
- 步骤:真机(Android/iOS)系统切深色 → 冷启动 App(先杀进程)→ 录屏并逐帧观察导航栏/页面顶部是否出现 `#000000` → `#272B3B` 的跳变;同时测下拉回弹区。
3. **`theme.json` 是否真的被原生层应用**(对应 P2-9)
- 步骤:构建小程序/App 后检查产物中 `app.json` 的 `themeLocation`,并在系统深色下观察窗口背景。若未生效,则 P2-9 应降级为「死配置」而非「色差」。
4. **暗色模式下圆角/间距令牌切换是否生效**(对应 P2-11)
- 步骤:Web 设置面板切换圆角档位,逐页观察 6px/8px 元素是否跟随。
### 8.2 屏幕阅读器与键盘
5. **`el-tooltip` 包裹的图标按钮是否可被读出名称**(对应 P2-4)
- 步骤:VoiceOver(macOS Safari/Chrome)或 NVDA(Windows Chrome)逐 Tab 遍历 `/module_task/storage/browse` 的后退/前进/刷新按钮,听是否读出「后退/前进/刷新」。
- 若读出 → P2-4 中的 tooltip 类可豁免;未读出 → 结论升级。
6. **表单无 label 的实际影响**(对应 P2-3)
- 步骤:在登录页 `/login`,用键盘 Tab 定位用户名/密码输入框,听 SR 是否朗读名称;再输入内容后确认 SR 是否仍能识别字段。
7. **自定义可点击元素的 Tab 顺序**(对应 P2-2)
- 步骤:在 `/dashboard/home` 全程 Tab,确认「返回顶部」「设置面板关闭」「顶栏搜索」是否可达。
8. **弹窗焦点陷阱与 Esc 关闭**(ElDialog / wd-popup)
- 步骤:打开 `FaDrawer` 与 `wd-popup`,Tab 是否循环在弹窗内、Esc 是否关闭、关闭后焦点是否回到触发按钮。
9. **toast 是否有无障碍通告**(`ElMessage` / `wd-toast`)
- 步骤:触发一次失败请求,SR 是否播报错误。源码层未见 `aria-live`,需实测(EP 内部可能实现)。
### 8.3 移动端真机
10. **触摸目标尺寸**(WCAG 2.5.8 建议 ≥ 24×24 CSS px,iOS HIG 建议 44×44)
- 步骤:真机打开工单列表,长按/开发者模式量取左滑删除按钮、`wd-fab`、Tabbar 项、以及 `views/dashboard/home/modules/quick-links.vue` 同类的 16px 小按钮(Web 侧是 `w-4 h-4`)。
11. **安全区与底部遮挡**(`env(safe-area-inset-bottom)`)
- 步骤:全面屏机型(iPhone 15 / Android 手势条),检查工单页浮动按钮(`tickets/index.vue:252` `:gap="{ bottom: 40 }"`)与底部 tabbar 是否遮挡列表最后一条、`ticket-detail` 输入栏(`:253`)是否被 Home 条覆盖。
12. **`wd-slide-verify` 在真机的初始化与重置**(`pages/login/index.vue:64-85` 已针对小程序布局时序做处理)
- 步骤:微信小程序 + iOS/Android App + H5 三端各登录失败 2 次,确认滑块能出现、能重置、能通过。
13. **导航栏标题首屏是否闪中文**(对应 P3-9)
- 步骤:语言设为 English → 冷启动 → 录屏观察导航栏标题是否先显示中文再被 `useI18nNavTitle` 覆盖。
14. **`uni.showModal` 在 H5 下的实际外观**(对应 P2-6)
- 步骤:`pnpm dev:h5` 打开工单页左滑删除,观察是否弹浏览器原生 confirm(外观与 App 内其他对话框不一致)。
15. **下拉刷新与触底加载的边界**
- 步骤:弱网(DevTools/真机限速)+ 空列表下拉、最后一页触底,确认 `uni.stopPullDownRefresh()` 一定被调用(`useListPage.ts:66-79` 的 `finally` 已保证,但需实测不会卡住指示器)。
### 8.4 需求/契约层(需与需求方确认,非缺陷)
16. 移动端**不支持批量操作与导入导出**(`REQUIREMENTS.md` 是否登记为已知范围外?)。
17. 移动端**搜索重置保留状态 Tab** 是否符合两端统一的预期语义(第 4 节)。
18. `notice.status` 的正确语义(P1-1)请后端确认 `model.py:16` 注释即权威,并确认线上是否已有 `status=2` 数据。
19. 移动端 `loginFormData.captcha = 'verified'` 占位值(`pages/login/index.vue:138`)是后端约定还是历史遗留(`// 占位值,后端只校验 captcha_key 状态`),需与后端确认后端是否确实忽略坐标校验。
20. Web 端将语言切到 English 后,**业务菜单名来自后端数据库**(不是 locales),因此即使补齐前端 i18n,菜单仍是中文——需确认后端是否提供 i18n 字段(这决定 P1-2 的整改半径)。
---
## 9. 附录:统计口径与复现命令
所有统计均在仓库根目录 `/Users/tao/workspace/FastapiAdmin` 下执行,只读。
**模板内硬编码中文(第 1 节 / P1-2)**
```bash
# 口径:取 <template> 到 <script> 之间的片段,剔除 <!-- --> 注释后,
# 统计 (a) 非 : 开头属性值含 CJK 的出现次数,(b) 纯文本节点含 CJK 的出现次数
python3 - <<'PY'
import re, os
cjk = re.compile(r'[\u4e00-\u9fff]')
files=tot=0; hits=0
for root,dirs,fs in os.walk('frontend/web/src'):
for f in fs:
if not f.endswith('.vue'): continue
s=open(os.path.join(root,f),encoding='utf-8').read()
m=re.search(r'<template>(.*?)</template>\s*(?=<script)', s, re.S)
if not m: continue
tpl=re.sub(r'<!--.*?-->','',m.group(1),flags=re.S); files+=1; n=0
for am in re.finditer(r'([a-zA-Z:@\-\[\]]+)\s*=\s*"([^"]*)"', tpl):
if not am.group(1).startswith(':') and cjk.search(am.group(2)): n+=1
for tm in re.finditer(r'>([^<>{}]*[\u4e00-\u9fff][^<>{}]*)<', tpl):
if tm.group(1).strip(): n+=1
if n: tot+=1; hits+=n
print(f"files={files} files_with_cjk={tot} cjk_occurrences={hits}")
PY
# → files=215 files_with_cjk=87 cjk_occurrences=1063
```
**i18n 覆盖率(P1-2)**
```bash
cd frontend/web/src && T=0; N=0
for f in $(find views -name "*.vue"); do T=$((T+1)); grep -q '\$t(\|useI18n' "$f" || N=$((N+1)); done
echo "views=$T no_i18n=$N" # → views=101 no_i18n=85
cd ../../app/src && T=0; N=0
for f in $(find pages subPages -name "*.vue"); do T=$((T+1)); grep -q '\$t(\|useI18n' "$f" || N=$((N+1)); done
echo "pages=$T no_i18n=$N" # → pages=14 no_i18n=0
```
**硬编码色值(第 6.2 节)**
```bash
# Web:全量
grep -rhoiE "#[0-9a-f]{3,8}\b" --include="*.vue" --include="*.scss" --include="*.ts" --include="*.css" frontend/web/src | wc -l # → 300
# Web:排除设计体系层
grep -rniE "#[0-9a-f]{3,8}\b" --include="*.vue" --include="*.scss" --include="*.ts" frontend/web/src | grep -v "/styles/" | wc -l # → 236 / 50 文件
# 移动端:排除 uni_modules 与令牌源
grep -rniE "#[0-9a-f]{3,8}\b" --include="*.vue" --include="*.ts" --include="*.scss" frontend/app/src \
| grep -vE "uni_modules|composables/types/theme.ts|App\.ku\.vue|uni\.scss" | wc -l # → 81 / 11 文件
```
**键盘可达性(P2-2)**
```bash
# 口径:<div|span|li|td> 且同标签内含 @click,且不含 tabindex / role= / @key*
python3 - <<'PY'
import re, os
n=0
for root,dirs,fs in os.walk('frontend/web/src'):
for f in fs:
if not f.endswith('.vue'): continue
s=open(os.path.join(root,f),encoding='utf-8').read()
for m in re.finditer(r'<(div|span|li|td)\b([^>]*?)>', s, re.S):
a=m.group(2)
if ('@click' in a or 'v-on:click' in a) and not ('tabindex' in a or '@key' in a or 'role=' in a): n+=1
print(n) # → 76
PY
grep -rn 'role="button"\|tabindex' --include="*.vue" frontend/web/src # → 正确模式仅 3 处
```
**ElTable `loading` 无效属性(P2-1)**
```bash
grep -rn "loading" frontend/web/node_modules/element-plus/es/components/table/src/table/defaults.d.ts | head
# loading 位于 interface TreeNode(:292),TableProps(49 个 prop)不含 loading
grep -rn "loading" frontend/web/node_modules/element-plus/es/components/table/src/table.mjs # → 无命中
```
**locale 键对齐(7.1)**
```bash
python3 -c "
import json
def flat(d,p=''):
o={}
for k,v in d.items():
kk=f'{p}.{k}' if p else k
o.update(flat(v,kk)) if isinstance(v,dict) else o.__setitem__(kk,v)
return o
for b in ['frontend/web/src/locales/langs','frontend/app/src/locales/langs']:
en=set(flat(json.load(open(b+'/en.json',encoding='utf-8'))))
zh=set(flat(json.load(open(b+'/zh.json',encoding='utf-8'))))
print(b, len(en), len(zh), 'only-en', sorted(en-zh), 'only-zh', sorted(zh-en))
"
# → web 509/509 [] [];app 331/331 [] []
```
**其他一次性核对命令**
```bash
grep -c "ElFormItem\|el-form-item" -r frontend/web/src --include="*.vue" # form-item 70 处,无 label 27 处
grep -rn "useListPage" -A 12 frontend/app/src/pages frontend/app/src/subPages # error 未被任何页面消费
grep -rn "void=ElMessageBox\|ElPopconfirm" frontend/web/src --include="*.vue" # 二次确认覆盖:11+ 文件
sed -n '16p' backend/app/modules/system/notice/model.py # 状态(0:草稿 1:已发布 2:已归档)
```
**本轮未执行的验证方式(重要)**:未运行 `pnpm dev`/`pnpm build`/`vitest`/`eslint`/`stylelint`,未截图,未上真机,未做页面性能与网络抓包。所有「未验证」结论已在正文与第 8 节标明。
---
## 10. 建议整改优先级(供 captain 汇总用)
| 顺序 | 动作 | 覆盖问题 | 预估成本 |
|---|---|---|---|
| 1 | 修正 Web 通知 `status` 枚举(+2 已归档,改 0 草稿/1 已发布) | P1-1 | 1 文件 / 3 处 |
| 2 | `FaTable` 增加 `error`/`retry` prop,移动端增加 `ErrorState` 组件并消费 `error` | P1-3、P2-7 | 2 个共享组件 + 16/2 页接入 |
| 3 | 移动端区分「加载失败」与「不存在/空」,修 3 个页面 | P1-4 | 3 文件 |
| 4 | 共享组件 + HTTP 层文案先走 i18n(Web 8 个文件 / 移动端 2 个文件) | P1-2 的 80% 收益、P2-5、P3-1、P3-3 | 10 文件 |
| 5 | `useClickable`/`aria-label` 批量补齐 + 修 `module_monitor/cache` 的 `v-loading` | P2-1、P2-2、P2-4 | 机械替换 |
| 6 | 抽 `constants/status.ts` 统一状态词表与色板,两端共用 | P2-8、P3-11 | 新增 1 文件 + 6 文件 |
| 7 | 移动端 `theme.scss` 收敛为唯一主题真值源,对齐 `theme.json` | P2-9、P2-10(App 侧) | 3 文件 |
| 8 | Web EP 默认色 → 语义令牌;`--font-size-md` 补齐;stylelint 规则拦截新增硬编码 | P2-10、P2-11 | 渐进 |
| 9 | 清理死代码与死样式 | P3-4、P3-5、P3-6 | 4 文件 |