feat:更新菜谱表单
This commit is contained in:
@@ -120,7 +120,7 @@ class MyApp extends StatelessWidget {
|
|||||||
routes: {
|
routes: {
|
||||||
'/home': (context) => HomePage(),
|
'/home': (context) => HomePage(),
|
||||||
'/recordForm': (context) => RecordForm(),
|
'/recordForm': (context) => RecordForm(),
|
||||||
'/recipeForm': (context) => RecipeFormPage(),
|
'/recipeForm': (context) => RecipeForm(),
|
||||||
'/recipeDetail': (context) => RecipeDetailPage(),
|
'/recipeDetail': (context) => RecipeDetailPage(),
|
||||||
'/momentForm': (context) => MomentForm(),
|
'/momentForm': (context) => MomentForm(),
|
||||||
'/moment': (context) => MomentPage(),
|
'/moment': (context) => MomentPage(),
|
||||||
|
|||||||
@@ -161,6 +161,29 @@ class Recipe {
|
|||||||
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
||||||
|
|
||||||
|
static Recipe getEmpty() {
|
||||||
|
return Recipe(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
category: '家常菜',
|
||||||
|
recommendRate: 3,
|
||||||
|
remark: '',
|
||||||
|
isShare: true,
|
||||||
|
userId: 0,
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
materialList: [],
|
||||||
|
stepList: [],
|
||||||
|
recordList: [],
|
||||||
|
likeList: [],
|
||||||
|
likeCount: 0,
|
||||||
|
favouriteList: [],
|
||||||
|
favouriteCount: 0,
|
||||||
|
commentList: [],
|
||||||
|
commentCount: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:food_hub_app/views/record.dart';
|
|||||||
class FoodProvider with ChangeNotifier {
|
class FoodProvider with ChangeNotifier {
|
||||||
late RecordTab currentRecordTab = RecordTab.recipe;
|
late RecordTab currentRecordTab = RecordTab.recipe;
|
||||||
late FoodRecord recordFormItem;
|
late FoodRecord recordFormItem;
|
||||||
|
late Recipe recipeFormItem;
|
||||||
late Moment momentFormItem;
|
late Moment momentFormItem;
|
||||||
|
|
||||||
late bool isEditing;
|
late bool isEditing;
|
||||||
@@ -55,6 +56,16 @@ class FoodProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetRecipeForm() {
|
||||||
|
recipeFormItem = Recipe.getEmpty();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initRecipeForm(Recipe recipe) {
|
||||||
|
recipeFormItem = recipe;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
|
void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
|
||||||
if (name != null) recordFormItem.name = name;
|
if (name != null) recordFormItem.name = name;
|
||||||
if (date != null) recordFormItem.date = date;
|
if (date != null) recordFormItem.date = date;
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_common/widget/common_widget.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:food_hub_app/models/recipe.dart';
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
import 'package:food_hub_app/widgets/common/form.dart';
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:flutter_common/utils/toast_util.dart';
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
import 'package:flutter_common/widget/loading_widget.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 {
|
class RecipeForm extends StatefulWidget {
|
||||||
final RecipeDetail? initialData;
|
const RecipeForm({super.key});
|
||||||
|
|
||||||
const RecipeFormPage({super.key, this.initialData});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecipeFormPage> createState() => _RecipeFormPageState();
|
State<RecipeForm> createState() => _RecipeFormState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecipeFormPageState extends State<RecipeFormPage> {
|
class _RecipeFormState extends State<RecipeForm> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
int _currentStep = 0;
|
int _currentStep = 0;
|
||||||
List<RecipeMaterial> _materials = [];
|
List<RecipeMaterial> _materials = [];
|
||||||
|
List<RecipeMaterial> _materialZhuliaos = [];
|
||||||
|
List<RecipeMaterial> _materialFuliaos = [];
|
||||||
List<RecipeStep> _steps = [];
|
List<RecipeStep> _steps = [];
|
||||||
|
|
||||||
// 表单字段名称常量
|
// 表单字段名称常量
|
||||||
@@ -45,32 +48,24 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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) {
|
switch (_currentStep) {
|
||||||
case 0:
|
case 0:
|
||||||
return _buildBasicInfoStep();
|
return _buildBasicInfoStep(provider);
|
||||||
case 1:
|
case 1:
|
||||||
return _buildMaterialsStep();
|
return _buildMaterialsStep();
|
||||||
case 2:
|
case 2:
|
||||||
return _buildStepsStep();
|
return _buildStepsStep();
|
||||||
default:
|
default:
|
||||||
return _buildBasicInfoStep();
|
return _buildBasicInfoStep(provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一步:基本信息
|
// 第一步:基本信息
|
||||||
Widget _buildBasicInfoStep() {
|
Widget _buildBasicInfoStep(FoodProvider provider) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -79,7 +74,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: _nameField,
|
name: _nameField,
|
||||||
initialValue: widget.initialData?.name ?? '',
|
initialValue: provider.recipeFormItem.name,
|
||||||
decoration: buildInputDecoration(
|
decoration: buildInputDecoration(
|
||||||
context: context,
|
context: context,
|
||||||
hintText: '请输入菜谱名称',
|
hintText: '请输入菜谱名称',
|
||||||
@@ -96,7 +91,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FormBuilderDropdown(
|
FormBuilderDropdown(
|
||||||
name: _categoryField,
|
name: _categoryField,
|
||||||
initialValue: widget.initialData?.category ?? '家常菜',
|
initialValue: provider.recipeFormItem.category,
|
||||||
decoration: buildInputDecoration(
|
decoration: buildInputDecoration(
|
||||||
context: context,
|
context: context,
|
||||||
hintText: '请选择分类',
|
hintText: '请选择分类',
|
||||||
@@ -119,11 +114,11 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FormBuilderSlider(
|
FormBuilderSlider(
|
||||||
name: _recommendRateField,
|
name: _recommendRateField,
|
||||||
initialValue: widget.initialData?.recommendRate ?? 3.0,
|
initialValue: provider.recipeFormItem.recommendRate,
|
||||||
min: 0,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
divisions: 10,
|
divisions: 4,
|
||||||
activeColor: Colors.orange,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
decoration: const InputDecoration(border: InputBorder.none),
|
decoration: const InputDecoration(border: InputBorder.none),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -132,18 +127,18 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: _remarkField,
|
name: _remarkField,
|
||||||
initialValue: widget.initialData?.remark ?? '',
|
initialValue: provider.recipeFormItem.remark,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
decoration: buildInputDecoration(
|
decoration: buildInputDecoration(
|
||||||
context: context,
|
context: context,
|
||||||
hintText: '请输入菜谱的特别说明或小贴士'
|
hintText: '请输入菜谱的特别说明或小贴士',
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
FormBuilderCheckbox(
|
FormBuilderCheckbox(
|
||||||
name: _isShareField,
|
name: _isShareField,
|
||||||
initialValue: widget.initialData?.isShare ?? false,
|
initialValue: provider.recipeFormItem.isShare,
|
||||||
title: const Text('公开分享此菜谱'),
|
title: const Text('公开分享此菜谱'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -159,20 +154,23 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('食材清单', style: Theme.of(context).textTheme.headlineSmall),
|
Text('食材清单', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add, color: Colors.blue),
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
onPressed: _addMaterial,
|
onPressed: _addMaterial,
|
||||||
tooltip: '添加食材',
|
tooltip: '添加食材',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
if (_materials.isEmpty)
|
if (_materials.isEmpty)
|
||||||
Container(
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
|
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
|
||||||
@@ -185,8 +183,19 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
..._materials.asMap().entries.map(
|
Column(
|
||||||
(entry) => _buildMaterialItem(entry.key, entry.value),
|
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) {
|
Widget _buildMaterialItem(int index, RecipeMaterial material) {
|
||||||
return Card(
|
return Column(
|
||||||
key: ValueKey(material.id),
|
key: ValueKey(material.id),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
children: [
|
||||||
child: Padding(
|
Row(
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: TextFormField(
|
||||||
Container(
|
initialValue: material.name,
|
||||||
padding: const EdgeInsets.symmetric(
|
decoration: buildInputDecoration(
|
||||||
horizontal: 12,
|
context: context,
|
||||||
vertical: 4,
|
hintText: '请输入名称',
|
||||||
),
|
prefixIcon: Icons.content_paste,
|
||||||
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(),
|
onChanged: (value) {
|
||||||
IconButton(
|
_updateMaterial(index, name: value);
|
||||||
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
|
},
|
||||||
onPressed: () => _removeMaterial(index),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(width: 12),
|
||||||
Row(
|
Expanded(
|
||||||
children: [
|
child: TextFormField(
|
||||||
Expanded(
|
initialValue: material.amount,
|
||||||
child: TextFormField(
|
decoration: buildInputDecoration(
|
||||||
initialValue: material.type,
|
context: context,
|
||||||
decoration: const InputDecoration(
|
hintText: '请输入用量',
|
||||||
labelText: '类型',
|
prefixIcon: Icons.scale,
|
||||||
border: OutlineInputBorder(),
|
|
||||||
hintText: '如:主料、辅料',
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
_updateMaterial(index, type: value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
onChanged: (value) {
|
||||||
Expanded(
|
_updateMaterial(index, amount: value);
|
||||||
flex: 2,
|
},
|
||||||
child: TextFormField(
|
),
|
||||||
initialValue: material.name,
|
),
|
||||||
decoration: const InputDecoration(
|
IconButton(
|
||||||
labelText: '食材名称',
|
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
|
||||||
border: OutlineInputBorder(),
|
onPressed: () => _removeMaterial(index),
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,20 +289,23 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('制作步骤', style: Theme.of(context).textTheme.headlineSmall),
|
Text('制作步骤', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add, color: Colors.blue),
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
onPressed: _addStep,
|
onPressed: _addStep,
|
||||||
tooltip: '添加步骤',
|
tooltip: '添加步骤',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
if (_steps.isEmpty)
|
if (_steps.isEmpty)
|
||||||
Container(
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
|
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
|
||||||
@@ -350,14 +318,19 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ReorderableListView.builder(
|
Column(
|
||||||
shrinkWrap: true,
|
children:
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
_steps.asMap().entries.map((entry) {
|
||||||
itemCount: _steps.length,
|
final index = entry.key;
|
||||||
itemBuilder:
|
final step = entry.value;
|
||||||
(context, index) => _buildStepItem(index, _steps[index]),
|
|
||||||
onReorder:
|
return Column(
|
||||||
(oldIndex, newIndex) => _reorderStep(oldIndex, newIndex),
|
children: [
|
||||||
|
_buildStepItem(index, step),
|
||||||
|
const SizedBox(height: 8)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -365,68 +338,50 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStepItem(int index, RecipeStep step) {
|
Widget _buildStepItem(int index, RecipeStep step) {
|
||||||
return Card(
|
return Row(
|
||||||
key: Key('step_$index'),
|
key: ValueKey(step.id),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Padding(
|
children: [
|
||||||
padding: const EdgeInsets.all(16),
|
// 步骤编号
|
||||||
child: Column(
|
Container(
|
||||||
children: [
|
width: 28,
|
||||||
Row(
|
height: 28,
|
||||||
children: [
|
alignment: Alignment.center,
|
||||||
Container(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.symmetric(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
horizontal: 12,
|
shape: BoxShape.circle,
|
||||||
vertical: 6,
|
),
|
||||||
),
|
child: Text(
|
||||||
decoration: BoxDecoration(
|
'${index + 1}',
|
||||||
color: Colors.green.shade50,
|
style: const TextStyle(
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: Colors.white,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
child: Text(
|
fontSize: 14,
|
||||||
'步骤 ${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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
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(() {
|
setState(() {
|
||||||
_steps.add(
|
_steps.add(
|
||||||
RecipeStep(
|
RecipeStep(
|
||||||
id: DateTime.now().microsecond,
|
id: DateTime.now().microsecondsSinceEpoch,
|
||||||
sort: _steps.length,
|
sort: _steps.length,
|
||||||
content: '',
|
content: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
@@ -446,15 +401,15 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
void _removeStep(int index) {
|
void _removeStep(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_steps.removeAt(index);
|
_steps.removeAt(index);
|
||||||
// 重新排序
|
// // 重新排序
|
||||||
for (int i = 0; i < _steps.length; i++) {
|
// for (int i = 0; i < _steps.length; i++) {
|
||||||
_steps[i] = RecipeStep(
|
// _steps[i] = RecipeStep(
|
||||||
id: DateTime.now().microsecond,
|
// id: DateTime.now().microsecond,
|
||||||
sort: i,
|
// sort: i,
|
||||||
content: _steps[i].content,
|
// content: _steps[i].content,
|
||||||
imageUrl: _steps[i].imageUrl,
|
// imageUrl: _steps[i].imageUrl,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +436,7 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
final step = _steps[index];
|
final step = _steps[index];
|
||||||
_steps[index] = RecipeStep(
|
_steps[index] = RecipeStep(
|
||||||
id: DateTime.now().microsecond,
|
id: step.id,
|
||||||
sort: step.sort,
|
sort: step.sort,
|
||||||
content: content ?? step.content,
|
content: content ?? step.content,
|
||||||
imageUrl: imageUrl ?? step.imageUrl,
|
imageUrl: imageUrl ?? step.imageUrl,
|
||||||
@@ -500,9 +455,13 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildStepCircle(0),
|
_buildStepCircle(0),
|
||||||
|
SizedBox(width: 10),
|
||||||
_buildStepConnector(0),
|
_buildStepConnector(0),
|
||||||
|
SizedBox(width: 10),
|
||||||
_buildStepCircle(1),
|
_buildStepCircle(1),
|
||||||
|
SizedBox(width: 10),
|
||||||
_buildStepConnector(1),
|
_buildStepConnector(1),
|
||||||
|
SizedBox(width: 10),
|
||||||
_buildStepCircle(2),
|
_buildStepCircle(2),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -513,9 +472,9 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildStepTitle(0, '基本信息'),
|
_buildStepTitle(0, '基本信息'),
|
||||||
SizedBox(width: 25),
|
SizedBox(width: 40),
|
||||||
_buildStepTitle(1, '食材清单'),
|
_buildStepTitle(1, '食材清单'),
|
||||||
SizedBox(width: 25),
|
SizedBox(width: 40),
|
||||||
_buildStepTitle(2, '制作步骤'),
|
_buildStepTitle(2, '制作步骤'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -529,27 +488,26 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
Widget _buildStepCircle(int stepIndex) {
|
Widget _buildStepCircle(int stepIndex) {
|
||||||
final isActive = _currentStep == stepIndex;
|
final isActive = _currentStep == stepIndex;
|
||||||
final isCompleted = _currentStep > stepIndex;
|
final isCompleted = _currentStep > stepIndex;
|
||||||
final boxColor = isActive || isCompleted
|
final boxColor =
|
||||||
? Theme.of(context).colorScheme.primary
|
isActive || isCompleted
|
||||||
: Colors.grey;
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
|
||||||
color: boxColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: isCompleted
|
child:
|
||||||
? const Icon(Icons.check, color: Colors.white, size: 16)
|
isCompleted
|
||||||
: Text(
|
? const Icon(Icons.check, color: Colors.white, size: 16)
|
||||||
'${stepIndex + 1}',
|
: Text(
|
||||||
style: TextStyle(
|
'${stepIndex + 1}',
|
||||||
color: isActive ? Colors.white : Colors.grey.shade700,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
color: Colors.white,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -570,9 +528,10 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
Widget _buildStepTitle(int stepIndex, String title) {
|
Widget _buildStepTitle(int stepIndex, String title) {
|
||||||
final isActive = _currentStep == stepIndex;
|
final isActive = _currentStep == stepIndex;
|
||||||
final isCompleted = _currentStep > stepIndex;
|
final isCompleted = _currentStep > stepIndex;
|
||||||
final textColor = isActive || isCompleted
|
final textColor =
|
||||||
? Theme.of(context).colorScheme.primary
|
isActive || isCompleted
|
||||||
: Colors.grey;
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
title,
|
title,
|
||||||
@@ -594,16 +553,10 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
children: [
|
children: [
|
||||||
if (!isFirstStep)
|
if (!isFirstStep)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: buildInfoButton(
|
||||||
|
context: context,
|
||||||
|
text: '上一步',
|
||||||
onPressed: _previousStep,
|
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),
|
if (!isFirstStep) const SizedBox(width: 12),
|
||||||
@@ -700,25 +653,25 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
final formData = _formKey.currentState!.value;
|
final formData = _formKey.currentState!.value;
|
||||||
|
|
||||||
// 构建完整的RecipeDetail对象
|
// 构建完整的RecipeDetail对象
|
||||||
final recipe = RecipeDetail(
|
// final recipe = RecipeDetail(
|
||||||
id: widget.initialData?.id ?? 0,
|
// id: widget.initialData?.id ?? 0,
|
||||||
name: formData[_nameField],
|
// name: formData[_nameField],
|
||||||
category: formData[_categoryField],
|
// category: formData[_categoryField],
|
||||||
recommendRate: formData[_recommendRateField] ?? 3.0,
|
// recommendRate: formData[_recommendRateField] ?? 3.0,
|
||||||
remark: formData[_remarkField] ?? '',
|
// remark: formData[_remarkField] ?? '',
|
||||||
isShare: formData[_isShareField] ?? false,
|
// isShare: formData[_isShareField] ?? false,
|
||||||
userId: widget.initialData?.userId ?? 0,
|
// userId: widget.initialData?.userId ?? 0,
|
||||||
username: widget.initialData?.username ?? '',
|
// username: widget.initialData?.username ?? '',
|
||||||
avatar: widget.initialData?.avatar ?? '',
|
// avatar: widget.initialData?.avatar ?? '',
|
||||||
materialList: _materials,
|
// materialList: _materials,
|
||||||
stepList: _steps,
|
// stepList: _steps,
|
||||||
recordList: widget.initialData?.recordList ?? [],
|
// recordList: widget.initialData?.recordList ?? [],
|
||||||
likeList: widget.initialData?.likeList ?? [],
|
// likeList: widget.initialData?.likeList ?? [],
|
||||||
favouriteList: widget.initialData?.favouriteList ?? [],
|
// favouriteList: widget.initialData?.favouriteList ?? [],
|
||||||
commentList: widget.initialData?.commentList ?? [],
|
// commentList: widget.initialData?.commentList ?? [],
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
_saveRecipe(recipe);
|
// _saveRecipe(recipe);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ToastUtil.error('请检查表单填写是否正确');
|
ToastUtil.error('请检查表单填写是否正确');
|
||||||
@@ -729,50 +682,37 @@ class _RecipeFormPageState extends State<RecipeFormPage> {
|
|||||||
LoadingDialog.show(context, message: '提交中');
|
LoadingDialog.show(context, message: '提交中');
|
||||||
|
|
||||||
// 模拟保存操作
|
// 模拟保存操作
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
// Future.delayed(const Duration(seconds: 2), () {
|
||||||
LoadingDialog.hide(context);
|
// LoadingDialog.hide(context);
|
||||||
|
//
|
||||||
if (mounted) {
|
// if (mounted) {
|
||||||
ToastUtil.success(widget.initialData == null ? '菜谱创建成功!' : '菜谱更新成功!');
|
// ToastUtil.success(widget.initialData == null ? '菜谱创建成功!' : '菜谱更新成功!');
|
||||||
|
//
|
||||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
// Future.delayed(const Duration(milliseconds: 1500), () {
|
||||||
if (mounted) {
|
// if (mounted) {
|
||||||
Navigator.pop(context);
|
// Navigator.pop(context);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final provider = context.watch<FoodProvider>();
|
||||||
final title = widget.initialData == null ? '创建新菜谱' : '编辑菜谱';
|
|
||||||
|
|
||||||
return Scaffold(
|
return SingleChildScrollView(
|
||||||
appBar: AppBar(
|
child: Padding(
|
||||||
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),
|
padding: EdgeInsets.all(10),
|
||||||
child: CommonCard(
|
child: GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(
|
||||||
|
glassColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildStepIndicator(),
|
_buildStepIndicator(),
|
||||||
Expanded(
|
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
|
||||||
child: FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: _buildStepContent(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildNavigationButtons(),
|
_buildNavigationButtons(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:easy_refresh/easy_refresh.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_common/widget/common_widget.dart';
|
|
||||||
import 'package:food_hub_app/provider/food_provider.dart';
|
import 'package:food_hub_app/provider/food_provider.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/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/widgets/common/easy_refresh.dart';
|
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/calendar.dart';
|
import 'package:food_hub_app/widgets/recipe/calendar.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/list.dart';
|
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||||
@@ -29,12 +27,6 @@ class RecordPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RecordPageState extends State<RecordPage> {
|
class _RecordPageState extends State<RecordPage> {
|
||||||
final EasyRefreshController _freshController = EasyRefreshController(
|
|
||||||
controlFinishRefresh: true,
|
|
||||||
controlFinishLoad: true,
|
|
||||||
);
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
bool _showScrollToTop = false;
|
|
||||||
final List<Widget> _tabPages = [
|
final List<Widget> _tabPages = [
|
||||||
RecipeList(userId: 0),
|
RecipeList(userId: 0),
|
||||||
RecipeCalendar(),
|
RecipeCalendar(),
|
||||||
@@ -44,53 +36,13 @@ class _RecordPageState extends State<RecordPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController.addListener(_onScroll);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.removeListener(_onScroll);
|
|
||||||
_freshController.dispose();
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
Future<void> _onRefresh() async {
|
|
||||||
_freshController.finishRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上拉加载
|
|
||||||
Future<void> _onLoad() async {
|
|
||||||
final provider = context.read<FoodProvider>();
|
|
||||||
if (provider.hasMore) {
|
|
||||||
await provider.refreshFoodStats();
|
|
||||||
_freshController.finishLoad(IndicatorResult.success);
|
|
||||||
} else {
|
|
||||||
_freshController.finishLoad(IndicatorResult.noMore);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onScroll() {
|
|
||||||
// 当滚动距离超过时显示返回顶部按钮
|
|
||||||
if (_scrollController.offset > 100) {
|
|
||||||
if (!_showScrollToTop) {
|
|
||||||
setState(() {
|
|
||||||
_showScrollToTop = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_showScrollToTop) {
|
|
||||||
setState(() {
|
|
||||||
_showScrollToTop = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动到顶部
|
|
||||||
void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
|
|
||||||
|
|
||||||
Widget _buildTabs({
|
Widget _buildTabs({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required RecordTab currentTab,
|
required RecordTab currentTab,
|
||||||
@@ -129,6 +81,25 @@ class _RecordPageState extends State<RecordPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleAddRecipe(FoodProvider provider) {
|
||||||
|
provider.resetRecipeForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleAddMoment(FoodProvider provider) {
|
void _handleAddMoment(FoodProvider provider) {
|
||||||
provider.resetMomentForm();
|
provider.resetMomentForm();
|
||||||
provider.isEditing = false;
|
provider.isEditing = false;
|
||||||
@@ -170,7 +141,7 @@ class _RecordPageState extends State<RecordPage> {
|
|||||||
'新增菜谱',
|
'新增菜谱',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
onTap: () => {},
|
onTap: () => _handleAddRecipe(provider),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
|||||||
Reference in New Issue
Block a user