Vue 核心技术与实战智慧商城项目Day08-10

温馨提示:这个黑马的视频在b占可以找到,里面有完整的教学过程

然后这个项目有完整的代码,我已经上传了,如果审核成功大家就可以看了,但是需要审核多久我也不是很确定

1.项目演示

2. 项目收获

3. 创建项目

4. 调整初始化目录

5. vant 组件库

6. 其他 Vue 组件库

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

全部导入:

按需导入:

8. 项目中的 vw 适配

记得执行yarn serve

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

9. 路由设计配置

一:

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

二:

引入:

引用:

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'import store from '@/store'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 }]
})// 所有的路由在真正被访问到之前(解析渲染对应组件页面前),都会先经过全局前置守卫
// 只有全局前置守卫放行了,才会到达对应的页面// 全局前置导航守卫
// to:   到哪里去,到哪去的完整路由信息对象 (路径,参数)
// from: 从哪里来,从哪来的完整路由信息对象 (路径,参数)
// next(): 是否放行
// (1) next()     直接放行,放行到to要去的路径
// (2) next(路径)  进行拦截,拦截到next里面配置的路径// 定义一个数组,专门用户存放所有需要权限访问的页面
const authUrls = ['/pay', '/myorder']router.beforeEach((to, from, next) => {// console.log(to, from, next)// 看 to.path 是否在 authUrls 中出现过if (!authUrls.includes(to.path)) {// 非权限页面,直接放行next()return}// 是权限页面,需要判断tokenconst token = store.getters.tokenif (token) {next()} else {next('/login')}
})export default router
<template><div><!-- 二级路由出口:二级组件展示的位置 --><router-view></router-view><van-tabbar route active-color="#ee0a24" inactive-color="#000"><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. 登录页静态布局

10.1准备工作

// 重置默认样式
* {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 Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/utils/vant-ui'
import '@/styles/common.less'// import { Toast } from 'vant'
// Toast('嘿嘿,你好哇')
// 全部导入
// import Vant from 'vant'
// import 'vant/lib/index.css'
// // 插件安装初始化:内部会将所有的vant所有组件进行导入注册
// Vue.use(Vant)Vue.config.productionTip = falsenew Vue({router,store,render: h => h(App)
}).$mount('#app')

10.2静态布局编写

10.21头部组件

10.22通用样式的覆盖

把箭头变成灰黑色

// 重置默认样式
* {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;}
}

10.23其他静态结构的编写

<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 @click="login" class="login-btn">登录</div></div></div>
</template><script>
import { codeLogin, getMsgCode, 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('获取成功')// this.$toast.success('成功文案')},// 校验 手机号 和 图形验证码 是否合法// 通过校验,返回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},// 获取短信验证码async getCode () {if (!this.validFn()) {// 如果没通过校验,没必要往下走了return}// 当前目前没有定时器开着,且 totalSecond 和 second 一致 (秒数归位) 才可以倒计时if (!this.timer && this.second === this.totalSecond) {// 发送请求// 预期:希望如果响应的status非200,最好抛出一个promise错误,await只会等待成功的promiseawait getMsgCode(this.picCode, this.picKey, this.mobile)this.$toast('短信发送成功,注意查收')// 开启倒计时this.timer = setInterval(() => {this.second--if (this.second <= 0) {clearInterval(this.timer)this.timer = null // 重置定时器 idthis.second = this.totalSecond // 归位}}, 1000)}},// 登录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)}},// 离开页面清除定时器destroyed () {clearInterval(this.timer)}
}
</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 封装

wiki - 智慧商城-实战项目

import store from '@/store'
import axios from 'axios'
import { Toast } from 'vant'// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',timeout: 5000
})// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {// 在发送请求之前做些什么// 开启loading,禁止背景点击 (节流处理,防止多次无效触发)Toast.loading({message: '加载中...',forbidClick: true, // 禁止背景点击loadingType: 'spinner', // 配置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)
})// 添加响应拦截器
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

12. 图形验证码功能完成

<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 @click="login" class="login-btn">登录</div></div></div>
</template><script>
import { codeLogin, getMsgCode, 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('获取成功')// this.$toast.success('成功文案')},// 校验 手机号 和 图形验证码 是否合法// 通过校验,返回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},// 获取短信验证码async getCode () {if (!this.validFn()) {// 如果没通过校验,没必要往下走了return}// 当前目前没有定时器开着,且 totalSecond 和 second 一致 (秒数归位) 才可以倒计时if (!this.timer && this.second === this.totalSecond) {// 发送请求// 预期:希望如果响应的status非200,最好抛出一个promise错误,await只会等待成功的promiseawait getMsgCode(this.picCode, this.picKey, this.mobile)this.$toast('短信发送成功,注意查收')// 开启倒计时this.timer = setInterval(() => {this.second--if (this.second <= 0) {clearInterval(this.timer)this.timer = null // 重置定时器 idthis.second = this.totalSecond // 归位}}, 1000)}},// 登录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)}},// 离开页面清除定时器destroyed () {clearInterval(this.timer)}
}
</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>

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

这里发请求:

// 此处用于存放所有登录相关的接口请求
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}})
}// 3. 登录接口
export const codeLogin = (mobile, smsCode) => {return request.post('/passport/login', {form: {isParty: false,partyData: {},mobile,smsCode}})
}

