react 使用antd的TreeSelect树选择组件实现多个树选择循环
需求说明,一个帐号角色可以设置管理多个项目的菜单权限
且菜单接口每次只能查询特定项目的菜单数据【无法查全部】
开发思路:
1,获取项目接口数组,得到项目数据
2,循环项目数据,以此为参数递归查询菜单数据【递归查询是为保证循环时数据异步请求顺序 不稳定】
3,将菜单数组组装成一个二维数组,以待循环树选择组件作展示 数据使用
4,循环树选择组件,实现树选择菜单功能
5,读取某条用户信息的菜单权限,将返回字符串菜单编码处理成与菜单数据相同的二维数组
6,奖该用户信息的菜单权限数组加载到循环树选择组件作默认选中
开发难点:
1,菜单编号要在指定数组中进行增删改,就需要对其分别 打上标签,这里是额外创建一个numListArry对象,打上对应项目标签顺序下标,以此匹配菜单数据二维数组的下标【因递归查询 ,所以次序固定】
2,理解树选择的点击事件返回的各种参数,将返回的数据 实时更新到该条用户信息的菜单权限数组中,实现树状选择功能
3,各种基础算法都用到不少,如字符串去重,数组求交集,递归,查找等
用户角色信息【表格数据】
项目数据:
菜单数据
numListArry对象,对应角标关系准确匹配数组数据
列表页面代码
1 import React from 'react'; 2 import {Table,Form,Button,Popconfirm } from 'antd'; 3 import LoadingMixin from '../../../../../libs/loading.common.mixin'; 4 import RequestMixin from '../../../../../libs/request.mixin'; 5 import NotificationMixin from '../../../../../libs/notification.mixin'; 6 import ModalWrapper from '../../../../../libs/modalwrapper'; 7 import Helper from '../../../../../libs/helper'; 8 import './index.css'; 9 import AddOrUpdateModal from './addorupdatemodal'; 10 import LocalService from "../../../../../services/local.services"; 11 12 let prolist = LocalService.getUserInfo() && JSON.parse(LocalService.getUserInfo()) || [] ; 13 const createForm = Form.create; 14 let user = React.createClass({ 15 mixins: [LoadingMixin,NotificationMixin,RequestMixin], 16 getInitialState(){ 17 return { 18 data: [], 19 menuList: [], //所有菜单信息集合 20 menuArry:[], 21 fetchCategoryListArry:[], 22 numListArry:{} //菜单ID匹配下标对象 23 } 24 25 }, 26 componentWillMount() { 27 this.fetch(); 28 if(prolist.prolist){ 29 this.fetchCategoryList(prolist.id,prolist.prolist,0); 30 } 31 32 33 }, 34 fetch() { 35 36 this.get({ //查询用户角色信息 【数据用以在表格中展示】 37 url: "Api/lists/module/role/key/dac509bd9***********a6f1af4bac", 38 param: {}, 39 noLoading: true 40 }).then(result=> { 41 this.setState({data: result.result || []}); 42 this.fetchCategoryListArry() 43 }); 44 }, 45 fetchCategoryListArry() { //查询项目信息 46 var getSessionId = LocalService.getSessionId() 47 this.get({ 48 url: "Api/lists/module/menu/key/dac509bd90a**************1af4bac", 49 param: { 50 sessionid:getSessionId || '' 51 }, 52 noLoading: true 53 }).then(result=> { 54 this.setState({fetchCategoryListArry: result.result || []}); 55 }); 56 }, 57 fetchCategoryList(userid,thisprolist,numIndex) { //查询菜单 58 var proid = thisprolist[numIndex]; 59 var getSessionId = LocalService.getSessionId() 60 this.get({ 61 url: "Api/search/module/menu/key/dac509bd90***************6f1af4bac", 62 param: { 63 userid: userid, 64 proid:proid, 65 sessionid:getSessionId || '' 66 }, 67 noLoading: true 68 }).then(result=> { 69 let menuList = result.result || []; 70 let treeData = []; 71 let menuArry = this.state.menuArry; 72 var numListArry=this.state.numListArry; 73 74 menuList && menuList.map(item => { 75 let treeItem = { 76 id: item.id, 77 label: item.title, 78 value: item.id, 79 key: item.id, 80 hasChildren: item.hasChildren, 81 proid:proid, //取出项目ID存入每条信息中,方便在树选择中显示为哪个项目的提示【placeholder】 82 numIndex:numIndex 83 }; 84 numListArry[item.id]=numIndex; 85 if (item.hasChildren) { //处理接口数据 86 let children = []; 87 item.children && item.children.map(menu => { 88 children.push({ 89 id: menu.id, 90 fid: item.id, 91 label: menu.title, 92 value: menu.id, 93 key: menu.id, 94 proid:proid, 95 numIndex:numIndex 96 }); 97 98 numListArry[menu.id]=numIndex; 99 }); 100 treeItem.children = children; 101 } 102 treeData.push(treeItem); 103 }); 104 menuArry.push(treeData) 105 106 var numArry = [] 107 this.setState({menuList: menuArry,numListArry:numListArry}); //将全部菜单数据和处理好的菜单ID下标对象存入各自全局变量 108 109 numIndex++; 110 if(numIndex<thisprolist.length){ 111 this.fetchCategoryList(userid,thisprolist,numIndex); //递归查询 112 } 113 }); 114 }, 115 deleteRole(parms){ //删除角色信息接口 116 let that = this; 117 if (!parms) return; 118 that.post({ 119 url: "Api/batchDelete/module/role/key/dac509********4bac", 120 param: {ids:parms.id}, 121 noLoading: true 122 }).then(result=> { 123 if (result.result) { 124 that.success("删除成功"); 125 that.fetch(); 126 } 127 }); 128 }, 129 addOrUpdate(modal,e) { 130 e && e.preventDefault() ; 131 e && e.stopPropagation(); 132 new ModalWrapper(AddOrUpdateModal, "addOrUpdateModal", () => { 133 this.fetch(); 134 }, null, { 135 menuList:this.state.menuList, //打开弹窗时,将全部菜单数据传入子组件 136 numListArry:this.state.numListArry, //菜单ID下标对象,同上 137 title: modal && modal.id ? '编辑角色' : '新增角色', 138 item: modal && modal.id ? Helper.copyObject(modal) : {}, //本条角色信息的数据,包含其已有的菜单数据【回显】 139 isEdit: modal && modal.id ? true:false 140 }).show(); 141 }, 142 render(){ 143 let statusObj = { 144 0: "有效", 145 1: "无效" 146 }; 147 148 let columns = [ 149 { title: '编号',dataIndex: 'id',key: 'id', width: '5%'}, 150 { title: '角色名称',dataIndex: 'role_name',key: 'role_name', width: '10%'}, 151 { title: '权限', dataIndex: 'permission',key: 'permission', width: '35%', 152 render: (text, record) => { 153 let menuList = []; 154 let permission = record.permission.split(',') 155 permission && permission.map( item =>{ 156 this.state.fetchCategoryListArry && this.state.fetchCategoryListArry.map( first =>{ 157 if(item == first.id){ 158 menuList.push(first.title); 159 } 160 }) 161 }) 162 163 if( !menuList==null || !menuList.length==0){ 164 return ( 165 menuList.join(',')+'...' 166 ) 167 } 168 169 } 170 }, 171 { title: '创建时间', dataIndex: 'create_time',key: 'create_time', width: '15%',}, 172 { title: '更新时间', dataIndex: 'update_time',key: 'update_time', width: '15%', }, 173 { title: '状态', dataIndex: 'is_del',key: 'is_del', width: '7%', 174 render: (text, record) => { 175 return ( 176 statusObj[record["is_del"]] 177 ) 178 } 179 }, 180 181 { title: '操作', key: '#', width: '23%', 182 render: (text, record) => { 183 return ( 184 <div> 185 <Button type="primary" style={{ marginRight: 8 }} onClick={this.addOrUpdate.bind(this,record)}>修改</Button> 186 <Popconfirm title="确定删除角色?" onConfirm={this.deleteRole.bind(this,record)} okText="确定" cancelText="取消"> 187 <Button type="primary">删除</Button> 188 </Popconfirm> 189 </div> 190 191 ) 192 } 193 } 194 ]; 195 return ( 196 <div className="role"> 197 <div className="title"> 198 <h2>角色管理</h2> 199 200 <Button type="primary" onClick={this.addOrUpdate.bind(this,'')}>添加角色</Button> 201 </div> 202 <div className="list-role"> 203 <Table columns={columns} 204 dataSource={this.state.data} 205 pagination={false} 206 scroll={{ y: 600 }} 207 rowKey={(record) => record.id} 208 > 209 </Table> 210 </div> 211 </div> 212 ) 213 } 214 }); 215 user = createForm()(user); 216 export default user;
弹窗组件:
1 import React from "react"; 2 import {Modal, Form, Input, Select, TreeSelect} from 'antd'; 3 import LoadingMixin from '../../../../../libs/loading.common.mixin'; 4 import RequestMixin from '../../../../../libs/request.mixin'; 5 import NotificationMixin from '../../../../../libs/notification.mixin'; 6 import LocalService from "../../../../../services/local.services"; 7 const FormItem = Form.Item; 8 const createForm = Form.create; 9 const Option = Select.Option; 10 const SHOW_PARENT = TreeSelect.SHOW_ALL; 11 let prolist = LocalService.getUserInfo() && JSON.parse(LocalService.getUserInfo()) || [] ; 12 13 let addOrUpdateModal = React.createClass({ 14 mixins: [LoadingMixin, NotificationMixin, RequestMixin], 15 propTypes: { 16 onManualClose: React.PropTypes.func, 17 onOk: React.PropTypes.func, 18 onCancel: React.PropTypes.func, 19 title: React.PropTypes.string, 20 item: React.PropTypes.object, 21 menuList: React.PropTypes.array, 22 isEdit: React.PropTypes.bool, 23 numListArry:React.PropTypes.object, 24 }, 25 getInitialState() { 26 return { 27 item: this.props.item || {}, //用户信息 28 menuList: this.props.menuList || [], //权限集合 29 numListArry: this.props.numListArry || [], 30 permissions: [], 31 oldPermissions: [], 32 headNav:[], 33 permissionsArry:[], 34 newPermissionsArry:[], 35 headNavsearchPlaceholder:[] 36 }; 37 }, 38 componentWillMount(){ 39 40 if (this.props.isEdit) { 41 let item = this.state.item || {}; //角色已有权限数据作去重去空转换成数组 42 let permissions = item.permission.split(","); 43 let menuList = this.state.menuList; 44 45 var arry = permissions.filter(function(element,index,self){ //去重去空 46 return self.indexOf(element) === index; 47 }); 48 for(var i = 0;i<arry.length;i++){ 49 if(arry[i]==''||arry[i]==null||typeof(arry[i])==undefined){ 50 arry.splice(i,1); 51 i=i-1; 52 } 53 } 54 permissions = arry; 55 56 this.setState({permissionsArry: permissions}); 57 } 58 this.getHeaderMenu() 59 60 }, 61 getHeaderMenu(){ //顶部菜单 62 this.get({ 63 url: "Api/lists/module/project/key/dac509bd90a82719a3569291e12c24a6f1af4bac", 64 param: { 65 } 66 }).then(result => { 67 this.setState({headNav: result.result || []}); 68 var permissions = [] 69 var itemList =[] 70 var itemStr = this.state.permissionsArry 71 var menuList = this.state.menuList; 72 var menuListArry =[] 73 menuList && menuList.map((item)=>{ //全部菜单数组转换成只含ID的数组【待与传来的角色已有权限数据作交集处理】 74 var newPermissions =[] 75 item && item.map((pros)=>{ 76 if (pros.children){ 77 newPermissions.push(pros.id); 78 pros.children.map( (list) =>{ 79 newPermissions.push(list.id); 80 }) 81 }else{ 82 newPermissions.push(pros.id); 83 } 84 }) 85 86 Array.intersect = function(arr1, arr2) { 87 if(Object.prototype.toString.call(arr1) === "[object Array]" && Object.prototype.toString.call(arr2) === "[object Array]") { 88 return arr1.filter(function(v){ 89 return arr2.indexOf(v)!==-1 90 }) 91 } 92 } 93 var mergeArry = Array.intersect(itemStr, newPermissions); // 此处求交集原因:因角色所含菜单数据是一个包含菜单ID的无绪字符串,转成数据也无法直接在树选择组件中循环回显 94 menuListArry.push(mergeArry) //所以要先将所有菜单数据的二维数组处理成一个只含ID的二维数组,将其每条子数组与角色所含菜单数据进行交集匹配, 95 }) //处理完的二维数组就可以用在树选择组件中循环回选了 96 97 this.setState({permissions: menuListArry}); 98 99 var headNavsearchPlaceholder = [] //权限列表提示 100 this.state.headNav && this.state.headNav.map((item)=>{ 101 headNavsearchPlaceholder[item.id] = item.title 102 }) 103 this.setState({headNavsearchPlaceholder:headNavsearchPlaceholder}); 104 }); 105 }, 106 107 onChange(value,label,extra){ 108 let newPermissions = []; 109 let proid ='' 110 let permissions = this.state.permissions; 111 var numListArry = this.state.numListArry; 112 var numIndex = numListArry[extra.triggerValue]; 113 114 if(numIndex != null){ //判断 点击数据是否有菜单ID,有的话按照之前numListArry对象中匹配该菜单ID所属项目下标,【对应递归查询的二维数组下标】 115 permissions[numIndex] = value; 116 } 117 this.setState({permissions:permissions ,oldPermissions:permissions}); 118 }, 119 postSubmit(url, param) { 120 this.post({ 121 url: url, 122 param: param, 123 noLoading: true 124 }).then(result => { 125 if (result && result.result) { 126 this.success(!this.props.isEdit ? '新增成功' : '修改成功'); 127 this.props.onManualClose && this.props.onManualClose(); 128 } 129 }); 130 }, 131 handleSubmit() { 132 this.props.form.validateFieldsAndScroll((errors, values) => { 133 if (!errors) { 134 var param = values; 135 param.permission = this.state.permissions.join(','); //循环后的树选择返回的数据也是个只含菜单ID二维数组,接口只能入传字符串,所以进行数据处理 136 if (!this.props.isEdit) { //判断是新增还是修改 137 this.postSubmit("Api/add/module/role/key/dac509bd90******1af4bac", param); 138 } 139 else { 140 param.id = this.state.item && this.state.item.id; 141 this.postSubmit("Api/edit/module/role/key/dac509bd90*****af4bac", param); 142 } 143 } 144 }); 145 }, 146 hideModal() { 147 this.props.onCancel && this.props.onCancel(); 148 }, 149 render() { 150 const {getFieldDecorator} = this.props.form; 151 const formItemLayout = { 152 labelCol: {span: 6}, 153 wrapperCol: {span: 10}, 154 }; 155 156 const treeSelectArry = [] 157 this.state.menuList && this.state.menuList.map((item,index)=>{ //循环树选择数据 158 const tProps = { 159 value: this.state.permissions[index], 160 treeData: item, 161 onChange: this.onChange, 162 multiple: true, 163 treeCheckable: true, 164 dropdownStyle: {maxHeight:"350px"}, 165 showCheckedStrategy: SHOW_PARENT, 166 searchPlaceholder: '请选择'+this.state.headNavsearchPlaceholder[item[0].proid]+'权限列表', 167 style: { 168 width: 300, 169 }, 170 }; 171 treeSelectArry.push(tProps) 172 }) 173 174 return ( 175 <Modal title={this.props.title || '新增'} visible={true} width="550px" onOk={this.handleSubmit} 176 onCancel={this.hideModal} maskClosable={false}> 177 <Form layout="horizontal" autoComplete="off"> 178 <FormItem {...formItemLayout} label="角色名称" > 179 {getFieldDecorator('role_name', { 180 initialValue: this.state.item && this.state.item.role_name || '', 181 rules: [{ required: true, message: '请输入角色名称!' }], 182 })( 183 <Input placeholder="请输入角色名称"/> 184 )} 185 </FormItem> 186 187 <FormItem {...formItemLayout} label="权限列表"> 188 { 189 this.state.menuList && this.state.menuList.map((item,index)=>{ //循环树选择DOM 190 191 return ( 192 <div style={{ marginBottom : "10px" }} key={index}> 193 <TreeSelect {...treeSelectArry[index]} /> 194 </div> 195 ) 196 }) 197 } 198 {/*<TreeSelect {...tProps} />*/} 199 </FormItem> 200 201 202 <FormItem{...formItemLayout} label="状态"> 203 {getFieldDecorator('is_del',{ 204 initialValue: this.state.is_del && this.state.is_del || '0' 205 })( 206 <Select style={{ width: 100 }}> 207 <Option value="1">无效</Option> 208 <Option value="0">有效</Option> 209 </Select> 210 )} 211 </FormItem> 212 213 </Form> 214 </Modal> 215 ) 216 } 217 }); 218 addOrUpdateModal = createForm()(addOrUpdateModal); 219 export default addOrUpdateModal;