12.React实战开发--团购webapp

源码:https://github.com/Ching-Lee/react_shopping

1.环境搭建

1.1 使用react脚本架搭建项目(详情参见第一节)
  • 创建项目:
create-react-app dianping
  • 安装react-router2.8版本


  • 使用的是react-router2.8版本,保证react和react-dom都是15的版本
    ,否则会报错。
   "react": "^15.6.2",
    "react-dom": "^15.6.2",
    "react-router": "^2.8.1",

创建路由routerMap.js

import React from 'react';
import {Router,hashHistory,Route,IndexRoute} from 'react-router';
import Home from './containers/home';
import App from './App';
export default class RouterMap extends React.Component{
    render(){
        return(
            <Router history={hashHistory}>
                <Route path='/' component={App}>
                    <IndexRoute component={Home}/>
                </Route>
            </Router>
        );
    }
}

父组件App.js

import React from 'react';

import './App.css';

class App extends React.Component {
    render() {
        return (
            <div>
                {this.props.children}
            </div>
        );
    }
}

export default App;
1.2 引入图标

进入官网https://icomoon.io/app/#/select,导入提供的图标图片,可以生成图标。

1.3 后端数据模拟
  • 使用koa
npm install koa koa-body koa-router --save-dev
  • 修改package.json


  • mock目录



    ad.js中是首页广告的json数据

module.exports = [
    {
        title: '暑假5折',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191639092-2000037796.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '特价出国',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191648124-298129318.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '亮亮车',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191653983-1962772127.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '学钢琴',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191700420-1584459466.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '电影',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191706733-367929553.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '旅游热线',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191713186-495002222.png',
        link: 'http://www.imooc.com/wap/index'
    }
]

list.js中是首页-推荐列表

module.exports = {
    hasMore: true,
    data: [
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201638030-473660627.png',
            title: '汉堡大大',
            subTitle: '叫我汉堡大大,还你多彩口味',
            price: '28',
            distance: '120m',
            mumber: '389'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201645858-1342445625.png',
            title: '北京开源饭店',
            subTitle: '[望京]自助晚餐',
            price: '98',
            distance: '140m',
            mumber: '689'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201652952-1050532278.png',
            title: '服装定制',
            subTitle: '原价xx元,现价xx元,可修改一次',
            price: '1980',
            distance: '160',
            mumber: '106'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201700186-1351787273.png',
            title: '婚纱摄影',
            subTitle: '免费试穿,拍照留念',
            price: '2899',
            distance: '160',
            mumber: '58'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201708124-1116595594.png',
            title: '麻辣串串烧',
            subTitle: '双人免费套餐等你抢购',
            price: '0',
            distance: '160',
            mumber: '1426'
        }
    ]
}

server.js模拟服务端

const Koa = require('koa');
const Router = require('koa-router');
const KoaBody=require('koa-body');
const app = new Koa();
const router = new Router();
const koaBody=new KoaBody();

// 首页 —— 广告(超值特惠)
const homeAdData = require('./home/ad.js');

// 首页 —— 推荐列表(猜你喜欢)
const homeListData = require('./home/list.js');


router.get('/api/homead', async (ctx) => {
    ctx.body = homeAdData
}).get('/api/homelist/:city/:page', async (ctx) => {
    // 参数
    const params = ctx.params;
    const paramsCity = params.city;
    const paramsPage = params.page;

    console.log('当前城市:' + paramsCity);
    console.log('当前页数:' + paramsPage);

    ctx.body = homeListData;
});


/*
router.post('/api/post',koaBody,async (ctx) => {
    ctx.body = JSON.stringify(this.request.body);
});

*/

// 开始服务并生成路由
app.use(router.routes(), router.allowedMethods());
app.listen(3000);
  • 服务开启



  • 服务器运行在3000端口,而网站前端运行在3001端口,我们发请求时是在3001端口,为了在3001端口也能获得同样的结果,需要在package.json中实现代理转发,将所有api/下的请求转发到3000端口。


2.Home页面

2.1 header头部

import React from 'react';
import '../../static/css/font.css';
import './homeheader.css'

export default class HomeHeader extends React.Component{
    render(){
        return(
            <header className='home_header'>
                <div className='left_header'>
                    <span>深圳</span>
                    <i className='icon-angle-down'/>
                </div>

                <div className='middle_header'>
                    <i className='icon-search'/>
                    &nbsp;
                    <input type='text' placeholder='请输入关键字'/>
                </div>

                <div className='right_header'>
                    <i className='icon-user'/>
                </div>
            </header>
        );
    }
}

使用了flex布局,两端分别靠左右,中间自适应。

.home_header{
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: rgb(233,32,61);
    padding: 10px;
    color:#fff;
    font-size: 16px;
    line-height: 1;
}
.left_header{
    width: 60px;
}
/*中间搜索框*/
.middle_header{
    width: 70%;
    border-radius: 15px;
    background-color: #fff;
    padding: 5px;
    overflow: hidden;
}

.middle_header i{
    color:#ccc
}

.middle_header input{
    font-size: 16px;
    font-weight: normal;
    padding: 0;
    border: 0;
}

.right_header{
    width:30px;
    text-align: right;
}
2.2 category分类
npm install react swipe-js-iso react-swipe

  • 在ReactSwipe中的每一个div,表示一个轮播面板。
    每个div都是由ul和li构成,ul采用自适应布局flex。
    我们自己定义了一个组件CategoryItem,用于表示每一项li,包括图片和文字。
    圆点circle类,表示轮播图下方的点
    1)CategoryItem组件


import React from 'react'

export default class CategoryItem extends React.Component {
    constructor() {
        super();
    }

    render() {
        let psty={
            textAlign:'center',
            margin:0,
            fontSize:'14px'

        };

        let imgsty={
          width:'40px',
        };
        let listy={
            textAlign:'center',
            width:'60px',
            marginLeft:'5px',
            marginRight:'5px',
            listStyle:'none'
        };
        return (
            <li style={listy}>
                <img src={this.props.src} style={imgsty}/>
                <p style={psty}>{this.props.text}</p>
            </li>
        );
    }
}

2)Category组件:

import React from 'react';
import ReactSwipe from 'react-swipe';
import './Category.css'
import CategoryItem from './category_item';

