1835 lines
64 KiB
Dart
1835 lines
64 KiB
Dart
/*
|
||
* 文件: discover_page.dart
|
||
* 名称: 发现页面
|
||
* 作用: iOS 26 风格的发现页面,整合热门排行+今天吃什么+搜索,支持下拉进入工具中心
|
||
* 更新: 2026-04-10 购物清单入口添加 Badge 显示数量
|
||
* 更新: 2026-04-13 推荐tab新增口味/工艺标签入口,修复分类导航
|
||
* 更新: 2026-04-16 新增下拉进入工具中心功能,移除顶部4个固定按钮至收藏页
|
||
*/
|
||
|
||
import 'dart:ui';
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'package:mom_kitchen/src/config/design_tokens.dart';
|
||
import 'package:mom_kitchen/src/config/app_routes.dart';
|
||
import 'package:mom_kitchen/src/models/recipe/category_model.dart';
|
||
import 'package:mom_kitchen/src/models/recipe/tag_model.dart';
|
||
import 'package:mom_kitchen/src/repositories/recipe_repository.dart';
|
||
import 'package:mom_kitchen/src/widgets/glass/glass_search_bar.dart';
|
||
import 'package:mom_kitchen/src/widgets/glass/glass_segmented_control.dart';
|
||
import 'package:mom_kitchen/src/controllers/feed/hot_controller.dart';
|
||
import 'package:mom_kitchen/src/controllers/tools/tools_controller.dart';
|
||
import 'package:mom_kitchen/src/models/tool_item_model.dart';
|
||
import 'package:mom_kitchen/src/services/ui/toast_service.dart';
|
||
import 'package:mom_kitchen/src/repositories/hot_repository.dart' as repo;
|
||
|
||
class DiscoverPage extends StatefulWidget {
|
||
const DiscoverPage({super.key});
|
||
|
||
@override
|
||
State<DiscoverPage> createState() => _DiscoverPageState();
|
||
}
|
||
|
||
class _DiscoverPageState extends State<DiscoverPage>
|
||
with SingleTickerProviderStateMixin {
|
||
int _segmentIndex = 0;
|
||
int _recommendTypeIndex = 0;
|
||
int _recommendSubIndex = 0;
|
||
late HotController _hotController;
|
||
final RecipeRepository _recipeRepo = RecipeRepository();
|
||
List<CategoryModel> _topCategories = [];
|
||
List<CategoryModel> _ingredientCategories = [];
|
||
List<TagModel> _tasteTags = [];
|
||
List<TagModel> _cookingTags = [];
|
||
bool _isLoadingCategories = true;
|
||
|
||
ToolsController? _toolsController;
|
||
|
||
static const double _pullThreshold = 80.0;
|
||
|
||
double _pullOffset = 0.0;
|
||
bool _isPanelOpen = false;
|
||
bool _isPulling = false;
|
||
double _panelHeight = 0.0;
|
||
|
||
late final AnimationController _panelController;
|
||
late final Animation<double> _panelAnimation;
|
||
AnimationStatusListener? _panelStatusListener;
|
||
|
||
final ScrollController _scrollController = ScrollController();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_hotController = Get.find<HotController>();
|
||
_loadCategories();
|
||
_initToolsController();
|
||
|
||
_panelController = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(milliseconds: 350),
|
||
);
|
||
_panelAnimation = CurvedAnimation(
|
||
parent: _panelController,
|
||
curve: Curves.easeOutCubic,
|
||
reverseCurve: Curves.easeInCubic,
|
||
);
|
||
_panelStatusListener = (status) {
|
||
if (status == AnimationStatus.dismissed) {
|
||
setState(() => _isPanelOpen = false);
|
||
} else if (status == AnimationStatus.completed) {
|
||
setState(() => _isPanelOpen = true);
|
||
}
|
||
};
|
||
_panelController.addStatusListener(_panelStatusListener!);
|
||
}
|
||
|
||
void _initToolsController() {
|
||
try {
|
||
if (Get.isRegistered<ToolsController>()) {
|
||
_toolsController = Get.find<ToolsController>();
|
||
}
|
||
} catch (e) {
|
||
debugPrint('DiscoverPage: ToolsController init error: $e');
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
if (_panelStatusListener != null) {
|
||
_panelController.removeStatusListener(_panelStatusListener!);
|
||
}
|
||
_panelController.dispose();
|
||
_scrollController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadCategories() async {
|
||
try {
|
||
final categories = await _recipeRepo.fetchCategories();
|
||
final ingredientCategories = await _recipeRepo.fetchCategories(
|
||
type: 'ingredient',
|
||
);
|
||
final tasteTags = await _recipeRepo.fetchTasteTags();
|
||
final cookingTags = await _recipeRepo.fetchCookingTags();
|
||
if (mounted) {
|
||
setState(() {
|
||
_topCategories = categories;
|
||
_ingredientCategories = ingredientCategories;
|
||
_tasteTags = tasteTags;
|
||
_cookingTags = cookingTags;
|
||
_isLoadingCategories = false;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
debugPrint('DiscoverPage loadCategories error: $e');
|
||
if (mounted) setState(() => _isLoadingCategories = false);
|
||
}
|
||
}
|
||
|
||
void _openPanel() {
|
||
_panelController.forward();
|
||
}
|
||
|
||
void _closePanel() {
|
||
_panelController.reverse();
|
||
}
|
||
|
||
bool _handleScrollNotification(ScrollNotification notification) {
|
||
if (_isPanelOpen) return false;
|
||
|
||
if (notification is OverscrollNotification) {
|
||
if (notification.overscroll < 0) {
|
||
_isPulling = true;
|
||
setState(() {
|
||
_pullOffset = (_pullOffset + (-notification.overscroll) * 0.5).clamp(
|
||
0.0,
|
||
_panelMaxHeight,
|
||
);
|
||
});
|
||
if (_pullOffset >= _pullThreshold) {
|
||
_pullOffset = 0;
|
||
_isPulling = false;
|
||
_openPanel();
|
||
}
|
||
return true;
|
||
}
|
||
} else if (notification is ScrollEndNotification) {
|
||
if (_pullOffset > 0 && _pullOffset < _pullThreshold) {
|
||
setState(() => _pullOffset = 0);
|
||
}
|
||
_isPulling = false;
|
||
} else if (notification is ScrollUpdateNotification) {
|
||
if (_isPulling &&
|
||
notification.scrollDelta != null &&
|
||
notification.scrollDelta! > 0) {
|
||
setState(() {
|
||
_pullOffset = (_pullOffset - notification.scrollDelta! * 0.5).clamp(
|
||
0.0,
|
||
_panelMaxHeight,
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark;
|
||
|
||
return CupertinoPageScaffold(
|
||
backgroundColor: isDark
|
||
? DarkDesignTokens.background
|
||
: DesignTokens.background,
|
||
child: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
vertical: DesignTokens.space3,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
'发现',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXxl,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Text(
|
||
'下拉查看更多工具',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
),
|
||
const Spacer(),
|
||
],
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
),
|
||
child: GlassSearchBar(
|
||
readOnly: true,
|
||
onTap: () {
|
||
Get.toNamed(AppRoutes.search);
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
_buildToolsBar(isDark),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
),
|
||
child: GlassSegmentedControl(
|
||
segments: const [
|
||
GlassSegment(label: '🔥 热门'),
|
||
GlassSegment(label: '🎲 今天吃什么'),
|
||
GlassSegment(label: '⭐ 推荐'),
|
||
],
|
||
selectedIndex: _segmentIndex,
|
||
onChanged: (i) {
|
||
setState(() => _segmentIndex = i);
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Expanded(
|
||
child: NotificationListener<ScrollNotification>(
|
||
onNotification: _handleScrollNotification,
|
||
child: CustomScrollView(
|
||
controller: _scrollController,
|
||
physics: const ClampingScrollPhysics(
|
||
parent: AlwaysScrollableScrollPhysics(),
|
||
),
|
||
slivers: [
|
||
SliverToBoxAdapter(child: _buildPullHint(isDark)),
|
||
SliverToBoxAdapter(child: _buildSegmentContent(isDark)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
_buildToolsPanel(isDark),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildToolsBar(bool isDark) {
|
||
if (_toolsController == null) return const SizedBox.shrink();
|
||
|
||
return Obx(() {
|
||
final tools = _toolsController!.frequentTools;
|
||
if (tools.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return Container(
|
||
height: 90,
|
||
margin: const EdgeInsets.only(bottom: DesignTokens.space2),
|
||
child: ListView.separated(
|
||
scrollDirection: Axis.horizontal,
|
||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
|
||
itemCount: tools.length + 1,
|
||
separatorBuilder: (context, index) =>
|
||
const SizedBox(width: DesignTokens.space2),
|
||
itemBuilder: (context, index) {
|
||
if (index == tools.length) {
|
||
return _buildMoreToolsCard(isDark);
|
||
}
|
||
return _buildToolShortcut(tools[index], isDark);
|
||
},
|
||
),
|
||
);
|
||
});
|
||
}
|
||
|
||
Widget _buildToolShortcut(ToolItem tool, bool isDark) {
|
||
return GestureDetector(
|
||
onTap: () => _navigateToTool(tool),
|
||
child: ClipRRect(
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
child: BackdropFilter(
|
||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||
child: Container(
|
||
width: 72,
|
||
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space2),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: DesignTokens.card.withValues(alpha: 0.75),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(alpha: 0.12),
|
||
),
|
||
),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Stack(
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(
|
||
alpha: 0.1,
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Center(
|
||
child: Text(tool.icon, style: TextStyle(fontSize: 24)),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 0,
|
||
right: 0,
|
||
child: Container(
|
||
width: 8,
|
||
height: 8,
|
||
decoration: BoxDecoration(
|
||
color: tool.needsNetwork
|
||
? DesignTokens.green
|
||
: DesignTokens.dynamicPrimary,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
tool.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMoreToolsCard(bool isDark) {
|
||
return GestureDetector(
|
||
onTap: _openPanel,
|
||
child: ClipRRect(
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
child: BackdropFilter(
|
||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||
child: Container(
|
||
width: 72,
|
||
padding: EdgeInsets.symmetric(vertical: DesignTokens.space2),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DesignTokens.dynamicPrimary.withValues(alpha: 0.08)
|
||
: DesignTokens.primaryLight.withValues(alpha: 0.7),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.25),
|
||
),
|
||
),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Center(
|
||
child: Text('🛠️', style: TextStyle(fontSize: 24)),
|
||
),
|
||
),
|
||
SizedBox(height: 6),
|
||
Text(
|
||
'更多',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _navigateToTool(ToolItem tool) {
|
||
_toolsController?.recordUsage(tool.id);
|
||
if (tool.route.isNotEmpty) {
|
||
Get.toNamed(tool.route);
|
||
}
|
||
}
|
||
|
||
Widget _buildPullHint(bool isDark) {
|
||
final showHint = _pullOffset > 0 && !_isPanelOpen;
|
||
if (!showHint) return const SizedBox.shrink();
|
||
|
||
final progress = (_pullOffset / _pullThreshold).clamp(0.0, 1.0);
|
||
return Container(
|
||
height: 40 + _pullOffset * 0.3,
|
||
alignment: Alignment.center,
|
||
child: Opacity(
|
||
opacity: progress,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.chevron_compact_up,
|
||
size: 18,
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: progress),
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
progress >= 1.0 ? '松开进入工具中心' : '下拉查看更多工具',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: progress),
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildToolsPanel(bool isDark) {
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
if (_panelHeight != screenHeight) {
|
||
_panelHeight = screenHeight;
|
||
}
|
||
|
||
return AnimatedBuilder(
|
||
animation: _panelAnimation,
|
||
builder: (context, child) {
|
||
final value = _panelAnimation.value;
|
||
if (value <= 0 && !_isPanelOpen) return const SizedBox.shrink();
|
||
|
||
return Stack(
|
||
children: [
|
||
GestureDetector(
|
||
onTap: _closePanel,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Container(
|
||
color: Colors.black.withValues(alpha: 0.5 * value),
|
||
),
|
||
),
|
||
Positioned(
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
child: Transform.translate(
|
||
offset: Offset(0, -_panelHeight * (1 - value)),
|
||
child: Container(
|
||
height: _panelHeight,
|
||
clipBehavior: Clip.hardEdge,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.background
|
||
: DesignTokens.background,
|
||
borderRadius: BorderRadius.only(
|
||
bottomLeft: Radius.circular(DesignTokens.radiusLg),
|
||
bottomRight: Radius.circular(DesignTokens.radiusLg),
|
||
),
|
||
),
|
||
child: SafeArea(
|
||
top: false,
|
||
child: Column(
|
||
children: [
|
||
_buildPanelDragHandle(isDark),
|
||
Expanded(
|
||
child: ListView(
|
||
physics: const BouncingScrollPhysics(),
|
||
padding: EdgeInsets.zero,
|
||
children: [
|
||
_buildPanelBasicInfo(isDark),
|
||
_buildPanelFrequentTools(isDark),
|
||
_buildPanelAllTools(isDark),
|
||
_buildPanelBrowseHistory(isDark),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
],
|
||
),
|
||
),
|
||
_buildPanelBottomActions(isDark),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildPanelDragHandle(bool isDark) {
|
||
return GestureDetector(
|
||
onVerticalDragEnd: (details) {
|
||
if (details.primaryVelocity != null && details.primaryVelocity! > 300) {
|
||
_closePanel();
|
||
}
|
||
},
|
||
behavior: HitTestBehavior.translucent,
|
||
child: Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(vertical: DesignTokens.space2),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 5,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.text3.withValues(alpha: 0.4)
|
||
: DesignTokens.text3.withValues(alpha: 0.25),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space1),
|
||
Text(
|
||
'下滑关闭',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3.withValues(alpha: 0.5),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPanelBasicInfo(bool isDark) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(
|
||
DesignTokens.space4,
|
||
DesignTokens.space2,
|
||
DesignTokens.space4,
|
||
DesignTokens.space3,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [
|
||
DesignTokens.dynamicPrimary.withValues(alpha: 0.15),
|
||
DesignTokens.secondary.withValues(alpha: 0.1),
|
||
],
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Center(child: Text('🛠️', style: TextStyle(fontSize: 24))),
|
||
),
|
||
const SizedBox(width: DesignTokens.space3),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'工具中心',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXxl,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
Text(
|
||
'发现更多烹饪好帮手',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Obx(() {
|
||
if (_toolsController == null) return const SizedBox.shrink();
|
||
final count = _toolsController!.tools.length;
|
||
return Container(
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space2 + 2,
|
||
vertical: DesignTokens.space1 + 1,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.1),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Text(
|
||
'$count 个工具',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
fontWeight: FontWeight.w600,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPanelFrequentTools(bool isDark) {
|
||
if (_toolsController == null) return const SizedBox.shrink();
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'常用工具',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.toNamed('/tools-center');
|
||
},
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
'更多',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: DesignTokens.dynamicPrimary,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
Icon(
|
||
CupertinoIcons.chevron_right,
|
||
size: 14,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(() {
|
||
final frequent = _toolsController!.frequentTools;
|
||
if (frequent.isEmpty) return const SizedBox.shrink();
|
||
return GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 4,
|
||
crossAxisSpacing: DesignTokens.space3,
|
||
mainAxisSpacing: DesignTokens.space3,
|
||
childAspectRatio: 0.85,
|
||
),
|
||
itemCount: frequent.length.clamp(0, 8),
|
||
itemBuilder: (context, index) {
|
||
final tool = frequent[index];
|
||
return _buildToolGridItem(tool, isDark);
|
||
},
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildToolGridItem(ToolItem tool, bool isDark) {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
_closePanel();
|
||
_navigateToTool(tool);
|
||
},
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [
|
||
DesignTokens.dynamicPrimary.withValues(alpha: 0.12),
|
||
DesignTokens.secondary.withValues(alpha: 0.06),
|
||
],
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Center(child: Text(tool.icon, fontSize: 24)),
|
||
),
|
||
const SizedBox(height: DesignTokens.space1 + 2),
|
||
Text(
|
||
tool.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
fontWeight: FontWeight.w500,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
if (tool.usageCount > 0)
|
||
Text(
|
||
'${tool.usageCount}次',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3.withValues(alpha: 0.6),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPanelAllTools(bool isDark) {
|
||
if (_toolsController == null) return const SizedBox.shrink();
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(
|
||
left: DesignTokens.space4,
|
||
right: DesignTokens.space4,
|
||
top: DesignTokens.space4,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'所有工具',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Obx(() {
|
||
final tools = _toolsController!.tools;
|
||
final groups = <String, List<ToolItem>>{};
|
||
for (final t in tools) {
|
||
groups.putIfAbsent(t.category, () => []).add(t);
|
||
}
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: groups.entries.map((entry) {
|
||
final category = entry.key;
|
||
final items = entry.value;
|
||
final info = _getCategoryStyle(category);
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: DesignTokens.space3),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(info['icon'], style: TextStyle(fontSize: 16)),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Text(
|
||
info['name'],
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Text(
|
||
'${items.length}个',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 4,
|
||
crossAxisSpacing: DesignTokens.space3,
|
||
mainAxisSpacing: DesignTokens.space3,
|
||
childAspectRatio: 0.85,
|
||
),
|
||
itemCount: items.length,
|
||
itemBuilder: (context, index) =>
|
||
_buildToolGridItem(items[index], isDark),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Map<String, dynamic> _getCategoryStyle(String category) {
|
||
const map = {
|
||
'cooking': {'name': '烹饪助手', 'icon': '🍳'},
|
||
'health': {'name': '健康营养', 'icon': '💊'},
|
||
'data': {'name': '数据查询', 'icon': '📊'},
|
||
'planning': {'name': '规划管理', 'icon': '📅'},
|
||
};
|
||
return map[category] ?? {'name': category, 'icon': '📦'};
|
||
}
|
||
|
||
Widget _buildPanelBrowseHistory(bool isDark) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(
|
||
left: DesignTokens.space4,
|
||
right: DesignTokens.space4,
|
||
top: DesignTokens.space4,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'浏览记录',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w700,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () => Get.toNamed('/favorites'),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
'更多',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: DesignTokens.dynamicPrimary,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
Icon(
|
||
CupertinoIcons.chevron_right,
|
||
size: 14,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
SizedBox(
|
||
height: 120,
|
||
child: FutureBuilder<List<Map<String, dynamic>>>(
|
||
future: _loadBrowseHistory(),
|
||
builder: (context, snapshot) {
|
||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.clock,
|
||
size: 32,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3.withValues(alpha: 0.4),
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Text(
|
||
'暂无浏览记录',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3.withValues(alpha: 0.5),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
final history = snapshot.data!;
|
||
return ListView.separated(
|
||
scrollDirection: Axis.horizontal,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space1,
|
||
),
|
||
separatorBuilder: (_, __) =>
|
||
const SizedBox(width: DesignTokens.space2),
|
||
itemCount: history.length,
|
||
itemBuilder: (context, index) {
|
||
final item = history[index];
|
||
return GestureDetector(
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.toNamed(
|
||
'/recipe-detail',
|
||
arguments: '${item['id']}',
|
||
);
|
||
},
|
||
child: Container(
|
||
width: 140,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.card
|
||
: DesignTokens.card.withValues(alpha: 0.6),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Expanded(
|
||
flex: 3,
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.vertical(
|
||
top: DesignTokens.radiusMd,
|
||
),
|
||
child: Container(
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
DesignTokens.dynamicPrimary.withValues(
|
||
alpha: 0.15,
|
||
),
|
||
DesignTokens.secondary.withValues(
|
||
alpha: 0.08,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
child: Center(
|
||
child: Text('🍽️', fontSize: 28),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(
|
||
DesignTokens.space2,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
item['name'] ?? '',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
Text(
|
||
item['time'] ?? '',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3.withValues(
|
||
alpha: 0.6,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<List<Map<String, dynamic>>> _loadBrowseHistory() async {
|
||
try {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final raw = prefs.getStringList('browse_history') ?? [];
|
||
return raw.map((e) {
|
||
final parts = e.split('|');
|
||
return {
|
||
'id': parts.length > 0 ? parts[0] : '',
|
||
'name': parts.length > 1 ? parts[1] : '',
|
||
'time': parts.length > 2 ? parts[2] : '',
|
||
};
|
||
}).toList();
|
||
} catch (_) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
Widget _buildPanelBottomActions(bool isDark) {
|
||
return Container(
|
||
padding: EdgeInsets.fromLTRB(
|
||
DesignTokens.space4,
|
||
DesignTokens.space3,
|
||
DesignTokens.space4,
|
||
DesignTokens.space4 + MediaQuery.of(context).padding.bottom,
|
||
),
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
top: BorderSide(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(alpha: 0.08),
|
||
),
|
||
),
|
||
),
|
||
child: SafeArea(
|
||
top: false,
|
||
child: Row(
|
||
children: [
|
||
_buildBottomActionItem(
|
||
icon: CupertinoIcons.house,
|
||
label: '首页',
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.offAllNamed('/home');
|
||
},
|
||
isDark: isDark,
|
||
),
|
||
_buildBottomActionItem(
|
||
icon: CupertinoIcons.heart,
|
||
label: '收藏',
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.toNamed('/favorites');
|
||
},
|
||
isDark: isDark,
|
||
),
|
||
_buildBottomActionItem(
|
||
icon: CupertinoIcons.gear_alt,
|
||
label: '设置',
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.toNamed('/settings');
|
||
},
|
||
isDark: isDark,
|
||
),
|
||
_buildBottomActionItem(
|
||
icon: CupertinoIcons.info_circle,
|
||
label: '关于',
|
||
onTap: () {
|
||
_closePanel();
|
||
Get.toNamed('/about');
|
||
},
|
||
isDark: isDark,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildBottomActionItem({
|
||
required IconData icon,
|
||
required String label,
|
||
required VoidCallback onTap,
|
||
required bool isDark,
|
||
}) {
|
||
return Expanded(
|
||
child: GestureDetector(
|
||
onTap: onTap,
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: DesignTokens.text3.withValues(alpha: 0.06),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
),
|
||
child: Icon(
|
||
icon,
|
||
size: 20,
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space1),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSegmentContent(bool isDark) {
|
||
switch (_segmentIndex) {
|
||
case 0:
|
||
return _buildHotSection(isDark);
|
||
case 1:
|
||
return _buildWhatToEatSection(isDark);
|
||
case 2:
|
||
return _buildRecommendSection(isDark);
|
||
default:
|
||
return const SizedBox.shrink();
|
||
}
|
||
}
|
||
|
||
Widget _buildHotSection(bool isDark) {
|
||
return Obx(() {
|
||
final List<repo.HotItem> hotList = _hotController.hotList;
|
||
final isLoading = _hotController.isLoading.value;
|
||
|
||
if (isLoading) {
|
||
return const SizedBox(
|
||
height: 300,
|
||
child: Center(child: CupertinoActivityIndicator()),
|
||
);
|
||
}
|
||
|
||
if (hotList.isEmpty) {
|
||
return SizedBox(
|
||
height: 300,
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.flame,
|
||
size: 48,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Text(
|
||
'暂无热门数据',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return SizedBox(
|
||
height: MediaQuery.of(context).size.height * 0.55,
|
||
child: Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
),
|
||
child: GlassSegmentedControl(
|
||
segments: HotController.periodNames
|
||
.map((name) => GlassSegment(label: name))
|
||
.toList(),
|
||
selectedIndex: _hotController.currentPeriod.value.index,
|
||
onChanged: (i) {
|
||
_hotController.switchPeriod(HotPeriod.values[i]);
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Expanded(
|
||
child: ListView.builder(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
),
|
||
itemCount: hotList.length,
|
||
itemBuilder: (context, index) {
|
||
final recipe = hotList[index];
|
||
return Padding(
|
||
padding: const EdgeInsets.only(
|
||
bottom: DesignTokens.space2 + 2,
|
||
),
|
||
child: Dismissible(
|
||
key: ValueKey('hot_${recipe.id}_$index'),
|
||
direction: DismissDirection.horizontal,
|
||
confirmDismiss: (direction) async {
|
||
if (direction == DismissDirection.endToStart) {
|
||
Get.toNamed(
|
||
'/recipe-detail',
|
||
arguments: '${recipe.id}',
|
||
);
|
||
return false;
|
||
} else {
|
||
return true;
|
||
}
|
||
},
|
||
onDismissed: (direction) {
|
||
if (direction == DismissDirection.startToEnd) {
|
||
_showQuickActions(recipe, isDark);
|
||
}
|
||
},
|
||
background: Container(
|
||
alignment: Alignment.centerLeft,
|
||
padding: EdgeInsets.only(left: 20),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.heart_fill,
|
||
color: CupertinoColors.white,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'收藏',
|
||
style: TextStyle(
|
||
color: CupertinoColors.white,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
secondaryBackground: Container(
|
||
alignment: Alignment.centerRight,
|
||
padding: const EdgeInsets.only(right: 20),
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.green,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
'查看详情',
|
||
style: TextStyle(
|
||
color: CupertinoColors.white,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Icon(
|
||
CupertinoIcons.eye,
|
||
color: CupertinoColors.white,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
child: GestureDetector(
|
||
onTap: () {
|
||
Get.toNamed(
|
||
'/recipe-detail',
|
||
arguments: '${recipe.id}',
|
||
);
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.all(DesignTokens.space3),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.card
|
||
: DesignTokens.card,
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
boxShadow: DesignTokens.shadowsSm,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 28,
|
||
height: 28,
|
||
decoration: BoxDecoration(
|
||
color: index < 3
|
||
? DesignTokens.orange.withValues(
|
||
alpha: 0.15,
|
||
)
|
||
: DesignTokens.text3.withValues(
|
||
alpha: 0.1,
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'${index + 1}',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w700,
|
||
color: index < 3
|
||
? DesignTokens.orange
|
||
: (isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space3),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
recipe.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
fontWeight: FontWeight.w500,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'${_hotController.sortByName}: ${recipe.count}',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
color: isDark
|
||
? DarkDesignTokens.text2
|
||
: DesignTokens.text2,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Icon(
|
||
CupertinoIcons.chevron_forward,
|
||
size: 16,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
});
|
||
}
|
||
|
||
Widget _buildWhatToEatSection(bool isDark) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
width: 100,
|
||
height: 100,
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.primaryLight,
|
||
borderRadius: DesignTokens.borderRadiusXl,
|
||
),
|
||
child: Icon(
|
||
CupertinoIcons.shuffle,
|
||
size: 44,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space4),
|
||
Text(
|
||
'不知道吃什么?',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXl,
|
||
fontWeight: FontWeight.w600,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Text(
|
||
'让小妈厨房帮你决定',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space6),
|
||
SizedBox(
|
||
width: 200,
|
||
child: CupertinoButton.filled(
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
onPressed: () {
|
||
Get.toNamed('/what-to-eat');
|
||
},
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(CupertinoIcons.shuffle, size: 20),
|
||
SizedBox(width: DesignTokens.space2),
|
||
Text('随机推荐'),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
SizedBox(
|
||
width: 200,
|
||
child: CupertinoButton(
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
color: DesignTokens.primaryLight,
|
||
onPressed: () {
|
||
Get.toNamed('/what-to-eat');
|
||
},
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.lightbulb,
|
||
size: 20,
|
||
color: DesignTokens.dynamicPrimary,
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Text(
|
||
'浏览推荐',
|
||
style: TextStyle(color: DesignTokens.dynamicPrimary),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildRecommendSection(bool isDark) {
|
||
if (_isLoadingCategories) {
|
||
return const SizedBox(
|
||
height: 300,
|
||
child: Center(child: CupertinoActivityIndicator()),
|
||
);
|
||
}
|
||
|
||
return SizedBox(
|
||
height: MediaQuery.of(context).size.height * 0.55,
|
||
child: Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
),
|
||
child: GlassSegmentedControl(
|
||
segments: const [
|
||
GlassSegment(label: '🍳 菜系'),
|
||
GlassSegment(label: '🥬 食材'),
|
||
GlassSegment(label: '🏷️ 标签'),
|
||
],
|
||
selectedIndex: _recommendSubIndex,
|
||
onChanged: (i) {
|
||
setState(() => _recommendSubIndex = i);
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Expanded(
|
||
child: _recommendSubIndex == 0
|
||
? _buildCategoryGrid(_topCategories, isDark, 'category')
|
||
: _recommendSubIndex == 1
|
||
? _buildCategoryGrid(
|
||
_ingredientCategories,
|
||
isDark,
|
||
'ingredient',
|
||
)
|
||
: _buildTagContent(isDark),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCategoryGrid(
|
||
List<CategoryModel> categories,
|
||
bool isDark,
|
||
String type,
|
||
) {
|
||
if (categories.isEmpty) {
|
||
return Center(
|
||
child: Text(
|
||
'暂无数据',
|
||
style: TextStyle(
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return GridView.builder(
|
||
padding: const EdgeInsets.all(DesignTokens.space4),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 4,
|
||
mainAxisSpacing: DesignTokens.space3,
|
||
crossAxisSpacing: DesignTokens.space3,
|
||
childAspectRatio: 0.75,
|
||
),
|
||
itemCount: categories.length,
|
||
itemBuilder: (context, index) {
|
||
final category = categories[index];
|
||
return GestureDetector(
|
||
onTap: () {
|
||
Get.toNamed(
|
||
'/category-browse',
|
||
arguments: {
|
||
'category': category,
|
||
'title': category.name,
|
||
'isIngredient': type == 'ingredient',
|
||
'loadRecipesDirectly': true,
|
||
},
|
||
);
|
||
},
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
width: 56,
|
||
height: 56,
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(alpha: 0.12),
|
||
borderRadius: DesignTokens.borderRadiusLg,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
category.icon ?? '🍽️',
|
||
style: const TextStyle(fontSize: 28),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space2),
|
||
Text(
|
||
category.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildTagContent(bool isDark) {
|
||
final tags = _recommendTypeIndex == 0 ? _tasteTags : _cookingTags;
|
||
if (tags.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.tag,
|
||
size: 48,
|
||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Text(
|
||
'暂无标签数据',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontMd,
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
|
||
child: GlassSegmentedControl(
|
||
segments: [
|
||
GlassSegment(label: '👅 口味'),
|
||
GlassSegment(label: '🔥 工艺'),
|
||
],
|
||
selectedIndex: _recommendTypeIndex,
|
||
onChanged: (i) {
|
||
setState(() => _recommendTypeIndex = i);
|
||
},
|
||
),
|
||
),
|
||
const SizedBox(height: DesignTokens.space3),
|
||
Expanded(
|
||
child: ListView.separated(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space4,
|
||
vertical: DesignTokens.space1,
|
||
),
|
||
itemCount: tags.length,
|
||
separatorBuilder: (context, index) =>
|
||
const SizedBox(height: DesignTokens.space2),
|
||
itemBuilder: (context, index) {
|
||
final tag = tags[index];
|
||
return GestureDetector(
|
||
onTap: () {
|
||
Get.toNamed(
|
||
'/tag-recipe-list',
|
||
arguments: {
|
||
'tagName': tag.name,
|
||
'tagId': tag.id,
|
||
'tagType': _recommendTypeIndex == 0 ? 'taste' : 'process',
|
||
},
|
||
);
|
||
},
|
||
child: ClipRRect(
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
child: BackdropFilter(
|
||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||
child: Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: DesignTokens.space3,
|
||
vertical: DesignTokens.space3,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? DarkDesignTokens.glass
|
||
: DesignTokens.card.withValues(alpha: 0.6),
|
||
borderRadius: DesignTokens.borderRadiusMd,
|
||
border: Border.all(
|
||
color: isDark
|
||
? DarkDesignTokens.glassBorder
|
||
: DesignTokens.text3.withValues(alpha: 0.08),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: DesignTokens.dynamicPrimary.withValues(
|
||
alpha: 0.1,
|
||
),
|
||
borderRadius: DesignTokens.borderRadiusSm,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'🏷️',
|
||
style: TextStyle(fontSize: 18),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space3),
|
||
Expanded(
|
||
child: Text(
|
||
tag.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontSm,
|
||
fontWeight: FontWeight.w500,
|
||
color: isDark
|
||
? DarkDesignTokens.text1
|
||
: DesignTokens.text1,
|
||
),
|
||
),
|
||
),
|
||
if (tag.count != null && tag.count! > 0)
|
||
Text(
|
||
'${tag.count}',
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontXs,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
),
|
||
const SizedBox(width: DesignTokens.space2),
|
||
Icon(
|
||
CupertinoIcons.chevron_right,
|
||
size: 14,
|
||
color: isDark
|
||
? DarkDesignTokens.text3
|
||
: DesignTokens.text3,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
void _showQuickActions(repo.HotItem recipe, bool isDark) {
|
||
showCupertinoModalPopup(
|
||
context: context,
|
||
builder: (BuildContext context) => CupertinoActionSheet(
|
||
title: Text(
|
||
recipe.name,
|
||
style: TextStyle(
|
||
fontSize: DesignTokens.fontLg,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
message: Text(
|
||
'浏览量: ${recipe.count}',
|
||
style: TextStyle(
|
||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||
),
|
||
),
|
||
actions: [
|
||
CupertinoActionSheetAction(
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
Get.toNamed('/recipe-detail', arguments: '${recipe.id}');
|
||
},
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(CupertinoIcons.eye, size: 20),
|
||
SizedBox(width: 8),
|
||
Text('查看详情'),
|
||
],
|
||
),
|
||
),
|
||
CupertinoActionSheetAction(
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
ToastService.show(message: '已添加到收藏');
|
||
},
|
||
isDefaultAction: true,
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(CupertinoIcons.heart, size: 20),
|
||
SizedBox(width: 8),
|
||
Text('添加收藏'),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
cancelButton: CupertinoActionSheetAction(
|
||
onPressed: () => Navigator.pop(context),
|
||
isDestructiveAction: true,
|
||
child: const Text('取消'),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|