Vue+Vuetify动态表格循环行中v-autocomplete选中值的维护实现
问题根因
- 状态共用冲突:代码中所有autocomplete组件(顶部搜索、每行内嵌搜索)都绑定了同一个全局
search响应式变量,新增行触发列表重渲染时,组件的searchInput同步逻辑会覆盖已选中的商品值,导致原有选中内容丢失。 - 渲染逻辑缺陷:
v-for使用数组索引作为组件key,加上新增行时复用同一个line对象引用推入数组,列表变动时Vue的组件复用逻辑判断出错,进一步加剧状态丢失。
优化实现方案
按照要求调整交互逻辑:将商品搜索autocomplete独立放在页面顶部作为唯一选品入口,选中商品后自动生成对应表格行,行内仅保留商品信息展示、数量/单价编辑能力,不再内嵌autocomplete组件,从根源上避免行内组件状态丢失问题。
关键调整点
- 状态拆分:顶部搜索autocomplete单独绑定专属的搜索关键词、选中值变量,和表格行数据完全解耦。
- 行实例独立化:每次生成行时创建全新的独立数据对象,不共用引用。
- 渲染key替换:为每行生成唯一自增id作为
v-for的key,不再使用数组索引,避免列表变动时组件复用错位。 - 选品联动:监听顶部autocomplete的选中事件,选中商品后自动带入商品名称、默认单价,初始化数量为1,自动生成表格行,完成后清空顶部选择框,支持连续添加多个商品。
完整参考代码
<template> <v-content id="purchaseOrder-line"> <v-card raised> <v-card-actions> <h2>Purchase Order List</h2> <v-spacer></v-spacer> <v-row class="align-center"> <!-- 顶部独立商品搜索组件 --> <v-autocomplete v-model="selectedProduct" :items="products" :loading="loading" :search-input.sync="searchQuery" color="white" hide-no-data item-text="productname" item-value="Id" label="Search Product" placeholder="Start typing to Search" return-object solo @change="addRowFromSelectedProduct" ></v-autocomplete> </v-row> <div class="my-2"> <v-btn class="primary" :text="true" @click="deleteAll"> <v-icon>mdi-delete-empty</v-icon>Delete All </v-btn> </div> </v-card-actions> <v-card-text> <div class="table-responsive"> <table id="LeadLineTable" class="table table-striped table-bordered"> <thead> <tr> <th>Product</th> <th>Qty</th> <th>Price</th> <th>Actions</th> </tr> </thead> <tbody> <!-- 用行唯一id做key,避免组件复用错位 --> <tr v-for="pline in purchaseOrderLine" :key="pline.lineId"> <td> <!-- 行内直接展示商品名,不再内嵌autocomplete --> {{ pline.productName }} </td> <td> <v-text-field v-model="pline.qty" solo label="Qty" clearable type="number" ></v-text-field> </td> <td> <v-text-field v-model="pline.price" solo label="Price" clearable type="number" ></v-text-field> </td> <td> <v-btn class="mx-2 waves-effect waves-block delete-po" @click="deleteRow(pline.lineId)" :text="true" small fab dark color="teal" > <v-icon small>mdi-delete-empty</v-icon> </v-btn> </td> </tr> <tr v-show="purchaseOrderLine.length == 0"> <td colspan="5" style="text-align: center; height: 100px; vertical-align: middle;" > No record available </td> </tr> </tbody> </table> </div> </v-card-text> <v-card-actions> <br /> <button v-show="purchaseOrderLine.length" type="button" @click="save"> Save </button> </v-card-actions> </v-card> </v-content> </template> <script lang="ts"> import Vue from "vue"; // 补全TypeScript类型定义 interface Product { Id: number; productname: string; price: number; } interface PurchaseOrderLine { lineId: number; productId: number; productName: string; qty: number | string; price: number | string; } interface APIResponse<T> { items: T[]; totalCount: number; } export default Vue.extend({ name: "purchaseOrder-line", props: { msg: String, }, data() { return { output: "", purchaseOrderLine: [] as PurchaseOrderLine[], headers: [ { text: "Product", align: "start", sortable: false }, { text: "Qty" }, { text: "Price" }, { text: "", value: "actions", sortable: false }, ], totalRecords: 0, products: [] as Product[], result: {} as APIResponse<Product>, loading: true, // 顶部搜索组件专属状态,不再和其他组件共用 searchQuery: "", selectedProduct: null as Product | null, // 自增行id计数器,给每行生成唯一标识 lineIdCounter: 1, }; }, watch: { searchQuery(val) { // 触发商品远程搜索,原有逻辑保留 val && this.getProductList(val); }, }, mounted() { this.getProductList(""); }, methods: { deleteAll() { this.purchaseOrderLine = []; }, // 选中顶部商品后自动生成表格行 addRowFromSelectedProduct(product: Product) { if (!product) return; // 可选:添加商品重复校验 const isExist = this.purchaseOrderLine.some(line => line.productId === product.Id); if (isExist) { this.selectedProduct = null; return; } // 生成独立的行实例,不共用引用 const newLine: PurchaseOrderLine = { lineId: this.lineIdCounter++, productId: product.Id, productName: product.productname, qty: 1, price: product.price || 0, }; this.purchaseOrderLine.push(newLine); // 清空顶部选中值,支持连续选品 this.selectedProduct = null; }, deleteRow(lineId: number) { const targetIndex = this.purchaseOrderLine.findIndex(line => line.lineId === lineId); if (targetIndex > -1) { this.purchaseOrderLine.splice(targetIndex, 1); } }, cloneRow(lineId: number) { const targetIndex = this.purchaseOrderLine.findIndex(line => line.lineId === lineId); if (targetIndex > -1) { const sourceLine = this.purchaseOrderLine[targetIndex]; const clonedLine: PurchaseOrderLine = { ...sourceLine, lineId: this.lineIdCounter++, }; this.purchaseOrderLine.splice(targetIndex + 1, 0, clonedLine); } }, save() { console.log(JSON.stringify(this.purchaseOrderLine)); this.output = JSON.stringify(this.purchaseOrderLine); }, async getProductList(value: string) { // 原有商品搜索接口逻辑保留 }, }, }); </script> <style> </style>
效果说明
- 数量、单价输入框双向绑定到对应行的独立属性,新增行时不会影响已有行的编辑状态和值
- 顶部搜索组件完全独立,选中商品自动生成行,无组件状态同步问题
- 删除行时通过唯一id匹配,不会出现索引错位导致删错行的问题
内容的提问来源于stack exchange,提问作者Ashwin Kachwala
相关产品推荐
相关产品推荐

