Vue递归拖拽与动态展示组件


实现一个支持嵌套拖拽组件的 Vue 应用涉及多个关键部分:设计合适的数据结构以支持嵌套、实现拖拽功能、进行增删改查操作,以及将数据序列化和反序列化为 JSON。以下是一个全面的指南,帮助你实现这一功能。

1. 数据结构设计

为了支持嵌套和灵活的增删改查操作,推荐使用树形数据结构。每个节点代表一个组件,包含自身的属性和一个子节点数组。

示例数据结构

[
  {
    "id": "1",
    "type": "Container",
    "props": {
      "title": "Container 1"
    },
    "children": [
      {
        "id": "1-1",
        "type": "Item",
        "props": {
          "content": "Item 1-1"
        },
        "children": []
      },
      {
        "id": "1-2",
        "type": "Container",
        "props": {
          "title": "Container 1-2"
        },
        "children": [
          {
            "id": "1-2-1",
            "type": "Item",
            "props": {
              "content": "Item 1-2-1"
            },
            "children": []
          }
        ]
      }
    ]
  },
  {
    "id": "2",
    "type": "Item",
    "props": {
      "content": "Item 2"
    },
    "children": []
  }
]

数据结构说明

  • id: 唯一标识符,方便操作特定节点。
  • type: 组件类型,决定渲染哪种组件。
  • props: 组件的属性,根据类型不同而不同。
  • children: 子节点数组,允许嵌套。

2. Vue 组件结构

主组件 (App.vue)

主组件负责管理整个树状数据,并提供序列化和反序列化的功能。

<template>
  <div>
    <button @click="saveToJSON">保存为 JSON</button>
    <button @click="loadFromJSON">从 JSON 恢复</button>
    <DraggableTree :nodes="treeData" @update="updateTreeData" />
  </div>
</template>

<script>
import DraggableTree from './components/DraggableTree.vue';

export default {
  components: { DraggableTree },
  data() {
    return {
      treeData: [] // 初始化为空,或从服务器加载
    };
  },
  methods: {
    saveToJSON() {
      const json = JSON.stringify(this.treeData, null, 2);
      // 可以将 JSON 保存到服务器或本地
      console.log(json);
    },
    loadFromJSON() {
      const json = /* 获取 JSON 字符串 */;
      this.treeData = JSON.parse(json);
    },
    updateTreeData(newData) {
      this.treeData = newData;
    }
  }
};
</script>

可拖拽树组件 (DraggableTree.vue)

使用递归组件来渲染树状结构,并集成拖拽功能。推荐使用 vue-draggableVue.Draggable 库。

安装 vue-draggable

首先,安装 Vue.Draggable

npm install vuedraggable

组件代码

<template>
  <draggable v-model="localNodes" :group="{ name: 'nodes', pull: true, put: true }" @end="onDragEnd">
    <div v-for="node in localNodes" :key="node.id" class="node">
      <div class="node-header">
        <span>{{ node.type }} - {{ node.props.title || node.props.content }}</span>
        <button @click="addChild(node)">添加子组件</button>
        <button @click="removeNode(node)">删除</button>
      </div>
      <div class="node-children" v-if="node.children && node.children.length">
        <DraggableTree :nodes="node.children" @update="updateChildren(node, $event)" />
      </div>
    </div>
  </draggable>
</template>

<script>
import draggable from 'vuedraggable';
import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一 ID

export default {
  name: 'DraggableTree',
  components: { draggable, DraggableTree: () => import('./DraggableTree.vue') },
  props: {
    nodes: {
      type: Array,
      required: true
    }
  },
  data() {
    return {
      localNodes: JSON.parse(JSON.stringify(this.nodes)) // 深拷贝
    };
  },
  watch: {
    localNodes: {
      handler(newVal) {
        this.$emit('update', newVal);
      },
      deep: true
    },
    nodes: {
      handler(newVal) {
        this.localNodes = JSON.parse(JSON.stringify(newVal));
      },
      deep: true
    }
  },
  methods: {
    addChild(node) {
      const newNode = {
        id: uuidv4(),
        type: 'Item', // 或其他类型
        props: { content: '新组件' },
        children: []
      };
      node.children.push(newNode);
    },
    removeNode(node) {
      const removeRecursively = (nodes, target) => {
        const index = nodes.findIndex(n => n.id === target.id);
        if (index !== -1) {
          nodes.splice(index, 1);
          return true;
        }
        for (let n of nodes) {
          if (n.children && n.children.length) {
            if (removeRecursively(n.children, target)) {
              return true;
            }
          }
        }
        return false;
      };
      this.$emit('update', this.removeRecursively(this.localNodes, node));
    },
    removeRecursively(nodes, target) {
      const index = nodes.findIndex(n => n.id === target.id);
      if (index !== -1) {
        nodes.splice(index, 1);
        return nodes;
      }
      for (let n of nodes) {
        if (n.children && n.children.length) {
          this.removeRecursively(n.children, target);
        }
      }
      return nodes;
    },
    updateChildren(parentNode, updatedChildren) {
      parentNode.children = updatedChildren;
    },
    onDragEnd(evt) {
      // 可选:处理拖拽结束后的逻辑
    }
  }
};
</script>

