React实现购物车

###购物车来啦
2020年疫情刚刚结束,好久没写React了,有点慌,为了不遗漏知识点,查漏补缺,写个简单的购物车,基本的功能有列表、新增、修改、删除
开发用的框架React+Antd+Koa+Mysql,开发工具hbuilder-x,上传代码gitee
好啦,开始写啦
1:新建一个文件夹,起名shopWeb,里面新建shop文件夹,存放前台所有代码,新建shopServer文件夹,存放后台接口api所有文件
image.png
2:先讲解前台部分吧,进入shop文件夹,安装项目,用官方提供的脚手架初始化一下

create-react-app shop 

等待安装完成后,基本的脚手架就搭建完了
下面安装所有依赖

cnpm i  antd axios react-router-dom babel-plugin-import --save

这个就是需要的安装包
image.png
这里antd最好用按需加载的方式动态引入css,我这里偷懒就不用了

3:要实现的效果基本如下图
image.png
我们先把路由定义好了在说,
src下面新建一个router文件夹,下面新建router.js

import  React  from  'react'
//导入路由所有需要的包 必须这样写 否则报错 
import {BrowserRouter as Router,Link,Route} from 'react-router-dom';

import './router.css';

import Shop  from  '../page/shop.js';
import User  from  '../page/user.js'

import Add from '../page/add.js';
import Edit from '../page/edit.js'

var router  = ()=>{
	return(
		<Router>
			<ul className="router">
				<li><Link to="/shoplist" >去购物车</Link></li>
				<li><Link to="/user" >去个人中心</Link></li>
			</ul>
			<div className="linkTag"> 
				<Link to="/shopAdd" className="add" >新增</Link>
				<Link to="/shopEdit/66"  className="edit">修改测试</Link>
			</div>
			<Route exact path='/shoplist' component={Shop}></Route>
			<Route  path='/user' component={User}></Route>
			
			<Route path='/shopAdd'  exact component={Add}></Route>
			<Route path='/shopEdit/:id'  exact component={Edit}></Route>
		</Router>
	)
	
}


export default router;

先配置好路由才可以哦,这样才可以切换呀~~~
image.png

记得要在index.js里面引入这个路由js哦,否则无法生效哦
image.png
当然也可以直接在app.js中直接映入也是可以的
4:src下面新建page文件夹,存放所有页面js
新建一个shop.js这就是我们的主页面,列表页面
基本代码如下

import  React,{Component} from  'react';
import { BrowserRouter  as Router,Route,Link,withRouter} from 'react-router-dom'
import '../assets/page/shop.css';

import  axios  from   'axios';
import { Table, Tag ,message,Modal,Button} from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons'


//注意 setState({})   我在这里浪费一个小时 =  导致浪费了1个小时找错误 
const { confirm } = Modal;


let api = "http://localhost:9090/";

//import historyRouter from '../history/index.js';

class   Shop  extends Component{
	
	constructor(props){
		super(props);
		this.state={
			isShow:false,
			username:"",
			list:[],
			columns: [
			  {
				title: 'ID',
				dataIndex: 'id',
				key: 'id',
			  },	
			  {
				title: '名称',
				dataIndex: 'name',
				key: 'name',
			  },
			  {
				title: '价格',
				dataIndex: 'price',
				key: 'price'
			  },
			  {
				title: '数量',
				dataIndex: 'number',
				key: 'number'
			  },
			  {
			      title: '操作',
			      key: 'action',
			      render: (text, record) => (
			        <span className="actionBtn">
			          <Button type="primary" className="btn editBtn" onClick={this.goEdit.bind(this,record)}>编辑</Button>
			          <Button type="primary" danger  className="btn deleteBtn"  onClick={this.deleteOne.bind(this,record)}  >删除</Button>
			        </span>
			      ),
			    }
			]
		};

	}
	//初始化查询列表
	async getData(){
		let  res = await axios.get(api+"list");
		this.setState({
			list:res.data.data
		})
	}
	
	goEdit(event){
		let  id  = event.id;
		//let { history } = this.props;
		//history.push("/shopEdit/"+id);
		this.props.history.push({pathname:'/shopEdit/'+id});
	}

