美文网首页
【教程】react实战

【教程】react实战

作者: giraffecode9668 | 来源:发表于2019-11-17 00:46 被阅读0次

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)
                }
        }

asyncawait的搭配使用,可以优化代码的格式,作用同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;

参考:https://www.cnblogs.com/qingchunshiguang/p/8011103.html

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
                })

            },
        }
    }

相关文章

网友评论

      本文标题:【教程】react实战

      本文链接:https://www.haomeiwen.com/subject/kzwuictx.html