- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
421 lines
14 KiB
Dart
421 lines
14 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 节气日历页面
|
||
/// 创建时间: 2026-05-02
|
||
/// 更新时间: 2026-05-02
|
||
/// 作用: 二十四节气展示 + 当前节气 + 诗词关联 + 季节切换
|
||
/// 上次更新: 修复 AppRadius 类型
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_animate/flutter_animate.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../core/theme/app_theme.dart';
|
||
import '../../core/theme/app_spacing.dart';
|
||
import '../../core/theme/app_typography.dart';
|
||
import '../../core/theme/app_radius.dart';
|
||
import 'solar_term_core.dart';
|
||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||
|
||
class SolarTermPage extends ConsumerWidget {
|
||
const SolarTermPage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final ext = AppTheme.ext(context);
|
||
final state = ref.watch(solarTermProvider);
|
||
|
||
return CupertinoPageScaffold(
|
||
backgroundColor: ext.bgPrimary,
|
||
navigationBar: CupertinoNavigationBar(
|
||
leading: const AdaptiveBackButton(),
|
||
middle: Text(
|
||
'🌿 节气日历',
|
||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||
),
|
||
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
|
||
border: null,
|
||
),
|
||
child: SafeArea(
|
||
child: ListView(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
children: [
|
||
_buildCurrentTermCard(ext, state),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildNextTermCard(ext, state),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildSeasonTabs(ext, ref, state),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildTermGrid(ext, state),
|
||
if (state.isTodayTerm && state.todayTerm != null) ...[
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildTodayTermBanner(ext, state.todayTerm!),
|
||
],
|
||
const SizedBox(height: AppSpacing.lg),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCurrentTermCard(AppThemeExtension ext, SolarTermState state) {
|
||
final term = state.currentTerm;
|
||
if (term == null) return const SizedBox.shrink();
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [
|
||
ext.accent.withValues(alpha: 0.15),
|
||
ext.accent.withValues(alpha: 0.05),
|
||
],
|
||
),
|
||
borderRadius: AppRadius.lgBorder,
|
||
border: Border.all(color: ext.accent.withValues(alpha: 0.2)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(
|
||
term.emoji,
|
||
style: const TextStyle(fontSize: 36),
|
||
).animate().scale(duration: 600.ms, curve: Curves.elasticOut),
|
||
const SizedBox(width: AppSpacing.md),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'当前节气',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
Text(
|
||
term.name,
|
||
style: AppTypography.title1.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.15),
|
||
borderRadius: AppRadius.smBorder,
|
||
),
|
||
child: Text(
|
||
'${term.season.label} ${term.season.emoji}',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.accent,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard.withValues(alpha: 0.6),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'「${term.poem}」',
|
||
style: AppTypography.body.copyWith(
|
||
color: ext.textPrimary,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'—— ${term.poemAuthor}',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
Text(
|
||
term.description,
|
||
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'民俗: ${term.customs}',
|
||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 400.ms);
|
||
}
|
||
|
||
Widget _buildNextTermCard(AppThemeExtension ext, SolarTermState state) {
|
||
final term = state.nextTerm;
|
||
if (term == null) return const SizedBox.shrink();
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard,
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.smBorder,
|
||
),
|
||
child: Center(
|
||
child: Text(term.emoji, style: const TextStyle(fontSize: 24)),
|
||
),
|
||
),
|
||
const SizedBox(width: AppSpacing.md),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'下一节气',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
Text(
|
||
term.name,
|
||
style: AppTypography.body.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.smBorder,
|
||
),
|
||
child: Text(
|
||
'${state.daysUntilNext}天后',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.accent,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSeasonTabs(
|
||
AppThemeExtension ext,
|
||
WidgetRef ref,
|
||
SolarTermState state,
|
||
) {
|
||
return Row(
|
||
children: SolarTermSeason.values.map((season) {
|
||
final isSelected = state.selectedSeason == season;
|
||
return Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => ref.read(solarTermProvider.notifier).setSeason(season),
|
||
child: Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
||
decoration: BoxDecoration(
|
||
color: isSelected
|
||
? ext.accent.withValues(alpha: 0.15)
|
||
: ext.bgCard,
|
||
borderRadius: AppRadius.smBorder,
|
||
border: isSelected
|
||
? Border.all(color: ext.accent.withValues(alpha: 0.3))
|
||
: null,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'${season.emoji} ${season.label}',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: isSelected ? ext.accent : ext.textSecondary,
|
||
fontWeight: isSelected
|
||
? FontWeight.w600
|
||
: FontWeight.normal,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
|
||
Widget _buildTermGrid(AppThemeExtension ext, SolarTermState state) {
|
||
final season =
|
||
state.selectedSeason ??
|
||
state.currentTerm?.season ??
|
||
SolarTermSeason.spring;
|
||
final terms = SolarTermService.getTermsBySeason(season);
|
||
|
||
return GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
mainAxisSpacing: AppSpacing.sm,
|
||
crossAxisSpacing: AppSpacing.sm,
|
||
childAspectRatio: 1.4,
|
||
),
|
||
itemCount: terms.length,
|
||
itemBuilder: (context, index) {
|
||
final term = terms[index];
|
||
final isCurrent = state.currentTerm?.name == term.name;
|
||
return _buildTermCard(ext, term, isCurrent, index);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildTermCard(
|
||
AppThemeExtension ext,
|
||
SolarTermInfo term,
|
||
bool isCurrent,
|
||
int index,
|
||
) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: isCurrent ? ext.accent.withValues(alpha: 0.12) : ext.bgCard,
|
||
borderRadius: AppRadius.mdBorder,
|
||
border: isCurrent
|
||
? Border.all(color: ext.accent.withValues(alpha: 0.3))
|
||
: null,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(term.emoji, style: const TextStyle(fontSize: 20)),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
term.name,
|
||
style: AppTypography.body.copyWith(
|
||
color: isCurrent ? ext.accent : ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
if (isCurrent) ...[
|
||
const Spacer(),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 4,
|
||
vertical: 1,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'当前',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.accent,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
term.poem.length > 12
|
||
? '${term.poem.substring(0, 12)}…'
|
||
: term.poem,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
term.description,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textHint,
|
||
fontSize: 11,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
).animate().fadeIn(duration: 300.ms, delay: (index * 50).ms);
|
||
}
|
||
|
||
Widget _buildTodayTermBanner(AppThemeExtension ext, SolarTermInfo term) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
ext.accent.withValues(alpha: 0.2),
|
||
ext.accent.withValues(alpha: 0.05),
|
||
],
|
||
),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text('🎉', style: TextStyle(fontSize: 24)),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'今日是${term.name}!',
|
||
style: AppTypography.body.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
Text(
|
||
SolarTermService.getSeasonGreeting(term.season),
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
).animate().shimmer(duration: 2000.ms);
|
||
}
|
||
}
|