Files
nj/app/lib/features/mood/views/mood_page.dart
iven b320641d9c
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 全链路验证修复 — 编译错误/CORS/迁移/启动脚本
前端修复:
- calendar_page: 移除不存在的 JournalEntry.content getter
- responsive_scaffold: 移除不存在的 notchThickness 参数
- splash_page: SingleTickerProvider → TickerProvider (多 AnimationController)
- profile_page: UserRoleType.name → .code (修复运行时崩溃)
- 导入缺失的 user.dart

后端修复:
- class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞
- diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT

基础设施:
- config/default.toml: CORS 改为通配符(开发模式)
- scripts/dev.sh: 统一启动脚本(自动清理端口)
- docs/opendesign/: Open Design 设计规格 HTML 原型稿

验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
2026-06-02 01:03:58 +08:00

381 lines
11 KiB
Dart

// 心情页面 — 心情统计 + 趋势图 + 连续天数
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nuanji_app/core/theme/app_colors.dart';
import 'package:nuanji_app/core/theme/app_radius.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
import 'package:nuanji_app/data/remote/api_client.dart';
import '../bloc/mood_bloc.dart';
/// 心情页面 — 统计卡片 + 心情分布饼图 + 详情列表
class MoodPage extends StatefulWidget {
const MoodPage({super.key});
@override
State<MoodPage> createState() => _MoodPageState();
}
class _MoodPageState extends State<MoodPage> {
late final MoodBloc _bloc;
@override
void initState() {
super.initState();
_bloc = MoodBloc(api: context.read<ApiClient>());
_bloc.load();
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return ListenableBuilder(
listenable: _bloc,
builder: (context, _) {
final state = _bloc.state;
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: colorScheme.error),
const SizedBox(height: 12),
Text(state.errorMessage!, style: theme.textTheme.bodyMedium),
const SizedBox(height: 16),
FilledButton.tonal(
onPressed: _bloc.load,
child: const Text('重试'),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 统计概览卡片
_StatsOverviewCard(stats: state.stats, colorScheme: colorScheme),
const SizedBox(height: 16),
// 周期选择器
_PeriodSelector(
selectedPeriod: state.selectedPeriod,
onPeriodChanged: _bloc.changePeriod,
),
const SizedBox(height: 16),
// 心情分布饼图
_MoodDistributionChart(
moodCounts: state.stats.moodCounts,
colorScheme: colorScheme,
),
const SizedBox(height: 24),
// 心情详情列表
Text(
'心情详情',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...state.stats.moodCounts.map((mc) => _MoodCountTile(mc: mc)),
const SizedBox(height: 24),
// 连续天数鼓励卡片
_StreakCard(streakDays: state.stats.streakDays),
],
),
);
},
);
}
}
/// 统计概览卡片
class _StatsOverviewCard extends StatelessWidget {
const _StatsOverviewCard({
required this.stats,
required this.colorScheme,
});
final MoodStats stats;
final ColorScheme colorScheme;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dominantEmoji = stats.dominantMood != null
? _moodEmoji(stats.dominantMood!)
: '📝';
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorder,
),
color: colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
// 主导心情图标
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.primary.withValues(alpha: 0.15),
),
alignment: Alignment.center,
child: Text(dominantEmoji, style: const TextStyle(fontSize: 28)),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'心情概览',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${stats.totalJournals} 篇日记 · 连续 ${stats.streakDays}',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
),
),
],
),
),
);
}
}
/// 周期选择器
class _PeriodSelector extends StatelessWidget {
const _PeriodSelector({
required this.selectedPeriod,
required this.onPeriodChanged,
});
final StatsPeriod selectedPeriod;
final ValueChanged<StatsPeriod> onPeriodChanged;
@override
Widget build(BuildContext context) {
return SegmentedButton<StatsPeriod>(
segments: const [
ButtonSegment(value: StatsPeriod.week, label: Text('')),
ButtonSegment(value: StatsPeriod.month, label: Text('')),
ButtonSegment(value: StatsPeriod.quarter, label: Text('')),
],
selected: {selectedPeriod},
onSelectionChanged: (set) => onPeriodChanged(set.first),
);
}
}
/// 心情分布饼图
class _MoodDistributionChart extends StatelessWidget {
const _MoodDistributionChart({
required this.moodCounts,
required this.colorScheme,
});
final List<MoodCount> moodCounts;
final ColorScheme colorScheme;
@override
Widget build(BuildContext context) {
if (moodCounts.isEmpty) {
return const SizedBox.shrink();
}
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorder,
side: BorderSide(color: colorScheme.outlineVariant),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
height: 200,
child: PieChart(
PieChartData(
sections: moodCounts.map((mc) {
final color =
AppColors.moodColors[mc.mood.value] ?? colorScheme.primary;
return PieChartSectionData(
value: mc.count.toDouble(),
color: color,
radius: 50,
title: '${mc.percentage.toStringAsFixed(0)}%',
titleStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onPrimary,
),
);
}).toList(),
sectionsSpace: 2,
centerSpaceRadius: 40,
),
),
),
),
);
}
}
/// 心情计数列表项
class _MoodCountTile extends StatelessWidget {
const _MoodCountTile({required this.mc});
final MoodCount mc;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color =
AppColors.moodColors[mc.mood.value] ?? theme.colorScheme.primary;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Text(_moodEmoji(mc.mood), style: const TextStyle(fontSize: 20)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_moodLabel(mc.mood),
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: mc.percentage / 100,
backgroundColor: color.withValues(alpha: 0.15),
color: color,
minHeight: 6,
),
),
],
),
),
const SizedBox(width: 12),
SizedBox(
width: 48,
child: Text(
'${mc.count}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
textAlign: TextAlign.end,
),
),
],
),
);
}
}
/// 连续天数鼓励卡片
class _StreakCard extends StatelessWidget {
const _StreakCard({required this.streakDays});
final int streakDays;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: AppRadius.lgBorder,
),
color: AppColors.tertiary.withValues(alpha: 0.15),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Text('🔥', style: TextStyle(fontSize: 32)),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'连续 $streakDays',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
streakDays >= 7
? '太棒了!你已经坚持了一周 ✨'
: '继续加油,坚持就是胜利!',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
),
),
],
),
),
);
}
}
// ===== 辅助函数 =====
String _moodEmoji(Mood mood) => switch (mood) {
Mood.happy => '😊',
Mood.calm => '😌',
Mood.sad => '😢',
Mood.angry => '😠',
Mood.thinking => '🤔',
};
String _moodLabel(Mood mood) => switch (mood) {
Mood.happy => '开心',
Mood.calm => '平静',
Mood.sad => '难过',
Mood.angry => '生气',
Mood.thinking => '思考',
};