feat:更新菜谱显示模块

This commit is contained in:
2025-11-18 15:57:00 +08:00
parent df26790389
commit 9a8e1a7419
9 changed files with 244 additions and 221 deletions

View File

@@ -32,14 +32,11 @@ final List<PageInfo> pageInfos = [
), ),
]; ];
class NavBar extends StatelessWidget { Widget buildBottomNavBar({
final int currentIndex; required int currentIndex,
required Function(int) onTap,
final Function(int) onTap; }) {
final List<BottomNavigationBarItem> items =
const NavBar({super.key, required this.currentIndex, required this.onTap});
List<BottomNavigationBarItem> get items =>
pageInfos pageInfos
.map( .map(
(item) => BottomNavigationBarItem( (item) => BottomNavigationBarItem(
@@ -50,37 +47,78 @@ class NavBar extends StatelessWidget {
) )
.toList(); .toList();
@override return BottomNavigationBar(
Widget build(BuildContext context) { currentIndex: currentIndex,
return Container( iconSize: 25,
decoration: BoxDecoration( type: BottomNavigationBarType.fixed,
border: Border(top: BorderSide(color: Theme.of(context).primaryColor)), backgroundColor: Colors.white,
), items: items,
child: BottomNavigationBar( onTap: onTap,
currentIndex: currentIndex, );
iconSize: 25,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: items,
onTap: onTap,
),
);
}
} }
List<StatelessWidget> homeActions(BuildContext context) { void buildBottomSheet(BuildContext context) {
return [ showModalBottomSheet(
IconButton( context: context,
icon: Icon(Icons.search, color: Colors.white), shape: const RoundedRectangleBorder(
onPressed: () { borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
// 搜索功能
},
), ),
// IconButton( builder: (context) => buildBottomSheetBody(context),
// icon: Icon(Icons.add, color: Colors.white), );
// onPressed: () { }
// Navigator.pushNamed(context, '/recordForm');
// }, Widget buildBottomSheetBody(BuildContext context) {
// ), final colors = Theme.of(context).colorScheme;
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const Text(
'请选择操作',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
ListTile(
leading: Icon(Icons.book, color: colors.primary),
title: const Text('新增菜谱'),
onTap: () {
Navigator.pop(context);
_handleAddRecipe(context);
},
),
ListTile(
leading: Icon(Icons.note_add, color: colors.primary),
title: const Text('新增记录'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, "/recordForm");
},
),
ListTile(
leading: Icon(Icons.group, color: colors.primary),
title: const Text('发布朋友圈'),
onTap: () {
Navigator.pop(context);
_handleAddRecord(context);
},
),
],
);
}
// 处理添加菜谱
void _handleAddRecipe(BuildContext context) {
// 这里添加跳转或处理添加菜谱的逻辑
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('添加菜谱功能')));
}
// 处理添加记录
void _handleAddRecord(BuildContext context) {
// 这里添加跳转或处理添加记录的逻辑
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('添加记录功能')));
} }

View File

@@ -14,79 +14,6 @@ class _HomePage extends State<HomePage> {
List<Widget> get tabPages => pageInfos.map((item) => item.page).toList(); List<Widget> get tabPages => pageInfos.map((item) => item.page).toList();
// 显示底部弹窗
void _showBottomSheet() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder:
(context) => Container(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'请选择操作',
style: TextStyle(fontSize: 18, 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();
},
),
],
),
),
);
}
// 处理添加菜谱
void _handleAddRecipe() {
// 这里添加跳转或处理添加菜谱的逻辑
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('添加菜谱功能')));
}
// 处理添加记录
void _handleAddRecord() {
// 这里添加跳转或处理添加记录的逻辑
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('添加记录功能')));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -103,23 +30,27 @@ class _HomePage extends State<HomePage> {
); );
}, },
), ),
actions: homeActions(context), actions: [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
// 搜索功能
},
),
],
), ),
backgroundColor: Color(0xFFF5F5F5), backgroundColor: Color(0xFFF5F5F5),
drawer: SettingsDrawer(), drawer: SettingsDrawer(),
body: Padding( body: Padding(padding: EdgeInsets.all(4), child: tabPages[_currentIndex]),
padding: EdgeInsets.all(4),
child: tabPages[_currentIndex],
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).colorScheme.primary,
onPressed: _showBottomSheet, onPressed: () => buildBottomSheet(context),
shape: const CircleBorder(), shape: const CircleBorder(),
mini: true, 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: buildBottomNavBar(
currentIndex: _currentIndex, currentIndex: _currentIndex,
onTap: (index) { onTap: (index) {
setState(() { setState(() {

View File

@@ -43,8 +43,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
Future<void> refreshRecipeDetail() async { Future<void> refreshRecipeDetail() async {
// 获取参数 // 获取参数
final args = ModalRoute.of(context)?.settings.arguments as Map; final args = ModalRoute.of(context)?.settings.arguments as Map;
final recipeId = args['id']; final result = await queryRecipeByIdApi(args['id']);
final result = await queryRecipeByIdApi(recipeId);
setState(() { setState(() {
recipe = result; recipe = result;
@@ -71,13 +70,8 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text('菜谱信息')),
title: Text('菜谱信息'), backgroundColor: Color(0xFFF4F4F4),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()), child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
), ),
@@ -89,12 +83,11 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_buildTitleSection(), _buildTitleSection(),
SizedBox(height: 10),
_buildInfoCard( _buildInfoCard(
context, context,
icon: Icons.info, icon: Icons.info,
title: "基础信息", title: "基础信息",
content: _buildTag(context, title: recipe.category), content: buildTag(context, recipe.category),
), ),
_buildInfoCard( _buildInfoCard(
context, context,
@@ -131,20 +124,6 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
); );
} }
Widget _buildTag(BuildContext context, {required String title}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Color.lerp(Theme.of(context).primaryColor, Colors.white, 0.8),
borderRadius: BorderRadius.circular(10),
),
child: Text(
title,
style: TextStyle(color: Theme.of(context).primaryColor),
),
);
}
Widget _buildTitleSection() { Widget _buildTitleSection() {
return Text( return Text(
recipe.name, recipe.name,
@@ -165,16 +144,14 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)), child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
), ),
if (materials.isEmpty) if (materials.isEmpty)
_buildTag(context, title: '') buildTag(context, '')
else else
Wrap( Wrap(
spacing: 8.0, spacing: 8.0,
runSpacing: 8.0, runSpacing: 8.0,
children: children:
materials materials
.map( .map((m) => buildTag(context, '${m.name} ${m.amount}'))
(m) => _buildTag(context, title: '${m.name} ${m.amount}'),
)
.toList(), .toList(),
), ),
], ],
@@ -229,7 +206,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
children: [ children: [
Row( Row(
children: [ children: [
Icon(icon, color: Theme.of(context).primaryColor), Icon(icon, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
title, title,
@@ -240,7 +217,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
SizedBox(height: 5), SizedBox(height: 5),
content, content,
], ],
) ),
); );
} }
} }

View File

@@ -19,6 +19,7 @@ class ImagePreviewPage extends StatefulWidget {
class _ImagePreviewPageState extends State<ImagePreviewPage> { class _ImagePreviewPageState extends State<ImagePreviewPage> {
// 声明 PageController 并初始化初始索引 // 声明 PageController 并初始化初始索引
late PageController _pageController; late PageController _pageController;
// 记录当前显示的图片索引(用于更新页码) // 记录当前显示的图片索引(用于更新页码)
int _currentIndex = 0; int _currentIndex = 0;
@@ -68,17 +69,18 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
centerTitle: true, centerTitle: true,
), ),
body: PhotoViewGallery( body: PhotoViewGallery(
pageOptions: widget.images.map((url) { pageOptions:
return PhotoViewGalleryPageOptions( widget.images.map((url) {
imageProvider: NetworkImage(url), return PhotoViewGalleryPageOptions(
minScale: PhotoViewComputedScale.contained, imageProvider: NetworkImage(url),
maxScale: PhotoViewComputedScale.covered * 2, minScale: PhotoViewComputedScale.contained,
// 点击空白处关闭预览 maxScale: PhotoViewComputedScale.covered * 2,
onTapDown: (context, details, controllerValue) { // 点击空白处关闭预览
Navigator.pop(context); onTapDown: (context, details, controllerValue) {
}, Navigator.pop(context);
); },
}).toList(), );
}).toList(),
pageController: _pageController, pageController: _pageController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
), ),
@@ -86,28 +88,92 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
} }
} }
Widget buildNetworkImage(String url) { Widget buildNetworkImage(BuildContext context, String url) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.network( child: GestureDetector(
url, onTap: () {
fit: BoxFit.cover, _showFullScreenImage(context, url);
loadingBuilder: (context, child, loadingProgress) {
// 加载中显示占位符
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
}, },
errorBuilder: (context, error, stackTrace) { child: Image.network(
return Container( url,
color: Colors.grey[200], fit: BoxFit.cover,
child: const Icon(Icons.image, color: Colors.grey, size: 30), loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingIndicator(loadingProgress);
},
errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
),
),
);
}
Widget _buildLoadingIndicator(ImageChunkEvent? loadingProgress) {
return Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
value:
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
}
Widget _buildErrorImage() {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
}
Widget _buildPhotoView(String url) {
return PhotoView(
imageProvider: NetworkImage(url),
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
initialScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(tag: url),
loadingBuilder: (context, event) => _buildLoadingIndicator(event),
errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
);
}
Widget _buildCloseImage(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 20,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.of(context).pop(),
),
);
}
void _showFullScreenImage(BuildContext context, String url) {
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return Scaffold(
backgroundColor: Colors.black.withAlpha(200),
body: Stack(
children: [
// 可缩放图片
Positioned.fill(child: _buildPhotoView(url)),
// 关闭按钮
_buildCloseImage(context),
],
),
); );
}, },
), ),

