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 设计稿完全对应,确保设计与实现一致。
-/// 颜色定义 - 与 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 {
}
-/// 基础定义 - 间距、圆角、阴影等
-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 #FFFFFF surface 上的文字
-errorError #EB5757 错误状态
-successSuccess #27AE60 成功状态
-warningWarning #F2C94C 警告状态
+Brand primary400 #5BA3F5
+primary500 #2F80ED
+primary700 #1A6BD4
+Neutral neutral0 #FFFFFF
+neutral50 #F8F9FA
+neutral100 #F1F3F4
+neutral200 #E8EAED
+neutral400 #BDC1C6
+neutral600 #80868B
+neutral800 #3C4043
+neutral850 #2C2C2E
+neutral900 #202124
+neutral950 #000000
+Neutral Alpha black12 12% 透明度黑色
+black60 60% 透明度黑色
+Status error500 #EB5757
+success500 #27AE60
+warning500 #F2C94C
-灰阶(固定值,不随主题变化)
+Layer 2-3: AppColors — 语义接口 + 亮暗实现
-名称 色值 名称 色值 名称 色值
+语义名 用途 亮色 暗色
-white #FFFFFF gray50 #F8F9FA gray100 #F1F3F4
-gray200 #E8EAED gray400 #BDC1C6 gray600 #80868B
-gray800 #3C4043 gray900 #202124 black #000000
+bgPrimaryScaffold 底色 neutral50 neutral900
+bgSecondaryCard / Surface neutral0 neutral800
+bgTertiary次要区块 neutral100 neutral850
+textPrimary主文本 neutral900 neutral0
+textSecondary辅助文本 neutral600 neutral400
+textDisabled禁用态 neutral400 neutral600
+textOnPrimary品牌色上的文字 neutral0
+borderDefault默认边框 neutral200 neutral800
+borderFocused聚焦边框 primary500 primary400
+brandPrimary主品牌色 primary500 primary400
+brandPrimaryHoverHover 态 primary700 primary500
+statusError错误 error500
+statusSuccess成功 success500
+statusWarning警告 warning500
+navBarBg底部导航背景 neutral0 neutral900
+navBarSelected导航选中色 primary500 primary400
+divider分割线 neutral100 neutral800
+shadow阴影颜色 black12 black60
-使用原则:需随主题切换 → 用语义色(primary、surface);亮暗保持不变 → 用灰阶固定值。
+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"