feat:重构UI模块

This commit is contained in:
2025-11-18 11:01:53 +08:00
parent 6bba9a488c
commit 21ff0ad94f
20 changed files with 501 additions and 605 deletions

View File

@@ -15,21 +15,6 @@ migration:
- platform: root - platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: linux
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: macos
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: web
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: windows - platform: windows
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="food_hub_app" android:label="食光集"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@@ -1,35 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class NavBar extends StatelessWidget {
final int currentIndex;
final List<BottomNavigationBarItem> navItems;
final Function(int) onTap;
const NavBar({
super.key,
required this.currentIndex,
required this.onTap,
required this.navItems,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Theme.of(context).primaryColor)),
),
child: BottomNavigationBar(
currentIndex: currentIndex,
iconSize: 25,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: navItems,
onTap: onTap,
),
);
}
}
class SettingsDrawer extends StatelessWidget { class SettingsDrawer extends StatelessWidget {
const SettingsDrawer({super.key}); const SettingsDrawer({super.key});
@@ -97,20 +67,3 @@ class SettingsDrawer extends StatelessWidget {
); );
} }
} }
List<StatelessWidget> homeActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
//
},
),
// IconButton(
// icon: Icon(Icons.add, color: Colors.white),
// onPressed: () {
// Navigator.pushNamed(context, '/recordForm');
// },
// ),
];
}

86
lib/layout/nav_bar.dart Normal file
View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/layout.dart';
import 'package:food_hub_app/views/moment.dart';
import 'package:food_hub_app/views/profile.dart';
import 'package:food_hub_app/views/record.dart';
import 'package:food_hub_app/views/stats.dart';
final List<PageInfo> pageInfos = [
PageInfo(
label: "记录",
icon: Icons.home_outlined,
activeIcon: Icons.home,
page: RecordPage(),
),
PageInfo(
label: "统计",
icon: Icons.pie_chart_outline,
activeIcon: Icons.pie_chart,
page: StatsPage(),
),
PageInfo(
label: "朋友圈",
icon: Icons.group_outlined,
activeIcon: Icons.group,
page: MomentPage(),
),
PageInfo(
label: "我的",
icon: Icons.account_circle_outlined,
activeIcon: Icons.account_circle,
page: ProfilePage(),
),
];
class NavBar extends StatelessWidget {
final int currentIndex;
final Function(int) onTap;
const NavBar({super.key, required this.currentIndex, required this.onTap});
List<BottomNavigationBarItem> get items =>
pageInfos
.map(
(item) => BottomNavigationBarItem(
icon: Icon(item.icon),
activeIcon: Icon(item.activeIcon),
label: item.label,
),
)
.toList();
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Theme.of(context).primaryColor)),
),
child: BottomNavigationBar(
currentIndex: currentIndex,
iconSize: 25,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: items,
onTap: onTap,
),
);
}
}
List<StatelessWidget> homeActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
// 搜索功能
},
),
// IconButton(
// icon: Icon(Icons.add, color: Colors.white),
// onPressed: () {
// Navigator.pushNamed(context, '/recordForm');
// },
// ),
];
}

View File

