尚品汇项目笔记

尚品汇项目笔记

项目初始化

创建一个空文件 project-SHP

  • 然后打开命令行窗口 vue create app

在这里插入图片描述

  • 选第一个

在这里插入图片描述
然后就开始下载了

  • 可以设置接下来浏览器自动打开

    在package.json文件中

"scripts": {
 "serve": "vue-cli-service serve --open",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
},

在这里插入图片描述

关闭eslint校验工具

创建vue.config.js文件:需要对外暴露

module.exports = {
   lintOnSave:false,
}

src文件夹的别名的设置

因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件

{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@/*": [
                "src/*"
            ]
        }
    },
    "exclude": [
        "node_modules",
        "dist"
    ]
}

项目主要界面搭建

创建Header、Footer 组件

辉洪老师的静态页面

在这里插入图片描述

引入静态页面,引入样式,引入图片静态资源,在APP.vue上注册使用

在这里插入图片描述
项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法

安装less less-loader@5

切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,报的错误setOption函数未定义
在这里插入图片描述

  • 2:需要在style标签的身上加上lang=“less”,不添加样式不生效

安装 vue-router

在这里插入图片描述

路由组件的搭建

创建router文件夹,里面创建index.js

// 配置路由的地方
import Vue from 'vue';
import VueRouter from 'vue-router';

// 使用插件
Vue.use(VueRouter)

// 引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'


// 配置路由
export default new VueRouter({
    routes:[
        {
            path: "/home",
            component: Home,
        },
        {
            path: "/search",
            component: Search,
        },
 
    ]
})

然后去main.js 里面注册

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 引入路由
import router from '@/router'

new Vue({
  render: h => h(App),
  // 注册路由
  // 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
  router
}).$mount('#app')

路由组件总结

路由组件一般需要在router文件夹进行注册(使用的是组件的名字)
非路由组件在使用时,一般是以标签的形式使用

$route: 一般获取路由信息【路径、query、params等等】
$router: 一般进行编程式导航进行路由跳转
在这里插入图片描述

配置路由元信息,控制组件的显示与隐藏

设置 meta 元信息
在这里插入图片描述
根据$route获取当前路由信息
在这里插入图片描述

路由跳转(params参数、query参数)

路由的跳转就两种形式:

  1. 声明式导航 (router-link:务必要有to属性)
  2. 编程式导航 $router.push||replace方法

编程式导航更好用:因为可以书写自己的业务逻辑

路由传递参数的三种方式

      // 路由传递参数
      // 第一种:字符串形式
      // this.$router.push('/search/' + this.keyword + '?k=' + this.keyword.toUpperCase());
      
      // 第二种:模板字符串形式
      // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`);
      
      // 第三种:对象
      this.$router.push(
        {
          name: "search",
          params: { 
            keyword: this.keyword
            },
          query: {
            k:this.keyword.toUpperCase()
            },
        }
      );

params参数:路由需要占位,程序就崩了,属于URL当中一部分
在这里插入图片描述

在这里插入图片描述
如果用对象形式传params参数,需要给路由命名,如下所示:
在这里插入图片描述
在这里插入图片描述

query参数:路由不需要占位,写法类似于ajax当中query参数
类似于 ?k=v&k2=v2&k3=v3
/home?name=lsh&id=666&sex=1
在这里插入图片描述

        {
            path: "/search/:keyword",
            name: 'search',
            component: Search,
            meta: {
                show: true
            },
        },

路由传递参数先关面试题
1:路由传递参数(对象写法)path是否可以结合params参数一起使用?
不可以:不能这样书写,程序会崩掉
2:如何指定params参数可传可不传?
3:params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
4:如果指定name与params配置, 但params中数据是一个"", 无法跳转,路径会出问题
5: 路由组件能不能传递props数据?

多次执行相同的push问题

多次执行相同的push问题,控制台会出现警告
例如:使用this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}})时,如果多次执行相同的push,控制台会出现警告。

let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)

执行一次上面代码:
在这里插入图片描述
多次执行出现警告:

在这里插入图片描述
原因:push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
方法:this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。
这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
push是VueRouter.prototype的一个方法,在router中的index重写该方法即可(看不懂也没关系,这是前端面试题)

//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
VueRouter.prototype.push = function (location,resolve,reject){
    if(resolve && reject){
        originPush.call(this,location,resolve,reject)
    }else{
        originPush.call(this,location,() => {},() => {})
    }
}

定义全局组件

我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置

//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);

在Home组件中使用该全局组件

<template>
<div>
<!--  三级联动全局组件已经注册为全局组件,因此不需要引入-->
  <TypeNav/>
</div>
</template>

全局组件可以在任一页面中直接使用,不需要导入声明
下面全部商品分类就是三级联动组件

继续开发Home首页

Home首页其它组件

home文件夹index.vue

<template>
<div>
<!--  三级联动全局组件已经注册为全局组件,因此不需要引入-->
  <TypeNav/>
<!--  轮播图列表-->
  <ListContainer/>
<!--  今日推荐-->
  <Recommend/>
<!--  商品排行-->
  <Rank/>
<!--  猜你喜欢-->
  <Like/>
<!-- 楼层 -->
  <Floor/>
  <Floor/>
<!--  商标-->
  <Brand/>
</div>
</template>

<script>
import ListContainer from './ListContainer'
import Recommend from './Recommend'
import Rank from './Rank'
import Like from './Like'
import Floor from './Floor'
import Brand from './Brand'
export default {
  name: "index",
  components: {
    ListContainer,
    Recommend,
    Rank,
    Like,
    Floor,
    Brand,
  }
}
</script>

<style scoped>

</style>

封装axios

首先安装axios

 cnpm i --save axios

在根目录下创建api文件夹,创建request.js文件。

// 对axios进行二次封装
import axios from "axios";

// 1、利用axios对象的方法create,去创建一个axios实例
///2、request就是axios,只不过稍微配置一下
const request = axios.create({
    // 配置对象
    // 基础路径,发请求时,路径会出现api
    baseURL: "/api",
    // 代表请求超时时间5s
    timeput: 5000,

});

// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
    // config:配置对象,对象里面有一个属性很重要,headers请求头
    return config;
});

// 响应拦截器
request.interceptors.response.use((res) => {
    // 成功的回调函数:服务器响应数据回来之后,响应拦截器可以做一些事情
    return res.data;
}, (error) => {
    // 响应失败的回调函数
    return Promise.reject(new Error('faile'));
});




// 对外暴露
export default request;

前端通过代理解决跨域问题

在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。

module.exports = {
  productionSourceMap:false,
  // 关闭ESLINT校验工具
  lintOnSave: false,
  //配置代理跨域
  devServer: {
    proxy: {
      "/api": {
        target: "http://39.98.123.211",
      },
    },
  },
};

nprogress进度条的使用

安装进度条

cnpm i --save nprogress

在发起请求时,开启进度条,请求成功后,关闭进度条
所以在request.js进行配置

  • 先引入进度条,引入进度条样式
  • 请求拦截器里开启进度条,响应拦截器里的成功回调函数里关闭进度条
// 对axios进行二次封装
import axios from "axios";

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

// 1、利用axios对象的方法create,去创建一个axios实例
///2、request就是axios,只不过稍微配置一下
const request = axios.create({
    // 配置对象
    // 基础路径,发请求时,路径会出现api
    baseURL: "/api",
    // 代表请求超时时间5s
    timeput: 5000,

});

// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
    // 进度条开始
    nprogress.start(); 

    // config:配置对象,对象里面有一个属性很重要,headers请求头
    return config;
});

// 响应拦截器
request.interceptors.response.use((res) => {
    // 成功的回调函数:服务器响应数据回来之后,响应拦截器可以做一些事情
    // 进度条结束
    nprogress.done(); 
    return res.data;
}, (error) => {
    // 响应失败的回调函数
    return Promise.reject(new Error('faile'));
});




// 对外暴露
export default request;

也可以修改进度条的样式
在这里插入图片描述

使用Vuex

首先,先下载Vuex

cnpm i --save vuex

如果想要使用vuex,还要再main.js中引入
main.js:
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性

// 引入仓库
import store from '@/store'

new Vue({
  render: h => h(App),
  // 注册路由
  // 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
  router,
  // 注册仓库:组件实例身上会多一个属性$store属性
  store,
}).$mount('#app')

根目录创建store文件夹,文件夹下创建index.js
建立多个小仓库,比如home、search小仓库,然后大仓库引入小仓库

大仓库如下

import Vue from 'vue';
import Vuex from 'vuex';

// 需要使用插件一次
Vue.use(Vuex);

// 引入小仓库
import home from './home';
import search from './search';


// 对外暴露Store类的一个实例
export default new Vuex.Store({
    moudles: {
        home,
        search
    }
});

小仓库如下
home

// home模块小仓库

// state:仓库存储数据的地方
const state = {
    a:1
};
// mutations:修改states的唯一手段
const mutations = {};
// actions:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};
// getters理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,

}

search

// search模块小仓库

const state = {
    b:2
};
const mutations = {};
const actions = {};
const getters = {};

export default {
    state,
    mutations,
    actions,
    getters,

}

Vuex使用小案例

Vuex工作原理图
在这里插入图片描述

发请求到Actions

页面加载时,通过Vuex发请求,获取数据

  mounted() {
    // 派发action || 获取商品分类的三级列表的数据
    this.$store.dispatch("categoryList");
  },

在这里插入图片描述Actions调用 commit 方法提交到Mutations

首先发 ajax 请求得到需要的数据

将commit结构赋值出来,调用commit数据提交到Mutations
在这里插入图片描述

在Mutations里面修改State里面的值
下图的 categoryList 就是上图的 result.data
在这里插入图片描述
State里面应该设置好初始值,数据类型应该与实际的数据类型保持一致

设置初始值categoryList
在这里插入图片描述
最后在相应的页面上借用辅助函数MapState来获取

import { mapState } from "vuex";

  computed: {
    ...mapState({
      categoryList: (state) => state.home.categoryList.slice(0, 16),
    }),
  },

在这里插入图片描述

我们可以在TypeNav里面看到 categoryList 这个数据
在这里插入图片描述

通过JS控制二三级分类显示与隐藏(函数节流)

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

函数防抖和函数节流

正常情况下:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)
在这里插入图片描述
防抖:前面的所有触发都被取消,最后一次执行在规定时间之后才会触发, 就是说如果连续快速的触发,只会执行最后一次

节流:在规定间隔时间内不会重复触发回调,只有大于这个时间间隔才会触发,把频繁触发变为少量触发

loadsh插件防抖和节流

不需要安装,有些依赖需要loadsh,所以之前就已经引入loadsh了

下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。

//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

  methods: {
    // 鼠标进入修改currentIndex属性
    changeIndex: throttle(function (index) {
      this.currentIndex = index;
    },50),
    leaveIndex() {
      this.currentIndex = -1;
    },
  },

三级联动路由跳转与传递参数

三级联动用户可以点击:一级分类、二级分类、三级分类,当你点击的时候,Home模块跳转到Search模块,同时会把你选中的产品(产品名字、ID)传递过去
在这里插入图片描述

路由跳转:
声明式导航: router-link
编程式导航:push || replace

三级联动: 如果使用声明式导航,可以实现跳转和参数传递,但是会出现卡顿现象。

router-link:router-link 是一个组件,当服务器返回数据之后,会循环出很多个router-link组件【需要创建组件实例,还要讲虚拟DOM转化成真实DOM】

创建组件实例的时候,一瞬间创建1000+需要很多内存的,因此出现卡顿现象。

所以最好利用编程式导航进行跳转
在这里插入图片描述

直接写在a标签里面不好,因为a标签是在v-for里面的,这样会有很多个a标签,很多个goSearch回调函数,所以使用事件委派,将goSearch写在a标签的父元素里面。这样只需要一个回调函数goSearch就可以解决。

在这里插入图片描述
所以最后是利用 编程式导航+事件委派 进行跳转

事件委派带来的问题:
1、点击的是a标签时,才可以跳转,如何确保是a标签
2、如何获取a标签的商品名称、商品Id

解决方法:
1、给a标签添加自定义属性data-categoryName,标识a标签
1、分别给a标签添加自定义属性data-category1Id、data-category2Id、data-category3Id,获取到不一样的商品Id,用于路由跳转。

我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。

    // 进行路由跳转
    goSearch(event) {
      // 编程式导航 + 事件委派
      // 利用事件委派存在一些问题:
      // 1:点击的一定是a标签,才进行跳转
      // 2.如何区分一级、二级、三级分类的标签

      // 第一个问题:把子节点中的a标签,加上自定义属性data-categoryName,其余节点没有
      let element = event.target;
      console.log(element);
      // 获取到当前触发这个事件的节点【h3、a、dt、dl】带有自定义属性data-categoryName就是a标签
      let { categoryname, category1id, category2id, category3id } = element.dataset;
       //当前这个if语句:一定是a标签才会进入
      if (categoryname) {
        let location = { name: 'search'};
        let query = { categoryName: categoryname};
        if (category1id) {
          query.category1Id = category1id;
        } else if (category2id) {
          query.category2Id = category2id;
        } else {
          query.category3Id = category3id;
        }
        // 整理完参数
        location.query = query;
        // 路由跳转
        this.$router.push(location);
      }
    },

注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。

<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){
      console.log(event.target)
    }

swiper插件实现轮播图

做一个简要的使用总结:

(1)安装swiper
在这里插入图片描述

(2)在需要使用轮播图的组件内导入swpier和它的css样式
引入swiper
在这里插入图片描述
引入样式,因为其他地方也需要这些样式,所以就全局引入
在这里插入图片描述

(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)

        <!--banner轮播-->
        <div class="swiper-container" id="mySwiper">
          <div class="swiper-wrapper">
            <div
              class="swiper-slide"
              v-for="(carousel, index) in bannerList"
              :key="carousel.id"
            >
              <img :src="carousel.imgUrl" />
            </div>
          </div>
          <!-- 如果需要分页器 -->
          <div class="swiper-pagination"></div>

          <!-- 如果需要导航按钮 -->
          <div class="swiper-button-prev"></div>
          <div class="swiper-button-next"></div>
        </div>

(4)创建swiper实例

接下来就是考虑在哪里创建swiper实例

直接在mounted里面创建是失败的

mounted() {
	//请求数据
    this.$store.dispatch("getBannerList")
    //创建swiper实例
    let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
        pagination:{
          el: '.swiper-pagination',
          clickable: true,
        },
        // 如果需要前进后退按钮
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        },
        // 如果需要滚动条
        scrollbar: {
          el: '.swiper-scrollbar',
        },
      })
  },

因为mounted里面先去异步请求了轮播图数据,然后再去创建swiper实例,由于请求数据是异步的,所以数据还没请求回来,swiper实例就创建好了,所以就导致轮播图展示失败了

解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:

mounted() {
    this.$store.dispatch("getBannerList")
    setTimeout(()=>{
      let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
        pagination:{
          el: '.swiper-pagination',
          clickable: true,
        },
        // 如果需要前进后退按钮
        navigation: {
          nextEl: '.swiper-button-next',
          prevEl: '.swiper-button-prev',
        },
        // 如果需要滚动条
        scrollbar: {
          el: '.swiper-scrollbar',
        },
      })
    },1000)
  },

上面的方法实现了功能,但不是最完美的

解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象

watch:{
    bannerList(newValue,oldValue){
        let mySwiper = new Swiper(this.$refs.cur,{
          pagination:{
            el: '.swiper-pagination',
            clickable: true,
          },
          // 如果需要前进后退按钮
          navigation: {
            nextEl: '.swiper-button-next',
            prevEl: '.swiper-button-prev',
          },
          // 如果需要滚动条
          scrollbar: {
            el: '.swiper-scrollbar',
          },
        })
    }
  }

但是还是不能实现轮播图的功能。因为我们的watch只能保证bannerList有数据,但是不能保证此时v-for已经执行完了,v-for执行也需要时间。假如watch监听到bannerList有数据变化了,执行回调函数创建swiper实例,之后v-for才执行,这样是无法渲染轮播图的

完美解决方案:使用watch+this.$nextTick()

this.$nextTick()解析:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。

  watch:{
    bannerList(newValue,oldValue){
        //this.$nextTick()使用
        this.$nextTick(()=>{
          let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
            pagination:{
              el: '.swiper-pagination',
              clickable: true,
            },
            // 如果需要前进后退按钮
            navigation: {
              nextEl: '.swiper-button-next',
              prevEl: '.swiper-button-prev',
            },
            // 如果需要滚动条
            scrollbar: {
              el: '.swiper-scrollbar',
            },
          })
        })
    }
  }

完善floor组件(父子组件通信)

  • 1、获取floor组件数据

先在 api/index.js 准备好接口

// 获取floor数据
export const reqFloorList = () => mockRequests.get("/floor");

在 store/home/index.js 准备好Vuex三连环
在这里插入图片描述
在 pages/Home/index.vue 触发actions,然后v-for循环Floor组件

<!-- 楼层 -->
  <Floor v-for="(floor,index) in floorList" :key="floor.id"/>


  mounted() {
    this.$store.dispatch("getFloorList")
  },
  computed:{
    ...mapState({
      floorList:state => state.home.floorList
    })
  }

获取到数据之后,将数据从index.vue传到Floor组件(父传子)

  • 父组件中
<!-- 楼层 -->
  <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
  • Floor组件中
  name: "floor",
  props: ["list"],

然后就是将对应的数据绑定到对应的位置上(只展示一部分的)

      <div class="title clearfix">
        <h3 class="fl">{{ list.name }}</h3>
        <div class="fr">
          <ul class="nav-tabs clearfix">
            <li
              class="active"
              v-for="(nav, index) in list.navList"
              :key="index"
            >
              <a href="#tab1" data-toggle="tab">{{nav.text}}</a>
            </li>
          </ul>
        </div>
      </div>

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

在这里插入图片描述
因为很多地方都用到轮播图,因此我们直接将它提取为公共组件

<template>
  <div class="swiper-container" ref="cur">
    <div class="swiper-wrapper">
      <div
        class="swiper-slide"
        v-for="(carousel, index) in list"
        :key="carousel.id"
      >
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>

<script>
//引入Swiper
import Swiper from "swiper";
export default {
  name: "Carousel",
  props: ["list"],
  watch: {
    list: {
      //立即监听:不管你数据有没有变化,我上来立即监听一次
      //为什么watch监听不大list:因为这个数据从来没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true,
            // 如果需要分页器
            pagination: {
              el: ".swiper-pagination",
              //点击小球的时候也切换图片
              clickable: true,
            },
            // 如果需要前进后退按钮
            navigation: {
              nextEl: ".swiper-button-next",
              prevEl: ".swiper-button-prev",
            },
          });
        });
      },
    },
  },
};
</script>

<style scoped></style>

然后在 main.js 中注册为公共组件


import Carousel from '@/components/Carousel';
//第一个参数:全局组件名字,第二个参数:全局组件

Vue.component(Carousel.name,Carousel);

然后再需要轮播图的地方使用组件

    <!-- 轮播图的地方 -->
    <Carousel :list="list.carouselList"/>

开发 Search 模块

Vuex获取Search模块数据

静态界面直接使用资料中准备好的文件,不再一一拆分了
在这里插入图片描述
然后使用Vuex发请求获取数据

import { reqGetSearchInfo } from "@/api";

// search模块小仓库

const state = {
    //仓库初始状态
    searchList: {}
};
const mutations = {
    GETSEARCHLIST(state, searchList) {
        state.searchList = searchList;
    },
};
const actions = {
    //获取search模块数据
    async getSearchList({ commit }, params = {}) {
        //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
        //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
        let result = await reqGetSearchInfo(params);
        if (result.code == 200) {
            commit("GETSEARCHLIST", result.data);
        }
    },
};
    mounted() {
      this.$store.dispatch('getSearchList', {});
    },

getters使用

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

注意:仓库中的getters是全局属性,是不分模块的。

//计算属性
//项目当中getters主要的作用是:简化仓库中的数据(简化数据而生)
//可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
const getters = {
    goodsList(state) {
        return state.searchList.goodsList || [];
    },

    trademarkList(state) {
        return state.searchList.trademarkList || [];
    },

    attrsList(state) {
        return state.searchList.attrsList || [];
    },
};

在Search组件中使用getters获取仓库数据

import { mapGetters } from "vuex";

  computed: {
    ...mapGetters(["goodsList"]),

  },

利用watch监听路由信息变化实现动态搜索

如果在每个三级分类列表和搜索按钮加一个点击按钮事件,只要点击了就执行搜索函数
但是这样子做会生成很多回调函数,很消耗性能。
最好解决方法:用watch监听路由信息变化
我们每次进行新的搜索时,我们的query和params参数中的部分内容会发生变化,而且这两个参数都是路由的属性,所以可以通过监听路由信息变化来动态发起搜索请求。

在这里插入图片描述
search组件watch部分代码。

  // 数据监听:监听组件实例身上的属性的属性值是否变化
  watch: {
    $route(newValue, oldValue) {
      // 再次处理请求参数
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
      console.log(this.searchParams);
      this.getData();

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

面包屑处理分类(处理query参数,地址栏的处理)

  • 删除分类的名字

在这里插入图片描述
在点击删除分类时,我们需要categoryName和 category3Id(或者是category1Id、category2Id)删除掉,但是params中的keyword参数(华为)不需要删除。

所以我们需要把 categoryName 、category3Id、category1Id 、category2Id 赋值为 undefined ;接着我们再次发请求更新页面上的数据

这个时候我们点击分类,页面上的数据的确发生变化了,但是地址栏上的内容并没有变化,事实上页面上地址栏同样也需要改变,而且路径中的params不应该删除,路由跳转的时候应该带着

我们应该重新跳转当前页面,并携带params参数

     <!-- 分类的面包屑 -->
     <li class="with-x" v-if="searchParams.categoryName">
       {{ searchParams.categoryName
       }}<i @click="removeCategoryName">×</i>
     </li>


    // 删除分类的名字
    removeCategoryName() {
      // 带给服务器的参数变为undefined,参数就不会传递给服务器
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      this.getData();

      // 地址栏也需要更改
      // 注意:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
      if (this.$route.params) {
        this.$router.push({ name: "search", params: this.$route.params });
      }
    },

最终的效果是这样的(对比一下上图就可以看出区别了
在这里插入图片描述

面包屑处理关键字(处理params参数,兄弟组件通信)

这一步实质就是在处理params参数,与处理query参数类似


   <!-- 关键字的面包屑 -->
   <li class="with-x" v-if="searchParams.keyword">
     {{ searchParams.keyword }}<i @click="removeKeyword">×</i>
   </li>


    // 删除关键字
    removeKeyword() {
      // 带给服务器的参数变为undefined,参数就不会传递给服务器
      this.searchParams.keyword = undefined;
      this.getData();

      // 通知兄弟组件Header清除关键字

      this.$bus.$emit("clear");

      //进行路由的跳转
      if (this.$route.query) {
        this.$router.push({ name: "search", query: this.$route.query });
      }
    },

在这里插入图片描述

唯一不同的就是,这一步需要去清除输入框中的关键字

输入框是在Header组件中的

在这里插入图片描述
Header和Search是兄弟组件,所以需要兄弟组件通信才可以完成该功能

这里通过$bus实现header和search组件的通信

全局事件总线$bus的使用

(1)在main.js中注册

new Vue({
  render: h => h(App),
  // 全局事件总线$bus配置
  beforeCreate() {
    Vue.prototype.$bus = this;
  },
  // 注册路由
  // 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
  router,
  // 注册仓库:组件实例身上会多一个属性$store属性
  store,
}).$mount('#app')

(2)Search组件中使用$bus通信。第一个参数可以理解为通信的暗号第二个参数可以传递数据。这里只是通知header组件进行相应操作,因此不需要第二个参数。

    // 删除关键字
    removeKeyword() {
      // 带给服务器的参数变为undefined,参数就不会传递给服务器
      this.searchParams.keyword = undefined;
      this.getData();

      // 通知兄弟组件Header清除关键字
      this.$bus.$emit("clear");

      //进行路由的跳转
      if (this.$route.query) {
        this.$router.push({ name: "search", query: this.$route.query });
      }
    },

(3)header组件接受$bus通信

注意:组件挂载时就监听clear事件

  mounted() {
    this.$bus.$on("clear", () => {
      this.keyword = '';
    });
  },

面包屑处理品牌信息(子传父)

在这里插入图片描述

此处生成面包屑时会涉及到子组件向父组件传递信息操作,之后的操作和前面讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。

首先在子组件中定义点击品牌的处理函数,函数体里面触发子组件的自定义事件、传递数据


    <!-- 品牌地方 -->
    <ul class="logo-list">
      <li
        v-for="(trademark, index) in trademarkList"
        :key="trademark.tmId"
        @click="tradeMarkHandler(trademark)"
      >
        {{ trademark.tmName }}
      </li>
    </ul>



    // 品牌处理函数
    tradeMarkHandler(trademark) {
      //点击了品牌(苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示
      //老师问题:在那个组件中发请求,父组件?
      //为什么那,因为父组件中searchParams参数是带给服务器参数,子组件组件把你点击的品牌的信息,需要给父组件传递过去---自定义事件
      this.$emit('tradeMarkInfo', trademark);
    },

自定义事件的回调函数跟自定义事件的名字一样

  • 回调函数里面处理请求参数,重新发请求
        <!--selector-->
        <SearchSelector @tradeMarkInfo="tradeMarkInfo" />


    // 自定义事件回调
    tradeMarkInfo(trademark) {
      //1:整理品牌字段的参数  "ID:品牌名称"
      this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;

      this.getData();
    },

品牌面包屑的展示与删除

  • 删除品牌时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。只需要将tademark置空就行
     <!-- 品牌的面包屑 -->
     <li class="with-x" v-if="searchParams.trademark">
       {{ searchParams.trademark.split(":")[1]
       }}<i @click="removeTradeMark">×</i>
     </li>

    // 删除品牌
    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(index) {
      this.searchParams.props.splice(index, 1);
      //再次发请求
      this.getData();
    },

商品排序(计算属性)

排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。

order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。

升降序是通过箭头图标来辨别的,如图所示:

在这里插入图片描述
图标是iconfont网站的图标,通过引入在线css的方式引入图标
在这里插入图片描述
在public文件index引入该css

<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">

在search模块使用该图标

这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码

              <!-- 排序结构 -->
              <ul class="sui-nav">
              <!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码-->
                <li :class="{ active: isOne }" @click="changeOrder('1')">
                  <a
                    >综合<span
                      v-show="isOne"
                      class="iconfont"
                      :class="{ 'icon-up': isAsc, 'icon-down': isDesc }"
                    ></span
                  ></a>
                </li>
                <li :class="{ active: isTwo }" @click="changeOrder('2')">
                  <a
                    >价格<span
                      v-show="isTwo"
                      class="iconfont"
                      :class="{ 'icon-up': isAsc, 'icon-down': isDesc }"
                    ></span
                  ></a>
                </li>
              </ul>

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

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

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

    // 排序操作
    changeOrder(flag) {
      //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)
      console.log(flag);
      //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】
      let originOrder = this.searchParams.order;
      let originFlag = this.searchParams.order.split(":")[0];
      let originSort = this.searchParams.order.split(":")[1];

      console.log(originFlag, originSort);
      //新的排序方式
      let newOrder = "";
      //判断的是多次点击的是不是同一个按钮
      if (originFlag == flag) {
        newOrder = `${originFlag}:${originSort == "desc" ? "asc" : "desc"}`;
      } else {
        //点击不是同一个按钮
        newOrder = `${flag}:${"desc"}`;
      }
      this.searchParams.order = newOrder;

      //再次发请求
      this.getData();
    },

手写分页器(v-for动态展示分页器)

一、传递需要的数据

分页器需要哪些数据

  • pageNo 当前页
  • pageSize 每一页展示多少数据
  • total 一共多少数据
  • continues 分页连续页码数

由pageSize和total可以得到另一信息:共页数

  • totalPage 总页数 == (total / pageSize)

这些数据父组件传递到子组件


	父组件代码:
          <!-- 分页器 -->
          <Pagination
            :pageNo="searchParams.pageNo"
            :pageSize="searchParams.pageSize"
            :total="total"
            :continues="5"
            @getPageNo="getPageNo"
          />

	子组件代码:
	  props: ["pageNo", "pageSize", "total", "continues"],

总页数通过计算属性computed计算得到

  • 向上取整

  computed: {
    // 总共多少页
    totalPage() {
      // 向上取整
      return Math.ceil(this.total / this.pageSize);
    },

二、处理连续页码

接下来要做的是:算出连续页码数字(起始页数和结束页数)
连续页码数字一般是基数,基数对称(比较好看)

  • 假设连续页码是5,如果总页数少于5,起始页数就是1,结束页数就是总页数
if (continues > this.totalPage) {
        start = 1;
        end = this.totalPage;
      }
  • 总页数大于连续页码数字,有两种不正常现象
  1. 起始数字出现0|负数
  2. 结束数字大于总页码

连续页码数字的代码如下:

  computed: {
    // 计算出连续的页码的起始数字与结束数字
    startNumAndEndNum() {
      const { continues, pageNo, totalPage } = this;
      // 先定义两个变量储存起始数字与结束数字的
      let start = 0,
        end = 0;

      // 连续页码数字至少是5(至少5页),如果不够5页
      if (continues > this.totalPage) {
        start = 1;
        end = this.totalPage;
      } else {
        //正常现象【连续页码5,但是你的总页数一定是大于5的】
        //起始数字
        start = pageNo - parseInt(continues / 2);
        //结束数字
        end = pageNo + parseInt(continues / 2);
        //把出现不正常的现象【start数字出现0|负数】纠正
        if (start < 1) {
          start = 1;
          end = continues;
        }
        //把出现不正常的现象[end数字大于总页码]纠正
        if (end > totalPage) {
          end = totalPage;
          start = totalPage - continues + 1;
        }
        return { start, end };
      }
    },
  },

三、 动态展示分页器(使用v-for)
在这里插入图片描述
v-for不仅可以遍历数组,还可以遍历number、object、string等等

因为分页器是子组件,这里点击某一页时,需要用到子传父,使用自定义事件将信息传递到父组件(传递参数,赋值给pageNo)


子组件:
  <div class="pagination">
    <!---->
    <button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
      上一页
    </button>
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">
      1
    </button>
    <button v-if="startNumAndEndNum.start > 2">···</button>

    <!-- 中间部分 -->
    <span v-for="(page, index) in startNumAndEndNum.end" :key="index">
      <button
        v-if="page >= startNumAndEndNum.start"
        @click="$emit('getPageNo', page)"
      >
        {{ page }}
      </button>
    </span>

    <!---->
    <button v-if="startNumAndEndNum.end < totalPage - 1">···</button>
    <button
      v-if="startNumAndEndNum.end < totalPage"
      @click="$emit('getPageNo', totalPage)"
    >
      {{ totalPage }}
    </button>
    <button
      :disabled="pageNo == totalPage"
      @click="$emit('getPageNo', pageNo + 1)"
    >
      下一页
    </button>

    <button style="margin-left: 30px">{{ total }}</button>
  </div>


父组件:
	// 自定义事件的回调函数--获取当前第几页
    getPageNo(pageNo) {
      this.searchParams.pageNo = pageNo;
      // 再次发请求
      this.getData();
    }
  • 点击上一页时, pageNo 应该减1

注意:当前页是第一页时,不能点击上一页

    <button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
      上一页
    </button>
  • 点击第一页时pageNo 应该为1
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">
      1
    </button>
  • 当点击连续分页上任意一页,就传递当前这个page值
    <span v-for="(page, index) in startNumAndEndNum.end" :key="index">
      <button
        v-if="page >= startNumAndEndNum.start"
        @click="$emit('getPageNo', page)"
      >
        {{ page }}
      </button>
    </span>
  • 点击最后一页时传递总页数totalPage
    <button
      v-if="startNumAndEndNum.end < totalPage"
      @click="$emit('getPageNo', totalPage)"
    >
      {{ totalPage }}
    </button>
  • 点击下一页时, pageNo 应该加1

注意:当前页是最后一页时,不能点击下一页

    <button
      :disabled="pageNo == totalPage"
      @click="$emit('getPageNo', pageNo + 1)"
    >
      下一页
    </button>

四、给点击的页数添加样式
这一步比较简单,就是给点击的页数添加一个样式而已

添加样式:
		.active {
		  background: skyblue;
		}


    <button
      v-if="startNumAndEndNum.start > 1"
      @click="$emit('getPageNo', 1)"
      :class="{ active: pageNo == 1 }"
    >
      1
    </button>

    <span v-for="(page, index) in startNumAndEndNum.end" :key="index">
      <button
        v-if="page >= startNumAndEndNum.start"
        @click="$emit('getPageNo', page)"
        :class="{ active: pageNo == page }"
      >
        {{ page }}
      </button>
    </span>

    <button
      v-if="startNumAndEndNum.end < totalPage"
      @click="$emit('getPageNo', totalPage)"
      :class="{ active: pageNo == totalPage }"
    >
      {{ totalPage }}
    </button>

开发Detail部分

引入Detail静态组件

首先引入Detail组件

src\router\routes.js

    {
        path: "/detail/:skuid",
        component: Detail,
        // 路由元信息Key不能乱写,只能是meta
        meta: {
            show: true
        },
    },

点击商品的图片时,进行跳转,需要传递id

src\pages\Search\index.vue

                  <div class="p-img">
                    <!-- 路由跳转时携带params参数 -->
                    <router-link :to="`/detail/${good.id}`">
                      <img :src="good.defaultImg" />
                    </router-link>
                  </div>

滚动行为

滚动行为

它让你可以自定义路由切换时页面如何滚动。

src\router\index.js

// 配置路由
export default new VueRouter({
    routes,
    // 滚动行为
    scrollBehavior(to, from, savedPosition) {
        // 始终滚动到顶部
        // y=0,代表滚动条在最上方
        return {  y: 0 }
    },
})

Vuex获取Detail模块数据

在这里插入图片描述
与前面获取数据是一样的,先准备好借口,然后就是Vuex获取数据

app\src\api\index.js

// 获取产品详情信息的接口   /api/item/{ skuId }  请求方式:get
export const reqGoodsInfo = (skuId) => requests({ url: `/item/${skuId}`, method: "get" });

app\src\store\detail\index.js

import { reqGoodsInfo } from "@/api";

// search模块小仓库

const state = {
    //仓库初始状态
    goodInfo:{}
};
const mutations = {
    GETGOODINFO(state, goodInfo) {
        state.goodInfo = goodInfo
    }
};
const actions = {
    //获取产品详情信息(detail模块)
    async getGoodInfo({ commit }, skuId) {
        //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
        let result = await reqGoodsInfo(skuId);
        if (result.code == 200) {
            commit("GETGOODINFO", result.data);
        }
    },
};



export default {
    state,
    mutations,
    actions,
    getters,

}

app\src\pages\Detail\index.vue

  mounted() {
    // 派发action获取产品详情信息
    this.$store.dispatch("getGoodInfo", this.$route.params.skuid);
  },

获取数据之后就是将数据动态展示在页面上,步骤很多,但都是比较简单的,所以就省略掉了。

点击轮播图片时,改变放大镜组件展示的图片(兄弟组件通信)

在这里插入图片描述

        <div class="previewWrap">
          <!--放大镜效果-->
          <Zoom :skuImageList="skuImageList" />
          <!-- 轮播小图列表 -->
          <ImageList :skuImageList="skuImageList" />
        </div>

因为轮播小图和放大镜效果图是兄弟组件,在轮播图组件中设置一个currendIndex,当轮播小图的currendIndex改变时,通知放大镜效果图也要做出改变

轮播小图:


      <div
        class="swiper-slide"
        v-for="(slide, index) in skuImageList"
        :key="slide.id"
      >
        <img
          :src="slide.imgUrl"
          :class="{ active: currentIndex == index }"
          @click="changeCurrentIndex(index)"
        />
      </div>
    changeCurrentIndex(index) {
      //修改响应式数据
      this.currentIndex = index;
      //通知兄弟组件:当前的索引值为几
      this.$bus.$emit("getIndex", this.currentIndex);
    },

放大镜效果图:
currentIndex改变之后,放大镜效果图就会切换

    <img :src="imgObj.imgUrl" />

  computed: {
    imgObj() {
      return this.skuImageList[this.currentIndex] || {};
    },
  },
  mounted() {
    //全局事件总线:获取兄弟组件传递过来的索引值
    this.$bus.$on("getIndex", (index) => {
      //修改当前响应式数据
      this.currentIndex = index;
    });
  },

购买产品个数的操作

在这里插入图片描述
这里可以点击 “+” 或者 “-” ,也可以在输入框输入

              <div class="controls">
                <input
                  autocomplete="off"
                  class="itxt"
                  v-model="skuNum"
                  @change="changeSkuNum"
                />
                <a href="javascript:" class="plus" @click="skuNum++">+</a>
                <a
                  href="javascript:"
                  class="mins"
                  @click="skuNum > 1 ? skuNum-- : (skuNum = 1)"
                  >-</a
                >
              </div>

    //表单元素修改产品个数
    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);
      }
    },

输入框绑定的change事件,需要判断用户输入是否合法,不能输入非数字或负数,当输入小数时,应该取整

加入购物车(sessionStorage存储数据)

点击加入购物车,向后端发请求,只需要根据状态码code判断是否跳转到“加入购物车成功页面”

跳转‘加入购物车成功页面’的同时要携带商品的信息,但是detail组件和‘加入购物车成功页面’组件毫无关系,要想传递数据,可以使用路由的query传递参数,但是query只适合传递单个数值的简单参数,若想传递对象之类的复杂信息,需要使用Web Storage实现

sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。

注意:无论是session还是local存储的值都是字符串形式的,如果我们需要存储对象,需要在存储之前JSON.stringfy()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象

detail store对应代码:

    //加入购物车的||修改某一个产品的个数
    async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
        //发请求:前端带一些参数给服务器【需要存储这些数据】,存储成功了,没有给返回数据
        //不需要在三连环(仓库存储数据了)
        //注意:async函数执行返回的结果一定是一个promise【要么成功,要么失败】
        let result = await reqAddOrUpdateShopCart(skuId, skuNum);
        if (result.code == 200) {
            //返回的是成功的标记
            return "ok";
        } else {
            //返回的是失败的标记
            return Promise.reject(new Error("faile"));
        }
    },

src\pages\Detail\index.vue

    //加入购物车
    async addShopcar() {
      //1:在点击加入购物车这个按钮的时候,做的第一件事情,将参数带给服务器(发请求),通知服务器加入购车的产品是谁
      //this.$store.dispatch('addOrUpdateShopCart'),说白了,它是在调用vuex仓库中的这个addOrUpdateShopCart函数。
      //2:你需要知道这次请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示
      try {
        //成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: this.$route.params.skuid,
          skuNum: this.skuNum,
        });
        //3:进行路由跳转
        //4:在路由跳转的时候还需要将产品的信息带给下一级的路由组件
        //一些简单的数据skuNum,通过query形式给路由组件传递过去
        //产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据在消失)
        //本地存储|会话存储,一般存储的是字符串
        sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo));
        this.$router.push({
          name: "addcartsuccess",
          query: { skuNum: this.skuNum },
        });
      } catch (error) {
        //失败
        alert(error.message);
      }
    },

这里使用sessionStorage传递的是一个对象
在这里插入图片描述
里面有AddCartSuccess组件需要的数据(detail组件中已经通过mapGeters获取到,所以传递过去给AddCartSuccess组件,这样就不需要重新发请求获取了

AddCartSuccess**组件代码截图:
在这里插入图片描述
AddCartSuccess组件通过计算属性获取

  computed: {
    skuInfo() {
      return JSON.parse(sessionStorage.getItem("SKUINFO"));
    },
  },

开发ShopCart部分

购物车组件开发(请求头添加一个uuid_Token)

在这里插入图片描述
一个网站是有很多用户的,每个用户自己的购物车都不一样,所以每一个人的购物车页面展示的东西都不一样

当你以游客身份访问网站时:

每个用户需要一个uuidToken,用来验证用户身份,让服务器知道你是谁,但是这个请求函数没有参数,所以我们把uuidToken加在请求头中

根据api接口文档封装请求函数

export const reqGetCartList = () => {
return requests({
	url:'/cart/cartList',
	method:'GET'
})}

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

单例模式,只赋值一次就不再赋值了
app\src\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模块
app\src\store\detail.js

//封装游客身份模块uuid--->生成一个随机字符串(不能在变了)
import {getUUID} from '@/utils/uuid_token';

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

在request.js中设置请求头
app\src\api\request.js

import store from '@/store';


// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {

    if (store.state.detail.uuid_token) {

        //请求头添加一个字段(userTempId):和后台老师商量好了
        config.headers.userTempId = store.state.detail.uuid_token;
    }
    // 进度条开始
    nprogress.start();

    // config:配置对象,对象里面有一个属性很重要,headers请求头
    return config;
});

购物车动态展示数据

将上一步获取到的数据展示在相应的地方(比较简单,直接省略)

购物车商品数量修改及个人疑问(函数节流)

在这里插入图片描述
这里有三个操作,减一、加一、中间是修改输入框的数字,统一使用一个回调函数
传三个参数,第一个表示操作类型、第二个是disNum(变化量)、第三个表示哪一个产品(身上有id)

 <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" @change="handler('change',$event.target.value,cartInfo)" minnum="1" class="itxt">
     <a href="javascript:void(0)" class="plus" @click="handler('add',1,cartInfo)">+</a>
 </li>

handler函数,修改商品数量时,加入节流操作。(防止用户快速点击,请求还没回来,导致输入框变为负数)

节流操作:在规定时间范围内不会重复触发回调函数,只有大于这个时间间隔才会触发下一次

    //修改某一个产品的个数[节流]
    handler: throttle(async function (type, disNum, cart) {
      //type:为了区分这三个元素
      //disNum形参:+ 变化量(1)  -变化量(-1)   input最终的个数(并不是变化量)
      //cart:哪一个产品【身上有id】
      //向服务器发请求,修改数量
      switch (type) {
        //加号
        case "add":
          disNum = 1;
          break;
        case "minus":
          //判断产品的个数大于1,才可以传递给服务器-1
          //如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          // //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum;
          }
          // disNum = (isNaN(disNum)||disNum<1)?0:parseInt(disNum) - cart.skuNum;
          break;
      }
      //派发action
      try {
        //代表的是修改成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: cart.skuId,
          skuNum: disNum,
        });
        //再一次获取服务器最新的数据进行展示
        this.getData();
      } catch (error) {}
    }, 1000),

购物车状态修改和商品删除

这部分都比较简单,这里不多做赘述,唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)

action部分:以修改某个产品的勾选状态为例

  //修改购物车某一个产品的选中状态
  async updateCheckedById({ commit }, { skuId, isChecked }) {
    let result = await reqUpdateCheckedByid(skuId, isChecked);
    if (result.code == 200) {
      return "ok";
    } else {
      return Promise.reject(new Error("faile"));
    }
  },

method部分:(重点是try、catch

 	    //修改某个产品的勾选状态
    async updateChecked(cart, event) {
      //带给服务器的参数isChecked,不是布尔值,应该是0|1
      try {
        //如果修改数据成功,再次获取服务器数据(购物车)
        let isChecked = event.target.checked ? "1" : "0";
        await this.$store.dispatch("updateCheckedById", {
          skuId: cart.skuId,
          isChecked,
        });
        this.getData();
      } catch (error) {
        //如果失败提示
        alert(error.message);
      }
    },

删除多个商品(actions扩展使用)

由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数

我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数

actions扩展
官网的教程,一个标准的actions函数如下所示:

 deleteAllCheckedById(context) {
        console.log(context)

    }

在这里插入图片描述
context中包含commit、dispatch、getters、state,之前我们只在actions函数中使用过commit,事实上也可以使用dispatch、getters和state

这样我们的批量删除就简单了,对应的actions函数代码让如下

  //删除全部勾选的产品
  deleteAllCheckedCart({ dispatch, getters }) {
    //context:小仓库,commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】
    //获取购物车中全部的产品(是一个数组)
    let PromiseAll = [];
    getters.cartList.cartInfoList.forEach((item) => {
      let promise =
        item.isChecked == 1
          ? dispatch("deleteCartListBySkuId", item.skuId)
          : "";
      //将每一次返回的Promise添加到数组当中
      PromiseAll.push(promise);
    });
    //只要全部的p1|p2....都成功,返回结果即为成功
    //如果有一个失败,返回即为失败结果
    return Promise.all(PromiseAll);
  },

上面代码使用到了Promise.all

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

ShopCart购物车组件method批量删除函数

    //删除全部选中的产品
    //这个回调函数咱门没办法手机到一些有用数据
    async deleteAllCheckedCart() {
      try {
        //派发一个action
        await this.$store.dispatch("deleteAllCheckedCart");
        //再发请求获取购物车列表
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },

修改商品的全部状态和删除多个商品的原理相同

src\store\shopcart.js


  //修改购物车某一个产品的选中状态
  async updateCheckedById({ commit }, { skuId, isChecked }) {
    let result = await reqUpdateCheckedByid(skuId, isChecked);
    if (result.code == 200) {
      return "ok";
    } else {
      return Promise.reject(new Error("faile"));
    }
  },
  
  //修改全部产品的状态
  updateAllCartIsChecked({ dispatch, state }, isChecked) {
    //数组
    let promiseAll = [];
    state.cartList[0].cartInfoList.forEach((item) => {
      let promise = dispatch("updateCheckedById", {
        skuId: item.skuId,
        isChecked,
      });
      promiseAll.push(promise);
    });
    //最终返回结果
    return Promise.all(promiseAll);
  },

app\src\pages\ShopCart\index.vue

    //修改全部产品的选中状态
    async updateAllCartChecked(event) {
      try {
        let isChecked = event.target.checked ? "1" : "0";
        //派发action
        await this.$store.dispatch("updateAllCartIsChecked", isChecked);
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },

注册登录业务

注册

在这里插入图片描述

先获取验证码,再输入密码,再点击注册

app\src\store\user.js

    //获取验证码
    async getCode({ commit }, phone) {
        //获取验证码的这个接口:把验证码返回,但是正常情况,后台把验证码发到用户手机上【省钱】
        let result = await reqGetCode(phone);
        if (result.code == 200) {
            commit("GETCODE", result.data);
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },
    //用户注册
    async userRegister({ commit }, user) {
        let result = await reqUserRegister(user);
        if (result.code == 200) {
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },

app\src\pages\Register\index.vue

    //获取验证码
    async getCode() {
      //简单判断一下---至少用数据
      try {
        //如果获取到验证码
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", phone));
        //将组件的code属性值变为仓库中验证码[验证码直接自己填写了]
        this.code = this.$store.state.user.code;
      } catch (error) {}
    },
    //用户注册
    async userRegister() {
      const { phone, password, code } = this;
      try {
        phone &&
          password &&
          code &&
          (await this.$store.dispatch("userRegister", {
            phone,
            password,
            code,
          }));
        //注册成功跳转到登陆页面,并且携带用户账号
        await this.$router.push({
          path: "/login",
          query: { name: this.phone },
        });
      } catch (error) {
        alert(error);
      }
    },

登录(持久化储存token)

在这里插入图片描述

用户登录时,会向服务器发请求(组件派发action:userLogin),登录成功的话服务器就会返回一个token,将token储存在vuex里面

app\src\pages\Login\index.vue

    //登录的回调函数
    async userLogin() {
      try {
        //登录成功
        const { phone, password } = this;
        phone &&
          password &&
          (await this.$store.dispatch("userLogin", { phone, password }));
        //登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
        //  let toPath = this.$route.query.redirect||"/home";
        this.$router.push("/home");
      } catch (error) {
        alert(error.message);
      }
    },

服务器返回token字段 ,将token保存在vuex里面

app\src\store\user.js

    USERLOGIN(state, token) {
        state.token = token;
    },

    //登录业务
    async userLogin({ commit }, data) {
        let result = await reqUserLogin(data);
        console.log(result, 'result');
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if (result.code == 200) {
            //用户已经登录成功且获取到token
            commit("USERLOGIN", result.data.token);
            //持久化存储token
            // setToken(result.data.token);
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },

(token代表一个用户的身份,不同token获取不同的用户信息)

这时,我们只是将token保存在仓库,还需要将token添加到请求头,这样就可以获取用户信息
app\src\api\request.js

// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
    if (store.state.detail.uuid_token) {
        //请求头添加一个字段(userTempId):和后台老师商量好了
        config.headers.userTempId = store.state.detail.uuid_token;
    }
    if (store.state.user.token) {
        //请求头添加一个字段(token)
        config.headers.token = store.state.user.token;
    }
    // 进度条开始
    nprogress.start();
    // config:配置对象,对象里面有一个属性很重要,headers请求头
    return config;
});

当跳转到首页时,请求头已经添加token字段,所以发请求可以获取到用户信息,将用户信息展示在首页

app\src\pages\Login\index.vue

  mounted() {
    this.$store.dispatch("getFloorList")
    // 获取用户信息在首页展示
    this.$store.dispatch("getUserInfo")
  },

app\src\store\user.js

    GETUSERINFO(state, userInfo) {
        state.userInfo = userInfo;
    },

    //获取用户信息
    async getUserInfo({ commit }) {
        let result = await reqUserInfo();
        if (result.code == 200) {
            //提交用户信息
            commit("GETUSERINFO", result.data);
            return 'ok';
        } else {
            return Promise.reject(new Error('faile'));
        }
    },

但是vuex储存数据不是持久化的 ,一旦刷新页面,vuex里面的数据就没了,即token也会清空,这样就没有token去发请求获取用户信息

因此我们需要持久化储存token

获取到token后,将token保存在本地(localStorage)点击刷新也可以在本地获取token
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码如下:
app\src\store\user.js

//登录与注册的模块
const state = {
    code: "",
    token: getToken(),
    userInfo: {},

};

    USERLOGIN(state, token) {
        state.token = token;
    },

    //登录业务
    async userLogin({ commit }, data) {
        let result = await reqUserLogin(data);
        console.log(result, 'result');
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if (result.code == 200) {
            //用户已经登录成功且获取到token
            commit("USERLOGIN", result.data.token);
            //持久化存储token
            setToken(result.data.token);
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },

app\src\utils\token.js

//存储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里面的token也不会被清空,实现了持久化储存token

在这里插入图片描述
但是目前还是存在一些问题的

token已经持久化储存,但是用户信息没有持久化储存,一刷新用户信息就会被清空

  • 只有首页可以获取用户信息,在其他页面(search组件)点击刷新,就无法获取用户信息其他组件也需要派发action
    this.$store.dispatch(’ getUserInfo ')
    // 获取用户信息在首页展示
    this.$store.dispatch("getUserInfo")

因为只有在首页有上面这一句代码,才可以获取到用户信息

  • 用户已经登录了,再次点击就不应该再跳回登录页

全局前置守卫(登录与未登录两种情况)

导航守卫

  • 导航:表示路由正在发生改变,进行路由跳转
  • 守卫:可以把它当做“紫禁城护卫”

全局守卫只要发生路由变化,守卫就可以监听到
举例子:紫禁城【皇帝、太后、妃子】,紫禁城大门守卫全要排查

全局前置路由守卫(比较常用)
有三个参数

  • to:获取到要跳转到的路由信息
  • from:获取到从哪个路由跳转过来来的信息
  • next: next() 代表放行 next(path) 代表放行

path 前面肯定有/ 例子:/login /home

  1. 已登录时的路由守卫

问题:

之前提到过,在首页之外的页面点击刷新,无法获取用户信息,因为其他页面没有派发action去获取用户信息,所以我们通过使用前置路由守卫来解决这个问题

解决方法:

用户已经登录的情况下(访问的是非登录与注册),在每次路由跳转之前,判断一下是否拥有用户信息,如果没有用户信息,就先去派发action获取用户信息再放行

获取用户信息需要token,如果token失效,就需要重新登录获取并保存token

app\src\router\index.js

// 全局守卫: 前置守卫(路由跳转之前进行判断)
router.beforeEach(async (to, from, next) => {

    // to:获取到要跳转到的路由信息
    // from:获取到从哪个路由跳转过来来的信息
    // next: next() 放行  next(path) 放行 
    // console.log(to);
    // console.log(from);
    let token = store.state.user.token;
    let name = store.state.user.userInfo.name;
    // 用户已经登录
    if (token) {
        // path  前面肯定有/  例子:/login  /home
        // 用户已经登录还想去login【不能去,停留在首页】
        if (to.path == '/login') {
            next('/home')
        } else {
            //已经登陆了,访问的是非登录与注册
            //登录了且拥有用户信息放行
            if (name) {
                next();
            } else {
                //登陆了且没有用户信息
                //在路由跳转之前获取用户信息且放行
                try {
                    await store.dispatch('getUserInfo');
                    next();
                } catch (error) {
                    //token失效从新登录
                    await store.dispatch('userLogout');
                    next('/login')
                }
            }
        }
    } else {
        // 未登录
        next()
    }
})
  1. 未登录时的路由守卫

用户未登录时,不能去交易、支付相关【pay|paysuccess】、个人中心

如果点击前往这些页面(例子:pay页面),首先会跳转到登录页面,并把未去成的信息存储在地址栏中,登陆之后就跳转到该页面(例子:pay页面)
app\src\router\index.js

else {
        //未登录:不能去交易相关、不能去支付相关【pay|paysuccess】、不能去个人中心
        //未登录去上面这些路由-----登录
        let toPath = to.path;
        if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
            //把未登录的时候向去而没有去成的信息,存储于地址栏中【路由】
            next('/login?redirect=' + toPath);
        } else {
            //去的不是上面这些路由(home|search|shopCart)---放行
            next();
        }

    }

登录的回调函数里面,需要判断一下路由当中是否包含query参数

有query参数跳转到指定的路由,没有就跳转到home

app\src\pages\Login\index.vue

    //登录的回调函数
    async userLogin() {
      try {
        //登录成功
        const { phone, password } = this;
        phone &&
          password &&
          (await this.$store.dispatch("userLogin", { phone, password }));
        //登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
         let toPath = this.$route.query.redirect||"/home";
        this.$router.push(toPath);
      } catch (error) {
        alert(error.message);
      }
    },

路由独享守卫(定义在对应的路由身上)

举例子:紫禁城【皇帝、太后、妃子】,是相应的【皇帝、太后、妃子】路上的守卫(只排查自己负责的皇帝或太后或妃子,而且是跳转路上的)

如果想跳转支付页面,必须是从交易页面跳转过来的

app\src\router\routes.js

    {
        path: '/pay',
        component: Pay,
        meta: {
            show: true
        },
        // 路由独享守卫
        beforeEnter: (to, from, next) => {
            // 去支付页面,必须是从交易页面而来
            if (from.path == '/trade') {
                next()
            } else {
                next(false)
                // console.log('不111可以跳转');
            }
        }
    },

如果要跳转交易页面,必须是从购物车跳转过来的

app\src\router\routes.js

    {
        path: '/trade',
        component: Trade,
        meta: {
            show: true
        },
        // 路由独享守卫
        beforeEnter: (to, from, next) => {
            // 去交易页面,必须是从购物车页面而来
            if (from.path == '/shopcart') {
                next()
            } else {
                next(false)
                // console.log('不111可以跳转');
            }
        }
    },

组件内守卫(定义在对应的组件身上)

组件内守卫: 要去皇帝屋子
举例子:已经来到皇帝屋子外面了(进入了)的守卫

有三种情况:

  1. beforeRouteEnter(在进入这个组件之前调用)

app\src\pages\PaySuccess\index.vue(进入Paysuccess前调用)

  name: "PaySuccess",
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    if (from.path == "/pay") {
      next();
    } else {
      next(false);
    }
  },
  1. beforeRouteUpdate(该组件被复用时调用)

app\src\pages\PaySuccess\index.vue

  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
    console.log("12313131311313");
  },
  1. beforeRouteLeave(导航离开该组件时调用)

app\src\pages\PaySuccess\index.vue

  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    next();
  },

路由懒加载

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了

原本路由是这样的

import AddCartSuccess from '@/pages/AddCartSuccess'

    {
        path: '/addcartsuccess',
        component: AddCartSuccess,
    },

路由懒加载:

const UserDetails = () => import('./views/UserDetails')
{ 
	path: '/users/:id', 
	component: UserDetails 
}

更简洁的写法:

    {
        path: "/register",
        component: () => import('@/pages/Register'),
    },

项目上线

项目打包

在这里插入图片描述
map文件可以准确输出哪一行哪一列报错,但是对于项目上线无意义

在vue.config.js设置项目打包时去掉map文件

module.exports = {
  // 打包时去掉map文件
  productionSourceMap:false,
  // 关闭ESLINT校验工具
  lintOnSave: false,
  //配置代理跨域
  devServer: {
    proxy: {
      "/api": {
        target: "http://39.98.123.211",
      },
    },
  },
};

然后将项目打包好

npm run build 

服务器的购买与使用

购买CentOS的服务器,比较好用

在这里插入图片描述

新建安全组,开放服务器的一下端口号22,80,443,3389
在这里插入图片描述

Xftp 和 Xshell的下载

下面需要使用Xftp 和 Xshell,在下面这个链接下载
Xftp 和 Xshell免费下载
在这里插入图片描述

Xftp的使用

创建会话
在这里插入图片描述
把本地的dist复制到Xftp创建的会话中去
在这里插入图片描述

Xshell的使用

创建会话
在这里插入图片描述
连接成功如下图所示:
在这里插入图片描述
切换到nginx目录
cd / => cd etc => cd nginx

vim nginx.conf

INSERT进入编辑模式,添加以下内容
esc退出编辑,:wq保存编辑的内容
在这里插入图片描述

XSHELL7启动nginx服务器

systemctl start nginx.service

具体指令介绍:

#启动nginx服务
systemctl start nginx.service
#停止nginx服务
systemctl stop nginx.service
#重启nginx服务
systemctl restart nginx.service
#重新读取nginx配置(这个最常用, 不用停止nginx服务就能使修改的配置生效)
systemctl reload nginx.service

这样就可以通过自己的服务器的ip地址访问尚品汇项目

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值