Files
photo-live/apps/mobile/lib/app/theme.dart
T
2026-09-21 14:55:29 +08:00

258 lines
9.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 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,
),
);
}
}