This commit is contained in:
sion
2026-04-21 08:09:45 +08:00
parent 0066615054
commit 5264043c21
1831 changed files with 15376 additions and 39973 deletions

View File

@@ -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);
},
);
}),
],
),
),
),
),
],
);
},
),

View File

@@ -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(

View File

@@ -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);

View File

@@ -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();
}