	//删除
	deleteOne(event){
		let  id  = event.id;
		var that = this;
	  confirm({
		title: '确定删除这条记录么?',
		icon: <ExclamationCircleOutlined />,
		content: '删除后不可恢复哦~~~',
		onOk() {
		  console.log('OK');
		  axios.get(api+"delete/"+id).then(res=>{
		  	var  result = res.data.data;
		  	if(res.data.code==1){
		  		message.success('删除成功');
		  		//再次查询
		  		that.getData();
		  	}else{
		  		message.success('删除失败');
		  	};
		  });
		},
		onCancel() {
		  console.log('Cancel');
		},
	  });

	}
	
	//组件初始化
	componentDidMount(){
		this.getData();
	}
	componentWillReceiveProps(nextProps){
		if (nextProps.location.pathname != this.props.location.pathname) {
		        this.getData();
		     } 
	}
	render(){
		return (
			<div>
				<Table dataSource={this.state.list} columns={this.state.columns} />
			</div>
		)
	}
		
}

export default withRouter(Shop);

分享我遇到的坑:
1:antd的table展示,最好有key,不然会一直提醒你
2:列表跳转到Add路由时候,最好用路由,不要用变量显示隐藏,不然刷新就没有了,如果用BrowserRouter就需要后台配合
3:路由最好单独提出去写,否则会乱七八糟。
4:在点击修改的时候,路由跳转遇到问题,

this.props.history.push({pathname:'/shopEdit/'+id});

提示找不到props,最后用了withRouter才解决,当然也可以用别的办法,比如自定义history路由
—好了,接下来讲解新增吧,新建add.js

import  React,{Component,useEffect,useState} from   'react'
import  axios  from  'axios'
import { Modal, Button,Input,message } from 'antd';
import '../assets/page/add.css';

import { withRouter } from 'react-router-dom'

class  Add extends Component{
	constructor(props) {
	    super(props);
		this.state={
			 ModalText: 'Content of the modal',
			 visible: true,
			 confirmLoading: false,
			 username:'',
			 price:'',
			 number:''
		}
	};
	
	 showModal = () => {
	    this.setState({
	      visible: true
	    });
	 };

	 handleOk = () => {

		console.log(this.username.value);
		console.log(this.price.value);
		console.log(this.number.value);

		let  name = this.username.value;
		let  price = this.price.value;
		let  number= this.number.value;
		let  key = null;
		
		if(!name){
		    message.error('必须输入名称');
			return;
		}else if(!price){
			 message.error('必须输入价格')
			 return;
		}else if(!number){
			 message.error('必须输入数量')
			 return;
		}else{
			
		}
		
		let { history } = this.props
		//var  that = this;
		let api = "http://localhost:9090/";
/*		
	    axios.get(api+"add?name="+name+"&price="+price+"&number="+number).then(res=>{
			let  code = res.data.code;
			if(code ==1){
				message.success('添加成功');
				this.setState({
				  ModalText: 'The modal will be closed after two seconds',
				  confirmLoading: true,
				});
				setTimeout(() => {
				  this.setState({
				    visible: true,
				    confirmLoading: false
				  });
				  window.location.href="/shoplist";
				  //history.push({pathname: '/shoplist'})
				}, 2000);
			}
		});
*/	

		
		let  postData={"name":name,"price":price,"number":number};
		
		axios.post(api+"add",postData).then(res=>{
			let  code = res.data.code;
			if(code ==1){
				message.success('添加成功');
				this.setState({
				  ModalText: 'The modal will be closed after two seconds',
				  confirmLoading: true,
				});
				setTimeout(() => {
				  this.setState({
				    visible: true,
				    confirmLoading: false
				  });
				  window.location.href="/shoplist";
				  //history.push({pathname: '/shoplist'})
				}, 2000);
			}
		});
		

	  };
	
	  handleCancel = () => {
	    console.log('Clicked cancel button');
	    this.setState({
	      visible: false
	    });
	  };
	 render(){
		 const { visible, confirmLoading, ModalText } = this.state;
		 return(
		 	<div className="dialog">
		 		<Modal
		 		  title="Title"
		 		  visible={visible}
		 		  onOk={this.handleOk}
		 		  confirmLoading={confirmLoading}
		 		  onCancel={this.handleCancel}
		 		>
				<form>
		 		  <input type='text' placeholder="商品名称" defaultValue={this.state.username}   ref={input => this.username = input} />
				  <input type='text' placeholder="商品价格" defaultValue={this.state.price}   ref={input => this.price = input} />
				  <input type='text' placeholder="商品数量" defaultValue={this.state.number}   ref={input => this.number = input} />
		 		</form>
				</Modal>
		 	</div>
		 ) 
		 
	 }	 
}

export default  Add;