export default class Category extends React.Component{
    constructor(){
        super();
        this.state={index:0};
    }
    render(){
        //当我们滑动轮播图,会返回一个索引值index,表示当前页面
        //index值为0,1,2
        let opt={
            continuous: false,
            callback: index=> {
                this.setState({index:index});
            }

        };


        return(
            <div>
                <ReactSwipe className="carousel" swipeOptions={opt}>
                    <div>
                        <ul>
                            <CategoryItem src={require('../../static/image/icon/01food_icon_1.png')} text='美食' />
                            <CategoryItem src={require('../../static/image/icon/01movie_icon_2.png')} text='电影' />
                            <CategoryItem src={require('../../static/image/icon/01hotel_icon_3.png')} text='酒店' />
                            <CategoryItem src={require('../../static/image/icon/01entertainment_icon_4.png')} text='休闲娱乐' />
                            <CategoryItem src={require('../../static/image/icon/01fast_food_icon_5.png')} text='外卖' />
                            <CategoryItem src={require('../../static/image/icon/01hot_pot_icon_6.png')} text='火锅' />
                            <CategoryItem src={require('../../static/image/icon/01beautiful_icon_7.png')} text='丽人' />
                            <CategoryItem src={require('../../static/image/icon/01travel_icon_8.png')} text='度假出行' />
                            <CategoryItem src={require('../../static/image/icon/01massage_icon_9.png')} text='足疗按摩' />
                            <CategoryItem src={require('../../static/image/icon/01around_travel_icon_10png.png')} text='周边游' />
                        </ul>

                    </div>
                    <div>
                        <ul>
                            <CategoryItem src={require('../../static/image/icon/02景点icon_1.png')} text='景点' />
                            <CategoryItem src={require('../../static/image/icon/02KTVicon_2.png')} text='ktv' />
                            <CategoryItem src={require('../../static/image/icon/02购物icon_3.png')} text='购物' />
                            <CategoryItem src={require('../../static/image/icon/02生活服务icon_4.png')} text='生活服务' />
                            <CategoryItem src={require('../../static/image/icon/02运动健身icon_5.png')} text='健身' />
                            <CategoryItem src={require('../../static/image/icon/02美发icon_6.png')} text='美发' />
                            <CategoryItem src={require('../../static/image/icon/02亲子icon_7.png')} text='亲子' />
                            <CategoryItem src={require('../../static/image/icon/02小吃快餐icon_8.png')} text='小吃快餐' />
                            <CategoryItem src={require('../../static/image/icon/02自助餐icon_9.png')} text='自助餐' />
                            <CategoryItem src={require('../../static/image/icon/02酒吧icon_10.png')} text='酒吧' />
                        </ul>
                    </div>
                    <div>
                        <ul>
                            <CategoryItem src={require('../../static/image/icon/03日本菜icon_1.png')} text='日本菜' />
                            <CategoryItem src={require('../../static/image/icon/03SPAicon_2.png')} text='SPA' />
                            <CategoryItem src={require('../../static/image/icon/03结婚icon_3.png')} text='结婚' />
                            <CategoryItem src={require('../../static/image/icon/03学习培训icon_4.png')} text='学习培训' />
                            <CategoryItem src={require('../../static/image/icon/03西餐icon_5.png')} text='西餐' />
                            <CategoryItem src={require('../../static/image/icon/03火车机票icon_6.png')} text='火车机票' />
                            <CategoryItem src={require('../../static/image/icon/03烧烤icon_7.png')} text='烧烤' />
                            <CategoryItem src={require('../../static/image/icon/03家装icon_8.png')} text='家装' />
                            <CategoryItem src={require('../../static/image/icon/03宠物icon_9.png')} text='宠物' />
                            <CategoryItem src={require('../../static/image/icon/03全部分类icon_10.png')} text='全部分类' />
                        </ul>
                    </div>
                </ReactSwipe>

                <ul className='circle'>
                    <li className={this.state.index===0?"selected":''}/>
                    <li className={this.state.index===1?"selected":''}/>
                    <li className={this.state.index===2?"selected":''}/>
                </ul>
            </div>

        );
    }
}

'./Category.css'

.carousel ul{
    padding-left: 0px;
    display: flex;
    justify-content: space-around;
    flex-wrap: wrap;
}

.circle{
    text-align: center;
    padding-left: 0px;
}

.circle li{
    list-style: none;
    width: 8px;
    height: 8px;
    border-radius: 4px;
    background-color: #ccc;
    margin:0 4px;
    display: inline-block;
}

li.selected{
    background-color: red;
}

2.3 超值特惠模块

  • 需要数据交互,首先开启后台数据。

  • 安装fetch框架用于发送Ajax请求
npm install fetch --save
  • recomment组件
    1)首先在componentWillMount()方法中向服务器发起请求,获取json,
    将获得的json值赋给this.state.ad。
    2)开始渲染时,将每一项使用map函数,包装成li,li的宽为120px。
    li中包括标题和图片,使其内容居中,使用text-align:center。
    3)将生成的adlist放入ul中,ul采用flex布局,设置justifyContent:'space-around'均匀分布,设置flexWrap:'wrap'自动换行。
import React from 'react';

export default class Recomment extends React.Component {
    constructor() {
        super();
        this.state = {ad: ''}
    }

    componentWillMount() {
        let myFetchOptions = {method: 'GET'};
        fetch('/api/homead', myFetchOptions)
            .then(response => response.json())
            .then(json => this.setState({ad: json}));
    };

    render() {
        const ad = this.state.ad;
        let colors=['green','blue','yellow','orange','red','pink'];
        const adList = ad.length ?
            ad.map((adItem, index) => (
                <li key={index} style={{width:'120px',listStyle:'none',paddingTop:'10px',textAlign:'center'}}>
                    <h4 style={{color:colors[index],margin:'0px'}}>{adItem.title}</h4>
                    <img src={adItem.img} style={{width:'120px'}} />
                </li>
            ))


            : '加载中';
        return (
            <div>
                <h3 style={{padding:'10px',borderTop:'1px #F8F8F8 solid',borderBottom:'1px #F8F8F8 solid',margin:'0px'}}>超值特惠</h3>
                <ul style={{paddingLeft:'0px',display:'flex',flexWrap:'wrap',justifyContent:'space-around'}}>
                    {adList}
                </ul>
            </div>
        );
    }
}
  • 遇到问题:
    本地调试的时候,图片src引用了第三方网站的图片资源,导致控制台出现了如下的报错:

解决方法:
在html中添加

<meta name="referrer" content="no-referrer" />

2.4 猜你喜欢模块


  • 首先在 componentWillMount()中向后台发起请求,获取json,将json中hasmore赋值给state中hasmore,将json中的data赋值给state中的list。
  • 渲染时遍历list,返回likelist。
    div是由img和content(div)两个部分构成。
    img:向左浮动,content也向左浮动,同时其父组件要清除浮动。
    content中分为上中下三个部分:
    1)上面的div中包括两个span,一个向左浮动,一个向右浮动,父组件清除浮动。
    2)中间是一个<p>。
    3)下面也是两个span,一个向左浮动,一个向右浮动。
import React from 'react';
import './likelist.css'

export default class LikeList extends React.Component {
    constructor() {
        super();
        this.state = {list: '',hasMore:false,page:0}
    }

    componentWillMount() {
        let myFetchOptions = {method: 'GET'};
        fetch('/api/homelist/'+this.props.city+'/'+this.state.page, myFetchOptions)
            .then(response => response.json())
            .then(json => this.setState({list: json.data,hasMore:json.hasMore}));
    };

