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

Vue.js 2拖拽组件如何结合localStorage保存布局?

解决拖拽仪表盘卡片布局无法保存到localStorage的问题

你当前的问题在于试图直接存储DOM元素,但localStorage仅支持存储字符串,直接存DOM会被转为[object HTMLDivElement];而DOM元素关联了Vue组件的内部属性(如Watcher、EffectScope),存在循环引用,导致JSON.stringify失败。

正确的思路是存储布局的结构化数据,而非DOM元素本身——比如记录每个容器(board)下包含哪些卡片(card)的ID,再通过这些ID在页面初始化时恢复布局。

解决方案步骤

1. 在父组件中维护布局状态

在仪表盘的父组件里定义响应式的布局数据,用来跟踪每个board下的卡片ID:

<template>
  <div class="col-12">
    <d-board 
      id="border-7" 
      @update-layout="updateLayout"
    >
      <d-card 
        v-for="cardId in layout['border-7']" 
        :key="cardId" 
        :id="cardId" 
        draggable="true"
      >
        <p>{{ cardId === 'card-7' ? 'Card 1' : 'Card 2' }}</p>
      </d-card>
    </d-board>
  </div>

  <div class="col-12">
    <d-board 
      id="border-8" 
      @update-layout="updateLayout"
    >
      <d-card 
        v-for="cardId in layout['border-8']" 
        :key="cardId" 
        :id="cardId" 
        draggable="true"
      >
        <p>{{ cardId === 'card-7' ? 'Card 1' : 'Card 2' }}</p>
      </d-card>
    </d-board>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 初始化布局,或从localStorage读取
      layout: JSON.parse(localStorage.getItem('chart-reorder')) || {
        "border-7": ["card-7"],
        "border-8": ["card-8"]
      }
    };
  },
  methods: {
    updateLayout(newLayout) {
      // 更新布局并存入localStorage
      this.layout = newLayout;
      localStorage.setItem('chart-reorder', JSON.stringify(this.layout));
    }
  }
};
</script>

2. 修改Board组件,更新布局数据

修改Board的drop方法,不再直接操作DOM(交给Vue的v-for渲染),而是通知父组件更新布局数据:

<template>
  <div v-if="id" :id="id" class="board" @dragover.prevent @drop.prevent="drop">
    <slot />
  </div>
</template>

<script>
export default {
  props: ["id"],
  methods: {
    drop(e) {
      const cardId = e.dataTransfer.getData("card_id");
      const targetBoardId = this.id;

      // 通知父组件更新布局:将卡片从原board移到当前board
      this.$emit('update-layout', this.updateLayoutData(cardId, targetBoardId));
    },
    updateLayoutData(cardId, targetBoardId) {
      // 复制当前布局,避免直接修改原数据
      const newLayout = { ...this.$parent.layout };
      
      // 从所有board中移除该卡片
      Object.keys(newLayout).forEach(boardId => {
        newLayout[boardId] = newLayout[boardId].filter(id => id !== cardId);
      });
      
      // 将卡片添加到目标board
      newLayout[targetBoardId].push(cardId);
      
      return newLayout;
    }
  }
};
</script>

3. 优化Card组件的拖拽逻辑

调整Card的dragStart方法,直接使用组件自身的id属性,避免因事件目标不准确导致的问题:

<template>
    <div :id="id"
        class="draggable-card"
        :draggable="draggable"
        @dragstart="dragStart"
        @dragover.stop
    >
        <slot />
    </div>
</template>

<script>
export default {
  props: ['id', 'draggable'],
  methods: {
    dragStart(e) {
      e.dataTransfer.setData('card_id', this.id);
      setTimeout(() => {
        e.target.style.opacity = "0.3";
      }, 0);
    }
  }
};
</script>

关键解释

  • 为什么不存DOM?:DOM元素包含大量无法序列化的内部属性(比如Vue的组件实例、DOM节点的父子引用),而且localStorage只能存储字符串,直接存储毫无意义。
  • 结构化数据的优势:存储卡片ID和容器的对应关系,体积小、易序列化,还能在页面刷新后通过Vue的v-for重新渲染正确的布局。
  • 响应式更新:通过父组件的响应式布局数据,Vue会自动重新渲染卡片的位置,不需要手动操作DOM(这也是Vue的最佳实践)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 01:55:37