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 -

2.2 整体目录图

+

2.2 整体目录图最后更新 2026-03-10-10-17

图表说明

@@ -2996,10 +3016,13 @@ flowchart TD │ │ └── guards/ │ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由) │ │ -│ └── di/ # 全局 DI — 手动装配的 Provider -│ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager -│ ├── db_provider.dart # StorageSdkApi(注入 AppDatabase factory) -│ └── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier +│ ├── di/ # 全局 DI — 手动装配的 Provider +│ │ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager +│ │ ├── db_provider.dart # storageSdkProvider + storageSdkLifecycleProvider +│ │ ├── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier +│ │ └── user_provider.dart # userRepositoryProvider / CRUD UseCase Providers / users + allUsers Stream +│ └── notifiers/ # 全局 Notifier(@riverpod,跨模块共用) +│ └── user_notifier.dart # UserNotifier — 单用户状态(family per-uid,含 watch + CRUD) │ ├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import │ │ @@ -3012,18 +3035,33 @@ flowchart TD │ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider │ │ ├── presentation/ │ │ │ ├── login_view_model.dart # @riverpod ViewModel(生成 login_view_model.g.dart) -│ │ │ └── login_state.dart # @freezed State(生成 login_state.freezed.dart) +│ │ │ └── login_state.dart # @freezed State(step / contact / isLoading / error) │ │ ├── usecases/ -│ │ │ └── login_usecase.dart # 格式校验 → Repository → User Entity +│ │ │ └── login_usecase.dart # 校验 → OTP → 登录 → WS 连接 → DB 开库 → 用户持久化 │ │ └── view/ -│ │ └── login_page.dart # 登录页 +│ │ ├── login_page.dart # 登录页(装配 + 控制器 + 回调) +│ │ └── widgets/ +│ │ ├── login_phone_step.dart # 手机号输入步骤(纯展示) +│ │ └── login_otp_step.dart # 验证码输入步骤(纯展示) │ │ │ ├── chat/ # 聊天 ── 开发中 +│ │ ├── di/ +│ │ │ └── chat_bot_provider.dart # chatBotRepositoryProvider / allChatBots / chatBot(id) +│ │ ├── call/ # 通话子模块 +│ │ │ └── di/ +│ │ │ └── call_log_provider.dart # callLogRepositoryProvider / allCallLogs / callLog(id) │ │ ├── presentation/ -│ │ │ └── chat_view_model.dart # @riverpod ViewModel +│ │ │ ├── chat_view_model.dart # @riverpod ViewModel(会话列表) +│ │ │ ├── chat_db_test_state.dart # Drift DB 测试 State(手写,含分页字段) +│ │ │ └── chat_db_test_view_model.dart # Drift DB 测试 ViewModel(startTest / loadMore) +│ │ ├── usecases/ +│ │ │ ├── insert_users_use_case.dart # 批量插入用户(200 条/批,去重) +│ │ │ ├── update_users_use_case.dart # 更新前 10 条用户昵称 +│ │ │ └── delete_users_use_case.dart # 删除前 10 条用户 │ │ └── view/ │ │ ├── chat_page.dart # 会话列表页(Tab 1) -│ │ └── chat_detail_page.dart # 聊天详情页 +│ │ ├── chat_detail_page.dart # 聊天详情页 +│ │ └── chat_db_test_page.dart # Drift DB 测试页 │ │ │ ├── contact/ # 通讯录 ── 骨架 │ │ └── view/ @@ -3031,12 +3069,12 @@ flowchart TD │ │ │ └── settings/ # 设置 ── 已实现(主题切换) │ ├── di/ -│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入) +│ │ └── settings_providers.dart # setThemeUseCaseProvider │ ├── presentation/ │ │ ├── settings_view_model.dart # @riverpod ViewModel(设置页导航) -│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart) +│ │ └── theme_view_model.dart # @riverpod ViewModel(主题切换) │ ├── usecases/ -│ │ └── set_theme_usecase.dart # 主题切换用例 +│ │ └── set_theme_usecase.dart # 主题切换用例(幂等校验,防重复切换) │ └── view/ │ ├── settings_page.dart # 设置主页(Tab 3) │ ├── theme_view.dart # 主题选择页 @@ -3045,34 +3083,60 @@ flowchart TD │ └── theme_option_tile.dart │ ├── domain/ # Domain 层(纯 Dart,零 Flutter / 零网络依赖) -│ ├── entities/ -│ │ └── user.dart # 用户实体 -│ │ # message / conversation / contact 待开发 -│ └── repositories/ -│ └── auth_repository.dart # abstract interface -│ # message / chat / contact_repository 待开发 +│ ├── entities/ # 21 个实体(每实体一文件) +│ │ ├── user.dart # 用户 +│ │ ├── message.dart # 消息 +│ │ ├── chat.dart # 会话 +│ │ ├── call_log.dart # 通话记录 +│ │ ├── chat_bot.dart # 聊天机器人 +│ │ ├── chat_category.dart # 会话分类 +│ │ ├── group.dart # 群组 +│ │ ├── group_member.dart # 群成员 +│ │ ├── workspace.dart # 工作区 +│ │ ├── company_member.dart # 企业成员 +│ │ ├── favourite.dart / favourite_detail.dart +│ │ ├── sound.dart / tag.dart / retry.dart +│ │ ├── pending_friend_request_history.dart +│ │ ├── user_request_history.dart +│ │ └── {discover,explore,favorite,recent}_mini_app.dart +│ └── repositories/ # 4 个抽象接口 +│ ├── auth_repository.dart # sendOtp / verifyOtp / login / logout +│ ├── user_repository.dart # watch / get / insert / update / upsert / delete +│ ├── call_log_repository.dart # 通话记录 CRUD + watch +│ └── chat_bot_repository.dart # 聊天机器人 CRUD + watch │ ├── data/ # Data 层(implements domain 接口) -│ ├── repositories/ -│ │ └── auth_repository_impl.dart # 认证仓库 -│ │ # message / chat / contact 待开发 +│ ├── repositories/ # 4 个实现类 +│ │ ├── auth_repository_impl.dart # 认证(登录 / 登出 / Token 回调) +│ │ ├── user_repository_impl.dart # 用户(Drift CRUD + Stream watch) +│ │ ├── call_log_repository_impl.dart # 通话记录 +│ │ └── chat_bot_repository_impl.dart # 聊天机器人 │ ├── local/ │ │ └── drift/ # Drift 本地数据库 -│ │ ├── app_database.dart # @DriftDatabase 定义 + onUpgrade 自动补列 -│ │ # database_connection.dart 已迁移至 storage_sdk(数据库生命周期统一在 SDK 层管理) +│ │ ├── app_database.dart # @DriftDatabase(19 张表)+ onUpgrade 自动补列 │ │ ├── mapper/ │ │ │ └── drift_path_mapper.dart # Drift 路径映射工具 -│ │ └── tables/ -│ │ └── users.dart # Users 表定义 +│ │ └── tables/ # 19 个表定义(每表一文件) +│ │ ├── users.dart / messages.dart / chats.dart / call_logs.dart +│ │ ├── chat_bots.dart / chat_categories.dart / groups.dart / workspaces.dart +│ │ ├── favourites.dart / favourite_details.dart +│ │ ├── sounds.dart / tags.dart / retries.dart +│ │ ├── pending_friend_request_histories.dart / user_request_histories.dart +│ │ └── {discover,explore,favorite,recent}_mini_apps.dart │ ├── remote/ # Request 文件(一个端点一个文件) +│ │ ├── send_otp_request.dart # 发送验证码 +│ │ ├── verify_otp_request.dart # 校验验证码 │ │ ├── login_request.dart # 登录 │ │ ├── logout_request.dart # 登出 │ │ ├── get_profile_request.dart # 获取用户信息 │ │ └── upload_file_request.dart # 文件上传 -│ │ # send_message / 其他业务端点 待开发 -│ └── models/ # 持久化 DTO(@JsonSerializable) -│ └── user_dto.dart # 用户持久化 DTO -│ # message / conversation / contact_dto 待开发 +│ └── models/ # 持久化 DTO(17 个,与表一一对应) +│ ├── user_dto.dart / message_dto.dart / chat_dto.dart +│ ├── chat_bot_dto.dart / chat_category_dto.dart / group_dto.dart / workspace_dto.dart +│ ├── favourite_dto.dart / favourite_detail_dto.dart +│ ├── sound_dto.dart / tag_dto.dart / retry_dto.dart +│ ├── pending_friend_request_history_dto.dart / user_request_history_dto.dart +│ └── {discover,explore,favorite,recent}_mini_app_dto.dart │ └── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data ├── foundation/ # 基础配置(各为单独文件,非子目录) @@ -3092,12 +3156,12 @@ flowchart TD │ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排) │ └── ui/ # Core UI(设计系统 + 可复用组件) - ├── base/ # 设计 Token + ├── base/ # 设计基础(颜色 / 字体 / 间距 / 圆角 / 阴影) │ ├── assets.dart # 静态资源路径常量(AppAssets:logo / 占位图) │ ├── icons.dart # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态) │ ├── app_theme.dart # ThemeData 组装(Light / Dark) │ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶) - │ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors) + │ ├── context_theme_ext.dart # BuildContext 扩展(context.colors / context.styles / context.shadows) │ └── font.dart # 字体(TextStyle 定义 + textTheme(brightness)) ├── components/ # 原子组件 │ └── app_button.dart # 按钮 @@ -3106,7 +3170,7 @@ flowchart TD # app_dialog / app_toast / app_empty_state 等 -

