8、Vue 核心技术与实战 智慧商城项目 DAY8~10

1、 项目演示

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第1张图片

2、 项目收获

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第2张图片

3、 创建项目

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第3张图片

4、调整初始化目录

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第4张图片

  • 1、删掉(1)assets中的文件(2)components中的文件(3)views中的文件
  • 2、(1)修改路由配置,默认的路由是个空数组即可,把路由规则都删掉
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: []
})

export default router

(2)修改App.vue,直留路由出口即可

<template>
  <div id="app">
    <router-view/>
  div>
template>

<style lang="less">

style>
  • 3.新增两个目录 api和utils

5、vant 组件库

目标:认识第三方 Vue组件库 vant-ui
组件库:第三方 封装 好了很多很多的 组件,整合到一起就是一个组件库。
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第5张图片

6、其他 Vue 组件库

目标:了解其他 Vue 组件库
Vue的组件库并不是唯一的,vant-ui 也仅仅只是组件库的一种。
一般会按照不同平台进行分类:
① PC端: 支持vue2:element-ui 支持vue3:(element-plus) 支持vue2\3:ant-design-vue
② 移动端:vant-ui、 Mint UI (饿了么)、 Cube UI (滴滴)
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第6张图片

7、vant 全部导入 和 按需导入

目标:明确 全部导入 和 按需导入 的区别
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第7张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第8张图片

7.1 全部导入:

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第9张图片

7.2 按需导入:

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第10张图片
单独提取到utils - vant-ui.js导入

// 按需导入
import Vue from 'vue'
import { Button, Switch } from 'vant'
Vue.use(Button)
Vue.use(Switch)

main.js

import '@/utils/vant-ui'

8、项目中的 vw 适配

目标:基于 postcss 插件 实现项目 vw 适配
官方配置
① 安装插件

yarn add [email protected] -D

② 根目录新建 postcss.config.js 文件,填入配置

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
    // vw适配的标准屏的宽度 iponeX
    //   设计图 750,调成1倍图 => 适配375标准屏幕
    //   设计图 640,调成1倍图 => 适配320标准屏幕
      viewportWidth: 375
    }
  }
}

9、路由设计配置 - 一级路由

目标:分析项目页面,设计路由,配置一级路由
但凡是单个页面,独立展示的,都是一级路由
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第11张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第12张图片

`router - index.js```

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import ProDetail from '@/views/prodetail'
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 },
    // 动态路由传参,确定将来哪个是商品,路由参数中携带id
    { path: '/prodetail/:id', component: ProDetail },
    { path: '/pay', component: Pay },
    { path: '/myorder', component: MyOrder }
  ]
})

export default router

10、路由设计配置 - 二级路由

10.1 目标:阅读vant组件库文档,实现底部导航 tabbar

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第13张图片
tabbar标签页:
① vant-ui.js 按需引入

import { Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)

② layout.vue 粘贴官方代码测试

<van-tabbar>
<van-tabbar-item icon="home-o">标签van-tabbar-item>
<van-tabbar-item icon="search">标签van-tabbar-item>
<van-tabbar-item icon="friends-o">标签van-tabbar-item>
<van-tabbar-item icon="setting-o">标签van-tabbar-item>
van-tabbar>

③ 修改文字、图标、颜色

<van-tabbar active-color="#ee0a24" inactive-color="#000">
<van-tabbar-item icon="wap-home-o">首页...>
<van-tabbar-item icon="apps-o">分类页...>
<van-tabbar-item icon="shopping-cart-o">购物车...>
<van-tabbar-item icon="user-o">我的...>
van-tabbar>

10.2 目标:基于底部导航,完成二级路由配置

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第14张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第15张图片
router - index,js配规则组件

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Layout from '@/views/layout'
import Search from '@/views/search'
import SearchList from '@/views/search/list'
import ProDetail from '@/views/prodetail'
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,
      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 },
    // 动态路由传参,确定将来哪个是商品,路由参数中携带id
    { path: '/prodetail/:id', component: ProDetail },
    { path: '/pay', component: Pay },
    { path: '/myorder', component: MyOrder }
  ]
})

