Files
monisuo/flutter_monisuo/lib/ui/pages/home/bills_page.dart
2026-04-07 01:32:32 +08:00

495 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 && r['status'] == 1)
.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();
}
}