2.3 整体分层图(MVVM + Riverpod 数据流)

+

2.3 整体分层图(MVVM + Riverpod 数据流)最后更新 2026-03-10-10-17

图表说明

@@ -3211,7 +3275,7 @@ flowchart LR
-

两大核心逻辑

+

三大核心逻辑

1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。

2. Riverpod 单向数据流:用户操作 → ref.read(vm.notifier).action() → ViewModel 处理逻辑 → state = newStateref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。

3. Widget 纯展示原则build() 只做一件事——把 State 属性映射成 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 TD

核心原则:与 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);
+
/// 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;
+  // ...
 }
 
@@ -3616,77 +3685,79 @@ class AppTypography { }
-
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(主题组装)
-
/// 主题定义 - 支持亮色/暗色模式
+
/// 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,
+      ),
+    );
+  }
 }
 
@@ -3706,8 +3777,8 @@ class AppTheme { Primary/Main -AppColors.primary -主色 +context.colors.brandPrimary +主品牌色 Button/Primary @@ -3716,18 +3787,18 @@ class AppTheme { Text/Headline/Large -AppTypography.headlineLarge +context.styles.headlineLarge 大标题 Spacing/16 -AppTokens.spacing16 +AppSpacing.s16 16pt 间距 -Radius/Medium -AppTokens.radiusMedium -中等圆角 +Radius/Card +AppRadius.card +卡片圆角 @@ -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
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:#f57c00

Riverpod 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), + ); });
@@ -6870,7 +6947,7 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));

为什么独立 Package:国际化服务于 core/ui(组件内置文案)和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。注意:foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。

-

7.5 Core UI(core/ui/)

+

7.5 Core UI(core/ui/)最后更新 2026-03-10-10-17

UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:

@@ -6879,10 +6956,13 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));

最底层的视觉规范定义,不含任何 Widget,只输出颜色/字体常量和 ThemeData:

第二层:基础组件(core/ui/components/)

@@ -9826,7 +9906,7 @@ flowchart TD

本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。

-

8.0 核心约定

+

8.0 核心约定最后更新 2026-03-10-10-17

全局只有一份

@@ -9853,39 +9933,76 @@ flowchart TD 图片和组件是重灾区:没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。
-

8.1 颜色体系

+

8.1 颜色体系最后更新 2026-03-10-10-17

-

所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。

+

颜色采用 5 层架构:ColorBases(颜色常量)→ AppColors(语义接口)→ LightColors/DarkColors(亮暗实现)→ AppTheme._build(AppColors c)(主题组装)→ context.colors(Widget 消费)。AppTheme 内部零 isDark 分支,亮暗差异完全由实现类决定。

-

语义色(随主题变化)

+

Layer 1: ColorBases — 颜色常量

- - - + - - - - - - - + + + + + + + + + + + + + + + + + +
抽象名Figma 名亮色暗色用途
色相常量名色值
primaryPrimary#2F80ED#5BA3F5主操作、链接、选中态
backgroundBackground#F8F9FA#202124页面底色
surfaceSurface#FFFFFF#3C4043卡片、弹框、输入框
onSurfaceOn Surface#202124#FFFFFFsurface 上的文字
errorError#EB5757错误状态
successSuccess#27AE60成功状态
warningWarning#F2C94C警告状态
Brandprimary400#5BA3F5
primary500#2F80ED
primary700#1A6BD4
Neutralneutral0#FFFFFF
neutral50#F8F9FA
neutral100#F1F3F4
neutral200#E8EAED
neutral400#BDC1C6
neutral600#80868B
neutral800#3C4043
neutral850#2C2C2E
neutral900#202124
neutral950#000000
Neutral Alphablack1212% 透明度黑色
black6060% 透明度黑色
Statuserror500#EB5757
success500#27AE60
warning500#F2C94C
-

