Vue2案例

一、自定义创建项目

1、基于 VueCli 自定义创建项目

  • Babel / Router / Vuex / CSS / Linter
  • Vue2.x
  • VueRouter hash模式
  • CSS预处理 Less
  • ESlint: Standard config
  • Lint on Save
  • In dedicated config files (配置文件所在位置)
  • Npm

2、ESlint 代码规范

1. 认识代码规范

  • 代码规范:一套写代码的约定规则。
    • 赋值符号的左右是否需要空格
    • 一句结束是否是要加;
  • JavaScript Standard Style 规范说明: https://standardjs.com/rules-zhcn.html
    • 字符串使用单引号:‘abc’
    • 无分号:const name = ‘zs’
    • 关键字后加空格:if (name = ‘ls’) { … }
    • 函数名后加空格:function name (arg) { … }
    • 坚持使用全等 === 摒弃 ==

2. 代码规范错误

  • 目标:学会解决代码规范错误
  • 如果你的代码不符合 standard 的要求,ESlint 会跳出来刀子嘴,豆腐心地提示你。
  • 比如:在main.js中随意做一些改动,添加一些分号,空行。
  • 两种解决方案:
    • 手动修正
      • 根据错误提示来一项一项手动修改纠正。
      • 如果你不认识命令行中的语法报错是什么意思,根据错误代码去 [ESLint 规则表] 中查找其具体含义。
    • 自动修正
      • 基于 vscode 插件 ESLint 高亮错误,并通过配置 自动 帮助我们修复错误。

二、Vant-ui

  • 开发手册:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/

1、Vue 组件库

  • 一般会按照不同平台进行分类:
    • PC端: element-ui(element-plus)、ant-design-vue
    • 移动端:vant-ui、Mint UI (饿了么)、Cube UI (滴滴

2、vant 全部导入

1. 安装 vant-ui

yarn add vant@latest-v2

2. 导入注册

  • main.js
import Vant from 'vant'
import 'vant/lib/index.css'
// 把vant中所有的组件都导入、注册
Vue.use(Vant)

3. 测试使用

<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>

3、按需导入

1. 安装 vant-ui

yarn add vant@latest-v2

2. 安装插件

yarn add babel-plugin-import -D

3. 配置信息

  • babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  // 需要添加的配置
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
}

4. 按需导入注册

  • @/plugins/vant-ui.js
import Vue from 'vue'
import { Button, Switch, Step, Steps, Tabbar, TabbarItem, NavBar, Toast, Search, Swipe, SwipeItem, Grid, GridItem, Icon, Lazyload, Rate, ActionSheet, Dialog, Checkbox, CheckboxGroup, Tab, Tabs } from 'vant'

// 按钮
Vue.use(Button)
// 开关
Vue.use(Switch)
// 步骤条
Vue.use(Step)
Vue.use(Steps)
// 标签栏
Vue.use(Tabbar)
Vue.use(TabbarItem)
// 导航栏
Vue.use(NavBar)
// 轻提示
Vue.use(Toast)
// 搜索框
Vue.use(Search)
// 轮播图
Vue.use(Swipe)
Vue.use(SwipeItem)
// 布局
Vue.use(Grid)
Vue.use(GridItem)
// Vant 图标
Vue.use(Icon)
// 懒加载
Vue.use(Lazyload)
//  评分
Vue.use(Rate)
// 反馈组件
Vue.use(ActionSheet)
// 弹窗组件
Vue.use(Dialog)
// 复选框
Vue.use(Checkbox)
Vue.use(CheckboxGroup)
// Tab 标签页
Vue.use(Tab)
Vue.use(Tabs)

5. 导入配置文件

  • main.js
import '@/plugins/vant-ui'

6. 测试使用

<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>

4、VW适配

  • 基于 postcss 插件 实现项目 vw 适配

1. 安装插件

yarn add postcss-px-to-viewport@1.1.1 -D

2. 添加配置

  • 根目录新建 postcss.config.js 文件,填入配置
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375
    }
  }
}

5、配置底部导航

1. 按需引入导航组件

  • @/plugins/vant-ui.js
import { Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)

2. 修改文字、图标、颜色

  • @/views/layout/index.vue
<template>
  <div>
    <!-- 二级路由出口,二级组件展示位置 -->
    <router-view />
    <!-- 底部导航栏:active-color,激活时的颜色 inactive-color,未激活时的颜色 -->
    <van-tabbar route v-model="active" active-color="#ee0a24" inactive-color="#000">
      <van-tabbar-item to="/home" name="home" icon="home-o">首页</van-tabbar-item>
      <van-tabbar-item to="/category" name="search" icon="apps-o">分类</van-tabbar-item>
      <van-tabbar-item to="/cart" name="friends" icon="cart-o">购物车</van-tabbar-item>
      <van-tabbar-item to="/user" name="setting" icon="contact-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<script>
export default {
  name: 'LayoutIndex',
  data () {
    return {
      // 默认展示首页
      active: 'home'
    }
  }
}
</script>

<style>
</style>

6、Toast 轻提示

  • Toast 默认单例模式,后面 Toast 调用会将前一个覆盖,同一时间只能存在一个 Toast

1. 注册安装

  • @/plugins/vant-ui.js
import Vue from 'vue'
import { Button, Toast } from 'vant'

Vue.use(Button)
Vue.use(Toast)

2. 调用方式

  • 导入调用:组件内 或 非组件中均可
<template>
  <div class="login"></div>
</template>

<script>
// 导入 Toast轻提示 组件
import { Toast } from 'vant'

export default {
  name: 'LoginPage',
  created () {
    Toast('提示内容')
  }
}
</script>

<style lang="less" scoped>
</style>
  • this直接调用 本质:将方法,注册挂载到了Vue原型上 Vue.prototype.$toast = xxx
<template>
  <div class="login"></div>
</template>

<script>
export default {
  name: 'LoginPage',
  created () {
    this.$toast('提示内容')
  }
}
</script>

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

三、Router

1、配置路由

  • 目标:分析项目页面,设计路由,配置一级路由
  • 但凡是单个页面,独立展示的,都是一级路由
  • @router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

import Layout from '@/views/layout'
import Login from '@/views/login'
import MyOrder from '@/views/myOrder'
import Pay from '@/views/pay'
import ProDetail from '@/views/proDetail'
import Search from '@/views/search'
import SearchList from '@/views/search/list'

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: '/',
      component: Layout,
      redirect: '/home',
      children: [
        { path: '/home', component: Home },
        { path: '/category', component: Category },
        { path: '/cart', component: Cart },
        { path: '/user', component: User }
      ]
    },
    { path: '/login', component: Login },
    { path: '/myOrder', component: MyOrder },
    { path: '/pay', component: Pay },
    { path: '/proDetail/:id', component: ProDetail },
    { path: '/search', component: Search },
    { path: '/searchList', component: SearchList }
  ]
})

export default router

2、导入路由

  • main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

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

3、页面访问拦截

  • 目标:基于全局前置守卫,进行页面访问拦截处理
  • 所有的路由一旦被匹配到,都会先经过全局前置守卫
  • 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
  • 访问权限页面时,拦截或放行的关键点? → 用户是否有登录权证 token

在这里插入图片描述

1. 获取 token

import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({
  getters: {
    token (state) {
      return state.user.userInfo.token
    }
  },
  modules: {
    user
  }
})

2. 全局前置导航守卫

import Vue from 'vue'
import VueRouter from 'vue-router'

import store from '@/store'

Vue.use(VueRouter)

// 定义一个数组,存放所以需要权限的页面
const authUrls = ['/user', '/myOrder', '/pay']

// 全局前置导航守卫
// to:往哪里去, 到哪去的路由信息对象
// from:从哪里来, 从哪来的路由信息对象
// 3. next() 是否放行
// 如果next()调用,就是放行
// next(路径) 拦截到某个路径页面, 跳转到该页面
router.beforeEach((to, from, next) => {
  console.log(to, from, next)
  // 获取 to.path 判断是否存在于 需要权限的页面
  if (!authUrls.includes(to.path)) {
    // 放行
    next()
  } else {
    const token = store.getters.token
    if (token) {
      // 已经登录,放行
      next()
    } else {
      // 未登录,跳转登陆页
      next('/login')
    }
  }
})

export default router

四、Axios 封装

  • 使用 axios 来请求后端接口, 一般都会对 axios 进行 一些配置 (比如: 配置基础地址,请求响应拦截器等)
  • 所以项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个 request 模块中, 便于维护使用
  • 中文文档:https://www.axios-http.cn/docs/intro

1、安装 Axios

yarn add axios

2、新建 request 模块

  • @/utils/request.js
  • 创建 axios 实例
  • 配置导出势力 instance
import axios from 'axios'

