源码: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(
);
}
}
父组件App.js
import React from 'react';
import './App.css';
class App extends React.Component {
render() {
return (
{this.props.children}
);
}
}
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(
深圳
);
}
}
使用了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分类
- 需要轮播功能,引入react-swipe插件
https://www.npmjs.com/package/react-swipe
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 (
{this.props.text}
);
}
}
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(
-
-
-
);
}
}
'./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) => (
{adItem.title}
))
: '加载中';
return (
超值特惠
{adList}
);
}
}
- 遇到问题:
本地调试的时候,图片src引用了第三方网站的图片资源,导致控制台出现了如下的报错:
解决方法:
在html中添加
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)中间是一个。
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)=>(
{likeItem.title}
{likeItem.distance}
{likeItem.subTitle}
¥{likeItem.price}
已售{likeItem.mumber}
))
:'加载中';
return(
猜你喜欢
{likeList}
);
}
}
.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(
{
this.props.isLoadingMore?
加载中...
:加载更多
}
);
}
}
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 (
猜你喜欢
{likeList}
{
this.state.hasMore?
: ''
}
);
- 上拉就加载更多
主要是监听滚轮事件,判断底部加载更多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
3.选择城市页面
- 因为城市信息是我们需要在各个组件中共享的信息,比如homeheader的city显示,猜你喜欢的部分也需要发送城市名称才能获取相应数据。所以我们首先需要搭建Redux数据流环境,参见第13节。
- 在routerMap中添加路由
- 跳转到城市选择页
不会刷新页面,react和react-router监听hash的变化,然后js层重新渲染页面,
里面并没有页面的请求。
在之前写的HomeHeader组件中添加跳转链接
{this.props.cityName}
.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(
);
}
}
// -------------------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 (
{this.props.title}
);
}
}
首先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 (
{this.props.currentCity}
);
}
}
-
热门城市列表
设计了一个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(
热门城市
- 北京
- 上海
- 杭州
- 深圳
- 广州
- 西安
- 成都
- 长沙
- 无锡
);
}
}
.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(
);
}
}
4.搜索功能
4.1 种类跳转
- 添加路由
- 在Category页面为CategoryItem添加
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))
}
......
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(
);
}
}
/*中间搜索框*/
.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 (
{this.props.cityName}
);
}
}
-
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(
);
}
}
-
实现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(
);
}
}
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(
{this.props.item.title}
{this.props.item.distance}
{this.props.item.subTitle}
¥{this.props.item.price}
已售{this.props.item.mumber}
);
}
}
-
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)=>(
))
:'加载中...';
return(
{searchList}
);
}
}
// -------------------redux react 绑定--------------------
function mapStateToProps(state) {
return {
userinfo: state.userinfo
}
}
function mapDispatchToProps(dispatch) {
return {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchList)
然后实现加载更多的功能,直接使用主页写好的loadmore组件
return(
{searchList}
{
this.state.hasMore?
: ''
}
);
//点击加载更多触发
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中添加路由
-
创建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(
);
}
}
- 为每一个item添加Link
在likeIte组件中
....
-
detail页面需要向后台发送数据获取商家信息和评论信息
修改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?
:'加载中...';
return(
{dataList}
);
}
}
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(
{
[1,2,3,4,5].map((item,index)=>{
const lightClass=star>=item?' light':'';
return
})
}
);
}
}
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(
{info.title}
¥{info.price}
{info.subTitle}
);
}
}
-
评论信息
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) => (
{item.username}
{item.comment}
))
: '加载中';
return (
用户点评
{commentList}
{
this.state.hasMore?
: ''
}
);
}
}
.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.登录页面
- 配置路由
主页面HomeHeader组件右边的icon添加跳转链接
-
登陆页面逻辑
首先要检测用户是否登录(通过连接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 (
{this.state.checking? /*正在检查是否登陆*/: 登录组件}
);
}
}
// -------------------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
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 (
{this.state.message}
);
}
}
.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 (
);
}
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.用户中心
- 配置路由
- 如果用户没有登录,就跳转到登录页面,用户已经登录,就显示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 ? :
);
}
}
// -------------------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(
{this.props.username}
{this.props.cityName}
);
}
}
-
创建您的订单组件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) => (
))
: '加载中...';
return (
您的订单
{
orderList
}
);
}
}
- orderListComponent组件
import React from 'react';
import './orderListComponent.css';
export default class OrderListComponent extends React.Component{
render(){
const item=this.props.item;
return(
{`商户:${item.title}` }
{`数量:${item.count}`}
{`价格:${item.price}`}
);
}
}
采用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 (
{`商户:${item.title}`}
{`数量:${item.count}`}
{`价格:${item.price}`}
{
this.state.commentState === 0 ?
//未评价
: this.state.commentState === 1 ?
//评价中
'' :
//已评价
}
{
this.state.commentState === 1 ?
: ''
}
);
}
}
- 点击提交按钮发送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
})
}