Files
nj/app/lib/features/home/views/home_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

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('写日记'),
),
],
),
),
);
}
}