diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html index badb54c..a4fc829 100644 --- a/Doc/IM_App_架构设计.html +++ b/Doc/IM_App_架构设计.html @@ -330,6 +330,20 @@ font-size: 28px; } } + + /* 章节最后更新时间标签 */ + .updated-tag { + font-size: 11px; + font-weight: 400; + color: #999; + background: #f3f4f6; + border: 1px solid #e0e3e8; + padding: 1px 8px; + border-radius: 20px; + margin-left: 10px; + vertical-align: middle; + letter-spacing: 0; + } @@ -2674,9 +2688,15 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) { ); }); -/// 4. UseCase(按需) +/// 4. UseCase(登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化) final loginUseCaseProvider = Provider<LoginUseCase>((ref) { - return LoginUseCase(authRepository: ref.read(authRepositoryProvider)); + return LoginUseCase( + authRepository: ref.read(authRepositoryProvider), + socketManager: ref.read(socketManagerProvider), + apiConfig: ref.read(apiConfigProvider), + storageApi: ref.read(storageSdkProvider), + userRepository: ref.read(userRepositoryProvider), + ); }); @@ -2977,7 +2997,7 @@ flowchart TD style CoreUI fill:#fff4e6,stroke:#f57c00 -
--两大核心逻辑:
+三大核心逻辑:
1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。
2. Riverpod 单向数据流:用户操作 →
ref.read(vm.notifier).action()→ ViewModel 处理逻辑 →state = newState→ref.watch(vm)检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。3. Widget 纯展示原则:
@@ -3461,7 +3525,7 @@ flowchart TD DesignSystem --> Colors[Colors 颜色] DesignSystem --> Typography[Typography 字体] - DesignSystem --> Tokens[Design Tokens 基础定义] + DesignSystem --> Tokens[间距 / 圆角 / 阴影] DesignSystem --> Theme[Theme 主题] Foundation --> Atoms[Atoms 原子组件] @@ -3522,39 +3586,44 @@ flowchart TDbuild()只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。核心原则:与 Figma 设计稿完全对应,确保设计与实现一致。
-1.1 Colors(颜色)
+1.1 Colors(颜色)— 5 层架构
--/// 颜色定义 - 与 Figma 设计稿对应 -class AppColors { - // Primary Colors - 主色 - static const primary = Color(0xFF667EEA); - static const primaryDark = Color(0xFF5568D3); - static const primaryLight = Color(0xFF8B9FFF); +@@ -3616,77 +3685,79 @@ class AppTypography { }/// Layer 1: 颜色常量,不带语义 +abstract class ColorBases { + static const primary400 = Color(0xFF5BA3F5); + static const primary500 = Color(0xFF2F80ED); + static const primary700 = Color(0xFF1A6BD4); + static const neutral0 = Color(0xFFFFFFFF); + static const neutral50 = Color(0xFFF8F9FA); + // ... neutral100~950, error500, success500, warning500 +} - // Secondary Colors - 辅助色 - static const secondary = Color(0xFF764BA2); - static const secondaryDark = Color(0xFF5E3882); - static const secondaryLight = Color(0xFF9B6FC4); +/// Layer 2: 语义颜色接口 +abstract class AppColors { + Color get bgPrimary; // Scaffold 底色 + Color get bgSecondary; // Card / Surface + Color get textPrimary; // 主文本 + Color get textSecondary; // 辅助文本 + Color get brandPrimary; // 主品牌色 + Color get statusError; // 错误 + // ... 完整定义见 colors.dart +} - // Neutral Colors - 中性色 - static const black = Color(0xFF000000); - static const white = Color(0xFFFFFFFF); - static const gray900 = Color(0xFF1A1A1A); - static const gray800 = Color(0xFF2D2D2D); - static const gray700 = Color(0xFF404040); - static const gray600 = Color(0xFF5C5C5C); - static const gray500 = Color(0xFF737373); - static const gray400 = Color(0xFF999999); - static const gray300 = Color(0xFFBFBFBF); - static const gray200 = Color(0xFFE6E6E6); - static const gray100 = Color(0xFFF5F5F5); - static const gray50 = Color(0xFFFAFAFA); +/// Layer 3: 亮暗实现 +class LightColors implements AppColors { + const LightColors(); + @override Color get bgPrimary => ColorBases.neutral50; + @override Color get textPrimary => ColorBases.neutral900; + @override Color get brandPrimary => ColorBases.primary500; + // ... +} - // Semantic Colors - 语义色 - static const success = Color(0xFF10B981); - static const warning = Color(0xFFF59E0B); - static const error = Color(0xFFEF4444); - static const info = Color(0xFF3B82F6); +class DarkColors implements AppColors { + const DarkColors(); + @override Color get bgPrimary => ColorBases.neutral900; + @override Color get textPrimary => ColorBases.neutral0; + @override Color get brandPrimary => ColorBases.primary400; + // ... }1.3 Design Tokens(基础定义)
+1.3 间距 / 圆角 / 阴影
-/// 基础定义 - 间距、圆角、阴影等 -class AppTokens { - // Spacing - 间距(8pt 网格系统) - static const spacing4 = 4.0; - static const spacing8 = 8.0; - static const spacing12 = 12.0; - static const spacing16 = 16.0; - static const spacing20 = 20.0; - static const spacing24 = 24.0; - static const spacing32 = 32.0; - static const spacing40 = 40.0; - static const spacing48 = 48.0; +-/// 间距常量 — spacing.dart +class AppSpacing { + static const double s4 = 4; + static const double s8 = 8; + static const double s12 = 12; + static const double s16 = 16; + static const double s24 = 24; + static const double s32 = 32; +} - // Border Radius - 圆角 - static const radiusSmall = 4.0; - static const radiusMedium = 8.0; - static const radiusLarge = 12.0; - static const radiusXLarge = 16.0; - static const radiusFull = 9999.0; +/// 圆角常量 — radius.dart +class AppRadius { + // 基础 Radius + static const Radius r4 = Radius.circular(4); + static const Radius r8 = Radius.circular(8); + static const Radius r12 = Radius.circular(12); + static const Radius r16 = Radius.circular(16); - // Elevation - 阴影 - static const elevationNone = 0.0; - static const elevationLow = 2.0; - static const elevationMedium = 4.0; - static const elevationHigh = 8.0; + // 组件级圆角 + static const BorderRadius card = BorderRadius.all(r12); + static const BorderRadius button = BorderRadius.all(r8); + static const BorderRadius dialog = BorderRadius.all(r16); +} + +/// 阴影常量 — shadows.dart +/// 颜色走 AppColors.shadow 自动适配亮暗模式 +class AppShadows { + List<BoxShadow> get bs4; // Elevation 4 — 小卡片 + List<BoxShadow> get bs8; // Elevation 8 — Card + List<BoxShadow> get bs12; // Elevation 12 — Dropdown + List<BoxShadow> get bs16; // Elevation 16 — Dialog }1.4 Theme(主题 - 黑暗模式)
+1.4 Theme(主题组装)
-@@ -6870,7 +6947,7 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));/// 主题定义 - 支持亮色/暗色模式 +@@ -3706,8 +3777,8 @@ class AppTheme {/// Layer 4: 主题组装 — 接收 AppColors 实例,零 isDark 分支 class AppTheme { - // Light Theme - static ThemeData light = ThemeData( - brightness: Brightness.light, - primaryColor: AppColors.primary, - scaffoldBackgroundColor: AppColors.white, - colorScheme: const ColorScheme.light( - primary: AppColors.primary, - secondary: AppColors.secondary, - error: AppColors.error, - surface: AppColors.white, - background: AppColors.gray50, - ), - textTheme: TextTheme( - displayLarge: AppTypography.displayLarge, - headlineMedium: AppTypography.headlineMedium, - bodyLarge: AppTypography.bodyLarge, - ), - ); + AppTheme._(); - // Dark Theme - static ThemeData dark = ThemeData( - brightness: Brightness.dark, - primaryColor: AppColors.primary, - scaffoldBackgroundColor: AppColors.gray900, - colorScheme: const ColorScheme.dark( - primary: AppColors.primary, - secondary: AppColors.secondary, - error: AppColors.error, - surface: AppColors.gray800, - background: AppColors.black, - ), - textTheme: TextTheme( - displayLarge: AppTypography.displayLarge.copyWith(color: AppColors.white), - headlineMedium: AppTypography.headlineMedium.copyWith(color: AppColors.white), - bodyLarge: AppTypography.bodyLarge.copyWith(color: AppColors.gray100), - ), - ); + static ThemeData get theme => _build(const LightColors()); + static ThemeData get darkTheme => _build(const DarkColors()); + + static ThemeData _build(AppColors c) { + final brightness = c is DarkColors ? Brightness.dark : Brightness.light; + return ThemeData( + useMaterial3: true, + brightness: brightness, + scaffoldBackgroundColor: c.bgPrimary, + colorScheme: ColorScheme( + brightness: brightness, + primary: c.brandPrimary, + onPrimary: c.textOnPrimary, + surface: c.bgSecondary, + onSurface: c.textPrimary, + error: c.statusError, + onError: c.textOnPrimary, + // ... + ), + appBarTheme: AppBarTheme(backgroundColor: c.bgPrimary, foregroundColor: c.textPrimary), + elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom( + backgroundColor: c.brandPrimary, foregroundColor: c.textOnPrimary, + )), + inputDecorationTheme: InputDecorationTheme(fillColor: c.bgSecondary, filled: true), + cardTheme: CardThemeData(color: c.bgSecondary), + dividerTheme: DividerThemeData(color: c.divider), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: c.navBarBg, selectedItemColor: c.navBarSelected, + ), + ); + } }- Primary/Main- AppColors.primary主色 ++ context.colors.brandPrimary主品牌色 @@ -3716,18 +3787,18 @@ class AppTheme { Button/Primary- Text/Headline/Large+ AppTypography.headlineLargecontext.styles.headlineLarge大标题 - Spacing/16+ AppTokens.spacing16AppSpacing.s1616pt 间距 - @@ -3777,7 +3848,7 @@ class AppButton extends StatelessWidget { foregroundColor: _getForegroundColor(), padding: _getPadding(), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppTokens.radiusMedium), + borderRadius: AppRadius.button, ), ), child: Text(text, style: _getTextStyle()), @@ -3806,7 +3877,7 @@ class AppTextField extends StatelessWidget { labelText: label, hintText: hint, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTokens.radiusMedium), + borderRadius: AppRadius.button, ), ), ); @@ -3831,11 +3902,11 @@ class SearchBar extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.all(AppTokens.spacing12), + padding: EdgeInsets.all(AppSpacing.s12), child: Row( children: [ - Icon(Icons.search, color: AppColors.gray500), - SizedBox(width: AppTokens.spacing8), + Icon(Icons.search, color: context.colors.textSecondary), + SizedBox(width: AppSpacing.s8), Expanded( child: AppTextField( hint: hint, @@ -3902,18 +3973,18 @@ class MessageBubble extends ConsumerWidget { alignment: isSender ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: EdgeInsets.symmetric( - horizontal: AppTokens.spacing16, - vertical: AppTokens.spacing8, + horizontal: AppSpacing.s16, + vertical: AppSpacing.s8, ), - padding: EdgeInsets.all(AppTokens.spacing12), + padding: EdgeInsets.all(AppSpacing.s12), decoration: BoxDecoration( - color: isSender ? AppColors.primary : AppColors.gray200, - borderRadius: BorderRadius.circular(AppTokens.radiusLarge), + color: isSender ? context.colors.brandPrimary : context.colors.bgTertiary, + borderRadius: AppRadius.card, ), child: Text( message.content, - style: AppTypography.bodyMedium.copyWith( - color: isSender ? AppColors.white : AppColors.black, + style: context.styles.bodyMedium?.copyWith( + color: isSender ? context.colors.textOnPrimary : context.colors.textPrimary, ), ), ), @@ -4143,7 +4214,7 @@ class PlatformButton extends StatelessWidget { if (PlatformAdapter.isIOS) { return CupertinoButton( onPressed: onPressed, - color: AppColors.primary, + color: context.colors.brandPrimary, child: Text(text), ); } @@ -5171,19 +5242,19 @@ flowchart TD- Radius/Medium- AppTokens.radiusMedium中等圆角 ++ Radius/Card+ AppRadius.card卡片圆角 flowchart TD - UI[UI Layer] -->|用户操作: vm.method()| VM[ViewModel] - VM -->|调用| Repo[Repository] - VM -.复杂场景.-> UC[UseCase(按需)] + UI["UI Layer"] -->|"用户操作 vm.method"| VM["ViewModel"] + VM -->|"调用"| Repo["Repository"] + VM -.->|"复杂场景"| UC["UseCase(按需)"] UC -.-> Repo - Repo -->|返回 Entity| VM - VM -->|state = newState| State[UI State] - State -->|ref.watch 自动刷新| UI + Repo -->|"返回 Entity"| VM + VM -->|"state = newState"| UIState["UI State"] + UIState -->|"ref.watch 自动刷新"| UI style UI fill:#e1f5ff,stroke:#0288d1 style VM fill:#fff4e6,stroke:#f57c00 style Repo fill:#e8f5e9,stroke:#388e3c - style UC fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5 - style State fill:#fff4e6,stroke:#f57c00 + style UC fill:#f3e5f5,stroke:#7b1fa2 + style UIState fill:#fff4e6,stroke:#f57c00Riverpod ViewModel 实现方式
@@ -6199,9 +6270,15 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) { ); }); -// UseCase(按需 — 登录有多步编排,所以需要 UseCase) +// UseCase(登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化) final loginUseCaseProvider = Provider<LoginUseCase>((ref) { - return LoginUseCase(authRepository: ref.read(authRepositoryProvider)); + return LoginUseCase( + authRepository: ref.read(authRepositoryProvider), + socketManager: ref.read(socketManagerProvider), + apiConfig: ref.read(apiConfigProvider), + storageApi: ref.read(storageSdkProvider), + userRepository: ref.read(userRepositoryProvider), + ); });为什么独立 Package:国际化服务于 core/ui(组件内置文案)和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。注意:foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。
UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:
@@ -6879,10 +6956,13 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));最底层的视觉规范定义,不含任何 Widget,只输出颜色/字体常量和 ThemeData:
colors.dart(已实现):颜色体系 —— 品牌色、语义色(success / warning / error)、中性灰阶colors.dart(已实现):5 层颜色体系 —— ColorBases(颜色常量)+ AppColors(语义接口)+ LightColors / DarkColors(亮暗实现)font.dart(已实现):字体 —— TextStyle 定义 + textTheme(brightness)(统一字族/字号/行高)app_theme.dart(已实现):主题组装 —— 将以上令牌组合为 Light / Dark ThemeDatashadows.dart(已实现):阴影常量 —— bs4 / bs8 / bs12 / bs16,颜色走 AppColors.shadow 自动适配亮暗spacing.dart(已实现):间距常量 —— s4 / s8 / s12 / s16 / s24 / s32radius.dart(已实现):圆角常量 —— 基础 r4~r20 + 组件级 card / button / dialogapp_theme.dart(已实现):主题组装 —— _build(AppColors c) 零 isDark 分支context_theme_ext.dart(已实现):统一 context 扩展 —— context.colors + context.styles + context.shadows本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。
-全局只有一份
@@ -9853,39 +9933,76 @@ flowchart TD 图片和组件是重灾区:没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。
+颜色采用 5 层架构:ColorBases(颜色常量)→ AppColors(语义接口)→ LightColors/DarkColors(亮暗实现)→ AppTheme._build(AppColors c)(主题组装)→ context.colors(Widget 消费)。AppTheme 内部零 isDark 分支,亮暗差异完全由实现类决定。
-| 抽象名 | Figma 名 | 亮色 | 暗色 | 用途 |
|---|---|---|---|---|
| 色相 | 常量名 | 色值 | ||
primary | Primary | #2F80ED | #5BA3F5 | 主操作、链接、选中态 |
background | Background | #F8F9FA | #202124 | 页面底色 |
surface | Surface | #FFFFFF | #3C4043 | 卡片、弹框、输入框 |
onSurface | On Surface | #202124 | #FFFFFF | surface 上的文字 |
error | Error | #EB5757 | 错误状态 | |
success | Success | #27AE60 | 成功状态 | |
warning | Warning | #F2C94C | 警告状态 | |
| Brand | primary400 | #5BA3F5 | ||
| primary500 | #2F80ED | |||
| primary700 | #1A6BD4 | |||
| Neutral | neutral0 | #FFFFFF | ||
| neutral50 | #F8F9FA | |||
| neutral100 | #F1F3F4 | |||
| neutral200 | #E8EAED | |||
| neutral400 | #BDC1C6 | |||
| neutral600 | #80868B | |||
| neutral800 | #3C4043 | |||
| neutral850 | #2C2C2E | |||
| neutral900 | #202124 | |||
| neutral950 | #000000 | |||
| Neutral Alpha | black12 | 12% 透明度黑色 | ||
| black60 | 60% 透明度黑色 | |||
| Status | error500 | #EB5757 | ||
| success500 | #27AE60 | |||
| warning500 | #F2C94C | |||
| 名称 | 色值 | 名称 | 色值 | 名称 | 色值 |
|---|---|---|---|---|---|
| 语义名 | 用途 | 亮色 | 暗色 | ||
| white | #FFFFFF | gray50 | #F8F9FA | gray100 | #F1F3F4 |
| gray200 | #E8EAED | gray400 | #BDC1C6 | gray600 | #80868B |
| gray800 | #3C4043 | gray900 | #202124 | black | #000000 |
bgPrimary | Scaffold 底色 | neutral50 | neutral900 | ||
bgSecondary | Card / Surface | neutral0 | neutral800 | ||
bgTertiary | 次要区块 | neutral100 | neutral850 | ||
textPrimary | 主文本 | neutral900 | neutral0 | ||
textSecondary | 辅助文本 | neutral600 | neutral400 | ||
textDisabled | 禁用态 | neutral400 | neutral600 | ||
textOnPrimary | 品牌色上的文字 | neutral0 | |||
borderDefault | 默认边框 | neutral200 | neutral800 | ||
borderFocused | 聚焦边框 | primary500 | primary400 | ||
brandPrimary | 主品牌色 | primary500 | primary400 | ||
brandPrimaryHover | Hover 态 | primary700 | primary500 | ||
statusError | 错误 | error500 | |||
statusSuccess | 成功 | success500 | |||
statusWarning | 警告 | warning500 | |||
navBarBg | 底部导航背景 | neutral0 | neutral900 | ||
navBarSelected | 导航选中色 | primary500 | primary400 | ||
divider | 分割线 | neutral100 | neutral800 | ||
shadow | 阴影颜色 | black12 | black60 | ||
使用原则:需随主题切换 → 用语义色(primary、surface);亮暗保持不变 → 用灰阶固定值。
三个 context 扩展:context.colors — 语义颜色;context.styles — 字体 + 预组合样式;context.shadows — 阴影常量。
final c = context.colors;
+final s = context.styles;
+
+Container(color: c.bgSecondary) // 语义颜色
+Text('标题', style: s.headlineMedium) // 字体
+Text('分组', style: s.sectionLabel) // 预组合:字体 + 品牌色
+Icon(Icons.check, color: c.brandPrimary) // 图标颜色
+Container(decoration: BoxDecoration(boxShadow: context.shadows.bs8)) // 阴影
+
+
+使用原则:颜色一律用 context.colors.xxx,禁止写 Color(0xFF...) 或 Theme.of(context).colorScheme.xxx。新增语义色先在 AppColors 加 getter,再在 LightColors / DarkColors 各实现一遍。