灰阶(固定值,不随主题变化)

+

Layer 2-3: AppColors — 语义接口 + 亮暗实现

- + - - - + + + + + + + + + + + + + + + + + +
名称色值名称色值名称色值
语义名用途亮色暗色
white#FFFFFFgray50#F8F9FAgray100#F1F3F4
gray200#E8EAEDgray400#BDC1C6gray600#80868B
gray800#3C4043gray900#202124black#000000
bgPrimaryScaffold 底色neutral50neutral900
bgSecondaryCard / Surfaceneutral0neutral800
bgTertiary次要区块neutral100neutral850
textPrimary主文本neutral900neutral0
textSecondary辅助文本neutral600neutral400
textDisabled禁用态neutral400neutral600
textOnPrimary品牌色上的文字neutral0
borderDefault默认边框neutral200neutral800
borderFocused聚焦边框primary500primary400
brandPrimary主品牌色primary500primary400
brandPrimaryHoverHover 态primary700primary500
statusError错误error500
statusSuccess成功success500
statusWarning警告warning500
navBarBg底部导航背景neutral0neutral900
navBarSelected导航选中色primary500primary400
divider分割线neutral100neutral800
shadow阴影颜色black12black60
-

使用原则:需随主题切换 → 用语义色(primarysurface);亮暗保持不变 → 用灰阶固定值。

+

Widget 侧使用(双入口)

+ +

三个 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 各实现一遍。

8.2 字体体系

