上一篇文章介绍如何实现类似百度网盘
功能,这一篇文章将继续围绕文件/文件夹相关内容,实现文件/文件夹权限管理
功能。
文章标题可能让大家一头雾水,说的是啥意思呢?异步加载?树形结构?文件夹管理?
直接上图来示意:
简单解释:将树形结构的文件夹一层一层加载出资源,展示给用户,同时能够取消和选择文件或文件夹,对其进行权限操作。
1.功能点剖析
看似简单的操作,但实际却麻烦的一逼。
话不多说,先仔细分析其功能点有哪些:
功能一:默认展示出一级目录的文件及文件夹,文件夹里有文件则展示出左侧点击节点,空文件夹或文件则不展示左侧点击节点。
功能二:点击节点可展开该文件夹,且动态加载出该文件夹下的所有文件及文件夹。
功能三:支持层层点击展开子文件夹。
功能四:支持重选文件或文件夹,重选的时候,多选框会出现全选、半选、取消状态,简单解释就是父子节点选中状态会有关联。
功能五:当重选文件或文件夹时,除了父子节点选中状态有改变外,还需动态改变选择的数据。
比如当选择一个文件夹里所有文件的时候,相当于选择该文件夹
。
又比如:当从一个选中的文件夹里,少选一个文件的时候,选中的应该是当前文件夹下除开这个文件的所有文件
。
举例:
现在有1个文件夹,里面有2个文件夹,1个文件。
id为1的文件夹里,有id为2、3的文件夹,id为4的文件;
id为2的文件夹里,有id为5、6的文件;
id为3的文件夹里,有id为7,8的文件。
如果我逐个选择id为4、5、6、7、8的文件,相当于选择id为1的文件夹,需要将5个id(4、5、6、7、8)合成1个id(1);
如果我选择id为1的文件夹后,在它的子文件夹,id为3的文件夹里,取消id为7的文件,需要将1个id(1)分成4个id(4、5、6、8);
功能六:支持动态展示已选择的文件数及所有文件夹下的文件总数。(1/5)
展示的都是文件数,而非文件夹数。
举例:
继续上面的例子,情况一逐个选择文件的时候,展示从1/5…一直到5/5;情况二从选择的文件夹中取消一个文件的时候,展示从5/5变成了4/5。
假如取消child2文件夹,child2文件夹里有2个文件,所以展示从5/5变成3/5。
功能七:当重选文件或文件夹时,依据功能五动态改变选择的数据,展示出已选择文件或文件夹的tag标签信息。一个文件夹名可能会被分解成多个文件名,多个文件名可能会被合成一个文件夹名
。同时还支持删除操作
,点击该tag标签右上角的删除图标,可以影响该树形结构的文件夹,多选框的全选、半选、取消状态。
功能八:支持回显已选择的文件或文件夹,且需同步该树形结构的文件夹多选框的选中状态(全选、半选、取消状态)。
举例:
继续上面的例子,比如我选择(4、5、6、8)的文件,在回显的时候,正确的显示是:
tag标签显示:4文件,2文件夹,8文件这三个标签。因为5、6文件,相当于2文件夹;
一级目录显示:id为1的文件夹为半选中状态,展开后,id为2的文件夹为全选状态(2里面的所有文件5、6都已选择),id为3的文件夹为半选状态(尽管3里面的文件8已选择,但7未选择),id为4、5、6、8的文件为全选状态,id为7的文件为取消状态。
还有更变态的情况:
比如:id为1的文件夹,该文件夹有三层(1 -> 2 -> 3),文件夹id依次为1、2、3,里面有很多文件,且id为3的文件夹里面有1个id为4的文件。
我只选择该文件夹下子文件夹里id为4的文件。
当我回显的时候,默认只能获取一层的文件夹信息
,因为文件夹是异步加载的,接口只允许一层一层加载出资源,如果一次性把资源给出来,性能会有问题。
没问题呀?有问题!
在回显的时候,只能获取文件夹id为1里面的文件和文件夹信息,并不能获取该文件夹的子文件夹的子文件夹id为3的id为4的文件信息,所以选择id为3的文件夹里面id为4的文件时,并不能确定一级目录、二级目录、三级目录的选中状态
,无法实现回显功能。怎么解决这个问题呢?后面会提到。
正确的显示是:
tag标签显示:4文件这一个标签;
一级目录显示:id为1的文件夹为半选状态,id为2的子文件夹为半选状态,id为3的子文件夹为半选状态,id为4的文件为全选状态。
是不是光看功能点就已经够复杂了?
这个业务如果不维护父子节点关联,多选框的全选、半选、取消状态
,不支持回显
,树形结构的数据一次性给完
、没有文件和文件夹来回转换
的话其实挺简单的。但偏偏都必须实现,为了用户体验和性能,那么只能硬着干了。
为了实现这些功能点,于是乎才有了标题说到的异步加载
、树形结构状态关联
、文件夹管理
。
整块涉及的Antd UI组件有:树形控件Tree、目录树形控件DirectoryTree、树形节点TreeNode。
下面对这些功能点,进行一个一个的详细讲解。
2.展示出一级目录
默认展示出一级目录的文件及文件夹,文件夹里有文件则展示出左侧点击节点,空文件夹或文件则不展示左侧点击节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { Tree } from 'antd' const { TreeNode, DirectoryTree } = Tree ... render() { const { tree: { treeData } } = this.props return( <DirectoryTree checkable> { treeData.map(item => ( <TreeNode key={item.key} isLeaf={item.isLeaf} title={item.title} icon={<img src={item.icon} className={styles.icon}/>} dataRef={item} />)) } </DirectoryTree> ) }
|
这些属性的内容可参考 TreeNode 文档:
值得注意的 TreeNode 属性有:
key
属性一定是字符串,并非整数,如果id是整型,需要转成id字符串。
isLeaf
属性可以控制左侧点击节点是否显示
,false则显示左侧节点,true则隐藏左侧节点。
icon
属性是ReactNode
,及react组件,并非字符串。
dataRef
不属于 TreeNode 文档中的属性,为自定义属性,可以在treeNode.props.dataRef
中获取到存储的值。
值得注意的 Tree 属性有:
checkable
属性,显示出节点前面的复选框。
3.点击节点展开文件夹
点击节点可展开该文件夹,且动态加载出该文件夹下的所有文件及文件夹。
要想实现这个功能,首先明确数据结构treeData是怎样的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [{ title: "img_test4", key: "1", icon: "xxx.png", isLeaf: false },{ title: "img_test3", key: "2", icon: "xxx.png", isLeaf: false },{ title: "img_test2", key: "3", icon: "xxx.png", isLeaf: false },{ title: "img_test1", key: "4", icon: "xxx.png", isLeaf: true }]
|
当展开 img_test4 文件夹的时候,相当于在该数组中,找到对应的文件夹对象,然后新增一个children
字段,存放当前文件夹下的子文件夹或文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| [{ title: "img_test4", key: "1", icon: "xxx.png", isLeaf: false, children: [{ title: "child", key: "5", icon: "xxx.png", isLeaf: false },{ title: "child2", key: "6", icon: "xxx.png", isLeaf: false },{ title: "child3", key: "7", icon: "xxx.png", isLeaf: false },{ title: "test_word.docx", key: "8", icon: "xxx.png", isLeaf: true }] },{ title: "img_test3", key: "2", icon: "xxx.png", isLeaf: false },{ title: "img_test2", key: "3", icon: "xxx.png", isLeaf: false },{ title: "img_test1", key: "4", icon: "xxx.png", isLeaf: true }]
|
数据改变后,我们需要使用递归算法,去生成相应的树形DOM结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| render() { const { tree: { treeData } } = this.props return( <DirectoryTree checkable> {{this.renderTreeNodes(treeData)}} </DirectoryTree> ) } ... renderTreeNodes = data => data.map(item => { if (item.children) { return ( <TreeNode key={item.key} isLeaf={item.isLeaf} title={item.title} icon={<img src={item.icon} className={styles.icon}/>}> {this.renderTreeNodes(item.children)} </TreeNode> ); } return <TreeNode key={item.key} isLeaf={item.isLeaf} title={item.title} icon={<img src={item.icon} className={styles.icon}/>} />; });
|
思路解析:判断当前是否有children
字段,有则一定有子文件夹或文件,因此isLeaf一定为flase,有左侧节点。isLeaf默认是false,因此可以去掉定义的isLeaf属性。
子文件夹可能还会有子子文件夹,因此需要继续递归renderTreeNodes方法。
递归的停止条件是判断当前文件夹对象中是否拥有children字段,没有则停止。
4.支持层层点击展开子文件夹
支持层层点击展开子文件夹。
想实现这个功能,除了渲染树形DOM结构时使用递归
外(上一节已经实现),还需要在获取单层数据时使用递归
,组装出数据结构treeData
。
因为我们会不停的展开文件夹,同层展开也好,一层一层展开也好,都需要新增children字段存放单层数据。
比如:同层依次展开img_test4、img_test3、img_test2、img_test1文件夹,
数据结构treeData变化:
1 2 3 4 5 6 7 8 9
| [{},{},{},{}]
[{ children:[...] },{},{},{}]
[{ children:[...] },{ children:[...] },{},{}]
[{ children:[...] },{ children:[...] },{ children:[...] },{}]
[{ children:[...] },{ children:[...]},{ children:[...] },{ children:[...] }]
|
再比如:上图中层层展开img_test4、child、child2、child2_1、child3文件夹,数据结构treeData变化:
1 2 3 4 5 6 7 8 9 10 11
| [{},{},{},{}]
[{children:[{},{},{},{}] },{},{},{}]
[{children:[{ children:[...] },{},{},{}] },{},{},{}]
[{children:[{ children:[...] },{ children:[...] },{},{}]},{},{},{}]
[{children:[{ children:[...] },{ children:[ {children:[...] }, {}, {} ] },{},{}]},{},{},{}]
[{children:[{ children:[...] },{ children:[ {children:[...] }, { children:[...] }, {} ] },{},{}]},{},{},{}]
|
思路分析的差不多了,来看下获取和组装treeData的具体实现:
首先页面上DirectoryTree新增loadData
方法,该方法展开节点时触发,注意这个方法不是普通的function,而是Promise
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| render() { const { tree: { treeData } } = this.props return( <DirectoryTree checkable loadData={this.onLoadData}> {{this.renderTreeNodes(treeData)}} </DirectoryTree> ) } ... componentDidMount() { this.fetchGetFile(0) } ... fetchGetFile = (folder_id) => { const { dispatch, } = this.props dispatch({ type: 'tree/getFolderTreeList', payload: { folder_id, }, }) } ... onLoadData = treeNode => new Promise(resolve => { const { children, eventKey } = treeNode.props if (children) { resolve(); return; } setTimeout(() => { this.fetchGetFile(parseInt(eventKey, 10)) resolve() }, 500); }); ... namespace: 'tree', effects:{ * getFolderTreeList({ payload }, { call, put, select }) { const url = '/partner/folder/get' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { const { children } = response.data let treeData = yield select(state => state.tree.treeData); if (treeData.length === 0) { treeData = dataToTree(children) } else { treeData = mapTree(treeData, payload.folder_id, dataToTree(children)) } yield put({ type: 'save', payload: { treeData, }, }) } else { notification.error({ message: response.description }); } }, } ...
const dataToTree = data => { const treeData = [] if (data.length > 0) { data.map(item => { let tree = {} if (item.file_count === 0 && item.folder_count === 0) { tree.isLeaf = true } else { tree.isLeaf = false } tree.title = item.name tree.key = item.id tree.icon = item.icon_url treeData.push(tree) }) } return treeData } ...
const mapTree = (treeData, id, childTreeData) => { treeData.map(tree => { if (!tree.isLeaf) { if (tree.key === id) { tree.children = childTreeData } else if (tree.children) { mapTree(tree.children, id, childTreeData) } } }) return treeData }
|
判断递归的开始条件是当前目录是否有子文件夹或文件,有则开始递归。
判断递归的停止条件是在treeData数据中,找到点击的目录id对象,找到则停止。
5.父子节点选中状态会有关联
支持重选文件或文件夹,重选的时候,多选框会出现全选、半选、取消状态,简单解释就是父子节点选中状态会有关联。
这个功能实现起来其实不难,在DirectoryTree新增onCheck
方法和checkedKeys
属性,两者配合实现该功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| render() { const { tree: { treeData, checkedKeys } } = this.props return( <DirectoryTree checkable loadData={this.onLoadData} checkedKeys={checkedKeys} onCheck={this.onCheck} > {{this.renderTreeNodes(treeData)}} </DirectoryTree> ) } ... onCheck = (checkeds) => { const { dispatch } = this.props dispatch({ type: 'tree/save', payload: { checkedKeys: checkeds, }, }) }; ... namespace: 'tree', reducers:{ save(state, { payload }) { return { ...state, ...payload, } }, }
|
Antd Tree默认就实现父子节点选中状态有关联的状态,如果想取消父子节点的关联,加上checkStrictly
属性。
6.动态改变选择的数据
当重选文件或文件夹时,除了父子节点选中状态有改变外,还需动态改变选择的数据。
介绍该功能点的时候,可能不理解这句话是什么含义。直接举例看效果:
我们先在onCheck
方法中输出antd返回的选择的id数组checkeds:
1 2 3 4 5 6 7 8 9
| onCheck = (checkeds) => { const { dispatch } = this.props dispatch({ type: 'tree/save', payload: { checkedKeys: checkeds, }, }) };
|
图1是我未展开img_test_1文件夹,全选img_test_1文件夹,打印的结果:
图2是我展开img_test_1文件夹后,全选img_test_1文件夹,打印的结果:
图3是我展开img_test_1文件夹后,再展开child_3文件夹后,全选img_test_1文件夹,打印的结果:
可以发现尽管我们操作的都是同一个文件夹img_test_1,全选后,打印的结果却不尽相同,返回选中的id个数分别是1、5、8。我们需要把5个、8个的数组合成1个。
因为当选择一个文件夹里所有文件的时候,相当于选择该文件夹。因此,我们需要获取全选的父节点id,其下面的子节点id都可以忽略
我们继续图3的操作,取消子文件夹child3的其中一个文件,打印的结果:
明明只取消了一个文件,id怎么从8变成5了?去除了哪3个id呢?
分别去除了全选的img_test_1文件夹父节点id、全选的child_3文件夹节点id、及取消的pdf_8文件id。因此,该逻辑,没毛病。
如何在onCheck
中,将多个id合成一个父id,成为了解决该功能的关键。
这只是一个img_test_1文件夹下的操作,还有其他文件夹下都需要合父id的操作,因此又需要使用递归。
具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| onCheck = (checkeds) => { const { dispatch, tree: { treeData } } = this.props let checkedKeys = [] const mapTree = (trees, keys, list) => trees.map(tree => { const key = tree.key.toString() if (keys.includes(key)) { list.push(key) checkedKeys = list } else if (tree.children) { mapTree(tree.children, keys, list) } }) mapTree(treeData, checkeds, []) dispatch({ type: 'tree/save', payload: { checkedKeys, }, }) };
|
7.动态展示已选择的文件数
支持动态展示已选择的文件数及所有文件夹下的文件总数。
展示的都是文件数,而非文件夹数。
实现这个功能也比较简单,需要后台能返回,文件类型(file 或 folder)和文件数量(file_count)。
在转换成treeData数据时,新增一个count字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const dataToTree = data => { const treeData = [] if (data.length > 0) { data.map(item => { let tree = {} ... tree.count = item.obj_type === 'folder' ? item.file_count : 1 ... treeData.push(tree) }) } return treeData }
|
在onCheck的递归算法中,将选中的文件数量count加起来,则是已选择的总文件数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| render() { const { tree: { checkedCount, total } } = this.props return( <div className={styles.title}> 已选择授权文件({checkedCount || 0}/{total || 0}) </div> ) } ... onCheck = (checkeds) => { const { dispatch, tree: { treeData } } = this.props let checkedKeys = [] let checkedCount = 0 const mapTree = (trees, keys, list) => trees.map(tree => { const key = tree.key.toString() if (keys.includes(key)) { list.push(key) checkedKeys = list checkedCount += tree.count } else if (tree.children) { mapTree(tree.children, keys, list) } }) mapTree(treeData, checkeds, []) dispatch({ type: 'tree/save', payload: { checkedKeys, checkedCount, }, }) };
|
那所有文件夹的总数total呢?
获取第一层文件夹的file_count,即为总数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| namespace: 'tree', effects:{ * getFolderTreeList({ payload }, { call, put, select }) { const url = '/partner/folder/get' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { const { children } = response.data let treeData = yield select(state => state.tree.treeData); if (treeData.length === 0) { treeData = dataToTree(children) yield put({ type: 'save', payload: { total: file_count, }, }) } else { ... } ... } else { notification.error({ message: response.description }); } }, }
|
8.动态展示已选择文件或文件夹的tag标签信息及删除操作
当重选文件或文件夹时,依据动态改变选择的数据,展示出已选择文件或文件夹的tag标签信息。一个文件夹名可能会被分解成多个文件名,多个文件名可能会被合成一个文件夹名。同时还支持删除操作
,点击该tag标签右上角的删除图标,可以影响该树形结构的文件夹,多选框的全选、半选、取消状态。
只要实现核心的在选择的时候,动态要么合并要么分解id的功能后,该功能点就不难实现了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| render() { const { tree: { checkedTags } } = this.props return( <div className={styles.tags}> {checkedTags.map(file => ( <div className={styles.tag_container} key={file.id}> <div className={styles.tag}>{file.name}</div> <div className={styles.close} onClick={() => { this.delChecked(file.id) }}></div> </div> ))} </div> ) } ... onCheck = (checkeds) => { const { dispatch, tree: { treeData } } = this.props let checkedKeys = [] let checkedCount = 0 let checkedTags = [] const mapTree = (trees, keys, list) => trees.map(tree => { const key = tree.key.toString() if (keys.includes(key)) { list.push(key) checkedKeys = list checkedCount += tree.count checkedTags.push({ id: key, name: tree.title, count: tree.count }) } else if (tree.children) { mapTree(tree.children, keys, list) } }) mapTree(treeData, checkeds, []) dispatch({ type: 'tree/save', payload: { checkedKeys, checkedCount, checkedTags, }, }) }; ...
delChecked = (id) => { const { tree: { checkedTags, checkedKeys }, dispatch } = this.props let checkedCount = 0 checkedKeys.splice(checkedKeys.findIndex(item => item === id), 1) checkedTags.splice(checkedTags.findIndex(item => item.id === id), 1) checkedTags.map(checked => { checkedCount += checked.count }) dispatch({ type: 'tree/save', payload: { checkedKeys, checkedTags, checkedCount, }, }) }
|
9.支持回显已选择的文件或文件夹到树形中
支持回显已选择的文件或文件夹,且需同步该树形结构的文件夹多选框的选中状态(全选、半选、取消状态)。
首先明确,回显的三个地方:
1.树形DOM结构
2.标签信息
3.已选择数量
回显标签信息和已选择数量,应该不难,难点在于树形DOM结构的回显。
其次明确,树形DOM结构回显的两个条件:
1.需要已选择的文件夹或文件夹id,有checkedKeys
2.需要完整的树形DOM结构,有treeData
想实现树形DOM结构的回显
功能,需使用checkedKeys
属性,与treeData
数据生成相应的树形DOM结构去作比较,就能实现回显。
但目前难就难在,数据是一层一层给的,treeData渲染出的树形DOM其实是不完整的
。
当时分析出三种方案:
方案一:使用原生DOM直接去操作该Tree,人为去添加选中状态(全选、半选、取消选择)
方案二:递归异步请求,一层一层获取,每个文件所在的父目录都请求一遍
方案三:找到所有文件的父目录,去重后都请求一遍
当然三种方案都试过,方案一会出来各种奇葩问题。理由也很简单,Antd组件自身封装一系列DOM事件及属性,有它自身的逻辑,强加自己的逻辑在DOM上,逻辑会混乱
。方案二递归去实现,会重复请求一个目录很多次,导致出来有很多无效请求
。最终方案用的方案三去实现,找到所有文件的父目录,然后去重,模拟一层一层展开的方式去请求数据。
思路分析:必须渲染出完整的树形DOM结构
,该完整不是说把所有目录所有子目录都请求一遍,不是递归,而是根据选中的文件夹或文件集合,找到它们相应的公共父目录id,和自己的父目录id,这样会减少很多无效请求
。
实现稍微有些复杂,首先后台会返回一个list,比如:
1 2 3 4 5
| grant_list = [ { id:xxx, name:xx, parent_path_list:["1035", "1410", "1416", "1440"] }, { id:xxx, name:xx, parent_path_list:["1035", "1388", "1393", "1396"] }, { id:xxx, name:xx, parent_path_list:["1035", "1363", "1372", "1396"] }, ]
|
parent_path_list表示当前文件夹或文件的父级id集合。
再来看看JS实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| namespace: 'tree', effects:{ * getFolderTreeList({ payload }, { call, put, select }) { const url = '/partner/folder/get' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { const { children } = response.data let treeData = yield select(state => state.tree.treeData); if (treeData.length === 0) { treeData = dataToTree(children) yield put({ type: 'getGrantInfo', payload: { target_id: payload.target_id, }, }) } else { ... } yield put({ type: 'save', payload: { treeData, }, }) } else { notification.error({ message: response.description }); } }, } ...
* getGrantInfo({ payload }, { call, put }) { const url = '/partner/folder/grant-info' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { let grant = response.data let checkedCount = 0 const checkedKeys = [] const checkedTags = [] let needRequestIds = [] grant.grant_list.map(e => { let count = e.obj_type === 'folder' ? e.file_count : 1 checkedCount += count checkedTags.push({ id: e.id.toString(), name: e.name, count }) checkedKeys.push(e.id.toString()) needRequestIds.push(...e.parent_path_list) }) needRequestIds = Array.from(new Set(needRequestIds)) needRequestIds.splice(needRequestIds.findIndex(item => item === payload.pid.toString()), 1) const delay = (timeout) => new Promise((resolve) => { setTimeout(resolve, timeout); }) for (let i = 0; i < needRequestIds.length; i++) { yield call(delay, 50); yield put({ type: 'getFolderTreeList', payload: { partner_id: localStorage.getItem('partnerId'), folder_id: parseInt(needRequestIds[i], 10), target_id: payload.target_id, }, }) } yield call(delay, 200); yield put({ type: 'save', payload: { checkedCount, checkedKeys, checkedTags, }, }) } else { notification.error({ message: response.description }); } },
|
总结一下步骤:
1.筛选出父级目录,(有序的一层一层的目录)
1 2 3 4 5 6 7 8
| ["1035", "1410", "1416", "1440", "1035", "1388", "1393", "1396", "1035", "1363", "1372"]
["1035", "1410", "1416", "1440", "1388", "1393", "1396", "1363", "1372"]
["1410", "1416", "1440", "1388", "1393", "1396", "1363", "1372"]
|
2.同步请求父级目录,将请求数据挨个放入渲染树形结构的treeData
3.赋值treeData成功,等待树形DOM结构渲染好
4.赋值checkedKeys,实现选中状态的渲染;
赋值checkedCount实现已选择数量的渲染;
赋值checkedTags实现标签信息的渲染。
这个方案还不是最佳的解决方案,因为体验不够好
,因为必须等待所有请求完成后的treeData
,等待渲染后(延迟200ms),才能集体出效果。
优化前:
优化后:
优化后方案:
1.筛选出父级目录,(有序的一层一层的目录)
2.赋值checkedCount实现已选择数量的渲染
3.同步请求父级目录,将请求数据挨个放入渲染树形结构的treeData,同时
赋值checkedKeys、checkedTags,实现选中状态和标签信息的实时渲染。
这个方案的体验就比较好了,能实时看到选中状态和标签信息的变化,不需要等待treeData全部赋值成功,才看到渲染效果。
看看JS最终实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| namespace: 'tree', effects:{ * getFolderTreeList({ payload }, { call, put, select }) { const url = '/partner/folder/get' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { const { children } = response.data let treeData = yield select(state => state.tree.treeData); if (treeData.length === 0) { treeData = dataToTree(children) yield put({ type: 'getGrantInfo', payload: { target_id: payload.target_id, children, }, }) } else { const defaultCheckedKeys = yield select(state => state.tree.defaultCheckedKeys); checkedKeys = yield select(state => state.tree.checkedKeys); checkedTags = yield select(state => state.tree.checkedTags); children.map(child => { if (defaultCheckedKeys.some(id => id === child.id)) { let count = child.obj_type === 'folder' ? child.file_count : 1 checkedKeys.push(child.id.toString()) checkedTags.push({ id: child.id.toString(), name: child.name, count }) } }) } yield put({ type: 'save', payload: { treeData, }, }) } else { notification.error({ message: response.description }); } }, } ...
* getGrantInfo({ payload }, { call, put }) { const url = '/partner/folder/grant-info' const response = yield call(sendPostRequest, url, payload) if (response && response.retcode === 'success') { let grant = response.data let checkedCount = 0 const checkedKeys = [] const checkedTags = [] const defaultCheckedKeys = [] let needRequestIds = [] payload.children.map(child => { if (grant.grant_list.some(item => item.id === child.id)) { let count = child.obj_type === 'folder' ? child.file_count : 1 checkedKeys.push(child.id.toString()) checkedTags.push({ id: child.id.toString(), name: child.name, count }) } }) grant.grant_list.map(e => { let count = e.obj_type === 'folder' ? e.file_count : 1 checkedCount += count defaultCheckedKeys.push(e.id) needRequestIds.push(...e.parent_path_list) }) yield put({ type: 'save', payload: { checkedCount, defaultCheckedKeys, checkedKeys, checkedTags, }, }) needRequestIds = Array.from(new Set(needRequestIds)) needRequestIds.splice(needRequestIds.findIndex(item => item === payload.pid.toString()), 1) const delay = (timeout) => new Promise((resolve) => { setTimeout(resolve, timeout); }) for (let i = 0; i < needRequestIds.length; i++) { yield call(delay, 50); yield put({ type: 'getFolderTreeList', payload: { partner_id: localStorage.getItem('partnerId'), folder_id: parseInt(needRequestIds[i], 10), target_id: payload.target_id, }, }) } } else { notification.error({ message: response.description }); } },
|
至此,所有功能的实现就介绍到这了。