@@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// 导航栏 class PageInfo {
class NavItem {
final String label; final String label;
final IconData icon; final IconData icon;
final IconData activeIcon; final IconData activeIcon;
final Widget page; final Widget page;
const NavItem({ const PageInfo({
required this.label, required this.label,
required this.icon, required this.icon,
required this.activeIcon, required this.activeIcon,

View File

@@ -1,10 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/layout/index.dart'; import 'package:food_hub_app/layout/drawer.dart';
import 'package:food_hub_app/models/layout.dart'; import 'package:food_hub_app/layout/nav_bar.dart';
import 'package:food_hub_app/views/moment.dart';
import 'package:food_hub_app/views/profile.dart';
import 'package:food_hub_app/views/record.dart';
import 'package:food_hub_app/views/stats.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@@ -16,70 +12,31 @@ class HomePage extends StatefulWidget {
class _HomePage extends State<HomePage> { class _HomePage extends State<HomePage> {
int _currentIndex = 0; int _currentIndex = 0;
final List<NavItem> navItems = [ List<Widget> get tabPages => pageInfos.map((item) => item.page).toList();
NavItem(
label: "记录",
icon: Icons.home_outlined,
activeIcon: Icons.home,
page: RecordPage(),
),
NavItem(
label: "统计",
icon: Icons.pie_chart_outline,
activeIcon: Icons.pie_chart,
page: StatsPage(),
),
NavItem(
label: "朋友圈",
icon: Icons.group_outlined,
activeIcon: Icons.group,
page: MomentPage(),
),
NavItem(
label: "我的",
icon: Icons.account_circle_outlined,
activeIcon: Icons.account_circle,
page: ProfilePage(),
),
];
List<BottomNavigationBarItem> get bottomNavItems =>
navItems
.map(
(item) => BottomNavigationBarItem(
icon: Icon(item.icon),
activeIcon: Icon(item.activeIcon),
label: item.label,
),
)
.toList();
List<Widget> get tabPages => navItems.map((item) => item.page).toList();
// 显示底部弹窗 // 显示底部弹窗
void _showBottomSheet() { void _showBottomSheet() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
top: Radius.circular(16),
), ),
), builder:
builder: (context) => Container( (context) => Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text( const Text(
'请选择操作', '请选择操作',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
ListTile( ListTile(
leading: Icon(Icons.book, color: Theme.of(context).primaryColor), leading: Icon(
Icons.book,
color: Theme.of(context).primaryColor,
),
title: const Text('新增菜谱'), title: const Text('新增菜谱'),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -87,7 +44,10 @@ class _HomePage extends State<HomePage> {
}, },
), ),
ListTile( ListTile(
leading: Icon(Icons.note_add, color: Theme.of(context).primaryColor), leading: Icon(
Icons.note_add,
color: Theme.of(context).primaryColor,
),
title: const Text('新增记录'), title: const Text('新增记录'),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
@@ -95,13 +55,16 @@ class _HomePage extends State<HomePage> {
}, },
), ),
ListTile( ListTile(
leading: Icon(Icons.group, color: Theme.of(context).primaryColor), leading: Icon(
Icons.group,
color: Theme.of(context).primaryColor,
),
title: const Text('发布朋友圈'), title: const Text('发布朋友圈'),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
_handleAddRecord(); _handleAddRecord();
}, },
) ),
], ],
), ),
), ),
@@ -111,17 +74,17 @@ class _HomePage extends State<HomePage> {
// 处理添加菜谱 // 处理添加菜谱
void _handleAddRecipe() { void _handleAddRecipe() {
// 这里添加跳转或处理添加菜谱的逻辑 // 这里添加跳转或处理添加菜谱的逻辑
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
const SnackBar(content: Text('添加菜谱功能')), context,
); ).showSnackBar(const SnackBar(content: Text('添加菜谱功能')));
} }
// 处理添加记录 // 处理添加记录
void _handleAddRecord() { void _handleAddRecord() {
// 这里添加跳转或处理添加记录的逻辑 // 这里添加跳转或处理添加记录的逻辑
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
const SnackBar(content: Text('添加记录功能')), context,
); ).showSnackBar(const SnackBar(content: Text('添加记录功能')));
} }
@override @override
@@ -129,8 +92,8 @@ class _HomePage extends State<HomePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text('Food Hub', style: TextStyle(color: Colors.white)), title: Text('食光集', style: TextStyle(color: Colors.white)),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).colorScheme.primary,
leading: Builder( leading: Builder(
builder: (context) { builder: (context) {
return IconButton( return IconButton(
@@ -143,17 +106,20 @@ class _HomePage extends State<HomePage> {
actions: homeActions(context), actions: homeActions(context),
), ),
backgroundColor: Color(0xFFF5F5F5), backgroundColor: Color(0xFFF5F5F5),
drawer: const SettingsDrawer(), drawer: SettingsDrawer(),
body: tabPages[_currentIndex], body: Padding(
padding: EdgeInsets.all(4),
child: tabPages[_currentIndex],
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
onPressed: _showBottomSheet, onPressed: _showBottomSheet,
shape: const CircleBorder(), shape: const CircleBorder(),
mini: true,
child: const Icon(Icons.add, color: Colors.white, size: 30), child: const Icon(Icons.add, color: Colors.white, size: 30),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: NavBar( bottomNavigationBar: NavBar(
navItems: bottomNavItems,
currentIndex: _currentIndex, currentIndex: _currentIndex,
onTap: (index) { onTap: (index) {
setState(() { setState(() {

View File

@@ -17,7 +17,7 @@ class LoginPage extends StatefulWidget {
class _LoginPage extends State<LoginPage> { class _LoginPage extends State<LoginPage> {
final GlobalKey _formKey = GlobalKey<FormState>(); final GlobalKey _formKey = GlobalKey<FormState>();
String _username = "", _password = ""; String _username = "Cxx0822", _password = "19940822Cxx";
bool _isRemember = false; bool _isRemember = false;
bool _isObscure = true; bool _isObscure = true;
Color _eyeColor = Colors.grey; Color _eyeColor = Colors.grey;

View File

@@ -2,8 +2,8 @@ import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/apis/moment.dart'; import 'package:food_hub_app/apis/moment.dart';
import 'package:food_hub_app/models/moment.dart'; import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/moment/card.dart'; import 'package:food_hub_app/widgets/moment/card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class MomentPage extends StatefulWidget { class MomentPage extends StatefulWidget {
const MomentPage({super.key}); const MomentPage({super.key});
@@ -126,16 +126,14 @@ class _MomentPageState extends State<MomentPage> {
return EasyRefresh( return EasyRefresh(
controller: _freshController, controller: _freshController,
onRefresh: _onRefresh, onRefresh: _onRefresh,
child: const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'), child: buildEmptyData(),
); );
} }
// 有数据时显示列表 // 有数据时显示列表
return Stack( return Stack(
children: [ children: [
Padding( EasyRefresh(
padding: const EdgeInsets.all(5),
child: EasyRefresh(
controller: _freshController, controller: _freshController,
header: ClassicHeader( header: ClassicHeader(
dragText: '下拉刷新', dragText: '下拉刷新',
@@ -171,21 +169,18 @@ class _MomentPageState extends State<MomentPage> {
}, },
), ),
), ),
),
// 返回顶部按钮 // 返回顶部按钮
if (_showScrollToTop) if (_showScrollToTop)
Positioned( Positioned(
right: 10, right: 0,
bottom: 20, bottom: 0,
child: FloatingActionButton( child: FloatingActionButton(
onPressed: _scrollToTop, onPressed: _scrollToTop,
backgroundColor: Colors.white, backgroundColor: Theme.of(context).colorScheme.primary,
elevation: 5, elevation: 5,
mini: true, mini: true,
child: const Icon( child: const Icon(Icons.arrow_upward, color: Colors.white),
Icons.arrow_upward
),
), ),
), ),
], ],

View File

@@ -72,14 +72,12 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('菜谱信息', style: TextStyle(color: Colors.white)), title: Text('菜谱信息'),
backgroundColor: Theme.of(context).primaryColor,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white), icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
), ),
backgroundColor: Color(0xFFF5F5F5),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()), child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
), ),
@@ -224,8 +222,9 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
required String title, required String title,
required Widget content, required Widget content,
}) { }) {
return cardContainer( return buildCard(
Column( context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
@@ -241,7 +240,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
SizedBox(height: 5), SizedBox(height: 5),
content, content,
], ],
), )
); );
} }
} }

View File

@@ -47,9 +47,8 @@ class _RecordPageState extends State<RecordPage>
backgroundColor: Colors.white, backgroundColor: Colors.white,
showIndicator: true, showIndicator: true,
), ),
SizedBox(height: 8),
Expanded( Expanded(
child: Padding(
padding: EdgeInsets.all(5),
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
@@ -59,7 +58,6 @@ class _RecordPageState extends State<RecordPage>
], ],
), ),
), ),
),
], ],
); );
} }

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:food_hub_app/apis/stats.dart'; import 'package:food_hub_app/apis/stats.dart';
import 'package:food_hub_app/models/stats.dart'; import 'package:food_hub_app/models/stats.dart';
import 'package:food_hub_app/widgets/common/chart.dart'; import 'package:food_hub_app/widgets/common/chart.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/stats/card.dart'; import 'package:food_hub_app/widgets/stats/card.dart';
class StatsPage extends StatefulWidget { class StatsPage extends StatefulWidget {
@@ -45,7 +46,10 @@ class _StatsPage extends State<StatsPage> {
rankStats = result4; rankStats = result4;
if (recordStats.isNotEmpty) { if (recordStats.isNotEmpty) {
double sumValue = recordStats.fold(0.0, (sum, item) => sum + item.value); double sumValue = recordStats.fold(
0.0,
(sum, item) => sum + item.value,
);
averageRecordCount = sumValue / recordStats.length; averageRecordCount = sumValue / recordStats.length;
} }
}); });
@@ -54,56 +58,23 @@ class _StatsPage extends State<StatsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(5),
child: Column( child: Column(
children: [ children: [
_buildSummaryStats(), _buildSummaryStats(),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: Card( child: buildCard(context: context, child: _buildRecordStats()),
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildRecordStats(),
),
),
), ),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: Card( child: buildCard(context: context, child: _buildCategoryStats()),
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCategoryStats(),
),
),
), ),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: Card( child: buildCard(context: context, child: _buildRankStats()),
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildRankStats(),
),
),
), ),
], ],
), ),
),
); );
} }

