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

@@ -2,10 +2,10 @@ import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart';
Future<Recipe> queryRecipeByIdApi(int id) {
return HttpUtil().get<Recipe>(
Future<RecipeDetail> queryRecipeByIdApi(int id) {
return HttpUtil().get<RecipeDetail>(
"/food/recipe/$id",
converter: (data) => Recipe.fromJson(data),
converter: (data) => RecipeDetail.fromJson(data),
);
}
@@ -35,11 +35,11 @@ Future<List<Recipe>> queryRecipeUserFavouriteApi() {
);
}
Future<List<Recipe>> queryRecipeApi(RecipeQuery recipeQuery) {
return HttpUtil().get<List<Recipe>>(
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
return HttpUtil().get<List<RecipeSummary>>(
"/food/recipe",
queryParameters: recipeQuery.toJson(),
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
converter: (data) => convertListResponse<RecipeSummary>(data, RecipeSummary.fromJson),
);
}

View File

@@ -3,8 +3,8 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/views/home.dart';
import 'package:food_hub_app/views/login.dart';
import 'package:food_hub_app/views/recordForm.dart';
import 'package:food_hub_app/widgets/recipe/detail.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/views/recipe_detail.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -47,7 +47,7 @@ class MyApp extends StatelessWidget {
routes: {
'/home': (context) => HomePage(),
'/recordForm': (context) => RecordFormPage(),
'/recipeDetail': (context) => RecipeDetail(),
'/recipeDetail': (context) => RecipeDetailPage(),
},
);
}

View File