// 创建 axios 实例,将来对创建出来的实例进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({
  baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
  timeout: 5000
})

// 自定义配置:请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

// 导出配置好的实例
export default instance

3、添加响应拦截器

import axios from 'axios'
import { Toast } from 'vant'

// 创建 axios 实例,将来对创建出来的实例进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({
  baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
  timeout: 5000
})

// 自定义配置:请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 开启loading,禁止背景点击(节流处理,防止多次无效触发)
  Toast.loading({
    message: '加载中...',
    forbidClick: true, // 禁止背景点击
    loadingType: 'spinner', // 加载图标类型,支持 spinner loading circle
    duration: 0 // 持续展示时间(ms),0 表示永久展示
  })
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  const res = response.data
  if (res.status !== 200) {
    // 获取提示
    Toast(res.message)
    // 抛出错误提示
    return Promise.reject(res.message)
  } else {
    // 正确情况,走业务核心逻辑,清除 loading 效果
    Toast.clear()
  }
  return res
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

// 导出配置好的实例
export default instance

4、封装 api 接口模块

  • @/api/login.js
import request from '@/utils/request'
// 用于存放所有登录相关的接口请求
// 1. 获取图形验证码
export const getPicCode = () => {
  return request({
    url: '/captcha/image',
    method: 'get'
  })
}

5、添加请求 loading 效果

  • 添加 loading 提示的好处:
    • 节流处理:防止用户在一次请求还没回来之前,多次进行点击,发送无效请求
    • 友好提示:告知用户,目前是在加载中,请耐心等待,用户体验会更好

1. 添加 loading 效果

  • 请求拦截器中,每次请求,打开 loading
// 自定义配置:请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 开启loading,禁止背景点击(节流处理,防止多次无效触发)
  Toast.loading({
    message: '加载中...',
    forbidClick: true, // 禁止背景点击
    loadingType: 'spinner', // 加载图标类型,支持 spinner loading circle
    duration: 0 // 持续展示时间(ms),0 表示永久展示
  })
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

2. 关闭 loading 效果

  • 响应拦截器中,每次响应,关闭 loading
  • Toast 默认单例模式,后面 Toast 调用会将前一个覆盖,同一时间只能存在一个 Toast
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  const res = response.data
  if (res.status !== 200) {
    // 获取提示
    Toast(res.message)
    // 抛出错误提示
    return Promise.reject(res.message)
  } else {
    // 正确情况,走业务核心逻辑,清除 loading 效果
    Toast.clear()
  }
  return res
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

6、统一携带请求头

// 自定义配置:请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  // 开启loading,禁止背景点击(节流处理,防止多次无效触发)
  Toast.loading({
    message: '加载中...',
    forbidClick: true, // 禁止背景点击
    loadingType: 'spinner', // 加载图标类型,支持 spinner loading circle
    duration: 0 // 持续展示时间(ms),0 表示永久展示
  })

  // 只要有 token, 就在请求的时候携带, 便于 请求需要授权的接口
  const token = store.getters.token
  if (token) {
    config.headers['Access-Token'] = token
    // 设置平台
    config.headers.platform = 'H5'
  }

  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

五、Vuex

1、登录权证信息存储

1. 持久化存储

  • 封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理:storage.js
// 约定一个通用的键名
const INFO_KEY = 'shopping_info'

// 获取个人信息
export const getUserInfo = () => {
  const defaultInfo = { tonken: '', userId: '' }
  const result = localStorage.getItem(INFO_KEY)
  return result ? JSON.parse(result) : defaultInfo
}

// 设置个人信息
export const setUserInfo = (info) => {
  localStorage.setItem(INFO_KEY, JSON.stringify(info))
}

// 移除个人信息
export const removeUserInfo = () => {
  localStorage.removeItem(INFO_KEY)
}

2. 构建 user 模块

  • @/store/modules/user.js
import { getUserInfo, setUserInfo } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      userInfo: getUserInfo()
    }
  },
  mutations: {
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
      setUserInfo(userInfo)
    }
  },
  actions: {},
  getters: {}
}

3. 引入模块

  • @/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user
  }
})

2、购物车信息存储

1. 构建 cart 模块

import { getCartList, updateCartNum, deleteCartGoods } from '@/api/cart'
import { Toast } from 'vant'
export default {
  namespaced: true,
  state () {
    return {
      cartTotal: 0, // 购物车总数
      cartList: [] // 购物车列表
    }
  },
  mutations: {
    // 设置购物车总数
    setcartTotal (state, cartTotal) {
      state.cartTotal = cartTotal
    },
    // 设置购物车列表
    setCartList (state, cartList) {
      state.cartList = cartList
    },
    // 切换商品选中状态
    toggleCheck (state, goodsId) {
      state.cartList.forEach(item => {
        if (item.goods_id === goodsId) {
          item.is_checked = !item.is_checked
        }
      })
    },
    // 切换全选状态
    toggleAllCheck (state, flag) {
      state.cartList.forEach(item => {
        item.is_checked = flag
      })
    }
  },
  actions: {
    // 获取购物车列表
    async getCartListAction (context) {
      const { data: { cartTotal, list } } = await getCartList()
      // 默认选中
      list.forEach(item => {
        item.is_checked = true
      })
      context.commit('setCartList', list)
      context.commit('setcartTotal', cartTotal)
    },
    // 修改商品数量
    async changeCountAction (context, obj) {
      const { goodsNum, goodsId, goodsSkuId } = obj
      // 修改后台数量
      await updateCartNum(goodsNum, goodsId, goodsSkuId)
      // 更新数据
      context.dispatch('getCartListAction')
    },
    // 删除选中的商品
    async deleteCheckedAction (context) {
      const cartIds = context.getters.getCheckedList.map(item => item.id)
      await deleteCartGoods(cartIds)
      // 更新数据
      await context.dispatch('getCartListAction')
      context.commit('toggleAllCheck', false)
      Toast.success('删除成功!')
    }
  },
  getters: {
    // 获取选中商品总数
    getCheckedTotal (state) {
      return state.cartList.reduce((sum, item) => sum + (item.is_checked ? item.goods_num : 0), 0)
    },
    // 获取选中商品列表
    getCheckedList (state) {
      return state.cartList.filter(item => item.is_checked)
    },
    // 获取选中商品总价
    getCheckedTotalPrice (state, getters) {
      return getters.getCheckedList.reduce((sum, item) => {
        return sum + item.goods.goods_price_min * item.goods_num
      }, 0).toFixed(2)
    },
    // 判断是否全选
    isAllChecked (state) {
      // return getters.getCheckedList.length === state.cartList.length && getters.getCheckedList.length !== 0
      return state.cartList.every(item => item.is_checked) && state.cartList.length !== 0
    }
  }
}

2. 引入模块

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({
  strict: true,
  state: {
  },
  getters: {
    token (state) {
      return state.user.userInfo.token
    }
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user,
    cart
  }
})

六、登陆页面

1、获取图形验证码

1. 接口封装

  • @/api/login.js
import request from '@/utils/request'
// 用于存放所有登录相关的接口请求
// 1. 获取图形验证码
export const getPicCode = () => {
  return request({
    url: '/captcha/image',
    method: 'get'
  })
}

2. 页面渲染

<template>
  <div class="login">
      <img v-if="picUrl" :src="picUrl" alt="" @click="getPicCode">
  </div>
</template>

<script>
// 导入 获取图形验证码 请求接口
import { getPicCode } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
      picUrl: '', // 图形验证码地址
      picKey: '' // 图形验证码唯一标识
    }
  },
  created () {
    this.getPicCode()
  },
  methods: {
    // 获取图形验证码
    async getPicCode () {
      // 解构 接口返回参数
      const { data: { base64, key } } = await getPicCode()
      this.picUrl = base64
      this.picKey = key
    }
  }
}
</script>

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

2、短信验证倒计时

1. 接口封装

import request from '@/utils/request'

// 2. 获取短信验证码
export const getSmsCode = (captchaCode, captchaKey, mobile) => {
  return request.post('/captcha/sendSmsCaptcha', {
    form: {
      captchaCode, // 图形验证码
      captchaKey, // 图形验证码key
      mobile // 手机号
    }
  })
}

2. 接口渲染

<template>
  <div class="login">
    <div class="form">
      <div class="form-item">
        <input v-model="smsCode" class="inp" placeholder="请输入短信验证码" type="text">
        <button @click="getCode">
          {{ secend === totalSecond ? '获取验证码' : secend + '秒后重新发送' }}
  			</button>
      </div>
    </div>
  </div>
</template>