    render(){
        const like=this.state.list;
        let likeList=like.length?
            like.map((likeItem,index)=>(
                <div key={index} className='like_item clearfix'>
                    <img src={likeItem.img} className='left'/>
                    <div className='content'>
                        <div className='clearfix'>
                            <span className='title'>{likeItem.title}</span>
                            <span className='distance'>{likeItem.distance}</span>
                        </div>

                        <p className='subTitle'>{likeItem.subTitle}</p>

                        <div className='clearfix'>
                            <span className='price'>¥{likeItem.price}</span>
                            <span className='mumber'>已售{likeItem.mumber}</span>
                        </div>

                    </div>
                </div>
            ))

            :'加载中';
        return(
            <div>
                <h3 style={{padding:'10px',borderTop:'1px #F8F8F8 solid',borderBottom:'1px #F8F8F8 solid',margin:'0px'}}>猜你喜欢</h3>
                {likeList}
            </div>

        );
    }

}
.like_item{
    width:100%;
    padding: 10px;
    border-bottom: 1px #cccccc solid;
}
.clearfix:after{
    display: block;
    clear: both;
    content: '';
}
.left{
    float: left;
    width:125px;
}

.content{
    float:left;
    width:220px;
    padding:0 10px ;
}

.title{
    float:left;
    font-size: 16px;
    font-weight: bold;

}

.distance{
    float: right;
    font-size: 12px;
    line-height: 21px;
    color:grey;
}

.subTitle{
    margin-top:10px;
    margin-bottom: 1.5em;
    font-size: 14px;
}

.price{
    float:left;
    color:red;
    font-family: Arial;
    font-size: 20px;
    font-weight: bold;
}

.mumber{
    float: right;
    color:grey;
    font-size: 14px;
}
  • 点击按钮加载更多功能。



    1)LoadMore组件
    传递了isLoadingMore属性,用来表示是加载中还是加载更多。
    传递了一个方法,this.props.loadMoreFn(),用来实现点击加载更多后的事件。

import React from 'react';
export default class LoadMore extends React.Component{
    constructor(props){
        super(props);
    }

    handleClick(){
        this.props.loadMoreFn();
    }
    render(){
        return(
            <div style={{textAlign:'center',backgroundColor:'#F8F8F8'}}>
                {
                    this.props.isLoadingMore?
                        <span>加载中...</span>
                        :<span onClick={this.handleClick.bind(this)}>加载更多</span>
                }
            </div>
        );
    }
}

2)在likeList中state添加属性isLoadingMore,方法loadMoreData(),LoadMore组件

 this.state = {
            list: [], //存储列表信息
            page: 0, //请求的页码
            hasMore:false,
            isLoadingMore:false,
        }

