424 lines
14 KiB
Dart
424 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// 应用主题:橙白「双橙策略」,回归并与旧版「云端手机端」视觉延续。
|
||
///
|
||
/// 设计依据:docs/手机端UI优化方案-v2.md
|
||
/// - 行动色 [brandPrimary] (#C2410C) 承载白字(对比度 5.18 ✅),用于主按钮 / 导航选中 / 可点文字。
|
||
/// - 品牌亮橙 [brandOrange] (#FF914D) 仅用于大色块 / 渐变 / 图表,绝不配白字(对比度仅 2.23 ❌)。
|
||
/// - 辅金 [accentGold] (#FFBE58) 用于高亮 / 星级 / 推荐位。
|
||
class AppTheme {
|
||
AppTheme._();
|
||
|
||
/// 行动色(主色)。
|
||
static const Color brandPrimary = Color(0xFFC2410C);
|
||
|
||
/// 品牌亮橙(仅大色块 / 渐变 / 图表)。
|
||
static const Color brandOrange = Color(0xFFFF914D);
|
||
|
||
/// 辅金。
|
||
static const Color accentGold = Color(0xFFFFBE58);
|
||
|
||
static const ColorScheme _light = ColorScheme(
|
||
brightness: Brightness.light,
|
||
primary: Color(0xFFC2410C),
|
||
onPrimary: Color(0xFFFFFFFF),
|
||
primaryContainer: Color(0xFFFFD3B3),
|
||
onPrimaryContainer: Color(0xFF4A1D00),
|
||
secondary: Color(0xFF8A5A3A),
|
||
onSecondary: Color(0xFFFFFFFF),
|
||
secondaryContainer: Color(0xFFF5DFCB),
|
||
onSecondaryContainer: Color(0xFF5A3A22),
|
||
tertiary: Color(0xFF9A6000),
|
||
onTertiary: Color(0xFFFFFFFF),
|
||
tertiaryContainer: Color(0xFFFFE0A3),
|
||
onTertiaryContainer: Color(0xFF5C3D00),
|
||
error: Color(0xFFB02A20),
|
||
onError: Color(0xFFFFFFFF),
|
||
errorContainer: Color(0xFFFFDDD8),
|
||
onErrorContainer: Color(0xFF7A2A22),
|
||
surface: Color(0xFFFCFAF8),
|
||
onSurface: Color(0xFF1A1614),
|
||
onSurfaceVariant: Color(0xFF6E655C),
|
||
surfaceContainerLowest: Color(0xFFFFFFFF),
|
||
surfaceContainerLow: Color(0xFFF7F3EF),
|
||
surfaceContainer: Color(0xFFF2EDE8),
|
||
surfaceContainerHigh: Color(0xFFECE5DF),
|
||
surfaceContainerHighest: Color(0xFFE5DED7),
|
||
outline: Color(0xFF8A8178),
|
||
outlineVariant: Color(0xFFDCCFC2),
|
||
shadow: Color(0xFF5C2E12),
|
||
scrim: Color(0xFF1A1614),
|
||
inverseSurface: Color(0xFF2E2823),
|
||
onInverseSurface: Color(0xFFF7EFE9),
|
||
inversePrimary: Color(0xFFFFB487),
|
||
surfaceTint: Color(0xFFC2410C),
|
||
);
|
||
|
||
static const ColorScheme _dark = ColorScheme(
|
||
brightness: Brightness.dark,
|
||
primary: Color(0xFFFFB487),
|
||
onPrimary: Color(0xFF542000),
|
||
primaryContainer: Color(0xFF63300D),
|
||
onPrimaryContainer: Color(0xFFFFDCC2),
|
||
secondary: Color(0xFFE8C39B),
|
||
onSecondary: Color(0xFF542000),
|
||
secondaryContainer: Color(0xFF5A3A22),
|
||
onSecondaryContainer: Color(0xFFFFDCC2),
|
||
tertiary: Color(0xFFF2BE62),
|
||
onTertiary: Color(0xFF542000),
|
||
tertiaryContainer: Color(0xFF5C3D00),
|
||
onTertiaryContainer: Color(0xFFFFDCC2),
|
||
error: Color(0xFFFF8474),
|
||
onError: Color(0xFF5A1610),
|
||
errorContainer: Color(0xFF7A2A22),
|
||
onErrorContainer: Color(0xFFFFC9C2),
|
||
surface: Color(0xFF16130F),
|
||
onSurface: Color(0xFFF3E9E2),
|
||
onSurfaceVariant: Color(0xFFC4B8AE),
|
||
surfaceContainerLowest: Color(0xFF0F0D0A),
|
||
surfaceContainerLow: Color(0xFF1E1A16),
|
||
surfaceContainer: Color(0xFF23201B),
|
||
surfaceContainerHigh: Color(0xFF2D2924),
|
||
surfaceContainerHighest: Color(0xFF38332D),
|
||
outline: Color(0xFF7A7068),
|
||
outlineVariant: Color(0xFF453F39),
|
||
shadow: Color(0xFF000000),
|
||
scrim: Color(0xFF0F0D0A),
|
||
inverseSurface: Color(0xFFF3E9E2),
|
||
onInverseSurface: Color(0xFF2E2823),
|
||
inversePrimary: Color(0xFFC2410C),
|
||
surfaceTint: Color(0xFFFFB487),
|
||
);
|
||
|
||
/// 浅色主题(橙白品牌主场)。
|
||
static ThemeData light() => _base(_light).copyWith(
|
||
extensions: const <ThemeExtension<dynamic>>[AppSemanticColors.light],
|
||
);
|
||
|
||
/// 深色主题(暖化 surface,主色提亮为 #FFB487)。
|
||
static ThemeData dark() => _base(_dark).copyWith(
|
||
extensions: const <ThemeExtension<dynamic>>[AppSemanticColors.dark],
|
||
);
|
||
|
||
static ThemeData _base(ColorScheme scheme) {
|
||
return ThemeData(
|
||
useMaterial3: true,
|
||
brightness: scheme.brightness,
|
||
colorScheme: scheme,
|
||
scaffoldBackgroundColor: scheme.surface,
|
||
// 暖棕低透明阴影,替代默认偏硬的黑色阴影(云感)。
|
||
shadowColor: const Color(0x245C2E12),
|
||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||
visualDensity: VisualDensity.standard,
|
||
extensions: const <ThemeExtension<dynamic>>[],
|
||
iconTheme: IconThemeData(
|
||
size: AppIconSize.regular,
|
||
color: scheme.onSurfaceVariant,
|
||
),
|
||
appBarTheme: AppBarTheme(
|
||
centerTitle: true,
|
||
backgroundColor: scheme.surface,
|
||
foregroundColor: scheme.onSurface,
|
||
elevation: 0,
|
||
// 滚动后轻微浮起,呼应云影。
|
||
scrolledUnderElevation: 3,
|
||
shadowColor: const Color(0x245C2E12),
|
||
),
|
||
cardTheme: CardTheme(
|
||
elevation: 0,
|
||
clipBehavior: Clip.antiAlias,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
),
|
||
),
|
||
chipTheme: ChipThemeData(
|
||
backgroundColor: scheme.surfaceContainerHighest,
|
||
labelStyle: TextStyle(color: scheme.onSurface),
|
||
selectedColor: scheme.primaryContainer,
|
||
checkmarkColor: scheme.onPrimaryContainer,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||
),
|
||
),
|
||
inputDecorationTheme: InputDecorationTheme(
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
),
|
||
filled: true,
|
||
fillColor: scheme.surfaceContainerHighest,
|
||
labelStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||
hintStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||
errorStyle: TextStyle(color: scheme.error),
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
),
|
||
snackBarTheme: const SnackBarThemeData(
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
// 全局兜底:30 处 showModalBottomSheet 一次性拿到把手 / 安全区 / 顶部圆角 / 限宽。
|
||
bottomSheetTheme: BottomSheetThemeData(
|
||
showDragHandle: true,
|
||
backgroundColor: scheme.surface,
|
||
surfaceTintColor: scheme.surfaceTint,
|
||
modalBackgroundColor: scheme.surface,
|
||
elevation: 0,
|
||
clipBehavior: Clip.antiAlias,
|
||
shape: RoundedRectangleBorder(borderRadius: AppRadius.sheetTop),
|
||
constraints: const BoxConstraints(maxWidth: 640),
|
||
),
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
height: 72,
|
||
elevation: 0,
|
||
backgroundColor: scheme.surfaceContainer,
|
||
surfaceTintColor: scheme.surfaceTint,
|
||
indicatorColor: scheme.secondaryContainer,
|
||
indicatorShape: const StadiumBorder(),
|
||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||
iconTheme: WidgetStateProperty.resolveWith((states) => IconThemeData(
|
||
size: 24,
|
||
color: states.contains(WidgetState.selected)
|
||
? scheme.onSecondaryContainer
|
||
: scheme.onSurfaceVariant,
|
||
)),
|
||
labelTextStyle: WidgetStateProperty.resolveWith((states) => TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: states.contains(WidgetState.selected)
|
||
? FontWeight.w600
|
||
: FontWeight.w500,
|
||
color: states.contains(WidgetState.selected)
|
||
? scheme.onSurface
|
||
: scheme.onSurfaceVariant,
|
||
)),
|
||
),
|
||
navigationRailTheme: NavigationRailThemeData(
|
||
backgroundColor: scheme.surfaceContainer,
|
||
elevation: 0,
|
||
indicatorColor: scheme.secondaryContainer,
|
||
indicatorShape: const StadiumBorder(),
|
||
minWidth: 72,
|
||
groupAlignment: -0.2,
|
||
labelType: NavigationRailLabelType.all,
|
||
selectedIconTheme:
|
||
IconThemeData(size: 24, color: scheme.onSecondaryContainer),
|
||
unselectedIconTheme:
|
||
IconThemeData(size: 24, color: scheme.onSurfaceVariant),
|
||
selectedLabelTextStyle: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: scheme.onSurface,
|
||
),
|
||
unselectedLabelTextStyle: TextStyle(
|
||
fontSize: 12,
|
||
color: scheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
dialogTheme: DialogThemeData(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.xxl),
|
||
),
|
||
backgroundColor: scheme.surfaceContainerLow,
|
||
surfaceTintColor: scheme.surfaceTint,
|
||
),
|
||
iconButtonTheme: IconButtonThemeData(
|
||
style: IconButton.styleFrom(
|
||
minimumSize: const Size(48, 48),
|
||
tapTargetSize: MaterialTapTargetSize.padded,
|
||
padding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
filledButtonTheme: FilledButtonThemeData(
|
||
style: FilledButton.styleFrom(
|
||
minimumSize: const Size(64, 52),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
),
|
||
),
|
||
),
|
||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||
style: ElevatedButton.styleFrom(
|
||
minimumSize: const Size(64, 52),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
),
|
||
),
|
||
),
|
||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size(64, 52),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
),
|
||
),
|
||
),
|
||
textButtonTheme: TextButtonThemeData(
|
||
style: TextButton.styleFrom(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 圆角梯度:非胶囊、四角一致、克制感(呼应 logo ≈3% 圆角)。
|
||
class AppRadius {
|
||
static const double xs = 4; // 进度条、缩略图角
|
||
static const double sm = 8; // Chip、SnackBar、小标签
|
||
static const double md = 12; // 输入框(沿用现值,保持手感连续)
|
||
static const double lg = 16; // ★ 标准卡片、按钮(主力值)
|
||
static const double xl = 20; // 分组容器、大 Banner 卡
|
||
static const double xxl = 28; // ★ 底部弹窗顶部两角(云朵弧线核心表达)
|
||
static const double full = 999; // 仅状态 Chip / 头像 / pill
|
||
|
||
/// 底部弹窗顶部两角大圆角、底部直角(云朵上缘多弧、下缘平缓)。
|
||
static const BorderRadius sheetTop =
|
||
BorderRadius.vertical(top: Radius.circular(xxl));
|
||
|
||
AppRadius._();
|
||
}
|
||
|
||
/// 云感暖阴影:模糊:偏移 ≈4:1,暖棕而非纯黑。
|
||
class AppShadows {
|
||
static const List<BoxShadow> xs = [
|
||
BoxShadow(
|
||
color: Color(0x0F5C2E12),
|
||
blurRadius: 8,
|
||
offset: Offset(0, 2),
|
||
spreadRadius: -1,
|
||
),
|
||
];
|
||
static const List<BoxShadow> sm = [
|
||
BoxShadow(
|
||
color: Color(0x145C2E12),
|
||
blurRadius: 12,
|
||
offset: Offset(0, 4),
|
||
spreadRadius: -2,
|
||
),
|
||
];
|
||
static const List<BoxShadow> md = [
|
||
BoxShadow(
|
||
color: Color(0x1A5C2E12),
|
||
blurRadius: 20,
|
||
offset: Offset(0, 8),
|
||
spreadRadius: -4,
|
||
),
|
||
];
|
||
static const List<BoxShadow> lg = [
|
||
BoxShadow(
|
||
color: Color(0x245C2E12),
|
||
blurRadius: 32,
|
||
offset: Offset(0, 12),
|
||
spreadRadius: -6,
|
||
),
|
||
];
|
||
static const List<BoxShadow> xl = [
|
||
BoxShadow(
|
||
color: Color(0x245C2E12),
|
||
blurRadius: 48,
|
||
offset: Offset(0, 20),
|
||
spreadRadius: -8,
|
||
),
|
||
];
|
||
|
||
/// 深色:暖色阴影在暗底不可见,改用纯黑 + 描边。
|
||
static const List<BoxShadow> dark = [
|
||
BoxShadow(
|
||
color: Color(0x66000000),
|
||
blurRadius: 24,
|
||
offset: Offset(0, 8),
|
||
spreadRadius: -4,
|
||
),
|
||
];
|
||
|
||
AppShadows._();
|
||
}
|
||
|
||
/// 图标尺寸栅格(24dp 为默认)。
|
||
class AppIconSize {
|
||
static const double inline = 16; // 正文行内
|
||
static const double dense = 20; // 密集信息行
|
||
static const double regular = 24; // ★ 默认:列表 leading、按钮内、AppBar actions
|
||
static const double medium = 28; // 卡片强调图标
|
||
static const double large = 32; // 列表主 leading
|
||
static const double display = 48; // 空状态 / 结果页
|
||
static const double hero = 64; // 关于页 logo、引导页
|
||
|
||
AppIconSize._();
|
||
}
|
||
|
||
/// 品牌视觉常量:logo 云朵橙→金渐变。
|
||
class AppBrand {
|
||
static const Color cloud = Color(0xFFFF914D); // logo 主色
|
||
static const Color cloudGold = Color(0xFFFFBE58); // logo 金黄
|
||
static const Color onCloud = Color(0xFF3D1A00); // 亮橙底上的深棕文字(6.99 ✅)
|
||
|
||
/// logo 橙→金渐变(云朵受光)。
|
||
static const LinearGradient cloudGradient = LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [Color(0xFFFF914D), Color(0xFFFFBE58)],
|
||
);
|
||
|
||
AppBrand._();
|
||
}
|
||
|
||
/// 语义色(成功 / 警告 / 错误 / 信息),通过 ThemeExtension 暴露。
|
||
///
|
||
/// 警告用琥珀黄而非橙,避免与品牌橙语义打架。
|
||
class AppSemanticColors extends ThemeExtension<AppSemanticColors> {
|
||
const AppSemanticColors({
|
||
required this.success,
|
||
required this.successContainer,
|
||
required this.onSuccessContainer,
|
||
required this.warning,
|
||
required this.warningContainer,
|
||
required this.onWarningContainer,
|
||
required this.info,
|
||
required this.infoContainer,
|
||
required this.onInfoContainer,
|
||
});
|
||
|
||
final Color success;
|
||
final Color successContainer;
|
||
final Color onSuccessContainer;
|
||
final Color warning;
|
||
final Color warningContainer;
|
||
final Color onWarningContainer;
|
||
final Color info;
|
||
final Color infoContainer;
|
||
final Color onInfoContainer;
|
||
|
||
static const light = AppSemanticColors(
|
||
success: Color(0xFF1E7A4E),
|
||
successContainer: Color(0xFFD3EDDD),
|
||
onSuccessContainer: Color(0xFF1E5138),
|
||
warning: Color(0xFF8A5C00),
|
||
warningContainer: Color(0xFFFFE9B8),
|
||
onWarningContainer: Color(0xFF5C4100),
|
||
info: Color(0xFF0B6397),
|
||
infoContainer: Color(0xFFD7E9FA),
|
||
onInfoContainer: Color(0xFF1A4763),
|
||
);
|
||
|
||
static const dark = AppSemanticColors(
|
||
success: Color(0xFF5FD398),
|
||
successContainer: Color(0xFF1E5138),
|
||
onSuccessContainer: Color(0xFFA8E6C4),
|
||
warning: Color(0xFFF0BE4E),
|
||
warningContainer: Color(0xFF5C4100),
|
||
onWarningContainer: Color(0xFFFFDCA8),
|
||
info: Color(0xFF7FC4F0),
|
||
infoContainer: Color(0xFF1A4763),
|
||
onInfoContainer: Color(0xFFB8DCF5),
|
||
);
|
||
|
||
@override
|
||
ThemeExtension<AppSemanticColors> copyWith() => this;
|
||
|
||
@override
|
||
ThemeExtension<AppSemanticColors> lerp(
|
||
covariant ThemeExtension<AppSemanticColors>? other, double t) =>
|
||
other ?? this;
|
||
}
|