export default router

11、登录页静态布局

目标:基于笔记,快速实现登录页静态布局

  1. 准备工作
    (1) 新建 styles/common.less 重置默认样式
    (2) main.js 导入 common.less
    (3) 图片素材拷贝到 assets 目录【备用】
  2. 登录页静态布局编写
    (1) 头部组件说明 (NavBar)
    (2) 通用样式覆盖
    (3) 其他静态结构编写
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第16张图片

(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. main.js 中导入应用
import '@/styles/common.less'
  1. 将准备好的一些图片素材拷贝到 assets 目录【备用】

(2) 登录静态布局

使用组件

  • van-nav-bar

vant-ui.js 注册

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

Login.vue 使用






添加通用样式

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

// 设置导航条 返回箭头 颜色
.van-nav-bar {
  .van-icon-arrow-left {
    color: #333;
  }
}

12、request模块 - axios 封装

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

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

所以项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个 request 模块中, 便于维护使用
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第17张图片
接口文档地址:
https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080
基地址:
http://cba.itlike.com/public/index.php?s=/api/

  1. 安装 axios
npm i axios
  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. 获取图形验证码,请求测试
import request from '@/utils/request'
export default {
  name: 'LoginPage',
  async created () {
    const res = await request.get('/captcha/image')
    console.log(res)
  }
}

13、图形验证码功能完成

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第18张图片
遇到的问题:res.data.xxx undefined
(1)关于axios返回结果
前端想引用返回数值里的某一项结果,但是却一直显示引用的结果是undefined
(2)问题分析
在浏览器进行断点调试后知道,之所以访问不到数据,是因为axios返回的时候对返回结果多封装了一层data。而我返回的数据里面也有一个data对象,导致我在点data的时候访问的是其外层封装的data,而不是里面具体的我想要的那层data数据。
(3)处理方法
我们只需要访问data里面的data数据就行啦!
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第19张图片

  1. 准备数据,获取图形验证码后存储图片路径,存储图片唯一标识
<script>
import request from '@/utils/request'
export default {
  name: 'LoginPage',
  data () {
    return {
      picCode: '', // 用户输入的图形验证码
      picKey: '', // 将来请求传递的图形验证码唯一标识
      picUrl: '' // 存储请求渲染的图片地址
    }
  },
  async created () {
    this.getPicCode()
  },
  methods: {
    // 获取图形验证码
    async getPicCode () {
      // const res = await request.get('/captcha/image')
      // console.log(res.data)
      // console.log(res.data.data.base64)
      const { data: { data: { base64, key } } } = await request.get('/captcha/image')
      // console.log(base64, key)
      this.picUrl = base64 // 存储地址
      this.picKey = key // 存储唯一标识
    }
  }
}
script>
  1. 动态渲染图形验证码,并且点击时要重新刷新验证码
<div class="form-item">
          <input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
          <img v-if="picUrl" :src="picUrl" @click="getPicCode" alt="">
        div>

14、api 接口模块 -封装图片验证码接口

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第20张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第21张图片
新建 api/login.js 提供获取图形验证码 Api 函数

import request from '@/utils/request'

// 获取图形验证码
export const getPicCode = () => {
  return request.get('/captcha/image')
}

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

async getPicCode () {
  const { data: { base64, key } } = await getPicCode()
  this.picUrl = base64
  this.picKey = key
},

15、Toast 轻提示

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第22张图片
两种使用方式

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

main.js 注册绑定到原型

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

16、短信验证倒计时

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第23张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第24张图片

(1) 倒计时基础效果

  1. 准备 data 数据
data () {
  return {
    totalSecond: 60, // 总秒数
    second: 60, // 倒计时的秒数
    timer: null // 定时器 id
  }
},
  1. 给按钮注册点击事件

  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. 离开页面销毁定时器
destroyed () {
  clearInterval(this.timer)
}

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

  1. 输入框 v-model 绑定变量
data () {
  return {
    mobile: '', // 手机号
    picCode: '' // 图形验证码
  }
},
    


  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. 请求倒计时前进行校验
// 获取短信验证码
async getCode () {
  if (!this.validFn()) {
    return
  }
  ...
}

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

  1. 封装接口 api/login.js
// 获取短信验证码
export const getMsgCode = (captchaCode, captchaKey, mobile) => {
  return request.post('/captcha/sendSmsCaptcha', {
    form: {
      captchaCode,
      captchaKey,
      mobile
    }
  })
}
  1. 调用接口,添加提示
// 获取短信验证码
async getCode () {
  if (!this.validFn()) {
    return
  }

  if (!this.timer && this.second === this.totalSecond) {
    // 发送请求,获取验证码
    await getMsgCode(this.picCode, this.picKey, this.mobile)
    this.$toast('发送成功,请注意查收')
    
    // 开启倒计时
    ...
  }
}

17、登录功能

目标:封装api登录接口,实现登录功能
步骤分析:

  1. 阅读接口文档,封装登录接口
  2. 登录前的校验 (手机号,图形验证码,短信验证码)
  3. 调用方法,发送请求,成功添加提示并跳转
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第25张图片
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第26张图片
    api/login.js 提供登录 Api 函数
// 验证码登录
export const codeLogin = (mobile, smsCode) => {
  return request.post('/passport/login', {
    form: {
      isParty: false,
      mobile,
      partyData: {},
      smsCode
    }
  })
}

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('登录成功')
  }
}

