优化:UI调整

This commit is contained in:
ivan 2026-04-13 23:15:04 +08:00
parent 85bed1cda2
commit 0de33cd575
8 changed files with 198 additions and 65 deletions

View File

@ -7,6 +7,20 @@ abstract final class PencilTheme {
static const Color homeTextPrimary = Colors.white; static const Color homeTextPrimary = Colors.white;
static const Color homeTabDivider = Color(0x66FFFFFF); static const Color homeTabDivider = Color(0x66FFFFFF);
/// Home Create Now
static const List<Shadow> 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); static const Color gemYellow = Color(0xFFFFD60A);
/// Create Now UI [PencilCreateNowButton] /// Create Now UI [PencilCreateNowButton]
@ -63,4 +77,10 @@ abstract final class PencilTheme {
/// ///
static const double designWidth = 390; 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;
}
} }

View File

@ -8,7 +8,10 @@ import '../../design/pencil_theme.dart';
/// WBRp4Credit Record `funymee_home.pen` [listCr]ez9wP /// WBRp4Credit Record `funymee_home.pen` [listCr]ez9wP
class CreditRecordTab extends StatefulWidget { class CreditRecordTab extends StatefulWidget {
const CreditRecordTab({super.key}); const CreditRecordTab({super.key, this.extraBottomInset = 0});
/// [MainScreen] [PencilTheme.mainTabBottomChromeReserve]
final double extraBottomInset;
@override @override
State<CreditRecordTab> createState() => _CreditRecordTabState(); State<CreditRecordTab> createState() => _CreditRecordTabState();
@ -71,13 +74,19 @@ class _CreditRecordTabState extends State<CreditRecordTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bottomPad = EdgeInsets.only(bottom: widget.extraBottomInset);
if (_loading) { if (_loading) {
return const Center( return Padding(
padding: bottomPad,
child: const Center(
child: CircularProgressIndicator(color: PencilTheme.underlineGold), child: CircularProgressIndicator(color: PencilTheme.underlineGold),
),
); );
} }
if (_error != null) { if (_error != null) {
return Center( return Padding(
padding: bottomPad,
child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -85,21 +94,30 @@ class _CreditRecordTabState extends State<CreditRecordTab> {
TextButton(onPressed: _load, child: const Text('Retry')), TextButton(onPressed: _load, child: const Text('Retry')),
], ],
), ),
),
); );
} }
if (_records.isEmpty) { if (_records.isEmpty) {
return Center( return Padding(
padding: bottomPad,
child: Center(
child: Text( child: Text(
'No records.', 'No records.',
style: GoogleFonts.inter(color: PencilTheme.inkSoft), style: GoogleFonts.inter(color: PencilTheme.inkSoft),
), ),
),
); );
} }
return RefreshIndicator( return RefreshIndicator(
color: PencilTheme.underlineGold, color: PencilTheme.underlineGold,
onRefresh: _load, onRefresh: _load,
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), padding: EdgeInsets.fromLTRB(
16,
8,
16,
28 + widget.extraBottomInset,
),
itemCount: _records.length, itemCount: _records.length,
separatorBuilder: (_, _) => const SizedBox(height: 12), separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (_, i) { itemBuilder: (_, i) {

View File

@ -150,7 +150,13 @@ class _HistoryScreenState extends State<HistoryScreen> {
), ),
), ),
Expanded( Expanded(
child: _tab == 0 ? _myHistoryBody() : const CreditRecordTab(), child: _tab == 0
? _myHistoryBody()
: CreditRecordTab(
extraBottomInset: widget.isRootTab
? PencilTheme.mainTabBottomChromeReserve(context)
: 0,
),
), ),
], ],
), ),
@ -196,12 +202,20 @@ class _HistoryScreenState extends State<HistoryScreen> {
if (!widget.isTabSelected) { if (!widget.isTabSelected) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final shellBottom = widget.isRootTab
? PencilTheme.mainTabBottomChromeReserve(context)
: 0.0;
if (!AuthService.isLoginComplete.value) { 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; final uid = UserState.userId.value;
if (uid == null || uid.isEmpty) { if (uid == null || uid.isEmpty) {
return Center( return Padding(
padding: EdgeInsets.only(bottom: shellBottom),
child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -211,13 +225,19 @@ class _HistoryScreenState extends State<HistoryScreen> {
), ),
], ],
), ),
),
); );
} }
if (_loading) { if (_loading) {
return const Center(child: CircularProgressIndicator()); return Padding(
padding: EdgeInsets.only(bottom: shellBottom),
child: const Center(child: CircularProgressIndicator()),
);
} }
if (_error != null) { if (_error != null) {
return Center( return Padding(
padding: EdgeInsets.only(bottom: shellBottom),
child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -225,6 +245,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
TextButton(onPressed: _load, child: const Text('Retry')), TextButton(onPressed: _load, child: const Text('Retry')),
], ],
), ),
),
); );
} }
return RefreshIndicator( return RefreshIndicator(
@ -237,14 +258,17 @@ class _HistoryScreenState extends State<HistoryScreen> {
), ),
if (_items.isEmpty) if (_items.isEmpty)
SliverFillRemaining( SliverFillRemaining(
child: Padding(
padding: EdgeInsets.only(bottom: shellBottom),
child: Center( child: Center(
child: Text('No tasks yet.', child: Text('No tasks yet.',
style: GoogleFonts.inter(color: PencilTheme.inkSoft)), style: GoogleFonts.inter(color: PencilTheme.inkSoft)),
), ),
),
) )
else else
SliverPadding( SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 28), padding: EdgeInsets.fromLTRB(16, 0, 16, 28 + shellBottom),
sliver: SliverGrid( sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: 2,

View File

@ -368,10 +368,10 @@ class _HomeScreenState extends State<HomeScreen> {
SafeArea( SafeArea(
bottom: false, bottom: false,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: EdgeInsets.only(
left: 16, left: 16,
right: 16, right: 16,
bottom: 16, bottom: 16 + PencilTheme.mainTabBottomChromeReserve(context),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@ -663,6 +663,8 @@ class _HomeScreenState extends State<HomeScreen> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: PencilTheme.homeTextPrimary color: PencilTheme.homeTextPrimary
.withValues(alpha: 0.85), .withValues(alpha: 0.85),
shadows:
PencilTheme.homeCreditsTextShadows,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),

View File

@ -72,7 +72,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
Expanded( Expanded(
child: ListView( child: ListView(
padding: const EdgeInsets.only(bottom: 28), padding: EdgeInsets.only(
bottom: 28 +
(widget.isRootTab
? PencilTheme.mainTabBottomChromeReserve(context)
: 0),
),
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),

View File

@ -769,6 +769,16 @@ class _ProductCard extends StatelessWidget {
return ((1 - a / o) * 100).round(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final rawTitle = item.title; final rawTitle = item.title;
@ -809,13 +819,13 @@ class _ProductCard extends StatelessWidget {
creditsTopLabel, creditsTopLabel,
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w800,
color: PencilTheme.stone600, color: PencilTheme.stone600,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
actual, _withDollarPrefix(actual),
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@ -834,7 +844,7 @@ class _ProductCard extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Text(
origin, _withDollarPrefix(origin),
style: GoogleFonts.inter( style: GoogleFonts.inter(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -1,13 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../core/auth/auth_service.dart'; import '../../core/auth/auth_service.dart';
import '../../core/payment/google_play_order_recovery.dart'; import '../../core/payment/google_play_order_recovery.dart';
import '../../core/theme/app_colors.dart';
import '../history/history_screen.dart'; import '../history/history_screen.dart';
import '../home/home_screen.dart'; import '../home/home_screen.dart';
import '../profile/profile_screen.dart'; import '../profile/profile_screen.dart';
/// + / [Shadow]
const List<Shadow> _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). /// Root shell: bottom tabs **Home**, **History**, **Profile** (English labels).
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
const MainScreen({super.key}); const MainScreen({super.key});
@ -38,19 +46,61 @@ class _MainScreenState extends State<MainScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final parentNav = NavigationBarTheme.of(context);
return Scaffold( return Scaffold(
// All tabs: body extends under the bottom bar (faint gradient + [NavigationBar]).
extendBody: true,
body: IndexedStack( body: IndexedStack(
index: _index, index: _index,
children: [ children: [
const HomeScreen(), const HomeScreen(),
HistoryScreen( HistoryScreen(isRootTab: true, isTabSelected: _index == 1),
isRootTab: true,
isTabSelected: _index == 1,
),
const ProfileScreen(isRootTab: true), const ProfileScreen(isRootTab: true),
], ],
), ),
bottomNavigationBar: NavigationBar( 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],
),
),
),
),
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, selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i), onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [ destinations: const [
@ -71,6 +121,9 @@ class _MainScreenState extends State<MainScreen> {
), ),
], ],
), ),
),
],
),
); );
} }
} }

View File

@ -67,6 +67,7 @@ class PencilGlassCreditsPill extends StatelessWidget {
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: PencilTheme.homeTextPrimary, color: PencilTheme.homeTextPrimary,
shadows: PencilTheme.homeCreditsTextShadows,
), ),
), ),
], ],