新增这里注意的是,表单的可控组件和不可控,注意要defaultValue的使用,获取值我用的是ref
接下来是修改,新建edit.js
代码如下

import  React,{Component,useEffect,useState} from   'react'
import  axios  from  'axios'
import { Modal, Button,Input,message } from 'antd';
import '../assets/page/add.css';

import { withRouter } from 'react-router-dom'

class  Edit extends Component{
	constructor(props) {
	    super(props);
		this.state={
			 sendId:0,
			 ModalText: 'Content of the modal',
			 visible: true,
			 confirmLoading: false,
			 username:'',
			 price:'',
			 number:''
		}
	};
	
	async initGetData(){
		let api = "http://localhost:9090/";
		let  sendId=this.props.match.params.id ;
		//query  传值
		//let   {location} = this.props;
		//let   {id}  =location.query;
		let  res = await axios.get(api+"select/"+sendId);
		if(res.data.code ==1){
			let  data = res.data.data[0];
			let  name = data.name;
			let  price = data.price;
			let  number= data.number;
			this.setState({
				sendId:sendId,
				username:name,
				price:price,
				number:number
			})
		};
	}
	componentDidMount(){
		this.initGetData()
	}
	 showModal = () => {
	    this.setState({
	      visible: true
	    });
	 };

	 handleOk = () => {

		let  name = this.state.username;
		let  price = this.state.price;
		let  number= this.state.number;
		let  key = null;
		
		if(!name){
		    message.error('必须输入名称');
			return;
		}else if(!price){
			 message.error('必须输入价格')
			 return;
		}else if(!number){
			 message.error('必须输入数量')
			 return;
		}else{
			
		}
		console.log(name,price,number);
		let { history } = this.props
		//var  that = this;
		let api = "http://localhost:9090/";
/*		
	    axios.get(api+"add?name="+name+"&price="+price+"&number="+number).then(res=>{
			let  code = res.data.code;
			if(code ==1){
				message.success('添加成功');
				this.setState({
				  ModalText: 'The modal will be closed after two seconds',
				  confirmLoading: true,
				});
				setTimeout(() => {
				  this.setState({
				    visible: true,
				    confirmLoading: false
				  });
				  window.location.href="/shoplist";
				  //history.push({pathname: '/shoplist'})
				}, 2000);
			}
		});
*/	
		let  postData={"id":this.state.sendId * 1,
						"name":name,
						"price":price,
						"number":number,
						"mark":Math.floor(Math.random()*100)};
		axios.post(api+"update",postData).then(res=>{
			let  code = res.data.code;
			if(code ==1){
				message.success('修改成功');
				this.setState({
				  ModalText: 'The modal will be closed after two seconds',
				  confirmLoading: true,
				});
				setTimeout(() => {
				  this.setState({
				    visible: true,
				    confirmLoading: false
				  });
				  history.push({pathname: '/shoplist'})
				}, 2000);
			}
		});
	
	  };
	
	  handleCancel = () => {
	    console.log('Clicked cancel button');
	    this.setState({
	      visible: false
	    });
	  };
	  //值改变输入
	  changeValue(event){
		  let name =event.target.name;
		  let value =event.target.value
		  this.setState({
			  [name]:value
		  })
	  }
	  
	 render(){
		 const { visible, confirmLoading, ModalText } = this.state;
		 return(
		 	<div className="dialog">
		 		<Modal
		 		  title="请输入内容"
		 		  visible={visible}
		 		  onOk={this.handleOk}
		 		  confirmLoading={confirmLoading}
		 		  onCancel={this.handleCancel}
		 		>
				<form>
		 		  <Input placeholder="输入名称" value={this.state.username} name="username" onChange={this.changeValue.bind(this)} />
				  <Input placeholder="输入价格"  value={this.state.price}  name="price" onChange={this.changeValue.bind(this)} />
				  <Input placeholder="输入数量" value={this.state.number}  name="number"  onChange={this.changeValue.bind(this)}  />
		 		</form>
				</Modal>
		 	</div>
		 ) 
		 
	 }
	 
}

export default  Edit;

修改我用的是antd的Input组件,然后用的onChange结合value实现,初始化填值,输入改变值存储来实现的,这里为了方便用name,动态传递参数

<form>
		 		  <Input placeholder="输入名称" value={this.state.username} name="username" onChange={this.changeValue.bind(this)} />
				  <Input placeholder="输入价格"  value={this.state.price}  name="price" onChange={this.changeValue.bind(this)} />
				  <Input placeholder="输入数量" value={this.state.number}  name="number"  onChange={this.changeValue.bind(this)}  />
		 		</form>