18、响应拦截器统一处理错误提示

目标:通过响应拦截器,统一处理接口的错误提示

问题:每次请求,都会有可能会错误,就都需要错误提示

说明:响应拦截器是咱们拿到数据的 第一个 数据流转站,可以在里面统一处理错误

只要不是 200, 就给默认提示,抛出错误
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第27张图片

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第28张图片
响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 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)
})

此时res = res.data
所以之前res.data.xxx undefined就不会undefined了,我们需要少包含一层data

19、登录权证信息存储

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第29张图片

  1. 新建 vuex user 模块 store/modules/user.js
export default {
  namespaced: true,
  state () {
    return {
      userInfo: {
        token: '',
        userId: ''
      },
    }
  },
  mutations: {},
  actions: {}
}
  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. 提供 mutations
mutations: {
  setUserInfo (state, obj) {
    state.userInfo = obj
  },
},
  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('登录成功')
}

20、storage存储模块 - vuex 持久化处理

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第30张图片

  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. 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: {}
}

21、添加请求 loading 效果

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第31张图片

  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. 响应时,关闭 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)
})

22、页面访问拦截

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第32张图片

  1. 所有的路由一旦被匹配到,都会先经过全局前置守卫
  2. 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
    访问权限页面时,拦截或放行的关键点? → 用户是否有登录权证 token
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第33张图片
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第34张图片
    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: {
    token (state) {
      return state.user.userInfo.token
    }
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user
  }
})
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')
  }
})

23、首页 - 静态结构准备 & 动态渲染

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第35张图片

(1)静态结构准备

  1. 静态结构和样式 layout/home.vue





  1. 新建components/GoodsItem.vue






  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. 封装准备接口 api/home.js
import request from '@/utils/request'

// 获取首页数据
export const getHomeData = () => {
  return request.get('/page/detail', {
    params: {
      pageId: 0
    }
  })
}
  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. 轮播图、导航、猜你喜欢渲染


  
    
  




  

    

—— 猜你喜欢 ——

  1. 商品组件内,动态渲染



24、搜索 - 历史记录管理

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第36张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第37张图片

(1)搜索 - 历史记录 - 基本管理

  1. data 中提供数据,和搜索框双向绑定 (实时获取用户内容)
