diff --git a/flutter_monisuo/lib/ui/pages/home/profit_analysis_page.dart b/flutter_monisuo/lib/ui/pages/home/profit_analysis_page.dart new file mode 100644 index 0000000..147a2fd --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/home/profit_analysis_page.dart @@ -0,0 +1,379 @@ +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_color_scheme.dart'; +import '../../../core/theme/app_spacing.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()); + } + + // ============================================ + // 主题感知颜色 + // ============================================ + + bool get _isDark => Theme.of(context).brightness == Brightness.dark; + + Color get _upColor => AppColorScheme.getUpColor(_isDark); + Color get _downColor => AppColorScheme.getDownColor(_isDark); + + Color get _scaffoldBg => + _isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground; + + Color get _cardBg => _isDark + ? AppColorScheme.darkSurfaceContainer + : AppColorScheme.lightSurfaceLowest; + + Color get _cardBorder => _isDark + ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) + : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5); + + // ============================================ + // 数据加载 + // ============================================ + + 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 colorScheme = Theme.of(context).colorScheme; + final now = DateTime.now(); + final isCurrentMonth = + _currentMonth.year == now.year && _currentMonth.month == now.month; + + return Scaffold( + backgroundColor: _scaffoldBg, + appBar: AppBar( + title: const Text('盈亏分析'), + backgroundColor: _scaffoldBg, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(AppSpacing.lg), + child: Container( + padding: EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: _cardBg, + borderRadius: BorderRadius.circular(AppRadius.xl), + border: Border.all(color: _cardBorder), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 月度盈亏摘要 + _buildSummarySection(colorScheme), + SizedBox(height: AppSpacing.md), + + // 月份导航 + _buildMonthNavigation(colorScheme, isCurrentMonth), + SizedBox(height: AppSpacing.sm), + + // 星期标题 + _buildWeekdayHeaders(colorScheme), + SizedBox(height: AppSpacing.xs), + + // 日历网格 + if (_isLoading) + _buildLoadingIndicator(colorScheme) + else + ..._buildCalendarGrid( + colorScheme, + now, + isCurrentMonth, + ), + ], + ), + ), + ), + ); + } + + /// 月度盈亏摘要 + Widget _buildSummarySection(ColorScheme colorScheme) { + final isProfit = _monthProfit >= 0; + final color = _isLoading ? colorScheme.onSurfaceVariant : (isProfit ? _upColor : _downColor); + + return Column( + children: [ + Text( + '月度盈亏', + style: AppTextStyles.bodyMedium(context).copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + SizedBox(height: AppSpacing.xs), + Text( + _isLoading + ? '--' + : '${isProfit ? '+' : ''}${_monthProfit.toStringAsFixed(2)} USDT', + style: AppTextStyles.displaySmall(context).copyWith( + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ); + } + + /// 月份导航行 + Widget _buildMonthNavigation(ColorScheme colorScheme, bool isCurrentMonth) { + return 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, + ), + ), + ), + // 当前年月 + 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 + ? colorScheme.surfaceContainerHigh.withValues(alpha: 0.5) + : colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Icon( + LucideIcons.chevronRight, + size: 16, + color: isCurrentMonth + ? colorScheme.onSurfaceVariant.withValues(alpha: 0.4) + : colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + } + + /// 星期标题行 + Widget _buildWeekdayHeaders(ColorScheme colorScheme) { + return Row( + children: ['一', '二', '三', '四', '五', '六', '日'].map((d) { + return Expanded( + child: Center( + child: Text( + d, + style: AppTextStyles.bodySmall(context).copyWith( + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), + ), + ), + ); + }).toList(), + ); + } + + /// 加载指示器 + Widget _buildLoadingIndicator(ColorScheme colorScheme) { + return Padding( + padding: EdgeInsets.symmetric(vertical: AppSpacing.xxl), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: colorScheme.primary, + ), + ), + ), + ); + } + + /// 日历网格 + List _buildCalendarGrid( + ColorScheme colorScheme, + DateTime now, + bool isCurrentMonth, + ) { + 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 + ? colorScheme.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: colorScheme.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 + ? 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; + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart b/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart index f2a54a2..e86ec54 100644 --- a/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart +++ b/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/app_spacing.dart'; +import '../../../../core/theme/app_theme.dart'; /// 信息行组件(用于关于对话框) class InfoRow extends StatelessWidget { @@ -18,8 +19,7 @@ class InfoRow extends StatelessWidget { SizedBox(width: AppSpacing.sm), Text( text, - style: TextStyle( - fontSize: 12, + style: AppTextStyles.bodyMedium(context).copyWith( color: colorScheme.onSurfaceVariant, ), ), diff --git a/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart b/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart index 1e4f3c4..1ba0560 100644 --- a/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart +++ b/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../../../../core/theme/app_color_scheme.dart'; import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; @@ -26,9 +25,7 @@ class LogoutButton extends StatelessWidget { child: Center( child: Text( '退出登录', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, + style: AppTextStyles.headlineMedium(context).copyWith( color: AppColorScheme.down, ), ), diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart index ff97ec8..fdc5a86 100644 --- a/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; -import 'package:provider/provider.dart'; import '../../../../core/theme/app_color_scheme.dart'; import '../../../../core/theme/app_spacing.dart'; -import '../../../../providers/theme_provider.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/providers/theme_provider.dart'; /// KYC 状态徽章 (e.g. "已认证" green badge + chevron) -/// + /// /// 根据 [kycStatus] 显示不同状态: -/// - 2: 已认证(绿色) -/// - 1: 审核中(橙色) -/// - 其他: 仅显示 chevron +/// - 2: 已认证(绿色) +/// - 1: 审核中(橙色) + /// - 其他: 仅显示 chevron + */ class KycBadge extends StatelessWidget { final int kycStatus; const KycBadge({super.key, required this.kycStatus}); @@ -21,63 +21,58 @@ class KycBadge extends StatelessWidget { final isDark = Theme.of(context).brightness == Brightness.dark; final green = AppColorScheme.getUpColor(isDark); - if (kycStatus == 2) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: green.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - '已认证', - style: GoogleFonts.inter( - fontSize: 10, - fontWeight: FontWeight.w500, - color: green, - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: green.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text( + '已认证', + style: AppTextStyles.labelMedium(context).copyWith( + color: green, ), ), - const SizedBox(width: 8), - Icon( - LucideIcons.chevronRight, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ], - ); - } + ), + const SizedBox(width: 8), + Icon( + LucideIcons.chevronRight, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ); + } if (kycStatus == 1) { return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: AppColorScheme.warning.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - '审核中', - style: GoogleFonts.inter( - fontSize: 10, - fontWeight: FontWeight.w500, - color: AppColorScheme.warning, - ), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: AppColorScheme.warning.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text( + '审核中', + style: AppTextStyles.labelMedium(context).copyWith( + color: AppColorScheme.warning, ), ), - const SizedBox(width: 8), - Icon( - LucideIcons.chevronRight, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ], - ); - } + ), + const SizedBox(width: 8), + Icon( + LucideIcons.chevronRight, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ); + } return Icon( LucideIcons.chevronRight, @@ -100,9 +95,8 @@ class RedDotIndicator extends StatelessWidget { width: 8, height: 8, decoration: BoxDecoration( - color: AppColorScheme.down, - shape: BoxShape.circle, - ), + color: AppColorScheme.down, + shape: BoxShape.circle, ), const SizedBox(width: 8), Icon( @@ -135,8 +129,8 @@ class DarkModeRow extends StatelessWidget { height: 36, decoration: BoxDecoration( color: isDark - ? colorScheme.surfaceContainerHigh - : colorScheme.surfaceContainerHighest, + ? colorScheme.surfaceContainerHigh + : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Center( @@ -151,14 +145,13 @@ class DarkModeRow extends StatelessWidget { Expanded( child: Text( '深色模式', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w500, - color: colorScheme.onSurface, - ), + style: AppTextStyles.headlineMedium(context), ), ), // Toggle switch - matching .pen design (44x24 rounded pill) + // thumb + custom radius +12) GestureDetector( onTap: () => themeProvider.toggleTheme(), child: AnimatedContainer( @@ -168,28 +161,26 @@ class DarkModeRow extends StatelessWidget { padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: isDark - ? colorScheme.surfaceContainerHigh - : colorScheme.surfaceContainerHighest, + ? colorScheme.surfaceContainerHigh + : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), - child: AnimatedAlign( - duration: const Duration(milliseconds: 200), - alignment: - themeProvider.isDarkMode - ? Alignment.centerRight - : Alignment.centerLeft, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: colorScheme.onSurface, - shape: BoxShape.circle, - ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: + themeProvider.isDarkMode + ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: colorScheme.onSurface, + shape: BoxShape.circle, ), ), ), - ), - ], + ], + ), ), ); }