<script>
// 导入请求接口
import { getSmsCode } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
      phone: '',
      smsCode: '', // 短信验证码
      picCode: '', // 图形验证码
      picUrl: '', // 图形验证码地址
      picKey: '', // 图形验证码唯一标识
      totalSecond: 10, // 总秒数
      secend: 10, // 当前秒数,开定时器:secend ---
      timer: null // 定时器
    }
  },
  methods: {
    // 获取短信验证码验证码
    async getCode () {
      // 获取验证码前进行校验
      if (!this.validFn()) {
        return
      }
      // 当前没有定时器,且 totalSecond 和 secend 一致才可以倒计时
      if (!this.timer && this.totalSecond === this.secend) {
        // 获取验证码
        // 响应结果200继续执行,非200则通过拦截器获取错误信息,并提示用户
        await getSmsCode(this.picCode, this.picKey, this.phone)

        // 验证码获取成功
        this.$toast.success('验证码发送成功!')

        // 开启倒计时
        this.timer = setInterval(() => {
          this.secend--
          // 倒计时结束
          if (this.secend <= 0) {
            // 销毁定时器
            clearInterval(this.timer)
            this.secend = this.totalSecond // 重置秒数
            this.timer = null // 关闭定时器
          }
        }, 1000)
      } else {
        this.$toast.fail('请勿重复点击!')
      }
    },
    // 校验手机号和验证码
    validFn () {
      if (!this.phone) {
        this.$toast.fail('请输入手机号码!')
        return false
      }
      if (!/^1[3-9]\d{9}$/.test(this.phone)) {
        this.$toast.fail('请输入正确的手机号码!')
        return false
      }
      if (!this.picKey) {
        this.$toast.fail('请获取图形验证码!')
      }
      if (!this.picCode) {
        this.$toast.fail('请输入图形验证码!')
        return false
      }
      if (!/^\w{4}$/.test(this.picCode)) {
        this.$toast.fail('请输入正确的图形验证码!')
        return false
      }
      return true
    },
  },
  destroyed () {
    // 销毁定时器
    clearInterval(this.timer)
  }
}
</script>

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

3、登陆接口

1. 接口封装

import request from '@/utils/request'

// 3. 登录
export const login = (mobile, smsCode) => {
  return request.post('/passport/login', {
    form: {
      mobile, // 手机号 必需
      smsCode, // 短信验证码 必需
      isParty: false, // 是否存在第三方用户信息 必需
      partyData: {} // 三方登录信息,默认为:{} 可选
    }
  })
}

2. 接口渲染

<template>
  <div class="login">
    <div class="login-btn" @click="login">登录</div>
  </div>
</template>

<script>
// 导入 请求接口
import { login } from '@/api/login'

export default {
  name: 'LoginPage',
  data () {
    return {
      phone: '',
      smsCode: '', // 短信验证码
      picCode: '', // 图形验证码
      picUrl: '', // 图形验证码地址
      picKey: '', // 图形验证码唯一标识
      totalSecond: 10, // 总秒数
      secend: 10, // 当前秒数,开定时器:secend ---
      timer: null // 定时器
    }
  },
  methods: {
    // 校验手机号和验证码
    validFn () {
      if (!this.phone) {
        this.$toast.fail('请输入手机号码!')
        return false
      }
      if (!/^1[3-9]\d{9}$/.test(this.phone)) {
        this.$toast.fail('请输入正确的手机号码!')
        return false
      }
      if (!this.picKey) {
        this.$toast.fail('请获取图形验证码!')
      }
      if (!this.picCode) {
        this.$toast.fail('请输入图形验证码!')
        return false
      }
      if (!/^\w{4}$/.test(this.picCode)) {
        this.$toast.fail('请输入正确的图形验证码!')
        return false
      }
      return true
    },
    // 登录
    async login () {
      // 登陆校验
      if (!this.validFn()) {
        return
      }
      if (!/^\d{6}$/.test(this.smsCode)) {
        this.$toast.fail('请输入正确的手机验证码!')
        return
      }
      const res = await login(this.phone, this.smsCode)
      // 登录成功
      this.$toast.success(res.message)
      // 跳转到首页
      this.$router.push('/')
    }
  }
}
</script>

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

七、首页渲染

1、首页静态结构

1. 静态结构

  • 引入组件:@/plugins/vant-ui.js
import Vue from 'vue'
import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'

// 搜索框
Vue.use(Search)
// 轮播图
Vue.use(Swipe)
Vue.use(SwipeItem)
// 布局
Vue.use(Grid)
Vue.use(GridItem)

2. 接口封装

  • @/api/home.js
import request from '@/utils/request'

// 1.获取首页数据
export const getHomeData = () => {
  return request.get('/page/detail', {
    params: {
      pageId: 0
    }
  })
}

3. 封装组件

  • @/components/GoodsItem.vue
<template>
  <div v-if="item.goods_id" class="goods-item" @click="$router.push(`/prodetail/${item.goods_id}`)">
    <div class="left">
      <img :src="item.goods_image" alt="" />
    </div>
    <div class="right">
      <p class="tit text-ellipsis-2">{{ item.goods_name }}</p>
      <p class="count">已售{{ item.goods_sales}}件</p>
      <p class="price">
        <span class="new">¥{{ item.goods_price_min }}</span>
        <span class="old">¥{{ item.goods_price_max }}</span>
      </p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'GoodsItem',
  props: {
    item: {
      type: Object,
      default: () => {}
    }
  }
}
</script>

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

4. 页面渲染

  • @/views/layout/home.vue
<template>
  <div class="home">
    <!-- 导航条 -->
    <van-nav-bar :title="title" fixed />

    <!-- 搜索框 -->
    <van-search readonly shape="round" background="#f1f1f2" :placeholder='searchPrompt' @click="$router.push('/search')" />

    <!-- 轮播图 -->
    <van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
      <van-swipe-item v-for="item in bannerList" :key="item.imgName">
        <img :src="item.imgUrl" alt="">
      </van-swipe-item>
    </van-swipe>

    <!-- 导航 -->
    <van-grid column-num="5" icon-size="40">
      <van-grid-item v-for="item in navList" :key="item.imgUrl" :icon="item.imgUrl" text="新品首发" @click="$router.push('/category')" />
    </van-grid>

    <!-- 主会场 -->
    <div class="main">
      <img src="@/assets/main.png" alt="">
    </div>

    <!-- 猜你喜欢 -->
    <div class="guess">
      <p class="guess-title">—— 猜你喜欢 ——</p>
      <div class="goods-list">
        <GoodsItem v-for="item in goodsList" :item="item" :key="item.goods_id"></GoodsItem>
      </div>
    </div>
  </div>
</template>

<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getHomeData } from '@/api/home'
export default {
  name: 'HomePage',
  components: {
    GoodsItem
  },
  data () {
    return {
      title: '', // 标题
      searchPrompt: '', // 搜索框提示值
      bannerList: [], // 轮播图数据
      navList: [], // 导航
      goodsList: [] // 商品列表
    }
  },
  created () {
    this.getGoodsList()
  },
  methods: {
    async getGoodsList () {
      const {
        data: { pageData }
      } = await getHomeData()
      this.title = pageData.page.params.title
      this.searchPrompt = pageData.items[0].params.placeholder
      this.bannerList = pageData.items[1].data
      this.navList = pageData.items[3].data
      this.goodsList = pageData.items[6].data
    }
  }
}
</script>

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

2、搜索功能

1. 历史记录管理

  • 搜索历史基本渲染
  • 点击搜索 (添加历史):点击 搜索按钮 或 底下历史记录,都能进行搜索
    • 若之前 没有 相同搜索关键字,则直接追加到最前面
    • 若之前 已有 相同搜索关键字,将该原有关键字移除,再追加
  • 清空历史:添加清空图标,可以清空历史记录
<template>
  <div class="search">
    <van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)" />

    <van-search v-model="keyword" show-action placeholder="请输入搜索关键词" clearable>
      <template #action>
        <div @click="goSearch(keyword)">搜索</div>
      </template>
    </van-search>

    <!-- 搜索历史基本渲染 -->
    <div class="search-history" v-if="history.length > 0">
      <div class="title">
        <span>最近搜索</span>
        <van-icon name="delete-o" size="16" @click="clearHistory"/>
      </div>
      <div class="list">
        <div v-for="item in history" :key="item" class="list-item" @click="goSearch(item)">
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getSearchHistory, setSearchHistory } from '@/utils/storage'

export default {
  name: 'SearchIndex',
  data () {
    return {
      history: getSearchHistory(), // 历史记录
      keyword: '' // 搜索关键字
    }
  },
  methods: {
    goSearch (keyword) {
      // 判断搜索关键字在历史记录中是否存在
      if (this.history.indexOf(keyword) !== -1) {
        // 存在,则将搜索关键字移除
        this.history.splice(this.history.indexOf(keyword), 1)
      }
      // 将搜索关键字添加到历史记录数组中
      this.history.unshift(keyword)
      // 将搜索历史记录存储到本地
      setSearchHistory(this.history)
      // 跳转到搜索结果页面
      this.$router.push(`/searchList?search=${keyword}`)
    },
    clearHistory () {
      this.history = []
    }
  }
}
</script>

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