//点击加载更多触发
    loadMoreData() {
        //记录状态
        this.setState({isLoadingMore: true});
        let page=this.state.page+1;
        //发送请求
        let myFetchOptions = {method: 'GET'};
        fetch('/api/homelist/' + this.props.city + '/' + page, myFetchOptions)
            .then(response => response.json())
            .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
        //设置page
        this.setState({page: page, isLoadingMore: false});
 return (
            <div>
                <h3 style={{
                    padding: '10px',
                    borderTop: '1px #F8F8F8 solid',
                    borderBottom: '1px #F8F8F8 solid',
                    margin: '0px'}}>
                    猜你喜欢
                </h3>
                {likeList}
                {
                    this.state.hasMore?
                        <LoadMore isLoadingMore={this.state.isLoadingMore} loadMoreFn={this.loadMoreData.bind(this)}/>
                        : ''
                }

            </div>

        );
  • 上拉就加载更多
    主要是监听滚轮事件,判断底部加载更多div和顶部的距离top,如果top比屏幕距离小,表示它被暴露出来,下滑加载数据。
    在LoadMore组件中添加方法
 componentDidMount(){
        const loadMoreFn=this.props.loadMoreFn;
        const wrapper=this.refs.wrapper;
        let timeoutId;
        function callback(){
            //得到加载更多div距离顶部的距离
           let top=wrapper.getBoundingClientRect().top;
           let windowHeight=window.screen.height;
           //如果top距离比屏幕距离小,说明加载更多被暴露
           if(top&&top<windowHeight)
               loadMoreFn();
        }
        //添加滚动事件监听
        window.addEventListener('scroll',function () {
            if(this.props.isLoadingMore)
                return;
            if(timeoutId)
                clearTimeout(timeoutId);
            //因为一滚动就会触发事件,我们希望50ms才触发一次
            timeoutId=setTimeout(callback,50);
        }.bind(this),false);
    }

3.选择城市页面

  • 因为城市信息是我们需要在各个组件中共享的信息,比如homeheader的city显示,猜你喜欢的部分也需要发送城市名称才能获取相应数据。所以我们首先需要搭建Redux数据流环境,参见第13节。
  • 在routerMap中添加路由
   <Router history={hashHistory}>
                <Route path='/' component={App}>
                    <IndexRoute component={Home}/>
                    <Route path='/city' component={City}/>
                </Route>
            </Router>
  • 跳转到城市选择页
    <Link>不会刷新页面,react和react-router监听hash的变化,然后js层重新渲染页面,
    里面并没有页面的请求。
    在之前写的HomeHeader组件中添加跳转链接
 <div className='left_header'>
                    <Link to='/city'>
                        <span>{this.props.cityName}</span>
                        <i className='icon-angle-down'/>
                    </Link>
 </div>
.left_header a{
    text-decoration:none;
    color:#fff;
}

.left_header a:hover{
    color:gray ;
}
  • city页面


import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as userInfoActions from '../action/userInfoActions'
import PageHeader from '../component/common/pageHeader'
class City extends React.Component{
    render(){
        return(
            <div>
                <PageHeader title='选择城市'/>
            </div>
        );
    }
}

// -------------------redux react 绑定--------------------

function mapStateToProps(state) {
    return {
        userinfo: state.userinfo
    }
}

//触发数据改变
function mapDispatchToProps(dispatch) {
    return {
        userInfoActions: bindActionCreators(userInfoActions, dispatch),
    }
}
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(City)
  • 头部组件PageHeader


import React from 'react';
import '../../static/css/font.css';
import './pageheader.css'

export default class PageHeader extends React.Component {

    constructor(props) {
        super(props);
    }

    clickHandle() {
        window.history.back();
    }


    render() {
        return (
            <div className='page_header'>
                <h1>{this.props.title}</h1>
                <i className='icon-chevron-left' onClick={this.clickHandle.bind(this)}/>
            </div>
        );
    }
}

首先p元素让他占一行,并且文字居中。之后i采用了绝对定位,采用绝对定位会脱离文档流。

.page_header{
    position: relative;
}
.page_header h1{
    text-align: center;
    background-color: rgb(233,32,61);;
    font-size: 16px;
    color:white;
    padding: 15px 10px;
    margin: 0;
}

.page_header i{
    position: absolute;
    left: 10px;
    top: 15px;
    color: white;
}
  • 显示当前城市



currentCity组件

import React from 'react';
import './currentCity.css'

export default class CurrentCity extends React.Component {

    constructor(props) {
        super(props);
    }


    render() {
        return (
            <div style={{paddingTop:'30px',paddingBottom:'30px',borderBottom:'1px #f1f1f1 solid',textAlign:'center'}}>
                <h2>{this.props.currentCity}</h2>
            </div>


        );
    }
}
  • 热门城市列表



    设计了一个cityList组件



    在cityList组件中涉及到当点击城市li时,会触发点击事件,这个事件需要调用父组件中的方法, changeCity(newCity),将新的城市更新到redux中,同时更新localstorage,最后跳转到首页。
    CityList组件
import React from 'react'
import './citylist.css'
export default class CityList extends React.Component{
    clickHandle(newcity){
        const changeFn=this.props.changeFn;
        changeFn(newcity);
    }
    render(){
        return(
            <div >
                <h3 style={{paddingLeft:'20px',fontSize:'16px'}}>热门城市</h3>
                <ul className='city_list'>
                    <li onClick={this.clickHandle.bind(this,'北京')}>北京</li>
                    <li onClick={this.clickHandle.bind(this,'上海')}>上海</li>
                    <li onClick={this.clickHandle.bind(this,'杭州')}>杭州</li>
                    <li onClick={this.clickHandle.bind(this,'深圳')}>深圳</li>
                    <li onClick={this.clickHandle.bind(this,'广州')}>广州</li>
                    <li onClick={this.clickHandle.bind(this,'西安')}>西安</li>
                    <li onClick={this.clickHandle.bind(this,'成都')}>成都</li>
                    <li onClick={this.clickHandle.bind(this,'长沙')}>长沙</li>
                    <li onClick={this.clickHandle.bind(this,'无锡')}>无锡</li>
                </ul>
            </div>
        );
    }
}
.city_list{
    display:flex;
    flex-wrap:wrap;
   justify-content:space-around;
    padding-left: 0px;

}

.city_list li{
    width:25%;
    list-style:none;
    border:#ccc 1px solid;
    text-align: center;
    margin-bottom: 1em;
}

city组件中的调用,及传递的方法

class City extends React.Component{
    changeCity(newCity){
        if(newCity==null)
            return;
      //修改redux
        const userinfo=this.props.userinfo;
        userinfo.cityName=newCity;
        this.props.userInfoActions.update(userinfo);
      //修改localstorage
        localStorage.cityName=newCity;

        //跳转到首页
        hashHistory.push('/');
    }
    render(){
        return(
            <div>
                <PageHeader title='选择城市'/>
                <CurrentCity currentCity={this.props.userinfo.cityName}/>
                <CityList changeFn={this.changeCity.bind(this)}/>
            </div>
        );
    }
}

4.搜索功能

4.1 种类跳转
  • 添加路由
<Route path='/search/:category(/:keyword)' component={Search}/>
  • 在Category页面为CategoryItem添加<Link>
 <Link to='/search/01food'><CategoryItem src={require('../../../static/image/icon/01food_icon_1.png')} text='美食'/></Link>

4.2 搜索跳转

HomeHeader组件中:
使用state来保存keyword搜索关键字,两个事件,onChange的时候更新state中的关键字,onKeyUp判断如果是enter,跳转页面,使用hashHistory.push

import {Link,hashHistory} from 'react-router'


export default class HomeHeader extends React.Component {
    constructor(){
        super();
        this.state={kwd:''}
    }
    ChangeHandle(e){
        let val=e.target.value;
        this.setState({
            kwd:val
        });
    }

    KeyUpHandle(e) {
        if (e.keyCode !== 13)
            return;
        hashHistory.push('/search/all/'+encodeURIComponent(this.state.kwd))
    }
......

  <input
                        type='text'
                        placeholder='请输入关键字'
                        onChange={this.ChangeHandle.bind(this)}
                        onKeyUp={this.KeyUpHandle.bind(this)}
                        value={this.state.kwd}
 />

4.3 search页面

  • 抽离SearchInput组件


    将这部分抽离出来复用
import React from 'react';
import './searchinput.css'
export default class HomeHeader extends React.Component {
    constructor() {
        super();
        this.state = {kwd: ''}
    }

    ChangeHandle(e) {
        let val = e.target.value;
        this.setState({
            kwd: val
        });
    }
   //在父组件中定义了一个页面跳转的方法。
    KeyUpHandle(e) {
        if (e.keyCode !== 13)
            return;
        this.props.enterHandle(this.state.kwd);
    }

    render(){
        return(
            <div className='middle_header'>
                <i className='icon-search'/>
                &nbsp;
                <input
                    type='text'
                    placeholder='请输入关键字'
                    onChange={this.ChangeHandle.bind(this)}
                    onKeyUp={this.KeyUpHandle.bind(this)}
                    value={this.state.kwd}
                />
            </div>
        );
    }

}
/*中间搜索框*/
.middle_header{
    width: 70%;
    border-radius: 15px;
    background-color: #fff;
    padding: 5px;
    overflow: hidden;
}

.middle_header i{
    color:#ccc
}

.middle_header input{
    font-size: 16px;
    font-weight: normal;
    padding: 0;
    border: 0;
    width: 80%;
}

在HomeHeader中修改,调用searchInput的时候传递了一个方法,用于实现页面跳转。

export default class HomeHeader extends React.Component {

    EnterHandle(value) {
        hashHistory.push('/search/all/'+encodeURIComponent(value));
    }

    render() {
        return (
            <header className='home_header'>
                <div className='left_header'>
                    <Link to='/city'>
                        <span>{this.props.cityName}</span>
                        <i className='icon-angle-down'/>
                    </Link>
                </div>

                <SearchInput enterHandle={this.EnterHandle.bind(this)}/>

                <div className='right_header'>
                    <i className='icon-user'/>
                </div>


            </header>
        );
    }
}
  • Search组件
    我们希望将主页搜索的内容显示到搜索页面的搜索框内。
    所以首先创建Search组件,通过this.props.params能够获取到传递的参数。
    将参数传递给SearchHeader


import React from 'react';
import SearchHeader from '../component/search/searchHeader';
export default class Search extends React.Component{
    render(){
        const params=this.props.params;
        return(
            <div>
                <SearchHeader keyword={params.keyword}/>
            </div>
        );
    }
}
  • 实现SearchHeader组件




    //在SearchHeader组件中向SearchInput组件传递了一个参数,keyword,
    需要在SearchInput组件中将keyword的值赋值给this.state>kwd

import React from 'react'
import SearchInput from '../common/searchInput'
import './searchHeader.css'
import '../../static/css/font.css'
import {Link,hashHistory} from 'react-router'
export default class SearchHeader extends React.Component{
    enterHandle(value) {
        hashHistory.push('/search/all/'+encodeURIComponent(value));
    }

    clickHandle() {
        window.history.back();
    }

    render(){
        return(
            <div className='search_header'>
                <i className='icon-chevron-left' onClick={this.clickHandle.bind(this)}/>
                <div className='search_input'>
                    <SearchInput keyword={this.props.keyword||''} enterHandle={this.enterHandle.bind(this)}/>
                </div>
            </div>
        );
    }
}

SearchInput组件中做相应的修改

    componentDidMount(){
       this.setState({kwd:this.props.keyword});
    }
  • 实现SearchList组件,需要向后台获取数据。
    首先在mock数据中添加搜索相关j的son



    在server.js中添加服务

const searchListData=require('./search/list');
//搜索结果页 - 搜索结果 - 两个参数
router.get('/api/search/:page/:city/:category', async (ctx) => {
    // 参数
    const params = ctx.params;
    const paramsPage = params.page;
    const paramsCity = params.city;
    const paramsCategory = params.category;

    console.log('当前页数:' + paramsPage);
    console.log('当前城市:' + paramsCity);
    console.log('当前类别:' + paramsCategory);

    ctx.body = searchListData;
});

重启服务



1)将首页中的猜你喜欢每一条抽取出来一个likeItem组件以供复用


抽离出来以供复用
import React from 'react';
import './likeItem.css'
export default class LikeItem extends React.Component{
    render(){
        return(
            <div key={this.props.key} className='like_item clearfix'>
                <img src={this.props.item.img} className='left'/>
                <div className='content'>
                    <div className='clearfix'>
                        <span className='title'>{this.props.item.title}</span>
                        <span className='distance'>{this.props.item.distance}</span>
                    </div>

                    <p className='subTitle'>{this.props.item.subTitle}</p>

                    <div className='clearfix'>
                        <span className='price'>¥{this.props.item.price}</span>
                        <span className='mumber'>已售{this.props.item.mumber}</span>
                    </div>

                </div>
            </div>
        );
    }
}
  1. searchList组件
    首先实现向后台获取数据并展示的功能:


import React from 'react';
import LikeItem from '../home/likelist/likeItem';
import { connect } from 'react-redux';
class SearchList extends React.Component{
    constructor(props){
     super(props);
        this.state = {
            list: [], //存储列表信息
            page: 0, //请求的页码
            hasMore:false,
            isLoadingMore:false,
        }
    }

    componentDidMount(){
        const myfetchOption={method:'GET'};
        const cityName=this.props.userinfo.cityName;
        const keyword=this.props.keyword;
        const category=this.props.category;
        fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category,myfetchOption).then(response => response.json())
            .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
    }
    render(){
        const search=this.state.list;
        const searchList=search.length?
            search.map((searchItem,index)=>(
                <LikeItem key={index} item={searchItem}/>
            ))

            :'加载中...';
        return(
             <div>
                 {searchList}
             </div>
        );
    }
}

