【技術博客】使用iview的Tree組件寫一棵文件樹


本次項目的前端部分使用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會為[])

1

得到某一節點的路徑

在該項目下,文件樹與后端的交互接口的參數往往是節點(文件/文件夾)從根目錄開始的查找路徑字符串。

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);
      }
    },
  • 在修改狀態下,可以單擊文本框實現全選文本,也就是可以方便地將光標移動到文本開頭或者末尾,或者直接清除全部內容。
  • 可以通過鍵盤回車保存修改。
1

以上兩點可以通過綁定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動作激活

1 1

拖拽功能與dragstart、dragover、dragend、drop動作相關,注意要設定好draggable屬性值為true才可以進行拖拽

1
{
  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,如果有不理解的地方可以在評論中提出,也歡迎在評論中留言交流~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM