c94b9e5e58
Co-Authored-By: Claude Code <noreply@anthropic.com>
258 lines
9.4 KiB
Dart
258 lines
9.4 KiB
Dart
/// 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,随系统字体缩放):22 页面标题 /
|
||
/// 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(0xFF5E6878);
|
||
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、轻阴影。
|
||
cardTheme: CardThemeData(
|
||
color: colorScheme.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
elevation: 1,
|
||
shadowColor: Colors.black.withValues(alpha: dark ? 0.4 : 0.08),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(kRadiusCard),
|
||
),
|
||
),
|
||
|
||
// 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,
|
||
),
|
||
|
||
// 底部导航:白色表面(深色深表面)+ 派生容器色指示器。
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
backgroundColor: colorScheme.surface,
|
||
surfaceTintColor: Colors.transparent,
|
||
indicatorColor: colorScheme.secondaryContainer,
|
||
height: 80,
|
||
labelTextStyle: WidgetStatePropertyAll(
|
||
textTheme.labelMedium!.copyWith(
|
||
color: colorScheme.onSurface,
|
||
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(kRadiusInput),
|
||
borderSide: BorderSide(color: color, width: width),
|
||
);
|
||
|
||
/// 字阶(方案 §3.1):22 页面标题 / 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: 22,
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
}
|