2. 历史记录持久化

  • 持久化:搜索历史需要持久化,刷新历史不丢失
  • @/utils/storage.js
// 约定一个通用的键名
const HISTORY_KEY = 'shopping_history_search'

// 获取搜索历史
export const getSearchHistory = () => {
  const history = localStorage.getItem(HISTORY_KEY)
  return history ? JSON.parse(history) : []
}

// 设置搜索历史
export const setSearchHistory = (history) => {
  localStorage.setItem(HISTORY_KEY, JSON.stringify(history))
}

// 移除搜索历史
export const removeSearchHistory = () => {
  localStorage.removeItem(HISTORY_KEY)
}

3、搜索列表

1. 接口渲染

  • @/api/product.js
import request from '@/utils/request'

// 1.获取搜索数据
export const getSearchData = (searchObj) => {
  const { sortType, sortPrice, categoryId, goodsName, page } = searchObj
  return request.get('/goods/list', {
    params: {
      sortType, // all-按综合搜索(默认),sales-按销量搜索,price-按价格搜索
      sortPrice, // 0-价格从低到高, 1-价格从高到低
      categoryId,
      goodsName, // 商品名称
      page// 页码
    }
  })
}

2. 页面调用

  • @/views/search/list.vue
<template>
  <div class="search">
    <van-nav-bar fixed title="商品列表" left-arrow @click-left="$router.go(-1)" />

    <van-search :value="searchValue || '搜索商品'" readonly shape="round" background="#ffffff" show-action @click="$router.push('/search')">
      <template #action>
        <van-icon class="tool" name="apps-o" />
      </template>
    </van-search>

    <!-- 排序选项按钮 -->
    <div class="sort-btns">
      <div class="sort-item" @click="getGoodsList('all', sortPrice)">综合</div>
      <div class="sort-item" @click="getGoodsList('sale', sortPrice)">销量</div>
      <div class="sort-item" @click="getGoodsList('price', sortPrice)">价格 </div>
    </div>

    <div class="goods-list">
      <GoodsItem v-for="item in goods" :item="item" :key="item.goods_id"></GoodsItem>
    </div>
  </div>
</template>

<script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getSearchData } from '@/api/product'

export default {
  name: 'SearchIndex',
  components: {
    GoodsItem
  },
  data () {
    return {
      goods: [],
      page: 1,
      sortPrice: '0' // 0-价格从低到高, 1-价格从高到低
    }
  },
  computed: {
    searchValue () {
      // 获取地址栏搜索关键字
      return this.$route.query.search
    }
  },
  created () {
    this.getGoodsList('all', '0')
  },
  methods: {
    getGoodsList (sortType, sortPrice) {
      if (sortPrice) {
        this.sortPrice = this.sortPrice === '0' ? '1' : '0'
      }
      getSearchData({
        sortType,
        sortPrice,
        goodsName: this.searchValue,
        page: this.page
      }).then(({ data: { list } }) => {
        this.goods = list.data
      })
    }
  }
}
</script>

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

八、商品详情

1、商品详情展示

1. 封装接口

import request from '@/utils/request'

// 1.获取搜索数据
export const getSearchData = (searchObj) => {
  const { sortType, sortPrice, categoryId, goodsName, page } = searchObj
  return request.get('/goods/list', {
    params: {
      sortType, // all-按综合搜索(默认),sales-按销量搜索,price-按价格搜索
      sortPrice, // 0-价格从低到高, 1-价格从高到低
      categoryId,
      goodsName, // 商品名称
      page// 页码
    }
  })
}

// 2.根据商品id获取商品详情
export const getGoodsDetail = (goodsId) => {
  return request.get('/goods/detail', {
    params: {
      goodsId
    }
  })
}

// 3.根据商品id获取评价总数
export const getCommentTotal = (goodsId) => {
  return request.get('/comment/total', {
    params: {
      goodsId
    }
  })
}

// 4.根据商品id获取评价
export const getComments = (goodsId, limit) => {
  return request.get('/comment/listRows', {
    params: {
      goodsId,
      limit
    }
  })
}

// 5.根据商品id获取评价
export const getGoodsService = (goodsId) => {
  return request.get('/goods.service/list', {
    params: {
      goodsId
    }
  })
}

2. 页面渲染

<template>
  <div class="prodetail">
    <van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" />

    <van-swipe :autoplay="3000" @change="onChange">
      <van-swipe-item v-for="item in images" :key="item.file_id">
        <img :src="item.external_url" />
      </van-swipe-item>

      <template #indicator>
        <div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
      </template>
    </van-swipe>

    <!-- 商品说明 -->
    <div class="info">
      <div class="title">
        <div class="price">
          <span class="now">¥{{ product.goods_price_min }}</span>
          <span class="oldprice">¥{{ product.goods_price_max }}</span>
        </div>
        <div class="sellcount">已售{{ product.goods_sales }}件</div>
      </div>
      <div class="msg text-ellipsis-2">
        {{ product.goods_name }}
      </div>

      <div class="service">
        <div class="left-words">
          <span v-for="item in goodsService" :key="item.service_id"><van-icon name="passed" />{{ item.name }}</span>
        </div>
        <div class="right-icon">
          <van-icon name="arrow" />
        </div>
      </div>
    </div>

    <!-- 商品评价 -->
    <div class="comment">
      <div class="comment-title">
        <div class="left">商品评价 ({{ commentTotal.all }}条)</div>
        <div class="right">查看更多 <van-icon name="arrow" /> </div>
      </div>
      <div class="comment-list">
        <div class="comment-item" v-for="item in commentList" :key="item.comment_id">
          <div class="top">
            <img :src="item.user.avatar_url || defaultImg" alt="">
            <div class="name">{{ item.user.nick_name }}</div>
            <van-rate :size="16" :value="item.score/2" color="#ffd21e" void-icon="star" void-color="#eee"/>
          </div>
          <div class="content">{{ item.content }}</div>
          <div class="time">{{ item.create_time }}</div>
        </div>
      </div>
    </div>

    <!-- 商品描述 -->
    <div class="desc" v-html="product.content">
    </div>

    <!-- 底部 -->
    <div class="footer">
      <div class="icon-home">
        <van-icon name="wap-home-o" />
        <span @click="$router.push('/')">首页</span>
      </div>
      <div class="icon-cart">
        <van-icon name="shopping-cart-o" />
        <span>购物车</span>
      </div>
      <div class="btn-add">加入购物车</div>
      <div class="btn-buy">立刻购买</div>
    </div>
  </div>
</template>

<script>
import { getGoodsDetail, getCommentTotal, getComments, getGoodsService } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'

export default {
  name: 'ProDetail',
  data () {
    return {
      images: [], // 商品图片
      current: 0, // 当前图片索引
      product: {}, // 商品详情
      commentTotal: {}, // 评论统计
      commentList: [], // 评论列表
      goodsService: [], // 商品服务
      defaultImg
    }
  },
  computed: {
    productId () {
      return this.$route.params.id
    }
  },
  created () {
    this.getProductDetail()
    this.getCommentTotal()
    this.getComments()
    this.getGoodsService()
  },
  methods: {
    // 获取商品详情
    getProductDetail () {
      getGoodsDetail(this.productId).then(({ data: { detail } }) => {
        this.product = detail
        this.images = detail.goods_images
      })
    },
    // 获取评论总数
    getCommentTotal () {
      getCommentTotal(this.productId).then(({ data: { total } }) => {
        console.log(total)
        this.commentTotal = total
      })
    },
    // 获取评论列表
    getComments () {
      getComments(this.productId, 5).then(({ data: { list } }) => {
        // this.commentTotal = total
        this.commentList = list
      })
    },
    // 获取商品保障服务
    getGoodsService () {
      getGoodsService(this.productId).then(({ data: { list } }) => {
        this.goodsService = list
      })
    },
    // 切换轮播图
    onChange (index) {
      this.current = index
    }
  }
}
</script>

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

2、加入购物车弹窗

1. 数字框组件

  • @/components/CountBox.vue
<template>
  <div class="count-box">
    <button @click="handleSub" class="minus">-</button>
    <input :value="value" @change="handleChange" class="inp" type="text">
    <button @click="handleAdd" class="add">+</button>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Number,
      default: 1
    }
  },
  methods: {
    handleSub () {
      if (this.value <= 1) return
      this.$emit('input', this.value - 1)
    },
    handleAdd () {
      this.$emit('input', this.value + 1)
    },
    handleChange (e) {
      // 过滤非数字字符
      const num = +e.target.value
      if (isNaN(num) || num < 1) {
        // 不合法的数字,重置为原值
        e.target.value = this.value
      } else {
        this.$emit('input', num)
      }
    }
  }
}
</script>

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

