前端Vue小兔鲜儿电商项目实战Day05

一、登录 - 整体认识和路由配置

1. 整体认识

登录页面的主要功能就是表单校验和登录退出业务

①src/views/Login/index.vue

<script setup></script><template><div><header class="login-header"><div class="container m-top-20"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><RouterLink class="entry" to="/">进入网站首页<i class="iconfont icon-angle-right"></i><i class="iconfont icon-angle-right"></i></RouterLink></div></header><section class="login-section"><div class="wrapper"><nav><a href="javascript:;">账户登录</a></nav><div class="account-box"><div class="form"><el-form label-position="right" label-width="60px" status-icon><el-form-item label="账户"><el-input /></el-form-item><el-form-item label="密码"><el-input /></el-form-item><el-form-item label-width="22px"><el-checkbox size="large">我已同意隐私条款和服务条款</el-checkbox></el-form-item><el-button size="large" class="subBtn">点击登录</el-button></el-form></div></div></div></section><footer class="login-footer"><div class="container"><p><a href="javascript:;">关于我们</a><a href="javascript:;">帮助中心</a><a href="javascript:;">售后服务</a><a href="javascript:;">配送与验收</a><a href="javascript:;">商务合作</a><a href="javascript:;">搜索推荐</a><a href="javascript:;">友情链接</a></p><p>CopyRight &copy; 小兔鲜儿</p></div></footer></div>
</template><style scoped lang="scss">
.login-header {background: #fff;border-bottom: 1px solid #e4e4e4;.container {display: flex;align-items: flex-end;justify-content: space-between;}.logo {width: 200px;a {display: block;height: 132px;width: 100%;text-indent: -9999px;background: url('@/assets/images/logo.png') no-repeat center 18px /contain;}}.sub {flex: 1;font-size: 24px;font-weight: normal;margin-bottom: 38px;margin-left: 20px;color: #666;}.entry {width: 120px;margin-bottom: 38px;font-size: 16px;i {font-size: 14px;color: $xtxColor;letter-spacing: -5px;}}
}.login-section {background: url('@/assets/images/login-bg.png') no-repeat center / cover;height: 488px;position: relative;.wrapper {width: 380px;background: #fff;position: absolute;left: 50%;top: 54px;transform: translate3d(100px, 0, 0);box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);nav {font-size: 14px;height: 55px;margin-bottom: 20px;border-bottom: 1px solid #f5f5f5;display: flex;padding: 0 40px;text-align: right;align-items: center;a {flex: 1;line-height: 1;display: inline-block;font-size: 18px;position: relative;text-align: center;}}}
}.login-footer {padding: 30px 0 50px;background: #fff;p {text-align: center;color: #999;padding-top: 20px;a {line-height: 1;padding: 0 10px;color: #999;display: inline-block;~ a {border-left: 1px solid #ccc;}}}
}.account-box {.toggle {padding: 15px 40px;text-align: right;a {color: $xtxColor;i {font-size: 14px;}}}.form {padding: 0 20px 20px 20px;&-item {margin-bottom: 28px;.input {position: relative;height: 36px;> i {width: 34px;height: 34px;background: #cfcdcd;color: #fff;position: absolute;left: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 18px;}input {padding-left: 44px;border: 1px solid #cfcdcd;height: 36px;line-height: 36px;width: 100%;&.error {border-color: $priceColor;}&.active,&:focus {border-color: $xtxColor;}}.code {position: absolute;right: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 14px;background: #f5f5f5;color: #666;width: 90px;height: 34px;cursor: pointer;}}> .error {position: absolute;font-size: 12px;line-height: 28px;color: $priceColor;i {font-size: 14px;margin-right: 2px;}}}.agree {a {color: #069;}}.btn {display: block;width: 100%;height: 40px;color: #fff;text-align: center;line-height: 40px;background: $xtxColor;&.disabled {background: #cfcdcd;}}}.action {padding: 20px 40px;display: flex;justify-content: space-between;align-items: center;.url {a {color: #999;margin-left: 10px;}}}
}.subBtn {background: $xtxColor;width: 100%;color: #fff;
}
</style>

②src/views/Layout/components/LayoutNav.vue

<script setup></script><template><nav class="app-topnav"><div class="container"><ul><!-- 多模板渲染 区分登录状态和非登录状态 --><template v-if="false"><li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li><li><el-popconfirmtitle="确认退出吗?"confirm-button-text="确认"cancel-button-text="取消"><template #reference><a href="javascript:;">退出登录</a></template></el-popconfirm></li><li><a href="javascript:;">我的订单</a></li><li><a href="javascript:;">会员中心</a></li></template><template v-else><li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li><li><a href="javascript:;">帮助中心</a></li><li><a href="javascript:;">关于我们</a></li></template></ul></div></nav>
</template><style scoped lang="scss">
<!-- ... ... -->
</style>

二、登录 - 表单校验实现

1. 为什么需要校验

作用:前端提前校验可以省去一些错误的请求提交,为后端节省接口压力

2. 表单如何进行校验

Form 表单 | Element Plus

ElementPlus表单组件内置了表单校验功能,只需要按照组件要求配置必要参数即可。

思想:当功能很复杂时,通过多个组件各自负责某个功能,再组合成一个大功能是组件设计中的常用方法。

表单校验步骤

  • 1. 按照接口字段准备表单对象并绑定
  • 2. 按照产品要求准备规则对象并绑定
  • 3. 指定表单域的校验字段名
  • 4. 把表单对象进行双向绑定

自定义校验规则

ElementPlus表单组件内置了初始的校验配置,应付简单的校验只需要通过配置即可,如果想要定制一些特殊的校验需求,可以使用自定义校验规则,格式如下:

校验逻辑:如果勾选了协议框,通过校验,如果没有勾选,不通过校验

src/views/Login/index.vue

<script setup>
// 表单校验
// 整个表单的校验规则
// 1. 非空校验 required: true   message消息提示,trigger触发校验的时机:blur change
// 2. 长度校验 min:xxx, max:xxx
// 3. 正则校验 pattern: 正则规则  \S:非空字符
// 4. 自定义校验 => 自己写逻辑校验(校验函数)
//    validator: (rule, value, callback)
//    (1)rule: 当前校验规则的相关信息
//    (2)value: 所校验的表单元素目前的表单值
//    (3)callback 无论成功还是失败,都需要callback回调
//        - callback()校验成功import { ref } from 'vue'
const form = ref()
// 1. 准备表单对象
const formModel = ref({account: '',password: '',agree: false
})// 2. 准备校验规则对象
const rules = {account: [{ required: true, message: '用户名不能为空', trigger: 'blur' },{pattern: /^\S{5,15}$/,message: '账户名必须是5-15位的非空字符',trigger: 'blur'}],password: [{ required: true, message: '密码不能为空', trigger: 'blur' },{pattern: /^\S{6,15}$/,message: '密码必须是5-16位的非空字符',trigger: 'blur'}],agree: [{// 自定义校验规则validator: (rule, value, callback) => {console.log(value)// 判断是否勾选协议if (!value) {callback(new Error('请先勾选同意协议'))} else {callback()}}}]
}
</script><template><div><header class="login-header"><div class="container m-top-20"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><RouterLink class="entry" to="/">进入网站首页<i class="iconfont icon-angle-right"></i><i class="iconfont icon-angle-right"></i></RouterLink></div></header><section class="login-section"><div class="wrapper"><nav><a href="javascript:;">账户登录</a></nav><div class="account-box"><div class="form"><el-form:model="formModel":rules="rules"ref="form"label-position="right"label-width="60px"status-icon><el-form-item label="账户" prop="account"><el-inputv-model="formModel.account"placeholder="请输入账户名"/></el-form-item><el-form-item label="密码" prop="password"><el-inputv-model="formModel.password"placeholder="请输入密码"/></el-form-item><el-form-item label-width="22px" prop="agree"><el-checkbox size="large" v-model="formModel.agree">我已同意隐私条款和服务条款</el-checkbox></el-form-item><el-button size="large" class="subBtn">点击登录</el-button></el-form></div></div></div></section><footer class="login-footer"><div class="container"><p><a href="javascript:;">关于我们</a><a href="javascript:;">帮助中心</a><a href="javascript:;">售后服务</a><a href="javascript:;">配送与验收</a><a href="javascript:;">商务合作</a><a href="javascript:;">搜索推荐</a><a href="javascript:;">友情链接</a></p><p>CopyRight &copy; 小兔鲜儿</p></div></footer></div>
</template><style scoped lang="scss">
.login-header {background: #fff;border-bottom: 1px solid #e4e4e4;.container {display: flex;align-items: flex-end;justify-content: space-between;}.logo {width: 200px;a {display: block;height: 132px;width: 100%;text-indent: -9999px;background: url('@/assets/images/logo.png') no-repeat center 18px /contain;}}.sub {flex: 1;font-size: 24px;font-weight: normal;margin-bottom: 38px;margin-left: 20px;color: #666;}.entry {width: 120px;margin-bottom: 38px;font-size: 16px;i {font-size: 14px;color: $xtxColor;letter-spacing: -5px;}}
}.login-section {background: url('@/assets/images/login-bg.png') no-repeat center / cover;height: 488px;position: relative;.wrapper {width: 380px;background: #fff;position: absolute;left: 50%;top: 54px;transform: translate3d(100px, 0, 0);box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);nav {font-size: 14px;height: 55px;margin-bottom: 20px;border-bottom: 1px solid #f5f5f5;display: flex;padding: 0 40px;text-align: right;align-items: center;a {flex: 1;line-height: 1;display: inline-block;font-size: 18px;position: relative;text-align: center;}}}
}.login-footer {padding: 30px 0 50px;background: #fff;p {text-align: center;color: #999;padding-top: 20px;a {line-height: 1;padding: 0 10px;color: #999;display: inline-block;~ a {border-left: 1px solid #ccc;}}}
}.account-box {.toggle {padding: 15px 40px;text-align: right;a {color: $xtxColor;i {font-size: 14px;}}}.form {padding: 0 20px 20px 20px;&-item {margin-bottom: 28px;.input {position: relative;height: 36px;> i {width: 34px;height: 34px;background: #cfcdcd;color: #fff;position: absolute;left: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 18px;}input {padding-left: 44px;border: 1px solid #cfcdcd;height: 36px;line-height: 36px;width: 100%;&.error {border-color: $priceColor;}&.active,&:focus {border-color: $xtxColor;}}.code {position: absolute;right: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 14px;background: #f5f5f5;color: #666;width: 90px;height: 34px;cursor: pointer;}}> .error {position: absolute;font-size: 12px;line-height: 28px;color: $priceColor;i {font-size: 14px;margin-right: 2px;}}}.agree {a {color: #069;}}.btn {display: block;width: 100%;height: 40px;color: #fff;text-align: center;line-height: 40px;background: $xtxColor;&.disabled {background: #cfcdcd;}}}.action {padding: 20px 40px;display: flex;justify-content: space-between;align-items: center;.url {a {color: #999;margin-left: 10px;}}}
}.subBtn {background: $xtxColor;width: 100%;color: #fff;
}
</style>

3. 整个表单的内容验证

思考:每个表单域都有自己的校验触发事件,如果用户一上来就点击登录怎么办呢?

答:在点击登录时需要对所有需要校验的表单进行统一校验

三、登录 - 基础登录业务实现

1. 封装登录接口 - src/apis/user.js

import instance from '@/utils/http.js'// 登录接口
// export const loginAPI = ({ account, password }) => {
//   instance.post('/login', { account, password })
// }
export const loginAPI = ({ account, password }) => {return instance({url: '/login',method: 'POST',data: {account,password}})
}

2. 登录成功后续逻辑处理 - src/views/Login/index.vue

<script setup>
// 表单校验
// 整个表单的校验规则
// 1. 非空校验 required: true   message消息提示,trigger触发校验的时机:blur change
// 2. 长度校验 min:xxx, max:xxx
// 3. 正则校验 pattern: 正则规则  \S:非空字符
// 4. 自定义校验 => 自己写逻辑校验(校验函数)
//    validator: (rule, value, callback)
//    (1)rule: 当前校验规则的相关信息
//    (2)value: 所校验的表单元素目前的表单值
//    (3)callback 无论成功还是失败,都需要callback回调
//        - callback()校验成功import { loginAPI } from '@/apis/user.js'
import { ref } from 'vue'
import { useRouter } from 'vue-router'const form = ref(null)
// 1. 准备表单对象
const formModel = ref({account: '',password: '',agree: false
})// 2. 准备校验规则对象
const rules = {account: [{ required: true, message: '用户名不能为空', trigger: 'blur' },{pattern: /^\S{5,15}$/,message: '账户名必须是5-15位的非空字符',trigger: 'blur'}],password: [{ required: true, message: '密码不能为空', trigger: 'blur' },{pattern: /^\S{6,15}$/,message: '密码必须是5-16位的非空字符',trigger: 'blur'}],agree: [{// 自定义校验规则validator: (rule, value, callback) => {console.log(value)// 判断是否勾选协议if (!value) {callback(new Error('请先勾选同意协议'))} else {callback()}}}]
}// 带r,调用方法;不带r,获取参数
const router = useRouter()
const doLogin = async () => {// 登录之前,先进行校验。校验成功,发请求;校验失败,自动提示await form.value.validate()const { account, password } = formModel.valueawait loginAPI({ account, password })ElMessage.success('登录成功')// 跳转首页router.replace({ path: '/' })
}
</script><template><div><header class="login-header"><div class="container m-top-20"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><RouterLink class="entry" to="/">进入网站首页<i class="iconfont icon-angle-right"></i><i class="iconfont icon-angle-right"></i></RouterLink></div></header><section class="login-section"><div class="wrapper"><nav><a href="javascript:;">账户登录</a></nav><div class="account-box"><!-- (1) el-form => :model="ruleForm"      绑定的整个form的数据对象 { xxx, xxx, xxx }(2) el-form => :rules="rules"         绑定的整个rules规则对象  { xxx, xxx, xxx }(3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性(4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)--><div class="form"><el-form:model="formModel":rules="rules"ref="form"label-position="right"label-width="60px"status-icon><el-form-item label="账户" prop="account"><el-inputv-model="formModel.account"placeholder="请输入账户名"/></el-form-item><el-form-item label="密码" prop="password"><el-inputv-model="formModel.password"placeholder="请输入密码"/></el-form-item><el-form-item label-width="22px" prop="agree"><el-checkbox size="large" v-model="formModel.agree">我已同意隐私条款和服务条款</el-checkbox></el-form-item><el-button @click="doLogin" size="large" class="subBtn">点击登录</el-button></el-form></div></div></div></section><footer class="login-footer"><div class="container"><p><a href="javascript:;">关于我们</a><a href="javascript:;">帮助中心</a><a href="javascript:;">售后服务</a><a href="javascript:;">配送与验收</a><a href="javascript:;">商务合作</a><a href="javascript:;">搜索推荐</a><a href="javascript:;">友情链接</a></p><p>CopyRight &copy; 小兔鲜儿</p></div></footer></div>
</template>

3. .eslintrc.cjs - 配置全局变量

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')module.exports = {root: true,extends: ['plugin:vue/vue3-essential','eslint:recommended','@vue/eslint-config-prettier/skip-formatting'],parserOptions: {ecmaVersion: 'latest'},rules: {// prettier专注于代码的美观度 (格式化工具)// 前置:// 1. 禁用格式化插件 prettier  format on save 关闭// 2. 安装Eslint插件, 并配置保存时自动修复'prettier/prettier': ['warn',{singleQuote: true, // 单引号semi: false, // 无分号printWidth: 80, // 每行宽度至多80字符trailingComma: 'none', // 不加对象|数组最后逗号endOfLine: 'auto' // 换行符号不限制(win mac 不一致)}],// ESLint关注于规范, 如果不符合规范,报错'vue/multi-word-component-names': ['warn',{ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)}],'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验 (props解构丢失响应式)// 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。'no-undef': 'error'},// 全局变量globals: {ElMessage: 'readonly',ElMessageBox: 'readonly',ElLoading: 'readonly'}
}

4. 登录失败的逻辑处理 - src/utils/http.js

import axios from 'axios'// 创建axios实例
const instance = axios.create({baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',timeout: 5000
})// axios请求拦截器
instance.interceptors.request.use((config) => {return config},(e) => Promise.reject(e)
)// axios响应式拦截器
instance.interceptors.response.use((res) => res.data,(e) => {console.log(e)// 统一错误提示ElMessage({type: 'warning',message: e.response.data.message})return Promise.reject(e)}
)export default instance

四、登录 - Pinia管理用户数据

1. 为什么要用Pinia管理数据

由于用户数据的特殊性,在很多组件中都有可能进行共享,共享的数据使用Pinia管理会更加方便

2. 如何使用Pinia管理数据

遵循理念:和数据相关的所有操作(state + action)都放到Pinia中,组件只负责触发action函数

①src/stores/user.js

import { defineStore } from 'pinia'
import { loginAPI } from '@/apis/user'
import { ref } from 'vue'export const useUserStore = defineStore('user',() => {// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.result}// 3. 以对象的形式把state和action returnreturn {userInfo,getUserInfo}},{persist: true}
)

②src/views/Login/index.vue

<script setup>
import { useUserStore } from '@/stores/user.js'const userStore = useUserStore()// ... ...// 带r,调用方法;不带r,获取参数
const router = useRouter()
const doLogin = async () => {// 登录之前,先进行校验。校验成功,发请求;校验失败,自动提示await form.value.validate()const { account, password } = formModel.valueawait userStore.getUserInfo({ account, password })ElMessage.success('登录成功')// 跳转首页router.replace({ path: '/' })
}
</script>

3. Pinia用户数据持久化

持久化用户数据说明

1. 用户数据中有一个关键的数据叫做Token(用来标识当前用户是否登录),而Token持续一段时间才会过期

2. Pinia的存储是基于内存的,刷新就丢失,为了保持登录状态就要做到刷新不丢失,需要配合持久化进行存储。

目的:保持token不丢失,保持登录状态

最终效果:操作state时会自动把用户数据在本地的localStorage也存一份,刷新的时候会从localStorage中先取

快速开始 | pinia-plugin-persistedstate

运行机制:在设置state的时候会自动把数据同步给localstorage,在获取state数据的时候会优先从localstorage中获取。

①安装插件

pnpm i pinia-plugin-persistedstate

②将插件添加到pinia实例上 - main.js

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'import App from './App.vue'
import router from './router'
// 引入初始化样式文件
import '@/styles/common.scss'
// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/direactives'
// 引入全局组件插件
import { componentPlugin } from '@/components/index.js'const app = createApp(App)
const pinia = createPinia()pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(lazyPlugin)
app.use(componentPlugin)app.mount('#app')

③创建Store时,将persist选项设置为true

import { defineStore } from 'pinia'
import { loginAPI } from '@/apis/user'
import { ref } from 'vue'export const useUserStore = defineStore('user',() => {// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.result}// 3. 以对象的形式把state和action returnreturn {userInfo,getUserInfo}},{persist: true}
)

五、登录 - 登录和非登录状态的模板适配

1. 需求理解

src/views/Layout/components/LayoutNav.vue

<script setup>
import { useUserStore } from '@/stores/user.js'
const userStore = useUserStore()
</script><template><nav class="app-topnav"><div class="container"><ul><!-- 多模板渲染 区分登录状态和非登录状态 --><!-- 判断是否有token --><template v-if="userStore.userInfo.token"><li><a href="javascript:;"><i class="iconfont icon-user"></i>{{ userStore.userInfo.nickname || userStore.userInfo.account }}</a></li><li><el-popconfirmtitle="确认退出吗?"confirm-button-text="确认"cancel-button-text="取消"><template #reference><a href="javascript:;">退出登录</a></template></el-popconfirm></li><li><a href="javascript:;">我的订单</a></li><li><a href="javascript:;">会员中心</a></li></template><template v-else><li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li><li><a href="javascript:;">帮助中心</a></li><li><a href="javascript:;">关于我们</a></li></template></ul></div></nav>
</template>

六、登录 - 请求拦截器携带Token

1. 为什么要在请求拦截器携带Token

Token作为用户标识,在很多个接口中都需要携带Token才可以正确获取数据,所以需要在接口调用时携带Token。另外,为了统一控制采取请求拦截器携带的方案。

2. 如何配置

Axios请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token数据会被注入到请求header中,格式按照后端要求的格式进行拼接处理

instance.interceptors.request.use(config => {const userStore = useUserStore()const token = userStore.userInfo.tokenif( token ) {config.headers.Authorization = `Bearer ${token}`}return config
}, e=> Promise.reject(e))

七、登录 - 退出登录功能实现

1. 退出登录业务实现

Popconfirm 气泡确认框 | Element Plus

①新增清除用户信息action - src/stores/user.js

import { defineStore } from 'pinia'
import { loginAPI } from '@/apis/user'
import { ref } from 'vue'export const useUserStore = defineStore('user',() => {// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.result}// 退出登录时清除用户信息const clearUserInfo = () => {userInfo.value = {}}// 3. 以对象的形式把state和action returnreturn {userInfo,getUserInfo,clearUserInfo}},{persist: true}
)

②组件中执行业务逻辑 - src/views/Layout/components/LayoutNav.vue

<script setup>
import { useUserStore } from '@/stores/user.js'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()const confirm = () => {// 清除登录信息userStore.clearUserInfo()// 跳转到登录页router.push('/login')
}
</script><template><nav class="app-topnav"><div class="container"><ul><!-- 多模板渲染 区分登录状态和非登录状态 --><!-- 判断是否有token --><template v-if="userStore.userInfo.token"><li><a href="javascript:;"><i class="iconfont icon-user"></i>{{ userStore.userInfo.nickname || userStore.userInfo.account }}</a></li><li><el-popconfirmtitle="确认退出吗?"confirm-button-text="确认"cancel-button-text="取消"@confirm="confirm"><template #reference><a href="javascript:;">退出登录</a></template></el-popconfirm></li><li><a href="javascript:;">我的订单</a></li><li><a href="javascript:;">会员中心</a></li></template><template v-else><li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li><li><a href="javascript:;">帮助中心</a></li><li><a href="javascript:;">关于我们</a></li></template></ul></div></nav>
</template>

八、登录 - Token失效401拦截

1. 业务背景

Token的有效性可以保持一定时间,如果用户一段时间不做任何操作,Token就会失效,使用失效的Token再去请求一些接口,接口就会报401状态码错误,需要我们做额外处理

两个需要思考的问题:

1. 我们能确定用户到底是在访问哪个接口时出现的401错误吗?在什么位置去拦截这个401?

答:响应拦截器

2. 检测到401之后又该干什么呢?

答:清除掉过期的用户信息,跳转到登录页

解决方案:在axios响应拦截器做统一处理

src/utils/http.js

import axios from 'axios'
import { useUserStore } from '@/stores/user.js'
import router from '@/router'// 创建axios实例
const instance = axios.create({baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',timeout: 5000
})// axios请求拦截器
instance.interceptors.request.use((config) => {// 1. 从pinia获取token数据const userStore = useUserStore()// 2. 按照后端的要求拼接token数据const token = userStore.userInfo.tokenif (token) {config.headers.Authorization = `Bearer ${token}`}return config},(e) => Promise.reject(e)
)// axios响应式拦截器
instance.interceptors.response.use((res) => res.data,(e) => {const userStore = useUserStore()// 统一错误提示ElMessage({type: 'warning',message: e.response.data.message})// 401 token失效处理if (e.response.status === 401) {// 1. 清除本地用户信息userStore.clearUserInfo()// 2. 跳转到登录页(进入到详情页才会)router.push('/login')}return Promise.reject(e)}
)export default instance

九、购物车功能实现

1. 购物车业务逻辑梳理拆解

1. 整个购物车的实现分为两个大分支,本地购物车操作和接口购物车操作

2. 由于购物车数据的特殊性,采取pinia管理购物车列表数据并添加持久化缓存

2. 本地购物车 - 加入购物车实现

Input Number 数字输入框 | Element Plus

①封装购物车模块 - src/stores/cart.js

// 封装购物车模块
import { ref } from 'vue'
import { defineStore } from 'pinia'export const useCartStore = defineStore('cart',() => {// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCartconst addCart = (goods) => {// 添加购物车操作// 思路:通过匹配传递过来的商品对象中是skuId能不能在cartList中找到,找到了就是添加过const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 已添加过,count + 1item.count++} else {// 没有添加过,直接pushcartList.value.push(goods)}}return {cartList,addCart}},{persist: true}
)

②src/views/Detail/index.vue

<script setup>
// ... ... 
import { useCartStore } from '@/stores/cart.js'const cartStore = useCartStore()// sku规格被操作时
let skuObj = {}
const skuChange = (sku) => {console.log(sku)skuObj = sku
}const count = ref(1)
const handleChange = (count) => {console.log(count)
}// 添加购物车
const addCart = () => {if (skuObj.skuId) {// 规格已选择cartStore.addCart({id: goods.value.id,name: goods.value.name,picture: goods.value.mainPictures[0],price: goods.value.price,count: count.value,skuId: skuObj.skuId,attrsText: skuObj.specsText,selected: true})} else {// 规格没有选择,提示用户ElMessage.warning('请选择规格')}
}
</script><template><!-- ... ... --><!-- sku组件 --><XtxSku :goods="goods" @change="skuChange"></XtxSku><!-- 数据组件 --><el-input-numberv-model="count"@change="handleChange":min="1"/><!-- 按钮组件 --><div><el-button @click="addCart" size="large" class="btn">加入购物车</el-button></div>
<!-- ... ... -->
</template>

3. 本地购物车 - 头部购物车列表渲染

①头部购物车组件 - src/views/Layout/components/HeaderCart.vue

<script setup>
import { useCartStore } from '@/stores/cart.js'
const cartStore = useCartStore()
</script><template><div class="cart"><a class="curr" href="javascript:;"><i class="iconfont icon-cart"></i><em>{{ cartStore.cartList.length }}</em></a><div class="layer"><div class="list"><div class="item" v-for="i in cartStore.cartList" :key="i"><RouterLink to=""><img :src="i.picture" alt="" /><div class="center"><p class="name ellipsis-2">{{ i.name }}</p><p class="attr ellipsis">{{ i.attrsText }}</p></div><div class="right"><p class="price">&yen;{{ i.price }}</p><p class="count">x{{ i.count }}</p></div></RouterLink><iclass="iconfont icon-close-new"@click="store.delCart(i.skuId)"></i></div></div><div class="foot"><div class="total"><p>共 10 件商品</p><p>&yen; 100.00</p></div><el-button size="large" type="primary">去购物车结算</el-button></div></div></div>
</template><style scoped lang="scss">
.cart {width: 50px;position: relative;z-index: 600;.curr {height: 32px;line-height: 32px;text-align: center;position: relative;display: block;.icon-cart {font-size: 22px;}em {font-style: normal;position: absolute;right: 0;top: 0;padding: 1px 6px;line-height: 1;background: $helpColor;color: #fff;font-size: 12px;border-radius: 10px;font-family: Arial;}}&:hover {.layer {opacity: 1;transform: none;}}.layer {opacity: 0;transition: all 0.4s 0.2s;transform: translateY(-200px) scale(1, 0);width: 400px;height: 400px;position: absolute;top: 50px;right: 0;box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);background: #fff;border-radius: 4px;padding-top: 10px;&::before {content: '';position: absolute;right: 14px;top: -10px;width: 20px;height: 20px;background: #fff;transform: scale(0.6, 1) rotate(45deg);box-shadow: -3px -3px 5px rgba(0, 0, 0, 0.1);}.foot {position: absolute;left: 0;bottom: 0;height: 70px;width: 100%;padding: 10px;display: flex;justify-content: space-between;background: #f8f8f8;align-items: center;.total {padding-left: 10px;color: #999;p {&:last-child {font-size: 18px;color: $priceColor;}}}}}.list {height: 310px;overflow: auto;padding: 0 10px;&::-webkit-scrollbar {width: 10px;height: 10px;}&::-webkit-scrollbar-track {background: #f8f8f8;border-radius: 2px;}&::-webkit-scrollbar-thumb {background: #eee;border-radius: 10px;}&::-webkit-scrollbar-thumb:hover {background: #ccc;}.item {border-bottom: 1px solid #f5f5f5;padding: 10px 0;position: relative;i {position: absolute;bottom: 38px;right: 0;opacity: 0;color: #666;transition: all 0.5s;}&:hover {i {opacity: 1;cursor: pointer;}}a {display: flex;align-items: center;img {height: 80px;width: 80px;}.center {padding: 0 10px;width: 200px;.name {font-size: 16px;}.attr {color: #999;padding-top: 5px;}}.right {width: 100px;padding-right: 20px;text-align: center;.price {font-size: 16px;color: $priceColor;}.count {color: #999;margin-top: 5px;font-size: 16px;}}}}}
}
</style>

②导入渲染 - src/views/Layout/components/LayoutHeader.vue

<script setup>
import { useCategoryStore } from '@/stores/category.js'
import HeaderCart from './HeaderCart.vue'// 使用pinia中的数据
const categoryStore = useCategoryStore()
</script><template><header class="app-header"><div class="container"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><ul class="app-header-nav"><liclass="home"v-for="item in categoryStore.categoryList":key="item.id"><RouterLink active-class="active" :to="`/category/${item.id}`">{{item.name}}</RouterLink></li></ul><div class="search"><i class="iconfont icon-search"></i><input type="text" placeholder="搜一搜" /></div><!-- 头部购物车 --><HeaderCart></HeaderCart></div></header>
</template>

4. 本地购物车 - 头部购物车删除实现

①src/stores/cart.js

// 封装购物车模块
import { ref } from 'vue'
import { defineStore } from 'pinia'export const useCartStore = defineStore('cart',() => {// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCart// 添加购物车const addCart = (goods) => {// 添加购物车操作// 思路:通过匹配传递过来的商品对象中是skuId能不能在cartList中找到,找到了就是添加过const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 已添加过,count + 1item.count++} else {// 没有添加过,直接pushcartList.value.push(goods)}}// 删除购物车const delCart = (skuId) => {// 思路:1. 找到要删除的下标值 - splice//       2. 使用组件的过滤方法 - filterconst idx = cartList.value.findIndex((item) => skuId === item.skuId)cartList.value.splice(idx, 1)}return {cartList,addCart,delCart}},{persist: true}
)

②src/views/Layout/components/HeaderCart.vue

<script setup>
import { useCartStore } from '@/stores/cart.js'
const cartStore = useCartStore()
</script><template><div class="cart"><a class="curr" href="javascript:;"><i class="iconfont icon-cart"></i><em v-if="cartStore.cartList.length">{{ cartStore.cartList.length }}</em></a><div class="layer"><div class="list"><div class="item" v-for="i in cartStore.cartList" :key="i"><RouterLink to=""><img :src="i.picture" alt="" /><div class="center"><p class="name ellipsis-2">{{ i.name }}</p><p class="attr ellipsis">{{ i.attrsText }}</p></div><div class="right"><p class="price">&yen;{{ i.price }}</p><p class="count">x{{ i.count }}</p></div></RouterLink><iclass="iconfont icon-close-new"@click="cartStore.delCart(i.skuId)"></i></div></div><div class="foot"><div class="total"><p>共 10 件商品</p><p>&yen; 100.00</p></div><el-button@click="$router.push('/cartlist')"size="large"type="primary">去购物车结算</el-button></div></div></div>
</template>

5. 本地购物车 - 头部购物车统计计算

实现思路:计算属性

计算逻辑是什么:

  • 1. 商品总数计算逻辑:商品列表中的所有商品count累加之和
  • 2. 商品总价钱计算逻辑:商品列表中的所有商品的count * price累加之和

①src/stores/cart.js

// 封装购物车模块
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'export const useCartStore = defineStore('cart',() => {// ... ...// 计算属性// 1. 总的数量 所有项的count之和const allCount = computed(() =>cartList.value.reduce((sum, item) => sum + item.count, 0))// 2. 总价 所有项的count * price之和const allPrice = computed(() =>cartList.value.reduce((sum, item) => sum + item.count * item.price, 0))return {cartList,addCart,delCart,allCount,allPrice}},{persist: true}
)

②src/views/Layout/components/HeaderCart.vue

      <div class="foot"><div class="total"><p>共 {{ cartStore.allCount }} 件商品</p><p>&yen; {{ cartStore.allPrice.toFixed(2) }}</p></div><el-button@click="$router.push('/cartlist')"size="large"type="primary">去购物车结算</el-button></div>

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

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

相关文章

微信小程序教程DAY3

box标签 第二种方法 绿色第一种 第一种更好 效果一样 完成这个项目 先写循环

Python深度学习基于Tensorflow(13)目标检测实战

文章目录 RPN 整体代码RPN 具体实现过程数据标注读取标注数据固定图片大小调整目标框使用预训练模型获取 feature_shape定义 RPN 网络生成RPN 的 CLS 和 REG 数据集获取所有的锚点计算锚点与目标框的IOU 定义 RPN loss 和 训练过程 参考资料 这里实现的是二阶段目标检测&#x…

十分钟快速搭建检索、排序的大模型RAG系统

以上为实现效果 RAG是目前最火的大模型应用之一&#xff0c;如何能快速实现一个不错的demo呢&#xff1f; 参考 https://github.com/LongxingTan/open-retrievalshttps://colab.research.google.com/drive/1fJC-8er-a4NRkdJkwWr4On7lGt9rAO4P?uspsharing#scrollTo2Hrfp96UY…

第二届“天洑杯”全国高校数据建模大赛圆满收官

近日&#xff0c;第二届“天洑杯”全国高校数据建模大赛在江苏省无锡市第七届智能优化与调度学术会议现场圆满收官。在为期四周的线上赛中&#xff0c;共有来自全国 71 所高校及企业的 117 支队伍参与角逐&#xff0c;共10支队伍进入决赛。 本届大赛评审组由西安电子科技大学教…

鸿蒙开发接口媒体:【@ohos.multimedia.camera (相机管理)】

相机管理 说明&#xff1a; 开发前请熟悉鸿蒙开发指导文档&#xff1a; gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 本模块首批接口从API version 9开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块…

低边驱动与高边驱动

一.高边驱动和低边驱动 低边驱动(LSD): 在电路的接地端加了一个可控开关&#xff0c;低边驱动就是通过闭合地线来控制这个开关的开关。容易实现&#xff08;电路也比较简单&#xff0c;一般由MOS管加几个电阻、电容&#xff09;、适用电路简化和成本控制的情况。 高边驱动&am…

Qt 窗口

在Qt Creator 中创建项目的时候&#xff0c;我们能够选择创建QMainWindow 还是 QWidget 两种窗口。 二者有什么区别呢&#xff1f;其中 QMainWindow 是一种主窗口&#xff0c;包含菜单栏&#xff0c;工具栏&#xff0c;状态栏&#xff0c;中心窗口和浮动窗口等多个窗口组合&…

位置参数

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 位置参数也称必备参数&#xff0c;是必须按照正确的顺序传到函数中&#xff0c;即调用时的数量和位置必须和定义时是一样的。 &#xff08;1&#x…

stack和queue(1)

一、stack的简单介绍和使用 1.1 stack的介绍 1.stack是一种容器适配器&#xff0c;专门用在具有先进后出&#xff0c;后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入和弹出操作。 2.stack是作为容器适配器被实现的&#xff0c;容器适配器即是…

信号与槽函数的魔法:QT 5编程中的核心机制

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、信号与槽函数的基本概念 二、信号与槽函数的实现原理 三、信号与槽函数的代码实例 四…

搭载算能 BM1684 芯片,面向AI推理计算加速卡

搭载算能 BM1684 芯片&#xff0c;是面向AI推理的算力卡。可集成于服务器、工控机中&#xff0c;高效适配市场上所有AI算法&#xff0c;实现视频结构化、人脸识别、行为分析、状态监测等应用&#xff0c;为智慧城市、智慧交通、智慧能源、智慧金融、智慧电信、智慧工业等领域进…

实用软件分享---- i茅台 在windows上自动预约和自动获取小茅运的软件

专栏介绍:本专栏主要分享一些实用的软件(Po Jie版); 声明1:软件不保证时效性;只能保证在写本文时,该软件是可用的;不保证后续时间该软件能一直正常运行;不保证没有bug;如果软件不可用了,我知道后会第一时间在题目上注明(已失效)。介意者请勿订阅。 声明2:本专栏的…

计算机基础学习路线

计算机基础学习路线 整理自学计算机基础的过程&#xff0c;虽学习内容众多&#xff0c;然始终相信世上无难事&#xff0c;只怕有心人&#xff0c;期间也遇到许多志同道合的同学&#xff0c;现在也分享自己的学习过程来帮助有需要的。 一、数据结构与算法 视频方面我看的是青…

C++_list简单源码剖析:list模拟实现

文章目录 &#x1f680;1. ListNode模板&#x1f680;2. List_iterator模板(重要)&#x1f331;2.1 List_iterator的构造函数&#x1f331;2.2 List_iterator的关于ListNode的行为 &#x1f680;3. Reverse_list_iterator模板(拓展)&#x1f680;4. List模板(核心)&#x1f331…

【计算机毕设】基于SpringBoot的房产销售系统设计与实现 - 源码免费(私信领取)

免费领取源码 &#xff5c; 项目完整可运行 &#xff5c; v&#xff1a;chengn7890 诚招源码校园代理&#xff01; 1. 研究目的 随着房地产市场的发展和互联网技术的进步&#xff0c;传统的房产销售模式逐渐向线上转移。设计并实现一个基于Spring Boot的房产销售系统&#xff0…

SpringCloud学习笔记(一)

SpringCloud、SpringCloud Alibaba 前置知识&#xff1a; 核心新组件&#xff1a; 所用版本&#xff1a; 学习方法&#xff1a; 1.看理论&#xff1a;官网 2.看源码&#xff1a;github 一、微服务理论知识 二、关于SpringCloud各种组件的停更/升级/替换 主业务逻辑是&#x…

尝试用智谱机器人+知识库,制作pytorch测试用例生成器

尝试用智谱机器人知识库,制作pytorch测试用例生成器 1 保存pytorch算子文档到txt2 创建知识库3 创建聊天机器人4 测试效果5 分享 背景:是否能将API的接口文档和sample放到RAG知识库,让LLM编写API相关的程序呢 小结:当前的实验效果并不理想,可以生成代码,但几乎都存在BUG 1 保存…

星闪在智能汽车端的应用

随着智能汽车、智能终端、智能家居和智能制造等多产业的快速发展&#xff0c;多应用领域对无线短距通信技术在低延时、高可靠、低功耗等方面提出共性要求&#xff0c;现有主流无线短距通信技术的先天局限和技术潜力无法满足新应用的技术要求&#xff0c;针对解决行业技术痛点的…

StrApi基本使用

1.创建项目(这里只使用默认的sqllite) 点击链接进入官网查看先决条件,看看自己的node,python等是否符合版本要求 运行以下命令进行创建项目(网慢导致下载失败的话可以尝试使用手机热点给电脑使用,我就是这样解决的,也可以看我csdn的资源这里进行下载) yarn create strapi-ap…

5.25.1 用于组织病理学图像分类的深度注意力特征学习

提出了一种基于深度学习的组织病理学图像分类新方法。我们的方法建立在标准卷积神经网络 (CNN) 的基础上,并结合了两个独立的注意力模块,以实现更有效的特征学习。 具体而言,注意力模块沿不同维度推断注意力图,这有助于将 CNN 聚焦于关键图像区域,并突出显示判别性特征通…