用vue3+elementplus+ts+vite4 实现树形菜单拖动排序,并可增、删、改、查

实现了树形菜单的拖动排序、新增、删除、修改和查询功能。其中使用了vue3、elementplus、ts和vite,需要提前安装,另外,这里因使用了unplugin-auto-import和unplugin-vue-components两个插件,所以能自动选择性导入elementplus元素,实际上可直接使用按需导入框架(链接地址)即可。

<!--suppress ALL -->
<template>
 <el-input v-model="filterText" placeholder="输入查找菜单关键词" />

 <el-tree
   ref="treeRef"
   class="filter-tree"
   :filter-node-method="filterNode"
   :props="defaultProps"
   :allow-drop="allowDrop"
   :allow-drag="allowDrag"
   :data="dataSource"
   :render-content="renderContent"
   :expand-on-click-node="false"
   draggable
   default-expand-all
   node-key="id"
   @node-drag-start="handleDragStart"
   @node-drag-enter="handleDragEnter"
   @node-drag-leave="handleDragLeave"
   @node-drag-over="handleDragOver"
   @node-drag-end="handleDragEnd"
   @node-drop="handleDrop"
   highlight-current
   check-on-click-node
 >
   <template #default="{ node, data }">
     <span class="custom-tree-node">
       <span>{{ node.label }}</span>
       <span>
         <a @click="append(data)"> 添加 </a>
         <a style="margin-left: 8px" @click="remove(node, data)"> 删除 </a>
       </span>
     </span>
   </template>
 </el-tree>
</template>

