2026-09-21 14:55:29 +08:00
|
|
|
|
/// 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 同系。
|
|
|
|
|
|
///
|
2026-09-22 00:28:39 +08:00
|
|
|
|
/// 字阶(fontSize 走 TextScaler,随系统字体缩放;UI-03 §3 校准):24
|
|
|
|
|
|
/// 页面标题 / 17 分组标题 / 16-14 正文 / 13-12 辅助。组件:主按钮高 48
|
|
|
|
|
|
/// (区间 48-52)、输入框与按钮圆角 12、卡片圆角 16、Dialog 圆角 16、
|
2026-09-21 14:55:29 +08:00
|
|
|
|
/// BottomSheet 顶部圆角 16。动效时长常量见 ui/tokens.dart。
|
|
|
|
|
|
library;
|
|
|
|
|
|
|
2026-09-19 00:22:04 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
|
2026-09-21 14:55:29 +08:00
|
|
|
|
import '../ui/tokens.dart';
|
|
|
|
|
|
|
2026-09-19 00:22:04 +08:00
|
|
|
|
abstract final class AppTheme {
|
2026-09-21 14:55:29 +08:00
|
|
|
|
/// Material 种子色(容器色派生基准)。
|
2026-09-19 00:22:04 +08:00
|
|
|
|
static const Color seedColor = Color(0xFF1A73E8);
|
|
|
|
|
|
|
2026-09-21 14:55:29 +08:00
|
|
|
|
// ── 浅色基准值(方案 §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);
|
2026-09-23 08:09:20 +08:00
|
|
|
|
static const Color _textSecondary = Color(0xFF69778E); // UI06: app-preview --muted
|
2026-09-21 14:55:29 +08:00
|
|
|
|
static const Color _divider = Color(0xFFE5EAF1);
|
|
|
|
|
|
static const Color _waiting = Color(0xFF996300);
|
|
|
|
|
|
static const Color _onWaiting = Color(0xFFFFFFFF);
|
|
|
|
|
|
static const Color _error = Color(0xFFB42318);
|
|
|
|
|
|
|
|
|
|
|
|
// ── 深色对应语义值(同语义、深色表面提亮)──────────────────────────
|
2026-09-19 00:22:04 +08:00
|
|
|
|
|
2026-09-21 14:55:29 +08:00
|
|
|
|
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(
|
2026-09-19 00:22:04 +08:00
|
|
|
|
seedColor: seedColor,
|
2026-09-21 14:55:29 +08:00
|
|
|
|
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,
|
2026-09-19 00:22:04 +08:00
|
|
|
|
),
|
2026-09-21 14:55:29 +08:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2026-09-23 08:09:20 +08:00
|
|
|
|
// 卡片:白色表面、圆角 16、细边框无阴影(app-preview .card 形态)。
|
2026-09-21 14:55:29 +08:00
|
|
|
|
cardTheme: CardThemeData(
|
|
|
|
|
|
color: colorScheme.surface,
|
|
|
|
|
|
surfaceTintColor: Colors.transparent,
|
2026-09-23 08:09:20 +08:00
|
|
|
|
elevation: 0,
|
|
|
|
|
|
shadowColor: Colors.transparent,
|
2026-09-21 14:55:29 +08:00
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(kRadiusCard),
|
2026-09-23 08:09:20 +08:00
|
|
|
|
side: BorderSide(color: dark ? _dividerDark : kColorCardBorder),
|
2026-09-21 14:55:29 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2026-09-23 08:09:20 +08:00
|
|
|
|
// 底部导航(app-preview .nav):白底 + 顶部细线 + **无胶囊指示器**;
|
|
|
|
|
|
// 选中 primary 蓝图标/文字,未选次文字色。
|
2026-09-21 14:55:29 +08:00
|
|
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
|
|
|
|
backgroundColor: colorScheme.surface,
|
|
|
|
|
|
surfaceTintColor: Colors.transparent,
|
2026-09-23 08:09:20 +08:00
|
|
|
|
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),
|
2026-09-21 14:55:29 +08:00
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// 轻分隔线(不用粗边框)。
|
|
|
|
|
|
dividerTheme: DividerThemeData(
|
|
|
|
|
|
color: dark ? _dividerDark : _divider,
|
|
|
|
|
|
thickness: 1,
|
|
|
|
|
|
space: 1,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static OutlineInputBorder _inputBorder(
|
|
|
|
|
|
ColorScheme scheme,
|
|
|
|
|
|
Color color, {
|
|
|
|
|
|
double width = 1,
|
|
|
|
|
|
}) => OutlineInputBorder(
|
2026-09-23 08:09:20 +08:00
|
|
|
|
borderRadius: BorderRadius.circular(kRadiusField),
|
2026-09-21 14:55:29 +08:00
|
|
|
|
borderSide: BorderSide(color: color, width: width),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-22 00:28:39 +08:00
|
|
|
|
/// 字阶(UI-03 §3):24 页面标题 / 17 分组 / 16-14 正文 / 13-12 辅助。
|
2026-09-21 14:55:29 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 只声明 fontSize/字重/颜色,字体族与缩放交给默认排版 + TextScaler
|
|
|
|
|
|
/// (fontSize 随系统字体大小缩放)。
|
|
|
|
|
|
static TextTheme _textTheme(ColorScheme scheme) {
|
|
|
|
|
|
final onSurface = scheme.onSurface;
|
|
|
|
|
|
final onSecondaryText = scheme.onSurfaceVariant;
|
|
|
|
|
|
return TextTheme(
|
|
|
|
|
|
titleLarge: TextStyle(
|
2026-09-22 00:28:39 +08:00
|
|
|
|
fontSize: 24,
|
2026-09-21 14:55:29 +08:00
|
|
|
|
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,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-19 00:22:04 +08:00
|
|
|
|
}
|