feat(mobile): UI-01 theme system, shared components and navigation
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,257 @@
|
||||
/// 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';
|
||||
|
||||
/// 基础主题(Material 3,种子色)。完整视觉规范随后续任务迭代。
|
||||
import '../ui/tokens.dart';
|
||||
|
||||
abstract final class AppTheme {
|
||||
/// Material 种子色(容器色派生基准)。
|
||||
static const Color seedColor = Color(0xFF1A73E8);
|
||||
|
||||
static ThemeData get light => ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: seedColor),
|
||||
);
|
||||
// ── 浅色基准值(方案 §3.1)──────────────────────────────────────────
|
||||
|
||||
static ThemeData get dark => ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
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.dark,
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user