111
This commit is contained in:
@@ -5,6 +5,8 @@ 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';
|
||||
@@ -24,6 +26,7 @@ class AssetPage extends StatefulWidget {
|
||||
|
||||
class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixin {
|
||||
StreamSubscription<AppEvent>? _eventSub;
|
||||
Timer? _refreshTimer;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -34,15 +37,27 @@ class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixi
|
||||
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, (_) {
|
||||
@@ -65,25 +80,34 @@ class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixi
|
||||
backgroundColor: colorScheme.background,
|
||||
body: Consumer<AssetProvider>(
|
||||
builder: (context, provider, _) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => provider.refreshAll(force: true),
|
||||
color: colorScheme.primary,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md + 8, AppSpacing.md, 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Page title
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
||||
child: Text(
|
||||
'資產',
|
||||
style: AppTextStyles.displaySmall(context),
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'資產',
|
||||
style: AppTextStyles.headlineLarge(context).copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
),
|
||||
),
|
||||
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: [
|
||||
@@ -119,10 +143,23 @@ class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixi
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// Holdings section
|
||||
HoldingsSection(holdings: provider.holdings),
|
||||
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);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,15 +5,14 @@ import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../data/models/account_models.dart';
|
||||
import '../../../components/glass_panel.dart';
|
||||
import '../../../components/coin_icon.dart';
|
||||
import '../../chart/chart_page.dart';
|
||||
|
||||
/// 持倉區域
|
||||
/// Header: "我的資產" + "查看全部 >"
|
||||
/// Holdings Card: cornerRadius lg, fill $surface-card, stroke $border-default 1px
|
||||
class HoldingsSection extends StatelessWidget {
|
||||
final List holdings;
|
||||
final Set<String> platformCoinCodes;
|
||||
final void Function(String coinCode)? onTapTrade;
|
||||
|
||||
const HoldingsSection({super.key, required this.holdings});
|
||||
const HoldingsSection({super.key, required this.holdings, this.platformCoinCodes = const {}, this.onTapTrade});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -60,6 +59,7 @@ class HoldingsSection extends StatelessWidget {
|
||||
children: List.generate(holdings.length, (index) {
|
||||
final h = holdings[index] as AccountTrade;
|
||||
final isProfit = h.profitRate >= 0;
|
||||
final isPlatform = platformCoinCodes.contains(h.coinCode);
|
||||
return Column(
|
||||
children: [
|
||||
HoldingRow(
|
||||
@@ -68,6 +68,8 @@ class HoldingsSection extends StatelessWidget {
|
||||
value: '${double.tryParse(h.currentValue)?.toStringAsFixed(2) ?? h.currentValue} USDT',
|
||||
profitRate: '${isProfit ? '+' : ''}${h.profitRate.toStringAsFixed(2)}%',
|
||||
isProfit: isProfit,
|
||||
isTradable: isPlatform,
|
||||
onTap: isPlatform ? () => onTapTrade?.call(h.coinCode) : null,
|
||||
),
|
||||
if (index < holdings.length - 1) const HoldingDivider(),
|
||||
],
|
||||
@@ -106,6 +108,8 @@ class HoldingRow extends StatelessWidget {
|
||||
final String value;
|
||||
final String profitRate;
|
||||
final bool isProfit;
|
||||
final bool isTradable;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const HoldingRow({
|
||||
super.key,
|
||||
@@ -114,6 +118,8 @@ class HoldingRow extends StatelessWidget {
|
||||
required this.value,
|
||||
required this.profitRate,
|
||||
required this.isProfit,
|
||||
this.isTradable = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -123,12 +129,7 @@ class HoldingRow extends StatelessWidget {
|
||||
final profitColor = isProfit ? context.appColors.up : context.appColors.down;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => ChartPage(symbol: coinCode)),
|
||||
);
|
||||
},
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14),
|
||||
child: Row(
|
||||
|
||||
@@ -56,6 +56,10 @@ class _DepositPageState extends State<DepositPage> {
|
||||
ToastUtils.showError('單筆最低充值 1000 USDT');
|
||||
return;
|
||||
}
|
||||
if (n % 1000 != 0) {
|
||||
ToastUtils.showError('充值金額必須為1000的整數倍');
|
||||
return;
|
||||
}
|
||||
if (_isSubmitting) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
@@ -47,17 +47,17 @@ class _TransferPageState extends State<TransferPage> {
|
||||
final provider = context.read<AssetProvider>();
|
||||
final balance = provider.fundAccount?.balance ??
|
||||
provider.overview?.fundBalance ??
|
||||
'0.00';
|
||||
return _formatBalance(balance);
|
||||
'0';
|
||||
return balance;
|
||||
} catch (e) {
|
||||
return '0.00';
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
String get _tradeUsdtBalance {
|
||||
try {
|
||||
final provider = context.read<AssetProvider>();
|
||||
if (provider.tradeAccounts.isEmpty) return '0.00';
|
||||
if (provider.tradeAccounts.isEmpty) return '0';
|
||||
final usdtHolding = provider.tradeAccounts.firstWhere(
|
||||
(t) => t.coinCode.toUpperCase() == 'USDT',
|
||||
orElse: () => AccountTrade(
|
||||
@@ -72,9 +72,9 @@ class _TransferPageState extends State<TransferPage> {
|
||||
profitRate: 0,
|
||||
),
|
||||
);
|
||||
return _formatBalance(usdtHolding.quantity);
|
||||
return usdtHolding.quantity;
|
||||
} catch (e) {
|
||||
return '0.00';
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,12 +140,16 @@ class _TransferPageState extends State<TransferPage> {
|
||||
|
||||
void _setQuickAmount(double percent) {
|
||||
final available = double.tryParse(_availableBalance) ?? 0;
|
||||
final amount = available * percent;
|
||||
// 向下截斷到2位小數,避免四捨五入超出餘額
|
||||
_amountController.text =
|
||||
((amount * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||
|
||||
// Trigger haptic feedback
|
||||
if (available <= 0) return;
|
||||
if (percent >= 1.0) {
|
||||
// 百分百直接用原始余额字符串,避免精度丢失
|
||||
_amountController.text = _availableBalance;
|
||||
} else {
|
||||
final amount = available * percent;
|
||||
// 向下截斷到2位小數,避免四捨五入超出餘額
|
||||
_amountController.text =
|
||||
((amount * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||
}
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user