Vue2积分商城项目

一、清空项目非必要文件和用户片段,路径提示的配置

  • views 下面的文件只保留 Home.vue ,其余删除,删除 components/HelloWorld.vue,并且 Home.vue 中不再引入 HelloWorld 组件。

  • 删除 src/assets 下的图片,换成我们的img文件夹。

  • router/index.jsabout 的路由注释掉。

  • 删除 App.vue 中的less样式

  • VsCode用户片段提供

请在VScode中设置如下代码片段,以用于后面每一个案例的创建

{
    "demo": {
      "prefix": "vue",
      "body": [
        "<template>",
        "\t<div>",
        "\t\t$0",
        "\t</div>",
        "</template>",
        "",
        "<script>",
        "export default {",
        "\tdata () {",
        "\t\treturn {\n",
        " ",
        "\t\t}",
        "\t}",
        "}",
        "</script>",
        " ",
        "<style lang = \"less\" scoped>",
        "\t",
        "</style>"
      ],
      "description": "自定义的一个vue代码段"
    }
  }

 

  • “@/”路径提示配置

    安装 Path Intellisense插件

    打开设置 - 首选项 - 搜索 Path Intellisense - 打开 settings.json ,添加:

"path-intellisense.mappings": {
     "@": "${workspaceRoot}/src"
 }

在项目 package.json 所在同级目录下创建文件 jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}

最后重启打开即可

 

二、样式初始化

发现页面会有自带的间距,这是浏览器本身的默认样式,所以我们需要进行样式初始化,清除浏览器默认样式。

安装初始化样式库reset-css:

npm i reset-css 或者   yarn add reset-css

安装成功后在main.js中引入即可:

import "reset-css"

三、网站结构布置

在App.vue中设置好头部,导航和尾部组件:

<template>
  <div id="app">
        <Tabbar></Tabbar>
        <Header></Header>
        <router-view/>
        <Footer></Footer>
  </div>
</template>
<script>
import Header from '@/components/Header'
import Tabbar from '@/components/Tabbar'
import Footer from '@/components/Footer'
​
export default {
    components:{
        Header,Tabbar,Footer
    },
   
}
</script>

在@/components目录下新建Header,Tabbar,Footer三个组件即可

 

四、网站数据请求模块

4.1、安装axios

作为一个网站前端,数据请求模块少不了。我们需要安装axios模块:

npm i axios

4.2、代理配置

我们对 vue.config.js 进行配置:

