feat:更新菜谱表单

This commit is contained in:
2026-06-28 23:17:00 +08:00
parent c10e8e5ffb
commit 613e50b0da
5 changed files with 275 additions and 330 deletions

View File

@@ -1,26 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_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';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
import 'package:provider/provider.dart';
class RecipeFormPage extends StatefulWidget {
final RecipeDetail? initialData;
const RecipeFormPage({super.key, this.initialData});
class RecipeForm extends StatefulWidget {
const RecipeForm({super.key});
@override
State<RecipeFormPage> createState() => _RecipeFormPageState();
State<RecipeForm> createState() => _RecipeFormState();
}
class _RecipeFormPageState extends State<RecipeFormPage> {
class _RecipeFormState extends State<RecipeForm> {
final _formKey = GlobalKey<FormBuilderState>();
int _currentStep = 0;
List<RecipeMaterial> _materials = [];
List<RecipeMaterial> _materialZhuliaos = [];
List<RecipeMaterial> _materialFuliaos = [];
List<RecipeStep> _steps = [];
// 表单字段名称常量
@@ -45,32 +48,24 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
@override
void initState() {
super.initState();
_initializeFormData();
}
void _initializeFormData() {
if (widget.initialData != null) {
_materials = List.from(widget.initialData!.materialList);
_steps = List.from(widget.initialData!.stepList);
}
}
// 构建步骤内容
Widget _buildStepContent() {
Widget _buildStepContent(FoodProvider provider) {
switch (_currentStep) {
case 0:
return _buildBasicInfoStep();
return _buildBasicInfoStep(provider);
case 1:
return _buildMaterialsStep();
case 2:
return _buildStepsStep();
default:
return _buildBasicInfoStep();
return _buildBasicInfoStep(provider);
}
}
// 第一步:基本信息
Widget _buildBasicInfoStep() {
Widget _buildBasicInfoStep(FoodProvider provider) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -79,7 +74,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
const SizedBox(height: 8),
FormBuilderTextField(
name: _nameField,
initialValue: widget.initialData?.name ?? '',
initialValue: provider.recipeFormItem.name,
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱名称',
@@ -96,7 +91,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
const SizedBox(height: 8),
FormBuilderDropdown(
name: _categoryField,
initialValue: widget.initialData?.category ?? '家常菜',
initialValue: provider.recipeFormItem.category,
decoration: buildInputDecoration(
context: context,
hintText: '请选择分类',
@@ -119,11 +114,11 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
const SizedBox(height: 8),
FormBuilderSlider(
name: _recommendRateField,
initialValue: widget.initialData?.recommendRate ?? 3.0,
min: 0,
initialValue: provider.recipeFormItem.recommendRate,
min: 1,
max: 5,
divisions: 10,
activeColor: Colors.orange,
divisions: 4,
activeColor: Theme.of(context).colorScheme.primary,
decoration: const InputDecoration(border: InputBorder.none),
),
const SizedBox(height: 16),
@@ -132,18 +127,18 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
const SizedBox(height: 8),
FormBuilderTextField(
name: _remarkField,
initialValue: widget.initialData?.remark ?? '',
initialValue: provider.recipeFormItem.remark,
maxLines: 3,
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱的特别说明或小贴士'
)
hintText: '请输入菜谱的特别说明或小贴士',
),
),
const SizedBox(height: 16),
FormBuilderCheckbox(
name: _isShareField,
initialValue: widget.initialData?.isShare ?? false,
initialValue: provider.recipeFormItem.isShare,
title: const Text('公开分享此菜谱'),
),
],
@@ -159,20 +154,23 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
children: [
Row(
children: [
Text('食材清单', style: Theme.of(context).textTheme.headlineSmall),
Text('食材清单', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(
icon: const Icon(Icons.add, color: Colors.blue),
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
onPressed: _addMaterial,
tooltip: '添加食材',
),
],
),
const SizedBox(height: 16),
if (_materials.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 40),
color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
@@ -185,8 +183,19 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
),
)
else
..._materials.asMap().entries.map(
(entry) => _buildMaterialItem(entry.key, entry.value),
Column(
children:
_materials.asMap().entries.map((entry) {
final index = entry.key;
final material = entry.value;
return Column(
children: [
_buildMaterialItem(index, material),
const SizedBox(height: 8)
],
);
}).toList(),
),
],
),
@@ -194,89 +203,45 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
}
Widget _buildMaterialItem(int index, RecipeMaterial material) {
return Card(
return Column(
key: ValueKey(material.id),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'食材 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Expanded(
child: TextFormField(
initialValue: material.name,
decoration: buildInputDecoration(
context: context,
hintText: '请输入名称',
prefixIcon: Icons.content_paste,
),
const Spacer(),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () => _removeMaterial(index),
),
],
onChanged: (value) {
_updateMaterial(index, name: value);
},
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextFormField(
initialValue: material.type,
decoration: const InputDecoration(
labelText: '类型',
border: OutlineInputBorder(),
hintText: '如:主料、辅料',
),
onChanged: (value) {
_updateMaterial(index, type: value);
},
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
initialValue: material.amount,
decoration: buildInputDecoration(
context: context,
hintText: '请输入用量',
prefixIcon: Icons.scale,
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: TextFormField(
initialValue: material.name,
decoration: const InputDecoration(
labelText: '食材名称',
border: OutlineInputBorder(),
hintText: '如:鸡蛋、西红柿',
),
onChanged: (value) {
_updateMaterial(index, name: value);
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
initialValue: material.amount,
decoration: const InputDecoration(
labelText: '用量',
border: OutlineInputBorder(),
hintText: '2个、100g',
),
onChanged: (value) {
_updateMaterial(index, amount: value);
},
),
),
],
onChanged: (value) {
_updateMaterial(index, amount: value);
},
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () => _removeMaterial(index),
),
],
),
),
],
);
}
@@ -324,20 +289,23 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
children: [
Row(
children: [
Text('制作步骤', style: Theme.of(context).textTheme.headlineSmall),
Text('制作步骤', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(
icon: const Icon(Icons.add, color: Colors.blue),
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
onPressed: _addStep,
tooltip: '添加步骤',
),
],
),
const SizedBox(height: 16),
if (_steps.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 40),
color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
@@ -350,14 +318,19 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
),
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _steps.length,
itemBuilder:
(context, index) => _buildStepItem(index, _steps[index]),
onReorder:
(oldIndex, newIndex) => _reorderStep(oldIndex, newIndex),
Column(
children:
_steps.asMap().entries.map((entry) {
final index = entry.key;
final step = entry.value;
return Column(
children: [
_buildStepItem(index, step),
const SizedBox(height: 8)
],
);
}).toList(),
),
],
),
@@ -365,68 +338,50 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
}
Widget _buildStepItem(int index, RecipeStep step) {
return Card(
key: Key('step_$index'),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'步骤 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeStep(index),
),
],
return Row(
key: ValueKey(step.id),
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 步骤编号
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
const SizedBox(height: 12),
TextFormField(
initialValue: step.content,
maxLines: 3,
decoration: const InputDecoration(
labelText: '步骤说明',
border: OutlineInputBorder(),
hintText: '详细描述此步骤的操作方法',
),
onChanged: (value) {
_updateStep(index, content: value);
},
),
const SizedBox(height: 12),
TextFormField(
initialValue: step.imageUrl,
decoration: const InputDecoration(
labelText: '步骤图片URL可选',
border: OutlineInputBorder(),
hintText: '添加步骤演示图片链接',
prefixIcon: Icon(Icons.image),
),
onChanged: (value) {
_updateStep(index, imageUrl: value);
},
),
],
),
),
),
const SizedBox(width: 12),
// 步骤内容
Expanded(
child: TextFormField(
initialValue: step.content,
decoration: buildInputDecoration(
context: context,
hintText: '请输入内容',
prefixIcon: Icons.content_paste
),
onChanged: (value) {
_updateStep(index, content: value);
},
),
),
// 删除按钮
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeStep(index),
),
],
);
}
@@ -434,7 +389,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
setState(() {
_steps.add(
RecipeStep(
id: DateTime.now().microsecond,
id: DateTime.now().microsecondsSinceEpoch,
sort: _steps.length,
content: '',
imageUrl: '',
@@ -446,15 +401,15 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
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,
);
}
// // 重新排序
// 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,
// );
// }
});
}
@@ -481,7 +436,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
setState(() {
final step = _steps[index];
_steps[index] = RecipeStep(
id: DateTime.now().microsecond,
id: step.id,
sort: step.sort,
content: content ?? step.content,
imageUrl: imageUrl ?? step.imageUrl,
@@ -500,9 +455,13 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepCircle(0),
SizedBox(width: 10),
_buildStepConnector(0),
SizedBox(width: 10),
_buildStepCircle(1),
SizedBox(width: 10),
_buildStepConnector(1),
SizedBox(width: 10),
_buildStepCircle(2),
],
),
@@ -513,9 +472,9 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepTitle(0, '基本信息'),
SizedBox(width: 25),
SizedBox(width: 40),
_buildStepTitle(1, '食材清单'),
SizedBox(width: 25),
SizedBox(width: 40),
_buildStepTitle(2, '制作步骤'),
],
),
@@ -529,27 +488,26 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
Widget _buildStepCircle(int stepIndex) {
final isActive = _currentStep == stepIndex;
final isCompleted = _currentStep > stepIndex;
final boxColor = isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
final boxColor =
isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: boxColor,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
child: Center(
child: isCompleted
? const Icon(Icons.check, color: Colors.white, size: 16)
: Text(
'${stepIndex + 1}',
style: TextStyle(
color: isActive ? Colors.white : Colors.grey.shade700,
fontWeight: FontWeight.bold,
),
),
child:
isCompleted
? const Icon(Icons.check, color: Colors.white, size: 16)
: Text(
'${stepIndex + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
@@ -570,9 +528,10 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
Widget _buildStepTitle(int stepIndex, String title) {
final isActive = _currentStep == stepIndex;
final isCompleted = _currentStep > stepIndex;
final textColor = isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
final textColor =
isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
return Text(
title,
@@ -594,16 +553,10 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
children: [
if (!isFirstStep)
Expanded(
child: OutlinedButton(
child: buildInfoButton(
context: context,
text: '上一步',
onPressed: _previousStep,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: BorderSide(color: Theme.of(context).colorScheme.primary),
),
child: Text(
'上一步',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
),
),
if (!isFirstStep) const SizedBox(width: 12),
@@ -700,25 +653,25 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
final formData = _formKey.currentState!.value;
// 构建完整的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);
// 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);
}
} else {
ToastUtil.error('请检查表单填写是否正确');
@@ -729,50 +682,37 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
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);
}
});
}
});
// 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 colors = Theme.of(context).colorScheme;
final title = widget.initialData == null ? '创建新菜谱' : '编辑菜谱';
final provider = context.watch<FoodProvider>();
return Scaffold(
appBar: AppBar(
title: Text(title, style: const TextStyle(color: Colors.white)),
backgroundColor: colors.primary,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: Padding(
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: CommonCard(
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
children: [
_buildStepIndicator(),
Expanded(
child: FormBuilder(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildStepContent(),
),
),
),
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
_buildNavigationButtons(),
],
),