按需导出

函数调用

14. Toast 轻提示

使用方式1:导入调用

组件内可以调用

非组件内也可以调用

使用方式2:this直接调用

15. 短信验证倒计时

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

index.vue

15.2验证码请求校验处理

15.3封装接口,请求获取验证码

16. 登录功能

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

utils/request.js

18. 登录权证信息存储

1.新建 vuex user 模块 store/modules/user.js

export default {namespaced: true,state () {return {userInfo: {token: '',userId: ''},}},mutations: {},actions: {}
}

2.挂载到 vuex 上

3.提供mutation

export default {namespaced: true,state () {return {userInfo: {token: '',userId: ''},}},mutations: {setUserInfo (state, obj) {state.userInfo = obj},
},actions: {}
}

4.页面中 commit 调用

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

// 约定一个通用的键名
const INFO_KEY = 'hm_shopping_info'
const HISTORY_KEY = 'hm_history_list'// 获取个人信息
export const getInfo = () => {const defaultObj = { token: '', userId: '' }const result = localStorage.getItem(INFO_KEY)//如果没有结果,就默认给一个默认值return result ? JSON.parse(result) : defaultObj
}// 设置个人信息
export const setInfo = (obj) => {localStorage.setItem(INFO_KEY, JSON.stringify(obj))//obj是一个对象,对象不能往本地存,应该转化成字符串
}// 移除个人信息
export const removeInfo = () => {localStorage.removeItem(INFO_KEY)
}
import { getInfo, setInfo } from '@/utils/storage'export default {namespaced: true,state () {return {// 个人权证相关userInfo: getInfo()}},mutations: {// 所有mutations的第一个参数,都是statesetUserInfo (state, obj) {state.userInfo = obj//存入vues的同时,传入一份在本地setInfo(obj)}},actions: {logout (context) {// 个人信息要重置context.commit('setUserInfo', {})// 购物车信息要重置 (跨模块调用 mutation)  cart/setCartListcontext.commit('cart/setCartList', [], { root: true })}},getters: {}
}

20. 添加请求 loading 效果

import store from '@/store'
import axios from 'axios'
import { Toast } from 'vant'// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',timeout: 5000
})// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {// 在发送请求之前做些什么// 开启loading,禁止背景点击 (节流处理,防止多次无效触发)Toast.loading({message: '加载中...',forbidClick: true, // 禁止背景点击loadingType: 'spinner', // 配置loading图标duration: 0 // loading图标不会自动消失})// 只要有token,就在请求时携带,便于请求需要授权的接口const token = store.getters.tokenif (token) {config.headers['Access-Token'] = tokenconfig.headers.platform = 'H5'}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. 页面访问拦截

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

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

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

静态结构:

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

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

import { Search, Swipe, SwipeItem, Grid, GridItem } from 'vant'Vue.use(GridItem)
Vue.use(Search)
Vue.use(Swipe)
Vue.use(SwipeItem)
Vue.use(Grid)

封装接口

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

页面调用

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

动态渲染

<!-- 轮播图 -->
<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="item.text"@click="$router.push('/category')"/>
</van-grid><!-- 猜你喜欢 -->
<div class="guess"><p class="guess-title">—— 猜你喜欢 ——</p><div class="goods-list">//:key="item.goods_id"避免重名就可以了//要传的是整个item对象,完全可以父传子的方式往下传进行渲染,一旦传了就可以通过item来接收<GoodsItem v-for="item in proList"  :item="item" :key="item.goods_id"></GoodsItem></div>
</div>

用props来接收

<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(默认值)就得是个函数,在函数中的返回值就是他的默认值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>

问题1:

问题2:

加这个item.goods_id}的原因

<div v-if="item.goods_id" class="goods-item" @click="$router.push(`/prodetail/${item.goods_id}`)"

点击进去跳转的时候没有携带任何id

23. 搜索 -历史记录管理

1.搜索历史基本渲染

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

2.点击搜索,添加历史

<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)//查找这个key在这个数组中的下标,如查到了便于删除//indexOf 方法用于查找一个元素在数组中的位置,//如果元素存在,会返回该元素的索引(索引从 0 开始);如果不存在,则返回 -1。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>

3.清空历史

<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)//查找这个key在这个数组中的下标,如查到了便于删除//indexOf 方法用于查找一个元素在数组中的位置,//如果元素存在,会返回该元素的索引(索引从 0 开始);如果不存在,则返回 -1。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>

4.持久化

<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)//查找这个key在这个数组中的下标,如查到了便于删除//indexOf 方法用于查找一个元素在数组中的位置,//如果元素存在,会返回该元素的索引(索引从 0 开始);如果不存在,则返回 -1。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>

5.跳转

问题1:

<van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable>

在这里绑定,是为了实时拿到搜索框的值,通过data能快速拿到搜索框的值

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

商品搜索页

代码

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

效果:

问题1:

问题2:

分类搜索页

import request from '@/utils/request'// 获取分类数据
export const getCategoryData = () => {return request.get('/category/list')
}
<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.静态结构

