Files
CloudHome-Ops/docs/手机端UI优化方案-v2.md

1487 lines
78 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.

# ops-console(爱视云管理)手机端 UI 优化方案 **v2**
## —— 回归橙白 · 冻结首页与应用显示图标
| 项 | 内容 |
| --- | --- |
| 项目 | ops-console(包名 `com.ainetv.aishicloud`) |
| 技术栈 | Flutter 3.27.4 / Dart 3.6.2 / Material 3 / go_router / Riverpod |
| 目标设备 | Android 手机;**实测横屏 1600×900(rotation=1)**,同时兼容竖屏 |
| 用户画像 | 门店店员 / 现场运维人员,**常在现场单手操作** |
| 文档作者 | 许清楚(产品经理) |
| **版本** | **v2(本版为唯一有效版本,v1 转为历史存档)** |
| 对标基准 | WCAG 2.1 AA;Material 3 规范;Android 无障碍触控建议 |
**版本沿革**
| 版本 | 状态 | 说明 |
| --- | --- | --- |
| v1 `手机端UI优化方案.md` | **历史存档** | 首次产出,确立橙化方向与 WCAG 实测基线。基线数据仍然有效,但**图标章节已被 v2 覆盖**,请勿据 v1 实施图标改动 |
| **v2(本文档)** | **有效** | ① 基调改为「回归旧版橙白 + 合理化改进」;② 新增「新旧版延续性对照」;③ 图标章节按冻结区重写;④ 导航栏/设置页/详情页/弹窗四类给出代码级指引 |
---
## 0. 本版修订说明(相对 v1)
| # | 变更 | 说明 |
| --- | --- | --- |
| 1 | **基调转换** | 从「重新定义品牌视觉语言」改为「**回归并延续旧版橙白色系 + 合理化改进**」。蓝色 `#2E7DFF` 定性为**新版做偏了的偏离**,不是原有设计 |
| 2 | **新增 §2 新旧版延续性对照** | 用表格明确区分「刻意保留 / 修正偏离 / 本轮改进」三类,一眼看出不是推翻重来 |
| 3 | **§6 图标章节重写** | 先划「冻结区 / 灰区 / 可改区」,再谈规范。v1 中 `Icons.dns → Icons.cloud_*` 的建议**已撤回** |
| 4 | **新增 §8 四类重设计落地指引** | 导航栏 / 设置页 / 详情页 / 弹窗,各给现状问题 + 改造代码 |
| 5 | **配色方案不变** | 双橙策略、WCAG 实测表、浅/深两套 ColorScheme、语义色、中性色阶 —— **全部沿用 v1,一字未改** |
| 6 | **圆角与阴影不变** | 沿用 v1(仅调整论述措辞) |
---
## 1. 结论:不是重新定义,是回归 + 合理化
### 1.1 一句话结论
> **旧版云端手机端就是橙白色系,与 logo `#FF914D` 一致;当前代码里的 `#2E7DFF` 科技蓝是新版做偏了的偏离。本轮的任务是「把颜色纠正回来」,并把「扁平、无触控规范、无横屏适配」这三处真正的落后项补上。**
### 1.2 三项任务的性质区分
| 任务 | 性质 | 用户的态度 |
| --- | --- | --- |
| 主题色蓝 → 橙白 | **修正偏离**(回归旧版) | 确认要做 |
| 首页应用图标 / 应用显示图标 | **不做任何修改** | **硬约束,冻结** |
| 导航栏 / 设置页 / 详情页 / 弹窗 | **重新设计** | 点名要做 |
| 圆角、阴影、触控、横屏 | **合理化改进**(补落后项) | 确认要做 |
> **关键认知**:本轮**不发明新风格**,是"把走偏的拉回来 + 把缺的补上"。因此所有"保留现设计"的地方要明确保留,不能为改而改。
---
## 2. ★ 新旧版延续性对照
### 2.1 基准说明(重要,请先读)
- **「旧版」基准来自用户口述确认**(橙白色系),**非源码基准** —— 当前代码库无 git 仓库、无旧版分支、无备份目录,我无法从源码反推旧版的确切色值。
- 因此下表中「延续」一列的判定依据是:**该特征是否与 logo 实测数据(`#FF914D` / `#FFBE58` / `#FEFEFE`、近白底 85%、无深色成分)一致**。一致即归为"延续旧版橙白",不一致即归为"新版偏离"。
- 若后续能拿到旧版截图或源码包,建议用实际值校准下表 **2.3 节**的「延续值」列。
### 2.2 三类归属总表
| # | 视觉特征 | 旧版(橙白) | 当前 v1 代码 | 本轮归属 | 目标值 |
| --- | --- | --- | --- | --- | --- |
| 1 | 品牌主色 | 橙 `#FF914D` | **`#2E7DFF` 蓝** | **修正偏离** | 行动色 `#C2410C` + 品牌色 `#FF914D` |
| 2 | 整体底色 | 近白 | 近白(M3 默认) | 延续 | 暖白 `#FCFAF8` |
| 3 | 强调/点缀色 | 金黄 | 无 | 延续 + 补全 | 辅金 `#FFBE58` |
| 4 | 明暗倾向 | 明亮轻盈(logo 最暗仅 163) | 默认 `ThemeMode.dark` | **修正偏离** | 默认浅色(待确认) |
| 5 | 圆角风格 | 小圆角、非胶囊(logo 3%) | 卡片 12 / 输入 10 | 延续 + 改进 | 梯度化 4/8/12/16/20/28 |
| 6 | 卡片层次 | 浅阴影、轻漂浮 | `elevation: 1`(几乎扁平) | 改进 | 云感暖阴影 |
| 7 | 首页应用/入口图标 | 既有设计 | 既有设计 | **冻结** | **不动** |
| 8 | 应用显示类图标(TV 应用、Logo、封面占位) | 既有设计 | 既有设计 | **冻结** | **不动** |
| 9 | 底部导航 | 既有设计 | 既有设计(配色随蓝) | **点名重设计** | 见 §8.1 |
| 10 | 设置页 | 既有设计 | `SegmentedButton` 挤在 `trailing` | **点名重设计** | 见 §8.2 |
| 11 | 详情页 | 既有设计 | label 列写死 88、Grid 写死 2 列 | **点名重设计** | 见 §8.3 |
| 12 | 弹窗 | 既有设计 | 无限高、无把手、无安全区 | **点名重设计** | 见 §8.4 |
| 13 | 触控目标 | — | 无规范 | 改进(补缺) | ≥48×48,间距 ≥8 |
| 14 | 横屏适配 | — | **0 处断点** | 改进(补缺) | 见 §7 |
### 2.3 【延续】刻意保留的(不要动)
| 特征 | 保留理由 | 本轮动作 |
| --- | --- | --- |
| 首页统计卡 4 个图标(`Icons.devices` / `Icons.store` / `Icons.check_circle_outline` / `Icons.highlight_off`) | 用户点名冻结 | **零改动**(含 `size: 28`) |
| 首页快捷入口 3 个图标(`Icons.devices` / `Icons.store` / `Icons.people`) | 用户点名冻结(首页应用图标) | **零改动**(含 `CircleAvatar` 结构) |
| TV 应用图标 `Icons.apps` ×2 | 用户点名冻结(应用显示相关) | **零改动** |
| 门店主页预览 Logo 占位 `Icons.storefront` | 应用显示相关 | **零改动** |
| 主题缩略图占位 `Icons.image_outlined` | 应用显示相关 | **零改动** |
| 品牌编辑器 Logo 占位 `Icons.image` | 应用显示相关 | **零改动** |
| 底部导航 5 个 tab 的**图标语义与配对方式**(线性/填充) | 语义准确、已符合 M3 | **保留图标本身**,仅改配色与容器形态(§8.1) |
| 近白底 + 深色文字的整体明暗关系 | 与 logo 明度一致(均值 245/255) | 延续并强化(暖白 `#FCFAF8`) |
| 居中标题 AppBar | 现状如此、符合中文习惯 | 延续 |
| 登录页 `maxWidth: 420` 的内容约束 | 是正确的做法 | 延续并推广到全站(§7) |
| 首页 Grid 2 列、快捷入口 3 个的**信息架构** | 不是本轮问题 | 保留结构,仅横屏下自动增列 |
### 2.4 【修正偏离】蓝色 → 橙白(本轮核心)
| 位置 | 现值 | 目标 | 说明 |
| --- | --- | --- | --- |
| `lib/app/theme.dart:8` | `#2E7DFF` | `#C2410C` | 主题主色,全站最关键的偏离 |
| `login_page.dart:205/233/280` | `#2E7DFF` | `scheme.primary` | 输入框聚焦边框 |
| `login_page.dart:350` | `#2E7DFF` | `scheme.primary` | 记住密码勾选框 |
| `login_page.dart:370` | `#2E7DFF` | `scheme.primary` | 登录按钮 |
| `login_page.dart:372` | `#1A4480` | `onSurface` 12% | 按钮禁用态 |
| `login_page.dart:149` | **`#0D2137`(深蓝)** | `scheme.surfaceContainerLow` | **登录卡片底色是深蓝,与暖橙直接冲突** |
| `login_page.dart:152` | `#121212` | `scheme.surface` | 冷黑 → 暖黑 |
| `store_theme_grid.dart:77` | `#2E7DFF` | `scheme.primary` | 主题模板选中边框 |
| `region_picker.dart:189` | `Colors.blue` | `scheme.primary` | 选中勾选图标 |
| `home_page.dart:78` | `Colors.blue` | 见 §6.1 注 | **落在冻结区,需用户确认** |
| `admin_hub_page.dart:19` | `Colors.blue` | 见 §6.2 注 | **落在灰区,需用户确认** |
| `org_tree_page.dart:282` | `Colors.blue` | 语义色 | 组织类型「渠道」标签色 |
| `alerts_page.dart:19` | `Colors.blue` | `AppSemanticColors.info` | 告警「信息」级 |
| `theme_provider.dart:7` | 默认 `ThemeMode.dark` | 建议 `light` / `system` | **与 logo"明亮轻盈"背离**(待确认) |
> **排查提醒**:`store_theme_preview.dart:110-115`、`store_theme_grid.dart:129-134`、`models/store_theme.dart:46-54` 里也有蓝色(`#3B82F6`、`#1565C0`…),但那是**下发给 TV 端的门店主题业务数据**,**不是 App 主题色,绝对不能动**。详见 §10.1。
### 2.5 【改进】本轮合理化提升(补落后项)
| # | 改进项 | 现状 | 目标 | 章节 |
| --- | --- | --- | --- | --- |
| 1 | 建立圆角梯度 | 12 / 10 两个孤立值 | 4/8/12/16/20/28/999 七档 | §5.1 |
| 2 | 云感暖阴影 | `elevation: 1`(几乎无层次) | 暖棕低透明、模糊:偏移 ≈ 4:1 | §5.2 |
| 3 | 触控规范 | 无 | ≥48×48、间距 ≥8、主按钮 52 | §7.1 |
| 4 | 横屏 1600×900 适配 | **0 处断点** | 断点工具 + Rail + 双栏 + 限宽 | §7.4 |
| 5 | 弹窗结构化 | 无限高/无把手/无安全区 | 限高 + 把手 + 吸底栏 + 安全区 | §8.4 |
| 6 | 设置页控件布局 | `SegmentedButton` 塞 `trailing`(**实测会挤爆**) | 全宽独立行 | §8.2 |
| 7 | 详情页响应式 | label 列写死 88、Grid 写死 2 列 | 响应式 | §8.3 |
| 8 | 状态栏图标亮度 | 硬编码 `Brightness.light`(**真 bug**) | 跟随主题 | §9 P0-4 |
| 9 | 语义色统一管理 | 散落 20+ 处硬编码 | `ThemeExtension` | §4.5 |
---
## 3. 设计语言(延续 + 改进)
### 3.1 logo 实测数据(v1 沿用,未改)
logo `assets/images/logo.png`(192×192,RGBA):
| 维度 | 实测值 | 推导 |
| --- | --- | --- |
| 容器形状 | 圆角矩形,圆角 ≈ 6px / 192px = **3%** | **克制的小圆角**,非胶囊 |
| 主色 `#FF914D` | 4.4% 非透明像素,**图形中心像素** | **品牌主色(暖橙)** |
| 金黄 `#FFBE58` | 1.9% | **品牌辅助色** |
| 渐变带 | `#FFA36A` / `#FFB487` / `#FFC56C` / `#FCE2CB` | 橙→金渐变 = 云朵受光 |
| 中性 `#DBBB88` | 3.5% | 中性色阶需**偏暖,不用纯灰** |
| 近白 `#FEFEFE` | 85.0% | 大面积留白、明亮轻盈 |
| 明度 | 均值 245/255,**最暗仅 163** | 无深色成分 → **浅色是品牌主场** |
| 橙色行分布 | `[0,0,0,18,34,28,13,85,103,71,45,11,35,0,0,0]` | 中部双峰 → 云朵多圆弧叠加 |
### 3.2 视觉关键词(延续旧版橙白的判据)
> **云 · 轻盈 · 暖橙 · 明亮 · 圆润弧线 · 大留白**
四条可执行判据(**与旧版橙白一致,不是新发明**):
1. 中性色一律**暖偏移**(R>G>B),禁用纯灰 `#9E9E9E` / 纯黑 `#000000`;
2. 阴影用**暖棕低透明**而非纯黑,大模糊、小偏移 → "托起"而非"压住";
3. 圆角**中等偏大但不成胶囊**,**顶部 ≥ 底部**(云朵上缘多弧、下缘平缓);
4. 品牌亮橙 `#FF914D` **只做大色块/渐变/插画/图表**,不做文字与图标色。
---
## 4. 配色体系(**完全沿用 v1,未作修改**)
### 4.1 关键约束:logo 亮橙不能配白字(WCAG 实测)
| 组合 | 对比度 | 结论 |
| --- | --- | --- |
| 白字 `#FFFFFF` on `#FF914D` | **2.23** | ❌ 不达标(正文需 4.5,大字需 3.0) |
| 白字 on `#FFBE58` | **1.65** | ❌ 严重不达标 |
| 深棕 `#3D1A00` on `#FF914D` | **6.99** | ✅ 达标 |
| 黑字 `#000000` on `#FF914D` | **9.41** | ✅ 达标(但纯黑偏硬,推荐深棕) |
### 4.2 双橙策略(本方案配色的核心,与旧版橙白一致)
| 角色 | 色值 | 用途 | 配字 |
| --- | --- | --- | --- |
| **品牌色 Brand** | `#FF914D` | 插画/渐变/图表/Banner 大色块/Splash/进度条 | 深棕 `#3D1A00`(6.99 ✅) |
| **行动色 Primary** | `#C2410C` | 主按钮/导航选中/可点文字/图标 | 白 `#FFFFFF`(5.18 ✅) |
| **辅金 Accent** | `#FFBE58` | 高亮/到期提醒/星级/推荐位 | 深棕(9.48 ✅) |
`#C2410C` = `#FF914D` 沿同色相(H≈23°)压暗到可承载白字。
> **一句话记忆:看用亮橙 `#FF914D`,点用深橙 `#C2410C`。**
### 4.3 浅色 ColorScheme(Light)
```dart
static const ColorScheme _light = ColorScheme(
brightness: Brightness.light,
primary: Color(0xFFC2410C), onPrimary: Color(0xFFFFFFFF), // 5.18 ✅
primaryContainer: Color(0xFFFFD3B3), onPrimaryContainer: Color(0xFF4A1D00), // 10.38 ✅
secondary: Color(0xFF8A5A3A), onSecondary: Color(0xFFFFFFFF), // 5.83 ✅
secondaryContainer: Color(0xFFF5DFCB), onSecondaryContainer: Color(0xFF5A3A22), // 7.90 ✅
tertiary: Color(0xFF9A6000), onTertiary: Color(0xFFFFFFFF), // 5.19 ✅
tertiaryContainer: Color(0xFFFFE0A3), onTertiaryContainer: Color(0xFF5C3D00), // 7.75 ✅
error: Color(0xFFB02A20), onError: Color(0xFFFFFFFF), // 6.57 ✅
errorContainer: Color(0xFFFFDDD8), onErrorContainer: Color(0xFF7A2A22), // 7.60 ✅
surface: Color(0xFFFCFAF8), onSurface: Color(0xFF1A1614), // 17.26 ✅
onSurfaceVariant: Color(0xFF6E655C), // 5.48 ✅
surfaceContainerLowest: Color(0xFFFFFFFF),
surfaceContainerLow: Color(0xFFF7F3EF),
surfaceContainer: Color(0xFFF2EDE8),
surfaceContainerHigh: Color(0xFFECE5DF),
surfaceContainerHighest: Color(0xFFE5DED7),
outline: Color(0xFF8A8178), outlineVariant: Color(0xFFDCCFC2), // 3.67 ✅
surfaceTint: Color(0xFFC2410C), shadow: Color(0xFF5C2E12),
scrim: Color(0xFF1A1614),
inverseSurface: Color(0xFF2E2823), onInverseSurface: Color(0xFFF7EFE9),
inversePrimary: Color(0xFFFFB487),
);
```
### 4.4 深色 ColorScheme(Dark)
```dart
static const ColorScheme _dark = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFFFFB487), onPrimary: Color(0xFF542000), // 7.66 ✅
primaryContainer: Color(0xFF63300D), onPrimaryContainer: Color(0xFFFFDCC2), // 8.30 ✅
secondary: Color(0xFFE8C39B), onSecondary: Color(0xFF542000), // 8.03 ✅
secondaryContainer: Color(0xFF5A3A22), onSecondaryContainer: Color(0xFFFFDCC2), // 7.89 ✅
tertiary: Color(0xFFF2BE62), onTertiary: Color(0xFF542000), // 7.78 ✅
tertiaryContainer: Color(0xFF5C3D00), onTertiaryContainer: Color(0xFFFFDCC2), // 7.66 ✅
error: Color(0xFFFF8474), onError: Color(0xFF5A1610), // 5.65 ✅
errorContainer: Color(0xFF7A2A22), onErrorContainer: Color(0xFFFFC9C2), // 6.59 ✅
surface: Color(0xFF16130F), onSurface: Color(0xFFF3E9E2), // 15.49 ✅
onSurfaceVariant: Color(0xFFC4B8AE), // 9.54 ✅
surfaceContainerLowest: Color(0xFF0F0D0A),
surfaceContainerLow: Color(0xFF1E1A16),
surfaceContainer: Color(0xFF23201B),
surfaceContainerHigh: Color(0xFF2D2924),
surfaceContainerHighest: Color(0xFF38332D),
outline: Color(0xFF7A7068), outlineVariant: Color(0xFF453F39), // 3.83 ✅
surfaceTint: Color(0xFFFFB487), shadow: Color(0xFF000000),
scrim: Color(0xFF0F0D0A),
inverseSurface: Color(0xFFF3E9E2), onInverseSurface: Color(0xFF2E2823),
inversePrimary: Color(0xFFC2410C),
);
```
### 4.5 语义色(**警告不用橙**——会与品牌橙语义打架,改用琥珀黄)
| 语义 | Light 主色 | Light 容器 / 文字 | Dark 主色 | Dark 容器 / 文字 |
| --- | --- | --- | --- | --- |
| 成功 | `#1E7A4E` (5.11 ✅) | `#D3EDDD` / `#1E5138` (7.39 ✅) | `#5FD398` (9.93 ✅) | `#1E5138` / `#A8E6C4` (6.46 ✅) |
| 警告 | `#8A5C00` (5.58 ✅) | `#FFE9B8` / `#5C4100` (7.96 ✅) | `#F0BE4E` (10.74 ✅) | `#5C4100` / `#FFDCA8` (7.26 ✅) |
| 错误 | `#B02A20` (6.31 ✅) | `#FFDDD8` / `#7A2A22` (7.60 ✅) | `#FF8474` (7.76 ✅) | `#7A2A22` / `#FFC9C2` (6.59 ✅) |
| 信息 | `#0B6397` (6.21 ✅) | `#D7E9FA` / `#1A4763` (7.96 ✅) | `#7FC4F0` (9.75 ✅) | `#1A4763` / `#B8DCF5` (6.87 ✅) |
通过 `ThemeExtension` 暴露(完整代码见 v1 §3.5 或本文档 §12 附录 B)。
### 4.6 暖中性色阶(全部 R>G>B 暖偏移,亮度严格单调)
**Light**:`#FFFFFF`(1.000) > `#FCFAF8`(0.958) > `#F7F3EF`(0.901) > `#F2EDE8`(0.853) > `#ECE5DF`(0.792) > `#E5DED7`(0.738) > `#DCCFC2`(0.637) > `#C8BFB4`(0.528) > `#8A8178`(0.225) > `#6E655C`(0.134) > `#1A1614`(0.008) > `#0F0D0A`(0.004)
**Dark**:`#F3E9E2`(0.828) > `#C4B8AE`(0.491) > `#7A7068`(0.167) > `#453F39`(0.051) > `#38332D`(0.034) > `#2D2924`(0.023) > `#23201B`(0.015) > `#1E1A16`(0.011) > `#16130F`(0.007) > `#0F0D0A`(0.004)
### 4.7 换色对可读性的影响(实测)
| 场景 | 旧(蓝 `#2E7DFF`) | 新(深橙 `#C2410C`) | 影响 |
| --- | --- | --- | --- |
| 主按钮白字 | 3.68(**原本就不达 AA**) | **5.18 ✅** | **提升** |
| 主色作文字/图标 | 3.68 | **4.97 ✅** | **提升** |
| 选中态图标 on 容器 | — | **7.90 ✅** | 达标 |
| 亮橙作图标(**禁止**) | — | 1.66 ❌ | **新增约束** |
> 换色顺带修复了一个一直存在的无障碍问题。这是"回归橙白"的额外收益。
---
## 5. 圆角与阴影(沿用 v1)
### 5.1 圆角梯度
logo 的 3% **不能等比照搬**(等比后按钮仅 1.4dp、卡片 4.2dp,生硬且与"云"无关)。
**可继承的是三条定性特征:非胶囊 / 四角一致 / 克制感**;定量落点 =「容器短边的 5%~10%」→ 12~28dp。
```dart
class AppRadius {
static const double xs = 4; // 进度条、缩略图角
static const double sm = 8; // Chip、SnackBar、小标签
static const double md = 12; // 输入框(沿用现值,保持手感连续)
static const double lg = 16; // ★ 标准卡片、按钮(主力值)
static const double xl = 20; // 分组容器、大 Banner 卡
static const double xxl = 28; // ★ 底部弹窗顶部两角(云朵弧线核心表达)
static const double full = 999; // 仅状态 Chip / 头像 / pill
static const BorderRadius sheetTop =
BorderRadius.vertical(top: Radius.circular(xxl));
}
```
> **注意**:M3 `FilledButton` 默认圆角 20(接近胶囊),必须显式覆盖为 `lg`(16),否则与卡片圆角脱节。
### 5.2 云感阴影(替代 `elevation: 1`)
现状 `elevation: 1` 的阴影由引擎按固定曲线绘制,**颜色可改、模糊/偏移曲线不可控**。A/B 双轨:
- **A(全局低风险)**:`ThemeData(shadowColor: #5C2E12)` + `CardThemeData(elevation: 0)`
- **B(推荐)**:新建 `CloudCard` 用 `BoxShadow` 精确控制
```dart
class AppShadows {
static const List<BoxShadow> xs = [BoxShadow(color: Color(0x0F5C2E12), blurRadius: 8, offset: Offset(0, 2), spreadRadius: -1)]; // 6%
static const List<BoxShadow> sm = [BoxShadow(color: Color(0x145C2E12), blurRadius: 12, offset: Offset(0, 4), spreadRadius: -2)]; // 8%
static const List<BoxShadow> md = [BoxShadow(color: Color(0x1A5C2E12), blurRadius: 20, offset: Offset(0, 8), spreadRadius: -4)]; // 10%
static const List<BoxShadow> lg = [BoxShadow(color: Color(0x245C2E12), blurRadius: 32, offset: Offset(0, 12), spreadRadius: -6)]; // 14%
static const List<BoxShadow> xl = [BoxShadow(color: Color(0x245C2E12), blurRadius: 48, offset: Offset(0, 20), spreadRadius: -8)]; // 14%
/// 深色:暖色阴影在暗底不可见(实测对比仅 1.05),改用纯黑 + 描边
static const List<BoxShadow> dark = [BoxShadow(color: Color(0x66000000), blurRadius: 24, offset: Offset(0, 8), spreadRadius: -4)];
}
```
三条依据:
1. **模糊:偏移 ≈ 4:1**(M2/M3 默认约 2:1 偏硬)→ 光是漫射的,像云影;
2. **暖棕 `#5C2E12` 而非纯黑** → 这是"暖"的关键;
3. **深色模式换纯黑 α=40% + `outlineVariant` 描边**(实测暖棕 6% 在 `#16130F` 上对比仅 1.05,完全不可见)。
> M3 elevation→染色曲线实测 `1→5% / 3→8% / 6→11%`。保留 `surfaceTintColor = primary`,全站卡片可白拿 5~8% 暖橙染色 —— 零成本的品牌渗透。
---
## 6. ★ 图标风格(**本版重大修订:冻结区先行**)
> ### ⛔ 实施前必读
> 用户原话:「**首页的应用图标及与应用显示相关的图标维持原有设计不做任何修改。**」
> 本章所有规范**只在 §6.3「C 区|可改区」内生效**。A 区、B 区一律不动。
> **v1 中「全项目 `Icons.xxx` → `_outlined`」的批量替换建议,必须先剔除 A/B 区才能执行。**
### 6.0 冻结区判定规则(按「图标实例」判定,不是按文件)
**判据:看这个图标"代表什么",而不是"在哪个文件里"。**
| 图标代表的对象 | 判定 | 举例 |
| --- | --- | --- |
| **应用本体 / 展示内容本体**(APP 图标、Logo、封面、缩略图、视频、门店主页 Logo) | **冻结** | `Icons.apps`、`Icons.storefront`、`Icons.video_library` |
| **模块入口**(首页快捷入口、管理中心网格入口) | **冻结**(用户点名"首页应用图标") | `Icons.devices`、`Icons.store`、`Icons.people` |
| **通用交互动作**(搜索、清除、新增、关闭、下拉、勾选、返回、编辑、删除) | **可改** | `Icons.search`、`Icons.clear`、`Icons.add` |
| **状态 / 属性**(在线、离线、告警、时间、地址、电话、分组) | **可改**(设备图标除外,见灰区) | `Icons.location_on`、`Icons.phone`、`Icons.access_time` |
> **⚠️ 这条规则的关键后果**:同一个文件里可能既有冻结项又有可改项。
> 例:`tv_app_page.dart` 在 `admin/app/` 目录下,但只有 `:301` `:304` 的 `Icons.apps`(应用本体)冻结;
> `:245` `Icons.search`、`:249` `Icons.clear`、`:341` `Icons.add`(通用交互)**不在冻结区,可统一**。
> **不要按文件整体冻结或整体放行。**
**四个分区一览**
| 区 | 名称 | 数量 | 性质 |
| --- | --- | --- | --- |
| **A** | 绝对冻结 | **12** | 用户硬约束,禁止任何修改 |
| **B** | 灰区(建议同冻结) | **26** | 性质同 A,需用户一句话确认 |
| **C** | 可改区 | 其余全部 | 本轮重设计范围 |
| **D** | 后端契约冻结 | **10** | **不是设计问题**,改了会破坏与 Web 后台的一致性(§6.2.1) |
> 另有 **1 项待确认**(首页统计卡四色硬编码),见 §6.1 末。
### 6.1 A 区|**绝对冻结清单(禁止任何修改)**
#### A1|首页 `lib/screens/home/home_page.dart`(用户点名「首页应用图标」)
| 文件:行 | 图标 | 尺寸 | 位置 / 用途 |
| --- | --- | --- | --- |
| `home_page.dart:77` | `Icons.devices` | 28 | 统计卡「设备总数」 |
| `home_page.dart:83` | `Icons.store` | 28 | 统计卡「门店总数」 |
| `home_page.dart:89` | `Icons.check_circle_outline` | 28 | 统计卡「在线设备」 |
| `home_page.dart:95` | `Icons.highlight_off` | 28 | 统计卡「离线设备」 |
| `home_page.dart:114` | `Icons.devices` | 默认(24) | 快捷入口「设备管理」(`CircleAvatar` 内) |
| `home_page.dart:121` | `Icons.store` | 默认(24) | 快捷入口「门店管理」(`CircleAvatar` 内) |
| `home_page.dart:128` | `Icons.people` | 默认(24) | 快捷入口「用户管理」(`CircleAvatar` 内) |
**冻结范围**:`IconData` 图形、`size`、布局结构、入口数量与顺序、快捷入口的 `CircleAvatar` 结构。
#### A2|应用显示相关图标(用户点名「与应用显示相关的图标」)
| 文件:行 | 图标 | 尺寸 | 用途 |
| --- | --- | --- | --- |
| `lib/screens/admin/app/tv_app_page.dart:301` | `Icons.apps` | 40 | TV 应用**网络图标加载失败时的占位** |
| `lib/screens/admin/app/tv_app_page.dart:304` | `Icons.apps` | 默认(24) | TV 应用**无 iconUrl 时的默认应用图标**(`CircleAvatar` 内) |
| `lib/widgets/store_theme_preview.dart:105` | `Icons.storefront` | 26 | **门店主页头部预览**的 Logo 占位图标 |
| `lib/widgets/store_theme_group_list.dart:174` | `Icons.image_outlined` | 28 | **主题页缩略图预览**占位图标 |
| `lib/widgets/branding_editor.dart:155` | `Icons.image` | 28 | **启动图 / Logo** 角标加载失败占位 |
> **A 区合计:12 处(A1 七处 + A2 五处)**
#### ⚠️ A 区待确认项:首页统计卡的四色硬编码
`home_page.dart:78` `Colors.blue`、`:84` `Colors.purple`、`:90` `Colors.green`、`:96` `Colors.red` —— 这四个是**统计卡图标的配色**,严格说属于"图标设计的一部分",落在冻结语义内;但其中 `Colors.blue` 是全站最显眼的蓝色之一,若不动会与"回归橙白"直接打架。
**本轮默认动作:冻结不动**(尊重字面指令)。**建议向用户确认**,两种选法:
| 选项 | 做法 | 后果 |
| --- | --- | --- |
| **① 保持现状(默认)** | 四色硬编码原样保留 | 首页仍有蓝/紫/绿/红四色,与新版橙白主题并存,观感割裂 |
| **② 改为语义色(推荐)** | 设备=`primary`、门店=`tertiary`、在线=`success`、离线=`onSurfaceVariant` | 图标图形与尺寸零改动(符合冻结),仅配色纳入新体系,与"回归橙白"一致 |
### 6.2 B 区|**建议一并按冻结处理(灰区,需用户一句话确认)**
**理由**:下列图标性质与 A2 相同 —— 都是**下发给 TV / 门店端展示内容的缩略图占位图形**,属于"应用显示相关",只是不在 `admin/app/` 目录下。保守起见本轮不动。
| 文件:行 | 图标 | 尺寸 | 用途 |
| --- | --- | --- | --- |
| `lib/screens/admin/intro/intro_category_page.dart:375` | `Icons.image` | 40 | 分类封面**加载失败**占位 |
| `intro_category_page.dart:378` | `Icons.folder` | 40 | 分类**无封面**占位 |
| `intro_category_page.dart:436` | `Icons.image` | 40 | 内容封面**加载失败**占位 |
| `intro_category_page.dart:439` | `Icons.article` | 40 | 内容**无封面**占位 |
| `intro_category_page.dart:1219` | `Icons.broken_image` | 48 | 预览图**损坏**占位 |
| `intro_category_page.dart:1226` | `Icons.image` | 48 | 预览图**空态**占位 |
| `lib/screens/admin/intro/intro_store_content_page.dart:332` | `Icons.image` | 32 | 分类封面**加载失败**占位 |
| `intro_store_content_page.dart:335` | `Icons.folder` | 32 | 分类**无封面**占位 |
| `intro_store_content_page.dart:407` | `Icons.image` | 32 | 内容封面**加载失败**占位 |
| `intro_store_content_page.dart:410` | `Icons.article` | 32 | 内容**无封面**占位 |
| `intro_store_content_page.dart:1100` | `Icons.broken_image` | 48 | 预览图**损坏**占位 |
| `intro_store_content_page.dart:1107` | `Icons.image` | 48 | 预览图**空态**占位 |
| `lib/screens/admin/media/media_list_page.dart:336` | `Icons.video_library` | 24 | 媒资缩略图占位 |
| `media_list_page.dart:347` | `Icons.video_library` | 24 | 媒资缩略图占位 |
| `lib/screens/admin/media/media_rank_page.dart:209` | `Icons.video_library` | 默认 | 排行缩略图占位 |
| `media_rank_page.dart:215` | `Icons.video_library` | 默认 | 排行缩略图占位 |
| `media_rank_page.dart:323` | `Icons.video_library` | 20 | 排行缩略图占位 |
| `media_rank_page.dart:330` | `Icons.video_library` | 20 | 排行缩略图占位 |
#### B 区另两类(性质不同,但同样建议先不动)
| 文件:行 | 图标 | 说明 |
| --- | --- | --- |
| `lib/widgets/device_list_tile.dart:26` | `Icons.dns` | 设备在线图标 |
| `device_list_tile.dart:28` | `Icons.dns_outlined` | 设备离线图标 |
| `device_list_tile.dart:30` | `Icons.warning_amber` | 设备告警图标 |
| `lib/screens/admin/admin_hub_page.dart:18` | `Icons.devices` | 管理入口网格「设备管理」 |
| `admin_hub_page.dart:24` | `Icons.vpn_key` | 管理入口网格「激活码」 |
| `admin_hub_page.dart:30` | `Icons.people` | 管理入口网格「用户管理」 |
| `admin_hub_page.dart:36` | `Icons.business` | 管理入口网格「公司介绍」 |
| `admin_hub_page.dart:42` | `Icons.subtitles` | 管理入口网格「滚动字幕」 |
- **设备图标**:`device_list_page` 是底部导航第 2 个 tab(**不是"首页"**),也不是"应用显示" → 严格说不在冻结区。但 `Icons.dns`(机柜)是**设备本体的代表图形**,性质接近 A2;且 **v1 曾建议改成 `Icons.cloud_*`**,本轮予以**撤回**,列入灰区待确认。
- **管理中心网格**:与首页快捷入口**同性质(模块入口图标)**,只是不在首页。若冻结逻辑成立,理应一并冻结。
> **B 区合计:26 处(18 处内容占位 + 3 处设备图标 + 5 处管理入口)**
> **建议**:请用户一句话确认「B 区是否同 A 区一并冻结」。若确认,本轮**连 A 区共 38 处图标全部不动**,图标改动范围收缩到 C 区。
### 6.2.1 D 区|**因后端契约冻结(不是设计选择,是接口约定)**
> **⚠️ 这一区与用户的冻结指令无关,但风险等级最高 —— 改了会直接造成线上功能错乱,而实施者极易误改。**
`lib/screens/admin/sys/menu_page.dart:277-300` 的 `_iconData(String icon)` 是**后台 `sys_menu` 表 `icon` 字段 → `IconData` 的映射表**:
| 文件:行 | 图标 | 后端 key |
| --- | --- | --- |
| `menu_page.dart:280` | `Icons.home` | `'home'` |
| `menu_page.dart:282` | `Icons.settings` | `'settings'` |
| `menu_page.dart:284` | `Icons.person` | `'person'` |
| `menu_page.dart:286` | `Icons.people` | `'people'` |
| `menu_page.dart:288` | `Icons.menu` | `'menu'` |
| `menu_page.dart:290` | `Icons.list` | `'list'` |
| `menu_page.dart:292` | `Icons.dashboard` | `'dashboard'` |
| `menu_page.dart:294` | `Icons.devices` | `'devices'` |
| `menu_page.dart:296` | `Icons.notifications` | `'notifications'` |
| `menu_page.dart:298` | `Icons.folder` | `default` |
**冻结理由**:这些 `IconData` 由**后端数据决定**,与 Web 后台的菜单图标配置是一套约定。若实施 P1-12「统一 `_outlined`」时把 `Icons.home` 改成 `Icons.home_outlined`,**移动端与 Web 后台的菜单图标将不再一致**,且后端若新增 key 会更难对齐。
**处置**:**绝对冻结,10 处全部不动**,且**不进 B 区待确认流程**(没有商量余地)。若确需统一风格,正确做法是**扩展映射表**(新增 key → outlined 图标),而非替换现有映射。
### 6.3 C 区|**可改清单(本轮重设计范围)**
#### C1|用户点名的四类
| 类别 | 文件 | 涉及图标(文件:行) |
| --- | --- | --- |
| **导航栏** | `lib/widgets/app_bottom_nav.dart` | `:32` `Icons.home_outlined` / `:33` `Icons.home`<br>`:37` `Icons.devices_outlined` / `:38` `Icons.devices`<br>`:42` `Icons.store_outlined` / `:43` `Icons.store`<br>`:47` `Icons.admin_panel_settings_outlined` / `:48` `Icons.admin_panel_settings`<br>`:52` `Icons.person_outline` / `:53` `Icons.person` |
| **设置页** | `lib/screens/profile/settings_page.dart` | `:166` `Icons.person`、`:177` `Icons.dark_mode_outlined`、`:184` `Icons.dark_mode`、`:188` `Icons.light_mode`、`:192` `Icons.settings_suggest`、`:203` `Icons.language_outlined`、`:233` `Icons.verified_user_outlined`、`:268` `Icons.lock_outline`、`:275` `Icons.info_outline`、`:294` `Icons.logout` |
| **详情页** | `lib/screens/home/device_detail_page.dart` | `:320` `Icons.memory`、`:328` `Icons.storage`、`:337` `Icons.sd_storage`、`:346` `Icons.network_check`、`:507` `Icons.dvr`、`:544` `Icons.meeting_room` |
| | `lib/screens/store/store_detail_page.dart` | 16 行桩页,无图标,按新规范实现 |
| **弹窗** | `lib/widgets/org_picker_sheet.dart` | `:109` `Icons.close`、`:126` `Icons.search`、`:130` `Icons.clear`、`:192` `Icons.check_circle`、`:321` `Icons.account_tree`、`:322` `Icons.arrow_drop_down` |
| | `lib/widgets/device_command_sheet.dart` | 纯 `ActionChip`,无图标 |
| | `lib/widgets/region_picker.dart` | `:189` `Icons.check`(含 `Colors.blue`)、`:190` `Icons.chevron_right` |
| | `lib/screens/store/store_list_page.dart` 弹窗内 | `:1265` `Icons.store`、`:1278` `Icons.location_on`、`:1292` `Icons.arrow_drop_down`、`:1303` `Icons.home`、`:1348` `Icons.devices`、`:1361` `Icons.vpn_key`、`:1376` `Icons.phone`、`:1220` `Icons.check_circle_outline`/`Icons.error_outline` |
#### C2|通用交互图标(**全项目统一,含冻结区文件内部**)
以下图标出现在任何位置(**包括 A/B 区所在的文件里**)都可统一,因为它们代表通用交互动作,不是"应用/内容本体":
`Icons.search` · `Icons.clear` · `Icons.close` · `Icons.add` · `Icons.edit` · `Icons.delete` · `Icons.refresh` · `Icons.check` · `Icons.chevron_right` · `Icons.arrow_drop_down` · `Icons.arrow_upward` · `Icons.arrow_downward` · `Icons.expand_less` / `Icons.expand_more` · `Icons.upload` · `Icons.copy` · `Icons.save` · `Icons.password` · `Icons.restart_alt`
> **注意**:`tv_app_page.dart:341` 的 `Icons.add`、`app_version_page.dart:353` 的 `Icons.add`、`:270` 的 `Icons.search` —— 都在 `admin/app/` 目录下,但**属于 C2,可改**。
#### C3|状态/属性图标(可改)
`Icons.location_on` · `Icons.phone` · `Icons.access_time` · `Icons.event` · `Icons.vpn_key` · `Icons.folder` · `Icons.folder_outlined` · `Icons.tag` · `Icons.label` · `Icons.category` · `Icons.account_tree` · `Icons.account_tree_outlined` · `Icons.error_outline` · `Icons.warning_amber` · `Icons.check_circle_outline` · `Icons.circle`(StatusChip 小圆点)
### 6.4 对 v1 图标建议的撤回与修正
| v1 建议 | v2 处置 | 原因 |
| --- | --- | --- |
| `Icons.dns` / `Icons.dns_outlined` → `Icons.cloud_done_outlined` / `Icons.cloud_off_outlined` | **❌ 撤回**(转 B 区待确认) | 设备图标属"设备本体代表图形",性质接近冻结区;且 `device_list_page` 是底部导航 tab,是否属"首页"存在歧义。**在用户澄清前不动** |
| 全项目 `Icons.xxx` → `Icons.xxx_outlined` 批量替换 | **⚠️ 限定范围后保留** | **必须先剔除 A 区 12 处 + B 区 26 处**,仅对 C 区执行。执行前须用 §6.1/§6.2 清单逐行比对 |
| 语义色图标映射表(在线/离线/告警→云图标) | **❌ 整表撤回** | 该表核心就是换图标,与冻结指令冲突 |
| 统一 `IconThemeData(size: 24, fill: 0)` | **⚠️ 修正** | `fill/weight/grade/opticalSize` 需可变字体支持,Flutter 内置 `MaterialIcons` **不支持**,当前是 **no-op**。P0 靠统一使用 `_outlined` 变体落地(仅 C 区);P2 再引入 Material Symbols 字体让四轴生效 |
### 6.5 冻结区外的统一图标语言(仅 C 区生效)
| 维度 | 规范 |
| --- | --- |
| 风格 | **线性(Outlined)为默认**,填充态仅用于"选中" |
| 描边 | 1.8 ~ 2.0 dp(24dp 栅格下) |
| 端点/连接 | **圆角端点 + 圆角连接** —— 呼应云朵弧线最直接的手法 |
| 栅格 | 24dp 网格 + 2dp 内边距 |
| 禁用 | 直角矩形类(`Icons.square`、`Icons.stop`) |
```dart
// 尺寸栅格
class AppIconSize {
static const double inline = 16; // 正文行内
static const double dense = 20; // 密集信息行
static const double regular= 24; // ★ 默认:列表 leading、按钮内、AppBar actions
static const double medium = 28; // 卡片强调图标
static const double large = 32; // 列表主 leading
static const double display= 48; // 空状态 / 结果页
static const double hero = 64; // 关于页 logo、引导页
}
// 主题层(注意 fill 当前为 no-op,见 §6.4)
iconTheme: IconThemeData(
size: AppIconSize.regular,
color: scheme.onSurfaceVariant,
fill: 0.0, weight: 400, grade: 0, opticalSize: 24,
),
```
---
## 7. 移动端布局与触控(沿用 v1)
### 7.1 触控规范
| 项 | 规范 |
| --- | --- |
| 最小触控区 | **48×48 dp** |
| 相邻可点元素间距 | **≥ 8 dp** |
| 主操作按钮高度 | ≥ 52 dp |
| 输入框高度 | ≥ 56 dp |
| 列表项高度 | 单行 ≥56,多行 ≥72 |
```dart
materialTapTargetSize: MaterialTapTargetSize.padded,
visualDensity: VisualDensity.standard,
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
minimumSize: const Size(48, 48), // 默认仅 40×40
tapTargetSize: MaterialTapTargetSize.padded,
padding: EdgeInsets.zero,
),
),
filledButtonTheme: FilledButtonThemeData(style: FilledButton.styleFrom(
minimumSize: const Size(64, 52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.lg)),
)),
```
> **误触风险最高处**:`device_list_tile.dart:81` 的 `IconButton` 与 `StatusChip` 挤在同一 `Row` 内、**间距为 0**。建议补 8dp 间距,或把"发送指令"改为长按/左滑。
### 7.2 单手操作
- AppBar `actions` **最多 2 个**(右上角是单手最难触及区);
- 破坏性操作(删除)放**弹窗吸底栏**,不放 AppBar;
- 长表单主操作按钮**吸底**,不埋在滚动区末尾;
- FAB 包 `SafeArea`。
### 7.3 窄屏排版
| 元素 | 规则 |
| --- | --- |
| 列表项 | 卡片式,`margin: symmetric(horizontal: 12, vertical: 6)`,圆角 `lg`(16),阴影 `xs` |
| 卡片内边距 | 16(横屏 20) |
| 标题 | `titleSmall` + `w600`,1 行省略 |
| 副信息行 | 图标 20 + 文字 12/13,最多 4 行 |
| 表单 | **单列**,`labelText` 常驻,字段间距 12 |
### 7.4 横屏 1600×900 适配
**现状**:`OrientationBuilder` **0 处**、断点 **0 处**、`home_page.dart:67` 与 `device_detail_page.dart:312` 写死 `crossAxisCount: 2` → 1600px 下卡片被拉到约 780px 宽。
**方案**:新建 `lib/utils/responsive.dart`,**用 `shortestSide` 而非 `width`**(1600×900 的 shortSide = 900 → `medium`):
```dart
enum AppWindowSize { compact, medium, expanded }
class AppResponsive {
AppResponsive(this.size, this.orientation);
final Size size;
final Orientation orientation;
factory AppResponsive.of(BuildContext context) =>
AppResponsive(MediaQuery.sizeOf(context), MediaQuery.orientationOf(context));
bool get isLandscape => orientation == Orientation.landscape;
AppWindowSize get windowSize => switch (size.shortestSide) {
< 600 => AppWindowSize.compact,
< 840 => AppWindowSize.medium, // ★ 手机横屏 900 落这里
_ => AppWindowSize.expanded,
};
bool get useMasterDetail => size.width >= 840 && isLandscape;
int gridCount({double minTileWidth = 320}) =>
(size.width / minTileWidth).floor().clamp(2, 6);
double get contentMaxWidth => switch (windowSize) {
AppWindowSize.compact => double.infinity,
AppWindowSize.medium => 560,
AppWindowSize.expanded => 720,
};
double get gutter => switch (windowSize) {
AppWindowSize.compact => 12, AppWindowSize.medium => 24, _ => 32,
};
double get cardPadding => windowSize == AppWindowSize.compact ? 16 : 20;
}
extension AppResponsiveX on BuildContext {
AppResponsive get responsive => AppResponsive.of(this);
}
```
**六项改造**:① 底部导航横屏改 `NavigationRail`(§8.1);② 列表页双栏主从;③ Grid 列数自适应;④ 内容限宽居中;⑤ 弹窗 `maxWidth: 640` 居中;⑥ AppBar 补 `scrolledUnderElevation: 3` + 横屏 `toolbarHeight: 48`。
---
## 8. ★ 可重设计范围落地指引:导航栏 / 设置页 / 详情页 / 弹窗
### 8.1 导航栏(`lib/widgets/app_bottom_nav.dart`)
**现状**
- M3 `NavigationBar`,5 个 tab;图标已正确使用线性/填充配对(`home_outlined` / `home`)→ **语义准确,保留**
- `indicatorColor` 默认取 `colorScheme.secondaryContainer`(Flutter 3.27 `navigation_bar.dart:1382` 已核实)→ **改主题后自动变暖橙 `#F5DFCB`,零改动生效**
- 横屏下 80dp 高度吃掉 900 的 8.9%
**改动(4 项)**
**① 主题层统一导航样式**
```dart
navigationBarTheme: NavigationBarThemeData(
height: 72, // 80 → 72
elevation: 0,
backgroundColor: scheme.surfaceContainer,
surfaceTintColor: scheme.surfaceTint,
indicatorColor: scheme.secondaryContainer, // → #F5DFCB(浅)/ #5A3A22(深)
indicatorShape: const StadiumBorder(), // 胶囊指示器,呼应云弧
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
iconTheme: WidgetStateProperty.resolveWith((states) => IconThemeData(
size: 24,
// 选中图标落在 indicator(secondaryContainer) 上 → 用 onSecondaryContainer(7.90 ✅)
color: states.contains(WidgetState.selected)
? scheme.onSecondaryContainer
: scheme.onSurfaceVariant,
)),
labelTextStyle: WidgetStateProperty.resolveWith((states) => TextStyle(
fontSize: 12,
fontWeight: states.contains(WidgetState.selected) ? FontWeight.w600 : FontWeight.w500,
color: states.contains(WidgetState.selected) ? scheme.onSurface : scheme.onSurfaceVariant,
)),
),
```
**② 横屏切换 `NavigationRail`**
```dart
navigationRailTheme: NavigationRailThemeData(
backgroundColor: scheme.surfaceContainer,
elevation: 0,
indicatorColor: scheme.secondaryContainer,
indicatorShape: const StadiumBorder(),
minWidth: 72,
groupAlignment: -0.2, // 略微上移,适配横屏矮屏
labelType: NavigationRailLabelType.all,
selectedIconTheme: IconThemeData(size: 24, color: scheme.onSecondaryContainer),
unselectedIconTheme: IconThemeData(size: 24, color: scheme.onSurfaceVariant),
selectedLabelTextStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: scheme.onSurface),
unselectedLabelTextStyle: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant),
),
```
**③ 组件层按断点切换**
```dart
// app_bottom_nav.dart
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
final r = context.responsive;
if (r.isLandscape && r.size.width >= 840) { // ★ 横屏改 Rail
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: shell.currentIndex,
onDestinationSelected: _onTap,
destinations: _destinations(t),
),
const VerticalDivider(width: 1, thickness: 1),
Expanded(child: ClipRect(child: shell)),
],
),
);
}
return Scaffold(
body: shell,
bottomNavigationBar: NavigationBar(
selectedIndex: shell.currentIndex,
onDestinationSelected: _onTap,
destinations: _destinations(t),
),
);
}
void _onTap(int index) =>
shell.goBranch(index, initialLocation: index == shell.currentIndex);
List<NavigationDestination> _destinations(AppLocale t) => [
NavigationDestination(icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home), label: t.home),
NavigationDestination(icon: const Icon(Icons.devices_outlined),
selectedIcon: const Icon(Icons.devices), label: t.devices),
NavigationDestination(icon: const Icon(Icons.store_outlined),
selectedIcon: const Icon(Icons.store), label: t.stores),
NavigationDestination(icon: const Icon(Icons.admin_panel_settings_outlined),
selectedIcon: const Icon(Icons.admin_panel_settings), label: t.admin),
NavigationDestination(icon: const Icon(Icons.person_outline),
selectedIcon: const Icon(Icons.person), label: t.profile),
];
```
> **收益**:横屏多出 80dp 垂直空间(占 900 的 8.9%);5 个 tab 移到左侧后**拇指横向可达,比底部更易点**。
**④ 图标处置**:`_destinations` 里的 10 个图标(5 组线性+填充)**全部保留原样**(语义准确);仅通过主题层改变配色与指示器形态。这符合"回归延续"基调 —— 导航栏是**点名重设计**区域,但重设计的是**容器与配色**,不是图标语义。
---
### 8.2 设置页(`lib/screens/profile/settings_page.dart`)
**现状问题(实测)**
| # | 问题 | 位置 | 严重度 |
| --- | --- | --- | --- |
| 1 | **三处 `SegmentedButton` 塞在 `ListTile.trailing`** | `:180-199` 主题、`:206-228` 语言、`:236-263` 环境 | **🔴 阻塞** |
| 2 | `Divider()` 满宽,与卡片化脱节 | `:174` `:230` `:265` `:272` `:290` | 🟡 |
| 3 | 退出登录**无二次确认**,点击即登出 | `:292-301` | 🔴 |
| 4 | 无分区标题,10 个条目平铺 | 全文 | 🟡 |
| 5 | 「关于」已有 logo 品牌位,但未强化 | `:278-289` | 🟢 |
> **问题 1 详解**:`SegmentedButton` 3 段的最小宽度约 3×48 = 144dp。竖屏 360dp 屏上,扣除 `leading`(24) + 间距 + 标题文字后,`trailing` 可用宽度不足 144dp,**必然挤压溢出**;语言/环境切换是 2 段(约 96dp),同样紧张。这是当前设置页最严重的布局缺陷。
**改动(5 项)**
**① 控件从 `trailing` 移到标题下方(全宽)**
```dart
/// 设置分组卡片。
class AppSettingsSection extends StatelessWidget {
const AppSettingsSection({super.key, this.title, required this.children});
final String? title;
final List<Widget> children;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.symmetric(
horizontal: context.responsive.gutter, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Text(title!,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: scheme.primary, // 暖橙分区标题
fontWeight: FontWeight.w600)),
),
CloudCard(
padding: EdgeInsets.zero,
level: CloudShadowLevel.xs,
child: Column(
children: [
for (var i = 0; i < children.length; i++) ...[
if (i > 0)
Divider(height: 1, indent: 56, endIndent: 16,
color: scheme.outlineVariant),
children[i],
],
],
),
),
],
),
);
}
}
/// 设置项:图标 + 标题 + 副标题 +(可选)标题下方的全宽控件。
class AppSettingsTile extends StatelessWidget {
const AppSettingsTile({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.child, // ★ SegmentedButton 等控件放这里,全宽渲染
this.onTap,
this.danger = false,
});
final IconData icon;
final String title;
final String? subtitle;
final Widget? child;
final VoidCallback? onTap;
final bool danger;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final c = danger ? scheme.error : scheme.onSurfaceVariant;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(icon, size: 24, color: c),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.bodyLarge?.copyWith(
color: danger ? scheme.error : scheme.onSurface)),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(subtitle!, style: theme.textTheme.bodySmall
?.copyWith(color: scheme.onSurfaceVariant)),
],
],
),
),
if (onTap != null && child == null)
Icon(Icons.chevron_right, size: 22, color: scheme.onSurfaceVariant),
]),
if (child != null) ...[
const SizedBox(height: 12),
SizedBox(width: double.infinity, child: child), // ★ 全宽,不再挤
const SizedBox(height: 4),
],
],
),
),
);
}
}
```
**② 页面结构改造**
```dart
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(t.settings)),
body: ListView(
padding: const EdgeInsets.only(bottom: 24),
children: [
if (authState.user != null)
_buildUserHeader(context, authState.user!),
const SizedBox(height: 8),
AppSettingsSection(
title: '外观与语言',
children: [
AppSettingsTile(
icon: Icons.dark_mode_outlined,
title: t.themeMode,
subtitle: _themeModeLabel(themeMode, t),
child: SegmentedButton<ThemeMode>(/* …原逻辑不变… */),
),
AppSettingsTile(
icon: Icons.language_outlined,
title: t.language,
subtitle: appLocale.label,
child: SegmentedButton<AppLocale>(/* …原逻辑不变… */),
),
],
),
AppSettingsSection(
title: '系统',
children: [
AppSettingsTile(
icon: Icons.verified_user_outlined,
title: '运行环境',
subtitle: _envLabel(AppConfig.environment),
child: SegmentedButton<AppEnvironment>(/* …原逻辑不变… */),
),
AppSettingsTile(
icon: Icons.lock_outline,
title: t.changePassword,
onTap: () => _showChangePasswordDialog(context, ref),
),
],
),
AppSettingsSection(
title: '关于',
children: [
AppSettingsTile(
icon: Icons.info_outline,
title: t.about,
subtitle: '${AppConstants.appName} v1.0.0',
onTap: () => showAboutDialog(/* …保留 logo… */),
),
AppSettingsTile(
icon: Icons.logout,
title: t.logout,
danger: true,
onTap: () => _confirmLogout(context, ref, t), // ★ 二次确认
),
],
),
],
),
);
}
```
**③ 退出登录二次确认**
```dart
Future<void> _confirmLogout(BuildContext context, WidgetRef ref, AppLocale t) async {
final scheme = Theme.of(context).colorScheme;
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
icon: Icon(Icons.logout, color: scheme.error),
title: const Text('确认退出登录?'),
content: const Text('退出后需要重新输入账号密码。'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(t.cancel)),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: scheme.error,
foregroundColor: scheme.onError,
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text('退出'),
),
],
),
);
if (ok == true) ref.read(authProvider.notifier).logout();
}
```
**④ 用户头部区加品牌渐变**(设置页在可改范围内,是理想的品牌渗透点)
```dart
Widget _buildUserHeader(BuildContext context, user) {
final scheme = Theme.of(context).colorScheme;
return Container(
margin: EdgeInsets.symmetric(horizontal: context.responsive.gutter, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
// logo 的橙→金渐变直接搬过来(浅色模式)
gradient: scheme.brightness == Brightness.light
? AppBrand.cloudGradient
: null,
color: scheme.brightness == Brightness.dark ? scheme.surfaceContainer : null,
borderRadius: BorderRadius.circular(AppRadius.xl), // 20
boxShadow: AppShadows.sm,
),
child: Row(children: [
CircleAvatar(
radius: 28,
backgroundColor: scheme.brightness == Brightness.light
? Colors.white : scheme.primaryContainer,
child: Icon(Icons.person, size: 30,
color: scheme.brightness == Brightness.light
? AppBrand.onCloud : scheme.onPrimaryContainer),
),
const SizedBox(width: 16),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.displayName, style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold, color: scheme.onSurface)),
Text(user.role.isEmpty ? '${t.account}:${user.account}' : user.role,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: scheme.onSurfaceVariant)),
],
)),
]),
);
}
```
**⑤ 修改密码弹窗**(`:27-131`)改用 `AppSettingsTile` 后布局自然对齐;`AlertDialog` 由 `dialogTheme`(圆角 28)统一接管。
---
### 8.3 详情页(`device_detail_page.dart` / `store_detail_page.dart`)
**现状问题(实测)**
| # | 问题 | 位置 |
| --- | --- | --- |
| 1 | `_infoRow` 的 label 列**写死 `width: 88`** | `:208-215`、`:284-292` |
| 2 | 指标 `GridView` **写死 `crossAxisCount: 2`** | `:312` |
| 3 | 操作按钮「发指令 / 改分组 / 状态 Chip」横排在内容区 | `:90-110` |
| 4 | `_sectionCard` 用满宽 `Divider` | `:183` |
| 5 | `store_detail_page.dart` 是 16 行桩页 | 全文 |
> 横屏 1600px 下,88dp 的 label 列仅占 5.5%,比例失衡;竖屏窄屏下又偏挤。
**改动(5 项)**
**① label 列宽响应式**
```dart
double _labelWidth(BuildContext context) => switch (context.responsive.windowSize) {
AppWindowSize.compact => 96,
AppWindowSize.medium => 128,
AppWindowSize.expanded => 160,
};
Widget _infoRow(BuildContext context, String label, String value) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: _labelWidth(context),
child: Text(label, style: theme.textTheme.bodyMedium
?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
),
Expanded(
child: Text(value, style: theme.textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.w600)),
),
],
),
);
}
```
**② 指标 Grid 列数自适应**
```dart
// device_detail_page.dart:311
GridView.count(
crossAxisCount: context.responsive.gridCount(minTileWidth: 300), // 竖屏2 / 横屏4
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: context.responsive.isLandscape ? 1.8 : 1.15,
children: [/* 4 个 MetricCard 不变 */],
)
```
> **注**:4 个 `MetricCard` 的图标(`:320` `Icons.memory`、`:328` `Icons.storage`、`:337` `Icons.sd_storage`、`:346` `Icons.network_check`)在 C 区,可统一为 `_outlined` 变体并接入语义色阈值(`:325` `:334` `Colors.red`、`:343` `Colors.orange` → `AppSemanticColors`)。
**③ 操作区改为吸底栏**(释放内容区,符合单手操作)
```dart
// Scaffold 上
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(children: [
Expanded(child: FilledButton.icon(
onPressed: () => _showCommandSheet(context, device),
icon: const Icon(Icons.send_outlined, size: 20),
label: Text(t.sendCommand),
)),
const SizedBox(width: 12),
Expanded(child: FilledButton.tonal.icon(
onPressed: () => _showEditSheet(context, ref, device),
icon: const Icon(Icons.edit_outlined, size: 20),
label: Text(t.editGroup),
)),
]),
),
),
```
> `StatusChip` 从操作行移出,改挂在 AppBar `actions` 或"基本信息"区(原 `:109` 位置)—— 释放底部空间、消除与按钮的误触风险。
**④ 分区卡片改 `CloudCard` + 内缩分隔线**
```dart
Widget _sectionCard(BuildContext context, String title, List<Widget> rows) {
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.symmetric(horizontal: context.responsive.gutter, vertical: 6),
child: CloudCard(
level: CloudShadowLevel.xs,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Divider(height: 1, color: scheme.outlineVariant), // 随卡片内缩
const SizedBox(height: 8),
...rows,
],
),
),
);
}
```
**⑤ `store_detail_page.dart` 桩页按新规范实现**
现为 `Center(child: Text('门店 $storeId\n功能开发中'))`。建议改为与设备详情同构的骨架(AppBar + 分区卡 + 吸底操作),保持详情页视觉一致;功能未开放处用统一的空状态组件(P2-8)。
---
### 8.4 弹窗(全项目 30 处 `showModalBottomSheet`)
**现状四个问题(已核实 Flutter 3.27 源码)**
| # | 问题 | 证据 |
| --- | --- | --- |
| 1 | `useSafeArea` **默认 `false`**,全项目 30 处无一处显式开启 | `bottom_sheet.dart:874` |
| 2 | `isScrollControlled: true` 但**未限高** | `store_list_page.dart:142` |
| 3 | **无 `showDragHandle`**,用户不知道能下滑关闭 | 全部 30 处 |
| 4 | **保存按钮埋在滚动区末尾** | `store_list_page.dart:1404-1416` |
**① 主题层全局兜底(一次性覆盖 30 处)**
```dart
bottomSheetTheme: BottomSheetThemeData(
showDragHandle: true,
useSafeArea: true, // ★ 默认 false,必须显式开启
backgroundColor: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
modalBackgroundColor: scheme.surface,
elevation: 0,
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(borderRadius: AppRadius.sheetTop), // 顶部 28
constraints: const BoxConstraints(maxWidth: 640), // ★ 横屏不铺满
),
```
**② 页面层统一封装 `showAppBottomSheet`**
```dart
Future<T?> showAppBottomSheet<T>({
required BuildContext context,
required Widget body,
Widget? footer, // 吸底操作栏(表单类必传)
String? title,
double heightFactor = 0.92,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
showDragHandle: true,
useSafeArea: true,
constraints: BoxConstraints(
maxWidth: 640,
maxHeight: MediaQuery.sizeOf(context).height * heightFactor,
),
builder: (ctx) => Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null)
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(title, style: Theme.of(ctx).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold)),
),
),
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.only(
left: 20, right: 20, bottom: 20,
top: 0,
),
child: body,
),
),
if (footer != null)
Container(
decoration: BoxDecoration(
color: Theme.of(ctx).colorScheme.surface,
border: Border(top: BorderSide(
color: Theme.of(ctx).colorScheme.outlineVariant)),
boxShadow: AppShadows.lg,
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
child: SafeArea(top: false, child: footer), // ★ 吸底,不受滚动影响
),
],
),
);
}
```
> **键盘适配**:`body` 内保留 `padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom)`;吸底栏用 `SafeArea(top: false)` 处理底部手势条。
**③ 30 处弹窗分类改造模板**
| 类型 | 数量 | 代表位置 | `heightFactor` | 是否有吸底栏 | 要点 |
| --- | --- | --- | --- | --- | --- |
| **表单类** | ~18 | `store_list_page.dart:142`(门店新增/编辑)<br>`org_tree_page.dart:87/103`<br>`device_group_page.dart:168`<br>`device_detail_page.dart:116`(改分组/房号)<br>`device_activation_page.dart:90`<br>`admin/sys/user:219` `role:84` `menu:82` `config:103` `schedule:85`<br>`admin/app/app_version:122` `tv_app:110`<br>`admin/intro/category:588/730/964` `intro_content:452/568/800`<br>`admin/media/list:109` `media_rank:129` | **0.92** | **✅ 必配** | 保存/创建按钮移出滚动区 |
| **选择类** | ~7 | `device_command_sheet.dart`<br>`org_picker_sheet.dart:280`<br>`region_picker.dart:51`<br>`marquee:93/587/902` | **0.70** | 视情况 | 需搜索框置顶固定 |
| **详情类** | 1 | `store_list_page.dart:158`(门店详情) | **0.80** | ✅(编辑/指令/删除) | 操作下沉到吸底栏 |
| **指令类** | 1 | `store_list_page.dart:188`(门店批量指令) | 自适应 | ❌ | `mainAxisSize: min`,不设限高 |
**④ 门店表单改造示意(P0 最高优先)**
```dart
// store_list_page.dart:140 改造后
void _showAddEditSheet({StoreInfo? store}) {
final currentUser = ref.read(authProvider).user;
showAppBottomSheet(
context: context,
title: store == null ? t.addStore : t.editStore,
heightFactor: 0.92,
body: _StoreFormBody( // 原 _StoreFormSheet 的滚动内容部分
orgService: _orgService,
store: store,
currentUserOrgId: currentUser?.orgId,
formKey: _formKey, // 由外部持有,供吸底按钮校验
),
footer: SizedBox(
width: double.infinity,
child: FilledButton( // ★ 吸底,始终可见
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(height: 20, width: 20,
child: CircularProgressIndicator(strokeWidth: 2))
: Text(store == null ? t.create : t.save),
),
),
);
}
```
---
## 9. 落地清单(按冻结区重排)
### 9.1 P0 —— 回归橙白 + 阻塞性修复
| # | 文件 | 改动 | 冻结区影响 |
| --- | --- | --- | --- |
| P0-1 | `lib/app/theme.dart` | 全量重写:`primaryBlue` → `brandPrimary = #C2410C`;`ColorScheme.fromSeed` → 显式 `ColorScheme(...)`;新增 `AppRadius`/`AppShadows`/`AppIconSize`/`AppBrand`/`AppSemanticColors` | 无 |
| P0-2 | `lib/app/theme.dart` | 新增 `iconTheme`(仅 C 区生效)/`cardTheme`/`bottomSheetTheme`/`navigationBarTheme`/`navigationRailTheme`/`dialogTheme`/`visualDensity`/`iconButtonTheme`/三 ButtonTheme;`appBarTheme` 补 `scrolledUnderElevation: 3` | 无 |
| P0-3 | `lib/screens/login/login_page.dart` | 整页去硬编码:L152 `#121212`→`surface`;L149 `#0D2137`(深蓝)→`surfaceContainerLow`;L145-148 输入色→token;L205/233/280/350/370→`primary`;L372 `#1A4480`→`onSurface` 12% | 无 |
| P0-4 | `lib/screens/login/login_page.dart` | 品牌化:Logo 80→112;加 `AppBrand.cloudGradient` 装饰带;`maxWidth: 420` 保留 | 无 |
| P0-5 | `lib/main.dart:60-61` | `statusBarIconBrightness` 硬编码 `light` → 跟随 `themeMode`(**修 bug**) | 无 |
| P0-6 | `lib/widgets/store_theme_grid.dart:77` | 选中边框 `#2E7DFF` → `scheme.primary` | 无 |
| P0-7 | `lib/screens/store/store_list_page.dart` | 三处弹窗(:142/:158/:188)改 `showAppBottomSheet`;**保存按钮移出滚动区改吸底**(原 :1404-1416) | 无 |
| P0-8 | `lib/utils/responsive.dart` | **新建** `AppResponsive` + `BuildContext.responsive` | 无 |
| P0-9 | `lib/widgets/app_bottom_sheet.dart` | **新建** `showAppBottomSheet` 统一封装 | 无 |
| P0-10 | **冻结区标记** | 在 A 区 12 处 + B 区 26 处图标上方加 `// [FROZEN] 冻结:勿改图标,见 docs/手机端UI优化方案-v2.md §6` 注释;D 区 10 处加 `// [FROZEN-BACKEND-CONTRACT] 后端 sys_menu.icon 映射,勿改` | **保护性改动(仅加注释)** |
### 9.2 P1 —— 四类重设计 + 布局触控
| # | 文件 | 改动 | 冻结区影响 |
| --- | --- | --- | --- |
| P1-1 | `lib/widgets/app_bottom_nav.dart` | 横屏切 `NavigationRail`;`destinations` 提取为方法;**图标保留原样** | 无 |
| P1-2 | `lib/screens/profile/settings_page.dart` | 三处 `SegmentedButton` 从 `trailing` 移到标题下方;分组卡 `AppSettingsSection`;退出登录加二次确认;用户头部加品牌渐变 | 无 |
| P1-3 | `lib/screens/home/device_detail_page.dart` | label 列宽响应式;Grid 自适应;操作区改吸底栏;`StatusChip` 移出操作行;分区卡改 `CloudCard`;`:325/334/343` 语义色 | `:320/328/337/346` 四图标在 C 区,**可改** |
| P1-4 | `lib/widgets/cloud_card.dart` | **新建** `CloudCard` | 无 |
| P1-5 | `lib/screens/home/home_page.dart` | `:67` Grid 列数自适应;`_StatCard` 换 `CloudCard`(**图标与配色不动**);`_QuickAction` 补 48 触控 | **⚠️ 图标冻结,只改容器** |
| P1-6 | `lib/widgets/device_list_tile.dart` | `:81` `IconButton` 与 `StatusChip` 补 8dp 间距;换 `CloudCard`(圆角 16 + 阴影 xs) | **⚠️ `:26/28/30` 设备图标属 B 区,本轮不动** |
| P1-7 | `lib/widgets/status_chip.dart` | `:16-20` `Colors.green/grey/orange` → 语义色 | `:43` `Icons.circle` 在 C 区,可改 |
| P1-8 | `lib/widgets/region_picker.dart` | `:189` `Colors.blue` → `scheme.primary`;`:154` 高度 0.6 改按断点 | 无 |
| P1-9 | `lib/screens/store/org_tree_page.dart` | `:280-290` 六色 → 主题色板;`:63/351` `Colors.red` → `error` | 无 |
| P1-10 | `lib/screens/device/device_activation_page.dart` | `:119/293` `Colors.orange` → `scheme.primary`(**换色后橙色=品牌色,必须改**);`:158` 语义色 | 无 |
| P1-11 | `lib/screens/alerts/alerts_page.dart` | `:15/17/19` → 语义色 | `:26/28/30` 告警图标在 C 区,可改 |
| P1-12 | **C 区图标统一** | 按 §6.3 清单,将 C 区图标统一为 `_outlined` 变体 + `AppIconSize` 栅格 | **执行前须逐行比对 §6.1 A 区 / §6.2 B 区 / §6.2.1 D 区,48 处全部排除** |
| P1-13 | 其余 20+ 处弹窗 | 补 `useSafeArea` + `constraints`(主题层已兜底把手/形状/宽度) | 无 |
### 9.3 P2 —— 增强(含待确认项解锁后再做)
| # | 文件 | 改动 | 前置条件 |
| --- | --- | --- | --- |
| P2-1 | `store_list_page.dart` / `device_list_page.dart` | 横屏双栏主从 | — |
| P2-2 | `login_page.dart` | 横屏左右分栏(左品牌渐变 + 右表单) | — |
| P2-3 | `pubspec.yaml` | 引入 Material Symbols 可变字体,让 `fill/weight/grade/opticalSize` 生效 | — |
| P2-4 | `store_detail_page.dart` | 按新规范实现详情页骨架 | — |
| P2-5 | `device_scan_page.dart` | 取景框按 `shortestSide` 计算(修横屏压扁) | — |
| P2-6 | 全局 | 空/加载/错误三态组件统一 | — |
| P2-7 | `main.dart:193/255` | 错误页 `Colors.red.shade50` 硬编码 → 跟随主题 | — |
| P2-8 | **B 区图标处理** | 若用户确认解冻,再统一 B 区 26 处 | **需用户确认** |
| P2-9 | **首页统计卡四色** | 若用户选择 §6.1 选项②,改为语义色 | **需用户确认** |
---
## 10. 风险与权衡
### 10.1 换色影响面
**必须改(13 处)**:见 §2.4 表。
**绝对不能动(业务数据色)**:
| 文件:行 | 值 | 原因 |
| --- | --- | --- |
| `store_theme_preview.dart:110-115` | `#3B82F6` `#8B5CF6` `#EC4899` `#F59E0B` `#10B981` `#6366F1` | **下发给 TV 端的门店主题模板预览色板** |
| `store_theme_grid.dart:129-134` | 同上 6 色 | 同上 |
| `models/store_theme.dart:46-54` | `#1565C0` `#00838F` `#2E7D32` `#E65100` `#455A64` | 门店主题色枚举 |
> **最大误伤陷阱**:`#3B82F6` 与 `#2E7DFF` 都是蓝色,**正则批量替换必然误伤**。
> 只做 `Color(0xFF2E7DFF)` **整词精确替换** + 逐处人工确认。
### 10.2 深色模式下橙色的 3 条硬约束
1. **主色必须提亮到 `#FFB487`,`onPrimary` 必须是深棕 `#542000`**(白字仅 1.73 ❌)。沿用 `#C2410C` 会让按钮在深底上像禁用态。
2. **亮橙只做小面积强调(<8%)**:图标/进度条/选中指示/链接;容器级用深橙底 `#63300D` + `#FFDCC2` 文字。大面积亮橙在 OLED 上会光晕刺眼。
3. **surface 必须暖化**:`#16130F`(R>G>B);不能用 `#121212`,更不能用现状的 `#0D2137`(深蓝)。
### 10.3 ★ 新增:冻结区实施风险(本轮最高优先级风险)
| 风险 | 触发场景 | 后果 | 缓解 |
| --- | --- | --- | --- |
| **批量替换误伤** | 执行 P1-12「`Icons.xxx` → `Icons.xxx_outlined`」时用全局正则 | 首页应用图标、TV 应用图标被改 → **直接违反用户硬约束** | **禁止全局正则**。逐文件按 §6.1 A 区 / §6.2 B 区 / §6.2.1 D 区清单比对后手工改;改完用 §11 的 grep 验收 |
| **后端契约被改** | 同上,且未识别 `menu_page.dart:_iconData()` 是后端映射表 | 移动端与 Web 后台菜单图标不一致,属**功能缺陷**而非样式问题 | §6.2.1 单列为 D 区并明确「无商量余地」;代码内加 `// [FROZEN-BACKEND-CONTRACT]` 注释 |
| **主题色间接改动** | `_QuickAction`(`home_page.dart:196-197`)用 `primaryContainer`/`primary`,换主题后快捷入口变橙 | 是否算"修改图标设计"存在歧义 | **已判定:颜色跟随主题属"回归橙白",是目标不是副作用**;但**图形/尺寸/结构不动**。若用户不认可,把这两行改为固定值即可 |
| **统计卡四色歧义** | `home_page.dart:78/84/90/96` 硬编码蓝紫绿红 | 不动则首页仍有蓝色;动则可能违反冻结 | **默认冻结不动**,列为 §6.1 待确认项 |
| **B 区归属未定** | 26 处内容占位/设备图标/管理入口图标 | 改了可能越界,不改可能风格不统一 | **默认不动**,P2-8 待用户一句话确认 |
| **按文件整体冻结** | 实施者误以为 `admin/app/` 整个目录冻结 | `tv_app_page.dart:245/249/341` 的 search/clear/add 本可统一却没改 | 明确规则:**按图标实例判定,不按文件**(§6.0) |
| **冻结区容器与图标的边界** | 首页统计卡换 `CloudCard` 时顺手改了 `Icon.size` | 违反冻结 | 明确边界:**冻结 = 图形 + size + 布局结构 + 入口数量**;**可改 = 卡片容器材质(圆角/阴影/内边距)** |
### 10.4 其他
| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| 弃用 `fromSeed` | 失去 M3 动态取色 | 强品牌识别诉求优先;且现状已用 `.copyWith(primary:)` 覆盖,动态取色本就未生效 → 可接受 |
| 亮橙不能作文字/图标色 | 少一个高亮手段 | 用 `primaryContainer` + `onPrimaryContainer` 组合替代;Banner 用大色块亮橙 + 深棕字(6.99 ✅) |
| 桌面图标/启动图未同步 | 主题改了图标没改 | **不在本方案范围**,建议单独立项(`android/app/src/main/res/`) |
| 登录页去硬编码 | 该页此前硬编码是为规避「M3 动态取色导致文字背景融为一体」(见 L144 注释) | 改显式 ColorScheme 后理由不成立;但**别顺手删 try/catch 兜底** |
| 默认深色→浅色切换 | 老用户升级后界面突变 | 建议无显式偏好时跟随 `ThemeMode.system`,而非硬切 light(**待确认**) |
---
## 11. 验收标准(可度量)
| 项 | 验收方式 | 目标 |
| --- | --- | --- |
| **冻结区零改动** | `git diff` / 逐行比对 §6.1 A 区 12 处 | A 区图标的 `IconData` 与 `size` **必须逐字节一致** |
| | 逐行比对 §6.2 B 区 26 处 | 未获用户确认前**不得出现任何 diff** |
| | 逐行比对 §6.2.1 D 区 10 处 | **任何情况下都不允许 diff**(后端契约) |
| **品牌一致性** | 5 个主页面截图取样主色相 | 主色 H ∈ [20°, 26°](logo 为 22.9°) |
| **对比度** | §4 全部配对复核 | 全部达 WCAG AA(正文 4.5 / 大字 3.0 / 非文本 3.0) |
| **触控** | Android 无障碍扫描 | 所有可点元素 ≥48×48,间距 ≥8 |
| **横屏** | PGBM10 实机横屏 1600×900 | 无横向失真;弹窗宽 ≤640 居中;底部导航为 Rail |
| **弹窗** | 实机打开全部 30 处 | 全部有把手 / 限高 / 不被手势条遮挡 / 表单类有吸底栏 |
| **设置页** | 竖屏 360dp 实机 | `SegmentedButton` **无溢出、无挤压** |
| **详情页** | 竖屏 + 横屏各走查 | label 列宽自适应;指标 Grid 竖屏 2 列 / 横屏 4 列 |
| **残留检查** | `grep -rn "0xFF2E7DFF\|Colors.blue" lib/` | 除 `store_theme_*` 与 `models/store_theme.dart` 业务色外,命中为 **0** |
| **图标检查** | `grep -rn "Icons\." lib/screens/home/home_page.dart` | 与 §6.1 A1 表**逐行一致**(图形与 size 未变) |
---
## 12. 附录
### 附录 A:冻结区速查表(打印贴在工位)
```
⛔ 绝对冻结(12 处,禁止任何修改)
首页 home_page.dart
:77 Icons.devices 统计卡-设备总数 size 28
:83 Icons.store 统计卡-门店总数 size 28
:89 Icons.check_circle_outline 统计卡-在线设备 size 28
:95 Icons.highlight_off 统计卡-离线设备 size 28
:114 Icons.devices 快捷入口-设备管理 CircleAvatar
:121 Icons.store 快捷入口-门店管理 CircleAvatar
:128 Icons.people 快捷入口-用户管理 CircleAvatar
应用显示相关
tv_app_page.dart:301 Icons.apps size 40 应用图标加载失败占位
tv_app_page.dart:304 Icons.apps 应用图标默认占位
store_theme_preview.dart:105 Icons.storefront size 26 门店主页预览 Logo 占位
store_theme_group_list.dart:174 Icons.image_outlined size 28 主题缩略图占位
branding_editor.dart:155 Icons.image size 28 Logo/启动图占位
⚠️ 灰区(26 处,需用户确认后处理,默认不动)
admin/intro/intro_category_page.dart :375 :378 :436 :439 :1219 :1226
admin/intro/intro_store_content_page.dart :332 :335 :407 :410 :1100 :1107
admin/media/media_list_page.dart :336 :347
admin/media/media_rank_page.dart :209 :215 :323 :330
widgets/device_list_tile.dart :26 :28 :30
screens/admin/admin_hub_page.dart :18 :24 :30 :36 :42
🚫 后端契约冻结(10 处,无商量余地,改了会破坏与 Web 后台的菜单图标一致性)
screens/admin/sys/menu_page.dart :280 :282 :284 :286 :288 :290 :292 :294 :296 :298
理由:_iconData() 是「后端 sys_menu.icon 字段 → IconData」的映射表,属接口约定
⚠️ 待确认(1 项)
home_page.dart :78 :84 :90 :96 统计卡四色硬编码(blue/purple/green/red)
✅ 可改(C 区)
导航栏 app_bottom_nav.dart :32-53(图标保留,改配色与容器)
设置页 settings_page.dart :166 :177 :184 :188 :192 :203 :233 :268 :275 :294
详情页 device_detail_page.dart :320 :328 :337 :346 :507 :544
弹窗 org_picker_sheet.dart / region_picker.dart / store_list_page 弹窗内
通用交互图标:search clear close add edit delete refresh check
chevron_right arrow_drop_down expand_less/more upload copy save
注:通用交互图标即便出现在冻结区文件内也可改(按实例判定,不按文件)
```
### 附录 B:`AppSemanticColors` 完整代码
```dart
class AppSemanticColors extends ThemeExtension<AppSemanticColors> {
const AppSemanticColors({
required this.success, required this.successContainer,
required this.onSuccessContainer,
required this.warning, required this.warningContainer,
required this.onWarningContainer,
required this.info, required this.infoContainer,
required this.onInfoContainer,
});
final Color success, successContainer, onSuccessContainer;
final Color warning, warningContainer, onWarningContainer;
final Color info, infoContainer, onInfoContainer;
static const light = AppSemanticColors(
success: Color(0xFF1E7A4E), successContainer: Color(0xFFD3EDDD),
onSuccessContainer: Color(0xFF1E5138),
warning: Color(0xFF8A5C00), warningContainer: Color(0xFFFFE9B8),
onWarningContainer: Color(0xFF5C4100),
info: Color(0xFF0B6397), infoContainer: Color(0xFFD7E9FA),
onInfoContainer: Color(0xFF1A4763),
);
static const dark = AppSemanticColors(
success: Color(0xFF5FD398), successContainer: Color(0xFF1E5138),
onSuccessContainer: Color(0xFFA8E6C4),
warning: Color(0xFFF0BE4E), warningContainer: Color(0xFF5C4100),
onWarningContainer: Color(0xFFFFDCA8),
info: Color(0xFF7FC4F0), infoContainer: Color(0xFF1A4763),
onInfoContainer: Color(0xFFB8DCF5),
);
@override ThemeExtension<AppSemanticColors> copyWith() => this;
@override ThemeExtension<AppSemanticColors> lerp(
covariant ThemeExtension<AppSemanticColors>? other, double t) =>
other ?? this;
}
class AppBrandExtension extends ThemeExtension<AppBrandExtension> {
const AppBrandExtension({
this.cloud = AppBrand.cloud,
this.cloudGold = AppBrand.cloudGold,
this.onCloud = AppBrand.onCloud,
this.gradient = AppBrand.cloudGradient,
});
final Color cloud, cloudGold, onCloud;
final LinearGradient gradient;
@override ThemeExtension<AppBrandExtension> copyWith() => this;
@override ThemeExtension<AppBrandExtension> lerp(
covariant ThemeExtension<AppBrandExtension>? other, double t) =>
this;
}
```
### 附录 C:`theme.dart` 改造完整代码
见 v1 `手机端UI优化方案.md` §7.4。**v2 相对 v1 需追加的项**:`navigationBarTheme`、`navigationRailTheme`、`dialogTheme`(§8.1 / §8.2)。其余部分(`AppBrand` / `AppRadius` / `AppShadows` / `AppIconSize` / 两套 `ColorScheme` / `_base()`)**完全沿用 v1,未作修改**。
---
## 13. 需要用户拍板的 3 件事
| # | 事项 | 选项 | 产品建议 |
| --- | --- | --- | --- |
| 1 | **首页统计卡四色**(`home_page.dart:78/84/90/96`) | ① 保持硬编码蓝紫绿红(冻结)<br>② 改语义色(图形尺寸不动) | **建议 ②** —— 图标图形与尺寸零改动(符合冻结),仅配色纳入橙白体系;否则首页会残留最显眼的蓝色 |
| 2 | **B 区 26 处是否同 A 区一并冻结** | ① 一并按冻结处理<br>② 解冻,按新规范统一 | **建议 ①** —— 这些都是"下发给 TV/门店端展示内容的占位图形",性质与 A2 完全相同;且解冻收益很小(占位图形本来就极少被看到) |
| 3 | **默认主题模式**(`theme_provider.dart:7`) | ① 保持 `ThemeMode.dark`<br>② 改 `ThemeMode.light`<br>③ 无偏好时跟随系统 | **建议 ③** —— logo 明度均值 245/255,"明亮轻盈"才是品牌主场;但跟随系统最稳妥,避免老用户升级突变 |
> 另:**桌面图标 / 启动图**(`android/app/src/main/res/`)不在本方案范围内,建议单独立项 —— 否则 App 内主题改了、桌面图标还是旧色,品牌一致性打折。