Vue从入门到实战 Day08~Day10

智慧商城项目

1. 项目演示

目标:查看项目效果,明确功能模块 -> 完整的电商购物流程

2. 项目收获

目标:明确做完本项目,能够收获哪些内容

3. 创建项目

目标:基于VueCli自定义创建项目架子

4. 调整初始化目录

目标:将目录调整成符合企业规范的目录

5. vant 组件库

目标:认识第三方Vue组件库 vant-ui

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

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

上面的链接打不开的试试这个:Vant 2 - Mobile UI Components built on Vue

6. 其他Vue组件库

目标:了解其他Vue组件库

Vue的组件库并不是唯一的,vant-ui也仅仅是组件库的一种。

一般会按照不同平台进行分类:

①PC端:element-ui、(element-plus)、ant-design-vue

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

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

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

目标:阅读文档,掌握 全部导入 的基本使用

官网:Vant 2 - Mobile UI Components built on Vue

全部导入:

①安装vant-ui

npm i vant@latest-v2 -S

如果出现下面的依赖冲突问题,可以尝试更新依赖

②main.js中注册

import Vant from 'vant'
import 'vant/lib/index.css'
// 把vant中所有的组件都导入了
Vue.use(Vant)

③使用测试

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

目标:阅读文档,掌握 按需导入 的基本使用

按需导入:

①安装vant-ui(已安装)

npm i vant@latest-v2 -S

②安装插件

npm i babel-plugin-import -D

③babel.config.js中配置

module.exports = {presets: ['@vue/cli-plugin-babel/preset'],plugins: [['import', {libraryName: 'vant',libraryDirectory: 'es',style: true}, 'vant']]
};

④main.js 按需导入注册

import Vue from 'vue';
import { Button } from 'vant';Vue.use(Button);

⑤测试使用

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

⑥提取到vant-ui.js中,main.js中导入

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

8. 项目中的 vw 适配

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

官方配置

①安装插件

npm i postcss-px-to-viewport@1.1.1 -D

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

// postcss.config.js
module.exports = {plugins: {'postcss-px-to-viewport': {// 标准屏宽度viewportWidth: 375}}
}

9. 路由设计配置

目标:分析项目页面,设计路由,配置一级路由

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

main.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

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

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><van-tabbar-item icon="apps-o">分类页</van-tabbar-item><van-tabbar-item icon="shopping-cart-o">购物车</van-tabbar-item><van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>

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

1. 配置二级路由 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,redirect: '/home',children: [{ path: '/home', component: Home },{ path: '/category', component: Category },{ path: '/cart', component: Cart },{ path: '/user', component: User }]},{ path: '/search', component: Search },{ path: '/searchlist', component: SearchList },// 动态路由传参,确认将来是哪个商品,路由参数中携带id{ path: '/prodetail/:id', component: ProDetail },{ path: '/pay', component: Pay },{ path: '/myorder', component: MyOrder }]
})export default router

2. 配置导航链接、配置二级路由出口 src/views/layout/index.vue

<template><div><!-- 二级路由出口,二级组件展示的位置 --><router-view></router-view><van-tabbar active-color="#ee0a24" inactive-color="#000" route><van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item><van-tabbar-item to="/category" icon="apps-o">分类页</van-tabbar-item><van-tabbar-item to="/cart" icon="shopping-cart-o">购物车</van-tabbar-item><van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item></van-tabbar></div>
</template><script>
export default {name: 'LayoutIndex'
}
</script><style></style>

10. 登录页静态布局

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

1. 准备工作

(1)新建`styles/common.less`充值默认样式

(2)main.js导入common.less

(3)图片素材拷贝到assets目录【备用】

2. 登录页静态布局编写

(1)头部组件说明(NavBar)

utils/vant-ui.js

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

views/login/index.vue

<template><div class="login"><!-- 头部 NavBar--><van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)"/><!-- 主体 --></div>
</template><script>
export default {name: 'LoginIndex'
}
</script><style></style>

(2)通用样式覆盖