data () {
  return {
    search: ''
  }
}


  

  1. 准备假数据,进行基本的历史纪录渲染
data () {
  return {
    ...
    history: ['手机', '空调', '白酒', '电视']
  }
},
    
...
{{ item }}
  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. 清空历史


clear () {
  this.history = []
}

(2)搜索 - 历史记录 - 持久化

  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. 页面中调用 - 实现持久化
data () {
  return {
    search: '',
    history: getHistoryList()
  }
},
methods: {
  goSearch (key) {
    ...
    setHistoryList(this.history)
    this.$router.push(`/searchlist?search=${key}`)
  },
  clear () {
    this.history = []
    setHistoryList([])
    this.$toast.success('清空历史成功')
  }
}

25、搜索列表 - 静态布局 & 动态渲染

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第38张图片

(1)搜索列表 - 静态布局






(2) 搜索列表 - 动态渲染

<1> 搜索关键字搜索
  1. 计算属性,基于query 解析路由参数
computed: {
  querySearch () {
    return this.$route.query.search
  }
}
  1. 根据不同的情况,设置输入框的值

  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. 页面中基于 goodsName 发送请求,动态渲染
data () {
  return {
    page: 1,
    proList: []
  }
},
async created () {
  const { data: { list } } = await getProList({
    goodsName: this.querySearch,
    page: this.page
  })
  this.proList = list.data
}

<2> 分类id搜索

1 封装接口 api/category.js

import request from '@/utils/request'

// 获取分类数据
export const getCategoryData = () => {
  return request.get('/category/list')
}

2 分类页静态结构






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

async created () {
  const { data: { list } } = await getProList({
    categoryId: this.$route.query.categoryId,
    goodsName: this.querySearch,
    page: this.page
  })
  this.proList = list.data
}

26、商品详情- 静态布局 & 渲染

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第39张图片

(1)静态结构 和 样式






Lazyload``RateVue 指令,使用前需要对指令进行注册。

import { Lazyload,Rate } from 'vant'

Vue.use(Rate)
Vue.use(Lazyload)

(2) 商品详情 - 动态渲染介绍

  1. 动态路由参数,获取商品 id
computed: {
  goodsId () {
    return this.$route.params.id
  }
},
  1. 封装 api 接口 api/product.js
// 获取商品详情数据
export const getProDetail = (goodsId) => {
  return request.get('/goods/detail', {
    params: {
      goodsId
    }
  })
}
  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. 动态渲染
¥{{ detail.goods_price_min }} ¥{{ detail.goods_price_max }}
已售{{ detail.goods_sales }}件
{{ detail.goods_name }}
七天无理由退货 48小时发货
商品描述

(3) 商品详情 - 动态渲染评价

  1. 封装接口 api/product.js
// 获取商品评价
export const getProComments = (goodsId, limit) => {
  return request.get('/comment/listRows', {
    params: {
      goodsId,
      limit
    }
  })
}
  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. 动态渲染评价

商品评价 ({{ total }}条)
查看更多
{{ item.user.nick_name }}
{{ item.content }}
{{ item.create_time }}

27、加入购物车 - 唤起弹层

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第40张图片

  1. 按需导入 van-action-sheet
import { ActionSheet } from 'vant'
Vue.use(ActionSheet)
  1. 准备 van-action-sheet 基本结构

	111

    
data () {
  return {
    ...
    mode: 'cart'
    showPannel: false
  }
},
  1. 注册点击事件,点击时唤起弹窗
加入购物车
立刻购买
addFn () { this.mode = 'cart' this.showPannel = true }, buyFn () { this.mode = 'buyNow' this.showPannel = true }
  1. 完善结构

  
¥ 9.99
库存 55
数量 数字框占位
加入购物车
立刻购买
该商品已抢完
.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. 动态渲染

  
¥ {{ detail.goods_price_min }}
库存 {{ detail.stock_total }}
数量 数字框组件
加入购物车
立刻购买
该商品已抢完