// -------------------redux react 绑定--------------------

function mapStateToProps(state) {
    return {
        userinfo: state.userinfo
    }
}

function mapDispatchToProps(dispatch) {
    return {
    }
}
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(SearchList)

然后实现加载更多的功能,直接使用主页写好的loadmore组件

 return(
             <div>
                 {searchList}
                 {
                     this.state.hasMore?
                         <LoadMore isLoadingMore={this.state.isLoadingMore} loadMoreFn={this.loadMoreData.bind(this)}/>
                         : ''
                 }
             </div>
        );


    //点击加载更多触发
    loadMoreData() {
        //记录状态
        this.setState({isLoadingMore: true});
        let page=this.state.page+1;
        //发送请求
        let myFetchOptions = {method: 'GET'};
        const cityName=this.props.userinfo.cityName;
        const category=this.props.category;
        fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category, myFetchOptions)
            .then(response => response.json())
            .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
        //设置page
        this.setState({page: page, isLoadingMore: false});

    }

处理重新搜索

    // 处理重新搜索
    componentDidUpdate(prevProps, prevState) {
        const keyword = this.props.keyword;
        const category = this.props.category;

        // 搜索条件完全相等时,忽略。重要!!!
        if (keyword === prevProps.keyword && category === prevProps.category) {
            return
        }

        // 重置 state
        this.setState(initialState);

        // 重新加载数据
        this.loadFirstPageData()
    }

  //第一次加载数据
    loadFirstPageData(){
        const myfetchOption={method:'GET'};
        const cityName=this.props.userinfo.cityName;
        const category=this.props.category;
        fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category,myfetchOption).then(response => response.json())
            .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
    }
const initialState={
    list: [], //存储列表信息
    page: 0, //请求的页码
    hasMore:false,
    isLoadingMore:false,
};

5.商户详情页

  • 在routerMap中添加路由
<Route path='/detail/:id' component={Detail}/>
  • 创建detail组件


import React from 'react';
import PageHeader from '../component/common/pageHeader';

export default class Detail extends React.Component{
    render(){
        const params=this.props.params;
        return(
            <div>
                <PageHeader title='商家信息'/>

            </div>
        );
    }
}

  • 为每一个item添加Link
    在likeIte组件中
<div key={this.props.key} className='like_item clearfix'>
  <Link to={`/detail/${this.props.item.id}`}>
....
  </Link>
</div>
  • detail页面需要向后台发送数据获取商家信息和评论信息


    mock中添加相关内容

    修改server.js

//详情页

//获取商家信息
const infoData=require('./detail/info');
router.get('/api/detail/info/:id', async (ctx) => {
    // 参数
    const params = ctx.params;
    const paramsId = params.id;

    console.log('当前商家id:' + paramsId);


    ctx.body = infoData;
});
//获取评论信息
const comment=require('./detail/comment');
router.get('/api/detail/comment/:page/:id', async (ctx) => {
    // 参数
    const params = ctx.params;
    const paramsId = params.id;
    console.log('当前商家id:' + paramsId);
    ctx.body = comment;
});

重启服务



  • 商户信息组件



    1)实现Info组件
    Info组件向后台获取信息

import React from 'react';
import DetailInfo from './detailInfo';
export default class Info extends React.Component{
    constructor(props){
        super(props);
        this.state={info:false};
    }

    componentDidMount(){
        const myfetchOption={method:'GET'};
        fetch('/api/detail/info/'+this.props.id,myfetchOption).then(response => response.json())
            .then(json => this.setState({info: json}));
    }

    //只有一个元素不能用array,json是一个Object,array的length还是0
    render(){
        const info=this.state.info;
        const dataList=info?
             <DetailInfo info={info}/>
            :'加载中...';

        return(
            <div>
                {dataList}
            </div>
        );
    }
}