<template><div class="prodetail"><van-nav-bar fixed title="商品详情页" left-arrow @click-left="$router.go(-1)" /><van-swipe :autoplay="3000" @change="onChange"><van-swipe-item v-for="(image, index) in images" :key="index"><img :src="image" /></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">¥0.01</span><span class="oldprice">¥6699.00</span></div><div class="sellcount">已售1001件</div></div><div class="msg text-ellipsis-2">三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23</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">商品评价 (5条)</div><div class="right">查看更多 <van-icon name="arrow" /> </div></div><div class="comment-list"><div class="comment-item" v-for="item in 3" :key="item"><div class="top"><img src="http://cba.itlike.com/public/uploads/10001/20230321/a0db9adb2e666a65bc8dd133fbed7834.png" alt=""><div class="name">神雕大侠</div><van-rate :size="16" :value="5" color="#ffd21e" void-icon="star" void-color="#eee"/></div><div class="content">质量很不错 挺喜欢的</div><div class="time">2023-03-21 15:01:35</div></div></div></div><!-- 商品描述 --><div class="desc"><img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/kHgx21fZMWwqirkMhawkAw.jpg" alt=""><img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/0rRMmncfF0kGjuK5cvLolg.jpg" alt=""><img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/2P04A4Jn0HKxbKYSHc17kw.jpg" alt=""><img src="https://uimgproxy.suning.cn/uimg1/sop/commodity/MT4k-mPd0veQXWPPO5yTIw.jpg" alt=""></div><!-- 底部 --><div class="footer"><div class="icon-home"><van-icon name="wap-home-o" /><span>首页</span></div><div class="icon-cart"><van-icon name="shopping-cart-o" /><span>购物车</span></div><div class="btn-add">加入购物车</div><div class="btn-buy">立刻购买</div></div></div>
</template><script>
export default {name: 'ProDetail',data () {return {images: ['https://img01.yzcdn.cn/vant/apple-1.jpg','https://img01.yzcdn.cn/vant/apple-2.jpg'],current: 0}},methods: {onChange (index) {this.current = index}}
}
</script><style lang="less" scoped>
.prodetail {padding-top: 46px;::v-deep .van-icon-arrow-left {color: #333;}img {display: block;width: 100%;}.custom-indicator {position: absolute;right: 10px;bottom: 10px;padding: 5px 10px;font-size: 12px;background: rgba(0, 0, 0, 0.1);border-radius: 15px;}.desc {width: 100%;overflow: scroll;::v-deep img {display: block;width: 100%!important;}}.info {padding: 10px;}.title {display: flex;justify-content: space-between;.now {color: #fa2209;font-size: 20px;}.oldprice {color: #959595;font-size: 16px;text-decoration: line-through;margin-left: 5px;}.sellcount {color: #959595;font-size: 16px;position: relative;top: 4px;}}.msg {font-size: 16px;line-height: 24px;margin-top: 5px;}.service {display: flex;justify-content: space-between;line-height: 40px;margin-top: 10px;font-size: 16px;background-color: #fafafa;.left-words {span {margin-right: 10px;}.van-icon {margin-right: 4px;color: #fa2209;}}}.comment {padding: 10px;}.comment-title {display: flex;justify-content: space-between;.right {color: #959595;}}.comment-item {font-size: 16px;line-height: 30px;.top {height: 30px;display: flex;align-items: center;margin-top: 20px;img {width: 20px;height: 20px;}.name {margin: 0 10px;}}.time {color: #999;}}.footer {position: fixed;left: 0;bottom: 0;width: 100%;height: 55px;background-color: #fff;border-top: 1px solid #ccc;display: flex;justify-content: space-evenly;align-items: center;.icon-home, .icon-cart {display: flex;flex-direction: column;align-items: center;justify-content: center;font-size: 14px;.van-icon {font-size: 24px;}}.btn-add,.btn-buy {height: 36px;line-height: 36px;width: 120px;border-radius: 18px;background-color: #ffa900;text-align: center;color: #fff;font-size: 14px;}.btn-buy {background-color: #fe5630;}}
}.tips {padding: 10px;
}
</style>

2.封装接口

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

3.动态路由传参

3.1动态路由参数,获取商品 id

3.2一进入页面发送请求,获取商品详情数据

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)},
3.3动态渲染

<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 () {if (this.loginConfirm()) {return}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>
问题1:

直接渲染出现问题

得这样子

商品评价:

接口:

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

<template><!-- 商品评价 --><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>
</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}
}
</script>

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

加深理解

代码区

