vue网易严选购物商城项目
vue网易严选购物商城项目项目展示项目上线(移动端访问):http://182.92.226.242:8080/#/indexlist项目源码: https://github.com/wy-linux/yan_xuan项目截图:项目模块登录模块页面布局密码与验证码登录切换密码输入框的密码显示与隐藏业务逻辑验证码发送登录功能警告提示框用户的持久登陆购物车模块页面布局vue动画业务逻辑购物车添加商品购
项目展示
项目上线(移动端访问):http://182.92.226.242:8080/#/indexlist
项目源码: https://github.com/wy-linux/yan_xuan
项目截图:
项目模块
登录模块
页面布局
<form @submit.prevent="login">
<div class="first " :class="{on: way}">
<section >
<input type="text" placeholder="请输入手机号" v-model="phone">
</section>
<section style="position: relative; margin-top: .7rem">
<input type="password" placeholder="请输入短信验证码" v-model="msg">
<button class="yan_zh" :class="{color: count}" @click.prevent="getMsg">{{count ? `已发送(${count}s)` : '获取验证码'}}</button>
<span class="phone" :class='{on: checkPhone}'>请输入正确的手机号</span>
<span class="phone" :class='{on: idenCode}'>验证码是{{idenCode}}</span>
</section>
<section style="margin-top: .8rem">
<div style="overflow: hidden">
<button>登录</button>
</div>
</section>
</div>
<div class="second " :class="{on: !way}">
<section >
<input type="text" placeholder="请输入手机号" v-model="phone">
</section>
<section style="position: relative">
<input type="password" placeholder="请输入密码" v-model="pwd" v-show="!showPwd">
<input type="text" placeholder="请输入密码" v-model="pwd" v-show="showPwd">
<div class="switchBtn" :class="{green: showPwd}" @click='showPwd = !showPwd'>
<div class="circle" :class="{scroll: showPwd}"></div>
<span style="float: left; font-size: .3rem">{{showPwd ? 'abc' : ''}}</span>
</div>
<div class="tishi" style="font-size: .25rem; font-weight: 700; width: 7rem; margin-top: .1rem; text-align: left">* 提示: 登录密码为用户第一次输入的密码,请务必牢记。<br />* 如有遗忘,请使用验证码登录</div>
</section>
<section>
<div style="overflow: hidden; margin-top: .4rem">
<button>登录</button>
</div>
</section>
</div>
</form>
密码与验证码登录切换
设置两个登录表单,用户点击时改变:class="{on: way}"中way的值来实现密码与验证码登录的切换
密码输入框的密码显示与隐藏
放置两个输入框
<input type="password" placeholder="请输入密码" v-model="pwd" v-show="!showPwd">
<input type="text" placeholder="请输入密码" v-model="pwd" v-show="showPwd">
<div class="switchBtn" :class="{green: showPwd}" @click='showPwd = !showPwd'>
<div class="circle" :class="{scroll: showPwd}"></div>
<span style="float: left; font-size: .3rem">{{showPwd ? 'abc' : ''}}</span>
</div>
1.两个输入框绑定相同数据pwd, 用户切换时隐藏其中一项表单
2.用户切换文本输入框时,给切换按钮添加类名scroll,实现按钮的滚动动画
业务逻辑
验证码发送
async getMsg() {
if(/^1\d{10}$/.test(this.phone)) {
let result = ''
if(!this.count) {//防止用户重复点击
this.count = 30
var intervalId = setInterval(() => {
this.count--;
if(this.count <= 0) {
clearInterval(intervalId)
}
}, 1000);
result = await axios.get('/idenCode?phone='+ this.phone)
}
if(result.status == 0) {
this.checkPhone = false;
if(this.count) {
clearInterval(intervalId)
this.count = 0;
}
this.idenCode = result.code;
}
}
else {
this.idenCode = ''
this.checkPhone = true
}
},
1.使用正则表达式检验用户输入手机号是否合法
2.在data中定义数据count,用户点击发送验证码时,触发定时器每1秒count减1;
登录功能
async login() {
let r;
if(this.way) {
if (!/^1\d{10}$/.test(this.phone)) {
this.text = '手机号输入不正确',
this.alertShow = true
return
} else if(!/^\d{6}$/.test(this.msg)) {
this.text = '验证码格式错误',
this.alertShow = true
return
} else {
r = await axios.post('/login_c', {
phone: this.phone,
code: this.msg
})
}
} else {
if(!/^1\d{10}$/.test(this.phone)) {
this.text = '手机号输入不正确',
this.alertShow = true
return
} else if (!/^\w{8,16}$/.test(this.pwd)) {
this.text = '密码必须8-16位数字或者字母',
this.alertShow = true
return
} else {
r = await axios.post('/login_pwd', {
phone: this.phone,
pwd: this.pwd,
})
}
}
this.checkPhone = false
if(this.count) {
clearInterval(intervalId)
this.count = 0;
}
if(r.status == 0) {
this.$store.commit('userChange',r.user)
this.$router.replace('/profile')
} else {
this.alertShow = true
this.text = r.msg
return
}
},
1.使用正则表达式对用户输入内容的校检
2.根据用户登录方式向后端发送请求。当返回data.status == 0 时,将拿到的数据放到vuex中管理,并跳转到登陆页;否则,将后端的错误信息展示在页面上
警告提示框
<alert-tip :msg='text' @destroy='change' v-show='alertShow' />
<template>
<div class="alertTip">
<div class="contain">
<div class="tap_icon">
<span class="top"></span>
<span class="foot"></span>
</div>
<p>{{msg}}</p>
<div class="enter" @click="$emit('destroy')">确认</div>
</div>
</div>
</template>
1.封装alert-tip组件,用自定义属性将警告信息传递到组件中
2.用户点击确认时触发自定义事件destory,将组件的状态重置为隐藏
用户的持久登陆
mounted() {
this.$store.dispatch('userGet')
},
async userGet(context) {
let res = await axios('/session')
if (res.status == 0) {
context.commit('userChange', res.user)
}
},
在app根组件中触发axios请求拿到对应的用户信息
<router-link :to="user.phone ? '/cared' : '/car'"> <i class="iconfont icon-cart" :class="{on: $route.path == '/car' || $route.path == '/cared'}"></i> <span class="text" :class="{on: $route.path == '/car' || $route.path =='/cared'}">购物车</span> </router-link >
<router-link :to="user.phone ? '/profile' : '/login'"> <i class="iconfont icon-nickname" :class="{on: $route.path == '/login' || $route.path=='/profile'}"></i> <span class="text" :class="{on: $route.path == '/login'|| $route.path == '/profile'}">个人</span> </router-link>
根据vuex中是否有用户手机号动态去往登陆与非登录页面
购物车模块
页面布局
vue动画
.move-enter-active,.move-leave-active {
transition: all .3s;
}
.move-enter, .move-leave-to {
transform: translateY(15rem)
}
在用户点击加入购物车时,将购物车的v-if设置为true,vue加载过渡动画的弹出效果
业务逻辑
购物车添加商品
this.$store.dispatch('carput',{
index: this.$store.state.indexGoodDetails.select[this.index],
value: this.value,
img: this.$store.state.indexGoodDetails.swiper[0],
title: this.$store.state.indexGoodDetails.title,
price: this.$store.state.indexGoodDetails.price,
check: 0,
} )
this.$store.commit('countAdd')
this.carShow = !this.carShow
1.首先判断用户是否选择商品规格
2.而后向后端发送请求,后端查询到用户列表下的购物车列表后用push方法新增当前项而后更新到数据库中
let user = await User.findOne({ phone: req.body.phone });
user.carlist.push(req.body.carlist)
let a = await User.updateOne({ phone: req.body.phone }, { carlist: user.carlist })
购物车的全选与单选
1.设置一个checkOne的类名,当拥有这个类名时处于选中状态
2.用户点击选中框时判断当前项是否有checkOne类名,如果有
则添加类名;反之,移除类名
checkshop(e,index) {
if(e.target.classList.contains('checked')) {
e.target.classList.remove('checked')
this.count--
this.$store.state.user.carlist[index].check = 0
} else {
e.target.classList.add('checked')
this.count++
this.$store.state.user.carlist[index].check = 1
}
},
3.每一次类名添加时将count值加1,渲染列表中当前项的check属性置为1
selectAll() {
var is = document.querySelectorAll('.checkbox')
if(!this.checkAll) {
for(var i = 0 ; i < is.length; i++){
is[i].classList.add('checked')
}
this.count = this.$store.state.user.carlist.length
for(let j = 0; j < this.$store.state.user.carlist.length; j++) {
this.$store.state.user.carlist[j].check = 1
}
} else {
for(var i = 0 ; i < is.length; i++){
is[i].classList.remove('checked')
}
this.count = 0
for(let j = 0; j < this.$store.state.user.carlist.length; j++) {
this.$store.state.user.carlist[j].check = 0
}
}
this.checkAll = !this.checkAll
},
4.用户点击全选按钮时,给每一项添加checkOne类名,将count的值置为列表数据的长度,每一项的check属性置为1
count() {
var is = document.querySelectorAll('.checkbox')
if(this.count == this.$store.state.user.carlist.length && this.count != 0) {
this.checkAll = true
for(var i = 0 ; i < is.length; i++){
is[i].classList.add('checked')
}
} else {
this.checkAll = false
}
},
5.在watch中监听count的变化, 当count的值等于列表长度时,将全选框添加checkAlll类名,其余项添加checkOne类名;否则,移除全选框的选中状态
购物车的商品结算
computed: {
total() {
let sum = 0;
for(let i = 0; i < this.$store.state.user.carlist.length; i++) {
sum += Number(this.$store.state.user.carlist[i].price.slice(1)) * Number(this.$store.state.user.carlist[i].value) * Number(this.$store.state.user.carlist[i].check)
}
return sum
}
},
添加计算属性total,循环购物车的商品列表,结算总价格 = 每一项商品的价格 * 商品数量 * 商品是否选中(选中为1,未选中是0)
购物车商品的删除
mode() {
var is = document.querySelectorAll('.checkbox')
var is = document.querySelectorAll('.checkbox')
for(var i = 0 ; i < is.length; i++){
is[i].classList.remove('checked')
}
for(let j = 0; j < this.$store.state.user.carlist.length; j++) {
this.$store.state.user.carlist[j].check = 0
}
this.checkAll = false
this.count = 0
}
}
1.当用户点击编辑时, 将mode = true。编辑按钮隐藏,删除按钮出现;
2.在watch中监听mode值的变化(mode值的变化标志用户在结算模式与删除模式的切换),将全选框与单选框的状态置为未选中状态, count = 0 ,所有项的check属性 = 0)
moded() {
for(let j = 0; j < this.$store.state.user.carlist.length; j++) {
if(this.$store.state.user.carlist[j].check) {
this.$store.state.user.carlist.splice(j, 1)
j-- //注意
}
}
this.$store.commit('countChange', this.$store.state.user.carlist.length)
Axios.post('/cardelete', {
carlist: this.$store.state.user.carlist
})
this.mode = false
}
3.用户点击删除时,遍历购物车列表,将check属性1的项全部删除(注意splice方法删除数组时,数组的后一项会自动变成前一项,记得循环计数器器 j --),然后将列表中的数据同步到后端
router.post('/cardelete', async(req, res) => {
console.log(req.body);
let a = await User.updateOne({ phone: req.session.phone }, { carlist: req.body.carlist })
})
4.后端拿到数据时,将数据库中的carlist列表更新为当前列表
购物车的状态维持
mounted() {
this.$store.dispatch('userGet')
},
用户每次刷新购物车页面时, 向后端发送请求;后端接收请求,下发购物车列表数据
首页模块
首页商品列表的渲染
watch: {
index() {
if(this.index <= 2) {
this.$store.dispatch('indexGet', this.index)
this.$store.dispatch('indexHeadGet', this.index)
if(this.$route.path !== '/indexlist') {
this.$router.replace('/indexlist')
}
} else if(this.index == 3){
this.$store.dispatch('indexHeadGet', this.index)
if(this.$route.path !== '/default') {
this.$router.replace('/default')
}
} else {
return
}
}
1.用户点击tab栏时将tab栏的索引传递datade的index中
2.index发生变化时向后端发送请求,并携带请求的索引
router.get('/goods/:index', async(req, res) => {
req.session.index = req.params.index
console.log(req.session.index);
switch (req.params.index) {
case '0':
let good0 = await Family.find({})
if (good0) {
res.send({
status: 0,
good0
})
}
break;
case '1':
let good1 = await clothes.find({})
if (good1) {
res.send({
status: 0,
good1
})
}
break;
case '2':
let good2 = await wine.find({})
if (good2) {
res.send({
status: 0,
good2
})
}
break;
}
})
3.后端根据请求的索引,查询不同的数据库并返回不同都商品列表
首页商品详情页的渲染
mounted() {
this.$store.dispatch('indexGoodDetailsGet', this.$route.params.index)
},
1.通过路由params传参,将点击的商品索引传递到详情页。
详情页携带索引向后端发送请求
router.get('/goodsDetails/:index', async(req, res) => {
switch (req.session.index) {
case '0':
let good0 = await FamilyDetails.find({}).limit(1).skip(Number(req.params.index))
if (good0) {
res.send({
status: 0,
good0: good0[0],
index: req.session.index
})
}
break;
case '1':
let good1 = await clothesdetails.find({}).limit(1).skip(Number(req.params.index))
if (good1) {
res.send({
status: 0,
good1: good1[0],
index: req.session.index
})
}
break;
case '2':
let good2 = await winedetails.find({}).limit(1).skip(Number(req.params.index))
if (good2) {
res.send({
status: 0,
good2: good2[0],
index: req.session.index
})
}
break;
}
})
2.后端在session中查询用户上次访问的数据库序列并确定本次查询的数据库序列,而后根据本次请求携带的索引跳过查询,限制查询获取当前的详情页数据
值得买模块
轮播图的数据处理
pay_swippers(state) {
var min = [];
var arr = []
state.pay_swipper.forEach(value => {
min.push(value)
if (min.length % 8 === 0) {
arr.push(min);
min = []
}
})
return arr
},
1.该页面轮播图为两页, 每页8个商品
2.后端返回数据为16个商品的数组,使用foreach方法遍历每一个项,当小数组的值等于8 个时将整个小数组添加到整个大数组中
评论页的数据处理
paylistPack(state) {
var min = [];
var arr = [];
state.paylist.forEach(value => {
min.push(value)
if (min.length == state.paylist.length / 2) {
arr.push(min);
min = [];
}
})
return arr
}
}
1.评论页为左右两个盒子, 每个盒子内部的项目个数不固定
2.后端返回数据为16个项目的数组,使用foreach方法遍历每一项,当小数组的值等于遍历数组长度的一半时,将小数组添加到大数组中
注意事项
axios配置跨域访问携带cookie
axios.defaults.withCredentials = true
1.在axios的默认设置中把withCredentials = true
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Credentials", true)
res.header("Access-Control-Allow-Headers", "Content-Type");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
next();
});
2.后端需要配置
res.header(“Access-Control-Allow-Credentials”, true)
注意: res.header(“Access-Control-Allow-Origin”, req.headers.origin);其中req.headers.origin 不能为 ’ * ',
*会与credentials起冲突
3.新版chrome 的cookie中新增了一个SameSite属性,默认http请求时不携带session id SameSite = Lax ;手动将SameSite = NONE时 站点必须开启https传输, 否则无效
4. 查阅资料
将以上三个选项禁用, 终于能够跨域携带session id
使用swiper插件创建实例的时间
watch : {
list() {
this.$nextTick(function () {
new Swiper ('.swiper-container', {
pagination: {
el: '.swiper-pagination',
},
}
})
}
}
使用this.$nextTick等待页面渲染完成后创建组件
更多推荐
所有评论(0)