Files
CloudHome-Ops/lib/app/theme.dart

424 lines
14 KiB
Dart
Raw Permalink 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.

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