Files
food_hub_app/lib/views/recipe_form.dart
2026-06-23 20:02:17 +08:00

784 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/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';
class RecipeFormPage extends StatefulWidget {
final RecipeDetail? initialData;
const RecipeFormPage({super.key, this.initialData});
@override
State<RecipeFormPage> createState() => _RecipeFormPageState();
}
class _RecipeFormPageState extends State<RecipeFormPage> {
final _formKey = GlobalKey<FormBuilderState>();
int _currentStep = 0;
List<RecipeMaterial> _materials = [];
List<RecipeStep> _steps = [];
// 表单字段名称常量
static const String _nameField = 'name';
static const String _categoryField = 'category';
static const String _recommendRateField = 'recommendRate';
static const String _remarkField = 'remark';
static const String _isShareField = 'isShare';
// 分类选项
final List<String> _categoryOptions = [
'家常菜',
'川菜',
'粤菜',
'湘菜',
'西餐',
'甜品',
'汤羹',
'其他',
];
@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() {
switch (_currentStep) {
case 0:
return _buildBasicInfoStep();
case 1:
return _buildMaterialsStep();
case 2:
return _buildStepsStep();
default:
return _buildBasicInfoStep();
}
}
// 第一步:基本信息
Widget _buildBasicInfoStep() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
const SizedBox(height: 8),
FormBuilderTextField(
name: _nameField,
initialValue: widget.initialData?.name ?? '',
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱名称',
prefixIcon: Icons.restaurant_menu,
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
]),
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '分类', isRequired: true),
const SizedBox(height: 8),
FormBuilderDropdown(
name: _categoryField,
initialValue: widget.initialData?.category ?? '家常菜',
decoration: buildInputDecoration(
context: context,
hintText: '请选择分类',
prefixIcon: Icons.widgets,
),
items:
_categoryOptions
.map(
(category) => DropdownMenuItem(
value: category,
child: Text(category),
),
)
.toList(),
validator: FormBuilderValidators.required(errorText: '请选择分类'),
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '推荐评分', isRequired: false),
const SizedBox(height: 8),
FormBuilderSlider(
name: _recommendRateField,
initialValue: widget.initialData?.recommendRate ?? 3.0,
min: 0,
max: 5,
divisions: 10,
activeColor: Colors.orange,
decoration: const InputDecoration(border: InputBorder.none),
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '备注', isRequired: false),
const SizedBox(height: 8),
FormBuilderTextField(
name: _remarkField,
initialValue: widget.initialData?.remark ?? '',
maxLines: 3,
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱的特别说明或小贴士'
)
),
const SizedBox(height: 16),
FormBuilderCheckbox(
name: _isShareField,
initialValue: widget.initialData?.isShare ?? false,
title: const Text('公开分享此菜谱'),
),
],
),
);
}
// 第二步:食材清单
Widget _buildMaterialsStep() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('食材清单', style: Theme.of(context).textTheme.headlineSmall),
const Spacer(),
IconButton(
icon: const Icon(Icons.add, color: Colors.blue),
onPressed: _addMaterial,
tooltip: '添加食材',
),
],
),
const SizedBox(height: 16),
if (_materials.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(
children: [
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无食材,请点击添加按钮添加食材',
style: TextStyle(color: Colors.grey[600]),
),
],
),
)
else
..._materials.asMap().entries.map(
(entry) => _buildMaterialItem(entry.key, entry.value),
),
],
),
);
}
Widget _buildMaterialItem(int index, RecipeMaterial material) {
return Card(
key: ValueKey(material.id),
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: 4,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'食材 ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () => _removeMaterial(index),
),
],
),
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(
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);
},
),
),
],
),
],
),
),
);
}
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() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('制作步骤', style: Theme.of(context).textTheme.headlineSmall),
const Spacer(),
IconButton(
icon: const Icon(Icons.add, color: Colors.blue),
onPressed: _addStep,
tooltip: '添加步骤',
),
],
),
const SizedBox(height: 16),
if (_steps.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(
children: [
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无步骤,请点击添加按钮添加制作步骤',
style: TextStyle(color: Colors.grey[600]),
),
],
),
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _steps.length,
itemBuilder:
(context, index) => _buildStepItem(index, _steps[index]),
onReorder:
(oldIndex, newIndex) => _reorderStep(oldIndex, newIndex),
),
],
),
);
}
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),
),
],
),
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);
},
),
],
),
),
);
}
void _addStep() {
setState(() {
_steps.add(
RecipeStep(
id: DateTime.now().microsecond,
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: DateTime.now().microsecond,
sort: step.sort,
content: content ?? step.content,
imageUrl: imageUrl ?? step.imageUrl,
);
});
}
// 步骤指示器
Widget _buildStepIndicator() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepCircle(0),
_buildStepConnector(0),
_buildStepCircle(1),
_buildStepConnector(1),
_buildStepCircle(2),
],
),
Container(
margin: const EdgeInsets.only(top: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepTitle(0, '基本信息'),
SizedBox(width: 25),
_buildStepTitle(1, '食材清单'),
SizedBox(width: 25),
_buildStepTitle(2, '制作步骤'),
],
),
),
],
),
);
}
// 只构建圆圈,不包含标题文字
Widget _buildStepCircle(int stepIndex) {
final isActive = _currentStep == stepIndex;
final isCompleted = _currentStep > stepIndex;
final boxColor = isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
return Container(
width: 32,
height: 32,
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,
),
),
),
);
}
// 箭头连接器
Widget _buildStepConnector(int stepIndex) {
final isActive = _currentStep > stepIndex;
return SizedBox(
width: 40,
child: Icon(
Icons.arrow_forward,
size: 20,
color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey,
),
);
}
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;
return Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: textColor,
fontSize: 12,
),
);
}
// 导航按钮
Widget _buildNavigationButtons() {
final isLastStep = _currentStep == 2;
final isFirstStep = _currentStep == 0;
return Row(
children: [
if (!isFirstStep)
Expanded(
child: OutlinedButton(
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),
Expanded(
child: buildPrimaryButton(
context: context,
text: isLastStep ? '完成创建' : '下一步',
onPressed: _nextStep,
),
),
],
);
}
void _nextStep() {
if (_currentStep < 2) {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
_currentStep++;
});
}
} else {
_submitForm();
}
}
void _previousStep() {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
}
}
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() {
if (_formKey.currentState?.saveAndValidate() ?? false) {
if (_validateCurrentStep()) {
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);
}
} 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 colors = Theme.of(context).colorScheme;
final title = widget.initialData == null ? '创建新菜谱' : '编辑菜谱';
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(
padding: EdgeInsets.all(10),
child: CommonCard(
child: Column(
children: [
_buildStepIndicator(),
Expanded(
child: FormBuilder(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildStepContent(),
),
),
),
_buildNavigationButtons(),
],
),
),
),
);
}
}