如何复用Livewire 3向导表单实现菜谱的创建与更新
复用Livewire向导表单实现新建/编辑菜谱的优化方案
针对你遇到的Livewire 3模型绑定、unique验证复用等问题,提供两种实用解决方案,适配你的多步骤动态表单与食材自动创建逻辑:
方案1:单组件区分创建/编辑模式(推荐)
通过新增状态标识,在单个组件内完成两种模式的适配,无需拆分组件,同时解决模型绑定与验证问题。
核心修改步骤
- 新增模式标识与模型属性
public ?Recipe $recipe = null; public $isEditing = false;
- Mount时填充数据
根据是否传入Recipe模型,自动切换模式并填充表单字段:
public function mount(?Recipe $recipe = null) { $this->dishCategories = DishCategory::get(); $this->cuisines = Cuisine::get(); $this->menus = Menu::get(); $this->units = Unit::get(); if ($recipe) { $this->recipe = $recipe; $this->isEditing = true; // 填充基础字段 $this->recipe_name = $recipe->name; $this->recipe_description = $recipe->description; $this->recipe_category = $recipe->dish_category_id; $this->recipe_cuisine = $recipe->cuisine_id; $this->recipe_menu = $recipe->menu_id; $this->recipe_time = $recipe->cook_time; $this->recipe_servings = $recipe->servings; // 填充关联食材 $this->ingredients = $recipe->ingredients->map(function ($ingredient) { return [ 'ingredient_name' => $ingredient->name, 'ingredient_quantity' => $ingredient->pivot->quantity, 'ingredient_unit' => $ingredient->pivot->unit_id, ]; })->toArray(); // 填充步骤 $this->guide_steps = $recipe->guideSteps->map(function ($step) { return [ 'step_image' => $step->step_image, 'step_text' => $step->step_text, ]; })->toArray(); // 兜底空数组初始化 if (empty($this->ingredients)) $this->ingredients[] = $this->ingredient; if (empty($this->guide_steps)) $this->guide_steps[] = $this->guide_step; } }
- 动态调整
unique验证规则
编辑时排除当前菜谱ID,避免验证冲突:
// 在validateFields方法中修改recipe_name规则 'recipe_name' => [ 'required', 'string', 'max:255', Rule::unique('recipes', 'name')->ignore($this->recipe?->id), ],
- 合并提交逻辑
将原store方法改为handleSubmit,根据模式执行创建或更新:
public function handleSubmit() { $this->resetErrorBag(); if ($this->form_step == 3){ $this->validate([ 'guide_steps.*.step_image' => ['nullable', 'mimes:jpeg,png,webp'], 'guide_steps.*.step_text' => ['required','string', 'max:255'], ]); } // 复用原食材处理逻辑 $ingredientsGrouped = collect($this->ingredients) ->groupBy('ingredient_name') ->map(function ($group) { return [ 'ingredient_name' => $group->first()['ingredient_name'], 'ingredient_quantity' => $group->sum('ingredient_quantity'), 'ingredient_unit' => $group->first()['ingredient_unit'], ]; }); $finalIngredients = []; foreach ($ingredientsGrouped as $ingredientData){ $ingredient = Ingredient::firstOrCreate( ['name' => trim($ingredientData['ingredient_name'])], ); $finalIngredients[] = [ 'id' => $ingredient->id, 'quantity' => $ingredientData['ingredient_quantity'], 'unit_id' => $ingredientData['ingredient_unit'], ]; } // 处理图片(编辑时自动删除旧图) $recipe_image_path = $this->recipe_image?->store('recipes-images', 'public'); $recipe_data = [ 'name' => $this->recipe_name, 'description' => $this->recipe_description, 'cook_time' => $this->recipe_time, 'servings' => $this->recipe_servings, 'dish_category_id' => $this->recipe_category, 'cuisine_id' => $this->recipe_cuisine, 'menu_id' => $this->recipe_menu, ]; if ($recipe_image_path) { $recipe_data['image'] = $recipe_image_path; if ($this->isEditing && $this->recipe->image) { Storage::disk('public')->delete($this->recipe->image); } } elseif (!$this->isEditing) { unset($recipe_data['image']); } // 创建/更新菜谱 if ($this->isEditing) { $this->recipe->update($recipe_data); $recipe = $this->recipe; $this->recipe->ingredients()->detach(); session()->flash('recipe_updated', '菜谱更新成功!'); } else { $recipe = Auth::user()->recipes()->create($recipe_data); session()->flash('recipe_created', '菜谱创建成功!'); } // 关联食材 $recipe->ingredients()->attach( collect($finalIngredients)->mapWithKeys(function ($ingredient){ return [$ingredient['id'] => [ 'quantity' => $ingredient['quantity'], 'unit_id' => $ingredient['unit_id'], 'created_at' => now(), 'updated_at' => now(), ]]; })->toArray() ); // 处理步骤(编辑时先删除旧步骤) if ($this->isEditing) { GuideStep::where('recipe_id', $recipe->id)->delete(); } $groupedSteps = collect($this->guide_steps)->map(function ($step, $index) use ($recipe){ $image_path = is_string($step['step_image']) ? $step['step_image'] : ($step['step_image']?->store('guides-images', 'public') ?? 'recipes-images/default/default_photo.png'); // 编辑时替换旧步骤图片 if ($this->isEditing && is_string($step['step_image']) && $step['step_image'] !== $recipe->guideSteps[$index]?->step_image) { Storage::disk('public')->delete($recipe->guideSteps[$index]->step_image); } return [ 'recipe_id' => $recipe->id, 'step_number' => $index + 1, 'step_text' => trim($step['step_text']), 'step_image' => $image_path, 'created_at' => now(), 'updated_at' => now(), ]; })->toArray(); GuideStep::insert($groupedSteps); $this->redirect('/recipes'); }
- 视图适配
根据$isEditing变量切换按钮文本与提示信息:
<button type="button" wire:click="handleSubmit"> {{ $isEditing ? '更新菜谱' : '创建菜谱' }} </button>
方案2:提取共享逻辑到Trait,拆分组件
若单组件逻辑过于臃肿,可将通用逻辑(步骤切换、动态字段增删、食材处理)提取到Trait,拆分出CreateRecipeWizard和EditRecipeWizard两个组件。
步骤1:创建共享Trait
trait RecipeWizardSharedLogic { use WithFileUploads; public $form_step = 1; public $dishCategories; public $cuisines; public $menus; public $units; // 所有表单字段定义 #[Validate] #[Rule(['nullable','mimes:jpeg,png,webp'])] public $recipe_image; public $recipe_name; public $recipe_description; public $recipe_category; public $recipe_cuisine; public $recipe_menu; public $recipe_time; public $recipe_servings; public $ingredients = [[]]; public $ingredient = ['ingredient_name' => null, 'ingredient_quantity' => null, 'ingredient_unit' => null]; public $guide_steps = [['step_image' => null, 'step_text' => null]]; public $guide_step = ['step_image' => null, 'step_text' => null]; public function mountShared() { $this->dishCategories = DishCategory::get(); $this->cuisines = Cuisine::get(); $this->menus = Menu::get(); $this->units = Unit::get(); } // 步骤切换、字段增删等通用方法 public function next_step() { $this->resetErrorBag(); $this->validateFields(); $this->form_step++; } public function prev_step() { $this->form_step--; } public function add_ingredient() { $this->ingredients[] = $this->ingredient; } public function remove_ingredient($index) { unset($this->ingredients[$index]); $this->ingredients = array_values($this->ingredients); if (empty($this->ingredients)) $this->ingredients[] = $this->ingredient; } // 其他通用方法(add_step、remove_step等) protected function processIngredients() { // 复用原食材处理逻辑 $ingredientsGrouped = collect($this->ingredients) ->groupBy('ingredient_name') ->map(function ($group) { return [ 'ingredient_name' => $group->first()['ingredient_name'], 'ingredient_quantity' => $group->sum('ingredient_quantity'), 'ingredient_unit' => $group->first()['ingredient_unit'], ]; }); $finalIngredients = []; foreach ($ingredientsGrouped as $ingredientData){ $ingredient = Ingredient::firstOrCreate( ['name' => trim($ingredientData['ingredient_name'])], ); $finalIngredients[] = [ 'id' => $ingredient->id, 'quantity' => $ingredientData['ingredient_quantity'], 'unit_id' => $ingredientData['ingredient_unit'], ]; } return $finalIngredients; } protected function messages() { // 复用原验证提示信息 } }
步骤2:创建创建/编辑组件
- CreateRecipeWizard:仅保留创建逻辑,使用
RecipeWizardSharedLogictrait - EditRecipeWizard:保留编辑逻辑,填充模型数据,调整验证规则
两种组件的核心差异在于mount方法、validateFields中的unique规则,以及提交方法(store/update)。
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 单组件模式 | 维护成本低,无需拆分文件,逻辑集中 | 代码量略大,需处理多条件判断 |
| Trait拆分模式 | 职责清晰,代码更易读,适合复杂场景 | 需要维护Trait和两个组件,文件数量增加 |
内容的提问来源于stack exchange,提问作者d.shved
相关产品推荐
相关产品推荐