这里绑定事件,注意this的指向
-this指向绑定有3中方法哦
1:点击的时候绑定

onChange={this.changeValue.bind(this)}

2:构造器里面绑定

this.changeValue=this.changeValue.bind(this)

3:点击的时候箭头函数绑定

onChange={()=>this.changeValue}

好了,接着讲表单输入取值,这里name注意是动态的

	  //值改变输入
	  changeValue(event){
		  let name =event.target.name;
		  let value =event.target.value
		  this.setState({
			  [name]:value
		  })
	  }

对啦,忘记讲解一下修改页面,如何动态路由传递参数啦
A:首先得再路由中定义一下呗,看图
image.png
这里用的params传递参数
B:然后再shop.js点击修改的时候,方法里面
image.png
当然也可以用简单写法

history.push("/shopEdit/"+id);

或者query也可以哦

this.props.history.push({pathname:'/shopEdit',query:{id:id}});

看你自己的喜欢啦~~~~
然后再edit.js里面就是获取路由传递过来的参数呗
如果是pamrms传递过来就是这么取值

let  sendId=this.props.match.params.id ;

如果是query传递过来就这么取值

		//query  传值
		let   {location} = this.props;
		let   {id}  =location.query;

贴图说明
image.png

这里基本就完成啦
image.png
然后就是获取id,请求后台接口,获取后台数据,填充到页面就好啦

#####现在来讲解后台啦
1:当然先安装mysql数据库啦,然后phpstudy管理工具
image.png

建立好shop表,字段如上,明白人一看就懂了吧~~~
2:进入shopServer文件夹,安装所有包

cnpm  i koa koa-bodyparser koa-router koa2-cors mysql nodemon --save

说明一下:
nodemon是一个自动修改配置重启生效的工具很好用
koa2-cors是跨域会用到的,前端访问后端会遇到跨域问题

image.png

类似上面这样,等会儿我们来解决吧~~~
image.png

安装好所有依赖后,先写个接口试试呗
3:好就先列表接口呗
src下面新建index.js

//列表
router.get("/list",async (ctx)=>{	
	let results= await mysqlData.query();
	if(results.length > 0){
		ctx.body={
			"code":1,
			"msg":"ok",
			"data":results
		}
	}else{
		ctx.body={
			"code":0,
			"msg":"fail",
			"data":"获取失败"
		}
	}
		
});

这里就是连接数据库,查询数据啦
image.png
image.png
这里连接数据库,启动端口9090

var  mysql  = require('mysql')
var    mysqlConfig  = require('../config/config.js')
	//列表查询
	query(){
		return new Promise((resolve,reject)=>{
			mysqlConfig.getConnection((err,connection)=>{
				const  listSql = "select * from shop";
				connection.query(listSql,(err,results,fields)=>{
					if(err){
						throw  err
					};
					resolve(results);
				})
			})
			
		})	
	}

启动后效果图
image.png

看到启动成功~~~~
虽然启动成功啦,但是前台访问后台跨域问题还没解决,怎么办了?
如果有webpack当然,跨域配置proxy,但是我们这里没有,我们用koa2-cors这个中间来处理下

var cors = require('koa2-cors');

// 配置跨域
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With')
  ctx.set('Access-Control-Allow-Origin', '*');
  ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
  ctx.set('Access-Control-Allow-Credentials', true);
  ctx.set('Access-Control-Max-Age', 3600 * 24);
  await next();
})

