You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何复用Livewire 3向导表单实现菜谱的创建与更新

复用Livewire向导表单实现新建/编辑菜谱的优化方案

针对你遇到的Livewire 3模型绑定、unique验证复用等问题,提供两种实用解决方案,适配你的多步骤动态表单与食材自动创建逻辑:


方案1:单组件区分创建/编辑模式(推荐)

通过新增状态标识,在单个组件内完成两种模式的适配,无需拆分组件,同时解决模型绑定与验证问题。

核心修改步骤

  1. 新增模式标识与模型属性
public ?Recipe $recipe = null;
public $isEditing = false;
  1. 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;
    }
}
  1. 动态调整unique验证规则
    编辑时排除当前菜谱ID,避免验证冲突:
// 在validateFields方法中修改recipe_name规则
'recipe_name' => [
    'required',
    'string',
    'max:255',
    Rule::unique('recipes', 'name')->ignore($this->recipe?->id),
],
  1. 合并提交逻辑
    将原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');
}
  1. 视图适配
    根据$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:仅保留创建逻辑,使用RecipeWizardSharedLogic trait
  • EditRecipeWizard:保留编辑逻辑,填充模型数据,调整验证规则

两种组件的核心差异在于mount方法、validateFields中的unique规则,以及提交方法(store/update)。


方案对比

方案优点缺点
单组件模式维护成本低,无需拆分文件,逻辑集中代码量略大,需处理多条件判断
Trait拆分模式职责清晰,代码更易读,适合复杂场景需要维护Trait和两个组件,文件数量增加

内容的提问来源于stack exchange,提问作者d.shved

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:47:32