28、加入购物车 - 封装数字框组件

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第41张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第42张图片

  1. 封装组件 components/CountBox.vue


    

    



  1. 使用组件
import CountBox from '@/components/CountBox.vue'

export default {
  name: 'ProDetail',
  components: {
    CountBox
  },
  data () {
    return {
      addCount: 1
      ...
    }
  },
}

数量

29、加入购物车 - 判断 token 添加登录提示

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第43张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第44张图片

  1. 按需注册 dialog 组件
import { Dialog } from 'vant'
Vue.use(Dialog)
  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. 登录后,若有回跳地址,则回跳页面
// 判断有无回跳地址
const url = this.$route.query.backUrl || '/'
this.$router.replace(url)

30、加入购物车 - 封装接口进行请求

目标:封装接口,进行加入购物车的请求

  1. api/cart.js 中封装接口
  2. 页面中调用接口
  3. 遇到问题:接口需要传递 token
  4. 解决问题:请求拦截器统一携带 token
  5. 小图标定制
    8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第45张图片
    1.封装接口 api/cart.js
// 加入购物车
export const addCart = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/add', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}

2.页面中调用请求

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
},

3.请求拦截器中,统一携带 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)
})

4.准备小图标

{{ cartTotal }} 购物车

5.定制样式

.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%;
  }
}

31、购物车模块

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第46张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第47张图片

(1) 购物车 - 静态布局

  1. 基本结构





  1. 按需导入组件
import { Checkbox } from 'vant'
Vue.use(Checkbox)

(2) 购物车 - 构建 vuex 模块 - 获取数据存储

  1. 新建 modules/cart.js 模块
export default {
  namespaced: true,
  state () {
    return {
      cartList: []
    }
  },
  mutations: {
  },
  actions: {
  },
  getters: {
  }
}
  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. 封装 API 接口 api/cart.js
// 获取购物车列表数据
export const getCartList = () => {
  return request.get('/cart/list')
}
  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. 页面中 dispatch 调用
computed: {
  isLogin () {
    return this.$store.getters.token
  }
},
created () {
  if (this.isLogin) {
    this.$store.dispatch('cart/getCartAction')
  }
},

(3) 购物车 - mapState - 渲染购物车列表

  1. 将数据映射到页面
import { mapState } from 'vuex'

computed: {
  ...mapState('cart', ['cartList'])
}
  1. 动态渲染

{{ item.goods.goods_name }}
¥ {{ item.goods.goods_price_min }}

(4) 购物车 - 封装 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. 页面中 mapGetters 映射使用
computed: {
  ...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
},
    

{{ cartTotal || 0 }}件商品 编辑

(5) 购物车 - 全选反选功能

  1. 全选 getters
getters: {
  isAllChecked (state) {
    return state.cartList.every(item => item.isChecked)
  }
}
    
...mapGetters('cart', ['isAllChecked']),

全选
  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. 点击全选,重置状态
全选
toggleAllCheck () { this.$store.commit('cart/toggleAllCheck', !this.isAllChecked) }, mutations: { toggleAllCheck (state, flag) { state.cartList.forEach(item => { item.isChecked = flag }) }, }

(6)购物车 - 数字框修改数量

  1. 封装 api 接口
// 更新购物车商品数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
  return request.post('/cart/update', {
    goodsId,
    goodsNum,
    goodsSkuId
  })
}
  1. 页面中注册点击事件,传递数据


changeCount (value, goodsId, skuId) {
  this.$store.dispatch('cart/changeCountAction', {
    value,
    goodsId,
    skuId
  })
},
  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)
  },
}

(7)购物车 - 编辑切换状态

  1. data 提供数据, 定义是否在编辑删除的状态
data () {
  return {
    isEdit: false
  }
},
  1. 注册点击事件,修改状态

  
  编辑

  1. 底下按钮根据状态变化
去结算({{ selCount }})
删除
  1. 监视编辑状态,动态控制复选框状态
