Files
monisuo/flutter_monisuo/lib/ui/pages/main/main_page.dart
2026-04-07 01:05:05 +08:00

264 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart';
import '../../../providers/asset_provider.dart';
import '../../../providers/market_provider.dart';
import '../home/home_page.dart';
import '../market/market_page.dart';
import '../trade/trade_page.dart';
import '../asset/asset_page.dart';
import '../mine/mine_page.dart';
/// 底部導航項
class _NavItem {
final String label;
final IconData icon;
const _NavItem({required this.label, required this.icon});
}
/// 主頁面 - "The Kinetic Vault" 設計風格
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
int _currentIndex = 0;
final Set<int> _loadedPages = {0};
String? _tradeCoinCode; // 交易頁面選中的幣種代碼
late final List<Widget> _pages;
// 防抖:記錄上次刷新時間,同一 Tab 500ms 內不重複刷新
final Map<int, DateTime> _lastRefreshTime = {};
@override
void initState() {
super.initState();
_pages = [
const HomePage(),
const MarketPage(),
TradePage(initialCoinCode: _tradeCoinCode),
const AssetPage(),
const MinePage(),
];
}
void _onTabChanged(int index) {
final wasLoaded = _loadedPages.contains(index);
setState(() {
_currentIndex = index;
_loadedPages.add(index);
});
// 切換到已加載的 Tab 時刷新數據
if (wasLoaded) {
_refreshTab(index);
}
}
/// 刷新對應 Tab 的數據(帶防抖)
void _refreshTab(int index) {
final now = DateTime.now();
final last = _lastRefreshTime[index];
if (last != null && now.difference(last).inMilliseconds < 500) return;
_lastRefreshTime[index] = now;
switch (index) {
case 0: // 首頁 - 刷新資產概覽
context.read<AssetProvider>().loadOverview(force: true);
break;
case 1: // 行情 - 刷新幣種列表
context.read<MarketProvider>().loadCoins(force: true);
break;
case 3: // 資產 - 刷新全部資產
context.read<AssetProvider>().refreshAll(force: true);
break;
}
}
/// 切換到交易頁面並選中指定幣種
void switchToTrade(String coinCode) {
setState(() {
_tradeCoinCode = coinCode;
_currentIndex = 2; // 交易頁面索引
_loadedPages.add(2);
// 重新構建交易頁面(用 Key 強制創建新 State
_pages[2] = TradePage(
key: ValueKey(_tradeCoinCode),
initialCoinCode: _tradeCoinCode,
);
});
}
/// 切換到指定 tab
void switchToTab(int index) {
setState(() {
_currentIndex = index;
_loadedPages.add(index);
});
}
static const _navItems = [
_NavItem(label: '首頁', icon: LucideIcons.house),
_NavItem(label: '行情', icon: LucideIcons.trendingUp),
_NavItem(label: '交易', icon: LucideIcons.arrowLeftRight),
_NavItem(label: '資產', icon: LucideIcons.wallet),
_NavItem(label: '我的', icon: LucideIcons.user),
];
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: LazyIndexedStack(
index: _currentIndex,
loadedIndexes: _loadedPages,
children: _pages,
),
bottomNavigationBar: _BottomNavBar(
items: _navItems,
currentIndex: _currentIndex,
onTap: _onTabChanged,
),
);
}
}
/// 底部導航欄 - 專業信任主題
class _BottomNavBar extends StatelessWidget {
final List<_NavItem> items;
final int currentIndex;
final ValueChanged<int> onTap;
const _BottomNavBar({
required this.items,
required this.currentIndex,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
// Light: #FFFFFF, Dark: #0F172A
final backgroundColor = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF);
return Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.xxl + AppSpacing.sm)),
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.15),
),
),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.sm, AppSpacing.md, AppSpacing.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items.asMap().entries.map((entry) {
return _NavItemWidget(
item: entry.value,
isSelected: entry.key == currentIndex,
onTap: () => onTap(entry.key),
);
}).toList(),
),
),
),
);
}
}
/// 導航項組件
class _NavItemWidget extends StatelessWidget {
final _NavItem item;
final bool isSelected;
final VoidCallback onTap;
const _NavItemWidget({
required this.item,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final color = isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant;
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
item.icon,
color: color,
size: 24,
),
SizedBox(height: AppSpacing.xs),
Text(
item.label,
style: AppTextStyles.bodyMedium(context).copyWith(
color: color,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
}
/// 懶加載 IndexedStack - 只渲染已訪問過的頁面
class LazyIndexedStack extends StatefulWidget {
final int index;
final Set<int> loadedIndexes;
final List<Widget> children;
const LazyIndexedStack({
super.key,
required this.index,
required this.loadedIndexes,
required this.children,
});
@override
State<LazyIndexedStack> createState() => _LazyIndexedStackState();
}
class _LazyIndexedStackState extends State<LazyIndexedStack> {
@override
Widget build(BuildContext context) {
return Stack(
children: widget.children.asMap().entries.map((entry) {
final isVisible = entry.key == widget.index;
final isLoaded = widget.loadedIndexes.contains(entry.key);
return Positioned.fill(
child: Offstage(
offstage: !isVisible,
child: TickerMode(
enabled: isVisible,
child: isLoaded ? entry.value : const SizedBox.shrink(),
),
),
);
}).toList(),
);
}
}