diff --git a/fonts/custom.ttf b/fonts/custom.ttf new file mode 100644 index 0000000..2ca97e8 Binary files /dev/null and b/fonts/custom.ttf differ diff --git a/forui.yaml b/forui.yaml new file mode 100644 index 0000000..12cc8d1 --- /dev/null +++ b/forui.yaml @@ -0,0 +1,9 @@ +cli: + # The default file or directory to output generated snippets. + snippet-output: lib + + # The default file or directory to output generated widget styles. + style-output: lib/theme + + # The default file or directory to output generated themes. + theme-output: lib/theme/theme.dart diff --git a/lib/main.dart b/lib/main.dart index 4318255..ce00f88 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,15 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp(debugShowCheckedModeBanner: false, home: MainPage()); + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData(fontFamily: 'CustomFont', primaryColor: Colors.green), + home: MainPage(), + routes: { + '/home': (context) => MainPage(), + '/new': (context) => NewPage(), + }, + ); } } @@ -32,14 +40,46 @@ class _mainPage extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("食光集", style: TextStyle(fontSize: 18)), - backgroundColor: Colors.green, + centerTitle: true, + title: Text('Food Hub'), + backgroundColor: Colors.white, + leading: IconButton( + icon: Icon(Icons.menu), + onPressed: () { + // 打开侧边栏或菜单 + }, + ), + actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + // 搜索功能 + }, + ), + IconButton( + icon: Icon(Icons.more_vert), + onPressed: () { + // 更多选项 + }, + ), + ], ), backgroundColor: Color(0xFFF5F5F5), body: _tabPages[_currentIndex], + floatingActionButton: + _currentIndex == _tabPages.length - 1 + ? null + : FloatingActionButton( + mini: true, + onPressed: () { + Navigator.pushNamed(context, '/new'); + }, + backgroundColor: Colors.green, + shape: const CircleBorder(), + child: Icon(Icons.add, color: Colors.white), + ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, - fixedColor: Colors.green, iconSize: 25, type: BottomNavigationBarType.fixed, items: const [ @@ -58,3 +98,24 @@ class _mainPage extends State { ); } } + +class NewPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('新页面')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('这是新页面'), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text('返回'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/models/recipe.dart b/lib/models/recipe.dart new file mode 100644 index 0000000..235cbc0 --- /dev/null +++ b/lib/models/recipe.dart @@ -0,0 +1,21 @@ +class Recipe { + final String name; + final String category; + final String date; + final String imageUrl; + final int likeCount; + final int favoriteCount; + final int commentCount; + + Recipe( + this.name, + this.category, + this.date, + this.imageUrl, + this.likeCount, + this.favoriteCount, + this.commentCount, + ); +} + +enum ViewType { recipe, calendar, timeline } diff --git a/lib/record.dart b/lib/record.dart index ccc8518..2731c41 100644 --- a/lib/record.dart +++ b/lib/record.dart @@ -1,4 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:food_hub_app/widgets/recipe/recipe_calendar.dart'; +import 'package:food_hub_app/widgets/recipe/recipe_list.dart'; +import 'package:food_hub_app/widgets/recipe/recipe_segment.dart'; +import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart'; + +import 'models/recipe.dart'; class RecordPage extends StatefulWidget { const RecordPage({super.key}); @@ -7,179 +13,48 @@ class RecordPage extends StatefulWidget { State createState() => _RecordPageState(); } -enum ViewType { recipe, daily, timeline } - class _RecordPageState extends State { ViewType _selectedViewType = ViewType.recipe; final List recipeList = [ - Recipe("韭菜炒鸡蛋", "2025-05-04", "https://cdn.seovx.com/?mom=302"), - Recipe("红烧肉", "2025-05-05", "https://cdn.seovx.com/?mom=302"), + Recipe("韭菜炒鸡蛋", "家常菜", "2025-05-04", "https://picsum.photos/200", 10, 2, 4), + Recipe("红烧肉", "家常菜", "2025-05-05", "https://picsum.photos/200", 12, 4, 7), ]; @override Widget build(BuildContext context) { - return Column( - children: [ - Card( - color: Colors.white, - margin: const EdgeInsets.all(10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), // 卡片圆角 + return Padding( + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RecipeSegment( + selectedViewType: _selectedViewType, + onViewTypeChanged: (value) { + setState(() { + _selectedViewType = value; + }); + }, ), - shadowColor: Colors.black.withValues(alpha: 0.1), - child: Container( - padding: const EdgeInsets.all(10), // 内部内容边距 - child: Row( - children: [ - const Text("查看类型:", style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(width: 8), - SegmentedButton( - segments: [ - ButtonSegment(value: ViewType.recipe, label: Text("菜谱")), - ButtonSegment(value: ViewType.daily, label: Text("日历")), - ButtonSegment(value: ViewType.timeline, label: Text("时间轴")), - ], - selected: {_selectedViewType}, - onSelectionChanged: (newSelection) { - setState(() { - _selectedViewType = newSelection.first; - }); - }, - style: ButtonStyle( - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), // 分段按钮圆角 - ), - ), - ), - ), - ], - ), - ), - ), - - // 分隔线 - const Divider(height: 1), - - // 内容区域 - Expanded( - child: _buildContent(), - ), - ], + const SizedBox(height: 10), + const Divider(height: 1), + const SizedBox(height: 10), + // 内容区域 + Expanded(child: _buildContent()), + ], + ), ); } Widget _buildContent() { switch (_selectedViewType) { case ViewType.recipe: - return ListView.builder( - itemCount: recipeList.length, - itemBuilder: (context, index) { - return RecipeCard(recipe: recipeList[index]); - } - ); - case ViewType.daily: - return const Center(child: Text("日历视图")); + return RecipeList(recipeList: recipeList); + case ViewType.calendar: + return RecipeCalendar(); case ViewType.timeline: - return const Center(child: Text("时间轴视图")); + return RecipeTimeline(recipeList: recipeList); } } } - -class Recipe { - final String name; - final String date; - final String imagePath; - - Recipe(this.name, this.date, this.imagePath); -} - -class RecipeCard extends StatelessWidget { - final Recipe recipe; - - const RecipeCard({super.key, required this.recipe}); - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.all(10), - color: Colors.white, - child: Column( - children: [ - Image.network( - recipe.imagePath, - height: 200, - fit: BoxFit.contain, - ), - const Divider( - height: 1, - // 分隔线本身的高度 - thickness: 1, - // 线条粗细 - color: Colors.green, - // 线条颜色 - indent: 0, - // 左侧缩进 - endIndent: 0, // 右侧缩进 - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - recipe.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - IconButton( - icon: Icon(Icons.edit), - onPressed: () {}, - ), - IconButton( - icon: Icon(Icons.book), - onPressed: () {}, - ), - IconButton( - icon: Icon(Icons.delete), - onPressed: () {}, - ), - ], - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon(Icons.food_bank, color: Colors.grey), - SizedBox(width: 4), - Text("家常菜", style: TextStyle(color: Colors.grey)), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.date_range, color: Colors.red[700]), - SizedBox(width: 4), - Text(recipe.date, style: TextStyle(color: Colors.grey)), - ], - ), - ], - ), - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/recipe/recipe_calendar.dart b/lib/widgets/recipe/recipe_calendar.dart new file mode 100644 index 0000000..2051e79 --- /dev/null +++ b/lib/widgets/recipe/recipe_calendar.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class RecipeCalendar extends StatelessWidget { + const RecipeCalendar({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Card( + elevation: 0, + color: Colors.white, + child: TableCalendar( + firstDay: DateTime.utc(2010, 10, 16), + lastDay: DateTime.utc(2030, 3, 14), + focusedDay: DateTime.now(), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/recipe/recipe_card.dart b/lib/widgets/recipe/recipe_card.dart new file mode 100644 index 0000000..69d60fa --- /dev/null +++ b/lib/widgets/recipe/recipe_card.dart @@ -0,0 +1,156 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'package:food_hub_app/models/recipe.dart'; + +class RecipeCard extends StatelessWidget { + final Recipe recipe; + + const RecipeCard({super.key, required this.recipe}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: Colors.white, + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + recipe.imageUrl, + height: 200, + width: double.infinity, + fit: BoxFit.contain, + errorBuilder: + (context, error, stackTrace) => Container( + height: 200, + color: Colors.grey[200], + child: const Icon(Icons.food_bank, color: Colors.grey), + ), + ), + const Divider( + height: 1, + thickness: 1, + color: Colors.green, + indent: 0, + endIndent: 0, + ), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + recipe.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + maxLines: 1, // 限制单行,避免挤压按钮 + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + IconButton( + icon: const Icon(Icons.edit, size: 14), + color: Colors.white, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.green, + ), + shape: WidgetStateProperty.all(CircleBorder()), + minimumSize: WidgetStateProperty.all(Size(20, 20)), + ), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.book, size: 14), + color: Colors.white, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Colors.green, + ), + shape: WidgetStateProperty.all(CircleBorder()), + minimumSize: WidgetStateProperty.all(Size(20, 20)), + ), + onPressed: () {}, + ), + ], + ), + ], + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + _buildIconText( + icon: Icons.food_bank, + text: recipe.category, + color: Colors.green, + ), + const SizedBox(width: 12), + _buildIconText( + icon: Icons.thumb_up_alt_outlined, + text: recipe.likeCount.toString(), + color: Colors.red, + ), + const SizedBox(width: 12), + _buildIconText( + icon: Icons.star_outline, + text: recipe.favoriteCount.toString(), + color: Colors.blue, + ), + const SizedBox(width: 12), + _buildIconText( + icon: Icons.comment_outlined, + text: recipe.commentCount.toString(), + color: Colors.deepOrange, + ), + ], + ), + Row( + children: [ + _buildIconText( + icon: Icons.date_range, + text: recipe.date, + color: Colors.red, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + // 通用图标文本组件 + Widget _buildIconText({ + required IconData icon, + required String text, + Color color = Colors.grey, // 默认灰色 + double iconSize = 16, + double textSize = 16, + double spacing = 2, // 图标与文本间距 + }) { + return Row( + children: [ + Icon(icon, size: iconSize, color: color), + SizedBox(width: spacing), + Text(text, style: TextStyle(fontSize: textSize, color: color)), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/recipe/recipe_list.dart b/lib/widgets/recipe/recipe_list.dart new file mode 100644 index 0000000..d30c8de --- /dev/null +++ b/lib/widgets/recipe/recipe_list.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:food_hub_app/models/recipe.dart'; +import 'package:food_hub_app/widgets/recipe/recipe_card.dart'; + +class RecipeList extends StatelessWidget { + final List recipeList; + + const RecipeList({super.key, required this.recipeList}); + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: recipeList.length, + itemBuilder: (context, index) { + return RecipeCard(recipe: recipeList[index]); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 10); + }, + ); + } +} diff --git a/lib/widgets/recipe/recipe_segment.dart b/lib/widgets/recipe/recipe_segment.dart new file mode 100644 index 0000000..a609e20 --- /dev/null +++ b/lib/widgets/recipe/recipe_segment.dart @@ -0,0 +1,45 @@ +import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:food_hub_app/models/recipe.dart'; + +class RecipeSegment extends StatelessWidget { + final ViewType selectedViewType; + final Function(ViewType) onViewTypeChanged; + + const RecipeSegment({super.key, required this.selectedViewType, required this.onViewTypeChanged}); + + @override + Widget build(BuildContext context) { + return CustomSlidingSegmentedControl( + initialValue: 1, + children: { + 1: SizedBox( + width: 50, + child: Text('菜谱', textAlign: TextAlign.center), + ), + 2: SizedBox( + width: 50, + child: Text('日历', textAlign: TextAlign.center), + ), + 3: SizedBox( + width: 50, + child: Text('时间轴', textAlign: TextAlign.center), + ), + }, + decoration: BoxDecoration( + color: CupertinoColors.lightBackgroundGray, + borderRadius: BorderRadius.circular(10), + ), + thumbDecoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + duration: Duration(milliseconds: 300), + curve: Curves.easeInToLinear, + onValueChanged: (value) { + onViewTypeChanged(ViewType.values[value - 1]); + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/recipe/recipe_timeline.dart b/lib/widgets/recipe/recipe_timeline.dart new file mode 100644 index 0000000..4efefe1 --- /dev/null +++ b/lib/widgets/recipe/recipe_timeline.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:food_hub_app/models/recipe.dart'; +import 'package:timelines_plus/timelines_plus.dart'; + +class RecipeTimeline extends StatelessWidget { + final List recipeList; + + const RecipeTimeline({super.key, required this.recipeList}); + + @override + Widget build(BuildContext context) { + return Timeline.tileBuilder( + theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0), + builder: TimelineTileBuilder.connected( + itemCount: recipeList.length, + connectorBuilder: + (context, index, type) => Connector.solidLine(thickness: 2), + indicatorBuilder: (context, index) { + return Indicator.dot(size: 12.0); + }, + contentsBuilder: (context, index) { + return TimelineCard(recipe: recipeList[index]); + }, + ), + ); + } +} + +class TimelineCard extends StatelessWidget { + final Recipe recipe; + + const TimelineCard({super.key, required this.recipe}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.only(left: 10, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + recipe.date, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Card( + elevation: 0, + color: Colors.white, + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + Text(recipe.name, style: TextStyle(fontSize: 16)), + const SizedBox(height: 10), + Image.network( + recipe.imageUrl, + width: double.infinity, + height: 250, + fit: BoxFit.contain, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 17a8eb2..ffe9fa5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.8" + custom_sliding_segmented_control: + dependency: "direct main" + description: + name: custom_sliding_segmented_control + sha256: bc747b1500284bff36fc8c61774c9a3ca35c1dca4d570141c8a2569b4f984bf3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.5" fake_async: dependency: transitive description: @@ -62,6 +70,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_date_pickers: + dependency: "direct main" + description: + name: flutter_date_pickers + sha256: "302b1200d8859ec0bfe51c4eaea5f4911d1dbfc3c3f7d256dcf32d99fec219ee" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.3" + flutter_datetime_picker_plus: + dependency: "direct main" + description: + name: flutter_datetime_picker_plus + sha256: "7d82da02c4e070bb28a9107de119ad195e2319b45c786fecc13482a9ffcc51da" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" flutter_lints: dependency: "direct dev" description: @@ -70,11 +94,24 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -139,6 +176,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -176,6 +221,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" term_glyph: dependency: transitive description: @@ -192,6 +245,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" + timelines_plus: + dependency: "direct main" + description: + name: timelines_plus + sha256: "335cd832e0aed7035458a5f336bc2c882dc70dcfa4d500d3bcadd764f1ac6553" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 711bc76..dfd6fc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,9 +31,14 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. + # The following adds the Cupertino Icons fonts to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + timelines_plus: ^1.0.7 + table_calendar: ^3.1.3 + custom_sliding_segmented_control: ^1.8.5 + flutter_datetime_picker_plus: ^2.2.0 + flutter_date_pickers: ^0.4.3 dev_dependencies: flutter_test: @@ -52,7 +57,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is + # The following line ensures that the Material Icons fonts is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true @@ -70,8 +75,8 @@ flutter: # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For + # "family" key with the fonts family name, and a "fonts" key with a + # list giving the asset and other descriptors for the fonts. For # example: # fonts: # - family: Schyler @@ -87,3 +92,7 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + fonts: + - family: CustomFont + fonts: + - asset: fonts/custom.ttf