// 月度概览页面 — 心情色彩月历 + 月度统计 + 精选日记 // 对齐 Open Design 原型稿 screens/monthly.html import 'package:flutter/material.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'; /// 月度概览页面 class MonthlyPage extends StatefulWidget { const MonthlyPage({super.key}); @override State createState() => _MonthlyPageState(); } class _MonthlyPageState extends State { late DateTime _focusedMonth; @override void initState() { super.initState(); _focusedMonth = DateTime.now(); } void _goToPreviousMonth() { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month - 1); }); } void _goToNextMonth() { setState(() { _focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1); }); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ // 月头部导航 _MonthHeader( month: _focusedMonth, onPrevious: _goToPreviousMonth, onNext: _goToNextMonth, ), // 可滚动内容区 Expanded( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 20), children: [ const SizedBox(height: 16), // 心情色彩月历 _MoodCalendar(month: _focusedMonth), const SizedBox(height: 20), // 月度统计 2x2 const _MonthSummary(), const SizedBox(height: 20), // 精选日记 const _Highlights(), const SizedBox(height: 32), ], ), ), ], ), ), ); } } // ===== 月头部导航 ===== class _MonthHeader extends StatelessWidget { const _MonthHeader({ required this.month, required this.onPrevious, required this.onNext, }); final DateTime month; final VoidCallback onPrevious; final VoidCallback onNext; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final title = '${month.year}年${month.month}月'; return Padding( padding: const EdgeInsets.fromLTRB(20, 8, 12, 0), child: Row( children: [ Expanded( child: Text( title, style: theme.textTheme.headlineSmall?.copyWith( fontFamily: AppTypography.displayFont, fontWeight: FontWeight.w700, ), ), ), _NavButton( icon: Icons.chevron_left_rounded, onTap: onPrevious, borderColor: colorScheme.outline, foregroundColor: colorScheme.onSurface, ), const SizedBox(width: 8), _NavButton( icon: Icons.chevron_right_rounded, onTap: onNext, borderColor: colorScheme.outline, foregroundColor: colorScheme.onSurface, ), ], ), ); } } /// 圆形导航按钮 (44px 触摸目标) class _NavButton extends StatelessWidget { const _NavButton({ required this.icon, required this.onTap, required this.borderColor, required this.foregroundColor, }); final IconData icon; final VoidCallback onTap; final Color borderColor; final Color foregroundColor; @override Widget build(BuildContext context) { return SizedBox( width: 44, height: 44, child: OutlinedButton( onPressed: onTap, style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, shape: const CircleBorder(), side: BorderSide(color: borderColor, width: 1.5), foregroundColor: foregroundColor, backgroundColor: Theme.of(context).colorScheme.surface, ), child: Icon(icon, size: 18), ), ); } } // ===== 心情色彩月历 ===== class _MoodCalendar extends StatelessWidget { const _MoodCalendar({required this.month}); final DateTime month; // 心情类型 static const _moodTypes = [ 'happy', 'calm', 'sad', 'tired', 'love', ]; // 心情 → emoji static const _moodEmojis = { 'happy': '😊', 'calm': '😐', 'sad': '😢', 'tired': '😐', 'love': '😡', }; // 心情 → 背景色 static const _moodBgColors = { 'happy': AppColors.secondarySoftLight, 'love': AppColors.roseSoftLight, 'calm': AppColors.tertiarySoftLight, 'sad': Color(0xFFD4DDE8), 'tired': Color(0xFFE8E4E0), }; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final now = DateTime.now(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: AppRadius.mdBorder, boxShadow: AppShadows.soft(context), border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ // 星期标题行 _WeekdayRow(colorScheme: colorScheme), const SizedBox(height: 8), // 7列网格 _buildGrid(context, now), ], ), ); } Widget _buildGrid(BuildContext context, DateTime now) { final firstDay = DateTime(month.year, month.month, 1); // 周日=0 → 偏移量; weekday 返回 1(周一)..7(周日) final startOffset = firstDay.weekday % 7; // 周日开头 final daysInMonth = DateTime(month.year, month.month + 1, 0).day; final cells = []; // 空白填充 for (var i = 0; i < startOffset; i++) { 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] ?? ''; cells.add( _MoodCell( day: d, emoji: emoji, bgColor: bgColor, isToday: isToday, ), ); } return GridView.count( crossAxisCount: 7, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 3, crossAxisSpacing: 3, childAspectRatio: 1, children: cells, ); } } /// 简单确定性伪随机数生成器(仅用于模拟数据) 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}); final ColorScheme colorScheme; @override Widget build(BuildContext context) { const weekdays = ['日', '一', '二', '三', '四', '五', '六']; return Row( children: weekdays.map((day) { return Expanded( child: Center( child: Text( day, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant, ), ), ), ); }).toList(), ); } } /// 单个心情格子 class _MoodCell extends StatelessWidget { const _MoodCell({ required this.day, required this.emoji, required this.bgColor, required this.isToday, }); final int day; final String emoji; final Color bgColor; final bool isToday; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: () { // TODO: 选择日期,跳转详情 }, child: Container( decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(6), border: isToday ? Border.all(color: AppColors.accent, width: 2) : null, ), alignment: Alignment.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$day', style: TextStyle( fontSize: 12, color: colorScheme.onSurface, fontWeight: isToday ? FontWeight.w700 : FontWeight.w400, ), ), const SizedBox(height: 1), Text( emoji, style: const TextStyle(fontSize: 10), ), ], ), ), ); } } // ===== 月度统计 2x2 ===== class _MonthSummary extends StatelessWidget { const _MonthSummary(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本月总结', style: theme.textTheme.headlineSmall?.copyWith( fontFamily: AppTypography.displayFont, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 16), GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.2, children: [ _StatCard( icon: '📝', value: '28', label: '日记篇数', bgColor: AppColors.tertiarySoftLight, valueColor: const Color(0xFFB8860B), ), _StatCard( icon: '🔥', value: '12', label: '最长连续', bgColor: AppColors.secondarySoftLight, valueColor: const Color(0xFF2D7D46), ), _StatCard( icon: '😊', value: '72%', label: '好心情占比', bgColor: AppColors.roseSoftLight, valueColor: const Color(0xFF9B4D4D), ), _StatCard( icon: '📸', value: '18', label: '照片数量', bgColor: const Color(0xFFD4DDE8), valueColor: const Color(0xFF4A6B8A), ), ], ), ], ); } } /// 单张统计小卡片 class _StatCard extends StatelessWidget { const _StatCard({ required this.icon, required this.value, required this.label, required this.bgColor, required this.valueColor, }); final String icon; final String value; final String label; final Color bgColor; final Color valueColor; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: bgColor, borderRadius: AppRadius.mdBorder, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(icon, style: const TextStyle(fontSize: 24)), const SizedBox(height: 8), Text( value, style: TextStyle( fontFamily: AppTypography.displayFont, fontSize: 24, fontWeight: FontWeight.w700, color: valueColor, ), ), const SizedBox(height: 2), Text( label, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ); } } // ===== 精选日记 ===== class _Highlights extends StatelessWidget { const _Highlights(); @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), ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本月精选', style: theme.textTheme.headlineSmall?.copyWith( fontFamily: AppTypography.displayFont, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 16), ...highlights.map((item) { return _HighlightCard( emoji: item.emoji, emojiBg: item.emojiBg, date: item.date, title: item.title, badge: item.badge, badgeBg: item.badgeBg, badgeFg: item.badgeFg, ); }), ], ); } } /// 单张精选日记卡片 class _HighlightCard extends StatelessWidget { const _HighlightCard({ required this.emoji, required this.emojiBg, required this.date, required this.title, required this.badge, required this.badgeBg, required this.badgeFg, }); final String emoji; final Color emojiBg; final String date; final String title; final String badge; final Color badgeBg; final Color badgeFg; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: AppRadius.mdBorder, boxShadow: AppShadows.soft(context), border: Border.all(color: colorScheme.outlineVariant), ), child: Row( children: [ // 48x48 emoji 圆圈 Container( width: 48, height: 48, decoration: BoxDecoration( color: emojiBg, shape: BoxShape.circle, ), alignment: Alignment.center, child: Text(emoji, style: const TextStyle(fontSize: 24)), ), const SizedBox(width: 12), // 标题 + 日期 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( date, style: theme.textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( title, style: theme.textTheme.titleMedium?.copyWith( fontFamily: AppTypography.displayFont, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // badge pill Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: badgeBg, borderRadius: AppRadius.pillBorder, ), child: Text( badge, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: badgeFg, ), ), ), ], ), ); } }