React实战
一、前期准备
1、React安装与创建项目
// 使用npm安装create-react-app
npm install -g create-react-app
// 在指定路径下使用create-react-app工具创建项目,xxx:项目名称
create-react-app xxx
// 进入项目,启动项目,测试项目是否创建成功
cd xxx
npm start
详情参见 【react教程】
2、组件的打包
以调用antd为例,在使用antd时如果只是单纯的
import { Button } from 'antd';
,该按钮组件因没有导入对应的css样式而没有显示预期的效果,所以需要将外部库先打包
方法一:全部导入
在根目录的index.js
导入相关包
// 该方式属于导入文件的全部样式,不推荐这么做
import 'antd/dist/antd.css'
方法二:按需导入
按需打包
参考文档:https://ant.design/docs/react/use-with-create-react-app-cn
/**
* 先安装antd库
*/
npm install antd
/**
* 安装按需打包依赖模块react-app-rewired customize-cra babel-plugin-import
*/
npm install react-app-rewired customize-cra babel-plugin-import
/**
* 定义加载配置的 js 模块: 根目录/config-overrides.js
*/
const {override, fixBabelImports, addLessLoader} = require('customize-cra');
module.exports = override(
// 针对antd实现按需打包: 根据import来打包(使用babel-plugin-import)
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css', // 自动打包相关的样式,如果自定义样式用 style: true
}),
// 使用less-loader对源码中的less的变量进行重新指定
// 该函数是自定义主题样式,将primary样式指定成绿色,按需添加修改
// addLessLoader({
// javascriptEnabled: true,
// modifyVars: {'@primary-color': '#1DA57A'},
// }),
)
/**
* 修改根目录/package.json配置,引入config-overrides.js配置
*/
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
完成上诉操作,antd库样式正常显示
3、路由选择
使用
react-router-dom
进行路由选择
/**
* 安装react-router-dom
*/
npm install react-router-dom
/**
* 在项目路径下创建src/pages/admin/admin.jsx
*/
import React, { Component } from 'react'
/**
* 后台管理的路由组件
*/
export default class Admin extends Component {
render() {
return (
<div>
<h1>Admin</h1>
</div>
)
}
}
/**
* 在项目路径下创建src/pages/login/login.jsx
*/
import React, { Component } from 'react'
/**
* 登录的路由组件
*/
export default class Login extends Component {
render() {
return (
<div>
<h1>Login</h1>
</div>
)
}
}
/**
* App.js中进行路由选择
*/
import React, { Component } from 'react'
import './App.css';
import {BrowserRouter, Route, Switch} from 'react-router-dom'
import Login from './pages/login/login'
import Admin from './pages/admin/admin'
/**
* 应用的根组件
*/
export default class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route path='/login' component={Login}></Route>
<Route path='/' component={Admin}></Route>
</Switch>
</BrowserRouter>
)
}
}
上诉操作,完成路由选择
说明:BrowserRouter
是"/",可替换使用HashRouter
是"#/"
<Switch>
标签只渲染第一个匹配到的<路由组件>或<重定向组件>
如果没有<Switch>
那么所有匹配的路由都将渲染
二、使用antd编写登录页面
详细的步骤不一一指出,以下列出调用的函数以及具体功能的实现
0、导入使用less(失败)
npm install less less-loader
npm run eject
暴露config文件夹,修改webpack.config.js
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
{
test: lessRegex,
exclude: lessModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: lessModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
//可能会报错
npm install react-scripts
1、内置校验
/**
* 将Login组件包裹,向Login组件传递一个props中带有form,form带有各种方法
*/
import { Form } from 'antd';
class Login extends Component {
}
const WrapLogin = Form.create()(Login)
export default WrapLogin
/**
* 在render中调用form对象内置getFieldDecorator方法进行校验
*/
render() {
// 得到具有强大功能的form对象
const { getFieldDecorator } = this.props.form
return(
// ...代码省略
<Form>
<Form.Item>
{getFieldDecorator('username', {
// 声明式验证:直接使用定义好的验证规则进行验证
rules: [
{ required: true, whitespace: true, message: '用户名必须输入!' },
{ min: 4, message: '用户名至少4位' },
{ max: 12, message: '用户名做多12位' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名必须是英文、数字或下划线组成' }
],
})(
<Input
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="用户名"
/>,
)}
</Form.Item>
</Form>
// ...代码省略
)
}
高级组件:getFieldDecorator('id',{规则})(渲染标签)
参考:https://ant.design/components/form-cn/#Form.create(options)
2、自定义校验
自定义校验同内置校验,需要引入form组件,步骤同 1、内置校验
/**
* getFieldDecorator的rules中使用validator规则
* 指定自定义验证方法validatePwd
*/
class Login extends Component {
/**
* 对密码进行自定义验证
*/
validatePwd = (rule, value, callback) => {
console.log('validatePwd', rule, value)
// 验证失败
// callback('xxxx')
if (!value) {
callback('密码必须输入')
} else if (value.length < 4) {
callback('密码长度不能小于4位')
} else if (value.length > 12) {
callback('密码长度不能大于12位')
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
callback('密码必须是英文、数字或下划线组成')
} else {
// 验证通过
callback()
}
}
render(){
// 得到具有强大功能的form对象
const { getFieldDecorator } = this.props.form
return(
<Form>
<Form.Item>
{getFieldDecorator('password', {
// 声明式验证:直接使用定义好的验证规则进行验证
rules: [
{ required: true, whitespace: true, message: '用户名必须输入!' },
// 不同点,这里引入自定义方法
{ validator: this.validatePwd }
],
})(
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="密码"
/>,
)}
</Form.Item>
</Form>
)
}
}
3、提交表单
/**
* 表单提交方法
*/
// Component 中的方法
handleSubmit = (event) => {
// 阻止事件的默认行为
event.preventDefault()
// form.validateFields 校验方法判断,err是状态,values是input值
this.props.form.validateFields((err, values) => {
//校验成功
if (!err) {
console.log('提交登录的ajax请求', values)
} else {
console.log('校验失败', values)
}
})
//form.getFieldsValue() 获取一组输入控件的值,如不传入参数,则获取全部组件的值
}
//render中的Form标签
<Form onSubmit={this.handleSubmit} >
// ...省略代码
</Form>
注意:使用
getFieldsValue
getFieldValue
setFieldsValue
等时,应确保对应的 field 已经用getFieldDecorator
注册过了。
方法 | 说明 | 类型 |
---|---|---|
getFieldsValue | 获取一组输入控件的值,如不传入参数,则获取全部组件的值 | Function([fieldNames: string[]]) |
getFieldValue | 获取一个输入控件的值 | Function(fieldName: string) |
setFieldsValue | 设置一组输入控件的值(注意:不要在 componentWillReceiveProps 内使用,否则会导致死循环,原因) |
( { [fieldName]: value }, callback: Function ) => void |
4、axios
// 安装axios模块
npm install axios
// 加上代理,实现跨域请求 package.json
"proxy": "http://localhost:5000"
/**
* axios的一般用法
* get方法调用axios.get,post方法调用axios.post
* axios.get方法返回promise对象
*/
axios.get(url, {
// 请求参数
params: data
}).then(respinse => {
// 成功的处理
}).catch(error => {
// 失败的处理
})
import axios from 'axios'
import {message} from 'antd'
/**
* 对异步请求的axios库进行封装
*/
export default function ajax(url, data = {}, method = 'GET') {
// 优化
return new Promise((resolve,reject) => {
//定义变量
let promise
// 1. 执行异步ajax请求
if (method === 'GET') {
// 配置对象
promise = axios.get(url, {
//请求参数
params: data
})
} else if (method === 'POST'){
promise = axios.post(url, data)
}
promise.then(response => {
// 2. 如果成功,调用resolve(value)
// 优化 resolve(value.data)
resolve(response.data)
}).catch(error => {
// 3. 如果失败,不调用reject(reason),而是提示异常信息
message.error('请求出错:'+error.message)
})
})
}
import ajax from './ajax'
import jsonp from 'jsonp'
import {message} from 'antd'
/**
* 对axios封装对象的调用,进行api封装,方便使用api请求
*/
export const reqLogin = (username,password) => ajax('/login',{username,password},'POST')
// export function reqLogin(username,password) {
// return ajax('/login',{username,password},'POST')
// }
/**
* 调用api请求实现登录
*/
handleSubmit = (event) => {
// 阻止事件的默认行为
event.preventDefault()
// 校验
this.props.form.validateFields(async (err, values) => {
//校验成功
if (!err) {
// 传统promise方法
reqLogin(username,password).then(response => {
console.log('成功了',response.data)
}).catch(error => {
console.log('失败了',error)
})
// async await方法 有错误返回
try {
const response = await reqLogin(username, password)
console.log('成功了', response.data)
} catch (error) {
console.log('登录请求出错', error)
alert('请求出错:' + error.message)
}
// async await方法 无错误返回
const result = await reqLogin(username, password)
if (result.status === 0) {
// 登录成功
message.success('登录成功')
const user = result.data
// 保存在内存中
memoryUtils.user = user
// 保存到local中
storageUtils.saveUser(user)
// 跳转到后天管理界面(不需要回退到登录)
this.props.history.replace('/')
} else {
// 登录失败
message.error(result.msg)
}
}
async
和await
的搭配使用,可以优化代码的格式,作用同promise.then,异步处理回调结果
5、本地缓存和内存管理
/**
* 使用localStorage可以将系统cookies保存到浏览器上
* JSON.stringify()以json编码格式保存,如无,将以特定编码保存
*/
// 保存
localStorage.setItem('user_key',JSON.stringify(user))
// 读取
JSON.parse(localStorage.getItem('user_key') || '{}')
// 删除
localStorage.removeItem('user_key')
/**
* localStorage可能存在浏览器不支持,引入store库,兼容所有浏览器
* 编写封装的存储类:utils/storageUtils.js
*/
// 安装store模块
npm install sotre
import store from 'store'
const USER_KEY = 'user_key'
export default {
/**
* 保存user
*/
saveUser(user) {
// localStorage.setItem(USER_KEY, JSON.stringify(user))
store.set(USER_KEY,user)
},
/**
* 读取user
*/
getUser() {
// return JSON.parse(localStorage.getItem(USER_KEY) || '{}')
return store.get(USER_KEY) || {}
},
/**
* 删除user
*/
removerUser() {
// localStorage.removeItem(USER_KEY)
store.remove(USER_KEY)
}
}
/**
* 内存管理工具类
* 用来在内存中保存数据的模块
*/
export default {
user: {},
}
/**
* 方法调用
*/
import memoryUtils from '../../utils/memoryUtils'
import storageUtils from '../../utils/storageUtils'
// 保存在内存中
memoryUtils.user = user
// 保存到local中
storageUtils.saveUser(user)
/**
* 登录控制:
* 本地缓存user,内存加入user
*/
handleSubmit = (event) => {
// 阻止事件的默认行为
event.preventDefault()
// 校验
this.props.form.validateFields(async (err, values) => {
//校验成功
if (!err) {
// 请求登录
const { username, password } = values
// 传统promise方法
reqLogin(username,password).then(response => {
console.log('成功了',response.data)
}).catch(error => {
console.log('失败了',error)
})
// async await方法 有错误返回
try {
const response = await reqLogin(username, password)
console.log('成功了', response.data)
} catch (error) {
console.log('登录请求出错', error)
alert('请求出错:' + error.message)
}
// async await方法 无错误返回
const result = await reqLogin(username, password)
// console.log('成功了', response.data)
// // 返回数据
// const result = response.data
if (result.status === 0) {
// 登录成功
message.success('登录成功')
const user = result.data
// 保存在内存中
memoryUtils.user = user
// 保存到local中
storageUtils.saveUser(user)
// 跳转到后天管理界面(不需要回退到登录)
this.props.history.replace('/')
} else {
// 登录失败
message.error(result.msg)
}
} else {
console.log('校验失败', values)
}
})
}
三、使用antd编写主页页面
1、后台管理的路由组件--Admin
/**
* pages/admin/admin页面
* 查看本地缓存是否有user选择显示页面
*/
import React, { Component } from 'react'
import { Redirect, Route, Switch } from 'react-router-dom'
import memoryUtils from '../../utils/memoryUtils'
import { Layout } from 'antd';
const { Footer, Sider, Content } = Layout;
export default class Admin extends Component {
render() {
const user = memoryUtils.user
// 如果内存中没有存储user ==> 当前没有登录
if (!user || !user._id) {
// 自动跳转到登录
return <Redirect to='/login' />
}
return (
<Layout style={{ height: '100%' }}>
<Sider>
<LeftNav />
</Sider>
<Layout>
<Header>Header</Header>
<Content style={{ margin: 20, backgroundColor: '#fff' }}>
<Switch>
<Route path='/home' component={Home} />
<Route path='/category' component={Category} />
<Route path='/product' component={Product} />
<Route path='/role' component={Role} />
<Route path='/user' component={User} />
<Route path='/charts/bar' component={Bar} />
<Route path='/charts/line' component={Line} />
<Route path='/charts/pie' component={Pie} />
<Redirect to='/home' />
</Switch>
</Content>
<Footer style={{ textAlign: 'center', color: '#cccccc' }}>推荐使用谷歌浏览器,可以获得最佳页面操作体验</Footer>
</Layout>
</Layout>
)
}
}
2、侧边菜单栏组件--LeftNav
/**
* 渲染函数
*/
render() {
const path = this.props.location.pathname
const openKey = this.openKey
return (
<div className="left-nav">
<Link to='/' className="left-nav-header">
<img src={logo} alt="logo" />
<h1>硅谷后台</h1>
</Link>
<Menu
mode="inline"
theme="dark"
selectedKeys={[path]}
defaultOpenKeys={[openKey]}
>
//菜单相关.....代码
</Menu>
</div>
)
}
/**
* 渲染函数中直接写菜单名称的方法较直接,普通
* 采用动态封装如下
* config/menuConfig.js
*/
const menuList = [
{
title: '首页', // 菜单标题名称
key: '/home', // 对应的 path
icon: 'home', // 图标名称
},
{
title: '商品',
key: '/products',
icon: 'appstore',
children: [ // 子菜单列表
{
title: '品类管理',
key: '/category',
icon: 'bars'
},
{
title: '商品管理',
key: '/product',
icon: 'tool'
},
]
},
{
title: '用户管理',
key: '/user',
icon: 'user'
},
{
title: '角色管理',
key: '/role',
icon: 'safety',
},
{
title: '图形图表',
key: '/charts',
icon: 'area-chart',
children: [
{
title: '柱形图',
key: '/charts/bar',
icon: 'bar-chart'
},
{
title: '折线图',
key: '/charts/line',
icon: 'line-chart'
},
{
title: '饼图',
key: '/charts/pie',
icon: 'pie-chart'
},
]
},
]
export default menuList
/**
* 完整的left-nav
*/
import React, { Component } from 'react'
import './index.less'
import logo from '../../assets/images/logo.png'
import { Link, withRouter } from 'react-router-dom'
import menuConfig from '../../config/menuConfig'
import { Menu, Icon, } from 'antd';
const { SubMenu } = Menu;
class LeftNav extends Component {
/**
* 根据menu的数据数组生成对应的项
* 使用map+递归调用
*/
getMenuNodes = (menuConfig) => {
const path = this.props.location.pathname
return menuConfig.map(item => {
if (!item.children) {
return (
<Menu.Item key={item.key}>
<Link to={item.key}>
<span>
<Icon type={item.icon} />
<span>{item.title}</span>
</span>
</Link>
</Menu.Item>
)
} else {
const cItem = item.children.find(cItem => cItem.key === path)
if (cItem) {
this.openKey = item.key
}
return (
<SubMenu
key={item.key}
title={
<span>
<Icon type={item.icon} />
<span>{item.title}</span>
</span>
}
>
{
this.getMenuNodes(item.children)
}
</SubMenu>
)
}
})
}
/**
* 使用reduce() + 递归
*/
getMenuNodes2 = (menuConfig) => {
const path = this.props.location.pathname
return menuConfig.reduce((pre, item) => {
if (!item.children) {
pre.push((
<Menu.Item key={item.key}>
<Link to={item.key}>
<span>
<Icon type={item.icon} />
<span>{item.title}</span>
</span>
</Link>
</Menu.Item>
))
} else {
pre.push((
<SubMenu
key={item.key}
title={
<span>
<Icon type={item.icon} />
<span>{item.title}</span>
</span>
}
>
{
this.getMenuNodes2(item.children)
}
</SubMenu>
))
// 如果当前请求路由与当前菜单的某个子菜单的 key 匹配, 将菜单的 key 保存为 openKey
if (item.children.find(cItem => path.indexOf(cItem.key) === 0)) {
this.openKey = item.key
}
}
return pre
}, [])
}
/**
* 渲染之前执行一次
*/
UNSAFE_componentWillMount() {
this.menuNodes = this.getMenuNodes(menuConfig)
}
render() {
const path = this.props.location.pathname
const openKey = this.openKey
return (
<div className="left-nav">
<Link to='/' className="left-nav-header">
<img src={logo} alt="logo" />
<h1>硅谷后台</h1>
</Link>
<Menu
mode="inline"
theme="dark"
selectedKeys={[path]}
defaultOpenKeys={[openKey]}
>
{
this.menuNodes
}
</Menu>
</div>
)
}
}
export default withRouter(LeftNav)
3、正文头部组件--Header
/**
* 编写头部组件
* 包括动态显示当前时间、天气、标题、用户名、退出
*/
import React, { Component } from 'react'
import { withRouter } from "react-router-dom";
import { formateDate } from '../../utils/dateUtils'
import memoryUtils from '../../utils/memoryUtils'
import storageUtils from '../../utils/storageUtils'
import menuConfig from '../../config/menuConfig'
import { reqWeather } from '../../api/index';
import './index.less'
import { Modal } from 'antd';
import LinkButton from '../link-button';
/**
* 顶部导航的组件
*/
class Header extends Component {
state = {
// 当前时间字符串
currentTime: formateDate(Date.now()),
// 天气图片
dayPictureUrl: '',
// 天气
weather: '',
}
getTime = () => {
// 每隔1s获取当前时间,并更新状态数据currentTime
this.intervalId = setInterval(() => {
const currentTime = formateDate(Date.now())
this.setState({ currentTime })
}, 1000)
}
getWeather = async () => {
// 调用接口请求异步获取数据
const { dayPictureUrl, weather } = await reqWeather('北京')
//更新状态
this.setState({ dayPictureUrl, weather })
}
getTitle = () => {
const path = this.props.location.pathname
let title
menuConfig.forEach(item => {
if (item.key === path) {
// 如果当前的item对象的Key与path一样
title = item.title
} else if (item.children) {
// 在所有的子item中查找匹配
const cItem = item.children.find(cItem => cItem.key === path)
// 如果有值才说明有匹配
if (cItem) {
title = cItem.title
}
}
})
return title
}
/**
* 退出登录
*/
logout = () => {
// 显示对话框
Modal.confirm({
content: '确定退出吗?',
onOk: () => {
// 删除保存的user数据
storageUtils.removerUser()
memoryUtils.user = {}
// 跳转到login
this.props.history.replace('/login')
}
});
}
/**
* 在第一次render()之后执行一次
* 一般执行异步操作:发ajax请求/启动定时器
*/
componentDidMount() {
// 获取当前的时间
this.getTime()
// 获取当前天气
this.getWeather()
}
/**
* 在组件卸载之前调用
*/
componentWillUnmount() {
// 清除定时器
clearInterval(this.intervalId)
}
render() {
const { currentTime, dayPictureUrl, weather } = this.state
const username = memoryUtils.user.username
const title = this.getTitle()
return (
<div className='header'>
<div className='header-top'>
<span>欢迎,{username}</span>
<LinkButton onClick={this.logout}>退出</LinkButton>
</div>
<div className='header-bottom'>
<div className='header-bottom-left'>{title}</div>
<div className='header-bottom-right'>
<span>{currentTime}</span>
<img src={dayPictureUrl} alt="weather" />
<span>{weather}</span>
</div>
</div>
</div>
)
}
}
export default withRouter(Header)
/*格式化日期工具 */
export function formateDate(time) {
if (!time) return ''
let date = new Date(time)
return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds()
}
四、品类管理的增加和修改
1、使用card与table组件构建基本页面
/**
* Card 组件
* title在左侧,extra在右侧
* Table 组件
* dataSource 字段
* columns 行
* pagination={{ defaultPageSize: 5, showQuickJumper: true }} 设置分页器
* loading 是否显示加载
*/
<Card title={title} extra={extra} >
<Table
dataSource={parentId === '0' ? categorys : subCategorys}
columns={this.columns}
bordered rowKey='_id'
pagination={{ defaultPageSize: 5, showQuickJumper: true }}
loading={loading} />
</Card>
/**
* Table 数据格式
*/
const dataSource = [
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖',
age: 42,
address: '西湖区湖底公园1号',
},
];
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
},
];
2、居中布局
/**
* css布局
* display 生成容器
* align-items 垂直
* justify-content
*/
display: flex;
align-items: center;
justify-content: center;
3、模态框
/**
* 模态框可以使用状态showStatus设置
*/
<Modal
title="添加分类"
visible={showStatus === 1}
onOk={this.addCategory}
onCancel={this.handleCancel}
>
<AddForm categorys={categorys} parentId={parentId} setForm={(form) => { this.form = form }} />
</Modal>
<Modal
title="更新分类"
visible={showStatus === 2}
onOk={this.updateCategory}
onCancel={this.handleCancel}
>
<UpdateForm categoryName={category.name} setForm={(form) => { this.form = form }} />
</Modal>
4、props传递父组件属性(属性方法)
/**
* 父组件,向子组件传递父组件的属性
* categorys,parentId属性值
* setForm 属性方法
*/
<AddForm
categorys={categorys}
parentId={parentId}
setForm={(form) => { this.form = form }} />
/**
* 子组件,获取父组件的属性
* categorys,parentId属性值
* setForm 属性方法
*
* 通过传递属性方法可以在子组件调用父组件的方法(逆向传递值)
* 也可以使用ref方法(逆向传递值)
*/
static propTypes = {
categorys: PropTypes.array.isRequired,
parentId: PropTypes.string.isRequired,
setForm: PropTypes.func.isRequired
}
componentWillMount(){
this.props.setForm(this.props.form)
}
5、input框可能会存在缓存
// 清除输入数据
this.form.resetFields()
五、商品管理
1、路由完全匹配
exact : 路由完全匹配
<Route path='/product' component={ProductHome} exact/>
2、事件改变监听
pagination={{
defaultPageSize: PAGE_SIZE,
showQuickJumper: true,
total,
onChange: (pageNum) => this.getProducts(pageNum)
}}
<Input
placeholder='关键字'
style={{ width: 150, margin: '0 15px' }}
value={searchName}
onChange={event => this.setState({ searchName: event.target.value })}
/>
3、Promise.all发送多个请求
async componentDidMount() {
const { pCategoryId, categoryId } = this.props.location.state.product
if (pCategoryId === '0') {
// 只有一级分类
const result = await reqCategory(categoryId)
const cName1 = result.data.name
this.setState({ cName1: cName1 })
} else {
/*
//通过await请求,依次等待
// 二级分类
const result1 = await reqCategory(pCategoryId)
const result2 = await reqCategory(categoryId)
const cName1 = result1.data.name
const cName2 = result2.data.name
*/
//一次性发送多个请求
const results = await Promise.all([reqCategory(pCategoryId), reqCategory(categoryId)])
const cName1 = results[0].data.name
const cName2 = results[1].data.name
this.setState({ cName1: cName1, cName2: cName2 })
}
}
4、内置html
<span dangerouslySetInnerHTML={{ __html: detail }}></span>
5、api key-value对应写法
/**
* 搜索商品分页列表(根据商品名称/描述)
*/
export const reqSearchProducts = ({ pageNum, pageSize, searchName, searchType }) => ajax('/manage/product/search', { pageNum, pageSize, [searchType]: searchName })
6、Cascader级联选择
/**
* 组件
*/
{
getFieldDecorator('categoryIds', {
initialValue: categoryIds,
rules: [
{ required: true, message: '必须输入商品分类' },
]
})(
<Cascader
placeholder='请指定商品分类'
options={this.state.options}
loadData={this.loadData}
/>
)
}
// options是选项中的值
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
isLeaf: false,
},
{
value: 'jiangsu',
label: 'Jiangsu',
isLeaf: false,
},
];
// loadData是动态选择加载回调: 分类选择
loadData = async selectedOptions => {
const targetOption = selectedOptions[0];
targetOption.loading = true;
// 显示
const subCategorys = await reqCategorys(targetOption.value)
targetOption.loading = false;
if (subCategorys.data && subCategorys.data.length > 0) {
// 生成二级列表的Option
const childOptions = subCategorys.data.map(c => ({
value: c._id,
label: c.name,
isLeaf: true
}))
// 关联当前的option上
targetOption.children = childOptions
} else {// 没有
targetOption.isLeaf = true
}
this.setState({
options: [...this.state.options],
});
};
7、上传图片
import React, { Component } from 'react'
import { Upload, Icon, Modal, message } from 'antd';
import { reqDeleteImg } from '../../api'
import PropTypes from 'prop-types';
import { BASE_IMG_URL } from '../../utils/constants';
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
/**
* 用于图片上传的组件
*/
export default class PicturesWall extends React.Component {
static propTypes = {
imgs: PropTypes.array
}
constructor(props) {
super(props)
let fileList = []
const { imgs } = this.props
if (imgs && imgs.length > 0) {
fileList = imgs.map((img, index) => ({
uid: -index,
name: img,
status: 'done',
url: BASE_IMG_URL + img
}))
}
this.state = {
previewVisible: false, //标识大图预览
previewImage: '',
fileList
}
}
getImgs = () => this.state.fileList.map(file => file.name)
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
});
};
handleChange = async ({ file, fileList }) => {
if (file.status === 'done') {
const result = file.response
if (result.status === 0) {
message.success('上传图片成功')
const { name, url } = result.data
file = fileList[fileList.length - 1]
file.name = name
file.url = url
console.log('done()', file)
} else {
message.error('上传图片失败')
}
}
else if (file.status === 'removed') {
// 删除图片
const result = await reqDeleteImg(file.name)
if (result.status === 0) {
message.success('删除图片成功')
} else {
message.error('删除图片失败')
}
}
this.setState({
fileList
})
};
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div >Upload</div>
</div>
);
return (
<div>
<Upload
action="/manage/img/upload" /** 上传图片接口 */
accept="image/*" /** 只接收图片格式 */
listType="picture-card"
name='image' /**请求参数名 */
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}
8、富文本编辑器
// 下载相关依赖
npm install react-draft-wysiwyg draftjs-to-html
react-draft-wysiwyg:编辑器
draftjs-to-html:html语言转化为富文本语言
/**
* rich-text-editor组件
*/
import React, { Component } from 'react';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import htmlToDraft from 'html-to-draftjs';
import PropTypes from 'prop-types';
export default class RichTextEditor extends Component {
static propTypes = {
detail: PropTypes.string
}
constructor(props) {
super(props);
const html = this.props.detail;
if (html) {
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
this.state = {
editorState,
};
}
} else {
this.state = {
editorState: EditorState.createEmpty(),
}
}
}
onEditorStateChange = (editorState) => {
this.setState({
editorState,
});
};
getDetail = () => {
// 返回对应数据的html格式文本
return draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))
}
// 上传图片回调函数
uploadImageCallBack = (file) => {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/manage/img/upload');
xhr.setRequestHeader('Authorization', 'Client-ID XXXXX');
const data = new FormData();
data.append('image', file);
xhr.send(data);
xhr.addEventListener('load', () => {
const response = JSON.parse(xhr.responseText);
resolve({ data: { link: response.data.url } });
});
xhr.addEventListener('error', () => {
const error = JSON.parse(xhr.responseText);
reject(error);
});
}
);
}
render() {
const { editorState } = this.state;
return (
<div>
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
// 编辑样式
editorStyle={{ border: '1px solid ', minHeight: 200, paddingLeft: 10 }}
onEditorStateChange={this.onEditorStateChange}
// 自定义上传图片
toolbar={{
image: { uploadCallback: this.uploadImageCallBack, alt: { present: true, mandatory: true } },
}}
/>
</div>
);
}
}
9、父组件获得子组件的值
/**
* 父组件
*/
constructor(props) {
super(props)
// 创建容器
this.pw = React.createRef()
this.editor = React.createRef()
}
const imgs = this.pw.current.getImgs()
/**
* 子组件
*/
static propTypes = {
detail: PropTypes.string
}
getDetail = () => {
// 返回对应数据的html格式文本
return draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))
}
六、角色管理
1、含单选按钮的表格
<Table
bordered
rowKey='_id'
dataSource={roles}
columns={this.columns}
pagination={{
defaultPageSize: PAGE_SIZE,
}}
rowSelection={{
type: 'radio',
selectedRowKeys: [role._id],
onSelect: (role) => {
this.setState({
role
})
}
}}
onRow={this.onRow}
/>
onRow = (role) => {
return {
onClick: event => {// 点击行
console.log('onRow()', role)
this.setState({
role
})
},
}
}
网友评论