diff --git a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill new file mode 100644 index 0000000..e5cafcb Binary files /dev/null and b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill differ diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache new file mode 100644 index 0000000..891131a --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache @@ -0,0 +1 @@ +{"version":2,"files":[{"path":"D:\\flutter\\bin\\cache\\dart-sdk\\version","hash":"800169ad7335b889bf428af171476466"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","hash":"03c567345af5a72ca098cfa0a67b3423"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","hash":"932fae7a247d7a3fd85340e755adb05b"},{"path":"D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","hash":"f78c405bcece3968277b212042da9ed6"},{"path":"d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"}]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/_composite.stamp b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/_composite.stamp new file mode 100644 index 0000000..1b2d28c --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/_composite.stamp @@ -0,0 +1 @@ +{"inputs":[],"outputs":[]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.d b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.d new file mode 100644 index 0000000..cd94f18 --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.d @@ -0,0 +1 @@ + D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json: D:\\flutter\\bin\\cache\\dart-sdk\\version D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.stamp b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.stamp new file mode 100644 index 0000000..db85c13 --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build.stamp @@ -0,0 +1 @@ +{"inputs":["D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","D:\\flutter\\bin\\cache\\dart-sdk\\version","D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json"],"outputs":["D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json"]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json new file mode 100644 index 0000000..4464929 --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json @@ -0,0 +1 @@ +{"build_start":"2026-04-06T01:07:05.140833","build_end":"2026-04-06T01:07:05.218007","dependencies":["file:///D:/flutter/bin/cache/dart-sdk/version","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/pubspec.yaml","file:///d:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json"],"code_assets":[],"data_assets":[]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_dart_plugin_registrant.stamp b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_dart_plugin_registrant.stamp new file mode 100644 index 0000000..1b2d28c --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_dart_plugin_registrant.stamp @@ -0,0 +1 @@ +{"inputs":[],"outputs":[]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_localizations.stamp b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_localizations.stamp new file mode 100644 index 0000000..1b2d28c --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/gen_localizations.stamp @@ -0,0 +1 @@ +{"inputs":[],"outputs":[]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/outputs.json b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/outputs.json new file mode 100644 index 0000000..52723e7 --- /dev/null +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/outputs.json @@ -0,0 +1 @@ +["D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json"] \ No newline at end of file diff --git a/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart b/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart index 5a6f309..dab3d02 100644 --- a/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart +++ b/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart @@ -473,6 +473,65 @@ void showWithdrawDialog(BuildContext context, String? balance) { validator: Validators.amount, ), const SizedBox(height: AppSpacing.md), + // 手续费/应收款提示 + ValueListenableBuilder( + valueListenable: feeNotifier, + builder: (_, feeText, __) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 14, color: colorScheme.onSurfaceVariant), + const SizedBox(width: AppSpacing.xs), + Expanded( + child: Text( + feeText, + style: AppTextStyles.bodySmall(context).copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: AppSpacing.md), + // 提现网络选择 + ValueListenableBuilder>( + valueListenable: networksNotifier, + builder: (_, networks, __) { + if (networks.isEmpty) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('提现网络', style: AppTextStyles.bodyMedium(context).copyWith( + fontWeight: FontWeight.w500, + )), + const SizedBox(height: AppSpacing.xs), + ValueListenableBuilder( + valueListenable: selectedNetworkNotifier, + builder: (_, selected, __) { + return ShadSelect( + placeholder: const Text('选择提现网络'), + initialValue: selected, + selectedOptionBuilder: (context, val) => Text(val), + onChanged: (value) { + if (value != null) selectedNetworkNotifier.value = value; + }, + options: networks.map((n) => ShadOption(value: n, child: Text(n))).toList(), + ); + }, + ), + ], + ); + }, + ), + const SizedBox(height: AppSpacing.md), ShadInputFormField( id: 'address', controller: addressController, @@ -516,6 +575,7 @@ void showWithdrawDialog(BuildContext context, String? balance) { withdrawContact: contactController.text.isNotEmpty ? contactController.text : null, + network: selectedNetworkNotifier.value, ); if (context.mounted) { showResultDialog( diff --git a/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart b/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart index 187c19a..edd73b5 100644 --- a/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart +++ b/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart @@ -20,9 +20,7 @@ class BalanceCard extends StatelessWidget { @override Widget build(BuildContext context) { - final displayBalance = activeTab == 0 - ? (provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00') - : _calculateTradeTotal(); + final displayBalance = balance; return GlassPanel( padding: const EdgeInsets.all(20), diff --git a/flutter_monisuo/lib/ui/pages/home/bills_page.dart b/flutter_monisuo/lib/ui/pages/home/bills_page.dart new file mode 100644 index 0000000..18591bd --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/home/bills_page.dart @@ -0,0 +1,450 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:provider/provider.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/app_color_scheme.dart'; +import '../../../core/theme/app_theme_extension.dart'; +import '../../../core/theme/app_spacing.dart'; +import '../../../data/models/account_models.dart'; +import '../../../data/services/bonus_service.dart'; +import '../../../providers/asset_provider.dart'; + +/// 账单页面 — 代币盈亏账单 + 新人福利账单 + 推广福利账单 +class BillsPage extends StatefulWidget { + const BillsPage({super.key}); + + @override + State createState() => _BillsPageState(); +} + +class _BillsPageState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + List _holdings = []; + List> _welfareRecords = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadData() async { + setState(() => _isLoading = true); + try { + final provider = context.read(); + final bonusService = context.read(); + + // 并行加载持仓和福利记录 + await provider.loadTradeAccount(force: true); + final welfareResponse = await bonusService.getWelfareStatus(); + + if (mounted) { + setState(() { + _holdings = provider.holdings; + if (welfareResponse.success && welfareResponse.data != null) { + _welfareRecords = _parseWelfareRecords(welfareResponse.data!); + } + _isLoading = false; + }); + } + } catch (_) { + if (mounted) { + setState(() { + _holdings = context.read().holdings; + _isLoading = false; + }); + } + } + } + + List> _parseWelfareRecords(Map data) { + final records = >[]; + // 新人福利 + final newUser = data['newUserBonus'] as Map?; + if (newUser != null) { + records.add({ + 'type': 'new_user', + 'title': '新人福利', + 'amount': newUser['amount']?.toString() ?? '0.00', + 'status': newUser['status'] ?? 0, + 'time': newUser['claimTime'] ?? newUser['createTime'], + }); + } + // 推广福利列表 + final referrals = data['referralBonuses'] as List?; + if (referrals != null) { + for (var r in referrals) { + final map = r as Map; + records.add({ + 'type': 'referral', + 'title': '推广福利 - ${map['referredUsername'] ?? '用户'}', + 'amount': map['amount']?.toString() ?? '0.00', + 'status': map['status'] ?? 0, + 'time': map['claimTime'] ?? map['createTime'], + }); + } + } + return records; + } + + bool get _isDark => Theme.of(context).brightness == Brightness.dark; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: _isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground, + appBar: AppBar( + leading: IconButton( + icon: const Icon(LucideIcons.arrowLeft, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text('我的账单', style: AppTextStyles.headlineLarge(context)), + backgroundColor: _isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + bottom: TabBar( + controller: _tabController, + labelColor: colorScheme.primary, + unselectedLabelColor: colorScheme.onSurfaceVariant, + indicatorColor: colorScheme.primary, + labelStyle: AppTextStyles.headlineMedium(context).copyWith(fontWeight: FontWeight.w600), + unselectedLabelStyle: AppTextStyles.headlineMedium(context), + tabs: const [ + Tab(text: '代币盈亏'), + Tab(text: '新人福利'), + Tab(text: '推广福利'), + ], + ), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + _buildCoinProfitTab(), + _buildWelfareTab('new_user'), + _buildWelfareTab('referral'), + ], + ), + ); + } + + // ============================================ + // 代币盈亏账单 + // ============================================ + Widget _buildCoinProfitTab() { + final colorScheme = Theme.of(context).colorScheme; + + if (_holdings.isEmpty) { + return _buildEmptyState(LucideIcons.wallet, '暂无持仓记录'); + } + + // 汇总统计 + double totalCost = 0; + double totalValue = 0; + double totalProfit = 0; + for (var h in _holdings) { + totalCost += double.tryParse(h.totalCost) ?? 0; + totalValue += double.tryParse(h.currentValue) ?? 0; + totalProfit += double.tryParse(h.profit) ?? 0; + } + final profitRate = totalCost > 0 ? (totalProfit / totalCost * 100) : 0.0; + final isProfit = totalProfit >= 0; + final profitColor = isProfit ? context.appColors.up : context.appColors.down; + + return RefreshIndicator( + onRefresh: _loadData, + child: ListView( + padding: const EdgeInsets.all(AppSpacing.md), + children: [ + // 汇总卡片 + Container( + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all( + color: _isDark + ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) + : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( + children: [ + Text('总盈亏 (USDT)', style: AppTextStyles.bodyMedium(context).copyWith( + color: colorScheme.onSurfaceVariant, + )), + const SizedBox(height: AppSpacing.xs), + Text( + '${isProfit ? '+' : ''}${totalProfit.toStringAsFixed(2)}', + style: AppTextStyles.displaySmall(context).copyWith( + fontWeight: FontWeight.bold, + color: profitColor, + ), + ), + const SizedBox(height: AppSpacing.sm), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSummaryItem('总成本', totalCost.toStringAsFixed(2)), + Container(width: 1, height: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.3)), + _buildSummaryItem('总市值', totalValue.toStringAsFixed(2)), + Container(width: 1, height: 16, color: colorScheme.outlineVariant.withValues(alpha: 0.3)), + _buildSummaryItem('收益率', '${profitRate >= 0 ? '+' : ''}${profitRate.toStringAsFixed(2)}%'), + ], + ), + ], + ), + ), + const SizedBox(height: AppSpacing.md), + + // 各币种盈亏明细 + ..._holdings.map((h) => _buildCoinProfitCard(h)), + ], + ), + ); + } + + Widget _buildSummaryItem(String label, String value) { + final colorScheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Column( + children: [ + Text(label, style: AppTextStyles.bodySmall(context).copyWith( + color: colorScheme.onSurfaceVariant, + )), + const SizedBox(height: 2), + Text(value, style: AppTextStyles.labelMedium(context).copyWith( + fontWeight: FontWeight.w600, + )), + ], + ), + ); + } + + Widget _buildCoinProfitCard(AccountTrade h) { + final colorScheme = Theme.of(context).colorScheme; + final profit = double.tryParse(h.profit) ?? 0; + final isProfit = profit >= 0; + final profitColor = isProfit ? context.appColors.up : context.appColors.down; + + return Container( + margin: const EdgeInsets.only(bottom: AppSpacing.sm), + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: _isDark + ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) + : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), + ), + ), + child: Column( + children: [ + // 币名 + 盈亏金额 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: colorScheme.primary.withValues(alpha: 0.1), + child: Text( + h.coinCode.substring(0, 1), + style: AppTextStyles.labelLarge(context).copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + Text(h.coinCode, style: AppTextStyles.headlineMedium(context).copyWith( + fontWeight: FontWeight.bold, + )), + const SizedBox(width: AppSpacing.xs), + Text('x ${double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity}', + style: AppTextStyles.bodySmall(context).copyWith(color: colorScheme.onSurfaceVariant), + ), + ], + ), + Text( + '${isProfit ? '+' : ''}${profit.toStringAsFixed(2)} USDT', + style: AppTextStyles.headlineMedium(context).copyWith( + color: profitColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + // 明细行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('均价: ${h.avgPrice}', style: AppTextStyles.bodySmall(context).copyWith( + color: colorScheme.onSurfaceVariant, + )), + Text('市值: ${h.currentValue} USDT', style: AppTextStyles.bodySmall(context).copyWith( + color: colorScheme.onSurfaceVariant, + )), + Text(h.formattedProfitRate, style: AppTextStyles.bodySmall(context).copyWith( + color: profitColor, + fontWeight: FontWeight.w600, + )), + ], + ), + ], + ), + ); + } + + // ============================================ + // 福利账单 + // ============================================ + Widget _buildWelfareTab(String type) { + final records = _welfareRecords.where((r) => r['type'] == type).toList(); + + if (records.isEmpty) { + return _buildEmptyState( + LucideIcons.gift, + type == 'new_user' ? '暂无新人福利记录' : '暂无推广福利记录', + ); + } + + return RefreshIndicator( + onRefresh: _loadData, + child: ListView.builder( + padding: const EdgeInsets.all(AppSpacing.md), + itemCount: records.length, + itemBuilder: (context, index) => _buildWelfareCard(records[index]), + ), + ); + } + + Widget _buildWelfareCard(Map record) { + final colorScheme = Theme.of(context).colorScheme; + final amount = double.tryParse(record['amount']?.toString() ?? '0') ?? 0; + final status = record['status'] as int? ?? 0; + + // status: 0=待领取, 1=已领取, 2=已过期 + String statusText; + Color statusColor; + switch (status) { + case 1: + statusText = '已领取'; + statusColor = context.appColors.up; + break; + case 2: + statusText = '已过期'; + statusColor = colorScheme.onSurfaceVariant; + break; + default: + statusText = '待领取'; + statusColor = AppColorScheme.warning; + } + + return Container( + margin: const EdgeInsets.only(bottom: AppSpacing.sm), + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: _isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: _isDark + ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) + : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(record['title'] ?? '', style: AppTextStyles.headlineMedium(context).copyWith( + fontWeight: FontWeight.bold, + )), + const SizedBox(height: AppSpacing.xs), + if (record['time'] != null) + Text( + _formatTime(record['time']), + style: AppTextStyles.bodySmall(context).copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '+${amount.toStringAsFixed(2)} USDT', + style: AppTextStyles.headlineMedium(context).copyWith( + color: context.appColors.up, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Container( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: 2), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text(statusText, style: AppTextStyles.bodySmall(context).copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + fontSize: 11, + )), + ), + ], + ), + ], + ), + ); + } + + // ============================================ + // 通用组件 + // ============================================ + Widget _buildEmptyState(IconData icon, String text) { + final colorScheme = Theme.of(context).colorScheme; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4)), + const SizedBox(height: AppSpacing.md), + Text(text, style: AppTextStyles.headlineMedium(context).copyWith( + color: colorScheme.onSurfaceVariant, + )), + ], + ), + ); + } + + String _formatTime(dynamic time) { + if (time == null) return '-'; + if (time is DateTime) { + return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} ' + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } + return time.toString(); + } +} diff --git a/flutter_monisuo/lib/ui/pages/home/home_page.dart b/flutter_monisuo/lib/ui/pages/home/home_page.dart index 7c2b8b2..a1df026 100644 --- a/flutter_monisuo/lib/ui/pages/home/home_page.dart +++ b/flutter_monisuo/lib/ui/pages/home/home_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -import 'package:bot_toast/bot_toast.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_color_scheme.dart'; @@ -16,17 +15,14 @@ import '../../../data/services/bonus_service.dart'; import '../../../providers/asset_provider.dart'; import '../../components/glass_panel.dart'; import '../../components/neon_glow.dart'; -import '../main/main_page.dart'; import '../asset/transfer_page.dart'; import '../asset/components/asset_dialogs.dart'; -import '../asset/transfer_page.dart'; import '../mine/welfare_center_page.dart'; import 'header_bar.dart'; import 'profit_analysis_page.dart'; import 'bills_page.dart'; import 'quick_actions_row.dart'; import 'hot_coins_section.dart'; -import 'profit_analysis_page.dart'; /// 首页 class HomePage extends StatefulWidget { @@ -121,10 +117,11 @@ class _HomePageState extends State SizedBox(height: AppSpacing.md), // 快捷操作栏 QuickActionsRow( - onDeposit: _showDeposit, - onWithdraw: () => _navigateToAssetPage(), - onTransfer: () => _navigateToAssetPage(), - onBills: () => _navigateToAssetPage(), + onDeposit: () => showDepositDialog(context), + onWithdraw: () => showWithdrawDialog(context, provider.fundAccount?.balance), + onTransfer: () => _navigateToTransfer(context), + onProfit: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfitAnalysisPage())), + onBills: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BillsPage())), ), SizedBox(height: AppSpacing.md), // 福利中心入口卡片 @@ -150,275 +147,9 @@ class _HomePageState extends State ); } - void _showDeposit() { - final amountController = TextEditingController(); - final formKey = GlobalKey(); - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.xxl), - padding: EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '充值', - style: AppTextStyles.headlineLarge(context).copyWith( - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: AppSpacing.xs), - Text( - '资产: USDT', - style: AppTextStyles.bodyMedium(context), - ), - ], - ), - Container( - padding: EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: context.colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: Icon(LucideIcons.wallet, color: context.colors.secondary), - ), - ], - ), - SizedBox(height: AppSpacing.lg), - ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - controller: amountController, - label: const Text('充值金额'), - placeholder: const Text('0.00'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (v) { - if (v == null || v.isEmpty) return '请输入金额'; - final n = double.tryParse(v); - if (n == null || n <= 0) return '请输入有效金额'; - return null; - }, - ), - ), - SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '取消', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(ctx).pop(), - height: 48, - showGlow: false, - ), - ), - SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '下一步', - type: NeonButtonType.primary, - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context - .read() - .deposit(amount: amountController.text); - if (mounted) { - if (response.success && response.data != null) { - _showDepositResultDialog(context, response.data!); - } else { - _showResultDialog( - '申请失败', - response.message ?? '请稍后重试', - ); - } - } - } - }, - height: 48, - showGlow: true, - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - void _showDepositResultDialog(BuildContext context, Map data) { - final orderNo = data['orderNo'] as String? ?? ''; - final amount = data['amount']?.toString() ?? '0.00'; - final walletAddress = data['walletAddress'] as String? ?? ''; - final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.xxl), - padding: EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - NeonIcon( - icon: Icons.check_circle, - color: AppColorScheme.up, - size: 24, - ), - SizedBox(width: AppSpacing.sm), - Text( - '充值申请成功', - style: AppTextStyles.headlineLarge(context).copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - SizedBox(height: AppSpacing.lg), - _InfoRow(label: '订单号', value: orderNo), - SizedBox(height: AppSpacing.sm), - _InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), - SizedBox(height: AppSpacing.lg), - Text( - '请向以下地址转账:', - style: AppTextStyles.bodyMedium(context), - ), - SizedBox(height: AppSpacing.sm), - _WalletAddressCard(address: walletAddress, network: walletNetwork), - SizedBox(height: AppSpacing.md), - Container( - padding: EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: AppColorScheme.warning.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColorScheme.warning.withValues(alpha: 0.2)), - ), - child: Row( - children: [ - Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), - SizedBox(width: AppSpacing.sm), - Expanded( - child: Text( - '转账完成后请点击"已打款"按钮确认', - style: AppTextStyles.bodyMedium(context).copyWith( - color: AppColorScheme.warning, - ), - ), - ), - ], - ), - ), - SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '稍后确认', - type: NeonButtonType.outline, - onPressed: () { - Navigator.of(ctx).pop(); - _navigateToAssetPage(); - }, - height: 44, - showGlow: false, - ), - ), - SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '已打款', - type: NeonButtonType.primary, - onPressed: () async { - Navigator.of(ctx).pop(); - final response = await context - .read() - .confirmPay(orderNo); - if (context.mounted) { - _showResultDialog( - response.success ? '确认成功' : '确认失败', - response.success ? '请等待管理员审核' : response.message, - ); - _navigateToAssetPage(); - } - }, - height: 44, - showGlow: true, - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - void _showResultDialog(String title, String? message) { - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.xxl), - padding: EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(title, - style: AppTextStyles.headlineLarge(context).copyWith( - fontWeight: FontWeight.bold, - )), - if (message != null) ...[ - SizedBox(height: AppSpacing.sm), - Text(message, - style: AppTextStyles.bodyMedium(context), - textAlign: TextAlign.center), - ], - SizedBox(height: AppSpacing.lg), - SizedBox( - width: double.infinity, - child: NeonButton( - text: '确定', - type: NeonButtonType.primary, - onPressed: () => Navigator.of(ctx).pop(), - height: 44, - showGlow: false, - ), - ), - ], - ), - ), - ), - ); - } - - /// 跳转到资产页面 - void _navigateToAssetPage() { - final mainState = context.findAncestorStateOfType(); - mainState?.switchToTab(3); - } - - void _navigateToWelfareCenter() { - Navigator.push( + /// 跳转到划转页面 + void _navigateToTransfer(BuildContext context) async { + final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => const TransferPage()), ); @@ -429,7 +160,7 @@ class _HomePageState extends State } /// 资产卡片(含总盈利 + 可折叠盈亏日历) -/// 资产卡片(含总盈利 + 可折叠盈亏日历) +/// 资产卡片(含总盈利) class _AssetCard extends StatefulWidget { final AssetOverview? overview; final VoidCallback onDeposit; @@ -441,14 +172,11 @@ class _AssetCard extends StatefulWidget { } class _AssetCardState extends State<_AssetCard> { - late DateTime _currentMonth; Map? _profitData; - bool _isLoadingCalendar = false; @override void initState() { super.initState(); - _currentMonth = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit()); } @@ -459,53 +187,19 @@ class _AssetCardState extends State<_AssetCard> { } Future _loadProfit() async { - setState(() => _isLoadingCalendar = true); try { + final now = DateTime.now(); final assetService = context.read(); final response = await assetService.getDailyProfit( - year: _currentMonth.year, - month: _currentMonth.month, + year: now.year, + month: now.month, ); if (mounted) { setState(() { _profitData = response.data; - _isLoadingCalendar = false; }); } - } catch (_) { - if (mounted) setState(() => _isLoadingCalendar = false); - } - } - - void _previousMonth() { - setState(() { - _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1); - }); - _loadProfit(); - } - - void _nextMonth() { - setState(() { - _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1); - }); - _loadProfit(); - } - - double? _getDayProfit(int day) { - if (_profitData == null) return null; - final daily = _profitData!['daily'] as Map?; - if (daily == null) return null; - final dateStr = '${_currentMonth.year}-${_currentMonth.month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; - final value = daily[dateStr]; - if (value == null) return null; - return (value is num) ? value.toDouble() : double.tryParse(value.toString()); - } - - double get _monthProfit { - if (_profitData == null) return 0; - final value = _profitData!['totalProfit']; - if (value == null) return 0; - return (value is num) ? value.toDouble() : double.tryParse(value.toString()) ?? 0; + } catch (_) {} } double? get _todayProfit { @@ -617,216 +311,11 @@ class _AssetCardState extends State<_AssetCard> { ), ], ), - - // 盈利日历 - _buildCalendarSection(context), ], ), ); } - Widget _buildCalendarSection(BuildContext context) { - final now = DateTime.now(); - final isCurrentMonth = _currentMonth.year == now.year && _currentMonth.month == now.month; - final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month, 1); - final daysInMonth = DateTime(_currentMonth.year, _currentMonth.month + 1, 0).day; - final startWeekday = firstDayOfMonth.weekday; - final upColor = context.appColors.up; - final downColor = context.appColors.down; - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 分隔线 - Padding( - padding: EdgeInsets.symmetric(vertical: AppSpacing.md), - child: Divider(color: context.appColors.ghostBorder, height: 1), - ), - - // 月份导航行 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: _previousMonth, - child: Container( - padding: EdgeInsets.all(AppSpacing.xs + 1), - decoration: BoxDecoration( - color: context.colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Icon(LucideIcons.chevronLeft, size: 16, color: context.colors.onSurfaceVariant), - ), - ), - Column( - children: [ - Text( - '${_currentMonth.year}年${_currentMonth.month}月', - style: AppTextStyles.headlineMedium(context).copyWith( - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 2), - if (!_isLoadingCalendar && _profitData != null) - Text( - '月度盈亏: ${_monthProfit >= 0 ? '+' : ''}${_monthProfit.toStringAsFixed(2)}', - style: AppTextStyles.bodySmall(context).copyWith( - fontWeight: FontWeight.w600, - color: _monthProfit >= 0 ? upColor : downColor, - ), - ), - ], - ), - GestureDetector( - onTap: isCurrentMonth ? null : _nextMonth, - child: Container( - padding: EdgeInsets.all(AppSpacing.xs + 1), - decoration: BoxDecoration( - color: isCurrentMonth - ? context.colors.surfaceContainerHigh.withValues(alpha: 0.5) - : context.colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Icon( - LucideIcons.chevronRight, - size: 16, - color: isCurrentMonth - ? context.colors.onSurfaceVariant.withValues(alpha: 0.4) - : context.colors.onSurfaceVariant, - ), - ), - ), - ], - ), - SizedBox(height: AppSpacing.sm), - - // 星期标题 - Row( - children: ['一', '二', '三', '四', '五', '六', '日'].map((d) { - return Expanded( - child: Center( - child: Text( - d, - style: AppTextStyles.bodySmall(context).copyWith( - fontWeight: FontWeight.w500, - color: context.colors.onSurfaceVariant.withValues(alpha: 0.6), - ), - ), - ), - ); - }).toList(), - ), - SizedBox(height: AppSpacing.xs), - - // 日历网格 - if (_isLoadingCalendar) - Padding( - padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), - child: SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: context.colors.primary, - ), - ), - ) - else - ..._buildCalendarGrid( - startWeekday, daysInMonth, now, isCurrentMonth, - ), - ], - ); - } - - List _buildCalendarGrid( - int startWeekday, - int daysInMonth, - DateTime now, - bool isCurrentMonth, - ) { - final upColor = context.appColors.up; - final downColor = context.appColors.down; - final List rows = []; - List cells = []; - - for (int i = 1; i < startWeekday; i++) { - cells.add(const Expanded(child: SizedBox.shrink())); - } - - for (int day = 1; day <= daysInMonth; day++) { - final profit = _getDayProfit(day); - final isToday = isCurrentMonth && day == now.day; - final hasProfit = profit != null && profit != 0; - - cells.add( - Expanded( - child: AspectRatio( - aspectRatio: 1, - child: Container( - margin: EdgeInsets.all(1), - decoration: BoxDecoration( - color: isToday - ? context.colors.primary.withValues(alpha: 0.12) - : hasProfit - ? (profit! > 0 - ? upColor.withValues(alpha: 0.08) - : downColor.withValues(alpha: 0.08)) - : Colors.transparent, - borderRadius: BorderRadius.circular(AppRadius.sm), - border: isToday - ? Border.all(color: context.colors.primary.withValues(alpha: 0.4), width: 1) - : null, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '$day', - style: AppTextStyles.bodySmall(context).copyWith( - fontSize: 10, - fontWeight: isToday ? FontWeight.bold : FontWeight.w400, - color: isToday - ? context.colors.primary - : context.colors.onSurface, - ), - ), - if (hasProfit) ...[ - SizedBox(height: 1), - Text( - '${profit! > 0 ? '+' : ''}${profit.abs() < 10 ? profit.toStringAsFixed(2) : profit.toStringAsFixed(1)}', - style: TextStyle( - fontSize: 7, - fontWeight: FontWeight.w600, - color: profit > 0 ? upColor : downColor, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - ), - ), - ); - - if (cells.length == 7) { - rows.add(Row(children: cells)); - cells = []; - } - } - - if (cells.isNotEmpty) { - while (cells.length < 7) { - cells.add(const Expanded(child: SizedBox.shrink())); - } - rows.add(Row(children: cells)); - } - - return rows; - } - String _formatAsset(String value) { final d = double.tryParse(value) ?? 0.0; return d.toStringAsFixed(2); @@ -1119,88 +608,6 @@ class _HoldingItem extends StatelessWidget { } } -/// 信息行组件 -class _InfoRow extends StatelessWidget { - final String label; - final String value; - final bool isBold; - - const _InfoRow({required this.label, required this.value, this.isBold = false}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, - style: AppTextStyles.bodyMedium(context)), - Text(value, - style: AppTextStyles.bodyMedium(context).copyWith( - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, - fontFeatures: isBold ? const [FontFeature.tabularFigures()] : null, - )), - ], - ); - } -} - -/// 钱包地址卡片 -class _WalletAddressCard extends StatelessWidget { - final String address; - final String network; - - const _WalletAddressCard({required this.address, required this.network}); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: context.colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: context.colors.outlineVariant.withValues(alpha: 0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - address, - style: AppTextStyles.bodyMedium(context).copyWith( - fontFamily: 'monospace', - ), - ), - ), - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: address)); - ToastUtils.show('地址已复制到剪贴板'); - }, - child: Container( - padding: EdgeInsets.all(AppSpacing.xs), - decoration: BoxDecoration( - color: context.colors.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Icon(LucideIcons.copy, - size: 16, color: context.colors.primary), - ), - ), - ], - ), - SizedBox(height: AppSpacing.sm), - Text( - '网络: $network', - style: AppTextStyles.bodySmall(context), - ), - ], - ), - ); - } -} - /// 盈亏统计小卡片 class _ProfitStatCard extends StatelessWidget { final String label; diff --git a/flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart b/flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart index ddd03ed..2b02d1c 100644 --- a/flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart +++ b/flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart @@ -3,19 +3,21 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; -/// 首页快捷操作栏 - 充值/提现/划转/账单 +/// 首页快捷操作栏 - 充值/提现/划转/盈亏/账单 class QuickActionsRow extends StatelessWidget { const QuickActionsRow({ super.key, this.onDeposit, this.onWithdraw, this.onTransfer, + this.onProfit, this.onBills, }); final VoidCallback? onDeposit; final VoidCallback? onWithdraw; final VoidCallback? onTransfer; + final VoidCallback? onProfit; final VoidCallback? onBills; @override @@ -64,6 +66,12 @@ class QuickActionsRow extends StatelessWidget { colorScheme: colorScheme, onTap: onTransfer, ), + _ActionItem( + icon: LucideIcons.chartPie, + label: '盈亏', + colorScheme: colorScheme, + onTap: onProfit, + ), _ActionItem( icon: LucideIcons.fileText, label: '账单',