這是一個比較完整的簡單的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;