// 按需导入
import Vue from 'vue'
import { ActionSheet } from 'vant'
Vue.use(ActionSheet)
<!-- 加入购物车/立即购买 公用的弹层 --><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><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 () {if (this.loginConfirm()) {return}const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)this.cartTotal = data.cartTotalthis.$toast('加入购物车成功')this.showPannel = false},}<style lang="less" scoped>
.prodetail {padding-top: 46px;::v-deep .van-icon-arrow-left {color: #333;}img {display: block;width: 100%;}.custom-indicator {position: absolute;right: 10px;bottom: 10px;padding: 5px 10px;font-size: 12px;background: rgba(0, 0, 0, 0.1);border-radius: 15px;}.desc {width: 100%;overflow: scroll;::v-deep img {display: block;width: 100%!important;}}.info {padding: 10px;}.title {display: flex;justify-content: space-between;.now {color: #fa2209;font-size: 20px;}.oldprice {color: #959595;font-size: 16px;text-decoration: line-through;margin-left: 5px;}.sellcount {color: #959595;font-size: 16px;position: relative;top: 4px;}}.msg {font-size: 16px;line-height: 24px;margin-top: 5px;}.service {display: flex;justify-content: space-between;line-height: 40px;margin-top: 10px;font-size: 16px;background-color: #fafafa;.left-words {span {margin-right: 10px;}.van-icon {margin-right: 4px;color: #fa2209;}}}.comment {padding: 10px;}.comment-title {display: flex;justify-content: space-between;.right {color: #959595;}}.comment-item {font-size: 16px;line-height: 30px;.top {height: 30px;display: flex;align-items: center;margin-top: 20px;img {width: 20px;height: 20px;}.name {margin: 0 10px;}}.time {color: #999;}}.footer {position: fixed;left: 0;bottom: 0;width: 100%;height: 55px;background-color: #fff;border-top: 1px solid #ccc;display: flex;justify-content: space-evenly;align-items: center;.icon-home, .icon-cart {display: flex;flex-direction: column;align-items: center;justify-content: center;font-size: 14px;.van-icon {font-size: 24px;}}.btn-add,.btn-buy {height: 36px;line-height: 36px;width: 120px;border-radius: 18px;background-color: #ffa900;text-align: center;color: #fff;font-size: 14px;}.btn-buy {background-color: #fe5630;}}
}.tips {padding: 10px;
}// 弹层的样式
.product {.product-title {display: flex;.left {img {width: 90px;height: 90px;}margin: 10px;}.right {flex: 1;padding: 10px;.price {font-size: 14px;color: #fe560a;.nowprice {font-size: 24px;margin: 0 5px;}}}}.num-box {display: flex;justify-content: space-between;padding: 10px;align-items: center;}.btn, .btn-none {height: 40px;line-height: 40px;margin: 20px;border-radius: 20px;text-align: center;color: rgb(255, 255, 255);background-color: rgb(255, 148, 2);}.btn.now {background-color: #fe5630;}.btn-none {background-color: #cccccc;}
}

效果图

加入style之后

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

<!-- 加入购物车/立即购买 公用的弹层 --><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><script>import CountBox from '@/components/CountBox.vue'export default {name: 'ProDetail',mixins: [loginConfirm],components: {CountBox},data () {return {addCount: 1...}},
}</script>

<template><div class="count-box"><button @click="handleSub" class="minus">-</button><input :value="value" @change="handleChange" class="inp" type="text"><button @click="handleAdd" class="add">+</button></div>
</template><script>
export default {props: {value: {type: Number,default: 1}},methods: {handleSub () {if (this.value <= 1) {return}this.$emit('input', this.value - 1)},handleAdd () {this.$emit('input', this.value + 1)},handleChange (e) {// console.log(e.target.value)const num = +e.target.value // 转数字处理 (1) 数字 (2) NaN// 输入了不合法的文本 或 输入了负值,回退成原来的 value 值if (isNaN(num) || num < 1) {e.target.value = this.valuereturn}this.$emit('input', num)}}
}
</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>

问题1:

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

push改为replace

加强理解

问题1:

问题2:

里面的push换成replace

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

1.封装接口

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

2.请求拦截器

3.页面调用请求

4.图标

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

问题1:

这个skuList.goods_sku_id是商品规格,不过在这个项目中,商品规格默认只有一种

问题2:

所有的请求在发出之前,都会先经过咱们的请求拦截器,我们可以在请求拦截器当中,统一的携带就可以了,找到请求拦截器

30. 购物车模块

1.基本静态结构

静态结构

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

然后加入跳转

需要用到vant组件库

小小修改

<template><div class="count-box"><button @click="handleSub" class="minus">-</button><input :value="value" @change="handleChange" class="inp" type="text"><button @click="handleAdd" class="add">+</button></div>
</template><script>
export default {props: {value: {type: Number,default: 1}},methods: {handleSub () {if (this.value <= 1) {return}this.$emit('input', this.value - 1)},handleAdd () {this.$emit('input', this.value + 1)},handleChange (e) {// console.log(e.target.value)const num = +e.target.value // 转数字处理 (1) 数字 (2) NaN// 输入了不合法的文本 或 输入了负值,回退成原来的 value 值if (isNaN(num) || num < 1) {e.target.value = this.valuereturn}this.$emit('input', num)}}
}
</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>

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

新建 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) {return state.user.userInfo.token}},modules: {user,cart}
})

封装 API 接口 api/cart.js

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

封装 action 和 mutation

export default {namespaced: true,state () {return {cartList: []}},mutations: {setCartList (state, newList) {state.cartList = newList},
},
actions: {async getCartAction (context) {const { data } = await getCartList()// 后台返回的数据中,不包含复选框的选中状态,为了实现将来的功能// 需要手动维护数据,给每一项,添加一个 isChecked 状态 (标记当前商品是否选中)data.list.forEach(item => {item.isChecked = true})context.commit('setCartList', data.list)}
},getters: {}
}

判断用户是否登录

==

加深理解

都要在head中传递,但是已经在请求拦截器中统一携带了这2个参数,所以只用调接口就可以了

问题1:

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

3.1数据映射到页面

要做动态渲染,然后数据告诉你了在vuex,在页面当中想要拿到这个state中数据,想在cart.vue拿到数据,提供对应的计算属性

3.2动态渲染

<!-- 购物车列表 -->
<div class="cart-list"><div class="cart-item" v-for="item in cartList" :key="item.goods_id"><van-checkbox icon-size="18" :value="item.isChecked"></van-checkbox><div class="show" @click="$router.push(`/prodetail/${item.goods_id}`)"><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 :value="item.goods_num"></CountBox></span></div></div>
</div>
问题1:

不能是v-model

应该是

因为v-model是直接跟vues里面的数据双向绑定,但是这里只是单纯的渲染,任何一个组件随便改就乱了

4.封装getter实现动态设计

4.1封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价

getters: {cartTotal (state) {return state.cartList.reduce((sum, item, index) => sum + item.goods_num, 0)},selCartList (state) {return state.cartList.filter(item => item.isChecked)},selCount (state, getters) {return getters.selCartList.reduce((sum, item, index) => sum + item.goods_num, 0)},selPrice (state, getters) {return getters.selCartList.reduce((sum, item, index) => {return sum + item.goods_num * item.goods.goods_price_min}, 0).toFixed(2)//.toFixed(2)保留2位小数}
}

4.2这些计算属性想在页面应用

4.3页面中 mapGetters 映射使用

computed: {...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
},<!-- 购物车开头 -->
<div class="cart-title"><span class="all">共<i>{{ cartTotal || 0 }}</i>件商品</span><span class="edit"><van-icon name="edit"  />编辑</span>
</div><div class="footer-fixed"><div  class="all-check"><van-checkbox  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="true" :class="{ disabled: selCount === 0 }" class="goPay">结算({{ selCount }})</div><div v-else  :class="{ disabled: selCount === 0 }" class="delete">删除({{ selCount }})</div></div>
</div>

4.4优化

问题1:

如何在getter属性中访问呢getter

5.全选反选功能

5.1可以点击小选

5.2点击小选可以控制大选+点击大选可以控制全部小选(青色)

import { changeCount, delSelect, getCartList } from '@/api/cart'
import { Toast } from 'vant'export default {namespaced: true,state () {return {cartList: []}},mutations: {toggleAllCheck (state, flag) {state.cartList.forEach(item => {item.isChecked = flag})},},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) => {return sum + item.goods_num * item.goods.goods_price_min}, 0).toFixed(2)},// 是否全选isAllChecked (state) {return state.cartList.every(item => item.isChecked)}}
}
<template><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',  '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)//点击之后原来的状态应该取反,然后原来的状态已经在上面映射了,如45行},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>

6.数字框修改数量功能

6.1封装接口

action{

}

调用mutations

问题1:

问题:

封装接口之后,要调用的话,就需要这个CountBox.vue事件,

添加input就可以拿到你所传递过来的参数,

传参就可以拿到数据

但是参数不只有goodsnum,还有其他

在遍历的是有的

所以只要把item传进去就有了,但是如果这么写

就会丢失原本的input传递过来的形参

解决:


既想要保留原本的形参,又需要调用函数传参,

问题2:

是后台更新成功了,但是页面没更新,

本地先修改,然后更新到后台

本地修改只需要传2个本地的购物车就能更新了,不用goods_sku_id

7.编辑切换状态

<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="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></template>data () {return {isEdit: false}},
watch: {isEdit (value) {if (value) {this.$store.commit('cart/toggleAllCheck', false)} else {this.$store.commit('cart/toggleAllCheck', true)}}}

效果

8.删除功能

8.1问题1:

代表这一条对应的购物车的数据,

后面如果要删除某一条数据,应该要把这个id组合起来传给后台,后台是数组包含这个id值,接口是string,所以是字符串数组

8.2代码:

1️⃣查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )

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})
}// 获取购物车列表
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})
}
2️⃣注册删除点击事件,调用接口

