feat(flutter): 应用浅色暖橙视觉主题

This commit is contained in:
Zhou Weidong committed 2026-10-20 10:15:03 +08:00
1 parent 0d2367431d
commit 634b63e16c
56 files changed
+941 -409

No files matched your search

+72 -49
View File
@@ -2,79 +2,102 @@
import 'package:flutter/material.dart';
/// Obsidian Flare palette from `docs/design/README.md` and screens 01/02.
/// K28-S 浅色暖橙主题色板。
///
/// Existing semantic names (`blue`, `blueSoft`, `ink`, ...) are kept so page
/// code does not need a mass rename; their values now follow the design tokens.
/// 保留既有语义名,避免页面大范围改名;实际颜色统一由本文件控制。
abstract final class AppColors {
/// Primary CTA, selected navigation, playback progress.
static const blue = Color(0xFFFF5B3D);
/// 主操作、选中态、播放进度。
static const blue = Color(0xFFFF7A1A);
static const primary = blue;
/// Pressed state for the primary CTA.
static const bluePressed = Color(0xFFFF4321);
/// 主操作按下态。
static const bluePressed = Color(0xFFFF6A00);
/// Secondary accent for tags, locks, and highlighted icons.
static const blueSoft = Color(0xFF9278EA);
/// 粉色辅助强调色。
static const blueSoft = Color(0xFFFF6A8A);
static const secondary = blueSoft;
static const lavender = blueSoft;
/// Coins and rewards.
static const gold = Color(0xFFFBBF24);
/// 金币与奖励。
static const gold = Color(0xFFF3B34C);
/// Success and free-state accents.
static const success = Color(0xFF34D399);
/// 成功状态。
static const success = Color(0xFF2FA873);
/// Liked hearts use the primary accent.
static const favorite = blue;
/// 收藏和喜欢使用粉色强调。
static const favorite = secondary;
/// Text placed on [gold].
static const onGold = Color(0xFF2A1810);
/// 金币底色上的文字。
static const onGold = Color(0xFF5A3511);
/// Text and icons placed directly on [blue].
/// 主色上的文字与图标。
static const onPrimary = Color(0xFFFFFFFF);
/// Page background.
static const ink = Color(0xFF0B0C10);
/// 页面背景。
static const ink = Color(0xFFFFF8F0);
/// Bottom navigation background (rendered at 95% with blur).
static const bar = Color(0xFF0D0D11);
/// 页面顶部暖色渐变起点。
static const surfaceTop = Color(0xFFFFE9D6);
/// Cards lifted off [ink].
static const card = Color(0xFF1B1C21);
/// 底部导航底色。
static const bar = Color(0xFFFFFDFB);
/// Alternate card surface from the mobile preview.
static const cardAlt = Color(0xFF1C1D21);
/// 中央操作按钮渐变。
static const navCenterStart = Color(0xFFFF9A3D);
static const navCenterEnd = Color(0xFFFF6A00);
static const navCenterShadow = Color(0x33FF7A1A);
/// Floating surfaces, inputs, and nested rows.
static const raised = Color(0xFF1C1D21);
static const quick = Color(0xFF202126);
static const input = Color(0xFF25252A);
static const goldCard = Color(0xFF201B1E);
/// 卡片轻阴影。
static const cardShadow = Color(0x14FF8A3D);
/// Body text on dark surfaces.
static const cream = Color(0xFFE6E1E3);
/// 卡片与弹层。
static const card = Color(0xFFFFFFFF);
/// Titles and high-emphasis text.
static const onSurface = Color(0xFFFFFFFF);
/// 次要卡片表面。
static const cardAlt = Color(0xFFFFF4E9);
/// Secondary text.
static const muted = Color(0xFFD2CDD0);
/// 浮层、输入框和嵌套区域。
static const raised = Color(0xFFFFF1E5);
static const quick = Color(0xFFFFF0E1);
static const input = Color(0xFFFFF4EA);
static const goldCard = Color(0xFFFFF4DF);
/// Weak hints and timestamps.
static const subtle = Color(0xFF9A96A3);
/// 正文颜色。
static const cream = Color(0xFF1F1B16);
/// Soft orange used for continue labels and secondary times.
static const accent = Color(0xFFFFA490);
/// 主文字。
static const onSurface = Color(0xFF1F1B16);
/// 8% white border used by cards and inputs.
static const border = Color(0x14FFFFFF);
/// 次要文字。
static const muted = Color(0xFF8C837B);
/// 14% white border used by glass controls.
static const glassBorder = Color(0x24FFFFFF);
/// 弱提示与时间。
static const subtle = Color(0xFFB3AAA1);
static const outlineDark = Color(0xFF2A2C36);
static const outlineVariantDark = Color(0xFF242329);
static const outlineLight = Color(0xFF8E8E93);
static const outlineVariantLight = Color(0xFFE4E4E7);
/// 橙色强调文案。
static const accent = blue;
/// 卡片描边。
static const border = Color(0xFFF0E4D6);
/// 玻璃面板描边。
static const glassBorder = Color(0x66FFFFFF);
/// 粉色角标。
static const badgePink = Color(0xFFFFE3EC);
static const badgePinkText = Color(0xFFFF6A8A);
/// 封面锁定数量与视频遮罩。
static const overlayDark = Color(0x33000000);
/// 封面集数胶囊。
static const badgeBeige = Color(0xFFFFF0DF);
/// 导航未选中。
static const navUnselected = muted;
static const outlineDark = border;
static const outlineVariantDark = border;
static const outlineLight = muted;
static const outlineVariantLight = border;
}
+21 -14
View File
@@ -1,6 +1,6 @@
// Author: fruit.com.cn
/// Cross-page spacing, type, radius, and control sizes.
/// 跨页面复用的间距、字号、圆角和控件尺寸。
abstract final class AppDimens {
static const double spaceXs = 4;
static const double spaceSm = 8;
@@ -9,17 +9,18 @@ abstract final class AppDimens {
static const double spaceXl = 24;
static const double pageHorizontal = 16;
static const double cardRadius = 12;
static const double cardRadius = 16;
static const double authCardRadius = 20;
static const double cardPadding = 16;
static const double innerRadius = 10;
static const double innerRadius = 12;
static const double innerPadding = 12;
static const double pillRadius = 20;
static const double glassSmallRadius = 12;
static const double glassRadius = 14;
static const double glassCardRadius = 14;
static const double buttonRadius = 9;
static const double coverRadius = 10;
static const double pillRadius = 999;
static const double glassSmallRadius = 16;
static const double glassRadius = 20;
static const double glassCardRadius = 20;
static const double buttonRadius = 999;
static const double coverRadius = 16;
static const double sheetRadius = 20;
static const double caption = 12;
static const double emptyStateSubtitle = 12.5;
@@ -33,7 +34,7 @@ abstract final class AppDimens {
static const double previewCardTitle = 13;
static const double previewSectionTitle = 14;
static const double chipFont = 10;
static const double navLabel = 9;
static const double navLabel = 10;
static const double buttonHeight = 48;
static const double primaryButtonHeight = 52;
@@ -44,8 +45,10 @@ abstract final class AppDimens {
static const double tapTarget = 44;
static const double contentHeaderHeight = 46;
static const double navHeight = 54;
static const double navIcon = 17;
static const double navHeight = 64;
static const double navIcon = 20;
static const double navCenter = 56;
static const double navCenterIcon = 28;
static const double progressHeight = 3;
static const double progressRadius = 2;
static const double progressLineHeight = 3;
@@ -60,7 +63,11 @@ abstract final class AppDimens {
static const double watchPlaceholderHeight = 114;
static const double heroBannerHeight = 176;
static const double heroBannerRadius = 20;
static const double cardMeta = 9;
static const double cardViews = 9;
static const double cardMeta = 12;
static const double cardViews = 11;
static const double cardTitle = 15;
static const double cardShadowBlur = 18;
static const double cardShadowY = 6;
}
+79 -86
View File
@@ -6,79 +6,62 @@ import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_dimens.dart';
/// Obsidian Flare themes. Dark is the product default; light mirrors the same
/// orange primary for system-light devices.
/// K28-S 浅色暖橙主题。
abstract final class AppTheme {
static const _lightBackground = Color(0xFFF7F7F8);
static const _darkBackground = AppColors.ink;
/// Light theme for system light mode.
static ThemeData light() => _build(
brightness: Brightness.light,
surface: _lightBackground,
accent: AppColors.blue,
/// 页面顶部暖色到米白背景的渐变。
static const LinearGradient pageGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.surfaceTop, AppColors.ink],
stops: [0, 0.24],
);
/// Dark theme for system dark mode.
static ThemeData dark() => _build(
brightness: Brightness.dark,
surface: _darkBackground,
accent: AppColors.blue,
/// 卡片轻阴影。
static const BoxShadow cardShadow = BoxShadow(
color: AppColors.cardShadow,
blurRadius: AppDimens.cardShadowBlur,
offset: Offset(0, AppDimens.cardShadowY),
);
static ThemeData _build({
required Brightness brightness,
required Color surface,
required Color accent,
}) {
final dark = brightness == Brightness.dark;
final scheme = dark
? const ColorScheme.dark(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.bluePressed,
onPrimaryContainer: AppColors.onPrimary,
secondaryContainer: AppColors.raised,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
onSurface: AppColors.onSurface,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
)
: ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.blue.withValues(alpha: 0.14),
onPrimaryContainer: AppColors.ink,
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.14),
onSecondaryContainer: AppColors.ink,
surface: _lightBackground,
onSurface: AppColors.ink,
outline: AppColors.outlineLight,
outlineVariant: AppColors.outlineVariantLight,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
);
/// 全局浅色暖橙主题。
static ThemeData light() {
const scheme = ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.surfaceTop,
onPrimaryContainer: AppColors.onSurface,
secondaryContainer: AppColors.badgePink,
onSecondaryContainer: AppColors.badgePinkText,
surface: AppColors.ink,
onSurface: AppColors.onSurface,
outline: AppColors.border,
outlineVariant: AppColors.border,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.bluePressed,
);
final base = ThemeData(
useMaterial3: true,
brightness: brightness,
brightness: Brightness.light,
colorScheme: scheme,
scaffoldBackgroundColor: surface,
scaffoldBackgroundColor: AppColors.ink,
fontFamily: 'PlusJakartaSans',
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent),
sliderTheme: SliderThemeData(
activeTrackColor: accent,
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.blue,
),
sliderTheme: const SliderThemeData(
activeTrackColor: AppColors.blue,
inactiveTrackColor: AppColors.border,
thumbColor: AppColors.onPrimary,
),
tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent),
tabBarTheme: const TabBarThemeData(
labelColor: AppColors.blue,
indicatorColor: AppColors.blue,
unselectedLabelColor: AppColors.muted,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: accent),
style: TextButton.styleFrom(foregroundColor: AppColors.blue),
),
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(
@@ -87,25 +70,26 @@ abstract final class AppTheme {
minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight),
),
shape: const WidgetStatePropertyAll(StadiumBorder()),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(
dark ? AppColors.blueSoft : AppColors.bluePressed,
),
foregroundColor: const WidgetStatePropertyAll(AppColors.blue),
minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight),
),
side: WidgetStatePropertyAll(
BorderSide(
color: dark ? AppColors.outlineDark : AppColors.outlineLight,
),
side: const WidgetStatePropertyAll(
BorderSide(color: AppColors.border),
),
shape: const WidgetStatePropertyAll(StadiumBorder()),
),
),
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle.light,
backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: TextStyle(
color: AppColors.onSurface,
fontFamily: 'Outfit',
@@ -113,6 +97,16 @@ abstract final class AppTheme {
fontWeight: FontWeight.w800,
),
),
iconTheme: const IconThemeData(color: AppColors.onSurface),
dividerTheme: const DividerThemeData(color: AppColors.border),
cardTheme: const CardThemeData(
color: AppColors.card,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(AppDimens.cardRadius)),
side: BorderSide(color: AppColors.border),
),
),
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected)
@@ -120,36 +114,35 @@ abstract final class AppTheme {
: null,
),
trackColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) ? accent : null,
(states) =>
states.contains(WidgetState.selected) ? AppColors.blue : null,
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.bluePressed,
contentTextStyle: const TextStyle(color: AppColors.onPrimary),
snackBarTheme: const SnackBarThemeData(
backgroundColor: AppColors.onSurface,
contentTextStyle: TextStyle(color: AppColors.onPrimary),
),
);
final textTheme = base.textTheme
.apply(
fontFamily: 'PlusJakartaSans',
bodyColor: dark ? AppColors.cream : AppColors.ink,
displayColor: dark ? AppColors.onSurface : AppColors.ink,
bodyColor: AppColors.cream,
displayColor: AppColors.onSurface,
)
.copyWith(
displayLarge: _display(base.textTheme.displayLarge, dark),
displayMedium: _display(base.textTheme.displayMedium, dark),
displaySmall: _display(base.textTheme.displaySmall, dark),
headlineLarge: _display(base.textTheme.headlineLarge, dark),
headlineMedium: _display(base.textTheme.headlineMedium, dark),
headlineSmall: _display(base.textTheme.headlineSmall, dark),
titleLarge: _display(base.textTheme.titleLarge, dark),
displayLarge: _display(base.textTheme.displayLarge),
displayMedium: _display(base.textTheme.displayMedium),
displaySmall: _display(base.textTheme.displaySmall),
headlineLarge: _display(base.textTheme.headlineLarge),
headlineMedium: _display(base.textTheme.headlineMedium),
headlineSmall: _display(base.textTheme.headlineSmall),
titleLarge: _display(base.textTheme.titleLarge),
);
return base.copyWith(textTheme: textTheme);
}
static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith(
fontFamily: 'Outfit',
color: dark ? AppColors.onSurface : AppColors.ink,
);
static TextStyle? _display(TextStyle? style) =>
style?.copyWith(fontFamily: 'Outfit', color: AppColors.onSurface);
static Color _buttonColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed) ||
+100 -22
View File
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// One destination rendered by [AppBottomNav].
/// 底部导航普通目的地。
class AppBottomNavItem {
const AppBottomNavItem({
required this.tabKey,
@@ -26,41 +26,69 @@ class AppBottomNavItem {
final bool selected;
}
/// Shared bottom navigation used by the root shell and standalone tabs.
/// 底部导航中央操作按钮。
class AppBottomNavCenterAction {
const AppBottomNavCenterAction({
required this.label,
required this.onTap,
this.key = const Key('nav-center-action'),
});
final Key key;
final String label;
final VoidCallback onTap;
}
/// 全局五槽底部导航。
class AppBottomNav extends StatelessWidget {
const AppBottomNav({
super.key,
required this.items,
this.centerAction,
this.backgroundColor = AppColors.bar,
this.barKey,
});
final List<AppBottomNavItem> items;
final AppBottomNavCenterAction? centerAction;
final Color backgroundColor;
final Key? barKey;
@override
Widget build(BuildContext context) => ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: SafeArea(
top: false,
child: Container(
key: barKey,
height: AppDimens.navHeight,
decoration: BoxDecoration(
color: backgroundColor.withValues(alpha: 0.95),
border: const Border(
top: BorderSide(color: AppColors.outlineVariantDark),
Widget build(BuildContext context) {
final action = centerAction;
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: SafeArea(
top: false,
child: Container(
key: barKey,
height: AppDimens.navHeight,
decoration: BoxDecoration(
color: backgroundColor.withValues(alpha: 0.95),
border: const Border(top: BorderSide(color: AppColors.border)),
),
),
child: Row(
children: [for (final item in items) _NavDestination(item: item)],
child: action == null
? Row(
children: [
for (final item in items) _NavDestination(item: item),
],
)
: Row(
children: [
for (final item in items.take(2))
_NavDestination(item: item),
Expanded(child: _CenterDestination(action: action)),
for (final item in items.skip(2))
_NavDestination(item: item),
],
),
),
),
),
),
);
);
}
}
class _NavDestination extends StatelessWidget {
@@ -78,7 +106,7 @@ class _NavDestination extends StatelessWidget {
key: item.tabKey,
onTap: item.onTap,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48),
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -91,7 +119,7 @@ class _NavDestination extends StatelessWidget {
size: AppDimens.navIcon,
color: item.selected
? AppColors.blue
: Colors.white.withValues(alpha: 0.45),
: AppColors.navUnselected,
),
),
const SizedBox(height: 2),
@@ -104,7 +132,7 @@ class _NavDestination extends StatelessWidget {
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
color: item.selected
? AppColors.blue
: Colors.white.withValues(alpha: 0.45),
: AppColors.navUnselected,
letterSpacing: 0,
),
),
@@ -115,3 +143,53 @@ class _NavDestination extends StatelessWidget {
),
);
}
class _CenterDestination extends StatelessWidget {
const _CenterDestination({required this.action});
final AppBottomNavCenterAction action;
@override
Widget build(BuildContext context) {
return Center(
child: Semantics(
button: true,
label: action.label,
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: Ink(
width: AppDimens.navCenter,
height: AppDimens.navCenter,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.navCenterStart, AppColors.navCenterEnd],
),
boxShadow: [
BoxShadow(
color: AppColors.navCenterShadow,
blurRadius: 14,
offset: Offset(0, 5),
),
],
),
child: InkWell(
key: action.key,
customBorder: const CircleBorder(),
onTap: action.onTap,
child: const Icon(
Icons.add_rounded,
color: AppColors.onPrimary,
size: AppDimens.navCenterIcon,
),
),
),
),
),
);
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// Dark glass surface used by the home info card and floating controls.
/// 首页信息卡与浮层控件使用的浅色玻璃面板。
class GlassPanel extends StatelessWidget {
const GlassPanel({
super.key,
@@ -38,7 +38,7 @@ class GlassPanel extends StatelessWidget {
const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)],
colors: [Color(0xE6FFFFFF), Color(0xB3FFFDFB)],
),
),
child: Padding(padding: padding ?? EdgeInsets.zero, child: child),