Files
kitchen/lib/src/pages/discover/discover_page.dart.bak
2026-04-17 07:00:26 +08:00

1835 lines
64 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.
/*
* 文件: 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('取消'),
),
),
);
}
}