Files
monisuo/flutter_monisuo/lib/ui/pages/home/profit_analysis_page.dart
sion123 7ed2435a4c refactor(theme): 迁移主题感知颜色至 ThemeExtension
- 创建 AppThemeColors ThemeExtension 类,统一管理主题感知颜色(涨跌色、卡片背景、渐变等)
- 从 AppColorScheme 移除主题感知辅助函数,仅保留静态颜色常量
- 在 AppTheme 中注册 ThemeExtension,支持深色/浅色主题工厂
- 重构所有 UI 组件使用 context.appColors 访问主题颜色,替代硬编码的 AppColorScheme 方法调用
- 移除组件中重复的 isDark 判断逻辑,简化颜色获取方式
- 保持向后兼容性,所有现有功能不变
2026-04-06 01:58:08 +08:00

361 lines
11 KiB
Dart

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<ProfitAnalysisPage> createState() => _ProfitAnalysisPageState();
}
class _ProfitAnalysisPageState extends State<ProfitAnalysisPage> {
late DateTime _currentMonth;
Map<String, dynamic>? _profitData;
bool _isLoading = false;
@override
void initState() {
super.initState();
_currentMonth = DateTime.now();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProfit());
}
// ============================================
// 数据加载
// ============================================
Future<void> _loadProfit() async {
setState(() => _isLoading = true);
try {
final assetService = context.read<AssetService>();
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<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;
}
// ============================================
// 构建 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<Widget> _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<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
? 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;
}
}