颜色,基础组件重新封装,降低理解难度,分层更明显。入口更抽象
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
/// 底部圆角 — Header、顶部卡片
|
||||
static BorderRadius bottom(Radius r) => BorderRadius.vertical(bottom: r);
|
||||
}
|
||||
|
||||
@@ -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<BoxShadow> _shadow({
|
||||
required double blur,
|
||||
required double dy,
|
||||
}) {
|
||||
return [
|
||||
BoxShadow(
|
||||
List<BoxShadow> _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<BoxShadow> get bs4 => _shadow(blur: 4, dy: 2);
|
||||
|
||||
/// 模糊半径(影响阴影扩散范围)
|
||||
blurRadius: blur,
|
||||
/// Elevation 8 — Card / 商品卡片
|
||||
List<BoxShadow> get bs8 => _shadow(blur: 8, dy: 4);
|
||||
|
||||
/// 阴影偏移
|
||||
offset: Offset(0, dy),
|
||||
)
|
||||
];
|
||||
}
|
||||
/// Elevation 12 — Dropdown / Popover
|
||||
List<BoxShadow> get bs12 => _shadow(blur: 12, dy: 8);
|
||||
|
||||
/// Elevation 4
|
||||
///
|
||||
/// 适用场景:
|
||||
/// - List Item
|
||||
/// - 小卡片
|
||||
List<BoxShadow> get bs4 =>
|
||||
_shadow(
|
||||
blur: 4,
|
||||
dy: 2,
|
||||
);
|
||||
|
||||
/// Elevation 8
|
||||
///
|
||||
/// 适用场景:
|
||||
/// - Card
|
||||
/// - 商品卡片
|
||||
List<BoxShadow> get bs8 =>
|
||||
_shadow(
|
||||
blur: 8,
|
||||
dy: 4,
|
||||
);
|
||||
|
||||
/// Elevation 12
|
||||
///
|
||||
/// 适用场景:
|
||||
/// - Dropdown
|
||||
/// - Popover
|
||||
List<BoxShadow> get bs12 =>
|
||||
_shadow(
|
||||
blur: 12,
|
||||
dy: 8,
|
||||
);
|
||||
|
||||
/// Elevation 16
|
||||
///
|
||||
/// 适用场景:
|
||||
/// - Dialog
|
||||
/// - Modal
|
||||
/// - Floating Panel
|
||||
List<BoxShadow> 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;
|
||||
}
|
||||
}
|
||||
/// Elevation 16 — Dialog / Modal / 悬浮面板
|
||||
List<BoxShadow> get bs16 => _shadow(blur: 16, dy: 8);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user