2)首先实现star组件
star从其父组件获取star个数,利用数组的map来实现,比较item的值和star的值。star值>=item值,则设置class light,将具有light类的i标签设置为红色。

import React from 'react';
import './star.css'
export default class DetailInfo extends React.Component{
    constructor(props){
        super(props);
    }
    render(){
        let star=this.props.star||0;
        if(star>5){
            star=star%5;
        }
        return(
            <span>
                {
                    [1,2,3,4,5].map((item,index)=>{
                        const lightClass=star>=item?' light':'';
                        return <i key={index} className={'icon-star'+ lightClass}/>
                    })

                }
            </span>
        );
    }
}
i.light {
   color:red;
}

3)实现DetailInfo组件

import React from 'react';
import './detailinfo.css';
import Star from './star';
export default class DetailInfo extends React.Component{
    render(){
        const info=this.props.info;
        return(
            <div className='info'>
                <div className='info_top clearfix'>
                    <img src={info.img} alt={info.title}/>
                    <div className='info_content'>
                        <h3>{info.title}</h3>
                        <p>
                            <Star star={info.star}/>
                            <span>  ¥{info.price}</span>
                        </p>

                        <p>{info.subTitle}</p>
                    </div>
                </div>

                <p className='info_bottom' dangerouslySetInnerHTML={{__html:info.desc}}/>

             </div>

        );
    }
}
  • 评论信息



    Comment组件:

import React from 'react';
import Star from '../info/star';
import LoadMore from '../../home/likelist/LoadMore';
import './comment.css'
const initialState = {
    list: [], //存储列表信息
    page: 0, //请求的页码
    hasMore: false,
    isLoadingMore: false,
};
export default class Comment extends React.Component {
    constructor(props) {
        super(props);
        this.state = initialState;
    }

    componentDidMount() {
        this.loadFirstPageData();
    }

    //第一次加载数据
    loadFirstPageData() {
        const myfetchOption = {method: 'GET'};
        fetch('/api/detail/comment/' + this.state.page + '/' + this.props.id, myfetchOption).then(response => response.json())
            .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
    }

    //点击加载更多触发
    loadMoreData() {
        //记录状态
        this.setState({isLoadingMore: true});
        let page = this.state.page + 1;
        //发送请求
        let myFetchOptions = {method: 'GET'};
        fetch('/api/detail/comment/' + this.state.page + '/' + this.props.id, myFetchOptions)
            .then(response => response.json())
            .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
        //设置page
        this.setState({page: page, isLoadingMore: false});

    }

    render() {
        let comment = this.state.list;
        const commentList = comment.length ?
            comment.map((item, index) => (
                <div key={index} className='comment_info'>
                    <p>
                        <i className='icon-user'/>
                        <span>{item.username}</span>
                    </p>
                    <Star star={item.star}/>
                    <p className='comment_content'>{item.comment}</p>
                </div>
            ))
            : '加载中';
        return (
            <div className='comment'>
                <h4>用户点评</h4>
                {commentList}
                {
                    this.state.hasMore?
                        <LoadMore isLoadingMore={this.state.isLoadingMore} loadMoreFn={this.loadMoreData.bind(this)}/>
                        : ''
                }
            </div>
        );
    }
}
.comment{
    padding:10px;
    border-top: solid 1em #f1f1f1 ;
}
.comment h4{
    margin-top: 0px;
}
.comment_info p{
    margin: 0;
}

.comment_info{
    margin-bottom: 2em;
}

.comment_content{
    color: grey;
}

6.登录页面

  • 配置路由
 <Route path='/login' component={Login}/>

主页面HomeHeader组件右边的icon添加跳转链接

 <div className='right_header'>
                    <Link to='/login' style={{color:'white'}}>
                        <i className='icon-user'/>
                    </Link>
 </div>
  • 登陆页面逻辑
    首先要检测用户是否登录(通过连接redux,判断是否有用户信息),如果没有登录显示登录组件,如果登录,直接跳转到用户中心界面。


import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as userInfoActions from '../action/userInfoActions';
class Login extends React.Component {
    constructor(props) {
        super(props);
        this.state={checking:true};
    }

    componentDidMount(){
        this.doCheck();
    }
    //检测是否登陆
    doCheck(){
        const userinfo=this.props.userinfo;
        if(userinfo.username){
            //已经登录,直接转到用户中心
            this.goUserpage();
        }else{
            //尚未登录
            this.setState({
               checking:false
            });
        }
    }
 
goUserpage(){
        hashHistory.push('/User');
    }

   render() {
        const params = this.props.params;
        return (
            <div>
                <PageHeader title='用户登录'/>
                {this.state.checking? <div>/*正在检查是否登陆*/</div>: <div>登录组件</div>}
            </div>


        );
    }


}

// -------------------redux react 绑定--------------------

function mapStateToProps(state) {
    return {
        userinfo: state.userinfo
    }
}

//触发数据改变
function mapDispatchToProps(dispatch) {
    return {
        userInfoActions: bindActionCreators(userInfoActions, dispatch),
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Login)
  • 登录组件



Login.js在调用LoginComponent组件时,传递了一个登录按钮点击后的方法。

 //登录成功之后的业务处理
    loginHandle(username){
        //保存用户名
        const actions=this.props.userInfoActions;
        let userinfo=this.props.userinfo;
        userinfo.username=username;
        if(username=='')
            return;

        actions.update(userinfo);

        //跳转连接
        const params=this.props.params;
        const router=params.router;
        if(router)
            hashHistory.push('/'+router);
        else
            this.goUserpage();
    }
 <LoginComponent loginHandle={this.loginHandle.bind(this)}/>

LoginComponent

import React from 'react';
import './loginComponent.css';
export default class LoginComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {phone: '',message:'该用户名为空'};
    }

    clickHandle() {
        const username = this.state.phone;
        //传过来了一个方法,是点击登录按钮后的处理
        const loginHandle = this.props.loginHandle;
        loginHandle(username);
    }

    changeHandle(e) {
      //做了两个验证,验证是否是空,验证是否是手机号
       let value=e.target.value;
       let telReg=/[1][34578]\d{9}$/;
        //验证用户名是否为空
        if(value=='')
            this.setState({message:'该用户名为空'});
        //验证用户名不是手机号
        else if(!telReg.test(value)){
            this.setState({message:'请输入正确的手机号码'});
        }
        else
            this.setState({message:''});
        this.setState({phone: e.target.value});
    }

    render() {
        const params = this.props.params;
        return (
            <div className='login-container'>
                <div className='input-container phone-container'>
                    <i className='icon-tablet'/>
                    <input type='text'
                           placeholder='输入手机号'
                           onChange={this.changeHandle.bind(this)}
                           value={this.state.phone}/>
                </div>
                <span style={{fontSize:'12px',color:'rgb(233,32,61)'}}>{this.state.message}</span>
                <div className='input-container password-container'>
                    <i className='icon-key'/>
                    <input type='text' placeholder='请输入验证码'/>
                    <button>发送验证码</button>
                </div>
                <button className='login-button' onClick={this.clickHandle.bind(this)}>登录</button>
            </div>
        );
    }
}
.login-container{
    width:300px;
    margin: 100px auto 0 auto;
}

