import 'package:flutter/material.dart'; import 'package:food_hub_app/api/recipe.dart'; import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/widgets/common/index.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:timelines_plus/timelines_plus.dart'; class RecipeTimeline extends StatefulWidget { const RecipeTimeline({super.key}); @override State createState() => _RecipeTimeline(); } class _RecipeTimeline extends State { List recordList = []; @override void initState() { super.initState(); refreshRecord(DateTime.now().year); } Future refreshRecord(int year) async { final result = await queryRecordApi("$year-01-01", "$year-12-31"); setState(() { recordList = result; }); } @override Widget build(BuildContext context) { return Column( children: [ YearSelector( initialYear: DateTime.now().year, minYear: 2000, maxYear: 2100, onYearChanged: (year) => refreshRecord(year) ), Expanded(child: timelineContainer(recordList)), ], ); } } Widget timelineContainer(List recordList) { if (recordList.isEmpty) { return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'); } else { return Padding( padding: EdgeInsets.all(10), child: Timeline.tileBuilder( theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0), builder: TimelineTileBuilder.connected( itemCount: recordList.length, connectorBuilder: (context, index, type) => Connector.solidLine(thickness: 2), indicatorBuilder: (context, index) { return Indicator.dot(size: 12.0); }, contentsBuilder: (context, index) { return TimelineCard(record: recordList[index]); }, ), ), ); } } class TimelineCard extends StatelessWidget { final Record record; const TimelineCard({super.key, required this.record}); Widget cardContent() { return Card( elevation: 0, color: Colors.white, child: Padding( padding: EdgeInsets.all(10), child: Column( children: [ Text(record.name, style: TextStyle(fontSize: 16)), const SizedBox(height: 5), Image.network( '${AppConfig.baseApiUrl}/${record.imageUrl}', width: double.infinity, height: 250, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) => errorImageContainer(250), ), ], ), ), ); } @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.only(left: 10, bottom: 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( record.date, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), cardContent(), ], ), ), ); } } class YearSelector extends StatefulWidget { final int initialYear; final int? minYear; final int? maxYear; final Function(int) onYearChanged; const YearSelector({ super.key, required this.initialYear, required this.onYearChanged, this.minYear, this.maxYear, }); @override State createState() => _YearSelectorState(); } // SingleTickerProviderStateMixin 动画控制器 class _YearSelectorState extends State with SingleTickerProviderStateMixin { late int _currentYear; // 用于动画效果 late AnimationController _animationController; late Animation _scaleAnimation; @override void initState() { super.initState(); _currentYear = widget.initialYear; // 初始化动画控制器 _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); // 缩放动画 _scaleAnimation = Tween(begin: 1.0, end: 1.1).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } /// 切换到上一年 void _previousYear() { if (widget.minYear == null || _currentYear > widget.minYear!) { _animateYearChange(() { setState(() { _currentYear--; }); widget.onYearChanged(_currentYear); }); } } /// 切换到下一年 void _nextYear() { if (widget.maxYear == null || _currentYear < widget.maxYear!) { _animateYearChange(() { setState(() { _currentYear++; }); widget.onYearChanged(_currentYear); }); } } /// 年份变化时的动画效果 void _animateYearChange(VoidCallback onComplete) { _animationController.forward().then((_) { onComplete(); _animationController.reverse(); }); } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TDButton( icon: Icons.chevron_left, size: TDButtonSize.small, type: TDButtonType.fill, shape: TDButtonShape.circle, theme: TDButtonTheme.primary, onTap: () => _previousYear(), ), SizedBox(width: 10), // 年份显示 AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale(scale: _scaleAnimation.value, child: child); }, child: Text( '$_currentYear', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor, ), ), ), SizedBox(width: 10), TDButton( icon: Icons.chevron_right, size: TDButtonSize.small, type: TDButtonType.fill, shape: TDButtonShape.circle, theme: TDButtonTheme.primary, onTap: () => _nextYear(), ), ], ); } }