【Vue】我的尚品汇项目笔记---20230109~20230120

news2024/11/14 20:10:31

014之前可参考官方笔记

https://blog.csdn.net/weixin_43424325/article/details/121684101

015-axios二次封装

api/index.js 设定

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";

//首页三级分类接口
export const reqgetCategoryList = () => {
    return  requests({
        url: '/product/getBaseCategoryList',
        method: 'GET'
    })
}

api/request.js 配置请求拦截器并开启进度条

import axios from "axios";

//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";

//1、对axios二次封装
const requests = axios.create({
    //基础路径,requests发出的请求在端口号后面会跟改baseURl
    baseURL:'/api',
    timeout: 5000,
})
//2、配置请求拦截器
requests.interceptors.request.use(config => {
    //config内主要是对请求头Header配置
    //比如添加token

    //开启进度条
    nprogress.start();
    
    return config;
})
//3、配置相应拦截器
requests.interceptors.response.use((res) => {
    //成功的回调函数

    //响应成功,关闭进度条
    nprogress.done()

    return  res.data;
},(error) => {
    //失败的回调函数
    console.log("响应失败"+error)
    return Promise.reject(new Error('fail'))
})
//4、对外暴露
export default requests;

main.js 测试api接口

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './router'
import TypeNav from './views/Home/TypeNav'
import store from './store'

Vue.config.productionTip = false

Vue.use(VueRouter)

//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav)

//测试:发起请求
import {reqgetCategoryList} from './api'
reqgetCategoryList();


new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')

在这里插入图片描述

018-vuex模块式开发

TypeNav/index.vue 读取vuex数据

<script>
import { mapState } from "vuex";

export default {
  name: 'TypeNav',
  mounted(){
    this.$store.dispatch('getCategoryList')
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
}
</script>

api/index.js 设定请求api

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";

//首页三级分类接口
export const reqgetCategoryList = () => {
    return  requests({
        url: '/product/getBaseCategoryList',
        method: 'GET'
    })
}

store/homes.js设定三连环

import { reqgetCategoryList, } from "@/api";

//home模块的仓库
const state = {
  //home仓库中存储三级菜单的数据
  categoryList: [],
};

//mutions是唯一修改state的地方
const mutations = {
    GETCATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCategoryList({ commit }) {
        //reqgetCategoryList返回的是一个Promise对象
        //需要用await接受成功返回的结果,await必须要结合async一起使用(CP)
        let result = await reqgetCategoryList();
        if (result.code == 200) {
          commit("GETCATEGORYLIST", result.data);
        }
      },
};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

在这里插入图片描述

019-026 三级联动数据

019-动态展示三级联动数据

TypeNav/index.vue 设定c1,c2,c3动态商品分类导航

<template>
        <!-- 商品分类导航 -->
        <div class="type-nav">
            <div class="container">
                <h2 class="all">全部商品分类</h2>
                <nav class="nav">
                    <a href="###">服装城</a>
                    <a href="###">美妆馆</a>
                    <a href="###">尚品汇超市</a>
                    <a href="###">全球购</a>
                    <a href="###">闪购</a>
                    <a href="###">团购</a>
                    <a href="###">有趣</a>
                    <a href="###">秒杀</a>
                </nav>
                <div class="sort">
                    <div class="all-sort-list2" >
                        <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
                            <h3>
                                <a href="">{{ c1.categoryName }}</a>
                            </h3>
                            <div class="item-list clearfix">
                                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                                    <dl class="fore">
                                        <dt>
                                            <a href="">{{ c2.categoryName }}</a>
                                        </dt>
                                        <dd>
                                            <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                                                <a href="">{{ c3.categoryName }}</a>
                                            </em>
                                        </dd>
                                    </dl>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  name: 'TypeNav',
  mounted(){
    this.$store.dispatch('getCategoryList')
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
}
</script>

在这里插入图片描述

020-完成三级联动动态背景

TypeNav/index.vue 设定index和currentIndex来启动cur class

<template>
 <h3 @mouseenter="changeIndex(index)" @mouseleave="leaveShow(index)"  :class="{ cur: currentIndex == index }">
     <a href="">{{ c1.categoryName }}</a>
</h3>
</template>

<script>
export default {
  name: 'TypeNav',
  data(){
    return {
        currentIndex:-1,
    }
  },
  mounted(){
    this.$store.dispatch('getCategoryList')
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    changeIndex(index){
        this.currentIndex = index
    },
    leaveShow(index){
        this.currentIndex = -1
    }
  },
}
</script>



<style scoped lang="less">
    .cur{
       background: skyblue;
    }
</style>

在这里插入图片描述

021-通过JS控制二三级

TypeNav/index.vue注释掉以下less后,用js来实现

&:hover {
    .item-list {
          display: block;
         }
  }

TypeNav/index.vue

                            <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}">
                                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                                    <dl class="fore">
                                        <dt>
                                            <a href="">{{ c2.categoryName }}</a>
                                        </dt>
                                        <dd>
                                            <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                                                <a href="">{{ c3.categoryName }}</a>
                                            </em>
                                        </dd>
                                    </dl>
                                </div>
                            </div>

在这里插入图片描述

025-三级联动节流

插件官网https://www.lodashjs.com/
npm i --save lodash

TypeNav/index.vue引用lodash的节流功能

<script>
import { mapState } from "vuex";
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

export default {
  name: 'TypeNav',
  data(){
    return {
        currentIndex:-1,
    }
  },
  mounted(){
...
  },
  computed: {
...
  },
  methods: {
    // changeIndex(index){
    //     this.currentIndex = index
    // },
    // 引入节流
    changeIndex:throttle(function(index){
        this.currentIndex = index
    },500),
...
}
</script>

026-三级联动路由跳转

在这里插入图片描述
在这里插入图片描述

方法1:TypeNav/index.vue使用<router-link to="/search">直接跳转,但是生成组件较多,内存消耗较大。

<template>
        <!-- 商品分类导航 -->
        <div class="type-nav">
            <div class="container" @mouseleave="leaveShow">
                <h2 class="all">全部商品分类</h2>
                <nav class="nav">
                    <a href="###">服装城</a>
                    <a href="###">美妆馆</a>
                    <a href="###">尚品汇超市</a>
                    <a href="###">全球购</a>
                    <a href="###">闪购</a>
                    <a href="###">团购</a>
                    <a href="###">有趣</a>
                    <a href="###">秒杀</a>
                </nav>
                <div class="sort">
                    <div class="all-sort-list2" >
                        <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
                            <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }">
                                <router-link :to="{
                                    name:'search',
                                    params:$route.params,
                                    query:{
                                        categoryName:c1.categoryName,
                                        category1Id:c1.categoryId,
                                    }
                                }"
                                >{{ c1.categoryName }}</router-link>
                            </h3>
                            <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}">
                                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                                    <dl class="fore">
                                        <dt>
                                            <router-link :to="{
                                                    name:'search',
                                                    params:$route.params,
                                                    query:{
                                                        categoryName:c2.categoryName,
                                                        category2Id:c2.categoryId,
                                                    }
                                                }"
                                            >{{ c2.categoryName }}</router-link>
                                        </dt>
                                        <dd>
                                            <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                                                <router-link :to="{
                                                        name:'search',
                                                        params:$route.params,
                                                        query:{
                                                            categoryName:c3.categoryName,
                                                            category3Id:c3.categoryId,
                                                        }
                                                    }"
                                                >{{ c3.categoryName }}</router-link>
                                            </em>
                                        </dd>
                                    </dl>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
</template>

<script>
import { mapState } from "vuex";
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

export default {
  name: 'TypeNav',
  data(){
    return {
        currentIndex:-1,
    }
  },
  mounted(){
    this.$store.dispatch('getCategoryList')
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    // changeIndex(index){
    //     this.currentIndex = index
    // },
    // 引入节流
    changeIndex:throttle(function(index){
        this.currentIndex = index
    },50),
    leaveShow(){
        this.currentIndex = -1
    },
  },
}
</script>

方法2:TypeNav/index.vue使用编程式导航,内存消耗小。
事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)
解决方法:
对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
对于问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。

TypeNav/index.vue

<template>
        <!-- 商品分类导航 -->
        <div class="type-nav">
            <div class="container" @mouseleave="leaveShow">
                <h2 class="all">全部商品分类</h2>
                <nav class="nav">
                    <a href="###">服装城</a>
                    <a href="###">美妆馆</a>
                    <a href="###">尚品汇超市</a>
                    <a href="###">全球购</a>
                    <a href="###">闪购</a>
                    <a href="###">团购</a>
                    <a href="###">有趣</a>
                    <a href="###">秒杀</a>
                </nav>
                <div class="sort">
                    <div class="all-sort-list2" @click="goSearch">
                        <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
                            <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }">
                                <a 
                                :data-categoryName="c1.categoryName"
                                :data-category1Id="c1.categoryId"
                                >{{ c1.categoryName }}</a>
                            </h3>
                            <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}">
                                <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                                    <dl class="fore">
                                        <dt>
                                            <a 
                                            :data-categoryName="c2.categoryName"
                                            :data-category2Id="c2.categoryId"
                                            >{{ c2.categoryName }}</a>
                                        </dt>
                                        <dd>
                                            <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                                                <a
                                                :data-categoryName="c3.categoryName"
                                                :data-category3Id="c3.categoryId"
                                                >{{ c3.categoryName }}</a>
                                            </em>
                                        </dd>
                                    </dl>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
</template>

<script>
import { mapState } from "vuex";
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

export default {
  name: 'TypeNav',
  data(){
    return {
        currentIndex:-1,
    }
  },
  mounted(){
    this.$store.dispatch('getCategoryList')
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    // changeIndex(index){
    //     this.currentIndex = index
    // },
    // 引入节流
    changeIndex:throttle(function(index){
        this.currentIndex = index
    },50),
    leaveShow(){
        this.currentIndex = -1
    },
        //进行路由跳转的回调函数
    goSearch(event) {
      //event.target:获取到的是出发事件的元素(div、h3、a、em、dt、dl)
      let node = event.target;
      //给a标签添加自定义属性data-categoryName,全部的字标签当中只有a标签带有自定义属性,别的标签名字----dataset纯属扯淡
      let {
        categoryname,
        category1id,
        category2id,
        category3id,
      } = node.dataset;
      console.log('dataset',node.dataset)
      //第二个问题解决了:点击的到底是不是a标签(只要这个标签身上带有categoryname)一定是a标签
      //当前这个if语句:一定是a标签才会进入
      if (categoryname) {
        //准备路由跳转的参数对象
        let loction = { name: "search" };
        let query = { categoryName: categoryname };
        //一定是a标签:一级目录
        if (category1id) {
          query.category1Id = category1id;
          //一定是a标签:二级目录
        } else if (category2id) {
          query.category2Id = category2id;
          //一定是a标签:三级目录
        } else {
          query.category3Id = category3id;
        }
        //判断:如果路由跳转的时候,带有params参数,捎带脚传递过去
        if (this.$route.params) {
          loction.params = this.$route.params;
          //动态给location配置对象添加query属性
          loction.query = query;
          //路由跳转
          this.$router.push(loction);
        }
      }
    },
  },
}
</script>

方法3:我重写了goSearch()方法
其实就是取出data标记的变量,然后用query去接收,用loction变量整合query和params参数,最后传给router。

    goSearch(event) {
        //取变量
        let node = event.target.dataset
        let loction = { name: "search" };
        let query = {'categoryName': node.categoryname,}
        if(node.category1id){query.category1Id = node.category1id}
        else if(node.category2id){query.category2Id = node.category2id}
        else{query.category3Id = node.category3id}

        //带参访问
        if (node.categoryname) {
            if (this.$route.params) {
            loction.params = this.$route.params;
            loction.query = query;
            this.$router.push(loction);
            }
        }
    },

029-Search模块中商品分类与过渡动画

商品分类

TypeNav/index.vue加上v-show=“show”、mounted()、leaveShow()、enterShow()
在这里插入图片描述
在这里插入图片描述

TypeNav/index.vue完整代码

<template>
        <!-- 商品分类导航 -->
        <div class="type-nav">
            <div class="container" @mouseleave="leaveShow" @mouseenter="enterShow">
                <h2 class="all">全部商品分类</h2>
                <nav class="nav">
                    <a href="###">服装城</a>
                    <a href="###">美妆馆</a>
                    <a href="###">尚品汇超市</a>
                    <a href="###">全球购</a>
                    <a href="###">闪购</a>
                    <a href="###">团购</a>
                    <a href="###">有趣</a>
                    <a href="###">秒杀</a>
                </nav>

                <!-- 过渡动画 -->
                <transition name="sort">
                    <div class="sort" v-show="show">
                        <div class="all-sort-list2" @click="goSearch">
                            <div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
                                <h3 @mouseenter="changeIndex(index)" :class="{ cur: currentIndex == index }">
                                    <a 
                                    :data-categoryName="c1.categoryName"
                                    :data-category1Id="c1.categoryId"
                                    >{{ c1.categoryName }}</a>
                                </h3>
                                <div class="item-list clearfix" :style="{display: currentIndex == index?'block':'none'}">
                                    <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
                                        <dl class="fore">
                                            <dt>
                                                <a 
                                                :data-categoryName="c2.categoryName"
                                                :data-category2Id="c2.categoryId"
                                                >{{ c2.categoryName }}</a>
                                            </dt>
                                            <dd>
                                                <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
                                                    <a
                                                    :data-categoryName="c3.categoryName"
                                                    :data-category3Id="c3.categoryId"
                                                    >{{ c3.categoryName }}</a>
                                                </em>
                                            </dd>
                                        </dl>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </transition>
            </div>
        </div>
</template>

<script>
import { mapState } from "vuex";
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

export default {
  name: 'TypeNav',
  data(){
    return {
        currentIndex:-1,
        show:true,
    }
  },
  mounted(){
    this.$store.dispatch('getCategoryList')
    //当组件挂载完毕,让show属性变为false
    //如果不是Home路由组件,将typeNav进行隐藏
    if (this.$route.path != '/home' && this.$route.path !='/'){
        this.show = false
    }
  },
  computed: {
    //state:他是咱们大仓库中的state(包含home|search)
    ...mapState({
      categoryList: (state) => state.home.categoryList,
    }),
  },
  methods: {
    // changeIndex(index){
    //     this.currentIndex = index
    // },
    // 引入节流
    changeIndex:throttle(function(index){
        this.currentIndex = index
    },50),
    //当鼠标离开的时候,让商品分类列表进行隐藏
    leaveShow(){
        this.currentIndex = -1
        if (this.$route.path != '/home' && this.$route.path !='/'){
            this.show = false
        }
    },
    //当鼠标移入的时候,让商品分类列表进行展示
    enterShow(){
        this.show = true
    },
    //进行路由跳转的回调函数
    goSearch(event) {
      //event.target:获取到的是出发事件的元素(div、h3、a、em、dt、dl)
      let node = event.target;
      //给a标签添加自定义属性data-categoryName,全部的字标签当中只有a标签带有自定义属性,别的标签名字----dataset纯属扯淡
      let {
        categoryname,
        category1id,
        category2id,
        category3id,
      } = node.dataset;
      console.log('dataset',node.dataset)
      console.log('categoryname',categoryname)
      //第二个问题解决了:点击的到底是不是a标签(只要这个标签身上带有categoryname)一定是a标签
      //当前这个if语句:一定是a标签才会进入
      if (categoryname) {
        //准备路由跳转的参数对象
        let loction = { name: "search" };
        let query = { categoryName: categoryname };
        //一定是a标签:一级目录
        if (category1id) {
          query.category1Id = category1id;
          //一定是a标签:二级目录
        } else if (category2id) {
          query.category2Id = category2id;
          //一定是a标签:三级目录
        } else {
          query.category3Id = category3id;
        }
        //判断:如果路由跳转的时候,带有params参数,捎带脚传递过去
        if (this.$route.params) {
          loction.params = this.$route.params;
          //动态给location配置对象添加query属性
          loction.query = query;
          //路由跳转
          this.$router.push(loction);
        }
      }
    },
  },
}
</script>
过渡动画

