Files
nj/app/lib/features/calendar/views/calendar_page.dart
iven f64355946c
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat(app): 共享 UI 组件 + 4 个关键 UX bug 修复
Phase 0 — 共享组件:
- EmptyStateWidget: 统一空状态 (icon + title + subtitle + CTA)
- ErrorStateWidget: 统一错误状态 (message + retry)
- SkeletonBox + SkeletonList: 统一骨架屏加载 (shimmer 动画)

Phase 1 — Bug 修复:
- 班级评论按 journalId 过滤,避免显示在错误日记卡片下
- moodCellColors key 修正: love/tired → angry/thinking
- 日历非 CalendarLoaded 状态改为加载指示器 (不再 SizedBox.shrink)
- 贴纸数统计改为 '--' 占位 (之前错误显示日记总数)
2026-06-07 13:36:10 +08:00

982 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 日历页面 — 月视图(心情色彩) + 周视图 + 时间轴
// 对齐 Open Design 原型稿: 日历格子用心情颜色填充背景 + 月/周/时间轴切换
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/utils/mood_utils.dart';
import 'package:nuanji_app/data/models/journal_entry.dart';
import 'package:nuanji_app/data/repositories/journal_repository.dart';
import '../bloc/calendar_bloc.dart';
/// 日历页面 — 月视图(心情色彩) + 周视图 + 时间轴
class CalendarPage extends StatelessWidget {
const CalendarPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CalendarBloc(
journalRepository: context.read<JournalRepository>(),
)..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 Center(child: CircularProgressIndicator());
}
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));
},
),
// 视图模式切换
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: SegmentedButton<CalendarViewMode>(
segments: const [
ButtonSegment(value: CalendarViewMode.month, label: Text('')),
ButtonSegment(value: CalendarViewMode.week, label: Text('')),
ButtonSegment(value: CalendarViewMode.timeline, label: Text('时间轴')),
],
selected: {loaded.viewMode},
onSelectionChanged: (modes) {
context.read<CalendarBloc>()
.add(CalendarViewModeChanged(modes.first));
},
style: ButtonStyle(
visualDensity: VisualDensity.compact,
textStyle: WidgetStatePropertyAll(theme.textTheme.labelSmall),
),
),
),
// 根据视图模式切换内容
switch (loaded.viewMode) {
CalendarViewMode.month => _MonthView(loaded: loaded),
CalendarViewMode.week => _WeekView(loaded: loaded),
CalendarViewMode.timeline => _TimelineView(loaded: loaded),
},
],
);
},
);
}
}
// ===== 月视图 =====
class _MonthView extends StatelessWidget {
const _MonthView({required this.loaded});
final CalendarLoaded loaded;
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
children: [
// 本月心情概览柱状图
_MoodSummaryChart(journalsByDate: loaded.journalsByDate),
const SizedBox(height: 8),
// 星期标题行
_WeekdayHeader(colorScheme: Theme.of(context).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),
),
],
),
);
}
}
/// 本月心情概览 — 5 柱状图
class _MoodSummaryChart extends StatelessWidget {
const _MoodSummaryChart({required this.journalsByDate});
final Map<DateTime, List<JournalEntry>> journalsByDate;
static const _moodConfig = [
(Mood.happy, '开心', AppColors.secondary),
(Mood.calm, '平静', AppColors.tertiary),
(Mood.sad, '难过', Color(0xFF5B7DB1)),
(Mood.angry, '生气', AppColors.accent),
(Mood.thinking, '思考', Color(0xFF8B7E74)),
];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// 统计每种心情的数量
final counts = <Mood, int>{};
for (final entry in journalsByDate.entries) {
for (final journal in entry.value) {
counts[journal.mood] = (counts[journal.mood] ?? 0) + 1;
}
}
final maxCount = counts.values.fold(0, (a, b) => a > b ? a : b).toDouble();
final barMaxHeight = 60.0;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.colorScheme.shadow.withValues(alpha: 0.05),
offset: const Offset(0, 2),
blurRadius: 8,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'本月心情概览',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: _moodConfig.map((config) {
final count = counts[config.$1] ?? 0;
final barHeight = maxCount > 0 && count > 0
? (count / maxCount * barMaxHeight).clamp(4.0, barMaxHeight)
: 4.0;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: barHeight,
decoration: BoxDecoration(
color: config.$3,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(6),
),
),
),
const SizedBox(height: 6),
Text(
config.$2,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}).toList(),
),
],
),
);
}
}
// ===== 周视图 =====
class _WeekView extends StatelessWidget {
const _WeekView({required this.loaded});
final CalendarLoaded loaded;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// 计算选中日期所在周
final selected = loaded.selectedDay;
final startOfWeek = selected.subtract(Duration(days: selected.weekday - 1));
return Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 7 天条目
...List.generate(7, (i) {
final day = startOfWeek.add(Duration(days: i));
final dayKey = DateTime(day.year, day.month, day.day);
final journals = loaded.journalsByDate[dayKey] ?? [];
final isToday = _isToday(day);
final isSelected = _isSameDay(day, loaded.selectedDay);
final hasEntry = journals.isNotEmpty;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () {
context.read<CalendarBloc>().add(CalendarDaySelected(day));
},
borderRadius: AppRadius.mdBorder,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primaryContainer
: hasEntry
? _getMoodBgColor(journals.first.mood.value)
: colorScheme.surface,
borderRadius: AppRadius.mdBorder,
border: Border.all(
color: isToday
? colorScheme.primary
: colorScheme.outlineVariant,
width: isToday ? 2 : 1,
),
),
child: Row(
children: [
// 日期
SizedBox(
width: 48,
child: Column(
children: [
Text(
_weekdayName(day.weekday),
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
Text(
'${day.day}',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
color: isToday ? colorScheme.primary : null,
),
),
],
),
),
const SizedBox(width: 16),
// 内容
Expanded(
child: hasEntry
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
journals.first.title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (journals.length > 1)
Text(
'还有 ${journals.length - 1} 篇日记',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
)
: Text(
'无日记',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
// 心情 emoji
if (hasEntry)
Text(
moodToEmoji(journals.first.mood),
style: const TextStyle(fontSize: 24),
),
],
),
),
),
);
}),
],
),
);
}
}
// ===== 时间轴视图 =====
class _TimelineView extends StatelessWidget {
const _TimelineView({required this.loaded});
final CalendarLoaded loaded;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// 获取当月所有日记,按日期排序
final allJournals = loaded.journalsByDate.entries
.expand((e) => e.value.map((j) => MapEntry(e.key, j)))
.toList()
..sort((a, b) => b.key.compareTo(a.key));
if (allJournals.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.timeline_outlined, size: 48,
color: colorScheme.onSurface.withValues(alpha: 0.2)),
const SizedBox(height: 16),
Text('本月还没有日记', style: theme.textTheme.bodyLarge),
],
),
);
}
return Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: allJournals.length,
itemBuilder: (context, index) {
final entry = allJournals[index];
final date = entry.key;
final journal = entry.value;
final isLast = index == allJournals.length - 1;
// 时间格式化 HH:mm
final timeStr =
'${journal.createdAt.hour.toString().padLeft(2, '0')}:${journal.createdAt.minute.toString().padLeft(2, '0')}';
// 摘要文本
final excerpt = journal.contentExcerpt ?? '';
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 时间轴线
SizedBox(
width: 60,
child: Column(
children: [
// 圆点 + emoji
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getMoodBgColor(journal.mood.value),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(moodToEmoji(journal.mood), style: const TextStyle(fontSize: 20)),
),
// 竖线
if (!isLast)
Expanded(
child: Container(
width: 2,
color: colorScheme.outlineVariant,
),
),
],
),
),
// 内容卡片
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Card(
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'${date.month}${date.day}',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
Text(
timeStr,
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],
),
const SizedBox(height: 4),
Text(
journal.title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (excerpt.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
excerpt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.4,
),
),
],
],
),
),
),
),
),
),
],
),
);
},
),
);
}
}
// ===== 月份导航栏 =====
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: [
SizedBox(
width: 44,
height: 44,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: const CircleBorder(),
padding: EdgeInsets.zero,
side: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
onPressed: onPrevious,
child: const Icon(Icons.chevron_left, size: 20),
),
),
Text(
monthName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(
width: 44,
height: 44,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: const CircleBorder(),
padding: EdgeInsets.zero,
side: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
onPressed: onNext,
child: const Icon(Icons.chevron_right, size: 20),
),
),
],
),
);
}
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 days = _generateDays(month);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: days.map((week) {
return Row(
children: week.map((dayInfo) {
final dayKey = DateTime(
dayInfo.date.year, dayInfo.date.month, dayInfo.date.day);
final journals = journalsByDate[dayKey] ?? [];
final moodValue = journals.isNotEmpty ? journals.first.mood.value : null;
return Expanded(
child: _DayCell(
dayInfo: dayInfo,
isToday: _isToday(dayInfo.date),
isSelected: _isSameDay(dayInfo.date, selectedDay),
hasJournals: journals.isNotEmpty,
moodValue: moodValue,
onTap: () => onDaySelected(dayInfo.date),
),
);
}).toList(),
);
}).toList(),
),
);
}
List<List<_DayInfo>> _generateDays(DateTime month) {
final firstDay = DateTime(month.year, month.month, 1);
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.moodValue,
required this.onTap,
});
final _DayInfo dayInfo;
final bool isToday;
final bool isSelected;
final bool hasJournals;
final String? moodValue;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// 心情色彩背景
final moodBg = hasJournals && moodValue != null
? _getMoodBgColor(moodValue!)
: Colors.transparent;
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primary
: moodBg != Colors.transparent
? moodBg
: isToday
? colorScheme.primaryContainer
: null,
borderRadius: AppRadius.smBorder,
border: isToday && !isSelected
? Border.all(color: colorScheme.primary, width: 2)
: null,
),
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
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 && moodBg == Colors.transparent)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(top: 1),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? colorScheme.onPrimary : AppColors.accent,
),
),
],
),
// 日记条目指示点
if (hasJournals)
Positioned(
top: 3,
right: 5,
child: Container(
width: 5,
height: 5,
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.push('/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: 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(
moodToEmoji(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),
),
],
),
),
),
);
},
);
}
}
// ===== 辅助函数 =====
/// 获取心情对应的日历格子背景色
Color _getMoodBgColor(String mood) {
return AppColors.moodCellColors[mood] ?? AppColors.secondarySoftLight;
}
/// 是否是今天
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year && date.month == now.month && date.day == now.day;
}
/// 是否同一天
bool _isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
/// 周几名称
String _weekdayName(int weekday) {
return switch (weekday) {
1 => '周一',
2 => '周二',
3 => '周三',
4 => '周四',
5 => '周五',
6 => '周六',
7 => '周日',
_ => '',
};
}