feat:更新编辑菜谱功能

This commit is contained in:
2026-06-30 23:26:49 +08:00
parent 613e50b0da
commit 9d3a3f2e80
9 changed files with 282 additions and 331 deletions

View File

@@ -6,10 +6,10 @@ import 'package:food_hub_app/utils/index.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl); final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<RecipeDetail> queryRecipeByIdApi(int id) { Future<Recipe> queryRecipeByIdApi(int id) {
return httpUtil.get<RecipeDetail>( return httpUtil.get<Recipe>(
"/food/recipe/$id", "/food/recipe/$id",
converter: (data) => RecipeDetail.fromJson(data), converter: (data) => Recipe.fromJson(data),
); );
} }

View File

@@ -24,7 +24,7 @@ void main() async {
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
await SPUtil.init(); await SPUtil.init();
// await initLogger(); await initLogger();
// 预编译 shader防止首帧白闪 // 预编译 shader防止首帧白闪
await LiquidGlassWidgets.initialize(); await LiquidGlassWidgets.initialize();

View File

@@ -131,11 +131,8 @@ class Recipe {
List<RecipeStep> stepList; List<RecipeStep> stepList;
List<FoodRecord> recordList; List<FoodRecord> recordList;
List<int> likeList; List<int> likeList;
int likeCount;
List<int> favouriteList; List<int> favouriteList;
int favouriteCount;
List<RecipeComment> commentList; List<RecipeComment> commentList;
int commentCount;
Recipe({ Recipe({
required this.id, required this.id,
@@ -151,11 +148,8 @@ class Recipe {
required this.stepList, required this.stepList,
required this.recordList, required this.recordList,
required this.likeList, required this.likeList,
required this.likeCount,
required this.favouriteList, required this.favouriteList,
required this.favouriteCount,
required this.commentList, required this.commentList,
required this.commentCount,
}); });
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json); factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
@@ -177,11 +171,8 @@ class Recipe {
stepList: [], stepList: [],
recordList: [], recordList: [],
likeList: [], likeList: [],
likeCount: 0,
favouriteList: [], favouriteList: [],
favouriteCount: 0, commentList: []
commentList: [],
commentCount: 0,
); );
} }
} }

View File

@@ -106,17 +106,14 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
(json['likeList'] as List<dynamic>) (json['likeList'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
likeCount: (json['likeCount'] as num).toInt(),
favouriteList: favouriteList:
(json['favouriteList'] as List<dynamic>) (json['favouriteList'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
favouriteCount: (json['favouriteCount'] as num).toInt(),
commentList: commentList:
(json['commentList'] as List<dynamic>) (json['commentList'] as List<dynamic>)
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>)) .map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
commentCount: (json['commentCount'] as num).toInt(),
); );
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
@@ -133,11 +130,8 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
'stepList': instance.stepList, 'stepList': instance.stepList,
'recordList': instance.recordList, 'recordList': instance.recordList,
'likeList': instance.likeList, 'likeList': instance.likeList,
'likeCount': instance.likeCount,
'favouriteList': instance.favouriteList, 'favouriteList': instance.favouriteList,
'favouriteCount': instance.favouriteCount,
'commentList': instance.commentList, 'commentList': instance.commentList,
'commentCount': instance.commentCount,
}; };
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) => RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>

View File

