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

BootstrapVue表格拖拽列异常:仅表头可拖,表格内容无变化

问题描述
  • 在BootstrapVue表格中实现列拖拽时,仅表头可拖拽,表格内容无法同步更新,打印updatedHeaders显示为undefined
  • 需要实现复选框选择显示列的功能
现有代码
<template>
  <div class="container mt-5">
    <div class="row">
      <h3>Managing Existing Accounts</h3>
    </div>
    <div class="row mt-3">
      This section shows all accounts you currently manage.<br>
      Click on a row to open up details about that account.<br>
      Accounts that have expired will have all their personally identifiable information scrubbed in 90 days.
    </div>

    <!--toggle for column customization-->
    <div>
      <b-button v-b-toggle.column-collapse variant="primary">Column</b-button>
      <b-collapse id="column-collapse" class="mt-2">
        <b-card>
          <p class="card-text">Choose the column</p>
          <b-form-checkbox-group v-model="selectedColumns">
            <b-form-checkbox value="Renew Date">Renew Date</b-form-checkbox>
            <b-form-checkbox value="Account Name">Account Name</b-form-checkbox>
            <b-form-checkbox value="Location">Location</b-form-checkbox>
            <b-form-checkbox value="Type">Type</b-form-checkbox>
            <b-form-checkbox value="Account Type">Account Type</b-form-checkbox>
          </b-form-checkbox-group>
        </b-card>
      </b-collapse>

      <div>
        <table class="table table-striped">
          <thead class="thead-dark">
          <draggable v-model="updatedHeaders" tag="tr" @end="dragged">
            <th v-for="header in headers" :key="header" scope="col">
              {{ header }}
            </th>
          </draggable>
          </thead>
          <tbody>
          <tr v-for="item in items" :key="item.gymID">
            <td v-for="header in headers" :key="header">{{ item[header] }}</td>
          </tr>
          </tbody>
        </table>

      </div>
      <rawDisplayer class="col-2" :value="items" title="Items"/>

      <rawDisplayer class="col-2" :value="updatedHeaders" title="Headers"/>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
  components: {
    draggable,
  },
  data() {
    return {
      dataLoaded: false,
      accountList: [],
      items: [],
      selectedColumns: ['Renew Date', 'Account Name', 'Location', 'Type', 'Account Type'],
      _xsrf: "",
      updatedHeaders: []
    };
  },
  created() {
    // Load the saved column order from local storage
    const headers = localStorage.getItem('tableHeaders');

    if (headers !== "undefined" && headers) {
      const selectedFields = JSON.parse(headers);
      this.selectedColumns = selectedFields.map(field => field.label);
      this.updatedHeaders = selectedFields.map(field => field.key);
    }

    fetch('/distributor/api/managing-existing-accounts')
        .then(response => response.json())
        .then(data => {
          this.accountList = data.accountList;
          this.items = data.accountList;
          this.dataLoaded = true;
          this._xsrf = data._xsrf;
          if(!this.updatedHeaders.length) {
            this.updatedHeaders = this.filteredFields?.map(field => field.key) || [];
          }
        })
        .catch(error => {
          console.error('Error fetching data:', error);
        });
  },
  computed: {
    filteredFields() {
      if (this.dataLoaded) {
        return Object.keys(this.items[0] ? this.items[0] : {})
            .filter(field => this.selectedColumns.includes(field))
            .map(field => ({
              key: field,
              label: field
            }));
      }
      return [];
    },
    headers() {
      return this.updatedHeaders.length ? this.updatedHeaders : this.filteredFields.map(field => field.key);
    },
  },

  methods: {
    dragged() {
      // Map the new order of headers to their corresponding labels
      const updatedLabels = this.updatedHeaders.map(header => this.filteredFields.find(field => field.key === header).label);
      // Save the new order of headers to local storage
      localStorage.setItem('tableHeaders', JSON.stringify(this.updatedHeaders.map(key => ({key, label: key}))));
    }
  }
}
</script>

<style>
/* Add your styles here */
</style>
解决方案

一、修复列拖拽同步问题

核心问题

  1. draggable组件绑定的updatedHeaders初始为空,数据加载时的赋值逻辑依赖延迟计算的filteredFields,导致拖拽时数组未正确初始化
  2. 表头和内容循环的数据源不一致,拖拽更新updatedHeaders后,内容未跟随新顺序渲染

修复步骤

  1. 修正updatedHeaders初始化逻辑
    去掉依赖filteredFields的赋值,直接用selectedColumns初始化,避免异步加载导致的延迟:
// 接口数据返回后
if(!this.updatedHeaders.length) {
  this.updatedHeaders = [...this.selectedColumns];
}
  1. 统一拖拽与渲染的数据源
    将draggable内部的表头循环、表格内容的单元格循环都改为使用updatedHeaders,确保拖拽顺序直接同步到内容:
<!-- 表头部分 -->
<draggable v-model="updatedHeaders" tag="tr" @end="dragged">
  <th v-for="header in updatedHeaders" :key="header" scope="col">
    {{ header }}
  </th>
</draggable>

<!-- 内容部分 -->
<tr v-for="item in items" :key="item.gymID">
  <td v-for="header in updatedHeaders" :key="header">{{ item[header] }}</td>
</tr>
  1. 简化本地存储逻辑
    去掉冗余的key/label结构,直接存储表头数组,避免解析错误:
// 拖拽结束后保存
dragged() {
  localStorage.setItem('tableHeaders', JSON.stringify(this.updatedHeaders));
  this.selectedColumns = [...this.updatedHeaders]; // 同步复选框选中状态
}

// 初始化读取
const savedHeaders = localStorage.getItem('tableHeaders');
if (savedHeaders) {
  const parsedHeaders = JSON.parse(savedHeaders);
  this.updatedHeaders = parsedHeaders;
  this.selectedColumns = [...parsedHeaders];
}

二、实现复选框选择显示列功能

核心逻辑

监听选中列变化,保留已有的拖拽顺序,只添加/移除选中状态变更的列,避免重置用户调整后的顺序:

  1. 添加监听逻辑
    添加watch监听selectedColumns,更新updatedHeaders时维持原有顺序:
watch: {
  selectedColumns(newCols) {
    // 保留现有表头中仍被选中的列
    const keptHeaders = this.updatedHeaders.filter(header => newCols.includes(header));
    // 添加新选中的列(按默认顺序)
    const newAddedCols = newCols.filter(col => !keptHeaders.includes(col));
    this.updatedHeaders = [...keptHeaders, ...newAddedCols];
    // 保存到本地存储
    localStorage.setItem('tableHeaders', JSON.stringify(this.updatedHeaders));
  }
}
  1. 修正计算属性
    简化filteredFields,直接基于selectedColumns生成:
filteredFields() {
  if (!this.dataLoaded || !this.items.length) return [];
  return this.selectedColumns.map(field => ({
    key: field,
    label: field
  }));
}
完整修正代码
<template>
  <div class="container mt-5">
    <div class="row">
      <h3>管理现有账户</h3>
    </div>
    <div class="row mt-3">
      此部分显示您当前管理的所有账户。<br>
      点击行可查看该账户的详细信息。<br>
      已过期的账户,其所有个人身份信息将在90天后被清除。
    </div>

    <!-- 列设置开关 -->
    <div>
      <b-button v-b-toggle.column-collapse variant="primary">列设置</b-button>
      <b-collapse id="column-collapse" class="mt-2">
        <b-card>
          <p class="card-text">选择要显示的列</p>
          <b-form-checkbox-group v-model="selectedColumns">
            <b-form-checkbox value="Renew Date">续费日期</b-form-checkbox>
            <b-form-checkbox value="Account Name">账户名称</b-form-checkbox>
            <b-form-checkbox value="Location">位置</b-form-checkbox>
            <b-form-checkbox value="Type">类型</b-form-checkbox>
            <b-form-checkbox value="Account Type">账户类型</b-form-checkbox>
          </b-form-checkbox-group>
        </b-card>
      </b-collapse>

      <div>
        <table class="table table-striped">
          <thead class="thead-dark">
            <draggable v-model="updatedHeaders" tag="tr" @end="dragged">
              <th v-for="header in updatedHeaders" :key="header" scope="col">
                {{ header }}
              </th>
            </draggable>
          </thead>
          <tbody>
            <tr v-for="item in items" :key="item.gymID">
              <td v-for="header in updatedHeaders" :key="header">{{ item[header] }}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <rawDisplayer class="col-2" :value="items" title="数据项"/>
      <rawDisplayer class="col-2" :value="updatedHeaders" title="表头顺序"/>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
  components: {
    draggable,
  },
  data() {
    return {
      dataLoaded: false,
      accountList: [],
      items: [],
      selectedColumns: ['Renew Date', 'Account Name', 'Location', 'Type', 'Account Type'],
      _xsrf: "",
      updatedHeaders: []
    };
  },
  created() {
    // 读取本地存储的表头顺序
    const savedHeaders = localStorage.getItem('tableHeaders');
    if (savedHeaders) {
      const parsedHeaders = JSON.parse(savedHeaders);
      this.updatedHeaders = parsedHeaders;
      this.selectedColumns = [...parsedHeaders];
    }

    fetch('/distributor/api/managing-existing-accounts')
      .then(response => response.json())
      .then(data => {
        this.accountList = data.accountList;
        this.items = data.accountList;
        this.dataLoaded = true;
        this._xsrf = data._xsrf;
        // 无存储数据时用默认选中列初始化
        if(!this.updatedHeaders.length) {
          this.updatedHeaders = [...this.selectedColumns];
        }
      })
      .catch(error => {
        console.error('获取数据失败:', error);
      });
  },
  computed: {
    filteredFields() {
      if (!this.dataLoaded || !this.items.length) return [];
      return this.selectedColumns.map(field => ({
        key: field,
        label: field
      }));
    }
  },
  watch: {
    selectedColumns(newCols) {
      const keptHeaders = this.updatedHeaders.filter(header => newCols.includes(header));
      const newAddedCols = newCols.filter(col => !keptHeaders.includes(col));
      this.updatedHeaders = [...keptHeaders, ...newAddedCols];
      localStorage.setItem('tableHeaders', JSON.stringify(this.updatedHeaders));
    }
  },
  methods: {
    dragged() {
      localStorage.setItem('tableHeaders', JSON.stringify(this.updatedHeaders));
      this.selectedColumns = [...this.updatedHeaders];
    }
  }
}
</script>

<style>
/* 自定义样式可在此添加 */
</style>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 06:54:55