module.exports = {
    devServer: {
        port: 8080,
        proxy: {
            '/api': {
                target: "http://192.168.113.249:8081/cms",
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
}

由于配置文件修改了,这里一定要记得重新 npm run serve !!

4.3、API与Request封装

src 下新建 request目录 ,

 

request目录下新建 request.js

request.js 中:

// 书写请求拦截器 和 响应拦截器
import axios from "axios";
import store from "@/store"



// 创建axios实例  (instance)
const instance = axios.create({
    baseURL: "/api",
    timeout: 5000
})

// 请求拦截器
instance.interceptors.request.use(config => {
    // 什么时候执行这个函数?   发送请求之前
    // config是什么?  记录了本地请求的相关信息的一个对象
    // 这个函数能用来做什么?  做一些请求之前的操作(例如:添加请求头,例如token)

    let token = localStorage.getItem("x-auth-token")
    if (token){
        config.headers["x-auth-token"] = token
    }

    // console.log("执行了 请求拦截器的代码", config);
    return config
}, err => {
    return Promise.reject(err)
});


// 响应拦截器
instance.interceptors.response.use(res => {
    // 什么时候执行这个函数?   在接收到后端服务器的响应之后,进入到组件内部的then方法之前执行这里的代码
    // res是什么?     res是axios封装好的一个响应对象
    // res.data就是后端服务器返回给我们的数据
    // 这个函数能用来做什么?   做一些数据的统一处理

    //统一处理响应码不为0的情况 

    let res_data = res.data
   
    let arr=[0,400,407]
    if (!arr.includes(res_data.code)) {
        //提示用户,操作失败
        store.dispatch("toastStatus/asyncChanIsShowToast", {
            msg: res_data.message,
            type: "danger",
        })
        return false
    }


    // console.log("执行了 响应拦截器 的代码", res);
    return res_data   // return 后面的这个值被组件中的请求的then方法的res接收
}, err => {
    // 相当于在组件中,发送请求之后的catch方法
    store.dispatch("toastStatus/asyncChanIsShowToast", {
        msg: err,
        type: "danger",
    })
    return Promise.reject(err)
});



export default instance

为了更好地管理我们的这些接口,我们把所有请求都抽取出来在一个api.js中

request目录下新建api.js,api.js 中:

import request from './request'

// 请求精品推荐数据
export const JingpinAPI = () => request.get('/products/recommend')

4.4、发起请求和获取数据

App.vue 中:

import {JingpinAPI} from "@/request/api"

async created() {
    let jingPinRes=await JingPinAPI();
    this.jingpinArr=jingPinRes.data.data.records;
  },

五、创建公共样式less文件

在assets在新建css目录,新建global.less文件

.wrap{
    width: 1200px;
    margin: 0 auto;
}

在main.js中全局引入

import "@/assets/css/global.less"

 

 

六、路由配置及导航项当前样式

在router/index.js中配置重定向,及几个导航路由:

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import store from "@/store"

const originalPush = VueRouter.prototype.push;

VueRouter.prototype.push = function (location) {
  return originalPush.call(this, location).catch(err => { })
};

Vue.use(VueRouter)

const routes = [
  {
    path:"/",
    redirect:"/home"
  },
  {
    path: '/home',
    name: 'home',
    component: HomeView
  },
  // {
  //   path: '/about',
  //   name: 'about',
  //   component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  // }
  {
    path: '/goods',
    name: 'goods',
    component: () => import(/* webpackChunkName: "goods" */ '../views/GoodsView.vue')
  },
  {
    path: '/user',
    name: 'user',
    redirect:"/user/cart",
    component: () => import(/* webpackChunkName: "user" */ '../views/UserView.vue'),
    children:[
      {
        path: '/user/cart',
        name: 'cart',
        component: () => import(/* webpackChunkName: "cart" */ '../components/user/Cart.vue')
      },
    ]
  },
  {
    path: '/order',
    name: 'order',
    component: () => import(/* webpackChunkName: "order" */ '../views/OrderView.vue')
  },
  {
    path: '/free',
    name: 'free',
    component: () => import(/* webpackChunkName: "free" */ '../views/FreeView.vue')
  },
  {
    path: '/details',
    name: 'details',
    component: () => import(/* webpackChunkName: "details" */ '../views/DetailsView.vue')
  },
  {
    path:"*",
    name: 'error',
    component: () => import(/* webpackChunkName: "error" */ '../views/ErrorView.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})



// 全局路由守卫 
// router.beforeEach((to, from, next)=>{
// //   // 什么执行这个函数?   路由跳转之前就会执行这里的代码
// //   console.log("to为: ", to);  // 要去到的路由对象
// //   console.log("from为: ", from);  //从哪个路由出发的路由对象


// //   // 需求: 在进入到/user之前, 判断有没有token   有就放行, 没有就提示请先登录, 不放行
//   if(to.path=="/user"){
//     let token = localStorage.getItem("x-auth-token");
//     if(token){
//       // 有就放行
//       next()
//     }else{
//       // 提示
//       store.dispatch("toastStatus/asyncChanIsShowToast",{
//         msg:"请先登录!",
//         type:"warning"
//       });

//     }

//     return // 需要写return  防止上面的next()执行完,还继续执行下面的next()
//   }

//   next()  // 放行
// })


export default router

在View目录中新建对应组件。

处理导航项当前样式,在Tabbar.vue中

<div class="c">
    <ul>                                                 
        <li :class="$route.path==='/home'?'active':''">首页</li>
        <li :class="$route.path==='/goods'?'active':''">全部商品</li>
        <li :class="$route.path==='/user'?'active':''">个人中心</li>
        <li :class="$route.path==='/order'?'active':''">我的订单</li>
        <li :class="$route.path==='/free'?'active':''">专属福利</li>
    </ul>
</div>
<style>
...
    .c ul{
            width: 500px;
            display: flex;
            justify-content: space-between;
            color:#242B39;
            font-size: 16px;
            font-family: SourceHanSansSC-Medium;
            font-weight: 500;
            .active{
                color:#0A328E;
            }
        }
</style>    

并且设置点击跳转路由:

<ul>                                                 
     <li @click="$router.push('/home')" :class="$route.path==='/home'?'active':''">首页</li>
     <li @click="$router.push('/goods')" :class="$route.path==='/goods'?'active':''">全部商品</li>
     <li @click="$router.push('/user')" :class="$route.path==='/user'?'active':''">个人中心</li>
     <li @click="$router.push('/order')" :class="$route.path==='/order'?'active':''">我的订单</li>
     <li @click="$router.push('/free')" :class="$route.path==='/free'?'active':''">专属福利</li>
</ul>

七、登录模块(登录即自动注册)

7.1、模态窗口的书写

点击登录按钮,弹出模态窗口,如下图:

 

我们在cpmonents目录下新建一个Login.vue组件

然后在App.vue组件中引入,注册,使用。

<div id="app">
    <Header></Header>
    <Nav></Nav>
    <router-view/>
    <Footer></Footer>
    <Login></Login>
</div>

7.2、设置点击展示模态窗口

因为项目中可能有在各个组件中触发这个模态窗口的展示,所以控制模态框展示的变量可以放在Vuex中

在store中新建showModal目录,并在其中新建index.js:

export default{
    namespaced:true,//记得加上命名空间
    state: {
        isShowLoginModal:false  // 用来表示是否展示登录模态窗口
    },
    mutations: {
        // 修改是否展示的值
        chanIsShowLoginModal(state,payload){
            state.isShowLoginModal = payload
        }
    },
    actions: {
    },
}

在store.js中引入:

import Vue from 'vue'
import Vuex from 'vuex'
import showModal from "./showModal"
Vue.use(Vuex)

export default new Vuex.Store({

  modules: {
    showModal
  }
})

Login.vue组件中:

<template>
    <div class="modal" v-show="isShowLoginModal">
        <div class="mask"></div>
        <div class="login-box">

        </div>
    </div>
</template>

<script>
import {mapState} from "vuex"
export default {
    computed:{
        ...mapState({
            isShowLoginModal:state=>state.showModal.isShowLoginModal
        })
    }
}
</script>

Header.vue组件中:

<template>
    <div class="header">
        <div class="wrap header-wrap">
            ...
            <div class="r">
                <ul>
                   ...
                    <li class="login-btn" @click="chanIsShowLoginModal(true)">登录</li>
                </ul>
            </div>
        </div>
    </div>
</template>

<script>
import {mapMutations} from "vuex"
export default {
    data () {},
    methods:{
        ...mapMutations({
            chanIsShowLoginModal:"showModal/chanIsShowLoginModal"
        })
    }
}
</script>

7.3、点击关闭模态窗口

Login.vue中:

<template>
    <div class="modal" v-show="isShowLoginModal">
        <!--点击遮罩层也可以关闭模态窗口-->
        <div class="mask"  @click="chanIsShowLoginModal(false)"></div>
        <div class="login-box">
            <div class="close" @click="chanIsShowLoginModal(false)"></div>
        </div>
    </div>
</template>
<script>
import {mapState,mapMutations} from "vuex"
export default {
      ...
     methods:{
        ...mapMutations({
            chanIsShowLoginModal:"showModal/chanIsShowLoginModal"
        })
    }
}
</script>
<style lang = "less" scoped>
    .modal{
        ...
        .login-box{
           ...
            .close{
                width: 22px;
                height: 22px;
                background: url("../assets/img/close.png");
                position: absolute;
                right: 60px;
                top: 16px;
                cursor: pointer;
            }
        }
    }
</style>

7.4、点击标题栏的切换效果

Login.vue中:

<ul class="title">
    <li @click="isShowForm=true" :class="{active:isShowForm}">手机号码登录</li>
    <li style="margin:0 10px">|</li>
    <li @click="isShowForm=false" :class="{active:!isShowForm}">微信扫码登录</li>
</ul>
<div class="body">
    <div class="form" v-show="isShowForm">
        表单
    </div>
    <div class="qrcode" v-show="!isShowForm">
        二维码
    </div>
</div>
<script>
export default {
    data () {
        return {
            isShowForm:true
        }
    }
    ...
}
</script>
<style lang = "less" scoped>
    .modal{
       ...
        .login-box{
            ...
            .title{
                display: flex;
                justify-content: center;
                width: 100%;
                padding-top: 50px;
                font-size:20px;
                color:#999;
                .active{
                    color:#333;
                }
            }
            .body{
                width: 355px;
                margin:20px auto 0;
                height:200px;
               
            }
        }
    }
</style>

 

八、拼图验证滑块

插件参考:https://gitee.com/monoplasty/vue-monoplasty-slide-verify

8.1、安装插件

npm install --save vue-monoplasty-slide-verify
或者
yarn add vue-monoplasty-slide-verify

8.2、main.js入口文件引中入

import SlideVerify from 'vue-monoplasty-slide-verify' // 拼图验证码

Vue.use(SlideVerify)

8.3、在组件中使用

<template>
    <slide-verify :l="42" :r="20" :w="362" :h="140" @success="onSuccess" @fail="onFail" @refresh="onRefresh" :style="{ width: '100%' }" class="slide-box" ref="slideBlock" :slider-text="msg"></slide-verify>
</template>

<script>
export default {
  data() {
    return {
      msg: "向右滑动"
    };
  },
  methods: {
    // 拼图成功
    onSuccess(times) {
      let ms = (times / 1000).toFixed(1);
      this.msg = "login success, 耗时 " + ms + "s";
    },
    // 拼图失败
    onFail() {
      this.onRefresh(); // 重新刷新拼图
    },
    // 拼图刷新
    onRefresh() {
      this.msg = "再试一次";
    },
  },
};
</script>

<style lang="less" scoped>
/deep/.slide-box {
    width: 100%;
    position: relative;
    box-sizing: border-box;
    canvas {
        position: absolute;
        left: 0;
        top: -120px;
        display: none;
        width: 100%;
        box-sizing: border-box;
    }
    .slide-verify-block{
        width: 85px;
        height: 136px;
    }
    .slide-verify-refresh-icon {
        top: -120px;
        display: none;
    }
    &:hover {
        canvas {
            display: block;
        }
        .slide-verify-refresh-icon {
            display: block;
        }
    }
}
</style>

8.4、点击登录按钮,判断是否有进行拼图滑块验证

登录之前,我们需要验证用户是否有拼图验证过,有拼图验证过才可以登录。

我们以msg文字内容来判断是否有进行拼图滑块验证

<div class="mb20 btn" @click="submitFn">
    登录
</div>
...
<script>
    ...
    methods:{
       ...
        // 点击登录按钮
        submitFn() {
               // 以msg文字内容来判断是否有进行拼图滑块验证
            if (this.msg == "再试一次" || this.msg == "向右滑动") {
                alert("请滑动拼图");
                return
            } 
               alert("拼图滑块验证通过,可以执行登录了")
        },
    }
</script>

九、点击获取验证码按钮的逻辑

9.1、逻辑分析

可以正常获取验证码的前提是:手机号格式正确

所以,点击获取验证码的逻辑如下:

1、如果校验手机号格式不正确,则return

2、滑块拼图验证不通过,则return

3、验证成功后,发起请求,获取验证码成功,则进行倒计时

【百度】结合运营商之后的手机号码的正则:

/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/

9.2、点击获取验证码判断手机号格式

<div class="btn checkcode-btn" @click="getCode">获取验证码</div>
...
<script>
    getCode(){
        // 1、验证手机号是否正确
        if(!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(this.phoneNum)){
            alert("请输入正确的手机号");
            this.$refs.phone.focus();
            return
        } 
        alert("手机号格式正确");
        
        // 2、进行滑块验证
           
        // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数
        
      
    },

</script> 

9.3、倒计时及其展示

<div class="btn checkcode-btn" @click="getCode">
    <span v-show="!isShowCount">获取验证码</span>
    <span v-show="isShowCount">{{count}} s</span>
</div>
<script>
    methods:{
        countdown(){
            // 计时的方法
            // 倒计时,实际上就是每隔1秒,count减去1
            
            // 每次点击先让count为60
            this.count=60;
            let timer = null;
            timer = setInterval(()=>{
                this.count--
                if(this.count===0){
                    // 清除定时器 
                    clearInterval(timer)
                }
            },1000);
        },
        getCode(){
            // 1、验证手机号是否正确
            /* if(!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(this.phoneNum)){
                alert("请输入正确的手机号");
                this.$refs.phone.focus();
                return
            } */
            // 2、进行滑块验证
            if (this.msg == "再试一次" || this.msg == "向右滑动") {
                alert("请先进行滑块验证");
                return 
            }
            // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数
            // 这里先展示秒数
            this.countdown();
            this.isShowCount=true;
           
            
        },
    }
</script>

9.4、连续点击倒计时bug

此时连续点击倒计时会有bug,数字越跳越快,主要是重复开启倒计时造成的。

其实我们只需要把事件给到 "获取验证码" 所在的span,就可以解决

<div class="btn checkcode-btn">
    <span v-show="!isShowCount" @click="getCode">获取验证码</span>
    <span v-show="isShowCount">{{count}} s</span>
</div>
<script>
data () {
    return {
        // 是否展示表单的布尔值
        isShowForm:true,
        // 拼图滑块的文字
        msg: "向右滑动",
        // 用户手机号
        phoneNum:"",
        // 最大的计时时间
        countMax:60,
        // 倒计时时间,每秒变化的那个数字
        count:0,
        // 是否展示秒数所在盒子
        isShowCount:false
    }
},
...
...
countdown(){
    // 计时的方法
    // 倒计时,实际上就是每隔1秒,count减去1
    let timer = null;
    this.count = this.countMax;
    timer = setInterval(()=>{
        this.count--
        if(this.count===0){
            // 清除定时器 
            clearInterval(timer);
        }
    },1000);
}
</script>

9.5、抽取工具函数(优化)

事实上,验证手机号本身是一个工具函数,和本身逻辑有关但是具体代码不需要在组件中体现。真实项目场景往往是放在工具函数的文件中。

src目录下新建utils文件夹,在里面新建index.js文件:

export const validateTelephoneNumber = value =>{
    let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
    return reg.test(value)
}

在Login.vue组件中:

import {validateTelephoneNumber} from "@/utils"
...
...

getCode(){
    // 1、验证手机号是否正确
    if(!validateTelephoneNumber(this.phoneNum)){
        alert("请输入正确的手机号");
        this.$refs.phone.focus();
        return
    }
    // 2、进行滑块验证
         ...      
    // 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数
        ...

},

9.6、发起获取验证码请求

接口: /sendSMS

api.js文件中

// 发送短信验证码请求
export const SendSMSAPI = params => request.post("/sendSMS",params);

Login.vue组件中:

// 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数
SendSMSAPI({
    phone:this.phoneNum.trim()
}).then(res=>{
    this.countdown();
    this.isShowCount=true;
    console.log(res);
})

 

发现返回的数据报如下错误:

其实接口上有需要我们修改请求头Content-Type字段,并使用qs.stringnify进行格式转换:

 

所以我们需要在请求拦截器加上:

instance.interceptors.request.use(config=>{
    
    if (config.url === "/sendSMS" || config.url === "/wechatUsers/PCLogin") {
        config.headers["Content-Type"] = "application/x-www-form-urlencoded";
    }
    return config
},err=>{
    return Promise.reject(err)
})

并且安装qs模块:

npm i qs

api.js中:

import qs from "qs"
// 发送短信验证码请求
export const SendSMSAPI = params => request.post("/sendSMS",qs.stringify(params));

再次到浏览器进行测试

看见如下响应则为发送成功。

 

后端对每一个手机号做了限制,如果看到如下图,说明已经达到最大可发送短信条数,请隔数个小时后再尝试:

9.7、请求成功回调函数的完善

// 3、验证成功后,发起请求,获取验证码成功,则进行倒计时,并展示秒数
SendSMSAPI({
    phone:this.phoneNum.trim()
}).then(res=>{
    if(res.code===0){
        this.countdown();
        this.isShowCount=true;
        console.log(res);    
    }else{
        //获取短信验证码失败
        alert(res.message)
    }
}).catch(err=>{
    // 发送请求失败
    alert("请重新发送")
})

 

十、手机号码登录逻辑分析

手机号码登录逻辑分析:

1、手机号码格式是否正确

2、拼图滑块验证通过

3、验证码是否为空(注意,这里我们前端并没有拿到发送的手机验证码,所以只能判空)

4、发起登录请求

10.1、抽取前两个验证的代码

前两个验证我们前面已经做了,可以直接封装成函数

Login.vue中:

    toVerify(){
            // 1、验证手机号是否正确
            if(!validateTelephoneNumber(this.phoneNum)){
                alert("请输入正确的手机号");
                this.$refs.phone.focus();
                return
            }

            // 2、进行滑块验证
            if (this.msg == "再试一次" || this.msg == "向右滑动") {
                alert("请先进行滑块验证");
                return 
            }
        },
        getCode(){
            this.toVerify();
            //3、发起请求
            ...
        }

如果只是按照上面进行抽取,则会有验证不通过还发起请求的bug

所以,this.toVerify();应换成:

    toVerify(){
            // 1、验证手机号是否正确
            if(!validateTelephoneNumber(this.phoneNum)){
                alert("请输入正确的手机号");
                this.$refs.phone.focus();
                return
            }

            // 2、进行滑块验证
            if (this.msg == "再试一次" || this.msg == "向右滑动") {
                alert("请先进行滑块验证");
                return 
            }
            // 【!!!!!!!!】
            return true
            // 【!!!!!!!!】
        },
        getCode(){
            if(!this.toVerify()){
                return
            };
            //3、验证成功后,发起请求
            ...
        }

10.2、发起登录请求

api.js中:

// 手机号登录请求
export const PhoneLoginAPI = params => request.post("/phoneRegin",qs.stringify(params));

Login.vue中:

import { SendSMSAPI, PhoneLoginAPI } from "@/request/api";
...
        // 点击登录按钮
        submitFn() {
            if(!this.toVerify()){
                return
            }; 
    
            // 验证码是否为空
            if (this.code.trim() === "") {
                alert("请输入验证码再进行登录");
                return;
            }

            // 发起登录请求
            PhoneLoginAPI({
                // 先根据后端给到的测试账号和密码做登录。
                // 但如果真正场景要传的是短信验证码和手机号
                phone:"13800138001",
                password:"qwerty567"
            }).then(res=>{
                // 登录成功
                console.log(res);
                
            })
        },

看到上图就表示登录成功。

10.3、登录成功后的逻辑

登录成功后,我们需要做什么?

1、提示登录成功

2、保存token值到localStorage

3、隐藏登录模态窗口

【!!!】4、登录状态的切换

所以在回调函数中:

        // 发起登录请求
            PhoneLoginAPI({
                phone:"13800138001",
                password:"qwerty567"
            }).then(res=>{
                if(res.code===0){
                    // 1、提示登录成功
                    alert("登录成功");

                    // 2、存储token
                    localStorage.setItem("x-auth-token", res["x-auth-token"]);
                    
                    // 3、隐藏登录模态窗口
                    this.chanIsShowLoginModal(false);
                    
                    // 【!!!】4、登录状态的切换
                }
                
             })

 

10.4、购物车按钮展示(登录状态)分析

因为该购物车按钮需要依靠 有没有登录这个状态的值 来进行展示,并且,这个值会在Login.vue这个文件中进行修改,所以,我们把有没有登录这个状态值放在Vuex中:

store目录中新建文件夹loginStatus,中新建index.js:

export default{
    namespaced:true,
    state: {
        isLogined:localStorage.getItem("x-auth-token")?true:false  // 用来表示是否登录的登录状态值
    },
    mutations: {
        chanIsLogined(state,payload){
            console.log("执行了chanIsLogined");
            state.isLogined = payload
        }
    },
    actions: {
        
    },
}

记得在store/index.js中引入。

在TopBar.vue中

<div class="cart_btn" v-if="isLogined">
    <img src="../assets/img/cart.png" alt="" width="20" />
    <span>购物车</span>
    <b>{{cartTotal}}</b>
</div>
<li class="login-btn" v-else @click="chanIsShowLoginModal(true)">登录</li>

...
<script>
    import {mapMutations,mapState} from "vuex"
    
    ...
    computed:{
        ...mapState({
            isLogined:state=>state.loginStatus.isLogined
        })
        
    },
</script>

最后在登录的回调函数中进行修改这个值。即在Login.vue中:

...mapMutations({
            chanIsShowLoginModal:"showModal/chanIsShowLoginModal",
            chanIsLogined:"loginStatus/chanIsLogined"
        }),
    ...
    ...
    
            PhoneLoginAPI({
               ...
            }).then(res=>{
                if(res.code===0){
                    // 提示登录成功
                    ...
                    // 存储token
                    ...
                    // 隐藏登录模态窗口
                    ...
                    // 登录状态值的切换
                    this.chanIsLogined(true)
                }
                
             })

 

十一、提示组件的封装

11.1、icon图标的使用

11.1.1、在全局中引入

效果图中icon的链接:

https://at.alicdn.com/t/font_2730880_ylrio3ahhx.css

11.1.2、具体图标名称

图标名称图标类名
YDUI-复选框(选中) icon-yduifuxuankuangxuanzhong
YDUI-复选框 icon-yduifuxuankuang
loading icon-loading
toast-失败_画板 1 icon-toast-shibai_huaban
toast-警告 icon-toast-jinggao
toast _成功 icon-toast_chenggong

11.1.3、在组件中使用

<i class="iconfont icon-loading"></i>

11.2、Toast组件的初步封装与使用

https://at.alicdn.com/t/font_2730880_ylrio3ahhx.css

iconfont的样式链接内容粘贴到src/assets/css/public.less中,

components目录下新建Toast.vue

<template>
    <div class="toast">
        <i class="iconfont icon-toast-shibai_huaban"></i>
        <span>提示内容</span>
    </div>
</template>

<script>
export default {
    data () {
        return {

 
        }
    }
}
</script>
 
<style lang = "less" scoped>
.toast{
  position: fixed;
  padding: 10px 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #fff;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  border-radius: 10px;

  .iconfont{
    margin-right: 10px;
  }

  .icon-toast-shibai_huaban{
    color: red;
  }

  .icon-toast_chenggong{
    color: green;
  }

  .icon-toast-jinggao{
    color: orange;
  }
}
</style>

App.vue中引入注册使用即可 。

11.3、Toast组件展示

Toast组件展示与否最终可以在各个组件中调用,所以放在Vuex中。

然后在App中:

<Toast v-show="isShowToast"></Toast>
<script>
...
import {mapState} from "vuex"
export default {
   ...
    computed:{
        ...mapState({
            isShowToast:state=>state.showToast.isShowToast
        })
    }
}
</script>

接下来尝试写个事件看是否能触发显示和隐藏切换

在TopBar组件中,先找头像尝试点击触发展示:

<img @click="showToastFn" src="../assets/img/userImg.f8bbec5e.png" width="26" alt="">

<script>
    methods:{
        ...mapMutations({
            chanIsShowLoginModal:"showModal/chanIsShowLoginModal",
            chanIsShowToast:"showToast/chanIsShowToast"
        }),
        showToastFn(){
            this.chanIsShowToast(true)
        }
    }
</script>

11.4、Toast组件的进场离场效果

Vue提供了transition组件,配合css3可以用来做进场离场效果:https://cn.vuejs.org/v2/guide/transitions.html

App.vue中添加:

<template>
      <div id="app">
        <transition name="slide">
            <Toast v-show="isShowToast"></Toast>
        </transition>
        ...
        ...
        ...
   </div>
</template>
<style lang="less">
/* 入场的起始状态 = 离场的结束状态 */
.slide-enter, .slide-leave-to{
  opacity: 0;
}

.slide-enter-active, .slide-leave-active{
  transition: opacity .3s linear;
}

.slide-enter-to, .slide-leave{
  opacity: 1;
}
</style>

在TobBar.vue组件中:

methods:{
    ...mapMutations({
        chanIsShowLoginModal:"showModal/chanIsShowLoginModal",
        chanIsShowToast:"showToast/chanIsShowToast"
    }),
    showToastFn(){
        this.chanIsShowToast(true);
        setTimeout(()=>{
            this.chanIsShowToast(false);
        },1500)
    }
}

记得把Toast组件中的opacity属性去掉

11.5、封装Toast的属性

一个完整的Toast组件最好需要有展示、颜色、类型三种属性:

在vuex中,补充:

export default{
    namespaced:true,
    state: {
        // 表示是否展示提示
        isShowToast:false, 
        // toast的内容
        toastMsg: "默认内容",
        // toast的类型(success, danger, info)
        toastType: "success"
    },
    mutations: {
        chanIsShowToast(state,payload){
            console.log(payload);
            state.isShowToast = payload.isShow;
            if(payload.isShow){
                state.toastMsg = payload.msg;
                state.toastType = payload.type;
            }
            
        }
    },
    actions: {
        
    },
}

Toast.vue组件:

<template>
    <div class="toast">
        <!-- <i class="iconfont icon-toast-shibai_huaban"></i>
        <span>提示内容</span> -->
        <i 
      :class="toastType=='success' ? 'iconfont icon-toast_chenggong' : (toastType=='danger' ? 'iconfont icon-toast-shibai_huaban' : 'iconfont icon-toast-jinggao')"
    ></i>
    <span>{{toastMsg}}</span>
    </div>
</template>

<script>
import {mapState} from "vuex"
export default {
    data () {
        return {

 
        }
    },
    computed:{
        ...mapState({
            toastMsg:state=>state.showToast.toastMsg,
            toastType:state=>state.showToast.toastType,
        })
    }
}
</script>

最后在TopBar.vue组件中,调用的时候传入对象:

    showToastFn(){
            this.chanIsShowToast({
                isShow:true,
                msg:"请先登录",
                type:"danger"
            });
            setTimeout(()=>{
                this.chanIsShowToast({
                    isShow:false,
                });
            },1500)
        }

11.6、Toast组件自动关闭的处理

Toast组件应该具备自动关闭的功能,而不是每次调用都要写一段定时器代码来关闭。

即TopBar.vue组件中去掉setTimeout方法,在TopBar.vue中:

import {mapMutations,mapState,mapActions} from "vuex"

    methods:{
        ...mapMutations({
            chanIsShowLoginModal:"showModal/chanIsShowLoginModal"
        }),
        ...mapActions({
            asyncIsShowToast:"showToast/asyncIsShowToast"
        }),
        showToastFn(){
            this.asyncIsShowToast({
                isShow:true,
                msg:"请先登录",
                type:"danger"
            });
            /* setTimeout(()=>{
                this.chanIsShowToast({
                    isShow:false,
                });
            },1500) */
        }
    }

在Vuex中书写actions:

actions: {
        asyncIsShowToast(context,payload){
            context.commit("chanIsShowToast",payload)
            setTimeout(()=>{
                context.commit("chanIsShowToast",{
                    isShow:false
                })
            },2000)

        }
    },

 

十二、微信登录二维码的获取与展示

 

12.1、获取微信二维码

在 public/ndex.html 的head标签中引入:

<script src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>

把Login.vue文件中展示二维码图片的盒子上添加 id=“weixin” :

<div id="weixin" class="qrcode" v-show="!isShowForm">
     二维码
</div>

在api.js中:

// 微信登录(这个接口必须用qs对数据进行格式化)
export const WeixinLoginApi = (params) => request.post(`/wechatUsers/PCLogin`, qs.stringify(params));

在点击切换到微信登录的函数中:

weixinClick(){// 点击切换微信扫码登录这一项,并向微信扫码登录
            this.isShowForm=false;

            // 申请微信登录二维码
            let _this = this;
            new WxLogin({
                id: "weixin",
                appid: "wx67cfaf9e3ad31a0d",  // 这个appid要填死
                scope: "snsapi_login",
                // 扫码成功后重定向的接口
                redirect_uri: "https://sc.wolfcode.cn/cms/wechatUsers/shop/PC",
                // state填写编码后的url
                state: encodeURIComponent(window.btoa("http://127.0.0.1:8080" + _this.$route.path)),
                // 调用样式文件
                href: "",
            });

        },


用户扫码完成后,当前页面刷新,并且页面url会拼接微信后台发给我们的微信code.

在TopBar.vue中:(拿页面url上面的code向后端发起请求换取token)

 created() {
    // 扫码的时候做两件事情:  浏览器刷新(created()执行), 浏览器拼接上 ?code=
    // 重载执行了created
    setTimeout(async () => {
      // console.log(this.$route.query.code);
      let mycode = this.$route.query.code;
      if (mycode) {
        // 发起微信扫码登录请求
        let res = await WeixinLoginAPI({
          code: mycode,
        });
        console.log(res);

        if (res.code == 0) {
          // 登录成功的逻辑
          this.asyncChanIsShowToast({
            msg: "登录成功!",
            type: "success",
          });

          // 保存token到localStorage
          localStorage.setItem("x-auth-token", res["x-auth-token"]);
          // 登录状态的切换 (用户的头像,昵称,购物车按钮,数量等)
          this.chanIsLogined(true);

          // 清除浏览器上的code
          this.$router.push(this.$route.path);

          //!!! 登录成功也要获取用户信息
          this.asyncChanUserInfo()
        } else if (res.code == 400) {
          // code过期的逻辑:
          // 1 提示用户进行 请重新进行扫码登录
          this.asyncChanIsShowToast({
            msg: "请重新扫码登录!",
            type: "warning",
          });
          // 2 弹出登录框(打开登录的模态窗口)
          this.chanIsShowLoginModal(true);
        } else if (res.code == 407) {
          // 返回407说明: 用户扫码的这个微信, 在这个网站上没有绑定手机号
          // 我们需要: 让用户绑定手机到扫码的这个微信上
          // 1 弹出提示框, 提示用户, 手机号登录进行绑定微信号
          this.asyncChanIsShowToast({
            msg: "请进行手机号登录绑定扫码的微信!",
            type: "danger",
          });
          // 2 弹出登录框(打开登录的模态窗口)
          this.chanIsShowLoginModal(true);
          // 3 保存uuid(待会用户输入手机点击登录按钮的时候,发送登录请求需要携带uuid)
          localStorage.setItem("uuid", res.uuid);
        }
      } else {
        // 没有code的情况

        // 判断有没有token,来改变用户的登录状态
        let mytoken = localStorage.getItem("x-auth-token");
        // if(mytoken){
        //     this.chanIsLogined(true);
        // }else{
        //     this.chanIsLogined(false);
        // }
        this.chanIsLogined(Boolean(mytoken));

        if(mytoken){
            // 请求登录的用户的信息
            this.asyncChanUserInfo()
        
        }else{
            // 初始化用户信息
            // 修改vuex中的cartTotal和userInfo属性变回初始值
            this.initUserInfo()

        }
        

      }
    }, 100);
  },

 

 

12.2、微信二维码样式调整

把今天其他文件夹中的wxLoginStyle文件夹放到utils文件夹中,然后在这个wxLoginStyle目录下用node执行js文件:

node data-url.js

得到:

data:text/css;base64,Lyogd3hsb2dpbi5jc3MgKi8NCi5pbXBvd2VyQm94IC50aXRsZSwgLmltcG93ZXJCb3ggLmluZm97DQogIGRpc3BsYXk6IG5vbmU7DQp9DQoNCi5pbXBvd2VyQm94IC5xcmNvZGV7DQogIG1hcmdpbi10b3A6IDIwcHg7DQp9

 

把它填到上面的href属性,隐藏头部尾部。

最后调整页面iframe外层盒子的样式,使二维码居中

#weixin{
    /* background-color: #fcf; */
    display: flex;
    justify-content: center;
    margin-top: -20px;
}

十三、组件重载

之前做的利用路由监听完成切换路由就更新用户登录状态值。

这个功能,也可以使用组件重载的方式来完成。即每次切换路由,我们都让TopBar.vue重新加载一次。

首先,现在TopBar的created函数中补充更新用户状态值的代码(此时注释掉前面的watch):

    if(mycode){
        ...
    }else{
        // !!!如果不是微信登录进来的,就会执行这里的代码
        let mytoken = localStorage.getItem("x-auth-token");
        this.chanIsLogined(Boolean(mytoken));
    }

如何进行组件重载?利用key属性。

先删除TopBar.vue中的watch。

然后来到App.vue组件,给调用TopBar.vue的地方添加key属性:

<template>    
    ...
    <!-- 顶部 -->
    <TopBar :key="topBarKeyValue"></TopBar>
    ...
</template>
<script>
    data() {
    return {
      topBarKeyValue:1
    };
  },
    ...
    watch: {
    // 监听路由的变化
    "$route.path": {
      handler(newVal, oldVal){
        // console.log(newVal, oldVal)
        if(newVal !== oldVal){
          // key属性的值一变化,就会做组件重载,从而执行created函数
          console.log("组件重载!!!!");
          this.topBarKeyValue++;
        }
      }
    }
  },
</script>    

十四、获取登录用户信息,请求头携带token

14.1、请求头携带token

在登录功能中,目前我们已经完成了保存token到本地存储中。

在真正项目中,只要我们本地存储中有token,在请求的时候都会带上这个token值在每一个请求头中,不管这个请求需不需token,都会带上

接下来,我们就需要在请求拦截器中判断token,携带token:

src/request/request.js中:

instance.interceptors.request.use(config => {
    const token = localStorage.getItem("x-auth-token");
    if (token) {
      // 判断是否存在token,如果存在的话,则每个请求的请求头上都加上token
      config.headers["x-auth-token"] = token; 
    }
    return config
}, err => {
    return Promise.reject(err)
})

14.2、获取用户登录信息

接口文档:http://www.docway.net/project/1h9xcTeAZzV/1hG2hFlipBQ?st=1iUU09vKhMm&sid=1iUU09vKhMm

在api.js中:

// 获取登录用户信息
export const UserProfilesAPI = () => request.get("/shop/userProfiles");

在TopBar组件中:

import {UserProfilesAPI} from "@/request/api"
setUserLoginstatus(){
      // 微信登录第二步:临时票据code换取token
      let mycode = this.$route.query.code;
      if (mycode) {  
          ...
      }else{
          // 没有code,说明没有扫码, 或者 用户已经登录了,不用扫码了
          // 判断有没有token,设置登录状态 (因为TopBar上面的信息,是需要靠 有没有登录来去展示的)
          let mytoken = localStorage.getItem("x-auth-token");
          this.chanIsLogined(Boolean(mytoken));
          if(mytoken){
              // 请求并渲染用户信息
              UserProfilesAPI().then(res=>{
                  // 打印用户信息
                  console.log(res);
              });
          }else{
              // 设置回默认的用户信息
          }
      }
}, 

十五、用户信息渲染

但是在项目中,任意组件都可以获取这个用户信息,比如:TopBar.vue,Login.vue等,所以用户信息可以放在vuex中:

store中新建userInfo文件夹,新建index.js

import {UserProfilesAPI} from "@/request/api"

export default {
    namespaced: true,
    state: {
        // 购物车数量
        cartTotal: 0,
        // 用户信息
        userInfo:{
            headImg:require("../../assets/img/userImg.f8bbec5e.png"),
            nickName:"--",
            coin:"--"
        }
    },
    mutations: {
        updateUserInfo(state,payload){
            console.log("payload为:",payload);
            state.cartTotal = payload.cartTotal;
            state.userInfo = payload.userInfo;
        }
    },
    actions: {
        asyncUpdateUserInfo(context){
            UserProfilesAPI().then(res=>{
                // 打印用户信息
                
                context.commit("updateUserInfo",res.data)
            });
        }
    },
}

在TopBar.vue组件中:

<template>
    ...
    <li>
        <img
             @click="clickAvatar"
             class="avatar"
             width="26"
             :src="userInfo.headImg"
             alt=""
             />用户名:{{userInfo.nickName}}
    </li>
    <li>我的鸡腿:{{userInfo.coin}}</li>
    <li>获取鸡腿</li>
    <li>叩丁狼官网</li>
    <li class="cart-btn btn" v-show="isLogined">
        <img src="../assets/img/cart.png" alt="" />
        <span>购物车</span>
        <b>{{ cartTotal }}</b>
    </li>
    ...
</template>
<script>
setUserLoginstatus(){
    setUserLoginstatus(){
      // 微信登录第二步:临时票据code换取token
      let mycode = this.$route.query.code;
      if (mycode) {  
          // 有code才去换取token
          WeixinLoginApi({
            code: mycode,
          }).then((res) => {
            console.log(res);
            if (res.code === 0) {
              // 登录成功
              // 1、提示用户登录成功
              // 2、保存token值
              // 3、改变登录状态
              // 4、清除浏览器地址栏上的code
              // 5、获取登录用户信息
              this.asyncUpdateUserInfo();

            }else{
              this.asyncChanToastState({
                msg:res.message,
                type:"danger"
              });
            }
          })
      }else{
        // 如果没有code,需要更新用户登录状态

        // 没有code说明,用户没扫码
        // 说明用户已经登录的不用扫码,或者用户没有登录没扫码,
        // 判断用户能否拿到token来做出登录状态的更新
        let mytoken = localStorage.getItem("x-auth-token");
        this.chanIsLogined(Boolean(mytoken));
        // 获取登录用户信息
        if(mytoken){
            this.asyncUpdateUserInfo();
        }
      }
    },
}
</script>

在Login.vue组件中登录成功也是异步更新用户信息:

import { mapMutations,mapActions } from "vuex";
...
    ...mapActions({
      asyncUpdateUserInfo:"userInfo/asyncUpdateUserInfo"
    }),
    toLogin() {
      // 1&&2、前两个验证已经完成
      // 3 判断验证码是否为空
      // 去做登录
      LoginAPI({
       ...
      }).then((res) => {
        console.log(res);
        if(res.code==0){
            // 1、提示登录成功
            // 2、保存token值,本地存储
            // 3、隐藏登录框
            // 【!!】4登录状态的切换
            // 5、更新用户信息
            this.asyncUpdateUserInfo();
        }
      });
    },

注:如果此时用户头像没有出来,在public文件夹中的index.html文件的head标签里,添加这个标签即可

<!-- 防止服务器检查防盗用链接 -->
<meta name="referrer" content="no-referrer" />

 

十六、删除token后的用户信息初始化

在删除token后,切换路由的时候,用户信息需要还原为默认值

TopBar.vue中:

setUserLoginState() { 
    if(mycode){
        ...
    }else {
        ...
          if (mytoken) {
            ...
          } else {
            // 初始化用户数据
            this.initUserInfo();
          }
     } 

十七、重复点击同一个路由出现的报错问题解决

在现有版本的vue-router中,重复点击同一个路由会出现报错。

解决方案如下:

方案1、vue-router降级处理(但不推荐)

npm i vue-router@3.0.7

方案2、直接在push方法最后添加异常捕获,例如:

<van-search v-model="SearchVal" shape="round" placeholder="请输入搜索关键词" disabled @click="$router.push('/home/searchPopup').catch(err=>{})"/>

方案3、直接修改原型方法push(推荐)

// 把这段代码直接粘贴到router/index.js中的Vue.use(VueRouter)之前
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location) {
  return originalPush.call(this, location).catch(err => {})
};

十八、抽取图片基本路径

通过 Vue.prototype.变量名=值,定义的变量,在组件内部可以直接 this.变量名来获取

有人称它为全局变量,但不是真正意义上的全局变量。跟Vuex也不一样。

它相当于在每一个组件中都定义一个组件内部变量

在main.js中:

// 定义“全局变量”(相当于在每一个组件中都定义一个组件内部变量imgBaseUrl)
Vue.prototype.imgBaseUrl = "https://sc.wolfcode.cn";

在Details.vue和List.vue组件中,图片基本路径的处理均调整为:

<img :src="imgBaseUrl+item.img" alt="">

十九、导航守卫(导航拦截、路由拦截)

文档地址: https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%85%A8%E5%B1%80%E5%89%8D%E7%BD%AE%E5%AE%88%E5%8D%AB

路由拦截(导航守卫:前置导航守卫和后置导航守卫) 前置导航守卫有三个参数 to 表示即将进入的路由 from 表示即将离开的路由 next() 表示执行进入这个路由

19.1、全局导航守卫

在router/index.js中:

// 全局导航守卫
router.beforeEach((to, from, next)=>{
  // 有token就表示已经登录
  // 想要进入个人中心页面,必须有登录标识token
    
  // console.log('to:', to)
  // console.log('from:', from)
  if(to.path=='/user'){
         let token = localStorage.getItem('x-auth-token')
        // 此时必须要有token
        if(token){
          next(); // next()去到to所对应的路由界面
        }else{
          // 提示没有登录
          store.dispatch("showToast/asyncChanIsShowToast",{
              msg: "你还没有登录!",
              type: "danger",
          })
        }
        return; // 需要些return,防止执行完上面的next(),还继续执行下面的next()
  }
 
  // 如果不是去往个人中心的路由,则直接通过守卫,去到to所对应的路由界面
  next()
})

export default router

19.2、组件内部导航守卫

全局导航守卫是每一次改变路由都会触发的,然而,目前我们只需要到个人中心页面才触发,所以我们在组件内部书写导航守卫:

User.vue组件中

import store from "@/store"

...
beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    let token = localStorage.getItem("x-auth-token");
    if(token){
      next()
    }else{
      // 提示没有登录
      store.dispatch("showToast/asyncChanIsShowToast",{
          msg: "你还没有登录!",
          type: "danger",
      });
      
    }
},

二十、404处理

新建Error.vue组件:

<template>
  <div class="wrap"><img width="100%" src="../assets/img/404.94e7c552.jpg" alt=""></div>
</template>

在路由表中配置剩余地址对应Error组件:

,
  {
    path: '*',
    name: 'Error',
    component: () => import(/* webpackChunkName: "error" */ '../views/Error.vue')
  }

二十一、全部商品中,滚动到底部加载更多

Goods.vue中:

<p style="text-align: center; margin-top: 20px">
    正在加载... ...
</p>

把三个函数(带兼容性的写法)写在工具类utils文件夹index.js中:

//获取滚动条当前的位置 
function getScrollTop() {
    var scrollTop = 0;
    if(document.documentElement && document.documentElement.scrollTop) {
        scrollTop = document.documentElement.scrollTop;
    } else if(document.body) {
        scrollTop = document.body.scrollTop;
    }
    return scrollTop;
  }

//获取当前可视范围的高度 
function getClientHeight() {
    var clientHeight = 0;
    if(document.body.clientHeight && document.documentElement.clientHeight) {
        clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight);
    } else {
        clientHeight = Math.max(document.body.clientHeight, document.documentElement.clientHeight);
    }
    return clientHeight;
}

//获取文档完整的高度 
function getScrollHeight() {
    return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
}

Goods.vue中:

<script>
import {getScrollTop,getClientHeight,getScrollHeight} from "@/utils"
...
data(){
    return{
        // 产品列表
      goodsList: [],
      // 用来展示的产品列表
      goodsListShow: [],
      // 默认展示第一页
      page:1,
      // 每页8条
      size:8,
      // false 表示没有正在加载
      isLoading: false,
      //是否已经到低
      isReachBottom:false,
     
    }
},
 ...  
     getGoodList() {
      GoodListAPI({
        ...
      }).then((res) => {
        ...
        // 在请求到数据之后,先展示前8条
        this.goodsListShow=this.goodsList.filter((item,index)=>index<8)
      });
    },
    scrollFn() {
      // 滚动就执行这里的代码频繁触发事件
      console.log(getClientHeight() + getScrollTop() == getScrollHeight() + 1);
      // console.log("页面正在滚动");
      // 如果滚动到底部的时候,
      // if (到底部了) {
      // if (窗口高度+scrollTop>=页面文档高度-20) {
      if (getClientHeight() + getScrollTop() >= getScrollHeight() - 20) {
        
        // 需要this.isLoading为false才能进行加载
        if (!this.isLoading) {
          // this.isLoading避免了重复触发这个到底了加载数据事件
          this.page++;
          this.isLoading = true;
          setTimeout(() => {
            // 往goodsListShow这个数组去push下一页的数据
            // 从goodsList数组中去  this.page页的数据 push到goodsListShow
            for (var i = this.size * (this.page - 1);i < this.size * this.page;i++) {
              //this.goodsList[i]必须有这个值,才能push到展示的数组里面去
              this.goodsList[i]
                ? this.goodsListShow.push(this.goodsList[i])
                : "";
            }
            this.isLoading = false;
          }, 500);
        }
      }
    },
  },
  mounted() {
    // 监听滚动
    window.addEventListener("scroll", this.scrollFn);
  },
  beforeDestroy() {
    // 取消监听
    window.removeEventListener("scroll", this.scrollFn);
  },
 </script>                        

处理是否 已经到底部 isReachBottom

结构完善:

<p style="text-align: center; margin-top: 20px">
    {{ isReachBottom ? "已经没有数据了" : "正在加载... ..." }}
</p>
<script>
...
// 定义是不是已经没有数据了
isReachBottom: false,
    
    
    ...
    if (getClientHeight() + getScrollTop() >= getScrollHeight() - 20) {
            if (this.goodsListShow.length >= this.goodsList.length) {
              // 没有数据了
              this.isReachBottom = true;
              return;
            }
</script>

最后解决选项切换时候的bug

async goodsSearch(){
        ... ...
        // 初始化数据
        this.isReachBottom = false;
          this.page = 1;

        // 判断是不是已经没有数据了
        if (this.goodsListShow.length >= this.goodsList.length) {
          // 每次请求到数据,把数据把页数和是否到底部初始化一下
          this.isReachBottom = true;
        }
}

 

二十二、跨域配置

我们对 vue.config.js 进行配置:

module.exports = {
    devServer: {
        proxy: {
            '/api': {
                target: "http://kumanxuan1.f3322.net:8881/cms",
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
}

request.js中:

const instance = axios.create({
    baseURL: "/api",
    timeout: 5000
})

记得配置完需要重启服务器!!

浏览器请求案例:

 

 

二十三、项目环境变量配置

项目目录下新建两个文件,分别是开发环境和生产环境下的两个不同配置

.env.dev

NODE_ENV=development
VUE_APP_BASE_URL=http://172.16.xxxxx
VUE_APP_STATE_URL=http://127.0.0.1:8080

.env.prod

NODE_ENV = production
VUE_APP_BASE_URL = http://xxxxxx
VUE_APP_STATE_URL="后端给的地址"

在package.json中修改启动命令:

"serve": "vue-cli-service serve --open --mode dev",
"servepro": "vue-cli-service serve --open --mode prod",

在vue.config.js中换成:

'/api': {
    target: process.env.VUE_APP_BASE_URL,
    pathRewrite: {
        '^/api': ''
    }
}

 

二十四、项目总结

24.1、项目介绍

《叩丁严选》是一个由vue-cli搭建的PC端SPA商城该商城主要涉及登录注册、商品列表、商品详情、个人中心、购物车及商品检索等主体功能该项目主要用于平台用户参与积分兑换商品,是一个中大型的PC端商城项目。

24.2、项目技术点

  1. 使用vue-cli搭建项目,并结合蓝湖+PS进行页面切图,实现对设计稿的高保真还原;

  2. 使用axios进行数据请求,并对其进行请求拦截器响应拦截器封装;

  3. 封装所有请求的api,统一管理项目所有的请求路径

  4. 鉴权,认证机制采用手机+验证码、手机+密码及微信扫码登录认证,其中微信扫码登录结合环境变量,调用后端接口实现平台切换验证;

  5. 使用localStorage对token进行存储;

  6. 使用原生JS在组件mounted中监听滚动,并实现向下滚动加载更多;

  7. 使用组件内导航守卫对每个进入个人中心页的路由进行拦截,判断路径后保证有token方能进入该路由;

  8. 使用路由监听解决路由跳转而页面不跳转的问题;

  9. 给组件绑定key属性,通过修改key值来进行组件重载;

posted @ 2022-04-13 12:33  李云蹊  阅读(386)  评论(0编辑  收藏  举报