Files
monisuo/flutter_monisuo/lib/ui/pages/home/home_page.dart
sion123 f5ac578892 docs(theme): update documentation and clean up deprecated color scheme definitions
Removed outdated compatibility aliases and deprecated methods from AppColorScheme,
and updated CLAUDE.md to reflect new theme system requirements with centralized
color management and no hard-coded values in UI components.
2026-04-05 23:37:27 +08:00

1356 lines
45 KiB
Dart

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';
import '../../../core/theme/app_spacing.dart';
import '../../../core/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../data/models/account_models.dart';
import '../../../data/services/asset_service.dart';
import '../../../data/services/bonus_service.dart';
import '../../../providers/asset_provider.dart';
import '../../../providers/auth_provider.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
import '../main/main_page.dart';
import '../mine/welfare_center_page.dart';
import 'header_bar.dart';
import 'quick_actions_row.dart';
import 'hot_coins_section.dart';
/// 首页
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin {
int _totalClaimable = 0;
StreamSubscription<AppEvent>? _eventSub;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadData();
_listenEvents();
});
}
@override
void dispose() {
_eventSub?.cancel();
super.dispose();
}
void _listenEvents() {
final eventBus = context.read<AppEventBus>();
_eventSub = eventBus.on(AppEventType.assetChanged, (_) {
if (mounted) {
context.read<AssetProvider>().loadOverview(force: true);
_checkBonusStatus();
}
});
}
void _loadData() {
final provider = context.read<AssetProvider>();
provider.loadOverview();
provider.loadTradeAccount();
_checkBonusStatus();
}
Future<void> _checkBonusStatus() async {
try {
final bonusService = context.read<BonusService>();
final response = await bonusService.getWelfareStatus();
if (response.success && response.data != null) {
setState(() {
_totalClaimable = response.data!['totalClaimable'] as int? ?? 0;
});
}
} catch (_) {}
}
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return RefreshIndicator(
onRefresh: () => provider.refreshAll(force: true),
color: colorScheme.primary,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
left: AppSpacing.lg,
right: AppSpacing.lg,
top: AppSpacing.sm,
bottom: 100,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
HeaderBar(),
SizedBox(height: AppSpacing.md),
// 资产卡片(含总盈利 + 可折叠盈亏日历)
_AssetCard(
overview: provider.overview,
onDeposit: _showDeposit,
),
SizedBox(height: AppSpacing.md),
// 快捷操作栏
QuickActionsRow(
onDeposit: _showDeposit,
onWithdraw: () => _navigateToAssetPage(),
onTransfer: () => _navigateToAssetPage(),
onProfit: () {},
onBills: () => _navigateToAssetPage(),
),
SizedBox(height: AppSpacing.md),
// 福利中心入口卡片
_WelfareCard(
totalClaimable: _totalClaimable,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
),
),
SizedBox(height: AppSpacing.lg),
// 热门币种
HotCoinsSection(),
SizedBox(height: AppSpacing.lg),
// 持仓
_HoldingsSection(holdings: provider.holdings),
],
),
),
);
},
),
);
}
void _showDeposit() {
final amountController = TextEditingController();
final formKey = GlobalKey<ShadFormState>();
final colorScheme = Theme.of(context).colorScheme;
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: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Icon(LucideIcons.wallet, color: colorScheme.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<AssetProvider>()
.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<String, dynamic> 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';
final colorScheme = Theme.of(context).colorScheme;
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.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: AppColorScheme.warning.withOpacity(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<AssetProvider>()
.confirmPay(orderNo);
if (context.mounted) {
_showResultDialog(
response.success ? '确认成功' : '确认失败',
response.success ? '请等待管理员审核' : response.message,
);
_navigateToAssetPage();
}
},
height: 44,
showGlow: true,
),
),
],
),
],
),
),
),
);
}
void _showResultDialog(String title, String? message) {
final colorScheme = Theme.of(context).colorScheme;
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<MainPageState>();
mainState?.switchToTab(3);
}
void _navigateToWelfareCenter() {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
}
}
/// Header 栏:品牌名 + 搜索/通知/头像
/// 资产卡片(含总盈利 + 可折叠盈亏日历)
class _AssetCard extends StatefulWidget {
final AssetOverview? overview;
final VoidCallback onDeposit;
const _AssetCard({required this.overview, required this.onDeposit});
@override
State<_AssetCard> createState() => _AssetCardState();
}
class _AssetCardState extends State<_AssetCard> {
bool _calendarExpanded = false;
late DateTime _currentMonth;
Map<String, dynamic>? _profitData;
bool _isLoadingCalendar = false;
@override
void initState() {
super.initState();
_currentMonth = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit());
}
double get _totalProfit {
final v = widget.overview?.totalProfit;
if (v == null) return 0;
return double.tryParse(v) ?? 0;
}
Future<void> _toggleCalendar() async {
setState(() => _calendarExpanded = !_calendarExpanded);
if (_calendarExpanded && _profitData == null) {
await _loadProfit();
}
}
Future<void> _loadProfit() async {
setState(() => _isLoadingCalendar = true);
try {
final assetService = context.read<AssetService>();
final response = await assetService.getDailyProfit(
year: _currentMonth.year,
month: _currentMonth.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<String, dynamic>?;
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;
}
double? get _todayProfit {
if (_profitData == null) return null;
final daily = _profitData!['daily'] as Map<String, dynamic>?;
if (daily == null) return null;
final now = DateTime.now();
final todayKey = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
final value = daily[todayKey];
if (value == null) return null;
return (value is num) ? value.toDouble() : double.tryParse(value.toString());
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
final isProfit = _totalProfit >= 0;
final todayProfit = _todayProfit;
final isTodayProfit = (todayProfit ?? 0) >= 0;
// 总资产
final totalAsset = widget.overview?.totalAsset ?? '0.00';
final displayAsset = _formatAsset(totalAsset);
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部行:总资产标签 + 充值按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'预估总资产(USDT)',
style: AppTextStyles.bodyMedium(context),
),
GestureDetector(
onTap: widget.onDeposit,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs + 2,
),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: 14, color: colorScheme.primary),
SizedBox(width: AppSpacing.xs),
Text(
'充值',
style: AppTextStyles.labelLarge(context).copyWith(
color: colorScheme.primary,
),
),
],
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
// 总资产金额
Text(
displayAsset,
style: AppTextStyles.displaySmall(context).copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(height: AppSpacing.md),
// 盈亏统计区:今日盈亏 | 总盈亏 + 盈亏分析按钮
Row(
children: [
// 今日盈亏卡片
Expanded(
child: _ProfitStatCard(
label: '今日盈亏',
value: _todayProfit,
upColor: upColor,
downColor: downColor,
),
),
SizedBox(width: AppSpacing.sm),
// 总盈亏卡片
Expanded(
child: _ProfitStatCard(
label: '总盈亏',
value: _totalProfit,
upColor: upColor,
downColor: downColor,
),
),
SizedBox(width: AppSpacing.sm),
// 盈亏分析按钮
GestureDetector(
onTap: _toggleCalendar,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _calendarExpanded
? colorScheme.primary.withOpacity(0.1)
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.full),
border: _calendarExpanded
? Border.all(color: colorScheme.primary.withOpacity(0.2))
: null,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.chartBar,
size: 13,
color: _calendarExpanded
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
SizedBox(width: AppSpacing.xs),
Text(
'盈亏分析',
style: AppTextStyles.labelMedium(context).copyWith(
fontWeight: _calendarExpanded ? FontWeight.w600 : FontWeight.w500,
color: _calendarExpanded
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
SizedBox(width: 2),
AnimatedRotation(
turns: _calendarExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: Icon(
LucideIcons.chevronDown,
size: 13,
color: _calendarExpanded
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
),
],
),
),
),
],
),
// 可折叠的盈利日历
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: _buildCalendarSection(
context, colorScheme, isDark, upColor, downColor,
),
crossFadeState: _calendarExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300),
sizeCurve: Curves.easeInOut,
),
],
),
);
}
Widget _buildCalendarSection(
BuildContext context,
ColorScheme colorScheme,
bool isDark,
Color upColor,
Color downColor,
) {
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;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 分隔线
Padding(
padding: EdgeInsets.symmetric(vertical: AppSpacing.md),
child: Divider(color: colorScheme.outlineVariant.withOpacity(0.15), height: 1),
),
// 月份导航行
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: _previousMonth,
child: Container(
padding: EdgeInsets.all(AppSpacing.xs + 1),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Icon(LucideIcons.chevronLeft, size: 16, color: colorScheme.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
? colorScheme.surfaceContainerHigh.withOpacity(0.5)
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Icon(
LucideIcons.chevronRight,
size: 16,
color: isCurrentMonth
? colorScheme.onSurfaceVariant.withOpacity(0.4)
: colorScheme.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: colorScheme.onSurfaceVariant.withOpacity(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: colorScheme.primary,
),
),
)
else
..._buildCalendarGrid(
startWeekday, daysInMonth, now, isCurrentMonth,
upColor, downColor, colorScheme,
),
],
);
}
List<Widget> _buildCalendarGrid(
int startWeekday,
int daysInMonth,
DateTime now,
bool isCurrentMonth,
Color upColor,
Color downColor,
ColorScheme colorScheme,
) {
final List<Widget> rows = [];
List<Widget> 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
? colorScheme.primary.withOpacity(0.12)
: hasProfit
? (profit! > 0
? upColor.withOpacity(0.08)
: downColor.withOpacity(0.08))
: Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: isToday
? Border.all(color: colorScheme.primary.withOpacity(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
? colorScheme.primary
: colorScheme.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);
}
}
/// 福利中心入口卡片
class _WelfareCard extends StatelessWidget {
final int totalClaimable;
final VoidCallback onTap;
const _WelfareCard({required this.totalClaimable, required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
colorScheme.primary.withOpacity(0.15),
colorScheme.secondary.withOpacity(0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
),
child: Row(
children: [
// 左侧图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.15),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Icon(
LucideIcons.gift,
color: colorScheme.primary,
size: 24,
),
),
SizedBox(width: AppSpacing.md),
// 中间文字
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'福利中心',
style: AppTextStyles.headlineLarge(context).copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(height: AppSpacing.xs),
Text(
totalClaimable > 0
? '您有 $totalClaimable 个奖励待领取'
: '首充奖励 + 推广奖励',
style: AppTextStyles.bodyMedium(context),
),
],
),
),
// 右侧按钮
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
gradient: isDark
? AppColorScheme.darkCtaGradient
: AppColorScheme.lightCtaGradient,
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (totalClaimable > 0)
Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColorScheme.darkError,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Text(
'$totalClaimable',
style: AppTextStyles.bodySmall(context).copyWith(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnPrimary,
),
),
),
SizedBox(width: totalClaimable > 0 ? 6 : 0),
Text(
'查看',
style: AppTextStyles.headlineSmall(context).copyWith(
fontWeight: FontWeight.w700,
color: isDark ? colorScheme.background : AppColorScheme.darkOnPrimary,
),
),
],
),
),
],
),
),
);
}
}
/// 持仓部分
class _HoldingsSection extends StatelessWidget {
final List<AccountTrade> holdings;
const _HoldingsSection({required this.holdings});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'我的持仓',
style: AppTextStyles.headlineLarge(context).copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
),
child: Row(
children: [
Text('资产详情',
style: AppTextStyles.headlineSmall(context).copyWith(
fontWeight: FontWeight.bold,
)),
const SizedBox(width: AppSpacing.xs),
Icon(LucideIcons.chevronRight,
size: 16, color: colorScheme.primary),
],
),
),
],
),
SizedBox(height: AppSpacing.md),
holdings.isEmpty ? const _EmptyHoldings() : _HoldingsList(holdings: holdings),
],
);
}
}
/// 空持仓
class _EmptyHoldings extends StatelessWidget {
const _EmptyHoldings();
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: AppSpacing.xxl, horizontal: AppSpacing.lg),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLow.withOpacity(0.5),
borderRadius: BorderRadius.circular(AppRadius.xxl),
border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.1)),
),
child: Column(
children: [
Icon(LucideIcons.wallet, size: 48, color: colorScheme.onSurfaceVariant),
SizedBox(height: AppSpacing.md),
Text(
'暂无持仓',
style: AppTextStyles.headlineMedium(context).copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: AppSpacing.sm),
Text(
'快去交易吧~',
style: AppTextStyles.bodyLarge(context),
),
],
),
);
}
}
/// 持仓列表
class _HoldingsList extends StatelessWidget {
final List<AccountTrade> holdings;
const _HoldingsList({required this.holdings});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final displayHoldings =
holdings.length > 5 ? holdings.sublist(0, 5) : holdings;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface.withOpacity(0.5),
borderRadius: BorderRadius.circular(AppRadius.xxl),
border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.1)),
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(AppSpacing.md),
itemCount: displayHoldings.length,
separatorBuilder: (_, __) => Divider(
color: colorScheme.outlineVariant.withOpacity(0.1),
height: 1,
),
itemBuilder: (context, index) =>
_HoldingItem(holding: displayHoldings[index]),
),
);
}
}
/// 持仓项
class _HoldingItem extends StatelessWidget {
final AccountTrade holding;
const _HoldingItem({required this.holding});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CircleAvatar(
radius: 18,
backgroundColor: colorScheme.primary.withOpacity(0.1),
child: Text(
holding.coinCode.substring(0, 1),
style: AppTextStyles.headlineMedium(context).copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(holding.coinCode,
style: AppTextStyles.headlineMedium(context).copyWith(
fontWeight: FontWeight.bold,
)),
Text(holding.quantity,
style: AppTextStyles.bodyMedium(context)),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${holding.currentValue} USDT',
style: AppTextStyles.headlineSmall(context).copyWith(
fontWeight: FontWeight.w500,
)),
Text(holding.formattedProfitRate,
style: AppTextStyles.numberSmall(context).copyWith(
color: holding.isProfit
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark),
)),
],
),
],
),
);
}
}
/// 信息行组件
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) {
final colorScheme = Theme.of(context).colorScheme;
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) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: colorScheme.outlineVariant.withOpacity(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: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Icon(LucideIcons.copy,
size: 16, color: colorScheme.primary),
),
),
],
),
SizedBox(height: AppSpacing.sm),
Text(
'网络: $network',
style: AppTextStyles.bodySmall(context),
),
],
),
);
}
}
/// 盈亏统计小卡片
class _ProfitStatCard extends StatelessWidget {
final String label;
final double? value;
final Color upColor;
final Color downColor;
const _ProfitStatCard({
required this.label,
required this.value,
required this.upColor,
required this.downColor,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final hasValue = value != null;
final isProfit = (value ?? 0) >= 0;
final color = hasValue ? (isProfit ? upColor : downColor) : colorScheme.onSurfaceVariant;
return Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2),
decoration: BoxDecoration(
color: hasValue
? (isProfit ? upColor : downColor).withOpacity(0.06)
: colorScheme.surfaceContainerHigh.withOpacity(0.5),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: hasValue
? (isProfit ? upColor : downColor).withOpacity(0.12)
: colorScheme.outlineVariant.withOpacity(0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
hasValue
? (isProfit ? LucideIcons.trendingUp : LucideIcons.trendingDown)
: LucideIcons.minus,
size: 11,
color: color.withOpacity(0.7),
),
SizedBox(width: 3),
Text(
label,
style: AppTextStyles.bodySmall(context).copyWith(
fontWeight: FontWeight.w500,
color: color.withOpacity(0.8),
),
),
],
),
SizedBox(height: AppSpacing.xs),
Text(
hasValue
? '${isProfit ? '+' : ''}${value!.toStringAsFixed(2)}'
: '--',
style: AppTextStyles.numberMedium(context).copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
}