View File

@@ -86,7 +86,7 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
} }
} }
Widget networkImage(String url) { Widget buildNetworkImage(String url) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.network( child: Image.network(

View File

@@ -27,15 +27,26 @@ InputDecoration formInputDecoration({
); );
} }
Widget cardContainer(Widget content) { Widget buildCard({required BuildContext context, required Widget child}) {
final colors = Theme.of(context).colorScheme;
return Card( return Card(
elevation: 0, elevation: 0,
color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Container(
child: Padding( width: double.infinity,
padding: const EdgeInsets.all(10), decoration: BoxDecoration(
child: content, color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
), ),
child: Padding(padding: EdgeInsets.all(8), child: child),
),
);
}
Widget buildEmptyData() {
return Center(
child: Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey)),
); );
} }
@@ -48,14 +59,14 @@ Widget circleIconButton({
return ElevatedButton( return ElevatedButton(
onPressed: onPressed, onPressed: onPressed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
fixedSize: Size(32, 32), fixedSize: Size(26, 26),
shape: CircleBorder(), shape: CircleBorder(),
elevation: 0, elevation: 0,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).colorScheme.primary,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
minimumSize: const Size(0, 0), minimumSize: const Size(0, 0),
), ),
child: Icon(icon, color: Colors.white, size: 18), child: Icon(icon, color: Colors.white),
); );
} }