src/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;
}// 添加导航的通用样式
.van-nav-bar {.van-nav-bar__arrow {color: #333;}
}

(3)其他静态结构编写

<template><div class="login"><van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" /><div class="container"><div class="title"><h3>手机号登录</h3><p>未注册的手机号登录后将自动注册</p></div><div class="form"><div class="form-item"><input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text"></div><div class="form-item"><input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text"><img v-if="picUrl" :src="picUrl" alt=""></div><div class="form-item"><input v-model="msgCode" class="inp" placeholder="请输入短信验证码" type="text"><button >{{ second === totalSecond ? '获取验证码' : second + '秒后重新发送'}}</button></div></div><div class="login-btn">登录</div></div></div>
</template><script>export default {name: 'LoginPage',data () {return {picKey: '', // 将来请求传递的图形验证码唯一标识picUrl: '', // 存储请求渲染的图片地址totalSecond: 60, // 总秒数second: 60, // 当前秒数,开定时器对 second--timer: null, // 定时器 idmobile: '', // 手机号picCode: '', // 用户输入的图形验证码msgCode: '' // 短信验证码}}
}
</script><style lang="less" scoped>
.container {padding: 49px 29px;.title {margin-bottom: 20px;h3 {font-size: 26px;font-weight: normal;}p {line-height: 40px;font-size: 14px;color: #b8b8b8;}}.form-item {border-bottom: 1px solid #f3f1f2;padding: 8px;margin-bottom: 14px;display: flex;align-items: center;.inp {display: block;border: none;outline: none;height: 32px;font-size: 14px;flex: 1;}img {width: 94px;height: 31px;}button {height: 31px;border: none;font-size: 13px;color: #cea26a;background-color: transparent;padding-right: 9px;}}.login-btn {width: 100%;height: 42px;margin-top: 39px;background: linear-gradient(90deg,#ecb53c,#ff9211);color: #fff;border-radius: 39px;box-shadow: 0 10px 20px 0 rgba(0,0,0,.1);letter-spacing: 2px;display: flex;justify-content: center;align-items: center;}
}
</style>

效果:

11. request模块 - axios封装

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

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

接口文档地址:https://apifox.com/apidoc/shared-12ab6b18-abc2-444c-ad11-0e60f5693f66/doc-2221080(已失效)

基地址:http://smart-shop.itheima.net/index.php?s=/api/

1. 安装axios

npm install axios

2. 新建request模块,创建实例 & 配置,导出实例

axios官方文档:Axios 实例 | Axios中文文档 | Axios中文网

utils/request.js

import axios from 'axios'// 创建axios实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的axios实例
const instance = axios.create({baseURL: 'http://smart-shop.itheima.net/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 范围内的状态码都会触发该函数。// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截器中处理一下)return response.data
}, function (error) {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么return Promise.reject(error)
})// 导出配置好的实例
export default instance

3. 测试使用

src/views/login/index.vue

<script>
import request from '@/utils/request'
export default {name: 'LoginPage',async created () {const res = await request.get('/captcha/image')console.log(res)}
}
</script>

12. 图形验证码功能完成

目标:基于请求回来的base64图片,实现图形验证码功能

说明:

1. 图形验证码,本质就是一个请求回来的图片

2. 用户将来输入图形验证码,用于强制人机交互,可以抵御机器自动化攻击(例如:避免批量请求获取短信)

需求:

1. 动态将请求回来的base64图片,解析渲染除了

2. 点击验证码图片盒子,要刷新验证码

src/views/login/index.vue

<template><div class="login"><van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" /><div class="container"><div class="title"><h3>手机号登录</h3><p>未注册的手机号登录后将自动注册</p></div><div class="form"><div class="form-item"><input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text"></div><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><div class="form-item"><input v-model="msgCode" class="inp" placeholder="请输入短信验证码" type="text"><button >{{ second === totalSecond ? '获取验证码' : second + '秒后重新发送'}}</button></div></div><div class="login-btn">登录</div></div></div>
</template><script>
import request from '@/utils/request'
export default {name: 'LoginPage',data () {return {picKey: '', // 将来请求传递的图形验证码唯一标识(将来验证需要携带)picUrl: '', // 存储请求渲染的图片地址totalSecond: 60, // 总秒数second: 60, // 当前秒数,开定时器对 second--timer: null, // 定时器 idmobile: '', // 手机号picCode: '', // 用户输入的图形验证码msgCode: '' // 短信验证码}},async created () {// const res = await request.get('/captcha/image')// console.log(res)this.getPicCode()},methods: {// 获取图形验证码async getPicCode () {const { data: { base64, key } } = await request.get('/captcha/image')this.picUrl = base64 // 存储地址this.picKey = key // 存储唯一标识}}
}
</script><style lang="less" scoped></style>

13. api接口模块 - 封装图片验证码接口

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

1. 新建请求模块,封装请求函数

src/api/login.js

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

2. 页面中导入调用

src/views/login/index.vue

<script>
import { getPicCode } from '@/api/login'
export default {name: 'LoginPage',data () {return {picKey: '', // 将来请求传递的图形验证码唯一标识(将来验证需要携带)picUrl: '', // 存储请求渲染的图片地址picCode: '' // 用户输入的图形验证码}},async created () {// const res = await request.get('/captcha/image')// console.log(res)this.getPicCode()},methods: {// 获取图形验证码async getPicCode () {const { data: { base64, key } } = await getPicCode()this.picUrl = base64 // 存储地址this.picKey = key // 存储唯一标识}}
}
</script>

14. Toast轻提示

目标:阅读文档,掌握toast轻提示

1. 注册安装:

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

2. 两种使用方式

①导入调用(组件内或非组件内均可

import { Toast } from 'vant'
Toast('提示内容')

②通过this直接调用(必须组件内

本质:将方法,注册挂载到了Vue原型上 Vue.prototype.$toast = xxx

this.$toast('提示内容')

15. 短信验证倒计时

目标:实现短信验证倒计时功能

步骤分析:

1. 点击按钮,实现 倒计时 效果

src/views/login/index.vue

<template><div class="login"><van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" /><div class="container"><div class="title"><h3>手机号登录</h3><p>未注册的手机号登录后将自动注册</p></div><div class="form"><div class="form-item"><input v-model="mobile" class="inp" maxlength="11" placeholder="请输入手机号码" type="text"></div><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><div class="form-item"><input v-model="msgCode" class="inp" placeholder="请输入短信验证码" type="text"><button @click="getCode">{{ second === totalSecond ? '获取验证码' : second + '秒后重新发送' }}</button></div></div><div class="login-btn">登录</div></div></div>
</template><script>
import { getPicCode } from '@/api/login'
// import { Toast } from 'vant'
export default {name: 'LoginPage',data () {return {picKey: '', // 将来请求传递的图形验证码唯一标识(将来验证需要携带)picUrl: '', // 存储请求渲染的图片地址totalSecond: 60, // 总秒数second: 60, // 当前秒数,开定时器对 second--timer: null, // 定时器 idmobile: '', // 手机号picCode: '', // 用户输入的图形验证码msgCode: '' // 短信验证码}},async created () {this.getPicCode()},methods: {// 获取图形验证码async getPicCode () {const { data: { base64, key } } = await getPicCode()this.picUrl = base64 // 存储地址this.picKey = key // 存储唯一标识// Toast('获取图形验证码成功')// this.$toast('666666')// this.$toast.success('成功文案')},// 获取短信验证码,getCode () {// 当目前没有定时器开着,且 totalSecond 和 second一致(秒数归位)才可以倒计时if (!this.timer && this.second === this.totalSecond) {// 开启倒计时this.timer = setInterval(() => {// console.log('正在倒计时……')this.second--if (this.second <= 0) {clearInterval(this.timer) // 清空计时器this.timer = null // 重置定时器idthis.second = this.totalSecond // 归位}}, 1000)}}},destroyed () {// 离开页面清除定时器clearInterval(this.timer)}
}
</script><style lang="less" scoped></style>

2. 倒计时之前的 校验处理(手机号、验证码)

src/views/login/index.vue

  methods: {// 校验手机号 、验证码是否合法// 通过校验,返回true; 不通过校验,返回falsevalidFn () {// 手机号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},// 获取短信验证码,getCode () {if (!this.validFn()) {// 如果没通过校验,直接返回return}// 当目前没有定时器开着,且 totalSecond 和 second一致(秒数归位)才可以倒计时if (!this.timer && this.second === this.totalSecond) {// 开启倒计时this.timer = setInterval(() => {// console.log('正在倒计时……')this.second--if (this.second <= 0) {clearInterval(this.timer) // 清空计时器this.timer = null // 重置定时器idthis.second = this.totalSecond // 归位}}, 1000)}}},

3. 封装 短信验证请求接口,发送请求添加提示

src/api/login.js

// 此处用于存放所有登录相关的接口请求
import request from '@/utils/request'
// 1. 获取图形验证码
export const getPicCode = () => {return request.get('/captcha/image')
}
// 2. 获取短信验证码
export const getMsgCode = (captchaCode, captchaKey, mobile) => {return request.post('/captcha/sendSmsCaptcha', {form: {captchaCode,captchaKey,mobile}})
}

src/views/login/index.vue

    // 获取短信验证码,async getCode () {if (!this.validFn()) {// 如果没通过校验,直接返回return}// 当目前没有定时器开着,且 totalSecond 和 second一致(秒数归位)才可以倒计时if (!this.timer && this.second === this.totalSecond) {// 发送请求await getMsgCode(this.picCode, this.picKey, this.mobile)this.$toast('短信发送成功,请注意查收')// 开启倒计时......}}

16. 登录功能

目标:封装api登录接口,实现登录功能

步骤分析:

1. 阅读接口文档,封装登录接口

src/api/login.js

// 3. 登录接口
export const codeLogin = (mobile, smsCode) => {return request.post('/passport/login', {form: {isParty: false,partyData: {},mobile,smsCode}})
}

2. 登录前的校验(手机号,图形验证码,短信验证码)

3. 调用方法,发送请求,成功添加提示并跳转

src/views/login/index.vue

    // 登录async login () {if (!this.validFn()) {return}if (!/^\d{6}$/.test(this.msgCode)) {this.$toast('请输入正确的手机验证码')return}const res = await codeLogin(this.mobile, this.msgCode)console.log(res)this.$toast('登录成功')this.$router.push('/')}

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

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

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

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

src/utils/request.js

import axios from 'axios'
import { Toast } from 'vant'
// 创建axios实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的axios实例
const instance = axios.create({baseURL: 'http://smart-shop.itheima.net/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 范围内的状态码都会触发该函数。// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截器中处理一下)const res = response.dataconsole.log(res)if (res.status !== 200) {// 给提示Toast(res.message)// 抛出一个错误的promisereturn Promise.reject(res.message)}return res
}, function (error) {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么return Promise.reject(error)
})// 导出配置好的实例
export default instance

如果出现了下面的错误,可以在src/utils/request.js中添加请求头

const instance = axios.create({baseURL: 'http://smart-shop.itheima.net/index.php?s=/api/',timeout: 5000,headers: {platform: 'H5'}
})

18. 登录权证信息存储

目标:vuex构建user模块存储登录权证(token & userId)

补充说明:

1. token存入vuex的好处,易获取,响应式

2. vuex需要分模块 => user模块

1. 构建user模块

src/store/modules/user.js


export default {namespaced: true,state () {return {// 个人权证相关userInfo: {token: '',userId: ''}}},mutations: {},actions: {},getters: {}
}

2. 挂载到vuex

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

3. 提供mutations

src/store/modules/user.js

  mutations: {// 所有mutations的第一个参数都是statesetUserInfo (state, obj) {state.userInfo = obj}},

4. 页面中commit调用

src/views/login/index.vue

    // 登录async login () {if (!this.validFn()) {return}if (!/^\d{6}$/.test(this.msgCode)) {this.$toast('请输入正确的手机验证码')return}const res = await codeLogin(this.mobile, this.msgCode)this.$store.commit('user/setUserInfo', res.data)console.log(res)this.$toast('登录成功')this.$router.push('/')}

19. storage存储模块 - vuex持久化处理

目标:封装storage存储模块,利用本地存储,进行vuex持久化处理

问题1:vuex刷新会丢失,怎么办?

// 将token存入本地
localStorage.setItem('hm_shopping_info', JSON.stringify(xxx))

src/utils/storage.js

// 约定一个通用的键名
const INFO_KEY = 'hm_shopping_info'// 获取个人信息
export const getInfo = () => {const defaultObj = { token: '', userId: '' }const res = localStorage.getItem(INFO_KEY)return res ? JSON.parse(res) : defaultObj
}// 设置个人信息
export const setInfo = (obj) => {localStorage.setItem(INFO_KEY, JSON.stringify(obj))
}// 移除个人信息
export const removeInfo = () => {localStorage.removeItem(INFO_KEY)
}

src/store/modules/user.js

import { getInfo, setInfo } from '@/utils/storage'export default {namespaced: true,state () {return {// 个人权证相关userInfo: getInfo()}},mutations: {// 所有mutations的第一个参数都是statesetUserInfo (state, obj) {state.userInfo = objsetInfo(obj)}},actions: {},getters: {}
}

20. 添加请求loading效果

目标:统一在每次请求后台时,添加loading效果

背景:有时候因为网络原因,一次请求的结果可能需要一段时间后才能回来,此时,需要给用户添加loading提示。

添加loading提示的好处:

1. 节流处理:防止用户在一次请求还没回来之前,多次进行点击,发送无效请求

2. 友好提示:告知用户,目前是在加载中,请耐心等待,用户体验会更好

实现步骤:

1. 请求拦截器中,每次请求,打开loading

2. 响应拦截器中,每次响应,关闭loading

src/utils/request.js

import axios from 'axios'
import { Toast } from 'vant'
// 创建axios实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的axios实例
const instance = axios.create({baseURL: 'http://smart-shop.itheima.net/index.php?s=/api/',timeout: 5000,headers: {platform: 'H5'}
})// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {// 在发送请求之前做些什么// 开启loading,禁止背景点击(节流处理,防止多次无效触发)Toast.loading({message: '加载中...',forbidClick: true,loadingType: 'spinner', // 配置loading图标duration: 0 // 不会自动消失})return config
}, function (error) {// 对请求错误做些什么return Promise.reject(error)
})// 添加响应拦截器
instance.interceptors.response.use(function (response) {// 2xx 范围内的状态码都会触发该函数。// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截器中处理一下)const res = response.dataif (res.status !== 200) {// 给错误提示,Toast默认是单例模式,后面的Toast调用了,会将前一个Toast效果覆盖,同时只能存在一个ToastToast(res.message)// 抛出一个错误的promisereturn Promise.reject(res.message)} else {// 正确情况,直接走业务核心逻辑,清除loading效果Toast.clear()}return res
}, function (error) {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么return Promise.reject(error)
})// 导出配置好的实例
export default instance

21. 页面访问拦截

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

说明:智慧商城项目,大部分页面,游客都可以直接访问,如遇到需要登录才能进行的操作,提示并跳转到登录。但是,对于支付页,订单页等,必须是登录的用户才能访问的,游客不能进入该页面,需要左拦截处理。

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

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

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

访问权限页面时,拦截或放行的关键点?-> 用户是否有登录权证token

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

1. src/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}
})

2. src/router/index.js

import store from '@/store'// 所有路由在真正被访问到之前(解析渲染对应的组件页面前),都会先经过全局前置守卫
// 只有全局前置守卫放行了,才会到达对应的页面// 全局前置导航守卫
// 1. to 往哪里去,到哪去的路由信息对象(路径,参数)
// 2. from 从哪里来,从哪里来的路由信息对象(路径,参数)
// 3. next() 是否放行
//     如果next()调用,就是放行到to要去的路径
//     next(路径) 拦截到某个路径页面// 定义一个数组,专门存放所有需要权限访问的页面
const authUrls = ['/pay', '/myorder']router.beforeEach((to, from, next) => {// 看 to.path是否在authUrls中出现过if (!authUrls.includes(to.path)) {// 非权限页面,直接放行next()} else {// 权限页面,需要判定tokenconst token = store.getters.token// console.log(token)if (token) {next()} else {next('/login')}}
})

22. 首页 - 静态结构准备 & 动态渲染

目标:实现首页静态结构,封装接口,完成首页动态渲染

1. src/utils/vant-ui.js

// 按需导入
import Vue from 'vue'
import { Button, Switch, Rate, Tabbar, TabbarItem, NavBar, Toast, Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'Vue.use(Grid)
Vue.use(GridItem)
Vue.use(Swipe)
Vue.use(SwipeItem)
Vue.use(Search)
Vue.use(Toast)
Vue.use(NavBar)
Vue.use(Tabbar)
Vue.use(TabbarItem)
Vue.use(Button)
Vue.use(Switch)
Vue.use(Rate)

2. src/views/layout/home.vue

<template><div class="home"><!-- 导航条 --><van-nav-bar title="智慧商城" fixed /><!-- 搜索框 --><van-searchreadonlyshape="round"background="#f1f1f2"placeholder="请在此输入搜索关键词"@click="$router.push('/search')"/><!-- 轮播图 --><van-swipe class="my-swipe" :autoplay="3000" indicator-color="white"><van-swipe-item v-for="item in bannerList" :key="item.imgUrl"><img :src="item.imgUrl" alt=""></van-swipe-item></van-swipe><!-- 导航 --><van-grid column-num="5" icon-size="40"><van-grid-itemv-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 proList" :key="item.goods_id" :item="item"></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 {bannerList: [], // 轮播navList: [], // 导航proList: [] // 商品}},async created () {const { data: { pageData } } = await getHomeData()this.bannerList = pageData.items[1].datathis.navList = pageData.items[3].datathis.proList = pageData.items[6].dataconsole.log(this.proList)}
}</script><style lang="less" scoped>
// 主题 padding
.home {padding-top: 100px;padding-bottom: 50px;
}// 导航条样式定制
.van-nav-bar {z-index: 999;background-color: #c21401;::v-deep .van-nav-bar__title {color: #fff;}
}// 搜索框样式定制
.van-search {position: fixed;width: 100%;top: 46px;z-index: 999;
}// 分类导航部分
.my-swipe .van-swipe-item {height: 185px;color: #fff;font-size: 20px;text-align: center;background-color: #39a9ed;
}
.my-swipe .van-swipe-item img {width: 100%;height: 185px;
}// 主会场
.main img {display: block;width: 100%;
}// 猜你喜欢
.guess .guess-title {height: 40px;line-height: 40px;text-align: center;
}// 商品样式
.goods-list {background-color: #f6f6f6;
}
</style>

3. src/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: () => {return {}}}}
}
</script><style lang="less" scoped>
.goods-item {height: 148px;margin-bottom: 6px;padding: 10px;background-color: #fff;display: flex;.left {width: 127px;img {display: block;width: 100%;}}.right {flex: 1;font-size: 14px;line-height: 1.3;padding: 10px;display: flex;flex-direction: column;justify-content: space-evenly;.count {color: #999;font-size: 12px;}.price {color: #999;font-size: 16px;.new {color: #f03c3c;margin-right: 10px;}.old {text-decoration: line-through;font-size: 12px;}}}
}
</style>

4. src/api/home.js

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

效果:

23. 搜索 - 历史记录管理

目标:构建搜索页的静态布局,完成历史记录的管理

需求:

1. 搜索历史基本渲染

2. 点击搜索(添加历史)

   点击搜索按钮或底下历史记录,都能进行搜索

  • ①若之前没有相同搜索关键字,则直接追加到最前面
  • ②若之前已有相同搜索关键字,将该原有关键字移除,再追加

3. 清空历史:添加清空图标,可以清空历史记录

4. 持久化:搜索历史需要持久化,刷新历史不丢失

代码:

src/views/search/index.vue

<template><div class="search"><van-nav-bar title="商品搜索" left-arrow @click-left="$router.go(-1)" /><van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable><template #action><div @click="goSearch(search)">搜索</div></template></van-search><!-- 搜索历史 --><div class="search-history" v-if="history.length > 0"><div class="title"><span>最近搜索</span><van-icon @click="clear" name="delete-o" size="16" /></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 { getHistoryList, setHistoryList } from '@/utils/storage'
export default {name: 'SearchIndex',data () {return {search: '', // 输入框的内容history: getHistoryList() // 历史记录}},methods: {goSearch (key) {// console.log('进行了搜索,搜索历史要更新', key)const index = this.history.indexOf(key)if (index !== -1) {// 存在相同的项,将原有关键字移除// splice(从哪开始, 删除几个, 项1, 项2)this.history.splice(index, 1)}this.history.unshift(key)setHistoryList(this.history)// 跳转到搜索列表页this.$router.push(`/searchlist?search=${key}`)},clear () {this.history = []setHistoryList([])}}
}
</script><style lang="less" scoped>
.search {.searchBtn {background-color: #fa2209;color: #fff;}::v-deep .van-search__action {background-color: #c21401;color: #fff;padding: 0 20px;border-radius: 0 5px 5px 0;margin-right: 10px;}::v-deep .van-icon-arrow-left {color: #333;}.title {height: 40px;line-height: 40px;font-size: 14px;display: flex;justify-content: space-between;align-items: center;padding: 0 15px;}.list {display: flex;justify-content: flex-start;flex-wrap: wrap;padding: 0 10px;gap: 5%;}.list-item {width: 30%;text-align: center;padding: 7px;line-height: 15px;border-radius: 50px;background: #fff;font-size: 13px;border: 1px solid #efefef;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;margin-bottom: 10px;}
}
</style>

src/utils/storage.js

// 约定一个通用的键名
const INFO_KEY = 'hm_shopping_info'
const HISTORY_KEY = 'hm_history_list'// 获取搜索历史
export const getHistoryList = () => {const res = localStorage.getItem(HISTORY_KEY)return res ? JSON.parse(res) : []
}
// 设置搜索历史
export const setHistoryList = (arr) => {localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}

效果:

24. 搜索列表 - 静态布局 & 动态渲染

目标:实现搜索列表页静态结构,封装接口,完成搜索列表页的渲染

(1)搜索关键字搜索

1. src/views/search/list.vue

<template><div class="search"><van-nav-bar fixed title="商品列表" left-arrow @click-left="$router.go(-1)" /><van-searchreadonlyshape="round"background="#ffffff":value="querySearch || '搜索商品'"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">综合</div><div class="sort-item">销量</div><div class="sort-item">价格 </div></div><div class="goods-list"><GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem></div></div>
</template><script>
import GoodsItem from '@/components/GoodsItem.vue'
import { getProList } from '@/api/product'
export default {name: 'SearchIndex',components: {GoodsItem},computed: {// 获取地址栏的搜索关键字querySearch () {return this.$route.query.search}},data () {return {page: 1,proList: []}},async created () {const { data: { list } } = await getProList({categoryId: this.$route.query.categoryId,goodsName: this.querySearch,page: this.page})this.proList = list.data}
}
</script><style lang="less" scoped>
.search {padding-top: 46px;::v-deep .van-icon-arrow-left {color: #333;}.tool {font-size: 24px;height: 40px;line-height: 40px;}.sort-btns {display: flex;height: 36px;line-height: 36px;.sort-item {text-align: center;flex: 1;font-size: 16px;}}
}// 商品样式
.goods-list {background-color: #f6f6f6;
}
</style>

2. src/api/product.js

import request from '@/utils/request'// 获取搜索商品列表的数据
export const getProList = (obj) => {const { categoryId, goodsName, page } = objreturn request.get('/goods/list', {params: {categoryId,goodsName,page}})
}// 获取商品详情数据
export const getProDetail = (goodsId) => {return request.get('/goods/detail', {params: {goodsId}})
}// 获取商品评价
export const getProComments = (goodsId, limit) => {return request.get('/comment/listRows', {params: {goodsId,limit}})
}

效果:

(2)分类id搜索

1. src/api/category.js

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

2. src/views/layout/category.vue

<template><div class="category"><!-- 分类 --><van-nav-bar title="全部分类" fixed /><!-- 搜索框 --><van-searchreadonlyshape="round"background="#f1f1f2"placeholder="请输入搜索关键词"@click="$router.push('/search')"/><!-- 分类列表 --><div class="list-box"><div class="left"><ul><li v-for="(item, index) in list" :key="item.category_id"><a :class="{ active: index === activeIndex }" @click="activeIndex = index" href="javascript:;">{{ item.name }}</a></li></ul></div><div class="right"><div @click="$router.push(`/searchlist?categoryId=${item.category_id}`)" v-for="item in list[activeIndex]?.children" :key="item.category_id" class="cate-goods"><img :src="item.image?.external_url" alt=""><p>{{ item.name }}</p></div></div></div></div>
</template><script>
import { getCategoryData } from '@/api/category'
export default {name: 'CategoryPage',created () {this.getCategoryList()},data () {return {list: [],activeIndex: 0}},methods: {async getCategoryList () {const { data: { list } } = await getCategoryData()this.list = list}}
}
</script><style lang="less" scoped>
// 主题 padding
.category {padding-top: 100px;padding-bottom: 50px;height: 100vh;.list-box {height: 100%;display: flex;.left {width: 85px;height: 100%;background-color: #f3f3f3;overflow: auto;a {display: block;height: 45px;line-height: 45px;text-align: center;color: #444444;font-size: 12px;&.active {color: #fb442f;background-color: #fff;}}}.right {flex: 1;height: 100%;background-color: #ffffff;display: flex;flex-wrap: wrap;justify-content: flex-start;align-content: flex-start;padding: 10px 0;overflow: auto;.cate-goods {width: 33.3%;margin-bottom: 10px;img {width: 70px;height: 70px;display: block;margin: 5px auto;}p {text-align: center;font-size: 12px;}}}}
}// 导航条样式定制
.van-nav-bar {z-index: 999;
}// 搜索框样式定制
.van-search {position: fixed;width: 100%;top: 46px;z-index: 999;
}
</style>

效果:

25. 商品详情 - 静态布局 & 渲染

目标:实现商品详情静态结构,封装接口,完成商品详情页渲染

1. src/views/prodetail/index.vue

见27

2. src/api/product.js

// 获取商品详情数据
export const getProDetail = (goodsId) => {return request.get('/goods/detail', {params: {goodsId}})
}// 获取商品评价
export const getProComments = (goodsId, limit) => {return request.get('/comment/listRows', {params: {goodsId,limit}})
}

3. src/utils/vant-ui.js

import { ActionSheet } from 'vant';Vue.use(ActionSheet);

26. 加入购物车 - 唤起弹层

目标:点击加入购物车,唤起弹层效果

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

目标:封装弹层中的数字框组件

分析:组件名CountBox

1. 静态结构,左中右三部分

2. 数字框的数字,应该是外部传递进来的(父传子)

3. 点击 + - 号,可以修改数字(子传父)

4. 使用v-model实现封装(:value和@input的简写)

5. 数字不能减到小于1

①src/components/CountBox.vue

<template><div class="count-box"><button @click="handleSub" class="minus">-</button><input :value="value" 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)}}
}
</script><style lang="less" scoped>
.count-box {width:110px;display: flex;.add, .minus {width: 30px;height: 30px;outline: none;border: none;background-color: #efefef;}.inp {width: 40px;height: 30px;outline: none;border: none;margin: 0 5px;background-color: #efefef;text-align: center;}
}
</style>

②src/views/prodetail/index.vue

<template><div class="prodetail"><van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" /><van-swipe :autoplay="4000" @change="onChange"><van-swipe-item v-for="(image, index) in images" :key="index"><img :src="image.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">¥{{ detail.goods_price_min }}</span><span class="oldprice">¥{{ detail.goods_price_max }}</span></div><div class="sellcount">已售 {{ detail.goods_sales }} 件</div></div><div class="msg text-ellipsis-2">{{ detail.goods_name }}</div><div class="service"><div class="left-words"><span><van-icon name="passed" />七天无理由退货</span><span><van-icon name="passed" />48小时发货</span></div><div class="right-icon"><van-icon name="arrow" /></div></div></div><!-- 商品评价 --><div class="comment"><div class="comment-title"><div class="left">商品评价 ({{ total }}条)</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="detail.content"></div><!-- 底部 --><div class="footer"><div @click="$router.push('/')" class="icon-home"><van-icon name="wap-home-o" /><span>首页</span></div><div @click="$router.push('/cart')" class="icon-cart"><span v-if="cartTotal > 0" class="num">{{ cartTotal }}</span><van-icon name="shopping-cart-o" /><span>购物车</span></div><div @click="addFn" class="btn-add">加入购物车</div><div @click="buyNow" 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="detail.goods_image" alt=""></div><div class="right"><div class="price"><span>¥</span><span class="nowprice">{{ detail.goods_price_min }}</span></div><div class="count"><span>库存</span><span>{{ detail.stock_total }}</span></div></div></div><div class="num-box"><span>数量</span><!-- v-model 本质上 :value 和 @input 的简写 --><CountBox v-model="addCount"></CountBox></div><!-- 有库存才显示提交按钮 --><div class="showbtn" v-if="detail.stock_total > 0"><div class="btn" v-if="mode === 'cart'">加入购物车</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 { getProComments, getProDetail } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'
import CountBox from '@/components/CountBox.vue'export default {name: 'ProDetail',// mixins: [loginConfirm],components: {CountBox},data () {return {images: [],current: 0,detail: {},total: 0, // 评价总数commentList: [], // 评价列表defaultImg,showPannel: false, // 控制弹层的显示隐藏mode: 'cart', // 标记弹层状态addCount: 1, // 数字框绑定的数据cartTotal: 0 // 购物车角标}},computed: {goodsId () {return this.$route.params.id}},created () {this.getDetail()this.getComments()},methods: {onChange (index) {this.current = index},async getDetail () {const { data: { detail } } = await getProDetail(this.goodsId)this.detail = detailthis.images = detail.goods_imagesconsole.log(this.images)},async getComments () {const { data: { list, total } } = await getProComments(this.goodsId, 3)this.commentList = listthis.total = total},addFn () {this.mode = 'cart'this.showPannel = true},buyNow () {this.mode = 'buyNow'this.showPannel = true},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}})}}
}
</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;}
}.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%;}
}
</style>

效果:

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

目标:给未登录的用户,添加登录提示

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

1.  若存在:继续加入购物车操作

2. 不存在:提示用户未登录,引导到登录页,登录完回跳

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

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

1. api/cart.js中封装接口

2. 页面中调用接口

3. 遇到问题:接口需要传递token

4. 解决问题:请求拦截器中统一携带token

5. 小图标定制

①src/utils/vant-ui.js

import { Dialog } from 'vant';// 全局注册
Vue.use(Dialog);

②src/views/login/index.vue

    // 登录async login () {if (!this.validFn()) {return}if (!/^\d{6}$/.test(this.msgCode)) {this.$toast('请输入正确的手机验证码')return}console.log('发送登录请求')const res = await codeLogin(this.mobile, this.msgCode)this.$store.commit('user/setUserInfo', res.data)this.$toast('登录成功')// 进行判断,看地址栏有无回跳地址// 1. 如果有,说明是其他页面,拦截到登录来的,需要回跳// 2. 如果没有,正常去首页const url = this.$route.query.backUrl || '/'this.$router.replace(url)}

③src/api/cart.js

import request from '@/utils/request'// 加入购物车
// goodsId    => 商品id     iphone8
// goodsSkuId => 商品规格id  红色的iphone8  粉色的iphone8
export const addCart = (goodsId, goodsNum, goodsSkuId) => {return request.post('/cart/add', {goodsId,goodsNum,goodsSkuId})
}

④src/mixins/loginConfirm.js

export default {// 此处编写的就是 Vue组件实例的 配置项,通过一定语法,可以直接混入到组件内部// data methods computed 生命周期函数 ...// 注意点:// 1. 如果此处 和 组件内,提供了同名的 data 或 methods, 则组件内优先级更高// 2. 如果编写了生命周期函数,则mixins中的生命周期函数 和 页面的生命周期函数,//    会用数组管理,统一执行created () {// console.log('嘎嘎')},data () {return {title: '标题'}},methods: {sayHi () {// console.log('你好')},// 根据登录状态,判断是否需要显示登录确认框// 1. 如果未登录 => 显示确认框 返回 true// 2. 如果已登录 => 啥也不干   返回 falseloginConfirm () {// 判断 token 是否存在if (!this.$store.getters.token) {// 弹确认框this.$dialog.confirm({title: '温馨提示',message: '此时需要先登录才能继续操作哦',confirmButtonText: '去登陆',cancelButtonText: '再逛逛'}).then(() => {// 如果希望,跳转到登录 => 登录后能回跳回来,需要在跳转去携带参数(当前的路径地址)this.$router.replace({path: '/login',query: {// this.$route.fullPath(会包含查询参数)backUrl: this.$route.fullPath}})}).catch(() => {})return true}return false}}
}

⑤src/views/prodetail/index.vue

<template><div class="prodetail"><van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" /><van-swipe :autoplay="4000" @change="onChange"><van-swipe-item v-for="(image, index) in images" :key="index"><img :src="image.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">¥{{ detail.goods_price_min }}</span><span class="oldprice">¥{{ detail.goods_price_max }}</span></div><div class="sellcount">已售 {{ detail.goods_sales }} 件</div></div><div class="msg text-ellipsis-2">{{ detail.goods_name }}</div><div class="service"><div class="left-words"><span><van-icon name="passed" />七天无理由退货</span><span><van-icon name="passed" />48小时发货</span></div><div class="right-icon"><van-icon name="arrow" /></div></div></div><!-- 商品评价 --><div class="comment"><div class="comment-title"><div class="left">商品评价 ({{ total }}条)</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="detail.content"></div><!-- 底部 --><div class="footer"><div @click="$router.push('/')" class="icon-home"><van-icon name="wap-home-o" /><span>首页</span></div><div @click="$router.push('/cart')" class="icon-cart"><span v-if="cartTotal > 0" class="num">{{ cartTotal }}</span><van-icon name="shopping-cart-o" /><span>购物车</span></div><div @click="addFn" class="btn-add">加入购物车</div><div @click="buyNow" 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="detail.goods_image" alt=""></div><div class="right"><div class="price"><span>¥</span><span class="nowprice">{{ detail.goods_price_min }}</span></div><div class="count"><span>库存</span><span>{{ detail.stock_total }}</span></div></div></div><div class="num-box"><span>数量</span><!-- v-model 本质上 :value 和 @input 的简写 --><CountBox v-model="addCount"></CountBox></div><!-- 有库存才显示提交按钮 --><div class="showbtn" v-if="detail.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 { getProComments, getProDetail } from '@/api/product'
import defaultImg from '@/assets/default-avatar.png'
import CountBox from '@/components/CountBox.vue'
import { addCart } from '@/api/cart'
import loginConfirm from '@/mixins/loginConfirm'export default {name: 'ProDetail',mixins: [loginConfirm],components: {CountBox},data () {return {images: [],current: 0,detail: {},total: 0, // 评价总数commentList: [], // 评价列表defaultImg,showPannel: false, // 控制弹层的显示隐藏mode: 'cart', // 标记弹层状态addCount: 1, // 数字框绑定的数据cartTotal: 0 // 购物车角标}},computed: {goodsId () {return this.$route.params.id}},created () {this.getDetail()this.getComments()},methods: {onChange (index) {this.current = index},async getDetail () {const { data: { detail } } = await getProDetail(this.goodsId)this.detail = detailthis.images = detail.goods_imagesconsole.log(this.images)},async getComments () {const { data: { list, total } } = await getProComments(this.goodsId, 3)this.commentList = listthis.total = total},addFn () {this.mode = 'cart'this.showPannel = true},buyNow () {this.mode = 'buyNow'this.showPannel = true},async addCart () {// 判断token是否存在// 1. 如果token不存在,弹确认框// 2. 如果token存在,继续请求操作if (this.loginConfirm()) {return}console.log('正常请求')const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)this.cartTotal = data.cartTotalthis.$toast('加入购物车成功')this.showPannel = false},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}})}}
}
</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;}
}// 角标
.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%;}
}
</style>

6. src/utils/request.js

import store from '@/store'// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {// 在发送请求之前做些什么// 开启loading,禁止背景点击(节流处理,防止多次无效触发)Toast.loading({message: '加载中...',forbidClick: true,loadingType: 'spinner', // 配置loading图标duration: 0 // 不会自动消失})// 只有有token,就在请求时携带,便于请求需要授权的接口const token = store.getters.tokenif (token) {config.headers['Access-Token'] = tokenconfig.headers.platform = 'H5'}return config
}, function (error) {// 对请求错误做些什么return Promise.reject(error)
})

30. 购物车模块

说明:购物车 数据联动关系 较多,且通常会封装一些 小组件,所以为了便于维护,一般都会将购物车的数据 基于vuex分模块管理

需求分析:

1. 基本静态结构(快速实现)

2. 构建vuex cart模块,获取数据存储

3. 基于数据动态渲染购物车列表

4. 封装getters实现动态统计

5. 全选反选功能

6. 数字框修改数量功能

7. 编辑切换状态,删除功能

8. 空购物车处理

①src/utils/vant-ui.js

import { Checkbox } from 'vant';Vue.use(Checkbox);

②新建 src/store/modules/cart.js 模块,封装action和mutation

import { changeCount, delSelect, getCartList } from '@/api/cart'
import { Toast } from 'vant'export default {namespaced: true,state () {return {cartList: []}},mutations: {// 提供一个设置cartList的mutationsetCartList (state, newList) {state.cartList = newList},toggleCheck (state, goodsId) {// 让对应的id的项的状态取反const goods = state.cartList.find(item => item.goods_id === goodsId)goods.isChecked = !goods.isChecked},toggleAllCheck (state, flag) {// 让所有的小选框同步设置state.cartList.forEach(item => {item.isChecked = flag})},changeCount (state, { goodsId, goodsNum }) {const goods = state.cartList.find(item => item.goods_id === goodsId)goods.goods_num = goodsNum}},actions: {async getCartAction (context) {const { data } = await getCartList()// 后台返回的数据中,不包含复选框的选中状态,为了实现将来的功能// 需要手动维护数据,给每一项,添加一个isChecked状态,标记当前商品是否选中data.list.forEach(item => {item.isChecked = true})context.commit('setCartList', data.list)},async changeCountAction (context, obj) {const { goodsNum, goodsId, goodsSkuId } = obj// 先本地修改context.commit('changeCount', { goodsId, goodsNum })// 再同步到后台await changeCount(goodsId, goodsNum, goodsSkuId)},// 删除购物车数据async delSelect (context) {const selCartList = context.getters.selCartListconst cartIds = selCartList.map(item => item.id)await delSelect(cartIds)Toast('删除成功')// 重新拉取最新的购物车数据(重新渲染)context.dispatch('getCartAction')}},getters: {// 求所有的商品累加总数cartTotal (state) {return state.cartList.reduce((sum, item) => sum + item.goods_num, 0)},// 选中的商品项selCartList (state) {return state.cartList.filter(item => item.isChecked)},// 选中的总数selCount (state, getters) {return getters.selCartList.reduce((sum, item) => sum + item.goods_num, 0)},// 选中的总价selPrice (state, getters) {return getters.selCartList.reduce((sum, item) => sum + item.goods_num * item.goods.goods_price_min, 0).toFixed(2)},// 判断是否全部选中isAllChecked (state) {return state.cartList.every(item => item.isChecked)}}
}

③挂载到store上面,scr/store/index.js

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

④封装API接口,src/api/cart.js

// 获取购物车列表
export const getCartList = () => {return request.get('/cart/list')
}// 更新购物车商品数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {return request.post('/cart/update', {goodsId,goodsNum,goodsSkuId})
}// 删除购物车商品
export const delSelect = (cartIds) => {return request.post('/cart/clear', {cartIds})
}

⑤src/views/layout/cart.vue

<template><div class="cart"><van-nav-bar title="购物车" fixed /><div v-if="isLogin && cartList.length > 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 @click="toggleCheck(item.goods_id)"  :value="item.isChecked"></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><!-- 既希望保留原本的形参,又需要通过调用函数传参 => 箭头函数包装一层 --><CountBox @input="(value) => changeCount(value, item.goods_id, item.goods_sku_id)" :value="item.goods_num"></CountBox></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">{{ selPrice }}</i></span></div><div v-if="!isEdit" class="goPay" :class="{ disabled: selCount === 0 }"  @click="goPay">结算({{ selCount }})</div><div v-else @click="handleDel" class="delete" :class="{ disabled: selCount === 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 { mapGetters, mapState } from 'vuex'
export default {name: 'CartPage',components: {CountBox},data () {return {isEdit: false}},computed: {...mapState('cart', ['cartList']),...mapGetters('cart', ['cartTotal', 'selCartList', 'selCount', 'selPrice', 'isAllChecked']),isLogin () {return this.$store.getters.token}},created () {// 必须是登录过的用户,才能用户购物车列表if (this.isLogin) {this.$store.dispatch('cart/getCartAction')}},methods: {// 小选控制全选toggleCheck (goodsId) {this.$store.commit('cart/toggleCheck', goodsId)},// 全选控制小选toggleAllCheck () {this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)},changeCount (goodsNum, goodsId, goodsSkuId) {// console.log(goodsNum, goodsId, goodsSkuId)// 调用 vuex 的 action,进行数量的修改this.$store.dispatch('cart/changeCountAction', {goodsNum,goodsId,goodsSkuId})},async handleDel () {if (this.selCount === 0) returnawait this.$store.dispatch('cart/delSelect')this.isEdit = false},goPay () {// 判断有没有选中商品if (this.selCount > 0) {// 有选中的 商品 才进行结算跳转this.$router.push({path: '/pay',query: {mode: 'cart',cartIds: this.selCartList.map(item => item.id).join(',') // 'cartId,cartId,cartId'}})}}},watch: {// 监视编辑状态,动态控制复选框状态isEdit (value) {if (value) {this.$store.commit('cart/toggleAllCheck', false)} else {this.$store.commit('cart/toggleAllCheck', true)}}}
}
</script><style lang="less" scoped>
// 主题 padding
.cart {padding-top: 46px;padding-bottom: 100px;background-color: #f5f5f5;min-height: 100vh;.cart-title {height: 40px;display: flex;justify-content: space-between;align-items: center;padding: 0 10px;font-size: 14px;.all {i {font-style: normal;margin: 0 2px;color: #fa2209;font-size: 16px;}}.edit {.van-icon {font-size: 18px;}}}.cart-item {margin: 0 10px 10px 10px;padding: 10px;display: flex;justify-content: space-between;background-color: #ffffff;border-radius: 5px;.show img {width: 100px;height: 100px;}.info {width: 210px;padding: 10px 5px;font-size: 14px;display: flex;flex-direction: column;justify-content: space-between;.bottom {display: flex;justify-content: space-between;.price {display: flex;align-items: flex-end;color: #fa2209;font-size: 12px;span {font-size: 16px;}}.count-box {display: flex;width: 110px;.add,.minus {width: 30px;height: 30px;outline: none;border: none;}.inp {width: 40px;height: 30px;outline: none;border: none;background-color: #efefef;text-align: center;margin: 0 5px;}}}}}
}.footer-fixed {position: fixed;left: 0;bottom: 50px;height: 50px;width: 100%;border-bottom: 1px solid #ccc;background-color: #fff;display: flex;justify-content: space-between;align-items: center;padding: 0 10px;.all-check {display: flex;align-items: center;.van-checkbox {margin-right: 5px;}}.all-total {display: flex;line-height: 36px;.price {font-size: 14px;margin-right: 10px;.totalPrice {color: #fa2209;font-size: 18px;font-style: normal;}}.goPay, .delete {min-width: 100px;height: 36px;line-height: 36px;text-align: center;background-color: #fa2f21;color: #fff;border-radius: 18px;&.disabled {background-color: #ff9779;}}}}.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;}
}
</style>

效果:

31. 订单结算台

说明1:所有的结算,本质上就是跳转到“订单结算台”,并且,跳转的同时,需要携带上对应的订单相关参数,具体需要哪些参数,基于“订单结算台”的需求来定。

目标:封装通用的订单信息确认接口

说明2:这里的订单信息确认结算,有两种情况

  • 1. 购物车结算
  • 2. 立即购买结算
  • 订单信息确认,可以共用同一个接口(参数不同)

①src/api/address.js

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

②src/api/order.js

import request from '@/utils/request'// 订单信息确认
// mode: cart => obj { cartIds }
// mode: buyNow => obj { goodsId, goodsNum, goodsSkuId }
export const checkOrder = (mode, obj) => {return request.get('/checkout/order', {params: {mode, // cart or buyNowdelivery: 10, // 10 快递, 20 门店自提couponId: 0, // 优惠券id,传0表示不使用优惠券isUsePoints: 0, // 积分,传0,表示不使用积分...obj // 将传递过来的参数动态展开}})
}// 提交订单
// 订单信息确认
// mode: cart => obj { cartIds, remark }
// mode: buyNow => obj { goodsId, goodsNum, goodsSkuId, remark }
export const submitOrder = (mode, obj) => {return request.post('/checkout/submit', {mode,delivery: 10, // 配送方式-快递couponId: 0, // 优惠券isUsePoints: 0, // 积分payType: 10, // 支付方式 - 余额支付...obj})
}

③src/views/pay/index.vue

<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">{{ longAddress }}</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.orderTotalPrice }}</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>无优惠券可用</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 v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea></div></div><!-- 底部提交 --><div class="footer-fixed"><div class="left">实付款:<span>¥{{ order.orderTotalPrice }}</span></div><div class="tipsbtn" @click="submitOrder">提交订单</div></div></div>
</template><script>
import { getAddressList } from '@/api/address'
import { checkOrder, submitOrder } from '@/api/order'
import loginConfirm from '@/mixins/loginConfirm'
export default {name: 'PayIndex',mixins: [loginConfirm],data () {return {addressList: [],order: {},personal: {},remark: '' // 备注留言}},computed: {selectedAddress () {// 这里地址管理非主线业务,直接获取第一个项作为选中的地址return this.addressList[0] || {}},longAddress () {const region = this.selectedAddress.regionreturn 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},goodsSkuId () {return this.$route.query.goodsSkuId},goodsNum () {return this.$route.query.goodsNum}},created () {this.getAddressList()this.getOrderList()},methods: {async submitOrder () {// 购物车结算if (this.mode === 'cart') {await submitOrder(this.mode, {cartIds: this.cartIds,remark: this.remark})}// 立即购买if (this.mode === 'buyNow') {await submitOrder(this.mode, {goodsId: this.goodsId,goodsSkuId: this.goodsSkuId,goodsNum: this.goodsNum,remark: this.remark})}this.$toast.success('支付成功')this.$router.replace('/myorder')},async getAddressList () {const { data: { list } } = await getAddressList()this.addressList = list},async getOrderList () {// 购物车结算if (this.mode === 'cart') {const { data: { order, personal } } = await checkOrder(this.mode, {cartIds: this.cartIds})this.order = orderthis.personal = personal}// 立刻购买结算if (this.mode === 'buyNow') {const { data: { order, personal } } = await checkOrder(this.mode, {goodsId: this.goodsId,goodsSkuId: this.goodsSkuId,goodsNum: this.goodsNum})this.order = orderthis.personal = personal}}}
}
</script><style lang="less" scoped>
.pay {padding-top: 46px;padding-bottom: 46px;::v-deep {.van-nav-bar__arrow {color: #333;}}
}
.address {display: flex;align-items: center;justify-content: flex-start;padding: 20px;font-size: 14px;color: #666;position: relative;background: url(@/assets/border-line.png) bottom repeat-x;background-size: 60px auto;.left-icon {margin-right: 20px;}.right-icon {position: absolute;right: 20px;top: 50%;transform: translateY(-7px);}
}
.goods-item {height: 100px;margin-bottom: 6px;padding: 10px;background-color: #fff;display: flex;.left {width: 100px;img {display: block;width: 80px;margin: 10px auto;}}.right {flex: 1;font-size: 14px;line-height: 1.3;padding: 10px;padding-right: 0px;display: flex;flex-direction: column;justify-content: space-evenly;color: #333;.info {margin-top: 5px;display: flex;justify-content: space-between;.price {color: #fa2209;}}}
}.flow-num-box {display: flex;justify-content: flex-end;padding: 10px 10px;font-size: 14px;border-bottom: 1px solid #efefef;.money {color: #fa2209;}
}.pay-cell {font-size: 14px;padding: 10px 12px;color: #333;display: flex;justify-content: space-between;.red {color: #fa2209;}
}
.pay-detail {border-bottom: 1px solid #efefef;
}.pay-way {font-size: 14px;padding: 10px 12px;border-bottom: 1px solid #efefef;color: #333;.tit {line-height: 30px;}.pay-cell {padding: 10px 0;}.van-icon {font-size: 20px;margin-right: 5px;}
}.buytips {display: block;textarea {display: block;width: 100%;border: none;font-size: 14px;padding: 12px;height: 100px;}
}.footer-fixed {position: fixed;background-color: #fff;left: 0;bottom: 0;width: 100%;height: 46px;line-height: 46px;border-top: 1px solid #efefef;font-size: 14px;display: flex;.left {flex: 1;padding-left: 12px;color: #666;span {color:#fa2209;}}.tipsbtn {width: 121px;background: linear-gradient(90deg,#f9211c,#ff6335);color: #fff;text-align: center;line-height: 46px;display: block;font-size: 14px;}
}
</style>

如果出现收货地址为空的问题,可以清空token,重新登录,手机号填:18917286702(这个号应该是实现了编辑收货地址)

效果:

32. 订单管理 & 个人中心(快速实现)

目标:基于笔记,快速实现订单管理和个人中心跑通流程

(1)订单管理

①src/utils/vant-ui.js

import { Tab, Tabs } from 'vant';Vue.use(Tab);
Vue.use(Tabs);

②src/api/order.js

// 订单列表
export const getMyOrderList = (dataType, page) => {return request.get('/order/list', {params: {dataType,page // List}})
}

③src/views/myorder/index.vue

<template><div class="order"><van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" /><van-tabs v-model="active" sticky><van-tab name="all" title="全部"></van-tab><van-tab name="payment" title="待支付"></van-tab><van-tab name="delivery" title="待发货"></van-tab><van-tab name="received" title="待收货"></van-tab><van-tab name="comment" title="待评价"></van-tab></van-tabs><OrderListItem v-for="item in list" :key="item.order_id" :item="item"></OrderListItem></div>
</template><script>
import OrderListItem from '@/components/OrderListItem.vue'
import { getMyOrderList } from '@/api/order'
export default {name: 'OrderPage',components: {OrderListItem},data () {return {active: this.$route.query.dataType || 'all',page: 1,list: []}},methods: {async getOrderList () {const { data: { list } } = await getMyOrderList(this.active, this.page)list.data.forEach((item) => {item.total_num = 0item.goods.forEach(goods => {item.total_num += goods.total_num})})this.list = list.data}},watch: {active: {immediate: true,handler () {this.getOrderList()}}}
}
</script><style lang="less" scoped>
.order {background-color: #fafafa;
}
.van-tabs {position: sticky;top: 0;
}
</style>

④src/components/OrderListItem.vue

<template><div class="order-list-item" v-if="item.order_id"><div class="tit"><div class="time">{{ item.create_time }}</div><div class="status"><span>{{ item.state_text }}</span></div></div><div class="list" ><div class="list-item" v-for="(goods, index) in item.goods" :key="index"><div class="goods-img"><img :src="goods.goods_image" alt=""></div><div class="goods-content text-ellipsis-2">{{ goods.goods_name }}</div><div class="goods-trade"><p>¥ {{ goods.total_pay_price }}</p><p>x {{ goods.total_num }}</p></div></div></div><div class="total">共 {{ item.total_num }} 件商品,总金额 ¥{{ item.total_price }}</div><div class="actions"><div v-if="item.order_status === 10"><span v-if="item.pay_status === 10">立刻付款</span><span v-else-if="item.delivery_status === 10">申请取消</span><span v-else-if="item.delivery_status === 20 || item.delivery_status === 30">确认收货</span></div><div v-if="item.order_status === 30"><span>评价</span></div></div></div>
</template><script>
export default {props: {item: {type: Object,default: () => {return {}}}}
}
</script><style lang="less" scoped>
.order-list-item {margin: 10px auto;width: 94%;padding: 15px;background-color: #ffffff;box-shadow: 0 0.5px 2px 0 rgba(0,0,0,.05);border-radius: 8px;color: #333;font-size: 13px;.tit {height: 24px;line-height: 24px;display: flex;justify-content: space-between;margin-bottom: 20px;.status {color: #fa2209;}}.list-item {display: flex;.goods-img {width: 90px;height: 90px;margin: 0px 10px 10px 0;img {width: 100%;height: 100%;}}.goods-content {flex: 2;line-height: 18px;max-height: 36px;margin-top: 8px;}.goods-trade {flex: 1;line-height: 18px;text-align: right;color: #b39999;margin-top: 8px;}}.total {text-align: right;}.actions {text-align: right;span {display: inline-block;height: 28px;line-height: 28px;color: #383838;border: 0.5px solid #a8a8a8;font-size: 14px;padding: 0 15px;border-radius: 5px;margin: 10px 0;}}
}
</style>

效果:

(2)个人中心

①src/api/user.js

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

②src/store/modules/user.js

import { getInfo, setInfo } from '@/utils/storage'export default {namespaced: true,state () {return {// 个人权证相关userInfo: getInfo()}},mutations: {// 所有mutations的第一个参数都是statesetUserInfo (state, obj) {state.userInfo = objsetInfo(obj)}},actions: {logout (context) {// 重置个人信息context.commit('setUserInfo', {})// 重置购物车信息(跨模块调用mutation) cart/setCartListcontext.commit('cart/setCartList', [], { root: true })}},getters: {}
}

③src/views/layout/user.vue

<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.pay_money || 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 v-if="isLogin" @click="logout">退出登录</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},logout () {this.$dialog.confirm({title: '温馨提示',message: '你确认要退出么'}).then(() => {// 退出是一个动作 => 包含了两步,分别是将 user 和 cart 进行重置this.detail = {}this.$store.dispatch('user/logout')}).catch(() => {})}}
}
</script><style lang="less" scoped>
.user {min-height: 100vh;background-color: #f7f7f7;padding-bottom: 50px;
}.head-page {height: 130px;background: url("http://cba.itlike.com/public/mweb/static/background/user-header2.png");background-size: cover;display: flex;align-items: center;.head-img {width: 50px;height: 50px;border-radius: 50%;overflow: hidden;margin: 0 10px;img {width: 100%;height: 100%;object-fit: cover;}}
}
.info {.mobile {margin-bottom: 5px;color: #c59a46;font-size: 18px;font-weight: bold;}.vip {display: inline-block;background-color: #3c3c3c;padding: 3px 5px;border-radius: 5px;color: #e0d3b6;font-size: 14px;.van-icon {font-weight: bold;color: #ffb632;}}
}.my-asset {display: flex;padding: 20px 0;font-size: 14px;background-color: #fff;.asset-left {display: flex;justify-content: space-evenly;flex: 3;.asset-left-item {display: flex;flex-direction: column;justify-content: center;align-items: center;span:first-child {margin-bottom: 5px;color: #ff0000;font-size: 16px;}}}.asset-right {flex: 1;.asset-right-item {display: flex;flex-direction: column;justify-content: center;align-items: center;.van-icon {font-size: 24px;margin-bottom: 5px;}}}
}.order-navbar {display: flex;padding: 15px 0;margin: 10px;font-size: 14px;background-color: #fff;border-radius: 5px;.order-navbar-item {display: flex;flex-direction: column;justify-content: center;align-items: center;width: 25%;.van-icon {font-size: 24px;margin-bottom: 5px;}}
}.service {font-size: 14px;background-color: #fff;border-radius: 5px;margin: 10px;.title {height: 50px;line-height: 50px;padding: 0 15px;font-size: 16px;}.content {display: flex;justify-content: flex-start;flex-wrap: wrap;font-size: 14px;background-color: #fff;border-radius: 5px;.content-item {display: flex;flex-direction: column;justify-content: center;align-items: center;width: 25%;margin-bottom: 20px;.van-icon {font-size: 24px;margin-bottom: 5px;color: #ff3800;}}}
}.logout-btn {button {width: 60%;margin: 10px auto;display: block;font-size: 13px;color: #616161;border-radius: 9px;border: 1px solid #dcdcdc;padding: 7px 0;text-align: center;background-color: #fafafa;}
}
</style>

效果:

33. 打包发布

目标:明确打包的作用

说明:vue脚手架只是开发过程中,协助开发的工具,当真正开发完成了,脚手架不参与上线。

打包的作用:

①将多个文件压缩合并成一个文件;

②语法降级

③less sass ts语法解析

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

目标:打包的命令和配置

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

①配置:默认情况下,需要放到服务器根目录打开,如果希望双击运行,需要配置publicPath配成相对路径

vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({publicPath: './',  // 相对路径transpileDependencies: true
})

②打包构建

命令: npm run build

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

③结果:将打包后的dist文件夹里的内容上传到服务器当中,双击index.html在浏览器中即可查看项目

34. 打包优化:路由懒加载

目标:配置路由懒加载,实现打包优化

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

步骤1:异步组件改造 => src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'import Layout from '@/views/layout'
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 store from '@/store'const Login = () => import('@/views/login')
const Search = () => import('@/views/search')
const SearchList = () => import('@/views/search/list')
const ProDetail = () => import('@/views/prodetail')
const Pay = () => import('@/views/pay')
const MyOrder = () => import('@/views/myorder')Vue.use(VueRouter)

步骤2:路由中应用

const router = new VueRouter({routes: [...{ path: '/prodetail/:id', component: ProDetail },{ path: '/pay', component: Pay },...]
})

步骤3:构建

npm run build

结果:

至此,Vue2学习告一段落,接下来进入Vue3的学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/13580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全之BGP详解

BGP&#xff1b;边界网关协议 使用范围&#xff1b;BGP范围&#xff0c;在AS之间使用的协议。 协议的特点&#xff08;算法&#xff09;&#xff1a;路径矢量型&#xff0c;没有算法。 协议是否传递网络掩码&#xff1a;传递网络掩码&#xff0c;支持VLSM&#xff0c;CIDR …

ASP+ACCESS基于B2C电子商务网站设计

摘 要 运用ASP技术结合了Access数据库原理&#xff0c;基于B/S模式我们开发了一个网上购物系统。在我们的系统中&#xff0c;顾客可以很方便的注册成为会员&#xff0c;对商品进行浏览检索&#xff0c;查看商品的详细资料&#xff0c;然后根据各人的喜好购买心仪的商品。系统…

CCF20220901——如此编码

CCF20220901——如此编码 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; int main() {int n,m,cnt1,a[1000],c[1000]{1};cin>>n>>m;for(int i1;i<n;i){cin>>a[i];cnt*a[i];c[i]cnt;}int b[1000]{0};for(int i1;i<n;i)b[i](…

JPHS-JMIR Public Health and Surveillance

文章目录 一、期刊简介二、征稿信息三、期刊表现四、投稿须知五、投稿咨询 一、期刊简介 JMIR Public Health and Surveillance是一本多学科期刊&#xff0c;专注于公共卫生创新与技术的交叉领域&#xff0c;包括公共卫生信息学、监测&#xff08;监测系统和快速报告&#xff…

CCF20220601——归一化处理

CCF20220601——归一化处理 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; int main() {int n,a[1000],sum0;scanf("%d",&n);for(int i1;i<n;i){scanf("%d",&a[i]);suma[i];}double aver1.0,b0.0,d1.0;aversum/(n*1…

Java基础(三)- 多线程、网络通信、单元测试、反射、注解、动态代理

多线程基础 线程&#xff1a;一个程序内部的一条执行流程&#xff0c;只有一条执行流程就是单线程 java.lang.Thread代表线程 主线程退出&#xff0c;子线程存在&#xff0c;进程不会退出 可以使用jconsole查看 创建线程 有多个方法可以创建线程 继承Thread类 优点&#x…

【学习】实验室服务器常用的Linux指令。

1. 下载GitHub代码。 使用代码&#xff1a; git clone https://github.com/Turoad/CLRNet.git2. 压缩 / 解压。 打包压缩 是日常工作中备份文件的一种方式 在不同操作系统中&#xff0c;常用的打包压缩方式是不同的选项 含义 Windows 常用 rarMac 常用 zipLinux 常用 tar.gz…

学硕都考11408的211院校!河北工业大学计算机考研考情分析!

河北工业大学&#xff08;Hebei University of Technology&#xff09;&#xff0c;简称河北工大&#xff0c;坐落于天津市&#xff0c;由河北省人民政府、天津市人民政府与中华人民共和国教育部共建&#xff0c; 隶属于河北省&#xff0c;是国家“双一流”建设高校、国家“211…

自动化测试在软件开发生命周期中如何提高代码质量?

自动化测试是一种在软件开发生命周期中使用软件工具来执行测试的方法&#xff0c;它可以大大提高代码质量&#xff0c;减少开发过程中的错误和缺陷。本文将从零开始&#xff0c;详细且规范地介绍如何使用自动化测试来提高代码质量。 第一步&#xff1a;明确测试目标 在开始自…

webgl入门-绘制三角形

绘制三角形 前言 三角形是一个最简单、最稳定的面&#xff0c;webgl 中的三维模型都是由三角面组成的。咱们这一篇就说一下三角形的绘制方法。 课堂目标 理解多点绘图原理。可以绘制三角形&#xff0c;并将其组合成多边形。 知识点 缓冲区对象点、线、面图形 第一章 web…

iPhone实况照片从Windows资源管理器复制的JPG+MOV无法正常还原到iPhone

背景&#xff1a; 之前使用的iPhone 15 Pro&#xff0c;使用的Windows资源管理器当中复制导出的实况照片&#xff0c;复制出来的格式例如IMG_0001.JPG, IMG_0001.MOV。之后手机就卖掉了。现在使用的iPhone 14 Pro Max&#xff0c;想要导回之前备份的实况照片。尝试使用爱思助手…

supOS NEO科技普惠!永久免费!亿元补贴

数字化转型正在全球蓬勃发展&#xff0c;工业操作系统进入大规模推广期&#xff01; 如果您正在被预算不足、技术团队不强、数字化投入产出比等问题困扰&#xff0c;supOS NEO是您最好的选择。 “让supOS走进万千工厂、千行百业&#xff01;让全世界每个工厂都能用得上supOS&am…

MM模块学习三 (创建采购申请)

采购信息记录比较特殊既是主数据又是货源 注&#xff1a;发票校验是指把供应商提供的发票做到系统里面产生一张应付凭证。 1.决定采购需求 采购需求可以手工创建&#xff08;ME51N&#xff09;&#xff0c;也可以自动产生&#xff08;比如&#xff1a;MRP&#xff0c;以及比如…

CTFshow之文件上传web入门151关-161关解密。包教包会!!!!

这段时间一直在搞文件上传相关的知识&#xff0c;正好把ctf的题目做做写写给自字做个总结&#xff01; 不过有一个确定就是所有的测试全部是黑盒测试&#xff0c;无法从代码层面和大家解释&#xff0c;我找个时间把upload-labs靶场做一做给大家讲讲白盒的代码审计 一、实验准…

2024-5-23 石群电路-14

2024-5-23&#xff0c;星期四&#xff0c;22:20&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天没有什么重要的事情发生&#xff0c;心情一如既往的平静&#xff0c;距离返校假期还有两天~~~。 今天观看了石群老师电路基础课程的第23/24个视频&#xff0…

真实案例分享,终端pc直接telnet不到出口路由器。

1、背景信息 我终端pc的网卡地址获取的网关是在核心交换机上&#xff0c;在核心交换机上telnet出口路由器可以实现。 所有终端网段都不能telnet出口路由器&#xff0c;客户希望能用最小的影响方式进行解决。 2、现有配置信息 终端的无线和有线分别在两个网段中&#xff0c;…

【课后练习分享】Java用户注册界面设计和求三角形面积的图形界面程序

目录 java编程题&#xff08;每日一练&#xff09;&#xff1a; 问题一的答案代码如下&#xff1a; 问题一的运行截图如下&#xff1a; 问题二的答案代码如下&#xff1a; 问题二的运行截图如下&#xff1a; java编程题&#xff08;每日一练&#xff09;&#xff1a; 1.…

大数据量MySQL的分页查询优化

目录 造数据查看耗时优化方案总结 造数据 我用MySQL存储过程生成了100多万条数据&#xff0c;存储过程如下。 DELIMITER $$ USE test$$ DROP PROCEDURE IF EXISTS proc_user$$CREATE PROCEDURE proc_user() BEGINDECLARE i INT DEFAULT 1;WHILE i < 1000000 DOINSERT INT…

提权方式及原理汇总

一、Linux提权 1、SUID提权 SUID&#xff08;设置用户ID&#xff09;是赋予文件的一种权限&#xff0c;它会出现在文件拥有者权限的执行位上&#xff0c;具有这种权限的文件会在其执行时&#xff0c;使调用者暂时获得该文件拥有者的权限。 为可执行文件添加suid权限的目的是简…

二叉树求解大小操作详解

目录 一、求所有结点个数 1.1 递归思路 1.2 递归分支图 1.3 递归栈帧图 1.4 C语言实现 二、求叶子结点个数 2.1 递归思路 2.2 递归分支图 2.3 递归栈帧图 2.4 C语言实现 三、求第K层的结点个数 3.1 递归思路 3.2 递归分支图 3.3 递归栈帧图 3.4 C语言实现 四、求…