目录

一、selectedKeys与onSelect

官方文档

代码演示

onSelect 

注意事项 

二、expandedKeys与onExpand

官方文档

代码演示

onExpand 

注意事项 

三、loadedKeys与onLoad和onExpand

官方文档

代码演示

 onExpand与onLoad:​

注意事项 

四、loadData

官方文档

代码演示

 loadData: 

注意事项 

五、树节点局部节点刷新

实现思路 

代码演示

六、递归获取与修改数据

获取数据

修改数据

七、总结


        最近一周都在忙关于文件管理的东西,从提出这个需求到目前实现为止已经快一周的时间了。从最开始的找插件,然后发现没有插件可以用,再到打算自己手撸一个发现手写树状图过于困难,且因为技术力的原因估计会留下很多坑。所以在经过多方考虑以后觉得还是通过 antd-tree+手动控制的方式去实现一个文件管理页面。

        下面我将着重讲解我在使用antd-tree组件时遇到的各种苦难已经官方文档中方法属性的应用。

一、selectedKeys与onSelect

官方文档

参数 说明 类型 版本
selectedKeys (受控)设置选中的树节点 string[]
onSelect 点击树节点触发 function(selectedKeys, e:{selected: bool, selectedNodes, node, event})

代码演示

AntD-tree组件使用详析

onSelect 

AntD-tree组件使用详析

形参:

selectedKeys: 代表当前选中的树节点的key值。获取的值的格式为:[ 'key' ]。可以通过selectedKeys[0]取值。

info: 当前选择的树节点的信息。可以通过info.selectedNodes.props.dataRef.children来获取当前节点的子节点。

注意事项 

        这里需要注意的是selectedKeys是一个数组类型。有且只有一个当前选中的节点key。一旦点击其他节点,数组内的值就会被替换。

如果树组件设置了selectedKeys这个属性,那么需要在onSelect函数执行时将值赋给该属性。

二、expandedKeys与onExpand

官方文档

参数 说明 类型 默认值 版本
expandedKeys (受控)展开指定的树节点 string[] []
onExpand 展开/收起节点时触发 function(expandedKeys, {expanded: bool, node}) -

代码演示

AntD-tree组件使用详析

onExpand 

AntD-tree组件使用详析 形参:

expandedKeys: 代表当前打开的树节点的key值。

info: 当前打开的树节点的信息。

注意事项 

        这里需要注意的是,expandedKeys也是一个数组的格式,但它与selectedKeys的区别是selectedKeys始终是一个长度为0或1的数组,而expandedKeys则是包含所有被打开的树节点的key值。

三、loadedKeys与onLoad和onExpand

官方文档

参数 说明 类型 默认值 版本
loadedKeys (受控)已经加载的节点,需要配合 loadData 使用 string[] [] 3.7.0
onExpand 展开/收起节点时触发 function(expandedKeys, {expanded: bool, node}) -
onLoad 节点加载完毕时触发 function(loadedKeys, {event, node}) - 3.7.0

代码演示

AntD-tree组件使用详析

 onExpand与onLoad:AntD-tree组件使用详析

形参:

loadedKeys:已经完成加载的树节点的key,是一个数组的数据类型。

注意事项 

       这里需要注意的是loadedKeys是一个数组数据类型,且可以存放多个key。一旦被加载过以后,无论怎么点击都不会再触发重新刷新重新加载了。如果想让其刷新,请移步至节点刷新。

四、loadData

官方文档

参数 说明 类型 默认值 版本
loadData 异步加载数据 function(node) -
loadedKeys (受控)已经加载的节点,需要配合 loadData 使用 string[] [] 3.7.0
treeData treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点(key 在整个树范围内唯一) array\<{key, title, children, [disabled, selectable]}> -

代码演示

 loadData: 

AntD-tree组件使用详析

形参:

treeNode :要加载的树节点的信息。

注意事项 

        这里需要注意的是如果你的树节点,是通过点击以后再加载子节点,那么对于后端的数据格式返回可能就有一些要求了。比如 title 与 isLeaf 等。当然也可以在loadData中自行设置。

        loadData中代码的大概流程就是先判断 treeNode 是否有 children这个属性,注意是是否有这个属性,如果有这个属性但这个属性为空数组,在执行中也会判定为true从而不会执行更新操作,而是直接return出去。

五、树节点局部节点刷新

