vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇

文章目录

    • 最好使用视频上的账号密码,13700000000 密:111111
    • 最新服务端接口地址:http://gmall-h5-api.atguigu.cn
    • 脚手架使用
      • 1、创建项目
      • 2、脚手架默认目录:
      • 3、脚手架下载的项目稍微配置一下
        • 1)浏览器自动打开
        • 2)关闭 eslint 校验工具,以防写代码时没错也报错。
        • 3)src文件夹的别名的设置
    • 一、项目路由分析
    • 二、Header、Footer非路由组件完成
      • 1、使用非路由组件步骤:
      • 2、使用路由组件步骤:
        • 1) 路由的跳转有两种形式:
        • 2) 路由元信息:
        • 3) 路由传递参数
        • 4) 重写push与repalce方法
    • 三、TypeNav三级联动组件完成
    • 四、Home首页拆分静态组件完成
    • 五、动态渲染三级联动部分
      • 1、postman测试接口
      • 2、axios 二次封装
      • 3、API接口统一管理
      • 4、nprogress进度条的使用
      • 5、vuex 模块式开发
      • 6、动态展示三级联动数据
      • 7、动态一级菜单背景颜色
      • 8、JS控制二三级分类显示与隐藏
      • 9、函数防抖与函数节流
      • 10、三级联动节流
      • 11、三级联动路由跳转与传递参数
      • 12、TypeNav组件一级商品分类的显示与隐藏,和过渡动画
      • 13、TypeNav商品分类列表的优化
      • 14、合并参数
    • 六、mockjs模拟数据 | Home首页完成
      • 1、获取Banner轮播图的数据
      • 2、使用swiper轮播图插件
      • 3、轮播图通过 watch+nectTick 解决问题
      • 4、获取Floor组件mock数据
      • 5、制作共用组件Carsouel轮播图组件
    • 七、Search模块
      • 1、Search模块vuex操作
      • 2、动态展示产品列表
      • 3、根据不同的参数 获取对应数据 进行展示
      • 4、子组件SearchSelector售卖属性的动态开发
      • 5、监听路由变化再次发请求获取数据
      • 6、面包屑
      • 7、平台售卖属性的面包屑
      • 8、排序操作
    • 八、分页器
    • 九、Detail商品详情组件
      • 1、滚动行为
      • 2、产品详情获取
      • 3、产品详情展示动态数据
      • 4、产品售卖属性值的排他操作(即切换高亮)
      • 5、放大镜
      • 6、购买产品个数的操作
      • 7、添加到购物车
    • 十、添加到购物车成功 路由组件
      • 1、路由传递参数(结合会话存储)
    • 十一、购物车组件
      • 1、uuid游客身份获取购物车数据
      • 2、购物车动态展示相应游客的数据
      • 3、修改购物车产品数量
      • 4、删除购物车某个产品
      • 5、修改某个产品选中状态
      • 6、删除全部选中的商品
      • 7、修改全部产品的勾选状态
    • 十二、登录注册组件
      • 1、注册业务
      • 2、登录业务(token)
      • 3、用户登录后携带token获取用户信息
      • 4、退出登录
      • 5、导航守卫,优化登录业务
    • 十三、Trade交易组件
      • 1、获取用户地址信息和商品清单
      • 2、展示用户地址数据
      • 3、交易信息展示
      • 4、提交订单
          • 从这里开始,不使用vuex了,因为需要学会无vuex时,将如何传递数据。(项目不大时,最好不用vuex)
    • 十四、支付页面
      • 1、展示支付信息
      • 2、支付页面中使用ElementUI(按需引入)
    • 十五、微信支付业务
      • 1、显示支付二维码
      • 2、若支付成功 跳至支付成功页面
    • 十六、Center个人中心组件
      • 1、个人中心二级路由搭建
      • 2、获取并展示myOrder组件我的订单列表
    • (17)用户登录后的导航守卫(路由独享守卫与组件内守卫)
    • (18)未登录的导航守卫
    • (19)图片懒加载
    • (20)vee-validate插件 表单验证
    • (21)路由懒加载
    • (22)处理map文件
    • (23)服务器
      • 1、购买服务器
      • 2、安全组
      • 3、nginx反向代理

最好使用视频上的账号密码,13700000000 密:111111

最新服务端接口地址:http://gmall-h5-api.atguigu.cn

因为该账号数据库存有地址信息 。

但是注意,因为很多同学同时操作,所以可能出现:购物车商品被其他同学增加或减少,订单不能重复提交等问题。此时稍等一会,或多试几次即可。

学习尚硅谷的商城前台尚品汇项目的笔记:

脚手架使用

1、创建项目

vue create 项目名称

2、脚手架默认目录:

  • node_modules:放置项目依赖的地方。
  • public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面。
  • src:程序员源代码文件夹:
    • assets:经常放置一些静态资源(公用的图片(即很多组件都用此图)),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
    • components:一般放置非路由组件(如共用的组件)
  • App.vue:唯一的根组件
  • main.js:入口文件【程序最先执行的文件】
  • babel.config.js:babel配置文件
  • package.json:项目描述、项目依赖、项目运行
  • README.md:项目说明文件

注意:

(放public中的图片,组件引用:images/1.png 组件的less中引用:url(/images/9.png);使用"/"作为根目录。 因为最后webpack打包,public中的图片原封不动打包到了images中,所以用绝对路径)

放assets中的图片,组件的html中引用:@/assets/2.png 组件的less中引用:~@/assets/7.png 因为assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面),所以用相对路径)

3、脚手架下载的项目稍微配置一下

  • 1)浏览器自动打开

    在 package.json 文件中

    ?        "scripts": {
    ?         "serve": "vue-cli-service serve --open",
    ?          "build": "vue-cli-service build",
    ?          "lint": "vue-cli-service lint"
    ?        },
    
  • 2)关闭 eslint 校验工具,以防写代码时没错也报错。

    在根目录创建 vue.config.js 文件:需要对外暴露

    module.exports = {
       lintOnSave: false,
    }
    
  • 3)src文件夹的别名的设置

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

    就不用用那么多…/…/了。直接@代替。

    (js用@代替src路径,css用~@代替src路径)

    创建 jsconfig.json 文件

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

一、项目路由分析

路由组件:

Home首页、Search搜索、login登录、Refister注册

非路由组件:

Header头部、Footer底部 (有Home首页、Search搜索组件。无login登录、Refister注册组件)

二、Header、Footer非路由组件完成

在开发项目的时候:
1: 书写静态页面(HTML + CSS)
2: 拆分组件
3: 获取服务器的数据动态展示
4: 完成相应的动态业务逻辑

需要用到less ,安装低版本,高版本容易出错。

yarn add less less-loader@5