2. 加入购物车弹层

<template>
  <div class="prodetail">
    <van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" />

    <van-swipe :autoplay="3000" @change="onChange">
      <van-swipe-item v-for="item in images" :key="item.file_id">
        <img :src="item.external_url" />
      </van-swipe-item>

      <template #indicator>
        <div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
      </template>
    </van-swipe>

    <!-- 商品说明 -->
    <div class="info">
      <div class="title">
        <div class="price">
          <span class="now">¥{{ product.goods_price_min }}</span>
          <span class="oldprice">¥{{ product.goods_price_max }}</span>
        </div>
        <div class="sellcount">已售{{ product.goods_sales }}件</div>
      </div>
      <div class="msg text-ellipsis-2">
        {{ product.goods_name }}
      </div>

      <div class="service">
        <div class="left-words">
          <span v-for="item in goodsService" :key="item.service_id"><van-icon name="passed" />{{ item.name }}</span>
        </div>
        <div class="right-icon">
          <van-icon name="arrow" />
        </div>
      </div>
    </div>

    <!-- 商品评价 -->
    <div class="comment">
      <div class="comment-title">
        <div class="left">商品评价 ({{ commentTotal.all }}条)</div>
        <div class="right">查看更多 <van-icon name="arrow" /> </div>
      </div>
      <div class="comment-list">
        <div class="comment-item" v-for="item in commentList" :key="item.comment_id">
          <div class="top">
            <img :src="item.user.avatar_url || defaultImg" alt="">
            <div class="name">{{ item.user.nick_name }}</div>
            <van-rate :size="16" :value="item.score/2" color="#ffd21e" void-icon="star" void-color="#eee"/>
          </div>
          <div class="content">{{ item.content }}</div>
          <div class="time">{{ item.create_time }}</div>
        </div>
      </div>
    </div>

    <!-- 商品描述 -->
    <div class="desc" v-html="product.content">
    </div>

    <!-- 底部 -->
    <div class="footer">
      <div class="icon-home" @click="$router.push('/')">
        <van-icon name="wap-home-o" />
        <span>首页</span>
      </div>
      <div class="icon-cart" @click="$router.push('/cart')">
        <van-icon v-if="$store.state.cart.cartTotal === 0" name="shopping-cart-o" />
        <van-icon v-else :badge="$store.state.cart.cartTotal" name="shopping-cart-o" />
        <span>购物车</span>
      </div>
      <div @click="addFn" class="btn-add">加入购物车</div>
      <div @click="buyFn" class="btn-buy">立刻购买</div>
    </div>

    <!-- 加入购物车弹层 -->
    <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
      <div class="product">
        <div class="product-title">
          <div class="left">
            <img :src="product.goods_image" alt="">
          </div>
          <div class="right">
            <div class="price">
              <span>¥</span>
              <span class="nowprice">{{ product.goods_price_min }}</span>
            </div>
            <div class="count">
              <span>库存</span>
              <span>{{ product.stock_total }}</span>
            </div>
          </div>
        </div>
        <div class="num-box">
          <span>数量</span>
          <count-box v-model="addCount"></count-box>
        </div>
        <div class="showbtn" v-if="product.stock_total > 0">
          <div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div>
          <div class="btn now" v-else>立刻购买</div>
        </div>
        <div class="btn-none" v-else>该商品已抢完</div>
      </div>
    </van-action-sheet>
  </div>
</template>

<script>
import { getGoodsDetail, getCommentTotal, getComments, getGoodsService } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'
import CountBox from '@/components/CountBox.vue'

export default {
  name: 'ProDetail',
  components: {
    CountBox
  },
  data () {
    return {
      images: [], // 商品图片
      current: 0, // 当前图片索引
      product: {}, // 商品详情
      commentTotal: {}, // 评论统计
      commentList: [], // 评论列表
      goodsService: [], // 商品服务
      defaultImg, // 默认头像
      showPannel: false, // 加入购物车弹层
      mode: 'cart',
      addCount: 1 // 加入购物车数量
    }
  },
  computed: {
    productId () {
      return this.$route.params.id
    }
  },
  created () {
    this.getProductDetail()
    this.getCommentTotal()
    this.getComments()
    this.getGoodsService()
  },
  methods: {
    // 获取商品详情
    getProductDetail () {
      getGoodsDetail(this.productId).then(({ data: { detail } }) => {
        this.product = detail
        this.images = detail.goods_images
      })
    },
    // 获取评论总数
    getCommentTotal () {
      getCommentTotal(this.productId).then(({ data: { total } }) => {
        this.commentTotal = total
      })
    },
    // 获取评论列表
    getComments () {
      getComments(this.productId, 5).then(({ data: { list } }) => {
        // this.commentTotal = total
        this.commentList = list
      })
    },
    // 获取商品保障服务
    getGoodsService () {
      getGoodsService(this.productId).then(({ data: { list } }) => {
        this.goodsService = list
      })
    },
    // 加入购物车
    addFn () {
      this.showPannel = true
      this.mode = 'cart'
    },
    // 立即购买
    buyFn () {
      this.showPannel = true
      this.mode = 'buyNow'
    },
    addCart () {
      if (!this.$store.getters.token) {
        // 未登录
        this.$dialog.confirm({
          title: '温馨提示',
          message: '此操作只有登陆才能进行!',
          confirmButtonText: '去登录',
          cancelButtonText: '在逛逛'
        }).then(() => {
          // 确定前往登录页(replace:跳转页面不会添加到历史记录)
          this.$router.replace({
            path: '/login',
            query: {
              // 登录成功后跳转回来
              backUrl: this.$route.fullPath
            }
          })
        }).catch(() => {
          // 取消
          this.$toast('已取消')
        })
      } else {
        this.showPannel = false
      }
    },
    // 获取购物车列表
    getCartList () {
      if (!this.$store.getters.token) return
      getCartList().then(({ data }) => {
        this.$store.dispatch('cart/getCartListAction')
      })
    },
    // 切换轮播图
    onChange (index) {
      this.current = index
    }
  }
}
</script>

<style lang="less" scoped>
.prodetail {
  padding-top: 46px;
  ::v-deep .van-icon-arrow-left {
    color: #333;
  }
  img {
    display: block;
    width: 100%;
  }
  .custom-indicator {
    position: absolute;
    right: 10px;
    bottom: 10px;
    padding: 5px 10px;
    font-size: 12px;
    background: rgba(0, 0, 0, 0.1);
    border-radius: 15px;
  }
  .desc {
    width: 100%;
    overflow: scroll;
    ::v-deep img {
      display: block;
      width: 100%!important;
    }
  }
  .info {
    padding: 10px;
  }
  .title {
    display: flex;
    justify-content: space-between;
    .now {
      color: #fa2209;
      font-size: 20px;
    }
    .oldprice {
      color: #959595;
      font-size: 16px;
      text-decoration: line-through;
      margin-left: 5px;
    }
    .sellcount {
      color: #959595;
      font-size: 16px;
      position: relative;
      top: 4px;
    }
  }
  .msg {
    font-size: 16px;
    line-height: 24px;
    margin-top: 5px;
  }
  .service {
    display: flex;
    justify-content: space-between;
    line-height: 40px;
    margin-top: 10px;
    font-size: 16px;
    background-color: #fafafa;
    .left-words {
      span {
        margin-right: 10px;
      }
      .van-icon {
        margin-right: 4px;
        color: #fa2209;
      }
    }
  }

  .comment {
    padding: 10px;
  }
  .comment-title {
    display: flex;
    justify-content: space-between;
    .right {
      color: #959595;
    }
  }

  .comment-item {
    font-size: 16px;
    line-height: 30px;
    .top {
      height: 30px;
      display: flex;
      align-items: center;
      margin-top: 20px;
      img {
        width: 20px;
        height: 20px;
      }
      .name {
        margin: 0 10px;
      }
    }
    .time {
      color: #999;
    }
  }

  .footer {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 55px;
    background-color: #fff;
    border-top: 1px solid #ccc;
    display: flex;
    justify-content: space-evenly;
    align-items: center;
    .icon-home, .icon-cart {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 14px;
      .van-icon {
        font-size: 24px;
      }
    }
    .btn-add,
    .btn-buy {
      height: 36px;
      line-height: 36px;
      width: 120px;
      border-radius: 18px;
      background-color: #ffa900;
      text-align: center;
      color: #fff;
      font-size: 14px;
    }
    .btn-buy {
      background-color: #fe5630;
    }
  }
}
.tips {
  padding: 10px;
}
.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;
  }
}
</style>

九、购物车

1、构建 vuex cart 模块

  • 构建模块 @/store/modules/cart.js

2、封装接口