实现思路 

        因为tree的机制问题,当key节点加载过以后该节点将不会再被重新加载,因此如果我上传了一个文件,实际上服务器上已经有文件了,但因为节点刷新问题,该节点没有重新刷新,我就看不到对应的节点文件。因此需要进行局部节点刷新。

        满足节点刷新的条件有这几个。

        完成这三点以后再将selectedKeys选取到该节点 ,并将以上数据重新赋值给对应的属性即可完成节点刷新操作。

代码演示

 updateTree = () =>{
        const { selectedKeys , expandedKeys, loadedKeys, treeData } = this.state
        // 获取新的expandedKeys数组,不包含该节点及子节点
        const newExpandedKeys = expandedKeys.filter(item =>{
            return item.indexOf(selectedKeys[0]) == -1
        })
        // 获取新的loadedKeys数组,不包含该节点及子节点
        const newLoadedKeys = loadedKeys.filter(item =>{
            return item.indexOf(selectedKeys[0]) == -1
        })
        const newTreedata = treeData
        this.setState({
            expandedKeys: [...newExpandedKeys,...[`${selectedKeys[0]}`]],
            loadedKeys: [...newLoadedKeys],
            treeData: this.removeShowData(newTreedata),
            selectedKeys: [`${selectedKeys[0]}`],
        })
    }
    // 获取新的treeData数据
    removeShowData = (datas) => {
        const { selectedKeys } = this.state
        const newData = datas;
        function setGrayNode(data){ //遍历树  获取id数组
            for(var i=0;i<data.length;i++){
              if(data[i].key == selectedKeys[0]){// 如果某一个节点是禁用的,它的所有子节点都应该禁用
                delete data[i].children
                continue;
              } else {
                if(data[i].children){// 如果当前节点有子节点,就递归调用一次
                  setGrayNode(data[i].children);
                }
              }
            }
          }
          setGrayNode(newData)
          return newData;
    }

        这里需要注意的是 expandedKeys 虽然删除了当前节点,但要想操作通顺需要再次手动赋值,将该节点打开,并获取新的数据。这样就省去了用户需要再次点击节点的尴尬情况。

六、递归获取与修改数据

        因为这是一个树状图,数据结构也稍微复杂一些,所以获取数据时难免需要通过递归拿取数据。所以需要一个递归函数取实现数据的拿取。

获取数据

    //递归获取Showdata数据
    getShowData = (datas) => {
        const { selectedKeys } = this.state
        datas.map(item => {
            const { key, children } = item
            if (key == selectedKeys[0]) {
                //符合条件
                this.setState({
                    showData: datas
                })
                return
            }
             //如果有孩子,再次调用自己,将孩子传进去。
            if (children && children.length > 0) {
                this.getShowData(children)
            }
        })
    }

修改数据

    // 获取新的treeData数据
    removeShowData = (datas) => {
        const { selectedKeys } = this.state
        const newData = datas;
        function setGrayNode(data){ //遍历树  获取id数组
            for(var i=0;i<data.length;i++){
              if(data[i].key == selectedKeys[0]){// 如果某一个节点是禁用的,它的所有子节点都应该禁用
                delete data[i].children
                continue;
              } else {
                if(data[i].children){// 如果当前节点有子节点,就递归调用一次
                  setGrayNode(data[i].children);
                }
              }
            }
          }
          setGrayNode(newData)
          return newData;
    }

七、总结

        一个星期下来还是比较累的,原因是因为组件使用不熟练,且自己的技术力较弱导致的,但好在也顺利完成任务,倒也没有什么大碍。记录一下这一个星期以来遇到的一些问题和实践吧。前端小白一枚,如有错误欢迎指正。

        源码:

