feat:更新编辑菜谱功能
This commit is contained in:
@@ -6,10 +6,10 @@ import 'package:food_hub_app/utils/index.dart';
|
||||
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<RecipeDetail> queryRecipeByIdApi(int id) {
|
||||
return httpUtil.get<RecipeDetail>(
|
||||
Future<Recipe> queryRecipeByIdApi(int id) {
|
||||
return httpUtil.get<Recipe>(
|
||||
"/food/recipe/$id",
|
||||
converter: (data) => RecipeDetail.fromJson(data),
|
||||
converter: (data) => Recipe.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ void main() async {
|
||||
|
||||
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
|
||||
await SPUtil.init();
|
||||
// await initLogger();
|
||||
await initLogger();
|
||||
|
||||
// 预编译 shader,防止首帧白闪
|
||||
await LiquidGlassWidgets.initialize();
|
||||
|
||||
@@ -131,11 +131,8 @@ class Recipe {
|
||||
List<RecipeStep> stepList;
|
||||
List<FoodRecord> recordList;
|
||||
List<int> likeList;
|
||||
int likeCount;
|
||||
List<int> favouriteList;
|
||||
int favouriteCount;
|
||||
List<RecipeComment> commentList;
|
||||
int commentCount;
|
||||
|
||||
Recipe({
|
||||
required this.id,
|
||||
@@ -151,11 +148,8 @@ class Recipe {
|
||||
required this.stepList,
|
||||
required this.recordList,
|
||||
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);
|
||||
@@ -177,11 +171,8 @@ class Recipe {
|
||||
stepList: [],
|
||||
recordList: [],
|
||||
likeList: [],
|
||||
likeCount: 0,
|
||||
favouriteList: [],
|
||||
favouriteCount: 0,
|
||||
commentList: [],
|
||||
commentCount: 0,
|
||||
commentList: []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,17 +106,14 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||
(json['likeList'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
likeCount: (json['likeCount'] as num).toInt(),
|
||||
favouriteList:
|
||||
(json['favouriteList'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||
commentList:
|
||||
(json['commentList'] as List<dynamic>)
|
||||
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
commentCount: (json['commentCount'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||
@@ -133,11 +130,8 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||
'stepList': instance.stepList,
|
||||
'recordList': instance.recordList,
|
||||
'likeList': instance.likeList,
|
||||
'likeCount': instance.likeCount,
|
||||
'favouriteList': instance.favouriteList,
|
||||
'favouriteCount': instance.favouriteCount,
|
||||
'commentList': instance.commentList,
|
||||
'commentCount': instance.commentCount,
|
||||
};
|
||||
|
||||
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -66,6 +66,86 @@ class FoodProvider with ChangeNotifier {
|
||||
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}) {
|
||||
if (name != null) recordFormItem.name = name;
|
||||
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 {
|
||||
if (isLoading) return;
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
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/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/widgets/common/index.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/recipe/calendar.dart';
|
||||
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||
import 'package:food_hub_app/widgets/recipe/timeline.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum ProfileTab {
|
||||
profile('基础信息'),
|
||||
moment('朋友圈'),
|
||||
recipe('菜谱');
|
||||
|
||||
@@ -30,7 +25,7 @@ class ProfileUserPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class ProfileUserPageState extends State<ProfileUserPage> {
|
||||
late ProfileTab currentTab = ProfileTab.moment;
|
||||
late ProfileTab currentTab = ProfileTab.profile;
|
||||
|
||||
void _onTabChange(ProfileTab tab) {
|
||||
setState(() {
|
||||
@@ -60,10 +55,6 @@ class ProfileUserPageState extends State<ProfileUserPage> {
|
||||
Widget _buildContent(UserProvider provider) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProfileInfo(user: provider.currentUser),
|
||||
),
|
||||
_buildTabs(
|
||||
context: context,
|
||||
currentTab: currentTab,
|
||||
@@ -73,6 +64,9 @@ class ProfileUserPageState extends State<ProfileUserPage> {
|
||||
child: IndexedStack(
|
||||
index: currentTab.index,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity, child: ProfileInfo(user: provider.currentUser)
|
||||
),
|
||||
MomentList(momentList: provider.momentList, isUser: false),
|
||||
RecipeList(userId: provider.currentUser.id!),
|
||||
],
|
||||
|
||||
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/apis/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/views/recipe_form.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecipeDetailPage extends StatefulWidget {
|
||||
const RecipeDetailPage({super.key});
|
||||
@@ -13,14 +16,11 @@ class RecipeDetailPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
RecipeDetail recipe = RecipeDetail.getEmpty();
|
||||
Recipe recipe = Recipe.getEmpty();
|
||||
final GlobalKey _recipeDetailKey = GlobalKey();
|
||||
bool _isSharing = false;
|
||||
|
||||
// 定义三个分类列表
|
||||
List<RecipeMaterial> mainMaterialList = []; // 主料
|
||||
List<RecipeMaterial> auxiliaryMaterialList = []; // 配料
|
||||
List<RecipeMaterial> accessoryMaterialList = []; // 辅料
|
||||
List<RecipeMaterial> materialList = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -40,25 +40,29 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
}
|
||||
|
||||
void getRecipeMaterial() {
|
||||
mainMaterialList.clear();
|
||||
auxiliaryMaterialList.clear();
|
||||
accessoryMaterialList.clear();
|
||||
materialList.clear();
|
||||
|
||||
for (var material in recipe.materialList) {
|
||||
switch (material.type) {
|
||||
case '主料':
|
||||
mainMaterialList.add(material);
|
||||
break;
|
||||
case '配料':
|
||||
auxiliaryMaterialList.add(material);
|
||||
break;
|
||||
case '辅料':
|
||||
accessoryMaterialList.add(material);
|
||||
break;
|
||||
}
|
||||
materialList.add(material);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (_isSharing) return;
|
||||
@@ -83,6 +87,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -104,7 +109,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
child: _buildRecipeDetail(),
|
||||
),
|
||||
),
|
||||
_buildButtons(),
|
||||
_buildButtons(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -121,7 +126,23 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
context,
|
||||
icon: Icons.info,
|
||||
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),
|
||||
_buildInfoCard(
|
||||
@@ -130,11 +151,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
title: "食材信息",
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMaterialSection(context, '主料', mainMaterialList),
|
||||
_buildMaterialSection(context, '辅料', auxiliaryMaterialList),
|
||||
_buildMaterialSection(context, '调料', accessoryMaterialList),
|
||||
],
|
||||
children: [_buildMaterialSection(materialList)],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
@@ -142,10 +159,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
context,
|
||||
icon: Icons.list,
|
||||
title: "步骤信息",
|
||||
content: Column(
|
||||
children:
|
||||
recipe.stepList.map((step) => _buildStepSection(step)).toList(),
|
||||
),
|
||||
content: Column(children: [_buildStepSection(recipe.stepList)]),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildInfoCard(
|
||||
@@ -168,34 +182,43 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaterialSection(
|
||||
BuildContext context,
|
||||
String title,
|
||||
List<RecipeMaterial> materials,
|
||||
) {
|
||||
Widget _buildMaterialSection(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, '无')
|
||||
else
|
||||
Wrap(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children:
|
||||
materials
|
||||
.map((m) => buildTag(context, '${m.name} ${m.amount}'))
|
||||
.toList(),
|
||||
children: materials.map((m) => _buildMaterialItem(m)).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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -213,16 +236,6 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.edit,
|
||||
onPressed: () => _handleEditRecipe(provider),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: _isSharing ? Icons.hourglass_top : Icons.share,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
@@ -21,10 +22,6 @@ class RecipeForm extends StatefulWidget {
|
||||
class _RecipeFormState extends State<RecipeForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
int _currentStep = 0;
|
||||
List<RecipeMaterial> _materials = [];
|
||||
List<RecipeMaterial> _materialZhuliaos = [];
|
||||
List<RecipeMaterial> _materialFuliaos = [];
|
||||
List<RecipeStep> _steps = [];
|
||||
|
||||
// 表单字段名称常量
|
||||
static const String _nameField = 'name';
|
||||
@@ -54,18 +51,18 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
Widget _buildStepContent(FoodProvider provider) {
|
||||
switch (_currentStep) {
|
||||
case 0:
|
||||
return _buildBasicInfoStep(provider);
|
||||
return _buildInfo(provider);
|
||||
case 1:
|
||||
return _buildMaterialsStep();
|
||||
return _buildMaterials(provider);
|
||||
case 2:
|
||||
return _buildStepsStep();
|
||||
return _buildSteps(provider);
|
||||
default:
|
||||
return _buildBasicInfoStep(provider);
|
||||
return _buildInfo(provider);
|
||||
}
|
||||
}
|
||||
|
||||
// 第一步:基本信息
|
||||
Widget _buildBasicInfoStep(FoodProvider provider) {
|
||||
Widget _buildInfo(FoodProvider provider) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -80,6 +77,9 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
hintText: '请输入菜谱名称',
|
||||
prefixIcon: Icons.restaurant_menu,
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(name: value ?? '');
|
||||
},
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
|
||||
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
|
||||
@@ -106,6 +106,9 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(category: value ?? '');
|
||||
},
|
||||
validator: FormBuilderValidators.required(errorText: '请选择分类'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -120,6 +123,9 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
divisions: 4,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
decoration: const InputDecoration(border: InputBorder.none),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(recommendRate: value ?? 0);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -133,6 +139,9 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
context: context,
|
||||
hintText: '请输入菜谱的特别说明或小贴士',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(remark: value ?? '');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -140,6 +149,9 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
name: _isShareField,
|
||||
initialValue: provider.recipeFormItem.isShare,
|
||||
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(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -161,12 +175,12 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: _addMaterial,
|
||||
onPressed: () => provider.addRecipeMaterial(),
|
||||
tooltip: '添加食材',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_materials.isEmpty)
|
||||
if (materials.isEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
@@ -185,24 +199,28 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
else
|
||||
Column(
|
||||
children:
|
||||
_materials.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final material = entry.value;
|
||||
materials.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final material = entry.value;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildMaterialItem(index, material),
|
||||
const SizedBox(height: 8)
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
return Column(
|
||||
children: [
|
||||
_buildMaterialItem(index, material, provider),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaterialItem(int index, RecipeMaterial material) {
|
||||
Widget _buildMaterialItem(
|
||||
int index,
|
||||
RecipeMaterial material,
|
||||
FoodProvider provider,
|
||||
) {
|
||||
return Column(
|
||||
key: ValueKey(material.id),
|
||||
children: [
|
||||
@@ -217,7 +235,7 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
prefixIcon: Icons.content_paste,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateMaterial(index, name: value);
|
||||
provider.updateRecipeMaterial(index, name: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -231,13 +249,13 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
prefixIcon: Icons.scale,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateMaterial(index, amount: value);
|
||||
provider.updateRecipeMaterial(index, amount: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
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(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -296,12 +280,12 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: _addStep,
|
||||
onPressed: () => provider.addRecipeStep(),
|
||||
tooltip: '添加步骤',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_steps.isEmpty)
|
||||
if (steps.isEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
@@ -320,24 +304,24 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
else
|
||||
Column(
|
||||
children:
|
||||
_steps.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final step = entry.value;
|
||||
steps.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final step = entry.value;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildStepItem(index, step),
|
||||
const SizedBox(height: 8)
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
return Column(
|
||||
children: [
|
||||
_buildStepItem(index, step, provider),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepItem(int index, RecipeStep step) {
|
||||
Widget _buildStepItem(int index, RecipeStep step, FoodProvider provider) {
|
||||
return Row(
|
||||
key: ValueKey(step.id),
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -369,81 +353,22 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入内容',
|
||||
prefixIcon: Icons.content_paste
|
||||
prefixIcon: Icons.content_paste,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateStep(index, content: value);
|
||||
provider.updateRecipeStep(index, content: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
// 删除按钮
|
||||
IconButton(
|
||||
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() {
|
||||
return Container(
|
||||
@@ -545,7 +470,7 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
}
|
||||
|
||||
// 导航按钮
|
||||
Widget _buildNavigationButtons() {
|
||||
Widget _buildNavigationButtons(FoodProvider provider) {
|
||||
final isLastStep = _currentStep == 2;
|
||||
final isFirstStep = _currentStep == 0;
|
||||
|
||||
@@ -563,15 +488,15 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
Expanded(
|
||||
child: buildPrimaryButton(
|
||||
context: context,
|
||||
text: isLastStep ? '完成创建' : '下一步',
|
||||
onPressed: _nextStep,
|
||||
text: isLastStep ? '提交' : '下一步',
|
||||
onPressed: () => _nextStep(provider),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _nextStep() {
|
||||
void _nextStep(FoodProvider provider) {
|
||||
if (_currentStep < 2) {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
setState(() {
|
||||
@@ -579,7 +504,7 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_submitForm();
|
||||
_submitForm(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,112 +516,37 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateCurrentStep() {
|
||||
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() {
|
||||
void _submitForm(FoodProvider provider) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
if (_validateCurrentStep()) {
|
||||
final formData = _formKey.currentState!.value;
|
||||
LoadingDialog.show(context, message: '提交中');
|
||||
final result = await provider.handleRecipe();
|
||||
if (result == true) {
|
||||
LoadingDialog.hide(context);
|
||||
|
||||
// 构建完整的RecipeDetail对象
|
||||
// final recipe = RecipeDetail(
|
||||
// id: widget.initialData?.id ?? 0,
|
||||
// name: formData[_nameField],
|
||||
// category: formData[_categoryField],
|
||||
// recommendRate: formData[_recommendRateField] ?? 3.0,
|
||||
// remark: formData[_remarkField] ?? '',
|
||||
// isShare: formData[_isShareField] ?? false,
|
||||
// userId: widget.initialData?.userId ?? 0,
|
||||
// username: widget.initialData?.username ?? '',
|
||||
// avatar: widget.initialData?.avatar ?? '',
|
||||
// materialList: _materials,
|
||||
// stepList: _steps,
|
||||
// recordList: widget.initialData?.recordList ?? [],
|
||||
// likeList: widget.initialData?.likeList ?? [],
|
||||
// favouriteList: widget.initialData?.favouriteList ?? [],
|
||||
// commentList: widget.initialData?.commentList ?? [],
|
||||
// );
|
||||
//
|
||||
// _saveRecipe(recipe);
|
||||
if (provider.isEditing) {
|
||||
showSuccessTip(context, '更新菜谱成功');
|
||||
} else {
|
||||
showSuccessTip(context, '新增菜谱成功');
|
||||
}
|
||||
|
||||
Future.delayed(Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LoadingDialog.hide(context);
|
||||
if (provider.isEditing) {
|
||||
showErrorTip(context, '更新菜谱失败');
|
||||
} else {
|
||||
showErrorTip(context, '新增菜谱失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
@@ -713,7 +563,7 @@ class _RecipeFormState extends State<RecipeForm> {
|
||||
children: [
|
||||
_buildStepIndicator(),
|
||||
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
|
||||
_buildNavigationButtons(),
|
||||
_buildNavigationButtons(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.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/models/session.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
|
||||
Reference in New Issue
Block a user