[sgLazyTree] Custom component: Dynamic lazy loading of el-tree tree node data, to achieve addition, deletion, modification, lazy loading and partial data refresh.

characteristic 

  1. You can customize the primary key and configuration options
  2. Supports predefined node icons: folder folder|normal common style
  3. Multiple prompt texts can be customized
  4. Support dynamic interface addition, deletion and modification of nodes
  5. You can customize the root node Id
  6. You can set the maximum layer depth allowed to be added

sgLazyTree source code

<template>
    <div :class="$options.name" v-loading="rootLoading">
        <div class="tree-header" v-if="!(readonly || readonly === '')">
            <div class="sg-left ">
                <template v-if="uploadData">
                    <el-tooltip popper-class="sg-el-tooltip" :enterable="false" effect="dark" :content="`支持拖拽到树上传文件`"
                        placement="top-start">
                        <el-button type="text" icon="el-icon-upload" size="mini"
                            @click="d => $refs.sgUpload.triggerUploadFile()">
                            批量导入
                        </el-button>
                    </el-tooltip>
                    <el-button type="text" icon="el-icon-download" size="mini" @click="downloadTpl">
                        下载模板
                    </el-button>
                </template>
            </div>
            <div class="sg-right ">
                <el-button type="text" size="mini" @click.stop="addRoot">{
   
   { (data.text || {}).addRootButtonText
                    || `添加根节点` }}<i class="el-icon-circle-plus-outline"></i></el-button>
            </div>
        </div>
        <div class="tree-container">
            <el-tree :load="loadNode" lazy ref="tree" :node-key="mainKey" :props="data.props || { label: 'label' }"
                :icon-class="`${data.iconType}-tree-node`" :indent="data.indent || 25" @current-change="current_change"
                @node-click="nodeClick" highlight-current>
                <el-popover popper-class="tree-el-popover" placement="right" trigger="hover" title="" content=""
                    :disabled="readonly || readonly === ''" slot-scope="{ node, data }">
                    <span class="right">
                        <el-button title="添加" type="text" size="" icon="el-icon-circle-plus-outline"
                            @click.stop="addNode(node, data)" v-if="showAddButton(node)">添加</el-button>
                        <el-button title="删除" type="text" size="" icon="el-icon-remove-outline"
                            @click.stop="remove(node, data)">删除</el-button>
                    </span>
                    <div slot="reference" class="node-label">
                        <label class="left" :title="node.label">
                            {
   
   { node.label }}
                        </label>
                    </div>
                </el-popover>
            </el-tree>

            <!-- 上传组件 -->
            <sgUpload drag ref="sgUpload" :data="uploadData" @success="uploadSuccess" @error="uploadError" hideUploadTray />
        </div>
    </div>
</template>
    
<script>
import sgUpload from "@/vue/components/admin/sgUpload";
export default {
    name: 'sgLazyTree',
    components: {
        sgUpload,
    },
    data() {
        return {
            // 动态树:增删改_________________________________________________________
            rootNode: null,//根节点
            rootResolve: null,//根节点
            focusNodeId: null,//聚焦高亮新添加ID
            rootLoading: false,//根节点列表加载
            mainKey: 'id',//默认主键
            defaultRootId: 'root',//默认根节点ID就是root
            maxAddLevel: null,// 最多允许添加的层级
            // _________________________________________________________
        }
    },
    props: [
        "data",
        "readonly",
        "uploadData",
        /* 例子 uploadData: {
             accept: '.xls,.xlsx',
             actionUrl: `${this.$d.API_ROOT_URL}/core/resource/upload`,
         }, */
    ],

    watch: {
        data: {
            handler(d) {
                d.nodeKey && (this.mainKey = d.nodeKey);//主键
                d.rootId && (this.defaultRootId = d.rootId);//根节点ID
                d.maxAddLevel && (this.maxAddLevel = d.maxAddLevel);// 最多允许添加的层级
            }, deep: true, immediate: true,
        },
    },
    methods: {
        showAddButton(node) {
            if (this.maxAddLevel) {
                return node.level < this.maxAddLevel; // 最多允许添加的层级
            } else return true;
        },
        downloadTpl(d) {
            this.$emit(`downloadTpl`);
        },
        uploadSuccess(d, f) {
            this.$emit(`uploadSuccess`, d, f);
        },
        uploadError(d, f) {
            this.$emit(`uploadError`, d, f);
        },
        // 动态懒加载树:增删改_________________________________________________________
        // 加载根节点
        loadRootNode() {
            this.rootNode.childNodes = [];
            this.loadNode(this.rootNode, this.rootResolve);
        },
        // 加载常规节点
        loadNode(node, resolve) {
            let data = {};
            if (node.level === 0) {
                data = { [this.mainKey]: this.defaultRootId };
                this.rootNode = node;//记录根节点
                this.rootResolve = resolve;//记录根节点
            } else {
                data = node.data;
            }
            this.loadNodeData(data, d => {
                resolve(d);
                this.rootLoading = false;
                this.$nextTick(() => {
                    this.focusNode(this.focusNodeId)
                });
            });
        },
        // 加载节点数据(通过接口向后台获取数据)
        loadNodeData(data, cb) {
            let resolve = d => { cb && cb(d) };
            this.$emit(`loadNode`, data, resolve);
        },
        // 聚焦到某一个节点
        focusNode(id) {
            if (!id) return;
            this.$nextTick(() => {
                this.$refs.tree.setCurrentKey(id);//高亮显示某个节点
                this.$emit(`currentChange`, this.$refs.tree.getCurrentNode());
                this.$nextTick(() => {
                    let dom = document.querySelector(`.el-tree-node.is-current`);
                    dom && dom.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });//缓慢滚动
                });
            });
        },
        // 添加根节点
        addRoot() { this.addNode(this.$refs.tree.root, { [this.mainKey]: this.defaultRootId }) },
        // 添加节点
        addNode(node, data) {
            let isRootNode = data[this.mainKey] === this.defaultRootId;
            isRootNode && (this.rootLoading = true);
            let resolve = d => {
                this.focusNodeId = d[this.mainKey];//记录加载完毕后需要聚焦的节点ID
                if (isRootNode) {
                    this.loadRootNode();//触发根节点加载
                } else {
                    node.loaded = false; //必须要设置loaded=false,否则第二次展开节点不会触发加载数据
                    node.expanded ? node.loadData() : node.expand();//如果已展开→触发加载,否则就先展开→触发加载 
                }
            };
            this.$emit(`addNode`, data, resolve);
        },
        // 删除节点
        remove(node, data) {
            this.$confirm(
                (this.data.text || {}).removeConfirmTip || `此操作将永久删除该节点及其下面的节点,是否继续?`,
                (this.data.text || {}).removeConfirmTitle || `提示`,
                {
                    dangerouslyUseHTMLString: true,
                    confirmButtonText: `确定`,
                    cancelButtonText: `取消`,
                    type: "warning",
                }).then(() => {
                    this.removeNodeData(node, data)
                }).catch(() => { });
        },
        // 删除节点数据(通过接口向后台删除数据)
        removeNodeData(node, data) {
            node.loading = true;//出现加载动画
            let resolve = d => {
                node.loading = false;
                this.$message.success(`删除成功`);
                // 从显示界面删除节点
                let childNodes = node.parent.childNodes;
                childNodes.splice(childNodes.findIndex(d => d.data[this.mainKey] === data[this.mainKey]), 1);
            };
            this.$emit(`removeNode`, data, resolve);
        },
        // 当前选中节点变化时触发的事件
        current_change(d) { this.$emit(`currentChange`, d); },
        //点击节点
        nodeClick(d) { this.focusNodeId = null; this.$emit(`nodeClick`, d); },
    }
};
</script>
    