@@ -4,43 +4,43 @@ part 'recipe.g.dart';
/// 食材信息
@JsonSerializable()
class Material {
class RecipeMaterial {
String type;
String name;
String amount;
Material({required this.type, required this.name, required this.amount});
RecipeMaterial({required this.type, required this.name, required this.amount});
factory Material.fromJson(Map<String, dynamic> json) =>
_$MaterialFromJson(json);
factory RecipeMaterial.fromJson(Map<String, dynamic> json) =>
_$RecipeMaterialFromJson(json);
Map<String, dynamic> toJson() => _$MaterialToJson(this);
Map<String, dynamic> toJson() => _$RecipeMaterialToJson(this);
}
/// 步骤信息
@JsonSerializable()
class Step {
class RecipeStep {
int sort;
String content;
String imageUrl;
Step({required this.sort, required this.content, required this.imageUrl});
RecipeStep({required this.sort, required this.content, required this.imageUrl});
factory Step.fromJson(Map<String, dynamic> json) => _$StepFromJson(json);
factory RecipeStep.fromJson(Map<String, dynamic> json) => _$RecipeStepFromJson(json);
Map<String, dynamic> toJson() => _$StepToJson(this);
Map<String, dynamic> toJson() => _$RecipeStepToJson(this);
}
/// 评论信息
@JsonSerializable()
class Comment {
class RecipeComment {
int id;
String username;
String avatar;
String content;
String date;
Comment({
RecipeComment({
required this.id,
required this.username,
required this.avatar,
@@ -48,10 +48,10 @@ class Comment {
required this.date,
});
factory Comment.fromJson(Map<String, dynamic> json) =>
_$CommentFromJson(json);
factory RecipeComment.fromJson(Map<String, dynamic> json) =>
_$RecipeCommentFromJson(json);
Map<String, dynamic> toJson() => _$CommentToJson(this);
Map<String, dynamic> toJson() => _$RecipeCommentToJson(this);
}
/// 成果信息
@@ -94,44 +94,44 @@ class RecipeQuery {
/// 菜谱信息
@JsonSerializable()
class Recipe {
int? id;
int id;
String name;
String category;
double recommendRate;
String? remark;
String remark;
bool isShare;
int? userId;
String? username;
String? avatar;
List<Material>? materialList;
List<Step>? stepList;
int userId;
String username;
String avatar;
List<RecipeMaterial> materialList;
List<RecipeStep> stepList;
List<Record> recordList;
List<int>? likeList;
int? likeCount;
List<int>? favouriteList;
int? favouriteCount;
List<Comment>? commentList;
int? commentCount;
List<int> likeList;
int likeCount;
List<int> favouriteList;
int favouriteCount;
List<RecipeComment> commentList;
int commentCount;
Recipe({
this.id,
required this.id,
required this.name,
required this.category,
required this.recommendRate,
this.remark,
required this.remark,
required this.isShare,
this.userId,
this.username,
this.avatar,
this.materialList,
this.stepList,
required this.userId,
required this.username,
required this.avatar,
required this.materialList,
required this.stepList,
required this.recordList,
this.likeList,
this.likeCount,
this.favouriteList,
this.favouriteCount,
this.commentList,
this.commentCount,
required this.likeList,
required this.likeCount,
required this.favouriteList,
required this.favouriteCount,
required this.commentList,
required this.commentCount,
});
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
@@ -139,4 +139,80 @@ class Recipe {
Map<String, dynamic> toJson() => _$RecipeToJson(this);
}
@JsonSerializable()
class RecipeSummary {
int id;
String name;
String category;
double recommendRate;
bool isShare;
List<Record> recordList;
int likeCount;
int favouriteCount;
int commentCount;
int userId;
String username;
String avatar;
RecipeSummary({
required this.id,
required this.name,
required this.category,
required this.recommendRate,
required this.isShare,
required this.userId,
required this.username,
required this.avatar,
required this.recordList,
required this.likeCount,
required this.favouriteCount,
required this.commentCount,
});
factory RecipeSummary.fromJson(Map<String, dynamic> json) => _$RecipeSummaryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this);
}
@JsonSerializable()
class RecipeDetail {
int id;
String name;
String category;
double recommendRate;
String remark;
bool isShare;
int userId;
String username;
String avatar;
List<RecipeMaterial> materialList;
List<RecipeStep> stepList;
List<Record> recordList;
List<int> likeList;
List<int> favouriteList;
List<RecipeComment> commentList;
RecipeDetail({
required this.id,
required this.name,
required this.category,
required this.recommendRate,
required this.remark,
required this.isShare,
required this.userId,
required this.username,
required this.avatar,
required this.materialList,
required this.stepList,
required this.recordList,
required this.likeList,
required this.favouriteList,
required this.commentList,
});
factory RecipeDetail.fromJson(Map<String, dynamic> json) => _$RecipeDetailFromJson(json);
Map<String, dynamic> toJson() => _$RecipeDetailToJson(this);
}
enum ViewType { recipe, calendar, timeline }

View File

@@ -6,45 +6,50 @@ part of 'recipe.dart';
// JsonSerializableGenerator
// **************************************************************************
Material _$MaterialFromJson(Map<String, dynamic> json) => Material(
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
RecipeMaterial(
type: json['type'] as String,
name: json['name'] as String,
amount: json['amount'] as String,
);
);
Map<String, dynamic> _$MaterialToJson(Material instance) => <String, dynamic>{
Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
<String, dynamic>{
'type': instance.type,
'name': instance.name,
'amount': instance.amount,
};
};
Step _$StepFromJson(Map<String, dynamic> json) => Step(
RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
sort: (json['sort'] as num).toInt(),
content: json['content'] as String,
imageUrl: json['imageUrl'] as String,
);
Map<String, dynamic> _$StepToJson(Step instance) => <String, dynamic>{
Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
<String, dynamic>{
'sort': instance.sort,
'content': instance.content,
'imageUrl': instance.imageUrl,
};
};
Comment _$CommentFromJson(Map<String, dynamic> json) => Comment(
RecipeComment _$RecipeCommentFromJson(Map<String, dynamic> json) =>
RecipeComment(
id: (json['id'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
content: json['content'] as String,
date: json['date'] as String,
);
);
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
<String, dynamic>{
'id': instance.id,
'username': instance.username,
'avatar': instance.avatar,
'content': instance.content,
'date': instance.date,
};
};
Record _$RecordFromJson(Map<String, dynamic> json) => Record(
id: (json['id'] as num?)?.toInt(),
@@ -71,42 +76,42 @@ Map<String, dynamic> _$RecipeQueryToJson(RecipeQuery instance) =>
<String, dynamic>{'category': instance.category};
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
id: (json['id'] as num?)?.toInt(),
id: (json['id'] as num).toInt(),
name: json['name'] as String,
category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(),
remark: json['remark'] as String?,
remark: json['remark'] as String,
isShare: json['isShare'] as bool,
userId: (json['userId'] as num?)?.toInt(),
username: json['username'] as String?,
avatar: json['avatar'] as String?,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
materialList:
(json['materialList'] as List<dynamic>?)
?.map((e) => Material.fromJson(e as Map<String, dynamic>))
(json['materialList'] as List<dynamic>)
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
.toList(),
stepList:
(json['stepList'] as List<dynamic>?)
?.map((e) => Step.fromJson(e as Map<String, dynamic>))
(json['stepList'] as List<dynamic>)
.map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
.toList(),
recordList:
(json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>))
.toList(),
likeList:
(json['likeList'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
(json['likeList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
likeCount: (json['likeCount'] as num?)?.toInt(),
likeCount: (json['likeCount'] as num).toInt(),
favouriteList:
(json['favouriteList'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
(json['favouriteList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
favouriteCount: (json['favouriteCount'] as num?)?.toInt(),
favouriteCount: (json['favouriteCount'] as num).toInt(),
commentList:
(json['commentList'] as List<dynamic>?)
?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
(json['commentList'] as List<dynamic>)
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
.toList(),
commentCount: (json['commentCount'] as num?)?.toInt(),
commentCount: (json['commentCount'] as num).toInt(),
);
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
@@ -129,3 +134,93 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
'commentList': instance.commentList,
'commentCount': instance.commentCount,
};
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
RecipeSummary(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(),
isShare: json['isShare'] as bool,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
recordList:
(json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>))
.toList(),
likeCount: (json['likeCount'] as num).toInt(),
favouriteCount: (json['favouriteCount'] as num).toInt(),
commentCount: (json['commentCount'] as num).toInt(),
);
Map<String, dynamic> _$RecipeSummaryToJson(RecipeSummary instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'category': instance.category,
'recommendRate': instance.recommendRate,
'isShare': instance.isShare,
'recordList': instance.recordList,
'likeCount': instance.likeCount,
'favouriteCount': instance.favouriteCount,
'commentCount': instance.commentCount,
'userId': instance.userId,
'username': instance.username,
'avatar': instance.avatar,
};
RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(),
remark: json['remark'] as String,
isShare: json['isShare'] as bool,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
materialList:
(json['materialList'] as List<dynamic>)
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
.toList(),
stepList:
(json['stepList'] as List<dynamic>)
.map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
.toList(),
recordList:
(json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>))
.toList(),
likeList:
(json['likeList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
favouriteList:
(json['favouriteList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
commentList:
(json['commentList'] as List<dynamic>)
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$RecipeDetailToJson(RecipeDetail instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'category': instance.category,
'recommendRate': instance.recommendRate,
'remark': instance.remark,
'isShare': instance.isShare,
'userId': instance.userId,
'username': instance.username,
'avatar': instance.avatar,
'materialList': instance.materialList,
'stepList': instance.stepList,
'recordList': instance.recordList,
'likeList': instance.likeList,
'favouriteList': instance.favouriteList,
'commentList': instance.commentList,
};

View File

@@ -14,11 +14,10 @@ class MomentPage extends StatefulWidget {
class _MomentPageState extends State<MomentPage> {
List<Moment> momentList = [];
// 分页相关变量
int _page = 1;
int _currentPage = 1;
final int _pageSize = 3;
bool _hasMore = true;
// 初始化EasyRefresh控制器
final EasyRefreshController _freshController = EasyRefreshController(
controlFinishRefresh: true,
@@ -49,11 +48,10 @@ class _MomentPageState extends State<MomentPage> {
try {
// 如果是刷新,重置页码
if (isRefresh) {
_page = 1;
_currentPage = 1;
}
// 调用API获取数据传入页码和页大小
final result = await queryMomentByPageApi(_page, _pageSize);
final result = await queryMomentByPageApi(_currentPage, _pageSize);
setState(() {
if (isRefresh) {
@@ -68,7 +66,7 @@ class _MomentPageState extends State<MomentPage> {
_hasMore = result.current < result.pages;
// 如果有更多数据,准备加载下一页
if (_hasMore) {
_page++;
_currentPage++;
}
});
} catch (e) {

View File

@@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class RecipeDetailPage extends StatefulWidget {
const RecipeDetailPage({super.key});
@override
State<RecipeDetailPage> createState() => _RecipeDetailState();
}
class _RecipeDetailState extends State<RecipeDetailPage> {
RecipeDetail recipe = RecipeDetail(
id: 0,
name: '',
category: '',
recommendRate: 0,
remark: '',
isShare: false,
userId: 0,
username: '',
avatar: '',
materialList: [],
stepList: [],
recordList: [],
likeList: [],
favouriteList: [],
commentList: [],
);
// 定义三个分类列表
List<RecipeMaterial> mainMaterialList = []; // 主料
List<RecipeMaterial> auxiliaryMaterialList = []; // 配料
List<RecipeMaterial> accessoryMaterialList = []; // 辅料
@override
void didChangeDependencies() {
super.didChangeDependencies();
refreshRecipeDetail();
}
Future<void> refreshRecipeDetail() async {
// 获取参数
final args = ModalRoute.of(context)?.settings.arguments as Map;
final recipeId = args['id'];
final result = await queryRecipeByIdApi(recipeId);
setState(() {
recipe = result;
getRecipeMaterial();
});
}
void getRecipeMaterial() {
for (var material in recipe.materialList) {
switch (material.type) {
case '主料':
mainMaterialList.add(material);
break;
case '配料':
auxiliaryMaterialList.add(material);
break;
case '辅料':
accessoryMaterialList.add(material);
break;
}
}
}
@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: _buildRecipeDetail()),
),
);
}
Widget _buildRecipeDetail() {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildTitleSection(),
SizedBox(height: 10),
_buildInfoCard(
context,
icon: Icons.info,
title: "基础信息",
content: _buildTag(context, title: recipe.category),
),
_buildInfoCard(
context,
icon: Icons.shopping_cart,
title: "食材信息",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMaterialSection(context, '主料', mainMaterialList),
_buildMaterialSection(context, '辅料', auxiliaryMaterialList),
_buildMaterialSection(context, '调料', accessoryMaterialList),
],
),
),
_buildInfoCard(
context,
icon: Icons.list,
title: "步骤信息",
content: Column(
children:
recipe.stepList.map((step) => _buildStepSection(step)).toList(),
),
),
_buildInfoCard(
context,
icon: Icons.note,
title: "其他信息",
content: Text(
'备注:${recipe.remark.isEmpty ? '' : recipe.remark}',
style: const TextStyle(height: 1.5),
),
),
],
);
}
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() {
return Text(
recipe.name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
}
Widget _buildMaterialSection(
BuildContext context,
String title,
List<RecipeMaterial> materials,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8, top: 12),
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
),
if (materials.isEmpty)
_buildTag(context, title: '')
else
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children:
materials
.map(
(m) => _buildTag(context, title: '${m.name} ${m.amount}'),
)
.toList(),
),
],
);
}
Widget _buildStepSection(RecipeStep step) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${step.sort + 1}"),
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 _buildInfoCard(
BuildContext context, {
required IconData icon,
required String title,
required Widget content,
}) {
return cardContainer(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).primaryColor),
const SizedBox(width: 5),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 5),
content,
],
),
);
}
}

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,9 +33,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
return Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
@@ -43,10 +41,8 @@ class _RecipeTimeline extends State<RecipeTimeline> {
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
),
SizedBox(height: 10),
Expanded(child: timelineContainer(recordList)),
],
),
);
}
}