本次项目的前端部分使用vue框架+iview组件构建,其中IDE的文件树部分使用了iview的Tree组件,但是Tree组件本身的接口功能极其有限,网上的相关资料也不多,在使用时费了一番功夫才摸索清楚使用方法。在这里总结一下使用Tree组件实现各种文件树相关功能的方法和坑点。

参考博客:

理解Tree组件的结构

在官方文档中有这样的说明:

Render 函数的第二个参数,包含三个字段:

  • root < Array >:树的根节点

  • node < Object >:当前节点

  • data < Object >:当前节点的数据

通过合理地使用 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;
}

在实际使用中,并没有发现nodedata的区别,基本可以相互替代。

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,如果有不理解的地方可以在评论中提出,也欢迎在评论中留言交流~~

【技术博客】使用iview的Tree组件写一棵文件树的更多相关文章

  1. 如何写出高质量的技术博客 这边文章出自http://www.jianshu.com/p/ae9ab21a5730 觉得不错直接拿过来了 好东西要大家分享嘛

        如何写出高质量的技术博客?答案是:如果你想,就一定能写出高质量的技术博客.看起来很唯心,但这就是事实.有足够愿力去做一件目标明确,有良好反馈系统的事情往往很简单.就是不停地训练,慢慢地,你自己 ...

  2. 全流程指导Visual Studio Code+Markdown Nice+gitee+PicGo管理自己的技术博客文章

    全流程指导Visual Studio Code+Markdown Nice+gitee+PicGo管理自己的技术博客 1.背景 我挺喜欢写博客,但每一次将博客转移到公众号或者知乎,总是需要调整格式,不 ...

  3. [技术博客]iview组件样式踩坑记录

    [技术博客]iview组件样式踩坑记录 iview官方文档. 在本次项目开发中,前端项目主要使用vue框架+iview组件构建,其中iview组件在使用过程中遇到了许多官方文档中没有明确说明或是很难注 ...

  4. 【转】【技术博客】Spark性能优化指南——高级篇

    http://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651745207&idx=1&sn=3d70d59cede236e ...

  5. 【新版】Android技术博客精华汇总

    [新版]Android技术博客精华汇总(原文链接内持续更新) http://www.apkbus.com/thread-313856-1-1.html Kotlin Kotlin学习资料汇总 http ...

  6. 50家硅谷IT公司技术博客

    分享一下 50 家硅谷优秀 IT 公司技术博客,从中可以了解企业文化,技术特色和设计语言,如果直接列出来很单调,加上点评,算吐槽版吧. 知名大厂   1. Facebook https://www.f ...

  7. 在Ubuntu14.04上安装WordPress4搭建技术博客

    1.安装LAMP环境 1.1 安装Apache2 1.2 安装MySQL5 1.3 安装PHP5 1.4 安装phpMyAdmin 2.初始化数据库 3.下载并配置WordPress 4.配置Apac ...

  8. 创建GitHub技术博客全攻略【转】

    本文转载自:http://blog.csdn.net/renfufei/article/details/37725057/ 说明: 首先,你需要注册一个 github 账号,最好取一个有意义的名字,比 ...

  9. 技术人如何利用 github+Jekyll ,搭建一个独立免费的技术博客

    上次有人留言说,技术博客是程序员的标配,但据我所知绝大部分技术同学到现在仍然没有自己的技术博客.原因有很多,有的是懒的写,有的是怕写不好,还有的是一直想憋个大招,幻想做到完美再发出来,结果一直胎死腹中 ...

随机推荐

  1. 如何优雅地学习计算机编程-C++1

    如何优雅的学习计算机编程--C++ 0.导入 如何优雅地学习计算机编程.我们得首先了解编程是什么?打个比方--写信. 大家都知道写信所用的语言双方都懂,这样的信才做到了信息交流,人和计算机也是如此人和 ...

  2. Androidd Studio 之多行文字跑马灯特效

    •效果展示图 •参考资料 两种方法实现TextView跑马灯效果(字体横向滚动) •出现的问题 新建 Java 文件继承 TextView 时出现问题: •解决方法 不应该继承 $TextView$ ...

  3. VisualGDB_VS2010_开发PHP扩展

    1.新建一个Linux项目

  4. Unity 背包系统的完整实现(基于MVC框架思想)

    前言: 项目源码上传GitHub:Unity-knapsack 背包系统: 背包系统是游戏中非常重要的元素,几乎每一款游戏都有背包系统,我们使用背包系统可以完成装备栏的数据管理,商店物体的数据管理等等 ...

  5. OGG-Oracle 集成模式抽取进程,REGISTER DATABASE都做了什么?

    一.学习目标 有同事问OGG技术问题,OGG软件,在oracle数据库中,集成模式抽取进程REGISTER DATABASE,都做了什么操作? 有什么风险? 并且提到了一个抽取进程注册,在瞬时间并发占 ...

  6. 原生php 实现 thinkphp 数据库链式操作!!!

    https://www.bilibili.com/video/BV1v4411A74Q?p=16&spm_id_from=pageDriver 没事可以看两遍,加深一下基础知识!!!

  7. 基于IMU与磁力计的手势提取手套-原理及其实现

    手势提取依据所采用传感器的不同,可以分为基于视觉,基于惯性传感器,基于FSR,基于EMG传感器的方法.其中基于视觉的方法使用场景有限,且无法获取精确的手指关节角度:基于FSR的方法难以布置传感器且难以 ...

  8. 【责任链模式】责任链模式结合Spring实战Demo

    备注: 责任链与策略模式有很多相似之处,如都是行为型设计模式,都能够处理代码中的if-else逻辑 主要区别在于: 策略模式 封装了算法,通过上下文对象去接受客户端的数据,根据数据类型执行不同的算法 ...

  9. 关于Hexo博客NEXT主题(Gmini)站点图标不显示,显示错误的解决办法

    关于Hexo博客NEXT主题(Gmini)站点图标不显示,显示错误的解决办法   最近闲着没事自己利用Hexo和Github搭了个博客,但是在NEXT(Gmini)主题优化时,出了很多错误,图标不显示 ...

  10. 《机器学习Python实现_10_09_集成学习_bagging_stacking原理及实现》

    介绍 前面对模型的组合主要用了两种方式: (1)一种是平均/投票: (2)另外一种是加权平均/投票: 所以,我们有时就会陷入纠结,是平均的好,还是加权的好,那如果是加权,权重又该如何分配的好?如果我们 ...