美文网首页React
12.React实战开发--团购webapp

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

作者: Ching_Lee | 来源:发表于2018-03-07 15:18 被阅读58次

    源码: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
            })
        }
    

    相关文章

      网友评论

        本文标题:12.React实战开发--团购webapp

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