本次项目的前端部分使用vue框架+iview组件构建,其中IDE的文件树部分使用了iview的Tree组件,但是Tree组件本身的接口功能极其有限,网上的相关资料也不多,在使用时费了一番功夫才摸索清楚使用方法。在这里总结一下使用Tree组件实现各种文件树相关功能的方法和坑点。
代码地址:vLab-Fronted/src/components/MySider/MyTree.vue
官方文档:iview的Tree组件文档
参考博客:
iView树形组件:增删改节点
iview tree 不可拖放,好在有render属性,可以自己写
在官方文档中有这样的说明:
Render 函数的第二个参数,包含三个字段:
root :树的根节点
node :当前节点 data :当前节点的数据 通过合理地使用 root、node 和 data 可以实现各种效果,其中,iView 给每个节点都设置了一个 nodeKey 字段,用来标识节点的 id。 root是一个数组,其中包含着整棵树的所有node,使用root[id].node就可以得到nodeKey的值为id的节点,比较正规的写法如下: var findnode = root.find(el => el.nodeKey == findkey).node; root只能通过render函数得到,如果想在render函数之外得到它,可以在data中增加变量rootData,然后在render函数的开始这样写: renderContent(h, { root, node, data }) { var that = this; that.rootData = root; } 在实际使用中,并没有发现node与data的区别,基本可以相互替代。 node表示一个节点的(数据),可以通过一些key获得该结点的信息 node.nodeKey为该节点的id node.title为该节点的名称 node.parent为该节点的父节点的id(并非父节点本身) node.expand为该节点的展开状态,true为展开false为未展开 node.children为该节点的子节点数组(其中的元素就是节点数据,不需要再使用.node) 也就可以在所有节点中索引得到所需的节点 注意:root中直接取出的元素并非是节点本身,不能想当然地进行形如var findnode = root.find(el => el.title == findtitle).node的检索,如果希望按名称、展开状态等等进行检索,则需要遍历所有结点,然后取到结点的信息。 在实际使用中,并没有发现node与data的区别,基本可以相互替代。 不要混淆root数组的结构和data数组的结构 总的来说,root数组的长度是文件树全部节点的数目,直接遍历root数组就相当于遍历了全部节点;data则是一个单独节点,但是data.children则是一个包含它所有子结点的数组,节点数据以嵌套的形式组织起来,通过递归的手段可以遍历得到全部节点。 如果希望得到一个节点的子结点,可以直接从data.children[key]中取得(key为在children数组中的索引);但如果希望得到一个节点的父结点,则需要先得到父结点id,再在root中遍历寻找。灵活地使用这两种形式可以完成许多任务,特别是root的可以直接顺序遍历,免去了递归的繁琐。 var parentKey = root.find(el => el.nodeKey === findkey).parent; var parent = root.find(el => el.nodeKey === parentKey).node; root是Tree结构特有的,一般来说从后端取得的都是形如data的嵌套的数据结构。 将后台数据渲染进文件树 在该项目中,文件树以项目名为根节点,根节点之下包含项目内容。 我们希望把一个从后端取到的文件树渲染为前端的文件树,在实际情况下,从后端只能得到某一目录下的全部文件,按数组组织起来,无法得到目录本身,也就是说从后端直接拿到的文件没有根节点。我们采用以下的方法: 在data中按文档样例正常声明树结构,在取到文件数据后使用this.$set(this.data4[0], "children", data)的方法将其渲染进文件树中,注意如果修改文件树的具体数据只能使用this.$set()的方法,直接修改数组元素是无效的,也要注意到Tree组件的输入data本身也是一个长度为1的数组,必须要使用索引才能获取到。 <template> <Tree :data="data4" :render="renderContent"></Tree> </template> <script> export default { data() { return { data4: [ { title: "", expand: true, children: [], render: (h, { root, node, data }) => { return h( "span", { class: "root", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, }, [data.title] ); } } ], } }, methods: { //前后文省略 //假设response为后端返回的数据 var _this = this; _this.$set(_this.data4[0], "children", response.data); } } </script> 按节点类型进行不同的渲染 在文件树中,除单独的根节点外,有文件夹和文件两类节点。需要对这两类节点进行不同的渲染(不同的图标、不同的右键菜单),区分的方法就是文件节点的children为undefined、文件夹节点的children不为undefined(如果是目录下没有文件的文件夹,则children会为[]) 得到某一节点的路径 在该项目下,文件树与后端的交互接口的参数往往是节点(文件/文件夹)从根目录开始的查找路径字符串。 getPath(root, nodekey, data) { var path = ""; var findkey = nodekey; if (data.children != undefined) { //若为文件夹,返回当前文件夹的路径 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } if (nodekey != 0) { path = "/code/" + path + data.title + "/"; } else { path = "/code/"; } } else { //若为文件,返回当前文件的上层目录 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } path = "/code/" + path; } return path; } 保存修改状态 修改文件数节点名称的功能在文章开头的链接中说的很清楚,本项目文件树的基础代码也是基于它编写的,但是它只能通过点击按钮确认对节点名称的修改,用户体验并不好,本项目对其进行了以下改进: 有单击其他节点、右键其他节点的行为则保存当前修改,也就是说只能同时修改一个节点名称。 方法:编写函数,遍历所有节点,如果发现有editState为true的节点,则修改它的editState,保存修改 saveEdit(root) { var i; var findnode = undefined; for (i = 0; i < root.length; i++) { var shownode = root.find(el => el.nodeKey === i).node; if (shownode.editState === true) { findnode = shownode; break; } } if (findnode != undefined) { this.confirmTheChange(root, i, findnode); } }, 在修改状态下,可以单击文本框实现全选文本,也就是可以方便地将光标移动到文本开头或者末尾,或者直接清除全部内容。 可以通过键盘回车保存修改。 以上两点可以通过绑定input的keyup与focus动作实现(具体见代码) h(`${data.editState ? "input" : ""}`, { attrs: { value: `${data.editState ? data.title : ""}`, autofocus: "true" }, style: { width: "50%", cursor: "auto" }, on: { change: event => { this.inputContent = event.target.value; }, keyup: event => { if (event.keyCode == 13) { this.confirmTheChange(root, data.nodeKey, data); } }, focus: event => { event.currentTarget.select(); } } }) 右键菜单和拖拽功能 右键菜单可以通过iview的DropDown组件实现,使用contextmenu动作激活 拖拽功能与dragstart、dragover、dragend、drop动作相关,注意要设定好draggable属性值为true才可以进行拖拽 { class: "hhhaha", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, attrs: { draggable: that.isWriteable ? "true" : "false" }, on: { dragstart: () => { this.handleDragStart(root, node, data); }, dragover: () => { this.handleDragOver(root, node, data); }, dragend: () => { this.handleDragEnd(root, node, data); }, drop: () => { this.handleDrop(root, node, data); }, click: () => { data.editState ? "" : this.handleClickTreeNode(root, node.nodeKey, data); }, contextmenu: e => { e.preventDefault(); this.hiddenRightMenu(); this.nodeInfo = data; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = !this.$refs .contentFileMenu.currentVisible; } } } 下拉菜单示例: <Dropdown transfer ref="contentFileMenu" style="display: none;" trigger="click"> <DropdownMenu slot="list" ref="pp" style="min-width: 80px;"> <DropdownItem @click.native="movefile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">剪切</DropdownItem> <DropdownItem @click.native="copyfile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">复制</DropdownItem> <DropdownItem @click.native="paste(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">粘贴</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="editTree(nodeInfo)" :disabled="!isWriteable">重命名</DropdownItem> <DropdownItem @click.native="remove(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">删除</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="download(rootData, nodeInfo.nodeKey, nodeInfo)">下载</DropdownItem> </DropdownMenu> </Dropdown> 注意要编写隐藏全部右键菜单的函数,并在单击动作或右键动作中调用,以此改良用户体验 hiddenRightMenu() { this.$refs.contentFolderMenu.$refs.reference = event.target; this.$refs.contentFolderMenu.currentVisible = false; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = false; this.$refs.contentRootMenu.$refs.reference = event.target; this.$refs.contentRootMenu.currentVisible = false; } 按规则排序 对于文件系统,我们一般的排序规则是:文件夹在前文件在后、同类型按字符大小排序。这本质上就是对node.children数组按照一定的规则进行排序,而js的数组用自定义规则进行排序是很方便的。 parent.children.sort(function(a, b) { if (a.children != undefined && b.children == undefined) { return -1; } else if (a.children == undefined && b.children != undefined) { return 1; } else { var x = a.title; var y = b.title; if (x < y) { return -1; } if (x > y) { return 1; } } return 0; }); 从后端获取数据并进行刷新 这一过程其实只需要重复进行文章开头的相同的this.$set()操作即可,难点主要在于:在Tree组件的初始默认状态下,所有节点均为折叠状态,而我们希望每次刷新后感知不到文件树的变化,也就是说,需要在刷新前保存下所有节点的展开状态,并在刷新后还原状态。 在实际编程中,我选择直接保存刷新前的root数组,对刷新后得到的root数组进行遍历,在判断是否为同一文件夹时使用getPath函数得到的路径作为比较依据。 注意,修改展开状态时,也只能使用this.$set()才能生效 if (oriPath == targetPath) { _this.$set(targetData, "expand", true); } 复制节点 如果想复制一个节点,就涉及到了object的深拷贝问题,如果希望将一个节点及其下嵌套的所有内容全部复制到另一节点的children内,仅仅使用newInfo = copyInfo是不同的,必须使用深拷贝,在查询资料后我选择了如下写法: deepcopy(copyInfo) { var newInfo = []; newInfo = JSON.parse(JSON.stringify(copyInfo)); return newInfo; }, 当然,也可以选择让后端先处理复制请求,再直接从后端获取更新后的数据,就不需要再考虑这些问题了。 以上只是几个重要功能的实现思路,具体问题可以查看具体代码:vLab-Fronted/src/components/MySider/MyTree.vue,如果有不理解的地方可以在评论中提出,也欢迎在评论中留言交流~~ posted @ 2020-05-27 18:28 syncline 阅读( ...) 评论( ...) 编辑 收藏 刷新评论 刷新页面 返回顶部
data :当前节点的数据 通过合理地使用 root、node 和 data 可以实现各种效果,其中,iView 给每个节点都设置了一个 nodeKey 字段,用来标识节点的 id。 root是一个数组,其中包含着整棵树的所有node,使用root[id].node就可以得到nodeKey的值为id的节点,比较正规的写法如下: var findnode = root.find(el => el.nodeKey == findkey).node; root只能通过render函数得到,如果想在render函数之外得到它,可以在data中增加变量rootData,然后在render函数的开始这样写: renderContent(h, { root, node, data }) { var that = this; that.rootData = root; } 在实际使用中,并没有发现node与data的区别,基本可以相互替代。 node表示一个节点的(数据),可以通过一些key获得该结点的信息 node.nodeKey为该节点的id node.title为该节点的名称 node.parent为该节点的父节点的id(并非父节点本身) node.expand为该节点的展开状态,true为展开false为未展开 node.children为该节点的子节点数组(其中的元素就是节点数据,不需要再使用.node) 也就可以在所有节点中索引得到所需的节点 注意:root中直接取出的元素并非是节点本身,不能想当然地进行形如var findnode = root.find(el => el.title == findtitle).node的检索,如果希望按名称、展开状态等等进行检索,则需要遍历所有结点,然后取到结点的信息。 在实际使用中,并没有发现node与data的区别,基本可以相互替代。 不要混淆root数组的结构和data数组的结构 总的来说,root数组的长度是文件树全部节点的数目,直接遍历root数组就相当于遍历了全部节点;data则是一个单独节点,但是data.children则是一个包含它所有子结点的数组,节点数据以嵌套的形式组织起来,通过递归的手段可以遍历得到全部节点。 如果希望得到一个节点的子结点,可以直接从data.children[key]中取得(key为在children数组中的索引);但如果希望得到一个节点的父结点,则需要先得到父结点id,再在root中遍历寻找。灵活地使用这两种形式可以完成许多任务,特别是root的可以直接顺序遍历,免去了递归的繁琐。 var parentKey = root.find(el => el.nodeKey === findkey).parent; var parent = root.find(el => el.nodeKey === parentKey).node; root是Tree结构特有的,一般来说从后端取得的都是形如data的嵌套的数据结构。 将后台数据渲染进文件树 在该项目中,文件树以项目名为根节点,根节点之下包含项目内容。 我们希望把一个从后端取到的文件树渲染为前端的文件树,在实际情况下,从后端只能得到某一目录下的全部文件,按数组组织起来,无法得到目录本身,也就是说从后端直接拿到的文件没有根节点。我们采用以下的方法: 在data中按文档样例正常声明树结构,在取到文件数据后使用this.$set(this.data4[0], "children", data)的方法将其渲染进文件树中,注意如果修改文件树的具体数据只能使用this.$set()的方法,直接修改数组元素是无效的,也要注意到Tree组件的输入data本身也是一个长度为1的数组,必须要使用索引才能获取到。 <template> <Tree :data="data4" :render="renderContent"></Tree> </template> <script> export default { data() { return { data4: [ { title: "", expand: true, children: [], render: (h, { root, node, data }) => { return h( "span", { class: "root", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, }, [data.title] ); } } ], } }, methods: { //前后文省略 //假设response为后端返回的数据 var _this = this; _this.$set(_this.data4[0], "children", response.data); } } </script> 按节点类型进行不同的渲染 在文件树中,除单独的根节点外,有文件夹和文件两类节点。需要对这两类节点进行不同的渲染(不同的图标、不同的右键菜单),区分的方法就是文件节点的children为undefined、文件夹节点的children不为undefined(如果是目录下没有文件的文件夹,则children会为[]) 得到某一节点的路径 在该项目下,文件树与后端的交互接口的参数往往是节点(文件/文件夹)从根目录开始的查找路径字符串。 getPath(root, nodekey, data) { var path = ""; var findkey = nodekey; if (data.children != undefined) { //若为文件夹,返回当前文件夹的路径 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } if (nodekey != 0) { path = "/code/" + path + data.title + "/"; } else { path = "/code/"; } } else { //若为文件,返回当前文件的上层目录 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } path = "/code/" + path; } return path; } 保存修改状态 修改文件数节点名称的功能在文章开头的链接中说的很清楚,本项目文件树的基础代码也是基于它编写的,但是它只能通过点击按钮确认对节点名称的修改,用户体验并不好,本项目对其进行了以下改进: 有单击其他节点、右键其他节点的行为则保存当前修改,也就是说只能同时修改一个节点名称。 方法:编写函数,遍历所有节点,如果发现有editState为true的节点,则修改它的editState,保存修改 saveEdit(root) { var i; var findnode = undefined; for (i = 0; i < root.length; i++) { var shownode = root.find(el => el.nodeKey === i).node; if (shownode.editState === true) { findnode = shownode; break; } } if (findnode != undefined) { this.confirmTheChange(root, i, findnode); } }, 在修改状态下,可以单击文本框实现全选文本,也就是可以方便地将光标移动到文本开头或者末尾,或者直接清除全部内容。 可以通过键盘回车保存修改。 以上两点可以通过绑定input的keyup与focus动作实现(具体见代码) h(`${data.editState ? "input" : ""}`, { attrs: { value: `${data.editState ? data.title : ""}`, autofocus: "true" }, style: { width: "50%", cursor: "auto" }, on: { change: event => { this.inputContent = event.target.value; }, keyup: event => { if (event.keyCode == 13) { this.confirmTheChange(root, data.nodeKey, data); } }, focus: event => { event.currentTarget.select(); } } }) 右键菜单和拖拽功能 右键菜单可以通过iview的DropDown组件实现,使用contextmenu动作激活 拖拽功能与dragstart、dragover、dragend、drop动作相关,注意要设定好draggable属性值为true才可以进行拖拽 { class: "hhhaha", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, attrs: { draggable: that.isWriteable ? "true" : "false" }, on: { dragstart: () => { this.handleDragStart(root, node, data); }, dragover: () => { this.handleDragOver(root, node, data); }, dragend: () => { this.handleDragEnd(root, node, data); }, drop: () => { this.handleDrop(root, node, data); }, click: () => { data.editState ? "" : this.handleClickTreeNode(root, node.nodeKey, data); }, contextmenu: e => { e.preventDefault(); this.hiddenRightMenu(); this.nodeInfo = data; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = !this.$refs .contentFileMenu.currentVisible; } } } 下拉菜单示例: <Dropdown transfer ref="contentFileMenu" style="display: none;" trigger="click"> <DropdownMenu slot="list" ref="pp" style="min-width: 80px;"> <DropdownItem @click.native="movefile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">剪切</DropdownItem> <DropdownItem @click.native="copyfile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">复制</DropdownItem> <DropdownItem @click.native="paste(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">粘贴</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="editTree(nodeInfo)" :disabled="!isWriteable">重命名</DropdownItem> <DropdownItem @click.native="remove(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">删除</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="download(rootData, nodeInfo.nodeKey, nodeInfo)">下载</DropdownItem> </DropdownMenu> </Dropdown> 注意要编写隐藏全部右键菜单的函数,并在单击动作或右键动作中调用,以此改良用户体验 hiddenRightMenu() { this.$refs.contentFolderMenu.$refs.reference = event.target; this.$refs.contentFolderMenu.currentVisible = false; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = false; this.$refs.contentRootMenu.$refs.reference = event.target; this.$refs.contentRootMenu.currentVisible = false; } 按规则排序 对于文件系统,我们一般的排序规则是:文件夹在前文件在后、同类型按字符大小排序。这本质上就是对node.children数组按照一定的规则进行排序,而js的数组用自定义规则进行排序是很方便的。 parent.children.sort(function(a, b) { if (a.children != undefined && b.children == undefined) { return -1; } else if (a.children == undefined && b.children != undefined) { return 1; } else { var x = a.title; var y = b.title; if (x < y) { return -1; } if (x > y) { return 1; } } return 0; }); 从后端获取数据并进行刷新 这一过程其实只需要重复进行文章开头的相同的this.$set()操作即可,难点主要在于:在Tree组件的初始默认状态下,所有节点均为折叠状态,而我们希望每次刷新后感知不到文件树的变化,也就是说,需要在刷新前保存下所有节点的展开状态,并在刷新后还原状态。 在实际编程中,我选择直接保存刷新前的root数组,对刷新后得到的root数组进行遍历,在判断是否为同一文件夹时使用getPath函数得到的路径作为比较依据。 注意,修改展开状态时,也只能使用this.$set()才能生效 if (oriPath == targetPath) { _this.$set(targetData, "expand", true); } 复制节点 如果想复制一个节点,就涉及到了object的深拷贝问题,如果希望将一个节点及其下嵌套的所有内容全部复制到另一节点的children内,仅仅使用newInfo = copyInfo是不同的,必须使用深拷贝,在查询资料后我选择了如下写法: deepcopy(copyInfo) { var newInfo = []; newInfo = JSON.parse(JSON.stringify(copyInfo)); return newInfo; }, 当然,也可以选择让后端先处理复制请求,再直接从后端获取更新后的数据,就不需要再考虑这些问题了。 以上只是几个重要功能的实现思路,具体问题可以查看具体代码:vLab-Fronted/src/components/MySider/MyTree.vue,如果有不理解的地方可以在评论中提出,也欢迎在评论中留言交流~~ posted @ 2020-05-27 18:28 syncline 阅读( ...) 评论( ...) 编辑 收藏 刷新评论 刷新页面 返回顶部
通过合理地使用 root、node 和 data 可以实现各种效果,其中,iView 给每个节点都设置了一个 nodeKey 字段,用来标识节点的 id。
nodeKey
root是一个数组,其中包含着整棵树的所有node,使用root[id].node就可以得到nodeKey的值为id的节点,比较正规的写法如下:
root[id].node
var findnode = root.find(el => el.nodeKey == findkey).node;
root只能通过render函数得到,如果想在render函数之外得到它,可以在data中增加变量rootData,然后在render函数的开始这样写:
renderContent(h, { root, node, data }) { var that = this; that.rootData = root; }
在实际使用中,并没有发现node与data的区别,基本可以相互替代。
node表示一个节点的(数据),可以通过一些key获得该结点的信息
也就可以在所有节点中索引得到所需的节点
注意:root中直接取出的元素并非是节点本身,不能想当然地进行形如var findnode = root.find(el => el.title == findtitle).node的检索,如果希望按名称、展开状态等等进行检索,则需要遍历所有结点,然后取到结点的信息。
var findnode = root.find(el => el.title == findtitle).node
总的来说,root数组的长度是文件树全部节点的数目,直接遍历root数组就相当于遍历了全部节点;data则是一个单独节点,但是data.children则是一个包含它所有子结点的数组,节点数据以嵌套的形式组织起来,通过递归的手段可以遍历得到全部节点。
如果希望得到一个节点的子结点,可以直接从data.children[key]中取得(key为在children数组中的索引);但如果希望得到一个节点的父结点,则需要先得到父结点id,再在root中遍历寻找。灵活地使用这两种形式可以完成许多任务,特别是root的可以直接顺序遍历,免去了递归的繁琐。
data.children[key]
var parentKey = root.find(el => el.nodeKey === findkey).parent; var parent = root.find(el => el.nodeKey === parentKey).node;
root是Tree结构特有的,一般来说从后端取得的都是形如data的嵌套的数据结构。
在该项目中,文件树以项目名为根节点,根节点之下包含项目内容。
我们希望把一个从后端取到的文件树渲染为前端的文件树,在实际情况下,从后端只能得到某一目录下的全部文件,按数组组织起来,无法得到目录本身,也就是说从后端直接拿到的文件没有根节点。我们采用以下的方法:
在data中按文档样例正常声明树结构,在取到文件数据后使用this.$set(this.data4[0], "children", data)的方法将其渲染进文件树中,注意如果修改文件树的具体数据只能使用this.$set()的方法,直接修改数组元素是无效的,也要注意到Tree组件的输入data本身也是一个长度为1的数组,必须要使用索引才能获取到。
this.$set(this.data4[0], "children", data)
this.$set()
data
<template> <Tree :data="data4" :render="renderContent"></Tree> </template> <script> export default { data() { return { data4: [ { title: "", expand: true, children: [], render: (h, { root, node, data }) => { return h( "span", { class: "root", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, }, [data.title] ); } } ], } }, methods: { //前后文省略 //假设response为后端返回的数据 var _this = this; _this.$set(_this.data4[0], "children", response.data); } } </script>
在文件树中,除单独的根节点外,有文件夹和文件两类节点。需要对这两类节点进行不同的渲染(不同的图标、不同的右键菜单),区分的方法就是文件节点的children为undefined、文件夹节点的children不为undefined(如果是目录下没有文件的文件夹,则children会为[])
在该项目下,文件树与后端的交互接口的参数往往是节点(文件/文件夹)从根目录开始的查找路径字符串。
getPath(root, nodekey, data) { var path = ""; var findkey = nodekey; if (data.children != undefined) { //若为文件夹,返回当前文件夹的路径 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } if (nodekey != 0) { path = "/code/" + path + data.title + "/"; } else { path = "/code/"; } } else { //若为文件,返回当前文件的上层目录 while (findkey !== 0) { var parentKey = root.find(el => el.nodeKey === findkey).parent; if (parentKey == 0) { break; } var parent = root.find(el => el.nodeKey === parentKey).node; path = parent.title + "/" + path; var findkey = parentKey; } path = "/code/" + path; } return path; }
修改文件数节点名称的功能在文章开头的链接中说的很清楚,本项目文件树的基础代码也是基于它编写的,但是它只能通过点击按钮确认对节点名称的修改,用户体验并不好,本项目对其进行了以下改进:
方法:编写函数,遍历所有节点,如果发现有editState为true的节点,则修改它的editState,保存修改
saveEdit(root) { var i; var findnode = undefined; for (i = 0; i < root.length; i++) { var shownode = root.find(el => el.nodeKey === i).node; if (shownode.editState === true) { findnode = shownode; break; } } if (findnode != undefined) { this.confirmTheChange(root, i, findnode); } },
以上两点可以通过绑定input的keyup与focus动作实现(具体见代码)
h(`${data.editState ? "input" : ""}`, { attrs: { value: `${data.editState ? data.title : ""}`, autofocus: "true" }, style: { width: "50%", cursor: "auto" }, on: { change: event => { this.inputContent = event.target.value; }, keyup: event => { if (event.keyCode == 13) { this.confirmTheChange(root, data.nodeKey, data); } }, focus: event => { event.currentTarget.select(); } } })
右键菜单可以通过iview的DropDown组件实现,使用contextmenu动作激活
拖拽功能与dragstart、dragover、dragend、drop动作相关,注意要设定好draggable属性值为true才可以进行拖拽
{ class: "hhhaha", style: { display: "inline-block", lineHeight: "20px", width: "100%", cursor: "pointer" }, attrs: { draggable: that.isWriteable ? "true" : "false" }, on: { dragstart: () => { this.handleDragStart(root, node, data); }, dragover: () => { this.handleDragOver(root, node, data); }, dragend: () => { this.handleDragEnd(root, node, data); }, drop: () => { this.handleDrop(root, node, data); }, click: () => { data.editState ? "" : this.handleClickTreeNode(root, node.nodeKey, data); }, contextmenu: e => { e.preventDefault(); this.hiddenRightMenu(); this.nodeInfo = data; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = !this.$refs .contentFileMenu.currentVisible; } } }
下拉菜单示例:
<Dropdown transfer ref="contentFileMenu" style="display: none;" trigger="click"> <DropdownMenu slot="list" ref="pp" style="min-width: 80px;"> <DropdownItem @click.native="movefile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">剪切</DropdownItem> <DropdownItem @click.native="copyfile_choose(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">复制</DropdownItem> <DropdownItem @click.native="paste(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">粘贴</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="editTree(nodeInfo)" :disabled="!isWriteable">重命名</DropdownItem> <DropdownItem @click.native="remove(rootData, nodeInfo.nodeKey, nodeInfo)" :disabled="!isWriteable">删除</DropdownItem> <Divider style="margin:0" /> <DropdownItem @click.native="download(rootData, nodeInfo.nodeKey, nodeInfo)">下载</DropdownItem> </DropdownMenu> </Dropdown>
注意要编写隐藏全部右键菜单的函数,并在单击动作或右键动作中调用,以此改良用户体验
hiddenRightMenu() { this.$refs.contentFolderMenu.$refs.reference = event.target; this.$refs.contentFolderMenu.currentVisible = false; this.$refs.contentFileMenu.$refs.reference = event.target; this.$refs.contentFileMenu.currentVisible = false; this.$refs.contentRootMenu.$refs.reference = event.target; this.$refs.contentRootMenu.currentVisible = false; }
对于文件系统,我们一般的排序规则是:文件夹在前文件在后、同类型按字符大小排序。这本质上就是对node.children数组按照一定的规则进行排序,而js的数组用自定义规则进行排序是很方便的。
parent.children.sort(function(a, b) { if (a.children != undefined && b.children == undefined) { return -1; } else if (a.children == undefined && b.children != undefined) { return 1; } else { var x = a.title; var y = b.title; if (x < y) { return -1; } if (x > y) { return 1; } } return 0; });
这一过程其实只需要重复进行文章开头的相同的this.$set()操作即可,难点主要在于:在Tree组件的初始默认状态下,所有节点均为折叠状态,而我们希望每次刷新后感知不到文件树的变化,也就是说,需要在刷新前保存下所有节点的展开状态,并在刷新后还原状态。
在实际编程中,我选择直接保存刷新前的root数组,对刷新后得到的root数组进行遍历,在判断是否为同一文件夹时使用getPath函数得到的路径作为比较依据。
注意,修改展开状态时,也只能使用this.$set()才能生效
if (oriPath == targetPath) { _this.$set(targetData, "expand", true); }
如果想复制一个节点,就涉及到了object的深拷贝问题,如果希望将一个节点及其下嵌套的所有内容全部复制到另一节点的children内,仅仅使用newInfo = copyInfo是不同的,必须使用深拷贝,在查询资料后我选择了如下写法:
newInfo = copyInfo
deepcopy(copyInfo) { var newInfo = []; newInfo = JSON.parse(JSON.stringify(copyInfo)); return newInfo; },
当然,也可以选择让后端先处理复制请求,再直接从后端获取更新后的数据,就不需要再考虑这些问题了。
以上只是几个重要功能的实现思路,具体问题可以查看具体代码:vLab-Fronted/src/components/MySider/MyTree.vue,如果有不理解的地方可以在评论中提出,也欢迎在评论中留言交流~~