app.use(cors({
  // origin: function(ctx) {
  //   if (ctx.url === '/test') {
  //     return false;
  //   }
  //   return '*';
  // },
  origin:'http://127.0.0.1:3000',
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,
  credentials: true,
  allowMethods: ['GET', 'POST', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));

app.use(router.routes());   /*启动路由*/
app.use(router.allowedMethods());

app.listen("9090");
console.log('app started ...');

最重要的origin,这里我们的跨域就配置成功啦!!

查询接口完成,接下来就是删除mysql文件下index.js

	//删除
	delete(id){
		return new Promise((resolve,reject)=>{
			mysqlConfig.getConnection((err,connection)=>{
				const  deleteSql = "delete  from shop where id =" +id;
				connection.query(deleteSql ,(err,results,fields)=>{
					if(err){
						throw  err
					};
					console.log(results);
					resolve(results)
				})
			})	
		})
	}

删除调用

//删除
router.get("/delete/:id",async (ctx)=>{
	let  id = ctx.params.id;
	let  data2 = await mysqlData.delete(id);
	let  affectedRows = data2.affectedRows;
	if(affectedRows==1){
		ctx.body={
			"code":1,
			"msg":"ok",
			"data":"删除成功"
		}
	}else{
		ctx.body={
			"code":0,
			"msg":"fail",
			"data":"删除失败"
		}
	}
	
});

注意因为所有的数据库操作都是异步,所以我们必须用promise异步处理下,然后返回处理结果才可以

新增接口

	//新增数据
	add(name,price,number,key){
		return new Promise((resolve,reject)=>{
			mysqlConfig.getConnection((err,connection)=>{
				const  addSql = `insert into  shop values(null,${name},${price},${number},${key},null)`;
				connection.query(addSql,(err,results, fields)=>{
					if(err){
						throw err
					}
					console.log("result", results)//表中数据
					resolve(results)
					connection.release()
				})
			});
			
		})
		
	}

新增调用

//用post请求 
router.post("/add",async (ctx)=>{
	let  body=ctx.request.body;
	let  name=ctx.request.body.name;
	let  price=ctx.request.body.price;
	let  number=ctx.request.body.number;
	let  key= Math.floor(Math.random()*100);
	//调取数据库插入方法
	let  isSuccess = await mysqlData.add(name,price,number,key);
	let  insertId =   isSuccess.insertId;
	if(insertId){
		ctx.body={
			"code":1,
			"msg":"ok",
			"data":"商品添加成功"
		}
	}else{
		ctx.body={
			"code":0,
			"msg":"fail",
			"data":"商品失败"
		}
	}
	
});

注意这里的新增是post提交请求,获取数据必须用到koa-bodyparser这个中间件才可以哦

修改前查询接口

	//修改查询
	select(id){
		return new Promise((resolve,reject)=>{
			mysqlConfig.getConnection((err,connection)=>{
				let  selectSql = 'select * from shop where id='+id;
				connection.query(selectSql,(err,results,fields)=>{
					if(err){
						throw  err
					};
					//console.log(results)
					resolve(results);
				})
			})
		})
		
	}

修改前查询调用

//修改  先根据id查询结果  1 
router.get("/select/:id",async (ctx)=>{
	let  id = ctx.params.id;
	let  result = await mysqlData.select(id);
	if(result.length > 0){
		ctx.body={
			"code":1,
			"msg":"ok",
			"data":result
		}
	}else{
		ctx.body={
			"code":0,
			"msg":"faile",
			"data":"查询失败"
		}
	}
	
});

修改更新接口

	//修改更新数据
	update(sendId,name,price,number,mark){
		return new Promise((resolve,reject)=>{
			mysqlConfig.getConnection((err,connection)=>{
			const  updateSql = `update shop set name = "${name}",price = ${price},number = ${number},mark=${mark} where id = ${sendId}`;
				connection.query(updateSql,(err,results, fields)=>{
					if(err){
						throw err
					}
					console.log("result", results)//表中数据
					resolve(results)
					connection.release()
				})
			});
			
		})
		
	}

修改更新接口调用

router.post("/update",async (ctx)=>{
	//body中间件解析post提交数据
	let  body=ctx.request.body;
	let  sendId = ctx.request.body.id * 1 ;
	let  name=ctx.request.body.name;
	let  price=ctx.request.body.price;
	let  number=ctx.request.body.number;
	let  key= Math.floor(Math.random()*100);
	let  mark= Math.floor(Math.random()*100);
	let  isSuccess = await mysqlData.update(sendId,name,price,number,mark);
	if(isSuccess.affectedRows == 1){
		ctx.body={
			"code":1,
			"msg":"ok",
			"data":"商品修改成功"
		}
	}else{
		ctx.body={
			"code":0,
			"msg":"fail",
			"data":"商品修改失败"
		}
	}

});

注意接口写完了,可以用postmon先测试下,注意我碰到过修改语句一直报错的问题
1:新增必须键名值对应,顺序很重要,null都可以,否则添加失败,也可以手动字段名称,防止出错,key很关键

const  addSql = `insert into  shop values(null,${name},${price},${number},${key},null)`;

2:修改更新时候,老是sql异常,找了很久百度才知道name只能是string类型才可以,单独加个“”就可以啦

const  updateSql = `update shop set name = "${name}",price = ${price},number = ${number},mark=${mark} where id = ${sendId}`;

最后上几个效果图

新增
image.png

删除
image.png
image.png

修改
image.png

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值