844 lines
28 KiB
Dart
844 lines
28 KiB
Dart
/*
|
|
* 文件: home_card_carousel.dart
|
|
* 说明: 首页卡片式横向滚动组件。支持左右滑动、过渡动画和卡片布局。
|
|
* 作用: 提供美观的卡片信息流浏览体验。
|
|
* 作者: 前端工程师
|
|
* 更新时间: 2026-04-10
|
|
* 上次更新: 添加 Badge 显示"新"/"热"标签
|
|
*/
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:mom_kitchen/src/controllers/home_controller.dart';
|
|
import 'package:mom_kitchen/src/controllers/shopping_list_controller.dart';
|
|
import 'package:mom_kitchen/src/models/recipe/recipe_model.dart';
|
|
import 'package:mom_kitchen/src/models/shopping_item_model.dart';
|
|
import 'package:mom_kitchen/src/services/ui/theme_service.dart';
|
|
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
|
|
|
|
class HomeCardCarousel extends StatefulWidget {
|
|
const HomeCardCarousel({super.key});
|
|
|
|
@override
|
|
State<HomeCardCarousel> createState() => _HomeCardCarouselState();
|
|
}
|
|
|
|
class _HomeCardCarouselState extends State<HomeCardCarousel> {
|
|
late PageController _pageController;
|
|
final _currentIndex = 0.obs;
|
|
late double _viewportFraction;
|
|
late bool _lastIsLandscape;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_viewportFraction = 0.85;
|
|
_lastIsLandscape = false;
|
|
_pageController = PageController(
|
|
viewportFraction: _viewportFraction,
|
|
initialPage: 0,
|
|
);
|
|
}
|
|
|
|
void _updatePageController(bool isLandscape) {
|
|
if (_lastIsLandscape != isLandscape) {
|
|
_lastIsLandscape = isLandscape;
|
|
final newFraction = isLandscape ? 0.30 : 0.85;
|
|
|
|
if (_viewportFraction != newFraction) {
|
|
_viewportFraction = newFraction;
|
|
_pageController.dispose();
|
|
_pageController = PageController(
|
|
viewportFraction: _viewportFraction,
|
|
initialPage: _currentIndex.value,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final homeController = Get.find<HomeController>();
|
|
final themeService = Get.find<ThemeService>();
|
|
final isLandscape =
|
|
MediaQuery.of(context).orientation == Orientation.landscape;
|
|
|
|
_updatePageController(isLandscape);
|
|
|
|
return Obx(() {
|
|
final recipes = homeController.filteredRecipes;
|
|
|
|
if (homeController.isLoading.value) {
|
|
return Center(child: CupertinoActivityIndicator(radius: 30));
|
|
}
|
|
|
|
if (recipes.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text('🔍', style: TextStyle(fontSize: 48)),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'暂无菜谱',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: themeService.textColor.value,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'试试其他搜索或分类',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: themeService.textColor.value.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: themeService.backgroundColor.value.withValues(
|
|
alpha: 0.5,
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: themeService.textColor.value.withValues(alpha: 0.1),
|
|
),
|
|
),
|
|
child: CupertinoSearchTextField(
|
|
placeholder: '搜索菜谱...',
|
|
style: TextStyle(color: themeService.textColor.value),
|
|
placeholderStyle: TextStyle(
|
|
color: themeService.textColor.value.withValues(alpha: 0.5),
|
|
),
|
|
decoration: null,
|
|
onChanged: homeController.search,
|
|
),
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: Obx(() {
|
|
final isVertical =
|
|
themeService.cardScrollDirection.value ==
|
|
CardScrollDirection.vertical;
|
|
|
|
if (isVertical) {
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
itemCount: recipes.length,
|
|
itemBuilder: (context, index) {
|
|
final recipe = recipes[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: _buildVerticalRecipeCard(
|
|
recipe,
|
|
themeService,
|
|
homeController,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
return PageView.builder(
|
|
controller: _pageController,
|
|
onPageChanged: (index) {
|
|
_currentIndex.value = index;
|
|
},
|
|
itemCount: recipes.length,
|
|
itemBuilder: (context, index) {
|
|
final recipe = recipes[index];
|
|
final padding = isLandscape
|
|
? const EdgeInsets.symmetric(horizontal: 4, vertical: 8)
|
|
: const EdgeInsets.symmetric(horizontal: 8, vertical: 16);
|
|
|
|
return Padding(
|
|
padding: padding,
|
|
child: _buildRecipeCard(
|
|
recipe,
|
|
themeService,
|
|
homeController,
|
|
isCompact: isLandscape,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildRecipeCard(
|
|
RecipeModel recipe,
|
|
ThemeService themeService,
|
|
HomeController homeController, {
|
|
bool isCompact = false,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: () {},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: themeService.backgroundColor.value,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: themeService.textColor.value.withValues(alpha: 0.1),
|
|
width: 0.5,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: themeService.textColor.value.withValues(alpha: 0.1),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: isCompact
|
|
? _buildCompactRecipeCard(recipe, themeService, homeController)
|
|
: _buildFullRecipeCard(recipe, themeService, homeController),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVerticalRecipeCard(
|
|
RecipeModel recipe,
|
|
ThemeService themeService,
|
|
HomeController homeController,
|
|
) {
|
|
return GestureDetector(
|
|
onTap: () {},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: themeService.backgroundColor.value,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: themeService.textColor.value.withValues(alpha: 0.1),
|
|
width: 0.5,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: themeService.textColor.value.withValues(alpha: 0.08),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 72,
|
|
height: 72,
|
|
decoration: BoxDecoration(
|
|
color: themeService.primaryColor.value.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
CupertinoIcons.book,
|
|
size: 28,
|
|
color: themeService.primaryColor.value.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
recipe.title,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: themeService.textColor.value,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (recipe.intro != null && recipe.intro!.isNotEmpty)
|
|
Text(
|
|
recipe.intro!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: themeService.textColor.value.withAlpha(153),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 6),
|
|
_buildStatsRow(recipe, themeService),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (recipe.ingredients.isNotEmpty)
|
|
GestureDetector(
|
|
onTap: () => _addToShoppingList(recipe),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: themeService.primaryColor.value.withValues(
|
|
alpha: 0.1,
|
|
),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
CupertinoIcons.cart_badge_plus,
|
|
color: themeService.primaryColor.value,
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFullRecipeCard(
|
|
RecipeModel recipe,
|
|
ThemeService themeService,
|
|
HomeController homeController,
|
|
) {
|
|
final badgeInfo = _getRecipeBadge(recipe);
|
|
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Stack(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(20),
|
|
topRight: Radius.circular(20),
|
|
),
|
|
child: Container(
|
|
color: themeService.primaryColor.value.withValues(alpha: 0.1),
|
|
child: Center(
|
|
child: Icon(
|
|
CupertinoIcons.book,
|
|
size: 64,
|
|
color: themeService.primaryColor.value.withValues(
|
|
alpha: 0.6,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (badgeInfo != null)
|
|
Positioned(
|
|
top: 10,
|
|
left: 10,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: badgeInfo['color'],
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: (badgeInfo['color'] as Color).withValues(
|
|
alpha: 0.3,
|
|
),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
badgeInfo['label'],
|
|
style: const TextStyle(
|
|
color: CupertinoColors.white,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
flex: 2,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
recipe.title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: themeService.textColor.value,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
if (recipe.intro != null && recipe.intro!.isNotEmpty)
|
|
Text(
|
|
recipe.intro!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: themeService.textColor.value.withAlpha(153),
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
_buildStatsRow(recipe, themeService),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (recipe.ingredients.isNotEmpty)
|
|
GestureDetector(
|
|
onTap: () => _addToShoppingList(recipe),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: themeService.primaryColor.value
|
|
.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
CupertinoIcons.cart_badge_plus,
|
|
color: themeService.primaryColor.value,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
GestureDetector(
|
|
onTap: () {
|
|
ToastService.show(message: '${recipe.title} 👀');
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: themeService.primaryColor.value,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: themeService.primaryColor.value
|
|
.withValues(alpha: 0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
CupertinoIcons.eye,
|
|
color: CupertinoColors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
|
child: Obx(
|
|
() => SizedBox(
|
|
height: 32,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: homeController.categoryNames.length,
|
|
itemBuilder: (context, index) {
|
|
final name = homeController.categoryNames[index];
|
|
final isAll = index == 0;
|
|
final isSelected = isAll
|
|
? homeController.selectedCategory.value == null
|
|
: homeController.selectedCategory.value?.name == name;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 3),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
homeController.selectCategoryByName(isAll ? '' : name);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? themeService.primaryColor.value
|
|
: themeService.textColor.value.withValues(
|
|
alpha: 0.08,
|
|
),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? themeService.primaryColor.value
|
|
: themeService.textColor.value.withValues(
|
|
alpha: 0.15,
|
|
),
|
|
width: 0.5,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: isSelected
|
|
? CupertinoColors.white
|
|
: themeService.textColor.value,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Obx(
|
|
() => Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(
|
|
homeController.filteredRecipes.length,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 3),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: _currentIndex.value == index ? 18 : 6,
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: _currentIndex.value == index
|
|
? themeService.primaryColor.value
|
|
: themeService.textColor.value.withValues(
|
|
alpha: 0.25,
|
|
),
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactRecipeCard(
|
|
RecipeModel recipe,
|
|
ThemeService themeService,
|
|
HomeController homeController,
|
|
) {
|
|
final badgeInfo = _getRecipeBadge(recipe);
|
|
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Stack(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(20),
|
|
topRight: Radius.circular(20),
|
|
),
|
|
child: Container(
|
|
color: themeService.primaryColor.value.withValues(alpha: 0.1),
|
|
child: Center(
|
|
child: Icon(
|
|
CupertinoIcons.book,
|
|
size: 40,
|
|
color: themeService.primaryColor.value.withValues(
|
|
alpha: 0.6,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (badgeInfo != null)
|
|
Positioned(
|
|
top: 6,
|
|
left: 6,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: badgeInfo['color'],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
badgeInfo['label'],
|
|
style: const TextStyle(
|
|
color: CupertinoColors.white,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
flex: 2,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
recipe.title,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: themeService.textColor.value,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Expanded(child: _buildStatsRow(recipe, themeService)),
|
|
GestureDetector(
|
|
onTap: () {
|
|
ToastService.show(message: '${recipe.title} 👀');
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: themeService.primaryColor.value,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: themeService.primaryColor.value.withValues(
|
|
alpha: 0.3,
|
|
),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
CupertinoIcons.eye,
|
|
color: CupertinoColors.white,
|
|
size: 16,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
|
child: Obx(
|
|
() => SizedBox(
|
|
height: 26,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: homeController.categoryNames.length,
|
|
itemBuilder: (context, index) {
|
|
final name = homeController.categoryNames[index];
|
|
final isAll = index == 0;
|
|
final isSelected = isAll
|
|
? homeController.selectedCategory.value == null
|
|
: homeController.selectedCategory.value?.name == name;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
homeController.selectCategoryByName(isAll ? '' : name);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? themeService.primaryColor.value
|
|
: themeService.textColor.value.withValues(
|
|
alpha: 0.08,
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? themeService.primaryColor.value
|
|
: themeService.textColor.value.withValues(
|
|
alpha: 0.15,
|
|
),
|
|
width: 0.5,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w500,
|
|
color: isSelected
|
|
? CupertinoColors.white
|
|
: themeService.textColor.value,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsRow(RecipeModel recipe, ThemeService themeService) {
|
|
final stats = recipe.statistics;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.eye,
|
|
size: 14,
|
|
color: themeService.textColor.value.withValues(alpha: 0.5),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Text(
|
|
'${stats?.views ?? 0}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: themeService.textColor.value.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Icon(
|
|
CupertinoIcons.heart,
|
|
size: 14,
|
|
color: themeService.textColor.value.withValues(alpha: 0.5),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Text(
|
|
'${stats?.likes ?? 0}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: themeService.textColor.value.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Icon(
|
|
CupertinoIcons.star,
|
|
size: 14,
|
|
color: themeService.textColor.value.withValues(alpha: 0.5),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Text(
|
|
'${stats?.recommends ?? 0}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: themeService.textColor.value.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic>? _getRecipeBadge(RecipeModel recipe) {
|
|
final stats = recipe.statistics;
|
|
final createdAt = recipe.createdAt;
|
|
|
|
final bool isHot = (stats?.views ?? 0) > 1000 || (stats?.likes ?? 0) > 100;
|
|
|
|
bool isNew = false;
|
|
if (createdAt != null) {
|
|
try {
|
|
final created = DateTime.parse(createdAt);
|
|
final now = DateTime.now();
|
|
final diff = now.difference(created).inDays;
|
|
isNew = diff <= 7;
|
|
} catch (_) {}
|
|
}
|
|
|
|
if (isNew) {
|
|
return {'label': '✨ 新', 'color': const Color(0xFF34C759)};
|
|
}
|
|
|
|
if (isHot) {
|
|
return {'label': '🔥 热', 'color': const Color(0xFFFF3B30)};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
void _addToShoppingList(RecipeModel recipe) {
|
|
final ctrl = Get.find<ShoppingListController>();
|
|
final items = recipe.ingredients.map((ing) {
|
|
return ShoppingItemModel(
|
|
name: ing.name,
|
|
amount: ing.amount,
|
|
unit: ing.unit,
|
|
category: ShoppingCategory.other.name,
|
|
recipeId: recipe.id,
|
|
createdAt: DateTime.now().toIso8601String(),
|
|
);
|
|
}).toList();
|
|
ctrl.addItemsFromRecipe(recipe.id, recipe.title, items);
|
|
}
|
|
}
|