Code前端首页关于Code前端联系我们

元素洋葱树

terry 2年前 (2023-09-08) 阅读数 159 #Vue

元素-洋葱树

以清晰的层次结构呈现信息,可以展开或折叠。

树木特征

参数 使用说明 类型 可选值 标准
数据 显示数据 数组
节点键 每个树节点作为唯一标识符,整棵树必须是唯一的
显示复选框 或者可以选择节点 布尔值
默认全部展开 是否默认展开所有节点 布尔值 fasle

基本树视图

<template>
    <el-tree 
        :data="data" 
        :props="defaultProps" >
    </el-tree>
</template>
<script>
  export default {
    data() {
      return {
        data: [{
          label: '一级 1',
          children: [{
            label: '二级 1-1',
            children: [{
              label: '三级 1-1-1'
            }]
          }]
        }, {
          label: '一级 2',
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
      };
    },
  };
</script>
 

树形结构

实施思路

  • tree.vue 显示树
  • tree-node.vue 显示子树
  • tree-store.js 树状态管理器
  • node.js定义了树节点的树形式和方法

tree.vue

tree.vue 是树组件的输入

  • 接收props data传递的数据
  • 根据root数据生成树节点
  • 基于root的观树
获取props data传递的数据
props: {
    // 树要展示的数据
    data: {
        type: Array,
    },
    // 树节点的key
    nodeKey: String,
    // 配置项
    props: {
        type: Object,
        default: function() {
            return {
                // 指定子树为节点某个对象的值即"children"对应值表示子节点的数据
                children: 'children',
                // 指定节点标签为节点对象的某个属性的值
                label: 'label',
            }
        }
    },
}
 
根据root数据生成树节点
created() {
    // 给子树判断父组件是否为树
    this.isTree = true;

    // 创建树的store
    this.store = new TreeStore({
        key: this.nodeKey,
        data: this.data,
        props: this.props,
    });

    // 从树根开始
    this.root = this.store.root;
},
 

root观树

<template>
    <div
        class="y-tree">
        <!-- 子树如何渲染?循环生成?多层嵌套? -->
        <y-tree-node
            v-for="(child) in root.childNodes"
            :node="child"
            :show-checkbox="showCheckbox"
            :key="getNodeKey(child)"
        >
        </y-tree-node>
    </div>
</template>
 

tree-node.vue

tree-node.vue 显示子树

如何渲染子树的子树...在循环中显示嵌套渲染子树?我最初的想法是循环嵌套越往下走;基本上循环嵌套就是一个环,下去之后还要回去

  • 显示该节点显示的内容
  • 显示该节点的子树
显示子树
<template>
    <div
        class="y-tree-node"
        :aria-expanded="expanded"
        @click.stop="handleClick"
    >
        <!-- 1.渲染树节点
            节点内容的展示,el-tree节点主要分四部分(展开图标展示,多选框,加载中图标,节点内容展示)
            动态计算偏移量:(node.level - 1) * treeC.indent + 'px', 形成阶梯式子树效果
        -->
        <div
            class="y-tree-node__content"
            :style="{'padding-left': (node.level - 1) * treeC.indent + 'px'}"
        >
            <!-- 多选框 
                @click.native.stop 阻止事件冒泡
                事件修饰符-官方文档:https://cn.vuejs.org/v2/guide/events.html
            -->
            <y-checkbox
                v-if="showCheckbox"
                v-model="node.checked"
                @click.native.stop
            >
            </y-checkbox>
            <!-- 内容 -->
            <node-content :node="node"></node-content>
        </div>
        <!-- 2.渲染该节点的子树
            子树如何渲染?
            组件YCollapseTransition是一个函数式组件
            官方文档:https://cn.vuejs.org/v2/guide/render-function.html
            这里为什么要用函数式组件(无状态、无实例)来包装树节点组件从而实现子树的渲染?为什么不直接使用? -- 子树的展开和收缩效果
         -->
         <y-collapse-transition>
             <!-- v-if="node.expanded && node.childNodes.length"
                v-if: 动态的控制DOM元素的添加和删除
                v-show: 同css的display来控制元素的显示和隐藏
              -->
            <div
                v-if="childNodeRendered"
                v-show="expanded"
                class="y-tree-node__children"
                :aria-expanded="expanded"
            >
                <y-tree-node
                    v-for="(child) in node.childNodes"
                    :key="getNodeKey(child)"
                    :node="child"
                    :show-checkbox="showCheckbox"
                >
                </y-tree-node>
            </div>
         </y-collapse-transition>
    </div>
</template>
 

tree-store.js

树的树存储状态管理器,生成树节点集

import Node from './node';

export default class TreeStore {
    constructor(options) {

        // 赋值初始化:options是对象,遍历使用for...in...
        for(let option in options) {
            if(options.hasOwnProperty(option)) {
                this[option] = options[option];
            }
        }
        
        /**
         * 实例化根节点Node
         * 根节点实例化
         * 由根节点开始生成树
         * 根节点->根节点的childNodes->...
         */
        this.root = new Node({
            data: this.data,
            store: this,
        });
    }
} 
 

node.js

node.js 每个节点拥有的树节点属性和方法,保证节点独立性

import objectAssign from '../../../../src/utils/merge';
import {
markNodeData,
} from './utils';
/**
* getPropertyFromData(this, 'children')
* node.store为tree-store中的this
* node.store.children: 函数 | 字符串 | undefined
* 从node.data中获取prop对应的值
* 
* store.props 是树的配置项
* 
* @param {*} node 
* @param {*} prop 
*/
const getPropertyFromData = function(node, prop) {
const props = node.store.props;
const data = node.data || {};
const config = props && props[prop];
// console.log('888', props, config, data[config]);
if(typeof config === 'function') {
return config(data, node);
} else if (typeof config === 'string') {
return data[config];
} else if (typeof config === 'undefined') {
const dataProp = data[prop];
// console.log('children', dataProp)
return dataProp === undefined ? '' : dataProp;
}
}
// 树节点的id
let nodeIdSeed = 0;
export default class Node {
constructor(options) {
this.id = nodeIdSeed++;
// 节点data
this.data = null;
// 是否选中,默认false:取消选中(true:选中)
this.checked = false;
// 半选中,默认false
this.indeterminate = false;
// 父亲节点
this.parent = null;
// 是否展开
this.expanded = false;
// 是否是当前节点
this.isCurrent = false;
// 赋初值
for(let option in options) {
if(options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
// internal
// 该节点的层级,默认为0
this.level = 0;
// 该节点的子节点
this.childNodes = [];
// 计算层级 根节点层级为0
if(this.parent) {
this.level = this.parent.level + 1;
}
const store = this.store;
if(!store) {
throw new Error('[Node]store is required!');
}
// 构建子树
this.setData(this.data);
// 设置节点的展开属性
// console.log('store', this.store.defaultExpandAll);
if(store.defaultExpandAll) {
this.expanded = true;
}
// 节点注册,为什么会在tree-store中呢?为什么要注册?
// store.registerNode(this);
// console.log('Node', this, options);
}
/**
* 通过 node.label 调用(即执行get方法)
*/
get label() {
return getPropertyFromData(this, 'label');
}
/**
* A instanceof B:A是否是B的实例
* 设置该节点的data和childNodes
* 根节点下的data是一个数组,其子节点便是根据此生成的
* @param {*} data 
*/
setData(data) {
// console.log('setData', Array.isArray(data), data instanceof Array);
// 如果data不是数组即非根节点,则需要给节点标记id
if(!Array.isArray(data)) {
markNodeData(this, data);
}
this.data = data;
this.childNodes = [];
let children;
// 如果该节点的层级为0,且该data为数组类型
// 根节点下的data是一个数组,其子节点便是根据此生成的
if(this.level === 0 && this.data instanceof Array) {
children = this.data;
} else {
// 非根节点,看其children字段是否还存在
children = getPropertyFromData(this, 'children') || [];
}
// 子节点的生成
for(let i = 0, j = children.length; i < j; i++) {
this.insertChild({data: children[i]});
}
// console.log('ndoe', this);
}
/**
* 插入子节点childNodes
* @param {*} child 
* @param {*} index 
*/
insertChild(child, index) {
// console.log('insertChild', child, child instanceof Node);
// 如果child不是Node的实例对象
if(!(child instanceof Node)) {
// 将后面的对象值添加到child
objectAssign(child, {
parent: this,
store: this.store,
});
// 创建child节点
child = new Node(child);
// console.log('chi', child);
}
child.level = this.level + 1;
// console.log('ch', child, index);
/**
* typeof index !== 'undefined'
* index !== undefined
* 
* 将child插入到childNodes
*/
if(typeof index === 'undefined' || index < 0) {
// console.log(typeof index !== 'undefined')
this.childNodes.push(child);
} else {
// console.log(index, index === undefined)
this.childNodes.splice(index, 0, child)
}
}
/**
* 子树收缩
* 设置展开属性
* node.expanded = false
*/
collapse() {
this.expanded = false;
// console.log('collapse', this, this.expanded);
}
/**
* 展开子树
* 设置节点的展开属性
* node.expanded = true
* 
* 注意:树上的每个节点都具有展开和伸缩子树的方法,而不是将这两个方法共享
* 保证了树节点的独立性质
*/
expand() {
// console.log('展开子树', this);
this.expanded = true;
}
}

总结

实现思路

  • tree.vue 显示树
  • tree-node.vue 显示子树
  • tree-store.js 树状态管理器
  • node.js 定义了树节点的属性和方法

学习知识

  • Vue功能组件
  • Vue组件的递归调用

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门