优化:UI调整
This commit is contained in:
parent
85bed1cda2
commit
0de33cd575
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,10 @@ import '../../design/pencil_theme.dart';
|
|||||||
|
|
||||||
/// WBRp4「Credit Record」内容区 — 对齐 `funymee_home.pen` [listCr](ez9wP)。
|
/// WBRp4「Credit 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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user