之前按照項目需求使用element中的tree來創建目錄列表,如今記錄一下。
一、項目需求
1.完整展示目錄列表
2.右擊節點選擇重命名,刪除,創建文件夾三個選項
3.拖拽文件夾,其中拖拽文件夾有以下要求:
a. 如果該文件夾內已存在上傳文件,則其他文件夾不能拖拽進入該文件夾內
b.整個目錄中有且僅有一個根目錄,拖拽文件夾的范圍只能在該根目錄里面
4.重命名文件夾要求:
a.重命名完成后按enter鍵完成
5.刪除文件夾要求:
a.如果該文件夾內已含有上傳文件,則刪除失敗
6.創建文件夾要求:
a.如果該文件夾內已含有上傳文件,則創建失敗
二、思路整理
1.在mounted中向后台請求項目列表,然后存儲treeData
2.鼠標右鍵彈出的是menu菜單選項框,對於獲取點擊節點的信息可以使用el-tree中的 @node-contextmenu="rightClick"
3.選擇刪除文件夾的時候要注意在發送請求的同時也要完成前端的“刪除”動作,使用map,indexOf, slice
4.重命名中使用@keyup.enter.native來觸發請求,使用focus()聚焦輸入框
5.鼠標右擊的時候menu的位置是隨之改變的,且當鼠標位置加上menu的高度或寬度超出視頻范圍的時候要合適調節menu的位置。
/* 菜單定位基於鼠標點擊位置 */ let height = document.documentElement.clientHeight || document.body.clientHeight if (event.clientY + 168 > height) { menu.style.left = event.clientX - 5 + 'px' menu.style.top = event.clientY - 10 - 168 + 'px' } else { menu.style.left = event.clientX + 10 + 'px' menu.style.top = event.clientY + 5 + 'px' }
6.menu的顯示與隱藏,當鼠標點擊其他位置的時候,menu隱藏
document.addEventListener('click', this.hide, true) document.removeEventListener('click', this.hide)
7.當文件夾命名過長的時候,出現橫向滾動條
.comp-tree { margin-top: 1em; overflow-y: hidden; overflow-x: scroll; } .el-tree { min-width: 100%; display:inline-block !important; }
三、源碼分析
參考:
在此基礎上做的修改

