diff --git a/lib/design/pencil_theme.dart b/lib/design/pencil_theme.dart index f0a4104..c2289c7 100644 --- a/lib/design/pencil_theme.dart +++ b/lib/design/pencil_theme.dart @@ -7,6 +7,20 @@ abstract final class PencilTheme { static const Color homeTextPrimary = Colors.white; static const Color homeTabDivider = Color(0x66FFFFFF); + + /// Home 积分数字(顶栏胶囊、Create Now 上方预估)共用投影。 + static const List homeCreditsTextShadows = [ + Shadow( + offset: Offset(0, 1), + blurRadius: 4, + color: Color(0x73000000), + ), + Shadow( + offset: Offset(0, 0), + blurRadius: 8, + color: Color(0x40000000), + ), + ]; static const Color gemYellow = Color(0xFFFFD60A); /// 旧版 Create Now 磨砂底(当前 UI 已改用金渐变 [PencilCreateNowButton];保留供参考)。 @@ -63,4 +77,10 @@ abstract final class PencilTheme { /// 设计宽度用于按比例缩放(可选)。 static const double designWidth = 390; + + /// [MainScreen] 使用 [Scaffold.extendBody] 时,M3 [NavigationBar] 浮在内容上方; + /// 内容区默认高度为 80(与 Flutter `navigation_bar.dart` 一致),外加系统底边安全区。 + static double mainTabBottomChromeReserve(BuildContext context) { + return 80 + MediaQuery.paddingOf(context).bottom; + } } diff --git a/lib/features/history/credit_record_tab.dart b/lib/features/history/credit_record_tab.dart index 98a192a..d168025 100644 --- a/lib/features/history/credit_record_tab.dart +++ b/lib/features/history/credit_record_tab.dart @@ -8,7 +8,10 @@ import '../../design/pencil_theme.dart'; /// WBRp4「Credit Record」内容区 — 对齐 `funymee_home.pen` [listCr](ez9wP)。 class CreditRecordTab extends StatefulWidget { - const CreditRecordTab({super.key}); + const CreditRecordTab({super.key, this.extraBottomInset = 0}); + + /// [MainScreen] 底栏浮在内容上时,为列表底部追加的留白(与 [PencilTheme.mainTabBottomChromeReserve] 一致)。 + final double extraBottomInset; @override State createState() => _CreditRecordTabState(); @@ -71,27 +74,37 @@ class _CreditRecordTabState extends State { @override Widget build(BuildContext context) { + final bottomPad = EdgeInsets.only(bottom: widget.extraBottomInset); if (_loading) { - return const Center( - child: CircularProgressIndicator(color: PencilTheme.underlineGold), + return Padding( + padding: bottomPad, + child: const Center( + child: CircularProgressIndicator(color: PencilTheme.underlineGold), + ), ); } if (_error != null) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_error!), - TextButton(onPressed: _load, child: const Text('Retry')), - ], + return Padding( + padding: bottomPad, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_error!), + TextButton(onPressed: _load, child: const Text('Retry')), + ], + ), ), ); } if (_records.isEmpty) { - return Center( - child: Text( - 'No records.', - style: GoogleFonts.inter(color: PencilTheme.inkSoft), + return Padding( + padding: bottomPad, + child: Center( + child: Text( + 'No records.', + style: GoogleFonts.inter(color: PencilTheme.inkSoft), + ), ), ); } @@ -99,7 +112,12 @@ class _CreditRecordTabState extends State { color: PencilTheme.underlineGold, onRefresh: _load, child: ListView.separated( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + 28 + widget.extraBottomInset, + ), itemCount: _records.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (_, i) { diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index e742c00..8cb36ef 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -150,7 +150,13 @@ class _HistoryScreenState extends State { ), ), Expanded( - child: _tab == 0 ? _myHistoryBody() : const CreditRecordTab(), + child: _tab == 0 + ? _myHistoryBody() + : CreditRecordTab( + extraBottomInset: widget.isRootTab + ? PencilTheme.mainTabBottomChromeReserve(context) + : 0, + ), ), ], ), @@ -196,34 +202,49 @@ class _HistoryScreenState extends State { if (!widget.isTabSelected) { return const SizedBox.shrink(); } + final shellBottom = widget.isRootTab + ? PencilTheme.mainTabBottomChromeReserve(context) + : 0.0; if (!AuthService.isLoginComplete.value) { - return const Center(child: CircularProgressIndicator()); + return Padding( + padding: EdgeInsets.only(bottom: shellBottom), + child: const Center(child: CircularProgressIndicator()), + ); } final uid = UserState.userId.value; if (uid == null || uid.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _error ?? 'Sign in failed', - textAlign: TextAlign.center, - ), - ], + return Padding( + padding: EdgeInsets.only(bottom: shellBottom), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _error ?? 'Sign in failed', + textAlign: TextAlign.center, + ), + ], + ), ), ); } if (_loading) { - return const Center(child: CircularProgressIndicator()); + return Padding( + padding: EdgeInsets.only(bottom: shellBottom), + child: const Center(child: CircularProgressIndicator()), + ); } if (_error != null) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_error!), - TextButton(onPressed: _load, child: const Text('Retry')), - ], + return Padding( + padding: EdgeInsets.only(bottom: shellBottom), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_error!), + TextButton(onPressed: _load, child: const Text('Retry')), + ], + ), ), ); } @@ -237,14 +258,17 @@ class _HistoryScreenState extends State { ), if (_items.isEmpty) SliverFillRemaining( - child: Center( - child: Text('No tasks yet.', - style: GoogleFonts.inter(color: PencilTheme.inkSoft)), + child: Padding( + padding: EdgeInsets.only(bottom: shellBottom), + child: Center( + child: Text('No tasks yet.', + style: GoogleFonts.inter(color: PencilTheme.inkSoft)), + ), ), ) else SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 28), + padding: EdgeInsets.fromLTRB(16, 0, 16, 28 + shellBottom), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index f8198a8..0abd026 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -368,10 +368,10 @@ class _HomeScreenState extends State { SafeArea( bottom: false, child: Padding( - padding: const EdgeInsets.only( + padding: EdgeInsets.only( left: 16, right: 16, - bottom: 16, + bottom: 16 + PencilTheme.mainTabBottomChromeReserve(context), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -663,6 +663,8 @@ class _HomeScreenState extends State { fontWeight: FontWeight.w600, color: PencilTheme.homeTextPrimary .withValues(alpha: 0.85), + shadows: + PencilTheme.homeCreditsTextShadows, ), ), const SizedBox(height: 12), diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index dd97626..2a4a617 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -72,7 +72,12 @@ class _ProfileScreenState extends State { ), Expanded( child: ListView( - padding: const EdgeInsets.only(bottom: 28), + padding: EdgeInsets.only( + bottom: 28 + + (widget.isRootTab + ? PencilTheme.mainTabBottomChromeReserve(context) + : 0), + ), children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), diff --git a/lib/features/purchase/purchase_screen.dart b/lib/features/purchase/purchase_screen.dart index 6a5a5b0..58f6420 100644 --- a/lib/features/purchase/purchase_screen.dart +++ b/lib/features/purchase/purchase_screen.dart @@ -769,6 +769,16 @@ class _ProductCard extends StatelessWidget { return ((1 - a / o) * 100).round(); } + /// 展示用:金额前加 `$`(已有 `$` / `¥` 则不改)。 + static String _withDollarPrefix(String amount) { + final t = amount.trim(); + if (t.isEmpty || t == '—') return t; + if (t.startsWith(r'$') || t.startsWith('¥') || t.startsWith('¥')) { + return t; + } + return r'$' + t; + } + @override Widget build(BuildContext context) { final rawTitle = item.title; @@ -809,13 +819,13 @@ class _ProductCard extends StatelessWidget { creditsTopLabel, style: GoogleFonts.inter( fontSize: 13, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w800, color: PencilTheme.stone600, ), ), const SizedBox(height: 8), Text( - actual, + _withDollarPrefix(actual), style: GoogleFonts.inter( fontSize: 26, fontWeight: FontWeight.w800, @@ -834,7 +844,7 @@ class _ProductCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - origin, + _withDollarPrefix(origin), style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, diff --git a/lib/features/shell/main_screen.dart b/lib/features/shell/main_screen.dart index 08c51bc..ef19cac 100644 --- a/lib/features/shell/main_screen.dart +++ b/lib/features/shell/main_screen.dart @@ -1,13 +1,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../core/auth/auth_service.dart'; import '../../core/payment/google_play_order_recovery.dart'; +import '../../core/theme/app_colors.dart'; import '../history/history_screen.dart'; import '../home/home_screen.dart'; import '../profile/profile_screen.dart'; +/// 底栏:极淡背景渐变 + 图标/标签 [Shadow],兼顾层次与可读性。 +const List _bottomNavItemShadows = [ + Shadow(offset: Offset(0, 1), blurRadius: 3, color: Color(0x72000000)), + Shadow(offset: Offset(0, 0), blurRadius: 8, color: Color(0x40000000)), +]; + /// Root shell: bottom tabs **Home**, **History**, **Profile** (English labels). class MainScreen extends StatefulWidget { const MainScreen({super.key}); @@ -38,36 +46,81 @@ class _MainScreenState extends State { @override Widget build(BuildContext context) { + final parentNav = NavigationBarTheme.of(context); return Scaffold( + // All tabs: body extends under the bottom bar (faint gradient + [NavigationBar]). + extendBody: true, body: IndexedStack( index: _index, children: [ const HomeScreen(), - HistoryScreen( - isRootTab: true, - isTabSelected: _index == 1, - ), + HistoryScreen(isRootTab: true, isTabSelected: _index == 1), const ProfileScreen(isRootTab: true), ], ), - bottomNavigationBar: NavigationBar( - selectedIndex: _index, - onDestinationSelected: (i) => setState(() => _index = i), - destinations: const [ - NavigationDestination( - icon: Icon(Icons.home_outlined), - selectedIcon: Icon(Icons.home_rounded), - label: 'Home', + bottomNavigationBar: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + AppColors.surface.withValues(alpha: 0.40), + AppColors.surface.withValues(alpha: 0.15), + AppColors.surface.withValues(alpha: 0), + ], + stops: const [0.0, 0.48, 1.0], + ), + ), + ), ), - NavigationDestination( - icon: Icon(Icons.history_outlined), - selectedIcon: Icon(Icons.history_rounded), - label: 'History', - ), - NavigationDestination( - icon: Icon(Icons.person_outline_rounded), - selectedIcon: Icon(Icons.person_rounded), - label: 'Profile', + NavigationBarTheme( + data: parentNav.copyWith( + labelTextStyle: WidgetStateProperty.resolveWith((states) { + final selected = states.contains(WidgetState.selected); + return GoogleFonts.inter( + fontSize: 12, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? AppColors.primary : AppColors.onSurface, + shadows: _bottomNavItemShadows, + ); + }), + iconTheme: WidgetStateProperty.resolveWith((states) { + final selected = states.contains(WidgetState.selected); + return IconThemeData( + color: selected ? AppColors.primary : AppColors.onSurface, + shadows: _bottomNavItemShadows, + ); + }), + ), + child: NavigationBar( + backgroundColor: Colors.transparent, + elevation: 0, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + selectedIndex: _index, + onDestinationSelected: (i) => setState(() => _index = i), + destinations: const [ + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home_rounded), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.history_outlined), + selectedIcon: Icon(Icons.history_rounded), + label: 'History', + ), + NavigationDestination( + icon: Icon(Icons.person_outline_rounded), + selectedIcon: Icon(Icons.person_rounded), + label: 'Profile', + ), + ], + ), ), ], ), diff --git a/lib/widgets/pencil_chrome.dart b/lib/widgets/pencil_chrome.dart index 7b3c552..746733f 100644 --- a/lib/widgets/pencil_chrome.dart +++ b/lib/widgets/pencil_chrome.dart @@ -67,6 +67,7 @@ class PencilGlassCreditsPill extends StatelessWidget { fontSize: 15, fontWeight: FontWeight.w600, color: PencilTheme.homeTextPrimary, + shadows: PencilTheme.homeCreditsTextShadows, ), ), ],