• Vue2+Vue3基础入门到实战项目(七)——智慧商城项目


    Vue 核心技术与实战 智慧商城

    接口文档:https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080

    演示地址:http://cba.itlike.com/public/mweb/#/

    01. 项目功能演示

    1.明确功能模块

    启动准备好的代码,演示移动端面经内容,明确功能模块

    在这里插入图片描述

    2.项目收获

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    02. 项目创建目录初始化

    vue-cli 建项目

    1.安装脚手架 (已安装)

    npm i @vue/cli -g
    
    • 1

    2.创建项目

    vue create hm-shopping
    
    • 1
    • 选项
    Vue CLI v5.0.8
    ? Please pick a preset:
      Default ([Vue 3] babel, eslint)
      Default ([Vue 2] babel, eslint)
    > Manually select features     选自定义
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 手动选择功能

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 选择vue的版本
      3.x
    > 2.x
    
    • 1
    • 2
    • 是否使用history模式

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 选择css预处理

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
    • 比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 选择校验的时机 (直接回车)

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 选择配置文件的生成方式 (直接回车)

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 是否保存预设,下次直接使用? => 不保存,输入 N

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 等待安装,项目初始化完成

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 启动项目
    npm run serve
    
    • 1

    03. 调整初始化目录结构

    强烈建议大家严格按照老师的步骤进行调整,为了符合企业规范

    为了更好的实现后面的操作,我们把整体的目录结构做一些调整。

    目标:

    1. 删除初始化的一些默认文件
    2. 修改没删除的文件
    3. 新增我们需要的目录结构

    1.删除文件

    • src/assets/logo.png
    • src/components/HelloWorld.vue
    • src/views/AboutView.vue
    • src/views/HomeView.vue

    2.修改文件

    main.js 不需要修改

    router/index.js

    删除默认的路由配置

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    const routes = [
    ]
    
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    App.vue

    <template>
      <div id="app">
        <router-view/>
      div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.新增目录

    • src/api 目录
      • 存储接口模块 (发送ajax请求接口的模块)
    • src/utils 目录
      • 存储一些工具模块 (自己封装的方法)

    目录效果如下:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    04. vant组件库及Vue周边的其他组件库

    组件库:第三方封装好了很多很多的组件,整合到一起就是一个组件库。

    https://vant-contrib.gitee.io/vant/v2/#/zh-CN/

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    比如日历组件、键盘组件、打分组件、下拉筛选组件等

    组件库并不是唯一的,常用的组件库还有以下几种:

    pc: element-ui element-plus iview ant-design

    移动:vant-ui Mint UI (饿了么) Cube UI (滴滴)

    05. 全部导入和按需导入的区别

    目标:明确 全部导入按需导入 的区别

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    区别:

    1.全部导入会引起项目打包后的体积变大,进而影响用户访问网站的性能

    2.按需导入只会导入你使用的组件,进而节约了资源

    06. 全部导入

    • 安装vant-ui
    yarn add vant@latest-v2
    
    • 1
    • 在main.js中
    import Vant from 'vant';
    import 'vant/lib/index.css';
    // 把vant中所有的组件都导入了
    Vue.use(Vant)
    
    • 1
    • 2
    • 3
    • 4
    • 即可使用
    主要按钮
    信息按钮
    
    • 1
    • 2

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    vant-ui提供了很多的组件,全部导入,会导致项目打包变得很大。

    07. 按需导入

    • 安装vant-ui
    yarn add vant@latest-v2
    
    • 1
    • 安装一个插件
    yarn add babel-plugin-import -D
    
    • 1
    • babel.config.js中配置
    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset'
      ],
      plugins: [
        ['import', {
          libraryName: 'vant',
          libraryDirectory: 'es',
          style: true
        }, 'vant']
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 按需加载,在main.js
    import { Button, Icon } from 'vant'
    
    Vue.use(Button)
    Vue.use(Icon)
    
    • 1
    • 2
    • 3
    • 4
    • app.vue中进行测试
    <van-button type="primary">主要按钮</van-button>
    <van-button type="info">信息按钮</van-button>
    <van-button type="default">默认按钮</van-button>
    <van-button type="warning">警告按钮</van-button>
    <van-button type="danger">危险按钮</van-button>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 把引入组件的步骤抽离到单独的js文件中比如 utils/vant-ui.js
    import { Button, Icon } from 'vant'
    
    Vue.use(Button)
    Vue.use(Icon)
    
    • 1
    • 2
    • 3
    • 4

    main.js中进行导入

    // 导入按需导入的配置文件
    import '@/utils/vant-ui'
    
    • 1
    • 2

    08. 项目中的vw适配

    官方说明:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage

    yarn add postcss-px-to-viewport@1.1.1 -D
    
    • 1
    • 项目根目录, 新建postcss的配置文件postcss.config.js
    // postcss.config.js
    module.exports = {
      plugins: {
        'postcss-px-to-viewport': {
          viewportWidth: 375,
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    viewportWidth:设计稿的视口宽度

    1. vant-ui中的组件就是按照375的视口宽度设计的
    2. 恰好面经项目中的设计稿也是按照375的视口宽度设计的,所以此时 我们只需要配置375就可以了
    3. 如果设计稿不是按照375而是按照750的宽度设计,那此时这个值该怎么填呢?

    09. 路由配置 - 一级路由

    但凡是单个页面,独立展示的,都是一级路由

    路由设计:

    • 登录页
    • 首页架子
      • 首页 - 二级
      • 分类页 - 二级
      • 购物车 - 二级
      • 我的 - 二级
    • 搜索页
    • 搜索列表页
    • 商品详情页
    • 结算支付页
    • 我的订单页

    router/index.js 配置一级路由,新建对应的页面文件

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Layout from '@/views/layout'
    import Search from '@/views/search'
    import SearchList from '@/views/search/list'
    import ProDetail from '@/views/prodetail'
    import Login from '@/views/login'
    import Pay from '@/views/pay'
    import MyOrder from '@/views/myorder'
    
    Vue.use(VueRouter)
    
    const router = new VueRouter({
      routes: [
        {
          path: '/login',
          component: Login
        },
        {
          path: '/',
          component: Layout
        },
        {
          path: '/search',
          component: Search
        },
        {
          path: '/searchlist',
          component: SearchList
        },
        {
          path: '/prodetail/:id',
          component: ProDetail
        },
        {
          path: '/pay',
          component: Pay
        },
        {
          path: '/myorder',
          component: MyOrder
        }
      ]
    })
    
    export default router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    10. 路由配置-tabbar标签页

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar

    vant-ui.js 引入组件

    import { Tabbar, TabbarItem } from 'vant'
    Vue.use(Tabbar)
    Vue.use(TabbarItem)
    
    • 1
    • 2
    • 3

    layout.vue

    1. 复制官方代码
    2. 修改显示文本及显示的图标
    3. 配置高亮颜色
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    11. 路由配置 - 二级路由

    1. router/index.js配置二级路由
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Layout from '@/views/layout'
    import Search from '@/views/search'
    import SearchList from '@/views/search/list'
    import ProDetail from '@/views/prodetail'
    import Login from '@/views/login'
    import Pay from '@/views/pay'
    import MyOrder from '@/views/myorder'
    
    import Home from '@/views/layout/home'
    import Category from '@/views/layout/category'
    import Cart from '@/views/layout/cart'
    import User from '@/views/layout/user'
    
    Vue.use(VueRouter)
    
    const router = new VueRouter({
      routes: [
        {
          path: '/login',
          component: Login
        },
        {
          path: '/',
          component: Layout,
          redirect: '/home',
          children: [
            {
              path: 'home',
              component: Home
            },
            {
              path: 'category',
              component: Category
            },
            {
              path: 'cart',
              component: Cart
            },
            {
              path: 'user',
              component: User
            }
          ]
        },
        {
          path: '/search',
          component: Search
        },
        {
          path: '/searchlist',
          component: SearchList
        },
        {
          path: '/prodetail/:id',
          component: ProDetail
        },
        {
          path: '/pay',
          component: Pay
        },
        {
          path: '/myorder',
          component: MyOrder
        }
      ]
    })
    
    export default router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    1. 准备对应的组件文件

      • layout/home.vue
      • layout/category.vue
      • layout/cart.vue
      • layout/user.vue
    2. layout.vue 配置路由出口, 配置 tabbar

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    12. 登录页静态布局

    (1) 准备工作

    1. 新建 styles/common.less 重置默认样式
    // 重置默认样式
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    // 文字溢出省略号
    .text-ellipsis-2 {
      overflow: hidden;
      -webkit-line-clamp: 2;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-box-orient: vertical;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. main.js 中导入应用
    import '@/styles/common.less'
    
    • 1
    1. 将准备好的一些图片素材拷贝到 assets 目录【备用】

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    (2) 登录静态布局

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    使用组件

    • van-nav-bar

    vant-ui.js 注册

    import { NavBar } from 'vant'
    Vue.use(NavBar)
    
    • 1
    • 2

    Login.vue 使用

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    添加通用样式

    styles/common.less 设置导航条,返回箭头颜色

    // 设置导航条 返回箭头 颜色
    .van-nav-bar {
      .van-icon-arrow-left {
        color: #333;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    13. request模块 - axios封装

    接口文档:https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080

    演示地址:http://cba.itlike.com/public/mweb/#/

    基地址:http://cba.itlike.com/public/index.php?s=/api/

    我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址,请求响应拦截器等等)

    一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用

    目标:将 axios 请求方法,封装到 request 模块

    1. 安装 axios
    npm i axios
    
    • 1
    1. 新建 utils/request.js 封装 axios 模块

      利用 axios.create 创建一个自定义的 axios 来使用

      http://www.axios-js.com/zh-cn/docs/#axios-create-config

    /* 封装axios用于发送请求 */
    import axios from 'axios'
    
    // 创建一个新的axios实例
    const request = axios.create({
      baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
      timeout: 5000
    })
    
    // 添加请求拦截器
    request.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      return config
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error)
    })
    
    // 添加响应拦截器
    request.interceptors.response.use(function (response) {
      // 对响应数据做点什么
      return response.data
    }, function (error) {
      // 对响应错误做点什么
      return Promise.reject(error)
    })
    
    export default request
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. 获取图形验证码,请求测试
    import request from '@/utils/request'
    export default {
      name: 'LoginPage',
      async created () {
        const res = await request.get('/captcha/image')
        console.log(res)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    14. 图形验证码功能完成

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 准备数据,获取图形验证码后存储图片路径,存储图片唯一标识
    async created () {
      this.getPicCode()
    },
    data () {
      return {
        picUrl: '',
        picKey: ''
      }
    },
    methods: {
      // 获取图形验证码
      async getPicCode () {
        const { data: { base64, key } } = await request.get('/captcha/image')
        this.picUrl = base64
        this.picKey = key
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 动态渲染图形验证码,并且点击时要重新刷新验证码
    
    
    • 1

    15. 封装api接口 - 图片验证码接口

    **1.目标:**将请求封装成方法,统一存放到 api 模块,与页面分离

    2.原因:以前的模式

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 页面中充斥着请求代码

    • 可阅读性不高

    • 相同的请求没有复用请求没有统一管理

    3.期望:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 请求与页面逻辑分离
    • 相同的请求可以直接复用请求
    • 进行了统一管理

    4.具体实现

    新建 api/login.js 提供获取图形验证码 Api 函数

    import request from '@/utils/request'
    
    // 获取图形验证码
    export const getPicCode = () => {
      return request.get('/captcha/image')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    login/index.vue页面中调用测试

    async getPicCode () {
      const { data: { base64, key } } = await getPicCode()
      this.picUrl = base64
      this.picKey = key
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5

    16. toast 轻提示

    https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast

    两种使用方式

    1. 导入调用 ( 组件内非组件中均可 )
    import { Toast } from 'vant';
    Toast('提示内容');
    
    • 1
    • 2
    1. 通过this直接调用 ( **组件内 **)

    main.js 注册绑定到原型

    import { Toast } from 'vant';
    Vue.use(Toast)
    
    • 1
    • 2
    this.$toast('提示内容')
    
    • 1

    17. 短信验证倒计时功能

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    (1) 倒计时基础效果

    1. 准备 data 数据
    data () {
      return {
        totalSecond: 60, // 总秒数
        second: 60, // 倒计时的秒数
        timer: null // 定时器 id
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 给按钮注册点击事件
    
    
    • 1
    • 2
    • 3
    1. 开启倒计时时
    async getCode () {
      if (!this.timer && this.second === this.totalSecond) {
        // 开启倒计时
        this.timer = setInterval(() => {
          this.second--
    
          if (this.second < 1) {
            clearInterval(this.timer)
            this.timer = null
            this.second = this.totalSecond
          }
        }, 1000)
    
        // 发送请求,获取验证码
        this.$toast('发送成功,请注意查收')
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 离开页面销毁定时器
    destroyed () {
      clearInterval(this.timer)
    }
    
    • 1
    • 2
    • 3

    (2) 验证码请求校验处理

    1. 输入框 v-model 绑定变量
    data () {
      return {
        mobile: '', // 手机号
        picCode: '' // 图形验证码
      }
    },
        
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. methods中封装校验方法
    // 校验输入框内容
    validFn () {
      if (!/^1[3-9]\d{9}$/.test(this.mobile)) {
        this.$toast('请输入正确的手机号')
        return false
      }
      if (!/^\w{4}$/.test(this.picCode)) {
        this.$toast('请输入正确的图形验证码')
        return false
      }
      return true
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 请求倒计时前进行校验
    // 获取短信验证码
    async getCode () {
      if (!this.validFn()) {
        return
      }
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    (3) 封装接口,请求获取验证码

    1. 封装接口 api/login.js
    // 获取短信验证码
    export const getMsgCode = (captchaCode, captchaKey, mobile) => {
      return request.post('/captcha/sendSmsCaptcha', {
        form: {
          captchaCode,
          captchaKey,
          mobile
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 调用接口,添加提示
    // 获取短信验证码
    async getCode () {
      if (!this.validFn()) {
        return
      }
    
      if (!this.timer && this.second === this.totalSecond) {
        // 发送请求,获取验证码
        await getMsgCode(this.picCode, this.picKey, this.mobile)
        this.$toast('发送成功,请注意查收')
        
        // 开启倒计时
        ...
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    18. 封装api接口 - 登录功能

    api/login.js 提供登录 Api 函数

    // 验证码登录
    export const codeLogin = (mobile, smsCode) => {
      return request.post('/passport/login', {
        form: {
          isParty: false,
          mobile,
          partyData: {},
          smsCode
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    login/index.vue 登录功能

    
    
    
    data () {
      return {
        msgCode: '',
      }
    },
    methods: {
      async login () {
        if (!this.validFn()) {
          return
        }
        if (!/^\d{6}$/.test(this.msgCode)) {
          this.$toast('请输入正确的手机验证码')
          return
        }
        await codeLogin(this.mobile, this.msgCode)
        this.$router.push('/')
        this.$toast('登录成功')
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    19. 响应拦截器统一处理错误提示

    响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 200 默认给提示,抛出错误

    utils/request.js

    import { Toast } from 'vant'
    
    ...
    
    // 添加响应拦截器
    request.interceptors.response.use(function (response) {
      const res = response.data
      if (res.status !== 200) {
        Toast(res.message)
        return Promise.reject(res.message)
      }
      // 对响应数据做点什么
      return res
    }, function (error) {
      // 对响应错误做点什么
      return Promise.reject(error)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    20. 将登录权证信息存入 vuex

    1. 新建 vuex user 模块 store/modules/user.js
    export default {
      namespaced: true,
      state () {
        return {
          userInfo: {
            token: '',
            userId: ''
          },
        }
      },
      mutations: {},
      actions: {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 挂载到 vuex 上
    import Vue from 'vue'
    import Vuex from 'vuex'
    import user from './modules/user'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      modules: {
        user,
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 提供 mutations
    mutations: {
      setUserInfo (state, obj) {
        state.userInfo = obj
      },
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 页面中 commit 调用
    // 登录按钮(校验 & 提交)
    async login () {
      if (!this.validFn()) {
        return
      }
      ...
      const res = await codeLogin(this.mobile, this.msgCode)
      this.$store.commit('user/setUserInfo', res.data)
      this.$router.push('/')
      this.$toast('登录成功')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    21. vuex持久化处理

    1. 新建 utils/storage.js 封装方法
    const INFO_KEY = 'hm_shopping_info'
    
    // 获取个人信息
    export const getInfo = () => {
      const result = localStorage.getItem(INFO_KEY)
      return result ? JSON.parse(result) : {
        token: '',
        userId: ''
      }
    }
    
    // 设置个人信息
    export const setInfo = (info) => {
      localStorage.setItem(INFO_KEY, JSON.stringify(info))
    }
    
    // 移除个人信息
    export const removeInfo = () => {
      localStorage.removeItem(INFO_KEY)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. vuex user 模块持久化处理
    import { getInfo, setInfo } from '@/utils/storage'
    export default {
      namespaced: true,
      state () {
        return {
          userInfo: getInfo()
        }
      },
      mutations: {
        setUserInfo (state, obj) {
          state.userInfo = obj
          setInfo(obj)
        }
      },
      actions: {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    22. 优化:添加请求 loading 效果

    1. 请求时,打开 loading
    // 添加请求拦截器
    request.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      Toast.loading({
        message: '请求中...',
        forbidClick: true,
        loadingType: 'spinner',
        duration: 0
      })
      return config
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 响应时,关闭 loading
    // 添加响应拦截器
    request.interceptors.response.use(function (response) {
      const res = response.data
      if (res.status !== 200) {
        Toast(res.message)
        return Promise.reject(res.message)
      } else {
        // 清除 loading 中的效果
        Toast.clear()
      }
      // 对响应数据做点什么
      return res
    }, function (error) {
      // 对响应错误做点什么
      return Promise.reject(error)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    23. 登录访问拦截 - 路由前置守卫

    目标:基于全局前置守卫,进行页面访问拦截处理

    说明:智慧商城项目,大部分页面,游客都可以直接访问, 如遇到需要登录才能进行的操作,提示并跳转到登录

    但是:对于支付页,订单页等,必须是登录的用户才能访问的,游客不能进入该页面,需要做拦截处理

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    路由导航守卫 - 全局前置守卫

    1.所有的路由一旦被匹配到,都会先经过全局前置守卫

    2.只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容

    router.beforeEach((to, from, next) => {
      // 1. to   往哪里去, 到哪去的路由信息对象  
      // 2. from 从哪里来, 从哪来的路由信息对象
      // 3. next() 是否放行
      //    如果next()调用,就是放行
      //    next(路径) 拦截到某个路径页面
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    const authUrl = ['/pay', '/myorder']
    router.beforeEach((to, from, next) => {
      const token = store.getters.token
      if (!authUrl.includes(to.path)) {
        next()
        return
      }
    
      if (token) {
        next()
      } else {
        next('/login')
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    24. 首页 - 静态结构准备

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 静态结构和样式 layout/home.vue
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    1. 新建components/GoodsItem.vue
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    1. 组件按需引入
    import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'
    
    Vue.use(GridItem)
    Vue.use(Search)
    Vue.use(Swipe)
    Vue.use(SwipeItem)
    Vue.use(Grid)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    25. 首页 - 动态渲染

    1. 封装准备接口 api/home.js
    import request from '@/utils/request'
    
    // 获取首页数据
    export const getHomeData = () => {
      return request.get('/page/detail', {
        params: {
          pageId: 0
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 页面中请求调用
    import GoodsItem from '@/components/GoodsItem.vue'
    import { getHomeData } from '@/api/home'
    export default {
      name: 'HomePage',
      components: {
        GoodsItem
      },
      data () {
        return {
          bannerList: [],
          navList: [],
          proList: []
        }
      },
      async created () {
        const { data: { pageData } } = await getHomeData()
        this.bannerList = pageData.items[1].data
        this.navList = pageData.items[3].data
        this.proList = pageData.items[6].data
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 轮播图、导航、猜你喜欢渲染
    
    
      
        
      
    
    
    
    
      
    
        
    
    

    —— 猜你喜欢 ——

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 商品组件内,动态渲染
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    26. 搜索 - 静态布局准备

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 静态结构和代码
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    1. 组件按需导入
    import { Icon } from 'vant'
    Vue.use(Icon)
    
    • 1
    • 2

    27. 搜索 - 历史记录 - 基本管理

    1. data 中提供数据,和搜索框双向绑定 (实时获取用户内容)
    data () {
      return {
        search: ''
      }
    }
    
    
      
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 准备假数据,进行基本的历史纪录渲染
    data () {
      return {
        ...
        history: ['手机', '空调', '白酒', '电视']
      }
    },
        
    
    ...
    {{ item }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 点击搜索,或者下面搜索历史按钮,都要进行搜索历史记录更新 (去重,新搜索的内容置顶)
    搜索
    {{ item }}
    goSearch (key) { const index = this.history.indexOf(key) if (index !== -1) { this.history.splice(index, 1) } this.history.unshift(key) this.$router.push(`/searchlist?search=${key}`) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 清空历史
    
    
    clear () {
      this.history = []
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    28. 搜索 - 历史记录 - 持久化

    1. 持久化到本地 - 封装方法
    const HISTORY_KEY = 'hm_history_list'
    
    // 获取搜索历史
    export const getHistoryList = () => {
      const result = localStorage.getItem(HISTORY_KEY)
      return result ? JSON.parse(result) : []
    }
    
    // 设置搜索历史
    export const setHistoryList = (arr) => {
      localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 页面中调用 - 实现持久化
    data () {
      return {
        search: '',
        history: getHistoryList()
      }
    },
    methods: {
      goSearch (key) {
        ...
        setHistoryList(this.history)
        this.$router.push(`/searchlist?search=${key}`)
      },
      clear () {
        this.history = []
        setHistoryList([])
        this.$toast.success('清空历史成功')
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    29. 搜索列表 - 静态布局

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    30. 搜索列表 - 动态渲染

    (1) 搜索关键字搜索

    1. 计算属性,基于query 解析路由参数
    computed: {
      querySearch () {
        return this.$route.query.search
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 根据不同的情况,设置输入框的值
    
    
    • 1
    • 2
    • 3
    • 4
    1. api/product.js 封装接口,获取搜索商品
    import request from '@/utils/request'
    
    // 获取搜索商品列表数据
    export const getProList = (paramsObj) => {
      const { categoryId, goodsName, page } = paramsObj
      return request.get('/goods/list', {
        params: {
          categoryId,
          goodsName,
          page
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 页面中基于 goodsName 发送请求,动态渲染
    data () {
      return {
        page: 1,
        proList: []
      }
    },
    async created () {
      const { data: { list } } = await getProList({
        goodsName: this.querySearch,
        page: this.page
      })
      this.proList = list.data
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    (2) 分类id搜索

    1 封装接口 api/category.js

    import request from '@/utils/request'
    
    // 获取分类数据
    export const getCategoryData = () => {
      return request.get('/category/list')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2 分类页静态结构

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124

    3 搜索页,基于分类 ID 请求

    async created () {
      const { data: { list } } = await getProList({
        categoryId: this.$route.query.categoryId,
        goodsName: this.querySearch,
        page: this.page
      })
      this.proList = list.data
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    31. 商品详情 - 静态布局

    静态结构 和 样式

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252

    LazyloadVue 指令,使用前需要对指令进行注册。

    import { Lazyload } from 'vant'
    Vue.use(Lazyload)
    
    • 1
    • 2

    32. 商品详情 - 动态渲染介绍

    1. 动态路由参数,获取商品 id
    computed: {
      goodsId () {
        return this.$route.params.id
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 封装 api 接口 api/product.js
    // 获取商品详情数据
    export const getProDetail = (goodsId) => {
      return request.get('/goods/detail', {
        params: {
          goodsId
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 一进入页面发送请求,获取商品详情数据
    data () {
      return {
        images: [
          'https://img01.yzcdn.cn/vant/apple-1.jpg',
          'https://img01.yzcdn.cn/vant/apple-2.jpg'
        ],
        current: 0,
        detail: {},
      }
    },
    
    async created () {
      this.getDetail()
    },
    
    methods: {
      ...
      async getDetail () {
        const { data: { detail } } = await getProDetail(this.goodsId)
        this.detail = detail
        this.images = detail.goods_images
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 动态渲染
    ¥{{ detail.goods_price_min }} ¥{{ detail.goods_price_max }}
    已售{{ detail.goods_sales }}件
    {{ detail.goods_name }}
    七天无理由退货 48小时发货
    商品描述
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    33. 商品详情 - 动态渲染评价

    1. 封装接口 api/product.js
    // 获取商品评价
    export const getProComments = (goodsId, limit) => {
      return request.get('/comment/listRows', {
        params: {
          goodsId,
          limit
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 页面调用获取数据
    import defaultImg from '@/assets/default-avatar.png'
    
    data () {
      return {
        ...
        total: 0,
        commentList: [],
        defaultImg
    },
    
    async created () {
      ...
      this.getComments()
    },
        
    async getComments () {
      const { data: { list, total } } = await getProComments(this.goodsId, 3)
      this.commentList = list
      this.total = total
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 动态渲染评价
    
    
    商品评价 ({{ total }}条)
    查看更多
    {{ item.user.nick_name }}
    {{ item.content }}
    {{ item.create_time }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    34. 加入购物车 - 唤起弹窗

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 按需导入 van-action-sheet
    import { ActionSheet } from 'vant'
    Vue.use(ActionSheet)
    
    • 1
    • 2
    1. 准备 van-action-sheet 基本结构
    
    	111
    
        
    data () {
      return {
        ...
        mode: 'cart'
        showPannel: false
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 注册点击事件,点击时唤起弹窗
    加入购物车
    立刻购买
    addFn () { this.mode = 'cart' this.showPannel = true }, buyFn () { this.mode = 'buyNow' this.showPannel = true }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 完善结构
    
      
    ¥ 9.99
    库存 55
    数量 数字框占位
    加入购物车
    立刻购买
    该商品已抢完
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    .product {
      .product-title {
        display: flex;
        .left {
          img {
            width: 90px;
            height: 90px;
          }
          margin: 10px;
        }
        .right {
          flex: 1;
          padding: 10px;
          .price {
            font-size: 14px;
            color: #fe560a;
            .nowprice {
              font-size: 24px;
              margin: 0 5px;
            }
          }
        }
      }
    
      .num-box {
        display: flex;
        justify-content: space-between;
        padding: 10px;
        align-items: center;
      }
    
      .btn, .btn-none {
        height: 40px;
        line-height: 40px;
        margin: 20px;
        border-radius: 20px;
        text-align: center;
        color: rgb(255, 255, 255);
        background-color: rgb(255, 148, 2);
      }
      .btn.now {
        background-color: #fe5630;
      }
      .btn-none {
        background-color: #cccccc;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    1. 动态渲染
    
      
    ¥ {{ detail.goods_price_min }}
    库存 {{ detail.stock_total }}
    数量 数字框组件
    加入购物车
    立刻购买
    该商品已抢完
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    35. 加入购物车 - 封装数字框组件

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 封装组件 components/CountBox.vue
    
    
        
    
        
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    1. 使用组件
    import CountBox from '@/components/CountBox.vue'
    
    export default {
      name: 'ProDetail',
      components: {
        CountBox
      },
      data () {
        return {
          addCount: 1
          ...
        }
      },
    }
    
    
    数量
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    36. 加入购物车 - 判断 token 登录提示

    说明:加入购物车,是一个登录后的用户才能进行的操作,所以需要进行鉴权判断,判断用户 token 是否存在

    1. 若存在:继续加入购物车操作
    2. 不存在:提示用户未登录,引导到登录页

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 按需注册 dialog 组件
    import { Dialog } from 'vant'
    Vue.use(Dialog)
    
    • 1
    • 2
    1. 按钮注册点击事件
    加入购物车
    • 1
    1. 添加 token 鉴权判断,跳转携带回跳地址
    async addCart () {
      // 判断用户是否有登录
      if (!this.$store.getters.token) {
        this.$dialog.confirm({
          title: '温馨提示',
          message: '此时需要先登录才能继续操作哦',
          confirmButtonText: '去登录',
          cancelButtonText: '再逛逛'
        })
          .then(() => {
            this.$router.replace({
              path: '/login',
              query: {
                backUrl: this.$route.fullPath
              }
            })
          })
          .catch(() => {})
        return
      }
      console.log('进行加入购物车操作')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    1. 登录后,若有回跳地址,则回跳页面
    // 判断有无回跳地址
    const url = this.$route.query.backUrl || '/'
    this.$router.replace(url)
    
    • 1
    • 2
    • 3

    37. 加入购物车 - 封装接口进行请求

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 封装接口 api/cart.js
    // 加入购物车
    export const addCart = (goodsId, goodsNum, goodsSkuId) => {
      return request.post('/cart/add', {
        goodsId,
        goodsNum,
        goodsSkuId
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 页面中调用请求
    data () {
      return {
          cartTotal: 0
      }  
    },
    
    async addCart () {
      ...
      const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)
      this.cartTotal = data.cartTotal
      this.$toast('加入购物车成功')
      this.showPannel = false
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 请求拦截器中,统一携带 token
    // 自定义配置 - 请求/响应 拦截器
    // 添加请求拦截器
    instance.interceptors.request.use(function (config) {
      ...
      const token = store.getters.token
      if (token) {
        config.headers['Access-Token'] = token
        config.headers.platform = 'H5'
      }
      return config
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 准备小图标
    {{ cartTotal }} 购物车
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 定制样式
    .footer .icon-cart {
      position: relative;
      padding: 0 6px;
      .num {
        z-index: 999;
        position: absolute;
        top: -2px;
        right: 0;
        min-width: 16px;
        padding: 0 4px;
        color: #fff;
        text-align: center;
        background-color: #ee0a24;
        border-radius: 50%;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    38. 购物车 - 静态布局

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 基本结构
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    1. 按需导入组件
    import { Checkbox } from 'vant'
    Vue.use(Checkbox)
    
    • 1
    • 2

    39. 购物车 - 构建 vuex 模块 - 获取数据存储

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 新建 modules/cart.js 模块
    export default {
      namespaced: true,
      state () {
        return {
          cartList: []
        }
      },
      mutations: {
      },
      actions: {
      },
      getters: {
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 挂载到 store 上面
    import Vue from 'vue'
    import Vuex from 'vuex'
    import user from './modules/user'
    import cart from './modules/cart'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      getters: {
        token: state => state.user.userInfo.token
      },
      modules: {
        user,
        cart
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 封装 API 接口 api/cart.js
    // 获取购物车列表数据
    export const getCartList = () => {
      return request.get('/cart/list')
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 封装 action 和 mutation
    mutations: {
      setCartList (state, newList) {
        state.cartList = newList
      },
    },
    actions: {
      async getCartAction (context) {
        const { data } = await getCartList()
        data.list.forEach(item => {
          item.isChecked = true
        })
        context.commit('setCartList', data.list)
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 页面中 dispatch 调用
    computed: {
      isLogin () {
        return this.$store.getters.token
      }
    },
    created () {
      if (this.isLogin) {
        this.$store.dispatch('cart/getCartAction')
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    40. 购物车 - mapState - 渲染购物车列表

    1. 将数据映射到页面
    import { mapState } from 'vuex'
    
    computed: {
      ...mapState('cart', ['cartList'])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 动态渲染
    
    
    {{ item.goods.goods_name }}
    ¥ {{ item.goods.goods_price_min }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    41. 购物车 - 封装 getters - 动态计算展示

    1. 封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
    getters: {
      cartTotal (state) {
        return state.cartList.reduce((sum, item, index) => sum + item.goods_num, 0)
      },
      selCartList (state) {
        return state.cartList.filter(item => item.isChecked)
      },
      selCount (state, getters) {
        return getters.selCartList.reduce((sum, item, index) => sum + item.goods_num, 0)
      },
      selPrice (state, getters) {
        return getters.selCartList.reduce((sum, item, index) => {
          return sum + item.goods_num * item.goods.goods_price_min
        }, 0).toFixed(2)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 页面中 mapGetters 映射使用
    computed: {
      ...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
    },
        
    
    
    {{ cartTotal || 0 }}件商品 编辑
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    42. 购物车 - 全选反选功能

    1. 全选 getters
    getters: {
      isAllChecked (state) {
        return state.cartList.every(item => item.isChecked)
      }
    }
        
    ...mapGetters('cart', ['isAllChecked']),
    
    
    全选
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 点击小选,修改状态
    
        
    toggleCheck (goodsId) {
      this.$store.commit('cart/toggleCheck', goodsId)
    },
        
    mutations: {
      toggleCheck (state, goodsId) {
        const goods = state.cartList.find(item => item.goods_id === goodsId)
        goods.isChecked = !goods.isChecked
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 点击全选,重置状态
    全选
    toggleAllCheck () { this.$store.commit('cart/toggleAllCheck', !this.isAllChecked) }, mutations: { toggleAllCheck (state, flag) { state.cartList.forEach(item => { item.isChecked = flag }) }, }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    43. 购物车 - 数字框修改数量

    1. 封装 api 接口
    // 更新购物车商品数量
    export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
      return request.post('/cart/update', {
        goodsId,
        goodsNum,
        goodsSkuId
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 页面中注册点击事件,传递数据
    
    
    changeCount (value, goodsId, skuId) {
      this.$store.dispatch('cart/changeCountAction', {
        value,
        goodsId,
        skuId
      })
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 提供 action 发送请求, commit mutation
    mutations: {
      changeCount (state, { goodsId, value }) {
        const obj = state.cartList.find(item => item.goods_id === goodsId)
        obj.goods_num = value
      }
    },
    actions: {
      async changeCountAction (context, obj) {
        const { goodsId, value, skuId } = obj
        context.commit('changeCount', {
          goodsId,
          value
        })
        await changeCount(goodsId, value, skuId)
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    44. 购物车 - 编辑切换状态

    1. data 提供数据, 定义是否在编辑删除的状态
    data () {
      return {
        isEdit: false
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 注册点击事件,修改状态
    
      
      编辑
    
    
    • 1
    • 2
    • 3
    • 4
    1. 底下按钮根据状态变化
    去结算({{ selCount }})
    删除
    • 1
    • 2
    • 3
    • 4
    1. 监视编辑状态,动态控制复选框状态
    watch: {
      isEdit (value) {
        if (value) {
          this.$store.commit('cart/toggleAllCheck', false)
        } else {
          this.$store.commit('cart/toggleAllCheck', true)
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    45. 购物车 - 删除功能完成

    1. 查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
    // 删除购物车
    export const delSelect = (cartIds) => {
      return request.post('/cart/clear', {
        cartIds
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 注册删除点击事件
    删除({{ selCount }})
    async handleDel () { if (this.selCount === 0) return await this.$store.dispatch('cart/delSelect') this.isEdit = false },
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 提供 actions
    actions: {
        // 删除购物车数据
        async delSelect (context) {
          const selCartList = context.getters.selCartList
          const cartIds = selCartList.map(item => item.id)
          await delSelect(cartIds)
          Toast('删除成功')
    
          // 重新拉取最新的购物车数据 (重新渲染)
          context.dispatch('getCartAction')
        }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    46. 购物车 - 空购物车处理

    1. 外面包个大盒子,添加 v-if 判断
    ...
    ...
    您的购物车是空的, 快去逛逛吧
    去逛逛
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 相关样式
    .empty-cart {
      padding: 80px 30px;
      img {
        width: 140px;
        height: 92px;
        display: block;
        margin: 0 auto;
      }
      .tips {
        text-align: center;
        color: #666;
        margin: 30px;
      }
      .btn {
        width: 110px;
        height: 32px;
        line-height: 32px;
        text-align: center;
        background-color: #fa2c20;
        border-radius: 16px;
        color: #fff;
        display: block;
        margin: 0 auto;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    47. 订单结算台

    所谓的 “立即结算”,本质就是跳转到订单结算台,并且跳转的同时,需要携带上对应的订单参数。

    而具体需要哪些参数,就需要基于 【订单结算台】 的需求来定。

    (1) 静态布局

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    准备静态页面

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257

    (2) 获取收货地址列表

    1 封装获取地址的接口

    import request from '@/utils/request'
    
    // 获取地址列表
    export const getAddressList = () => {
      return request.get('/address/list')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2 页面中 - 调用获取地址

    data () {
      return {
        addressList: []
      }
    },
    computed: {
      selectAddress () {
        // 这里地址管理不是主线业务,直接获取默认第一条地址
        return this.addressList[0] 
      }
    },
    async created () {
      this.getAddressList()
    },
    methods: {
      async getAddressList () {
        const { data: { list } } = await getAddressList()
        this.addressList = list
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    3 页面中 - 进行渲染

    computed: {
      longAddress () {
        const region = this.selectAddress.region
        return region.province + region.city + region.region + this.selectAddress.detail
      }
    },
    
    
    {{ selectAddress.name }} {{ selectAddress.phone }}
    {{ longAddress }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    (3) 订单结算 - 封装通用接口

    **思路分析:**这里的订单结算,有两种情况:

    1. 购物车结算,需要两个参数

      ① mode=“cart”

      ② cartIds=“cartId, cartId”

    2. 立即购买结算,需要三个参数

      ① mode=“buyNow”

      ② goodsId=“商品id”

      ③ goodsSkuId=“商品skuId”

    都需要跳转时将参数传递过来


    封装通用 API 接口 api/order

    import request from '@/utils/request'
    
    export const checkOrder = (mode, obj) => {
      return request.get('/checkout/order', {
        params: {
          mode,
          delivery: 0,
          couponId: 0,
          isUsePoints: 0,
          ...obj
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    (4) 订单结算 - 购物车结算

    1 跳转时,传递查询参数

    layout/cart.vue

    结算({{ selCount }})
    goPay () { if (this.selCount > 0) { this.$router.push({ path: '/pay', query: { mode: 'cart', cartIds: this.selCartList.map(item => item.id).join(',') } }) } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2 页面中接收参数, 调用接口,获取数据

    data () {
      return {
        order: {},
        personal: {}
      }
    },
        
    computed: {
      mode () {
        return this.$route.query.mode
      },
      cartIds () {
        return this.$route.query.cartIds
      }
    }
    
    async created () {
      this.getOrderList()
    },
    
    async getOrderList () {
      if (this.mode === 'cart') {
        const { data: { order, personal } } = await checkOrder(this.mode, { cartIds: this.cartIds })
        this.order = order
        this.personal = personal
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    3 基于数据进行渲染

    
    

    {{ item.goods_name }}

    x{{ item.total_num }} ¥{{ item.total_pay_price }}

    共 {{ order.orderTotalNum }} 件商品,合计: ¥{{ order.orderTotalPrice }}
    订单总金额: ¥{{ order.orderTotalPrice }}
    优惠券: 无优惠券可用
    配送费用: 请先选择配送地址 +¥0.00
    支付方式
    余额支付(可用 ¥ {{ personal.balance }} 元)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    (5) 订单结算 - 立即购买结算

    1 点击跳转传参

    prodetail/index.vue

    立刻购买
    goBuyNow () { this.$router.push({ path: '/pay', query: { mode: 'buyNow', goodsId: this.goodsId, goodsSkuId: this.detail.skuList[0].goods_sku_id, goodsNum: this.addCount } }) }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2 计算属性处理参数

    computed: {
      ...
      goodsId () {
        return this.$route.query.goodsId
      },
      goodsSkuId () {
        return this.$route.query.goodsSkuId
      },
      goodsNum () {
        return this.$route.query.goodsNum
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3 基于请求时携带参数发请求渲染

    async getOrderList () {
      ...
      
      if (this.mode === 'buyNow') {
        const { data: { order, personal } } = await checkOrder(this.mode, {
          goodsId: this.goodsId,
          goodsSkuId: this.goodsSkuId,
          goodsNum: this.goodsNum
        })
        this.order = order
        this.personal = personal
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    (6) mixins 复用 - 处理登录确认框的弹出

    1 新建一个 mixin 文件 mixins/loginConfirm.js

    export default {
      methods: {
        // 是否需要弹登录确认框
        // (1) 需要,返回 true,并直接弹出登录确认框
        // (2) 不需要,返回 false
        loginConfirm () {
          if (!this.$store.getters.token) {
            this.$dialog.confirm({
              title: '温馨提示',
              message: '此时需要先登录才能继续操作哦',
              confirmButtonText: '去登陆',
              cancelButtonText: '再逛逛'
            })
              .then(() => {
                // 如果希望,跳转到登录 => 登录后能回跳回来,需要在跳转去携带参数 (当前的路径地址)
                // this.$route.fullPath (会包含查询参数)
                this.$router.replace({
                  path: '/login',
                  query: {
                    backUrl: this.$route.fullPath
                  }
                })
              })
              .catch(() => {})
            return true
          }
          return false
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    2 页面中导入,混入方法

    import loginConfirm from '@/mixins/loginConfirm'
    
    export default {
      name: 'ProDetail',
      mixins: [loginConfirm],
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3 页面中调用 混入的方法

    async addCart () {
      if (this.loginConfirm()) {
        return
      }
      const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)
      this.cartTotal = data.cartTotal
      this.$toast('加入购物车成功')
      this.showPannel = false
      console.log(this.cartTotal)
    },
    
    goBuyNow () {
      if (this.loginConfirm()) {
        return
      }
      this.$router.push({
        path: '/pay',
        query: {
          mode: 'buyNow',
          goodsId: this.goodsId,
          goodsSkuId: this.detail.skuList[0].goods_sku_id,
          goodsNum: this.addCount
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    48. 提交订单并支付

    1 封装 API 通用方法(统一余额支付)

    // 提交订单
    export const submitOrder = (mode, params) => {
      return request.post('/checkout/submit', {
        mode,
        delivery: 10, // 物流方式  配送方式 (10快递配送 20门店自提)
        couponId: 0, // 优惠券 id
        payType: 10, // 余额支付
        isUsePoints: 0, // 是否使用积分
        ...params
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2 买家留言绑定

    data () {
      return {
        remark: ''
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3 注册点击事件,提交订单并支付

    提交订单
    // 提交订单 async submitOrder () { if (this.mode === 'cart') { await submitOrder(this.mode, { remark: this.remark, cartIds: this.cartIds }) } if (this.mode === 'buyNow') { await submitOrder(this.mode, { remark: this.remark, goodsId: this.goodsId, goodsSkuId: this.goodsSkuId, goodsNum: this.goodsNum }) } this.$toast.success('支付成功') this.$router.replace('/myorder') }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    49. 订单管理

    (1) 静态布局

    1 基础静态结构

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    2 components/OrderListItem

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131

    3 导入注册

    import { Tab, Tabs } from 'vant'
    Vue.use(Tab)
    Vue.use(Tabs)
    
    • 1
    • 2
    • 3

    (2) 点击 tab 切换渲染

    1 封装获取订单列表的 API 接口

    // 订单列表
    export const getMyOrderList = (dataType, page) => {
      return request.get('/order/list', {
        params: {
          dataType,
          page
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2 给 tab 绑定 name 属性

    
      
      
      
      
      
    
    
    data () {
      return {
        active: this.$route.query.dataType || 'all',
        page: 1,
        list: []
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3 封装调用接口获取数据

    methods: {
      async getOrderList () {
        const { data: { list } } = await getMyOrderList(this.active, this.page)
        list.data.forEach((item) => {
          item.total_num = 0
          item.goods.forEach(goods => {
            item.total_num += goods.total_num
          })
        })
        this.list = list.data
      }
    },
    watch: {
      active: {
        immediate: true,
        handler () {
          this.getOrderList()
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4 动态渲染

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    50. 个人中心 - 基本渲染

    1 封装获取个人信息 - API接口

    import request from '@/utils/request'
    
    // 获取个人信息
    export const getUserInfoDetail = () => {
      return request.get('/user/info')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2 调用接口,获取数据进行渲染

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284

    51. 个人中心 - 退出功能

    1 注册点击事件

    
    
    • 1

    2 提供方法

    methods: {
      logout () {
        this.$dialog.confirm({
          title: '温馨提示',
          message: '你确认要退出么?'
        })
          .then(() => {
            this.$store.dispatch('user/logout')
          })
          .catch(() => {
    
          })
      }
    }
    
    actions: {
      logout (context) {
        context.commit('setUserInfo', {})
        context.commit('cart/setCartList', [], { root: true })
      }
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    52. 项目打包优化

    vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线

    参与上线的是 => 打包后的源代码

    打包:

    • 将多个文件压缩合并成一个文件
    • 语法降级
    • less sass ts 语法解析, 解析成css

    打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!

    (1) 打包命令

    vue脚手架工具已经提供了打包命令,直接使用即可。

    yarn build
    
    • 1

    在项目的根目录会自动创建一个文件夹dist,dist中的文件就是打包后的文件,只需要放到服务器中即可。

    (2) 配置publicPath

    module.exports = {
      // 设置获取.js,.css文件时,是以相对地址为基准的。
      // https://cli.vuejs.org/zh/config/#publicpath
      publicPath: './'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3) 路由懒加载

    路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件

    官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E4%BD%BF%E7%94%A8-webpack

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

    const ProDetail = () => import('@/views/prodetail')
    const Pay = () => import('@/views/pay')
    const MyOrder = () => import('@/views/myorder')
    
    • 1
    • 2
    • 3
  • 相关阅读:
    WPF 使用Image控件显示图片
    php异常和错误处理机制
    多尺度特征融合
    python 将一列文本或字符串调整成一行 中间用逗号 单引号分隔
    Flutter学习5-集成-打包-发布
    SASS常用内置函数
    A - Turn the Rectangles
    烟雾报警系统设计与实现
    vue-pdf多页预览异常,Rendering cancelled, page 1 Error at BaseExceptionClosure xxx
    Matlab 删除含有特定数值的行 / Matlab定义excel表头
  • 原文地址:https://blog.csdn.net/upgrade_bro/article/details/132745233