feat:更新菜谱表单
This commit is contained in:
@@ -65,8 +65,8 @@ class AppActionsState extends State<AppActions> {
|
|||||||
|
|
||||||
void _handleAddRecipe(FoodProvider provider) {
|
void _handleAddRecipe(FoodProvider provider) {
|
||||||
// provider.resetRecordForm();
|
// provider.resetRecordForm();
|
||||||
// Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
// Navigator.pushNamed(context, "/recordForm");
|
Navigator.pushNamed(context, "/recipeForm");
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleAddRecord(FoodProvider provider) {
|
void _handleAddRecord(FoodProvider provider) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:food_hub_app/views/moment.dart';
|
|||||||
import 'package:food_hub_app/views/moment_form.dart';
|
import 'package:food_hub_app/views/moment_form.dart';
|
||||||
import 'package:food_hub_app/views/moment_user.dart';
|
import 'package:food_hub_app/views/moment_user.dart';
|
||||||
import 'package:food_hub_app/views/profile_user.dart';
|
import 'package:food_hub_app/views/profile_user.dart';
|
||||||
|
import 'package:food_hub_app/views/recipe_form.dart';
|
||||||
import 'package:food_hub_app/views/record_form.dart';
|
import 'package:food_hub_app/views/record_form.dart';
|
||||||
import 'package:food_hub_app/views/recipe_detail.dart';
|
import 'package:food_hub_app/views/recipe_detail.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
@@ -71,6 +72,7 @@ class MyApp extends StatelessWidget {
|
|||||||
routes: {
|
routes: {
|
||||||
'/home': (context) => HomePage(),
|
'/home': (context) => HomePage(),
|
||||||
'/recordForm': (context) => RecordFormPage(),
|
'/recordForm': (context) => RecordFormPage(),
|
||||||
|
'/recipeForm': (context) => RecipeFormPage(),
|
||||||
'/recipeDetail': (context) => RecipeDetailPage(),
|
'/recipeDetail': (context) => RecipeDetailPage(),
|
||||||
'/momentForm': (context) => MomentFormPage(),
|
'/momentForm': (context) => MomentFormPage(),
|
||||||
'/moment': (context) => MomentPage(),
|
'/moment': (context) => MomentPage(),
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ part 'recipe.g.dart';
|
|||||||
/// 食材信息
|
/// 食材信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class RecipeMaterial {
|
class RecipeMaterial {
|
||||||
|
int id;
|
||||||
String type;
|
String type;
|
||||||
String name;
|
String name;
|
||||||
String amount;
|
String amount;
|
||||||
|
|
||||||
RecipeMaterial({
|
RecipeMaterial({
|
||||||
|
required this.id,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
@@ -24,11 +26,13 @@ class RecipeMaterial {
|
|||||||
/// 步骤信息
|
/// 步骤信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class RecipeStep {
|
class RecipeStep {
|
||||||
|
int id;
|
||||||
int sort;
|
int sort;
|
||||||
String content;
|
String content;
|
||||||
String imageUrl;
|
String imageUrl;
|
||||||
|
|
||||||
RecipeStep({
|
RecipeStep({
|
||||||
|
required this.id,
|
||||||
required this.sort,
|
required this.sort,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ part of 'recipe.dart';
|
|||||||
|
|
||||||
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
||||||
RecipeMaterial(
|
RecipeMaterial(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
amount: json['amount'] as String,
|
amount: json['amount'] as String,
|
||||||
@@ -15,12 +16,14 @@ RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
|||||||
|
|
||||||
Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
|
Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
'type': instance.type,
|
'type': instance.type,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'amount': instance.amount,
|
'amount': instance.amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
sort: (json['sort'] as num).toInt(),
|
sort: (json['sort'] as num).toInt(),
|
||||||
content: json['content'] as String,
|
content: json['content'] as String,
|
||||||
imageUrl: json['imageUrl'] as String,
|
imageUrl: json['imageUrl'] as String,
|
||||||
@@ -28,6 +31,7 @@ RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
|||||||
|
|
||||||
Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
|
Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
'sort': instance.sort,
|
'sort': instance.sort,
|
||||||
'content': instance.content,
|
'content': instance.content,
|
||||||
'imageUrl': instance.imageUrl,
|
'imageUrl': instance.imageUrl,
|
||||||
|
|||||||
783
lib/views/recipe_form.dart
Normal file
783
lib/views/recipe_form.dart
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'index.dart';
|
||||||
|
|
||||||
/// 通用输入框样式
|
/// 通用输入框样式
|
||||||
InputDecoration buildInputDecoration({
|
InputDecoration buildInputDecoration({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
@@ -87,40 +89,19 @@ Widget buildFormButtonGroup({
|
|||||||
|
|
||||||
/// 取消按钮
|
/// 取消按钮
|
||||||
Widget _buildCancelButton(BuildContext context) {
|
Widget _buildCancelButton(BuildContext context) {
|
||||||
return ElevatedButton(
|
return buildInfoButton(
|
||||||
onPressed: () => Navigator.pop(context), // 直接使用传入的context返回
|
context: context,
|
||||||
style: ElevatedButton.styleFrom(
|
text: '取消',
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
onPressed: () => Navigator.pop(context),
|
||||||
foregroundColor: const Color(0xFF4E5969),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text('取消'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提交按钮
|
/// 提交按钮
|
||||||
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
||||||
return ElevatedButton(
|
return buildPrimaryButton(context: context, text: '提交', onPressed: onConfirm);
|
||||||
onPressed: () => onConfirm(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text('提交', style: TextStyle(color: Colors.white)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除按钮
|
/// 删除按钮
|
||||||
Widget _buildDeleteButton(BuildContext context, VoidCallback onDelete) {
|
Widget _buildDeleteButton(BuildContext context, VoidCallback onDelete) {
|
||||||
return ElevatedButton(
|
return buildErrorButton(text: '删除', onPressed: onDelete);
|
||||||
onPressed: () => onDelete(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text('删除', style: TextStyle(color: Colors.white)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,13 +92,50 @@ Widget buildTag(BuildContext context, String title) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 确认按钮样式
|
Widget buildPrimaryButton({
|
||||||
ButtonStyle primaryButtonStyle() {
|
required BuildContext context,
|
||||||
return ButtonStyle(
|
required String text,
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.green),
|
required VoidCallback onPressed,
|
||||||
shape: WidgetStateProperty.all(
|
}) {
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
),
|
),
|
||||||
|
child: Text(text, style: TextStyle(color: Colors.white)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildErrorButton({
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(text, style: TextStyle(color: Colors.white)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildInfoButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(text, style: TextStyle(color: Colors.white)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user