<style>
.node {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 5px;
}
.node-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.node-children {
  margin-left: 20px;
}
</style>

组件说明

  1. 递归渲染: DraggableTree 组件内部递归渲染自身,以支持无限嵌套。
  2. 拖拽功能: 使用 Vue.Draggable 实现拖拽。配置 group 以允许节点在不同层级间拖拽。
  3. 增删操作:
    • 添加子组件: addChild 方法向 children 数组添加新节点。
    • 删除组件: removeNode 方法递归查找并删除指定节点。
  4. 数据同步: 使用 v-model 绑定 localNodes,并通过 watch 监听变化,向父组件同步更新。

3. 增删改查操作

添加节点

DraggableTree.vue 中,addChild 方法创建一个新节点并添加到 children 数组。可以根据需要修改 typeprops 来支持不同类型的组件。

删除节点

removeNode 方法通过递归搜索并删除指定节点。确保每个节点都有唯一的 id,以便准确定位。

修改节点

可以在节点渲染部分添加编辑功能,例如表单输入,允许用户修改 props 中的属性。修改后的数据会自动通过 v-modelwatch 同步。

<!-- 在节点渲染中添加编辑功能 -->
<div class="node-header">
  <span>{{ node.type }} - {{ node.props.title || node.props.content }}</span>
  <button @click="editNode(node)">编辑</button>
  <button @click="addChild(node)">添加子组件</button>
  <button @click="removeNode(node)">删除</button>
</div>

<!-- 编辑节点方法 -->
methods: {
  editNode(node) {
    // 实现编辑逻辑,例如弹出模态框或内联编辑
    // 这里以简单 prompt 为例
    if (node.type === 'Container') {
      const newTitle = prompt('修改标题', node.props.title);
      if (newTitle !== null) {
        node.props.title = newTitle;
      }
    } else if (node.type === 'Item') {
      const newContent = prompt('修改内容', node.props.content);
      if (newContent !== null) {
        node.props.content = newContent;
      }
    }
  },
  // 其他方法...
}

4. 序列化和反序列化 JSON

保存为 JSON

在主组件 (App.vue) 中,通过 JSON.stringifytreeData 转换为 JSON 字符串,并保存到服务器或本地存储。

methods: {
  saveToJSON() {
    const json = JSON.stringify(this.treeData, null, 2);
    // 示例:保存到本地
    localStorage.setItem('treeData', json);
    console.log('已保存 JSON:', json);
  },
  loadFromJSON() {
    const json = localStorage.getItem('treeData');
    if (json) {
      this.treeData = JSON.parse(json);
    } else {
      console.warn('没有找到保存的 JSON 数据');
    }
  },
  // 其他方法...
}

从 JSON 恢复

通过 JSON.parse 将 JSON 字符串转换回 JavaScript 对象,并赋值给 treeData。组件会自动根据数据渲染树状结构。

methods: {
  loadFromJSON() {
    const json = /* 从服务器或本地获取 JSON 字符串 */;
    try {
      this.treeData = JSON.parse(json);
      console.log('已加载 JSON 数据');
    } catch (error) {
      console.error('JSON 解析失败:', error);
    }
  },
  // 其他方法...
}

5. 完整示例

以下是一个完整的简化示例,整合上述所有部分。

安装依赖

确保安装 Vue, Vue.Draggable, 和 uuid

npm install vue vuedraggable uuid

App.vue

<template>
  <div id="app">
    <h1>嵌套拖拽组件示例</h1>
    <button @click="saveToJSON">保存为 JSON</button>
    <button @click="loadFromJSON">从 JSON 恢复</button>
    <DraggableTree :nodes="treeData" @update="updateTreeData" />
  </div>
</template>

<script>
import DraggableTree from './components/DraggableTree.vue';

export default {
  name: 'App',
  components: { DraggableTree },
  data() {
    return {
      treeData: [
        {
          id: '1',
          type: 'Container',
          props: { title: '默认容器' },
          children: []
        }
      ]
    };
  },
  methods: {
    saveToJSON() {
      const json = JSON.stringify(this.treeData, null, 2);
      localStorage.setItem('treeData', json);
      alert('已保存为 JSON');
    },
    loadFromJSON() {
      const json = localStorage.getItem('treeData');
      if (json) {
        this.treeData = JSON.parse(json);
        alert('已从 JSON 恢复');
      } else {
        alert('没有找到保存的 JSON 数据');
      }
    },
    updateTreeData(newData) {
      this.treeData = newData;
    }
  }
};
</script>