import React, { Component } from 'react';
import { connect } from 'dva';
import {
    Modal,
    Button,
    Tree,
    Row,
    Col,
    Empty,
    Tooltip,
    Icon,
    Upload,
    Popconfirm,
    Select,
    Spin,
    notification
} from 'antd';
import { formatMessage, FormattedMessage } from 'umi-plugin-locale';
import globalUtil from '../../utils/global'
import download from '@/utils/download';
import apiconfig from '../../../config/api.config';
import SVG from './svg';
import styles from './index.less';
const { TreeNode, DirectoryTree } = Tree;
@connect(
    ({ appControl }) => ({
        appDetail: appControl.appDetail,
    })
)
class Index extends Component {
    constructor(props) {
        super(props);
        this.state = {
            treeData: [],
            selectedKeys: [],
            expandedKeys: [],
            pathArr: [],
            keyArr: [],
            dowloadArr: [],
            path: '',
            podsList: [],
            selectDefaultValue: '',
            hostPath: this.props && this.props.hostPath,
            selectLoading: false,
            treeDataLoading: false,
            loadedKeys:[]
        }
    }
    componentDidMount() {
        this.fetchInstanceInfo()
    }
    // 获取podname
    fetchInstanceInfo = () => {
        const { dispatch } = this.props;
        dispatch({
            type: 'appControl/fetchPods',
            payload: {
                team_name: globalUtil.getCurrTeamName(),
                app_alias: this.props.appAlias,
            },
            callback: res => {
                if (res && res.list) {
                    this.setState({
                        podsList: res.list.new_pods,
                        selectDefaultValue: res && res.list && res.list.new_pods[0] && res.list.new_pods[0].pod_name,
                        selectLoading: true
                    }, () => {
                        if (this.props.isType) {
                            this.determineStorageType()
                        }else{
                            this.getListFiles()
                        }
                    })
                }
            }
        });
    };
    // 获取文件类型
    determineStorageType = () => {
        this.props.dispatch({
            type: 'appControl/determineStorageType',
            payload: {
                team_name: globalUtil.getCurrTeamName(),
                group_id: this.props.appAlias,
                region_name: globalUtil.getCurrRegionName(),
                pod_name: this.state.selectDefaultValue,
                namespace: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.namespace,
                volume_path: this.props && his.props.volumePath,
            },
            callback: res => {
                if(res){
                    this.setState({
                        hostPath: res.bean,
                    },()=>{
                        this.getListFiles()
                    })
                }
            }
        });
    };
    // 获取文件列表
    getListFiles = () => {
        this.props.dispatch({
            type: 'appControl/getListFiles',
            payload: {
                team_name: globalUtil.getCurrTeamName(),
                group_id: this.props.appAlias,
                region_name: globalUtil.getCurrRegionName(),
                pod_name: this.state.selectDefaultValue,
                host_path: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method == 'state_multiple' ? `${this.state.hostPath}/${this.state.selectDefaultValue}` : this.state.hostPath,
                extend_method: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method
            },
            callback: res => {
                if (res && res.list) {
                    res.list.map((item, index) => {
                        item.key = index,
                            item.isLeaf = item.is_leaf
                    })
                    this.setState({
                        treeData: res.list,
                        showData: res.list,
                        treeDataLoading: true
                    })
                }
            },
            handleError: res =>{
                if(res){
                    notification.error({ message: formatMessage({id:'componentOverview.body.DirectoryPersistence.error'}) });
                    this.setState({
                        showData: [],
                        treeData: []
                    })
                }
            }
        });
    }
    // 获取文件列表
    updataListFiles = (path) => {
        this.setState({
            treeDataLoading: false
        },()=>{
            this.props.dispatch({
                type: 'appControl/getListFiles',
                payload: {
                    team_name: globalUtil.getCurrTeamName(),
                    group_id: this.props.appAlias,
                    region_name: globalUtil.getCurrRegionName(),
                    pod_name: this.state.selectDefaultValue,
                    host_path: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method == 'state_multiple' ? `${this.state.hostPath}/${this.state.selectDefaultValue}${path}` : `${this.state.hostPath}${path}`,
                    extend_method: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method
                },
                callback: res => {
                    if (res && res.list) {
                        res.list.map((item, index) => {
                            item.key = index,
                            item.isLeaf = item.is_leaf
                        })
                        this.setState({
                            treeData: res.list,
                            showData: res.list,
                            treeDataLoading: true
                        })
                    }
                },
                handleError: res =>{
                    if(res){
                        notification.error({ message: formatMessage({id:'componentOverview.body.DirectoryPersistence.error'}) });
                        this.setState({
                            showData: [],
                            treeData: []
                        })
                    }
                }
            });
        })
    }
    // 加载树图
    onLoadData = treeNode =>
        new Promise(resolve => {
            if (treeNode.props.children) {
                resolve();
                return;
            }
            setTimeout(() => {
                this.props.dispatch({
                    type: 'appControl/getListFiles',
                    payload: {
                        team_name: globalUtil.getCurrTeamName(),
                        group_id: this.props.appAlias,
                        region_name: globalUtil.getCurrRegionName(),
                        pod_name: this.state.selectDefaultValue,
                        host_path: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method == 'state_multiple' ? `${this.state.hostPath}/${this.state.selectDefaultValue}/${this.state.path}` : `${this.state.hostPath}/${this.state.path}`,
                        extend_method: this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method
                    },
                    callback: res => {
                        if (res) {
                            if (res.list && res.list.length == 0) {
                                this.setState({
                                    treeData: [...this.state.treeData],
                                    showData: res.list
                                });
                                treeNode.props.dataRef.children = []
                                resolve();
                            } else {
                                const arr = res.list
                                arr.map((item, index) => {
                                    item.key = `${treeNode.props.eventKey}-${index}`
                                    item.isLeaf = item.is_leaf
                                })
                                treeNode.props.dataRef.children = arr
                                this.setState({
                                    treeData: [...this.state.treeData],
                                    showData: res.list
                                });
                                resolve();
                            }
                        }
                    }
                });
            }, 100)
        });
    // 渲染函数
    renderTreeNodes = data =>
        data && data.map((item, index) => {
            if (item.isLeaf) {
                return (
                    <TreeNode title={item.title} key={item.key} dataRef={item} >
                        {this.renderTreeNodes(item.children)}
                    </TreeNode>
                );
            }
            return null;
        });
    //选择树节点 
    onSelect = (selectedKeys, info) => {
        // 选择为空时直接return
        if (selectedKeys && selectedKeys.length == 0) {
            return null
        }
        if (info) {
            const { selectedNodes } = info
            const { props } = selectedNodes[0]
            const { dataRef } = props
            this.setState({
                selectedKeys: selectedKeys,
                expandedKeys: this.state.expandedKeys.includes(selectedKeys[0]) ? [...this.state.expandedKeys] : [...this.state.expandedKeys, ...selectedKeys],
                showData: dataRef.children || this.state.showData,
                dowloadArr: [],
                pathArr: [],
                path: ''
            }, () => {
                this.getPath()
            })
        } else {
            this.setState({
                selectedKeys: selectedKeys,
                expandedKeys: this.state.expandedKeys.includes(selectedKeys[0]) ? [...this.state.expandedKeys] : [...this.state.expandedKeys, ...selectedKeys],
                dowloadArr: [],
                pathArr: [],
                path: ''
            }, () => {
                this.getPath()
            })
        }
    }
    onLoad = (loadedKeys) =>{
        this.setState({
            loadedKeys: loadedKeys
        })
      }
    // 展开树图
    onExpand = (expandedKeys, info) => {
        let newLoadKeys = this.state.loadedKeys
        if (this.state.expandedKeys.length > expandedKeys.length) {
            //  当是收起的时候,把这个收起的节点从loadedKeys中移除
          newLoadKeys = this.state.loadedKeys.filter((i) => expandedKeys.includes(i))
        }
        this.setState({
            expandedKeys: expandedKeys,
            selectedKeys: [`${info.node.props.dataRef.key}`],
            showData: info.node.props.dataRef.children,
            loadedKeys: newLoadKeys
        }, () => {
            this.getPath()
        })
    };
    // 获取后缀名
    getSvgIcon = (name) => {
        if (name) {
            const str = name.substr(name.lastIndexOf('.') + 1)
            return `${str}`
        }
    }
    // 鼠标点击
    folderClick = (data) => {
        // 判断data数据是否有孩子,如果没有就加载,如果有就
        if (data && data.children && data.children.length > 0) {
            this.setState({
                expandedKeys: [...this.state.expandedKeys, ...[`${data.key}`]],
                selectedKeys: [`${data.key}`],
                showData: data.children,
                dowloadArr: []
            }, () => {
                this.getPath()
            })
        } else {
            this.setState({
                expandedKeys: [...this.state.expandedKeys, ...[`${data.key}`]],
                selectedKeys: [`${data.key}`],
                dowloadArr: []
            }, () => {
                this.getPath()
            })
        }
    }
    //递归获取Showdata数据 
    getShowData = (datas) => {
        const { selectedKeys } = this.state
        datas.map(item => {
            const { key, children } = item
            if (key == selectedKeys[0]) {
                this.setState({
                    showData: datas
                })
            }
            if (children && children.length > 0) {
                this.getShowData(children)
            }
        })
    }
    // 获取key值的path数据
    getPathData = (data) => {
        const { treeData, keyArr } = this.state
        data.map(item => {
            const { title, children } = item
            if (keyArr.indexOf(`${item.key}`) != -1) {
                const arr = this.state.pathArr
                arr.push(title)
                this.setState({
                    pathArr: arr
                })
            }
            if (children && children.length > 0) {
                this.getPathData(children)
            }
        })
    }
    //递归获取path数据 
    getPath = () => {
        const { selectedKeys, treeData, pathArr } = this.state
        if (selectedKeys == []) {
            return
        }
        if (selectedKeys && selectedKeys[0]) {
            const length = selectedKeys[0].length
            const str = selectedKeys[0]
            const arr = str.split("-")
            const keyArr = []
            for (let index = 0; index < arr.length + 1; index++) {
                const newarr = arr.slice(0, index)
                const newstr = newarr.join("-")
                keyArr.push(newstr)
            }
            keyArr.shift();
            this.setState({
                keyArr: keyArr,
                pathArr: []
            }, () => {
                this.getPathData(treeData)
            })
            setTimeout(() => {
                const path = this.state.pathArr.join("/")
                this.setState({
                    path: path
                })
            }, 100)
        }
    }
    // 返回上一级
    goBack = () => {
        const { selectedKeys } = this.state
        // 如果选择为空,则展示所有数据
        if (selectedKeys[0] == undefined) {
            return
        }
        // 如果选择有值且值不大于1
        if ((selectedKeys[0]).indexOf("-") == -1) {
            this.setState({
                selectedKeys: [],
                showData: this.state.treeData,
                dowloadArr: []
            }, () => {
                this.getPath()
            })
            // 如果选择有值且值大于1
        } else {
            this.getShowData(this.state.treeData)
            this.setState({
                selectedKeys: [`${selectedKeys[0].substring(0, (selectedKeys[0]).lastIndexOf("-"))}`],
                dowloadArr: []
            }, () => {
                this.getPath()
            })
        }
    }
    // 下载
    dowloadTitle = (val) => {
        const { dowloadArr } = this.state
        setTimeout(() => {
            if (dowloadArr.includes(val)) {
                const arr = []
                dowloadArr.map(item => {
                    if (item != val) {
                        arr.push(item)
                    }
                })
                this.setState({
                    dowloadArr: [...arr]
                })
            } else {
                const arr = []
                arr.push(val)
                this.setState({
                    dowloadArr: [...this.state.dowloadArr, ...arr]
                })
            }
        }, 10)
    }
    // 下拉框选择
    selectChange = (val) => {
        this.setState({
            selectDefaultValue: val
        },()=>{
            this.getListFiles()
        })
    }
    fileDownload = () => {
        const { dowloadArr } = this.state
        if(dowloadArr.length == 0 ){
            notification.info({ message: formatMessage({id:'componentOverview.body.DirectoryPersistence.download'}) });
        }else{
            dowloadArr.map(item =>{
                this.fileDownloadApi(item)
            })
        }
        setTimeout(()=>{
            this.setState({
                dowloadArr:[]
            })
        },100)
    }
    // 下载接口
    fileDownloadApi = ( title ) =>{
        const dowloadPath = this.state.path ? this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method == 'state_multiple' ? `${this.state.hostPath}/${this.state.selectDefaultValue}/${this.state.path}` : `${this.state.hostPath}/${this.state.path}` : this.props.appDetail && this.props.appDetail.service && this.props.appDetail.service.extend_method == 'state_multiple' ? `${this.state.hostPath}/${this.state.selectDefaultValue}` : `${this.state.hostPath}`;
        const host = apiconfig.baseUrl;
        const url = host.slice(0,host.lastIndexOf(":"))
        // const path = `${url}:6060/v2/ws/download/${title}?path=${dowloadPath}`
        const path = `nShow}>
                                    {showDataArr && showDataArr.length > 0 ? (
                                        showDataArr.map((item, index) => {
                                            const { title, isLeaf } = item
                                            if (isLeaf) {
                                                return <div className={styles.outerLayer} style={{ cursor: "pointer" }} onDoubleClick={() => this.folderClick(item)}>
                                                    <div>
                                                        {SVG.getSvg('file', 70)}
                                                    </div>
                                                    <div>
                                                        <Tooltip placement="top" title={item.title}>
                                                            {item.title}
                                                        </Tooltip>
                                                    </div>
                                                </div>
                                            } else {
                                                return <div className={styles.outerLayer} onClick={() => this.dowloadTitle(item.title)} style={{ background: dowloadArr.includes(item.title) ? "#e6f7ff" : '#fff' }}>
                                                    <div>
                                                        {SVG.getSvg(this.getSvgIcon(title), 70)}
                                                    </div>
                                                    <div>
                                                        <Tooltip placement="top" title={item.title}>
                                                            {item.title}
                                                        </Tooltip>
                                                    </div>
                                                </div>
                                            }
                                        })
                                    ) : (
                                        <Empty className={styles.emptyStyle} />
                                    )}
                                </div>
                            </Col>
                        </Row>
                    ) : (
                        <Spin size="large" style={{
                            width: '100%',
                            height: 400,
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                        }} />
                    )}
                </Modal>
            </div>
        );
    }
}
export default Index;

发表回复