然后重置一下删除状态,等删除完就转换为结算的按钮

<div v-else :class="{ disabled: selCount === 0 }" @click="handleDel" class="delete">删除({{ selCount }})
</div>async handleDel () {if (this.selCount === 0) returnawait this.$store.dispatch('cart/delSelect')//这个删除选中,其实在本身的actions里是能够获取到的this.isEdit = false
},
理解

🎴

if (this.selCount === 0) return

🎴

3️⃣提供 actions
actions: {// 删除购物车数据async delSelect (context) {const selCartList = context.getters.selCartListconst cartIds = selCartList.map(item => item.id)await delSelect(cartIds)Toast('删除成功')// 重新拉取最新的购物车数据 (重新渲染)context.dispatch('getCartAction')}
},
理解

🪂

🪂

这步的思路,

直接调用我们封装的方法delSelect,但是需要cartIds,就得先拿到数组,要怎么去拿到我们的购物车数组,有context就有state,有state就能拿到数组,而且通过context还能拿到getters,能拿到getters就能拿到选中商品的项,

然后传入这个cartIds , delSelect(cartIds),然后静待他的请求(await)

🪂

拉去的是这一步,action里面调用action

9.空购物车

1.外面包个大盒子,添加 v-if 判断,然后添加一下购物车的渲染

<div class="cart-box" v-if="isLogin && cartList.length > 0"><!-- 购物车开头 --><div class="cart-title">...</div><!-- 购物车列表 --><div class="cart-list">...</div><div class="footer-fixed">...</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>