import request from '@/utils/request'

// 添加购物车
export const addCart = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/add', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}

// 获取购物车商品总数量
export const getCartTotalCount = () => {
  return request.get('/cart/total')
}

// 获取购物车商品列表
export const getCartList = () => {
  return request.get('/cart/list')
}

// 更新购物车商品数量
export const updateCartNum = (goodsNum, goodsId, goodsSkuId) => {
  return request.post('/cart/update', {
    goodsNum,
    goodsId,
    goodsSkuId
  })
}

// 删除购物车商品
export const deleteCartGoods = (cartIds) => {
  return request.post('/cart/clear', {
    cartIds
  })
}

3、动态渲染功能实现

  • 封装 getters 实现动态统计
  • 全选反选功能
  • 数字框修改数量功能
  • 编辑切换状态,删除功能
  • 空购物车处理
<template>
  <div class="cart">
    <van-nav-bar title="购物车" fixed />
    <div v-if="isLogin && cartTotal > 0 ">
      <!-- 购物车开头 -->
      <div class="cart-title">
        <span class="all">共<i>{{ cartTotal }}</i>件商品</span>
        <span class="edit" @click="isEdit = !isEdit">
          <van-icon name="edit"/>
          编辑
        </span>
      </div>

      <!-- 购物车列表 -->
      <div class="cart-list">
        <div class="cart-item" v-for="item in cartList" :key="item.goods_id">
          <van-checkbox :value="item.is_checked" @click="toggleCheck(item.goods_id)"></van-checkbox>
          <div class="show">
            <img :src="item.goods.goods_image" alt="">
          </div>
          <div class="info">
            <span class="tit text-ellipsis-2">{{ item.goods.goods_name }}</span>
            <span class="bottom">
              <div class="price">¥ <span>{{ item.goods.goods_price_min }}</span></div>
              <!-- 既保留原本的形参,又需要调用函数传参,可以通过箭头函数包装 -->
              <count-box @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)" :value="item.goods_num" ></count-box>
            </span>
          </div>
        </div>
      </div>

      <div class="footer-fixed">
        <div @click="toggleAllCheck" class="all-check">
          <van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
          全选
        </div>

        <div class="all-total">
          <div class="price">
            <span>合计:</span>
            <span>¥ <i class="totalPrice">{{ checkedTotalPrice }}</i></span>
          </div>
          <div v-if="!isEdit" class="goPay" :class="{disabled: getCheckedTotal === 0}">结算({{ getCheckedTotal }})</div>
          <div v-else @click="deleteCheckedGoods" class="delete" :class="{disabled: getCheckedTotal === 0}">删除</div>
        </div>
      </div>
    </div>
    <div class="empty-cart" v-else>
      <img src="@/assets/empty.png" alt="">
      <div class="tips">
        您的购物车是空的, 快去逛逛吧
      </div>
      <div class="btn" @click="$router.push('/')">去逛逛</div>
    </div>
  </div>
</template>

<script>
import CountBox from '@/components/CountBox.vue'
import { mapState, mapGetters } from 'vuex'

export default {
  name: 'CartPage',
  components: {
    CountBox
  },
  data () {
    return {
      isEdit: false
    }
  },
  computed: {
    ...mapState('cart', ['cartList', 'cartTotal']),
    ...mapGetters('cart', ['getCheckedTotal', 'isAllChecked']),
    isLogin () {
      return this.$store.getters.token
    },
    checkedTotalPrice () {
      return this.$store.getters['cart/getCheckedTotalPrice']
    }
  },
  created () {
    if (this.isLogin) {
      this.$store.dispatch('cart/getCartListAction')
    }
  },
  methods: {
    toggleCheck (goodsId) {
      this.$store.commit('cart/toggleCheck', goodsId)
    },
    toggleAllCheck () {
      this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
    },
    changeCount (goodsNum, goodsId, goodsSkuId) {
      this.$store.dispatch('cart/changeCountAction', {
        goodsNum, goodsId, goodsSkuId
      })
    },
    deleteCheckedGoods () {
      if (this.getCheckedTotal === 0) return
      this.$store.dispatch('cart/deleteCheckedAction')
    }
  },
  watch: {
    isEdit (value) {
      if (value) {
        this.$store.commit('cart/toggleAllCheck', false)
      } else {
        this.$store.commit('cart/toggleAllCheck', true)
      }
    }
  }
}
</script>

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

十、订单结算

1、订单结算台

1. 封装接口

import request from '@/utils/request'

// 1.获取收货地址
export const getAddress = () => {
  return request.get('/address/list')
}

// 2.获取默认收货地址
export const getDefaultAddress = () => {
  return request.get('/address/defaultId')
}

// 3.订单结算
// mode: cart => obj: { cartId }
// mode: buyNow => obj: { goodsId, goodsNum, goodsSkuId }
export const checkoutOrder = (mode, obj) => {
  return request.get('/checkout/order', {
    params: {
      delivery: 10, // 10 快递 20 自提
      couponId: 0, // 优惠券id, 0:不适用优惠
      isUsePoints: 0, // 是否使用积分, 0:不使用
      mode, // cart/buyNow
      ...obj // 将传递过来的参数对象动态展开
    }
  })
}

2. 订单页面渲染

<template>
  <div class="pay">
    <van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />

    <!-- 地址相关 -->
    <div class="address">

      <div class="left-icon">
        <van-icon name="logistics" />
      </div>

      <div class="info" v-if="selectedAddress.address_id">
        <div class="info-content">
          <span class="name">{{ selectedAddress.name }}</span>
          <span class="mobile">{{ selectedAddress.phone }}</span>
        </div>
        <div class="info-address">
          {{ this.address }}
        </div>
      </div>

      <div class="info" v-else>
        请选择配送地址
      </div>

      <div class="right-icon">
        <van-icon name="arrow" />
      </div>
    </div>

    <!-- 订单明细 -->
    <div class="pay-list" v-if="order.goodsList">
      <div class="list">
        <div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id">
            <div class="left">
              <img :src="item.goods_image" alt="" />
            </div>
            <div class="right">
              <p class="tit text-ellipsis-2">
                 {{ item.goods_name }}
              </p>
              <p class="info">
                <span class="count">x{{ item.total_num }}</span>
                <span class="price">¥{{ item.total_pay_price }}</span>
              </p>
            </div>
        </div>
      </div>

      <div class="flow-num-box">
        <span>共 {{ order.orderTotalNum }} 件商品,合计:</span>
        <span class="money">¥{{ order.orderPrice }}</span>
      </div>

      <div class="pay-detail">
        <div class="pay-cell">
          <span>订单总金额:</span>
          <span class="red">¥{{ order.orderTotalPrice }}</span>
        </div>

        <div class="pay-cell">
          <span>优惠券:</span>
          <span> {{ order.isUsePoints === '0' ? '无优惠券可用' : ''}}</span>
        </div>

        <div class="pay-cell">
          <span>配送费用:</span>
          <span v-if="!selectedAddress">请先选择配送地址</span>
          <span v-else class="red">+¥0.00</span>
        </div>
      </div>

      <!-- 支付方式 -->
      <div class="pay-way">
        <span class="tit">支付方式</span>
        <div class="pay-cell">
          <span><van-icon name="balance-o" />余额支付(可用 ¥ {{ personal.balance }} 元)</span>
          <!-- <span>请先选择配送地址</span> -->
          <span class="red"><van-icon name="passed" /></span>
        </div>
      </div>

      <!-- 买家留言 -->
      <div class="buytips">
        <textarea placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
      </div>
    </div>

    <!-- 底部提交 -->
    <div class="footer-fixed">
      <div class="left">实付款:<span>¥{{ order.orderPayPrice }}</span></div>
      <div class="tipsbtn">提交订单</div>
    </div>
  </div>
</template>

<script>
import { getAddress, getDefaultAddress, checkoutOrder } from '@/api/order'

export default {
  name: 'PayIndex',
  data () {
    return {
      selectedAddressId: '', // 默认地址id
      addressList: [], // 地址列表
      order: {}, // 订单信息
      personal: {}, // 个人信息
      setting: {}// 系统设置
    }
  },
  computed: {
    selectedAddress () {
      return this.addressList.find(item => item.address_id === this.selectedAddressId) || {}
    },
    address () {
      const region = this.selectedAddress.region
      return region.province + region.city + region.region + this.selectedAddress.detail
    },
    mode () {
      return this.$route.query.mode
    },
    cartIds () {
      return this.$route.query.cartIds
    },
    goodsId () {
      return this.$route.query.goodsId
    },
    goodsNum () {
      return this.$route.query.goodsNum
    },
    goodsSkuId () {
      return this.$route.query.goodsSkuId
    }
  },
  created () {
    this.getAddress()
    this.getSelectedAddressId()
    this.checkoutOrder()
  },
  methods: {
    getAddress () {
      getAddress().then(res => {
        this.addressList = res.data.list
      })
    },
    getSelectedAddressId () {
      getDefaultAddress().then(res => {
        const selectedId = res.data.defaultId
        if (selectedId) {
          this.selectedAddressId = selectedId
        }
      })
    },
    checkoutOrder () {
      checkoutOrder(this.mode, {
        cartIds: this.cartIds,
        goodsId: this.goodsId,
        goodsNum: this.goodsNum,
        goodsSkuId: this.goodsSkuId
      }).then(res => {
        const { order, personal, setting } = res.data
        this.order = order
        this.personal = personal
        this.setting = setting
      })
    }
  }
}
</script>

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