View File

@@ -93,6 +93,7 @@ class _YearSelectorState extends State<YearSelector>
icon: Icons.chevron_left, icon: Icons.chevron_left,
onPressed: () => _previousYear(), onPressed: () => _previousYear(),
), ),
SizedBox(width: 5),
// 年份显示 // 年份显示
AnimatedBuilder( AnimatedBuilder(
animation: _scaleAnimation, animation: _scaleAnimation,
@@ -102,12 +103,13 @@ class _YearSelectorState extends State<YearSelector>
child: Text( child: Text(
'$_currentYear', '$_currentYear',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
), ),
), ),
SizedBox(width: 5),
circleIconButton( circleIconButton(
context: context, context: context,
icon: Icons.chevron_right, icon: Icons.chevron_right,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/moment.dart'; import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
class MomentCard extends StatelessWidget { class MomentCard extends StatelessWidget {
@@ -11,12 +12,8 @@ class MomentCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return buildCard(
elevation: 0, context: context,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -42,7 +39,6 @@ class MomentCard extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
@@ -132,7 +128,7 @@ class MomentCard extends StatelessWidget {
// 点击图片时,跳转到预览页面 // 点击图片时,跳转到预览页面
onTap: () => imageTapClick(index), onTap: () => imageTapClick(index),
// 原图片组件 // 原图片组件
child: networkImage(imageUrls[index]), child: buildNetworkImage(imageUrls[index]),
); );
}), }),
); );

View File