2.因为一直要判断,所以小小完善一下

3.样式

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

31. 订单结算台

31.1静态样式

<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="true"><div class="info-content"><span class="name">小红</span><span class="mobile">13811112222</span></div><div class="info-address">江苏省 无锡市 南长街 110号 504</div></div><div class="info" v-else>请选择配送地址</div><div class="right-icon"><van-icon name="arrow" /></div></div><!-- 订单明细 --><div class="pay-list"><div class="list"><div class="goods-item"><div class="left"><img src="http://cba.itlike.com/public/uploads/10001/20230321/8f505c6c437fc3d4b4310b57b1567544.jpg" alt="" /></div><div class="right"><p class="tit text-ellipsis-2">三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23</p><p class="info"><span class="count">x3</span><span class="price">¥9.99</span></p></div></div></div><div class="flow-num-box"><span>共 12 件商品,合计:</span><span class="money">¥1219.00</span></div><div class="pay-detail"><div class="pay-cell"><span>订单总金额:</span><span class="red">¥1219.00</span></div><div class="pay-cell"><span>优惠券:</span><span>无优惠券可用</span></div><div class="pay-cell"><span>配送费用:</span><span v-if="false">请先选择配送地址</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" />余额支付(可用 ¥ 999919.00 元)</span><!-- <span>请先选择配送地址</span> --><span class="red"><van-icon name="passed" /></span></div></div><!-- 买家留言 --><div class="buytips"><textarea placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea></div></div><!-- 底部提交 --><div class="footer-fixed"><div class="left">实付款:<span>¥999919</span></div><div class="tipsbtn">提交订单</div></div></div>
</template><script>
export default {name: 'PayIndex',data () {return {}},methods: {}
}
</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>

31.2获取收货地址列表

1️⃣封装地址的接口4️⃣

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

2️⃣调用获取地址

一进入页面就发请求,用created

import { getAddressList } from '@/api/address'
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.regionreturn region.province + region.city + region.region + this.selectAddress.detail}
},<div class="info" v-if="selectAddress?.address_id"><div class="info-content"><span class="name">{{ selectAddress.name }}</span><span class="mobile">{{ selectAddress.phone }}</span></div><div class="info-address">{{ longAddress }}</div>
</div>

理解:

这里还有一个贴心的判断

32. 订单结算台 -确认订单信息

33. 订单结算台 -购物车结算

33.1跳转查询参数mode='cart'和

封装接口

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 buyNowdelivery: 10, // 10 快递配送 20 门店自提couponId: 0, // 优惠券ID 传0 不使用优惠券isUsePoints: 0, // 积分 传0 不使用积分...obj // 将传递过来的参数对象 动态展开}})
}

注册点击事件

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

模式:cart

method:{
goPay () {// 判断有没有选中商品if (this.selCount > 0) {// 有选中的 商品 才进行结算跳转this.$router.push({path: '/pay',query: {mode: 'cart',cartIds: this.selCartList.map(item => item.id).join(',') // 'cartId,cartId,cartId'}})}}
}

33.2页面中$routr.query接收参数

理解

实现效果

成功把2个参数传递过来,拿是拿过来了,但是没有去获取他,对于地址栏上的参数,直接$.route.query

为了访问,可以在这个pay的页面去定义2个计算属性,找起来方便一点

33.3调用接口,获得数据

<script>
import {checkOrder} from '@/api/order'
export default {data () {return {order: {},personal: {}}
},computed: {mode () {return this.$route.query.mode},cartIds () {return this.$route.query.cartIds}
}}async created () {//一进页面就发请求,要用createdthis.getOrderList()
},async getOrderList () {if (this.mode === 'cart') {const { data: { order, personal } } = await checkOrder(this.mode, { cartIds: this.cartIds })this.order = orderthis.personal = personal}
}

33.4基于数据进行渲染

<!-- 订单明细 -->
<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 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">提交订单</div>
</div>

34. 订单结算台 -立即购买结算

34.1点击跳转传参

<div class="btn" v-if="mode === 'buyNow'" @click="goBuyNow">立刻购买</div>goBuyNow () {this.$router.push({path: '/pay',query: {mode: 'buyNow',goodsId: this.goodsId,goodsSkuId: this.detail.skuList[0].goods_sku_id,goodsNum: this.addCount}})
}

报错

代码没问题,是进入这个页面之后,会立刻去做渲染,而上一个渲染,他没有做判断就报错,

这里是直接去做渲染,基于cartIds去渲染,一旦跳转过来变成buyNow之后,应该要传递 的是goodsId,···,所以需要去准备对应的计算属性,去获取传递过来的参数,

参数传递过来了只不过问题是没有去拿,拿完之后没有基于他去渲染,所以就是地址栏有,但是请求的时候没有带,

34.2计算属性处理参数

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

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

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

理解

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

原因:

这个goBuyNow没有做任何未登录的处理

按道理说直接这段复制粘贴就可以了,但是代码太长,可以封装一下

直接封装成一个方法,但是后面其他页面也可能也会用

希望把这个方法也封装到一个专门的公共位置去复用,这个方法又不是一个普通的函数,又访问store,访问到组件内的一些东西

如果冲突了以组件内部为主

代码

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: {backUrl: this.$route.fullPath}})}).catch(() => {})return true}return false}}
}

