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