3. mixins 混入

  • 如果此处和组件内,提供同名的 data 和 methods, 则会覆盖此处同名方法
// 登录确认
export default {
  methods: {
    // 登录确认
    loginConfirm () {
      if (!this.$store.getters.token) {
        // 未登录,引导前往登陆页面
        this.$dialog.confirm({
          title: '温馨提示',
          message: '此操作只有登陆才能进行!',
          confirmButtonText: '去登录',
          cancelButtonText: '再逛逛'
        }).then(() => {
          // 确定前往登录页(replace:跳转页面不会添加到历史记录)
          this.$router.replace({
            path: '/login',
            query: {
              // 登录成功后跳转回来
              backUrl: this.$route.fullPath
            }
          })
        }).catch(() => {})
        return true
      }
      return false
    }
  }
}

4. 立即购买

  • @/views/proDetail/index.vue
<template>
  <div class="prodetail">

    <!-- 加入购物车弹层 -->
    <van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
      <div class="product">
        <div class="product-title">
          <div class="left">
            <img :src="product.goods_image" alt="">
          </div>
          <div class="right">
            <div class="price">
              <span>¥</span>
              <span class="nowprice">{{ product.goods_price_min }}</span>
            </div>
            <div class="count">
              <span>库存</span>
              <span>{{ product.stock_total }}</span>
            </div>
          </div>
        </div>
        <div class="num-box">
          <span>数量</span>
          <count-box v-model="addCount"></count-box>
        </div>
        <div class="showbtn" v-if="product.stock_total > 0">
          <div class="btn" v-if="mode === 'cart'" @click="addCart">加入购物车</div>
          <div class="btn now" v-else @click="goBuyNow">立刻购买</div>
        </div>
        <div class="btn-none" v-else>该商品已抢完</div>
      </div>
    </van-action-sheet>
  </div>
</template>

<script>
import { addCart, getCartList } from '@/api/cart'
import { Toast } from 'vant'
// 引入 mixins 
import loginConfirm from '@/mixins/loginConfirm'

export default {
  name: 'ProDetail',
  // 引入 mixins 
  mixins: [loginConfirm],
  methods: {
    // 加入购物车
    addCart () {
      // 调用 mixins 方法判断是否登陆
      if (this.loginConfirm()) {
        return
      }
      addCart(this.productId, this.addCount, this.product.skuList[0].goods_sku_id).then(({ data, message }) => {
        this.showPannel = false
        this.getCartList()
        Toast.success(message)
      })
    },
    // 立即购买
    goBuyNow () {
      // 调用 mixins 方法判断是否登陆
      if (this.loginConfirm()) {
        return
      }
      if (this.addCount <= 0) return
      this.$router.push({
        path: '/pay',
        query: {
          mode: 'buyNow',
          goodsId: this.product.goods_id,
          goodsNum: this.addCount,
          goodsSkuId: this.product.skuList[0].goods_sku_id
        }
      })
    }
  }
}
</script>

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

5. 购物车结算

<template>
  <div class="cart">
    <van-nav-bar title="购物车" fixed />
    <div v-if="isLogin && cartTotal > 0 ">
      <div class="footer-fixed">
        <div class="all-total">
          <div class="price">
            <span>合计:</span>
            <span>¥ <i class="totalPrice">{{ checkedTotalPrice }}</i></span>
          </div>
          <div v-if="!isEdit" class="goPay" @click="goPay" :class="{disabled: getCheckedTotal === 0}">结算({{ getCheckedTotal }})</div>
          <div v-else @click="deleteCheckedGoods" class="delete" :class="{disabled: getCheckedTotal === 0}">删除</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import CountBox from '@/components/CountBox.vue'
import { mapState, mapGetters } from 'vuex'

export default {
  name: 'CartPage',
  computed: {
    ...mapState('cart', ['cartList', 'cartTotal']),
    ...mapGetters('cart', ['getCheckedTotal', 'getCheckedList', 'isAllChecked']),
  },
  methods: {
    // 跳转到支付页面
    goPay () {
      // 判断是否勾选商品
      if (this.getCheckedTotal === 0) return
      this.$router.push({
        path: '/pay',
        query: {
          mode: 'cart',
          cartIds: this.getCheckedList.map(item => item.id).join(',')
        }
      })
    }
  }
}
</script>

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

2、提交订单并支付

1. 封装接口

import request from '@/utils/request'

// 4.订单提交
// mode: cart => obj: { cartId, remark }
// mode: buyNow => obj: { goodsId, goodsNum, goodsSkuId, remark }
export const submitOrder = (mode, obj) => {
  return request.get('/checkout/submit', {
    params: {
      delivery: 10, // 10 快递 20 自提
      couponId: 0, // 优惠券id, 0:不适用优惠
      isUsePoints: 0, // 是否使用积分, 0:不使用
      payType: 10, // 10:余额支付
      mode, // cart/buyNow
      ...obj // 将传递过来的参数对象动态展开
    }
  })
}

2. 页面渲染

<template>
  <div class="pay">
    <van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />

    <!-- 买家留言 -->
    <div class="buytips">
      <textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
  	</div>

    <!-- 底部提交 -->
    <div class="footer-fixed">
      <div class="left">实付款:<span>¥{{ order.orderPayPrice }}</span></div>
      <div class="tipsbtn" @click="submitOrder">提交订单</div>
    </div>
  </div>
</template>

<script>
import { submitOrder } from '@/api/order'
import { Toast } from 'vant'

export default {
  name: 'PayIndex',
  data () {
    return {
      selectedAddressId: '', // 默认地址id
      addressList: [], // 地址列表
      order: {}, // 订单信息
      personal: {}, // 个人信息
      setting: {}, // 系统设置
      remark: ''
    }
  },
  computed: {
    mode () {
      return this.$route.query.mode
    },
    cartIds () {
      return this.$route.query.cartIds
    },
    goodsId () {
      return this.$route.query.goodsId
    },
    goodsNum () {
      return this.$route.query.goodsNum
    },
    goodsSkuId () {
      return this.$route.query.goodsSkuId
    }
  },
  methods: {
    // 提交订单
    async submitOrder () {
      const obj = {
        remark: this.remark
      }
      if (this.mode === 'cart') {
        Object.assign(obj, { cartIds: this.cartIds })
      } else {
        Object.assign(obj, { goodsId: this.goodsId })
        Object.assign(obj, { goodsNum: this.goodsNum })
        Object.assign(obj, { goodsSkuId: this.goodsSkuId })
      }
      await submitOrder(this.mode, obj)
      Toast.success('支付成功')
      this.$router.replace('/myOrder')
    }
  }
}
</script>

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

3、订单管理

1. 封装接口

import request from '@/utils/request'

// 5. 我的订单
export const getOrderList = (dataType, page) => {
  return request.get('/order/list', {
    params: {
      dataType, // 订单类型,all-全部,payment-待支付,delivery-待发货,received-待收货,comment-待评价
      page
    }
  })
}

2. 订单组件

<template>
  <div class="order-list-item">
    <div class="tit">
      <div class="time">{{ value.create_time }}</div>
      <div class="status">
        <span>{{ value.state_text }}</span>
      </div>
    </div>
    <div class="list">
      <div class="list-item" v-for="item in value.goods" :key="item.goods_id">
        <div class="goods-img">
          <img :src="item.goods_image" alt="">
        </div>
        <div class="goods-content text-ellipsis-2">
          {{ item.goods_name }}
        </div>
        <div class="goods-trade">
          <p>¥ {{ item.total_pay_price }}</p>
          <p>x {{ item.total_num }}</p>
        </div>
      </div>
    </div>
    <div class="total">
      共{{ totalNum }}件商品,总金额 ¥{{ value.total_price }}
    </div>
    <div class="actions">
      <div v-if="value.order_status === 10">
        <span v-if="value.pay_status === 10">立刻付款</span>
        <span v-else-if="value.delivery_status === 10">申请取消</span>
        <span v-else-if="value.delivery_status === 20 || value.delivery_status === 30">确认收货</span>
      </div>
      <span v-if="value.order_status === 30">评价</span>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Object,
      required: true
    }
  },
  computed: {
    totalNum () {
      return this.value.goods.reduce((sum, item) => sum + item.total_num, 0)
    }
  }
}
</script>

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