@@ -66,6 +66,86 @@ class FoodProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void updateRecipeFormItem({
String? name,
String? category,
double? recommendRate,
String? remark,
bool? isShare,
}) {
if (name != null) recipeFormItem.name = name;
if (category != null) recipeFormItem.category = category;
if (recommendRate != null) recipeFormItem.recommendRate = recommendRate;
if (remark != null) recipeFormItem.remark = remark;
if (isShare != null) recipeFormItem.isShare = isShare;
notifyListeners();
}
void addRecipeMaterial() {
recipeFormItem.materialList.add(
RecipeMaterial(
id: DateTime.now().microsecondsSinceEpoch,
type: '主料',
name: '',
amount: '',
),
);
notifyListeners();
}
void updateRecipeMaterial(
int index, {
String? type,
String? name,
String? amount,
}) {
final material = recipeFormItem.materialList[index];
recipeFormItem.materialList[index] = RecipeMaterial(
id: material.id,
type: type ?? material.type,
name: name ?? material.name,
amount: amount ?? material.amount,
);
notifyListeners();
}
void removeRecipeMaterial(int index) {
recipeFormItem.materialList.removeAt(index);
notifyListeners();
}
void addRecipeStep() {
recipeFormItem.stepList.add(
RecipeStep(
id: DateTime.now().microsecondsSinceEpoch,
sort: recipeFormItem.stepList.length,
content: '',
imageUrl: '',
),
);
notifyListeners();
}
void updateRecipeStep(int index, {String? content, String? imageUrl}) {
final step = recipeFormItem.stepList[index];
recipeFormItem.stepList[index] = RecipeStep(
id: step.id,
sort: step.sort,
content: content ?? step.content,
imageUrl: imageUrl ?? step.imageUrl,
);
}
void removeRecipeStep(int index) {
recipeFormItem.stepList.removeAt(index);
for (int i = 0; i < recipeFormItem.stepList.length; i++) {
recipeFormItem.stepList[i].sort = i;
}
notifyListeners();
}
void updateRecordFormItem({String? name, String? date, String? imageUrl}) { void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
if (name != null) recordFormItem.name = name; if (name != null) recordFormItem.name = name;
if (date != null) recordFormItem.date = date; if (date != null) recordFormItem.date = date;
@@ -167,6 +247,30 @@ class FoodProvider with ChangeNotifier {
} }
} }
Future<bool> handleRecipe() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
if (isEditing) {
await updateRecipeApi(recipeFormItem.id!, recipeFormItem);
} else {
await addRecipeApi(recipeFormItem);
}
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshRecordList() async { Future<void> refreshRecordList() async {
if (isLoading) return; if (isLoading) return;

View File

@@ -1,19 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_common/widget/common_widget.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/provider/app_provider.dart';
import 'package:food_hub_app/provider/user_provider.dart'; import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/moment/list.dart'; import 'package:food_hub_app/widgets/moment/list.dart';
import 'package:food_hub_app/widgets/profile/basic_info.dart'; import 'package:food_hub_app/widgets/profile/basic_info.dart';
import 'package:food_hub_app/widgets/recipe/calendar.dart';
import 'package:food_hub_app/widgets/recipe/list.dart'; import 'package:food_hub_app/widgets/recipe/list.dart';
import 'package:food_hub_app/widgets/recipe/timeline.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum ProfileTab { enum ProfileTab {
profile('基础信息'),
moment('朋友圈'), moment('朋友圈'),
recipe('菜谱'); recipe('菜谱');
@@ -30,7 +25,7 @@ class ProfileUserPage extends StatefulWidget {
} }
class ProfileUserPageState extends State<ProfileUserPage> { class ProfileUserPageState extends State<ProfileUserPage> {
late ProfileTab currentTab = ProfileTab.moment; late ProfileTab currentTab = ProfileTab.profile;
void _onTabChange(ProfileTab tab) { void _onTabChange(ProfileTab tab) {
setState(() { setState(() {
@@ -60,10 +55,6 @@ class ProfileUserPageState extends State<ProfileUserPage> {
Widget _buildContent(UserProvider provider) { Widget _buildContent(UserProvider provider) {
return Column( return Column(
children: [ children: [
SizedBox(
width: double.infinity,
child: ProfileInfo(user: provider.currentUser),
),
_buildTabs( _buildTabs(
context: context, context: context,
currentTab: currentTab, currentTab: currentTab,
@@ -73,6 +64,9 @@ class ProfileUserPageState extends State<ProfileUserPage> {
child: IndexedStack( child: IndexedStack(
index: currentTab.index, index: currentTab.index,
children: [ children: [
SizedBox(
width: double.infinity, child: ProfileInfo(user: provider.currentUser)
),
MomentList(momentList: provider.momentList, isUser: false), MomentList(momentList: provider.momentList, isUser: false),
RecipeList(userId: provider.currentUser.id!), RecipeList(userId: provider.currentUser.id!),
], ],

View File

@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart'; import 'package:flutter_common/widget/common_widget.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/provider/food_provider.dart';
import 'package:food_hub_app/utils/screenshot_util.dart'; import 'package:food_hub_app/utils/screenshot_util.dart';
import 'package:food_hub_app/views/recipe_form.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:provider/provider.dart';
class RecipeDetailPage extends StatefulWidget { class RecipeDetailPage extends StatefulWidget {
const RecipeDetailPage({super.key}); const RecipeDetailPage({super.key});
@@ -13,14 +16,11 @@ class RecipeDetailPage extends StatefulWidget {
} }
class _RecipeDetailState extends State<RecipeDetailPage> { class _RecipeDetailState extends State<RecipeDetailPage> {
RecipeDetail recipe = RecipeDetail.getEmpty(); Recipe recipe = Recipe.getEmpty();
final GlobalKey _recipeDetailKey = GlobalKey(); final GlobalKey _recipeDetailKey = GlobalKey();
bool _isSharing = false; bool _isSharing = false;
// 定义三个分类列表 List<RecipeMaterial> materialList = [];
List<RecipeMaterial> mainMaterialList = []; // 主料
List<RecipeMaterial> auxiliaryMaterialList = []; // 配料
List<RecipeMaterial> accessoryMaterialList = []; // 辅料
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -40,25 +40,29 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
} }
void getRecipeMaterial() { void getRecipeMaterial() {
mainMaterialList.clear(); materialList.clear();
auxiliaryMaterialList.clear();
accessoryMaterialList.clear();
for (var material in recipe.materialList) { for (var material in recipe.materialList) {
switch (material.type) { materialList.add(material);
case '主料':
mainMaterialList.add(material);
break;
case '配料':
auxiliaryMaterialList.add(material);
break;
case '辅料':
accessoryMaterialList.add(material);
break;
}
} }
} }
void _handleEditRecipe(FoodProvider provider) {
provider.initRecipeForm(recipe);
provider.isEditing = true;
showModalBottomSheet(
context: context,
constraints: BoxConstraints(minHeight: 600),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecipeForm(),
);
}
// 分享图片 // 分享图片
Future<void> _shareRecipeAsImage() async { Future<void> _shareRecipeAsImage() async {
if (_isSharing) return; if (_isSharing) return;
@@ -83,6 +87,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
final provider = context.watch<FoodProvider>();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -104,7 +109,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
child: _buildRecipeDetail(), child: _buildRecipeDetail(),
), ),
), ),
_buildButtons(), _buildButtons(provider),
], ],
), ),
), ),
@@ -121,7 +126,23 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
context, context,
icon: Icons.info, icon: Icons.info,
title: "基础信息", title: "基础信息",
content: buildTag(context, recipe.category), content: Text(recipe.category),
),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.star,
title: "推荐指数",
content: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
return Icon(
index < recipe.recommendRate ? Icons.star : Icons.star_border,
color: Theme.of(context).primaryColor,
size: 20,
);
}),
),
), ),
SizedBox(height: 8), SizedBox(height: 8),
_buildInfoCard( _buildInfoCard(
@@ -130,11 +151,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
title: "食材信息", title: "食材信息",
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [_buildMaterialSection(materialList)],
_buildMaterialSection(context, '主料', mainMaterialList),
_buildMaterialSection(context, '辅料', auxiliaryMaterialList),
_buildMaterialSection(context, '调料', accessoryMaterialList),
],
), ),
), ),
SizedBox(height: 8), SizedBox(height: 8),
@@ -142,10 +159,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
context, context,
icon: Icons.list, icon: Icons.list,
title: "步骤信息", title: "步骤信息",
content: Column( content: Column(children: [_buildStepSection(recipe.stepList)]),
children:
recipe.stepList.map((step) => _buildStepSection(step)).toList(),
),
), ),
SizedBox(height: 8), SizedBox(height: 8),
_buildInfoCard( _buildInfoCard(
@@ -168,34 +182,43 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
); );
} }
Widget _buildMaterialSection( Widget _buildMaterialSection(List<RecipeMaterial> materials) {
BuildContext context,
String title,
List<RecipeMaterial> materials,
) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding(
padding: const EdgeInsets.only(bottom: 8, top: 12),
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
),
if (materials.isEmpty) if (materials.isEmpty)
buildTag(context, '') buildTag(context, '')
else else
Wrap( Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0, spacing: 8.0,
runSpacing: 8.0, children: materials.map((m) => _buildMaterialItem(m)).toList(),
children:
materials
.map((m) => buildTag(context, '${m.name} ${m.amount}'))
.toList(),
), ),
], ],
); );
} }
Widget _buildStepSection(RecipeStep step) { Widget _buildMaterialItem(RecipeMaterial material) {
return Text('${material.name} ${material.amount}');
}
Widget _buildStepSection(List<RecipeStep> steps) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (steps.isEmpty)
buildTag(context, '')
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: steps.map((m) => _buildStepItem(m)).toList(),
),
],
);
}
Widget _buildStepItem(RecipeStep step) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -213,16 +236,6 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
), ),
), ),
const SizedBox(height: 8), 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,
),
),
], ],
), ),
), ),
@@ -257,10 +270,16 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
); );
} }
Widget _buildButtons() { Widget _buildButtons(FoodProvider provider) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
circleIconButton(
context: context,
icon: Icons.edit,
onPressed: () => _handleEditRecipe(provider),
),
SizedBox(width: 8),
circleIconButton( circleIconButton(
context: context, context: context,
icon: _isSharing ? Icons.hourglass_top : Icons.share, icon: _isSharing ? Icons.hourglass_top : Icons.share,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart'; import 'package:food_hub_app/provider/food_provider.dart';
@@ -21,10 +22,6 @@ class RecipeForm extends StatefulWidget {
class _RecipeFormState extends State<RecipeForm> { class _RecipeFormState extends State<RecipeForm> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
int _currentStep = 0; int _currentStep = 0;
List<RecipeMaterial> _materials = [];
List<RecipeMaterial> _materialZhuliaos = [];
List<RecipeMaterial> _materialFuliaos = [];
List<RecipeStep> _steps = [];
// 表单字段名称常量 // 表单字段名称常量
static const String _nameField = 'name'; static const String _nameField = 'name';
@@ -54,18 +51,18 @@ class _RecipeFormState extends State<RecipeForm> {
Widget _buildStepContent(FoodProvider provider) { Widget _buildStepContent(FoodProvider provider) {
switch (_currentStep) { switch (_currentStep) {
case 0: case 0:
return _buildBasicInfoStep(provider); return _buildInfo(provider);
case 1: case 1:
return _buildMaterialsStep(); return _buildMaterials(provider);
case 2: case 2:
return _buildStepsStep(); return _buildSteps(provider);
default: default:
return _buildBasicInfoStep(provider); return _buildInfo(provider);
} }
} }
// 第一步:基本信息 // 第一步:基本信息
Widget _buildBasicInfoStep(FoodProvider provider) { Widget _buildInfo(FoodProvider provider) {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -80,6 +77,9 @@ class _RecipeFormState extends State<RecipeForm> {
hintText: '请输入菜谱名称', hintText: '请输入菜谱名称',
prefixIcon: Icons.restaurant_menu, prefixIcon: Icons.restaurant_menu,
), ),
onChanged: (value) {
provider.updateRecipeFormItem(name: value ?? '');
},
validator: FormBuilderValidators.compose([ validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: '菜谱名称不能为空'), FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'), FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
@@ -106,6 +106,9 @@ class _RecipeFormState extends State<RecipeForm> {
), ),
) )
.toList(), .toList(),
onChanged: (value) {
provider.updateRecipeFormItem(category: value ?? '');
},
validator: FormBuilderValidators.required(errorText: '请选择分类'), validator: FormBuilderValidators.required(errorText: '请选择分类'),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -120,6 +123,9 @@ class _RecipeFormState extends State<RecipeForm> {
divisions: 4, divisions: 4,
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,
decoration: const InputDecoration(border: InputBorder.none), decoration: const InputDecoration(border: InputBorder.none),
onChanged: (value) {
provider.updateRecipeFormItem(recommendRate: value ?? 0);
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -133,6 +139,9 @@ class _RecipeFormState extends State<RecipeForm> {
context: context, context: context,
hintText: '请输入菜谱的特别说明或小贴士', hintText: '请输入菜谱的特别说明或小贴士',
), ),
onChanged: (value) {
provider.updateRecipeFormItem(remark: value ?? '');
},
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -140,6 +149,9 @@ class _RecipeFormState extends State<RecipeForm> {
name: _isShareField, name: _isShareField,
initialValue: provider.recipeFormItem.isShare, initialValue: provider.recipeFormItem.isShare,
title: const Text('公开分享此菜谱'), title: const Text('公开分享此菜谱'),
onChanged: (value) {
provider.updateRecipeFormItem(isShare: value ?? false);
},
), ),
], ],
), ),
@@ -147,7 +159,9 @@ class _RecipeFormState extends State<RecipeForm> {
} }
// 第二步:食材清单 // 第二步:食材清单
Widget _buildMaterialsStep() { Widget _buildMaterials(FoodProvider provider) {
final materials = provider.recipeFormItem.materialList;
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -161,12 +175,12 @@ class _RecipeFormState extends State<RecipeForm> {
Icons.add, Icons.add,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
onPressed: _addMaterial, onPressed: () => provider.addRecipeMaterial(),
tooltip: '添加食材', tooltip: '添加食材',
), ),
], ],
), ),
if (_materials.isEmpty) if (materials.isEmpty)
Container( Container(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10), margin: EdgeInsets.only(bottom: 10),
@@ -185,24 +199,28 @@ class _RecipeFormState extends State<RecipeForm> {
else else
Column( Column(
children: children:
_materials.asMap().entries.map((entry) { materials.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final material = entry.value; final material = entry.value;
return Column( return Column(
children: [ children: [
_buildMaterialItem(index, material), _buildMaterialItem(index, material, provider),
const SizedBox(height: 8) const SizedBox(height: 8),
], ],
); );
}).toList(), }).toList(),
), ),
], ],
), ),
); );
} }
Widget _buildMaterialItem(int index, RecipeMaterial material) { Widget _buildMaterialItem(
int index,
RecipeMaterial material,
FoodProvider provider,
) {
return Column( return Column(
key: ValueKey(material.id), key: ValueKey(material.id),
children: [ children: [
@@ -217,7 +235,7 @@ class _RecipeFormState extends State<RecipeForm> {
prefixIcon: Icons.content_paste, prefixIcon: Icons.content_paste,
), ),
onChanged: (value) { onChanged: (value) {
_updateMaterial(index, name: value); provider.updateRecipeMaterial(index, name: value);
}, },
), ),
), ),
@@ -231,13 +249,13 @@ class _RecipeFormState extends State<RecipeForm> {
prefixIcon: Icons.scale, prefixIcon: Icons.scale,
), ),
onChanged: (value) { onChanged: (value) {
_updateMaterial(index, amount: value); provider.updateRecipeMaterial(index, amount: value);
}, },
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20), icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () => _removeMaterial(index), onPressed: () => provider.removeRecipeMaterial(index),
), ),
], ],
), ),
@@ -245,44 +263,10 @@ class _RecipeFormState extends State<RecipeForm> {
); );
} }
void _addMaterial() {
setState(() {
_materials.add(
RecipeMaterial(
id: DateTime.now().microsecondsSinceEpoch,
type: '主料',
name: '',
amount: '',
),
);
});
}
void _removeMaterial(int index) {
setState(() {
_materials.removeAt(index);
});
}
void _updateMaterial(
int index, {
String? type,
String? name,
String? amount,
}) {
setState(() {
final material = _materials[index];
_materials[index] = RecipeMaterial(
id: material.id,
type: type ?? material.type,
name: name ?? material.name,
amount: amount ?? material.amount,
);
});
}
// 第三步:制作步骤 // 第三步:制作步骤
Widget _buildStepsStep() { Widget _buildSteps(FoodProvider provider) {
final steps = provider.recipeFormItem.stepList;
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -296,12 +280,12 @@ class _RecipeFormState extends State<RecipeForm> {
Icons.add, Icons.add,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
onPressed: _addStep, onPressed: () => provider.addRecipeStep(),
tooltip: '添加步骤', tooltip: '添加步骤',
), ),
], ],
), ),
if (_steps.isEmpty) if (steps.isEmpty)
Container( Container(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10), margin: EdgeInsets.only(bottom: 10),
@@ -320,24 +304,24 @@ class _RecipeFormState extends State<RecipeForm> {
else else
Column( Column(
children: children:
_steps.asMap().entries.map((entry) { steps.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final step = entry.value; final step = entry.value;
return Column( return Column(
children: [ children: [
_buildStepItem(index, step), _buildStepItem(index, step, provider),
const SizedBox(height: 8) const SizedBox(height: 8),
], ],
); );
}).toList(), }).toList(),
), ),
], ],
), ),
); );
} }
Widget _buildStepItem(int index, RecipeStep step) { Widget _buildStepItem(int index, RecipeStep step, FoodProvider provider) {
return Row( return Row(
key: ValueKey(step.id), key: ValueKey(step.id),
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -369,81 +353,22 @@ class _RecipeFormState extends State<RecipeForm> {
decoration: buildInputDecoration( decoration: buildInputDecoration(
context: context, context: context,
hintText: '请输入内容', hintText: '请输入内容',
prefixIcon: Icons.content_paste prefixIcon: Icons.content_paste,
), ),
onChanged: (value) { onChanged: (value) {
_updateStep(index, content: value); provider.updateRecipeStep(index, content: value);
}, },
), ),
), ),
// 删除按钮 // 删除按钮
IconButton( IconButton(
icon: const Icon(Icons.delete, color: Colors.red), icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeStep(index), onPressed: () => provider.removeRecipeStep(index),
), ),
], ],
); );
} }
void _addStep() {
setState(() {
_steps.add(
RecipeStep(
id: DateTime.now().microsecondsSinceEpoch,
sort: _steps.length,
content: '',
imageUrl: '',
),
);
});
}
void _removeStep(int index) {
setState(() {
_steps.removeAt(index);
// // 重新排序
// for (int i = 0; i < _steps.length; i++) {
// _steps[i] = RecipeStep(
// id: DateTime.now().microsecond,
// sort: i,
// content: _steps[i].content,
// imageUrl: _steps[i].imageUrl,
// );
// }
});
}
void _reorderStep(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final RecipeStep item = _steps.removeAt(oldIndex);
_steps.insert(newIndex, item);
// 更新排序序号
for (int i = 0; i < _steps.length; i++) {
_steps[i] = RecipeStep(
id: DateTime.now().microsecond,
sort: i,
content: _steps[i].content,
imageUrl: _steps[i].imageUrl,
);
}
});
}
void _updateStep(int index, {String? content, String? imageUrl}) {
setState(() {
final step = _steps[index];
_steps[index] = RecipeStep(
id: step.id,
sort: step.sort,
content: content ?? step.content,
imageUrl: imageUrl ?? step.imageUrl,
);
});
}
// 步骤指示器 // 步骤指示器
Widget _buildStepIndicator() { Widget _buildStepIndicator() {
return Container( return Container(
@@ -545,7 +470,7 @@ class _RecipeFormState extends State<RecipeForm> {
} }
// 导航按钮 // 导航按钮
Widget _buildNavigationButtons() { Widget _buildNavigationButtons(FoodProvider provider) {
final isLastStep = _currentStep == 2; final isLastStep = _currentStep == 2;
final isFirstStep = _currentStep == 0; final isFirstStep = _currentStep == 0;
@@ -563,15 +488,15 @@ class _RecipeFormState extends State<RecipeForm> {
Expanded( Expanded(
child: buildPrimaryButton( child: buildPrimaryButton(
context: context, context: context,
text: isLastStep ? '完成创建' : '下一步', text: isLastStep ? '提交' : '下一步',
onPressed: _nextStep, onPressed: () => _nextStep(provider),
), ),
), ),
], ],
); );
} }
void _nextStep() { void _nextStep(FoodProvider provider) {
if (_currentStep < 2) { if (_currentStep < 2) {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() { setState(() {
@@ -579,7 +504,7 @@ class _RecipeFormState extends State<RecipeForm> {
}); });
} }
} else { } else {
_submitForm(); _submitForm(provider);
} }
} }
@@ -591,112 +516,37 @@ class _RecipeFormState extends State<RecipeForm> {
} }
} }
bool _validateCurrentStep() { void _submitForm(FoodProvider provider) async {
return true;
switch (_currentStep) {
case 0:
// 验证基本信息
final name =
_formKey.currentState?.fields[_nameField]?.value?.toString() ?? '';
final category =
_formKey.currentState?.fields[_categoryField]?.value?.toString() ??
'';
if (name.isEmpty) {
ToastUtil.error('请输入菜谱名称');
return false;
}
if (category.isEmpty) {
ToastUtil.error('请选择分类');
return false;
}
return true;
case 1:
// 验证食材清单
if (_materials.isEmpty) {
ToastUtil.error('请至少添加一个食材');
return false;
}
for (var material in _materials) {
if (material.name.isEmpty || material.amount.isEmpty) {
ToastUtil.error('请完善食材信息');
return false;
}
}
return true;
case 2:
// 验证制作步骤
if (_steps.isEmpty) {
ToastUtil.error('请至少添加一个制作步骤');
return false;
}
for (var step in _steps) {
if (step.content.isEmpty) {
ToastUtil.error('请完善步骤说明');
return false;
}
}
return true;
default:
return true;
}
}
void _submitForm() {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
if (_validateCurrentStep()) { LoadingDialog.show(context, message: '提交中');
final formData = _formKey.currentState!.value; final result = await provider.handleRecipe();
if (result == true) {
LoadingDialog.hide(context);
// 构建完整的RecipeDetail对象 if (provider.isEditing) {
// final recipe = RecipeDetail( showSuccessTip(context, '更新菜谱成功');
// id: widget.initialData?.id ?? 0, } else {
// name: formData[_nameField], showSuccessTip(context, '新增菜谱成功');
// category: formData[_categoryField], }
// recommendRate: formData[_recommendRateField] ?? 3.0,
// remark: formData[_remarkField] ?? '', Future.delayed(Duration(milliseconds: 1500), () {
// isShare: formData[_isShareField] ?? false, if (mounted) {
// userId: widget.initialData?.userId ?? 0, Navigator.pop(context);
// username: widget.initialData?.username ?? '', }
// avatar: widget.initialData?.avatar ?? '', });
// materialList: _materials, } else {
// stepList: _steps, LoadingDialog.hide(context);
// recordList: widget.initialData?.recordList ?? [], if (provider.isEditing) {
// likeList: widget.initialData?.likeList ?? [], showErrorTip(context, '更新菜谱失败');
// favouriteList: widget.initialData?.favouriteList ?? [], } else {
// commentList: widget.initialData?.commentList ?? [], showErrorTip(context, '新增菜谱失败');
// ); }
//
// _saveRecipe(recipe);
} }
} else { } else {
ToastUtil.error('请检查表单填写是否正确'); ToastUtil.error('请检查表单填写是否正确');
} }
} }
void _saveRecipe(RecipeDetail recipe) {
LoadingDialog.show(context, message: '提交中');
// 模拟保存操作
// Future.delayed(const Duration(seconds: 2), () {
// LoadingDialog.hide(context);
//
// if (mounted) {
// ToastUtil.success(widget.initialData == null ? '菜谱创建成功!' : '菜谱更新成功!');
//
// Future.delayed(const Duration(milliseconds: 1500), () {
// if (mounted) {
// Navigator.pop(context);
// }
// });
// }
// });
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>(); final provider = context.watch<FoodProvider>();
@@ -713,7 +563,7 @@ class _RecipeFormState extends State<RecipeForm> {
children: [ children: [
_buildStepIndicator(), _buildStepIndicator(),
FormBuilder(key: _formKey, child: _buildStepContent(provider)), FormBuilder(key: _formKey, child: _buildStepContent(provider)),
_buildNavigationButtons(), _buildNavigationButtons(provider),
], ],
), ),
), ),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/utils/date_utils.dart'; import 'package:flutter_common/utils/date_utils.dart';
import 'package:flutter_common/widget/common_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/session.dart'; import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';