Files
monisuo/flutter_monisuo/lib/ui/pages/asset/asset_page.dart
2026-04-21 08:09:45 +08:00

207 lines
6.9 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../providers/asset_provider.dart';
import '../../../providers/market_provider.dart';
import '../main/main_page.dart';
import 'components/action_buttons_row.dart';
import 'components/balance_card.dart';
import 'components/holdings_section.dart';
import 'components/records_link_row.dart';
import '../orders/fund_orders_page.dart';
import 'deposit_page.dart';
import 'transfer_page.dart';
import 'withdraw_page.dart';
/// 資產頁面 - Matching .pen design spec (CMcqs)
class AssetPage extends StatefulWidget {
const AssetPage({super.key});
@override
State<AssetPage> createState() => _AssetPageState();
}
class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixin {
StreamSubscription<AppEvent>? _eventSub;
Timer? _refreshTimer;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadData();
_listenEvents();
_startAutoRefresh();
});
}
@override
void dispose() {
_eventSub?.cancel();
_refreshTimer?.cancel();
super.dispose();
}
void _startAutoRefresh() {
_refreshTimer?.cancel();
_refreshTimer = Timer.periodic(const Duration(seconds: 15), (_) {
if (!mounted) return;
final mainState = context.findAncestorStateOfType<MainPageState>();
if (mainState?.isPageVisible(3) != true) return;
context.read<AssetProvider>().refreshAll(force: true);
});
}
void _listenEvents() {
final eventBus = context.read<AppEventBus>();
_eventSub = eventBus.on(AppEventType.assetChanged, (_) {
if (mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
});
}
void _loadData() {
context.read<AssetProvider>().refreshAll(force: true);
}
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return Column(
children: [
Container(
height: 48,
alignment: Alignment.center,
child: Text(
'資產',
style: AppTextStyles.headlineLarge(context).copyWith(
fontWeight: FontWeight.w700,
),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => provider.refreshAll(force: true),
color: colorScheme.primary,
backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(
top: AppSpacing.sm,
left: AppSpacing.md,
right: AppSpacing.md,
bottom: AppSpacing.xl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 資金賬戶 + 交易賬戶 左右並排
Row(
children: [
Expanded(
child: BalanceCard(
label: '資金賬戶',
balance: provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00',
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: BalanceCard(
label: '交易賬戶',
balance: _calculateTradeTotal(provider),
),
),
],
),
const SizedBox(height: AppSpacing.md),
// Action buttons row — matching .pen pIpHe (gap 12)
ActionButtonsRow(
onDeposit: () => _navigateToDeposit(context),
onWithdraw: () => _navigateToWithdraw(context, provider.fundAccount?.balance),
onTransfer: () => _navigateToTransfer(context),
),
const SizedBox(height: AppSpacing.md),
// Records link row — matching .pen fLHtq (cornerRadius lg, padding [14,16], stroke)
RecordsLinkRow(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FundOrdersPage()),
),
),
const SizedBox(height: AppSpacing.md),
// Holdings section
Builder(builder: (context) {
final platformCodes = context.watch<MarketProvider>().platformCoins.map((c) => c.code).toSet();
return HoldingsSection(
holdings: provider.holdings,
platformCoinCodes: platformCodes,
onTapTrade: (code) {
final mainState = context.findAncestorStateOfType<MainPageState>();
mainState?.switchToTrade(code);
},
);
}),
],
),
),
),
),
],
);
},
),
);
}
String _calculateTradeTotal(AssetProvider provider) {
double total = 0;
for (var h in provider.holdings) {
total += double.tryParse(h.currentValue?.toString() ?? '0') ?? 0;
}
return total.toStringAsFixed(2);
}
void _navigateToTransfer(BuildContext context) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const TransferPage()),
);
if (result == true && context.mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
}
void _navigateToDeposit(BuildContext context) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const DepositPage()),
);
if (result == true && context.mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
}
void _navigateToWithdraw(BuildContext context, String? balance) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => WithdrawPage(balance: balance)),
);
if (result == true && context.mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
}
}