watch: {
  isEdit (value) {
    if (value) {
      this.$store.commit('cart/toggleAllCheck', false)
    } else {
      this.$store.commit('cart/toggleAllCheck', true)
    }
  }
}

(8)购物车 - 删除功能完成

  1. 查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
// 删除购物车
export const delSelect = (cartIds) => {
  return request.post('/cart/clear', {
    cartIds
  })
}
  1. 注册删除点击事件
删除({{ selCount }})
async handleDel () { if (this.selCount === 0) return await this.$store.dispatch('cart/delSelect') this.isEdit = false },
  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')
    }
},

(9)购物车 - 空购物车处理

  1. 外面包个大盒子,添加 v-if 判断
...
...
您的购物车是空的, 快去逛逛吧
去逛逛
  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;
  }
}

32、订单结算台

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第48张图片

(1) 静态布局

准备静态页面






(2) 获取收货地址列表

1 封装获取地址的接口

import request from '@/utils/request'

// 获取地址列表
export const getAddressList = () => {
  return request.get('/address/list')
}

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
  }
}

3 页面中 - 进行渲染

computed: {
  longAddress () {
    const region = this.selectAddress.region
    return region.province + region.city + region.region + this.selectAddress.detail
  }
},

{{ selectAddress.name }} {{ selectAddress.phone }}
{{ longAddress }}

33、订单结算台 - 确认订单信息

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第49张图片

订单结算 - 封装通用接口

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

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

34、订单结算台 - 购物车结算

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第50张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第51张图片

订单结算 - 购物车结算

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(',') } }) } }

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
  }
}

3 基于数据进行渲染


{{ item.goods_name }}

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

共 {{ order.orderTotalNum }} 件商品,合计: ¥{{ order.orderTotalPrice }}
订单总金额: ¥{{ order.orderTotalPrice }}
优惠券: 无优惠券可用
配送费用: 请先选择配送地址 +¥0.00
支付方式
余额支付(可用 ¥ {{ personal.balance }} 元)

35、订单结算台 - 立即购买结算

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第52张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第53张图片

订单结算 - 立即购买结算

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

2 计算属性处理参数

computed: {
  ...
  goodsId () {
    return this.$route.query.goodsId
  },
  goodsSkuId () {
    return this.$route.query.goodsSkuId
  },
  goodsNum () {
    return this.$route.query.goodsNum
  }
}

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
  }
}

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

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

export default {
  // 此处编写的就是 Vue组件实例的 配置项,通过一定语法,可以直接混入到组件内部
  // data methods computed 生命周期函数 ...
  // 注意点:
  //   1.如果此处 和 组件内,提供了同名的 data 和 methods ,则组件内优先级更高
  //   2.如果编写了生命周期函数,则mixins中的生命周期函数 和 页面的生命周期函数,会用数组管理统一执行
  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
    }
  }
}

2 页面中导入,混入方法

import loginConfirm from '@/mixins/loginConfirm'

export default {
  name: 'ProDetail',
  mixins: [loginConfirm],
  ...
}

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

36、提交订单并支付

目标:封装 API 请求方法,提交订单并支付
核心步骤:

  1. 封装通用请求方法
  2. 买家留言绑定
  3. 注册事件,调用方法提交订单并支付

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第54张图片
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
  })
}

2 买家留言绑定

data () {
  return {
    remark: ''
  }
},

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') }

37、订单管理 & 个人中心 (快速实现)

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第55张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第56张图片

(1)个人中心 - 基本渲染

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

import request from '@/utils/request'

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

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






(2)个人中心 - 退出功能

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 })
  }
},

38、打包发布

8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第57张图片
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第58张图片

39、打包优化:路由懒加载

目标:配置路由懒加载,实现打包优化
说明:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
官方链接
8、Vue 核心技术与实战 智慧商城项目 DAY8~10_第59张图片

你可能感兴趣的:(Vue,vue.js,前端,javascript)