<style>
#app {
  font-family: Arial, sans-serif;
  padding: 20px;
}
</style>

DraggableTree.vue

<template>
  <draggable v-model="localNodes" :group="{ name: 'nodes', pull: true, put: true }" @end="onDragEnd">
    <div v-for="node in localNodes" :key="node.id" class="node">
      <div class="node-header">
        <span>{{ node.type }} - {{ node.props.title || node.props.content }}</span>
        <div>
          <button @click="editNode(node)">编辑</button>
          <button @click="addChild(node)">添加子组件</button>
          <button @click="removeNode(node)">删除</button>
        </div>
      </div>
      <div class="node-children" v-if="node.children && node.children.length">
        <DraggableTree :nodes="node.children" @update="updateChildren(node, $event)" />
      </div>
    </div>
  </draggable>
</template>

<script>
import draggable from 'vuedraggable';
import { v4 as uuidv4 } from 'uuid';

export default {
  name: 'DraggableTree',
  components: { draggable, DraggableTree: () => import('./DraggableTree.vue') },
  props: {
    nodes: {
      type: Array,
      required: true
    }
  },
  data() {
    return {
      localNodes: JSON.parse(JSON.stringify(this.nodes))
    };
  },
  watch: {
    localNodes: {
      handler(newVal) {
        this.$emit('update', newVal);
      },
      deep: true
    },
    nodes: {
      handler(newVal) {
        this.localNodes = JSON.parse(JSON.stringify(newVal));
      },
      deep: true
    }
  },
  methods: {
    addChild(node) {
      const newNode = {
        id: uuidv4(),
        type: 'Item',
        props: { content: '新项目' },
        children: []
      };
      node.children.push(newNode);
    },
    removeNode(node) {
      const removeRecursively = (nodes, target) => {
        const index = nodes.findIndex(n => n.id === target.id);
        if (index !== -1) {
          nodes.splice(index, 1);
          return true;
        }
        for (let n of nodes) {
          if (n.children && n.children.length) {
            if (removeRecursively(n.children, target)) {
              return true;
            }
          }
        }
        return false;
      };
      removeRecursively(this.localNodes, node);
    },
    editNode(node) {
      if (node.type === 'Container') {
        const newTitle = prompt('修改标题', node.props.title);
        if (newTitle !== null) {
          node.props.title = newTitle;
        }
      } else if (node.type === 'Item') {
        const newContent = prompt('修改内容', node.props.content);
        if (newContent !== null) {
          node.props.content = newContent;
        }
      }
    },
    updateChildren(parentNode, updatedChildren) {
      parentNode.children = updatedChildren;
    },
    onDragEnd(evt) {
      // 可选:处理拖拽结束后的逻辑,例如保存状态
    }
  }
};
</script>

<style>
.node {
  border: 1px solid #ddd;
  padding: 10px;
  margin: 5px;
  background-color: #f9f9f9;
}
.node-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.node-children {
  margin-left: 20px;
}
button {
  margin-left: 5px;
}
</style>

运行应用

确保所有组件和依赖正确安装后,启动 Vue 应用:

npm run serve

6. 进一步优化

组件类型动态渲染

根据 type 动态渲染不同类型的组件,可以使用 component 动态组件。

<template>
  <draggable ...>
    <div v-for="node in localNodes" :key="node.id" class="node">
      <div class="node-header">
        <component :is="getComponentType(node.type)" :props="node.props" />
        <!-- 操作按钮 -->
      </div>
      <!-- 子组件 -->
    </div>
  </draggable>
</template>

<script>
import draggable from 'vuedraggable';
import ContainerComponent from './ContainerComponent.vue';
import ItemComponent from './ItemComponent.vue';
// 其他组件

export default {
  // ...
  components: { draggable, ContainerComponent, ItemComponent },
  methods: {
    getComponentType(type) {
      const map = {
        Container: 'ContainerComponent',
        Item: 'ItemComponent'
        // 添加其他类型映射
      };
      return map[type] || 'div';
    },
    // 其他方法...
  }
};
</script>

状态管理

对于更复杂的应用,考虑使用 Vuex 或 Pinia 来集中管理状态,便于维护和扩展。

性能优化

对于大型树结构,使用 virtual-scroll 或其他性能优化技术,确保渲染效率。

总结

通过设计一个树形数据结构,并结合 Vue 的递归组件和 Vue.Draggable 库,你可以轻松实现一个支持嵌套拖拽的组件系统。确保每个节点都有唯一的标识符,并提供便捷的增删改查方法,以维护数据的完整性和一致性。序列化和反序列化 JSON 使得数据的存储和恢复变得简单,适用于持久化存储或与服务器交互。

声明:HEUE NOTE|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA 4.0协议进行授权

转载:转载请注明原文链接 - Vue递归拖拽与动态展示组件