import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme_extension.dart'; import '../../../data/services/asset_service.dart'; /// 盈亏分析页面 - 月度盈亏日历 class ProfitAnalysisPage extends StatefulWidget { const ProfitAnalysisPage({super.key}); @override State createState() => _ProfitAnalysisPageState(); } class _ProfitAnalysisPageState extends State { late DateTime _currentMonth; Map? _profitData; bool _isLoading = false; @override void initState() { super.initState(); _currentMonth = DateTime.now(); WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit()); } // ============================================ // 数据加载 // ============================================ Future _loadProfit() async { setState(() => _isLoading = true); try { final assetService = context.read(); final response = await assetService.getDailyProfit( year: _currentMonth.year, month: _currentMonth.month, ); if (mounted) { setState(() { _profitData = response.data; _isLoading = false; }); } } catch (_) { if (mounted) setState(() => _isLoading = 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; } // ============================================ // 构建 UI // ============================================ @override Widget build(BuildContext context) { final now = DateTime.now(); final isCurrentMonth = _currentMonth.year == now.year && _currentMonth.month == now.month; return Scaffold( backgroundColor: context.colors.surface, appBar: AppBar( title: const Text('盈亏分析'), backgroundColor: context.colors.surface, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, ), body: SingleChildScrollView( padding: EdgeInsets.all(AppSpacing.lg), child: Container( padding: EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: context.appColors.surfaceCard, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: context.appColors.ghostBorder), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 月度盈亏摘要 _buildSummarySection(), SizedBox(height: AppSpacing.md), // 月份导航 _buildMonthNavigation(isCurrentMonth), SizedBox(height: AppSpacing.sm), // 星期标题 _buildWeekdayHeaders(), SizedBox(height: AppSpacing.xs), // 日历网格 if (_isLoading) _buildLoadingIndicator() else ..._buildCalendarGrid( now, isCurrentMonth, ), ], ), ), ), ); } /// 月度盈亏摘要 Widget _buildSummarySection() { final upColor = context.appColors.up; final downColor = context.appColors.down; final isProfit = _monthProfit >= 0; final color = _isLoading ? context.colors.onSurfaceVariant : (isProfit ? upColor : downColor); return Column( children: [ Text( '月度盈亏', style: AppTextStyles.bodyMedium(context).copyWith( color: context.colors.onSurfaceVariant, ), ), SizedBox(height: AppSpacing.xs), Text( _isLoading ? '--' : '${isProfit ? '+' : ''}${_monthProfit.toStringAsFixed(2)} USDT', style: AppTextStyles.displaySmall(context).copyWith( fontWeight: FontWeight.bold, color: color, ), ), ], ); } /// 月份导航行 Widget _buildMonthNavigation(bool isCurrentMonth) { return 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, ), ), ), // 当前年月 Text( '${_currentMonth.year}年${_currentMonth.month}月', style: AppTextStyles.headlineMedium(context).copyWith( fontWeight: FontWeight.bold, ), ), // 下一月(当前月禁用) 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, ), ), ), ], ); } /// 星期标题行 Widget _buildWeekdayHeaders() { return 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(), ); } /// 加载指示器 Widget _buildLoadingIndicator() { return Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.xxl), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: context.colors.primary, ), ), ), ); } /// 日历网格 List _buildCalendarGrid( DateTime now, bool isCurrentMonth, ) { final upColor = context.appColors.up; final downColor = context.appColors.down; final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month, 1); final daysInMonth = DateTime(_currentMonth.year, _currentMonth.month + 1, 0).day; final startWeekday = firstDayOfMonth.weekday; 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; } }