/// UI-01 主题(视觉规范 §3.1 精确落地)。 /// /// 浅色基准:primary #1A73E8 / surface #FFFFFF / 页面背景 #F6F8FB / /// 主文字 #172033 / 次文字 #5E6878 / 分隔线 #E5EAF1;等待色 #996300 承载 /// ColorScheme.secondary、错误 #B42318 承载 error。成功色**不进** /// ColorScheme(Material 无该语义角色),以独立令牌提供(见 ui/tokens.dart, /// 成功/等待/错误均另有深色提亮变体)。 /// /// 深色:语义对应色(深表面 + 浅文字 + 提亮的主色/等待色/错误色);图片 /// 与内容不做反色处理,沿用跟随系统的深浅色切换。容器色(secondary/ /// primaryContainer 等)由种子色派生,与 #1A73E8 同系。 /// /// 字阶(fontSize 走 TextScaler,随系统字体缩放;UI-03 §3 校准):24 /// 页面标题 / 17 分组标题 / 16-14 正文 / 13-12 辅助。组件:主按钮高 48 /// (区间 48-52)、输入框与按钮圆角 12、卡片圆角 16、Dialog 圆角 16、 /// BottomSheet 顶部圆角 16。动效时长常量见 ui/tokens.dart。 library; import 'package:flutter/material.dart'; import '../ui/tokens.dart'; abstract final class AppTheme { /// Material 种子色(容器色派生基准)。 static const Color seedColor = Color(0xFF1A73E8); // ── 浅色基准值(方案 §3.1)────────────────────────────────────────── static const Color _primary = Color(0xFF1A73E8); static const Color _onPrimary = Color(0xFFFFFFFF); static const Color _surface = Color(0xFFFFFFFF); static const Color _onSurface = Color(0xFF172033); static const Color _background = Color(0xFFF6F8FB); static const Color _textSecondary = Color(0xFF69778E); // UI06: app-preview --muted static const Color _divider = Color(0xFFE5EAF1); static const Color _waiting = Color(0xFF996300); static const Color _onWaiting = Color(0xFFFFFFFF); static const Color _error = Color(0xFFB42318); // ── 深色对应语义值(同语义、深色表面提亮)────────────────────────── static const Color _primaryDark = Color(0xFF8AB4F8); static const Color _onPrimaryDark = Color(0xFF0B2E6F); static const Color _surfaceDark = Color(0xFF1F2530); static const Color _onSurfaceDark = Color(0xFFE6EAF2); static const Color _backgroundDark = Color(0xFF12161D); static const Color _textSecondaryDark = Color(0xFFA6B0C0); static const Color _dividerDark = Color(0xFF333B47); static const Color _waitingDark = Color(0xFFFCD34D); static const Color _onWaitingDark = Color(0xFF402E00); static const Color _errorDark = Color(0xFFF87171); static const Color _onErrorDark = Color(0xFF5C1A15); static ThemeData get light => _build(Brightness.light); static ThemeData get dark => _build(Brightness.dark); static ThemeData _build(Brightness brightness) { final dark = brightness == Brightness.dark; final colorScheme = ColorScheme.fromSeed( seedColor: seedColor, brightness: brightness, ).copyWith( primary: dark ? _primaryDark : _primary, onPrimary: dark ? _onPrimaryDark : _onPrimary, secondary: dark ? _waitingDark : _waiting, onSecondary: dark ? _onWaitingDark : _onWaiting, error: dark ? _errorDark : _error, onError: dark ? _onErrorDark : Colors.white, surface: dark ? _surfaceDark : _surface, onSurface: dark ? _onSurfaceDark : _onSurface, surfaceContainerHighest: dark ? _dividerDark : _divider, onSurfaceVariant: dark ? _textSecondaryDark : _textSecondary, ); final textTheme = _textTheme(colorScheme); final buttonBorder = RoundedRectangleBorder( borderRadius: BorderRadius.circular(kRadiusInput), ); return ThemeData( brightness: brightness, colorScheme: colorScheme, scaffoldBackgroundColor: dark ? _backgroundDark : _background, textTheme: textTheme, // AppBar:干净(零海拔 + 页面背景同色),标题用 titleLarge。 appBarTheme: AppBarTheme( backgroundColor: dark ? _backgroundDark : _background, surfaceTintColor: Colors.transparent, foregroundColor: colorScheme.onSurface, titleTextStyle: textTheme.titleLarge, elevation: 0, scrolledUnderElevation: 1, centerTitle: false, ), // 按钮:主/次操作统一高 48(规格 48-52)、圆角 12。 filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size(64, 48), shape: buttonBorder, ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( minimumSize: const Size(64, 48), shape: buttonBorder, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( minimumSize: const Size(48, 48), shape: buttonBorder, ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( minimumSize: const Size(48, 48), shape: buttonBorder, ), ), // 输入框:圆角 12,轻分隔线边框,聚焦主色 2px。 inputDecorationTheme: InputDecorationTheme( border: _inputBorder(colorScheme, colorScheme.outline), enabledBorder: _inputBorder( colorScheme, dark ? _dividerDark : _divider, ), focusedBorder: _inputBorder(colorScheme, colorScheme.primary, width: 2), errorBorder: _inputBorder(colorScheme, colorScheme.error), focusedErrorBorder: _inputBorder( colorScheme, colorScheme.error, width: 2, ), contentPadding: const EdgeInsets.symmetric( horizontal: kSpace16, vertical: kSpace12, ), ), // 卡片:白色表面、圆角 16、细边框无阴影(app-preview .card 形态)。 cardTheme: CardThemeData( color: colorScheme.surface, surfaceTintColor: Colors.transparent, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(kRadiusCard), side: BorderSide(color: dark ? _dividerDark : kColorCardBorder), ), ), // Dialog 圆角 16;BottomSheet 仅顶部圆角 16。 dialogTheme: DialogThemeData( surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(kRadiusCard), ), ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(kRadiusCard), ), ), ), // Snackbar:浮动 + inverseSurface(自动随深浅色),动作色 inversePrimary。 snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: colorScheme.inverseSurface, contentTextStyle: textTheme.bodyMedium?.copyWith( color: colorScheme.onInverseSurface, ), actionTextColor: colorScheme.inversePrimary, ), // 底部导航(app-preview .nav):白底 + 顶部细线 + **无胶囊指示器**; // 选中 primary 蓝图标/文字,未选次文字色。 navigationBarTheme: NavigationBarThemeData( backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, indicatorColor: Colors.transparent, height: 68, iconTheme: WidgetStateProperty.resolveWith( (states) => IconThemeData( size: 24, color: states.contains(WidgetState.selected) ? colorScheme.primary : (dark ? _textSecondaryDark : kColorTextSecondary), ), ), labelTextStyle: WidgetStateProperty.resolveWith( (states) => textTheme.labelMedium!.copyWith( color: states.contains(WidgetState.selected) ? colorScheme.primary : (dark ? _textSecondaryDark : kColorTextSecondary), fontWeight: FontWeight.w500, ), ), ), // 轻分隔线(不用粗边框)。 dividerTheme: DividerThemeData( color: dark ? _dividerDark : _divider, thickness: 1, space: 1, ), ); } static OutlineInputBorder _inputBorder( ColorScheme scheme, Color color, { double width = 1, }) => OutlineInputBorder( borderRadius: BorderRadius.circular(kRadiusField), borderSide: BorderSide(color: color, width: width), ); /// 字阶(UI-03 §3):24 页面标题 / 17 分组 / 16-14 正文 / 13-12 辅助。 /// /// 只声明 fontSize/字重/颜色,字体族与缩放交给默认排版 + TextScaler /// (fontSize 随系统字体大小缩放)。 static TextTheme _textTheme(ColorScheme scheme) { final onSurface = scheme.onSurface; final onSecondaryText = scheme.onSurfaceVariant; return TextTheme( titleLarge: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, color: onSurface, ), titleMedium: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: onSurface, ), titleSmall: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: onSurface, ), bodyLarge: TextStyle(fontSize: 16, color: onSurface), bodyMedium: TextStyle(fontSize: 14, color: onSurface), bodySmall: TextStyle(fontSize: 13, color: onSecondaryText), labelLarge: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: onSurface, ), labelMedium: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: onSurface, ), labelSmall: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: onSecondaryText, ), ); } }