You need to enable JavaScript to run this app. Vue+Vuetify动态表格循环行中v-autocomplete选中值的维护实现
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.11 20:24:03