diff --git a/apps/im_app/lib/core/ui/base/app_theme.dart b/apps/im_app/lib/core/ui/base/app_theme.dart index ed6aeb0..ae4e18a 100644 --- a/apps/im_app/lib/core/ui/base/app_theme.dart +++ b/apps/im_app/lib/core/ui/base/app_theme.dart @@ -3,18 +3,20 @@ import 'package:flutter/material.dart'; import 'package:im_app/core/ui/base/colors.dart'; import 'package:im_app/core/ui/base/font.dart'; -/// 主题组装 -- 将 AppColors / AppFont 组装为 ThemeData +/// 主题组装 — 将 AppColors / AppFont 组装为 ThemeData /// -/// 同时提供 Light / Dark 双主题,按钮形状/颜色/字体统一在此定义, -/// AppButton 只负责变体切换和 loading 逻辑,不硬编码颜色和字体。 +/// 接收 [AppColors] 语义颜色实例,零 isDark 分支。 +/// 亮暗色差异完全由 [LightColors] / [DarkColors] 实现决定, +/// 本类只做「语义 → ThemeData 属性」的映射。 /// /// ## 数据流位置 /// /// ``` -/// AppColors + AppFont (L1 常量) -/// → ★ AppTheme ★ (L1 组装) ← 你在这里 -/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme) -/// → Theme.of(context) → 所有 Widget 自动响应主题变化 +/// ColorBases(颜色常量)→ AppColors(语义接口) +/// → LightColors / DarkColors(实现) +/// → ★ AppTheme._build(AppColors c) ★ ← 你在这里 +/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme) +/// → Theme.of(context) → 所有 Widget 自动响应主题变化 /// ``` /// /// ## 使用 @@ -22,7 +24,7 @@ import 'package:im_app/core/ui/base/font.dart'; /// ```dart /// // app/app.dart /// MaterialApp( -/// theme: AppTheme.theme, // getter 名与 MaterialApp 参数名一一对应 +/// theme: AppTheme.theme, /// darkTheme: AppTheme.darkTheme, /// ) /// ``` @@ -30,62 +32,128 @@ class AppTheme { AppTheme._(); /// 亮色主题 — 对应 MaterialApp `theme:` 参数 - static ThemeData get theme => _build(Brightness.light); + static ThemeData get theme => _build(const LightColors()); /// 暗色主题 — 对应 MaterialApp `darkTheme:` 参数 - static ThemeData get darkTheme => _build(Brightness.dark); + static ThemeData get darkTheme => _build(const DarkColors()); - static ThemeData _build(Brightness brightness) { - final isDark = brightness == Brightness.dark; - final primary = isDark ? AppColors.primaryLight : AppColors.primary; + static ThemeData _build(AppColors c) { + final brightness = c is DarkColors ? Brightness.dark : Brightness.light; return ThemeData( useMaterial3: true, brightness: brightness, + + // ── ColorScheme ──────────────────────────────────────────────────── colorScheme: ColorScheme( brightness: brightness, - primary: primary, - onPrimary: AppColors.white, - secondary: primary, - onSecondary: AppColors.white, - error: AppColors.error, - onError: AppColors.white, - surface: isDark ? AppColors.gray800 : AppColors.white, - onSurface: isDark ? AppColors.white : AppColors.gray900, + primary: c.brandPrimary, + onPrimary: c.textOnPrimary, + secondary: c.brandPrimary, + onSecondary: c.textOnPrimary, + error: c.statusError, + onError: c.textOnPrimary, + surface: c.bgSecondary, + onSurface: c.textPrimary, ), - scaffoldBackgroundColor: isDark ? AppColors.gray900 : AppColors.gray50, + scaffoldBackgroundColor: c.bgPrimary, - // 字体 + // ── Typography ───────────────────────────────────────────────────── textTheme: AppFont.textTheme(brightness), - // ElevatedButton → AppButton.primary + // ── AppBar ───────────────────────────────────────────────────────── + appBarTheme: AppBarTheme( + backgroundColor: c.bgPrimary, + foregroundColor: c.textPrimary, + elevation: 0, + centerTitle: true, + ), + + // ── ElevatedButton → AppButton.primary ───────────────────────────── elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: AppColors.white, - disabledBackgroundColor: AppColors.gray400, + backgroundColor: c.brandPrimary, + foregroundColor: c.textOnPrimary, + disabledBackgroundColor: c.borderDefault, + disabledForegroundColor: c.textDisabled, minimumSize: const Size.fromHeight(48), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), - // OutlinedButton → AppButton.secondary + // ── OutlinedButton → AppButton.secondary ────────────────────────── outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: primary, - side: BorderSide(color: primary), + foregroundColor: c.brandPrimary, + side: BorderSide(color: c.brandPrimary), minimumSize: const Size.fromHeight(48), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), - // TextButton → AppButton.text + // ── TextButton → AppButton.text ──────────────────────────────────── textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - foregroundColor: primary, + foregroundColor: c.brandPrimary, minimumSize: const Size.fromHeight(48), ), ), + + // ── InputDecoration ──────────────────────────────────────────────── + inputDecorationTheme: InputDecorationTheme( + fillColor: c.bgSecondary, + filled: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderSide: BorderSide(color: c.borderDefault), + borderRadius: BorderRadius.circular(8), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: c.borderDefault), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: c.borderFocused, width: 2), + borderRadius: BorderRadius.circular(8), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: c.statusError), + borderRadius: BorderRadius.circular(8), + ), + hintStyle: TextStyle(color: c.textDisabled), + ), + + // ── Card ─────────────────────────────────────────────────────────── + cardTheme: CardThemeData( + color: c.bgSecondary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: c.borderDefault), + ), + ), + + // ── Divider ──────────────────────────────────────────────────────── + dividerTheme: DividerThemeData( + color: c.divider, + thickness: 1, + ), + + // ── BottomNavigationBar ──────────────────────────────────────────── + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: c.navBarBg, + selectedItemColor: c.navBarSelected, + unselectedItemColor: c.textDisabled, + showUnselectedLabels: true, + type: BottomNavigationBarType.fixed, + ), ); } } diff --git a/apps/im_app/lib/core/ui/base/app_theme_ext.dart b/apps/im_app/lib/core/ui/base/app_theme_ext.dart deleted file mode 100644 index 3e8cc5d..0000000 --- a/apps/im_app/lib/core/ui/base/app_theme_ext.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:im_app/core/ui/base/shadows.dart'; - - -extension AppThemeExt on BuildContext { - AppShadows get shadows => AppShadows(this); -} \ No newline at end of file diff --git a/apps/im_app/lib/core/ui/base/colors.dart b/apps/im_app/lib/core/ui/base/colors.dart index 435e2ad..a1adec0 100644 --- a/apps/im_app/lib/core/ui/base/colors.dart +++ b/apps/im_app/lib/core/ui/base/colors.dart @@ -1,44 +1,158 @@ import 'package:flutter/material.dart'; -/// 颜色体系 — 与 Figma 设计稿对应 -/// -/// L1 基础常量 -- 不含任何 Widget,只输出颜色常量。 -/// View 层不直接引用 AppColors,通过 Theme.of(context) 访问语义色; -/// 有特殊硬编码需求(插图、固定品牌色)时可直接引用。 -/// -/// ## 数据流位置 -/// -/// ``` -/// AppColors(颜色常量)← 你在这里 -/// → AppTheme(组装为 ThemeData) -/// → MaterialApp(注入) -/// → Theme.of(context)(View 层消费) -/// ``` -class AppColors { - AppColors._(); +// ============================================================================= +// LAYER 1: ColorBases — 颜色常量,不带语义 +// ============================================================================= +// +// 色值与 Figma 设计稿一一对应。命名用 {色相}{明度} 格式(如 primary500), +// 不暗示任何使用场景。 +// +// View 层不直接引用 ColorBases,通过 AppColors(语义接口)间接使用。 - // ── Brand Primary ────────────────────────────────────────────────────────── - static const primary = Color(0xFF2F80ED); - static const primaryDark = Color(0xFF1A6BD4); - static const primaryLight = Color(0xFF5BA3F5); +abstract class ColorBases { + ColorBases._(); - // ── Semantic ─────────────────────────────────────────────────────────────── - static const success = Color(0xFF27AE60); - static const warning = Color(0xFFF2C94C); - static const error = Color(0xFFEB5757); + // ── Brand ──────────────────────────────────────────────────────────────── + static const primary400 = Color(0xFF5BA3F5); + static const primary500 = Color(0xFF2F80ED); + static const primary700 = Color(0xFF1A6BD4); - // ── Neutral Gray Scale ───────────────────────────────────────────────────── - static const white = Color(0xFFFFFFFF); - static const gray50 = Color(0xFFF8F9FA); - static const gray100 = Color(0xFFF1F3F4); - static const gray200 = Color(0xFFE8EAED); - static const gray400 = Color(0xFFBDC1C6); - static const gray600 = Color(0xFF80868B); - static const gray800 = Color(0xFF3C4043); - static const gray900 = Color(0xFF202124); - static const black = Color(0xFF000000); + // ── Neutral ────────────────────────────────────────────────────────────── + static const neutral0 = Color(0xFFFFFFFF); + static const neutral50 = Color(0xFFF8F9FA); + static const neutral100 = Color(0xFFF1F3F4); + static const neutral200 = Color(0xFFE8EAED); + static const neutral400 = Color(0xFFBDC1C6); + static const neutral600 = Color(0xFF80868B); + static const neutral800 = Color(0xFF3C4043); + static const neutral850 = Color(0xFF2C2C2E); + static const neutral900 = Color(0xFF202124); + static const neutral950 = Color(0xFF000000); - // ── Neutral black Scale ───────────────────────────────────────────────────── - static const black12 = Color(0x1F000000); // 12% opacity - static const black60 = Color(0x99000000); // 60% opacity + // ── Neutral Alpha(带透明度,阴影等场景)────────────────────────────────── + static const black12 = Color(0x1F000000); + static const black60 = Color(0x99000000); + + // ── Status ─────────────────────────────────────────────────────────────── + static const error500 = Color(0xFFEB5757); + static const success500 = Color(0xFF27AE60); + static const warning500 = Color(0xFFF2C94C); +} + +// ============================================================================= +// LAYER 2: AppColors — 语义颜色抽象接口 +// ============================================================================= +// +// 按用途命名(bgPrimary、textSecondary),不关心具体色值。 +// LightColors / DarkColors 各自实现,AppTheme._build() 消费此接口。 +// Widget 侧通过 context.colors 访问(见 context_theme_ext.dart)。 +// +// ## 数据流 +// +// ``` +// ColorBases(颜色常量) +// → AppColors(语义接口)← 你在这里 +// → LightColors / DarkColors(实现) +// → AppTheme._build(AppColors c)(组装 ThemeData) +// → context.colors(Widget 消费) +// ``` + +abstract class AppColors { + // ── Background ─────────────────────────────────────────────────────────── + Color get bgPrimary; + Color get bgSecondary; + Color get bgTertiary; + + // ── Text ───────────────────────────────────────────────────────────────── + Color get textPrimary; + Color get textSecondary; + Color get textDisabled; + Color get textOnPrimary; + + // ── Border ─────────────────────────────────────────────────────────────── + Color get borderDefault; + Color get borderFocused; + + // ── Brand ──────────────────────────────────────────────────────────────── + Color get brandPrimary; + Color get brandPrimaryHover; + + // ── Status ─────────────────────────────────────────────────────────────── + Color get statusError; + Color get statusSuccess; + Color get statusWarning; + + // ── Component ──────────────────────────────────────────────────────────── + Color get navBarBg; + Color get navBarSelected; + Color get divider; + + // ── Effect ─────────────────────────────────────────────────────────────── + Color get shadow; +} + +// ============================================================================= +// LAYER 3a: LightColors — 亮色实现 +// ============================================================================= + +class LightColors implements AppColors { + const LightColors(); + + @override Color get bgPrimary => ColorBases.neutral50; + @override Color get bgSecondary => ColorBases.neutral0; + @override Color get bgTertiary => ColorBases.neutral100; + + @override Color get textPrimary => ColorBases.neutral900; + @override Color get textSecondary => ColorBases.neutral600; + @override Color get textDisabled => ColorBases.neutral400; + @override Color get textOnPrimary => ColorBases.neutral0; + + @override Color get borderDefault => ColorBases.neutral200; + @override Color get borderFocused => ColorBases.primary500; + + @override Color get brandPrimary => ColorBases.primary500; + @override Color get brandPrimaryHover => ColorBases.primary700; + + @override Color get statusError => ColorBases.error500; + @override Color get statusSuccess => ColorBases.success500; + @override Color get statusWarning => ColorBases.warning500; + + @override Color get navBarBg => ColorBases.neutral0; + @override Color get navBarSelected => ColorBases.primary500; + @override Color get divider => ColorBases.neutral100; + + @override Color get shadow => ColorBases.black12; +} + +// ============================================================================= +// LAYER 3b: DarkColors — 暗色实现 +// ============================================================================= + +class DarkColors implements AppColors { + const DarkColors(); + + @override Color get bgPrimary => ColorBases.neutral900; + @override Color get bgSecondary => ColorBases.neutral800; + @override Color get bgTertiary => ColorBases.neutral850; + + @override Color get textPrimary => ColorBases.neutral0; + @override Color get textSecondary => ColorBases.neutral400; + @override Color get textDisabled => ColorBases.neutral600; + @override Color get textOnPrimary => ColorBases.neutral0; + + @override Color get borderDefault => ColorBases.neutral800; + @override Color get borderFocused => ColorBases.primary400; + + @override Color get brandPrimary => ColorBases.primary400; + @override Color get brandPrimaryHover => ColorBases.primary500; + + @override Color get statusError => ColorBases.error500; + @override Color get statusSuccess => ColorBases.success500; + @override Color get statusWarning => ColorBases.warning500; + + @override Color get navBarBg => ColorBases.neutral900; + @override Color get navBarSelected => ColorBases.primary400; + @override Color get divider => ColorBases.neutral800; + + @override Color get shadow => ColorBases.black60; } diff --git a/apps/im_app/lib/core/ui/base/context_theme_ext.dart b/apps/im_app/lib/core/ui/base/context_theme_ext.dart index 2c1c18c..3f44de4 100644 --- a/apps/im_app/lib/core/ui/base/context_theme_ext.dart +++ b/apps/im_app/lib/core/ui/base/context_theme_ext.dart @@ -1,27 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:im_app/core/ui/base/colors.dart'; import 'package:im_app/core/ui/base/font.dart'; +import 'package:im_app/core/ui/base/shadows.dart'; -/// 主题样式快捷封装 +/// 主题样式快捷封装 — 字体 + 预组合样式 /// -/// `context.styles` 返回此对象,build 方法里一行获取所有样式, -/// 之后直接用 `s.bodySmall`、`s.primary`,不再写 Theme.of(context)。 +/// `context.styles` 返回此对象,build 方法里一行获取字体样式, +/// 之后直接用 `s.bodySmall`、`s.sectionLabel`,不再写 Theme.of(context)。 +/// +/// 颜色访问走 `context.colors`(见下方 [AppColorsX] 扩展), +/// 本类只负责字体和「字体+颜色」的预组合。 /// /// ```dart /// final s = context.styles; +/// final c = context.colors; /// /// Text('标题', style: s.titleMedium) /// Text('描述', style: s.bodySmall) -/// Icon(Icons.home, color: s.primary) -/// Text('改色', style: s.bodySmall?.copyWith(color: s.primary)) +/// Text('分组', style: s.sectionLabel) // 预组合:字体 + 品牌色 +/// Icon(Icons.home, color: c.brandPrimary) /// ``` class AppStyles { AppStyles(BuildContext context) : _t = Theme.of(context).textTheme, - _c = Theme.of(context).colorScheme; + _colors = Theme.of(context).brightness == Brightness.dark + ? const DarkColors() + : const LightColors(); final TextTheme _t; - final ColorScheme _c; + final AppColors _colors; + + // ── 亮暗判断 ────────────────────────────────────────────────────────────── + + bool get isDark => _colors is DarkColors; + Brightness get brightness => isDark ? Brightness.dark : Brightness.light; // ── 字体 ────────────────────────────────────────────────────────────────── @@ -45,45 +58,58 @@ class AppStyles { TextStyle? get labelMedium => _t.labelMedium; TextStyle? get labelSmall => _t.labelSmall; - // ── 颜色 + 亮暗 ─────────────────────────────────────────────────────────── - - Brightness get brightness => _c.brightness; - bool get isDark => _c.brightness == Brightness.dark; - - Color get primary => _c.primary; - Color get onPrimary => _c.onPrimary; - Color get secondary => _c.secondary; - Color get onSecondary => _c.onSecondary; - Color get error => _c.error; - Color get onError => _c.onError; - Color get surface => _c.surface; - Color get onSurface => _c.onSurface; - Color get outline => _c.outline; - Color get outlineVariant => _c.outlineVariant; - // ── 预组合样式(字体 + 颜色,开箱即用)────────────────────────────────────── // // 与 AppButton 变体理念一致:按语义选用,无需手动拼 TextStyle 或 copyWith。 // 新增场景时在此扩展,保持全局一致。 - /// 分组标题 — 列表 Section、设置分组等(sectionLabel 字体 + primary 色) - TextStyle get sectionLabel => AppFont.sectionLabel.copyWith(color: primary); + /// 分组标题 — 列表 Section、设置分组等(sectionLabel 字体 + 品牌色) + TextStyle get sectionLabel => + AppFont.sectionLabel.copyWith(color: _colors.brandPrimary); - /// 辅助文字 — 元数据、次要信息、时间戳等(labelMedium + outline 色) - TextStyle? get labelMuted => labelMedium?.copyWith(color: outline); + /// 辅助文字 — 元数据、次要信息、时间戳等(labelMedium + 次要文字色) + TextStyle? get labelMuted => + labelMedium?.copyWith(color: _colors.textSecondary); - /// 正文次要 — 描述、提示等(bodySmall + outline 色) - TextStyle? get bodyMuted => bodySmall?.copyWith(color: outline); + /// 正文次要 — 描述、提示等(bodySmall + 次要文字色) + TextStyle? get bodyMuted => + bodySmall?.copyWith(color: _colors.textSecondary); - /// 错误提示 — 表单错误、警告等(bodySmall + error 色) - TextStyle? get bodyError => bodySmall?.copyWith(color: error); + /// 错误提示 — 表单错误、警告等(bodySmall + 错误色) + TextStyle? get bodyError => + bodySmall?.copyWith(color: _colors.statusError); } -/// BuildContext 主题入口 +/// 语义颜色入口 — 按亮暗模式返回对应的 [AppColors] 实例 +/// +/// ```dart +/// final c = context.colors; +/// Container(color: c.bgSecondary) +/// Text('hello', style: TextStyle(color: c.textPrimary)) +/// ``` +extension AppColorsX on BuildContext { + AppColors get colors => Theme.of(this).brightness == Brightness.dark + ? const DarkColors() + : const LightColors(); +} + +/// 字体样式入口 /// /// ```dart /// final s = context.styles; +/// Text('标题', style: s.headlineMedium) /// ``` extension AppThemeX on BuildContext { AppStyles get styles => AppStyles(this); } + +/// 阴影入口 +/// +/// ```dart +/// Container( +/// decoration: BoxDecoration(boxShadow: context.shadows.bs8), +/// ) +/// ``` +extension AppShadowsX on BuildContext { + AppShadows get shadows => AppShadows(this); +} diff --git a/apps/im_app/lib/core/ui/base/radius.dart b/apps/im_app/lib/core/ui/base/radius.dart index e93a157..bc91c25 100644 --- a/apps/im_app/lib/core/ui/base/radius.dart +++ b/apps/im_app/lib/core/ui/base/radius.dart @@ -1,131 +1,57 @@ import 'package:flutter/widgets.dart'; -/// 圆角设计 Token +/// 圆角常量 — 统一管理项目中的圆角规范 /// -/// 统一管理项目中的圆角规范,避免在业务代码中直接写 -/// `Radius.circular()` 或 `BorderRadius.circular()` -/// -/// 使用方式: +/// 避免在业务代码中直接写 `BorderRadius.circular()`。 /// /// ```dart -/// Container( -/// decoration: BoxDecoration( -/// borderRadius: AppRadius.card, -/// ), -/// ) +/// Container(decoration: BoxDecoration(borderRadius: AppRadius.card)) /// ``` /// -/// 设计规范来源: -/// 通常来自 UI 设计系统,例如 -/// 4 / 8 / 12 / 16 / 20 +/// 基础档位:4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20 class AppRadius { - /// 私有构造函数,防止被实例化 AppRadius._(); - // ================================ - // 基础 Radius Token - // ================================ - // 用于组合 BorderRadius + // ── 基础 Radius ──────────────────────────────────────────────────────── - /// 4px 圆角 static const Radius r4 = Radius.circular(4); - - /// 6px 圆角 static const Radius r6 = Radius.circular(6); - - /// 8px 圆角(常用于按钮) static const Radius r8 = Radius.circular(8); - - /// 10px 圆角 static const Radius r10 = Radius.circular(10); - - /// 12px 圆角(常用于卡片) static const Radius r12 = Radius.circular(12); - - /// 14px 圆角 static const Radius r14 = Radius.circular(14); - - /// 16px 圆角(常用于弹窗) static const Radius r16 = Radius.circular(16); - - /// 18px 圆角 static const Radius r18 = Radius.circular(18); - - /// 20px 圆角 static const Radius r20 = Radius.circular(20); - // ================================ - // 组件级设计 Token - // ================================ - // 推荐优先使用这些,而不是直接使用 brXX + // ── 组件级圆角(优先使用)───────────────────────────────────────────── - /// 卡片圆角 - /// - /// 示例:Card / 商品卡片 / 信息卡片 + /// 卡片 — Card / 商品卡片 / 信息卡片 static const BorderRadius card = BorderRadius.all(r12); - /// 按钮圆角 - /// - /// 示例:PrimaryButton / SecondaryButton + /// 按钮 — PrimaryButton / SecondaryButton static const BorderRadius button = BorderRadius.all(r8); - /// 弹窗圆角 - /// - /// 示例:Dialog / Modal + /// 弹窗 — Dialog / Modal static const BorderRadius dialog = BorderRadius.all(r16); - // ================================ - // 通用 BorderRadius - // ================================ - // 当组件 Token 不满足需求时使用 + // ── 通用 BorderRadius(组件级圆角不满足时使用)────────────────────────── static const BorderRadius br4 = BorderRadius.all(r4); - static const BorderRadius br6 = BorderRadius.all(r6); - static const BorderRadius br8 = BorderRadius.all(r8); - static const BorderRadius br10 = BorderRadius.all(r10); - static const BorderRadius br12 = BorderRadius.all(r12); - static const BorderRadius br14 = BorderRadius.all(r14); - static const BorderRadius br16 = BorderRadius.all(r16); - static const BorderRadius br18 = BorderRadius.all(r18); - static const BorderRadius br20 = BorderRadius.all(r20); - // ================================ - // 辅助方法 - // ================================ - // 用于生成顶部或底部圆角 + // ── 方向性圆角 ────────────────────────────────────────────────────────── - /// 生成顶部圆角 - /// - /// 常用于: - /// - BottomSheet - /// - 底部弹窗 - /// - 半屏弹层 - /// - /// 示例: - /// ```dart - /// borderRadius: AppRadius.top(AppRadius.r16) - /// ``` - static BorderRadius top(Radius r) => - BorderRadius.vertical(top: r); + /// 顶部圆角 — BottomSheet、底部弹窗 + static BorderRadius top(Radius r) => BorderRadius.vertical(top: r); - /// 生成底部圆角 - /// - /// 常用于: - /// - Header - /// - 顶部卡片 - /// - /// 示例: - /// ```dart - /// borderRadius: AppRadius.bottom(AppRadius.r16) - /// ``` - static BorderRadius bottom(Radius r) => - BorderRadius.vertical(bottom: r); -} \ No newline at end of file + /// 底部圆角 — Header、顶部卡片 + static BorderRadius bottom(Radius r) => BorderRadius.vertical(bottom: r); +} diff --git a/apps/im_app/lib/core/ui/base/shadows.dart b/apps/im_app/lib/core/ui/base/shadows.dart index 93e6e2e..1d67a24 100644 --- a/apps/im_app/lib/core/ui/base/shadows.dart +++ b/apps/im_app/lib/core/ui/base/shadows.dart @@ -1,129 +1,59 @@ import 'package:flutter/material.dart'; -import 'colors.dart'; -/// 阴影 Design Token +import 'package:im_app/core/ui/base/colors.dart'; + +/// 阴影常量 — 统一管理项目中的阴影规范 /// -/// 统一管理项目中的阴影规范,避免在业务代码中直接书写 `BoxShadow`。 -/// 所有阴影通过 Design Token 提供,保证: +/// 阴影颜色通过 [AppColors.shadow] 自动适配亮暗模式, +/// 无需手动判断 Brightness。 /// -/// - UI 风格统一 -/// - 支持 Dark / Light Mode -/// - 与设计稿(Figma)保持一致 -/// -/// ## 数据流位置 +/// ## 数据流 /// /// ``` -/// AppColors(颜色常量) -/// → AppShadows(阴影 Token) -/// → Context Extension(context.shadows) -/// → View 层消费 -/// ``` -/// -/// ## 使用示例 -/// -/// ```dart -/// Container( -/// decoration: BoxDecoration( -/// color: Colors.white, -/// boxShadow: context.shadows.bs8, -/// ), -/// ) +/// ColorBases(颜色常量)→ AppColors.shadow(语义色) +/// → AppShadows(阴影常量) +/// → context.shadows(View 层消费) /// ``` /// /// ## Elevation 体系 /// -/// 阴影遵循常见 UI 设计系统的层级规范: -/// /// - **4** : 小卡片 / List Item /// - **8** : Card / 商品卡片 /// - **12** : Dropdown / Popover /// - **16** : Dialog / Modal / 悬浮面板 +/// +/// ## 使用 +/// +/// ```dart +/// Container( +/// decoration: BoxDecoration( +/// color: context.colors.bgSecondary, +/// boxShadow: context.shadows.bs8, +/// ), +/// ) +/// ``` class AppShadows { - /// 构造函数,通过 BuildContext 获取当前主题 - AppShadows(this.context); + AppShadows(BuildContext context) + : _color = (Theme.of(context).brightness == Brightness.dark + ? const DarkColors() + : const LightColors()) + .shadow; - /// 当前 Widget 的 BuildContext - /// - /// 用于根据 Theme 判断 Light / Dark Mode, - /// 从而动态获取阴影颜色。 - final BuildContext context; + final Color _color; - /// 内部统一阴影生成方法 - /// - /// 避免重复创建 `BoxShadow` 逻辑, - /// 所有阴影 Token 都通过该方法生成。 - List _shadow({ - required double blur, - required double dy, - }) { - return [ - BoxShadow( + List _shadow({required double blur, required double dy}) => [ + BoxShadow(color: _color, blurRadius: blur, offset: Offset(0, dy)), + ]; - /// 阴影颜色来自 Design Token - color: _shadowColor, + /// Elevation 4 — 小卡片 / List Item + List get bs4 => _shadow(blur: 4, dy: 2); - /// 模糊半径(影响阴影扩散范围) - blurRadius: blur, + /// Elevation 8 — Card / 商品卡片 + List get bs8 => _shadow(blur: 8, dy: 4); - /// 阴影偏移 - offset: Offset(0, dy), - ) - ]; - } + /// Elevation 12 — Dropdown / Popover + List get bs12 => _shadow(blur: 12, dy: 8); - /// Elevation 4 - /// - /// 适用场景: - /// - List Item - /// - 小卡片 - List get bs4 => - _shadow( - blur: 4, - dy: 2, - ); - - /// Elevation 8 - /// - /// 适用场景: - /// - Card - /// - 商品卡片 - List get bs8 => - _shadow( - blur: 8, - dy: 4, - ); - - /// Elevation 12 - /// - /// 适用场景: - /// - Dropdown - /// - Popover - List get bs12 => - _shadow( - blur: 12, - dy: 8, - ); - - /// Elevation 16 - /// - /// 适用场景: - /// - Dialog - /// - Modal - /// - Floating Panel - List get bs16 => - _shadow( - blur: 16, - dy: 8, - ); - - /// 阴影颜色 Token - Color get _shadowColor { - final brightness = Theme - .of(context) - .brightness; - - return brightness == Brightness.dark - ? AppColors.black60 - : AppColors.black12; - } -} \ No newline at end of file + /// Elevation 16 — Dialog / Modal / 悬浮面板 + List get bs16 => _shadow(blur: 16, dy: 8); +} diff --git a/apps/im_app/lib/core/ui/base/spacing.dart b/apps/im_app/lib/core/ui/base/spacing.dart index d735c1c..615a27f 100644 --- a/apps/im_app/lib/core/ui/base/spacing.dart +++ b/apps/im_app/lib/core/ui/base/spacing.dart @@ -1,72 +1,32 @@ -/// 间距设计 Token +/// 间距常量 — 统一管理项目中的间距规范 /// -/// 统一管理项目中的间距规范,避免在业务代码中直接写 magic number,例如: +/// 避免在业务代码中直接写 magic number。 /// -/// ❌ 不推荐 -/// ```dart -/// Padding(padding: EdgeInsets.all(16)) -/// ``` -/// -/// ✅ 推荐 /// ```dart +/// // 用常量,不写裸数字 /// Padding(padding: EdgeInsets.all(AppSpacing.s16)) +/// SizedBox(height: AppSpacing.s8) /// ``` /// -/// 常用于: -/// - Padding -/// - Margin -/// - SizedBox -/// - Sliver 间距 -/// -/// 设计规范通常来源于 UI 设计系统,例如: -/// 4 / 8 / 12 / 16 / 24 / 32 +/// 基础档位:4 / 8 / 12 / 16 / 24 / 32 class AppSpacing { - /// 私有构造函数,防止实例化 AppSpacing._(); - // ================================ - // 基础间距 Token - // ================================ - - /// 4px 间距(最小间距) - /// - /// 常用于: - /// - icon 与文字之间 - /// - 紧凑布局 + /// 4px — icon 与文字之间、紧凑布局 static const double s4 = 4; - /// 8px 间距(小间距) - /// - /// 常用于: - /// - 列表 item 内间距 - /// - 小组件之间 + /// 8px — 列表 item 内间距、小组件之间 static const double s8 = 8; - /// 12px 间距(中小间距) - /// - /// 常用于: - /// - 表单组件 - /// - 信息块之间 + /// 12px — 表单组件、信息块之间 static const double s12 = 12; - /// 16px 间距(标准间距) - /// - /// 常用于: - /// - 页面 Padding - /// - Card 内边距 + /// 16px — 页面 Padding、Card 内边距 static const double s16 = 16; - /// 24px 间距(大间距) - /// - /// 常用于: - /// - 模块之间 - /// - Section 分隔 + /// 24px — 模块之间、Section 分隔 static const double s24 = 24; - /// 32px 间距(超大间距) - /// - /// 常用于: - /// - 页面大区块 - /// - 顶部/底部留白 + /// 32px — 页面大区块、顶部/底部留白 static const double s32 = 32; -} \ No newline at end of file +} diff --git a/apps/im_app/lib/core/ui/components/app_button.dart b/apps/im_app/lib/core/ui/components/app_button.dart index 0373a72..2b5aa8d 100644 --- a/apps/im_app/lib/core/ui/components/app_button.dart +++ b/apps/im_app/lib/core/ui/components/app_button.dart @@ -116,10 +116,10 @@ class AppButton extends StatelessWidget { } Widget _buildInverse(BuildContext context, Widget label) { - final s = context.styles; + final c = context.colors; final style = FilledButton.styleFrom( - backgroundColor: s.onSurface, - foregroundColor: s.surface, + backgroundColor: c.textPrimary, + foregroundColor: c.bgSecondary, ); if (icon != null) { return FilledButton.icon( diff --git a/apps/im_app/lib/features/login/view/widgets/login_otp_step.dart b/apps/im_app/lib/features/login/view/widgets/login_otp_step.dart index 557bb57..dd55425 100644 --- a/apps/im_app/lib/features/login/view/widgets/login_otp_step.dart +++ b/apps/im_app/lib/features/login/view/widgets/login_otp_step.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:im_app/core/ui/base/context_theme_ext.dart'; +import 'package:im_app/core/ui/components/app_button.dart'; import 'package:im_app/features/login/presentation/login_state.dart'; /// 登录步骤 2 — 输入验证码并完成登录 @@ -31,21 +33,22 @@ class LoginOtpStep extends StatelessWidget { @override Widget build(BuildContext context) { + final s = context.styles; + final c = context.colors; + return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( '输入验证码', - style: Theme.of(context).textTheme.headlineSmall, + style: s.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( '验证码已发送至 ${state.countryCode} ${state.maskedContact}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: s.bodyMedium?.copyWith(color: c.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 32), @@ -55,7 +58,6 @@ class LoginOtpStep extends StatelessWidget { maxLength: 4, decoration: const InputDecoration( labelText: '4 位验证码', - border: OutlineInputBorder(), counterText: '', ), autofillHints: const [AutofillHints.oneTimeCode], @@ -66,24 +68,19 @@ class LoginOtpStep extends StatelessWidget { padding: const EdgeInsets.only(bottom: 16), child: Text( state.error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: s.bodyError, textAlign: TextAlign.center, ), ), - FilledButton( + AppButton.primary( + label: '登录', onPressed: state.isLoading ? null : onVerifyAndLogin, - child: state.isLoading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('登录'), + isLoading: state.isLoading, ), const SizedBox(height: 12), - TextButton( + AppButton.text( + label: '返回修改手机号', onPressed: state.isLoading ? null : onBackToPhone, - child: const Text('返回修改手机号'), ), ], ); diff --git a/apps/im_app/lib/features/login/view/widgets/login_phone_step.dart b/apps/im_app/lib/features/login/view/widgets/login_phone_step.dart index 025e34a..eb9f3ab 100644 --- a/apps/im_app/lib/features/login/view/widgets/login_phone_step.dart +++ b/apps/im_app/lib/features/login/view/widgets/login_phone_step.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:im_app/core/ui/base/context_theme_ext.dart'; +import 'package:im_app/core/ui/components/app_button.dart'; import 'package:im_app/features/login/presentation/login_state.dart'; /// 登录步骤 1 — 输入国家代码 + 手机号 @@ -28,13 +30,16 @@ class LoginPhoneStep extends StatelessWidget { @override Widget build(BuildContext context) { + final s = context.styles; + final c = context.colors; + return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( '手机号登录', - style: Theme.of(context).textTheme.headlineSmall, + style: s.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 40), @@ -43,25 +48,17 @@ class LoginPhoneStep extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), + border: Border.all(color: c.borderDefault), borderRadius: BorderRadius.circular(4), ), - child: Text( - state.countryCode, - style: Theme.of(context).textTheme.bodyLarge, - ), + child: Text(state.countryCode, style: s.bodyLarge), ), const SizedBox(width: 8), Expanded( child: TextField( controller: phoneCtrl, keyboardType: TextInputType.phone, - decoration: const InputDecoration( - labelText: '手机号', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: '手机号'), autofillHints: const [AutofillHints.telephoneNumber], ), ), @@ -73,19 +70,14 @@ class LoginPhoneStep extends StatelessWidget { padding: const EdgeInsets.only(bottom: 16), child: Text( state.error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), + style: s.bodyError, textAlign: TextAlign.center, ), ), - FilledButton( + AppButton.primary( + label: '获取验证码', onPressed: state.isLoading ? null : onSendOtp, - child: state.isLoading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('获取验证码'), + isLoading: state.isLoading, ), ], ); diff --git a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart index 9e5a6f6..860dc99 100644 --- a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart +++ b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart @@ -29,11 +29,11 @@ class ThemeOptionTile extends StatelessWidget { @override Widget build(BuildContext context) { - final s = context.styles; + final c = context.colors; return ListTile( title: Text(label), - trailing: isSelected ? Icon(Icons.check, color: s.primary) : null, + trailing: isSelected ? Icon(Icons.check, color: c.brandPrimary) : null, onTap: onTap, ); } diff --git a/pubspec.yaml b/pubspec.yaml index c5c5851..ad96e89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ melos: scripts: analyze: description: "Run flutter analyze in all packages" - run: melos exec -- flutter analyze . + run: melos exec --concurrency=1 -- flutter analyze . test: description: "Run flutter test in all packages"