TypeNav/index.vue加上transition标签。
在这里插入图片描述
TypeNav/index.vue设定好less,记得.sort-enter需放在.container大括号里面。

        //过渡动画的样式
        //过渡动画开始状态(进入)
        .sort-enter {
            height: 0px;
        }
        // 过渡动画结束状态(进入)
        .sort-enter-to {
            height: 461px;
        }
        // 定义动画时间、速率
        .sort-enter-active {
            transition: all 0.5s linear;
        }

030-typeNav商品分类列

typeNav商品分类列信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。
将typeNav/index.vue的mount()改到App.vue的mount()。
在这里插入图片描述
App.vue增加mount()

<template>
  <div>
    <Header></Header>
    <router-view></router-view>
    <Footer v-show="$route.meta.showFooter"></Footer>
  </div>
</template>

<script>
import Header from './components/Header'
import Footer from './components/Footer'


export default {
  name: 'App',
  components: {
    Header,
    Footer,
  },
  mounted() {
    this.$store.dispatch('getCategoryList')
  },
}
</script>

<style lang="less">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

031-合并参数

Header/index.vue仿照这一段把query参数带过去。

        //判断:如果路由跳转的时候,带有params参数,捎带脚传递过去
        if (this.$route.params) {
          loction.params = this.$route.params;
          //动态给location配置对象添加query属性
          loction.query = query;
          //路由跳转
          this.$router.push(loction);
        }

Header/index.vue

<template>
        <header class="header">
            <!-- 头部的第一行 -->
            <div class="top">
                <div class="container">
                    <div class="loginList">
                        <p>尚品汇欢迎您!</p>
                        <p>
                            <span></span>
                            <router-link to="/login">登录</router-link>
                            <router-link class="register" to="/register">免费注册</router-link>
                        </p>
                    </div>
                    <div class="typeList">
                        <a href="###">我的订单</a>
                        <a href="###">我的购物车</a>
                        <a href="###">我的尚品汇</a>
                        <a href="###">尚品汇会员</a>
                        <a href="###">企业采购</a>
                        <a href="###">关注尚品汇</a>
                        <a href="###">合作招商</a>
                        <a href="###">商家后台</a>
                    </div>
                </div>
            </div>
            <!--头部第二行 搜索区域-->
            <div class="bottom">
                <h1 class="logoArea">
                    <router-link class="logo" title="尚品汇" to="/">
                        <img src="./images/logo.png" alt="">
                    </router-link>
                </h1>
                <div class="searchArea">
                    <form action="###" class="searchForm">
                        <input type="text" id="autocomplete" class="input-error input-xxlarge" v-model="keyword"/>
                        <button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button>
                    </form>
                </div>
            </div>
        </header>
</template>

<script>
export default {
  name: 'Header',
  data() {
    return {
        keyword:'',
    }
  },
  methods:{
    goSearch(){
        //治标不治本
        // this.$router.push(`/search/${this.keyword}`,()=>{},()=>{})

        if (this.$route.query) {
          let location = {
            name: "search",
            params:{keyword:this.keyword || undefined}
        }
          //动态给location配置对象添加query属性
          location.query = this.$route.query;
          //路由跳转
          this.$router.push(location);
        }
    },
  },

}
</script>

在这里插入图片描述

032-mockjs模拟数据 & 033-尚硅谷-尚品汇-获取Banner轮播图数据

官网链接
在这里插入图片描述

第一步:安装依赖包mockjs

安装mockjs `npm install --save mockjs`

第二步:在src文件夹下创建一个文件夹mock。

第三步:准备模拟的数据。!
mock/banner.json

[
    {
        "id": "1",
        "imgUrl": "/images/banner1.jpg"
    },
    {
        "id": "2",
        "imgUrl": "/images/banner2.jpg"
    },
    {
        "id": "3",
        "imgUrl": "/images/banner3.jpg"
    },
    {
        "id": "4",
        "imgUrl": "/images/banner4.jpg"
    }
]

mock/floor.json

[
    {
        "id": "001",
        "name": "家用电器",
        "keywords": [
            "节能补贴",
            "4K电视",
            "空气净化器",
            "IH电饭煲",
            "滚筒洗衣机",
            "电热水器"
        ],
        "imgUrl": "/images/floor-1-1.png",
        "navList": [
            {
                "url": "#",
                "text": "热门"
            },
            {
                "url": "#",
                "text": "大家电"
            },
            {
                "url": "#",
                "text": "生活电器"
            },
            {
                "url": "#",
                "text": "厨房电器"
            },
            {
                "url": "#",
                "text": "应季电器"
            },
            {
                "url": "#",
                "text": "空气/净水"
            },
            {
                "url": "#",
                "text": "高端电器"
            }
        ],
        "carouselList": [
            {
                "id": "0011",
                "imgUrl": "/images/floor-1-b01.png"
            },
            {
                "id": "0012",
                "imgUrl": "/images/floor-1-b02.png"
            },
            {
                "id": "0013",
                "imgUrl": "/images/floor-1-b03.png"
            }
        ],
        "recommendList": [
            "/images/floor-1-2.png",
            "/images/floor-1-3.png",
            "/images/floor-1-5.png",
            "/images/floor-1-6.png"
        ],
        "bigImg": "/images/floor-1-4.png"
    },
    {
        "id": "002",
        "name": "手机通讯",
        "keywords": [
            "节能补贴2",
            "4K电视2",
            "空气净化器2",
            "IH电饭煲2",
            "滚筒洗衣机2",
            "电热水器2"
        ],
        "imgUrl": "/images/floor-1-1.png",
        "navList": [
            {
                "url": "#",
                "text": "热门2"
            },
            {
                "url": "#",
                "text": "大家电2"
            },
            {
                "url": "#",
                "text": "生活电器2"
            },
            {
                "url": "#",
                "text": "厨房电器2"
            },
            {
                "url": "#",
                "text": "应季电器2"
            },
            {
                "url": "#",
                "text": "空气/净水2"
            },
            {
                "url": "#",
                "text": "高端电器2"
            }
        ],
        "carouselList": [
            {
                "id": "0011",
                "imgUrl": "/images/floor-1-b01.png"
            },
            {
                "id": "0012",
                "imgUrl": "/images/floor-1-b02.png"
            },
            {
                "id": "0013",
                "imgUrl": "/images/floor-1-b03.png"
            }
        ],
        "recommendList": [
            "/images/floor-1-2.png",
            "/images/floor-1-3.png",
            "/images/floor-1-5.png",
            "/images/floor-1-6.png"
        ],
        "bigImg": "/images/floor-1-4.png"
    }
]

把mock数据需要的图片放置于public文件夹中
在这里插入图片描述

第四步:在mock文件夹中创建一个mockServe.js文件。
(注意:在server.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。)

第五步:通过mock模块模拟出数据
mockServe.js

import Mock  from 'mockjs'
//webpack默认对外暴露:json、图片
import banner from './banner.json'
import floor from './floor.json'

//mock数据:第一个参数请求地址、第二个参:请求数据
Mock.mock("/mock/banner",{code:200,data:banner})
Mock.mock("/mock/floor",{code:200,data:floor})
//记得要在main.js中引入一下
//import ''@/mock/mockServer

第六步:回到入口文件,引入serve.js。

import '@/mock/mockServe'

在这里插入图片描述

第七步:在API文件夹中创建mockRequest【axios实例:baseURL:’/mock’】
1.api/mockAjax.js几乎跟request.js一模一样,默认暴露request,import时重命名为mockRequest。

import axios from "axios";

//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";

//1、对axios二次封装
const requests = axios.create({
    //基础路径,requests发出的请求在端口号后面会跟改baseURl
    baseURL:'/mock',
    timeout: 5000,
})
//2、配置请求拦截器
requests.interceptors.request.use(config => {
    //config内主要是对请求头Header配置
    //比如添加token

    //开启进度条
    nprogress.start();
    
    return config;
})
//3、配置相应拦截器
requests.interceptors.response.use((res) => {
    //成功的回调函数

    //响应成功,关闭进度条
    nprogress.done()

    return  res.data;
},(error) => {
    //失败的回调函数
    console.log("响应失败"+error)
    return Promise.reject(new Error('fail'))
})
//4、对外暴露
export default requests;

2.api/index.js暴露mock数据

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";
import mockRequests from "./mockAjax";

//首页三级分类接口
export const reqgetCategoryList = () => {
    return  requests({
        url: '/product/getBaseCategoryList',
        method: 'GET'
    })
}

//切记:当前函数执行需要把服务器返回结果返回
//获取banner(Home首页轮播图接口)
export const reqGetBannerList = () => mockRequests.get("/banner");
//获取floor数据
export const reqFloorList = () => mockRequests.get("/floor");


第八步:Vuex三连(actions、mutations、state)
store/home.js新增 async getBannerList()、GETBANNERLIST()、bannerList,新增 async getFloorList()、GETFLOORLIST()、floorList。

import { reqgetCategoryList,reqGetBannerList } from "@/api";

//home模块的仓库
const state = {
  //home仓库中存储三级菜单的数据
  categoryList: [],
  //轮播图的数据
  bannerList: [],
  //floor组件的数据
  floorList:[]
};

//mutions是唯一修改state的地方
const mutations = {
    GETCATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList;
      },
    GETBANNERLIST(state, bannerList) {
        state.bannerList = bannerList;
        console.log('GETBANNERLIST')
      },
    GETFLOORLIST(state,floorList){
         state.floorList = floorList;
      }
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCategoryList({ commit }) {
        //reqgetCategoryList返回的是一个Promise对象
        //需要用await接受成功返回的结果,await必须要结合async一起使用(CP)
        let result = await reqgetCategoryList();
        if (result.code == 200) {
          commit("GETCATEGORYLIST", result.data);
        }
      },
    async getBannerList({ commit }) {
        let result = await reqGetBannerList();
        if (result.code == 200) {
          commit("GETBANNERLIST", result.data);
          console.log('result.data',result.data)
        }
      },
    //获取floor数据
    async getFloorList({ commit }) {
      let result = await reqFloorList();
      if (result.code == 200) {
        //提交mutation
        commit("GETFLOORLIST", result.data);
        console.log('result.data',result.data)
      }
    },
};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

第九步: 组件请求数据
ListContainer/index.vue 请求数据,用计算属性bannerList去接收数据。

<script>
import { mapState } from "vuex";
export default {
  name: 'ListContainer',
  mounted() {
    //mounted:组件挂载完毕,正常说组件结构(DOM)已经全有了
    //为什么swiper实例在mounted当中直接书写不可以:因为结构还没有完整
    this.$store.dispatch("getBannerList");
  },
  computed: {
    ...mapState({
      bannerList: (state) => state.home.bannerList,
    }),
  }
}
</script>

在这里插入图片描述
在这里插入图片描述

034~036-Banner实现轮播图

【24、swiper插件实现轮播图】

官方做法请见https://blog.csdn.net/weixin_43424325/article/details/121684101中的【24、swiper插件实现轮播图】。

ElementUI轮播图用10行代码就能解决

链接https://element.eleme.cn/#/zh-CN/component/carousel

main.js全局引用ElementUI

//引入ElementUI组件库
import ElementUI from 'element-ui';
//引入ElementUI全部样式
import 'element-ui/lib/theme-chalk/index.css';
//使用ElementUI
Vue.use(ElementUI)

ListContainer/index.vue引用【Carousel 走马灯】
在这里插入图片描述

<!--banner轮播-->
 <div class="block">
       <el-carousel height="455px">
       <el-carousel-item v-for="(carousel,index) in bannerList" :key="carousel.id">
            <img style="width:100%" :src="carousel.imgUrl" />
       </el-carousel-item>
       </el-carousel>
 </div>

ListContainer/index.vue完整代码

<template>

        <!--列表-->
        <div class="list-container">
            <div class="sortList clearfix">
                <div class="center">
                    <!--banner轮播-->
                        <div class="block">
                            <el-carousel height="455px">
                            <el-carousel-item v-for="(carousel,index) in bannerList" :key="carousel.id">
                                <img style="width:100%" :src="carousel.imgUrl" />
                            </el-carousel-item>
                            </el-carousel>
                        </div>

                </div>
                <div class="right">
                    <div class="news">
                        <h4>
                            <em class="fl">尚品汇快报</em>
                            <span class="fr tip">更多 ></span>
                        </h4>
                        <div class="clearix"></div>
                        <ul class="news-list unstyled">
                            <li>
                                <span class="bold">[特惠]</span>备战开学季 全民半价购数码
                            </li>
                            <li>
                                <span class="bold">[公告]</span>备战开学季 全民半价购数码
                            </li>
                            <li>
                                <span class="bold">[特惠]</span>备战开学季 全民半价购数码
                            </li>
                            <li>
                                <span class="bold">[公告]</span>备战开学季 全民半价购数码
                            </li>
                            <li>
                                <span class="bold">[特惠]</span>备战开学季 全民半价购数码
                            </li>
                        </ul>
                    </div>
                    <ul class="lifeservices">
                        <li class=" life-item ">
                            <i class="list-item"></i>
                            <span class="service-intro">话费</span>
                        </li>
                        <li class=" life-item ">
                            <i class="list-item"></i>
                            <span class="service-intro">机票</span>
                        </li>
                        <li class=" life-item ">
                            <i class="list-item"></i>
                            <span class="service-intro">电影票</span>
                        </li>
                        <li class=" life-item ">
                            <i class="list-item"></i>
                            <span class="service-intro">游戏</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">彩票</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">加油站</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">酒店</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">火车票</span>
                        </li>
                        <li class=" life-item ">
                            <i class="list-item"></i>
                            <span class="service-intro">众筹</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">理财</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">礼品卡</span>
                        </li>
                        <li class=" life-item">
                            <i class="list-item"></i>
                            <span class="service-intro">白条</span>
                        </li>
                    </ul>
                    <div class="ads">
                        <img src="./images/ad1.png" />
                    </div>
                </div>
            </div>
        </div>

</template>

