Files
monisuo/flutter_monisuo/lib/ui/pages/home/bills_page.dart
2026-04-06 11:31:28 +08:00

493 lines
17 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: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<BillsPage> createState() => _BillsPageState();
}
class _BillsPageState extends State<BillsPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
List<AccountTrade> _holdings = [];
List<Map<String, dynamic>> _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<void> _loadData() async {
setState(() => _isLoading = true);
try {
final provider = context.read<AssetProvider>();
final bonusService = context.read<BonusService>();
// 并行加载持仓和福利记录
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<AssetProvider>().holdings;
_isLoading = false;
});
}
}
}
List<Map<String, dynamic>> _parseWelfareRecords(Map<String, dynamic> data) {
final records = <Map<String, dynamic>>[];
// 新人福利
final newUser = data['newUserBonus'] as Map<String, dynamic>?;
if (newUser != null) {
final claimed = newUser['claimed'] as bool? ?? false;
final eligible = newUser['eligible'] as bool? ?? false;
// 状态: 1=已领取, 0=可领取(待领取), 2=不可用(未解锁)
final int status;
if (claimed) {
status = 1;
} else if (eligible) {
status = 0;
} else {
status = 2;
}
records.add({
'type': 'new_user',
'title': '新人福利',
'amount': newUser['amount']?.toString() ?? '100.00',
'status': status,
'time': newUser['claimTime'] ?? newUser['createTime'],
});
}
// 推广福利列表
final referralRewards = data['referralRewards'] as List<dynamic>? ?? [];
for (var r in referralRewards) {
final map = r as Map<String, dynamic>;
final username = map['username'] as String? ?? '用户';
final milestones = map['milestones'] as List<dynamic>? ?? [];
final claimableCount = map['claimableCount'] as int? ?? 0;
// 每个 milestone 生成一条记录
for (var m in milestones) {
final ms = m as Map<String, dynamic>;
final earned = ms['earned'] as bool? ?? false;
final claimable = ms['claimable'] as bool? ?? false;
final milestoneVal = ms['milestone'] as int? ?? 1;
final int status;
if (earned) {
status = 1; // 已领取
} else if (claimable) {
status = 0; // 可领取
} else {
status = 2; // 未达标
}
records.add({
'type': 'referral',
'title': '推广福利 - $username (${milestoneVal}000)',
'amount': '100.00',
'status': status,
'time': ms['claimTime'] ?? ms['createTime'],
});
}
// 如果没有 milestone 但有 claimableCount也生成记录
if (milestones.isEmpty && claimableCount > 0) {
records.add({
'type': 'referral',
'title': '推广福利 - $username',
'amount': '${claimableCount * 100}',
'status': 0,
'time': null,
});
}
}
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<String, dynamic> 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();
}
}