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;
|
|||
|
|
}
|