<template> <div v-loading="isLoading" class="comp-tree" @click="hiddenMenu"> <el-tree ref="SlotTree" :data="treeData" :props="defaultProps" :expand-on-click-node="true" highlight-current :node-key="NODE_KEY" @node-click="handleNodeClick" @node-contextmenu="rightClick" @node-drag-start = "handleDragStart" @node-drop="handleDrop" :draggable = draggable default-expand-all > <div class="comp-tr-node" slot-scope="{ node, data }"> <!-- 編輯狀態 --> <template v-if="node.isEdit"> <el-input v-model="data.menuName" autofocus size="mini" :ref="'slotTreeInput'+data.recordId" @keyup.enter.native="handleInput(node, data)"> </el-input> </template> <!-- 非編輯狀態 --> <template v-else> <!-- 名稱: 新增節點增加class(is-new) --> <span> <span> {{ node.label }} </span> </span> </template> </div> </el-tree> <!-- 鼠標右鍵產生的選項 --> <div v-show="menuVisible" id="menu"> <el-menu class="el-menu-vertical rightClickMenu" @select="handleRightSelect" text-color="#303133" active-text-color = "#303133" > <el-menu-item index="1" class="menuItem"> <i class="el-icon-edit"></i> <span slot="title">重命名</span> </el-menu-item> <el-menu-item index="2" class="menuItem"> <i class="el-icon-folder-add"></i> <span slot="title">新建文件夾</span> </el-menu-item> <el-menu-item index="3" class="menuItem"> <i class="el-icon-delete"></i> <span slot="title">刪除</span> </el-menu-item> </el-menu> </div> <detail-win v-if="isShow" @close="closeWin" :node = NODE v-bind="$attrs" v-on="$listeners"></detail-win> </div> </template> <script> import detailWin from './detail-win' import { baseWarn } from 'src/common/fn' export default { components: { detailWin }, data () { return { isLoading: false, // 是否加載 NODE_KEY: 'recordId', // id對應字段 defaultProps: {// 默認設置 children: 'children', label: 'menuName' }, menuVisible: false, isShow: false, clickNode: '', DATA: null, NODE: null, objectID: null, dragPid: null, dragIndex: null, newParentId: null, oldMenuName: null } }, computed: { treeData () { return this.$sysStore.fileTreeData }, addNode () { return this.$sysStore.addNode }, draggable () { if (!this.$store.state.user.isOuterUser) { return true } else { return false } } }, watch: { }, created () { }, mounted () { }, methods: { // 修改節點 handleInput (node, data) { // 退出編輯狀態 if (node.isEdit) { this.$set(node, 'isEdit', false) } if (this.oldMenuName === data.menuName) { } else { this.$axios({ url: '........', method: 'post', data: { ... } }) .then(res => { this.$message({ type: 'success', message: '重命名成功' }) }) .catch(function (error) { this.$message({ type: 'error', message: '重命名失敗' }) console.log(error) }) } }, // 重命名 handleEdit (node, data) { // 模板文件夾名稱不能重命名 if (data.isTemplate === 1) { this.$message.error('該文件夾無法重命名') } else { // 設置編輯狀態 if (!node.isEdit) { this.$set(node, 'isEdit', true) } // 輸入框聚焦 this.$nextTick(() => { if (this.$refs['slotTreeInput' + data.recordId]) { this.$refs['slotTreeInput' + data.recordId].$refs.input.focus() } }) } }, // 新增節點 handleAdd (node, data) { // console.log(node,data) if (data.fileSum !== 0) { this.$message.error('此文件夾已存在上傳文件,不能新增文件夾!') return } this.isShow = true }, // 刪除節點 handleDelete (node, data) { if (data.children && data.children.length !== 0) { this.$message.error('此文件夾內含有其他文件夾,不可刪除!') return } if (data.fileSum !== 0) { this.$message.error('此文件夾內含有上傳文件,不可刪除!') } else { this.$confirm(`是否刪除${node.label}?`, '提示', { confirmButtonText: '確認', cancelButtonText: '取消', type: 'warning' }).then(() => { this.deleteProj(node, data) }).catch(() => {}) } }, deleteProj (node, data) { this.$axios({ url: '...', method: 'post', data: { ... } }) .then(res => { if (res.success) { let _list = node.parent.data.children || node.parent.data// 節點同級數據 let _index = _list.map((c) => c.recordId).indexOf(data.recordId) _list.splice(_index, 1) this.$message.success('刪除成功') // 使右側列表消失 this.$parent.$parent.disabled = true // 直接改變vuex中treeData的數值 let fileTreeData = this.filter(this.treeData, node.data.kid) this.$store.dispatch(`${this.wpstore}/setFileTreeData`, fileTreeData) } else { this.$message.error('刪除失敗,請重試') } }) }, /** * 刪除或者更改treeData中指定kid的子節點 * * @returns arr */ filter (arr, kid) { for (var i = 0; i < arr.length; i++) { var el = arr[i] if (el.kid === kid) { arr.splice(i, 1) } else { if (el.children && el.children.length) { this.filter(el.children, kid) } } } return arr }, // 點擊節點 handleNodeClick (node) { this.menuVisible = false this.$store.dispatch(`${this.wpstore}/selectNode`, node) }, // 拖拽開始時記錄該節點的pid及其在原來數組中的原始數據 handleDragStart (node) { this.dragPid = node.data.recordPid let p = this.$refs.SlotTree.getNode(this.dragPid) let _list = p.data.children // 節點同級數據 let _index = _list.map((c) => c.recordId).indexOf(node.data.recordId) this.dragIndex = _index }, // 拖拽成功時觸發請求 handleDrop (draggingNode, dropNode, dropType, e) { // debugger if (dropNode.data.fileSum !== 0 && dropType === 'inner') { baseWarn('該文件夾內含有上傳文件,拖拽失敗', null, null, null) let _list = dropNode.data.children // 節點同級數據 let _index = _list.map((c) => c.recordId).indexOf(draggingNode.data.recordId) _list.splice(_index, 1) // 將節點返回到原來的位置上 let p = this.$refs.SlotTree.getNode(this.dragPid) // console.log(p,'p') p.data.children.splice(this.dragIndex, 0, draggingNode.data) } else if (dropNode.data.recordPid === 'root') { baseWarn('無法拖拽為根文件夾', null, null, null) this.$refs.SlotTree.remove(draggingNode.data.recordId) let p = this.$refs.SlotTree.getNode(this.dragPid) p.data.children.splice(this.dragIndex, 0, draggingNode.data) } else { // 算出子節點對於父節點的相對位置 let _list = dropNode.parent.data.children // 拖拽節點同級數據 let _index = _list.map((c) => c.recordId).indexOf(draggingNode.data.recordId) if (_index === -1) { _list = dropNode.childNodes _index = _list.map((c) => c.data.recordId).indexOf(draggingNode.data.recordId) } // 得出目標節點的recordId if (dropNode.parent.data.recordId === this.dragPid) { // 相同的父節點 this.newParentId = this.dragPid } else { this.newParentId = dropNode.parent.data.recordId } // 發送請求 this.$axios({ url: '...', method: 'post', data: { ... } }) .then(res => { this.$message({ type: 'success', message: '拖拽成功' }) }) .catch(function (error) { console.log(error) }) } }, // 鼠標右擊 rightClick (event, object, value, element) { this.oldMenuName = object.menuName if (this.$store.state.user.isOuterUser) { this.menuVisible = false } else { if (this.objectID !== object.recordId) { this.objectID = object.recordId this.menuVisible = true this.DATA = object this.NODE = value } else { this.menuVisible = !this.menuVisible } // document.addEventListener('click', (e) => { // this.menuVisible = false // }) this.hiddenMenu() let menu = document.querySelector('.rightClickMenu') /* 菜單定位基於鼠標點擊位置 */ let height = document.documentElement.clientHeight || document.body.clientHeight if (event.clientY + 168 > height) { menu.style.left = event.clientX - 5 + 'px' menu.style.top = event.clientY - 10 - 168 + 'px' } else { menu.style.left = event.clientX + 10 + 'px' menu.style.top = event.clientY + 5 + 'px' } menu.style.position = 'fixed' } }, handleRightSelect (key) { this.menuVisible = false if (key == 1) { this.handleEdit(this.NODE, this.DATA) } else if (key == 2) { this.handleAdd(this.NODE, this.DATA) } else if (key == 3) { this.handleDelete(this.NODE, this.DATA) } }, hiddenMenu () { document.addEventListener('click', this.hide, true) document.removeEventListener('click', this.hide) }, hide() { this.menuVisible = false }, closeWin (val) { this.isShow = val } } } </script> <style lang="scss" scoped> .el-menu { border: solid 1px #e6e6e6 } .comp-tree { margin-top: 1em; overflow-y: hidden; overflow-x: scroll; } .el-tree { min-width: 100%; display:inline-block !important; } .rightClickMenu { z-index: 999 } </style>
這里僅供參考,具體的編寫代碼還需根據各位實際項目需求來修改