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

60 KiB
Raw Blame History

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。页面普遍写成:
} 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 处(#90939916 + #409eff6 + #e6a23c5 + #c0c4cc4 + #67c23a1 + #f56c6c1 + #6062661 + #f5f7fa1 + #67C23A1 + 其它)。这直接导致:用户切换主题色 / 切换暗色模式时这些元素不跟随。

移动端(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 屏幕阅读器与键盘

  1. el-tooltip 包裹的图标按钮是否可被读出名称(对应 P2-4)
    • 步骤:VoiceOver(macOS Safari/Chrome)或 NVDA(Windows Chrome)逐 Tab 遍历 /module_task/storage/browse 的后退/前进/刷新按钮,听是否读出「后退/前进/刷新」。
    • 若读出 → P2-4 中的 tooltip 类可豁免;未读出 → 结论升级。
  2. 表单无 label 的实际影响(对应 P2-3)
    • 步骤:在登录页 /login,用键盘 Tab 定位用户名/密码输入框,听 SR 是否朗读名称;再输入内容后确认 SR 是否仍能识别字段。
  3. 自定义可点击元素的 Tab 顺序(对应 P2-2)
    • 步骤:在 /dashboard/home 全程 Tab,确认「返回顶部」「设置面板关闭」「顶栏搜索」是否可达。
  4. 弹窗焦点陷阱与 Esc 关闭(ElDialog / wd-popup)
    • 步骤:打开 FaDrawer 与 wd-popup,Tab 是否循环在弹窗内、Esc 是否关闭、关闭后焦点是否回到触发按钮。
  5. toast 是否有无障碍通告(ElMessage / wd-toast)
    • 步骤:触发一次失败请求,SR 是否播报错误。源码层未见 aria-live,需实测(EP 内部可能实现)。

8.3 移动端真机

  1. 触摸目标尺寸(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)。
  2. 安全区与底部遮挡(env(safe-area-inset-bottom))
    • 步骤:全面屏机型(iPhone 15 / Android 手势条),检查工单页浮动按钮(tickets/index.vue:252 :gap="{ bottom: 40 }")与底部 tabbar 是否遮挡列表最后一条、ticket-detail 输入栏(:253)是否被 Home 条覆盖。
  3. wd-slide-verify 在真机的初始化与重置(pages/login/index.vue:64-85 已针对小程序布局时序做处理)
    • 步骤:微信小程序 + iOS/Android App + H5 三端各登录失败 2 次,确认滑块能出现、能重置、能通过。
  4. 导航栏标题首屏是否闪中文(对应 P3-9)
    • 步骤:语言设为 English → 冷启动 → 录屏观察导航栏标题是否先显示中文再被 useI18nNavTitle 覆盖。
  5. uni.showModal 在 H5 下的实际外观(对应 P2-6)
    • 步骤:pnpm dev:h5 打开工单页左滑删除,观察是否弹浏览器原生 confirm(外观与 App 内其他对话框不一致)。
  6. 下拉刷新与触底加载的边界
    • 步骤:弱网(DevTools/真机限速)+ 空列表下拉、最后一页触底,确认 uni.stopPullDownRefresh() 一定被调用(useListPage.ts:66-79 的 finally 已保证,但需实测不会卡住指示器)。

8.4 需求/契约层(需与需求方确认,非缺陷)

  1. 移动端不支持批量操作与导入导出(REQUIREMENTS.md 是否登记为已知范围外?)。
  2. 移动端搜索重置保留状态 Tab 是否符合两端统一的预期语义(第 4 节)。
  3. notice.status 的正确语义(P1-1)请后端确认 model.py:16 注释即权威,并确认线上是否已有 status=2 数据。
  4. 移动端 loginFormData.captcha = 'verified' 占位值(pages/login/index.vue:138)是后端约定还是历史遗留(// 占位值,后端只校验 captcha_key 状态),需与后端确认后端是否确实忽略坐标校验。
  5. Web 端将语言切到 English 后,业务菜单名来自后端数据库(不是 locales),因此即使补齐前端 i18n,菜单仍是中文——需确认后端是否提供 i18n 字段(这决定 P1-2 的整改半径)。

9. 附录:统计口径与复现命令

所有统计均在仓库根目录 /Users/tao/workspace/FastapiAdmin 下执行,只读。

模板内硬编码中文(第 1 节 / P1-2)

# 口径:取 <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)

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 节)

# 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)

# 口径:<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)

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)

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 [] []

其他一次性核对命令

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 文件