.input-container{
    border:1px solid rgb(233,32,61);
    padding: 5px 10px;
    border-radius: 5px;
    overflow: hidden;
}

.input-container input{
    font-size:16px;
    line-height: 1.5em;
    border: none;
    margin-left: 1em;
    width: 85%;
}

.input-container i{
    color: rgb(233,32,61);
    width:16px;
}

.password-container{
    margin-top: 0.5em;
}
.password-container input{
    width: 50%;
}
.password-container button{
    float: right;
    margin-top: 1px;
    border-radius: 5px;
    background-color: #f1f1f1;
    font-size: 14px;

}

.login-button{
    width:100%;
    background-color: rgb(233,32,61);
    color: white;
    border: none;
    border-radius: 5px;
    padding: 0.5em;
    font-size: 16px;
    margin-top: 0.5em;
}

7.收藏功能


收藏功能的实现需要redux,redux-thunk插件,具体使用参考第13节。

  • action



    创建三个action,负责收藏信息的更新,添加收藏,删除收藏。
    收藏信息的更新在app.js中初始化从后台获得当前用户所有收藏列表的时候会使用。(初始化功能的实现具体讲解参见13节)
    同时在登录界面,用户一旦登录就应该触发redux中收藏列表的更新。

//更新收藏列表
export function update(data) {
    return {
        type: 'STORE_UPDATE',
        data
    }
}

export function add(item) {
    console.log('触发了add');
    return {
        type: 'STORE_ADD',
        data: item
    }
}

export function remove(item) {
    return {
        type: 'STORE_REMOVE',
        data: item
    }
}

//在App.js中完成页初始化,从后台获取该用户的所有收藏商品id存储到redux中
export function getInitStore(username) {
    return function (dispatch) {
        console.log('getInitStore执行了');
        let option = {method: 'GET'};
        fetch(`/api/store/getStore/${username}`, option)
            .then(res => res.json())
            .then(json => dispatch(update(json)));
    };
}

//用户添加收藏时,先向后台发送请求
//item就是调用方法时传入的{id:''}
export function addStore(item) {
    return function (dispatch) {
        let option = {method: 'GET'};
        fetch(`/api/store/addStore/${JSON.stringify(item)}`, option)
            .then(res => res.json())
            .then(json => {
                    if (json)
                        dispatch(add(item));
                    else
                        alert('网络不畅');
                }
            );
    };

}

//用户删除收藏时,先向后台发送请求
export function removeStore(item) {
    return function (dispatch) {
        let option = {method: 'GET'};
        fetch(`/api/store/removeStore/${JSON.stringify(item)}`, option)
            .then(res => res.json())
            .then(json => {
                if (json)
                    dispatch(remove(item));
                else
                    alert('删除失败');
            });
    };

}
  • reducer


const initialState =[];
//收藏创建的规则
export default function store(state=initialState,action) {
    switch (action.type){
        //修改城市名字
        case 'STORE_UPDATE':
            return action.data;
        case 'STORE_ADD':
            state.unshift(action.data);
            return state;
        case 'STORE_REMOVE':
            return state.filter(item=>{
                if(item.id!==action.data.id)
                    return item;
            });
        default:
            return state;
    }
}
  • 收藏组件


  • 通过isStore属性来保存是否被收藏的状态。
  • 当渲染结束后,执行componentDidMount()方法,里面包括了 checkStoreState() 方法用来检验用户是否收藏该id的商品。通过获取redux中的store属性的列表,判断其中是否有当前的商品id,如果有,就说明被收藏了,isStore=true。
  • 点击收藏按钮时,绑定了storeHandle()事件。事件中首先判断该用户是否登录。如果没登录,跳转到登录界面。
    如果isStore=true,就触发removeStore的action(向后台发送删除请求,返回true,执行redux中store的删除方法)。
    如果isStore=false,就触发addStore的action(向后台发送添加请求,返回true,执行redux中store的添加方法)
/*收藏组件*/

import React from 'react';
import {hashHistory} from 'react-router'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux';
import * as storeActions from '../../../action/storeActions';


class Store extends React.Component {
    constructor(props) {
        super(props);
        this.state = {isStore: false}
    }



    //验证是否登录
    loginCheck() {
        const id = this.props.id;
        const userinfo = this.props.userinfo;
        //把当前详情页的router传递过去,登录完了之后会跳转到原来的页面
        //如果没有用户名,跳转到登录页面
        if (!userinfo.username) {
            hashHistory.push('/login/' + encodeURIComponent('detail/' + id));
            return false;
        }
        return true;
    }

    //收藏事件
    storeHandle() {

        //验证登录
        const loginFlag = this.loginCheck();
        if (!loginFlag)
            return;
        //收藏的流程
        const id = this.props.id;
        //判断当前页面是否收藏,如果收藏,就取消
        if (this.state.isStore) {
           this.props.storeActions.removeStore({id: id});
           this.setState({isStore: false});
        } else {
            this.props.storeActions.addStore({id: id});
            this.setState({isStore: true});
        }

        //跳转到用户主页
        // hashHistory.push('/User');

    }


    render() {

        return (

            <i className={'icon-star'} onClick={this.storeHandle.bind(this)} style={
                this.state.isStore ? {color: '#FFD700'} : {color: '#ccc'}
            }/>

        );
    }

    componentDidMount() {


        this.checkStoreState();

    }

    //检验当前商户是否被收藏
    checkStoreState() {
        //从父组件传递过来的
        const id = this.props.id;

        //some函数只要有一个满足即可
        this.props.store.some(item => {
                if (item.id === id) {
                    this.setState({isStore: true});
                    return true;
                }

            }
        );
    }


}


// -------------------redux react 绑定--------------------

//用于收藏和购买部分的功能

function mapStateToProps(state) {
    return {
        userinfo: state.userinfo,
        store: state.store
    }
}