<script lang="ts" setup>
import type Node from "element-plus/es/components/tree/src/model/node";
import type { DragEvents } from "element-plus/es/components/tree/src/model/useDragNode";
import type {
 AllowDropType,
 NodeDropType,
} from "element-plus/es/components/tree/src/tree.type";
import { ref, watch } from "vue";
import { ElMessageBox } from "element-plus";
// import Modal from '@/component/Modal.vue'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDragStart = (node: Node, ev: DragEvents) => {
 console.log("drag start", node);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDragEnter = (
 draggingNode: Node,
 dropNode: Node,
 ev: DragEvents
) => {
 console.log("tree drag enter:", dropNode.label);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDragLeave = (
 draggingNode: Node,
 dropNode: Node,
 ev: DragEvents
) => {
 console.log("tree drag leave:", dropNode.label);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDragOver = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
 console.log("tree drag over:", dropNode.label);
};
const handleDragEnd = (
 draggingNode: Node,
 dropNode: Node,
 dropType: NodeDropType,
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 ev: DragEvents
) => {
 console.log("tree drag end:", dropNode && dropNode.label, dropType);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDrop = (
 draggingNode: Node,
 dropNode: Node,
 dropType: NodeDropType,
 ev: DragEvents
) => {
 console.log("tree drop:", dropNode.label, dropType);
};
const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
 if (dropNode.data.label === "二级 3-1") {
   return type !== "inner";
 }
 return true;
};
const allowDrag = (draggingNode: Node) => {
 return !draggingNode.data.label.includes("三级 3-1-1");
};

// 内部调用
interface Tree {
 id: number;
 label: string;
 children?: Tree[];
}
// 初始化id
let id = 1000;

// 没什么用不定义无问题
const defaultProps = {
 children: "children",
 label: "label",
};

const filterText = ref("");
const treeRef = ref<InstanceType<any>>();

// 监听查找框变化
watch(filterText, (val) => {
 treeRef.value!.filter(val);
});

// 查找框数据 这里 data原来是Tree,在vscode中报错现在更改为any正常
const filterNode = (value: string, data: any) => {
 if (!value) return true;
 return data.label.includes(value);
};

// 添加
const append = (data: Tree) => {
 const newChild = { id: id++, label: "New node", children: [] };
 if (!data.children) {
   data.children = [];
 }
 data.children.push(newChild);
 dataSource.value = [...dataSource.value];
};

// 删除
const remove = (node: Node, data: Tree) => {
 const parent = node.parent;
 const children: Tree[] = parent.data.children || parent.data;
 const index = children.findIndex((d) => d.id === data.id);
 children.splice(index, 1);
 dataSource.value = [...dataSource.value];
};

// 编辑(data.label="赋值")
const edit = (data: Tree) => {
 ElMessageBox.prompt("请输入标题", "提示", {
   confirmButtonText: "确定",
   cancelButtonText: "取消",
   customClass: "my-message-box",
 })
   .then(({ value }) => {
     if (value !== null) {
       data.label = value;
     }
   })
   .catch(() => {
     // 处理取消按钮的点击事件
   });
};

// 更新页面
const renderContent = (
 h: any,
 {
   node,
   data,
   store,
 }: {
   node: Node;
   data: Tree;
   store: Node["store"];
 }
) => {
 return h(
   "span",
   {
     class: "custom-tree-node",
   },
   h("span", null, node.label),
   h(
     "span",
     null,
     h(
       "a",
       {
         style: "margin-left: 58px",
         onClick: () => edit(data),
       },
       "编辑 "
     ),
     h(
       "a",
       {
         style: "margin-left: 8px",
         onClick: () => append(data),
       },
       "添加 "
     ),
     h(
       "a",
       {
         style: "margin-left: 8px",
         onClick: () => remove(node, data),
       },
       "删除 "
     )
   )
 );
};

// 初始数据
const dataSource = ref<Tree[]>([
 {
   id: 1,
   label: "一级 1",
   children: [
     {
       id: 2,
       label: "二级 1-1",
       children: [
         {
           id: 3,
           label: "三级 1-1-1",
         },
       ],
     },
   ],
 },
 {
   id: 4,
   label: "一级 2",
   children: [
     {
       id: 5,
       label: "二级 2-1",
       children: [
         {
           id: 6,
           label: "三级 2-1-1",
         },
       ],
     },
     {
       id: 7,
       label: "二级 2-2",
       children: [
         {
           id: 8,
           label: "三级 2-2-1",
         },
       ],
     },
   ],
 },
 {
   id: 9,
   label: "一级 3",
   children: [
     {
       id: 10,
       label: "二级 3-1",
       children: [
         {
           id: 11,
           label: "三级 3-1-1",
         },
       ],
     },
     {
       id: 12,
       label: "二级 3-2",
       children: [
         {
           id: 13,
           label: "三级 3-2-1",
         },
       ],
     },
   ],
 },
]);
</script>
<style scoped>
.custom-tree-node {
 flex: 1;
 display: flex;
 align-items: center;
 justify-content: space-between;
 font-size: 12px;
 padding-right: 8px;
}
.filter-tree {
 font-size: 16px;
}
.my-message-box .el-message-box__wrapper {
 width: 500px;
 height: 300px;
}
</style>

注意:ElMessageBox、ElMenu等如果在.eslintrc-auto-import.json或components.d.ts中全局引入则不要再像上面代码中那样再次引入,否则二次重复引入将会显示不正常。


另附一例:下面是vue3+typescript+scss简单的拖曳排序代码

<template>
 <div class="main">
   <div
     class="item"
     v-for="item in dataList"
     :key="item"
     draggable="true"
     @dragstart="dragstart(item)"
     @dragenter="dragenter(item)"
     @dragend="dragend(item)"
   >
     {{ item }}
   </div>
 </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const dataList = ref<string[]>([
 '1第一条数据',
 '2第二条数据',
 '3第三条数据',
 '4第四条数据',
 '5第五条数据'
])
const clickVal = ref<string>()
const moveVal = ref<string>()
const endVal = ref<string>()
const dragstart = (item: string): void => {
 clickVal.value = item
}
const dragenter = (item: string): void => {
 moveVal.value = item
}
const dragend = (item: string): void => {
 endVal.value = item
 let index = dataList.value.indexOf(item) //移动元素的当前位置
 let moveObj = moveVal.value ? moveVal.value : ''
 let addIndex = dataList.value.indexOf(moveObj) //要移动后的位置
 dataList.value.splice(index, 1)
 dataList.value.splice(addIndex, 0, item)
}
</script>
<style lang="scss" scoped>
.main {
 margin-left: 10px;
 margin-top: 50px;
 display: flex;
 .item {
   cursor: pointer;
   margin-bottom: 10px;
   width: 300px;
   text-align: center;
   height: 60px;
   line-height: 60px;
   background-color: cadetblue;
   color: #fff;
   margin-left: 50px;
 }
}
</style>