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

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>
效果

在这里插入图片描述

112-尚品汇-尚硅谷-nginx反向代理

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

我的实践(windows)
1.打包 npm run build

在这里插入图片描述

2.下载nginx

https://blog.csdn.net/GyaoG/article/details/124081770

3.拷贝打包文件到nginx

在这里插入图片描述

4.修改nginx配置nginx.conf

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       5005;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   demo/static;
            index  index.html index.htm;
        }

        location /api {
            proxy_pass   http://gmall-h5-api.atguigu.cn;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

5.关闭并重新打nginx

https://blog.csdn.net/weixin_42116703/article/details/127482331
在这里插入图片描述

6.效果

在这里插入图片描述

我的实践(ubuntu)
1.打包 npm run build

在这里插入图片描述

2.下载nginx

sudo apt-get install nginx

3.拷贝打包文件到虚拟机,不用在nginx文件夹中

在这里插入图片描述

4.sudo vim修改nginx配置default

1.sudo vim修改nginx配置default

season@ZHS-190213650:/$ ls
bin   dev  home  lib    lib64   lost+found  mnt  proc  run   snap  sys  usr
boot  etc  init  lib32  libx32  media       opt  root  sbin  srv   tmp  var
season@ZHS-190213650:/$ cd etc
season@ZHS-190213650:/etc$ cd nginx
season@ZHS-190213650:/etc/nginx$ ls
conf.d        fastcgi_params  koi-win     modules-available  nginx.conf    scgi_params      sites-enabled  uwsgi_params
fastcgi.conf  koi-utf         mime.types  modules-enabled    proxy_params  sites-available  snippets       win-utf
season@ZHS-190213650:/etc/nginx$ cd sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim
[sudo] password for season:
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ ls
default  sites-available
season@ZHS-190213650:/etc/nginx/sites-available$ sudo vim default

2.Vim指令https://blog.csdn.net/dotdotyy/article/details/120108308
若权限不足,则用sudo vim打开default。
在这里插入图片描述

3.default配置(个人觉得配置用windows那一版也可以)

server {
	listen 5005  default_server;
	#listen [::]:80 default_server;

	# SSL configuration
	#
	# listen 443 ssl default_server;
	# listen [::]:443 ssl default_server;
	#
	# Note: You should disable gzip for SSL traffic.
	# See: https://bugs.debian.org/773332
	#
	# Read up on ssl_ciphers to ensure a secure configuration.
	# See: https://bugs.debian.org/765782
	#
	# Self signed certs generated by the ssl-cert package
	# Don't use them in a production server!
	#
	# include snippets/snakeoil.conf;

	#root /var/www/html;
	root /home/season/myseason/myfile/dist/static;

	# Add index.php to the list if you are using PHP
	index index.html index.htm index.nginx-debian.html;

	server_name _;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}
        
        location /api {
                proxy_pass http://gmall-h5-api.atguigu.cn;
        }


	# pass PHP scripts to FastCGI server
	#
	#location ~ \.php$ {
	#	include snippets/fastcgi-php.conf;
	#
	#	# With php-fpm (or other unix sockets):
	#	fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
	#	# With php-cgi (or other tcp sockets):
	#	fastcgi_pass 127.0.0.1:9000;
	#}

	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	#
	#location ~ /\.ht {
	#	deny all;
	#}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
#	listen 80;
#	listen [::]:80;
#
#	server_name example.com;
#
#	root /var/www/example.com;
#	index index.html;
#
#	location / {
#		try_files $uri $uri/ =404;
#	}
#}

5.关闭并重新打nginx

sudo service nginx restart

6.效果(有bug)

很多时候导航列表无法带出,有时候可以带出。不知道bug在哪?
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值