欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

与 ant-design-vue tree 组件自定义图标的相爱相杀

程序员文章站 2022-07-14 16:42:19
...

与 ant-design-vue tree 组件自定义图标的相爱相杀

  1. 最近遇到了在 tree 组件中需要用到自定义 icon 图标的需求

    <!-- 首先,请求回来的数据是多层结构且不知道嵌套几层,所以自己多层 v-for 自然不现实 -->
    <!-- 官方也考虑到这个问题,只要通过 tree-data 属性直接给出数据即可,如下 -->
    
    <a-tree
        draggable
        show-icon
        default-expand-all
        :tree-data="treeNodeData"
        :replaceFields="{
            children: 'children',
            title: 'label',
            key: 'id'
        }"
        @drop="handleDropTreeNode"
        @rightClick="handleRightClickNode"
    >
        <a-icon slot="switchIcon" type="caret-down"> <!-- 替换树结构可展开目录前的图标 -->
    </a-tree>
    
    <!-- 上面的 a-tree 组件属性不清楚的,直接看 ant-design-vue 官方文档即可 -->
    

    给出的需求是:返回数据的节点 类型 是不同的,要根据不同类型加不同图标展示

    这里,我看到 tree 组件的自定义图标是通过 slots 属性或 scopedSlots 属性 (当时也没细细比较二者的区别,后者好像可以在 template 结构里通过 slot-scope 拿到 slot 匹配到的数据源) 来区分匹配的,所以就想到,拿到数据就先根据类型给每个节点加一个插槽识别属性作为 slot 匹配点, 这里使用递归插入的方式处理

    /**
    返回数据格式大致为:
    [
        {
            label: "parentNode",
            id: "parent001",
            type: "Folder",
            subType: "Folder",
            children: [
                {
                    label: "childNode",
                    id: "children001",
                    type: "Point",
                    subType: "Interval",
                    children: [
                        {
                            // ... 更多的层
                        }
                    ]
                },
            ]
        },
        {
            label: "parentNode",
            id: "parent101",
            type: "Folder",
            subType: "Folder",
            children: []
        }
    ]
    */
    // 节点类型是由 type 和 subType 组合决定的
    function recursion(dataArray){
        let res = [];
        dataArray.forEach(item => {
            item.scopedSlots = { icon: this.matchType(item.type, item.subType) }; // 因为是在 vue2 单组件文件中写的,所以这里有个 this
            res.push(item);
            if(!children || !item.children.length) return;
            this.recursion(item.children);
        });
        return res;
    }
    // 匹配类型返回 icon 的值
    matchType(type, subType){
        const typeStr = type + subType; // 用 typeStr 也可以用 switch case 匹配
        if(type === "Folder"){ // type 为 Folder 只有这一种类型(这是与后台协商好类型匹配字段)
            return "Folder";
        }else if(typeStr === "PointInterval"){
            return "pointInterval";
        }
        // ... 其他 6 种类型
        // 写到这儿,突然想到 直接返回 type + subType 的值不就好了!还免得那么多判断!!!
    }
    

    傻了, 傻了 ~~~ 我去改一下项目代码,马上回来!!!

    ============================================================= 认真的分割线

    // 上面的方法改成这个写法
    function recursion(dataArray){
        let res = [];
        dataArray.forEach(item => {
            item.scopedSlots = { icon: item.type + item.subType };
            res.push(item);
            if(!children || !item.children.length) return;
            this.recursion(item.children);
        });
        return res;
    }
    // 只要 slot 匹配上返回节点 type + subType 的值即可
    
  2. 上面的数据请求到,就递归插入一个属性 scopedSlots:{ icon: <对应的数据点类型> } 备用:

    const id = xxx.id // 就是那啥啥请求必要的参数 id
    this.getTreeNodeData({ // 这个方法你自己封装一下 axios,在引入后再封装调用就行
        id
    }).then(res => {
        if(res.success){
            this.treeNodeData = this.recursion(res.result); // 这里递归处理一下
        }
        // other handle code ...
    }).catch( err => {
        // handle error message code ...
    });
    

    用官方图标测试一下:

    <!-- ok 可行 -->
    <a-tree
        draggable
        show-icon
        default-expand-all
        :tree-data="treeNodeData"
        :replaceFields="{
            children: 'children',
            title: 'label',
            key: 'id'
        }"
        @drop="handleDropTreeNode"
        @rightClick="handleRightClickNode"
    >
        <a-icon slot="switchIcon" type="caret-down"/>
        <a-icon slot="FolderFolder" type="dropbox"/>
        <a-icon slot="PointInterval" type="chrome"/>
    </a-tree>
    
  3. 接下来,就是 自定义图标 了!!!

    官方给出的自定义图标的解决方案是:

    // 利用 Icon 组件封装一个可复用的自定义图标。可以通过 component 属性传入一个组件来渲染最终的图标,以满足特定的需求。
    <template>
        <div class="custom-icons-list">
            <heart-icon :style="{ color: 'hotpink' }" />
        </div>
    </template>
    <script>
    const HeartSvg = { // 官网的小红心
        template: `
            <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
                <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
            </svg>
        `,
    };
    
    const HeartIcon = {
        template: `
            <a-icon :component="HeartSvg" />
        `,
        data() {
            return {
                HeartSvg,
            };
        },
    };
    
    export default {
        components: {
            HeartIcon,
        }
    };
    </script>
    

    好嘞,这还没涉及插槽呢,试试能不能用 ~
    因为是基于现有项目的二次开发,ant-design-vue 的版本是 1.5.2, 不知道与版本有关系没
    好家伙,不加载图标,直接报错(大致意思就是模板编译器不可用), 如图:
    与 ant-design-vue tree 组件自定义图标的相爱相杀
    好嘛,那试试 jsxrender 函数

    <template>
        <a-tree
            draggable
            show-icon
            default-expand-all
            :tree-data="treeNodeData"
            :replaceFields="{
                children: 'children',
                title: 'label',
                key: 'id'
            }"
            @drop="handleDropTreeNode"
            @rightClick="handleRightClickNode"
        >
            <a-icon slot="switchIcon" type="caret-down"/>
            <a-icon slot="FolderFolder" type="dropbox"/>
            <!-- 这里倒是能展示小爱心了 -->
            <heart-icon slot="PointInterval" :style="{ color: 'hotpink', fontSize: '16px' }"/>
        </a-tree>
    </template>
    
    <script type="text/jsx"> // 记得加上 type="text/jsx" —— 免得一片红
        const HeartSvg = {
            render() {
                return (
                    <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
                        <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
                    </svg>
                );
            }
        };
    
        const HeartIcon = {
            props: {
                slot: String
            },
            render() {
                return (<a-icon component={this.HeartSvg} slot={this.slot} />) // 注意这里需要要用 this
            },
            data() {
                return {
                    HeartSvg,
                };
            },
        };
    
        export default {
            components: {
                HeartIcon,
            }
        };
    </script>
    

    到这一步,自定义小爱心图标是展示出来了,页面上也没看出有什么问题,但是咱得看看还有啥问题没

    嗯,自找麻烦 ?! 来了,看图:
    与 ant-design-vue tree 组件自定义图标的相爱相杀
    意思是 slot 作为保留字,不能被用作组件的 prop 属性;再来尝试替换 slot props 名为 slotCont 或者任意其他名字, 直接看写法吧(其他省略了哈):

    <heart-icon slotCont="PointInterval"/>
    
    <script>
        const HeartIcon = {
            props: { slotCont: String },
            rentder(){
                return ( <a-icon component={this.HeartSvg} slot={this.slotCont} /> )
            },
            // data 就省略了哈,看上面就好
        }
    </script>
    

    好嘛,小图标又跑了,又不显示了!!! OMG …

    好嘞,再试试 <template> 匹配插槽,不通过 slot 传值了:

    <a-tree
       draggable
       show-icon
       default-expand-all
       :tree-data="treeNodeData"
       :replaceFields="{
           children: 'children',
           title: 'label',
           key: 'id'
       }"
       @drop="handleDropTreeNode"
       @rightClick="handleRightClickNode"
    >
    	<template slot="PointInterval">
    		<heart-icon :style="{color: 'hotpink', fontSize: '16px'}"/>
    	</template>
    </a-tree>
    
    <script type="text/jsx">
    	const HeartSvg = {
    		// ... 小红心的 svg
    	}
    	const HeartIcon = {
            // props: { slotCont: String }, // 重要的这句可以删了
            rentder(){
                return (<a-icon component={this.HeartSvg} />)
            },
           	data(){
    			return {
    				HeartSvg
    			}
    		}
        }
    </script>
    

    好了,达到目标。正常显示,也只匹配 PointInterval 类型的位置,重要的是没有报错哇,O(∩_∩)O哈哈~

  4. 这就可以把 HeartIcon 抽出去作为一个组件(虽然这里本来就是一个组件),作为模块分离出去更清晰,命名为 HeartIcon.jsx (因为 jsx 语法,所以存为 jsx 文件), 如下:

    const HeartSvg = {
    	render() {
            return (
                 <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
                     <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
                 </svg>
             );
         }
     };
    
    export default {
    	 rentder(){
         	return (<a-icon component={this.HeartSvg} />)
         },
         data(){
    		return {
    			HeartSvg
    		}
    	}
    }
    

小结

一路坎坎坷坷,一波三折啊 ~~~

相关标签: 前端 vue.js