Files
ts-mobile-go/docs/UI重构设计规范_审查报告.md
T
sansenandClaude Fable 5 8716b391f6 工程改进:跨语言契约固化、日志脱敏、状态模型收敛
依据 docs/项目工程改进方案.md 实施(第五梯队 CI/协作除外)。

安全性
- Release 剥离 Log.v/d/i/w(-assumenosideeffects),保留 Log.e
- 全量频道/成员 dump、消息正文、发送者 UID 改为 BuildConfig.DEBUG 保护
  (实测 release dex 中 "Channel: id=" / "Client: id=" / selfId= 等均为 0 命中)

构建产物体积
- .gitignore 补 *.a(保留 .opus/lib 四个 ABI 预编译库)、.opus/install、
  app/libs/*.aar、dnn/torch
- git rm --cached 除名 AAR、.gradle、.opus/{build,install}、dnn/torch
  跟踪体积 50MB → 17.4MB,工作区文件不受影响

跨语言契约(本方案核心)
- 新增 go/teamspeak/contract.go:字段名常量 + BridgeContractVersion 单一事实来源
- 新增契约测试:反射断言常量与 struct tag 一致;Go/Kotlin 共用 golden 样本
- 新增 BridgeContract.kt:启动校验 AAR 契约版本,不匹配则阻止连接
- 修正文档:channel_order 是前驱频道 ID(链表指针)而非排序权重,
  按它数值排序会打乱频道树;ChannelInfo.order 在 Go 侧不存在

状态模型
- ConnectionState 增 Idle 取代 null 编码,connectionState 不再可空
- applyClients 改为按频道差分更新,避免全量刷新导致频道树整体重组
- 频道数据 freshness 统一由 Repository 维护,修复 ViewModel 与 Repository
  两份时间戳不同步导致的重复刷新

UI
- 修复 collectAsState() 在参数位置调用导致的 isSelf 快照失效
- 消除 9 处 !! 断言(ChannelListScreen)
- 硬编码 24.5/14.5dp 与 depth*24 收敛到 UiTokens.Spacing,缩进加 4 层上限

测试
- Kotlin 测试 7 → 106;Go 契约测试新增 24 个用例
- 各覆盖 InputValidator、消息送达确认、僵尸会话过滤、频道差分、
  频道顺序语义、错误分类

其他修复
- classifyError 提取为纯函数并补测试;修复 too many clones (id=521)
  未识别导致英文原文直接暴露给用户

版本号提升至 1.0.13(versionCode 13)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 14:58:15 +08:00

325 lines
28 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.
# 《TS Mobile UI / UX 重构设计规范》审查报告
审查对象:`E:\MyProject\ts-mobile-go\docs\TS Mobile UI UX 重构设计规范.md`(47 章)
交叉参照:
- `E:\MyProject\ts-mobile-go\docs\UI架构设计.md`
- `E:\MyProject\ts-mobile-go\docs\流程\00_总览.md` ~ `09_EventBus架构.md`
- `E:\MyProject\ts-mobile-go\docs\implementation\00_实施总览.md` ~ `12_主题与收尾.md`
- `E:\MyProject\ts-mobile-go\CLAUDE.md`(已知 SDK 限制)
- Android 实现:`android/app/src/main/java/com/tsmobile/app/`(Models、Repository、NavGraph、UiTokens、Shapes、SemanticColors、TSBridge、各 ViewModel 与组件)
审查结论:**方向正确,但当前版本不具备落地条件。**
"Server 是工作空间 + Current Channel 是核心上下文 + Chat/Channels 只是视图"这个模型判断是对的,比现有的三页跳转(server_config → channel_list → chat)更贴合 TeamSpeak 的使用心智。但文档停留在"理念陈述"层面:状态模型与代码不符、多个核心组件缺数据来源、异常路径几乎空白、Design Token 与现有实现两套命名并存。按此文档开工,实现者会在第 3 天卡住。
---
## 一、阻断级缺陷(不补齐无法开工)
### A1. 顶层状态模型与代码不一致,且漏掉必然出现的中间态
文档第 3 章只定义 `Disconnected / Connecting / Connected` 三态,并断言"不应该存在 Connected but no channel"。
实际 `data/Models.kt` 的 `ConnectionState` 是 5 态:
| 文档 | 代码实际 |
| --- | --- |
| Disconnected | `Disconnected(reason, wasKicked)` |
| Connecting | 无独立态(连接中由 ViewModel 的 `isConnecting` 类标志表达) |
| Connected | `Connected` |
| — 未定义 | `Disconnecting` |
| — 未定义 | `Reconnecting(attempt, maxAttempts, reason)` |
| — 未定义 | `WaitingForNetwork(since)` |
更关键的是:**"Connected 但没有频道"在真实链路上必然存在**,文档把它当成不该出现的状态,但它不是 UI 设计问题,是协议时序问题:
1. `ConnectionState.Connected` 之后还有独立的第二层状态 `SyncState`(`ChannelViewModel`:`Unsynced / Syncing / Synchronized / SyncFailed`)。首次同步期间频道树是空的,`UI架构设计.md` 2.2 节为此专门设计了"正在同步服务器数据..."加载态。新文档完全没提这一层。
2. `Repository.currentChannelId` 初值就是 `"0"`;`NavGraph.kt` 里存在兜底逻辑——当 `currentChannelId <= 0` 时改用 `Repository.getSelfChannelId()` 从客户端列表反查。这说明 `GetChannelID` 失败是已知的真实场景。
**需要补的规格**:Connected 之后的完整状态矩阵,至少覆盖
`Connected × {Unsynced, Syncing, Synchronized, SyncFailed}` × `currentChannelId ∈ {有效, "0", 反查失败}`,
并明确每种组合下 ServerHeader / CurrentChannelBar / Content 区各显示什么(骨架屏?占位名?"正在同步"?)。这是全文档最大的空洞——CurrentChannelBar 被称为"最重要的 UI 组件",却没有定义它在数据未就绪时的样子。
### A2. 成员状态系统缺数据来源,且未列入任何优先级
文档第 16 章要求 8 种成员状态:`Online / Idle / Away / Muted / Deafened / Speaking / Server Muted / Disconnected`。
实际数据能力:
- `ClientInfo` 字段仅有 `id / nickname / uid / channelId / serverGroups / isSelf`——**没有 away、idle、muted、deafened、serverMuted、talkPower 任何一项**。
- `TSBridge` 回调只有 `onClientSpeaking(clientID, speaking)`(Go 侧判定,Kotlin 不得自行从 PCM 推断)。
- `setRemoteClientMuted` 是本地静音远端,不等于对方的 Muted 状态。
也就是说 8 种状态里只有 Speaking 有数据支撑,其余 6 种(Online 可由在场推断)需要改 `go/teamspeak/bridge.go`、扩展 `getClientsJSON` 字段、**重新编译 AAR**。文档第 40~44 章的重构方向和 P0/P1/P2 优先级里完全没有"需要改协议层"这一项,会让排期严重失真。
**需要补**:每个状态标注「数据已具备 / 需扩展 bridge / 本期不做」,并把 bridge 改造单列为 P0 前置项。
### A3. Avatar 体系凭空出现,无任何数据来源与降级规格
文档第 7、15、16、31、39 章大量依赖头像:CurrentChannelBar 的 `● ● ● ● +8`、Member Item 的 `[Avatar] Alex`、Speaking 的 Avatar Ring、第 31 章还定义了三档尺寸令牌(32/40/56dp)。
实际情况:TeamSpeak 协议**没有用户头像**。`ServerInfo.iconId` 和 `ChannelDetailInfo.iconId` 是服务器图标 / 频道图标,不是客户端头像;代码中不存在任何 avatar 相关实现。
文档从头到尾没有说明:头像内容从哪来?无头像时显示什么(昵称首字母?固定图形?服务器组图标?)?头像是否可上传(TS 无此能力)?
这不是细节问题——CurrentChannelBar 的成员预览、Member Item 的主视觉、Speaking 的主要表达方式全部建立在头像上。**必须先定义"无头像时的 Avatar 替代方案",否则第 7、15、16 章的示意图无法实现。**
建议:改为首字母色块(昵称首字符 + 由 UID 哈希出的稳定背景色),Avatar Ring 表达 Speaking,并把"服务器组徽章"作为唯一的真实图形来源。
### A4. PTT 在最终结构里没有位置
文档第 19 章说"PTT 是语音 UI 的核心操作",第 20 章专门定义 PTT 状态,第 39 章组件树里有 `VoiceStatus`。
但第 38 章"主页面最终结构"的两张完整示意图(Chat / Channels)里**没有任何语音或 PTT 元素**,第 5 章的 Server Space 结构图里也只有 `Current Content`。
同时文档第 18 章否定了 Voice 独立页面,第 19 章又把 Voice UI 画成一个大卡片(含 🎙 / ● Connected / [PTT] / Mic / Speaker)——这个卡片挂在哪一层?常驻还是弹出?与底部 BottomNavigation 如何共存?现状 `UI架构设计.md` 是"底部语音控制栏常驻所有页面 + 展开为语音卡(BottomSheet)",新文档既没说保留也没说取消。
**核心操作缺失布局定义,属于必须补齐的阻断项。**
附带三个未定义问题:
- PTT 触发方式(屏内按钮 / 悬浮窗 / 音量键 / 声控)一个都没指定。
- **App 在后台或锁屏时如何发言**——全文未涉及后台场景,但代码有 `ConnectionService` 前台服务与 keepalive 通知,说明后台常驻是真实需求,PTT 的后台可用性是移动端语音客户端的关键设计点。
- 第 20 章的 `Idle → Pressed → Speaking` 与代码 `VoiceState`(`Idle / Transmitting / Blocked(reason)`)不匹配:文档漏了 `Blocked`(未连接、采集失败、发送异常)及其 UI 表现;且 `Speaking` 在代码语义里指**远端**用户在说话(`onClientSpeaking`),文档却把它当作自己 PTT 的第三态,概念混用需澄清。
### A5. Chat 范围收窄会砍掉私聊与服务器聊天,文档未声明取舍
文档第 9 章把 Chat 定义为"当前频道的文字消息流",第 41 章 `ServerSpaceState` 只有单个 `chatMessages`,第 46 章 `Current View = Chat or Channels`。
实际现有能力覆盖三种会话:`ChatMessage.targetMode` 为 `1=私聊 / 2=频道 / 3=服务器`;`Repository` 按 `"${targetMode}_${targetId}"` 分档归档(每会话上限 500 条);`ServerViewModel.sendMessageNotification` 会区分标题"频道消息"/"私聊消息";`MainActivity.DeepLinkAction` 携带 `targetMode/targetId`,通知点击可直达指定会话。
新架构里私聊和服务器聊天**没有任何入口**。这会直接导致:
- 系统通知点击 → 跳转会话的 deep link 断链;
- Repository 的多会话归档能力被闲置;
- 从 Member Action Menu 发起私聊的路径消失(第 17 章的菜单里也只有 Poke / Copy / Move / Kick)。
**必须明确写**:本期是否支持私聊与服务器聊天?若不支持,通知 deep link 如何降级?若支持,入口在哪(Drawer?Member Menu?第三个 Tab?会话列表?),`ServerSpaceState` 需改为会话集合而非单个 `chatMessages`。
---
## 二、与现有文档 / 代码的直接冲突(需裁决,否则三套规范并存)
| # | 冲突点 | 新规范 | 现状 |
| --- | --- | --- | --- |
| B1 | 断线重连 | 第 33 章 `Reconnecting → Warning`、第 35 章推荐 Reconnect banner 动画 | `UI架构设计.md` 5.4 为顶部横幅不阻塞页面、最多 5 次递增重连;代码实际是 `ReconnectOverlay` **全屏遮罩**;`CLAUDE.md` 又写明"自动重连默认禁用,频繁重连会触发服务端限流/封禁"。三方不一致,新文档未裁决 |
| B2 | 头部是否显示地址 | 第 6 章"不建议长期显示 server.example.com:9987" | `UI架构设计.md` 1.2 与 `AppTopBar(subtitle=...)` 明确显示 `192.168.1.1:9987`。文档未说明这是有意变更;且服务器重名时地址是唯一辨识信息,第 24 章 Recent Servers 又显示地址,自相矛盾 |
| B3 | 端口输入 | 第 23 章把 Port 拆为独立输入框 | `InputValidator.validateServerAddress` 支持 `address:port` 一体式并校验 1..65535;`ServerConfig` / `RecentConnection` 只有 `address` 字段;TSDNS 地址本身可能含端口。第 24 章 Recent Servers 又写成一体式 |
| B4 | 连接页字段 | 第 23 章表单无 Password、无默认频道 | 第 3 章 Disconnected 明确列了 Password;`ServerConfig` 有 `password / defaultChannel / defaultChannelPassword` |
| B5 | 被踢路径 | 第 26 章 Disconnect 流程只有 `Server Space → Disconnected → Connection Screen` | 代码有 `Disconnected(wasKicked=true)` → `Routes.KICKED` → `KickedScreen`(重连 / 返回主页)。流程图漏了这条已实现的路径 |
| B6 | 成员状态是否显示文字 | 第 15 章"正常状态不要一直显示文字" | `UI架构设计.md` 与现有实现用文字标注;纯颜色/圆点表达会降低可访问性(见 D4) |
| B7 | Drawer 条目 | 第 21 章:Server Info / Voice / Notifications / Appearance / Settings / Disconnect | 第 4 章信息架构:Server Info / Voice Settings / Notifications / Appearance / Disconnect。同一文档内两处条目不同(Settings 有无、Voice vs Voice Settings) |
| B8 | CurrentChannelBar 位置 | 第 38 章放在 Header 之下(顶部) | 第 5 章、第 8 章结构图放在 Content 之下、Tab 之上(底部)。同一文档内位置矛盾 |
| B9 | 主题切换入口 | 第 21 章移入 Server Drawer → Appearance | 现状在服务器配置页右上角,未连接时也可切换。移入 Drawer 后**未连接状态无法切换主题**,属交互回退,文档未察觉 |
| B10 | 频道切换语义 | 第 27 章"本质上只是 currentChannelId 发生变化" | 代码是异步 `ClientMove` + `ChannelSwitchState` 等服务端确认,可能因密码 / 满员 / 权限 / talkPower 失败(`mapMoveError`)。按文档描述实现会做成无回滚的乐观更新 |
**文档治理问题(最需要先解决的一条)**:项目已有 `UI架构设计.md`(3 页 3 卡)、`docs/流程/00~09`、`docs/implementation/00~12` 三套体系,新文档**没有版本、日期、作者、适用范围、变更记录,也没有声明它与上述文档的关系**——是取代还是补充?冲突以谁为准?不明确这一点,上表 10 处冲突会全部变成实现期的反复拉扯。
建议在文档开头加一节「本文档效力」:明确取代 `UI架构设计.md` 的哪些章节、保留哪些、`docs/流程` 作为行为权威源不变。
---
## 三、覆盖缺口(现有功能在新规范里消失)
新规范描述的是一个"理想晴天的正常路径"。以下已实现或已设计的能力在 47 章中**一次都没出现**:
**异常与错误态**(现状代码已有对应实现,文档全部丢失)
- 连接失败 6 类错误分类与文案(`ServerViewModel.classifyError`:密码错误 / 昵称冲突 / 网络不可达 / 地址无效 / 超时 / 服务器满)
- 首次同步失败与重试(`SyncState.SyncFailed`)
- 频道切换失败(`ChannelSwitchState` / `mapMoveError`)、密码频道输入(`ChannelPasswordDialog`)、频道确认(`ChannelConfirmDialog`)
- 消息发送失败与重试(`MessageSendState.Failed` / `MessageDeliveryState.FAILED` / 10 秒超时)
- 空状态(`EmptyStateView`:空频道、无消息)
- 网络丢失等待(`WaitingForNetwork`)
- 应用更新提示(`UpdateChecker` / `UpdateBanner` / `UpdateCheckDialog`)在 Server Space 中的位置
**已存在的 Dialog / Menu 全部未纳入规范**
第 28 章只抽象地说 Dialog 用于"确认 / 危险操作 / 输入",但没有清单。代码中已有:`ChannelPasswordDialog`、`ChannelConfirmDialog`、`DisconnectConfirmDialog`、`PokeDialog`、`ClientActionMenu`、`MessageContextMenu`、`UpdateCheckDialog`、`PokeNotification`。规范应逐个给出触发条件、内容、按钮、危险等级。
**Poke**
现状是核心交互(三个入口 + 顶部气泡 + 系统通知 + `App.POKE_CHANNEL_ID`)。新文档只在第 17 章 Member Action 里出现一次 "Poke",未定义接收端表现。
**权限与降级**
第 17 章要求 Move / Kick from Channel / Kick from Server,但没定义**无权限时的表现**(隐藏 / 置灰 / 点击后报错)。TS 的权限由服务器组与频道组决定(`ServerInfo.defaultServerGroup / defaultChannelGroup`、`ChannelDetailInfo.neededTalkPower`),这是必须定义的分支。
**Recent Servers 管理**
第 24 章只给了两行卡片示意,缺:最多 10 条、按时间倒序、长按删除、清空、成功/失败标记(这些在 `UI架构设计.md` 2.1 有),以及**密码的存储安全规范**(`RecentConnectionsStore` 持久化了 password,文档未涉及是否加密、是否用 Keystore)。
**文件与富文本消息**
代码有 `Bbcode.kt`、`FileMessageMeta`(图片 / MyTS 文件)、`EmojiMapper`。文档第 10 章讲消息合并、第 11 章说"不要把 Emoji / 文件 / 图片放一级 UI",但没说明**接收侧**这些内容如何渲染。"不放到一级输入 UI"和"能不能收发"是两件事,当前表述容易被误读为砍功能。
**后台与生命周期**
前台服务、keepalive 通知、`NetworkMonitor`、进程被杀后的状态恢复、`connectionGeneration` 防止陈旧回调——移动端语音客户端的核心体验,文档零覆盖。
---
## 四、Design Token 章节(29~34)的具体问题
### D1. 与现有 Token 两套命名并存
第 29 章给 `xs=4 sm=8 md=12 lg=16 xl=24`,代码 `UiTokens.Spacing` 已是 `None=0 ExtraSmall=4 Small=8 Medium=12 Large=16 ExtraLarge=24 Huge=32`。文档说"建议继续强化现有体系",却给了一套新命名,且漏了 `None` 和 `Huge`。照文档写会出现 `Spacing.sm` 与 `Spacing.Small` 并存。
→ 直接引用现有名称,补上 None / Huge。
### D2. 用区间代替确定值,等于没有约束
- 第 30 章 Radius:`List Item 8~12dp`、`Card 12~16dp`、`Floating Card 16~20dp`
- 第 32 章 Typography:`Title 16~20sp`、`Body 14~16sp`、`Secondary 12~14sp`、`Caption 11~12sp`
- 第 31 章:`List Item 52~56dp`
给区间意味着两个开发者会各选一端,最终仍然不统一——这正是文档第 31 章想避免的"14.5dp / 17dp 局部修正值"问题的另一种形式。
→ 规范应给确定映射,并复用代码已有档位:`Shapes`(extraSmall 4 / small 8 / medium 12 / large 16 / extraLarge 24)、`UiTokens.Size`(TouchTarget 48 / ControlHeight 40 / AppBarHeight 56 / Icon 16-20-24)、`MaterialTheme.typography`(`AppTopBar` 已用 `titleMedium`)。文档自建的 Title/Body/Secondary/Caption 与 M3 的 display/headline/title/body/label 是两套体系,未给映射关系。
### D3. Semantic Colors 与代码不匹配
第 33 章列 `Primary / Accent / Success / Warning / Error / Muted / Disabled / Speaking`;代码 `SemanticColors` 只有 `success / warning / info` 三组(各含 on / container 变体)。差异:
- 缺 `accent / muted / disabled / speaking` 四组,需明确新增字段
- `Primary / Error` 在 M3 `colorScheme` 里已有(`primary / error / errorContainer`),文档另立一份会造成双源
- `info` 在代码里有、文档没提
- `Accent` 与 M3 的 `tertiary` 关系未定义;`Speaking → Accent` 与 `Primary Action` 同色会削弱第 37 章的视觉层级
- 未给出 onXxx 前景色与 container 变体,而代码现有 token 都是成组定义的
### D4. 状态仅靠颜色区分,缺冗余编码
第 33~34 章把 Connected/Speaking/Reconnecting/Disconnected 全部映射到颜色,没有要求形状、图标或文字的第二重编码。这违反 WCAG 1.4.1(不能仅用颜色传达信息),对色觉障碍用户不可用。
第 15 章"正常状态不要一直显示文字"进一步削弱了这一点。
→ 建议规定:颜色 + 图标形状(如 Speaking 用波形而非仅高亮环)双重编码;纯装饰性状态点可只用颜色。
### D5. 暗色主题规范完全缺失
代码有 `ThemeMode.kt` / `ThemePreferences.kt`,`UI架构设计.md` 4.4 定义了主题切换与持久化。新文档第 21 章列了 Appearance、第 29 章提了 Theme,但**全文没有一条暗色规格**:Semantic Colors 在暗色下的取值、Speaking 高亮的对比度、第 37 章 Background/Surface/Elevated Surface 三级层级在暗色下如何区分(暗色主题通常靠 tonal elevation 而非阴影)。
→ 第 33/34/37 章均需给出亮/暗双套取值,或明确引用 M3 tonal palette 的生成规则。
### D6. 动画规范无数值
第 35 章只说"短时、低幅度",没有时长、easing、可打断性、并发规则。代码现状已有具体值(`tween(340/320/280/240, FastOutSlowInEasing)`)。
→ 给出 duration token(如 `Fast=150ms / Normal=250ms / Slow=350ms`)与 easing token,并说明是否沿用现有值。同时缺"系统开启减弱动效时如何降级"。
---
## 五、组件级设计漏洞
### E1. CurrentChannelBar(第 7 章,自称最重要组件)缺规格
- 成员头像预览的**排序规则**未定义(说话者优先?加入时间?权限高低?)
- 折叠阈值未定义(几个之后显示 `+N`)
- 点击行为未定义(跳 Channels Tab?弹 Member 列表 Sheet?)
- **缺变体定义**:第 38 章 Channels 视图里它被简化为 `# General 12`,第 7 章却是含头像的完整版。同一组件两种形态,正文未说明,也无变体命名
- 与 Channels Tab 的 Channel Tree 成员列表**信息重复**:Channels 视图下当前频道成员会出现两次
- 数据刷新抖动:`CLAUDE.md` 说明 client enter/leave 会触发**全量 clientlist 刷新**(因 `notifycliententerview` 的 ChannelID 不可靠),头像预览会整组重排。第 35 章讲动画却没考虑这个高频刷新场景
### E2. Channel Tree(第 12~14 章)缺关键规格
- **嵌套缩进上限**未定义。TS 频道可深层嵌套,窄屏 5 层以上必然溢出。需要规定最大缩进层级、超限处理(截断缩进 / 横向滚动 / 面包屑)
- 排序规则未定义(`ChannelInfo.order` 字段存在但文档未提)
- 默认展开策略未定义(全部折叠?只展开当前频道路径?)
- 展开状态在切 Tab / 切频道后是否保持
- 大量频道时的性能要求(LazyColumn、key 稳定性)
- 实时性假设不成立:`CLAUDE.md` 明确 SDK **无 channel create/update/delete 事件**,频道列表 5 分钟陈旧或切换前才刷新。第 12 章把它当实时树来设计,需注明刷新时机与"数据可能滞后"的表达
### E3. 消息合并(第 10 章)缺规则
- 合并的**时间窗口**未定义(跨 5 分钟还合并吗?)
- 合并后时间戳如何显示
- 系统消息(`MessageType.SYSTEM`)是否打断合并
- 合并组内单条消息的 `deliveryState`(PENDING/SENT/FAILED)如何展示、失败重试入口在哪
### E4. 用 emoji 充当设计规格
第 13 章 `▼ 🎙 General 12`、示意图中的 `☰ ⋮ ● ▼ ▶ 💬 🎤 🔒 🔇`。emoji 跨设备渲染不一致,且与第 36 章"Low Decoration / 不是 Material 3 Demo"的诉求冲突。
→ 应指定 Material Icons 具体图标名,并补一张「频道类型 → 图标」映射表。文档提到"Channel Type"但从未枚举 TS 实际的频道属性(默认 / 密码 / 永久 / 半永久 / 临时 / codec / neededTalkPower / maxClients),而这些字段 `ChannelInfo` 和 `ChannelDetailInfo` 里都有。
### E5. 触控目标低于平台标准
第 31 章 `Small Button 40dp`。Android 与 M3 推荐最小触控目标 48dp(代码 `UiTokens.Size.TouchTarget = 48`)。视觉高度 40dp 可以,但必须规定用 padding 补足到 48dp 命中区——文档未说明,实现者会直接做成 40dp 命中区。
### E6. 第 28 章 Sheet/Dialog/Drawer 分层规则不完整
规则本身合理(避免 BottomSheet → Dialog → BottomSheet 链),但缺:
- Bottom Sheet 的档位(半屏 / 全屏 / 拖拽行为),现状语音卡是可拖拽半屏
- 系统返回键在每种容器上的行为
- 多层容器同时存在时的层级与遮罩规则
- 键盘弹起时 Dialog / Sheet 内输入框的避让(现状 `UI架构设计.md` 2.3 有键盘联动规格,新文档丢失)
---
## 六、可访问性与适配(全文零覆盖)
一份移动端 UI/UX 重构规范里没有以下任何一项:
- **无障碍语义**:contentDescription 规范、状态变化的 `liveRegion` 声明。Speaking 是纯视觉高频状态,TalkBack 用户完全无法感知谁在说话——这对语音客户端是严重问题
- **字体缩放**:全用 sp 定义字号,但第 31 章的固定 dp 高度(Input 52 / List Item 52~56)在大字号下会截断,需定义最小/最大缩放下的行为
- **横屏 / 平板 / 折叠屏**:全文只有一种窄屏竖版布局。Compose 项目大概率会跑在平板上,Server Space 是否用双栏(左频道树 + 右内容)?未提
- **对比度**:第 33/37 章定义颜色与层级,无任何对比度要求(WCAG AA 4.5:1)
- **减弱动效**:第 35 章无降级方案
- **单手可达性**:PTT、发送、Tab 都在底部是对的,但 CurrentChannelBar 按第 38 章在顶部,未讨论可达性
---
## 七、实时性能约束缺失
对一个语音客户端,这是不该缺的一章:
- Speaking 状态由 Go 侧 400ms 静音超时判定,属高频变化。第 16 章要求 Avatar Ring + 状态点 + 音量指示器**三种同时表达**,12 人频道会引发成员列表大范围重组
- 文档未规定重组边界(`derivedStateOf`、稳定 key、把高频状态下推到最小 composable)
- 未规定 Speaking 指示的更新节流(是否需要 100ms 级去抖)
- 未规定频道树全量刷新时避免闪烁的策略(结合 `CLAUDE.md` 的"client enter/leave 触发全量 clientlist 刷新")
---
## 八、文档形式与可执行性
### F1. 信噪比过低
25KB / 47 章中大量是把一两个词包进 ```text 代码块(如单独一行的 `currentChannelId`、`Chat`、`Channels`)。真正的规格信息(确定数值、状态枚举、交互规则)占比很小。同样的内容可以压缩到 1/3 篇幅且更有用。
### F2. 措辞无约束力
"推荐"约 20 次、"建议"约 25 次、"不要/避免"若干。规范里全是建议就无法作为验收依据。
→ 改用 MUST / SHOULD / MAY(或 必须 / 应当 / 可以),并对每条给出可检验的判据。
### F3. 缺规范应有的载体
没有:组件规格表(尺寸/间距/状态/交互的确定值)、状态枚举表、Do/Don't 对照、验收清单、视觉稿或线框图链接。ASCII 示意图只能表达布局意图,无法表达规格。
### F4. 术语不统一
- `Client` / `Member` / `User` 混用:代码是 `ClientInfo` / `ClientActionMenu` / `ClientItem`,文档第 15~17 章叫 Member,第 40 章目录又叫 `client/`
- `Channel Tree` / `Channels` / `Channel List` 混用
- `Server Space` / 服务器工作空间(第 2 章)混用
→ 加一节术语表,锁定唯一名称(建议跟随代码用 Client)。
### F5. P0/P1/P2 不可排期
第 42~44 章的优先级只有条目名,缺:工作量、依赖关系、验收标准、**是否需要改 Go 协议层**。按 A2/A3 的分析,成员状态与头像都需要先改 `bridge.go` 并重编 AAR,这必须是 P0 前置项,但文档把它归到了 P1(Member Item)和未提及(Avatar)。
### F6. 缺迁移映射
第 40 章给了目标目录结构,但没有"现有文件 → 新结构"的对照表。当前实现是 4 路由(`server_config / channel_list / chat / kicked`)+ 独立 Screen;目标是 Server Space 单容器 + 内部 Tab。这需要重写 `NavGraph.kt`(含 deep link、`BackHandler`、`LaunchedEffect` 导航、`ReconnectOverlay` 与 `PokeNotification` 全局覆盖层),是本次重构成本最高的部分,文档一句未提。
建议补一张迁移表,例如:
| 现有 | 目标 | 处理 |
| --- | --- | --- |
| `AppTopBar` | `server/ServerHeader` | 改造(增加状态与在线数) |
| `ChannelListScreen` | `ServerSpace` + `channel/ChannelTree` | 拆分 |
| `ChatScreen` | `chat/ChatView` | 降级为视图 |
| `VoiceCard` + `VoiceControlBar` + `PTTButton` | `voice/*` | 合并(需先定 A4) |
| `ServerDetailCard` | `server/ServerDrawer` | 形态从 BottomSheet 改 Drawer |
| `NavGraph` 4 路由 | 2 路由(Connection / ServerSpace)+ Tab | 重写 |
| `KickedScreen` | ? | 文档未定义(见 B5) |
---
## 九、优化建议:文档结构重组
按当前 47 章平铺结构,实现者无法按需查阅。建议重组为 6 部分:
1. **范围与效力**(新增,最高优先):版本 / 日期 / 适用模块 / 与 `UI架构设计.md`、`docs/流程`、`docs/implementation` 的取代关系 / 冲突裁决规则 / 术语表
2. **模型与状态**(现第 2~4、26~27、41 章):连接状态机(补齐 5 态 + SyncState + currentChannelId 就绪态)、频道切换的异步语义与失败回滚
3. **信息架构与导航**(现第 5、8、28、38、39 章):Server Space 布局、Tab 规则、容器分层、迁移映射表
4. **组件规格**(现第 6~7、9~25 章):每个组件一节,统一模板 = 结构 / 尺寸 token / 状态枚举 / 交互 / 数据源 / 异常态 / 无障碍 / 变体
5. **视觉与 Token**(现第 29~37 章):确定值映射到现有代码 token,亮暗双套,对比度要求,动画数值
6. **横切规范**(新增):异常与错误态全集、空态、后台与生命周期、实时性能约束、可访问性、多窗口适配、凭据存储安全
另外两条具体建议:
- **把"数据可用性"作为每个组件规格的必填字段**。本次审查中 A2(成员状态)、A3(头像)、E2(频道实时性)三处阻断问题的共同根因,是文档从 UI 效果倒推、没有先核对协议层能提供什么。建议在每个组件规格里强制填「数据源 / 是否需改 bridge / 数据未就绪时的表现」。
- **P0 之前先做一次"数据能力对齐"**:列出 `bridge.go` 当前导出的全部字段与事件,标出规范中依赖但缺失的项,形成 bridge 改造清单。这一步不做,P0 的 CurrentChannelBar 和 Voice 都只能做成半成品。
---
## 十、总体评价
| 维度 | 评价 |
| --- | --- |
| 产品模型判断 | 好。Server Space + Current Channel 上下文的方向正确,优于现有三页跳转 |
| 与代码现状的吻合度 | 差。状态机、成员数据、头像、会话范围四处脱节 |
| 完整性 | 不足。异常路径、后台、可访问性、暗色、性能基本空白 |
| 可执行性 | 不足。区间值代替确定值、"建议"代替约束、无迁移映射、无验收标准 |
| 文档治理 | 缺失。无版本信息,未声明与既有三套文档的关系 |
| 内部一致性 | 有 4 处自相矛盾(B3/B4/B7/B8) |
**建议处理方式**:不要按当前版本直接进入实现。先补三样东西——① 效力声明与冲突裁决(第二节表格逐条定调);② 数据能力对齐清单(Go bridge 现有字段 vs 规范依赖字段);③ 状态矩阵(A1)。这三项完成后,第 40 章的组件重构才有确定的输入。其余缺口可在各组件规格细化时逐个补齐。