feat(ui): 增加视觉 token 与统一导航

This commit is contained in:
Zhou Weidong committed 2026-10-19 10:16:12 +08:00
1 parent 42ce1d1d84
commit df5fbe5b08
7 files changed
+260 -1

No files matched your search

+3
View File
@@ -20,6 +20,9 @@ abstract final class AppColors {
/// Coins, rewards, and the playback progress line.
static const gold = Color(0xFFF6C453);
/// Favorite/like heart without competing with the chili primary action.
static const favorite = Color(0xFFD74A7D);
/// Text placed on [gold].
static const onGold = Color(0xFF2A1810);
+40
View File
@@ -0,0 +1,40 @@
// Author: fruit.com.cn
/// K20 cross-page spacing, type, radius, and control sizes.
abstract final class AppDimens {
static const double spaceXs = 4;
static const double spaceSm = 8;
static const double spaceMd = 12;
static const double spaceLg = 16;
static const double spaceXl = 24;
static const double pageHorizontal = 16;
static const double cardRadius = 16;
static const double cardPadding = 16;
static const double innerRadius = 12;
static const double innerPadding = 12;
static const double pillRadius = 20;
static const double caption = 12;
static const double body = 14;
static const double listTitle = 15;
static const double sectionTitle = 16;
static const double pageTitle = 20;
static const double detailHeadline = 24;
static const double navLabel = 11;
static const double buttonHeight = 48;
static const double primaryButtonHeight = 52;
static const double tapTarget = 44;
static const double navHeight = 64;
static const double navIcon = 22;
static const double progressHeight = 4;
static const double progressRadius = 4;
static const double accountCardHeight = 64;
static const double accountIcon = 18;
static const double accountLabel = 11;
static const double watchPlaceholderHeight = 114;
}
+28
View File
@@ -1,8 +1,10 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_dimens.dart';
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
abstract final class AppTheme {
@@ -80,6 +82,32 @@ abstract final class AppTheme {
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith(_buttonColor),
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(
dark ? AppColors.blueSoft : AppColors.bluePressed,
),
minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight),
),
side: WidgetStatePropertyAll(
BorderSide(
color: dark ? AppColors.outlineDark : AppColors.outlineLight,
),
),
),
),
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: TextStyle(
color: AppColors.cream,
fontSize: AppDimens.pageTitle,
fontWeight: FontWeight.w800,
),
),
switchTheme: SwitchThemeData(
+109
View File
@@ -0,0 +1,109 @@
// Author: fruit.com.cn
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,
required this.icon,
required this.selectedIcon,
required this.label,
required this.onTap,
this.selected = false,
});
final Key tabKey;
final IconData icon;
final IconData selectedIcon;
final String label;
final VoidCallback onTap;
final bool selected;
}
/// Shared bottom navigation used by all root tabs and the player fallback.
class AppBottomNav extends StatelessWidget {
const AppBottomNav({
super.key,
required this.items,
this.backgroundColor = AppColors.bar,
});
final List<AppBottomNavItem> items;
final Color backgroundColor;
@override
Widget build(BuildContext context) => SafeArea(
top: false,
child: Container(
height: AppDimens.navHeight,
decoration: BoxDecoration(
color: backgroundColor,
border: const Border(
top: BorderSide(color: AppColors.outlineVariantDark),
),
),
child: Row(
children: [for (final item in items) _NavDestination(item: item)],
),
),
);
}
class _NavDestination extends StatelessWidget {
const _NavDestination({required this.item});
final AppBottomNavItem item;
@override
Widget build(BuildContext context) => Expanded(
child: Semantics(
button: true,
selected: item.selected,
label: item.label,
child: InkWell(
key: item.tabKey,
onTap: item.onTap,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: AppDimens.tapTarget),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 3,
),
decoration: BoxDecoration(
color: item.selected ? AppColors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
item.selected ? item.selectedIcon : item.icon,
size: AppDimens.navIcon,
color: item.selected ? AppColors.onPrimary : AppColors.muted,
),
),
const SizedBox(height: 2),
Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDimens.navLabel,
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
color: item.selected ? AppColors.blueSoft : AppColors.muted,
),
),
],
),
),
),
),
);
}