这是一个比较完整的简单的react移动端项目,提及来页面少,其实,构思如果精巧,也并不容易作
先放源码:https://github.com/bailicangdu/react-pxq
接下来咱们简单的看看代码
项目有用到react-redux和axios处理数据css
//根index.js引用了一些基本的方法 //定义了渲染组件 <Component /> import React from 'react'; import ReactDOM from 'react-dom'; import Route from './router/'; import FastClick from 'fastclick'; import registerServiceWorker from './registerServiceWorker'; import { AppContainer } from 'react-hot-loader'; import {Provider} from 'react-redux'; import store from '@/store/store'; import './utils/setRem'; import './style/base.css'; //解决tab穿透的问题 FastClick.attach(document.body); // 监听state变化 // store.subscribe(() => { // console.log('store发生了变化'); // }); const render = Component => { ReactDOM.render( //绑定redux、热加载 <Provider store={store}> <AppContainer> <Component /> </AppContainer> </Provider>, document.getElementById('root'), ) } render(Route); // Webpack Hot Module Replacement API if (module.hot) { module.hot.accept('./router/', () => { render(Route); }) } registerServiceWorker();
//src/utils/setRem.js //定义的是单位的转换 (function(psdw){ var dpr=0 , rem=0 , scale=0; var htmlDOM=document.documentElement; dpr=window.devicePixelRatio; var currentWidth=htmlDOM.clientWidth; scale=currentWidth/psdw; rem=psdw/10; rem=rem*scale; htmlDOM.style.fontSize=rem+'px'; htmlDOM.setAttribute('data-dpr',dpr) })(750)
//src/store/store.js //与store相对的就是reducer和action //这里用到的是react-thunk import {createStore, combineReducers, applyMiddleware} from 'redux'; import * as home from './home/reducer'; import * as production from './production/reducer'; import thunk from 'redux-thunk'; let store = createStore( combineReducers({...home, ...production}), applyMiddleware(thunk) ); export default store;
//reducer里面主要是纯函数 //src/store/home/reducer.js import * as home from './action-type'; let defaultState = { orderSum: '', //金额 name: '', //姓名 phoneNo: '', //手机号 imgpath: '', //图片地址 } // 首页表单数据 export const formData = (state = defaultState , action = {}) => { switch(action.type){ case home.SAVEFORMDATA: return {...state, ...{[action.datatype]: action.value}}; case home.SAVEIMG: return {...state, ...{imgpath: action.path}}; case home.CLEARDATA: return {...state, ...defaultState}; default: return state; } }
//src/store/production/reducer.js import * as pro from './action-type'; import Immutable from 'immutable'; let defaultState = { /** * 商品数据 * @type {Array} * example: [{ * product_id: 1, 商品ID * product_name: "PaiBot(2G/32G)", 商品名称 * product_price: 2999, 商品价格 * commission: 200, 佣金 * selectStatus: false, 是否选择 * selectNum: 0, 选择数量 * }] */ dataList: [], } export const proData = (state = defaultState, action) => { let imuDataList; let imuItem; switch(action.type){ case pro.GETPRODUCTION: return {...state, ...action} case pro.TOGGLESELECT: //避免引用类型数据,使用immutable进行数据转换 imuDataList = Immutable.List(state.dataList); imuItem = Immutable.Map(state.dataList[action.index]); imuItem = imuItem.set('selectStatus', !imuItem.get('selectStatus')); imuDataList = imuDataList.set(action.index, imuItem); // redux必须返回一个新的state return {...state, ...{dataList: imuDataList.toJS()}}; case pro.EDITPRODUCTION: //避免引用类型数据,使用immutable进行数据转换 imuDataList = Immutable.List(state.dataList); imuItem = Immutable.Map(state.dataList[action.index]); imuItem = imuItem.set('selectNum', action.selectNum); imuDataList = imuDataList.set(action.index, imuItem); // redux必须返回一个新的state return {...state, ...{dataList: imuDataList.toJS()}}; // 清空数据 case pro.CLEARSELECTED: imuDataList = Immutable.fromJS(state.dataList); for (let i = 0; i < state.dataList.length; i++) { imuDataList = imuDataList.update(i, item => { item = item.set('selectStatus', false); item = item.set('selectNum', 0); return item }) } return {...state, ...{dataList: imuDataList.toJS()}}; default: return state; } }
//action-type里面是对象 //src/store/production/action-type.js // 保存商品数据 export const GETPRODUCTION = 'GETPRODUCTION'; // 选择商品 export const TOGGLESELECT = 'TOGGLESELECT'; // 编辑商品 export const EDITPRODUCTION = 'EDITPRODUCTION'; // 清空选择 export const CLEARSELECTED = 'CLEARSELECTED';
//action 里面经过dispatch把数据发送给其余数据 import * as pro from './action-type'; import API from '@/api/api'; // 初始化获取商品数据,保存至redux export const getProData = () => { // 返回函数,异步dispatch return async dispatch => { try{ let result = await API.getProduction(); result.map(item => { item.selectStatus = true; item.selectNum = 0; return item; }) dispatch({ type: pro.GETPRODUCTION, dataList: result, }) }catch(err){ console.error(err); } } } // 选择商品 export const togSelectPro = index => { return { type: pro.TOGGLESELECT, index, } } // 编辑商品 export const editPro = (index, selectNum) => { return { type: pro.EDITPRODUCTION, index, selectNum, } } // 清空选择 export const clearSelected = () => { return { type: pro.CLEARSELECTED, } }
//src/store/home/action-type.js // 保存表单数据 export const SAVEFORMDATA = 'SAVEFORMDATA'; // 保存图片 export const SAVEIMG = 'SAVEIMG'; // 清空数据 export const CLEARDATA = 'CLEARDATA';
//src/store/home/action.js import * as home from './action-type'; // 保存表单数据 export const saveFormData = (value, datatype) => { return { type: home.SAVEFORMDATA, value, datatype, } } // 保存图片地址 export const saveImg = path => { return { type: home.SAVEIMG, path, } } // 保存图片地址 export const clearData = () => { return { type: home.CLEARDATA, } }
//工具函数中定义了异步组件 //src/utils/asyncComponent.jsx import React, { Component } from "react"; export default function asyncComponent(importComponent) { class AsyncComponent extends Component { constructor(props) { super(props); this.state = { component: null }; } async componentDidMount() { const { default: component } = await importComponent(); this.setState({component}); } render() { const C = this.state.component; return C ? <C {...this.props} /> : null; } } return AsyncComponent; }
//路由部门,定义首页就home页面 //src/router/index.js //路由配置是异步加载的 import React, { Component } from 'react'; import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; import asyncComponent from '@/utils/asyncComponent'; import home from "@/pages/home/home"; const record = asyncComponent(() => import("@/pages/record/record")); const helpcenter = asyncComponent(() => import("@/pages/helpcenter/helpcenter")); const production = asyncComponent(() => import("@/pages/production/production")); const balance = asyncComponent(() => import("@/pages/balance/balance")); // react-router4 再也不推荐将全部路由规则放在同一个地方集中式路由,子路由应该由父组件动态配置,组件在哪里匹配就在哪里渲染,更加灵活 export default class RouteConfig extends Component{ render(){ return( <HashRouter> <Switch> <Route path="/" exact component={home} /> <Route path="/record" component={record} /> <Route path="/helpcenter" component={helpcenter} /> <Route path="/production" component={production} /> <Route path="/balance" component={balance} /> <Redirect to="/" /> </Switch> </HashRouter> ) } }
有公共组件header和alerthtml
//header组件 //src/components/header/header.jsx import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import './header.less'; export default class PublicHeader extends Component{ static propTypes = { record: PropTypes.any, title: PropTypes.string.isRequired, confirm: PropTypes.any, } state = { navState: false, //导航栏是否显示 }; // 切换左侧导航栏状态 toggleNav = () => { this.setState({navState: !this.state.navState}); } // css动画组件设置为目标组件 FirstChild = props => { const childrenArray = React.Children.toArray(props.children); return childrenArray[0] || null; } shouldComponentUpdate(nextProps, nextState) { return !is(fromJS(this.props), fromJS(nextProps))|| !is(fromJS(this.state),fromJS(nextState)) } render(){ return( <header className="header-container"> <span className="header-slide-icon icon-catalog" onClick={this.toggleNav}></span> <span className="header-title">{this.props.title}</span> { this.props.record&&<NavLink to="/record" exact className="header-link icon-jilu"></NavLink> } { this.props.confirm&&<NavLink to="/" exact className="header-link header-link-confim">肯定</NavLink> } <ReactCSSTransitionGroup component={this.FirstChild} transitionName="nav" transitionEnterTimeout={300} transitionLeaveTimeout={300}> { this.state.navState && <aside key='nav-slide' className="nav-slide-list" onClick={this.toggleNav}> <NavLink to="/" exact className="nav-link icon-jiantou-copy-copy">首页</NavLink> <NavLink to="/balance" exact className="nav-link icon-jiantou-copy-copy">提现</NavLink> <NavLink to="/helpcenter" exact className="nav-link icon-jiantou-copy-copy">帮助中心</NavLink> </aside> } </ReactCSSTransitionGroup> </header> ); } }
//src/components/alert/alert.jsx import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { is, fromJS } from 'immutable'; import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import './alert.less'; export default class Alert extends Component{ static propTypes = { closeAlert: PropTypes.func.isRequired, alertTip: PropTypes.string.isRequired, alertStatus: PropTypes.bool.isRequired, } // css动画组件设置为目标组件 FirstChild = props => { const childrenArray = React.Children.toArray(props.children); return childrenArray[0] || null; } // 关闭弹框 confirm = () => { this.props.closeAlert(); } shouldComponentUpdate(nextProps, nextState){ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } render(){ return ( <ReactCSSTransitionGroup component={this.FirstChild} transitionName="alert" transitionEnterTimeout={300} transitionLeaveTimeout={300}> { this.props.alertStatus&&<div className="alert-con"> <div className="alert-context"> <div className="alert-content-detail">{this.props.alertTip}</div> <TouchableOpacity className="confirm-btn" clickCallBack={this.confirm}/> </div> </div> } </ReactCSSTransitionGroup> ); } }
//src/pages/home/home.jsx import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { is, fromJS } from 'immutable'; import PropTypes from 'prop-types'; import API from '@/api/api'; import envconfig from '@/envconfig/envconfig'; import { saveFormData, saveImg, clearData } from '@/store/home/action'; import { clearSelected } from '@/store/production/action'; import PublicHeader from '@/components/header/header'; import PublicAlert from '@/components/alert/alert'; import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; import mixin, { padStr } from '@/utils/mixin'; import './home.less'; @mixin({padStr}) class Home extends Component { static propTypes = { formData: PropTypes.object.isRequired, saveFormData: PropTypes.func.isRequired, saveImg: PropTypes.func.isRequired, clearData: PropTypes.func.isRequired, clearSelected: PropTypes.func.isRequired, } state = { alertStatus: false, //弹框状态 alertTip: '', //弹框提示文字 } /** * 已选择的商品数据 * @type {Array} */ selectedProList = []; /** * 将表单数据保存至redux,保留状态 * @param {string} type 数据类型 orderSum||name||phoneNo * @param {object} event 事件对象 */ handleInput = (type, event) => { let value = event.target.value; switch(type){ case 'orderSum': value = value.replace(/\D/g, ''); break; case 'name': break; case 'phoneNo': value = this.padStr(value.replace(/\D/g, ''), [3, 7], ' ', event.target); break; default:; } this.props.saveFormData(value, type); } /* 上传图片,并将图片地址存到redux,保留状态 */ uploadImg = async event => { try{ let formdata = new FormData(); formdata.append('file', event.target.files[0]); let result = await API.uploadImg({data: formdata}); this.props.saveImg(envconfig.imgUrl + result.image_path); console.log(result); }catch(err){ console.error(err); } } // 提交表单 sumitForm = () => { const {orderSum, name, phoneNo} = this.props.formData; let alertTip = ''; if(!orderSum.toString().length){ alertTip = '请填写金额'; }else if(!name.toString().length){ alertTip = '请填写姓名'; }else if(!phoneNo.toString().length){ alertTip = '请填写正确的手机号'; }else{ alertTip = '添加数据成功'; this.props.clearSelected(); this.props.clearData(); } this.setState({ alertStatus: true, alertTip, }) } // 关闭弹款 closeAlert = () => { this.setState({ alertStatus: false, alertTip: '', }) } // 初始化数据,获取已选择的商品 initData = props => { this.selectedProList = []; props.proData.dataList.forEach(item => { if(item.selectStatus && item.selectNum){ this.selectedProList.push(item); } }) } componentWillReceiveProps(nextProps){ if(!is(fromJS(this.props.proData), fromJS(nextProps.proData))){ this.initData(nextProps); } } shouldComponentUpdate(nextProps, nextState) { return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state),fromJS(nextState)) } componentWillMount(){ this.initData(this.props); } render() { return ( <main className="home-container"> <PublicHeader title='首页' record /> <p className="common-title">请录入您的信息</p> <form className="home-form"> <div className="home-form-tiem"> <span>销售金额:</span> <input type="text" placeholder="请输入订单金额" value={this.props.formData.orderSum} onChange={this.handleInput.bind(this, 'orderSum')}/> </div> <div className="home-form-tiem"> <span>客户姓名:</span> <input type="text" placeholder="请输入客户姓名" value={this.props.formData.name} onChange={this.handleInput.bind(this, 'name')}/> </div> <div className="home-form-tiem"> <span>客户电话:</span> <input type="text" maxLength="13" placeholder="请输入客户电话" value={this.props.formData.phoneNo} onChange={this.handleInput.bind(this, 'phoneNo')}/> </div> </form> <div> <p className="common-title">请选择销售的产品</p> <Link to="/production" className="common-select-btn"> { this.selectedProList.length ? <ul className="selected-pro-list"> { this.selectedProList.map((item, index) => { return <li key={index} className="selected-pro-item ellipsis">{item.product_name}x{item.selectNum}</li> }) } </ul>:'选择产品' } </Link> </div> <div className="upload-img-con"> <p className="common-title">请上传发票凭证</p> <div className="file-lable"> <span className="common-select-btn">上传图片</span> <input type="file" onChange={this.uploadImg}/> </div> <img src={this.props.formData.imgpath} className="select-img" alt=""/> </div> <TouchableOpacity className="submit-btn" clickCallBack={this.sumitForm} text="提交" /> <PublicAlert closeAlert={this.closeAlert} alertTip={this.state.alertTip} alertStatus={this.state.alertStatus} /> </main> ); } } export default connect(state => ({ formData: state.formData, proData: state.proData, }), { saveFormData, saveImg, clearData, clearSelected, })(Home);
//src/utils/mixin.js export default methods => { return target => { Object.assign(target.prototype, methods); } } /** * 字符串填充函数 * @param {string} value 目标字符串 * @param {array} position 须要填充的位置 * @param {string} padstr 填充字符串 * @return {string} 返回目标字符串 */ export const padStr = (value, position, padstr, inputElement) => { position.forEach((item, index) => { if (value.length > item + index) { value = value.substring(0, item + index) + padstr + value.substring(item + index) } }) value = value.trim(); // 解决安卓部分浏览器插入空格后光标错位问题 requestAnimationFrame(() => { inputElement.setSelectionRange(value.length, value.length); }) return value; }
//帮助中心 //src/pages/helpcenter/helpcenter.jsx import React, { Component } from 'react'; import PublicHeader from '@/components/header/header'; import { is, fromJS } from 'immutable'; import './helpcenter.less'; export default class HelpCenter extends Component { shouldComponentUpdate(nextProps, nextState){ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } render(){ return ( <main> <PublicHeader title="帮助中心" record /> <article className="context-con"> <h2>介绍</h2> <p>本项目主要用于理解 react 和 redux 的编译方式,以及 react + redux 之间的配合方式</p> <h2>技术要点</h2> <p>react:v16.2</p> <p>redux:v3.7</p> <p>webpack:v3.8</p> <p>react-router:v4.2</p> <p>ES 6/7/8</p> <p>code split</p> <p>hot loader</p> <p>axios:v0.17</p> <p>less:v2.7</p> <p>immutable:v3.8</p> <p>项目地址 <a href="https://github.com/bailicangdu/react-pxq">github</a></p> </article> </main> ) } }
//src/pages/production/production.jsx import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import { connect } from 'react-redux'; import { getProData, togSelectPro, editPro } from '@/store/production/action'; import PropTypes from 'prop-types'; import PublicHeader from '@/components/header/header'; import './production.less'; class Production extends Component{ static propTypes = { proData: PropTypes.object.isRequired, getProData: PropTypes.func.isRequired, togSelectPro: PropTypes.func.isRequired, editPro: PropTypes.func.isRequired, } /** * 添加或删减商品,交由redux进行数据处理,做为全局变量 * @param {int} index 编辑的商品索引 * @param {int} num 添加||删减的商品数量 */ handleEdit = (index, num) => { let currentNum = this.props.proData.dataList[index].selectNum + num; if(currentNum < 0){ return } this.props.editPro(index, currentNum); } // 选择商品,交由redux进行数据处理,做为全局变量 togSelect = index => { this.props.togSelectPro(index); } shouldComponentUpdate(nextProps, nextState) { return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } componentDidMount(){ if(!this.props.proData.dataList.length){ this.props.getProData(); } } render(){ return ( <main className="common-con-top"> <PublicHeader title='首页' confirm /> <section className="pro-list-con"> <ul className="pro-list-ul"> { this.props.proData.dataList.map((item, index) => { return <li className="pro-item" key={index}> <div className="pro-item-select" onClick={this.togSelect.bind(this, index)}> <span className={`icon-xuanze1 pro-select-status ${item.selectStatus? 'pro-selected': ''}`}></span> <span className="pro-name">{item.product_name}</span> </div> <div className="pro-item-edit"> <span className={`icon-jian ${item.selectNum > 0? 'edit-active':''}`} onClick={this.handleEdit.bind(this, index, -1)}></span> <span className="pro-num">{item.selectNum}</span> <span className={`icon-jia`} onClick={this.handleEdit.bind(this, index, 1)}></span> </div> </li> }) } </ul> </section> </main> ) } } export default connect(state => ({ proData: state.proData, }), { getProData, togSelectPro, editPro })(Production);
//src/pages/balance/balance.jsx import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import PublicHeader from '@/components/header/header'; import TouchableOpacity from '@/components/TouchableOpacity/TouchableOpacity'; import PublicAlert from '@/components/alert/alert'; import API from '@/api/api'; import './balance.less'; class BrokeRage extends Component{ state = { applyNum: '', //输入值 alertStatus: false, //弹框状态 alertTip: '', //弹框提示文字 balance: { //可提现金额 balance: 0, }, } // 初始化数据 initData = async () => { try{ let result = await API.getBalance(); console.log(result); this.setState({balance: result}); }catch(err){ console.error(err); } } /** * 格式化输入数据 * 格式为微信红包格式:最大 200.00 * @param {object} event 事件对象 */ handleInput = event => { let value = event.target.value; if((/^\d*?\.?\d{0,2}?$/gi).test(value)){ if((/^0+[1-9]+/).test(value)) { value = value.replace(/^0+/,''); } if((/^0{2}\./).test(value)) { value = value.replace(/^0+/,'0'); } value = value.replace(/^\./gi,'0.'); if(parseFloat(value) > 200){ value = '200.00'; } this.setState({applyNum: value}); } } /** * 提交判断条件 */ sumitForm = () => { let alertTip; if(!this.state.applyNum){ alertTip = '请输入提现金额'; }else if(parseFloat(this.state.applyNum) > this.state.balance.balance){ alertTip = '申请提现金额不能大于余额'; }else{ alertTip = '申请提现成功'; } this.setState({ alertStatus: true, alertTip, applyNum: '', }) } /* 关闭弹框 */ closeAlert = () => { this.setState({ alertStatus: false, alertTip: '', }) } shouldComponentUpdate(nextProps, nextState) { return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state),fromJS(nextState)) } componentDidMount(){ this.initData(); } render(){ return ( <main className="home-container"> <PublicHeader title='提现' record /> <section className="broke-main-content"> <p className="broke-header">您的可提现金额为:¥ {this.state.balance.balance}</p> <form className="broke-form"> <p>请输入提现金额(元)</p> <p>¥ <input type="text" value={this.state.applyNum} placeholder="0.00" onInput={this.handleInput} maxLength="5" /></p> </form> <TouchableOpacity className="submit-btn" clickCallBack={this.sumitForm} text="申请提现" /> </section> <PublicAlert closeAlert={this.closeAlert} alertTip={this.state.alertTip} alertStatus={this.state.alertStatus} /> </main> ); } } export default BrokeRage;
//src/pages/record/components/recordList.jsx import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import API from '@/api/api'; import './recordList.less'; class RecordList extends Component{ state = { recordData: [], } /** * 初始化获取数据 * @param {string} type 数据类型 */ getRecord = async type => { try{ let result = await API.getRecord({type}); this.setState({recordData: result.data||[]}) }catch(err){ console.error(err); } } componentWillReceiveProps(nextProps){ // 判断类型是否重复 let currenType = this.props.location.pathname.split('/')[2]; let type = nextProps.location.pathname.split('/')[2]; if(currenType !== type){ this.getRecord(type); } } shouldComponentUpdate(nextProps, nextState){ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } componentWillMount(){ let type = this.props.location.pathname.split('/')[2]; this.getRecord(type); } render(){ return ( <div> {/* 这个记录页面与数据的渲染造成总体的组件 */} <ul className="record-list-con"> { this.state.recordData.map((item, index) => { return <li className="record-item" key={index}> <section className="record-item-header"> <span>建立时间:{item.created_at}</span> <span>{item.type_name}</span> </section> <section className="record-item-content"> <p><span>用户名:</span>{item.customers_name}   {item.customers_phone}</p> <p><span>商 品:</span>{item.product[0].product_name}</p> <p><span>金 额:</span>{item.sales_money}   佣金:{item.commission}</p> </section> <p className="record-item-footer">等待管理员审核,审核经过后,佣金将结算至帐户</p> </li> }) } </ul> </div> ); } } export default RecordList;
//src/pages/record/record.jsx import React, { Component } from 'react'; import { is, fromJS } from 'immutable'; import { NavLink, Switch, Route, Redirect } from 'react-router-dom'; import PublicHeader from '@/components/header/header'; import RecordList from './components/recordList'; import './record.less'; class Record extends Component { state = { flagBarPos: '17%', } /** * 设置头部底部标签位置 * @param {string} type 数据类型 */ setFlagBarPos = type => { let flagBarPos; switch(type){ case 'passed': flagBarPos = '17%'; break; case 'audited': flagBarPos = '50%'; break; case 'failed': flagBarPos = '83%'; break; default: flagBarPos = '17%'; } this.setState({flagBarPos}) } componentWillReceiveProps(nextProps){ // 属性变化时设置头部底部标签位置 let currenType = this.props.location.pathname.split('/')[2]; let type = nextProps.location.pathname.split('/')[2]; if(currenType !== type){ this.setFlagBarPos(type); } } shouldComponentUpdate(nextProps, nextState){ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState)) } componentWillMount(){ // 初始化设置头部底部标签位置 let type = this.props.location.pathname.split('/')[2]; this.setFlagBarPos(type); } render() { return ( <main className="common-con-top"> <PublicHeader title='记录' /> <section className="record-nav-con"> <nav className="record-nav"> <NavLink to={`${this.props.match.path}/passed`} className="nav-link">已经过</NavLink> <NavLink to={`${this.props.match.path}/audited`} className="nav-link">待审核</NavLink> <NavLink to={`${this.props.match.path}/failed`} className="nav-link">未经过</NavLink> </nav> <i className="nav-flag-bar" style={{left: this.state.flagBarPos}}></i> </section> {/* 子路由在父级配置,react-router4新特性,更加灵活 */} <Switch> <Route path={`${this.props.match.path}/:type`} component={RecordList} /> <Redirect from={`${this.props.match.path}`} to={`${this.props.match.path}/passed`} exact component={RecordList} /> </Switch> </main> ); } } export default Record;