diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html index badb54c..3b7bfa0 100644 --- a/Doc/IM_App_架构设计.html +++ b/Doc/IM_App_架构设计.html @@ -3092,7 +3092,7 @@ 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) @@ -3461,7 +3461,7 @@ flowchart TD DesignSystem --> Colors[Colors 颜色] DesignSystem --> Typography[Typography 字体] - DesignSystem --> Tokens[Design Tokens 基础定义] + DesignSystem --> Tokens[间距 / 圆角 / 阴影] DesignSystem --> Theme[Theme 主题] Foundation --> Atoms[Atoms 原子组件] @@ -3522,39 +3522,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 +3621,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 +3713,8 @@ class AppTheme { Primary/Main -AppColors.primary -主色 +context.colors.brandPrimary +主品牌色 Button/Primary @@ -3716,18 +3723,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 +3784,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 +3813,7 @@ class AppTextField extends StatelessWidget { labelText: label, hintText: hint, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTokens.radiusMedium), + borderRadius: AppRadius.button, ), ), ); @@ -3831,11 +3838,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 +3909,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 +4150,7 @@ class PlatformButton extends StatelessWidget { if (PlatformAdapter.isIOS) { return CupertinoButton( onPressed: onPressed, - color: AppColors.primary, + color: context.colors.brandPrimary, child: Text(text), ); } @@ -6879,10 +6886,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 ThemeData
  • -
  • spacing / radius / shadows 等(待开发,按需添加)
  • +
  • shadows.dart(已实现):阴影常量 —— bs4 / bs8 / bs12 / bs16,颜色走 AppColors.shadow 自动适配亮暗
  • +
  • spacing.dart(已实现):间距常量 —— s4 / s8 / s12 / s16 / s24 / s32
  • +
  • radius.dart(已实现):圆角常量 —— 基础 r4~r20 + 组件级 card / button / dialog
  • +
  • app_theme.dart(已实现):主题组装 —— _build(AppColors c) 零 isDark 分支
  • +
  • context_theme_ext.dart(已实现):统一 context 扩展 —— context.colors + context.styles + context.shadows

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

@@ -9855,37 +9865,74 @@ flowchart TD

8.1 颜色体系

-

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

+

颜色采用 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"