← API | 列表 | Design Token 迁移计划
提示信息
# 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(` 确认该模块内无残留内联