<script>
import { mapState } from "vuex";
export default {
  name: 'ListContainer',
  mounted() {
    //mounted:组件挂载完毕,正常说组件结构(DOM)已经全有了
    //为什么swiper实例在mounted当中直接书写不可以:因为结构还没有完整
    this.$store.dispatch("getBannerList");
  },
  computed: {
    ...mapState({
      bannerList: (state) => state.home.bannerList,
    }),
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">

    .list-container {
        width: 1200px;
        margin: 0 auto;

        .sortList {
            height: 464px;
            padding-left: 210px;

            .center {
                box-sizing: border-box;
                width: 740px;
                height: 100%;
                padding: 5px;
                float: left;


            

                .el-carousel__item:nth-child(2n) {
                    background-color: #99a9bf;
                }
                
                .el-carousel__item:nth-child(2n+1) {
                    background-color: #d3dce6;
                }
            }

            .right {
                float: left;
                width: 250px;

                .news {
                    border: 1px solid #e4e4e4;
                    margin-top: 5px;

                    h4 {
                        border-bottom: 1px solid #e4e4e4;
                        padding: 5px 10px;
                        margin: 5px 5px 0;
                        line-height: 22px;
                        overflow: hidden;
                        font-size: 14px;

                        .fl {
                            float: left;
                        }

                        .fr {
                            float: right;
                            font-size: 12px;
                            font-weight: 400;
                        }
                    }

                    .news-list {
                        padding: 5px 15px;
                        line-height: 26px;

                        .bold {
                            font-weight: 700;
                        }
                    }
                }

                .lifeservices {
                    border-right: 1px solid #e4e4e4;
                    overflow: hidden;
                    display: flex;
                    flex-wrap: wrap;

                    .life-item {
                        border-left: 1px solid #e4e4e4;
                        border-bottom: 1px solid #e4e4e4;
                        margin-right: -1px;
                        height: 64px;
                        text-align: center;
                        position: relative;
                        cursor: pointer;
                        width: 25%;

                        .list-item {
                            background-image: url(./images/icons.png);
                            width: 61px;
                            height: 40px;
                            display: block;
                        }

                        .service-intro {
                            line-height: 22px;
                            width: 60px;
                            display: block;
                        }

                        &:nth-child(1) {
                            .list-item {
                                background-position: 0px -5px;
                            }
                        }

                        &:nth-child(2) {
                            .list-item {
                                background-position: -62px -5px;
                            }
                        }

                        &:nth-child(3) {
                            .list-item {
                                background-position: -126px -5px;
                            }
                        }

                        &:nth-child(4) {
                            .list-item {
                                background-position: -190px -5px;
                            }
                        }

                        &:nth-child(5) {
                            .list-item {
                                background-position: 0px -76px;
                            }
                        }

                        &:nth-child(6) {
                            .list-item {
                                background-position: -62px -76px;
                            }
                        }

                        &:nth-child(7) {
                            .list-item {
                                background-position: -126px -76px;
                            }
                        }

                        &:nth-child(8) {
                            .list-item {
                                background-position: -190px -76px;
                            }
                        }

                        &:nth-child(9) {
                            .list-item {
                                background-position: 0px -146px;
                            }
                        }

                        &:nth-child(10) {
                            .list-item {
                                background-position: -62px -146px;
                            }
                        }

                        &:nth-child(11) {
                            .list-item {
                                background-position: -126px -146px;
                            }
                        }

                        &:nth-child(12) {
                            .list-item {
                                background-position: -190px -146px;
                            }
                        }
                    }
                }

                .ads {
                    margin-top: 5px;

                    img {
                        opacity: 0.8;
                        transition: all 400ms;

                        &:hover {
                            opacity: 1;
                        }
                    }
                }
            }
        }
    }

</style>

最终效果:
在这里插入图片描述

037~038-动态展示Floor组件(ElementUI做轮播图)

1.获取floor组件mock数据

在这里插入图片描述

2.分析数据结构

在这里插入图片描述

3.动态展示Floor组件(ElementUI做轮播图)

Home/index.vue 里面有Floor组件,并用props传值。

<template>
    <div>
      <!--  三级联动全局组件已经注册为全局组件,因此不需要引入-->
      <TypeNav/>
      <ListContainer/>
      <Recommend/>
      <Rank/>
      <Like/>
      <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
      <Brand/>
    </div>
</template>

<script>
import ListContainer from '@/views/Home/ListContainer'
import Recommend from '@/views/Home/Recommend'
import Rank from '@/views/Home/Rank'
import Like from '@/views/Home/Like'
import Floor from '@/views/Home/Floor'
import Brand from '@/views/Home/Brand'
import { mapState } from 'vuex'

export default {
  name: 'Home',
  components:{
    ListContainer,
    Recommend,
    Rank,
    Like,
    Floor,
    Brand,
  },
  mounted(){
    //派发action,获取floor组件的数据
    this.$store.dispatch('getFloorList')
  },
  computed: {
    ...mapState({
      floorList: (state) => state.home.floorList,
    }),
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
</style>

Floor/index.vue 动态传值

<template>

        <!--楼层-->
        <div class="floor">
            <div class="py-container">
                <div class="title clearfix">
                    <h3 class="fl">{{ list.name }}</h3>
                    <div class="fr">
                        <ul class="nav-tabs clearfix">
                            <li class="active">
                                <a :href="item.url" data-toggle="tab" v-for="(item,index) in list.navList" :key="index">{{ item.text }}</a>
                            </li>
                        </ul>
                    </div>
                </div>
                <div class="tab-content">
                    <div class="tab-pane">
                        <div class="floor-1">
                            <div class="blockgary">
                                <ul class="jd-list">
                                    <li  v-for="(keyword,index) in list.keywords" :key="index">
                                        {{ keyword }}
                                    </li>
                                </ul>
                                <img :src="list.imgUrl" />
                            </div>
                            <div class="floorBanner">
                                <!-- 引入轮播图 -->
                                <div class="block">
                                    <el-carousel height="355px" arrow="always">
                                        <el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id">
                                            <img style="width:100%" :src="carousel.imgUrl" />
                                        </el-carousel-item>
                                    </el-carousel>
                                </div>
                            </div>
                            <div class="split">
                                <span class="floor-x-line"></span>
                                <div class="floor-conver-pit">
                                    <img :src="list.recommendList[0]" />
                                </div>
                                <div class="floor-conver-pit">
                                    <img :src="list.recommendList[1]" />
                                </div>
                            </div>
                            <div class="split center">
                                <img :src="list.bigImg" />
                            </div>
                            <div class="split">
                                <span class="floor-x-line"></span>
                                <div class="floor-conver-pit">
                                    <img :src="list.recommendList[2]" />
                                </div>
                                <div class="floor-conver-pit">
                                    <img :src="list.recommendList[3]" />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>


</template>

<script>
export default {
  name: 'Floor',
  props:['list'],
}
</script>

039-将轮播图模块提取为公共组件

1.编写公共组件
Carousel/index.vue

<template>
    <!-- 引入轮播图 -->
    <div class="block">
        <el-carousel :height="height" arrow="always">
            <el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id">
                <img style="width:100%" :src="carousel.imgUrl" />
            </el-carousel-item>
        </el-carousel>
    </div>
</template>

<script>
export default {
  name: 'Carousel',
  props:['list','height'],

}
</script>

<style scoped lang="less">
</style>

2.全局注册
main.js

import Carousel from '@/components/Carousel'
Vue.component(Carousel.name,Carousel)

3.组件内引用
Floor/index.vue

 <!-- 引入轮播图 -->
 <!-- <div class="block">
<el-carousel height="355px" arrow="always">
 <el-carousel-item v-for="(carousel,index) in list.carouselList" :key="carousel.id">
     <img style="width:100%" :src="carousel.imgUrl" />
 </el-carousel-item>
</el-carousel>
</div> -->

<Carousel :list="list" :height="`355px`"/>

040~042-search模块中动态展现数据

API、vuex三连、mapstate、动态展现

在这里插入图片描述

api/index.js 根据接口文件,给服务器传递一个默认参数【至少是一个空对象】

//当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数【至少是一个空对象】
export const reqGetSearchInfo = (params)=>requests({url:"/list",method:"post",data:params});

store/search.js我没有使用getters

    computed:{
      ...mapState({
        goodsList: (state)=> state.search.searchList.goodsList || [],
        attrsList: (state)=> state.search.searchList.attrsList,
        trademarkList: (state)=> state.search.searchList.trademarkList,
      })
    },

store/search.js

import { reqGetSearchInfo } from "@/api";

//home模块的仓库
const state = {
  //仓库初始状态
  searchList:{}
};

//mutions是唯一修改state的地方
const mutations = {
  REQGETSEARCHINFO(state, searchList) {
        state.searchList = searchList;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getSearchList({ commit },params={}) {
        let result = await reqGetSearchInfo(params);
        if (result.code == 200) {
          commit("REQGETSEARCHINFO", result.data);
        }
      },
};


// 计算属性
const getters = {
};


export default {
  state,
  mutations,
  actions,
  getters,
};

Search/index.vue 采取mapState,并动态展现数据

<template>
  <div>
    <TypeNav />
    <div class="main">
      <div class="py-container">
        <!--bread-->
        <div class="bread">
          <ul class="fl sui-breadcrumb">
            <li>
              <a href="#">全部结果</a>
            </li>
          </ul>
          <ul class="fl sui-tag">
            <li class="with-x">手机</li>
            <li class="with-x">iphone<i>×</i></li>
            <li class="with-x">华为<i>×</i></li>
            <li class="with-x">OPPO<i>×</i></li>
          </ul>
        </div>


        <!--selector-->
        <SearchSelector />

        <!--details-->
        <div class="details clearfix">
          <div class="sui-navbar">
            <div class="navbar-inner filter">
              <ul class="sui-nav">
                <li class="active">
                  <a href="#">综合</a>
                </li>
                <li>
                  <a href="#">销量</a>
                </li>
                <li>
                  <a href="#">新品</a>
                </li>
                <li>
                  <a href="#">评价</a>
                </li>
                <li>
                  <a href="#">价格⬆</a>
                </li>
                <li>
                  <a href="#">价格⬇</a>
                </li>
              </ul>
            </div>
          </div>
          <div class="goods-list">
            <ul class="yui3-g">
              <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id">
                <div class="list-wrap">
                  <div class="p-img">
                    <a href="item.html" target="_blank"><img :src="good.defaultImg" /></a>
                  </div>
                  <div class="price">
                    <strong>
                      <em>¥</em>
                      <i>{{ good.price }}.00</i>
                    </strong>
                  </div>
                  <div class="attr">
                    <a target="_blank" href="item.html" :title="good.title" v-html="good.title"></a>
                  </div>
                  <div class="commit">
                    <i class="command">已有<span>{{ good.hotScore }}</span>人评价</i>
                  </div>
                  <div class="operate">
                    <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                    <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
                  </div>
                </div>
              </li>
            </ul>
          </div>
          <div class="fr page">
            <div class="sui-pagination clearfix">
              <ul>
                <li class="prev disabled">
                  <a href="#">«上一页</a>
                </li>
                <li class="active">
                  <a href="#">1</a>
                </li>
                <li>
                  <a href="#">2</a>
                </li>
                <li>
                  <a href="#">3</a>
                </li>
                <li>
                  <a href="#">4</a>
                </li>
                <li>
                  <a href="#">5</a>
                </li>
                <li class="dotted"><span>...</span></li>
                <li class="next">
                  <a href="#">下一页»</a>
                </li>
              </ul>
              <div><span>10&nbsp;</span></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import { mapState,mapGetters } from 'vuex';
  import SearchSelector from './SearchSelector/SearchSelector'
  export default {
    name: 'Search',
    data(){
      return {
        searchParams:{
          "category1Id": "",
          "category2Id": "",
          "category3Id": "",
          "categoryName": "",
          "keyword": "小米",
          //排序
          "order": "",
          "pageNo": 1,
          "pageSize": 10,
          //平台属性的操作
          "props": [],
          //品牌
          "trademark": ""
        },
      }
    },
    components: {
      SearchSelector
    },
    mounted() {
      this.$store.dispatch("getSearchList",this.searchParams);
    },
    computed:{
      ...mapState({
        goodsList: (state)=> state.search.searchList.goodsList || [],
        attrsList: (state)=> state.search.searchList.attrsList,
        trademarkList: (state)=> state.search.searchList.trademarkList,
      })
    },
  }
</script>

一些坑

1.POST请求数据,可能会请求数据延时,导致无法取得数据。
假如网络不给力或没有网state.searchList.goodsList应该返回的是undefined

2.goodsList写成goodslist
goodsList: (state)=> state.search.searchList.goodsList

044-Search模块中子组件(SearchSelector)动态展示

在这里插入图片描述
Search/SearchSelector/SearchSelector.vue

<template>
  <div class="clearfix selector">
    <div class="type-wrap logo">
      <div class="fl key brand">品牌</div>
      <div class="value logos">
        <ul class="logo-list">
          <li v-for="(trademark,index) in trademarkList" :key="trademark.tmId">{{ trademark.tmName }}</li>
        </ul>
      </div>
      <div class="ext">
        <a href="javascript:void(0);" class="sui-btn">多选</a>
        <a href="javascript:void(0);">更多</a>
      </div>
    </div>

    <div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
      <div class="fl key">{{ attr.attrName }}</div>
      <div class="fl value">
        <ul class="type-list" v-for="(attrValue,index) in attr.attrValueList" :key="index">
          <li>
            <a>{{ attrValue }}</a>
          </li>
        </ul>
      </div>
      <div class="fl ext"></div>
    </div>

  </div>
</template>

<script>
  import { mapState,mapGetters } from 'vuex';
  export default {
    name: 'SearchSelector',
    computed:{
      ...mapState({
        attrsList: (state)=> state.search.searchList.attrsList,
        trademarkList: (state)=> state.search.searchList.trademarkList,
      })
    },
  }
</script>

Vuex 数据结构
在这里插入图片描述
在这里插入图片描述

045-监听路由的变化再发请求获取数据

watch监听路由的变化再发请求获取数据

Search/index.vue

    watch:{
      //监听路由的信息是否发生变化,如果发生变化,再次发起请求
      $route(newValue,oldValue){
        console.log(newValue)
        //每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3
        //再次发请求之前整理带给服务器参数
        Object.assign(this.searchParams,this.$route.query,this.$route.params);
        this.getData()
        console.log(this.searchParams)

      //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      //分类Season清掉
      // this.searchParams.categoryName = undefined;
      // this.searchParams.keyword = undefined;
      }
    }
坑:三个ID只能带一个

三个ID只能带一个,所以需要重置。分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
在这里插入图片描述

  //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
  this.searchParams.category1Id = undefined;
  this.searchParams.category2Id = undefined;
  this.searchParams.category3Id = undefined;
  //分类Season清掉
  this.searchParams.categoryName = undefined;
  this.searchParams.keyword = undefined;

046~049-面包屑

在这里插入图片描述

面包屑categoryName

Search\index.vue 把带给服务器的参数置空了,还需要向服务器发请求。undefined字段不会带给服务器。通过自己跳自己,删除query,保留params参数。

<template>
	<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
</template>


<script>
      removeCategoryName() {
        //把带给服务器的参数置空了,还需要向服务器发请求
        //带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
        //但是你把相应的字段变为undefined,当前这个字段不会带给服务器
        this.searchParams.category1Id = undefined
        this.searchParams.category2Id = undefined
        this.searchParams.category3Id = undefined
        this.searchParams.categoryName = undefined
        this.getData()
        
        //地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
        //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
        if (this.$route.params) {
            this.$router.push({ name: "search", params: this.$route.params });
        }
      },
</script>

在这里插入图片描述

面包屑keyword

Search\index.vue 给服务器带的参数searchParams的keyword置空,还需要向服务器发请求。undefined字段不会带给服务器。全局路线总线通知兄弟组件Header清除关键字。通过自己跳自己,删除路径上的params,保留路径上的query参数。

<template>
	<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
</template>


<script>
      removeKeyword(){
        //给服务器带的参数searchParams的keyword置空
        this.searchParams.keyword = undefined
        //再次发请求
        this.getData()
        //通知兄弟组件Header清除关键字
        this.$bus.$emit('clear')
        //进行路由的跳转
        if (this.$route.query) {
            this.$router.push({ name: "search", query: this.$route.query });
        }
      }
</script>

全局路线总线通知Header/index.vue

  mounted() {
    //通过全局事件总线清除关键字
    this.$bus.$on("clear", () => {
      this.keyword = "";
    });
  },

在这里插入图片描述

组件通信方式

第一种 父子组件通信:$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。
第二种 全局事件总线 $bus(适用于所有的场景)
第三种 Vuex
第四种 插槽(适用于父子组件通信)

面包屑trademark

点击子组件SearchSelector商标时,父组件Search的数据重新发请求。

Search/SearchSelector/SearchSelector.vue给父组件传递参数。

<template>
<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="tradeMatkHandler(trademark)" >{{ trademark.tmName }}</li>
</template>

<script>
    methods: {
      //品牌的事件处理函数
      tradeMatkHandler(trademark){
        //点击了品牌(苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示
        this.$emit('trademarkInfo',trademark)
      },
    },
</script>

Search/index.vue删除品牌的信息,更新searchParams.trademark = undefined。采取自定义事件回调,再次发请求获取search模块列表数据进行展示。

<template>
  <li class="with-x" v-if="searchParams.trademark">{{ searchParams.trademark.split(':')[1] }}<i @click="removeTradeMark">×</i></li>

 <SearchSelector @trademarkInfo="trademarkInfo"  />
</template>

<script>
    methods: {
      //删除品牌的信息
      removeTradeMark(){
        this.searchParams.trademark = undefined
        this.getData()
      },
      //自定义事件回调
      trademarkInfo(trademark){
        //1:整理品牌字段的参数  "ID:品牌名称"
        this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
        //再次发请求获取search模块列表数据进行展示
        this.getData()
      },
    },
</script>

在这里插入图片描述

面包屑props

点击子组件SearchSelector属性时,父组件Search的数据重新发请求。

Search/SearchSelector/SearchSelector.vue给父组件传递参数。

<template>
          <!-- 平台相应售卖的属性的属性值:粉色,蓝色,黑色... -->
          <li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)">
            <a>{{ attrValue }}</a>
          </li>
</template>

<script>
    methods: {
      //平台售卖属性值的点击事件
      attrInfo(attr,attrValue){
        //["属性ID:属性值:属性名"]
        this.$emit("attrInfo",attr,attrValue);
      }
    },
</script>

Search/index.vue采取自定义事件回调,根据所选择的属性,再次发请求获取search模块列表数据进行展示。也可以删除属性,更新this.searchParams.props.splice(index, 1)。

<template>
 <li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index">
 {{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i>
</li>
            
  <SearchSelector @trademarkInfo="trademarkInfo"  @attrInfo="attrInfo"/>
  
</template>

<script>
    methods: {
      //收集平台属性地方回调函数(自定义事件)
      attrInfo(attr, attrValue) {
        //["属性ID:属性值:属性名"]
        console.log(attr, attrValue);
        //参数格式整理好
        let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
        //数组去重
        //if语句里面只有一行代码:可以省略大花括号
        if (this.searchParams.props.indexOf(props) == -1)
          this.searchParams.props.push(props);
        //再次发请求
        this.getData();
      },
      //removeAttr删除售卖的属性
      removeAttr(index) {
        //再次整理参数splice(index,howmany,item1,…itemx);
        this.searchParams.props.splice(index, 1);
        //再次发请求
        this.getData();
      },
    },
</script>

在这里插入图片描述

050~051-排序

排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。

我们的升降序是通过箭头图标来辨别的,如图所示:
在这里插入图片描述

Search/index.vue
1.在search模块使用该图标

              <ul class="sui-nav">
                <li :class="{ active: isOne }" @click="changeOrder('1')">
                  <a >综合
                    <span v-show="isOne && isAsc"></span>
                    <span v-show="isOne && isDesc"></span>
                  </a>
                </li>
                <li :class="{ active: isTwo }" @click="changeOrder('2')">
                  <a >价格
                    <span v-show="isTwo && isAsc"></span>
                    <span v-show="isTwo && isDesc"></span>
                  </a>
                </li>
              </ul>

2.isOne、isTwo、isAsc、isDesc计算属性代码

    computed:{
	...
      isOne(){
        return this.searchParams.order.indexOf('1') !== -1
      },
      isTwo(){
        return this.searchParams.order.indexOf('2') !== -1
      },
      isAsc(){
        return this.searchParams.order.indexOf('asc') !== -1
      },
      isDesc(){
        return this.searchParams.order.indexOf('desc') !== -1
      },
    },

3.点击‘综合’或‘价格’的触发函数changeOrder

      //排序的操作
      changeOrder(flag) {
        //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)
        //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】
        let originOrder = this.searchParams.order;
        let orginsFlag = originOrder.split(":")[0];
        let originSort = originOrder.split(":")[1];
        //新的排序方式
        let newOrder = "";
        //判断的是多次点击的是不是同一个按钮
        if (flag == orginsFlag) {
          newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`;
        } else {
          //点击不是同一个按钮
          newOrder = `${flag}:${"desc"}`;
        }
        //需要给order重新赋值
        this.searchParams.order = newOrder;
        //再次发请求
        this.getData();
      },

字符串拼接

如果你想在你的字符串内加入某个变量的值,就需要字符串拼接使用 ``(飘符号),由于 飘在markdown是单行代码标记所以下面我们用··代替。
字符串拼接 ·${}·,使用方法如下:
在js中使用

var a = 1;
console.log(`a的值是:${a}`); //a的值是:1

在html中使用

<router-link :to="`/detail/${goods.id}`"></router-link>

052~058-分页器

先查看Vuex/search返回数组情况
在这里插入图片描述

官方笔记

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。

Pagination/index.vue

  //连续页码的起始页码、末尾页码
    startNumAndEnd(){
      let start = 0 , end = 0;
      //规定连续页码数字5(totalPage至少5页)
      //不正常现象
      if(this.continues > this.totalPage){
        start = 1
        end = this.totalPage
      }else{
        //正常现象      Math.floor:想下取整
        start = this.pageNo - Math.floor(this.continues/2)
        end = this.pageNo + Math.floor(this.continues/2)
        //start出现不正常现象纠正
        if(start < 1){
          start = 1
          end = this.continues
        }
        //end出现不正常现象纠正
        if(end > this.totalPage){
          end = this.totalPage
          start = this.totalPage - this.continues + 1
        }
      }
      return {start,end}
    }

ElementUI的分页器

在这里插入图片描述

Pagination/index.vue使用ElementUI的分页器,简单很多。

<template>
  <div class="block">
    <el-pagination
      @current-change="handleCurrentChange"
      :current-page.sync="currentPage3"
      :page-size="pageSize"
      layout="prev, pager, next, jumper"
      :total="total">
    </el-pagination>
  </div>

</template>
<script>
  export default {
    name: "Pagination",
    props: ["pageNo", "pageSize", "total", "continues"],
    methods: {
      handleCurrentChange(val) {
        console.log(`当前页: ${val}`);
        this.$emit('getPageNo', val)
      }
    },
    data() {
      return {
        //当前页数
        currentPage3: 1,
      };
    }
  }
</script>

Search\index.vue传入分页器所需内容,并注册getPageNo自定义事件。

<template>
  <div>
    <TypeNav />
    <div class="main">
      <div class="py-container">
        <!--bread-->
        <div class="bread">
          <ul class="fl sui-breadcrumb">
            <li>
              <a href="#">全部结果</a>
            </li>
          </ul>
          <ul class="fl sui-tag">
            <li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
            <li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li>
            <li class="with-x" v-if="searchParams.trademark">{{ searchParams.trademark.split(':')[1] }}<i @click="removeTradeMark">×</i></li>
            <li class="with-x"  v-for="(attrValue, index) in searchParams.props" :key="index">
              {{ attrValue.split(":")[1] }}<i @click="removeAttr(index)">×</i>
            </li>
          </ul>
        </div>


        <!--selector-->
        <SearchSelector @trademarkInfo="trademarkInfo"  @attrInfo="attrInfo"/>

        <!--details-->
        <div class="details clearfix">
          <div class="sui-navbar">
            <div class="navbar-inner filter">
              <ul class="sui-nav">
                <li :class="{ active: isOne }" @click="changeOrder('1')">
                  <a >综合
                    <span v-show="isOne && isAsc"></span>
                    <span v-show="isOne && isDesc"></span>
                  </a>
                </li>
                <li :class="{ active: isTwo }" @click="changeOrder('2')">
                  <a >价格
                    <span v-show="isTwo && isAsc"></span>
                    <span v-show="isTwo && isDesc"></span>
                  </a>
                </li>
              </ul>
            </div>
          </div>
          <div class="goods-list">
            <ul class="yui3-g">
              <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id">
                <div class="list-wrap">
                  <div class="p-img">
                    <a href="item.html" target="_blank"><img :src="good.defaultImg" /></a>
                  </div>
                  <div class="price">
                    <strong>
                      <em>¥</em>
                      <i>{{ good.price }}.00</i>
                    </strong>
                  </div>
                  <div class="attr">
                    <a target="_blank" href="item.html" :title="good.title" v-html="good.title"></a>
                  </div>
                  <div class="commit">
                    <i class="command">已有<span>{{ good.hotScore }}</span>人评价</i>
                  </div>
                  <div class="operate">
                    <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                    <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
                  </div>
                </div>
              </li>
            </ul>
          </div>

          <Pagination
            :pageNo="searchParams.pageNo"
            :pageSize="searchParams.pageSize"
            :total="total"
            :continues="5"
            @getPageNo="getPageNo"
          />

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

<script>
  import { mapState,mapGetters } from 'vuex';
  import SearchSelector from './SearchSelector/SearchSelector'
  export default {
    name: 'Search',
    data(){
      return {
        searchParams:{
          "category1Id": "",
          "category2Id": "",
          "category3Id": "",
          "categoryName": "",
          "keyword": "",
          //排序
          "order": "1:desc",
          "pageNo": 1,
          "pageSize": 4,
          //平台属性的操作
          "props": [],
          //品牌
          "trademark": ""
        },
      }
    },
    components: {
      SearchSelector
    },
    beforeMount() {
      //在发请求之前咱们需要将searchParams里面参数进行修改带给服务器
      Object.assign(this.searchParams,this.$route.query,this.$route.params)
    },
    mounted() {
      //在发请求之前咱们需要将searchParams里面参数进行修改带给服务器
      this.getData()
    },
    methods: {
      getData(){
        this.$store.dispatch("getSearchList",this.searchParams);
      },
      //删除分类的名字
      removeCategoryName() {
        //把带给服务器的参数置空了,还需要向服务器发请求
        //带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
        //但是你把相应的字段变为undefined,当前这个字段不会带给服务器
        this.searchParams.category1Id = undefined
        this.searchParams.category2Id = undefined
        this.searchParams.category3Id = undefined
        this.searchParams.categoryName = undefined
        this.getData()
        
        //地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
        //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
        if (this.$route.params) {
            this.$router.push({ name: "search", params: this.$route.params });
        }
      },
      removeKeyword(){
        //给服务器带的参数searchParams的keyword置空
        this.searchParams.keyword = undefined
        //再次发请求
        this.getData()
        //通知兄弟组件Header清除关键字
        this.$bus.$emit('clear')
        //进行路由的跳转
        if (this.$route.query) {
            this.$router.push({ name: "search", query: this.$route.query });
        }
      },
      //自定义事件回调
      trademarkInfo(trademark){
        //1:整理品牌字段的参数  "ID:品牌名称"
        this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
        //再次发请求获取search模块列表数据进行展示
        this.getData()
      },
      //删除品牌的信息
      removeTradeMark() {
        //将品牌信息置空
        this.searchParams.trademark = undefined;
        //再次发请求
        this.getData();
      },
      //收集平台属性地方回调函数(自定义事件)
      attrInfo(attr, attrValue) {
        //["属性ID:属性值:属性名"]
        console.log(attr, attrValue);
        //参数格式整理好
        let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
        //数组去重
        //if语句里面只有一行代码:可以省略大花括号
        if (this.searchParams.props.indexOf(props) == -1)
          this.searchParams.props.push(props);
        //再次发请求
        this.getData();
      },
      //removeAttr删除售卖的属性
      removeAttr(index) {
        //再次整理参数splice(index,howmany,item1,…itemx);
        this.searchParams.props.splice(index, 1);
        //再次发请求
        this.getData();
      },
      //排序的操作
      changeOrder(flag) {
        //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)
        //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】
        let originOrder = this.searchParams.order;
        let orginsFlag = originOrder.split(":")[0];
        let originSort = originOrder.split(":")[1];
        //新的排序方式
        let newOrder = "";
        //判断的是多次点击的是不是同一个按钮
        if (flag == orginsFlag) {
          newOrder = `${orginsFlag}:${originSort == "desc" ? "asc" : "desc"}`;
        } else {
          //点击不是同一个按钮
          newOrder = `${flag}:${"desc"}`;
        }
        //需要给order重新赋值
        this.searchParams.order = newOrder;
        //再次发请求
        this.getData();
      },
      //自定义事件的回调函数---获取当前第几页
      getPageNo(pageNo) {
        //整理带给服务器参数
        this.searchParams.pageNo = pageNo;
        //再次发请求
        this.getData();
      },
      },
    computed:{
      ...mapState({
        goodsList: (state)=> state.search.searchList.goodsList || [],
        attrsList: (state)=> state.search.searchList.attrsList,
        trademarkList: (state)=> state.search.searchList.trademarkList,
        total:(state)=> state.search.searchList.total,
      }),
      isOne(){
        return this.searchParams.order.indexOf('1') !== -1
      },
      isTwo(){
        return this.searchParams.order.indexOf('2') !== -1
      },
      isAsc(){
        return this.searchParams.order.indexOf('asc') !== -1
      },
      isDesc(){
        return this.searchParams.order.indexOf('desc') !== -1
      },
    },
    watch:{
      //监听路由的信息是否发生变化,如果发生变化,再次发起请求
      $route(newValue,oldValue){
        console.log(newValue)
        //每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3
        //再次发请求之前整理带给服务器参数
        Object.assign(this.searchParams,this.$route.query,this.$route.params);
        this.getData()
        console.log(this.searchParams)

      //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;

      }
    }
  }
</script>

main.js:Pagination注册为全局组件。

import Pagination from '@/components/Pagination'
Vue.component(Pagination.name,Pagination)

059-滚动条

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
https://router.vuejs.org/zh/guide/advanced/scroll-behavior.html

router/index.js

// 向外默认暴露路由器对象
export default new VueRouter({
	mode: 'history', // 没有#的模式
	routes, // 注册所有路由
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        return { y: 0 }
      },
  })

undefined细节(*****)

访问undefined的属性值会引起红色警告,可以不处理,但是要明白警告的原因。
以获取商品categoryView信息为例,categoryView是一个对象。

对应的getters代码

const getters =  {
    categoryView(state){
        return state.goodInfo.categoryView
    }
}

对应的computed代码

 computed:{
      ...mapGetters(['categoryView'])
    }

html代码

<div class="conPoin">
        <span v-show="categoryView.category1Name" >{{categoryView.category1Name}}</span>
        <span v-show="categoryView.category2Name" >{{categoryView.category2Name}}</span>
        <span v-show="categoryView.category3Name" >{{categoryView.category3Name}}</span>
      </div>

注意下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,页面可以正常运行,但是会出现红色警告。
在这里插入图片描述
原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结:所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。如果返回值为对象加||{},数组:||[ ]。此处categoryView为对象,所以将getters代码改为return state.goodInfo.categoryView||{}

060~068 产品详情Detail

路由的坑

1.接口文档说明:参数是params的skuId。
在这里插入图片描述

2.router/routes.js

		{
			path:'/detail/:skuId',//params参数需要占位
			component:Detail,
			meta:{
				showFooter: true,
			}
		},

3.Search/index.vue

  <router-link :to="`/detail/${good.id}`"><img :src="good.defaultImg" /></router-link>

4.api/index.js

//产品详情
export const reqGoodsInfo = (skuId)=>requests({url:`/item/${skuId}`,method:"get"});
取得数据

1.api已经写了。

2.vuex三连
store/detail.js

import { reqgetCategoryList,reqGetBannerList,reqFloorList,reqGoodsInfo } from "@/api";

//仓库
const state = {
  goodInfo: {},
};

//mutions是唯一修改state的地方
const mutations = {
  GETGOODINFO(state, goodInfo) {
        state.goodInfo = goodInfo;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getGoodInfo({ commit },skuId) {
        //reqgetCategoryList返回的是一个Promise对象
        //需要用await接受成功返回的结果,await必须要结合async一起使用(CP)
        let result = await reqGoodsInfo(skuId);
        if (result.code == 200) {
          commit("GETGOODINFO", result.data);
        }
      },
};

//计算属性
const getters = {};

export default {
  state,
  mutations,
  actions,
  getters,
};

3.组件取得数据
Detail/index.vue

<script>
  import ImageList from './ImageList/ImageList'
  import Zoom from './Zoom/Zoom'
  import { mapState } from 'vuex'

  export default {
    name: 'Detail',
    mounted() {
      this.$store.dispatch('getGoodInfo',this.$route.params.skuId)
    },
    components: {
      ImageList,
      Zoom
    },
    computed:{
      ...mapState({
        goodInfo: (state) => state.detail.goodInfo,
      })
    },

  }
</script>
展示图片/数据

在这里插入图片描述
Detail/index.vue展示部分数据(面包屑、价格、商品名称、商品详情)

      <!-- 导航路径区域 -->
      <div class="conPoin">
        <span v-show="goodInfo.categoryView.category1Name">{{ goodInfo.categoryView.category1Name }}</span>
        <span v-show="goodInfo.categoryView.category2Name">{{ goodInfo.categoryView.category2Name }}</span>
        <span v-show="goodInfo.categoryView.category3Name">{{ goodInfo.categoryView.category3Name }}</span>
      </div>

 <!-- 左侧放大镜区域 -->
        <div class="previewWrap">
          <!--放大镜效果-->
          <Zoom :skuImageList="goodInfo.skuInfo.skuImageList" />
          <!-- 小图列表 -->
          <ImageList :skuImageList="goodInfo.skuInfo.skuImageList" />
        </div>
        
  <!-- 右侧选择区域布局 -->
          <div class="goodsDetail">
            <h3 class="InfoName">{{ goodInfo.skuInfo.skuName }}</h3>
            <p class="news">{{ goodInfo.skuInfo.skuDesc }}</p>
            <div class="priceArea">
              <div class="priceArea1">
                <div class="title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
                <div class="price">
                  <i>¥</i>
                  <em>{{ goodInfo.skuInfo.price }}</em>
                  <span>降价通知</span>
                </div>
                <div class="remark">
                  <i>累计评价</i>
                  <em>65545</em>
                </div>
              </div>
              <div class="priceArea2">
                <div class="title">
                  <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</i>
                </div>
                <div class="fixWidth">
                  <i class="red-bg">加价购</i>
                  <em class="t-gray">999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换购热销商品</em>
                </div>
              </div>
            </div>
            <div class="support">
              <div class="supportArea">
                <div class="title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
                <div class="fixWidth">以旧换新,闲置手机回收 4G套餐超值抢 礼品购</div>
              </div>
              <div class="supportArea">
                <div class="title">配 送 至</div>
                <div class="fixWidth">广东省 深圳市 宝安区</div>
              </div>
            </div>
          </div>

轮播图互动

老师的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。
在这里插入图片描述

ImageList.vue用ElementUI实现轮播图

<template>
  <div class="swiper-container">
    <!-- <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="(slide, index) in skuImageList" :key="slide.id">
        <img :src="slide.imgUrl">
      </div>
    </div>
    <div class="swiper-button-next"></div>
    <div class="swiper-button-prev"></div> -->

    <!-- ElementUI写法 -->
    <el-carousel :autoplay="false" type="card" height="50px" arrow="always" @change="change" trigger="click">
      <el-carousel-item v-for="(slide, index) in skuImageList" :key="slide.id" >
        <img :src="slide.imgUrl" height="40px" :class="{active:currentIndex==index}"  style="margin-left: 70px;" @click="changeCurrentIndex(index)">
      </el-carousel-item>
      
    </el-carousel>
  </div>
</template>

<script>

  import Swiper from 'swiper'
  export default {
    name: "ImageList",
    data(){
      return {
        currentIndex:0,
      }
    },
    props:['skuImageList',],
    methods: {
      changeCurrentIndex(index){
        this.currentIndex = index
        //通知兄弟组件:当前的索引值为几
        this.$bus.$emit('getIndex',index)
      },
      change(newIndex,oldIndex){
        // console.log(newIndex,oldIndex)
        this.changeCurrentIndex(newIndex)
      },
    },
  }
</script>

<style lang="less" scoped>
  .active {
            border: 2px solid #f60;
            padding: 1px;
          }
</style>

Zoom.vue 展示照片

<template>
  <div class="spec-preview">
    <img :src="skuImageList[currentIndex].imgUrl" />
    <div class="event"></div>
    <div class="big">
      <img :src="skuImageList[currentIndex].imgUrl" />
    </div>
    <div class="mask"></div>
  </div>
</template>

<script>
  export default {
    name: "Zoom",
    data(){
      return{
        currentIndex:0,
      }
    },
    props:['skuImageList'],
    mounted() {
      //全局事件总线:获取兄弟组件传递过来的索引值
      this.$bus.$on('getIndex',(index)=>this.currentIndex=index)
    },
  }
</script>
添加放大镜(难)

在这里插入图片描述

zoom.vue添加handler,使用big和mask。

<template>
  <div class="spec-preview">
    <img :src="skuImageList[currentIndex].imgUrl" />
    <div class="event" @mousemove="handler"></div>
    <div class="big">
      <img :src="skuImageList[currentIndex].imgUrl"  ref="big"/>
    </div>
    <div class="mask" ref="mask"></div>
  </div>
</template>

<script>
  export default {
    name: "Zoom",
    data(){
      return{
        currentIndex:0,
      }
    },
    props:['skuImageList'],
    mounted() {
      //全局事件总线:获取兄弟组件传递过来的索引值
      this.$bus.$on('getIndex',(index)=>this.currentIndex=index)
    },
    methods: {
      handler(event) {
        let mask = this.$refs.mask;
        let big = this.$refs.big;
        let left = event.offsetX - mask.offsetWidth/2;
        let top = event.offsetY - mask.offsetHeight/2;
        //约束范围
        if(left <=0) left = 0;
        if(left >=mask.offsetWidth) left = mask.offsetWidth;
        if(top<=0)top = 0;
        if(top>=mask.offsetHeight) top = mask.offsetHeight;
        //修改元素的left|top属性值
        mask.style.left = left+'px';
        mask.style.top = top +'px';
        big.style.left = - 2 * left+'px';
        big.style.top = -2 * top +'px';


      },
    },
  }
</script>

<style lang="less">
  .spec-preview {
    position: relative;
    width: 400px;
    height: 400px;
    border: 1px solid #ccc;

    img {
      width: 100%;
      height: 100%;
    }

    .event {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 998;
    }

    .mask {
      width: 50%;
      height: 50%;
      background-color: rgba(0, 255, 0, 0.3);
      position: absolute;
      left: 0;
      top: 0;
      display: none;
    }

    .big {
      width: 100%;
      height: 100%;
      position: absolute;
      top: -1px;
      left: 100%;
      border: 1px solid #aaa;
      overflow: hidden;
      z-index: 998;
      display: none;
      background: white;

      img {
        width: 200%;
        max-width: 200%;
        height: 200%;
        position: absolute;
        left: 0;
        top: 0;
      }
    }

    .event:hover~.mask,
    .event:hover~.big {
      display: block;
    }
  }
</style>

在这里插入图片描述
在这里插入图片描述

单纯放大镜zoom的插件(可单独使用)

单纯zoom的插件 zoom2.vue

<template>
  <div class="spec-preview">
    <img src="./images/intro01.png" />
    <div class="event" @mousemove="handler"></div>
    <div class="big">
      <img src="./images/intro01.png"  ref="big"/>
    </div>
    <div class="mask" ref="mask"></div>
  </div>
</template>

<script>
  export default {
    name: "Zoom2",
    methods: {
      handler(event) {
        let mask = this.$refs.mask;
        let big = this.$refs.big;
        let left = event.offsetX - mask.offsetWidth/2;
        let top = event.offsetY - mask.offsetHeight/2;
        //约束范围
        if(left <=0) left = 0;
        if(left >=mask.offsetWidth) left = mask.offsetWidth;
        if(top<=0)top = 0;
        if(top>=mask.offsetHeight) top = mask.offsetHeight;
        //修改元素的left|top属性值
        mask.style.left = left+'px';
        mask.style.top = top +'px';
        big.style.left = - 2 * left+'px';
        big.style.top = -2 * top +'px';
      },
    },
  }
</script>

<style lang="less">
  .spec-preview {
    position: relative;
    width: 400px;
    height: 400px;
    border: 1px solid #ccc;

    img {
      width: 100%;
      height: 100%;
    }

    .event {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 998;
    }

    .mask {
      width: 50%;
      height: 50%;
      background-color: rgba(0, 255, 0, 0.3);
      position: absolute;
      left: 0;
      top: 0;
      display: none;
    }

    .big {
      width: 100%;
      height: 100%;
      position: absolute;
      top: -1px;
      left: 100%;
      border: 1px solid #aaa;
      overflow: hidden;
      z-index: 998;
      display: none;
      background: white;

      img {
        width: 200%;
        max-width: 200%;
        height: 200%;
        position: absolute;
        left: 0;
        top: 0;
      }
    }

    .event:hover~.mask,
    .event:hover~.big {
      display: block;
    }
  }
</style>

在这里插入图片描述

显示属性

使用spuSaleAttrList数据。
在这里插入图片描述

Detail/index.vue动态展现属性

<div class="chooseArea">
  <div class="choosed"></div>
  <dl v-for="(spuSaleAttr,index) in goodInfo.spuSaleAttrList" :key="index">
    <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
    <dd changepirce="0" class="active" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" :key="index">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
   </dl>
</div>


    computed:{
      ...mapState({
        goodInfo: (state) => state.detail.goodInfo,
      })
    },

在这里插入图片描述

Detail/index.vue选择数量和输入值校对

<div class="cartWrap">
    <div class="controls">
     <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum">
         <a href="javascript:" @click="skuNum++" class="plus">+</a>
         <a href="javascript:" @click="skuNum>1 ?skuNum--:1" class="mins">-</a>
     </div>
     <div class="add">
         <a href="javascript:">加入购物车</a>
     </div>
</div>

<script>
  export default {
    name: 'Detail',
    data() {
      return {
        skuNum: 1,
      };
    },

    methods: {
      //表单元素修改产品个数
      changeSkuNum(event) {
        //用户输入进来的文本 * 1
        let value = event.target.value * 1;
        //如果用户输入进来的非法,出现NaN或者小于1
        if (isNaN(value) || value < 1) {
          this.skuNum = 1;
        } else {
          //正常大于1【大于1整数不能出现小数】
          this.skuNum = parseInt(value);
        }
      },
    },
   }
</script>

在这里插入图片描述

排他操作

数据结构
在这里插入图片描述
Detail/index.vue遍历全部售卖属性值isChecked为零没有高亮了,点击的那个售卖属性值变为1

          <div class="choose">
            <div class="chooseArea">
              <div class="choosed"></div>
              <dl v-for="(spuSaleAttr,index) in goodInfo.spuSaleAttrList" :key="index">
                <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
                <dd changepirce="0" :class="{ active: spuSaleAttrValue.isChecked == 1 }" @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)" v-for="(spuSaleAttrValue,index) in spuSaleAttr.spuSaleAttrValueList" :key="index">{{ spuSaleAttrValue.saleAttrValueName }}</dd>
              </dl>
            </div>


    methods: {
      changeActive(saleAttrValue, arr){
        arr.forEach(function (item, index) {
          //遍历全部售卖属性值isChecked为零没有高亮了
          item.isChecked = 0
        })
        //点击的那个售卖属性值变为1
        saleAttrValue.isChecked = 1
        console.log(saleAttrValue.saleAttrValueName)
      },
    },

在这里插入图片描述

069-073 加入购物车

API接口说明

在这里插入图片描述
api/index.js

//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
直接发请求并路由跳转(没必要vuex储存数据)

在这里插入图片描述
Detail/index.vue调用api后,根据返回状态直接进行路由跳转,不用去vuex三连。(下面那个params参数skuId只是试验我会传参,是要删掉的)

              <div class="add">
                <a href="javascript:" @click="addShopcar(goodInfo.skuInfo.id,skuNum)">加入购物车</a>
              </div>
              
<script>
methods: {
      async addShopcar(skuId,skuNum){
        let result = await reqAddOrUpdateShopCart(skuId,skuNum)
        if (result.code == 200){
          this.$router.push({name:'AddCartSuccess',params:{'skuId':skuId},query:{'skuNum':skuNum}})
          console.log(this.$route)
        }else{
          alert('error')
        }
      }
     }
</script>

router/routes.js设置params参数占位(记得删掉占位skuId)

		{
			path:'/AddCartSuccess/:skuId?',
			name:'AddCartSuccess',
			component:AddCartSuccess,
			meta:{
				showFooter: true,
			}
		},
query无法带走所有参数,需使用本地存储

当我们想要实现两个毫无关系的组件传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。

Detail/index.vue将数据存储在本地。

      async addShopcar(skuId,skuNum){
        //直接发请求
        let result = await reqAddOrUpdateShopCart(skuId,skuNum)
        console.log('result',result)
        if (result.code == 200){
          // 本地存储
          sessionStorage.setItem('GOODINFO',JSON.stringify(this.goodInfo))
          // 路由跳转
          this.$router.push({name:'AddCartSuccess',query:{'skuNum':skuNum}})
          // console.log(this.$route)
        }else{
          alert('error')
        }
      }

AddCartSuccess/index.vue读取本地存储并展现。

<template>
  <div class="cart-complete-wrap">
    <div class="cart-complete">
      <h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3>
      <div class="goods">
        <div class="left-good">
          <div class="left-pic">
            <img src="good.skuDefaultImg">
          </div>
          <div class="right-info">
            <p class="title">{{ goodInfo.skuInfo.skuName }}</p>
            <p class="attr">{{ goodInfo.skuInfo.skuDesc }} 数量:{{ $route.query.skuNum }}</p>
          </div>
        </div>
        <div class="right-gocart">
          <a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a>
          <a href="javascript:" >去购物车结算 > </a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'AddCartSuccess',
    computed:{
      goodInfo(){
        console.log(11111,JSON.parse(sessionStorage.getItem('GOODINFO')))
        return JSON.parse(sessionStorage.getItem('GOODINFO'))
      }
    }
  }
</script>

在这里插入图片描述

回退查看查看商品详情 和

AddCartSuccess/index.vue

          <a href="javascript:" class="sui-btn btn-xlarge" @click="goBack">查看商品详情</a>

也可以这样写

          <router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link>
跳转购物车结算

AddCartSuccess/index.vue

          <router-link href="javascript:" to="/ShopCart">去购物车结算 > </router-link>

在这里插入图片描述

073-075 购物车之一:获取购物车列表

API接口

在这里插入图片描述

如果没有uuidToken,返回数据为空

但是如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。
在这里插入图片描述

加入uuidToken

创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储。
utils/uuid_token.js

import {v4 as uuidv4} from 'uuid'
//生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
export const getUUID = () => {
    //1、判断本地存储是否由uuid
    let uuid_token = localStorage.getItem('UUIDTOKEN')
    //2、本地存储没有uuid
    if(!uuid_token){
        //2.1生成uuid
        uuid_token = uuidv4()
        //2.2存储本地
        localStorage.setItem("UUIDTOKEN",uuid_token)
    }
    //当用户有uuid时就不会再生成
    return uuid_token
}

用户的uuid_token定义在store中的detail模块

const state =  {
    goodInfo:{},
    //游客身份
    uuid_token: getUUID()
}

在request.js中设置请求头

import store from '@/store';
requests.interceptors.request.use(config => {
    //config内主要是对请求头Header配置

    //1、先判断uuid_token是否为空
    if(store.state.detail.uuid_token){
        //2、userTempId字段和后端统一
        config.headers['userTempId'] = store.state.detail.uuid_token
    }
    //比如添加token

    //开启进度条
    nprogress.start();
    return config;
})

注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from ‘@/store’;

在这里插入图片描述

购物车商品信息展示

在这里插入图片描述

ShopCart/index.vue动态展示数据,增加小计和总价。

<template>
  <div class="cart">
    <h4>全部商品</h4>
    <div class="cart-main">
      <div class="cart-th">
        <div class="cart-th1">全部</div>
        <div class="cart-th2">商品</div>
        <div class="cart-th3">单价(元)</div>
        <div class="cart-th4">数量</div>
        <div class="cart-th5">小计(元)</div>
        <div class="cart-th6">操作</div>
      </div>
      <div class="cart-body">
        <ul class="cart-list" v-for="(cartInfo,index) in cartList[0].cartInfoList" :key="cartInfo.id">
          <li class="cart-list-con1">
            <input type="checkbox" name="chk_list">
          </li>
          <li class="cart-list-con2">
            <img :src="cartInfo.imgUrl">
            <div class="item-msg">{{ cartInfo.skuName }}</div>
          </li>
          <li class="cart-list-con4">
            <span class="price">{{ cartInfo.skuPrice }}</span>
          </li>
          <li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins">-</a>
            <input autocomplete="off" type="text" :value="cartInfo.skuNum" minnum="1" class="itxt">
            <a href="javascript:void(0)" class="plus">+</a>
          </li>
          <li class="cart-list-con6">
            <span class="sum">{{ cartInfo.skuNum * cartInfo.skuPrice }}</span>
          </li>
          <li class="cart-list-con7">
            <a href="#none" class="sindelet">删除</a>
            <br>
            <a href="#none">移到收藏</a>
          </li>
        </ul>

      </div>
    </div>
    <div class="cart-tool">
      <div class="select-all">
        <input class="chooseAll" type="checkbox">
        <span>全选</span>
      </div>
      <div class="option">
        <a href="#none">删除选中的商品</a>
        <a href="#none">移到我的关注</a>
        <a href="#none">清除下柜商品</a>
      </div>
      <div class="money-box">
        <div class="chosed">已选择
          <span>0</span>件商品</div>
        <div class="sumprice">
          <em>总价(不含运费) :</em>
          <i class="summoney">{{ totalPrice }}</i>
        </div>
        <div class="sumbtn">
          <a class="sum-btn" href="###" target="_blank">结算</a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import { mapState } from 'vuex';

  export default {
    name: 'ShopCart',
    mounted() {
      this.getData()
    },
    methods: {
      getData(){
        this.$store.dispatch('getCartList')
      },
    },
    computed:{
      ...mapState({
        cartList: (state) => state.shop.cartList,
      }),
      totalPrice(){
        let sum = 0
        this.cartList[0].cartInfoList.forEach(element => {
          sum += element.skuNum * element.skuPrice
        });
        return sum
      }
    },
  }
</script>

调整产品选中状态(every函数使用)

every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
例如判断底部勾选框是否全部勾选代码部分

//判断底部勾选框是否全部勾选
      isAllCheck() {
        //every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
        return this.cartInfoList.every(item => item.isChecked === 1)
      }

      <div class="select-all">
        <input class="chooseAll" type="checkbox" :checked="this.isAllCheck">
        <span>全选</span>
      </div>

在这里插入图片描述

076-077 购物车之二:购物车的商品数量变动

复用加入购物车的API

重点是,API商品数量是变动值(+100),不就是最终数量(105)。
在这里插入图片描述
api/index.js

//加入购物车 /api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`,method:"post"});
用handler函数处理数量变动(直接发请求,不用vuex)

ShopCart/index.vue注意看校对输入值部分。handler函数有三个参数,type区分操作,disNum用于表示数量变化(正负),cart商品的信息。

          <li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cartInfo)">-</a>
            <input autocomplete="off" type="text" :value="cartInfo.skuNum" minnum="1" class="itxt" @change="handler('change',$event.target.value*1,cartInfo)">
            <a href="javascript:void(0)" class="plus" @click="handler('add',1,cartInfo)">+</a>
          </li>
      async handler(type,disNum,cartInfo){
        if(type == 'minus'){
          cartInfo.disNum > 0 ? disNum = -1 : disNum = 0
        }else if(type == 'add'){
          disNum = 1
        }else{
          // 检验disNum输入值是否合法
          (isNaN(disNum) || disNum < 1)? disNum = 0 : disNum = parseInt(disNum) - cartInfo.skuNum
        }
        
        //直接发请求
        let result = await reqAddOrUpdateShopCart(cartInfo.skuId,disNum)
        console.log('result',result)
        if (result.code == 200){
          this.getData()
        }else{
          alert('error')
        }
    }

修改产品数据时,实时发请求。
在这里插入图片描述
在这里插入图片描述

加入节流throttle

ShopCart/index.vue

import throttle from 'lodash/throttle';

methods: {
      handler:throttle(async function(type,disNum,cartInfo){
          if(type == 'minus'){
            cartInfo.disNum > 0 ? disNum = -1 : disNum = 0
          }else if(type == 'add'){
            disNum = 1
          }else{
            // 检验disNum输入值是否合法
            (isNaN(disNum) || disNum < 1)? disNum = 0 : disNum = parseInt(disNum) - cartInfo.skuNum
          }
          
          //直接发请求
          let result = await reqAddOrUpdateShopCart(cartInfo.skuId,disNum)
          console.log('result',result)
          if (result.code == 200){
            this.getData()
          }else{
            alert('error')
          }
        },1000)
 }

078 购物车之三:删除购物车的商品

删除购物车的商品的API

重点是:delete请求
在这里插入图片描述
api/index.js

//删除购物车单个商品 /api/cart/deleteCart/{skuId}
export const reqDeleteCartById = (skuId)=>requests({url:`/cart/deleteCart/${skuId}`,method:"DELETE"});
用函数处理删除购物车的商品(有bug)

ShopCart/index.vue

<template>
          <li class="cart-list-con7">
            <a href="#none" class="sindelet" @click="deleteCartById(cartInfo.skuId)">删除</a>
            <br>
            <a href="#none">移到收藏</a>
          </li>
</template>

<script>
  import { reqAddOrUpdateShopCart,reqDeleteCartById } from '@/api/index'
      methods: {
          //删除购物车商品
        async deleteCartById(skuId){
          console.log('cartInfo',skuId)
          let result = await reqDeleteCartById(skuId)
          console.log('result',result)
          result.code == 200? this.getData():alert('error')
        }
     }
</script>

在这里插入图片描述
因网速原因,删除可能不及时。

用函数处理删除购物车的商品(没有bug)

链接
办法就是去掉[0],并使用默认值。
ShopCart/index.vue

<script>
  import { mapState,mapGetters } from 'vuex';
  import { reqAddOrUpdateShopCart,reqDeleteCartById,reqUpdateCheckedByid } from '@/api/index'
  import throttle from 'lodash/throttle';
  
  export default {
    computed:{
      ...mapGetters(['cartList']),
      cartInfoList(){
        console.log('cartInfoList',this.cartList.cartInfoList)
        return this.cartList.cartInfoList || []
      },
    },
  }
</script>

shop.js

import { reqCartList } from "@/api";

//home模块的仓库
const state = {
  //仓库初始状态
  cartList:[],
};

//mutions是唯一修改state的地方
const mutations = {
  GETCARTLIST(state, cartList) {
        state.cartList = cartList;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCartList({ commit }) {
        let result = await reqCartList();
        if (result.code == 200) {
          commit("GETCARTLIST", result.data);
        }
      },
};


// 计算属性
const getters = {
  cartList(state) {
    return state.cartList[0] || {}
  },
};


export default {
  state,
  mutations,
  actions,
  getters,
};

在这里插入图片描述

079 购物车之四:修改购物车的商品状态

修改购物车的商品的API

重点是:其实不太懂为什么商品状态还有分选中不选中?
在这里插入图片描述
api/index.js

//修改购物车商品选中状态 /api/cart/checkCart/{skuID}/{isChecked}
export const reqUpdateCheckedByid = (skuId,isChecked)=>requests({url:`/cart/checkCart/${skuId}/${isChecked}`,method:"get"});
用函数处理产品状态的变更

ShopCart/index.vue将event事件中的值传给方法。

<template>
          <li class="cart-list-con1">
            <input type="checkbox" name="chk_list" :checked="cartInfo.isChecked" @change="updateChecked(cartInfo,$event)">
          </li>
</template>

<script>
  import { reqUpdateCheckedByid } from '@/api/index'
      methods: {
        //修改某个产品的勾选状态
        async updateChecked(cartInfo,e){
          let isChecked = e.target.checked? '1':'0'
          console.log('isChecked',isChecked)
          let result = await reqUpdateCheckedByid(cartInfo.skuId,isChecked)
          console.log('result',result)
          result.code == 200? this.getData():alert('error')
        }
     }
</script>

在这里插入图片描述

081 购物车之五:删除全部选中商品

删除购物车的商品的API【复用删除单个商品的API】
删除全部选中商品(有bug)

ShopCart/index.vue循环删除单个商品。this.getData()要放在循环外面。

<template>
	<a href="#none" class="sindelet" @click="deleteCartById(cartInfo.skuId)">删除</a>
</template>

<script>
  import { reqDeleteCartById } from '@/api/index'
      methods: {
        //删除全部选中商品
        deleteAllCheckedCart(cartInfoList){
          cartInfoList.forEach(async function(element){
            if (element.isChecked == '1'){
              // console.log('isChecked',element.isChecked)
              let result = await reqDeleteCartById(element.skuId)
              console.log('result',result)
              }
            }
          )
          //再发请求获取购物车列表,需要放在for循环外面
          this.getData()
        },
     }
</script>

点击【删除选中的商品】
在这里插入图片描述
在这里插入图片描述
因网速原因,删除可能不及时。

bug:多于2个可正常删除后,剩下最后一栏无法删除会出现bug。

![![在这里插入图片描述](https://img-blog.csdnimg.cn/9653ab556b324e6bb8b327f8b83456b8.png)
在这里插入图片描述

删除全部选中商品(没有bug)

办法就是使用mapGetter去掉[0],并使用默认值。
ShopCart/index.vue

 <ul class="cart-list" v-for="(cartInfo,index) in cartInfoList" :key="cartInfo.id">

<script>
  import { mapState,mapGetters } from 'vuex';
  import { reqAddOrUpdateShopCart,reqDeleteCartById,reqUpdateCheckedByid } from '@/api/index'
  import throttle from 'lodash/throttle';
  
  export default {
    methods: {
      getData(){
        this.$store.dispatch('getCartList')        
      },
      //选中全部商品
      async updateAllCartChecked(e,cartInfoList){
        let isChecked = e.target.checked? '1':'0'
        cartInfoList.forEach(async function(element){
          let result = await reqUpdateCheckedByid(element.skuId,isChecked)
          console.log('updateChecked',result)
        })
        console.log('this',this)
        this.getData()
      },
    },

    computed:{
      ...mapGetters(['cartList']),
      cartInfoList(){
        console.log('cartInfoList',this.cartList.cartInfoList)
        return this.cartList.cartInfoList || []
      },
    },
  }
</script>

shop.js

import { reqCartList } from "@/api";

//home模块的仓库
const state = {
  //仓库初始状态
  cartList:[],
};

//mutions是唯一修改state的地方
const mutations = {
  GETCARTLIST(state, cartList) {
        state.cartList = cartList;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCartList({ commit }) {
        let result = await reqCartList();
        if (result.code == 200) {
          commit("GETCARTLIST", result.data);
        }
      },
};


// 计算属性
const getters = {
  cartList(state) {
    return state.cartList[0] || {}
  },
};


export default {
  state,
  mutations,
  actions,
  getters,
};

在这里插入图片描述

082 购物车之六:全部产品的勾选状态修改

全部产品的勾选状态修改的API【复用勾选单个商品的API】
全部产品的勾选状态修改(有bug)
      <div class="select-all">
        <input class="chooseAll" type="checkbox" :checked="this.isAllCheck"  @click="updateAllCartChecked($event,cartList[0].cartInfoList)">
        <span>全选</span>
      </div>

      //选中全部商品
      async updateAllCartChecked(e,cartInfoList){
        let isChecked = e.target.checked? '1':'0'
        cartInfoList.forEach(async function(element){
          let result = await reqUpdateCheckedByid(element.skuId,isChecked)
          console.log('updateChecked',result)
          
          if (result.code == 200) {
            return "ok";
          } else {
            return Promise.reject(new Error("faile"));
          }
        })
        console.log('this',this)
        this.getData()
      },

在这里插入图片描述

全部产品的勾选状态修改(没有bug)

方法就是写在computed里面,不要写在methods里面。

      <div class="select-all">
        <input class="chooseAll" type="checkbox" 
        :checked="isAllCheck && cartInfoList.length>0"  
        @click="updateAllCartChecked($event,cartInfoList)">
        <span>全选</span>
      </div>

    computed:{
      //判断底部复选框是否勾选【全部产品都选中,采勾选】
      isAllCheck() {
        //遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true
        //只要有一个不是1======>假false
        let result = this.cartInfoList.every((item) => item.isChecked == 1)
        console.log('isAllCheck',result)
        return result
      },

在这里插入图片描述

ES6 const新用法

const {comment,index,deleteComment} = this 

上面的这句话是一个简写,最终的含义相当于:

const  comment = this.comment
const  index = this.index
const   deleteComment = this.deleteComment

083-089 登录和注册

083-085 注册

静态页面

在这里插入图片描述
在这里插入图片描述
Register/index.vue

<template>
  <div class="register-container">
    <!-- 注册内容 -->
    <div class="register">
      <h3>注册新用户
        <span class="go">我有账号,去 <a href="login.html" target="_blank">登陆</a>
        </span>
      </h3>
      <div class="content">
        <label>手机号:</label>
        <input type="text" placeholder="请输入你的手机号">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input type="text" placeholder="请输入验证码">
        <img ref="code" src="http://182.92.128.115/api/user/passport/code" alt="code">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input type="text" placeholder="请输入你的登录密码">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="text" placeholder="请输入确认密码">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="controls">
        <input name="m1" type="checkbox">
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="btn">
        <button>完成注册</button>
      </div>
    </div>

    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6</div>
      <div class="beian">ICP19006430</div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'Register'
  }
</script>

<style lang="less" scoped>
  .register-container {
    .register {
      width: 1200px;
      height: 445px;
      border: 1px solid rgb(223, 223, 223);
      margin: 0 auto;

      h3 {
        background: #ececec;
        margin: 0;
        padding: 6px 15px;
        color: #333;
        border-bottom: 1px solid #dfdfdf;
        font-size: 20.04px;
        line-height: 30.06px;

        span {
          font-size: 14px;
          float: right;

          a {
            color: #e1251b;
          }
        }
      }

      div:nth-of-type(1) {
        margin-top: 40px;
      }

      .content {
        padding-left: 390px;
        margin-bottom: 18px;
        position: relative;

        label {
          font-size: 14px;
          width: 96px;
          text-align: right;
          display: inline-block;
        }

        input {
          width: 270px;
          height: 38px;
          padding-left: 8px;
          box-sizing: border-box;
          margin-left: 5px;
          outline: none;
          border: 1px solid #999;
        }

        img {
          vertical-align: sub;
        }

        .error-msg {
          position: absolute;
          top: 100%;
          left: 495px;
          color: red;
        }
      }

      .controls {
        text-align: center;
        position: relative;

        input {
          vertical-align: middle;
        }

        .error-msg {
          position: absolute;
          top: 100%;
          left: 495px;
          color: red;
        }
      }

      .btn {
        text-align: center;
        line-height: 36px;
        margin: 17px 0 0 55px;

        button {
          outline: none;
          width: 270px;
          height: 36px;
          background: #e1251b;
          color: #fff !important;
          display: inline-block;
          font-size: 16px;
        }
      }
    }

    .copyright {
      width: 1200px;
      margin: 0 auto;
      text-align: center;
      line-height: 24px;

      ul {
        li {
          display: inline-block;
          border-right: 1px solid #e4e4e4;
          padding: 0 20px;
          margin: 15px 0;
        }
      }
    }
  }
</style>

Login/index.vue

<template>
  <div class="login-container">
    <!-- 登录 -->
    <div class="login-wrap">
      <div class="login">
        <div class="loginform">
          <ul class="tab clearFix">
            <li>
              <a href="##" style="border-right: 0;">扫描登录</a>
            </li>
            <li>
              <a href="##" class="current">账户登录</a>
            </li>
          </ul>

          <div class="content">
            <form action="##">
              <div class="input-text clearFix">
                <span></span>
                <input type="text" placeholder="邮箱/用户名/手机号">
              </div>
              <div class="input-text clearFix">
                <span class="pwd"></span>
                <input type="text" placeholder="请输入密码">
              </div>
              <div class="setting clearFix">
                <label class="checkbox inline">
                  <input name="m1" type="checkbox" value="2" checked="">
                  自动登录
                </label>
                <span class="forget">忘记密码?</span>
              </div>
              <button class="btn">&nbsp;&nbsp;</button>
            </form>

            <div class="call clearFix">
              <ul>
                <li><img src="./images/qq.png" alt=""></li>
                <li><img src="./images/sina.png" alt=""></li>
                <li><img src="./images/ali.png" alt=""></li>
                <li><img src="./images/weixin.png" alt=""></li>
              </ul>
              <router-link class="register" to="/register">立即注册</router-link>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6</div>
      <div class="beian">ICP19006430</div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'Login',
  }
</script>

<style lang="less" scoped>
  .login-container {
    .login-wrap {
      height: 487px;
      background-color: #e93854;

      .login {
        width: 1200px;
        height: 487px;
        margin: 0 auto;
        background: url(./images/loginbg.png) no-repeat;
      }

      .loginform {
        width: 420px;
        height: 406px;
        box-sizing: border-box;
        background: #fff;
        float: right;
        top: 45px;
        position: relative;
        padding: 20px;

        .tab {

          li {
            width: 50%;
            float: left;
            text-align: center;

            a {
              width: 100%;
              display: block;
              height: 50px;
              line-height: 50px;
              font-size: 20px;
              font-weight: 700;
              color: #333;
              border: 1px solid #ddd;
              box-sizing: border-box;
              text-decoration: none;

            }

            .current {
              border-bottom: none;
              border-top-color: #28a3ef;
              color: #e1251b;
            }
          }
        }

        .content {
          width: 380px;
          height: 316px;
          box-sizing: border-box;
          border: 1px solid #ddd;
          border-top: none;
          padding: 18px;

          form {
            margin: 15px 0 18px 0;
            font-size: 12px;
            line-height: 18px;

            .input-text {
              margin-bottom: 16px;

              span {
                float: left;
                width: 37px;
                height: 32px;
                border: 1px solid #ccc;
                background: url(../../assets/images/icons.png) no-repeat -10px -201px;
                box-sizing: border-box;
                border-radius: 2px 0 0 2px;
              }

              .pwd {
                background-position: -72px -201px;
              }

              input {
                width: 302px;
                height: 32px;
                box-sizing: border-box;
                border: 1px solid #ccc;
                border-left: none;
                float: left;
                padding-top: 6px;
                padding-bottom: 6px;
                font-size: 14px;
                line-height: 22px;
                padding-right: 8px;
                padding-left: 8px;

                border-radius: 0 2px 2px 0;
                outline: none;
              }
            }

            .setting {
              label {
                float: left;
              }

              .forget {
                float: right;
              }
            }

            .btn {
              background-color: #e1251b;
              padding: 6px;
              border-radius: 0;
              font-size: 16px;
              font-family: 微软雅黑;
              word-spacing: 4px;
              border: 1px solid #e1251b;
              color: #fff;
              width: 100%;
              height: 36px;
              margin-top: 25px;
              outline: none;
            }
          }

          .call {
            margin-top: 30px;

            ul {
              float: left;

              li {
                float: left;
                margin-right: 5px;
              }
            }

            .register {
              float: right;
              font-size: 15px;
              line-height: 38px;
            }

            .register:hover {
              color: #4cb9fc;
              text-decoration: underline;
            }
          }

        }
      }
    }

    .copyright {
      width: 1200px;
      margin: 0 auto;
      text-align: center;
      line-height: 24px;

      ul {
        li {
          display: inline-block;
          border-right: 1px solid #e4e4e4;
          padding: 0 20px;
          margin: 15px 0;
        }
      }
    }

  }
</style>
API:获取验证码、注册用户

在这里插入图片描述
在这里插入图片描述
api/index.js

//获取注册验证码 /api/user/passport/sendCode/{phone}
export const reqGetCode = (phone)=>requests({url:`/user/passport/sendCode/${phone}`,method:"get"});

//获取注册验证码 /api/user/passport/register
export const reqUserRegister = ({phone,password,code})=>requests(
    {url:`user/passport/register`,
    method:"post",
    data:{phone,password,code}}
    );

函数处理:获取验证码、注册用户(关键点在于参数的格式{phone,password,code})

Register/index.vue

<template>
  <div class="register-container">
    <!-- 注册内容 -->
    <div class="register">
      <h3>注册新用户
        <span class="go">我有账号,去 <a href="login.html" target="_blank">登陆</a>
        </span>
      </h3>
      <div class="content">
        <label>手机号:</label>
        <input type="text" placeholder="请输入你的手机号" v-model="phone">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input type="text" placeholder="请输入验证码" v-model="code">
        <button style="width:100px;height:38px" @click="getCode">
          获取验证码
        </button>
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input type="text" placeholder="请输入你的登录密码"  v-model="password">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="text" placeholder="请输入确认密码"  v-model="password1">
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="controls">
        <input name="m1" type="checkbox"  v-model="agree">
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="btn">
        <button @click="UserRegister">完成注册</button>
      </div>
    </div>

    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6</div>
      <div class="beian">ICP19006430</div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'Register',
    data() {
      return {
        phone:'',
        code:'',
        password:'',
        password1:'',
        agree:true,
      }
    },
    methods: {
      //获取验证码
      async getCode(){
        const { phone } = this;
        phone && (await this.$store.dispatch('getCode',phone));
        this.code = this.$store.state.user.code;
      },
      //注册用户
      async UserRegister(){
        const { phone,password,password1,code,agree } = this;
        if ( phone && password && password1==password && code && agree) {
          let result = await this.$store.dispatch('UserRegister',{phone,password,code})
          console.log('UserRegister result',result)
          if(result){
          //注册成功进行路由的跳转
          this.$router.push("/login");
          }
        }
      }
    },
  }
</script>

在这里插入图片描述
store/user.js

import { reqGetCode,reqUserRegister } from "@/api";

//home模块的仓库
const state = {
  phone:'',
  code: '',
  password:'',
  agree:'',
};

//mutions是唯一修改state的地方
const mutations = {
    GETCODE(state, code) {
        state.code = code;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCode({ commit },phone) {
        let result = await reqGetCode(phone);
        if (result.code == 200) {
          commit("GETCODE", result.data);
        }
      },
    async UserRegister({ commit },{phone,password,code}) {
        let result = await reqUserRegister({phone,password,code});
        console.log('vuex UserRegister',result)
        if (result.code == 200) {
          return "ok";
        }
      },
};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

在这里插入图片描述

注册功能就返回ok,并跳转到登录页面
在这里插入图片描述

086-088 登录

API接口:登录

在这里插入图片描述

api/index.js

//用户登录 /api/user/passport/login
export const reqUserLogin = ({phone,password})=>requests(
    {url:`user/passport/login`,
    method:"post",
    data:{phone,password}}
    );
函数处理:用户登录(登录成功后跳转home页)

Login/index.vue关键点在于参数{phone,password}

<template>
  <div class="login-container">
    <!-- 登录 -->
    <div class="login-wrap">
      <div class="login">
        <div class="loginform">
          <ul class="tab clearFix">
            <li>
              <a href="##" style="border-right: 0;">扫描登录</a>
            </li>
            <li>
              <a href="##" class="current">账户登录</a>
            </li>
          </ul>

          <div class="content">
            <form action="##">
              <div class="input-text clearFix">
                <span></span>
                <input type="text" placeholder="邮箱/用户名/手机号" v-model="phone">
              </div>
              <div class="input-text clearFix">
                <span class="pwd"></span>
                <input type="text" placeholder="请输入密码"  v-model="password">
              </div>
              <div class="setting clearFix">
                <label class="checkbox inline">
                  <input name="m1" type="checkbox" value="2" checked="">
                  自动登录
                </label>
                <span class="forget">忘记密码?</span>
              </div>
              <button class="btn" @click="UserLogin">&nbsp;&nbsp;</button>
            </form>

            <div class="call clearFix">
              <ul>
                <li><img src="./images/qq.png" alt=""></li>
                <li><img src="./images/sina.png" alt=""></li>
                <li><img src="./images/ali.png" alt=""></li>
                <li><img src="./images/weixin.png" alt=""></li>
              </ul>
              <router-link class="register" to="/register">立即注册</router-link>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6</div>
      <div class="beian">ICP19006430</div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'Login',
    data() {
      return {
        phone:'',
        password:'',
        nickName:'',
        name:'',
        token:'',
      }
    },
    methods: {
      //用户登录
      async UserLogin(){
        const { phone,password } = this;
        if ( phone && password) {
          console.log('phone',phone)
          let result = await this.$store.dispatch('UserLogin',{phone,password})
          console.log('result',result)
          if(result){
          //注册成功进行路由的跳转
          this.$router.push("/home");
          }
        }
      }
    },
  }
</script>

store/user.js

import { reqGetCode,reqUserRegister,reqUserLogin } from "@/api";

//home模块的仓库
const state = {
  phone:'',
  code: '',
  password:'',
  agree:'',
  nickName:'',
  name:'',
  token:'',
};

//mutions是唯一修改state的地方
const mutations = {
    GETCODE(state, code) {
        state.code = code;
      },
    USERLOGIN(state, data) {
        state.nickName = data.nickName;
        state.name = data.name;
        state.token = data.token;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCode({ commit },phone) {
        let result = await reqGetCode(phone);
        if (result.code == 200) {
          commit("GETCODE", result.data);
        }
      },
    async UserRegister({ commit },{phone,password,code}) {
        let result = await reqUserRegister({phone,password,code});
        console.log('vuex UserRegister',result)
        if (result.code == 200) {
          return "ok";
        }
      },
    async UserLogin({ commit },{phone,password}) {
        let result = await reqUserLogin({phone,password});
        console.log('vuex UserLogin',result)
        if (result.code == 200) {
          commit("USERLOGIN", result.data);
          return "ok";
        }
      },

};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

登录成功的截图
在这里插入图片描述

API接口:获取用户登录信息

在这里插入图片描述
api/index.js

//获取用户信息【需要带着用户的token向服务器要用户信息】
//URL:/api/user/passport/auth/getUserInfo  method:get 
export const reqUserInfo = ()=>requests({url:'/user/passport/auth/getUserInfo',method:'get'});
获取用户登录信息

store/user.js储存数据(UserLogin之setToken(result.data.token), getUserInfo获取用户信息)

import { reqGetCode,reqUserRegister,reqUserLogin,reqUserInfo } from "@/api";
import {setToken,getToken,removeToken } from "@/utils/token"

//仓库
const state = {
  code: '',
  token: getToken(),
  userInfo: {},
};

//mutions是唯一修改state的地方
const mutations = {
    GETCODE(state, code) {
        state.code = code;
      },
    USERLOGIN(state, token) {
        state.token = token;
      },
    GETUSERINFO(state, userInfo) {
        state.userInfo = userInfo;
      },
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {
    async getCode({ commit },phone) {
        let result = await reqGetCode(phone);
        if (result.code == 200) {
          commit("GETCODE", result.data);
        }
      },
    async UserRegister({ commit },{phone,password,code}) {
        let result = await reqUserRegister({phone,password,code});
        console.log('vuex UserRegister',result)
        if (result.code == 200) {
          return "ok";
        }
      },
    async UserLogin({ commit },{phone,password}) {
        let result = await reqUserLogin({phone,password});
        console.log('vuex UserLogin',result)
        if (result.code == 200) {
          commit("USERLOGIN", result.data.token);
          //持久化存储token
          setToken(result.data.token);
          return "ok";
        }
      },
    //获取用户信息
    async getUserInfo({ commit }) {
      let result = await reqUserInfo();
      console.log('getUserInfo',result)
      if (result.code == 200) {
        //提交用户信息
        commit("GETUSERINFO", result.data);
        return 'ok';
      }else{
        return Promise.reject(new Error('faile'));
      }
    },
      // 退出登录
    // async userLogout({commit}) {
    //   //只是向服务器发起一次请求,通知服务器清除token
    //   let result = await reqLogout();
    //   //action里面不能操作state,提交mutation修改state
    //   if(result.code==200){
    //     commit("CLEAR");
    //     return 'ok';
    //   }else{
    //     return Promise.reject(new Error('faile'));
    //   }
    // },
};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

utils/tokens.js定义token的存储、获取、消除方法

//存储token
export const setToken = (token) => {
  localStorage.setItem("TOKEN", token);
};
//获取token
export const getToken = () => {
  return localStorage.getItem("TOKEN");
};

//清除本地存储的token
export const removeToken=()=>{
   localStorage.removeItem("TOKEN");
}

发请求,读取数据
Home/index.vue在mounted时自动读取用户信息。

<template>
    <div>
      <!--  三级联动全局组件已经注册为全局组件,因此不需要引入-->
      <TypeNav/>
      <ListContainer/>
      <Recommend/>
      <Rank/>
      <Like/>
      <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
      <Brand/>
    </div>
</template>

<script>
import ListContainer from '@/views/Home/ListContainer'
import Recommend from '@/views/Home/Recommend'
import Rank from '@/views/Home/Rank'
import Like from '@/views/Home/Like'
import Floor from '@/views/Home/Floor'
import Brand from '@/views/Home/Brand'
import { mapState } from 'vuex'

export default {
  name: 'Home',
  components:{
    ListContainer,
    Recommend,
    Rank,
    Like,
    Floor,
    Brand,
  },
  mounted(){
    //派发action,获取floor组件的数据
    this.$store.dispatch('getFloorList');
    this.$store.dispatch('getUserInfo');
  },
  computed: {
    ...mapState({
      floorList: (state) => state.home.floorList,
    }),
  }
}
</script>

Header展示数据:userName()

<template>
        <header class="header">
            <!-- 头部的第一行 -->
                        <!-- 没有用户名:未登录 -->
                        <p v-if="!userName">
                            <span></span>
                            <router-link to="/login">登录</router-link>
                            <router-link class="register" to="/register">免费注册</router-link>
                        </p>
                        <!-- 已登录 -->
                        <p v-else>
                            <a>{{ userName }}</a>
                            <a>退出登录</a>
                        </p>
                    </div>
        </header>
</template>

<script>
export default {
  name: 'Header',
  data() {
    return {
        keyword:'',
    }
  },
  methods:{
    goSearch(){
	  },
  mounted() {
  },
  computed:{
    userName(){
        return this.$store.state.user.userInfo.name
    }
  }
}
</script>

request.js带请求头token,否则请求失败。

    //需要携带token带给服务器
    if(store.state.user.token){
        config.headers.token = store.state.user.token;
    }
效果

未带请求头token,发生208错误
在这里插入图片描述
带请求头token,用户验证成功
在这里插入图片描述
在这里插入图片描述

bug:跳出了home组件就会失去登录信息

解法1:将读取用户信息放在公共组件
App.vue在mounted时读取用户信息

  mounted(){
    this.$store.dispatch('getUserInfo');
  },

解法2:路由守卫

089 退出登录

API:退出登录

在这里插入图片描述
api/index.js

//退出登录
//URL:/api/user/passport/logout  get
export const reqLogout = ()=> requests({url:'/user/passport/logout',method:'get'});
函数实现退出登录

1.帮仓库中先关用户信息清空
2.本地存储数据清空

utils/tokens.js定义token的存储、获取、消除方法

//存储token
export const setToken = (token) => {
  localStorage.setItem("TOKEN", token);
};
//获取token
export const getToken = () => {
  return localStorage.getItem("TOKEN");
};

//清除本地存储的token
export const removeToken=()=>{
   localStorage.removeItem("TOKEN");
}

vuex三连

import { reqGetCode,reqUserRegister,reqUserLogin,reqUserInfo,reqLogout } from "@/api";
import {setToken,getToken,removeToken } from "@/utils/token"

//仓库
const state = {
  code: '',
  token: getToken(),
  userInfo: {},
};

//mutions是唯一修改state的地方
const mutations = {

    //清除本地数据
    CLEAR(state){
      //帮仓库中先关用户信息清空
      state.token = '';
      state.userInfo={};
      //本地存储数据清空
      removeToken();
    }
};

//action|用户处理派发action地方的,可以书写异步语句、自己逻辑地方
const actions = {

    //获取用户信息
    async getUserInfo({ commit }) {
      let result = await reqUserInfo();
      console.log('getUserInfo',result)
      if (result.code == 200) {
        //提交用户信息
        commit("GETUSERINFO", result.data);
        return 'ok';
      }else{
        return Promise.reject(new Error('faile'));
      }
    },
    // 退出登录
    async userLogout({commit}) {
      //只是向服务器发起一次请求,通知服务器清除token
      let result = await reqLogout();
      //action里面不能操作state,提交mutation修改state
      if(result.code==200){
        commit("CLEAR");
        return 'ok';
      }else{
        return Promise.reject(new Error('faile'));
      }
    },
};


//计算属性
const getters = {};


export default {
  state,
  mutations,
  actions,
  getters,
};

Header组件定义退出方法logout()

<template>
        <header class="header">
            <!-- 头部的第一行 -->
            <div class="top">
                <div class="container">
                    <div class="loginList">
                        <p>尚品汇欢迎您!</p>
                        <!-- 没有用户名:未登录 -->
                        <p v-if="!userName">
                            <span></span>
                            <router-link to="/login">登录</router-link>
                            <router-link class="register" to="/register">免费注册</router-link>
                        </p>
                        <!-- 已登录 -->
                        <p v-else>
                            <a>{{ userName }}</a>
                            <a @click="logout">退出登录</a>
                        </p>
                    </div>
                    <div class="typeList">
                        <a href="###">我的订单</a>
                        <a href="###">我的购物车</a>
                        <a href="###">我的尚品汇</a>
                        <a href="###">尚品汇会员</a>
                        <a href="###">企业采购</a>
                        <a href="###">关注尚品汇</a>
                        <a href="###">合作招商</a>
                        <a href="###">商家后台</a>
                    </div>
                </div>
            </div>
            <!--头部第二行 搜索区域-->
            <div class="bottom">
                <h1 class="logoArea">
                    <router-link class="logo" title="尚品汇" to="/">
                        <img src="./images/logo.png" alt="">
                    </router-link>
                </h1>
                <div class="searchArea">
                    <form action="###" class="searchForm">
                        <input type="text" id="autocomplete" class="input-error input-xxlarge" v-model="keyword"/>
                        <button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button>
                    </form>
                </div>
            </div>
        </header>
</template>

<script>
export default {
  name: 'Header',
  data() {
    return {
        keyword:'',
    }
  },
  methods:{
    goSearch(){
        //治标不治本
        // this.$router.push(`/search/${this.keyword}`,()=>{},()=>{})

        if (this.$route.query) {
          let location = {
            name: "search",
            params:{keyword:this.keyword || undefined}
        }
          //动态给location配置对象添加query属性
          location.query = this.$route.query;
          //路由跳转
          this.$router.push(location);
        }
    },
    //退出登录
    async logout(){
      //退出登录需要做的事情
      //1:需要发请求,通知服务器退出登录【清除一些数据:token】
      //2:清除项目当中的数据【userInfo、token】
        try {
          //如果退出成功
          await this.$store.dispatch('userLogout');
          //回到首页
          this.$router.push('/home');
        } catch (error) {
        }
    }
  },
  mounted() {
    //通过全局事件总线清除关键字
    this.$bus.$on("clear", () => {
      this.keyword = "";
    });
  },
  computed:{
    userName(){
        return this.$store.state.user.userInfo.name
    }
  }
}
</script>
效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176790.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通达信插件获取并存储通达信商品指数的实时数据

一、引子 通达信商品指数一共有23个&#xff0c;如下图所示&#xff1a; 如果想获取历史数据&#xff0c;只需要通过通达信的数据下载和导出功能即可&#xff0c;现在我们需要获取这23个指数的实时数据&#xff0c;通过导出功能就没有办法了。 在最初的阶段&#xff0c;考虑的…

微服务自动化管理【IDEA使用Docker插件进行一键部署】

本章目标 IDEA使用Docker插件实现springboot项目的一键部署 要开两个虚拟机 server registry server上进行操作 Docker开启远程api端口(注意这种配置方式只适用于开发和学习&#xff0c;在公共网络中不要这样配置&#xff0c;容易引发安全问题) 默认情况下dokcer是不支持远程…

linux基本功系列之find命令实战

文章目录前言&#x1f680;&#x1f680;&#x1f680;一. find命令介绍二. find常用参数及语法格式三. 示范案例3.1 查找符合文件名规则的文件3.2 根据文件类型类查找文件3.3 按照更改时间或访问时间等查找文件3.4 查找并执行相应的命令3.5 按照文件大小来查找3.6 按照文件所有…

启动hive报错no hbase in

启动hive报错no hbase in 将hdfs和yarn都启动成功之后&#xff0c;启动hive&#xff0c;如下所示&#xff1a;[atguiguhadoop102 conf]$ cd /opt/module/hive/ [atguiguhadoop102 hive]$ bin/hive报错信息如下which: no hbase in (/usr/local/bin:/usr/bin:/usr/local/sbin:/us…

C++ · 类和对象 · 02 | 类的6个默认成员函数

啊我摔倒了..有没有人扶我起来学习.... &#x1f471;个人主页&#xff1a;《CGod的个人主页》\color{Darkorange}{《CGod的个人主页》}《CGod的个人主页》交个朋友叭~ &#x1f492;个人社区&#xff1a;《编程成神技术交流社区》\color{Darkorange}{《编程成神技术交流社区》…

【操作系统】—— 如何安装双系统与多系统(带你快速了解)

&#x1f4dc; “作者 久绊A” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴。 &#x1f341; 操作系统【带你快速了解】对于电脑来说&#xff0c;如果说…

博客系统项目的自动化测试

作者&#xff1a;~小明学编程 文章专栏&#xff1a;测试开发 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 博客界面 测试用例 界面测试 功能测试 性能测试 兼容性测试 易用性测试 安全性测试 ​编辑自动化测试 登录界面的测试 界面文字模块 测…

ubuntu使用教程与常用命令

ubuntu使用教程 一、 Ubuntu简介 Ubuntu&#xff08;乌班图&#xff09;是一个基于Debian的以桌面应用为主的Linux操作系统&#xff0c;据说其名称来自非洲南部祖鲁语或科萨语的“ubuntu”一词&#xff0c;意思是“人性”、“我的存在是因为大家的存在”&#xff0c;是非洲传…

Python爬虫之Scrapy框架系列(5)——项目实战【某瓣Top250电影所有信息的txt文本存储】

上篇文章已经成功解析提取到豆瓣Top250电影想要的所有数据。下一步就是将其交给管道进行存储。 目录&#xff1a;1. 编写items.py文件&#xff08;定义结构化数据字段&#xff09;2. 爬虫文件里将数据一一对应字段名&#xff1a;3. 将数据返回给管道&#xff1a;4. 编写pipelin…

MCAL系列介绍04-ICU

本文框架1. 前言2. 基本概念3. ICU采集过程3.1 获取周期占空比3.2 获取边沿数量3.3 获取时间戳4. Autosar系列文章快速链接1. 前言 ICU驱动器是使用Input Capture Uint模块&#xff08;ICU&#xff09;解调PWM信号、计数脉冲、测量频率和占空比、生成简单中断以及唤醒中断的模…

树莓派配置Python虚拟环境、安装PyQt5、安装PySide2

要从头设置好一台可用于开发的树莓派&#xff0c;可以参考树莓派 4B 无屏幕&#xff0c;连接WiFi、SSH、VNC&#xff0c;系统换源、pip换源&#xff0c;安装中文输入法 Python虚拟环境 树莓派&#xff08;或者说arm平台&#xff09;使用Python虚拟环境的正确方式是使用pipenv…

【手写 Vue2.x 源码】第三十九篇 - 组件部分 - 创建组件虚拟节点

一&#xff0c;前言 上篇&#xff0c;介绍了组件部分-组件的合并&#xff0c;主要涉及以下几个点&#xff1a; 组件初始化情况&#xff1b;组件合并的位置&#xff1b;组件合并的策略&#xff1b;组件合并后测试&#xff1b; 本篇&#xff0c;组件部分-组件的编译&#xff1…

【C语言】对<进阶版三子棋>的完善和改进

这篇文章主要是对前面三子棋游戏的完善和改进。 文章目录 目录 1.将棋子*和#&#xff0c;改为1和0&#xff1b; 2.电脑下棋显示坐标 3.可以选择电脑先手或玩家先手 4.在退出游戏时显示游戏信息 5.完善后的游戏效果 二、完整程序代码 1.game.h 2.test.c 3.game.c 总结 前言 h…

【教程】虚拟环境与Pytorch安装

【教程】虚拟环境与Pytorch安装NVIDIA驱动安装虚拟环境创建激活/删除相关库的安装Pytorch安装安装地址可能遇到的问题处理报错安装卡顿测试是否安装完成参考NVIDIA驱动安装 NVIDIA驱动可在官网进行安装&#xff1a;NVIDIA驱动官网 命令行输入nvidia-smi可查看cuda版本等信息&…

Linux常用命令——ss命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) ss 比 netstat 好用的socket统计信息&#xff0c;iproute2 包附带的另一个工具&#xff0c;允许你查询 socket 的有关统计信息。 补充说明 ss命令用来显示处于活动状态的套接字信息。ss命令可以用来获取socket…

XGBoost的原理、工程实现与优缺点

Xgboost简介 XGBoost是陈天奇等人开发的一个开源机器学习项目&#xff0c;高效地实现了GBDT算法并进行了算法和工程上的许多改进&#xff0c;被广泛应用在Kaggle竞赛及其他许多机器学习竞赛中并取得了不错的成绩。XGBoost本质上还是一个GBDT&#xff0c;但是力争把速度和效率发…

C++普通类,派生类,虚基类的成员构造顺序以及构造函数调用顺序详解

目录前言普通类构造析构顺序解析依赖关系产生的错误派生类构造析构顺序解析扩展菱形多继承场景含虚基类的派生类构造析构顺序解析扩展菱形多继承场景(引入虚继承)前言 C规定“对象的析构过程必须与其构造过程相反”这一语法规则。 因此我们研究透彻了构造过程&#xff0c;那么…

宕机了,Redis如何避免数据丢失?

今天是大年初一&#xff0c;祝大家新年快乐&#xff0c;新的一年技术增进&#xff0c;工资翻倍。 目前&#xff0c;Redis的持久化主要有两大机制&#xff0c;即AOF日志和RDB快照&#xff0c;在接下来的两节课里&#xff0c;我们就分别学习一下吧。 AOF日志是如何实现的&#…

结构型模式-装饰器模式

1.概述 快餐店有炒面、炒饭这些快餐&#xff0c;可以额外附加鸡蛋、火腿、培根这些配菜&#xff0c;当然加配菜需要额外加钱&#xff0c;每个配菜的价钱通常不太一样&#xff0c;那么计算总价就会显得比较麻烦。 使用继承的方式存在的问题&#xff1a; 扩展性不好 如果要再加…

Alibaba微服务组件Sentinel学习笔记

1 .Sentinel 是什么 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件&#xff0c;主要以 流量为切入点&#xff0c;从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的…