<style lang="scss" scoped>
@import "~@/css/sg";

.sgLazyTree {
    $treeHeaderHeight: 30px;
    width: 100%;
    height: 100%;
    display: flex;
    flex-wrap: nowrap;
    flex-direction: column;
    white-space: nowrap;
    flex-shrink: 0;
    flex-grow: 1;
    position: relative;

    .tree-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        height: $treeHeaderHeight;

        &>.sg-left {}

        &>.sg-right {}

    }

    .tree-container {
        position: relative;
        overflow: auto;
        box-sizing: border-box;
        height: calc(100% - #{$treeHeaderHeight});
        user-select: none;
        @include scrollbarHover();

        >>>.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
            background-color: #409EFF22; // 高亮当前选中节点背景
        }

        >>>.el-tree {
            * {
                transition: none;
            }


            .el-tree-node__children {
                min-width: max-content; //这样才会出现水平滚动条
            }

            .normal-tree-node,
            .folder-tree-node {
                flex-shrink: 0;
                display: block;
                padding: 0 !important;
                margin: 0;
                width: 20px;
                height: 20px;
                margin-right: 5px;
                background: transparent url("/static/img/fileType/folder/folder.svg") no-repeat center / contain;

                margin-left: 20px;

                &~span:not(.el-icon-loading) {
                    width: 100%;

                    .node-label {
                        height: 40px;
                        display: flex;
                        align-items: center;
                    }
                }

                &.expanded,
                &.is-leaf {
                    flex-shrink: 0;
                    transform: rotate(0deg);
                    background-image: url("/static/img/fileType/folder/folder-open.svg");
                }
            }
        }
    }
}

.tree-el-popover {
    .el-button {
        padding-top: 0;
        padding-bottom: 0;
    }
}
</style>

Example

<template>
    <div style="width: 300px;padding-right: 100px;">
        <sgLazyTree :data="lazyTreeData" @currentChange="currentChange" @loadNode="loadNode"
            @addNode="addNode" @removeNode="removeNode" />
    </div>
</template>
    
<script>
import sgLazyTree from "@/vue/components/admin/sgLazyTree";
export default {
    components: { sgLazyTree, },
    data() {
        return {
            autoId: 0,//自增编号
            lazyTreeData: {
                nodeKey: `ID`,//主键
                props: { label: 'MC' },//配置选项
                iconType: 'folder',//节点图标:folder文件夹|normal普通样式
                text: {
                    addRootButtonText: '添加根目录',//添加根节点按钮文本
                    removeConfirmTitle: '警告!!!',//删除节点提示标题
                    removeConfirmTip: '此操作将永久删除该文件夹及其下面的文件,是否继续?',//删除节点提示内容
                },
            },
        }
    },
    methods: {
        // 获取当前聚焦节点的数据
        currentChange(d) {
            console.log(`currentChange`, d);
        },
        // 加载节点数据
        loadNode(data, resolve) { this.$d.column_queryByPid({ data: { PID: data.ID }, doing: { s: d => resolve(d) } }); },
        // 添加节点
        addNode(data, resolve) {
            this.$d.column_save({
                data: {
                    MC: `新增栏目名称(${++this.autoId})`,
                    LX: 0, 
                    PID: data.ID,//上一级id
                }, doing: { s: d => resolve(d) }
            });
        },
        // 删除节点
        removeNode(data, resolve) {
            this.$d.column_delete({ data: { ID: data.ID }, doing: { s: d => resolve(d) } });
        },
    },

};
</script> 

Guess you like

Origin blog.csdn.net/qq_37860634/article/details/132639389