本次介绍的内容, 稍稍复杂了一点, 用 vue 实现树形结构. 目前这个属性结构还没有编辑功能, 仅仅是展示. 明天再开一篇文章, 介绍如何增加编辑功能, 标题都想好了. 先看今天的展示效果:
构建树必须用到递归, 使用 slot 这种直观明了的方式, 已经行不通了. 只能通过属性参数, 传递一个树形的数据结构给组件, 传入的数据结构大致是这个样子:
- [
- {
- title:'页面'
- selected:false,
- opened:false,
- isFolder:true,
- children:[
- {
- title:'index.html',
- selected:false,
- opened:false,
- icon:"far fa-file-code",
- },
- {
- title:'product.html',
- selected:false,
- opened:false,
- icon:"far fa-file-code",
- },
- ],
- },
- {
- title:'样式'
- selected:false,
- opened:false,
- isFolder:true,
- children:[
- {
- title:'style.CSS',
- selected:false,
- opened:false,
- icon:"far fa-file-code",
- },
- ],
- },
- ]
每个节点通过 children 嵌套子节点. 需要注意的是, 我们希望这颗树是可以被编辑的, 可以增加, 删除, 编辑其节点, 所以需要数据的双向绑定, 不能通过普通属性 props 传递给组件, 而是通过 v-model 传递.
RXEditor 项目中, 只有两个地方用到了树形结构, 要制作的组件满足这两处需求就可以, 因为不是构建一个通用类库, 就可以相对简单些. 这两处地方一处用于展示并编辑文件目录结构, 一处是节点树, 纯显示, 没有编辑功能. 文件树只有叶子节点可以被选中, 节点树所有节点都可以被选中. 都是单选, 无复选需求.
给这个控件取个大气的名字, 叫 NodeTree 吧, 先看如何使用 NodeTree.
第一处调用:
- <NodeTree v-model="files" :openIcon="'fas fa-folder-open'" :closeIcon="'fas fa-folder'">
- </NodeTree>
第二处调用:
- <NodeTree v-model="nodes" :openIcon="'fas fa-caret-down'" :closeIcon="'fas fa-caret-right'"
- :leafIcon="''" :folderCanbeSelected='true'>
- </NodeTree>
通过 v-model 传递树形数据结构, openIcon 是节点展开时的图标, closeIcion 是节点闭合时的图标, leafIcon 是没有子节点时的图标. 这些图标如果不设置, 会有缺省值, 是文件夹跟文件的样子. 为了增加可扩展性, 树形数据结构也可以放置图标, 数据结构里的图标设置优先级高, 可以覆盖控件的设置. 明白个原理, 想做成什么样子, 看自己的项目需求. folderCanbeSelected 参数是指含有子节点的节点 (比如文件夹) 是否可以被选中.
在 src 目录下新建 tree 目录, 放两个文件:
NodeTree 是树形控件, TreeNode 是树形控件内部的节点, 名字稍微优点绕, 但是是我喜欢的命名方式.
NodeTree.vue 的代码(省略 CSS):
- <template>
- <div class="node-tree">
- <TreeNode v-for = "(node, i) in inputValue"
- :key = "i"
- v-model = "inputValue[i]"
- :openIcon = "openIcon"
- :closeIcon = "closeIcon"
- :leafIcon = "leafIcon"
- :folderCanbeSelected = "folderCanbeSelected"
- @nodeSelected = "nodeSelected"
- ></TreeNode>
- </div>
- </template>
- <script>
- import TreeNode from "./TreeNode.vue"
- export default {
- name: 'FileTree',
- props: {
- value: { default: []},
- openIcon:{ default: 'fas fa-folder-open'},
- closeIcon:{ default: 'fas fa-folder'},
- leafIcon:{ default: 'fas fa-file' },
- folderCanbeSelected:{ default:false }
- },
- components:{
- TreeNode
- },
- data() {
- return {
- };
- },
- computed:{
- inputValue: {
- get:function() {
- return this.value;
- },
- set:function(val) {
- this.$emit('input', val);
- },
- },
- },
- methods: {
- nodeSelected(selectedNode){
- this.inputValue.forEach(child=>{
- this.resetSelected(selectedNode, child)
- })
- this.$emit('nodeSelected', selectedNode)
- },
- // 递归充置选择状态
- resetSelected(selectedNode, node){
- node.selected = (node === selectedNode)
- if(node.children){
- node.children.forEach(child=>{
- this.resetSelected(selectedNode, child)
- })
- }
- }
- },
- }
- </script>
这个代码逻辑很简单, 就是接收外面参数, 循环调用 TreeNode. 要自定义 v-model 的话, 需要用到属性(props)value, 计算属性 inputValue 用于修改 value, 具体原理, 可以参考 VUE 官方文档.
需要特殊注意的是 nodeSelected 事件, 这个事件在子节点产生, 通过冒泡的方式层层往父节点发送, 最后到达 NodeTree 组件. NodeTree 组件再通过 $emit 方法, 分发到外层调用组件.
这次实现的控件是单选, 排他的, 需要递归调用 resetSelected 方法消除其它节点的选中状态.
TreeNode 组件的代码如下(省略 CSS, 如需要, 请到 GitHub 获取):
- <template>
- <div class="tree-node" :class="inputValue.selected ?'selected':''"
- >
- <div class="node-title"
- @click="click"
- @contextmenu.prevent = 'onContextMenu'
- >
- <div class="node-icon" @click="iconClick">
- <i v-show="icon" :class="icon"></i>
- </div>
- {{inputValue.title}}
- </div>
- <div v-show="showChild" class="children-nodes">
- <TreeNode v-for="(child, i) in inputValue.children"
- :openIcon = "openIcon"
- :closeIcon = "closeIcon"
- :leafIcon = "leafIcon"
- :key="i"
- :folderCanbeSelected = "folderCanbeSelected"
- v-model="inputValue.children[i]"
- @nodeSelected = "nodeSelected"
- ></TreeNode>
- </div>
- </div>
- </template>
- <script>
- export default {
- name: 'TreeNode',
- props: {
- value: { default: {}},
- openIcon:{ default: 'fas fa-folder-open'},
- closeIcon:{ default: 'fas fa-folder'},
- leafIcon:{ default: 'fas fa-file' },
- folderCanbeSelected:{default: false},
- },
- data() {
- return {
- }
- },
- computed:{
- inputValue: {
- get:function() {
- return this.value;
- },
- set:function(val) {
- this.$emit('input', val);
- },
- },
- icon(){
- if(this.hasChildren){
- return this.inputValue.opened ? this.openIcon : this.closeIcon
- }
- return this.inputValue.icon !== undefined ? this.inputValue.icon : this.leafIcon
- },
- showChild(){
- return this.hasChildren && this.inputValue.opened
- },
- hasChildren(){
- return this.inputValue.children
- &&this.inputValue.children.length> 0
- },
- },
- methods: {
- click(){
- if((this.hasChildren && this.folderCanbeSelected) || !this.hasChildren){
- this.inputValue.selected = true
- this.$emit('nodeSelected', this.inputValue)
- }
- else {
- this.inputValue.opened = !this.inputValue.opened
- }
- },
- iconClick(event){
- if(this.hasChildren && this.folderCanbeSelected){
- event.stopPropagation()
- this.inputValue.opened = !this.inputValue.opened
- }
- },
- nodeSelected(node){
- this.$emit('nodeSelected', node)
- },
- onContextMenu(event){
- console.log(event)
- }
- },
- }
- </script>
父组件调用时通过 v-mode, 把整个节点的数据传入该控件. 该组件递归调用自身, 从而形成树形结构. 三个状态: opened(展开),closed(闭合),selected(选中)存于 model 数据中, 这样在控件外部, 通过修改 model, 也可以控制节点状态.
本功能介绍完毕, 代码请自行到 GitHub 获取相应历史版本:
https://github.com/vularsoft/studio-ui
来源: http://www.tuicool.com/articles/2aaAZjQ