feat:新增菜谱详细信息模块

This commit is contained in:
2025-07-31 22:55:39 +08:00
parent b5dd0ab749
commit 504ee073ce
12 changed files with 556 additions and 352 deletions

View File

@@ -27,6 +27,19 @@ InputDecoration formInputDecoration({
);
}
Widget cardContainer(Widget content) {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(10),
child: content,
),
);
}
/// 圆形图标按钮
Widget circleIconButton({
required IconData icon,
required VoidCallback onPressed,
@@ -70,6 +83,7 @@ Text buttonText({required String text}) {
return Text(text, style: TextStyle(color: Colors.white));
}
/// 图片错误显示
Widget errorImageContainer(double height) {
return Container(
height: 200,

View File

@@ -7,7 +7,7 @@ import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class RecipeCard extends StatelessWidget {
final Recipe recipe;
final RecipeSummary recipe;
const RecipeCard({super.key, required this.recipe});
@@ -104,7 +104,12 @@ class RecipeCard extends StatelessWidget {
circleIconButton(
context: context,
icon: Icons.book,
onPressed: () => Navigator.pushNamed(context, '/recipeDetail'),
onPressed:
() => Navigator.pushNamed(
context,
'/recipeDetail',
arguments: {'id': recipe.id},
),
),
],
),

View File

@@ -1,227 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/recipe.dart' as recipeModel;
class RecipeDetail extends StatefulWidget {
const RecipeDetail({super.key});
@override
State<RecipeDetail> createState() => _RecipeDetailState();
}
class _RecipeDetailState extends State<RecipeDetail> {
@override
void initState() {
super.initState();
}
recipeModel.Recipe recipe = recipeModel.Recipe(
name: '雪糕',
category: '冷饮',
recommendRate: 4,
isShare: true,
recordList: [],
stepList: [
recipeModel.Step(
sort: 1,
content: '将牛奶倒入锅中,小火加热至微沸',
imageUrl: 'https://picsum.photos/400/300?step=1', // 步骤图片URL
),
recipeModel.Step(sort: 2, content: '你好', imageUrl: ''),
],
materialList: [],
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('菜谱信息', style: TextStyle(color: Colors.white)),
backgroundColor: Theme.of(context).primaryColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
backgroundColor: Color(0xFFF5F5F5),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
recipe.name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
// 基础信息卡片
_buildInfoCard(
context,
icon: Icons.info,
title: "基础信息",
content: Column(
children: [_buildInfoRow("类别", recipe.category)],
),
),
// 食材信息卡片
_buildInfoCard(
context,
icon: Icons.shopping_cart,
title: "食材信息",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMaterialSection("主料", 1),
_buildMaterialSection("配料", 2),
_buildMaterialSection("辅料", 3),
],
),
),
// 步骤信息卡片
_buildInfoCard(
context,
icon: Icons.list,
title: "步骤信息",
content: Column(
children:
recipe.stepList?.map((step) {
return _buildStepSection(step);
}).toList() ??
[],
),
),
// 其他信息卡片
if (recipe.remark != null && recipe.remark!.isNotEmpty)
_buildInfoCard(
context,
icon: Icons.note,
title: "其他信息",
content: Text(
recipe.remark!,
style: const TextStyle(height: 1.5),
),
),
const SizedBox(height: 20),
],
),
),
),
);
}
// 构建食材分类部分
Widget _buildMaterialSection(String title, int type) {
final materials =
recipe.materialList
?.where((material) => material.type == type)
?.toList() ??
[];
if (materials.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
),
...materials.map((material) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
children: [
const Icon(Icons.circle, size: 6, color: Colors.grey),
const SizedBox(width: 8),
Text("${material.name} ${material.amount ?? ''}"),
],
),
);
}).toList(),
],
);
}
Widget _buildStepSection(recipeModel.Step step) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${step.sort}"),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(step.content, style: const TextStyle(height: 1.4, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (step.imageUrl.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
step.imageUrl,
width: double.infinity,
height: 180,
fit: BoxFit.contain,
),
),
],
),
),
],
),
);
}
// 构建信息行
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(label), Text(value)],
),
);
}
// 通用卡片组件
Widget _buildInfoCard(
BuildContext context, {
required IconData icon,
required String title,
required Widget content,
}) {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 5),
content,
],
),
),
);
}
}

View File

@@ -12,7 +12,7 @@ class RecipeList extends StatefulWidget {
}
class _RecipeListState extends State<RecipeList> {
List<Recipe> recipeList = [];
List<RecipeSummary> recipeSummaryList = [];
@override
void initState() {
@@ -24,19 +24,19 @@ class _RecipeListState extends State<RecipeList> {
final result = await queryRecipeApi(RecipeQuery(category: ""));
setState(() {
recipeList = result;
recipeSummaryList = result;
});
}
@override
Widget build(BuildContext context) {
if (recipeList.isEmpty) {
if (recipeSummaryList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
} else {
return ListView.builder(
itemCount: recipeList.length,
itemCount: recipeSummaryList.length,
itemBuilder: (context, index) {
return RecipeCard(recipe: recipeList[index]);
return RecipeCard(recipe: recipeSummaryList[index]);
}
);
}

View File

@@ -33,20 +33,16 @@ class _RecipeTimeline extends State<RecipeTimeline> {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
),
SizedBox(height: 10),
Expanded(child: timelineContainer(recordList)),
],
),
return Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
),
Expanded(child: timelineContainer(recordList)),
],
);
}
}