35. 提交订单并支付

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

import request from '@/utils/request'
// 提交订单
// 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, // 10 快递配送couponId: 0,isUsePoints: 0,payType: 10, // 余额支付...obj})
}// 订单列表
export const getMyOrderList = (dataType, page) => {return request.get('/order/list', {params: {dataType,page // List}})
}

35.2买家留言绑定

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

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

<div class="tipsbtn" @click="submitOrder">提交订单</div>
import {submitOrder} from '@/api/order'method{
// 提交订单
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')
}
}

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

🪂订单管理

36.1 静态布局

1️⃣ 基础静态结构

<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>
2️⃣components/OrderListItem

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

3️⃣导入注册

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

36.2点击 tab 切换渲染

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

// 订单列表
export const getMyOrderList = (dataType, page) => {return request.get('/order/list', {params: {dataType,page}})
}
2️⃣给 tab 绑定 name 属性
封装调用接口获取数据

<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>
3️⃣动态渲染

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

🪂个人中心

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

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

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

<template><div class="user"><div class="head-page" v-if="isLogin"><div class="head-img"><img src="@/assets/default-avatar.png" alt="" /></div><div class="info"><div class="mobile">{{ detail.mobile }}</div><div class="vip"><van-icon name="diamond-o" />普通会员</div></div></div><div v-else class="head-page" @click="$router.push('/login')"><div class="head-img"><img src="@/assets/default-avatar.png" alt="" /></div><div class="info"><div class="mobile">未登录</div><div class="words">点击登录账号</div></div></div><div class="my-asset"><div class="asset-left"><div class="asset-left-item"><span>{{ detail.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 @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.$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>

🪂退出功能

这个退出,是需要操作vuex里面的数据的,操作vuex里面的数据,一定要提交mutation

1.注册点击事件

2.提供方法

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

mutations: {// 所有mutations的第一个参数,都是statesetUserInfo (state, obj) {state.userInfo = obj//存入vues的同时,传入一份在本地setInfo(obj)}},actions: {logout (context) {// 个人信息要重置context.commit('setUserInfo', {})// 购物车信息要重置 (跨模块调用 mutation)  cart/setCartListcontext.commit('cart/setCartList', [], { root: true })}},getters: {}
}

跨模块

问题1:

一点退出的提示框,有多个数据要清,尤其vuex里面的多个数据,在vuex现在有2个分模块的数据

虽然这个cart看起来没什么数据,但是一旦加入了几个商品,就会有数据了,无论是cart还是user,都要在退出之后重置数据,但是有多个模块的数据需要去重置,就需要多mutations,这个退出是一个动作=> 包含了两步,分别是将 user 和 cart 进行重置,所以就封装action

37. 打包发布

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

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

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

相关文章

AR/MR实时光照阴影开发教程

一、效果演示 1、PICO4 Ultra MR 发光的球 2、AR实时光照 二、实现原理 PICO4 Ultra MR开发时&#xff0c;通过空间网格能力扫描周围环境&#xff0c;然后将扫描到的环境网格材质替换为一个透明材质并停止扫描&#xff1b;基于Google ARCore XR Plugin和ARFoundation进行安卓手…

【Python训练营打卡】day42 @浙大疏锦行

DAY 42 Grad-CAM与Hook函数 知识点回顾 1. 回调函数 2. lambda函数 3. hook函数的模块钩子和张量钩子 4. Grad-CAM的示例 作业&#xff1a;理解下今天的代码即可 Grad-CAM 在深度学习中&#xff0c;我们经常需要查看或修改模型中间层的输出或梯度。然而&#xff0c;标准的…

创建ipv6 only和ipv6+ip4的k8s集群的注意事项

关键字 : CNI calico vxlan flannel ipv6-only ipv6ipv4 在搭建ipv6-only或ipv6ipv4的k8s集群时&#xff0c;在worker节点加入集群后&#xff0c;发现worker节点上的CNI启动失败。 以下是calico的启动失败情况 : kubectl get pod -A输出如下 : NAMESPACE NAME …

鸿蒙OSUniApp离线优先数据同步实战:打造无缝衔接的鸿蒙应用体验#三方框架 #Uniapp

UniApp离线优先数据同步实战&#xff1a;打造无缝衔接的鸿蒙应用体验 最近在开发一个面向鸿蒙生态的UniApp应用时&#xff0c;遇到了一个有趣的挑战&#xff1a;如何在网络不稳定的情况下保证数据的实时性和可用性。经过一番探索和实践&#xff0c;我们最终实现了一套行之有效…

day 43

应用cnn对kaggle上的图像数据集进行练习 数据集地址&#xff1a;Cat and Dog import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt im…

Spring Boot 如何实现定时任务

Spring Boot 如何实现定时任务 在现代的微服务架构中&#xff0c;定时任务是一个常见的需求。无论是数据定时同步、定时清理缓存&#xff0c;还是定时发送通知&#xff0c;Spring Boot 提供了非常强大且灵活的定时任务支持。本文将详细介绍如何在 Spring Boot 中实现定时任务&…

“粽”览全局:分布式系统架构与实践深度解析(端午特别版)

第一部分&#xff1a;引言——技术世界的“端午”第二部分&#xff1a;分布式系统概述——粽子节点初探第三部分&#xff1a;核心技术详解——技术“粽子”大解构 粽叶篇&#xff1a;通信协议糯米篇&#xff1a;一致性算法馅料篇&#xff1a;任务调度与计算包扎篇&#xff1a;系…

AppTrace 视角下 App 一键拉起:提升应用转化率的高效方案​

官网地址&#xff1a;AppTrace - 专业的移动应用推广追踪平台 在大规模开展 App 推广、用户召回、广告投放、邀请传播等活动时&#xff0c;高效的深度链接方案至关重要。它不仅能缩短用户路径&#xff0c;带来无缝、流畅的跳转体验&#xff0c;更核心的是通过参数传递打通 web…

手拆STL

vector v e c t o r vector vector&#xff0c;动态数组。 先来看一下它的一些基本操作及其拆后残渣。 1.a.push_back(x)&#xff0c;将 x x x加入动态数组 a a a的末尾。 实现&#xff1a;a[cnt]x 2.a.size()&#xff0c;查询动态数组 a a a中元素的数量。 实现&#xff1a;cn…

6.01打卡

浙大疏锦行 DAY 40 训练和测试的规范写法 知识点回顾&#xff1a; 1. 彩色和灰度图片测试和训练的规范写法&#xff1a;封装在函数中 2. 展平操作&#xff1a;除第一个维度batchsize外全部展平 3. dropout操作&#xff1a;训练阶段随机丢弃神经元&#xff0c;测试阶段eval模…

CSS专题之层叠上下文

前言 石匠敲击石头的第 15 次 在平常开发的时候&#xff0c;有时候会遇到使用 z-index 调整元素层级没有效果的情况&#xff0c;究其原因还是因为对层叠上下文不太了解&#xff0c;看了网上很多前辈的文章&#xff0c;决定打算写一篇文章来梳理一下&#xff0c;如果哪里写的有问…

RabbitMQ集群与负载均衡实战指南

文章目录 集群架构概述仲裁队列的使用1. 使用Spring框架代码创建2. 使用amqp-client创建3. 使用管理平台创建 负载均衡引入HAProxy 负载均衡&#xff1a;使用方法1. 修改配置文件2. 声明队列 test_cluster3. 发送消息 集群架构 概述 RabbitMQ支持部署多个结点&#xff0c;每个…

Prometheus + Grafana + Cadvisor:构建高效企业级服务监控体系

在现代软件开发和运维领域&#xff0c;容器化技术的应用越来越广泛&#xff0c;其中 Docker 作为最受欢迎的容器化解决方案之一&#xff0c;其容器的监控管理变得至关重要。本文将详细介绍如何使用 cadvisor、Prometheus 和 Grafana 来监控 Docker 容器的状态。 一、安装镜像 …

小提琴图绘制-Graph prism

在 GraphPad Prism 中为小提琴图添加显著性标记(如*P<0.05)的步骤如下: 步骤1:完成统计检验 选择数据表:确保数据已按分组排列(如A列=Group1,B列=Group2)。执行统计检验: 点击工具栏 Analyze → Column analyses → Mann-Whitney test(非参数检验,适用于非正态数…

【开源工具】跳过网页APP禁止粘贴限制:自动输入键盘模拟工具

&#x1f4cc; 【黑科技】跳过网页APP禁止粘贴限制&#xff1a;自动输入键盘模拟工具 &#x1f308; 个人主页&#xff1a;创客白泽 - CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f40d;《Python开源项目实战》 &#x1f4a1; 热爱不止于代码&#xff0c;热情源自每一…

深度学习篇---face-recognition的优劣点

face_recognition库是一个基于 Python 的开源人脸识别工具&#xff0c;封装了 dlib 库的深度学习模型&#xff0c;具有易用性高、集成度强的特点。以下从技术实现、应用场景等维度分析其优劣势&#xff1a; 一、核心优势 1. 极简 API 设计&#xff0c;开发效率极高 代码量少…

Git深入解析功能逻辑与核心业务场景流程

一、Git核心功能逻辑架构 #mermaid-svg-9tj1iCr99u6QenJM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-9tj1iCr99u6QenJM .error-icon{fill:#552222;}#mermaid-svg-9tj1iCr99u6QenJM .error-text{fill:#552222;st…

【大模型】情绪对话模型项目研发

一、使用框架&#xff1a; Qwen大模型后端Open-webui前端实现使用LLamaFactory的STF微调数据集&#xff0c;vllm后端部署&#xff0c; 二、框架安装 下载千问大模型 安装魔塔社区库文件 pip install modelscope Download.py 内容 from modelscope import snapshot_downlo…

Java基础 Day26

一、网络编程简介 1、概念 网络编程指在网络通信协议下&#xff0c;不同计算机上运行的程序&#xff0c;进行数据传输 2、软件架构 &#xff08;1&#xff09;CS架构&#xff08;客户端和服务端&#xff09; 在用户本地有一个客户端程序&#xff0c;在远程有一个服务器端程…

【Hot 100】45. 跳跃游戏 II

目录 引言跳跃游戏 IIdp解题贪心解题 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;【Hot 100】45. 跳跃游戏 II❣️ 寄语&#xff1a;书到用时方恨少&#xff0c;事非经过不知难&#xff01; 引言 跳跃…