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

Laravel Inertia Vue新增记录后下拉操作按钮失效问题求助

问题描述

使用Laravel+Inertia+Vue开发时,新增产品记录后,表格中新增行的编辑/删除下拉按钮无法展开,但原有记录的下拉按钮正常;刷新页面后所有按钮恢复正常。仅新增操作会触发此问题,更新/删除操作无影响。

后端新增记录代码

public function store(Request $request)
{
    $product = new Product();
    $product->title = $request->title;
    $product->price = $request->price;
    $product->quantity = $request->quantity;
    $product->description = $request->description;
    $product->category_id = $request->category_id;
    $product->brand_id = $request->brand_id;

    if ($request->quantity > 0)
        $product->inStock = 1;

    $product->save();

    //check if product has images upload
    if ($request->hasFile('product_images')) {
        $productImages = $request->file('product_images');
        foreach ($productImages as $image) {
            // Generate a unique name for the image using timestamp and random string
            $uniqueName = time() . '-' . Str::random(10) . '.' . $image->getClientOriginalExtension();
            // Store the image in the public folder with the unique name
            $image->move('product_images', $uniqueName);
            // Create a new product image record with the product_id and unique name
            ProductImage::create([
                'product_id' => $product->id,
                'image' => 'product_images/' . $uniqueName,
            ]);
        }
    }

    return to_route('admin.products.index')->with('success', 'Product created successfully.')->with('prueba', 'Esto fue una prueba');

}

前端新增记录代码

const AddProduct = async () => {

    const formData = new FormData();

    formData.append("title", title.value);
    formData.append("price", price.value);
    formData.append("quantity", quantity.value);
    formData.append("description", description.value);
    formData.append("brand_id", brand_id.value);
    formData.append("category_id", category_id.value);

    // Append product images to the FormData
    for (const image of productImages.value) {
        formData.append("product_images[]", image.raw);
    }

    try {
        await router.post("products/store", formData, {

            preserveState: true,
            onSuccess: (page) => {

                dialogVisible.value = false;
                resetFormData();

                Swal.fire({
                    toast: true,
                    icon: "success",
                    position: "top-end",
                    showConfirmButton: false,
                    title: page.props.flash.success,
                    showCloseButton: true,
                });


            },
        });
    } catch (err) {
        console.log(err);
    }
};

表格组件代码

<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
    <thead
        class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
    >
        <tr>
            <th scope="col" class="px-4 py-3">
                Product name
            </th>
            <th scope="col" class="px-4 py-3">Category</th>
            <th scope="col" class="px-4 py-3">Brand</th>
            <th scope="col" class="px-4 py-3">Quantity</th>
            <th scope="col" class="px-4 py-3">Price</th>
            <th scope="col" class="px-4 py-3">Stock</th>
            <th scope="col" class="px-4 py-3">Publish</th>
            <th scope="col" class="px-4 py-3">
                <span class="sr-only">Actions</span>
            </th>
        </tr>
    </thead>
    <tbody>
        <tr v-for="(product, index) in products" :key="product.id" class="border-b dark:border-gray-700">
            <th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
                {{ product.title }}
            </th>
            <td class="px-4 py-3">
                {{ product.category.name }}
            </td>
            <td class="px-4 py-3">
                {{ product.brand.name }}
            </td>
            <td class="px-4 py-3">
                {{ product.quantity }}
            </td>
            <td class="px-4 py-3">{{ product.price }}</td>
            <td class="px-4 py-3">
                <span v-if="product.inStock == 1"
                    class="bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-green-900 dark:text-green-300"
                    >inStock
                </span>
                <span
                    v-else
                    class="bg-red-100 text-red-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300"
                    >Out of Stock
                </span>
            </td>
            <td class="px-4 py-3">
                <button
                    v-if="product.published == 1"
                    type="button"
                    class="px-3 py-2 text-xs font-medium text-center text-white bg-green-700 rounded-lg hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
                >
                    Published
                </button>
                <button
                    v-else
                    type="button"
                    class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
                >
                    UnPublished
                </button>
            </td>
            <td class="px-4 py-3 flex items-center justify-end">
                <button
                    :id="`${product.id}-button`"
                    :data-dropdown-toggle="`${product.id}`"
                    class="inline-flex items-center p-0.5 text-sm font-medium text-center text-gray-500 hover:text-gray-800 rounded-lg focus:outline-none dark:text-gray-400 dark:hover:text-gray-100"
                    type="button"
                >
                    <svg
                        class="w-5 h-5"
                        aria-hidden="true"
                        fill="currentColor"
                        viewbox="0 0 20 20"
                        xmlns="http://www.w3.org/2000/svg"
                    >
                        <path
                            d="M6 10a2 2 0 11-4 0 2 2 0 014 0zM12 10a2 2 0 11-4 0 2 2 0 014 0zM16 12a2 2 0 100-4 2 2 0 000 4z"
                        />
                    </svg>
                </button>
                <div
                    :id="`${product.id}`"
                    class="hidden z-10 w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
                >
                    <ul
                        class="py-1 text-sm text-gray-700 dark:text-gray-200"
                        :aria-labelledby="`${product.id}-button`"
                    >
                        <li>
                            <a
                                href="#"
                                @click="
                                    openEditModal(product)
                                "
                                class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
                                >Edit</a
                            >
                        </li>
                    </ul>
                    <div class="py-1">
                        <a
                            href="#"
                            @click="
                                deleteProduct(
                                    product,
                                    index
                                )
                            "
                            class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
                            >Delete</a
                        >
                    </div>
                </div>
            </td>
        </tr>
    </tbody>
