目录
一、selectedKeys与onSelect
官方文档
代码演示
onSelect
注意事项
二、expandedKeys与onExpand
官方文档
代码演示
onExpand
注意事项
三、loadedKeys与onLoad和onExpand
官方文档
代码演示
onExpand与onLoad:
注意事项
四、loadData
官方文档
代码演示
loadData:
注意事项
五、树节点局部节点刷新
实现思路
代码演示
六、递归获取与修改数据
获取数据
修改数据
七、总结
最近一周都在忙关于文件管理的东西,从提出这个需求到目前实现为止已经快一周的时间了。从最开始的找插件,然后发现没有插件可以用,再到打算自己手撸一个发现手写树状图过于困难,且因为技术力的原因估计会留下很多坑。所以在经过多方考虑以后觉得还是通过 antd-tree+手动控制的方式去实现一个文件管理页面。
下面我将着重讲解我在使用antd-tree组件时遇到的各种苦难已经官方文档中方法属性的应用。
参数 | 说明 | 类型 | 版本 |
---|---|---|---|
selectedKeys | (受控)设置选中的树节点 | string[] | |
onSelect | 点击树节点触发 | function(selectedKeys, e:{selected: bool, selectedNodes, node, event}) |
形参:
selectedKeys: 代表当前选中的树节点的key值。获取的值的格式为:[ 'key' ]。可以通过selectedKeys[0]取值。
info: 当前选择的树节点的信息。可以通过info.selectedNodes.props.dataRef.children来获取当前节点的子节点。
这里需要注意的是selectedKeys是一个数组类型。有且只有一个当前选中的节点key。一旦点击其他节点,数组内的值就会被替换。
如果树组件设置了selectedKeys这个属性,那么需要在onSelect函数执行时将值赋给该属性。
参数 | 说明 | 类型 | 默认值 | 版本 |
---|---|---|---|---|
expandedKeys | (受控)展开指定的树节点 | string[] | [] | |
onExpand | 展开/收起节点时触发 | function(expandedKeys, {expanded: bool, node}) | - |
expandedKeys: 代表当前打开的树节点的key值。
info: 当前打开的树节点的信息。
这里需要注意的是,expandedKeys也是一个数组的格式,但它与selectedKeys的区别是selectedKeys始终是一个长度为0或1的数组,而expandedKeys则是包含所有被打开的树节点的key值。
参数 | 说明 | 类型 | 默认值 | 版本 |
---|---|---|---|---|
loadedKeys | (受控)已经加载的节点,需要配合 loadData 使用 |
string[] | [] | 3.7.0 |
onExpand | 展开/收起节点时触发 | function(expandedKeys, {expanded: bool, node}) | - | |
onLoad | 节点加载完毕时触发 | function(loadedKeys, {event, node}) | - | 3.7.0 |
形参:
loadedKeys:已经完成加载的树节点的key,是一个数组的数据类型。
这里需要注意的是loadedKeys是一个数组数据类型,且可以存放多个key。一旦被加载过以后,无论怎么点击都不会再触发重新刷新重新加载了。如果想让其刷新,请移步至节点刷新。
参数 | 说明 | 类型 | 默认值 | 版本 |
---|---|---|---|---|
loadData | 异步加载数据 | function(node) | - | |
loadedKeys | (受控)已经加载的节点,需要配合 loadData 使用 |
string[] | [] | 3.7.0 |
treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点(key 在整个树范围内唯一) | array\<{key, title, children, [disabled, selectable]}> | - |
形参:
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
这里需要注意的是 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
一个星期下来还是比较累的,原因是因为组件使用不熟练,且自己的技术力较弱导致的,但好在也顺利完成任务,倒也没有什么大碍。记录一下这一个星期以来遇到的一些问题和实践吧。前端小白一枚,如有错误欢迎指正。
源码:
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 (
{this.renderTreeNodes(item.children)}
);
}
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 = `http://47.104.161.96:6060/v2/ws/download/${title}?path=${dowloadPath}`
this.download(`${path}`,title)
}
download = (downloadPath, title) => {
console.log(title.indexOf("txt") == -1,"title.indexOf() == -1");
if(title.indexOf("txt") == -1){
let aEle = document.querySelector('#down-a-element');
if (!aEle) {
aEle = document.createElement('a');
aEle.setAttribute('target', '_blank')
aEle.setAttribute('download', title);
document.body.appendChild(aEle);
}
aEle.href = downloadPath;
if (document.all) {
aEle.click();
} else {
const e = document.createEvent('MouseEvents');
e.initEvent('click', true, true);
aEle.dispatchEvent(e);
}
}else{
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(title));
element.setAttribute('download', title);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
};
uploadChange = info => {
const { path, selectedKeys } = this.state
if (info && info.file && info.file.status === 'done') {
notification.success({ message: formatMessage({id:'notification.success.upload'})});
if(selectedKeys[0] == undefined){
this.getListFiles()
}else{
this.updateTree()
}
} else if (info && info.file && info.file.status === 'error') {
notification.error({ message: formatMessage({id:'notification.error.update'}) });
}
};
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 { return item.title.indexOf('.') == -1 })
const notFile = showData.filter(item => { return item.title.indexOf('.') != -1 })
const folder = []
isFile.map((item,index) =>{
if(item.isLeaf == true){
folder.unshift(item)
}else{
folder.push(item)
}
})
const showDataArr = [...folder,...notFile]
return (
{formatMessage({id:'componentOverview.body.DirectoryPersistence.example'})}
>}
visible={true}
width={1000}
closable={false}
footer={
<>
>
}
>
{treeDataLoading ? (
}
onLoad={this.onLoad}
loadedKeys={loadedKeys}
>
{this.renderTreeNodes(this.state.treeData)}
{showDataArr && showDataArr.length > 0 ? (
showDataArr.map((item, index) => {
const { title, isLeaf } = item
if (isLeaf) {
return this.folderClick(item)}>
{SVG.getSvg('file', 70)}
{item.title}
} else {
return this.dowloadTitle(item.title)} style={{ background: dowloadArr.includes(item.title) ? "#e6f7ff" : '#fff' }}>
{SVG.getSvg(this.getSvgIcon(title), 70)}
{item.title}
}
})
) : (
)}
) : (
)}
);
}
}
export default Index;