feat(diary): B4+B5+B6 后端服务 + F5/F6/F7 前端模块
后端 (erp-diary): - B4: CommentService 班级成员验证 + 删除评语 + SSE 通知推送 - B4: NotificationService 评语/主题/成就三类通知事件 - B5: StickerService 贴纸包列表 + 贴纸查询 + 模板管理 - B5: AchievementService 成就列表 + 解锁 + SSE 通知 - B6: MoodStatsService 心情统计 + 连续天数 - B6: ContentSafetyService 敏感词过滤框架 - SSE handler 增加 diary.notification.* 事件处理 - 新增 14 个 API 端点 + diary.comment.delete 权限 前端 (Flutter): - F5: CalendarBloc + 月视图日历 + 日记列表 - F6: MoodBloc + fl_chart 心情饼图 + 统计卡片 + 连续天数 - F7: 贴纸库分类浏览 + 模板画廊 - 首页改为日记流 + 心情快速选择 - 成就页改为徽章收集展示 验证: cargo check ✓ cargo test 17/17 ✓ flutter analyze 0 error
This commit is contained in:
@@ -1,13 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// 成就页面 — 徽章收集展示
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||
|
||||
/// 成就数据模型
|
||||
class Achievement {
|
||||
final String id;
|
||||
final String code;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String icon;
|
||||
final String category;
|
||||
final bool isUnlocked;
|
||||
|
||||
const Achievement({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.icon,
|
||||
required this.category,
|
||||
this.isUnlocked = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// 成就页面 — 徽章收集和展示
|
||||
class AchievementPage extends StatelessWidget {
|
||||
const AchievementPage({super.key});
|
||||
|
||||
static const _achievements = [
|
||||
Achievement(id: '1', code: 'first_diary', name: '初次落笔', description: '写下第一篇日记', icon: '✏️', category: 'writing', isUnlocked: true),
|
||||
Achievement(id: '2', code: 'streak_7', name: '坚持一周', description: '连续写日记 7 天', icon: '🔥', category: 'writing'),
|
||||
Achievement(id: '3', code: 'streak_30', name: '月度达人', description: '连续写日记 30 天', icon: '💪', category: 'writing'),
|
||||
Achievement(id: '4', code: 'sticker_collector', name: '贴纸收藏家', description: '收集 10 张贴纸', icon: '🎨', category: 'collection'),
|
||||
Achievement(id: '5', code: 'social_butterfly', name: '分享之星', description: '分享 5 篇日记到班级', icon: '🌟', category: 'social'),
|
||||
Achievement(id: '6', code: 'mood_tracker', name: '心情记录员', description: '连续记录心情 14 天', icon: '🌈', category: 'writing'),
|
||||
Achievement(id: '7', code: 'early_bird', name: '早起日记', description: '在早上 7 点前写日记', icon: '🌅', category: 'special'),
|
||||
Achievement(id: '8', code: 'artist', name: '小画家', description: '在日记中画 10 幅涂鸦', icon: '🖌️', category: 'collection'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('成就 - 占位页面'),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final unlocked = _achievements.where((a) => a.isUnlocked).length;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('成就'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 进度概览
|
||||
_AchievementProgressCard(
|
||||
unlocked: unlocked,
|
||||
total: _achievements.length,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'全部成就',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.75,
|
||||
),
|
||||
itemCount: _achievements.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _AchievementCard(
|
||||
achievement: _achievements[index],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 成就进度卡片
|
||||
class _AchievementProgressCard extends StatelessWidget {
|
||||
const _AchievementProgressCard({
|
||||
required this.unlocked,
|
||||
required this.total,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final int unlocked;
|
||||
final int total;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final progress = total > 0 ? unlocked / total : 0.0;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
color: colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'收集进度',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$unlocked / $total',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 10,
|
||||
backgroundColor: colorScheme.primary.withValues(alpha: 0.15),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 成就卡片
|
||||
class _AchievementCard extends StatelessWidget {
|
||||
const _AchievementCard({
|
||||
required this.achievement,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final Achievement achievement;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: achievement.isUnlocked
|
||||
? AppColors.accent.withValues(alpha: 0.4)
|
||||
: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: achievement.isUnlocked
|
||||
? AppColors.accent.withValues(alpha: 0.15)
|
||||
: colorScheme.onSurface.withValues(alpha: 0.05),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: achievement.isUnlocked
|
||||
? Text(achievement.icon, style: const TextStyle(fontSize: 28))
|
||||
: Icon(
|
||||
Icons.lock_outline,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
achievement.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: achievement.isUnlocked
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
if (achievement.description != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
achievement.description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(
|
||||
alpha: achievement.isUnlocked ? 0.6 : 0.3,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
175
app/lib/features/calendar/bloc/calendar_bloc.dart
Normal file
175
app/lib/features/calendar/bloc/calendar_bloc.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
// 日历 BLoC — 管理日历视图状态和日记列表
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
|
||||
// ===== Events =====
|
||||
|
||||
sealed class CalendarEvent {
|
||||
const CalendarEvent();
|
||||
}
|
||||
|
||||
/// 切换到指定月份
|
||||
final class CalendarMonthChanged extends CalendarEvent {
|
||||
final DateTime month;
|
||||
const CalendarMonthChanged(this.month);
|
||||
}
|
||||
|
||||
/// 选择某一天
|
||||
final class CalendarDaySelected extends CalendarEvent {
|
||||
final DateTime day;
|
||||
const CalendarDaySelected(this.day);
|
||||
}
|
||||
|
||||
/// 切换视图模式(月/周/时间轴)
|
||||
final class CalendarViewModeChanged extends CalendarEvent {
|
||||
final CalendarViewMode mode;
|
||||
const CalendarViewModeChanged(this.mode);
|
||||
}
|
||||
|
||||
/// 加载某月的日记列表
|
||||
final class CalendarLoadJournals extends CalendarEvent {
|
||||
final DateTime month;
|
||||
const CalendarLoadJournals(this.month);
|
||||
}
|
||||
|
||||
// ===== State =====
|
||||
|
||||
/// 日历视图模式
|
||||
enum CalendarViewMode { month, week, timeline }
|
||||
|
||||
/// 日历状态
|
||||
sealed class CalendarState {
|
||||
const CalendarState();
|
||||
}
|
||||
|
||||
/// 初始加载中
|
||||
final class CalendarInitial extends CalendarState {
|
||||
const CalendarInitial();
|
||||
}
|
||||
|
||||
/// 日历已加载 — 包含当前月份、选中日期、日记列表
|
||||
final class CalendarLoaded extends CalendarState {
|
||||
/// 当前显示的月份
|
||||
final DateTime focusedMonth;
|
||||
|
||||
/// 选中的日期
|
||||
final DateTime selectedDay;
|
||||
|
||||
/// 当前月份所有日记(按日期索引)
|
||||
final Map<DateTime, List<JournalEntry>> journalsByDate;
|
||||
|
||||
/// 当前选中日期的日记列表
|
||||
final List<JournalEntry> selectedDayJournals;
|
||||
|
||||
/// 视图模式
|
||||
final CalendarViewMode viewMode;
|
||||
|
||||
/// 是否正在加载
|
||||
final bool isLoading;
|
||||
|
||||
const CalendarLoaded({
|
||||
required this.focusedMonth,
|
||||
required this.selectedDay,
|
||||
required this.journalsByDate,
|
||||
required this.selectedDayJournals,
|
||||
this.viewMode = CalendarViewMode.month,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
CalendarLoaded copyWith({
|
||||
DateTime? focusedMonth,
|
||||
DateTime? selectedDay,
|
||||
Map<DateTime, List<JournalEntry>>? journalsByDate,
|
||||
List<JournalEntry>? selectedDayJournals,
|
||||
CalendarViewMode? viewMode,
|
||||
bool? isLoading,
|
||||
}) =>
|
||||
CalendarLoaded(
|
||||
focusedMonth: focusedMonth ?? this.focusedMonth,
|
||||
selectedDay: selectedDay ?? this.selectedDay,
|
||||
journalsByDate: journalsByDate ?? this.journalsByDate,
|
||||
selectedDayJournals: selectedDayJournals ?? this.selectedDayJournals,
|
||||
viewMode: viewMode ?? this.viewMode,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
/// 加载失败
|
||||
final class CalendarError extends CalendarState {
|
||||
final String message;
|
||||
const CalendarError(this.message);
|
||||
}
|
||||
|
||||
// ===== BLoC =====
|
||||
|
||||
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
CalendarBloc() : super(const CalendarInitial()) {
|
||||
on<CalendarMonthChanged>(_onMonthChanged);
|
||||
on<CalendarDaySelected>(_onDaySelected);
|
||||
on<CalendarViewModeChanged>(_onViewModeChanged);
|
||||
on<CalendarLoadJournals>(_onLoadJournals);
|
||||
}
|
||||
|
||||
void _onMonthChanged(
|
||||
CalendarMonthChanged event,
|
||||
Emitter<CalendarState> emit,
|
||||
) {
|
||||
final currentState = state is CalendarLoaded ? state as CalendarLoaded : null;
|
||||
|
||||
emit(CalendarLoaded(
|
||||
focusedMonth: event.month,
|
||||
selectedDay: event.month,
|
||||
journalsByDate: currentState?.journalsByDate ?? {},
|
||||
selectedDayJournals: [],
|
||||
viewMode: currentState?.viewMode ?? CalendarViewMode.month,
|
||||
));
|
||||
|
||||
add(CalendarLoadJournals(event.month));
|
||||
}
|
||||
|
||||
void _onDaySelected(
|
||||
CalendarDaySelected event,
|
||||
Emitter<CalendarState> emit,
|
||||
) {
|
||||
if (state is! CalendarLoaded) return;
|
||||
final current = state as CalendarLoaded;
|
||||
|
||||
// 查找选中日期的日记
|
||||
final dayKey = DateTime(event.day.year, event.day.month, event.day.day);
|
||||
final dayJournals = current.journalsByDate[dayKey] ?? [];
|
||||
|
||||
emit(current.copyWith(
|
||||
selectedDay: event.day,
|
||||
selectedDayJournals: dayJournals,
|
||||
));
|
||||
}
|
||||
|
||||
void _onViewModeChanged(
|
||||
CalendarViewModeChanged event,
|
||||
Emitter<CalendarState> emit,
|
||||
) {
|
||||
if (state is! CalendarLoaded) return;
|
||||
final current = state as CalendarLoaded;
|
||||
emit(current.copyWith(viewMode: event.mode));
|
||||
}
|
||||
|
||||
Future<void> _onLoadJournals(
|
||||
CalendarLoadJournals event,
|
||||
Emitter<CalendarState> emit,
|
||||
) async {
|
||||
if (state is! CalendarLoaded) return;
|
||||
final current = state as CalendarLoaded;
|
||||
|
||||
emit(current.copyWith(isLoading: true));
|
||||
|
||||
// Phase 1: 使用空数据占位,待 Repository 集成后替换
|
||||
// 实际将从 JournalRepository.loadByMonth(event.month) 获取
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
emit(current.copyWith(
|
||||
isLoading: false,
|
||||
journalsByDate: current.journalsByDate,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,489 @@
|
||||
import 'package:flutter/material.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/data/models/journal_entry.dart';
|
||||
import '../bloc/calendar_bloc.dart';
|
||||
|
||||
/// 日历页面 — 月视图 + 选中日期的日记列表
|
||||
class CalendarPage extends StatelessWidget {
|
||||
const CalendarPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('日历 - 占位页面'),
|
||||
return BlocProvider(
|
||||
create: (context) => CalendarBloc()
|
||||
..add(CalendarMonthChanged(DateTime.now())),
|
||||
child: const _CalendarView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalendarView extends StatelessWidget {
|
||||
const _CalendarView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return BlocBuilder<CalendarBloc, CalendarState>(
|
||||
builder: (context, state) {
|
||||
if (state is CalendarInitial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is CalendarError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 48, color: colorScheme.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message, style: theme.textTheme.bodyLarge),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => context.read<CalendarBloc>()
|
||||
.add(CalendarMonthChanged(DateTime.now())),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is! CalendarLoaded) return const SizedBox.shrink();
|
||||
final loaded = state;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 月份导航
|
||||
_MonthNavigator(
|
||||
month: loaded.focusedMonth,
|
||||
onPrevious: () {
|
||||
final prev = DateTime(
|
||||
loaded.focusedMonth.year,
|
||||
loaded.focusedMonth.month - 1,
|
||||
);
|
||||
context.read<CalendarBloc>().add(CalendarMonthChanged(prev));
|
||||
},
|
||||
onNext: () {
|
||||
final next = DateTime(
|
||||
loaded.focusedMonth.year,
|
||||
loaded.focusedMonth.month + 1,
|
||||
);
|
||||
context.read<CalendarBloc>().add(CalendarMonthChanged(next));
|
||||
},
|
||||
),
|
||||
|
||||
// 星期标题行
|
||||
_WeekdayHeader(colorScheme: colorScheme),
|
||||
|
||||
// 日历网格
|
||||
_CalendarGrid(
|
||||
month: loaded.focusedMonth,
|
||||
selectedDay: loaded.selectedDay,
|
||||
journalsByDate: loaded.journalsByDate,
|
||||
onDaySelected: (day) {
|
||||
context.read<CalendarBloc>().add(CalendarDaySelected(day));
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// 选中日期的日记列表
|
||||
Expanded(
|
||||
child: loaded.selectedDayJournals.isEmpty
|
||||
? _EmptyDayView(selectedDay: loaded.selectedDay)
|
||||
: _DayJournalList(journals: loaded.selectedDayJournals),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 月份导航栏
|
||||
class _MonthNavigator extends StatelessWidget {
|
||||
const _MonthNavigator({
|
||||
required this.month,
|
||||
required this.onPrevious,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final DateTime month;
|
||||
final VoidCallback onPrevious;
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final monthName = _formatMonth(month);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onPrevious,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
tooltip: '上个月',
|
||||
),
|
||||
Text(
|
||||
monthName,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
tooltip: '下个月',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatMonth(DateTime date) {
|
||||
const months = [
|
||||
'1月', '2月', '3月', '4月', '5月', '6月',
|
||||
'7月', '8月', '9月', '10月', '11月', '12月',
|
||||
];
|
||||
return '${date.year}年 ${months[date.month - 1]}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 星期标题行
|
||||
class _WeekdayHeader extends StatelessWidget {
|
||||
const _WeekdayHeader({required this.colorScheme});
|
||||
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: weekdays.map((day) {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
day,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 日历网格 — 6行7列
|
||||
class _CalendarGrid extends StatelessWidget {
|
||||
const _CalendarGrid({
|
||||
required this.month,
|
||||
required this.selectedDay,
|
||||
required this.journalsByDate,
|
||||
required this.onDaySelected,
|
||||
});
|
||||
|
||||
final DateTime month;
|
||||
final DateTime selectedDay;
|
||||
final Map<DateTime, List<JournalEntry>> journalsByDate;
|
||||
final ValueChanged<DateTime> onDaySelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final today = DateTime.now();
|
||||
final days = _generateDays(month);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: days.map((week) {
|
||||
return Row(
|
||||
children: week.map((dayInfo) {
|
||||
return Expanded(
|
||||
child: _DayCell(
|
||||
dayInfo: dayInfo,
|
||||
isToday: dayInfo.date.year == today.year &&
|
||||
dayInfo.date.month == today.month &&
|
||||
dayInfo.date.day == today.day,
|
||||
isSelected: dayInfo.date.year == selectedDay.year &&
|
||||
dayInfo.date.month == selectedDay.month &&
|
||||
dayInfo.date.day == selectedDay.day,
|
||||
hasJournals: journalsByDate.containsKey(
|
||||
DateTime(dayInfo.date.year, dayInfo.date.month, dayInfo.date.day),
|
||||
),
|
||||
colorScheme: colorScheme,
|
||||
onTap: () => onDaySelected(dayInfo.date),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 生成当月日历数据(包含前后补齐)
|
||||
List<List<_DayInfo>> _generateDays(DateTime month) {
|
||||
final firstDay = DateTime(month.year, month.month, 1);
|
||||
// 周一为第一天:weekday 1=Mon...7=Sun
|
||||
final startOffset = firstDay.weekday - 1;
|
||||
final daysInMonth = DateTime(month.year, month.month + 1, 0).day;
|
||||
|
||||
final allDays = <_DayInfo>[];
|
||||
|
||||
// 前面的空白
|
||||
for (var i = 0; i < startOffset; i++) {
|
||||
allDays.add(_DayInfo(date: firstDay.subtract(Duration(days: startOffset - i)), isCurrentMonth: false));
|
||||
}
|
||||
|
||||
// 当月日期
|
||||
for (var d = 1; d <= daysInMonth; d++) {
|
||||
allDays.add(_DayInfo(
|
||||
date: DateTime(month.year, month.month, d),
|
||||
isCurrentMonth: true,
|
||||
));
|
||||
}
|
||||
|
||||
// 后面的补齐到完整行
|
||||
while (allDays.length % 7 != 0) {
|
||||
final last = allDays.last.date;
|
||||
allDays.add(_DayInfo(date: last.add(const Duration(days: 1)), isCurrentMonth: false));
|
||||
}
|
||||
|
||||
// 分周
|
||||
return List.generate(allDays.length ~/ 7, (i) => allDays.sublist(i * 7, (i + 1) * 7));
|
||||
}
|
||||
}
|
||||
|
||||
class _DayInfo {
|
||||
const _DayInfo({required this.date, required this.isCurrentMonth});
|
||||
final DateTime date;
|
||||
final bool isCurrentMonth;
|
||||
}
|
||||
|
||||
/// 单日格子
|
||||
class _DayCell extends StatelessWidget {
|
||||
const _DayCell({
|
||||
required this.dayInfo,
|
||||
required this.isToday,
|
||||
required this.isSelected,
|
||||
required this.hasJournals,
|
||||
required this.colorScheme,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _DayInfo dayInfo;
|
||||
final bool isToday;
|
||||
final bool isSelected;
|
||||
final bool hasJournals;
|
||||
final ColorScheme colorScheme;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: isToday
|
||||
? colorScheme.primaryContainer
|
||||
: null,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${dayInfo.date.day}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected || isToday ? FontWeight.bold : FontWeight.normal,
|
||||
color: !dayInfo.isCurrentMonth
|
||||
? colorScheme.onSurface.withValues(alpha: 0.3)
|
||||
: isSelected
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 日记指示点
|
||||
if (hasJournals)
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimary
|
||||
: AppColors.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 无日记的空状态
|
||||
class _EmptyDayView extends StatelessWidget {
|
||||
const _EmptyDayView({required this.selectedDay});
|
||||
|
||||
final DateTime selectedDay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_note_rounded,
|
||||
size: 64,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.2),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'${selectedDay.month}月${selectedDay.day}日',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'这一天还没有日记',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => context.go('/editor'),
|
||||
child: const Text('写一篇'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 日记列表
|
||||
class _DayJournalList extends StatelessWidget {
|
||||
const _DayJournalList({required this.journals});
|
||||
|
||||
final List<JournalEntry> journals;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: journals.length,
|
||||
itemBuilder: (context, index) {
|
||||
final journal = journals[index];
|
||||
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,
|
||||
),
|
||||
if (journal.tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
journal.tags.take(3).join(' · '),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _moodEmoji(Mood mood) {
|
||||
return switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// 首页 — 日记流 + 心情概览
|
||||
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
/// 首页 — 展示最近日记流和心情概览
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('首页 - 占位页面'),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 心情快速选择卡片
|
||||
_QuickMoodCard(colorScheme: colorScheme),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 最近日记标题
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'最近日记',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/calendar'),
|
||||
child: const Text('查看全部'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 日记流占位 — 待数据层集成后替换
|
||||
const _EmptyJournalState(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 心情快速选择卡片
|
||||
class _QuickMoodCard extends StatelessWidget {
|
||||
const _QuickMoodCard({required this.colorScheme});
|
||||
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final moods = [
|
||||
('😊', '开心', Mood.happy),
|
||||
('😌', '平静', Mood.calm),
|
||||
('😢', '难过', Mood.sad),
|
||||
('😠', '生气', Mood.angry),
|
||||
('🤔', '思考', Mood.thinking),
|
||||
];
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: moods.map((mood) {
|
||||
return GestureDetector(
|
||||
onTap: () => context.go('/editor'),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: (AppColors.moodColors[mood.$3.value] ??
|
||||
colorScheme.primary)
|
||||
.withValues(alpha: 0.15),
|
||||
),
|
||||
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 _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.go('/editor'),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('写日记'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
128
app/lib/features/mood/bloc/mood_bloc.dart
Normal file
128
app/lib/features/mood/bloc/mood_bloc.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
// 心情 BLoC — 管理心情统计和趋势数据
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
|
||||
// ===== 心情统计模型 =====
|
||||
|
||||
/// 心情统计数据
|
||||
class MoodStats {
|
||||
final List<MoodCount> moodCounts;
|
||||
final int streakDays;
|
||||
final int totalJournals;
|
||||
final Mood? dominantMood;
|
||||
|
||||
const MoodStats({
|
||||
this.moodCounts = const [],
|
||||
this.streakDays = 0,
|
||||
this.totalJournals = 0,
|
||||
this.dominantMood,
|
||||
});
|
||||
}
|
||||
|
||||
/// 单种心情的统计
|
||||
class MoodCount {
|
||||
final Mood mood;
|
||||
final int count;
|
||||
final double percentage;
|
||||
|
||||
const MoodCount({
|
||||
required this.mood,
|
||||
required this.count,
|
||||
required this.percentage,
|
||||
});
|
||||
}
|
||||
|
||||
/// 心情趋势数据点
|
||||
class MoodTrendPoint {
|
||||
final DateTime date;
|
||||
final Mood mood;
|
||||
final int journalCount;
|
||||
|
||||
const MoodTrendPoint({
|
||||
required this.date,
|
||||
required this.mood,
|
||||
required this.journalCount,
|
||||
});
|
||||
}
|
||||
|
||||
/// 统计周期
|
||||
enum StatsPeriod { week, month, quarter }
|
||||
|
||||
// ===== 心情页面状态 =====
|
||||
|
||||
/// 心情页面状态
|
||||
class MoodState {
|
||||
final MoodStats stats;
|
||||
final List<MoodTrendPoint> trendData;
|
||||
final StatsPeriod selectedPeriod;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const MoodState({
|
||||
this.stats = const MoodStats(),
|
||||
this.trendData = const [],
|
||||
this.selectedPeriod = StatsPeriod.week,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
MoodState copyWith({
|
||||
MoodStats? stats,
|
||||
List<MoodTrendPoint>? trendData,
|
||||
StatsPeriod? selectedPeriod,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) =>
|
||||
MoodState(
|
||||
stats: stats ?? this.stats,
|
||||
trendData: trendData ?? this.trendData,
|
||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 心情 BLoC =====
|
||||
|
||||
class MoodBloc extends ChangeNotifier {
|
||||
MoodState _state = const MoodState();
|
||||
MoodState get state => _state;
|
||||
|
||||
/// 切换统计周期
|
||||
void changePeriod(StatsPeriod period) {
|
||||
_state = _state.copyWith(selectedPeriod: period, isLoading: true);
|
||||
notifyListeners();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
/// 加载统计数据
|
||||
Future<void> _loadStats() async {
|
||||
// Phase 1: 占位数据,待 API 集成后替换
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
_state = _state.copyWith(
|
||||
isLoading: false,
|
||||
stats: MoodStats(
|
||||
moodCounts: [
|
||||
const MoodCount(mood: Mood.happy, count: 12, percentage: 40.0),
|
||||
const MoodCount(mood: Mood.calm, count: 8, percentage: 26.7),
|
||||
const MoodCount(mood: Mood.thinking, count: 5, percentage: 16.7),
|
||||
const MoodCount(mood: Mood.sad, count: 3, percentage: 10.0),
|
||||
const MoodCount(mood: Mood.angry, count: 2, percentage: 6.6),
|
||||
],
|
||||
streakDays: 7,
|
||||
totalJournals: 30,
|
||||
dominantMood: Mood.happy,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 初始加载
|
||||
void load() {
|
||||
_state = _state.copyWith(isLoading: true);
|
||||
notifyListeners();
|
||||
_loadStats();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,356 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// 心情页面 — 心情统计 + 趋势图 + 连续天数
|
||||
|
||||
class MoodPage extends StatelessWidget {
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||
import '../bloc/mood_bloc.dart';
|
||||
|
||||
/// 心情页面 — 统计卡片 + 心情分布饼图 + 趋势折线图
|
||||
class MoodPage extends StatefulWidget {
|
||||
const MoodPage({super.key});
|
||||
|
||||
@override
|
||||
State<MoodPage> createState() => _MoodPageState();
|
||||
}
|
||||
|
||||
class _MoodPageState extends State<MoodPage> {
|
||||
final _bloc = MoodBloc();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc.load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('心情 - 占位页面'),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: _bloc,
|
||||
builder: (context, _) {
|
||||
final state = _bloc.state;
|
||||
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 统计概览卡片
|
||||
_StatsOverviewCard(stats: state.stats, colorScheme: colorScheme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 周期选择器
|
||||
_PeriodSelector(
|
||||
selectedPeriod: state.selectedPeriod,
|
||||
onPeriodChanged: _bloc.changePeriod,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 心情分布饼图
|
||||
_MoodDistributionChart(
|
||||
moodCounts: state.stats.moodCounts,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 心情详情列表
|
||||
Text(
|
||||
'心情详情',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...state.stats.moodCounts.map((mc) => _MoodCountTile(mc: mc)),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 连续天数鼓励卡片
|
||||
_StreakCard(streakDays: state.stats.streakDays),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 统计概览卡片
|
||||
class _StatsOverviewCard extends StatelessWidget {
|
||||
const _StatsOverviewCard({
|
||||
required this.stats,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final MoodStats stats;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final dominantEmoji = stats.dominantMood != null
|
||||
? _moodEmoji(stats.dominantMood!)
|
||||
: '📝';
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
color: colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
// 主导心情图标
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(dominantEmoji, style: const TextStyle(fontSize: 28)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'心情概览',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'共 ${stats.totalJournals} 篇日记 · 连续 ${stats.streakDays} 天',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 周期选择器
|
||||
class _PeriodSelector extends StatelessWidget {
|
||||
const _PeriodSelector({
|
||||
required this.selectedPeriod,
|
||||
required this.onPeriodChanged,
|
||||
});
|
||||
|
||||
final StatsPeriod selectedPeriod;
|
||||
final ValueChanged<StatsPeriod> onPeriodChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SegmentedButton<StatsPeriod>(
|
||||
segments: const [
|
||||
ButtonSegment(value: StatsPeriod.week, label: Text('周')),
|
||||
ButtonSegment(value: StatsPeriod.month, label: Text('月')),
|
||||
ButtonSegment(value: StatsPeriod.quarter, label: Text('季')),
|
||||
],
|
||||
selected: {selectedPeriod},
|
||||
onSelectionChanged: (set) => onPeriodChanged(set.first),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 心情分布饼图
|
||||
class _MoodDistributionChart extends StatelessWidget {
|
||||
const _MoodDistributionChart({
|
||||
required this.moodCounts,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final List<MoodCount> moodCounts;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (moodCounts.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: moodCounts.map((mc) {
|
||||
final color = AppColors.moodColors[mc.mood.value] ?? colorScheme.primary;
|
||||
return PieChartSectionData(
|
||||
value: mc.count.toDouble(),
|
||||
color: color,
|
||||
radius: 50,
|
||||
title: '${mc.percentage.toStringAsFixed(0)}%',
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 心情计数列表项
|
||||
class _MoodCountTile extends StatelessWidget {
|
||||
const _MoodCountTile({required this.mc});
|
||||
|
||||
final MoodCount mc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final color = AppColors.moodColors[mc.mood.value] ?? theme.colorScheme.primary;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(_moodEmoji(mc.mood), style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_moodLabel(mc.mood),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: mc.percentage / 100,
|
||||
backgroundColor: color.withValues(alpha: 0.15),
|
||||
color: color,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Text(
|
||||
'${mc.count} 篇',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 连续天数鼓励卡片
|
||||
class _StreakCard extends StatelessWidget {
|
||||
const _StreakCard({required this.streakDays});
|
||||
|
||||
final int streakDays;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
color: AppColors.tertiary.withValues(alpha: 0.15),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('🔥', style: TextStyle(fontSize: 32)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'连续 $streakDays 天',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
streakDays >= 7
|
||||
? '太棒了!你已经坚持了一周 ✨'
|
||||
: '继续加油,坚持就是胜利!',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 辅助函数 =====
|
||||
|
||||
String _moodEmoji(Mood mood) => switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
|
||||
String _moodLabel(Mood mood) => switch (mood) {
|
||||
Mood.happy => '开心',
|
||||
Mood.calm => '平静',
|
||||
Mood.sad => '难过',
|
||||
Mood.angry => '生气',
|
||||
Mood.thinking => '思考',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// 贴纸库页面 — 贴纸包浏览 + 贴纸网格
|
||||
|
||||
class StickerLibraryPage extends StatelessWidget {
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||
|
||||
/// 贴纸包数据模型
|
||||
class StickerPack {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? coverEmoji;
|
||||
final int stickerCount;
|
||||
final bool isFree;
|
||||
final String? category;
|
||||
|
||||
const StickerPack({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.coverEmoji,
|
||||
this.stickerCount = 0,
|
||||
this.isFree = true,
|
||||
this.category,
|
||||
});
|
||||
}
|
||||
|
||||
/// 贴纸库页面 — 分类浏览贴纸包
|
||||
class StickerLibraryPage extends StatefulWidget {
|
||||
const StickerLibraryPage({super.key});
|
||||
|
||||
@override
|
||||
State<StickerLibraryPage> createState() => _StickerLibraryPageState();
|
||||
}
|
||||
|
||||
class _StickerLibraryPageState extends State<StickerLibraryPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
// Phase 1 占位数据
|
||||
final _categories = ['全部', '动物', '食物', '自然', '节日', '表情'];
|
||||
|
||||
final _packs = const [
|
||||
StickerPack(id: '1', name: '可爱猫咪', coverEmoji: '🐱', stickerCount: 24, isFree: true, category: '动物'),
|
||||
StickerPack(id: '2', name: '小兔子系列', coverEmoji: '🐰', stickerCount: 20, isFree: true, category: '动物'),
|
||||
StickerPack(id: '3', name: '甜品派对', coverEmoji: '🍰', stickerCount: 18, isFree: true, category: '食物'),
|
||||
StickerPack(id: '4', name: '花朵合集', coverEmoji: '🌸', stickerCount: 22, isFree: true, category: '自然'),
|
||||
StickerPack(id: '5', name: '夏日清凉', coverEmoji: '🍉', stickerCount: 16, isFree: true, category: '食物'),
|
||||
StickerPack(id: '6', name: '星空物语', coverEmoji: '⭐', stickerCount: 20, isFree: false, category: '自然'),
|
||||
StickerPack(id: '7', name: '开心表情', coverEmoji: '😄', stickerCount: 30, isFree: true, category: '表情'),
|
||||
StickerPack(id: '8', name: '新年快乐', coverEmoji: '🎉', stickerCount: 15, isFree: false, category: '节日'),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _categories.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('贴纸库 - 占位页面'),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('贴纸库'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
tabs: _categories.map((c) => Tab(text: c)).toList(),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _categories.map((category) {
|
||||
final filtered = category == '全部'
|
||||
? _packs
|
||||
: _packs.where((p) => p.category == category).toList();
|
||||
return _StickerPackGrid(packs: filtered, colorScheme: colorScheme);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 贴纸包网格
|
||||
class _StickerPackGrid extends StatelessWidget {
|
||||
const _StickerPackGrid({
|
||||
required this.packs,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final List<StickerPack> packs;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (packs.isEmpty) {
|
||||
return const Center(child: Text('暂无贴纸包'));
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.85,
|
||||
),
|
||||
itemCount: packs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final pack = packs[index];
|
||||
return _StickerPackCard(pack: pack, colorScheme: colorScheme);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 贴纸包卡片
|
||||
class _StickerPackCard extends StatelessWidget {
|
||||
const _StickerPackCard({
|
||||
required this.pack,
|
||||
required this.colorScheme,
|
||||
});
|
||||
|
||||
final StickerPack pack;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// Phase 1: 展示贴纸包详情页(待实现)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('打开贴纸包: ${pack.name}')),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 贴纸包封面图标
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
pack.coverEmoji ?? '🎨',
|
||||
style: const TextStyle(fontSize: 32),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 名称
|
||||
Text(
|
||||
pack.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 数量和价格标签
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${pack.stickerCount} 张',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
if (!pack.isFree) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'积分',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: AppColors.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// 模板画廊页面 — 日记模板浏览和选择
|
||||
|
||||
class TemplateGalleryPage extends StatelessWidget {
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||
|
||||
/// 模板数据模型
|
||||
class Template {
|
||||
final String id;
|
||||
final String name;
|
||||
final String emoji;
|
||||
final String? category;
|
||||
final bool isFree;
|
||||
final String? description;
|
||||
|
||||
const Template({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.emoji,
|
||||
this.category,
|
||||
this.isFree = true,
|
||||
this.description,
|
||||
});
|
||||
}
|
||||
|
||||
/// 模板画廊页面 — 浏览和选择日记模板
|
||||
class TemplateGalleryPage extends StatefulWidget {
|
||||
const TemplateGalleryPage({super.key});
|
||||
|
||||
@override
|
||||
State<TemplateGalleryPage> createState() => _TemplateGalleryPageState();
|
||||
}
|
||||
|
||||
class _TemplateGalleryPageState extends State<TemplateGalleryPage> {
|
||||
String _selectedCategory = '全部';
|
||||
|
||||
final _categories = ['全部', '日常', '旅行', '校园', '节日', '创意'];
|
||||
|
||||
// Phase 1 占位数据
|
||||
final _templates = const [
|
||||
Template(id: '1', name: '今日心情', emoji: '💭', category: '日常', description: '记录今天的心情和感受'),
|
||||
Template(id: '2', name: '校园日记', emoji: '📚', category: '校园', description: '在学校的一天'),
|
||||
Template(id: '3', name: '旅行手账', emoji: '🗺️', category: '旅行', description: '记录旅行中的美好瞬间'),
|
||||
Template(id: '4', name: '美食记录', emoji: '🍜', category: '日常', description: '记录今天吃到的美食'),
|
||||
Template(id: '5', name: '读书笔记', emoji: '📖', category: '校园', description: '记录读完一本书的感想'),
|
||||
Template(id: '6', name: '节日特辑', emoji: '🎄', category: '节日', description: '特别的节日记录'),
|
||||
Template(id: '7', name: '自然观察', emoji: '🌿', category: '创意', description: '记录大自然的发现'),
|
||||
Template(id: '8', name: '梦想清单', emoji: '✨', category: '创意', description: '写下心中的梦想'),
|
||||
Template(id: '9', name: '周末时光', emoji: '☀️', category: '日常', description: '悠闲的周末记录'),
|
||||
Template(id: '10', name: '运动打卡', emoji: '🏃', category: '日常', description: '记录运动和锻炼'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('模板画廊 - 占位页面'),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
final filtered = _selectedCategory == '全部'
|
||||
? _templates
|
||||
: _templates.where((t) => t.category == _selectedCategory).toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('模板画廊'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 分类选择器
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: _categories.map((cat) {
|
||||
final isSelected = cat == _selectedCategory;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(cat),
|
||||
onSelected: (_) {
|
||||
setState(() => _selectedCategory = cat);
|
||||
},
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
checkmarkColor: colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 模板网格
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.78,
|
||||
),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _TemplateCard(template: filtered[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 模板卡片
|
||||
class _TemplateCard extends StatelessWidget {
|
||||
const _TemplateCard({required this.template});
|
||||
|
||||
final Template template;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// 使用模板创建日记
|
||||
context.go('/editor?template=${template.id}');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 模板预览区
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primaryContainer.withValues(alpha: 0.5),
|
||||
AppColors.tertiary.withValues(alpha: 0.3),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
template.emoji,
|
||||
style: const TextStyle(fontSize: 36),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 模板名称
|
||||
Text(
|
||||
template.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 描述
|
||||
if (template.description != null)
|
||||
Text(
|
||||
template.description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user