项目展示

项目上线(移动端访问):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等待页面渲染完成后创建组件

Logo

更多推荐