- 引导页协议多语言支持(languageId传递) - 登录页双书名号修复 + 注册页协议勾选 - 个人中心页面多语言(18个翻译键) - 网络断开提示增加关闭/刷新按钮 - 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索 - iOS快捷按钮重复修复(删除Info.plist静态定义) - 测试账号123456警告提示 - 扫码登录自动跳转(HTTP轮询+WebSocket双通道) - 登录页老用户按钮改次要色 - Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0) - macOS标题栏跟随软件夜间模式 - 平台兼容分发渠道弹窗 - 软件著作权图片+交叉水印 - 桌面小部件平台兼容说明默认收起 - iOS/macOS图标更新+名称确认为闲言 - 12个语言文件补全roleNative+7个分发渠道翻译字段
746 lines
24 KiB
Dart
746 lines
24 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 学习进度可视化页面
|
|
/// 创建时间: 2026-05-10
|
|
/// 更新时间: 2026-05-10
|
|
/// 作用: 环形进度图 + 趋势折线图 + 分类柱状图 + 目标设置
|
|
/// 上次更新: 修复fl_chart API兼容性
|
|
/// ============================================================
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
|
|
import '../../../../core/theme/app_radius.dart';
|
|
import '../../../../core/theme/app_spacing.dart';
|
|
import '../../../../core/theme/app_theme.dart';
|
|
import '../../../../core/theme/app_typography.dart';
|
|
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
|
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
|
import '../../../../shared/widgets/containers/glass_container.dart';
|
|
import '../providers/learning_progress_provider.dart';
|
|
|
|
class LearningProgressPage extends ConsumerStatefulWidget {
|
|
const LearningProgressPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<LearningProgressPage> createState() =>
|
|
_LearningProgressPageState();
|
|
}
|
|
|
|
class _LearningProgressPageState extends ConsumerState<LearningProgressPage> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
ref.read(learningProgressProvider.notifier).loadAll();
|
|
});
|
|
}
|
|
|
|
Future<void> _refresh() async {
|
|
await ref.read(learningProgressProvider.notifier).loadAll();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ext = AppTheme.ext(context);
|
|
final state = ref.watch(learningProgressProvider);
|
|
|
|
return CupertinoPageScaffold(
|
|
backgroundColor: ext.bgPrimary,
|
|
navigationBar: CupertinoNavigationBar(
|
|
middle: Text(
|
|
'📊 学习进度',
|
|
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
|
),
|
|
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
|
|
border: null,
|
|
leading: const AdaptiveBackButton(),
|
|
),
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: CustomScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
slivers: [
|
|
CupertinoSliverRefreshControl(onRefresh: _refresh),
|
|
if (state.isLoading && state.overviewData == null)
|
|
SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CupertinoActivityIndicator(color: ext.accent),
|
|
const SizedBox(height: AppSpacing.md),
|
|
Text(
|
|
'加载中...',
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
else if (state.error != null && state.overviewData == null)
|
|
SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: _buildErrorState(ext, state.error!),
|
|
)
|
|
else ...[
|
|
SliverToBoxAdapter(
|
|
child: _buildGoalRingCard(ext, state)
|
|
.animate()
|
|
.fadeIn(duration: 300.ms)
|
|
.slideY(begin: 0.05, end: 0),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: _buildTrendCard(ext, state)
|
|
.animate()
|
|
.fadeIn(duration: 300.ms, delay: 80.ms)
|
|
.slideY(begin: 0.05, end: 0),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: _buildCategoryCard(ext, state)
|
|
.animate()
|
|
.fadeIn(duration: 300.ms, delay: 130.ms)
|
|
.slideY(begin: 0.05, end: 0),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: _buildGoalSettingCard(ext, state)
|
|
.animate()
|
|
.fadeIn(duration: 300.ms, delay: 180.ms)
|
|
.slideY(begin: 0.05, end: 0),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: MediaQuery.of(context).viewInsets.bottom + 40,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorState(AppThemeExtension ext, String error) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(AppSpacing.xl),
|
|
child: GlassContainer(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.exclamationmark_triangle,
|
|
size: 48,
|
|
color: ext.textHint,
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
Text(
|
|
error,
|
|
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
CupertinoButton(
|
|
color: ext.accent,
|
|
borderRadius: AppRadius.pillBorder,
|
|
onPressed: _refresh,
|
|
child: Text(
|
|
'重试',
|
|
style: AppTypography.body.copyWith(
|
|
color: CupertinoColors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGoalRingCard(
|
|
AppThemeExtension ext,
|
|
LearningProgressState state,
|
|
) {
|
|
final rate = state.goalCompletionRate.clamp(0.0, 1.0);
|
|
final percent = (rate * 100).toInt();
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
child: GlassContainer(
|
|
depth: GlassDepth.elevated,
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.chart_pie_fill,
|
|
size: 16,
|
|
color: ext.accent,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
'今日目标',
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
SizedBox(
|
|
width: 180,
|
|
height: 180,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: 180,
|
|
height: 180,
|
|
child: DeferredBuilder(
|
|
builder: (context) => SfCircularChart(
|
|
margin: EdgeInsets.zero,
|
|
series: [
|
|
DoughnutSeries<_RingData, String>(
|
|
dataSource: [
|
|
_RingData('已完成', rate, ext.accent),
|
|
_RingData('剩余', 1.0 - rate, ext.accent.withValues(alpha: 0.12)),
|
|
],
|
|
xValueMapper: (d, _) => d.label,
|
|
yValueMapper: (d, _) => d.value,
|
|
pointColorMapper: (d, _) => d.color,
|
|
innerRadius: '66%',
|
|
strokeWidth: 0,
|
|
animationDuration: 0,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'$percent%',
|
|
style: AppTypography.title1.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
Text(
|
|
'${state.todayProgress}/${state.dailyGoal}',
|
|
style: AppTypography.caption1.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
_buildStatRow(ext, state),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatRow(AppThemeExtension ext, LearningProgressState state) {
|
|
final overview = state.overviewData;
|
|
final totalViews = overview?['total_view_count'] as int? ?? 0;
|
|
final totalFavorites = overview?['total_favorite_count'] as int? ?? 0;
|
|
final streakDays = overview?['streak_days'] as int? ?? 0;
|
|
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildStatItem(ext, CupertinoIcons.eye_fill, '$totalViews', '浏览'),
|
|
Container(
|
|
width: 1,
|
|
height: 30,
|
|
color: ext.textHint.withValues(alpha: 0.2),
|
|
),
|
|
_buildStatItem(ext, CupertinoIcons.star_fill, '$totalFavorites', '收藏'),
|
|
Container(
|
|
width: 1,
|
|
height: 30,
|
|
color: ext.textHint.withValues(alpha: 0.2),
|
|
),
|
|
_buildStatItem(ext, CupertinoIcons.flame_fill, '$streakDays', '连续天数'),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatItem(
|
|
AppThemeExtension ext,
|
|
IconData icon,
|
|
String value,
|
|
String label,
|
|
) {
|
|
return Column(
|
|
children: [
|
|
Icon(icon, size: 16, color: ext.accent),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
value,
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: AppTypography.caption2.copyWith(color: ext.textSecondary),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTrendCard(AppThemeExtension ext, LearningProgressState state) {
|
|
final trendData = state.trendData;
|
|
final points = <_TrendPoint>[];
|
|
|
|
if (trendData != null && trendData['points'] is List) {
|
|
final rawPoints = trendData['points'] as List;
|
|
for (int i = 0; i < rawPoints.length; i++) {
|
|
final item = rawPoints[i];
|
|
if (item is Map<String, dynamic>) {
|
|
points.add(
|
|
_TrendPoint(
|
|
(item['value'] as num?)?.toDouble() ?? 0.0,
|
|
item['label'] as String? ?? '',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (points.isEmpty) {
|
|
final now = DateTime.now();
|
|
for (int i = 6; i >= 0; i--) {
|
|
final day = now.subtract(Duration(days: i));
|
|
points.add(_TrendPoint(0, _weekDayLabel(day.weekday)));
|
|
}
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
child: GlassContainer(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.graph_square_fill,
|
|
size: 16,
|
|
color: ext.accent,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
'7天趋势',
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
SizedBox(
|
|
height: 180,
|
|
child: DeferredBuilder(
|
|
builder: (context) => SfCartesianChart(
|
|
plotAreaBorderWidth: 0,
|
|
margin: EdgeInsets.zero,
|
|
primaryXAxis: CategoryAxis(
|
|
majorGridLines: const MajorGridLines(width: 0),
|
|
majorTickLines: const MajorTickLines(size: 0),
|
|
axisLine: const AxisLine(width: 0),
|
|
labelStyle: AppTypography.caption2.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
primaryYAxis: NumericAxis(
|
|
majorGridLines: MajorGridLines(
|
|
width: 0.5,
|
|
color: ext.textHint.withValues(alpha: 0.1),
|
|
),
|
|
majorTickLines: const MajorTickLines(size: 0),
|
|
axisLine: const AxisLine(width: 0),
|
|
labelStyle: AppTypography.caption2.copyWith(
|
|
color: ext.textSecondary,
|
|
fontSize: 0,
|
|
),
|
|
minimum: 0,
|
|
),
|
|
tooltipBehavior: TooltipBehavior(
|
|
enable: true,
|
|
format: 'point.y',
|
|
),
|
|
series: [
|
|
AreaSeries<_TrendPoint, String>(
|
|
dataSource: points,
|
|
xValueMapper: (d, _) => d.label,
|
|
yValueMapper: (d, _) => d.value,
|
|
color: ext.accent,
|
|
borderColor: ext.accent,
|
|
borderWidth: 2.5,
|
|
animationDuration: 0,
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
ext.accent.withValues(alpha: 0.25),
|
|
ext.accent.withValues(alpha: 0.02),
|
|
],
|
|
),
|
|
markerSettings: MarkerSettings(
|
|
isVisible: true,
|
|
height: 6,
|
|
width: 6,
|
|
color: ext.accent,
|
|
borderColor: ext.bgPrimary,
|
|
borderWidth: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCategoryCard(
|
|
AppThemeExtension ext,
|
|
LearningProgressState state,
|
|
) {
|
|
final categoryData = state.categoryData;
|
|
var categories = <_CategoryItem>[];
|
|
|
|
if (categoryData != null && categoryData['categories'] is List) {
|
|
final rawList = categoryData['categories'] as List;
|
|
for (final item in rawList) {
|
|
if (item is Map<String, dynamic>) {
|
|
categories.add(
|
|
_CategoryItem(
|
|
name: item['name'] as String? ?? '',
|
|
value: (item['value'] as num?)?.toDouble() ?? 0.0,
|
|
color: _categoryColor(categories.length, ext),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (categories.isEmpty) {
|
|
categories = [
|
|
_CategoryItem(name: '诗词', value: 5, color: ext.accent),
|
|
_CategoryItem(name: '成语', value: 3, color: ext.accentLight),
|
|
const _CategoryItem(
|
|
name: '典故',
|
|
value: 2,
|
|
color: CupertinoColors.systemOrange,
|
|
),
|
|
const _CategoryItem(
|
|
name: '名言',
|
|
value: 4,
|
|
color: CupertinoColors.systemGreen,
|
|
),
|
|
];
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
child: GlassContainer(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.chart_bar_fill,
|
|
size: 16,
|
|
color: ext.accent,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
'分类统计',
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
SizedBox(
|
|
height: 180,
|
|
child: DeferredBuilder(
|
|
builder: (context) => SfCartesianChart(
|
|
plotAreaBorderWidth: 0,
|
|
margin: EdgeInsets.zero,
|
|
primaryXAxis: CategoryAxis(
|
|
majorGridLines: const MajorGridLines(width: 0),
|
|
majorTickLines: const MajorTickLines(size: 0),
|
|
axisLine: const AxisLine(width: 0),
|
|
labelStyle: AppTypography.caption2.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
primaryYAxis: const NumericAxis(
|
|
majorGridLines: MajorGridLines(width: 0),
|
|
majorTickLines: MajorTickLines(size: 0),
|
|
axisLine: AxisLine(width: 0),
|
|
labelStyle: TextStyle(fontSize: 0),
|
|
minimum: 0,
|
|
),
|
|
tooltipBehavior: TooltipBehavior(
|
|
enable: true,
|
|
),
|
|
series: [
|
|
ColumnSeries<_CategoryItem, String>(
|
|
dataSource: categories,
|
|
xValueMapper: (d, _) => d.name,
|
|
yValueMapper: (d, _) => d.value,
|
|
pointColorMapper: (d, _) => d.color,
|
|
width: 0.5,
|
|
animationDuration: 0,
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(AppRadius.md),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGoalSettingCard(
|
|
AppThemeExtension ext,
|
|
LearningProgressState state,
|
|
) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
child: GlassContainer(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(CupertinoIcons.flag_fill, size: 16, color: ext.accent),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
'目标设置',
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'每日学习目标',
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.sm,
|
|
vertical: AppSpacing.xs,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ext.accent.withValues(alpha: 0.12),
|
|
borderRadius: AppRadius.pillBorder,
|
|
),
|
|
child: Text(
|
|
'${state.dailyGoal} 篇',
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
CupertinoSlider(
|
|
value: state.dailyGoal.toDouble(),
|
|
min: 5,
|
|
max: 50,
|
|
divisions: 9,
|
|
activeColor: ext.accent,
|
|
onChanged: (value) {
|
|
ref
|
|
.read(learningProgressProvider.notifier)
|
|
.setDailyGoal(value.toInt());
|
|
},
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'5篇',
|
|
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
|
),
|
|
Text(
|
|
'50篇',
|
|
style: AppTypography.caption2.copyWith(color: ext.textHint),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
_buildProgressBar(ext, state),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProgressBar(AppThemeExtension ext, LearningProgressState state) {
|
|
final rate = state.goalCompletionRate.clamp(0.0, 1.0);
|
|
final isCompleted = rate >= 1.0;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'今日进度',
|
|
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
|
),
|
|
const Spacer(),
|
|
if (isCompleted)
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.checkmark_circle_fill,
|
|
size: 14,
|
|
color: CupertinoColors.systemGreen,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'已完成',
|
|
style: AppTypography.caption1.copyWith(
|
|
color: CupertinoColors.systemGreen,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
)
|
|
else
|
|
Text(
|
|
'${state.todayProgress}/${state.dailyGoal}',
|
|
style: AppTypography.caption1.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
ClipRRect(
|
|
borderRadius: AppRadius.pillBorder,
|
|
child: LinearProgressIndicator(
|
|
value: rate,
|
|
minHeight: 8,
|
|
backgroundColor: ext.accent.withValues(alpha: 0.12),
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
isCompleted ? CupertinoColors.systemGreen : ext.accent,
|
|
),
|
|
borderRadius: AppRadius.pillBorder,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _weekDayLabel(int weekday) {
|
|
return switch (weekday) {
|
|
1 => '周一',
|
|
2 => '周二',
|
|
3 => '周三',
|
|
4 => '周四',
|
|
5 => '周五',
|
|
6 => '周六',
|
|
7 => '周日',
|
|
_ => '',
|
|
};
|
|
}
|
|
|
|
Color _categoryColor(int index, AppThemeExtension ext) {
|
|
final colors = [
|
|
ext.accent,
|
|
ext.accentLight,
|
|
CupertinoColors.systemOrange,
|
|
CupertinoColors.systemGreen,
|
|
CupertinoColors.systemBlue,
|
|
CupertinoColors.systemPurple,
|
|
CupertinoColors.systemPink,
|
|
CupertinoColors.systemTeal,
|
|
];
|
|
return colors[index % colors.length];
|
|
}
|
|
}
|
|
|
|
class _CategoryItem {
|
|
const _CategoryItem({
|
|
required this.name,
|
|
required this.value,
|
|
required this.color,
|
|
});
|
|
|
|
final String name;
|
|
final double value;
|
|
final Color color;
|
|
}
|
|
|
|
class _RingData {
|
|
const _RingData(this.label, this.value, this.color);
|
|
final String label;
|
|
final double value;
|
|
final Color color;
|
|
}
|
|
|
|
class _TrendPoint {
|
|
const _TrendPoint(this.value, this.label);
|
|
final double value;
|
|
final String label;
|
|
}
|