前端修复: - 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个页面全部渲染正常
574 lines
17 KiB
Dart
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)),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|