</table>
原因分析

你的下拉按钮依赖基于DOM的JavaScript交互逻辑(比如Flowbite、Tailwind UI这类组件库的下拉功能),这类组件的核心逻辑是:页面首次加载时,JavaScript会遍历DOM中所有匹配的下拉按钮,给它们绑定点击事件,以此控制下拉菜单的显示/隐藏。

但新增产品时你设置了preserveState: true,Inertia会保留当前页面的状态,不会重新加载页面,而是通过更新组件的props来渲染新增的产品行。此时新增的下拉按钮和菜单是动态插入DOM的新元素,没有被初始化绑定事件监听器,所以点击时没有任何反应。

刷新页面后,所有DOM元素会重新加载,JavaScript会重新初始化所有下拉组件,新增行的按钮自然就能正常工作了。

解决方案

有两种常用的修复方式,你可以根据业务需求选择:

方案1:移除preserveState: true,让页面重新渲染

如果业务允许页面刷新,直接去掉preserveState: true配置,Inertia会重新渲染整个页面,所有下拉组件都会被初始化:

await router.post("products/store", formData, {
    // 移除 preserveState: true
    onSuccess: (page) => {
        dialogVisible.value = false;
        resetFormData();
        Swal.fire({
            toast: true,
            icon: "success",
            position: "top-end",
            showConfirmButton: false,
            title: page.props.flash.success,
            showCloseButton: true,
        });
    },
});

方案2:动态初始化新增的下拉组件

如果需要保留preserveState: true不刷新页面,就在新增产品成功后,手动初始化新增行的下拉组件。

假设你用的是Flowbite的下拉组件,你可以在onSuccess回调中找到新增的产品对应的按钮,重新初始化:

import { Dropdown } from 'flowbite';

const AddProduct = async () => {
    // ... 现有代码
    try {
        await router.post("products/store", formData, {
            preserveState: true,
            onSuccess: (page) => {
                dialogVisible.value = false;
                resetFormData();
                Swal.fire({/* ... */});

                // 手动初始化新增的下拉组件
                const newProduct = page.props.products.at(-1); // 获取最后一个新增的产品
                const dropdownButton = document.getElementById(`${newProduct.id}-button`);
                if (dropdownButton) {
                    new Dropdown(dropdownButton);
                }
            },
        });
    } catch (err) {
        console.log(err);
    }
};

如果是自定义的下拉逻辑,你需要把绑定事件的代码封装成独立函数,在新增DOM元素后调用该函数,为新元素绑定点击事件。

内容的提问来源于stack exchange,提问作者albertolg89

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 07:04:51