@@ -5,7 +5,6 @@ import 'package:food_hub_app/utils/date_util.dart';
import 'package:food_hub_app/utils/index.dart'; import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeCalendar extends StatefulWidget { class RecipeCalendar extends StatefulWidget {
const RecipeCalendar({super.key}); const RecipeCalendar({super.key});
@@ -90,49 +89,50 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
); );
} }
Widget dailyItem() { Widget dailyItem(BuildContext context) {
return Card( return Column(
elevation: 0,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')), Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
if (selectRecordList.isEmpty) if (selectRecordList.isEmpty)
TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据') buildEmptyData()
else else
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemCount: selectRecordList.length, itemCount: selectRecordList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return recipeRecordItem(selectRecordList[index]); return recipeRecordItem(context, selectRecordList[index]);
}, },
), ),
], ],
),
),
); );
} }
Widget recipeRecordItem(Record record) { Widget recipeRecordItem(BuildContext context, Record record) {
final colors = Theme.of(context).colorScheme;
return Card( return Card(
elevation: 0, elevation: 0,
color: Color(0xFFF5F5DC), color: colors.inversePrimary,
child: Padding( child: Padding(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
child: Row( child: Row(
children: [ children: [
TDAvatar( CircleAvatar(
size: TDAvatarSize.medium, radius: 24,
type: TDAvatarType.customText, backgroundColor: colors.primary,
text: record.category[0], child: Text(
record.category[0],
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
), ),
SizedBox(width: 10), SizedBox(width: 10),
Expanded( Expanded(
// 使用Expanded让文本区域占据剩余空间
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -149,7 +149,7 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
circleIconButton( circleIconButton(
context: context, context: context,
icon: Icons.chevron_right, icon: Icons.chevron_right,
onPressed: () => {} onPressed: () => {},
), ),
], ],
), ),
@@ -205,8 +205,8 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Card(elevation: 0, color: Colors.white, child: recipeCalendar()), buildCard(context: context, child: recipeCalendar()),
Expanded(child: dailyItem()), Expanded(child: buildCard(context: context, child: dailyItem(context))),
], ],
); );
} }

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
@@ -18,68 +17,7 @@ class RecipeCard extends StatelessWidget {
.map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}') .map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}')
.toList(); .toList();
Widget buildCarouselItem(String url) { Widget buildRecipeContent(BuildContext context) {
return Builder(
builder: (BuildContext context) {
return AspectRatio(
aspectRatio: 2,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: networkImage(url),
),
);
},
);
}
Widget buildRecipeCarousel() {
return FlutterCarousel(
// 轮播项
items:
imageUrls.map((url) {
return buildCarouselItem(url);
}).toList(),
// 轮播配置
options: FlutterCarouselOptions(
height: 300,
autoPlay: imageUrls.length > 1,
enableInfiniteScroll: imageUrls.length > 1,
autoPlayInterval: const Duration(seconds: 3),
viewportFraction: 0.9,
showIndicator: true,
),
);
}
return Card(
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.0),
borderRadius: BorderRadius.circular(10.0),
),
elevation: 0,
color: Colors.white,
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildRecipeCarousel(),
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).primaryColor,
indent: 0,
endIndent: 0,
),
Padding(
padding: const EdgeInsets.all(10),
child: _buildRecipeContent(context),
),
],
),
);
}
Widget _buildRecipeContent(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -90,7 +28,7 @@ class RecipeCard extends StatelessWidget {
child: Text( child: Text(
recipe.name, recipe.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1, // 限制单行,避免挤压按钮 maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -101,6 +39,7 @@ class RecipeCard extends StatelessWidget {
icon: Icons.edit, icon: Icons.edit,
onPressed: () => {}, onPressed: () => {},
), ),
SizedBox(width: 8),
circleIconButton( circleIconButton(
context: context, context: context,
icon: Icons.book, icon: Icons.book,
@@ -125,25 +64,25 @@ class RecipeCard extends StatelessWidget {
_buildIconText( _buildIconText(
icon: Icons.food_bank, icon: Icons.food_bank,
text: recipe.category, text: recipe.category,
color: Theme.of(context).primaryColor, color: Color(0xFF6A5ACD),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
_buildIconText( _buildIconText(
icon: Icons.thumb_up_alt_outlined, icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(), text: recipe.likeCount.toString(),
color: Colors.red, color: Color(0xFFDC143C),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
_buildIconText( _buildIconText(
icon: Icons.star_outline, icon: Icons.star_outline,
text: recipe.favouriteCount.toString(), text: recipe.favouriteCount.toString(),
color: Colors.blue, color: Color(0xFF20B2AA),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
_buildIconText( _buildIconText(
icon: Icons.comment_outlined, icon: Icons.comment_outlined,
text: recipe.commentCount.toString(), text: recipe.commentCount.toString(),
color: Colors.deepOrange, color: Color(0xFFDAC570),
), ),
], ],
), ),
@@ -152,7 +91,7 @@ class RecipeCard extends StatelessWidget {
_buildIconText( _buildIconText(
icon: Icons.date_range, icon: Icons.date_range,
text: recipe.recordList[0].date, text: recipe.recordList[0].date,
color: Colors.red, color: Color(0xFF32CD32),
), ),
], ],
), ),
@@ -162,6 +101,22 @@ class RecipeCard extends StatelessWidget {
); );
} }
return buildCard(
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1.5,
child: buildNetworkImage(imageUrls.first),
),
SizedBox(height: 8),
buildRecipeContent(context),
],
),
);
}
// 通用图标文本组件 // 通用图标文本组件
Widget _buildIconText({ Widget _buildIconText({
required IconData icon, required IconData icon,

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/apis/recipe.dart'; import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/recipe/card.dart'; import 'package:food_hub_app/widgets/recipe/card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeList extends StatefulWidget { class RecipeList extends StatefulWidget {
const RecipeList({super.key}); const RecipeList({super.key});
@@ -31,13 +31,16 @@ class _RecipeListState extends State<RecipeList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (recipeSummaryList.isEmpty) { if (recipeSummaryList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'); return buildEmptyData();
} else { } else {
return ListView.builder( return ListView.separated(
itemCount: recipeSummaryList.length, itemCount: recipeSummaryList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return RecipeCard(recipe: recipeSummaryList[index]); return RecipeCard(recipe: recipeSummaryList[index]);
} },
separatorBuilder: (context, index) {
return SizedBox(height: 10);
},
); );
} }
} }