View File

@@ -83,7 +83,7 @@ Widget circleIconButton({
required IconData icon, required IconData icon,
required VoidCallback onPressed, required VoidCallback onPressed,
required BuildContext context, required BuildContext context,
bool? isSmall bool? isSmall,
}) { }) {
final double size = isSmall == true ? 24 : 36; final double size = isSmall == true ? 24 : 36;
@@ -101,6 +101,19 @@ Widget circleIconButton({
); );
} }
Widget buildTag(BuildContext context, String title) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Color.lerp(colors.primary, Colors.white, 0.8),
borderRadius: BorderRadius.circular(10),
),
child: Text(title, style: TextStyle(color: colors.primary)),
);
}
/// 确认按钮样式 /// 确认按钮样式
ButtonStyle primaryButtonStyle() { ButtonStyle primaryButtonStyle() {
return ButtonStyle( return ButtonStyle(

View File

@@ -94,7 +94,6 @@ class _YearSelectorState extends State<YearSelector>
isSmall: true, isSmall: true,
onPressed: () => _previousYear(), onPressed: () => _previousYear(),
), ),
SizedBox(width: 5),
// 年份显示 // 年份显示
AnimatedBuilder( AnimatedBuilder(
animation: _scaleAnimation, animation: _scaleAnimation,
@@ -110,7 +109,6 @@ class _YearSelectorState extends State<YearSelector>
), ),
), ),
), ),
SizedBox(width: 5),
circleIconButton( circleIconButton(
context: context, context: context,
icon: Icons.chevron_right, icon: Icons.chevron_right,

View File

@@ -128,7 +128,7 @@ class MomentCard extends StatelessWidget {
// 点击图片时,跳转到预览页面 // 点击图片时,跳转到预览页面
onTap: () => imageTapClick(index), onTap: () => imageTapClick(index),
// 原图片组件 // 原图片组件
child: buildNetworkImage(imageUrls[index]), child: buildNetworkImage(context, imageUrls[index]),
); );
}), }),
); );

View File

@@ -10,12 +10,23 @@ class RecipeCard extends StatelessWidget {
const RecipeCard({super.key, required this.recipe}); const RecipeCard({super.key, required this.recipe});
void navigatorToRecipeDetail(BuildContext context) {
Navigator.pushNamed(context, '/recipeDetail', arguments: {'id': recipe.id});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> imageUrls = final String firstImageUrl = recipe.recordList.first.imageUrl;
recipe.recordList
.map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}') Widget buildRecipeImage() {
.toList(); return AspectRatio(
aspectRatio: 1.5,
child: buildNetworkImage(
context,
'${AppConfig.baseApiUrl}/$firstImageUrl',
),
);
}
Widget buildRecipeContent(BuildContext context) { Widget buildRecipeContent(BuildContext context) {
return Column( return Column(
@@ -32,26 +43,6 @@ class RecipeCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Row(
children: [
circleIconButton(
context: context,
icon: Icons.edit,
onPressed: () => {},
),
SizedBox(width: 4),
circleIconButton(
context: context,
icon: Icons.book,
onPressed:
() => Navigator.pushNamed(
context,
'/recipeDetail',
arguments: {'id': recipe.id},
),
),
],
),
], ],
), ),
Row( Row(
@@ -105,12 +96,12 @@ class RecipeCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( buildRecipeImage(),
aspectRatio: 1.5,
child: buildNetworkImage(imageUrls.first),
),
SizedBox(height: 8), SizedBox(height: 8),
buildRecipeContent(context), GestureDetector(
onTap: () => navigatorToRecipeDetail(context),
child: buildRecipeContent(context)
)
], ],
), ),
); );
@@ -120,10 +111,10 @@ class RecipeCard extends StatelessWidget {
Widget _buildIconText({ Widget _buildIconText({
required IconData icon, required IconData icon,
required String text, required String text,
Color color = Colors.grey, // 默认灰色 Color color = Colors.grey,
double iconSize = 16, double iconSize = 16,
double textSize = 16, double textSize = 16,
double spacing = 2, // 图标与文本间距 double spacing = 2,
}) { }) {
return Row( return Row(
children: [ children: [

View File

@@ -48,19 +48,28 @@ class _RecipeTimeline extends State<RecipeTimeline> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
record.date, children: [
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), Icon(Icons.date_range, color: Theme.of(context).colorScheme.primary),
SizedBox(width: 5),
Text(
record.date,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
), ),
Column( Column(
children: [ children: [
Text(record.name, style: TextStyle(fontSize: 16)), Text(
record.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 5), const SizedBox(height: 5),
GestureDetector( GestureDetector(
onTap: () => imageTapClick(), onTap: () => imageTapClick(),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.5, aspectRatio: 1.5,
child: buildNetworkImage(imageUrls.first), child: buildNetworkImage(context, imageUrls.first),
), ),
), ),
], ],