提示信息
# Design Token 迁移计划
> 目标:在保留系统深/浅双主题自适应的前提下,统一颜色、间距、圆角、阴影等视觉规范,逐步消除内联字面量,不要求一次性完成所有组件。
---
## 一、约束前提
| 决策点 | 结论 |
|--------|------|
| 颜色主题 | **保留双主题**(深色 + 浅色自动适配),Token 使用 `Color(light:dark:)` 双值定义 |
| 字体 | **使用系统默认字体**(`Font.system`),AppFonts 保持现有结构,只统一命名 |
| 迁移节奏 | **增量迁移**,不要求一次改完所有组件,按模块分批推进 |
| 系统组件 | NavigationBar、TabBar、Alert、Menu、Sheet **全部保留系统原生** |
---
## 二、Token 设计原则(双主题版)
### 颜色 Token 定义方式
双主题需要每个颜色给出浅色/深色两套值:
```swift
// AppColors.swift 扩展方式(推荐)
extension Color {
// 语义色:主背景
static let backgroundPrimary = Color(
light: Color(hex: "#FFFFFF"),
dark: Color(hex: "#0E0E0E")
)
// 品牌强调色(两套主题共用同一个品牌蓝)
static let brand = Color(hex: "#5F9EFF")
static let brandSoft = Color(hex: "#5F9EFF").opacity(0.12)
// 文字色(跟随系统 label,天然双主题)
static let textPrimary = Color(.label)
static let textSecondary = Color(.secondaryLabel)
static let textMuted = Color(.tertiaryLabel)
}
```
> **注意**:能用系统语义色(`.label`、`.systemBackground` 等)的地方**优先用系统语义色**,只有系统没有对应语义的场景(如卡片气泡、在线状态点、badge)才自定义双值。
### 字体 Token(系统字体,保持现有结构)
```swift
// AppFonts.swift — 仅统一命名,值不变
enum AppFonts {
static let displayXL = Font.system(size: 30, weight: .bold)
static let displayL = Font.system(size: 24, weight: .bold)
static let headingL = Font.system(size: 20, weight: .semibold)
static let headingM = Font.system(size: 16, weight: .semibold)
static let bodyM = Font.system(size: 14)
static let bodyS = Font.system(size: 12)
static let labelM = Font.system(size: 11, weight: .medium)
static let labelS = Font.system(size: 10, weight: .semibold)
static let labelXS = Font.system(size: 10)
static let badge = Font.system(size: 10, weight: .semibold)
// 保留现有
static let button = Font.system(size: 16, weight: .semibold)
static let caption = Font.system(size: 13)
}
```
---
## 三、新增 Token 文件清单
以下为需要**新建**的文件(对应设计稿中现有 Theme/ 没有覆盖的部分):
| 文件 | 内容 | 优先级 |
|------|------|--------|
| `Theme/AppShadow.swift` | 卡片阴影、气泡阴影、导航栏阴影 | 阶段 A |
| `Theme/AppBlur.swift` | 顶栏模糊、底栏模糊、状态标签模糊 | 阶段 A |
### AppShadow.swift(双主题友好版)
```swift
import SwiftUI
struct AppShadowSpec {
let color: Color
let radius: CGFloat
let x: CGFloat
let y: CGFloat
}
enum AppShadow {
// 导航栏 / 顶栏
static let topBar = AppShadowSpec(color: .black.opacity(0.12), radius: 16, x: 0, y: 4)
// 卡片(通用)
static let card = AppShadowSpec(color: .black.opacity(0.08), radius: 12, x: 0, y: 4)
// 聊天气泡
static let bubble = AppShadowSpec(color: .black.opacity(0.10), radius: 8, x: 0, y: 2)
// 底部导航悬浮
static let bottomNav = AppShadowSpec(color: .black.opacity(0.20), radius: 32, x: 0, y: 8)
// FAB 按钮
static let fab = AppShadowSpec(color: .black.opacity(0.20), radius: 24, x: 0, y: 8)
}
extension View {
func appShadow(_ spec: AppShadowSpec) -> some View {
self.shadow(color: spec.color, radius: spec.radius, x: spec.x, y: spec.y)
}
}
```
### AppBlur.swift
```swift
import SwiftUI
enum AppBlur {
static let topBar: CGFloat = 32
static let bottomNav: CGFloat = 32
static let statusTag: CGFloat = 6
}
```
---
## 四、现有 Token 文件修改清单
### AppColors.swift — 重点增补
当前 `AppColors` 已有基础系统语义色,需要**新增**以下缺失的语义色:
```swift
extension Color {
// MARK: - 新增:卡片/Surface 色(气泡、固定消息背景等)
// 深色用偏深灰,浅色用白/极浅灰
static let surface1 = Color(UIColor { t in
t.userInterfaceStyle == .dark
? UIColor(hex: "#131313")
: UIColor.systemBackground
})
static let surface2 = Color(UIColor { t in
t.userInterfaceStyle == .dark
? UIColor(hex: "#1F1F1F")
: UIColor.secondarySystemBackground
})
static let surface3 = Color(UIColor { t in
t.userInterfaceStyle == .dark
? UIColor(hex: "#27272A")
: UIColor.tertiarySystemBackground
})
// MARK: - 新增:品牌色(固定色,双主题共用)
static let brandBlue = Color(hex: "#5F9EFF")
static let brandBlueSoft = Color(hex: "#5F9EFF").opacity(0.12)
// MARK: - 新增:气泡颜色
static let bubbleOutgoing = Color(UIColor { t in
t.userInterfaceStyle == .dark
? UIColor(hex: "#C6C6C7")
: UIColor(hex: "#E8E8EA")
})
// incoming 气泡用 surface1 即可
// MARK: - 新增:在线状态
static let statusOnline = Color(hex: "#5F9EFF")
// MARK: - 新增:Badge
static let badgeBg = Color(hex: "#5F9EFF")
static let badgeText = Color.white
}
```
### AppSizes.swift — 补充聊天组件规格
```swift
enum AppSizes {
// … 现有内容保留 …
// MARK: - 新增:聊天列表行
static let chatRowHeight: CGFloat = 88
static let chatAvatarSize: CGFloat = 56
static let chatAvatarRadius: CGFloat = 16
// MARK: - 新增:在线状态徽章
static let onlineBadge: CGFloat = 14
static let onlineBadgeBorder: CGFloat = 3
// MARK: - 新增:Badge 数字
static let badgeHeight: CGFloat = 20
static let badgeMinWidth: CGFloat = 20
static let badgePaddingH: CGFloat = 6
// MARK: - 新增:顶栏/底栏
static let topBarHeight: CGFloat = 64
static let fabSize: CGFloat = 56
}
```
### AppSpacing.swift — 补充页面边距
```swift
enum AppSpacing {
// … 现有内容保留 …
// MARK: - 新增:页面滚动边距(列表页顶/底留白)
static let pageTop: CGFloat = 96 // 顶栏高度 64 + 32
static let pageBottom: CGFloat = 128 // 底栏 + 余量
}
```
---
## 五、系统组件 + Token 的结合方式(代码参考)
### NavigationBar
```swift
// ✅ 保持系统 NavigationBar,用 Token 控制颜色
.toolbarBackground(Color.backgroundPrimary, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar) // 仅在深色主题时加
```
> 更推荐:深色页面的 Nav 用 `.toolbarBackground(.hidden)` 隐藏,自行在 ZStack 叠一个自定义顶栏(ConversationView 已这样做)。
### Alert / ConfirmationDialog
```swift
// ✅ 保持系统 Alert,用 .tint 传递品牌色
.alert("退出登录", isPresented: $show) {
Button("确认", role: .destructive) { logout() }
Button("取消", role: .cancel) { }
}
.tint(Color.brandBlue)
```
### Sheet / 半模态
```swift
// ✅ Sheet 内部设置背景 Token
.sheet(isPresented: $show) {
MySheetContent()
.presentationBackground(Color.backgroundSecondary)
.presentationDetents([.medium, .large])
}
```
### Menu
```swift
// ✅ 系统 Menu 原生保留,图标/文字颜色用 Token
Menu {
Button("举报") { }
Button("屏蔽", role: .destructive) { }
} label: {
Image(systemName: "ellipsis")
.foregroundStyle(Color.textSecondary)
}
```
---
## 六、增量迁移节奏(不需要一次全改)
> **原则**:每次只改一个模块,改完可独立上线,风险可控。不需要等所有模块都改完才能看到效果。
### 阶段 A:基础建设(约 4 小时)✦✦ 优先
不动任何 Feature 代码,只改 Theme 层 —— 所有已正确使用 Token 的地方自动受益。
- [ ] 新建 `Theme/AppShadow.swift`
- [ ] 新建 `Theme/AppBlur.swift`
- [ ] 修改 `AppColors.swift`:增补 `surface1/2/3`、`brandBlue`、`bubbleOutgoing`、`statusOnline`、`badgeBg`
- [ ] 修改 `AppFonts.swift`:对齐命名(`headingL`、`bodyM`、`labelS` 等)
- [ ] 修改 `AppSizes.swift`:增补 `chatRowHeight`、`chatAvatarSize`、`topBarHeight` 等
- [ ] 修改 `AppSpacing.swift`:增补 `pageTop`、`pageBottom`
### 阶段 B:优先模块——Chat(约 1 天)
Chat 是使用频率最高、视觉问题最明显的模块:
- [ ] `MessageBubble.swift`:替换所有内联 `.font(.system(...))` → `AppFonts.*`,替换气泡颜色 → `Color.bubbleOutgoing / Color.surface1`
- [ ] `ConversationListView.swift`:行高、头像尺寸使用 `AppSizes.chatRowHeight / chatAvatarSize`,阴影用 `AppShadow.card`
- [ ] `ChatInputBar.swift`:字体图标尺寸统一
### 阶段 C:Signal 模块(约 半天)
- [ ] `SignalHomeView.swift`、`SignalDetailView.swift`:字体 → `AppFonts`,颜色 → Token
### 阶段 D:Profile 模块(约 半天)
- [ ] `ProfileView.swift`、`UserProfileView.swift`、`EditProfileView.swift`:同上
### 阶段 E:收尾(低优先级,按需)
- [ ] `SplashView.swift`、`PrivacyView.swift`、`GlobalDialogsView.swift`
---
## 七、禁止规则(写入代码规范)
以下写法禁止出现在 `Features/` 和 `Shared/` 层:
```swift
// ❌ 禁止
.foregroundStyle(Color(hex: "#5F9EFF"))
.foregroundStyle(.blue)
.font(.system(size: 14))
.font(.caption)
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
// ✅ 应该用
.foregroundStyle(Color.brandBlue)
.foregroundStyle(Color.textSecondary)
.font(AppFonts.bodyM)
.font(AppFonts.labelXS)
.appShadow(AppShadow.card)
```
---
## 八、验证方式
每完成一个阶段,通过以下方式验证统一性:
1. 在模拟器 **同时切换深/浅色模式**(设置→外观),确认所有自定义颜色正确反转
2. 对比设计稿截图,检查间距、圆角、阴影是否与规范一致
3. 用 Xcode 全局搜索 `.font(.system(` 确认该模块内无残留内联