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,112 +12,79 @@ 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: (context) => Container( builder:
padding: const EdgeInsets.all(10), (context) => Container(
child: Column( padding: const EdgeInsets.all(10),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
const Text( children: [
'请选择操作', const Text(
style: TextStyle( '请选择操作',
fontSize: 18, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold, ),
), const SizedBox(height: 10),
ListTile(
leading: Icon(
Icons.book,
color: Theme.of(context).primaryColor,
),
title: const Text('新增菜谱'),
onTap: () {
Navigator.pop(context);
_handleAddRecipe();
},
),
ListTile(
leading: Icon(
Icons.note_add,
color: Theme.of(context).primaryColor,
),
title: const Text('新增记录'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, "/recordForm");
},
),
ListTile(
leading: Icon(
Icons.group,
color: Theme.of(context).primaryColor,
),
title: const Text('发布朋友圈'),
onTap: () {
Navigator.pop(context);
_handleAddRecord();
},
),
],
), ),
const SizedBox(height: 10), ),
ListTile(
leading: Icon(Icons.book, color: Theme.of(context).primaryColor),
title: const Text('新增菜谱'),
onTap: () {
Navigator.pop(context);
_handleAddRecipe();
},
),
ListTile(
leading: Icon(Icons.note_add, color: Theme.of(context).primaryColor),
title: const Text('新增记录'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, "/recordForm");
},
),
ListTile(
leading: Icon(Icons.group, color: Theme.of(context).primaryColor),
title: const Text('发布朋友圈'),
onTap: () {
Navigator.pop(context);
_handleAddRecord();
},
)
],
),
),
); );
} }
// 处理添加菜谱 // 处理添加菜谱
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,66 +126,61 @@ 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), controller: _freshController,
child: EasyRefresh( header: ClassicHeader(
controller: _freshController, dragText: '下拉刷新',
header: ClassicHeader( armedText: '释放刷新',
dragText: '下拉刷新', readyText: '准备刷新',
armedText: '释放刷新', processingText: '刷新中...',
readyText: '准备刷新', processedText: '刷新完成',
processingText: '刷新中...', failedText: '刷新失败',
processedText: '刷新完成', noMoreText: '没有更多数据',
failedText: '刷新失败', showText: true,
noMoreText: '没有更多数据', messageText: '更新于 %T',
showText: true, showMessage: true,
messageText: '更新于 %T', ),
showMessage: true, footer: ClassicFooter(
), dragText: '上拉加载',
footer: ClassicFooter( armedText: '释放加载',
dragText: '上拉加载', readyText: '准备加载',
armedText: '释放加载', processingText: '加载中...',
readyText: '准备加载', processedText: '加载完成',
processingText: '加载中...', failedText: '加载失败',
processedText: '加载完成', noMoreText: '没有更多数据',
failedText: '加载失败', showText: true,
noMoreText: '没有更多数据', messageText: '更新于 %T',
showText: true, showMessage: true,
messageText: '更新于 %T', ),
showMessage: true, onRefresh: _onRefresh,
), onLoad: _onLoad,
onRefresh: _onRefresh, child: ListView.builder(
onLoad: _onLoad, controller: _scrollController,
child: ListView.builder( itemCount: momentList.length,
controller: _scrollController, itemBuilder: (context, index) {
itemCount: momentList.length, return MomentCard(moment: momentList[index]);
itemBuilder: (context, index) { },
return MomentCard(moment: momentList[index]);
},
),
), ),
), ),
// 返回顶部按钮 // 返回顶部按钮
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,17 +47,15 @@ class _RecordPageState extends State<RecordPage>
backgroundColor: Colors.white, backgroundColor: Colors.white,
showIndicator: true, showIndicator: true,
), ),
SizedBox(height: 8),
Expanded( Expanded(
child: Padding( child: TabBarView(
padding: EdgeInsets.all(5), controller: _tabController,
child: TabBarView( children: [
controller: _tabController, RecipeList(),
children: [ RecipeCalendar(),
RecipeList(), RecipeTimeline(),
RecipeCalendar(), ],
RecipeTimeline(),
],
),
), ),
), ),
], ],

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,55 +58,22 @@ class _StatsPage extends State<StatsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Column(
padding: const EdgeInsets.all(5), children: [
child: Column( _buildSummaryStats(),
children: [ SizedBox(
_buildSummaryStats(), height: chartHeight,
SizedBox( child: buildCard(context: context, child: _buildRecordStats()),
height: chartHeight, ),
child: Card( SizedBox(
color: Colors.white, height: chartHeight,
elevation: 0, child: buildCard(context: context, child: _buildCategoryStats()),
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(10), SizedBox(
), height: chartHeight,
child: Padding( child: buildCard(context: context, child: _buildRankStats()),
padding: const EdgeInsets.all(10), ),
child: _buildRecordStats(), ],
),
),
),
SizedBox(
height: chartHeight,
child: Card(
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCategoryStats(),
),
),
),
SizedBox(
height: chartHeight,
child: Card(
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,18 +27,29 @@ 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)),
);
}
/// 圆形图标按钮 /// 圆形图标按钮
Widget circleIconButton({ Widget circleIconButton({
required IconData icon, required IconData icon,
@@ -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,37 +12,32 @@ 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, child: Row(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), crossAxisAlignment: CrossAxisAlignment.start,
child: Padding( children: [
padding: const EdgeInsets.all(10), _buildAvatar(context),
child: Row( const SizedBox(width: 12),
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
children: [ child: Column(
_buildAvatar(context), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(width: 12), children: [
Expanded( _buildNickname(),
child: Column( const SizedBox(height: 5),
crossAxisAlignment: CrossAxisAlignment.start, _buildContent(),
children: [ if (moment.imageList.isNotEmpty) ...[
_buildNickname(), const SizedBox(height: 8),
const SizedBox(height: 5), _buildPostImages(context),
_buildContent(),
if (moment.imageList.isNotEmpty) ...[
const SizedBox(height: 8),
_buildPostImages(context),
],
const SizedBox(height: 10),
_buildTime(),
const SizedBox(height: 5),
_buildActionButtons(),
], ],
), const SizedBox(height: 10),
_buildTime(),
const SizedBox(height: 5),
_buildActionButtons(),
],
), ),
], ),
), ],
), ),
); );
} }
@@ -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, crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.white, children: [
child: Padding( Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
padding: EdgeInsets.all(5), if (selectRecordList.isEmpty)
child: Column( buildEmptyData()
crossAxisAlignment: CrossAxisAlignment.start, else
children: [ ListView.builder(
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')), shrinkWrap: true,
if (selectRecordList.isEmpty) physics: NeverScrollableScrollPhysics(),
TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据') itemCount: selectRecordList.length,
else itemBuilder: (context, index) {
ListView.builder( return recipeRecordItem(context, selectRecordList[index]);
shrinkWrap: true, },
physics: NeverScrollableScrollPhysics(), ),
itemCount: selectRecordList.length, ],
itemBuilder: (context, index) {
return recipeRecordItem(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,150 +17,106 @@ 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( return Column(
builder: (BuildContext context) { crossAxisAlignment: CrossAxisAlignment.start,
return AspectRatio( children: [
aspectRatio: 2, Row(
child: ClipRRect( mainAxisAlignment: MainAxisAlignment.spaceBetween,
borderRadius: BorderRadius.circular(12), children: [
child: networkImage(url), Expanded(
), child: Text(
); recipe.name,
}, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
circleIconButton(
context: context,
icon: Icons.edit,
onPressed: () => {},
),
SizedBox(width: 8),
circleIconButton(
context: context,
icon: Icons.book,
onPressed:
() => Navigator.pushNamed(
context,
'/recipeDetail',
arguments: {'id': recipe.id},
),
),
],
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Color(0xFF6A5ACD),
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(),
color: Color(0xFFDC143C),
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.star_outline,
text: recipe.favouriteCount.toString(),
color: Color(0xFF20B2AA),
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.comment_outlined,
text: recipe.commentCount.toString(),
color: Color(0xFFDAC570),
),
],
),
Row(
children: [
_buildIconText(
icon: Icons.date_range,
text: recipe.recordList[0].date,
color: Color(0xFF32CD32),
),
],
),
],
),
],
); );
} }
Widget buildRecipeCarousel() { return buildCard(
return FlutterCarousel( context: context,
// 轮播项
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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildRecipeCarousel(), AspectRatio(
Divider( aspectRatio: 1.5,
height: 1, child: buildNetworkImage(imageUrls.first),
thickness: 1,
color: Theme.of(context).primaryColor,
indent: 0,
endIndent: 0,
),
Padding(
padding: const EdgeInsets.all(10),
child: _buildRecipeContent(context),
), ),
SizedBox(height: 8),
buildRecipeContent(context),
], ],
), ),
); );
} }
Widget _buildRecipeContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
recipe.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1, // 限制单行,避免挤压按钮
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
circleIconButton(
context: context,
icon: Icons.edit,
onPressed: () => {},
),
circleIconButton(
context: context,
icon: Icons.book,
onPressed:
() => Navigator.pushNamed(
context,
'/recipeDetail',
arguments: {'id': recipe.id},
),
),
],
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Theme.of(context).primaryColor,
),
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.favouriteCount.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.recordList[0].date,
color: Colors.red,
),
],
),
],
),
],
);
}
// 通用图标文本组件 // 通用图标文本组件
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,45 +44,73 @@ class TimelineCard extends StatelessWidget {
); );
} }
return Card( return buildCard(
elevation: 0, context: context,
color: Colors.white, child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: EdgeInsets.all(10), children: [
child: Column( Text(
children: [ record.date,
Text(record.name, style: TextStyle(fontSize: 16)), style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
const SizedBox(height: 5), ),
GestureDetector( Column(
onTap: () => imageTapClick(), children: [
child: AspectRatio( Text(record.name, style: TextStyle(fontSize: 16)),
aspectRatio: 1.5, const SizedBox(height: 5),
child: networkImage(imageUrls[0]), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Column(
width: double.infinity, crossAxisAlignment: CrossAxisAlignment.start,
child: Padding( children: [
padding: const EdgeInsets.only(left: 10, bottom: 5), YearSelector(
child: Column( initialYear: DateTime.now().year,
crossAxisAlignment: CrossAxisAlignment.start, minYear: 2000,
children: [ maxYear: 2100,
Text( onYearChanged: (year) => refreshRecord(year),
record.date,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
cardContent(context),
],
), ),
), 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,41 +20,36 @@ 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, child: Row(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), mainAxisAlignment: MainAxisAlignment.center,
child: Padding( children: [
padding: const EdgeInsets.all(10.0), SizedBox(
child: Row( width: 40,
mainAxisAlignment: MainAxisAlignment.center, height: 40,
children: [ child: Icon(icon, color: color, size: 40),
SizedBox( ),
width: 40, const SizedBox(width: 10),
height: 40, Column(
child: Icon(icon, color: color, size: 40), crossAxisAlignment: CrossAxisAlignment.center,
), mainAxisAlignment: MainAxisAlignment.center,
const SizedBox(width: 10), children: [
Column( Text(title, style: TextStyle(fontSize: 20, color: color)),
crossAxisAlignment: CrossAxisAlignment.center, const SizedBox(height: 4),
mainAxisAlignment: MainAxisAlignment.center, Row(
children: [ children: [
Text(title, style: TextStyle(fontSize: 20, color: color)), Text(
const SizedBox(height: 4), value.toString(),
Row( style: TextStyle(fontSize: 20, color: color),
children: [ ),
Text( const SizedBox(width: 5),
value.toString(), Text(unit, style: TextStyle(fontSize: 20, color: color)),
style: TextStyle(fontSize: 20, color: color), ],
), ),
const SizedBox(width: 5), ],
Text(unit, style: TextStyle(fontSize: 20, color: color)), ),
], ],
),
],
),
],
),
), ),
); );
} }