View File

@@ -3,6 +3,7 @@ import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/common/year_selector.dart'; import 'package:food_hub_app/widgets/common/year_selector.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:timelines_plus/timelines_plus.dart'; import 'package:timelines_plus/timelines_plus.dart';
@@ -30,55 +31,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
}); });
} }
@override Widget buildTimelineCard(BuildContext context, Record record) {
Widget build(BuildContext context) {
return Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
),
Expanded(child: timelineContainer(recordList)),
],
);
}
}
Widget timelineContainer(List<Record> recordList) {
if (recordList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
} else {
return Timeline.tileBuilder(
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
builder: TimelineTileBuilder.connected(
itemCount: recordList.length,
connectorBuilder:
(context, index, type) => Connector.solidLine(
thickness: 2,
color: Theme.of(context).primaryColor,
),
indicatorBuilder: (context, index) {
return Indicator.dot(
size: 12.0,
color: Theme.of(context).primaryColor,
);
},
contentsBuilder: (context, index) {
return TimelineCard(record: recordList[index]);
},
),
);
}
}
class TimelineCard extends StatelessWidget {
final Record record;
const TimelineCard({super.key, required this.record});
Widget cardContent(BuildContext context) {
final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}']; final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}'];
void imageTapClick() { void imageTapClick() {
@@ -91,34 +44,8 @@ class TimelineCard extends StatelessWidget {
); );
} }
return Card( return buildCard(
elevation: 0, context: context,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Text(record.name, style: TextStyle(fontSize: 16)),
const SizedBox(height: 5),
GestureDetector(
onTap: () => imageTapClick(),
child: AspectRatio(
aspectRatio: 1.5,
child: networkImage(imageUrls[0]),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 10, bottom: 5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -126,10 +53,64 @@ class TimelineCard extends StatelessWidget {
record.date, record.date,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
cardContent(context), Column(
children: [
Text(record.name, style: TextStyle(fontSize: 16)),
const SizedBox(height: 5),
GestureDetector(
onTap: () => imageTapClick(),
child: AspectRatio(
aspectRatio: 1.5,
child: buildNetworkImage(imageUrls.first),
),
),
], ],
), ),
],
),
);
}
Widget buildTimeline(BuildContext context, List<Record> recordList) {
if (recordList.isEmpty) {
return buildEmptyData();
} else {
return Timeline.tileBuilder(
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
builder: TimelineTileBuilder.connected(
itemCount: recordList.length,
connectorBuilder:
(context, index, type) => Connector.solidLine(
thickness: 2,
color: Theme.of(context).colorScheme.primary,
),
indicatorBuilder: (context, index) {
return Indicator.dot(
size: 12.0,
color: Theme.of(context).colorScheme.primary,
);
},
contentsBuilder: (context, index) {
return buildTimelineCard(context, recordList[index]);
},
), ),
); );
} }
} }
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
),
Expanded(child: buildTimeline(context, recordList)),
],
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/models/stats.dart'; import 'package:food_hub_app/models/stats.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class StatisticCard extends StatelessWidget { class StatisticCard extends StatelessWidget {
final IconData icon; final IconData icon;
@@ -19,12 +20,8 @@ class StatisticCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return buildCard(
color: Colors.white, context: context,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -54,7 +51,6 @@ class StatisticCard extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }