本次介绍的内容,稍稍复杂了一点,用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传递。 <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官方文档。 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 = truethis.$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获取相应历史版本: |
|