/* * 文件: tools_center_page.dart * 名称: 工具中心页面 * 作用: 展示所有工具,支持分类筛选和搜索 * 更新: 2026-04-12 全新设计 - 分组卡片布局风格 */ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mom_kitchen/src/config/design_tokens.dart'; import 'package:mom_kitchen/src/controllers/tools_controller.dart'; import 'package:mom_kitchen/src/models/tool_item_model.dart'; class ToolsCenterPage extends StatefulWidget { const ToolsCenterPage({super.key}); @override State createState() => _ToolsCenterPageState(); } class _ToolsCenterPageState extends State with SingleTickerProviderStateMixin { ToolsController? _controller; bool _isInitialized = false; late AnimationController _animationController; String _searchQuery = ''; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_isInitialized) { _isInitialized = true; _initController(); _animationController.forward(); } } void _initController() { try { if (Get.isRegistered()) { _controller = Get.find(); } } catch (e) { debugPrint('ToolsCenterPage: Controller init error: $e'); } } @override Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; if (_controller == null) { return CupertinoPageScaffold( backgroundColor: isDark ? DarkDesignTokens.background : DesignTokens.background, child: const Center(child: CupertinoActivityIndicator()), ); } return CupertinoPageScaffold( backgroundColor: isDark ? DarkDesignTokens.background : DesignTokens.background, child: SafeArea( child: Column( children: [ _buildHeader(isDark), _buildSearchBar(isDark), Expanded( child: Obx(() => _buildGroupedTools(isDark)), ), ], ), ), ); } Widget _buildHeader(bool isDark) { return Padding( padding: const EdgeInsets.fromLTRB( DesignTokens.space4, DesignTokens.space3, DesignTokens.space4, DesignTokens.space2, ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ DesignTokens.primary.withValues(alpha: 0.2), DesignTokens.secondary.withValues(alpha: 0.2), ], ), borderRadius: BorderRadius.circular(14), ), child: const Center( child: Text('🛠️', style: TextStyle(fontSize: 22)), ), ), 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(() { final count = _controller!.tools.length; return Container( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.space2, vertical: DesignTokens.space1, ), decoration: BoxDecoration( color: DesignTokens.primary.withValues(alpha: 0.1), borderRadius: DesignTokens.borderRadiusSm, ), child: Text( '$count 个工具', style: TextStyle( fontSize: DesignTokens.fontXs, fontWeight: FontWeight.w600, color: DesignTokens.primary, ), ), ); }), ], ), ); } Widget _buildSearchBar(bool isDark) { return Padding( padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4), child: Container( height: 44, decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.text3.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(14), border: Border.all( color: isDark ? DarkDesignTokens.glassBorder : DesignTokens.text3.withValues(alpha: 0.08), ), ), child: Row( children: [ const SizedBox(width: 14), Icon( CupertinoIcons.search, size: 18, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), const SizedBox(width: 10), Expanded( child: CupertinoTextField( placeholder: '搜索工具...', placeholderStyle: TextStyle( fontSize: DesignTokens.fontMd, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), style: TextStyle( fontSize: DesignTokens.fontMd, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), decoration: null, onChanged: (value) { setState(() { _searchQuery = value; }); _controller!.search(value); }, ), ), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { setState(() { _searchQuery = ''; }); _controller!.search(''); }, child: Padding( padding: const EdgeInsets.only(right: 12), child: Icon( CupertinoIcons.clear_circled_solid, size: 18, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), ) else const SizedBox(width: 12), ], ), ), ); } Widget _buildGroupedTools(bool isDark) { final filteredTools = _controller!.filteredTools; if (filteredTools.isEmpty) { return _buildEmptyState(isDark); } final groups = _groupToolsByCategory(filteredTools); final categories = groups.keys.toList(); return ListView.builder( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.space4, vertical: DesignTokens.space3, ), itemCount: categories.length, itemBuilder: (context, groupIndex) { final category = categories[groupIndex]; final tools = groups[category]!; final categoryInfo = _getCategoryInfo(category); return AnimatedBuilder( animation: _animationController, builder: (context, child) { final delay = groupIndex * 0.1; final slideAnimation = Tween( begin: const Offset(0, 0.3), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: Interval(delay, delay + 0.3, curve: Curves.easeOutCubic), )); final fadeAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation( parent: _animationController, curve: Interval(delay, delay + 0.3, curve: Curves.easeOut), ), ); return SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: child, ), ); }, child: Padding( padding: const EdgeInsets.only(bottom: DesignTokens.space4), child: _buildCategoryGroup(category, tools, categoryInfo, isDark), ), ); }, ); } Map> _groupToolsByCategory(List tools) { final groups = >{}; for (final tool in tools) { groups.putIfAbsent(tool.category, () => []).add(tool); } return groups; } Map _getCategoryInfo(String categoryId) { final categoryMap = { 'cooking': { 'name': '烹饪助手', 'icon': '🍳', 'gradient': [const Color(0xFFFF6B35), const Color(0xFFFF9F1C)], }, 'health': { 'name': '健康营养', 'icon': '💊', 'gradient': [const Color(0xFF2ECC71), const Color(0xFF27AE60)], }, 'data': { 'name': '数据查询', 'icon': '📊', 'gradient': [const Color(0xFF3498DB), const Color(0xFF2980B9)], }, 'planning': { 'name': '规划管理', 'icon': '📅', 'gradient': [const Color(0xFF9B59B6), const Color(0xFF8E44AD)], }, }; return categoryMap[categoryId] ?? { 'name': categoryId, 'icon': '📦', 'gradient': [DesignTokens.primary, DesignTokens.secondary], }; } Widget _buildCategoryGroup( String category, List tools, Map categoryInfo, bool isDark, ) { return Container( decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.05), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCategoryHeader(categoryInfo, tools.length, isDark), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Wrap( spacing: 12, runSpacing: 12, children: tools.map((tool) { return _buildToolChip(tool, categoryInfo, isDark); }).toList(), ), ), ], ), ); } Widget _buildCategoryHeader( Map categoryInfo, int count, bool isDark, ) { final gradientColors = categoryInfo['gradient'] as List; return Container( padding: const EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ gradientColors[0].withValues(alpha: 0.15), gradientColors[1].withValues(alpha: 0.08), ], ), borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: gradientColors, ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: gradientColors[0].withValues(alpha: 0.4), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Center( child: Text( categoryInfo['icon'] as String, style: const TextStyle(fontSize: 20), ), ), ), const SizedBox(width: DesignTokens.space3), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( categoryInfo['name'] as String, style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), Text( '$count 个工具', style: TextStyle( fontSize: DesignTokens.fontXs, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), ], ), ), ], ), ); } Widget _buildToolChip( ToolItem tool, Map categoryInfo, bool isDark, ) { final gradientColors = categoryInfo['gradient'] as List; return GestureDetector( onTap: () => _openTool(tool), child: Container( width: (MediaQuery.of(context).size.width - 56) / 2, padding: const EdgeInsets.all(DesignTokens.space3), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.glass : DesignTokens.text3.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(16), border: Border.all( color: isDark ? DarkDesignTokens.glassBorder : DesignTokens.text3.withValues(alpha: 0.08), ), ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ gradientColors[0].withValues(alpha: 0.2), gradientColors[1].withValues(alpha: 0.1), ], ), borderRadius: BorderRadius.circular(12), ), child: Center( child: Text(tool.icon, style: const TextStyle(fontSize: 22)), ), ), const SizedBox(width: DesignTokens.space2), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tool.name, style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w600, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Row( children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: tool.needsNetwork ? DesignTokens.green : DesignTokens.primary, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( tool.needsNetwork ? '联网' : '本地', style: TextStyle( fontSize: 10, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), ], ), ], ), ), Icon( CupertinoIcons.chevron_right, size: 14, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ], ), ), ); } Widget _buildEmptyState(bool isDark) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: DesignTokens.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(24), ), child: const Center( child: Text('🔍', style: TextStyle(fontSize: 36)), ), ), const SizedBox(height: DesignTokens.space4), Text( '未找到相关工具', style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), const SizedBox(height: DesignTokens.space2), Text( '试试其他关键词吧', style: TextStyle( fontSize: DesignTokens.fontSm, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), ], ), ); } void _openTool(ToolItem tool) { _controller?.recordUsage(tool.id); Navigator.of(context).push( CupertinoPageRoute( builder: (context) => _ToolDetailPage(tool: tool), ), ); } } class _ToolDetailPage extends StatelessWidget { final ToolItem tool; const _ToolDetailPage({required this.tool}); @override Widget build(BuildContext context) { final isDark = CupertinoTheme.brightnessOf(context) == Brightness.dark; return CupertinoPageScaffold( backgroundColor: isDark ? DarkDesignTokens.background : DesignTokens.background, child: SafeArea( child: Column( children: [ _buildHeader(context, isDark), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(DesignTokens.space4), child: Column( children: [ _buildHeroCard(isDark), const SizedBox(height: DesignTokens.space4), _buildInfoCards(isDark), ], ), ), ), ], ), ), ); } Widget _buildHeader(BuildContext context, bool isDark) { return Padding( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.space4, vertical: DesignTokens.space2, ), child: Row( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: isDark ? DarkDesignTokens.glass : DesignTokens.text3.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), ), child: Icon( CupertinoIcons.back, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), ), const SizedBox(width: DesignTokens.space3), Expanded( child: Text( tool.name, style: TextStyle( fontSize: DesignTokens.fontLg, fontWeight: FontWeight.w600, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), ), ], ), ); } Widget _buildHeroCard(bool isDark) { return Container( width: double.infinity, padding: const EdgeInsets.all(DesignTokens.space5), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ DesignTokens.primary.withValues(alpha: 0.15), DesignTokens.secondary.withValues(alpha: 0.08), ], ), borderRadius: BorderRadius.circular(24), border: Border.all( color: DesignTokens.primary.withValues(alpha: 0.1), ), ), child: Column( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: DesignTokens.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(24), ), child: Center( child: Text(tool.icon, style: const TextStyle(fontSize: 40)), ), ), const SizedBox(height: DesignTokens.space4), Text( tool.name, style: TextStyle( fontSize: DesignTokens.fontXxl, fontWeight: FontWeight.w700, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), if (tool.description != null) ...[ const SizedBox(height: DesignTokens.space2), Text( tool.description!, textAlign: TextAlign.center, style: TextStyle( fontSize: DesignTokens.fontMd, color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2, height: 1.5, ), ), ], const SizedBox(height: DesignTokens.space4), Container( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.space3, vertical: DesignTokens.space2, ), decoration: BoxDecoration( color: tool.needsNetwork ? DesignTokens.green.withValues(alpha: 0.15) : DesignTokens.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( tool.needsNetwork ? CupertinoIcons.wifi : CupertinoIcons.device_phone_portrait, size: 14, color: tool.needsNetwork ? DesignTokens.green : DesignTokens.primary, ), const SizedBox(width: 6), Text( tool.needsNetwork ? '需要网络连接' : '本地运行', style: TextStyle( fontSize: DesignTokens.fontSm, fontWeight: FontWeight.w500, color: tool.needsNetwork ? DesignTokens.green : DesignTokens.primary, ), ), ], ), ), ], ), ); } Widget _buildInfoCards(bool isDark) { return Column( children: [ _buildInfoCard( icon: CupertinoIcons.chart_bar, title: '使用统计', value: '已使用 ${tool.usageCount} 次', isDark: isDark, ), const SizedBox(height: DesignTokens.space2), _buildInfoCard( icon: CupertinoIcons.folder, title: '所属分类', value: _getCategoryName(tool.category), isDark: isDark, ), ], ); } Widget _buildInfoCard({ required IconData icon, required String title, required String value, required bool isDark, }) { return Container( padding: const EdgeInsets.all(DesignTokens.space4), decoration: BoxDecoration( color: isDark ? DarkDesignTokens.card : DesignTokens.card, borderRadius: BorderRadius.circular(16), border: Border.all( color: isDark ? DarkDesignTokens.glassBorder : DesignTokens.text3.withValues(alpha: 0.08), ), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: DesignTokens.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 20, color: DesignTokens.primary), ), const SizedBox(width: DesignTokens.space3), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: DesignTokens.fontXs, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3, ), ), Text( value, style: TextStyle( fontSize: DesignTokens.fontMd, fontWeight: FontWeight.w600, color: isDark ? DarkDesignTokens.text1 : DesignTokens.text1, ), ), ], ), ), ], ), ); } String _getCategoryName(String categoryId) { const categoryNames = { 'cooking': '烹饪助手', 'health': '健康营养', 'data': '数据查询', 'planning': '规划管理', }; return categoryNames[categoryId] ?? categoryId; } }