3. 页面渲染

<template>
  <div class="order">
    <van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" />
    <van-tabs v-model="active"  @click="onClick">
      <van-tab title="全部" name="all"></van-tab>
      <van-tab title="待支付" name="payment"></van-tab>
      <van-tab title="待发货" name="delivery"></van-tab>
      <van-tab title="待收货" name="received"></van-tab>
      <van-tab title="待评价" name="comment"></van-tab>
    </van-tabs>

    <OrderListItem v-for="item in orderList" :key="item.id" :value="item"></OrderListItem>
  </div>
</template>

<script>
import OrderListItem from '@/components/OrderListItem.vue'
import { getOrderList } from '@/api/order'

export default {
  name: 'OrderPage',
  components: {
    OrderListItem
  },
  data () {
    return {
      active: this.$route.query.dataType || 'all',
      orderList: []
    }
  },
  created () {
    this.getOrderList()
  },
  methods: {
    getOrderList () {
      getOrderList(this.active, 1).then(res => {
        this.orderList = res.data.list.data
      })
    },
    onClick (name) {
      console.log(name)
      // this.getOrderList()
    }
  },
  watch: {
    active: {
      immediate: true,
      handler () {
        this.getOrderList()
      }
    }
  }
}
</script>

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

4、个人中心

1. 封装接口

import request from '@/utils/request'

// 获取个人信息
export const getUserInfoDetail = () => {
  return request.get('/user/info')
}

2. 页面渲染

<template>
  <div class="user">
    <div class="head-page" v-if="isLogin">
      <div class="head-img">
        <img src="@/assets/default-avatar.png" alt="" />
      </div>
      <div class="info">
        <div class="mobile">{{ detail.mobile }}</div>
        <div class="vip">
          <van-icon name="diamond-o" />
          普通会员
        </div>
      </div>
    </div>

    <div v-else class="head-page" @click="$router.push('/login')">
      <div class="head-img">
        <img src="@/assets/default-avatar.png" alt="" />
      </div>
      <div class="info">
        <div class="mobile">未登录</div>
        <div class="words">点击登录账号</div>
      </div>
    </div>

    <div class="my-asset">
      <div class="asset-left">
        <div class="asset-left-item">
          <span>{{ detail.balance || 0 }}</span>
          <span>账户余额</span>
        </div>
        <div class="asset-left-item">
          <span>0</span>
          <span>积分</span>
        </div>
        <div class="asset-left-item">
          <span>0</span>
          <span>优惠券</span>
        </div>
      </div>
      <div class="asset-right">
        <div class="asset-right-item">
          <van-icon name="balance-pay" />
          <span>我的钱包</span>
        </div>
      </div>
    </div>
    <div class="order-navbar">
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=all')">
        <van-icon name="balance-list-o" />
        <span>全部订单</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=payment')">
        <van-icon name="clock-o" />
        <span>待支付</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=delivery')">
        <van-icon name="logistics" />
        <span>待发货</span>
      </div>
      <div class="order-navbar-item" @click="$router.push('/myorder?dataType=received')">
        <van-icon name="send-gift-o" />
        <span>待收货</span>
      </div>
    </div>

    <div class="service">
      <div class="title">我的服务</div>
      <div class="content">
        <div class="content-item">
          <van-icon name="records" />
          <span>收货地址</span>
        </div>
        <div class="content-item">
          <van-icon name="gift-o" />
          <span>领券中心</span>
        </div>
        <div class="content-item">
          <van-icon name="gift-card-o" />
          <span>优惠券</span>
        </div>
        <div class="content-item">
          <van-icon name="question-o" />
          <span>我的帮助</span>
        </div>
        <div class="content-item">
          <van-icon name="balance-o" />
          <span>我的积分</span>
        </div>
        <div class="content-item">
          <van-icon name="refund-o" />
          <span>退换/售后</span>
        </div>
      </div>
    </div>

    <div class="logout-btn">
     <button>退出登录</button>
    </div>
  </div>
</template>

<script>
import { getUserInfoDetail } from '@/api/user.js'
export default {
  name: 'UserPage',
  data () {
    return {
      detail: {}
    }
  },
  created () {
    if (this.isLogin) {
      this.getUserInfoDetail()
    }
  },
  computed: {
    isLogin () {
      return this.$store.getters.token
    }
  },
  methods: {
    async getUserInfoDetail () {
      const { data: { userInfo } } = await getUserInfoDetail()
      this.detail = userInfo
      console.log(this.detail)
    }
  }
}
</script>

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

3. 退出登陆

  • @/views/layout/user.vue
<template>
  <div class="user">
    <div class="logout-btn" v-if="isLogin">
     <button>退出登录</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserPage',
  methods: {
    logout () {
      this.$dialog.confirm({
        title: '温馨提示',
        message: '你确认要退出么?'
      })
        .then(() => {
        	this.detail = {}
          this.$store.dispatch('user/logout')
        })
        .catch(() => {
        })
    }
  }
}
</script>

<style lang="less" scoped>
</style>
  • @/store/modules/user.js
import { getUserInfo, setUserInfo } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      userInfo: getUserInfo()
    }
  },
  mutations: {
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
      // 缓存用户信息
      setUserInfo(userInfo)
    }
  },
  actions: {
    logout (context) {
      // 清除用户信息
      context.commit('setUserInfo', {})
      // 清除购物车信息
      context.commit('cart/setCartList', [], { root: true })
    }
  },
  getters: {}
}

十一、打包

1、打包命令

yarn build

2、配置publicPath

  • vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  publicPath: './',
  transpileDependencies: true
})

3、路由懒加载

import Vue from 'vue'
import VueRouter from 'vue-router'

import Home from '@/views/layout/home'
import Category from '@/views/layout/category'
import Cart from '@/views/layout/cart'
import User from '@/views/layout/user'
import Layout from '@/views/layout'

const Login = () => import('@/views/login')
const MyOrder = () => import('@/views/myOrder')
const Pay = () => import('@/views/pay')
const ProDetail = () => import('@/views/proDetail')
const Search = () => import('@/views/search')
const SearchList = () => import('@/views/search/list')

// import store from '@/store'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Layout,
      redirect: '/home',
      children: [
        { path: '/home', component: Home },
        { path: '/category', component: Category },
        { path: '/cart', component: Cart },
        { path: '/user', component: User }
      ]
    },
    { path: '/login', component: Login },
    { path: '/myOrder', component: MyOrder },
    { path: '/pay', component: Pay },
    { path: '/proDetail/:id', component: ProDetail },
    { path: '/search', component: Search },
    { path: '/searchList', component: SearchList }
  ]
})

// 定义一个数组,存放所以需要权限的页面
const authUrls = ['/myOrder', '/pay']
// 全局前置导航守卫
// to:往哪里去, 到哪去的路由信息对象
// from:从哪里来, 从哪来的路由信息对象
// 3. next() 是否放行
// 如果next()调用,就是放行
// next(路径) 拦截到某个路径页面, 跳转到该页面
router.beforeEach((to, from, next) => {
  // 获取 to.path 判断是否存在于 需要权限的页面
  if (!authUrls.includes(to.path)) {
    // 放行
    next()
  } else {
    // const token = store.getters.token
    if (store.getters.token) {
      // 已经登录,放行
      next()
    } else {
      // 未登录,跳转登陆页
      next('/login')
    }
  }
})

export default router
  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue2中,可以利用axios进行网络请求的操作。下面是一个使用axios的案例: ```javascript // 引入axios库 import axios from 'axios' // 创建Vue实例 new Vue({ methods: { // 定义一个方法来发送请求 async fetchData() { try { // 使用axios发送GET请求获取数据 const response = await axios.get('/api/data') // 打印返回的数据 console.log(response.data) } catch (error) { // 发生错误时打印错误信息 console.error(error) } } }, // 在Vue的生命周期钩子中调用fetchData方法 created() { this.fetchData() } }) ``` 在这个案例中,我们首先需要引入axios库。接着,在Vue的方法中定义一个async函数fetchData,在这个函数中使用await关键字来发送GET请求获取数据。如果请求成功,我们将打印返回的数据,如果发生错误,则打印错误信息。 请注意,这只是一个简单的案例,实际使用中可能需要配置axios的请求头、超时时间等参数,以及处理其他类型的请求。 需要注意的是,async和await关键字是ES6中的新特性,用于简化异步操作的代码结构。在上述案例中,通过使用async函数和await关键字,可以使代码更加简洁易读。 <span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [VUE-Axios案例](https://blog.csdn.net/weixin_53244569/article/details/119953557)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值