# react_admin **Repository Path**: ofkangk/react_admin ## Basic Information - **Project Name**: react_admin - **Description**: React的后台管理项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-01-18 - **Last Updated**: 2022-02-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1. Login组件的背景问题 1. login.less ```less .login { width: 100%; height: 100%; background-image: url("./images/bg.jpg"); background-size: 100% 100%; .login-header { } .login-content { } } ``` 使用后,body、root节点的高度为0,需要在public/css/reset.css中增加 ```css html, body { width:100%; height:100%; } #root { width:100%; height:100%; } ``` 2. 上述修改后,Login组件高度还是0.是由于App组件中的BrowserRouter外层有一个div,去掉即可 # 2. antd4.18 Form表单的验证 1. 声明式验证:在`Form.Item`中使用rules属性。rules值是一个列表,列表中是规则对象。 2. 自定义验证:在rules列表中使用对象属性`validator`,其值是一个函数(rules,value)=>Promise。 3. 当某一规则校验不通过时,是否停止剩下的规则的校验:`validateFirst`:Boolean|parallel(parallel开启并行校验) 4. `Form`的`onFinish`和`onFinishFailed`:校验成功和失败后的回调,用于发送ajax请求 # 3. 封装axios异步请求 1. 封装`ajax.js`模块: 能发送异步请求ajax的函数模块 - 封装axios库 - 函数的返回值是Promise对象 2. 包含应用中所有请求接口函数的模块,采用分别暴露 3. 使用代理解决跨域问题:脚手架自带的代理服务器,仅需要配置对应地址即可。(在package.json中添加`proxy`) 4. 使用async和await: - 作用:简化promise对象:不再使用`.then()`在指定成功/失败的回调 以同步编码方式(没有了回调函数)实现异步流程 - await的使用位置:在返回promise对象的表达式左侧使用await,可以得到promise请求成功的value数据 - async使用的位置:await所在函数(最近的)定义的左侧写async 5. 优化:统一处理请求异常 - 在封装的ajax请求中统一处理 - 在外层包一个自己创建的promise - 请求出错时,不reject(),直接提示出错 # 4. 登录成功后跳转到管理界面 1. 登录成功的判断 2. 跳转界面:登录界面不应该可以回退,所以应该使用replace(v6不在使用histroy,这里回退到v5) 3. 优化ajax请求:请求成功时直接resolve(response.data) 4. 将登录的用户信息保存到内存中:新建`/utils/memoryUtil.js`,用于保存数据 # 5. 维持登录和自动登录 1. 登录后,刷新依然是登录状态(维持登录) - 利用浏览器的localstorage,将用户信息保存到local(local可以保存多个信息) - 新建/utils/storageUtil.js: 实现以下功能:localStorage的参数是:key、value都是字符串。而实际value——user是对象,需要用到JSON 1. 保存用户信息 2. 读取用户信息 3. 删除用户信息 4. 可以使用`store`库便捷实现 - 在`Login`组件中,请求成功时将user保存到local - 每次刷新都是重新加载`index.js`,要维持登录,需要在`index.js`中,将local中的数据同步到内存中 2. 登录后,关闭浏览器后打开浏览器访问依然是登录状态(自动登录) - 完成1中内容,即可实现。 3. 登录后,访问登录路径自动跳转到管理界面 - 在`Login`组件的`render`中,增加判断: - 如果local中有信息,则直接跳转到`Admin`组件 # 6. 管理界面 ## 6.1 管理界面布局 1. 布局:使用`antd`的`layout`布局,并创建导航组件`LeftNav`和头部组件`Header` 2. 在导航组件`LeftNav`中添加`Menu`导航菜单,注意菜单、菜单项、子菜单的关系。 3. 创建路由组件 ``` /home # 首页路由组件 - home.jsx /category # 商品分类路由组件 - category.jsx / product # 商品管理路由组件 - product.jsx / role # 角色路由组件 - role.jsx / user # 用户路由组件 - user.jsx / charts # 图形路由 - bar.jsx # 柱状图 - line.jsx # 折线图 - pie.jsx # 饼图 ``` 4. 将路由组件映射成路由:在`Admin`组件的`Content`标签中配置路由 5. 在导航栏建立路由链接 ## 6.2菜单项的生成 6. 使用数组动态生成菜单 - 抽取每个菜单项的关键信息,建立对象列表 - 二级子菜单项作为嵌套列表 ```js menuList = [ { key, icon, title }, { key, icon, title, children:[] } ] ``` - 使用数组map或者reduce函数,将数据数组转换为标签数组 ## 6.3 选中菜单项 7. 自动选中当前菜单项 - 获取当前路径 1. 需要先将`LeftNav`组件包装成路由组件 2. `const path = this.props.location.pathName` - `defaultSelectedKeys`设置菜单默认选中项:仅在第一次`render`时生效。直接请求根路径时,先跳转到`/`,再跳转到`/home`,因此失效 - `selectedKeys`当前选中项:每次`render`都会生效 ## 6.4 打开子菜单 8. 自动打开选中了子菜单项的子菜单 - 如果选中了子菜单项,才需要打开相应的子菜单:判断是否打开了子菜单项 > 用路由路径path与子菜单children中的key进行对比,返回子菜单的key - 使用`defaultOpenKeys`初始展开的 `SubMenu` 菜单项 `key` 数组 - 要在初始展开之前,获取子菜单的`key`:必须在render前项生成菜单节点 # 7. Header组件 1. 划分区域:分为上下等大的两部分,中间有分界线 - 上半部分:表示欢迎以及退出登录 - 下半部分:表示当前菜单项标题和天气 2. 动态显示时间和天气、 - 创建获取天气的jsonp请求 - 在`Header`组件中是创建`state`保存当前时间、天气 - 在`DidMount`钩子内,定时更新时间和天气 3. 动态显示当前标题 - 获取当前路由路径path - 遍历menuList,根据path获取相应的title 4. 退出登录 - `antd`的对话框 - 卸载组件前,清除定时器 - 路由跳转 5. 用封装的LinkButton代替a标签 # 8. 完善导航栏的路由组件 ## 8.1 Home路由组件 1. 简化的`Home`组件:居中显示欢迎 - `less`样式: ```less display:flex; // 水平布局 align-items:center;// 垂直居中 jusitify-content:center;// 水平居中 ``` ## 8.2 category组件 1. 静态的`category`组件 - 添加`antd`中的`Card`组件 1. `Card`组件头部的左侧为标题,右侧为链接按钮 2. `Card`组件内部为`Table`组件 2. 请求qpi: - 获取一、二级分类 - 添加分类 - 更新品类名称 3. 异步显示一级分类列表 - 使用`state`驱动组件更新 - 在DidMount钩子中,发送异步请求,将返回的数据保存到`state`中 - 配置`Table`组件的`loading`和分页器`Pagination`(分页器的文字无法改中文) 4. 异步显示二级分类列表 - 点击链接按钮获取二级分类列表数据 - 异步更新状态重新render - 控制子列表操作列的按钮显示 5. 完善`Category`组件 - 返回上一级:重新获取一级列表 - 添加功能:使用`antd`中的`Madol`确认框 1. `AddForm`中的下拉框选项:由父组件将`categories`传给子组件 2. 子列表中添加时,表单默认值为父分类的名称 3. 只有在当前列表中添加分类时,才需要刷新列表 - 修改功能: 1. 点击Ok是隐藏确认框 2. 准备数据,发送ajax请求 - 使用实例变量保存数据 - 子组件的Form表单值传递给父组件:使用refs传递Form对象 3. 重新发送`reqCategoryList`请求获取最新的分类列表 4. 点击确认和取消时,清除`Input`中默认值 - 表单验证:只有表单验证通过的时候才能按下确认键 ## 8.3 Product组件 ### 1. 搭建整体路由 1. 创建三个子路由: - Home - Detail - AddUpdate 2. 搭建路由: ```jsx {/* 开启精准匹配 */} ``` 3. 精准匹配路由和重定向 - 路由默认是逐层匹配:当前面一层匹配到时,后面就不再将继续匹配 - 重定向:将匹配不到的路由重新定位到指定路由 ### 2. Home子路由组件 #### 2.1 静态界面 1. 划分界面: - 整体是一个`Card`组件 - `Card`组件头部左侧:title属性 - `Select` - `Input` - `Button` - `Card`组件头部右侧:extra属性 - 带图标的`Button` - `Card`组件内部:`Table` 2. 填充组件 - title、extra属性的参数是一个ReactNode - Table属性: 1. bordered:边框 2. dataSource:行数据。对象数组,每个对象代表一个行数据 3. columns: 列样式数据。对象数据,每个对象描述表格中一列样式。 #### 2.2. 分页列表 | | 纯前台的分页 | 基于后台的分页 | | ---------- | ----------------------------------------------------- | --------------------------------------------------- | | 请求的数据 | 一次性获取所有数据,翻页时不需要再请求 | 每次只获取当前页的数据,翻页时要发请求 | | 请求的接口 | 不需要指定请求参数:页码(pageNum)和每页数量(pageSize) | 需要指定请求参数:页码(pageNum)和每页数量(pageSize) | | 响应的数据 | 所有数据的数组 | 当前页数据的数组+总记录数(total) | #### 2.3. 异步分页列表 1. 接口请求函数`reqProducts` 2. 在组件完成挂载后异步请求商品列表 3. 设置`Table`组件的分页器属性`pagination` #### 2.4. 搜索分页 1. 测试api:同一个请求可以根据不同的参数(`productName`/`productDesc`)可以实现两种搜索请求 2. 编写请求函数: - 两个请求函数:简单 - 一个请求函数:多接受一个控制请求类型的参数 3. 将`Select`和`Input`组件变成受控组件(实时获取数据) - `Option`的value属性调整为与`searchtype`对应 - 受控组件: 1. 在`state`中添加收集数据的属性 2. 实时监控组件变化:`onChange={value=>{this.setState({searchType:value})}}` ### 3. Detail子路由组件 #### 3.1 静态页面 1. 整体是一个`Card` 2. 下面是一个`List` 3. ``React`标签的`dangerouslySetInnerHTML`属性:允许使用原生的`html`标签 #### 3.2 动态显示商品详情 1. 路由跳转:使用`push`、`replace`进行路由跳转时,是可以携带对象参数传递给目标路由的(位于目标路由的`location.state`中) 2. 取出`Home`路由传递的对象属性,替换掉节点中的值 3. 异步显示商品分类 1. 编写根据ID获取商品分类的Api 2. 当`Detail`组件完成挂载时,发送请求获取分类信息 3. 将响应的数据保存到`state`中。(因为数据是异步请求获取的,在完成一次`render`后才能获取数据。如果仅适用类变量保存,那么当得到数据后,无法再驱动节点刷新;而保存在`state`中,可以使用`setState`再得到响应数据后再刷新节点) 3. 使用`await`进行多个`ajax`请求时,下一个请求会等待上一个请求完成后才会进行。想要进行同时进行多个`ajax`请求,可以使用`Promise.all([请求列表])`,返回响应列表 #### 3.3 动态更新商品状态(上架/下架) 1. 根据商品状态显示不同的节点 2. 编写更新商品状态的api 3. 点击按钮时,根据所在行的当前状态发送请求,获取当前页码对应的商品列表 4. 每次请求商品列表时,保存页码,方便其他方法使用 #### 3.4 进入Product子路由时导航栏的问题 1. 导航栏`LeftNav`组件不自动选中 2. 导航栏`LeftNav`组件不自动展开 3. 问题原因:因为是原先是按照完全的路由匹配的`Menu`中的`selectedKeys={[**path**]}`和`defaultOpenKeys` 4. 解决方法:只要`path`的开头包含`/product`即可(`path.indexOf('/product'===0)`) ### 4. AddUpdate组件 路由跳转:点击添加按钮跳转到`AddUpdate`组件 #### 4.1 静态页面 1. 布局 - `Card` - `Icon` - `Form` - `Input` - `Input.TextArea` #### 4.2 表单验证 1. 声明式验证:`rules={[验证规则配置对象]}` 2. 自定义验证:`validator:(rule,value)=>Promise` 3. 提交时验证: - 按钮绑定表单:在按钮中添加属性`htmlType="submit"`(一般表单提交时会接收event参数,并默认跳转页面,需要阻止跳转。但是antd4.x中Form只需要绑定`onFinish`即可,当所有验证通过时,按下按钮就会执行。) - 按钮不绑定表单:此时不需要阻止页面跳转,但是需要使用`form`属性收集表单信息(antd4.x中使用`refs`获取表单实例信息) 4. 验证数字输入框中的数字大小:使用自定义验证 ```jsx validateNum = (rule, value) => { if (value * 1 > 0) { return Promise.resolve() // 验证通过的时的回调 } else { return Promise.reject('商品价格不能低于0') // 验证失败时的回调 } } ``` 5. 当某一规则校验不通过时,是否停止剩下的规则的校验。:`Form.Item`的`validateFirst`属性 #### 4.3 静态的商品分类项 使用`Cascader`级联选择,选择动态加载项 1. `options`属性 1. `option`对象: 1. `value` 2. `label` 3. `isLeaf` 4. `children` 5. `disabled` 2. `loadData`属性:点击选项时的加载回调 - 接收参数:`selectedOptions` - 回调中完成以下事情: 1. 获取当前选项对象:`const targetOption = selectedOptions[0]` 2. 显示加载动画:`targetOption.loading = true` 3. 完成异步任务 4. 取消加载动画:`targetOption.loading = false` 5. 设置`targetOption`的子选项列表(实际上修改了`options`属性值) 6. 更新状态,驱动页面更新 #### 4.4 商品分类显示一级分类列表 1. 需要异步请求数据: - 使用`state`保存数据 - 在`componentDidMount`中发送请求 2. 根据返回的数据,生成所需的`options`数组 - `map` - `reduce` 3. 更新状态,刷新页面 #### 4.5 显示二级分类列表 1. 异步请求子列表: - 准备请求参数 - 发送请求 - 重新调用接口请求函数 - 复用`getCategories`方法(因为getCategories方法被`async`修饰,也是异步函数,返回的也是Promised对象) 2. 判断请求结果 - 如果有子列表 :为目标选项设置`children`属性 - 如果没有子列表:将目标选项的`isLeaf`属性改为`true` 3. 更新状态,刷新页面: - 准备新的options数据:1. 从`state`中取出;2. 重组装`options`:`[...this.state.options]` - 调用`setState`,更新状态 #### 4.6 更新时显示默认值 1. 修改时的路由跳转:携带数据跳转 2. `AddUpdate`组件实现添加、更新功能: 在组件挂载前, 1. 保存`Home`组件携带的数据:如果是添加,则Home不携带数据,将this.product设为空对象;如果是更新,接收Home路由携带的数据。 `this.product = props.location.state.product || {}` 2. 设置标志变量`isUpdate`:判断`this.product`为空对象 - 如果`isUpdate`为`true`表示更新 - 如果`isUpdate`为`false`表示添加 3. `Form`表单设置默认值 - 一般项 - 分类的级联选择:初始值为数组 4. 默认分类: - 初始值类型为数组 - 有可能仅有一级分类:数组中只有一个元素`categoryId`,此时`pCategoryId==='0'` - 有可能有二级分类:数组中含有两个元素`pCategoryId`和`categoryId` #### 4.7 UpLoad组件分析 1. 照片墙的使用 2. `Upload`是受控组件 3. 常用参数: - 上传时发送请求的参数名称:`name` - 预览功能:`onPreview`,可以使用`Modal`实现 - 上传地址:`action` - 图片展示方式:`listType`,*text, picture 和 picture-card* - 接收文件类型:`accept` - 上传、删除功能:`onChange` 3. 上传按钮:`{fileList.length >= 8 ? null : uploadButton}` 3. `onChange`的回调参数:`{file,fileList,event}` - `file`:当前文件对象 - `fileList`:文件对象列表 - `event`:事件对象 #### 4.8 修改上传的图片属性 1. 在发送添加商品的api中,需要使用到图片的名称,且是上传返回的图片名称 2. 在`Upload`中收集图片名称 #### 4.9 提交时收集图片名称 1. 在子组件中调用父组件方法:将父组件方法以函数形式通过`props`传递给子组件 2. 在父组件中调用子组件方法:使用`refs`标记子组件标签的实例对象 #### 4.10 删除上传的图片 如果不擅长,图片会越来越多 1. 在`onChange`的回调中处理删除 2. 发送删除图片的api #### 4.11 更新商品时显示图片 1. 将`AddUpdate`组件中的`product`的`imgs`属性传递给`PicturesWall`组件 1. `PicturesWall`组件中接收`imgs`属性,并在`constructor`钩子中初始化`state`中的`fileList` #### 4.12 使用富文本编辑器 1. 安装富文本组件:安装`react-draft-wysiwyg`和`draft-js` ```jsx import { Editor } from 'react-draft-wysiwyg'; } // 包裹的样式 editorStyle={} // 编辑器的样式 toolbarStyle={} // 工具栏的样式 /> ``` 2. 修改布局: - 导入样式:`import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'` - 更改`Form`中的商品详情`Item`的布局:将水平布局变长——`wrapperCol={{span:20}}` - 设置编辑器样式:`editorStyle={{border:'1px solid black',height:200,padding:10}}` - 调整`Admin`整体界面布局:因为`AddUpdate`组件高度超出`100%`,导致`Footer`组件看不到。 - 将`Admin`组件中的``设置为最小高度 3. 点击提交按钮时,在`AddUpdate`父组件中获取子组件`RichTextEditor`的值 - 将富文本中的`editorState`值转化为`html`字符串 1. 安装转换工具库:`draftjs-to-html` 2. 调用方法转换:`draftToHtml(convertToRaw(editorState.getCurrentContent()` - 在子组件创建获取值的方法: ```jsx getDetail = ()=>draftToHtml(convertToRaw(this.state.editorState.getCurrentContent() ``` - 在父组件`AddUpdate`中使用`ref`标记编辑器标签对象,在提交按钮时调用子组件方法获取`html`值 4. 更新时,子组件中显示父组件传入的`html`字符串 - 将商品详情以`props`的方法传给子组件 - 子组件声明接收,并在组件挂载前设置状态 - 如果接收到非`undefined`,说明是更新功能 - 将`html`字符串转换为富文本后,赋值给`editorState` - 如果接收到`undefined`,说明是添加功能,将`editorState`设置为`EditorState.createEmpty()`空对 5. 添加本地文件 - 配置工具栏属性: ```jsx toolbar={{ image: { previewImage: true, uploadCallback: this.uploadImageCallBack, alt: { present: true, mandatory: false }, } }} ``` - 上传监听: ```jsx uploadImageCallBack = (file) => { return new Promise( (resolve, reject) => { const xhr = new XMLHttpRequest(); // 创建xml请求 xhr.open('POST', '/manage/img/upload'); // 发送请求 xhr.setRequestHeader('Authorization', 'Client-ID 8d26ccd12712fca'); const data = new FormData(); // eslint-disable-line no-undef data.append('image', file); xhr.send(data); // 请求监听 xhr.addEventListener('load', () => { // 请求响应 const response = JSON.parse(xhr.responseText); // 提取响应的图片url const url = response.data.url resolve({ data: { link: url } }); // 成功时返回的配置对象(格式固定) }); xhr.addEventListener('error', () => { const error = JSON.parse(xhr.responseText); reject(error); }); }, ); } ``` #### 4.13 添加和更新商品功能 1. 编写api:将添加和更新写成一个函数 2. 准备数据 3. 判断请求结果 # 9. 角色与用户管理 ## 9.1 需求分析 ### 1. 角色管理 1. 添加、删除角色 2. 分配角色权限 3. 数据对象 - `menu`:权限对应的菜单 - `_id`:唯一标识 - `name`:角色名称 - `auth_time`:授权时间 - `auth_name`:授权对象名称 - `create_time`:创建时间 ### 2. 用户管理 ## 9.2 角色路由功能 ### 1. 角色管理 1. 结构:`Card`、`Table` 2. `Table`的可选性:`rowSelection` 3. `Table`对行的监听 ```jsx { return { // 返回一个监听配置对象 onClick: event => {}, // 点击行 onDoubleClick: event => {}, onContextMenu: event => {}, onMouseEnter: event => {}, // 鼠标移入行 onMouseLeave: event => {}, }; }} onHeaderRow={(columns, index) => { return { onClick: () => {}, // 点击表头行 }; }} /> ``` ### 2. 动态显示角色列变 1. 编写获取角色列表的api 2. 在`DidMount`中发送请求 3. 点击行,自动选中 - 在`state`中记录当前选中角色 - `rowSelection`中配置`selectedRowKeys`:指定选中项的 key 数组 4. 选中角色时,设置角色权限按钮可用 ### 3. 显示添加角色确认框 1. 创建`AddForm`组件:只有一个输入框`Input`的表单`Form` 2. 将`form`属性传递给父组件`Role` 3. 在`Role`中判断表单是否验证通过 - 验证通过:隐藏确认框、发送请求、清除输入框数据 - 验证失败:清除输入框数据、隐藏对话框 ### 4. 显示权限设置对话框 1. 使用`Tree`组件 1. 根据`menuList`生成权限树 1. 根据`role.menus`选中默认节点: - `checkedKeys`:受控 - 使用`state`保存已选中节点,并在`constructor`中初始化为`role.menus` - `onChecked`:选中节点时的回调,选中时更新状态 ### 5. 更新角色权限 1. 在父组件`Role`中获取子组件`AuthForm`中获取子组件中`state.checkedKeys` 2. 使用`ref`调用子组件实例方法获取 3. 编写更新角色api 4. 准备数据,发送请求 ### 6. 权限列表显示bug 1. 每次点击设置权限按钮,显示对话框时,之后显示第一个点击的角色的权限 - 问题原因: 1. 第一显示,即为挂载,先执行构造器,初始化状态,接着render渲染组件 2. checkedKeys属性根据state来决定选中的节点 3. 关闭对话框后,组件并未卸载,仅仅是隐藏,再次显示时,不再执行构造器,因此state不会更新,选中的节点也不会更新。 2. 在一个角色中,改变一个权限后,点击取消,然后查看权限,发现改变的权限仍然存在 - 问题原因:因为已经缓存数据 3. 解决方法: 1. react16中可以使用`componentWillReceiveProps(nextProps){}`,根据新传入的props,更新state - 同时解决问题1,2 2. react17中已经弃用上述方法,可以`UNSAFE_componentWillReceiveProps` 3. 有 key 的非可控组件,将`Tree`的key设置为根据props改变的值(比如:role._id),然后使用defaultCheckedKeys。如此以来无需使用state保存数据,但是也不方便将数据提交给父组件 - 使用实例变量保存每次选中时的节点:`this.checkedKeys = checkedKeys` - 将key改为时间,解决问题2 ### 7. 格式化时间和授权人 更新权限时,给`role`对象中添加属性 1. `role.auth_name = memoryUtil**.user.username` 2. 格式化`Table`中的时间:在列属性中使用`render` ## 9.3 用户管理 ### 1. 静态页面 1. `Card`、`Table` 1. 准备`Card`的`title` 1. 准备`Table`的列 1. 配置分页器 ### 2. 动态显示用户列表 1. api接口 2. 发送请求,接收数据`users`、`roles`,更新状态 3. 创建角色id和name对象 ### 3. 显示添加/更新对话框 1. 使用`Modal` 2. 使用`state`保存控制状态 3. 表单验证,设置`Item`规则: - 用户名规则: - 密码规则: - 手机号码规则 - 邮箱规则: ### 4. 删除用户功能 1. 编写api 2. 点击删除按钮,实现删除功能 3. 使用`Modal.confirm({配置对象})`来显示对话框 ```jsx const {getUsers} = this Modal.confirm({ title:'标题', async onOk(){ // 发送请求 const result = await reqDeleteUser(user._id) // 离得最近的方法是onOk // 判断响应结果 if(result.status===0){ // 更新用户列表,此时不能直接用this,因为离得最近的是函数onOk // 提前获取getUsers getUsers() } }, // 不需要使用,点击取消会自动关闭 onCancle(){ } }) ``` ### 5. 添加用户 1. 静态页面:`Modal`中添加`Form`表单,表单中包含:`Item`和`Select` - 从父组件`User`中获取`roles`数组,然后使用`map`方法生成`Option` 2. 编写api 3. 从子组件`UserForm`中收集数据,封装成`user`对象,发送请求 ### 6. 更新用户 1. 与添加用户共用一个组件: - 请求区别:更新时需要多提交一个`user._id` - 功能区别:点击修改时,可以获取所在行的user对象。使用实例变量`this.user`保存`user`对象 - 在`User`组件的`render`取出`user`对象,如果是`undefined`则设为空:`const user = this.user || {}` 2. 点击修改按钮时,新增`showUpdate`的回调:接收参数`user` - 用`this.user`保存`user` - 更新状态,显示对话框 3. 修改`Modal`: - 标题:如果`user`非空,则是`更新用户`,否则是`创建用户` - 给子组件`UserForm`传入`user={user}` 4. 修改`UserForm`组件: - 声明props接收对象 - 将`Form`的初始值设为`user` - `Form`中增加`key={user._id}`:==为了解决render时,props未更新的问题== - 修改的对话框中不显示密码:如果`user._id`为非空,则不显示,否则显示 ```jsx { user._id ? null : ( ) } ``` 5. 修改点击`OK`按钮的回调:仅修改表单验证通过后的部分 - 判断表单是否被修改过,如果没有直接结束 - 取出表单数据`user`后,判断`this.user`是否为空,如果非空,则为更新,在`user`中新增`_id` 6. 点击添加用户按钮时,新增`showAdd`的回调: - 将`this.user`设空:==为了解决点击修改按钮后,再点击添加按钮,表单中有值的问题== - 更新状态,显示对话框 ## 9.4 菜单栏的显示 根据用户权限,显示菜单 ### 1. 显示菜单时,匹配当前用户的菜单 1. 当前登录的用户信息保存在本地中`memoryUtil.user`和浏览器的本地`storage`中 - 权限菜单位于`user.role.menus`中 2. 修改`LeftNav`组件中的`getMenuNodes`(生成菜单节点): - 递归遍历`menuList`,生成的菜单节点 - 生成节点前,判断一下`menuList`中的菜单项是否在`user.role.menus`中 - 如果不在则不生成 3. 编写判断是否生成菜单节点的方法:hasMenu(item) ```jsx hasMenu = (item) => { // 1. 如果是admin账号:即本地存储的用户的username是admin // 2. 如果item的key在本地user.role.menus中 // 3. 至少允许用户可以查看首页,将首页设为公开的:增加isPublic属性 // 4. 如果当前用户,拥有此item的某个子item的权限:menus数组中的某一个项位于item.children数组中 const { user } = memoryUtil if (user.username === 'admin' || user.role.menus.indexOf(item.key) !== -1 || item.isPublic) { return true } else if (item.children && !!item.children.find(c => user.role.menus.indexOf(c.key) !== -1)) { // 先判断是否有子item // item.children.find(c => user.role.menus.indexOf(c.key) !== -1) 如果非空说明存在,应该返回true return true } return false } ``` ### 2. 当修改当前用户权限时,重新登录 1. 删除当前用户:退出登录、清除本地和浏览器存储 - 先清除登录用户 - 再跳转到登录界面 2. 修改当前用户权限时:退出登录 - 修改角色权限 1. 如果打开的是修改当前用户角色的对话框,提示会重新登录 2. 在点击Ok的回调中,修改成功后,增加判断是否是当前用户角色:`role._id === memoryUtil.user.role_id` - 修改用户所属角色 1. 点击修改时,提示如果修改了所属角色,将会退出登录 2. 在修改用户的回调中,如果修改成功,增加判断返回的数据中的角色id是否与当前用户的角色id一致: - 如果不一致,说明修改了角色,清除本地用户,跳转到登录界面 - 如果一致,则提示更新成功,刷新用户列表 # 10. 两个bug 1. 选择角色时,点击`radio`无法选中 - 原因:没有设置`radio`的回调 - 解决:在`Table`的`rowSelection`中增加`onSelect`配置 ```jsx rowSelection={{ type: 'radio', selectedRowKeys: [role._id], onSelect: (role) => { this.setState({ role }) } }} ``` 2. 在商品管理中,查询商品时,分页器的页码问题 - 原因:查询时,会重新更新商品列表,但是分页器页面没有改变 - 解决:给分页器配置对象中添加当前页项(current) >>>>>>> dev2