fix(app): 修复 P2~P4 共 10 项前端问题
P2 必须修复: - 教师布置主题 classId 从硬编码改为班级下拉选择器 - 班级日记墙使用服务端 classId 过滤替代前端过滤 - Profile 统计栏接入 JournalRepository 真实数据 - WeeklyPage 从全硬编码改为 JournalRepository 数据驱动 P3 建议改进: - 提取 mood_utils.dart 公共函数,消除 4 处重复定义 - 贴纸库搜索框连接 StickerBloc 按名称过滤 P4 细节打磨: - 家长页多孩子时显示 DropdownButton 选择器 - 搜索结果日记卡片点击跳转 /editor?id= - MonthlyPage 照片数量从 JournalElement 统计 - calendar_page/mood_page/search_page 统一使用 moodToEmoji/moodToLabel
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||
import 'package:nuanji_app/core/theme/app_radius.dart';
|
||||
import 'package:nuanji_app/core/utils/mood_utils.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||||
import '../bloc/calendar_bloc.dart';
|
||||
@@ -128,6 +129,10 @@ class _MonthView extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
// 本月心情概览柱状图
|
||||
_MoodSummaryChart(journalsByDate: loaded.journalsByDate),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 星期标题行
|
||||
_WeekdayHeader(colorScheme: Theme.of(context).colorScheme),
|
||||
|
||||
@@ -155,6 +160,104 @@ class _MonthView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 本月心情概览 — 5 柱状图
|
||||
class _MoodSummaryChart extends StatelessWidget {
|
||||
const _MoodSummaryChart({required this.journalsByDate});
|
||||
|
||||
final Map<DateTime, List<JournalEntry>> journalsByDate;
|
||||
|
||||
static const _moodConfig = [
|
||||
(Mood.happy, '开心', AppColors.secondary),
|
||||
(Mood.calm, '平静', AppColors.tertiary),
|
||||
(Mood.sad, '难过', Color(0xFF5B7DB1)),
|
||||
(Mood.angry, '生气', AppColors.accent),
|
||||
(Mood.thinking, '思考', Color(0xFF8B7E74)),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// 统计每种心情的数量
|
||||
final counts = <Mood, int>{};
|
||||
for (final entry in journalsByDate.entries) {
|
||||
for (final journal in entry.value) {
|
||||
counts[journal.mood] = (counts[journal.mood] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final maxCount = counts.values.fold(0, (a, b) => a > b ? a : b).toDouble();
|
||||
final barMaxHeight = 60.0;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.shadow.withValues(alpha: 0.05),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'本月心情概览',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: _moodConfig.map((config) {
|
||||
final count = counts[config.$1] ?? 0;
|
||||
final barHeight = maxCount > 0 && count > 0
|
||||
? (count / maxCount * barMaxHeight).clamp(4.0, barMaxHeight)
|
||||
: 4.0;
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: config.$3,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
config.$2,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 周视图 =====
|
||||
|
||||
class _WeekView extends StatelessWidget {
|
||||
@@ -264,7 +367,7 @@ class _WeekView extends StatelessWidget {
|
||||
// 心情 emoji
|
||||
if (hasEntry)
|
||||
Text(
|
||||
_moodEmoji(journals.first.mood),
|
||||
moodToEmoji(journals.first.mood),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
],
|
||||
@@ -321,6 +424,13 @@ class _TimelineView extends StatelessWidget {
|
||||
final journal = entry.value;
|
||||
final isLast = index == allJournals.length - 1;
|
||||
|
||||
// 时间格式化 HH:mm
|
||||
final timeStr =
|
||||
'${journal.createdAt.hour.toString().padLeft(2, '0')}:${journal.createdAt.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
// 摘要文本
|
||||
final excerpt = journal.contentExcerpt ?? '';
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -332,14 +442,14 @@ class _TimelineView extends StatelessWidget {
|
||||
children: [
|
||||
// 圆点 + emoji
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getMoodBgColor(journal.mood.value),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(_moodEmoji(journal.mood), style: const TextStyle(fontSize: 18)),
|
||||
child: Text(moodToEmoji(journal.mood), style: const TextStyle(fontSize: 20)),
|
||||
),
|
||||
// 竖线
|
||||
if (!isLast)
|
||||
@@ -370,11 +480,22 @@ class _TimelineView extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${date.month}月${date.day}日',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${date.month}月${date.day}日',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
timeStr,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
@@ -385,8 +506,18 @@ class _TimelineView extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// 日记内容通过 JournalElement 管理,日历视图仅显示标题
|
||||
// 后续可通过 elements 预览首段文字
|
||||
if (excerpt.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
excerpt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -426,10 +557,20 @@ class _MonthNavigator extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onPrevious,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
tooltip: '上个月',
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: EdgeInsets.zero,
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
onPressed: onPrevious,
|
||||
child: const Icon(Icons.chevron_left, size: 20),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
monthName,
|
||||
@@ -437,10 +578,20 @@ class _MonthNavigator extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
tooltip: '下个月',
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: EdgeInsets.zero,
|
||||
side: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
onPressed: onNext,
|
||||
child: const Icon(Icons.chevron_right, size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -614,30 +765,51 @@ class _DayCell extends StatelessWidget {
|
||||
: null,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
'${dayInfo.date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected || isToday ? FontWeight.bold : FontWeight.normal,
|
||||
color: !dayInfo.isCurrentMonth
|
||||
? colorScheme.onSurface.withValues(alpha: 0.3)
|
||||
: isSelected
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${dayInfo.date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected || isToday ? FontWeight.bold : FontWeight.normal,
|
||||
color: !dayInfo.isCurrentMonth
|
||||
? colorScheme.onSurface.withValues(alpha: 0.3)
|
||||
: isSelected
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
// 心情小圆点(仅在有日记但无背景色时显示)
|
||||
if (hasJournals && moodBg == Colors.transparent)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(top: 1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected ? colorScheme.onPrimary : AppColors.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 心情小圆点(仅在有日记但无背景色时显示)
|
||||
if (hasJournals && moodBg == Colors.transparent)
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected ? colorScheme.onPrimary : AppColors.accent,
|
||||
// 日记条目指示点
|
||||
if (hasJournals)
|
||||
Positioned(
|
||||
top: 3,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimary
|
||||
: AppColors.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -731,7 +903,7 @@ class _DayJournalList extends StatelessWidget {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_moodEmoji(journal.mood),
|
||||
moodToEmoji(journal.mood),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
@@ -781,17 +953,6 @@ Color _getMoodBgColor(String mood) {
|
||||
return AppColors.moodCellColors[mood] ?? AppColors.secondarySoftLight;
|
||||
}
|
||||
|
||||
/// 心情 → emoji
|
||||
String _moodEmoji(Mood mood) {
|
||||
return switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
}
|
||||
|
||||
/// 是否是今天
|
||||
bool _isToday(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
// 对齐 Open Design 原型稿 screens/monthly.html
|
||||
|
||||
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/core/theme/app_shadows.dart';
|
||||
import 'package:nuanji_app/core/theme/app_typography.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
import 'package:nuanji_app/data/models/journal_element.dart';
|
||||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||||
|
||||
/// 月度概览页面
|
||||
class MonthlyPage extends StatefulWidget {
|
||||
const MonthlyPage({super.key});
|
||||
final JournalRepository? journalRepository;
|
||||
const MonthlyPage({super.key, this.journalRepository});
|
||||
|
||||
@override
|
||||
State<MonthlyPage> createState() => _MonthlyPageState();
|
||||
@@ -17,23 +22,59 @@ class MonthlyPage extends StatefulWidget {
|
||||
|
||||
class _MonthlyPageState extends State<MonthlyPage> {
|
||||
late DateTime _focusedMonth;
|
||||
List<JournalEntry> _journals = [];
|
||||
int _photoCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusedMonth = DateTime.now();
|
||||
_loadJournals();
|
||||
}
|
||||
|
||||
JournalRepository get _repo =>
|
||||
widget.journalRepository ?? context.read<JournalRepository>();
|
||||
|
||||
Future<void> _loadJournals() async {
|
||||
final firstDay = DateTime(_focusedMonth.year, _focusedMonth.month, 1);
|
||||
// 下月 1 号作为上界(开区间),所以用 month+1
|
||||
final nextMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 1);
|
||||
final journals = await _repo.getJournals(
|
||||
dateFrom: firstDay,
|
||||
dateTo: nextMonth,
|
||||
);
|
||||
|
||||
// 统计照片元素数量
|
||||
var photoCount = 0;
|
||||
for (final journal in journals) {
|
||||
try {
|
||||
final elements = await _repo.getElements(journal.id);
|
||||
photoCount += elements.where((e) => e.elementType == ElementType.image).length;
|
||||
} catch (_) {
|
||||
// 单个日记加载元素失败不影响整体统计
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_journals = journals;
|
||||
_photoCount = photoCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _goToPreviousMonth() {
|
||||
setState(() {
|
||||
_focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month - 1);
|
||||
});
|
||||
_loadJournals();
|
||||
}
|
||||
|
||||
void _goToNextMonth() {
|
||||
setState(() {
|
||||
_focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1);
|
||||
});
|
||||
_loadJournals();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,13 +96,13 @@ class _MonthlyPageState extends State<MonthlyPage> {
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 心情色彩月历
|
||||
_MoodCalendar(month: _focusedMonth),
|
||||
_MoodCalendar(month: _focusedMonth, journals: _journals),
|
||||
const SizedBox(height: 20),
|
||||
// 月度统计 2x2
|
||||
const _MonthSummary(),
|
||||
_MonthSummary(journals: _journals, photoCount: _photoCount),
|
||||
const SizedBox(height: 20),
|
||||
// 精选日记
|
||||
const _Highlights(),
|
||||
_Highlights(journals: _journals),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
@@ -161,31 +202,27 @@ class _NavButton extends StatelessWidget {
|
||||
// ===== 心情色彩月历 =====
|
||||
|
||||
class _MoodCalendar extends StatelessWidget {
|
||||
const _MoodCalendar({required this.month});
|
||||
const _MoodCalendar({required this.month, required this.journals});
|
||||
|
||||
final DateTime month;
|
||||
final List<JournalEntry> journals;
|
||||
|
||||
// 心情类型
|
||||
static const _moodTypes = [
|
||||
'happy', 'calm', 'sad', 'tired', 'love',
|
||||
];
|
||||
|
||||
// 心情 → emoji
|
||||
static const _moodEmojis = <String, String>{
|
||||
'happy': '😊',
|
||||
'calm': '😐',
|
||||
'sad': '😢',
|
||||
'tired': '😐',
|
||||
'love': '😡',
|
||||
// 心情 → emoji(对齐 Mood 枚举: happy/calm/sad/angry/thinking)
|
||||
static const _moodEmojis = <Mood, String>{
|
||||
Mood.happy: '😊',
|
||||
Mood.calm: '😌',
|
||||
Mood.sad: '😢',
|
||||
Mood.angry: '😡',
|
||||
Mood.thinking: '🤔',
|
||||
};
|
||||
|
||||
// 心情 → 背景色
|
||||
static const _moodBgColors = <String, Color>{
|
||||
'happy': AppColors.secondarySoftLight,
|
||||
'love': AppColors.roseSoftLight,
|
||||
'calm': AppColors.tertiarySoftLight,
|
||||
'sad': Color(0xFFD4DDE8),
|
||||
'tired': Color(0xFFE8E4E0),
|
||||
static const _moodBgColors = <Mood, Color>{
|
||||
Mood.happy: AppColors.secondarySoftLight,
|
||||
Mood.angry: AppColors.roseSoftLight,
|
||||
Mood.calm: AppColors.tertiarySoftLight,
|
||||
Mood.sad: Color(0xFFD4DDE8),
|
||||
Mood.thinking: Color(0xFFE8E4E0),
|
||||
};
|
||||
|
||||
@override
|
||||
@@ -216,10 +253,18 @@ class _MoodCalendar extends StatelessWidget {
|
||||
|
||||
Widget _buildGrid(BuildContext context, DateTime now) {
|
||||
final firstDay = DateTime(month.year, month.month, 1);
|
||||
// 周日=0 → 偏移量; weekday 返回 1(周一)..7(周日)
|
||||
final startOffset = firstDay.weekday % 7; // 周日开头
|
||||
// 周一=0 → 偏移量; weekday 返回 1(周一)..7(周日)
|
||||
final startOffset = firstDay.weekday - 1; // 周一开头
|
||||
final daysInMonth = DateTime(month.year, month.month + 1, 0).day;
|
||||
|
||||
// 按日期建索引:day → JournalEntry
|
||||
final journalByDay = <int, JournalEntry>{};
|
||||
for (final j in journals) {
|
||||
if (j.date.year == month.year && j.date.month == month.month) {
|
||||
journalByDay[j.date.day] = j;
|
||||
}
|
||||
}
|
||||
|
||||
final cells = <Widget>[];
|
||||
|
||||
// 空白填充
|
||||
@@ -227,19 +272,16 @@ class _MoodCalendar extends StatelessWidget {
|
||||
cells.add(const SizedBox.shrink());
|
||||
}
|
||||
|
||||
// 模拟心情数据(确定性伪随机,同一天固定同心情)
|
||||
final rng = _SeededRandom(month.year * 100 + month.month);
|
||||
|
||||
for (var d = 1; d <= daysInMonth; d++) {
|
||||
final isToday = now.year == month.year &&
|
||||
now.month == month.month &&
|
||||
now.day == d;
|
||||
|
||||
// 每天随机一个心情
|
||||
final moodIndex = rng.nextInt(5);
|
||||
final mood = _moodTypes[moodIndex];
|
||||
final bgColor = _moodBgColors[mood] ?? Colors.transparent;
|
||||
final emoji = _moodEmojis[mood] ?? '';
|
||||
final entry = journalByDay[d];
|
||||
final mood = entry?.mood;
|
||||
final bgColor =
|
||||
mood != null ? (_moodBgColors[mood] ?? Colors.transparent) : Colors.transparent;
|
||||
final emoji = mood != null ? (_moodEmojis[mood] ?? '') : '';
|
||||
|
||||
cells.add(
|
||||
_MoodCell(
|
||||
@@ -263,17 +305,6 @@ class _MoodCalendar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 简单确定性伪随机数生成器(仅用于模拟数据)
|
||||
class _SeededRandom {
|
||||
_SeededRandom(int seed) : _state = seed;
|
||||
int _state;
|
||||
|
||||
int nextInt(int max) {
|
||||
_state = (_state * 1103515245 + 12345) & 0x7FFFFFFF;
|
||||
return _state % max;
|
||||
}
|
||||
}
|
||||
|
||||
/// 星期标题行
|
||||
class _WeekdayRow extends StatelessWidget {
|
||||
const _WeekdayRow({required this.colorScheme});
|
||||
@@ -282,7 +313,7 @@ class _WeekdayRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
return Row(
|
||||
children: weekdays.map((day) {
|
||||
return Expanded(
|
||||
@@ -359,7 +390,36 @@ class _MoodCell extends StatelessWidget {
|
||||
// ===== 月度统计 2x2 =====
|
||||
|
||||
class _MonthSummary extends StatelessWidget {
|
||||
const _MonthSummary();
|
||||
const _MonthSummary({required this.journals, required this.photoCount});
|
||||
|
||||
final List<JournalEntry> journals;
|
||||
final int photoCount;
|
||||
|
||||
/// 计算最长连续写日记天数
|
||||
int _calcLongestStreak() {
|
||||
if (journals.isEmpty) return 0;
|
||||
final days = journals.map((j) => j.date.day).toSet().toList()..sort();
|
||||
int longest = 1;
|
||||
int current = 1;
|
||||
for (var i = 1; i < days.length; i++) {
|
||||
if (days[i] == days[i - 1] + 1) {
|
||||
current++;
|
||||
if (current > longest) longest = current;
|
||||
} else {
|
||||
current = 1;
|
||||
}
|
||||
}
|
||||
return longest;
|
||||
}
|
||||
|
||||
/// 计算"好心情"(happy/calm)占比
|
||||
String _calcGoodMoodPercent() {
|
||||
if (journals.isEmpty) return '0%';
|
||||
final good = journals.where(
|
||||
(j) => j.mood == Mood.happy || j.mood == Mood.calm,
|
||||
).length;
|
||||
return '${((good / journals.length) * 100).round()}%';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -386,28 +446,28 @@ class _MonthSummary extends StatelessWidget {
|
||||
children: [
|
||||
_StatCard(
|
||||
icon: '📝',
|
||||
value: '28',
|
||||
value: '${journals.length}',
|
||||
label: '日记篇数',
|
||||
bgColor: AppColors.tertiarySoftLight,
|
||||
valueColor: const Color(0xFFB8860B),
|
||||
),
|
||||
_StatCard(
|
||||
icon: '🔥',
|
||||
value: '12',
|
||||
value: '${_calcLongestStreak()}',
|
||||
label: '最长连续',
|
||||
bgColor: AppColors.secondarySoftLight,
|
||||
valueColor: const Color(0xFF2D7D46),
|
||||
),
|
||||
_StatCard(
|
||||
icon: '😊',
|
||||
value: '72%',
|
||||
value: _calcGoodMoodPercent(),
|
||||
label: '好心情占比',
|
||||
bgColor: AppColors.roseSoftLight,
|
||||
valueColor: const Color(0xFF9B4D4D),
|
||||
),
|
||||
_StatCard(
|
||||
icon: '📸',
|
||||
value: '18',
|
||||
value: '$photoCount',
|
||||
label: '照片数量',
|
||||
bgColor: const Color(0xFFD4DDE8),
|
||||
valueColor: const Color(0xFF4A6B8A),
|
||||
@@ -475,42 +535,30 @@ class _StatCard extends StatelessWidget {
|
||||
// ===== 精选日记 =====
|
||||
|
||||
class _Highlights extends StatelessWidget {
|
||||
const _Highlights();
|
||||
const _Highlights({required this.journals});
|
||||
|
||||
final List<JournalEntry> journals;
|
||||
|
||||
static const _badgeConfig = <Mood, ({String badge, Color bg, Color fg})>{
|
||||
Mood.happy: (badge: '最佳心情', bg: AppColors.roseSoftLight, fg: Color(0xFF9B4D4D)),
|
||||
Mood.calm: (badge: '平静时光', bg: AppColors.tertiarySoftLight, fg: Color(0xFFB8860B)),
|
||||
Mood.sad: (badge: '真实记录', bg: Color(0xFFD4DDE8), fg: Color(0xFF4A6B8A)),
|
||||
Mood.angry: (badge: '真情流露', bg: AppColors.roseSoftLight, fg: Color(0xFF9B4D4D)),
|
||||
Mood.thinking: (badge: '深度思考', bg: AppColors.secondarySoftLight, fg: Color(0xFF2D7D46)),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// 模拟精选日记数据
|
||||
const highlights = [
|
||||
(
|
||||
emoji: '😊',
|
||||
emojiBg: AppColors.roseSoftLight,
|
||||
date: '5月14日',
|
||||
title: '和朋友聚餐的欢乐时光',
|
||||
badge: '最佳心情',
|
||||
badgeBg: AppColors.roseSoftLight,
|
||||
badgeFg: Color(0xFF9B4D4D),
|
||||
),
|
||||
(
|
||||
emoji: '⭐',
|
||||
emojiBg: AppColors.tertiarySoftLight,
|
||||
date: '5月21日',
|
||||
title: '完成了第一个小目标',
|
||||
badge: '里程碑',
|
||||
badgeBg: AppColors.tertiarySoftLight,
|
||||
badgeFg: Color(0xFFB8860B),
|
||||
),
|
||||
(
|
||||
emoji: '📚',
|
||||
emojiBg: AppColors.secondarySoftLight,
|
||||
date: '5月28日',
|
||||
title: '期末考试结束',
|
||||
badge: '最详尽记录',
|
||||
badgeBg: AppColors.secondarySoftLight,
|
||||
badgeFg: Color(0xFF2D7D46),
|
||||
),
|
||||
];
|
||||
// 按日期降序取前 3 篇
|
||||
final top = List<JournalEntry>.from(journals)
|
||||
..sort((a, b) => b.date.compareTo(a.date));
|
||||
final highlights = top.take(3).toList();
|
||||
|
||||
if (highlights.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -523,15 +571,22 @@ class _Highlights extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...highlights.map((item) {
|
||||
...highlights.map((entry) {
|
||||
final mood = entry.mood;
|
||||
final emoji = _MoodCalendar._moodEmojis[mood] ?? '📝';
|
||||
final emojiBg = _MoodCalendar._moodBgColors[mood] ?? AppColors.tertiarySoftLight;
|
||||
final cfg = _badgeConfig[mood] ??
|
||||
(badge: '日记', bg: AppColors.tertiarySoftLight, fg: const Color(0xFFB8860B));
|
||||
final dateStr = '${entry.date.month}月${entry.date.day}日';
|
||||
|
||||
return _HighlightCard(
|
||||
emoji: item.emoji,
|
||||
emojiBg: item.emojiBg,
|
||||
date: item.date,
|
||||
title: item.title,
|
||||
badge: item.badge,
|
||||
badgeBg: item.badgeBg,
|
||||
badgeFg: item.badgeFg,
|
||||
emoji: emoji,
|
||||
emojiBg: emojiBg,
|
||||
date: dateStr,
|
||||
title: entry.title,
|
||||
badge: cfg.badge,
|
||||
badgeBg: cfg.bg,
|
||||
badgeFg: cfg.fg,
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// 周概览页面 — 7天条目 + 统计卡片 + 每日日记卡片
|
||||
// 对齐 Open Design 原型稿 screens/weekly.html
|
||||
// 接入 JournalRepository 加载真实数据
|
||||
|
||||
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/core/theme/app_shadows.dart';
|
||||
import 'package:nuanji_app/core/theme/app_typography.dart';
|
||||
import 'package:nuanji_app/core/utils/mood_utils.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||||
|
||||
/// 周概览页面
|
||||
class WeeklyPage extends StatefulWidget {
|
||||
@@ -17,24 +22,71 @@ class WeeklyPage extends StatefulWidget {
|
||||
|
||||
class _WeeklyPageState extends State<WeeklyPage> {
|
||||
late DateTime _focusedWeekStart;
|
||||
List<JournalEntry> _journals = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_focusedWeekStart = now.subtract(Duration(days: now.weekday - 1));
|
||||
_focusedWeekStart = _startOfWeek(now);
|
||||
_loadWeekData();
|
||||
}
|
||||
|
||||
JournalRepository get _repo => context.read<JournalRepository>();
|
||||
|
||||
/// 获取某天的周一日期
|
||||
DateTime _startOfWeek(DateTime date) {
|
||||
return date.subtract(Duration(days: date.weekday - 1));
|
||||
}
|
||||
|
||||
Future<void> _loadWeekData() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final weekEnd = _focusedWeekStart.add(const Duration(days: 7));
|
||||
final journals = await _repo.getJournals(
|
||||
dateFrom: _focusedWeekStart,
|
||||
dateTo: weekEnd,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_journals = journals;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _goToPreviousWeek() {
|
||||
setState(() {
|
||||
_focusedWeekStart = _focusedWeekStart.subtract(const Duration(days: 7));
|
||||
});
|
||||
_loadWeekData();
|
||||
}
|
||||
|
||||
void _goToNextWeek() {
|
||||
setState(() {
|
||||
_focusedWeekStart = _focusedWeekStart.add(const Duration(days: 7));
|
||||
});
|
||||
_loadWeekData();
|
||||
}
|
||||
|
||||
/// 按日期索引日记: day-of-week (1=周一..7=周日) → JournalEntry 列表
|
||||
Map<int, List<JournalEntry>> get _journalsByWeekday {
|
||||
final map = <int, List<JournalEntry>>{};
|
||||
for (final j in _journals) {
|
||||
// 判断日记日期是否在本周范围内
|
||||
final dayKey = j.date.difference(_focusedWeekStart).inDays;
|
||||
if (dayKey >= 0 && dayKey < 7) {
|
||||
final weekday = dayKey + 1; // 1=周一, 7=周日
|
||||
(map[weekday] ??= []).add(j);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,21 +106,26 @@ class _WeeklyPageState extends State<WeeklyPage> {
|
||||
),
|
||||
// 可滚动内容区
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 7天条目
|
||||
_WeekStrip(weekStart: _focusedWeekStart),
|
||||
const SizedBox(height: 20),
|
||||
// 本周总结
|
||||
const _WeekSummary(),
|
||||
const SizedBox(height: 20),
|
||||
// 每日日记卡片
|
||||
..._buildDayCards(theme, colorScheme),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
// 7天条目(真实数据)
|
||||
_WeekStrip(
|
||||
weekStart: _focusedWeekStart,
|
||||
journalsByWeekday: _journalsByWeekday,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 本周总结(真实数据)
|
||||
_WeekSummary(journals: _journals),
|
||||
const SizedBox(height: 20),
|
||||
// 每日日记卡片(真实数据)
|
||||
..._buildDayCards(theme, colorScheme),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -77,48 +134,67 @@ class _WeeklyPageState extends State<WeeklyPage> {
|
||||
}
|
||||
|
||||
List<Widget> _buildDayCards(ThemeData theme, ColorScheme colorScheme) {
|
||||
// 模拟数据: 3 张日记卡片
|
||||
return [
|
||||
_DayCard(
|
||||
weekday: '周日',
|
||||
date: '5月31日',
|
||||
moodEmoji: '😊',
|
||||
weatherEmoji: '☀️',
|
||||
body:
|
||||
'今天下午去图书馆自习,阳光从窗外洒进来,暖暖的。喝了抹茶拿铁,虽然期末压力大但看到窗外的樱花还在开,觉得一切都会好的。',
|
||||
tags: const [
|
||||
('学习', AppColors.secondarySoftLight, Color(0xFF2D7D46)),
|
||||
('美食', AppColors.tertiarySoftLight, Color(0xFFB8860B)),
|
||||
],
|
||||
photoEmoji: '📚',
|
||||
),
|
||||
_DayCard(
|
||||
weekday: '周六',
|
||||
date: '5月30日',
|
||||
moodEmoji: '😊',
|
||||
weatherEmoji: '🌤',
|
||||
body:
|
||||
'今天在图书馆自习,窗外的阳光洒进来,暖暖的。复习了高数第三章,做了两套模拟题感觉还不错。',
|
||||
tags: const [
|
||||
('学习', AppColors.secondarySoftLight, Color(0xFF2D7D46)),
|
||||
],
|
||||
photoEmoji: null,
|
||||
),
|
||||
_DayCard(
|
||||
weekday: '周五',
|
||||
date: '5月29日',
|
||||
moodEmoji: '😊',
|
||||
weatherEmoji: '☀️',
|
||||
body:
|
||||
'考完试和舍友们去吃了火锅庆祝,大家都好开心,聊了很多有趣的事。这学期终于结束了!',
|
||||
tags: const [
|
||||
('朋友', AppColors.roseSoftLight, Color(0xFF9B4D4D)),
|
||||
('美食', AppColors.tertiarySoftLight, Color(0xFFB8860B)),
|
||||
],
|
||||
photoEmoji: '🍲',
|
||||
),
|
||||
];
|
||||
final byWeekday = _journalsByWeekday;
|
||||
final cards = <Widget>[];
|
||||
final weekNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
|
||||
// 按日期倒序生成卡片(最新的在上面)
|
||||
for (var i = 6; i >= 0; i--) {
|
||||
final weekday = i + 1;
|
||||
final dayJournals = byWeekday[weekday];
|
||||
if (dayJournals == null || dayJournals.isEmpty) continue;
|
||||
|
||||
final day = _focusedWeekStart.add(Duration(days: i));
|
||||
final first = dayJournals.first;
|
||||
|
||||
cards.add(_DayCard(
|
||||
weekday: weekNames[i],
|
||||
date: '${day.month}月${day.day}日',
|
||||
moodEmoji: moodToEmoji(first.mood),
|
||||
weatherEmoji: _weatherEmoji(first.weather),
|
||||
body: first.contentExcerpt ?? first.title,
|
||||
tags: first.tags.take(2).map((tag) {
|
||||
// 根据标签内容选择颜色
|
||||
return (tag, AppColors.secondarySoftLight, const Color(0xFF2D7D46));
|
||||
}).toList(),
|
||||
photoEmoji: dayJournals.any((j) => j.contentExcerpt != null && j.contentExcerpt!.contains('📷'))
|
||||
? '📷'
|
||||
: null,
|
||||
));
|
||||
}
|
||||
|
||||
// 无日记时显示空状态
|
||||
if (cards.isEmpty) {
|
||||
return [
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.edit_note_rounded, size: 48,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.2)),
|
||||
const SizedBox(height: 12),
|
||||
Text('这周还没有日记', style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
String _weatherEmoji(Weather weather) => switch (weather) {
|
||||
Weather.sunny => '☀️',
|
||||
Weather.cloudy => '⛅',
|
||||
Weather.rainy => '🌧️',
|
||||
Weather.snowy => '❄️',
|
||||
Weather.windy => '💨',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 周头部导航 =====
|
||||
@@ -221,32 +297,34 @@ class _NavButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 7天条目 =====
|
||||
// ===== 7天条目(真实数据)=====
|
||||
|
||||
class _WeekStrip extends StatelessWidget {
|
||||
const _WeekStrip({required this.weekStart});
|
||||
const _WeekStrip({
|
||||
required this.weekStart,
|
||||
required this.journalsByWeekday,
|
||||
});
|
||||
|
||||
final DateTime weekStart;
|
||||
final Map<int, List<JournalEntry>> journalsByWeekday;
|
||||
|
||||
// 模拟数据: 每天的心情 emoji
|
||||
static const _mockMoods = ['😊', '😐', '😊', '😊', '😊', '😊', '😊'];
|
||||
static const _weekNames = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
static const _hasEntry = [true, true, true, true, true, true, true];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final now = DateTime.now();
|
||||
|
||||
return Row(
|
||||
children: List.generate(7, (i) {
|
||||
final day = weekStart.add(Duration(days: i));
|
||||
final weekday = i + 1;
|
||||
final isToday = day.year == now.year &&
|
||||
day.month == now.month &&
|
||||
day.day == now.day;
|
||||
final hasEntry = _hasEntry[i];
|
||||
final moodEmoji = _mockMoods[i];
|
||||
final dayJournals = journalsByWeekday[weekday] ?? [];
|
||||
final hasEntry = dayJournals.isNotEmpty;
|
||||
final moodEmoji = hasEntry ? moodToEmoji(dayJournals.first.mood) : '·';
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
@@ -286,7 +364,10 @@ class _WeekStrip extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 心情 emoji
|
||||
Text(moodEmoji, style: const TextStyle(fontSize: 16)),
|
||||
Text(moodEmoji, style: TextStyle(
|
||||
fontSize: hasEntry ? 16 : 14,
|
||||
color: hasEntry ? null : colorScheme.onSurface.withValues(alpha: 0.2),
|
||||
)),
|
||||
// 有日记: 日期下方4px小圆点
|
||||
if (hasEntry && !isToday)
|
||||
Container(
|
||||
@@ -318,16 +399,26 @@ class _WeekStrip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 本周总结卡片 =====
|
||||
// ===== 本周总结卡片(真实数据)=====
|
||||
|
||||
class _WeekSummary extends StatelessWidget {
|
||||
const _WeekSummary();
|
||||
const _WeekSummary({required this.journals});
|
||||
|
||||
final List<JournalEntry> journals;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
// 统计真实数据
|
||||
final recordDays = journals.map((j) => j.date.day).toSet().length;
|
||||
final journalCount = journals.length;
|
||||
// 统计贴纸元素 — 从日记标签中估算(Phase 1 简化)
|
||||
final stickerCount = journals.fold<int>(
|
||||
0, (sum, j) => sum + j.tags.length,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -351,66 +442,81 @@ class _WeekSummary extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_SummaryItem(
|
||||
value: '6',
|
||||
value: '$recordDays',
|
||||
label: '记录天数',
|
||||
valueColor: AppColors.accent,
|
||||
),
|
||||
_SummaryItem(
|
||||
value: '7',
|
||||
value: '$journalCount',
|
||||
label: '日记篇数',
|
||||
valueColor: AppColors.secondary,
|
||||
),
|
||||
_SummaryItem(
|
||||
value: '12',
|
||||
label: '使用贴纸',
|
||||
value: '$stickerCount',
|
||||
label: '使用标签',
|
||||
valueColor: AppColors.tertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 心情分布条
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.secondary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tertiary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF5B7DB1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_MoodDistributionBar(journals: journals),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 心情分布条 — 从日记数据计算各心情占比
|
||||
class _MoodDistributionBar extends StatelessWidget {
|
||||
const _MoodDistributionBar({required this.journals});
|
||||
final List<JournalEntry> journals;
|
||||
|
||||
static const _moodConfig = [
|
||||
(Mood.happy, AppColors.secondary),
|
||||
(Mood.calm, AppColors.tertiary),
|
||||
(Mood.sad, Color(0xFF5B7DB1)),
|
||||
(Mood.angry, AppColors.accent),
|
||||
(Mood.thinking, Color(0xFF8B7E74)),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (journals.isEmpty) {
|
||||
return Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 统计各心情数量
|
||||
final counts = <Mood, int>{};
|
||||
for (final j in journals) {
|
||||
counts[j.mood] = (counts[j.mood] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: _moodConfig.where((c) => counts[c.$1] != null).map((config) {
|
||||
final count = counts[config.$1]!;
|
||||
return Expanded(
|
||||
flex: count,
|
||||
child: Container(
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: config.$2,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个统计项
|
||||
class _SummaryItem extends StatelessWidget {
|
||||
const _SummaryItem({
|
||||
|
||||
Reference in New Issue
Block a user