Files
nj/app/lib/features/calendar/views/weekly_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

574 lines
17 KiB
Dart

// 周概览页面 — 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<WeeklyPage> createState() => _WeeklyPageState();
}
class _WeeklyPageState extends State<WeeklyPage> {
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<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: '🍲',
),
];
}
}
// ===== 周头部导航 =====
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)),
),
],
],
),
);
}
}