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>
目标:认识第三方 Vue组件库 vant-ui
组件库:第三方 封装 好了很多很多的 组件,整合到一起就是一个组件库。
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
目标:了解其他 Vue 组件库
Vue的组件库并不是唯一的,vant-ui 也仅仅只是组件库的一种。
一般会按照不同平台进行分类:
① PC端: 支持vue2:element-ui 支持vue3:(element-plus) 支持vue2\3:ant-design-vue
② 移动端:vant-ui、 Mint UI (饿了么)、 Cube UI (滴滴)
// 按需导入
import Vue from 'vue'
import { Button, Switch } from 'vant'
Vue.use(Button)
Vue.use(Switch)
main.js
import '@/utils/vant-ui'
目标:基于 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
}
}
}
目标:分析项目页面,设计路由,配置一级路由
但凡是单个页面,独立展示的,都是一级路由
`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
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>
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
目标:基于笔记,快速实现登录页静态布局
styles/common.less
重置默认样式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;
}
import '@/styles/common.less'
使用组件
vant-ui.js
注册
import { NavBar } from 'vant'
Vue.use(NavBar)
Login.vue
使用
手机号登录
未注册的手机号登录后将自动注册
登录
添加通用样式
styles/common.less
设置导航条,返回箭头颜色
// 设置导航条 返回箭头 颜色
.van-nav-bar {
.van-icon-arrow-left {
color: #333;
}
}
目标:将 axios 请求方法,封装到 request 模块
使用 axios 来请求后端接口, 一般都会对 axios 进行 一些配置 (比如: 配置基础地址,请求响应拦截器等)
所以项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个 request 模块中, 便于维护使用
接口文档地址:
https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080
基地址:
http://cba.itlike.com/public/index.php?s=/api/
npm i axios
新建 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
import request from '@/utils/request'
export default {
name: 'LoginPage',
async created () {
const res = await request.get('/captcha/image')
console.log(res)
}
}
遇到的问题:res.data.xxx undefined
(1)关于axios返回结果
前端想引用返回数值里的某一项结果,但是却一直显示引用的结果是undefined
(2)问题分析
在浏览器进行断点调试后知道,之所以访问不到数据,是因为axios返回的时候对返回结果多封装了一层data。而我返回的数据里面也有一个data对象,导致我在点data的时候访问的是其外层封装的data,而不是里面具体的我想要的那层data数据。
(3)处理方法
我们只需要访问data里面的data数据就行啦!
<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>
<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>
新建 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
},
import { Toast } from 'vant';
Toast('提示内容');
main.js 注册绑定到原型
import { Toast } from 'vant';
Vue.use(Toast)
this.$toast('提示内容')
data () {
return {
totalSecond: 60, // 总秒数
second: 60, // 倒计时的秒数
timer: null // 定时器 id
}
},
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('发送成功,请注意查收')
}
}
destroyed () {
clearInterval(this.timer)
}
data () {
return {
mobile: '', // 手机号
picCode: '' // 图形验证码
}
},
// 校验输入框内容
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
},
// 获取短信验证码
async getCode () {
if (!this.validFn()) {
return
}
...
}
api/login.js
// 获取短信验证码
export const getMsgCode = (captchaCode, captchaKey, mobile) => {
return request.post('/captcha/sendSmsCaptcha', {
form: {
captchaCode,
captchaKey,
mobile
}
})
}
// 获取短信验证码
async getCode () {
if (!this.validFn()) {
return
}
if (!this.timer && this.second === this.totalSecond) {
// 发送请求,获取验证码
await getMsgCode(this.picCode, this.picKey, this.mobile)
this.$toast('发送成功,请注意查收')
// 开启倒计时
...
}
}
目标:封装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('登录成功')
}
}
目标:通过响应拦截器,统一处理接口的错误提示
问题:每次请求,都会有可能会错误,就都需要错误提示
说明:响应拦截器是咱们拿到数据的 第一个 数据流转站,可以在里面统一处理错误。
响应拦截器是咱们拿到数据的 第一个 “数据流转站”,可以在里面统一处理错误,只要不是 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
export default {
namespaced: true,
state () {
return {
userInfo: {
token: '',
userId: ''
},
}
},
mutations: {},
actions: {}
}
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
}
})
mutations: {
setUserInfo (state, obj) {
state.userInfo = obj
},
},
// 登录按钮(校验 & 提交)
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('登录成功')
}
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)
}
import { getInfo, setInfo } from '@/utils/storage'
export default {
namespaced: true,
state () {
return {
userInfo: getInfo()
}
},
mutations: {
setUserInfo (state, obj) {
state.userInfo = obj
setInfo(obj)
}
},
actions: {}
}
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
Toast.loading({
message: '请求中...',
forbidClick: true,
loadingType: 'spinner',
duration: 0
})
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
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)
})
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')
}
})
layout/home.vue
—— 猜你喜欢 ——
components/GoodsItem.vue
三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫
5G手机 游戏拍照旗舰机s23
已售104件
¥3999.00
¥6699.00
import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'
Vue.use(GridItem)
Vue.use(Search)
Vue.use(Swipe)
Vue.use(SwipeItem)
Vue.use(Grid)
api/home.js
import request from '@/utils/request'
// 获取首页数据
export const getHomeData = () => {
return request.get('/page/detail', {
params: {
pageId: 0
}
})
}
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
}
}
—— 猜你喜欢 ——
{{ item.goods_name }}
已售 {{ item.goods_sales }}件
¥{{ item.goods_price_min }}
¥{{ item.goods_price_max }}
data () {
return {
search: ''
}
}
搜索
data () {
return {
...
history: ['手机', '空调', '白酒', '电视']
}
},
...
{{ item }}
搜索
{{ 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}`)
}
clear () {
this.history = []
}
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))
}
data () {
return {
search: '',
history: getHistoryList()
}
},
methods: {
goSearch (key) {
...
setHistoryList(this.history)
this.$router.push(`/searchlist?search=${key}`)
},
clear () {
this.history = []
setHistoryList([])
this.$toast.success('清空历史成功')
}
}
综合
销量
价格
computed: {
querySearch () {
return this.$route.query.search
}
}
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
}
})
}
data () {
return {
page: 1,
proList: []
}
},
async created () {
const { data: { list } } = await getProList({
goodsName: this.querySearch,
page: this.page
})
this.proList = list.data
}
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
}
{{ current + 1 }} / {{ images.length }}
¥0.01
¥6699.00
已售1001件
三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23
七天无理由退货
48小时发货
商品评价 (5条)
查看更多
神雕大侠
质量很不错 挺喜欢的
2023-03-21 15:01:35
Lazyload``Rate
是 Vue
指令,使用前需要对指令进行注册。
import { Lazyload,Rate } from 'vant'
Vue.use(Rate)
Vue.use(Lazyload)
computed: {
goodsId () {
return this.$route.params.id
}
},
api/product.js
// 获取商品详情数据
export const getProDetail = (goodsId) => {
return request.get('/goods/detail', {
params: {
goodsId
}
})
}
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
}
}
{{ current + 1 }} / {{ images.length }}
¥{{ detail.goods_price_min }}
¥{{ detail.goods_price_max }}
已售{{ detail.goods_sales }}件
{{ detail.goods_name }}
七天无理由退货
48小时发货
商品描述
(3) 商品详情 - 动态渲染评价
- 封装接口
api/product.js
// 获取商品评价
export const getProComments = (goodsId, limit) => {
return request.get('/comment/listRows', {
params: {
goodsId,
limit
}
})
}
- 页面调用获取数据
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
},
- 动态渲染评价
商品评价 ({{ total }}条)
查看更多
{{ item.user.nick_name }}
{{ item.content }}
{{ item.create_time }}
27、加入购物车 - 唤起弹层
- 按需导入 van-action-sheet
import { ActionSheet } from 'vant'
Vue.use(ActionSheet)
- 准备 van-action-sheet 基本结构
111
data () {
return {
...
mode: 'cart'
showPannel: false
}
},
- 注册点击事件,点击时唤起弹窗
加入购物车
立刻购买
addFn () {
this.mode = 'cart'
this.showPannel = true
},
buyFn () {
this.mode = 'buyNow'
this.showPannel = true
}
- 完善结构
¥
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;
}
}
- 动态渲染
¥
{{ detail.goods_price_min }}
库存
{{ detail.stock_total }}
数量
数字框组件
加入购物车
立刻购买
该商品已抢完
28、加入购物车 - 封装数字框组件
- 封装组件
components/CountBox.vue
- 使用组件
import CountBox from '@/components/CountBox.vue'
export default {
name: 'ProDetail',
components: {
CountBox
},
data () {
return {
addCount: 1
...
}
},
}
数量
29、加入购物车 - 判断 token 添加登录提示
- 按需注册 dialog 组件
import { Dialog } from 'vant'
Vue.use(Dialog)
- 按钮注册点击事件
加入购物车
- 添加 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('进行加入购物车操作')
}
- 登录后,若有回跳地址,则回跳页面
// 判断有无回跳地址
const url = this.$route.query.backUrl || '/'
this.$router.replace(url)
30、加入购物车 - 封装接口进行请求
目标:封装接口,进行加入购物车的请求
// 加入购物车
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、购物车模块
(1) 购物车 - 静态布局
- 基本结构
共4件商品
编辑
新Pad 14英寸 12+128 远峰蓝 M6平板电脑 智能安卓娱乐十核游戏学习二合一 低蓝光护眼超清4K全面三星屏5GWIFI全网通 蓝魔快本平板
¥ 1247.04
- 按需导入组件
import { Checkbox } from 'vant'
Vue.use(Checkbox)
(2) 购物车 - 构建 vuex 模块 - 获取数据存储
- 新建
modules/cart.js
模块
export default {
namespaced: true,
state () {
return {
cartList: []
}
},
mutations: {
},
actions: {
},
getters: {
}
}
- 挂载到 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
}
})
- 封装 API 接口
api/cart.js
// 获取购物车列表数据
export const getCartList = () => {
return request.get('/cart/list')
}
- 封装 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)
}
},
- 页面中 dispatch 调用
computed: {
isLogin () {
return this.$store.getters.token
}
},
created () {
if (this.isLogin) {
this.$store.dispatch('cart/getCartAction')
}
},
(3) 购物车 - mapState - 渲染购物车列表
- 将数据映射到页面
import { mapState } from 'vuex'
computed: {
...mapState('cart', ['cartList'])
}
- 动态渲染
{{ item.goods.goods_name }}
¥ {{ item.goods.goods_price_min }}
(4) 购物车 - 封装 getters - 动态计算展示
- 封装 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)
}
}
- 页面中 mapGetters 映射使用
computed: {
...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
},
共{{ cartTotal || 0 }}件商品
编辑
(5) 购物车 - 全选反选功能
- 全选 getters
getters: {
isAllChecked (state) {
return state.cartList.every(item => item.isChecked)
}
}
...mapGetters('cart', ['isAllChecked']),
全选
- 点击小选,修改状态
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
},
}
- 点击全选,重置状态
全选
toggleAllCheck () {
this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
},
mutations: {
toggleAllCheck (state, flag) {
state.cartList.forEach(item => {
item.isChecked = flag
})
},
}
(6)购物车 - 数字框修改数量
- 封装 api 接口
// 更新购物车商品数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
return request.post('/cart/update', {
goodsId,
goodsNum,
goodsSkuId
})
}
- 页面中注册点击事件,传递数据
changeCount(value, item.goods_id, item.goods_sku_id)">
changeCount (value, goodsId, skuId) {
this.$store.dispatch('cart/changeCountAction', {
value,
goodsId,
skuId
})
},
- 提供 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)购物车 - 编辑切换状态
- data 提供数据, 定义是否在编辑删除的状态
data () {
return {
isEdit: false
}
},
- 注册点击事件,修改状态
编辑
- 底下按钮根据状态变化
去结算({{ selCount }})
删除
- 监视编辑状态,动态控制复选框状态
watch: {
isEdit (value) {
if (value) {
this.$store.commit('cart/toggleAllCheck', false)
} else {
this.$store.commit('cart/toggleAllCheck', true)
}
}
}
(8)购物车 - 删除功能完成
- 查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
// 删除购物车
export const delSelect = (cartIds) => {
return request.post('/cart/clear', {
cartIds
})
}
- 注册删除点击事件
删除({{ selCount }})
async handleDel () {
if (this.selCount === 0) return
await this.$store.dispatch('cart/delSelect')
this.isEdit = false
},
- 提供 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)购物车 - 空购物车处理
- 外面包个大盒子,添加 v-if 判断
...
...
您的购物车是空的, 快去逛逛吧
去逛逛
- 相关样式
.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、订单结算台
(1) 静态布局
准备静态页面
小红
13811112222
江苏省 无锡市 南长街 110号 504
请选择配送地址
三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23
x3
¥9.99
共 12 件商品,合计:
¥1219.00
订单总金额:
¥1219.00
优惠券:
无优惠券可用
配送费用:
请先选择配送地址
+¥0.00
支付方式
余额支付(可用 ¥ 999919.00 元)
(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、订单结算台 - 确认订单信息
订单结算 - 封装通用接口
思路分析: 这里的订单结算,有两种情况:
-
购物车结算,需要两个参数
① mode=“cart”
② cartIds=“cartId, cartId”
-
立即购买结算,需要三个参数
① 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、订单结算台 - 购物车结算
订单结算 - 购物车结算
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、订单结算台 - 立即购买结算
订单结算 - 立即购买结算
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 请求方法,提交订单并支付
核心步骤:
- 封装通用请求方法
- 买家留言绑定
- 注册事件,调用方法提交订单并支付
// 提交订单
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、订单管理 & 个人中心 (快速实现)
(1)个人中心 - 基本渲染
1 封装获取个人信息 - API接口
import request from '@/utils/request'
// 获取个人信息
export const getUserInfoDetail = () => {
return request.get('/user/info')
}
2 调用接口,获取数据进行渲染
{{ detail.mobile }}
普通会员
未登录
点击登录账号
{{ detail.pay_money || 0 }}
账户余额
0
积分
0
优惠券
我的钱包
我的服务
收货地址
领券中心
优惠券
我的帮助
我的积分
退换/售后
(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、打包发布
39、打包优化:路由懒加载
目标:配置路由懒加载,实现打包优化
说明:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
官方链接