前端修复: - 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个页面全部渲染正常
313 lines
11 KiB
Dart
313 lines
11 KiB
Dart
// 首页 — 日记流 + 心情概览
|
|
|
|
import 'package:flutter/material.dart';
|
|
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/theme/app_typography.dart';
|
|
import 'package:nuanji_app/data/models/journal_entry.dart';
|
|
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
|
import '../bloc/home_bloc.dart';
|
|
|
|
/// 首页 — 展示最近日记流和心情概览
|
|
class HomePage extends StatelessWidget {
|
|
const HomePage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider(
|
|
create: (context) => HomeBloc(
|
|
journalRepository: context.read<JournalRepository>(),
|
|
)..add(const HomeLoadData()),
|
|
child: const _HomeView(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HomeView extends StatelessWidget {
|
|
const _HomeView();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return BlocBuilder<HomeBloc, HomeState>(
|
|
builder: (context, state) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(
|
|
'暖记',
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontFamily: AppTypography.handwrittenFont,
|
|
color: colorScheme.primary,
|
|
),
|
|
),
|
|
actions: [
|
|
// 搜索按钮(设计稿要求右上角圆形搜索按钮)
|
|
IconButton(
|
|
onPressed: () => context.push('/discover'),
|
|
icon: const Icon(Icons.search_rounded),
|
|
tooltip: '搜索',
|
|
),
|
|
IconButton(
|
|
onPressed: () => context.go('/stickers'),
|
|
icon: const Icon(Icons.emoji_emotions_outlined),
|
|
tooltip: '贴纸库',
|
|
),
|
|
IconButton(
|
|
onPressed: () => context.go('/templates'),
|
|
icon: const Icon(Icons.dashboard_customize_outlined),
|
|
tooltip: '模板',
|
|
),
|
|
],
|
|
),
|
|
body: state is HomeLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: state is HomeLoaded
|
|
? _buildContent(context, state)
|
|
: _buildContent(context, const HomeLoaded()),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(BuildContext context, HomeLoaded state) {
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
context.read<HomeBloc>().add(const HomeRefresh());
|
|
},
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 心情快速选择卡片
|
|
_QuickMoodCard(
|
|
hasTodayEntry: state.hasTodayEntry,
|
|
topMood: state.topMood,
|
|
streakDays: state.streakDays,
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// 最近日记
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'最近日记',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
TextButton(
|
|
onPressed: () => context.go('/calendar'),
|
|
child: const Text('查看全部'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
state.recentJournals.isEmpty
|
|
? const _EmptyJournalState()
|
|
: _JournalList(journals: state.recentJournals),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 心情快速选择卡片
|
|
class _QuickMoodCard extends StatelessWidget {
|
|
const _QuickMoodCard({
|
|
required this.hasTodayEntry,
|
|
this.topMood,
|
|
this.streakDays = 0,
|
|
});
|
|
|
|
final bool hasTodayEntry;
|
|
final Mood? topMood;
|
|
final int streakDays;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
final moods = [
|
|
('😊', '开心', Mood.happy),
|
|
('😌', '平静', Mood.calm),
|
|
('😢', '难过', Mood.sad),
|
|
('😠', '生气', Mood.angry),
|
|
('🤔', '思考', Mood.thinking),
|
|
];
|
|
|
|
return Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder),
|
|
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text('今天心情如何?', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
|
const Spacer(),
|
|
if (streakDays > 0)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.tertiary.withValues(alpha: 0.2),
|
|
borderRadius: AppRadius.xsBorder,
|
|
),
|
|
child: Text('🔥 连续 $streakDays 天', style: theme.textTheme.labelSmall),
|
|
),
|
|
if (hasTodayEntry)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.secondary.withValues(alpha: 0.2),
|
|
borderRadius: AppRadius.xsBorder,
|
|
),
|
|
child: Text('✅ 今日已写', style: theme.textTheme.labelSmall),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: moods.map((mood) {
|
|
final isTop = topMood == mood.$3;
|
|
return GestureDetector(
|
|
onTap: () => context.push('/editor'),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: (AppColors.moodColors[mood.$3.value] ?? colorScheme.primary)
|
|
.withValues(alpha: isTop ? 0.3 : 0.15),
|
|
border: isTop ? Border.all(color: AppColors.accent, width: 2) : null,
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(mood.$1, style: const TextStyle(fontSize: 22)),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(mood.$2, style: theme.textTheme.labelSmall?.copyWith(
|
|
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 日记列表
|
|
class _JournalList extends StatelessWidget {
|
|
const _JournalList({required this.journals});
|
|
final List<JournalEntry> journals;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Column(
|
|
children: journals.map((journal) {
|
|
final moodColor = AppColors.moodColors[journal.mood.value] ?? colorScheme.primary;
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: AppRadius.mdBorder,
|
|
side: BorderSide(color: colorScheme.outlineVariant),
|
|
),
|
|
child: InkWell(
|
|
onTap: () => context.push('/editor?id=${journal.id}'),
|
|
borderRadius: AppRadius.mdBorder,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: moodColor.withValues(alpha: 0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(_moodEmoji(journal.mood), style: const TextStyle(fontSize: 20)),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(journal.title, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
|
maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
const SizedBox(height: 4),
|
|
Text('${journal.date.month}月${journal.date.day}日',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurface.withValues(alpha: 0.5)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: colorScheme.onSurface.withValues(alpha: 0.3)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
}
|
|
|
|
String _moodEmoji(Mood mood) => switch (mood) {
|
|
Mood.happy => '😊', Mood.calm => '😌', Mood.sad => '😢',
|
|
Mood.angry => '😠', Mood.thinking => '🤔',
|
|
};
|
|
}
|
|
|
|
/// 空日记状态
|
|
class _EmptyJournalState extends StatelessWidget {
|
|
const _EmptyJournalState();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 48),
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.edit_note_rounded, size: 64, color: colorScheme.onSurface.withValues(alpha: 0.2)),
|
|
const SizedBox(height: 16),
|
|
Text('开始你的第一篇手账日记吧!',
|
|
style: theme.textTheme.bodyLarge?.copyWith(color: colorScheme.onSurface.withValues(alpha: 0.5))),
|
|
const SizedBox(height: 24),
|
|
FilledButton.icon(
|
|
onPressed: () => context.push('/editor'),
|
|
icon: const Icon(Icons.add_rounded),
|
|
label: const Text('写日记'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|