// 周概览页面 — 7天条目 + 统计卡片 + 每日日记卡片 // 对齐 Open Design 原型稿 screens/weekly.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 WeeklyPage extends StatefulWidget { const WeeklyPage({super.key}); @override State createState() => _WeeklyPageState(); } class _WeeklyPageState extends State { late DateTime _focusedWeekStart; @override void initState() { super.initState(); final now = DateTime.now(); _focusedWeekStart = now.subtract(Duration(days: now.weekday - 1)); } void _goToPreviousWeek() { setState(() { _focusedWeekStart = _focusedWeekStart.subtract(const Duration(days: 7)); }); } void _goToNextWeek() { setState(() { _focusedWeekStart = _focusedWeekStart.add(const Duration(days: 7)); }); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Scaffold( body: SafeArea( child: Column( children: [ // 周头部导航 _WeekHeader( weekStart: _focusedWeekStart, onPrevious: _goToPreviousWeek, onNext: _goToNextWeek, ), // 可滚动内容区 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), ], ), ), ], ), ), ); } List _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: '🍲', ), ]; } } // ===== 周头部导航 ===== class _WeekHeader extends StatelessWidget { const _WeekHeader({ required this.weekStart, required this.onPrevious, required this.onNext, }); final DateTime weekStart; final VoidCallback onPrevious; final VoidCallback onNext; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; // 格式化: "2026年6月 第1周" final monthNames = [ '', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', ]; final title = '${weekStart.year}年${monthNames[weekStart.month]} 第${_weekOfMonth(weekStart)}周'; 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, ), ], ), ); } /// 计算是当月第几周 int _weekOfMonth(DateTime date) { final firstDay = DateTime(date.year, date.month, 1); final offset = firstDay.weekday - 1; return ((date.day + offset) / 7).ceil(); } } /// 圆形导航按钮 (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), ), ); } } // ===== 7天条目 ===== class _WeekStrip extends StatelessWidget { const _WeekStrip({required this.weekStart}); final DateTime weekStart; // 模拟数据: 每天的心情 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 now = DateTime.now(); return Row( children: List.generate(7, (i) { final day = weekStart.add(Duration(days: i)); final isToday = day.year == now.year && day.month == now.month && day.day == now.day; final hasEntry = _hasEntry[i]; final moodEmoji = _mockMoods[i]; return Expanded( child: GestureDetector( onTap: () { // TODO: 选择某天后刷新下方日记卡片 }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: isToday ? AppColors.accent : null, borderRadius: AppRadius.mdBorder, ), child: Column( children: [ // 周名 Text( _weekNames[i], style: TextStyle( fontSize: 11, color: isToday ? const Color(0xFFFFF8F0).withValues(alpha: 0.85) : colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), // 日期数字 Text( '${day.day}', style: TextStyle( fontFamily: AppTypography.displayFont, fontSize: 20, fontWeight: FontWeight.w700, color: isToday ? const Color(0xFFFFF8F0) // accent-on : colorScheme.onSurface.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), // 心情 emoji Text(moodEmoji, style: const TextStyle(fontSize: 16)), // 有日记: 日期下方4px小圆点 if (hasEntry && !isToday) Container( width: 4, height: 4, margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.accent, ), ), if (hasEntry && isToday) Container( width: 4, height: 4, margin: const EdgeInsets.only(top: 4), decoration: const BoxDecoration( shape: BoxShape.circle, color: Color(0xFFFFF8F0), ), ), ], ), ), ), ); }), ); } } // ===== 本周总结卡片 ===== class _WeekSummary extends StatelessWidget { const _WeekSummary(); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: AppRadius.mdBorder, boxShadow: AppShadows.soft(context), border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '本周总结', style: theme.textTheme.titleMedium?.copyWith( fontFamily: AppTypography.displayFont, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), // 3个统计数字 Row( children: [ _SummaryItem( value: '6', label: '记录天数', valueColor: AppColors.accent, ), _SummaryItem( value: '7', label: '日记篇数', valueColor: AppColors.secondary, ), _SummaryItem( value: '12', 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), ), ), ), ], ), ], ), ); } } /// 单个统计项 class _SummaryItem extends StatelessWidget { const _SummaryItem({ required this.value, required this.label, required this.valueColor, }); final String value; final String label; final Color valueColor; @override Widget build(BuildContext context) { return Expanded( child: Column( children: [ Text( value, style: TextStyle( fontFamily: AppTypography.displayFont, fontSize: 24, fontWeight: FontWeight.w700, color: valueColor, ), ), const SizedBox(height: 2), Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ); } } // ===== 每日日记卡片 ===== class _DayCard extends StatelessWidget { const _DayCard({ required this.weekday, required this.date, required this.moodEmoji, required this.weatherEmoji, required this.body, required this.tags, this.photoEmoji, }); final String weekday; final String date; final String moodEmoji; final String weatherEmoji; final String body; final List<(String, Color, Color)> tags; // (label, bg, fg) final String? photoEmoji; @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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头部: 日期 + 心情/weather emoji Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '$weekday · $date', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface.withValues(alpha: 0.7), ), ), Text( '$moodEmoji $weatherEmoji', style: const TextStyle(fontSize: 13), ), ], ), const SizedBox(height: 12), // 正文预览 (3行截断) Text( body, style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, height: 1.6, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), // 标签 pills if (tags.isNotEmpty) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 4, children: tags.map((tag) { return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 3, ), decoration: BoxDecoration( color: tag.$2, borderRadius: AppRadius.pillBorder, ), child: Text( tag.$1, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: tag.$3, ), ), ); }).toList(), ), ], // 照片占位 if (photoEmoji != null) ...[ const SizedBox(height: 12), Container( width: double.infinity, height: 80, decoration: BoxDecoration( borderRadius: AppRadius.smBorder, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ colorScheme.surfaceContainerHighest .withValues(alpha: 0.6), colorScheme.outlineVariant.withValues(alpha: 0.3), ], ), ), alignment: Alignment.center, child: Text(photoEmoji!, style: const TextStyle(fontSize: 24)), ), ], ], ), ); } }