//触发数据变化
function mapDispatchToProps(dispatch) {
    return {
        storeActions: bindActionCreators(storeActions, dispatch)
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Store)

8.用户中心

  • 配置路由
<Route path='/user' component={User}/>
  • 如果用户没有登录,就跳转到登录页面,用户已经登录,就显示User组件
import React from 'react';
import PageHeader from '../component/common/pageHeader';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {hashHistory} from 'react-router';
import * as userInfoActions from '../action/userInfoActions';
 class User extends React.Component {
    constructor(props) {
        super(props);
        this.state={ischecking:true};
    }

    componentDidMount(){
        //判断有没有登陆,没有登陆直接跳转
        const userinfo=this.props.userinfo;
        //如果尚未登录
        if(!userinfo.username){
            hashHistory.push('/login');
        }
        //如果已经登录
        else{
            this.setState({ischecking:false});
        }



    }
    render(){
        return(
            this.state.ischecking ? <div/>:
                <div>
                    <PageHeader title='用户中心' backRouter='/'/>
                </div>


        );
    }
}

// -------------------redux react 绑定--------------------

function mapStateToProps(state) {
    return {
        userinfo: state.userinfo
    }
}

//触发数据改变
function mapDispatchToProps(dispatch) {
    return {
        userInfoActions: bindActionCreators(userInfoActions, dispatch),
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(User)
  • 这里的返回会有问题,退不出去,所以我们修改一下pageHeader
    使用pageHeader组件的时候传递一个backRouter。
 clickHandle() {
        const backRouter=this.props.backRouter;
        if(backRouter)
            hashHistory.push(backRouter);
        else {
            window.history.back();
        }

    }
  • 创建该用户的详细信息组件
    UserInfo


import React from 'react';
export default class User extends React.Component {
    constructor(props) {
        super(props);
    }

    render(){
        return(
            <div style={{padding:'1em',borderBottom:'1px solid #ccc'}}>
                <p>
                    <i className='icon-user'/>
                    <span style={{marginLeft:'1em'}}>{this.props.username}</span>
                </p>
                <p>
                    <i className='icon-map-marker'/>
                    <span style={{marginLeft:'1em'}}>{this.props.cityName}</span>
                </p>
            </div>
        );
    }
}
  • 创建您的订单组件OrderList


您的订单部分需要连接后台服务器获取数据,通过传递用户名作为参数获取其相应订单。
在mock中添加相应json数据


import React from 'react';
import OrderListComponent from './orderListComponent';

export default class OrderList extends React.Component {
    constructor(props) {
        super(props);
        this.state = {order: []}

    }

    componentDidMount() {
        const fetchOption = {method: 'GET'};
        fetch('/api/orderlist/' + this.props.username, fetchOption).then(response => response.json()).then(json => this.setState({order: json}));

    }

    render() {
        const order = this.state.order;
        const orderList = order.length ?
            order.map((item, index) => (
                <OrderListComponent key={index} item={item}/>
               ))
            : '加载中...';
        return (
            <div>
                <h4 style={{borderBottom:'1px solid #ccc',margin:'0 0 0 10px',padding:'0.5em 10px'}}>您的订单</h4>
                {
                    orderList
                }
            </div>

        );
    }
}

  • orderListComponent组件
import React from 'react';
import  './orderListComponent.css';
export default class OrderListComponent extends React.Component{
    render(){
        const item=this.props.item;
        return(
            <div className='order'>
                <img src={item.img}/>
                <section>
                    <p>{`商户:${item.title}` }</p>
                    <p>{`数量:${item.count}`}</p>
                    <p>{`价格:${item.price}`}</p>
                </section>
                <button>评价</button>
            </div>
        );
    }
}

采用flex布局:

.order{
    display:flex;
    justify-content: space-between;
    padding: 10px;
    align-items: center;
    border-bottom: solid 1px #ccc;
}

.order img{
    width:30%;
}

.order p{
    margin: 0.5em ;
}

.order button{
    width: 20%;
    background-color: rgb(233,32,61);
    color: white;
    font-size: 16px;
    padding-top: 1px;
    padding-bottom: 1px;
    border-radius: 10px;
}

9.评论功能

  • 从后台传递过来的每一项内容中是有一个commentState的,表示评论的状态(未评价0 已评价2),
    commentState===0显示评价按钮,commentState===2显示已评价按钮,commentState===1表示评价中不显示按钮
  • 当点击评价按钮时,设置commentState为1,显示评论框。
  • 点击取消,设置commentState为0,隐藏评论框。
export default class OrderListComponent extends React.Component {


    constructor() {
        super();
        this.state = {commentState: '',commentValue:''};
    }

    componentDidMount() {
        //0未评价,1评价中,2已评价
        this.setState({commentState: this.props.item.commentState});
    }


    //显示评价框
    showComment() {
        //当未评价的时候,点击按钮的响应事件
        this.setState({commentState: 1});
    }

    //隐藏评价框
    hideComment(){
        this.setState({commentState:0});
    }

    //双向绑定评论内容
    commentText(e){
        this.setState({commentValue:e.target.value});
    }

 render() {
        const item = this.props.item;
        return (
            <div className='orderContainer'>
                <div className='order'>
                    <img src={item.img}/>
                    <section>
                        <p>{`商户:${item.title}`}</p>
                        <p>{`数量:${item.count}`}</p>
                        <p>{`价格:${item.price}`}</p>
                    </section>
                    {
                        this.state.commentState === 0 ?
                            //未评价
                            <button onClick={this.showComment.bind(this)}>评价</button> : this.state.commentState === 1 ?
                            //评价中
                            '' :
                            //已评价
                            <button disabled="disabled" style={{backgroundColor: '#ccc'}}>已评价</button>
                    }
                </div>

                {
                    this.state.commentState === 1 ?
                        <div className='order_comment'>
                            <textarea value={this.state.commentValue} onChange={this.commentText.bind(this)}/>
                            <button onClick={this.submitComment.bind(this)}>提交</button>
                            <button onClick={this.hideComment.bind(this)} style={{backgroundColor: '#ccc'}}>取消</button>
                        </div> : ''
                }
            </div>

        );
    }
}
  • 点击提交按钮发送post请求到后台
    首先服务器端server.js增加提交部分

//提交评论

router.post('/api/submitComment',koaBody ,async (ctx)=>{
    console.log('提交评论');

    // 获取参数
    console.log(ctx.request.body);

    ctx.body = {
        errno: 0,
        msg: 'ok'
    }
});

前端点击提交按钮,发送post请求

//提交评价
    submitComment(){
        const data={"id":`${this.props.item.id}`,
            "commentText":`${this.state.commentValue}`};
        if(!data.commentText)
            return;

       fetch('/api/submitComment', {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Accept': 'application/json, text/plain, */*',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body:`data=${JSON.stringify(data)}`,
        })
           .then(response=>response.json())
           .then(json=>{
               if(json.errno===0)
                   //已经评价,修改状态
                   this.commentOK();
           });


    }

    //评价成功
    commentOK(){
        //已经评价,修改状态
        this.setState({
            commentState:2
        })
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,117评论 25 707
  • 可能前一世我偷喝了你酿过最烈的酒 今生才千千万万遍走进你又离开 我是个醉鬼 在红尘里吃着廉价的感情
    秦秦0609阅读 191评论 0 0
  • 又是一年毕业季,又是一年入职时。刚刚进入职场,仅仅给你几点建议,仅供参考。 1、薪资保密 在我大四开始实习的时候,...
    平平和平平阅读 607评论 10 11