feat(app): BLoC 集成 Repository + SettingsBloc 主题切换
全局依赖注入: - app.dart 注入 JournalRepository + ClassRepository + SettingsBloc - ApiClient token 自动注入(监听 AuthBloc 状态) BLoC 重构 (占位数据 → Repository): - CalendarBloc: 通过 JournalRepository 加载月度日记 - ClassBloc: 通过 ClassRepository + JournalRepository 加载班级数据 - 新增 ClassJoin 事件支持班级码加入 - HomeBloc: 加载最近日记 + 心情概览 + 连续天数 + 今日是否已写 设置系统: - SettingsBloc: ThemeMode 切换 (system/light/dark) - app.dart 通过 ListenableBuilder 响应主题变化 - HomeBloc 支持下拉刷新 首页增强: - 连续天数徽章 + 今日已写标记 + 最常用心情高亮 - RefreshIndicator 下拉刷新 - 日记列表卡片显示日期 验证: flutter analyze 0 error
This commit is contained in:
@@ -1,59 +1,96 @@
|
||||
// 首页 — 日记流 + 心情概览
|
||||
|
||||
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/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 Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'暖记',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontFamily: 'Caveat',
|
||||
color: colorScheme.primary,
|
||||
return BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'暖记',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontFamily: 'Caveat',
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
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: '模板',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
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: SingleChildScrollView(
|
||||
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(colorScheme: colorScheme),
|
||||
_QuickMoodCard(
|
||||
hasTodayEntry: state.hasTodayEntry,
|
||||
topMood: state.topMood,
|
||||
streakDays: state.streakDays,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 最近日记标题
|
||||
// 最近日记
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'最近日记',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/calendar'),
|
||||
@@ -63,8 +100,9 @@ class HomePage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 日记流占位 — 待数据层集成后替换
|
||||
const _EmptyJournalState(),
|
||||
state.recentJournals.isEmpty
|
||||
? const _EmptyJournalState()
|
||||
: _JournalList(journals: state.recentJournals),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -74,13 +112,20 @@ class HomePage extends StatelessWidget {
|
||||
|
||||
/// 心情快速选择卡片
|
||||
class _QuickMoodCard extends StatelessWidget {
|
||||
const _QuickMoodCard({required this.colorScheme});
|
||||
const _QuickMoodCard({
|
||||
required this.hasTodayEntry,
|
||||
this.topMood,
|
||||
this.streakDays = 0,
|
||||
});
|
||||
|
||||
final ColorScheme colorScheme;
|
||||
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),
|
||||
@@ -91,25 +136,42 @@ class _QuickMoodCard extends StatelessWidget {
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'今天心情如何?',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
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: BorderRadius.circular(8),
|
||||
),
|
||||
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: BorderRadius.circular(8),
|
||||
),
|
||||
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.go('/editor'),
|
||||
child: Column(
|
||||
@@ -119,20 +181,17 @@ class _QuickMoodCard extends StatelessWidget {
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: (AppColors.moodColors[mood.$3.value] ??
|
||||
colorScheme.primary)
|
||||
.withValues(alpha: 0.15),
|
||||
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),
|
||||
),
|
||||
),
|
||||
Text(mood.$2, style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -145,6 +204,74 @@ class _QuickMoodCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 日记列表
|
||||
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: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => context.go('/editor?id=${journal.id}'),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
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();
|
||||
@@ -159,18 +286,10 @@ class _EmptyJournalState extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_note_rounded,
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.2),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
Text('开始你的第一篇手账日记吧!',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: colorScheme.onSurface.withValues(alpha: 0.5))),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.go('/editor'),
|
||||
|
||||
Reference in New Issue
Block a user