(组件中,需要加

四、Home首页拆分静态组件完成

vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第1张图片

Home/index.vue






五、动态渲染三级联动部分

1、postman测试接口

填入服务器地址和请求地址,即
http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList

在这里插入图片描述

200ok 说明成功。接口没问题。

2、axios 二次封装

为什么二次封装?

为了请求拦截器、响应拦截器。

请求拦截器:在发请求之前可以处理一些业务;

响应拦截器:当服务器数据返回以后,可以处理一些事情。

安装axios

yarn add axios

在项目中经常有 api文件夹,一般都是放 axios的

api/request.js

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

// 利用axios对象的方法create,去创建一个axios实例
// 这里的request 就是 axios,在这里配置一下
const request = axios.create({
    // 配置对象
    // 基础路径,发请求的时候,路径当中会默认有/api,不用自己写了
    baseURL: "/api",
    // 请求超时5s
    timeout: 5000,
})

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

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


// 对外暴露
export default requests;

3、API接口统一管理

若项目很小,可以在组件的生命周期函数中发请求

但项目大,组件多,若有更改,将麻烦。所以API接口统一管理。

什么是跨域?

同源就是指,域名、协议、端口均为相同。

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

vue.config.js

	// webpack中的代理跨域
    devServer: {
        proxy: {
            '/api': {
                // 服务器地址
                target: 'http://gmall-h5-api.atguigu.cn',
            },
        },
    },

api/index.js

import requests from "./request";

// 三级联动接口
export const reqCategoryList = () =>
    // 发请求:axios发请求返回结果是Promise对象
    requests({ url: "/product/getBaseCategoryList", method: "get" })

4、nprogress进度条的使用

安装: yarn add nprogress

在响应拦截器使用

api/request.js

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


// 请求拦截器:
requests.interceptors.request.use((config) => {
    // config:配置对象,其有一个重要属性:header请求头
    // 进度条开始动
    nprogress.start();
    return config;

})
// 响应拦截器:
requests.interceptors.response.use((res) => {
    // 服务器响应成功的回调函数
    // 进度条结束
    nprogress.done();
    return res.data;
}, (err) => {
    // 服务器响应失败的回调函数
    return Promise.reject(new Error('faile'));
})

5、vuex 模块式开发

vuex 是官方提供的插件, 状态管理库,集中式管理项目中组件共用的数据 。

切记,并不是全部项目都需要 Vuex,如果项目很小,完全不需要Vuex,如果项目很大,组件很多、数据很多,数据维护很费劲,用Vuex

安装vuex yarn add vuex
vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第2张图片

main.js

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

store/home/index.js

// home 模块的小仓库

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

// 对外暴露
export default {
    state,
    mutations,
    actions,
    getters,
}

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

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

export default new Vuex.Store({
    modules: {
        home,
        search,
    }
})

6、动态展示三级联动数据

api/index.js

import requests from "./request";

// 三级联动接口
export const reqCategoryList = () =>
    // 发请求:axios发请求返回结果是Promise对象
    requests({ url: "/product/getBaseCategoryList", method: "get" })

store/home/index.js

// home 模块的小仓库

import { reqCategoryList } from "@/api"

// state:仓库存储数据的地方
const state = {
    categoryList: [],
}
// mutations:修改state(数据)的唯一手段
const mutations = {
    CATEGORYLIST(state, categoryList) {
        state.categoryList = categoryList
    }
}
// actions:处理action,书写自己的业务逻辑、也可以处理异步
const actions = {
    // {commit}是因为其中要用到commit,原本是categoryList(context,value){}, 用commit时需要context.commit,{commit}可以省略context. 若不需要value则可不写。
    async categoryList({commit}) {
        // 向服务器发请求
        let result = await reqCategoryList();
        // console.log(result)
        // result.code == 200代表请求成功
        if (result.code == 200) {
            // 修改数据
            commit("CATEGORYLIST", result.data)
        }
    },
}

// 对外暴露
export default {
    state,
    mutations,
    actions,
    getters,
}

components/TypeNav/index.vue




    

7、动态一级菜单背景颜色

鼠标移出全部商品分类时,一级菜单背景消失

     

全部商品分类

实现鼠标在当前标题,当前标题背景颜色设为蓝色,排他高亮显示

              

{{ c1.categoryName }}

data() { return { // 存储用户移上哪一个一级分类 currentIndex: -1, }; }, methods: { // 鼠标进入则修改响应式数据currentIndex属性 changeIndex(index) { // index:鼠标移上某个一级分类元素的索引值 this.currentIndex = index; }, // 鼠标移出 leaveIndex() { this.currentIndex = -1; }, },

8、JS控制二三级分类显示与隐藏

之前是使用 css的 display: block | none;

现用 js 实现

              

9、函数防抖与函数节流

  methods: {
    // 鼠标进入则修改响应式数据currentIndex属性
    changeIndex(index) {
      // index:鼠标移上某个一级分类元素的索引值
      this.currentIndex = index;
      console.log(this.currentIndex);
      // 当鼠标移动过快,可能只有部分h3触发了鼠标进入事件,会出现,1 2 3 9 的情况。
    },

用户行为过快,导致浏览器反应不过来。

如果当前回调函数中有一些大量业务,有可能出现卡顿现象。

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

防抖: 前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次。

防抖案例:

    

输入搜索内容:

节流案例:

我是计数器:0

使用Carousel组件并给Carousel组件传数据:

ListContainer/index.vue

        
        

Floor/index.vue

              
              

七、Search模块

1、Search模块vuex操作

api/index.js

// 获取搜索模块数据
// reqGetSearchInfo函数在获取服务器数据时,至少传递一个参数(空对象)
export const reqGetSearchInfo = (params) => requests({
    url: "/list",
    method: "post",
    data: params
})

2、动态展示产品列表

store/search/index.js

// getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
const getters = {
    goodsList(state) {
        return state.searchList.goodsList
    }
}

Search/index.vue

              
  • import { mapGetters } from "vuex"; ...... computed: { ...mapGetters(["goodsList"]), },
  • 3、根据不同的参数 获取对应数据 进行展示

    从首页三级菜单或搜索框中传参,至服务器返回对应商品数据。

    Search/index.vue

      data() {
        return {
          // 带给服务器的参数
          searchParams: {
            // 一级分类的id
            category1Id: "",
            // 二级分类的id
            category2Id: "",
            // 三级分类的id
            category3Id: "",
            // 分类名字
            categoryName: "",
            // 关键字
            keyword: "",
            // 排序
            order: "",
            // 分页器用的:代表当前是第几页
            pageNo: 1,
            // 代表每一页展示的数据个数
            pageSize: 3,
            // 平台售卖属性操作带的参数
            props: [],
            // 品牌
            trademark: "",
          },
        };
      },
      // 当组件挂载完毕之前执行一次(mounted之前)
      beforeMount() {
        // 整合要传递的参数
        // 复杂的写法:
        // this.searchParams.category1Id = this.$route.query.category1Id
        // this.searchParams.category2Id = this.$route.query.category2Id
        // this.searchParams.keyword = this.$route.params.keyword
        // 简单的写法:用Object.assign:es6新增语法,可以合并对象
        // Object.assign(target, ...sources)  sources是源对象。target是目标对象。返回值是目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。
        Object.assign(this.searchParams, this.$route.query, this.$route.params);
      },
      // 当组件挂载完毕后执行一次
      mounted() {
        this.getData();
      },
      methods: {
        // 向服务器发请求获取search模块数据(根据参数不同返回不同的数据进行展示)
        getData() {
          this.$store.dispatch("getSearchList", this.searchParams);
        },
      },
    

    4、子组件SearchSelector售卖属性的动态开发

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第3张图片

    Search/SearchSelector/index.vue

           
    • {{trademark.tmName}}
    ...... import { mapGetters } from 'vuex' export default { name: 'SearchSelector', computed: { ...mapGetters(['trademarkList','attrsList']) }

    5、监听路由变化再次发请求获取数据

    点击三级分类后,跳至搜索页面,再在搜索框搜索或点三级分类,根据相应参数请求对应数据。可以监听路由变化,就可以再次发请求了。

    Search/index.vue

      // 当组件挂载完毕之前执行一次(mounted之前)
      beforeMount() {
        // 传递参数
        // 复杂的写法:
        // this.searchParams.category1Id = this.$route.query.category1Id
        // this.searchParams.category2Id = this.$route.query.category2Id
        // this.searchParams.keyword = this.$route.params.keyword
        // 简单的写法:用Object.assign:es6新增语法,可以合并对象
        // Object.assign(target, ...sources) target目标对象。sources源对象。返回值是目标对象。
        Object.assign(this.searchParams, this.$route.query, this.$route.params);
      },
      // 当组件挂载完毕后执行一次
      mounted() {
        this.getData();
      },
      methods: {
        // 向服务器发请求获取search模块数据(根据参数不同返回不同的数据进行展示)
        getData() {
          this.$store.dispatch("getSearchList", this.searchParams);
        },
      },
      computed: {
        ...mapGetters(["goodsList"]),
      },
      watch: {
        // 监听$route,如果路由信息变化,则会向服务器发请求获取当前参数对应的数据
        $route(newValue, oldValue) {
          Object.assign(this.searchParams, this.$route.query, this.$route.params);
          // 向服务器发请求
          this.getData();
          // 每次发请求后,应把三级分类的id置空,以便接收下次
          this.searchParams.category1Id = ''
          this.searchParams.category2Id = ''
          this.searchParams.category3Id = ''
        },
      },
    };
    

    6、面包屑

    Search/index.vue

        	  ......
              
    • {{ searchParams.categoryName }}x
    • {{ searchParams.keyword }}x
    • {{ searchParams.trademark.split(":")[1] }}x
    // 点击三级分类的面包屑的x后删除分类名字 removeCategoryName() { // 因为带给服务器的参数都可以是空,当参数值是空时还是会把参数带给服务器会降低性能,设置为undefined时该参数不会带给服务器 this.searchParams.categoryName = undefined; this.searchParams.category1Id = undefined; this.searchParams.category2Id = undefined; this.searchParams.category3Id = undefined; // 需要展示剩余参数对应的数据,向服务器发请求 this.getData(); // 地址栏也需要修改,即地址栏去掉query参数(三级分类) if (this.$route.params) { this.$router.push({ name: "search", params: this.$route.params, }); } }, // 点击关键字(搜索框)的面包屑的x后删除关键字对应的面包屑 removeKeyword() { this.searchParams.keyword = undefined; // 需要展示剩余参数对应的数据,向服务器发请求 this.getData(); // 通知兄弟组件Header清除关键字 this.$bus.$emit("clear"); // 地址栏也需要修改,即地址栏去掉params参数,即关键字 if (this.$route.query) { this.$router.push({ name: "search", query: this.$route.query }); } }, // 自定义事件回调 // 当点击品牌,展示该品牌数据 trademarkInfo(trademark) { // 接收子组件SearchSelector传来的品牌信息数据 this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}` // 再次发请求 this.getData() }, // 点击品牌的面包屑的x后删除品牌对应的面包屑 removeTrademark() { this.searchParams.trademark = undefined this.getData() } }

    Header/index.vue

      mounted() {
        // 通过全局事件总线,当面包屑所在组件触发clear事件,则Header组件清除关键字
        this.$bus.$on("clear", () => {
          this.keyword = "";
        });
      },
    

    Search/SearchSelector.vue

              
  • {{ trademark.tmName }}
  • ...... methods: { // 品牌 tradeMatkHanler(trademark) { // 点击品牌后,需整理参数,向服务器发请求获取相应数据进行展示 // 因为父组件中数据searchParams参数是带给服务器的参数,所以子组件应把点击的品牌信息,给父组件传过去 // 用自定义事件传参 this.$emit("trademarkInfo", trademark); },

    search路由配置时一定要设置,path上加个 号,代表可传params参数也可不传;若不加 ,则URL会出现问题。

    router/routes.js

    {
         name: "search",
         // 配置路由时,path上加个 ? 号,代表可传params参数也可不传;若不加 ? ,则URL会出现问题。
         path: "/search/:keyword?",
         component: () => import('@/pages/Search'),
         meta: {
             show: true
         },
    

    7、平台售卖属性的面包屑

    Search/index.vue

                
                
  • {{ attrValue.split(":")[1] }}x
  • // 自定义事件回调 // 当点击平台售卖属性,展示该属性数据 attrInfo(attr, attrValue) { // 接收子组件SearchSelector传来的平台售卖属性数据 // 根据API文档参数格式:["属性ID:属性值:属性名"] let props = `${attr.attrId}:${attrValue}:${attr.attrName}`; // 数组去重 if (this.searchParams.props.indexOf(props) == -1) { // 若数组没有重复,则追加到props数组 this.searchParams.props.push(props); } // 再次发请求 this.getData(); }, // 点击平台售卖属性的面包屑的x后删除对应的面包屑 removeAttr(index) { this.searchParams.props.splice(index,1); this.getData(); },

    Search/SearchSelector.vue

        
        
    {{ attr.attrName }}
    • ...... // 售卖属性值的点击事件 attrInfo(attr, attrValue) { this.$emit("attrInfo", attr, attrValue) },

    8、排序操作

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第4张图片

    • 综合排序和价格排序

    Search/index.vue

          searchParams: {
            ......
            // 排序:初始状态为综合:降序
            order: "1:desc",
            ......
          },
        };
    
    
                  
    
     ......
     computed: {
        isOne() {
          return this.searchParams.order.indexOf("1") != -1;
        },
        isTwo() {
          return this.searchParams.order.indexOf("2") != -1;
        },
    
    • 升序降序

    用阿里图标,生成链接,//at.alicdn.com/t/font_xxxxxx.css,前面加https:

    将此链接加入index.html,然后vue文件用对应类名即可引用 如iconfont icon-down

                    
  • 综合
  • 价格
  • ...... ...... 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; },
    • 点击

      点击价格 价格排序,再点击价格,降序变升序,再点价格,升序变降序

                    
                    
      • 综合
      • 价格 ...... changeOrder(flag) { // flag是形参,传来1|2代表点击的是综合|价格 // 获取起始的排序 let originOrder = this.searchParams.order; // 获取起始的综合|价格 let originFlag = this.searchParams.order.split(":")[0]; // 获取起始的升序|降序 let originSort = this.searchParams.order.split(":")[1]; // 准备一个新的order属性值 let newOrder = ''; if (flag == originFlag) { // 如果flag相同,则将升序变为降序,降序变为升序 newOrder = `${originFlag}:${originSort=="desc"?"asc":"desc"}`; } else { newOrder = `${flag}:${'desc'}`; } // 将新order给searchParams this.searchParams.order = newOrder; // 发请求获取当前排序设置的商品数据 this.getData(); },

    八、分页器

    是个全局组件,很多组件都用到分页器。

    略,时间紧迫

    九、Detail商品详情组件

    现将静态组件放 pages中,

    routes.js

    import Detail from '@/pages/Detail'
    export default [
    		......
            {
                path: "/home",
                component: Home,
                meta: { show: true }
            },
            {
                path: "/detail/:skuid",
                component: Detail,
                meta: { show: true }
            },
    

    Search/index.vue

              
              

    1、滚动行为

    使用前端路由,当切换到新路由时,想要页面滚动到顶部,或保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,让你自定义路由切换时页面如何滚动。

    当前项目问题:从搜索路由切换到详情路由,页面处于底部,想要页面处于顶部,就需要一些配置。

    router/index.js

    export default new VueRouter({
        routes,
        // 滚动行为
        // 当前项目问题:从搜索路由切换到详情路由,页面处于底部,想要页面处于顶部,就需要一些配置。
        scrollBehavior (to, from, savedPosition) {
            // 从搜索路由切换到详情路由,滚动条在最上方,即页面在顶部
            return { y: 0 }
        }
    })
    

    2、产品详情获取

    api/index.js

    // 获取Detail组件商品详情数据
    export const reqGoodsInfo = (skuId) => requests({
        url: `/item/${skuId}`,
        method: "get"
    })
    

    store/detail.js

    // detail 模块的小仓库
    import {
        reqGoodsInfo
    } from "@/api"
    
    // state:仓库存储数据的地方
    const state = {
        goodInfo: {},
    }
    // mutations:修改state的唯一手段
    const mutations = {
        GETGOODINFO(state, goodInfo) {
            state.goodInfo = goodInfo
        }
    }
    // actions:处理action,书写自己的业务逻辑、也可以处理异步
    const actions = {
        // 获取detail模块数据
        async getGoodInfo({
            commit
        }, skuId) {
            // 向服务器发请求
            let result = await reqGoodsInfo(skuId)
            if (result.code == 200) {
                commit("GETGOODINFO", result.data)
            }
        }
    }
    // getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
    const getters = {
    }
    
    // 对外暴露
    export default {
        state,
        mutations,
        actions,
        getters,
    }
    

    store/index.js

    import detail from './detail.js'
    export default new Vuex.Store({
        modules: {
    		.....
            detail
        }
    })
    

    3、产品详情展示动态数据

    store/detail.js

    // getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
    const getters = {
        categoryView(state) {
            // 需要加上 || {},否则若没请求到数据,则会报错
            return state.goodInfo.categoryView || {};
        },
        skuInfo(state) {
            return state.goodInfo.skuInfo || {};
        } 
    }
    

    Detail/index.vue

    import { mapGetters } from "vuex";
    ......
      computed: {
        ...mapGetters(["categoryView", "skuInfo"]),
      },
    

    展示略

    4、产品售卖属性值的排他操作(即切换高亮)

    实现:选择某属性值,其高亮,其余属性值不高亮。
    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第5张图片

    Detail/index.vue

              
              
    {{ spuSaleAttr.saleAttrName }}
    {{ spuSaleAttrValue.saleAttrValueName }}
    ...... methods: { // 点击产品售卖属性切换高亮 changeActive(saleAttrValue, arr) { // 先将所有售卖属性值取消高亮 arr.forEach(item => { item.isChecked = 0 }) // 将点击的售卖属性值设为高亮 saleAttrValue.isChecked = 1 } }

    5、放大镜

    Detail/index.vue

              
              
              
              
              
              
    

    两个组件,一个Zoom组件,一张图。一个ImageList组件,是小图商品图片列表。

    ImageList组件中有轮播效果,且每次展示3张图(swiper配置即可实现)。左右点击图片切换。

    • 实现点击哪个图片,哪个图片有边框

    Detail/ImageList/ImageList.vue

        ......  
    	
    ...... data() { return { currentIndex: "", } }, methods: { changecurrentIndex(index) { this.currentIndex = index; } }
    • 实现点击列表某小图显示对应大图

    Detail/ImageList/ImageList.vue

      methods: {
        changecurrentIndex(index) {
          // 当点击触发,修改响应式数据currentIndex为当前index
          this.currentIndex = index;
          // 通知兄弟组件Zoom,传当前索引值
          this.$bus.$emit('getIndex', this.currentIndex)
        }
      }
    

    Detail/Zoom/Zoom.vue

      name: "Zoom",
      data() {
        return {
          currentIndex: 0,
        }
      },
      props: ["skuImageList"],
      computed: {
        imgObj() {
          return this.skuImageList[this.currentIndex] || {}
        }
      },
      mounted() {
        // 全局事件总线,获取兄弟组件传递过来的索引值
        this.$bus.$on('getIndex', (index) => {
          this.currentIndex = index;
        })
      }
    
    • 放大镜效果

      Detail/Zoom/Zoom.vue

      
      
      
        methods: {
          // 鼠标移动触发
          handler(event) {
            let mask = this.$refs.mask;
            let big = this.$refs.big;
            // 获取mask应距盒子左边的距离 = 鼠标距盒子左边的距离 - mask宽度的一半
            let left = event.offsetX - mask.offsetWidth / 2;
            // 获取mask应距盒子顶部的距离
            let top = event.offsetY - mask.offsetHeight / 2;
            // 约束范围,防止mask移出盒子
            if (left <= 0) left = 0;
            if (left >= mask.offsetWidth) left = mask.offsetWidth;
            if (top <= 0) top = 0;
            if (top >= mask.offsetHeight) top = mask.offsetHeight;
            // 修改mask的left和top属性值
            mask.style.left = left + "px";
            mask.style.top = top + "px";
            // 对应修改大图的位置 (大图是原图的2倍)
            big.style.left = -2 * left + "px";
            big.style.top = -2 * top + "px";
          },
        },
      };
      

    6、购买产品个数的操作

    Detail/index.vue

                    
                    
                    +
                    -
    
    
        // 表单元素修改产品个数
        changeSkuNum(event) {
          // 用户输入进来的文本   event.target.value 获取当前文本框的值(由事件触发时)
          let value = event.target.value;
          // 如果用户输入的是非数字,或<1
          if (isNaN(value) || value < 1) {
            this.skuNum = 1;
          } else {
            // 为整数,不能是小数
            this.skuNum = parseInt(value);
          }
        },
    

    7、添加到购物车

    请求地址

    /api/cart/addToCart/{ skuId }/{ skuNum }

    请求方式

    POST

    参数类型

    参数名称

    类型

    是否必选

    描述

    skuID

    string

    Y

    商品ID

    skuNum

    string

    Y

    商品数量、正数代表增加、负数代表减少

    api/index.js

    // 添加到购物车
    export const reqAddOrUpdateShopCart = (skuId,skuNum) =>
        requests({
            url: `/cart/addToCart/${ skuId }/${ skuNum }`,
            method: "post",
        })
    

    store/detail.js

        // 添加到购物车
        async addOrUpdateShopCart({
            commit
        }, {skuId, skuNum}) {
            // 向服务器发请求,服务器写入数据成功,无数据返回,仓库无需存储数据
            let result = await reqAddOrUpdateShopCart(skuId, skuNum)
            // 200代表服务器写入数据成功
            if (result.code == 200) {
                return "ok"
            } else {
                return Promise.reject(new Error('faile'))
            }
        }
    

    Detail/index.vue

                  
    加入购物车 // 添加到购物车 async addshopcar() { // 把购买商品的信息通过请求的方式通知服务器,服务器进行相应的存储 // try...catch语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。 try { await this.$store.dispatch("addOrUpdateShopCart", { skuId: this.$route.params.skuid, skuNum: this.skuNum, }); // 若成功,路由跳转,并将产品信息带给 添加到购物车成功路由组件 console.log('略,看下节') } catch (error) { alert(error.message); } },

    十、添加到购物车成功 路由组件

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第6张图片

    创建pages/AddCartSuccess/index.vue

    router/routes.js

    import AddCartSuccess from '@/pages/AddCartSuccess'
    ......
            {
                path: "/detail/:skuid",
                component: Detail,
                meta: { show: true }
            },
            {
                name: "addcartsuccess",
                path: "/addcartsuccess",
                component: AddCartSuccess,
                meta: { show: true }
            }
    

    1、路由传递参数(结合会话存储)

    将Detail组件中的产品信息带给 添加到购物车成功AddCartSuccess路由组件。

    (因Detail组件已有数据,无需再在AddCartSuccess组件中重新发请求获取数据。所以用到会话存储。)

    一些简单的数据,通过query给路由组件传递,复杂的数据(如对象)通过会话存储。

    H5新增的浏览器存储功能:本地存储localStorage和会话存储sessionStorage。

    浏览器存储有哪些方法呢?主要有cookielocalStoragesessionStorage

    cookie属于文档对象模型DOM树根节点document,而 sessionStoragelocalStorage 属于浏览器对象模型BOM的对象window。

    cookie: h5之前,存储主要用cookies,缺点是在请求头上带着数据,导致流量增加。大小限制4k过期时间,当过了到期日期时,浏览器会自动删除该cookie,如果想删除一个cookie,只需要把它过期时间设置成过去的时间即可。如果不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。

    其中 sessionStoragelocalStorage 是 HTML5 Web Storage API 提供的

    • sessionStorage:会话存储。为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复),但是浏览器关闭,数据消失。
    • localStorage:本地存储。在浏览器关闭,然后重新打开后数据仍然存在。以键值对(Key-Value)的方式存储,永久存储,永不失效,除非手动删除。IE8+支持,每个域名限制5M。打开同域的新页面也能访问得到。

    sessionStorage、localStorage 可以存储数组、数字、对象等可以被序列化为字符串的内容。

    Detail/index.vue

            // 若成功,路由跳转,并将产品信息带给 添加到购物车成功路由组件
            // 会话存储,一些简单的数据,通过query给路由组件传递,复杂的数据(如对象)通过会话存储。
            // 因为sessionStorage只存储字符串,所以JSON.stringify()把js对象转换为字符串
            sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo));
            this.$router.push({
              name: "addcartsuccess",
              query: { skuNum: this.skuNum },
            });
    

    pages/AddCartSuccess/index.vue

                
              

    {{ skuInfo.skuName }}

    {{ skuInfo.skuDesc }} 数量:{{ $route.query.skuNum }}

    computed: { skuInfo() { // JSON.parse()将数据转换为js对象。 return JSON.parse(sessionStorage.getItem("SKUINFO")); }, },

    十一、购物车组件

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第7张图片

    router/routes.js

    import ShopCart from '@/pages/ShopCart'
    ......
            {
                name: "shopcart",
                path: "/shopcart",
                component: ShopCart,
                meta: { show: true }
            },
    

    pages/AddCartSuccess/index.vue

            
    查看商品详情 去购物车结算

    1、uuid游客身份获取购物车数据

    无需安装 uuid,因为有的包依赖uuid,所以uuid已存在。

    localStorage:本地存储,存 uuid

    创建src/utils文件夹,放功能模块

    创建src/utils/uuid_token.js

    import {
        v4 as uuidv4
    } from 'uuid'
    
    // 生成一个随机字符串,且每次执行不能发生变化,游客身份持久
    export const getUUID = () => {
        // 先从本地存储获取uuid游客身份
        let uuid_token = localStorage.getItem('UUIDTOKEN');
        // 如果本地存储没有uuid
        if (!uuid_token) {
            // 生成临时游客身份,即生成一个随机字符串
            uuid_token = uuidv4();
            // 本地存储游客身份
            localStorage.setItem('UUIDTOKEN', uuid_token);
        }
        // 返回游客身份uuid
        return uuid_token;
    }
    

    store/detail.js

    // 引入封装游客身份模块uuid(会封装一个随机字符串)
    import {getUUID} from '@/utils/uuid_token';
    
    // state:仓库存储数据的地方
    const state = {
        goodInfo: {},
        // 游客临时身份
        uuid_token: getUUID(),
    }
    

    在请求拦截器中(项目发请求之前)请求头添加一个字段:游客身份uuid

    api/ajax.js

    // 引入store
    import store from "@/store"
    ......
    // 请求拦截器:在发请求之前,请求拦截器可以检测到,在请求发出之前做一些事情;
    requests.interceptors.request.use((config) => {
        // config:配置对象,其有一个重要属性:header请求头
        // 请求头添加一个字段:userTempId:游客身份uuid,此字段已和后端协商好了。
        if (store.state.detail.uuid_token) {
            config.headers.userTempId = store.state.detail.uuid_token;
        }
        // 进度条开始动
        nprogress.start();
        return config;
    }
    

    2、购物车动态展示相应游客的数据

    向服务器发请求,获取对应游客身份的购物车列表数据,并展示数据。

    store/shopcart.js

    // shopcart 模块的小仓库
    import {
        reqCartList,
    } from "@/api"
    
    // state:仓库存储数据的地方
    const state = {
        cartList: {},
    }
    // mutations:修改state的唯一手段
    const mutations = {
        GETCARTLIST(state, cartList) {
            state.cartList = cartList
        },
    }
    // actions:处理action,书写自己的业务逻辑、也可以处理异步
    const actions = {
        // 获取产品信息
        async getCartList({
            commit
        }) {
            // 向服务器发请求
            let result = await reqCartList()
            if (result.code == 200) {
                commit("GETCARTLIST", result.data)
            }
        },
    
    }
    // getters:计算属性,用于简化仓库数据,让组件获取仓库的数据更方便
    const getters = {
        cartList(state) {
            // 需要加上 || {},否则若没请求到数据,则会报错
            return state.cartList[0] || {};
        },
    }
    // 对外暴露
    export default {
        state,
        mutations,
        actions,
        getters,
    }
    

    pages/ShopCart/index.vue

    获取购物车数据:

    import { mapGetters } from 'vuex';
    export default {
      name: "ShopCart",
      mounted() {
        this.getData();
      },
      methods: {
        getData() {
          this.$store.dispatch('getCartList')
        }
      },
      computed: {
        ...mapGetters(['cartList']),
        // 购物车数据
        cartInfoList() {
          return this.cartList.cartInfoList || []
        }
      }
    

    展示购物车数据:

            

    计算总价:

              总价(不含运费) :
              {{ totalPrice }}
    
    
      computed: {
        ...mapGetters(["cartList"]),
        // 购物车数据
        cartInfoList() {
          return this.cartList.cartInfoList || [];
        },
        // 计算购买产品的总价
        totalPrice() {
          let sum = 0;
          this.cartInfoList.forEach((item) => {
            sum += item.skuNum * item.skuPrice;
          });
          return sum;
        },
    

    全选操作:

          
    全选 computed: { // 判断底部复选框是否勾选(全部商品都选中,才勾选) isAllCheck() { // 遍历数组,若全部元素isChecked==1,则返回true,若有1个不等于1,则返回false return this.cartInfoList.every((item) => item.isChecked == 1); },

    3、修改购物车产品数量

    和Detail组件中的添加到购物车接口一样,

    对已有物品进行数量改动接口:/api/cart/addToCart/{ skuId }/{ skuNum }

    请求方式: POST

    参数类型

    参数名称

    类型

    是否必选

    描述

    skuID

    string

    Y

    商品ID

    skuNum

    string

    Y

    商品数量 正数代表增加、负数代表减少

    不同的是:Detail组件商品传入数据库是要买的商品数量。

    而ShopCart购物车组件,要增加或减少数据库中已有的商品数量。

    pages/ShopCart/index.vue

                -
                
                +
    
    
    import throttle from "lodash/throttle";
    ......
    	// 修改某一个产品的个数后,需要发请求存入数据库
        // 参数:type修改操作类型,disNum给服务器的变化量,cart对应产品
        // 但是,当用户行为过快,快速点击减-,浏览器反应不过来,会导致数量成负数。所以我们用到了节流。使用lodash插件。
        handler: throttle(async function (type, disNum, cart) {
          // 判断
          switch (type) {
            case "add":
              disNum = 1;
              break;
            case "minus":
              // 判断产品个数大于1,才能将-1传给服务器
              disNum = cart.skuNum > 1 ? -1 : 0;
              break;
            case "change":
              // 如果用户输入的是非数字,或<1
              if (isNaN(disNum) || disNum < 1) {
                this.disNum = 0;
              } else {
                // 为整数,不能是小数
                this.disNum = parseInt(disNum) - cart.skuNum;
              }
              break;
          }
          try {
            // 派发action
            await this.$store.dispatch("addOrUpdateShopCart", {
              skuId: cart.skuId,
              skuNum: disNum,
            });
            // 请求成功,再一次获取服务器最新数据
            this.getData();
          } catch (error) {}
        }, 500),
    

    4、删除购物车某个产品

    请求地址 /api/cart/deleteCart/{skuId}

    请求方式 DELETE

    参数类型

    参数名称

    类型

    是否必选

    描述

    skuId

    string

    Y

    商品id

    api/index.js

    // 删除购物车某个产品
    export const reqDeleteCartById = (skuId) => requests({
        url: `/cart/deleteCart/${skuId}`,
        method: "delete"
    })
    

    store/shopcart.js

    import {
        reqCartList,
        reqDeleteCartById,
    } from "@/api"
    

    pages/ShopCart/index.vue

              
  • 删除 // 删除购物车某个产品 async deleteCartById(cart) { try { // 发送请求删除某个产品 await this.$store.dispatch("deleteCartListBySkuId", cart.skuId); // 再次展示新的数据 this.getData(); } catch (error) { alert(error.message); } },
  • 5、修改某个产品选中状态

    请求地址: /api/cart/checkCart/{skuID}/{isChecked}

    请求方式:GET

    参数类型:

    参数名称

    类型

    是否必选

    描述

    skuID

    string

    Y

    商品ID

    isChecked

    string

    Y

    商品选中状态 0代表取消选中,1代表选中

    api/index.js

    // 修改某个产品选中状态
    export const reqUpdateCheckedById = (skuId,isChecked) => requests({
        url: `/cart/checkCart/${skuId}/${isChecked}`,
        method: "get"
    })
    

    store/shopcart.js

    import {
        reqCartList,
        reqDeleteCartById,
        reqUpdateCheckedById,
    } from "@/api"
    
    
        // 修改某个产品选中状态
        async updateCheckedById({
            commit
        }, {
            skuId,
            isChecked
        }) {
            let result = await reqUpdateCheckedById(skuId, isChecked);
            if (result.code == 200) {
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'));
            }
        }
    

    pages/ShopCart/index.vue

                
    
    
        // 修改某个产品选中状态
        async updateChecked(cart, event) {
          try {
            let isChecked = event.target.checked ? "1" : "0";
            await this.$store.dispatch("updateCheckedById", {
              skuId: cart.skuId,
              isChecked,
            });
            // 再次展示数据
            this.getData();
          } catch (error) {
            alert(error.message);
          }
        },
    

    6、删除全部选中的商品

    pages/ShopCart/index.vue

            
            全选
    
    
        // 删除全部选中的商品
        async deleteAllCheckedCart() {
          try {
            // 派发action
            await this.$store.dispatch("deleteAllCheckedCart");
            // 再次获取购物车列表数据
            this.getData();
          } catch (error) {
            alert(error.message);
          }
        },
    

    store/shopcart.js

    之前已写过 删除购物车某个产品 的action,删除全部选中商品的action 可以逐个派发删除购物车某个产品 的action。

        // 删除购物车某个产品
        async deleteCartListBySkuId({
            commit
        }, skuId) {
            // 向服务器发请求
            let result = await reqDeleteCartById(skuId);
            if (result.code == 200) {
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'));
            }
        },
    
    
        // 删除全部选中的商品
        deleteAllCheckedCart({
            // context:是第一个参数,包括commit【提交mutations修改state】、getters【计算属性】、dispatch【派发action】、state【当前仓库数据】等
            dispatch,
            getters
        }) {
            // 获取购物车中全部的产品(是一个数组)
            var allCart = getters.cartList.cartInfoList
            let PromiseAll = [];
            // 遍历,将选中的产品逐个删除
            allCart.forEach(item => {
                // 若当前产品被选中了,则删除
                // 派发了之前的 删除购物车某个产品这个action
                let promise = item.isChecked == 1 ? dispatch("deleteCartListBySkuId", item.skuId) : '';
                // 将每次返回的Promise添加到数组中
                PromiseAll.push(promise);
            });
            // 只有所有的promise都返回成功,返回结果才成功,若有一个失败,则返回为失败。
            return Promise.all(PromiseAll);
        }
    

    7、修改全部产品的勾选状态

    当选中全选按钮,则每个商品前的按钮都要选中。

    pages/ShopCart/index.vue

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

    store/shopcart.js

    之前已写过 修改某个产品选中状态 的action,修改全部产品勾选状态的action 可以逐个派发修改某个产品选中状态 的action。

        // 修改全部产品的勾选状态
        updateAllCartChecked({
            dispatch,
            getters,
        }, isChecked) {
            // 获取购物车中全部的产品(是一个数组)
            var allCart = getters.cartList.cartInfoList
            let PromiseAll = [];
            // 遍历,逐个修改商品的选中状态
            allCart.forEach(item => {
                // 派发了之前的 修改某个产品选中状态这个action
                let promise = dispatch("updateCheckedById", {skuId:item.skuId,isChecked});
                // 将每次返回的Promise添加到数组中
                PromiseAll.push(promise);
            });
            // 只有所有的promise都返回成功,返回结果才成功,若有一个失败,则返回为失败。
            return Promise.all(PromiseAll);
        }
    

    十二、登录注册组件

    路由之前已配置好。

    router/routes.js

            },
            {
                path: "/login",
                component: Login,
                meta: { show: false }
            }, 
            {
                path: "/register",
                component: Register,
                meta: { show: false }
            }
    

    components/Header/index.vue

                登录
                免费注册
    

    1、注册业务

    获取注册验证码,返回验证码。但是正常情况,后端将验证码发到用户手机,前端不会接收验证码(但是发验证码有条数限制,超出会付费,所以此接口验证码就不发给用户手机)。

    获取注册验证码接口:

    请求地址: /api/user/passport/sendCode/{phone}

    请求方式:GET

    api/index.js

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

    store/index.js

    import user from './user.js'
    ......
    export default new Vuex.Store({
        modules: {
            ......
            user,
        }
    })
    

    store/user.js

    // 登录和注册的模块 
    import {
        reqGetCode,
    } from "@/api"
    
    // state:仓库存储数据的地方
    const state = {
        code: '',
    }
    // mutations:修改state的唯一手段
    const mutations = {
        GETCODE(state, code) {
            state.code = code
        },
    
    }
    // actions:处理action,书写自己的业务逻辑、也可以处理异步
    const actions = {
        // 获取注册验证码,返回验证码,但是正常情况,后端将验证码发到用户手机,前端不会接收验证码(但是发验证码有条数限制,超出会付费,所以此接口验证码就不发给用户手机)
        async getCode({
            commit
        },phone) {
            // 向服务器发请求
            let result = await reqGetCode(phone)
            if (result.code == 200) {
                commit("GETCODE", result.data)
            }
        },
    }
    

    pages/Register/index.vue

         
    错误提示信息
    data() { return { // 手机号 phone: "", // 注册验证码 code: "", }; }, methods: { // 获取注册验证码 async getCode() { try { // 写法是es6的写法,其实就相当于:const phone = this.phone const { phone } = this; // 判断phone是否存在,若存在则派发action phone && (await this.$store.dispatch("getCode", phone)); // 将验证码的输入框值变为获取到的验证码值 this.code = this.$store.state.user.code; } catch (error) { alert(error.message) } },
    • 注册用户接口:

      点击完成注册按钮,进行用户注册。后端将用户账号和密码存入数据库。

    请求地址: /api/user/passport/register

    请求方式: POST

    参数类型

    参数名称

    类型

    是否必选

    描述

    phone

    string

    Y

    注册手机号

    password

    string

    Y

    密码

    code

    string

    Y

    验证码

    api/index.js

    // 注册用户
    export const reqUserRegister = (data) =>
        requests({
            url: '/user/passport/register',
            method: "post",
            data
        })
    

    store/user.js

    import {
        reqUserRegister,
    } from "@/api"
    ......
        // 注册用户
        async userRegister({commit},user) {
            // 向服务器发请求,后端将用户账号和密码存入数据库。
            let result = await reqUserRegister(user)
            if (result.code == 200) {
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'));
            }
        }
    

    pages/Register/index.vue

          
    // 点击完成注册按钮 async userRegister() { try { // const {xxx} = this es6语法,相当于const xxx = this.xxx const { phone, code, password, password1 } = this; // 当phone等数据存在再派发action (phone && code && password == password1) && await this.$store.dispatch('userRegister', { phone, code, password }); // 注册成功,跳转至登录页面 this.$router.push('/login'); } catch(error) { alert(error.message); } },

    2、登录业务(token)

    将用户名和密码发给服务器,判断数据库中是否有,有则登录成功。

    请求地址: /api/user/passport/login

    请求方式:POST

    参数类型

    参数名称

    类型

    是否必选

    描述

    phone

    string

    Y

    用户名

    password

    string

    Y

    密码

    api/index.js

    // 用户登录
    export const reqUserLogin = (data) =>
        requests({
            url: '/user/passport/login',
            method: "post",
            data
        })
    

    token 在计算机身份认证中是令牌的意思,在词法分析中是标记的意思。一般作为邀请、登录系统使用。

    登录成功后,服务器返回的数据中,有token: "d20386b3c2554014931a5d124733185f"

    客户端需要持久存储 token,客户端每次向服务端请求资源的时候需要带着服务端签发的 token。

    注意:在vuex中不能持久存token,因为在登录页面跳到首页,若在首页刷新后,并没有派发用户登录action,vuex中将无token。所以用localStorage本地存储。

    store/user.js

    // 登录和注册的模块 
    import {
    	......
        reqUserLogin,
    } from "@/api"
    ......
    const state = {
        ......
        token: localStorage.getItem('token'),
    }
    // mutations:修改state的唯一手段
    const mutations = {
        ......
        USERLOGIN(state, token) {
            state.token = token;
        }
    }
    const actions = {
        .....
        // 用户登录
        async userLogin({
            commit
        }, user) {
            // 向服务器发请求
            let result = await reqUserLogin(user);
            // 服务器下发token,用户唯一标识符。
            if (result.code == 200) {
                // 客户端需要持久存储 token,以后客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
                commit("USERLOGIN", result.data.token);
                // 持久化存储token,存储到本地
                localStorage.setItem("TOKEN", result.data.token);
                return 'ok'
            } else {
                return Promise.reject(new Error('faile'));
            }
        }
    

    pages/Login/index.vue

                  
                  
                
    
    
      data() {
        return {
          phone: "",
          password: "",
        };
      },
      methods: {
        // 用户登录
        async userLogin() {
          try {
            const { phone, password } = this;
            phone && password && (await this.$store.dispatch("userLogin", { phone, password }));
            // 登录成功,跳转到Home路由组件
            this.$router.push("/home");
          } catch (error) {
            alert(error.message);
          }
        },
    

    3、用户登录后携带token获取用户信息

    在请求头携带token

    api/ajax.js

    // 请求拦截器:在发请求之前,请求拦截器可以检测到,在请求发出之前做一些事情;
    requests.interceptors.request.use((config) => {
        // config:配置对象,其有一个重要属性:header请求头
        // 请求头添加一个字段:userTempId:游客身份uuid,此字段已和后端协商好了。
        // if (store.state.detail.uuid_token) {
        //     config.headers.userTempId = store.state.detail.uuid_token;
        // }
        // 请求头添加一个字段:token:用户标识
        if (store.state.user.token) {
            config.headers.token = store.state.user.token;
        }
        // 进度条开始动
        nprogress.start();
        return config;
    })
    

    api/index.js

    // 获取用户信息
    export const reqUserInfo = () => requests({
        url: '/user/passport/auth/getUserInfo',
        method: "get"
    })
    

    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");
    }
    

    store/user.js

    import {
      	......
        reqUserInfo,
    } from "@/api"
    
    const state = {
        code: '',
        token: getToken(),
        userInfo: {},
    }
    // mutations:修改state的唯一手段
    const mutations = {
        ......
        USERINFO(state, userInfo) {
            state.userInfo = userInfo;
        }
    }
    const actions = {
        ......
        // 获取用户信息
        async userInfo({
            commit
        }) {
            // 向服务器发请求
            let result = await reqUserInfo();
            if (result.code == 200) {
                commit("USERINFO", result.data);
                return 'ok';
            }
        }
    

    从登录页面登录后进入首页Home,Home组件需要发请求获取用户信息存到仓库。

    pages/Home/index.vue

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

    顶部Header组件登录注册按钮变为用户名和退出登录。

    components/Header/index.vue

              
              

    登录 免费注册

    {{ userName }} 退出登录

    computed: { // 用户的名字 userName() { return this.$store.state.user.userInfo.name; },

    但是此业务存在问题:若很多组件如Search等都需要获取用户信息,则需要写很多获取用户信息相关代码,比较繁琐。下面几节再优化。

    4、退出登录

    api/index.js

    // 退出登录
    export const reqLogout = () => requests({
        url: '/user/passport/logout',
        method: "get"
    })
    

    store/user.js

    const mutations = {   
        // 清除本地和仓库的token和用户信息
        CLEAR(state) {
            state.token = '';
            state.userInfo = {};
            removeToken();
        }
        
    const actions = {
    	// 退出登录
        async userLogout({commit}){
            // 向服务器发请求,通知服务器清除token
            let result = await reqLogout();
            if (result.code == 200) {
                // 提交给mutations清除本地和仓库的token和用户信息
                commit("CLEAR");
                return 'ok';
            } else {
                return Promise.reject(new Error('faile'));
            }
        },
    

    components/Header/index.vue

        // 退出登录
        // 通知服务器清除token,并清除本地和仓库的token和用户信息 并返回首页
        async logout() {
          try {
            await this.$store.dispatch("userLogout");
            this.$router.push("/home");
          } catch (error) {
            alert(error.message);
          }
        },
    

    5、导航守卫,优化登录业务

    用户已经登录,就不能回到 /login页面等。

    路由守卫知识笔记在在notion笔记—vue知识补充–路由中,此处就不解释了。

    router/routes.js

    let router = new VueRouter({
        ......
    })
        
    // 全局路由守卫,前置守卫(初始化时被调用,每次路由切换之前调用)
    router.beforeEach(async (to, from, next) => {
        // to:即将要进入的目标路由  from:当前导航正要离开的路由 next放行函数
        let token = store.state.user.token;
        // 用户信息
        let name = store.state.user.userInfo.name;
        // 当token存在,即用户登录了
        if (token) {
            // 当地址栏输入/login 用户登陆了,就不能再去登录页面,只能去首页
            if (to.path == '/login') {
                next('/home')
            } else {
                // 不是去/login,放行
                if (name) {
                    // 如果用户信息存在,则放行
                    next();
                } else {
                    // 用户信息不存在,派发action获取用户信息后再放行
                    try {
                        await store.dispatch("getUserInfo")
                        next();
                    } catch (error) {
                        // 如果不能获取用户信息,说明token失效,此时需要清除token,重新登录
                        await store.dispatch("userLogout")
                        next('/login');
                    }
                }
            }
        } else {
            // 未登录则放行,此处还有其他逻辑 后期再处理
            next();
        }
    })
    
    export default router;
    

    十三、Trade交易组件

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第8张图片

    router/routes.js

    import Trade from '@/pages/Trade'
    export default [
        	......
            {
                name: "trade",
                path: "/trade",
                component: Trade,
                meta: { show: true }
            },
    

    点击购物车的结算按钮跳至交易组件

    shopCart/index.vue

    结算
    

    1、获取用户地址信息和商品清单

    api/index.js

    // 获取用户地址信息
    export const reqAddressInfo = () => requests({
        url: '/user/userAddress/auth/findUserAddressList',
        method: "get"
    })
    
    // 获取交易页信息(商品清单)
    export const reqOrderInfo = () => requests({
        url: '/order/auth/trade',
        method: "get"
    })
    

    (最好使用视频上的账号密码,因为该账号数据库存有地址信息 。 13700000000 密:111111)

    store/trade.js

    import {
        reqAddressInfo,
        reqOrderInfo,
    } from "@/api"
    
    const state = {
        address: "",
        orderInfo: "",
    }
    const mutations = {
        GETUSERADDRESS(state, address) {
            state.address = address;
        },
        GETORDERINFO(state, orderInfo) {
            state.orderInfo = orderInfo;
        }
    }
    const actions = {
        // 获取用户地址信息
        async getUserAddress({
            commit
        }) {
            // 向服务器发请求
            let result = await reqAddressInfo()
            if (result.code == 200) {
                commit("GETUSERADDRESS", result.data)
            }
        },
        // 获取交易页信息(商品清单)
        async getOrderInfo({
            commit
        }) {
            // 向服务器发请求
            let result = await reqOrderInfo()
            if (result.code == 200) {
                commit("GETORDERINFO", result.data)
            }
        },
    

    pages/Trade/index.vue

      mounted() {
        // 获取用户地址信息
        this.$store.dispatch("getUserAddress");
        // 获取交易页信息(商品清单)
        this.$store.dispatch("getOrderInfo");
      },
    

    2、展示用户地址数据

    pages/Trade/index.vue

    展示用户地址数据,并实现选择地址,排他切换效果。

          
    收件人信息
    {{ address.consignee }}

    {{ address.fullAddress }} {{ address.phoneNum }} 默认地址

    computed: { ...mapState({ // 用户地址信息 addressInfo: (state) => state.trade.address, }), }, methods: { // 地址选中,排他切换 changeDefault(address, addressInfo) { addressInfo.forEach((item) => (item.isDefault = 0)); address.isDefault = 1; }, },

    交易页下方还需要展示交易最终选中的地址:

    // 交易最终选中的地址
    userDefaultAddress() {
      // find()查找并返回数组中第一个符合条件的元素
      return this.addressInfo.find((item) => item.isDefault == 1);
    },
    
    
    
    应付金额: ¥5399.00
    寄送至: {{ userDefaultAddress.fullAddress }} 收货人:{{ userDefaultAddress.consignee }} {{ userDefaultAddress.phoneNum }}

    注意,后端数据改了,需要设置默认地址,否则出错。

      mounted() {
        // 获取用户地址信息
        this.init();
      },
      computed: {
        ...mapState({
          // 用户地址信息
          addressInfo: (state) => state.trade.address,
          orderInfo: (state) => state.trade.orderInfo,
        }),
        // 交易最终选中的地址
        userDefaultAddress() {
          return this.addressInfo.find((item) => item.isDefault == 1);
        }
      },
      methods: {
        // 获取用户地址信息
        async init() {
          await this.$store.dispatch("getUserAddress");
          // 获取交易页信息(商品清单)
          await this.$store.dispatch("getOrderInfo");
          // 默认选中第一个地址(后端改了,改成了全是0,所以此处要设置默认地址)
          this.addressInfo[0].isDefault = 1;
        },
    

    3、交易信息展示

            
    商品清单
    • {{order.skuName}}

      7天无理由退货

    • {{order.orderPrice}}

    • X{{order.skuNum}}
    • ......
      买家留言:
      ......
    • {{orderInfo.totalNum}}件商品,总商品金额 ¥{{orderInfo.totalAmount}}.00
    • ......
      应付金额: ¥{{orderInfo.totalAmount}}.00
      data() { return { // 买家留言 msg: "", }

    4、提交订单

    当在交易页面提交订单按钮时,跳至支付页面,在跳前应该发请求获取支付页信息。

    请求地址: /api/order/auth/submitOrdertradeNo={tradeNo}

    请求方式: POST

    参数类型:

    参数名称

    类型

    是否必选

    描述

    traderNo

    string

    Y

    交易编号(拼接在路径中)

    consignee

    string

    Y

    收件人姓名

    consigneeTel

    string

    Y

    收件人电话

    deliveryAddress

    string

    Y

    收件地址

    paymentWay

    string

    Y

    支付方式(ONLINE代表在线)

    orderComment

    string

    Y

    订单备注

    orderDetailList

    Array

    Y

    存储多个商品对象的数组

    api/index.js

    // 提交订单
    export const reqSubmitOrder = (tradeNo, data) => requests({
        url: `/order/auth/submitOrder?tradeNo=${tradeNo}`,
        method: "post",
        data
    })
    
    从这里开始,不使用vuex了,因为需要学会无vuex时,将如何传递数据。(项目不大时,最好不用vuex)

    可是每个组件发请求时,要引入api文件中某请求函数比较麻烦,所以用到Vue原型:

    main.js

    // 引入 统一接口api中,全部请求函数
    // 好处是只需引一次,所有组件不用引就可使用。
    import * as API from '@api'
    
    new Vue({
      render: h => h(App),
      beforeCreate() {
        // 全局事件总线$bus的配置 
        Vue.prototype.$bus = this;
        // 统一接口api中,全部请求函数。好处是只配置一次,所有组件顺着原型即可使用。
        Vue.prototype.$API = API;
      },
      router,
      store,
    }).$mount("#app")
    

    提交订单,发请求,传递参数,接收订单号,传给支付页面

    pages/Trade/index.vue

          提交订单
    
    
      data() {
        return {
          // 买家留言
          msg: "",
          // 订单号
          orderId: "",
        };
      },
      ......
        // 提交订单,路由跳转至支付页面,跳之前向服务器发请求
        submitOrder() {
          // 参数:交易编码
          let { tradeNo } = this.orderInfo;
          // 参数:向服务器传用户选择的各种数据
          let data = {
            consignee: this.userDefaultAddress.consignee, // 最终收件人名字
            consigneeTel: this.userDefaultAddress.phoneNum, // 最终收件人手机
            deliveryAddress: this.userDefaultAddress.fullAddress, // 最终收件人地址
            paymentWay: "ONLINE", // 支付方式
            orderComment: this.img, // 买家留言
            orderDetailList: this.orderInfo.detailArrayList, // 商品清单
          };
          // 向服务器发请求
          let result = await this.$API.reqSubmitOrder(tradeNo, data);
          if (result.code == 200) {
            // 如果请求成功,则存订单号以供跳转至支付页面使用
            this.orderId = result.data;
            this.$router.push("/pay?orderId=" + this.orderId);
          } else {
            alert(result.data);
          }
        },
    

    (注意:跳转至支付页面可能会被其他同学影响,报错,因为使用的是相同账号,所以多试几次,就可以了)
    因为很多同学同时操作,所以可能出现:购物车商品被其他同学增加或减少,订单不能重复提交等问题。此时稍等一会,或多试几次(自己修改数量以防重复)即可。

    十四、支付页面

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第9张图片

    router/routes.js

    import Pay from '@/pages/Pay'
    ......
            {
                name: "pay",
                path: "/pay",
                component: Pay,
                meta: { show: true }
            },
    

    1、展示支付信息

    发请求获取支付信息,

    请求地址: /api/payment/weixin/createNative/{orderId}

    请求方式:GET

    参数类型:

    参数名称

    类型

    是否必选

    描述

    orderId

    string

    Y

    支付订单ID(通过提交订单得到)

    api/index.js

    // 获取支付信息
    export const reqPayInfo = (orderId) => requests({
        url: `payment/weixin/createNative/${orderId}`,
        method: "get"
    })
    

    pages/Pay/index.vue

      data() {
        return {
          // 支付信息
          payInfo: "",
        }
      },
      computed: {
        orderId() {
          return this.$route.query.orderId;
        },
      },
      // 不允许给生命周期函数加async|await,但是发请求需要用到async|await,所以this.getPayInfo();这样写,getPayInfo()写在methods中
      mounted() {
        this.getPayInfo();
      },
      methods: {
        // 获取支付信息 
        async getPayInfo() {
          let result = await this.$API.reqPayInfo(this.orderId);
          if(result.code == 200) {
            this.payInfo = result.data;
          }
        },
    
    
    		之内完成支付,超时订单会自动取消。订单号:{{
                  orderId
                }}
              应付金额:¥{{ payInfo.totalFee }}.00
    

    2、支付页面中使用ElementUI(按需引入)

    组件库:

    React和Vue都可用:antd(PC端适用); antd-mobile(移动端适用)

    Vue可用:ElementUI(PC端适用); vant(移动端适用)

    按需引入:

    安装ElementUI yarn add element-ui

    借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

    首先,安装 babel-plugin-component:

    npm install babel-plugin-component -D
    

    yarn add babel-plugin-component --dev
    

    然后,将 .babelrc (即babel.config.js)修改为:

    module.exports = {
      presets: ['@vue/cli-plugin-babel/preset'],
      plugins: [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          },
        ],
      ],
    };
    

    注意:配置文件发生变化,需要重启项目。

    接下来,如果你只希望引入部分组件,比如 Button 和 MessageBox,那么需要在 main.js 中写入以下内容:

    import Vue from 'vue';
    import { Button, MessageBox } from 'element-ui';
    import App from './App.vue';
    // 注册组件
    Vue.component(Button.name, Button);
    // 或写为 Vue.use(Button)
    
    // 注册组件的另一种写法,挂在原型上
    Vue.prototype.$msgbox = MessageBox;
    Vue.prototype.$alert = MessageBox.alert;
    

    (最好在vscode上安装个vue-help插件,elementUI提示插件,打el后面会有提示)

    使用:

    支付页面使用 elementUI 的 MessageBox 弹框组件 和Button组件

    实现点击 立即支付按钮,弹出支付框。

    MessageBox 弹框组件中,选使用 HTML 片段

    message 属性支持传入 HTML 片段。

    dangerouslyUseHTMLString属性设置为 true,message 就会被当作 HTML 片段处理。

      点击打开 Message Box
    ......
    
    

    pages/Pay/index.vue

         立即支付
    
    
        // elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
        open() {
            this.$alert('这是 HTML 片段', 'HTML 片段', {
              // message属性支持传入 HTML 片段
              dangerouslyUseHTMLString: true,
              // 中间布局
              center: true,
              // 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
              showCancelButton: true,
              // 取消按钮的文本内容
              cancelButtonText: "支付遇见问题",
              // 确定按钮的文本内容
              confirmButtonText: "已经支付成功",
              // MessageBox 是否显示右上角关闭按钮
              showClose: false,
              // 还有其他内容,下节再写
            });
          }
    

    十五、微信支付业务

    1、显示支付二维码

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第10张图片

    服务器返回payInfo支付信息数据中有 codeUrl: “weixin://wxpay/bizpayurlpr=ruk7767zz” 二维码生成插件根据此相应生成二维码。

    使用二维码生成插件:qrcode

    (去npm官网搜qrcode,有教程)

    用教程中 ES6/ES7写法:

    安装: yarn add qrcode

    在Pay支付页面,点击立即支付出现支付弹窗,内有支付二维码

    pages/Pay/index.vue

    // 引入二维码生成插件
    import QRCode from "qrcode";
    ......
        // elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
        async open() {
          // 使用qrcode插件生成二维码
          // QRCode.toDataURL()返回的是Promise
          // QRCode.toDataURL() 生成二维码图片的url
          let url = await QRCode.toDataURL(this.payInfo.codeUrl);
          // this.$alert(message, title),message参数是MessageBox 消息正文内容,title是MessageBox 标题
          // 第一个参数是字符串,但有js,所以要用模板字符串
          this.$alert(``, "请你微信支付", {
            // message属性支持传入 HTML 片段
            dangerouslyUseHTMLString: true,
            // 中间布局
            center: true,
            // 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
            showCancelButton: true,
            // 取消按钮的文本内容
            cancelButtonText: "支付遇见问题",
            // 确定按钮的文本内容
            confirmButtonText: "已经支付成功",
            // MessageBox 是否显示右上角关闭按钮
            showClose: false,
          });
        },
    

    2、若支付成功 跳至支付成功页面

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第11张图片

    请求地址:/api/payment/weixin/queryPayStatus/{orderId}

    请求方式:GET

    参数类型:

    参数名称

    类型

    是否必选

    描述

    orderId

    string

    Y

    支付订单ID

    api/index.js

    // 获取订单支付状态
    export const reqPayStatus = (orderId) => requests({
        url: `/payment/weixin/queryPayStatus/${orderId}`,
        method: "get"
    })
    

    当微信支付弹窗弹出,就要持续发获取订单支付状态的请求(持续是因为并不知道用户什么时候支付,所以需要持续发请求,直到能获取到支付状态),若接收到支付成功,则路由跳转至支付成功页面。若收到支付失败,则向用户显示提示信息。

    方法:加定时器,1s发1次获取订单支付状态的请求。

    pages/Pay/index.vue

        // 点击立即支付出现支付弹窗,微信支付业务等
        async open() {
          // 使用qrcode插件生成二维码
          // QRCode.toDataURL()返回的是Promise
          // QRCode.toDataURL() 生成二维码图片的url
          let url = await QRCode.toDataURL(this.payInfo.codeUrl);
          // elementUI 的 MessageBox 弹框组件,点击立即支付出现支付弹窗
          // this.$alert(message, title),message参数是MessageBox 消息正文内容,title是MessageBox 标题
          // 第一个参数是字符串,但有js,所以要用模板字符串
          this.$alert(``, "请你微信支付", {
            // message属性支持传入 HTML 片段
            dangerouslyUseHTMLString: true,
            // 中间布局
            center: true,
            // 是否显示取消按钮 (默认显示确认按钮,不显示取消按钮)
            showCancelButton: true,
            // 取消按钮的文本内容
            cancelButtonText: "支付遇见问题",
            // 确定按钮的文本内容
            confirmButtonText: "已经支付成功",
            // MessageBox 是否显示右上角关闭按钮
            showClose: false,
            // MessageBox 关闭前的回调,会暂停实例(即弹框)的关闭
            beforeClose: (type, instance, done) => {
              // type:区分取消|确定按钮;instance:当前组件实例;done:关闭弹出框的方法
              // 如果用户点击取消按钮
              if (type == "cancel") {
                alert("请联系管理员");
                // 清除定时器(即停止请求支付状态)
                clearInterval(this.timer);
                this.timer = null;
                // 关闭弹出框
                done();
              } else {
                // 用户点击了支付成功按钮
                // 判断是否支付了
                // if (this.code == 200) {  将此步关闭,是为了不付钱就能跳到支付成功页面,方便调试
                // 清除定时器(即停止请求支付状态)
                clearInterval(this.timer);
                this.timer = null;
                // 关闭弹出框
                done();
                // 跳至支付成功页面
                this.$router.push("/paysuccess");
                // }
              }
            },
          });
          // 持续发获取订单支付状态的请求(持续是因为并不知道用户什么时候支付,所以需要持续发请求,直到能获取到支付状态)
          // 加定时器,1s发1次获取订单支付状态的请求。
          // 若无定时器,则开启新定时器
          if (!this.timer) {
            this.timer = setInterval(async () => {
              // 发请求获取用户支付状态
              let result = await this.$API.reqPayStatus(this.orderId);
              // 如果支付成功
              if (result.code == 200) {
                // 清除定时器,即停止发请求
                clearInterval(this.timer);
                // 保存支付成功返回的code
                this.code = result.code;
                // 关闭弹出框
                this.$msgbox.close();
                // 跳至支付成功页面
                this.$router.push("/paysuccess");
              }
            }, 1000);
          }
        },
    

    router/routes.js

    import PaySuccess from '@/pages/PaySuccess'
    ......
            {
                name: "paysuccess",
                path: "/paysuccess",
                component: PaySuccess,
                meta: { show: true }
            }
    

    pages/PaySuccess/index.vue

              
              查看订单
              
              继续购物
    

    十六、Center个人中心组件

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第12张图片

    router/routes.js

    import Center from '@/pages/Center'
    ......
            {
                name: "center",
                path: "/center",
                component: Center,
                meta: { show: true }
            },
    

    Center路由组件中又有 路由组件:myOrder我的订单 和 groupOrder团购订单组件。

    1、个人中心二级路由搭建

    点击左侧 我的订单 或 团购订单。显示 相应的路由组件。

    router/routes.js

            {
                name: "center",
                path: "/center",
                component: Center,
                meta: { show: true },
                children: [
                    {
                        path: "myorder",
                        component: myOrder
                    },
                    {
                        path: "grouporder",
                        component: groupOrder
                    },
                    // 重定向,当进入center时,默认显示myorder
                    {
                        path: "/center",
                        redirect: '/center/myorder'
                    }
                ]
            },
    

    pages/Center/index.vue

            
            
    · 订单中心
    我的订单
    团购订单
    ......
    ......

    2、获取并展示myOrder组件我的订单列表

    获取我的订单列表 请求地址: /api/order/auth/{page}/{limit}

    请求方式: GET

    参数类型:

    参数名称

    类型

    是否必选

    描述

    page

    string

    Y

    页码

    limit

    string

    Y

    每页显示数量

    api/index.js

    // 获取我的订单列表
    export const reqMyorderList = (page, limit) => requests({
        url: `/order/auth/${page}/${limit}`,
        method: "get"
    })
    

    pages/Center/myOrder/index.vue

      data() {
        return {
          // 当前第几页
          page: 1,
          // 每一页展示数据个数
          limit: 3,
          // 我的订单数据
          myOrder: {},
        };
      },
      mounted() {
        // 获取我的订单数据
        this.getData();
      },
      methods: {
        // 获取我的订单数据
        async getData() {
          const { page, limit } = this;
          let result = await this.$API.reqMyorderList(page, limit);
          if (result.code == 200) {
            this.myOrder = result.data;
          }
        },
        // 获取组件分页器当前点击的那一页,(用到了自定义事件,子给父传数据)
        getPageNo(page) {
            // 修改page为当前点击的页
            this.page = page;
            // 获取最新点击页的数据
            this.getData();
        }
      },
    

    展示:

        

    (17)用户登录后的导航守卫(路由独享守卫与组件内守卫)

    例如,为防止,不经过某路由就直接 在地址栏输入本路由地址而跳转至 本路由,导致本路由获取不到前面路由的某些数据,所以给本路由加 路由独享守卫。

    **业务:只能从购物车跳到交易页面,只能从交易页面跳到支付页面。**而不能在地址栏直接更改地址跳转至交易页面等。

    路由独享守卫:

    router/routes.js

        {
            name: "trade",
            path: "/trade",
            component: Trade,
            meta: {
                show: true
            },
            // 路由独享守卫,
            beforeEnter: (to, from, next) => {
                if (from.path == '/shopcart') {
                    // 如果从shopcart路由跳到当前路由trade,则放行
                    next();
                } else {
                    // 不是从shopcart路由跳到当前路由trade,不放行,还是停留到原来的路由
                    next(false);
                }
            }
        },
        {
            name: "pay",
            path: "/pay",
            component: Pay,
            meta: {
                show: true
            },
            // 路由独享守卫,
            beforeEnter: (to, from, next) => {
                if (from.path == '/trade') {
                    // 如果从trade路由跳到当前路由pay,则放行
                    next();
                } else {
                    // 不是从trade路由跳到当前路由pay,不放行,还是停留到原来的路由
                    next(false);
                }
            }
        },
    

    组件内守卫:

    pages/PaySuccess/index.vue

      name: "PaySuccess",
      // 钩子函数
      // 在渲染该组件的对应路由被confirm前 调用
      // 不能获取组件实例this,因为守卫执行前,组件实例还没被创建
      beforeRouteEnter(to, from, next) {
        // 当然,也可以用路由独享守卫也可以实现此
        if (from.path == "/pay") {
          next();
        } else {
          next(false);
        }
      },
      // 在当前路由改变,该组件被复用时调用(即地址栏中的参数改变,展示对应数据的该组件时调用)
      // 举例:从 /foo/1 到 /foo/2 之前跳转时调用。由于会渲染同样的Foo组件,因此组件实例会被复用。
      // 可以访问组件实例的this
      // beforeRouteUpdate(to, from, next) {
      // },
      
      // 导航离开该组件对应路由时调用
      // 可以访问组件实例的this
      // beforeRouteLeave(to, from, next) {
      //   next();
      // }
    

    (18)未登录的导航守卫

    若用户未登录,则不能跳到我的订单、交易页面、个人中心。

    用到了全局路由守卫

    router/routes.js

    // 全局路由守卫,前置守卫(初始化时被调用,每次路由切换之前调用)
    router.beforeEach(async (to, from, next) => {
        // to:即将要进入的目标路由  from:当前导航正要离开的路由 next放行函数
        let token = store.state.user.token;
        // 用户信息
        let name = store.state.user.userInfo.name;
        // 当token存在,即用户登录了
        if (token) {
            // 当地址栏输入/login 用户登陆了,就不能再去登录页面,只能去首页
            if (to.path == '/login') {
                next('/home')
            } else {
                // 不是去/login,放行
                if (name) {
                    // 如果用户信息存在,则放行
                    next();
                } else {
                    // 用户信息不存在,派发action获取用户信息后再放行
                    try {
                        await store.dispatch("getUserInfo")
                        next();
                    } catch (error) {
                        // 如果不能获取用户信息,说明token失效,此时需要清除token,重新登录
                        await store.dispatch("userLogout")
                        next('/login');
                    }
                }
            }
        } else {
            // 未登录,不能去交易相关页面和个人中心
            let toPath = to.path;
            // indexOf()返回某个字符串值在字符串中首次出现的位置。如果要检索的字符串值没有出现,则该方法返回 -1。
            if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
                // 如果是去交易相关页面和个人中心,则不能去,需登录后方可跳转
                // 把未登录时候没有去成的路由,存储于地址栏中,以便登录后去。
                next('/login?redirect=' + toPath);
            } else {
                // 不是去交易相关页面和个人中心,则放行
                next();
            }
        }
    })
    

    pages/Login/index.vue

    // 登录成功,判断login路由有无query参数,若有则跳至参数对应的页面。若无则跳转到Home路由组件
    // login路由有query参数情况,是因为全局守卫中设置,若用户未登录则不能进交易等页面,此时存储要跳转的路由放在query上,登录后可进入。
    let toPath = this.$route.query.redirect || "/home";
    this.$router.push(toPath);
    

    (19)图片懒加载

    使用插件:vue-lazyload

    安装:yarn add vue-lazyload

    main.js

    // 引入图片懒加载插件
    import VueLazeload from 'vue-lazyload';
    // 引入懒加载默认图片(即真实图片没加载好之前,加载时显示的图片)
    import tp from '@/assets/images/1.png';
    // 注册插件
    Vue.use(VueLazeload, {
      // 懒加载默认图片,(即真实图片没加载好之前,加载时显示的图片)
      loading: tp,
    })
    

    pages/Search/index.vue

    
    
    

    温故 Vue插件 知识:

    src/plugins/myPlugins.js

    // Vue插件一定暴露一个对象
    let myPlugins = {};
    
    // vue提供install可供我们开发新的插件及全局注册组件等
    // install方法第一个参数是vue的构造器,第二个参数是可选的选项对象(即配置)
    myPlugins.install = function (Vue, options) {
        // 可以设置 Vue.prototype.$bus
        // Vue.directive
        // Vue.component
        // Vue.filter......
        // 例如:设置自定义指令
        Vue.directive(options.name, (el, binding) => {
            // 将元素的内容转为大写
            el.innerHTML = binding.value.toUpperCase();
        })
    }
    
    // 对外暴露
    exprot default myPlugins;
    

    main.js

    import myPlugins from '@/plugins/myPlugins';
    Vue.use(myPlugins,{
        name: 'upper',
    });
    

    Aba.vue

    
    

    ...... data() { return { msg: "abc" } }

    可能这时已经忘了自定义指令,去看看vue文档吧!很详细。

    (20)vee-validate插件 表单验证

    最好安装2版本的,yarn add vee-validate@2

    src/plugins/validate.js

    // 封装的表单验证插件
    
    // 引入vee-validate插件 表单验证
    import Vue from 'vue';
    import VeeValidate from 'vee-validate';
    // 中文提示信息
    import zh_CN from 'vee-validate/dist/locale/zh_CN';
    Vue.use(VeeValidate);
    
    // 表单验证
    // 第一个参数 'zh_CN',设置提示信息为中文(默认为英文)
    VeeValidate.Validator.localize('zh_CN', {
        messages: {
            ...zh_CN.messages,
            // 用于确认密码的提示信息,若两密码不同,则提示确认密码必须与密码相同
            is: (field) => `${field}必须与密码相同`,
        },
        attributes: {
            // 提示信息,无'手机号',则是phone无效;有'手机号',则是手机号无效
            phone: '手机号',
            code: '验证码',
            password: '密码',
            password1: '确认密码',
            agree: '协议'
        }
    })
    
    // 自定义校验规则
    VeeValidate.Validator.extend("agree", {
        validate: (value) => {
            return value;
        },
        getMessage: (field) => field + "必须同意",
    })
    

    main.js

    // 引入封装的表单验证插件
    import "@/plugins/validate";
    

    src/pages/Register/index.vue 注册组件

          
    {{ errors.first("phone") }}
    {{ errors.first("code") }}
    {{ errors.first("password") }}
    {{ errors.first("password1") }}
    同意协议并注册《尚品汇用户协议》 {{ errors.first("agree") }}
    // 点击完成注册按钮 async userRegister() { // 如果表单各项都验证成功,才能向服务器发请求进行注册 const success = await this.$validator.validateAll(); if (success) { try { // const {xxx} = this es6语法,相当于const xxx = this.xxx const { phone, code, password } = this; await this.$store.dispatch("userRegister", { phone, code, password, }); // 注册成功,跳转至登录页面 this.$router.push("/login"); } catch (error) { alert(error.message); }

    (21)路由懒加载

    路由懒加载 在Vue Router官方文档中,版本选3。

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

    结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载。

    router/routes.js

    无需像以下代码一样 全部引入路由组件

    // import Home from '@/pages/Home'
    // import Search from '@/pages/Search'
    // import Login from '@/pages/Login'
    // import Register from '@/pages/Register'
       ......
    
    
        {
            path: "/home",
            // component: Home,
            meta: {
                show: true
            }
        },
    

    只需在配置路由时写 component: () => import('@/pages/Home'),

        {
            path: "/home",
            // component: Home,
            component: () => import('@/pages/Home'),
            meta: {
                show: true
            }
        },
    

    (22)处理map文件

    打包,yarn build

    js文件夹中会有map文件。

    项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错,有了 map 文件就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。

    但是我们有时候并不需要这个文件,通过以下的设置可以让 Vue 打包的时候不生成 .map 文件,缩小生产环境的包大小。

    在vue.config.js设置让 Vue 打包的时候不生成 .map 文件

    module.exports = {
        productionSourceMap: false,
    

    (23)服务器

    将我们的前端的前台项目部署在自己买的服务器上。

    (架构有很多种,有所有资源等都放在一台服务器,好处是成本低;但是用户量多,一台服务器就不够了,所以一台服务器放Tomcat,一台服务器放数据库,一台服务器OSS放资源文件等。但是用户量更多,就多个放Tomcat的服务器,分布式缓存服务器、剥离前端和后端等等)

    中小型企业开发一个项目刚开始都是使用前后端分离架构,不考虑三高、微服务等。因为一开始并没有太大用户流量,所以使用成本低的前后端分离架构,是后续好扩展好改造的架构。

    1、购买服务器

    阿里云、腾讯云、

    腾讯云便宜,已买腾讯云的服务器。

    2、安全组

    在腾讯云搜 安全组,设置为这样:
    在这里插入图片描述

    因为我的服务器是Windows的,所以不需要xshell,远程控制,输入用户名密码即可控制服务器。

    3、nginx反向代理

    当用户访问服务器ip地址,(有域名则访问域名,无域名访问ip地址)

    当用户访问服务器ip地址,就会展现网站首页:需要如下配置:在nginx的conf文件夹中的nginx.conf配置:

       server {
            listen       80;
            server_name  localhost;
            location / {
                root   dist;
                index  index.html index.htm;
            }
    

    打包好的dist文件需要放下图此处,即放nginx文件夹中。

    vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第13张图片

    还需能访问到http://gmall-h5-api.atguigu.cn这台服务器的数据。(此服务器是尚硅谷的相应后端接口服务器)

    代理服务器就是位于发起请求的客户端与原始服务器端之间的一台跳板服务器,正向代理可以隐藏客户端,反向代理可以隐藏原始服务器。

    Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。

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

    记得,要打开nginx服务(查看任务管理器看有无nginx服务)若无,则双击nginx.exe或,通过命令行:nginx.exe即可。

    并且服务器要保持开机。

    这样就可以在很多地方通过 IP(没有域名前提下)访问我的项目了。

    上传本地文件到服务器,看腾讯云文档,很详细:

    上传文件

    1. 在本地计算机,使用快捷键【Windows + R】,打开【运行】窗口。
    2. 在弹出的【运行】窗口中,输入 mstsc,单击【确定】,打开【远程桌面连接】对话框。
    3. 在【远程桌面连接】对话框中,输入轻量应用服务器公网 IP 地址,单击【选项】。如下图所示:
      vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第14张图片
    4. 在【常规】页签中,输入轻量应用服务器公网 IP 地址和用户名 Administrator。如下图所示:
      vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第15张图片
    5. 选择【本地资源】页签,单击【详细信息】。如下图所示:
      vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第16张图片
    6. 在弹出的【本地设备和资源】窗口中,选择【驱动器】模块,勾选需要上传到 Windows 轻量应用服务器的文件所在的本地硬盘,单击【确定】。如下图所示:
      vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第17张图片
    7. 本地配置完成后,单击【连接】,远程登录 Windows 轻量应用服务器。
    8. 在 Windows 轻量应用服务器中,单击 img >【这台电脑】,即可以看到挂载到轻量应用服务器上的本地硬盘。如下图所示:
      vue实战项目-电商商城前台-(学习尚硅谷的)尚品汇_第18张图片
    9. 双击打开已挂载的本地硬盘,并将需要拷贝的本地文件复制到 Windows 轻量应用服务器的其他硬盘中,即完成文件上传操作。
      例如,将本地硬盘(E)中的 A 文件复制到 Windows 轻量应用服务器的 C: 盘中。

    下载文件:

    如需将 Windows 轻量应用服务器中的文件下载至本地计算机,也可以参照上传文件的操作,将所需文件从 Windows 轻量应用服务器中复制到挂载的本地硬盘中,即可完成文件下载操作。

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

    你可能感兴趣的:(面试,学习路线,阿里巴巴,android,前端,后端)

    {{ order.createTime }} 订单编号:{{ order.outTradeNo }}
    {{ cart.skuName }} x{{ cart.skuNum }} 售后申请
    {{ order.consignee }}
    • 总金额¥{{ order.totalAmount }}.00
    • 在线支付
    {{ order.orderStatusName }}