day-118-one-hundred-and-eighteen-20230722-vue3项目实战-收尾
vue3项目实战-收尾
对数据统一做处理
- src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作
class Http {
setInterceptor(instance) {
instance.interceptors.response.use(
(res) => {
if (res.data.resultCode == 200) {
return res.data.data
}
return res.data // {resultCode:200,data:{}}
},
(err) => {
return Promise.reject(err)
}
)
}
}
export default new Http()
- src/api/index.js
import http from '@/utils/http.js'
const API_LIST = {
queryIndexInfos: '/index-infos' // 首页的获取数据接口,都在这里
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
return http.get(API_LIST.queryIndexInfos)
}
- src/views/Home.vue
<script setup>
import { queryIndexInfos } from '../api/index'
const carousels = ref([])
const hotGoodses = ref([])
const newGoodses = ref([])
const recommendGoodses = ref([])
const loading = ref(true)
onMounted(async () => {
let data = await queryIndexInfos()
carousels.value = data.carousels // 轮播图的数据获取成功
hotGoodses.value = data.hotGoodses
newGoodses.value = data.newGoodses
recommendGoodses.value = data.recommendGoodses
loading.value = false
console.log(hotGoodses.value[0])
})
</script>
后退按钮
- src/components/TopBar.vue
<template>
<van-nav-bar left-arrow @click-left="router.back()">
<template #title>
<slot name="title">
{{ title || $route.meta.title }}
</slot>
</template>
<template #right>
<slot name="right"></slot>
</template>
</van-nav-bar>
</template>
<script setup>
defineProps(['title'])
const router = useRouter()
</script>
表单事件初步
- src/views/Login.vue
<template>
<div class="login">
<TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
<img :src="logo" class="logo" />
<van-form>
<van-field
label="手机号"
name="loginName"
v-model="state.loginName"
:rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
/>
<van-field
label="密码"
type="password"
name="password"
v-model="state.password"
:rules="[{ required: true, message: '密码不能为空' }]"
/>
<van-field label="验证码" name="captcha" v-model="state.captcha">
<template #button> 验证码 </template>
</van-field>
<a class="text" @click="changeLogin">
{{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
</a>
<div style="margin: 16px">
<van-button round block color="#1baeae"> 确认提交 </van-button>
</div>
</van-form>
</div>
</template>
<script setup>
import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
import { reactive } from 'vue'
const isLogin = ref(false) // 这个状态用来控制当前是哪一个页面
function changeLogin() {
isLogin.value = !isLogin.value
}
// 对用户的数据进行维护
const state = reactive({
loginName: '', // 用户名
password: '', // 密码
captcha: '' // 验证码
})
</script>
<style scoped lang="less">
.login {
padding: 0 20px;
.logo {
display: block;
margin: 20px auto 20px;
width: 140px;
height: 140px;
}
}
.text {
display: inline-block;
margin-top: 20px;
margin-bottom: 20px;
padding: 0 15px;
color: #1989fa;
font-size: 14px;
}
</style>
验证码组件封装
- src/components/Captcha.vue
<template>
<canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas>
</template>
<script setup>
const width = 120
const height = 30
function getRandom() {
let str = 'abcdefgghijklmnopqrstuvwxyz'
str += str.toUpperCase()
str += '0123456789'
let idx = 0
let status = ''
while (idx < 4) {
status += str[Math.round(Math.random() * 61)]
idx++
}
return status
}
const canvas = ref(null)
const emit = defineEmits(['update:text'])
function generateCaptcha() {
const context = canvas.value.getContext('2d')
context.clearRect(0, 0, width, height)
context.fillStyle = '#00f'
context.font = 'italic 30px 黑体'
context.textBaseline = 'top'
const text = getRandom()
context.fillText(text, 10, 0)
for (let i = 0; i < 4; i++) {
context.beginPath()
context.moveTo(Math.random() * width, Math.random() * height)
context.lineTo(Math.random() * width, Math.random() * height)
context.strokeStyle = '#005588'
context.stroke()
}
emit('update:text', text)
}
defineExpose({
reset: generateCaptcha
})
onMounted(() => {
generateCaptcha()
})
</script>
验证码使用及校验
- src/components/Captcha.vue
<template>
<canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas>
</template>
<script setup>
const width = 120
const height = 30
function getRandom() {
let str = 'abcdefgghijklmnopqrstuvwxyz'
str += str.toUpperCase()
str += '0123456789'
let idx = 0
let status = ''
while (idx < 4) {
status += str[Math.round(Math.random() * 61)]
idx++
}
return status
}
const canvas = ref(null)
const emit = defineEmits(['update:text'])
function generateCaptcha() {
const context = canvas.value.getContext('2d')
context.clearRect(0, 0, width, height)
context.fillStyle = '#00f'
context.font = 'italic 30px 黑体'
context.textBaseline = 'top'
const text = getRandom()
context.fillText(text, 10, 0)
for (let i = 0; i < 4; i++) {
context.beginPath()
context.moveTo(Math.random() * width, Math.random() * height)
context.lineTo(Math.random() * width, Math.random() * height)
context.strokeStyle = '#005588'
context.stroke()
}
emit('update:text', text)
}
defineExpose({
reset: generateCaptcha
})
onMounted(() => {
generateCaptcha()
})
</script>
- src/views/Login.vue
<template>
<div class="login">
<TopBar :title="isLogin ? '登录' : '注册'"></TopBar>
<img :src="logo" class="logo" />
<van-form>
<van-field
label="手机号"
name="loginName"
v-model="state.loginName"
:rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]"
/>
<van-field
label="密码"
type="password"
name="password"
v-model="state.password"
:rules="[{ required: true, message: '密码不能为空' }]"
/>
<van-field
label="验证码"
name="captcha"
v-model="state.captcha"
:rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]"
>
<template #button>
<Captcha v-model:text="captchaImgText" ref="captcha"></Captcha>
</template>
</van-field>
<a class="text" @click="changeLogin">
{{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }}
</a>
<div style="margin: 16px">
<van-button round block color="#1baeae"> 确认提交 </van-button>
</div>
</van-form>
{{ captchaImgText }}
</div>
</template>
<script setup>
import logo from '@/assets/images/newbee-mall-vue3-app-logo.png'
import { reactive } from 'vue'
const isLogin = ref(false) // 这个状态用来控制当前是哪一个页面
function changeLogin() {
isLogin.value = !isLogin.value
}
// 对用户的数据进行维护
const state = reactive({
loginName: '', // 用户名
password: '', // 密码
captcha: '' // 验证码
})
const captchaImgText = ref('')
const captcha = ref(null)
function validatorCaptcha() {
let isSame = state.captcha === captchaImgText.value // 如果相同
if (!isSame) {
captcha.value.reset()
return false
}
return true
}
</script>
<style scoped lang="less">
.login {
padding: 0 20px;
.logo {
display: block;
margin: 20px auto 20px;
width: 140px;
height: 140px;
}
}
.text {
display: inline-block;
margin-top: 20px;
margin-bottom: 20px;
padding: 0 15px;
color: #1989fa;
font-size: 14px;
}
</style>
md5摘要算法
pnpm i blueimp-md5
- md5特点?
- 不是加密算法,摘要算法。
- 内容不同摘要的结果不同
- 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
- 生成长度一致
- 相同的内容生成的结果一直
- 不可逆
- src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
userLogin: '/user/login',
userRegister: '/user/register'
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
password = md5(password)
return http.post(API_LIST.userLogin, {
loginName,
passwordMd5: password
})
}
// 用户注册
export const userRegister = (loginName, password) => {
return http.post(API_LIST.userRegister, {
loginName,
password
})
}
注册登录操作
-
核心操作:
-
src/utils/http.js
import axios from 'axios' // 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作 class Http { setInterceptor(instance) { instance.interceptors.response.use( (res) => { if (res.data.resultCode == 200) { // 对返回值的状态码是200的情况统一处理 return res.data.data } if (res.data.resultCode === 500) { return Promise.reject(res.data) } return res.data // {resultCode:200,data:{}} }, (err) => { return Promise.reject(err) } ) } } export default new Http()
-
src/api/index.js
import http from '@/utils/http.js' import md5 from 'blueimp-md5' const API_LIST = { userLogin: '/user/login', userRegister: '/user/register' } // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应 // 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆 export const userLogin = (loginName, password) => { password = md5(password) return http.post(API_LIST.userLogin, { loginName, passwordMd5: password }) } // 用户注册 export const userRegister = (loginName, password) => { return http.post(API_LIST.userRegister, { loginName, password }) }
-
src/views/Login.vue
<template> <div class="login"> <TopBar :title="isLogin ? '登录' : '注册'"></TopBar> <van-form @submit="submit"> <van-field label="手机号" name="loginName" v-model="state.loginName" :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]" /> <van-field label="密码" type="password" name="password" v-model="state.password" :rules="[{ required: true, message: '密码不能为空' }]" /> <van-field label="验证码" name="captcha" v-model="state.captcha" :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]" > <template #button> <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha> </template> </van-field> <a class="text" @click="changeLogin"> {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }} </a> <div style="margin: 16px"> <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button> </div> </van-form> {{ captchaImgText }} </div> </template> <script setup> import { getCurrentInstance, reactive } from 'vue' const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面 function changeLogin() { isLogin.value = !isLogin.value state.loginName = '' state.password = '' state.captcha = '' captcha.value.reset() } // 对用户的数据进行维护 const state = reactive({ loginName: '', // 用户名 password: '', // 密码 captcha: '' // 验证码 }) const captchaImgText = ref('') const captcha = ref(null) function validatorCaptcha() { let isSame = state.captcha === captchaImgText.value // 如果相同 if (!isSame) { captcha.value.reset() return false } return true } // 提交操作 import { userLogin, userRegister } from '@/api' const { proxy } = getCurrentInstance() const router = useRouter() async function submit() { if (isLogin.value) { // 如果是200 我们直接拿到的就是token try { let token = await userLogin(state.loginName, state.password) proxy.$showToast('登录成功') localStorage.setItem('token', token) router.push('/') } catch (e) { proxy.$showToast(e.message) } } else { try { await userRegister(state.loginName, state.password) proxy.$showToast('注册成功') changeLogin() // 注册成功跳转到登录 } catch (e) { proxy.$showToast(e.message) } } } </script>
-
-
代码示例
-
src/utils/http.js
import axios from 'axios' // 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作 class Http { constructor() { // 根据环境变量设置请求的路径 this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/' this.timeout = 5000 } setInterceptor(instance) { instance.interceptors.request.use( (config) => { // 携带token来做处理 return config }, (err) => { return Promise.reject(err) } ) instance.interceptors.response.use( (res) => { if (res.data.resultCode == 200) { // 对返回值的状态码是200的情况统一处理 return res.data.data } if (res.data.resultCode === 500) { return Promise.reject(res.data) } return res.data // {resultCode:200,data:{}} }, (err) => { return Promise.reject(err) } ) } request(options) { // 请求会实现拦截器 const instance = axios.create() // 1.每次请求要创建一个新的实例 this.setInterceptor(instance) // 2.设置拦截器 // 发送请求参数 return instance({ ...options, baseURL: this.baseURL, timeout: this.timeout }) } get(url, data) { return this.request({ method: 'get', url, params: data }) } post(url, data) { return this.request({ method: 'post', url, data }) } } export default new Http()
-
src/api/index.js
import http from '@/utils/http.js' import md5 from 'blueimp-md5' const API_LIST = { queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里 userLogin: '/user/login', userRegister: '/user/register' } // 首页数据的获取,直接通过 api 这个文件来操作 export function queryIndexInfos() { return http.get(API_LIST.queryIndexInfos) } // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应 // 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆 export const userLogin = (loginName, password) => { password = md5(password) return http.post(API_LIST.userLogin, { loginName, passwordMd5: password }) } // 用户注册 export const userRegister = (loginName, password) => { return http.post(API_LIST.userRegister, { loginName, password }) }
-
src/components/Captcha.vue
<template> <canvas :width="width" :height="height" ref="canvas" @click="generateCaptcha"></canvas> </template> <script setup> const width = 120 const height = 30 function getRandom() { let str = 'abcdefgghijklmnopqrstuvwxyz' str += str.toUpperCase() str += '0123456789' let idx = 0 let status = '' while (idx < 4) { status += str[Math.round(Math.random() * 61)] idx++ } return status } const canvas = ref(null) const emit = defineEmits(['update:text']) function generateCaptcha() { const context = canvas.value.getContext('2d') context.clearRect(0, 0, width, height) context.fillStyle = '#00f' context.font = 'italic 30px 黑体' context.textBaseline = 'top' const text = getRandom() context.fillText(text, 10, 0) for (let i = 0; i < 4; i++) { context.beginPath() context.moveTo(Math.random() * width, Math.random() * height) context.lineTo(Math.random() * width, Math.random() * height) context.strokeStyle = '#005588' context.stroke() } emit('update:text', text) } defineExpose({ reset: generateCaptcha }) onMounted(() => { generateCaptcha() }) </script>
-
src/views/Login.vue
<template> <div class="login"> <TopBar :title="isLogin ? '登录' : '注册'"></TopBar> <img :src="logo" class="logo" /> <van-form @submit="submit"> <van-field label="手机号" name="loginName" v-model="state.loginName" :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]" /> <van-field label="密码" type="password" name="password" v-model="state.password" :rules="[{ required: true, message: '密码不能为空' }]" /> <van-field label="验证码" name="captcha" v-model="state.captcha" :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]" > <template #button> <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha> </template> </van-field> <a class="text" @click="changeLogin"> {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }} </a> <div style="margin: 16px"> <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button> </div> </van-form> {{ captchaImgText }} </div> </template> <script setup> import logo from '@/assets/images/newbee-mall-vue3-app-logo.png' import { getCurrentInstance, reactive } from 'vue' const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面 function changeLogin() { isLogin.value = !isLogin.value state.loginName = '' state.password = '' state.captcha = '' captcha.value.reset() } // 对用户的数据进行维护 const state = reactive({ loginName: '', // 用户名 password: '', // 密码 captcha: '' // 验证码 }) const captchaImgText = ref('') const captcha = ref(null) function validatorCaptcha() { let isSame = state.captcha === captchaImgText.value // 如果相同 if (!isSame) { captcha.value.reset() return false } return true } // 提交操作 import { userLogin, userRegister } from '@/api' const { proxy } = getCurrentInstance() const router = useRouter() async function submit() { if (isLogin.value) { // 如果是200 我们直接拿到的就是token try { let token = await userLogin(state.loginName, state.password) proxy.$showToast('登录成功') localStorage.setItem('token', token) router.push('/') } catch (e) { proxy.$showToast(e.message) } } else { try { await userRegister(state.loginName, state.password) proxy.$showToast('注册成功') changeLogin() // 注册成功跳转到登录 } catch (e) { proxy.$showToast(e.message) } } } </script> <style scoped lang="less"> .login { padding: 0 20px; .logo { display: block; margin: 20px auto 20px; width: 140px; height: 140px; } } .text { display: inline-block; margin-top: 20px; margin-bottom: 20px; padding: 0 15px; color: #1989fa; font-size: 14px; } </style>
-
登录态校验
-
代码示例
-
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', redirect: '/home' }, { path: '/home', name: 'home', component: Home, meta: { title: '首页' } }, { path: '/category', meta: { title: '分类' }, name: 'category', component: () => import('../views/Category.vue') }, { path: '/cart', name: 'cart', component: () => import('../views/Cart.vue'), meta: { needLogin: true // 需要登录才能访问的 我们需要配置这个属性 } }, { path: '/user', name: 'user', component: () => import('../views/User.vue') }, { path: '/login', name: 'login', component: () => import('../views/Login.vue') } ] }) router.beforeEach(async (to, from) => { let token = localStorage.getItem('token') if (token) { // 如果有token 说明登录成功,但是如果你访问的还是登录 if (to.name === 'login') { return { path: '/' } } } else { // 没有登录,跳转到登录页面 // /a/b -> ['/a','/a/b'] if (to.matched.some((item) => item.meta.needLogin)) { // 此路由需要登录但是没有登录, 应该跳转到登录页面 return { path: '/login', query: { redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来 ...to.query // 当前页面的其他参数也添加进去 } } } } }) export default router
-
src/views/Login.vue
<template> <div class="login"> <TopBar :title="isLogin ? '登录' : '注册'"></TopBar> <img :src="logo" class="logo" /> <van-form @submit="submit"> <van-field label="手机号" name="loginName" v-model="state.loginName" :rules="[{ pattern: /^1\d{10}/, message: '请输入正确手机号' }]" /> <van-field label="密码" type="password" name="password" v-model="state.password" :rules="[{ required: true, message: '密码不能为空' }]" /> <van-field label="验证码" name="captcha" v-model="state.captcha" :rules="[{ validator: validatorCaptcha, message: '验证码不正确' }]" > <template #button> <Captcha v-model:text="captchaImgText" ref="captcha"></Captcha> </template> </van-field> <a class="text" @click="changeLogin"> {{ isLogin ? '还没账号,请点此注册' : '已有账号,请点此登录' }} </a> <div style="margin: 16px"> <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button> </div> </van-form> {{ captchaImgText }} </div> </template> <script setup> import logo from '@/assets/images/newbee-mall-vue3-app-logo.png' import { getCurrentInstance, reactive } from 'vue' const isLogin = ref(true) // 这个状态用来控制当前是哪一个页面 function changeLogin() { isLogin.value = !isLogin.value state.loginName = '' state.password = '' state.captcha = '' captcha.value.reset() } // 对用户的数据进行维护 const state = reactive({ loginName: '', // 用户名 password: '', // 密码 captcha: '' // 验证码 }) const captchaImgText = ref('') const captcha = ref(null) function validatorCaptcha() { let isSame = state.captcha === captchaImgText.value // 如果相同 if (!isSame) { captcha.value.reset() return false } return true } // 提交操作 import { userLogin, userRegister } from '@/api' const { proxy } = getCurrentInstance() const router = useRouter() const route = useRoute() async function submit() { if (isLogin.value) { // 如果是200 我们直接拿到的就是token try { let token = await userLogin(state.loginName, state.password) proxy.$showToast('登录成功') localStorage.setItem('token', token) // 从别的页面跳转过来,会携带跳回的路径,和其他的查询参数 let { redirect, ...query } = route.query redirect ? router.push({ path: redirect, query }) : router.push('/') } catch (e) { proxy.$showToast(e.message) } } else { try { await userRegister(state.loginName, state.password) proxy.$showToast('注册成功') changeLogin() // 注册成功跳转到登录 } catch (e) { proxy.$showToast(e.message) } } } </script> <style scoped lang="less"> .login { padding: 0 20px; .logo { display: block; margin: 20px auto 20px; width: 140px; height: 140px; } } .text { display: inline-block; margin-top: 20px; margin-bottom: 20px; padding: 0 15px; color: #1989fa; font-size: 14px; } </style>
-
-
核心代码
-
src/router/index.js
const router = createRouter({ routes: [ { path: '/cart', name: 'cart', component: () => import('../views/Cart.vue'), meta: { needLogin: true // 需要登录才能访问的 我们需要配置这个属性 } }, { path: '/login', name: 'login', component: () => import('../views/Login.vue') } ] }) router.beforeEach(async (to, from) => { let token = localStorage.getItem('token') if (token) { // 如果有token 说明登录成功,但是如果你访问的还是登录 if (to.name === 'login') { return { path: '/' } } } else { // 没有登录,跳转到登录页面 // /a/b -> ['/a','/a/b'] if (to.matched.some((item) => item.meta.needLogin)) { // 此路由需要登录但是没有登录, 应该跳转到登录页面 return { path: '/login', query: { redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来 ...to.query // 当前页面的其他参数也添加进去 } } } } }) export default router
-
src/views/Login.vue
<template> <div class="login"> <van-form @submit="submit"> <div style="margin: 16px"> <van-button round block color="#1baeae" native-type="submit"> 确认提交 </van-button> </div> </van-form> </div> </template> <script setup> // 提交操作 import { userLogin, userRegister } from '@/api' const { proxy } = getCurrentInstance() const router = useRouter() const route = useRoute() async function submit() { if (isLogin.value) { // 如果是200 我们直接拿到的就是token try { let token = await userLogin(state.loginName, state.password) proxy.$showToast('登录成功') localStorage.setItem('token', token) // 从别的页面跳转过来,会携带跳回的路径,和其他的查询参数 let { redirect, ...query } = route.query redirect ? router.push({ path: redirect, query }) : router.push('/') } catch (e) { proxy.$showToast(e.message) } } else { //... } } </script>
-
使用pinia的集成
-
核心代码:
-
src/api/index.js
import http from '@/utils/http.js' import md5 from 'blueimp-md5' const API_LIST = { queryCategories: '/categories' // 查询分类的路径 } // 查询所有的分类 export function queryCategories() { return http.get(API_LIST.queryCategories) }
-
src/stores/category.js
import { queryCategories } from '@/api' export const useCategoryStore = defineStore('category', () => { const list = ref([]) // 默认所有分类的信息 async function getCategories() { list.value = await queryCategories() } return { list, getCategories } })
-
src/views/Category.vue
<script setup> import classify from '@/assets/images/classify.png' import { useCategoryStore } from '../stores/category' import { onMounted } from 'vue' const store = useCategoryStore() // 通过storeToRefs 将store中的所有属性,转换成ref不包过方法 // toRefs 在转化的时候会将函数也转成ref const { list } = storeToRefs(store) onMounted(async () => { // 没有数据我要获取数据 if (list.value.length == 0) { await store.getCategories() } console.log(`list.value-->`, list.value) }) </script>
-
-
代码示例:
-
src/api/index.js
import http from '@/utils/http.js' import md5 from 'blueimp-md5' const API_LIST = { queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里 userLogin: '/user/login', userRegister: '/user/register', queryCategories: '/categories' // 查询分类的路径 } // 首页数据的获取,直接通过 api 这个文件来操作 export function queryIndexInfos() { return http.get(API_LIST.queryIndexInfos) } // md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应 // 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆 export const userLogin = (loginName, password) => { password = md5(password) return http.post(API_LIST.userLogin, { loginName, passwordMd5: password }) } // 用户注册 export const userRegister = (loginName, password) => { return http.post(API_LIST.userRegister, { loginName, password }) } // 查询所有的分类 export function queryCategories() { return http.get(API_LIST.queryCategories) }
-
src/stores/category.js
import { queryCategories } from '@/api' export const useCategoryStore = defineStore('category', () => { const list = ref([]) // 默认所有分类的信息 async function getCategories() { list.value = await queryCategories() } return { list, getCategories } })
-
src/views/Category.vue
<script setup> import classify from '@/assets/images/classify.png' import { useCategoryStore } from '../stores/category' import { onMounted } from 'vue' const store = useCategoryStore() // 通过storeToRefs 将store中的所有属性,转换成ref不包过方法 // toRefs 在转化的时候会将函数也转成ref const { list } = storeToRefs(store) onMounted(async () => { // 没有数据我要获取数据 if (list.value.length == 0) { await store.getCategories() } console.log(`list.value-->`, list.value) }) </script>
-
多层级渲染
- src/views/Category.vue
<script setup>
import classify from '@/assets/images/classify.png'
import { useCategoryStore } from '../stores/category'
import { computed, onMounted } from 'vue'
const store = useCategoryStore()
// 通过storeToRefs 将store中的所有属性,转换成ref不包过方法
// toRefs 在转化的时候会将函数也转成ref
const { list } = storeToRefs(store)
onMounted(async () => {
// 没有数据我要获取数据
if (list.value.length == 0) {
await store.getCategories()
}
})
const activeIndex = ref(5)
const contentArray = computed(() => {
return list.value[activeIndex.value]?.secondLevelCategoryVOS
})
</script>
<template>
<TopBar>
<template #title>
<van-search placeholder="搜索需要的产品" @click="$router.push('/search')"></van-search>
</template>
</TopBar>
<div class="main">
<van-sidebar v-model="activeIndex">
<van-sidebar-item
:title="category.categoryName"
v-for="category in list"
:key="category.categoryName"
/>
</van-sidebar>
<div class="content" v-if="contentArray && contentArray.length > 0">
<div v-for="content in contentArray" :key="content.categoryId">
<h3 class="title">{{ content.categoryName }}</h3>
<div class="list">
<router-link
:to="{
path: '/search',
query: {
categoryId: item.categoryId
}
}"
v-for="item in content.thirdLevelCategoryVOS"
:key="item.categoryId"
>
<van-image :src="classify" />
<span>{{ item.categoryName }}</span>
</router-link>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.van-search {
:deep(.van-search__content) {
border-radius: 20px;
}
}
.main {
height: calc(100vh - 96px);
display: flex;
}
.van-sidebar {
width: 120px;
height: 100%;
background-color: #f7f8fa;
}
.van-sidebar-item {
padding: 15px 10px;
}
.content {
width: 255px;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
.title {
padding-left: 15px;
font-size: 16px;
line-height: 40px;
margin-bottom: 15px;
}
.list {
display: flex;
flex-wrap: wrap;
padding: 0 10px;
a {
margin-right: 2%;
margin-bottom: 10px;
width: 32%;
.van-image {
display: block;
margin: 0 auto;
width: 30px;
height: 30px;
}
span {
display: block;
text-align: center;
line-height: 30px;
color: #555;
}
&:nth-child(3n) {
margin-right: 0;
}
}
}
}
</style>
新增一个跳转详情页
- src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作
class Http {
constructor() {
// 根据环境变量设置请求的路径
this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
this.timeout = 5000
}
setInterceptor(instance) {
instance.interceptors.request.use(
(config) => {
// 携带token来做处理
let token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
},
(err) => {
return Promise.reject(err)
}
)
instance.interceptors.response.use(
(res) => {
if (res.data.resultCode == 200) {
// 对返回值的状态码是200的情况统一处理
return res.data.data
}
if (res.data.resultCode === 500) {
return Promise.reject(res.data)
}
if (res.data.resultCode === 416) {
localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
// 刷新后就在此路由的全局钩子,就会走没有token的逻辑
return window.location.reload()
// return Promise.reject(res.data)
}
return res.data // {resultCode:200,data:{}}
},
(err) => {
return Promise.reject(err)
}
)
}
request(options) {
// 请求会实现拦截器
const instance = axios.create() // 1.每次请求要创建一个新的实例
this.setInterceptor(instance) // 2.设置拦截器
// 发送请求参数
return instance({
...options,
baseURL: this.baseURL,
timeout: this.timeout
})
}
get(url, data) {
return this.request({
method: 'get',
url,
params: data
})
}
post(url, data) {
return this.request({
method: 'post',
url,
data
})
}
}
export default new Http()
- src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
userLogin: '/user/login',
userRegister: '/user/register',
queryCategories: '/categories', // 查询分类的路径
queryGoodsInfo: '/goods/detail'
}
export const queryGoodsInfo = (id) => {
return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
password = md5(password)
return http.post(API_LIST.userLogin, {
loginName,
passwordMd5: password
})
}
// 用户注册
export const userRegister = (loginName, password) => {
return http.post(API_LIST.userRegister, {
loginName,
password
})
}
// 查询所有的分类
export function queryCategories() {
return http.get(API_LIST.queryCategories)
}
- src/components/HomeGoodsItem.vue
<script setup>
import { processURL, addPrefix } from '../utils'
defineProps(['title', 'goodsList'])
</script>
<template>
<div class="title">{{ title }}</div>
<van-grid :column-num="2" :border="false">
<van-grid-item v-for="goods in goodsList" :key="goods.goodsId">
<router-link
:to="`/detail/${goods.goodsId}`"
style="display: flex; flex-direction: column; align-items: center"
>
<van-image :src="processURL(goods.goodsCoverImg)" lazy-load />
<div class="desc">
{{ goods.goodsIntro }}
</div>
<div class="price">
{{ addPrefix(goods.sellingPrice) }}
</div>
</router-link>
</van-grid-item>
</van-grid>
</template>
<style scoped lang="less">
.title {
color: @theme;
font-size: 20px;
text-align: center;
font-weight: bold;
padding: 10px 0;
}
.van-image {
:deep(.van-image__img) {
width: 120px;
height: 140px;
}
}
.desc {
font-size: 14px;
color: #555;
}
.price {
color: @theme;
font-size: 16px;
}
</style>
- src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
if (!route.params.id) {
return router.push('/')
}
currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>
<template>
<div class="detail-box">
<!-- 导航 -->
<TopBar title="商品详情"></TopBar>
<!-- 商品详情 -->
<div class="info" v-if="currentGoods">
<van-image lazy-load :src="currentGoods.goodsCoverImg" />
<div class="desc">
<h3 class="title">{{ currentGoods.goodsName }}</h3>
<p class="tag">{{ currentGoods.goodsIntro }}</p>
<p class="price">¥{{ currentGoods.sellingPrice }}</p>
</div>
<div class="tab">
<a href="javascript:;">概述</a>
<span>|</span>
<a href="javascript:;">参数</a>
<span>|</span>
<a href="javascript:;">安装服务</a>
<span>|</span>
<a href="javascript:;">常见问题</a>
</div>
<div class="content" v-html="currentGoods.goodsDetailContent"></div>
</div>
<!-- 相关操作 -->
<van-action-bar style="max-width: 540px">
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
<van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
<van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
</van-action-bar>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.van-skeleton {
margin-top: 20px;
}
.detail-box {
padding-bottom: 50px;
.info {
padding: 0 10px;
.content {
:deep(img) {
max-width: 100%;
}
}
.van-image {
width: 100%;
min-height: 240px;
}
.desc {
.title {
font-size: 18px;
color: #555;
font-weight: normal;
}
.tag {
padding: 10px 0;
font-size: 14px;
color: #999;
}
.price {
font-size: 18px;
color: #f63515;
}
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
a,
span {
font-size: 15px;
color: #555;
}
a {
padding: 0 10px;
}
}
}
}
</style>
请求携带token
- 在请求拦截器中做,如果本地有token这个字段,就在请求头上带着token。
- src/utils/http.js
class Http {
setInterceptor(instance) {
instance.interceptors.request.use(
(config) => {
// 携带token来做处理
let token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
},
(err) => {
return Promise.reject(err)
}
)
}
}
- 每次请求如果有token,就带上token。
- src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作
class Http {
constructor() {
// 根据环境变量设置请求的路径
this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
this.timeout = 5000
}
setInterceptor(instance) {
instance.interceptors.request.use(
(config) => {
// 携带token来做处理
let token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
},
(err) => {
return Promise.reject(err)
}
)
instance.interceptors.response.use(
(res) => {
if (res.data.resultCode == 200) {
// 对返回值的状态码是200的情况统一处理
return res.data.data
}
if (res.data.resultCode === 500) {
return Promise.reject(res.data)
}
return res.data // {resultCode:200,data:{}}
},
(err) => {
return Promise.reject(err)
}
)
}
request(options) {
// 请求会实现拦截器
const instance = axios.create() // 1.每次请求要创建一个新的实例
this.setInterceptor(instance) // 2.设置拦截器
// 发送请求参数
return instance({
...options,
baseURL: this.baseURL,
timeout: this.timeout
})
}
get(url, data) {
return this.request({
method: 'get',
url,
params: data
})
}
post(url, data) {
return this.request({
method: 'post',
url,
data
})
}
}
export default new Http()
处理无效token
- src/utils/http.js
import axios from 'axios'
// 请求的时候可以做什么事? 1)拦截 携带token, 对响应状态码处理, 2)增加loading (增添一个队列) 3)接口可以保存取消的操作
class Http {
constructor() {
// 根据环境变量设置请求的路径
this.baseURL = import.meta.env.DEV ? 'http://backend-api-01.newbee.ltd/api/v1' : '/'
this.timeout = 5000
}
setInterceptor(instance) {
instance.interceptors.request.use(
(config) => {
// 携带token来做处理
let token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
},
(err) => {
return Promise.reject(err)
}
)
instance.interceptors.response.use(
(res) => {
if (res.data.resultCode == 200) {
// 对返回值的状态码是200的情况统一处理
return res.data.data
}
if (res.data.resultCode === 500) {
return Promise.reject(res.data)
}
if (res.data.resultCode === 416) {
localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
// 刷新后就在此路由的全局钩子,就会走没有token的逻辑
return window.location.reload()
// return Promise.reject(res.data)
}
return res.data // {resultCode:200,data:{}}
},
(err) => {
return Promise.reject(err)
}
)
}
request(options) {
// 请求会实现拦截器
const instance = axios.create() // 1.每次请求要创建一个新的实例
this.setInterceptor(instance) // 2.设置拦截器
// 发送请求参数
return instance({
...options,
baseURL: this.baseURL,
timeout: this.timeout
})
}
get(url, data) {
return this.request({
method: 'get',
url,
params: data
})
}
post(url, data) {
return this.request({
method: 'post',
url,
data
})
}
}
export default new Http()
class Http {
setInterceptor(instance) {
instance.interceptors.response.use(
(res) => {
if (res.data.resultCode === 416) {
localStorage.removeItem('token') // 416 可能是token错误,这个时候清除token,重新刷新
// 刷新后就在此路由的全局钩子,就会走没有token的逻辑
return window.location.reload()
// return Promise.reject(res.data)
}
return res.data // {resultCode:200,data:{}}
},
(err) => {
return Promise.reject(err)
}
)
}
}
- src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home,
meta: { title: '首页' }
},
{
path: '/category',
meta: { title: '分类' },
name: 'category',
component: () => import('../views/Category.vue')
},
{
path: '/cart',
name: 'cart',
component: () => import('../views/Cart.vue'),
meta: {
needLogin: true // 需要登录才能访问的 我们需要配置这个属性
}
},
{
path: '/user',
name: 'user',
component: () => import('../views/User.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
{
path: '/detail/:id',
name: 'detail',
meta: {
needLogin: true
},
component: () => import('../views/Detail.vue')
}
]
})
router.beforeEach(async (to, from) => {
let token = localStorage.getItem('token')
if (token) {
// 如果有token 说明登录成功,但是如果你访问的还是登录
if (to.name === 'login') {
return { path: '/' }
}
} else {
// 没有登录,跳转到登录页面
// /a/b -> ['/a','/a/b']
if (to.matched.some((item) => item.meta.needLogin)) {
// 此路由需要登录但是没有登录, 应该跳转到登录页面
return {
path: '/login',
query: {
redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
...to.query // 当前页面的其他参数也添加进去
}
}
}
}
})
export default router
购物车相关操作
- src/api/index.js
// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')
// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
return http.request(
{
url: '/shop-cart',
method: 'PUT'
},
{
cartItemId,
goodsCount
}
)
}
// 删除某条购物车数据
export const removeCart = (cartItemId) => {
return http.request({
url: `/shop-cart/${cartItemId}`,
method: 'DELETE'
})
}
// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
return http.post('/shop-cart', {
goodsId,
goodsCount
})
}
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
}
return { list, getShopCartList, setShopCartItem, removeShopCartItem, addShopCartItem }
})
- src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
if (!route.params.id) {
return router.push('/')
}
currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>
<template>
<div class="detail-box">
<!-- 导航 -->
<TopBar title="商品详情"></TopBar>
<!-- 商品详情 -->
<div class="info" v-if="currentGoods">
<van-image lazy-load :src="currentGoods.goodsCoverImg" />
<div class="desc">
<h3 class="title">{{ currentGoods.goodsName }}</h3>
<p class="tag">{{ currentGoods.goodsIntro }}</p>
<p class="price">¥{{ currentGoods.sellingPrice }}</p>
</div>
<div class="tab">
<a href="javascript:;">概述</a>
<span>|</span>
<a href="javascript:;">参数</a>
<span>|</span>
<a href="javascript:;">安装服务</a>
<span>|</span>
<a href="javascript:;">常见问题</a>
</div>
<div class="content" v-html="currentGoods.goodsDetailContent"></div>
</div>
<!-- 相关操作 -->
<van-action-bar style="max-width: 540px">
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
<van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
<van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
</van-action-bar>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.van-skeleton {
margin-top: 20px;
}
.detail-box {
padding-bottom: 50px;
.info {
padding: 0 10px;
.content {
:deep(img) {
max-width: 100%;
}
}
.van-image {
width: 100%;
min-height: 240px;
}
.desc {
.title {
font-size: 18px;
color: #555;
font-weight: normal;
}
.tag {
padding: 10px 0;
font-size: 14px;
color: #999;
}
.price {
font-size: 18px;
color: #f63515;
}
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
a,
span {
font-size: 15px;
color: #555;
}
a {
padding: 0 10px;
}
}
}
}
</style>
购物车列表获取
- 在pinia中定义两个状态,一个hasCartStore表示是否已经请求过。一个list表示购物车列表。
- 购物车列表需要在用户跳转时,于路由前置守卫中在pinia中获取。
- 如果有token,就从pinia中获取hasCartStore。
- hasCartStore为true,就代表已经请求过后端了,页面中就可以直接获取到pinia中的购物车列表了。
- hasCartStore为false,就表示没请求过后端了。调用pinia中的方法,更新数据,在请求完成后,缓存数据,并设置hasCartStore为true。
- 如果有token,就从pinia中获取hasCartStore。
- src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
userLogin: '/user/login',
userRegister: '/user/register',
queryCategories: '/categories', // 查询分类的路径
queryGoodsInfo: '/goods/detail'
}
// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')
// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
return http.request(
{
url: '/shop-cart',
method: 'PUT'
},
{
cartItemId,
goodsCount
}
)
}
// 删除某条购物车数据
export const removeCart = (cartItemId) => {
return http.request({
url: `/shop-cart/${cartItemId}`,
method: 'DELETE'
})
}
// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
return http.post('/shop-cart', {
goodsId,
goodsCount
})
}
export const queryGoodsInfo = (id) => {
return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
password = md5(password)
return http.post(API_LIST.userLogin, {
loginName,
passwordMd5: password
})
}
// 用户注册
export const userRegister = (loginName, password) => {
return http.post(API_LIST.userRegister, {
loginName,
password
})
}
// 查询所有的分类
export function queryCategories() {
return http.get(API_LIST.queryCategories)
}
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
const hasCartStore = ref(false)
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
hasCartStore.value = true
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
}
return {
list,
getShopCartList,
setShopCartItem,
removeShopCartItem,
addShopCartItem,
hasCartStore
}
})
- src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { useCartStore } from '@/stores/cart.js'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home,
meta: { title: '首页' }
},
{
path: '/category',
meta: { title: '分类' },
name: 'category',
component: () => import('../views/Category.vue')
},
{
path: '/cart',
name: 'cart',
component: () => import('../views/Cart.vue'),
meta: {
needLogin: true // 需要登录才能访问的 我们需要配置这个属性
}
},
{
path: '/user',
name: 'user',
component: () => import('../views/User.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
{
path: '/detail/:id',
name: 'detail',
meta: {
needLogin: true
},
component: () => import('../views/Detail.vue')
}
]
})
// 登陆态校验。
router.beforeEach(async (to, from) => {
let token = localStorage.getItem('token')
if (token) {
// 如果有token 说明登录成功,但是如果你访问的还是登录
if (to.name === 'login') {
return { path: '/' }
}
} else {
// 没有登录,跳转到登录页面
// /a/b -> ['/a','/a/b']
if (to.matched.some((item) => item.meta.needLogin)) {
// 此路由需要登录但是没有登录, 应该跳转到登录页面
return {
path: '/login',
query: {
redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
...to.query // 当前页面的其他参数也添加进去
}
}
}
}
})
//
router.beforeEach(() => {
let token = localStorage.getItem('token')
if (token) {
const store = useCartStore()
if (!store.hasCartStore) {
// 如果当前没有获取过购物车信息
store.getShopCartList()
}
}
})
export default router
- src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
if (!route.params.id) {
return router.push('/')
}
currentGoods.value = await queryGoodsInfo(route.params.id)
})
</script>
<template>
<div class="detail-box">
<!-- 导航 -->
<TopBar title="商品详情"></TopBar>
<!-- 商品详情 -->
<div class="info" v-if="currentGoods">
<van-image lazy-load :src="currentGoods.goodsCoverImg" />
<div class="desc">
<h3 class="title">{{ currentGoods.goodsName }}</h3>
<p class="tag">{{ currentGoods.goodsIntro }}</p>
<p class="price">¥{{ currentGoods.sellingPrice }}</p>
</div>
<div class="tab">
<a href="javascript:;">概述</a>
<span>|</span>
<a href="javascript:;">参数</a>
<span>|</span>
<a href="javascript:;">安装服务</a>
<span>|</span>
<a href="javascript:;">常见问题</a>
</div>
<div class="content" v-html="currentGoods.goodsDetailContent"></div>
</div>
<!-- 相关操作 -->
<van-action-bar style="max-width: 540px">
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon icon="cart-o" text="购物车" @click="$router.push('/cart')" />
<van-action-bar-button color="linear-gradient(90deg,#6bd8d8,#1baeae)" text="加入购物车" />
<van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
</van-action-bar>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.van-skeleton {
margin-top: 20px;
}
.detail-box {
padding-bottom: 50px;
.info {
padding: 0 10px;
.content {
:deep(img) {
max-width: 100%;
}
}
.van-image {
width: 100%;
min-height: 240px;
}
.desc {
.title {
font-size: 18px;
color: #555;
font-weight: normal;
}
.tag {
padding: 10px 0;
font-size: 14px;
color: #999;
}
.price {
font-size: 18px;
color: #f63515;
}
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
a,
span {
font-size: 15px;
color: #555;
}
a {
padding: 0 10px;
}
}
}
}
</style>
Vue.js devtools的使用
购物车数量及添加
- src/api/index.js
import http from '@/utils/http.js'
import md5 from 'blueimp-md5'
const API_LIST = {
queryIndexInfos: '/index-infos', // 首页的获取数据接口,都在这里
userLogin: '/user/login',
userRegister: '/user/register',
queryCategories: '/categories', // 查询分类的路径
queryGoodsInfo: '/goods/detail'
}
// 获取购物车列表
export const queryShopCart = () => http.get('/shop-cart')
// 修改购买数量
export const setCartCount = (cartItemId, goodsCount) => {
return http.request({
url: '/shop-cart',
method: 'PUT',
data: {
cartItemId,
goodsCount
}
})
}
// 删除某条购物车数据
export const removeCart = (cartItemId) => {
return http.request({
url: `/shop-cart/${cartItemId}`,
method: 'DELETE'
})
}
// 新增购物车数据
export const addNewCart = (goodsId, goodsCount = 1) => {
return http.post('/shop-cart', {
goodsId,
goodsCount
})
}
export const queryGoodsInfo = (id) => {
return http.get(API_LIST.queryGoodsInfo + `/${id}`)
}
// 首页数据的获取,直接通过 api 这个文件来操作
export function queryIndexInfos() {
return http.get(API_LIST.queryIndexInfos)
}
// md5特点? 不是加密算法,摘要算法 1)内容不同摘要的结果不同 2) 如果内容发生一点变化就会发生翻天覆地的变化 , 雪崩效应
// 3) 生成长度一致 4)相同的内容生成的结果一直 5) 不可逆
export const userLogin = (loginName, password) => {
password = md5(password)
return http.post(API_LIST.userLogin, {
loginName,
passwordMd5: password
})
}
// 用户注册
export const userRegister = (loginName, password) => {
return http.post(API_LIST.userRegister, {
loginName,
password
})
}
// 查询所有的分类
export function queryCategories() {
return http.get(API_LIST.queryCategories)
}
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
const hasCartStore = ref(false)
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
hasCartStore.value = true
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
list.value.find((item) => item.cartItemId == id).goodsCount = count
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
// 正常操作的情况下 应该是添加成功后,返回添加后的值
getShopCartList()
}
return {
hasCartStore,
list,
getShopCartList,
setShopCartItem,
removeShopCartItem,
addShopCartItem
}
})
- src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
import { processURL } from '../utils'
import { useCartStore } from '../stores/cart.js'
import { computed, getCurrentInstance } from 'vue'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
const id = route.params.id
if (!id) {
return router.push('/')
}
currentGoods.value = await queryGoodsInfo(id)
})
// 购物车store
const store = useCartStore()
const { list } = storeToRefs(store) // 购物车的数据
function findCurrentItem() {
return list.value.find((item) => item.goodsId == route.params.id)
}
const badge = computed(() => {
// 暂时先计算出 我们当前选中的详情页的数量
const current = findCurrentItem()
return current?.goodsCount || 0
})
const { proxy } = getCurrentInstance()
function addCart() {
const current = findCurrentItem()
if (current) {
// 修改数量
if (current.goodsCount == 5) {
return proxy.$showToast('超出最大购买限制')
}
store.setShopCartItem(current.cartItemId, current.goodsCount + 1)
} else {
// 新增
store.addShopCartItem(route.params.id)
}
}
</script>
<template>
<div class="detail-box">
<!-- 导航 -->
<TopBar title="商品详情"></TopBar>
<!-- 商品详情 -->
<div class="info" v-if="currentGoods">
<van-image lazy-load :src="processURL(currentGoods.goodsCoverImg)" />
<div class="desc">
<h3 class="title">{{ currentGoods.goodsName }}</h3>
<p class="tag">{{ currentGoods.goodsIntro }}</p>
<p class="price">¥{{ currentGoods.sellingPrice }}</p>
</div>
<div class="tab">
<a href="javascript:;">概述</a>
<span>|</span>
<a href="javascript:;">参数</a>
<span>|</span>
<a href="javascript:;">安装服务</a>
<span>|</span>
<a href="javascript:;">常见问题</a>
</div>
<div class="content" v-html="currentGoods.goodsDetailContent"></div>
</div>
<!-- 相关操作 -->
<van-action-bar style="max-width: 540px">
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon
icon="cart-o"
text="购物车"
@click="$router.push('/cart')"
:badge="badge"
/>
<van-action-bar-button
color="linear-gradient(90deg,#6bd8d8,#1baeae)"
text="加入购物车"
@click="addCart"
/>
<van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
</van-action-bar>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.van-skeleton {
margin-top: 20px;
}
.detail-box {
padding-bottom: 50px;
.info {
padding: 0 10px;
.content {
:deep(img) {
max-width: 100%;
}
}
.van-image {
width: 100%;
min-height: 240px;
}
.desc {
.title {
font-size: 18px;
color: #555;
font-weight: normal;
}
.tag {
padding: 10px 0;
font-size: 14px;
color: #999;
}
.price {
font-size: 18px;
color: #f63515;
}
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
a,
span {
font-size: 15px;
color: #555;
}
a {
padding: 0 10px;
}
}
}
}
</style>
购物车页渲染
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
const hasCartStore = ref(false)
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
hasCartStore.value = true
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
list.value.find((item) => item.cartItemId == id).goodsCount = count
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
// 正常操作的情况下 应该是添加成功后,返回添加后的值
getShopCartList()
}
return {
hasCartStore,
list,
getShopCartList,
setShopCartItem,
removeShopCartItem,
addShopCartItem
}
})
- src/views/Cart.vue
<script setup>
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'
const store = useCartStore()
const { list } = storeToRefs(store)
</script>
<template>
<div class="cart-box">
<!-- 头部导航 -->
<TopBar title="购物车"></TopBar>
<van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
<!-- 中间列表 -->
<div class="cart-list" v-else>
<van-swipe-cell v-for="item in list" :key="item.goodsId">
<div class="item">
<van-checkbox checked-color="#1baeae"></van-checkbox>
<GoodsItem :cart="true" :info="item" />
</div>
<template #right>
<van-button square type="danger" text="删除" />
</template>
</van-swipe-cell>
</div>
<!-- 底部结算 -->
<van-submit-bar :price="100" button-text="结算" button-type="primary">
<van-checkbox>全选</van-checkbox>
</van-submit-bar>
</div>
</template>
<style lang="less" scoped>
.van-submit-bar {
bottom: 60px;
}
.cart-box {
padding: 0 10px;
.cart-list {
height: calc(100vh - 46px - 110px);
overflow-y: auto;
.item {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
.van-checkbox {
flex: 0 0 auto;
width: 20px;
}
}
}
}
.van-button--square {
height: 100%;
}
:deep(.van-swipe-cell__right) {
right: -1px;
}
</style>
- src/components/GoodsItem.vue
<template>
<div :class="{ 'goods-item-box': true, 'goods-cart': cart }">
<router-link :to="`/detail/${info.goodsId}`">
<van-image :src="processURL(info.goodsCoverImg)" />
<div class="desc" v-if="!cart">
<h3 class="title">{{ info.goodsName }}</h3>
<p class="info">商品介绍</p>
<p class="price">{{ info.sellingPrice }}</p>
</div>
<div class="cart-desc" v-else>
<h3 class="title">{{ info.goodsName }}</h3>
<p class="info">
<span>{{ info.sellingPrice }}</span>
<van-stepper max="5" />
</p>
</div>
</router-link>
</div>
</template>
<script setup>
import { processURL } from '../utils'
defineProps(['cart', 'info'])
</script>
<style lang="less" scoped>
.clip {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.goods-item-box {
padding: 10px;
border-bottom: 1px solid #eee;
a {
display: flex;
.van-image {
margin-right: 10px;
width: 120px;
height: 120px;
flex: 0 0 auto;
}
.desc {
color: #555;
flex: 1 0;
.title {
.clip;
font-size: 14px;
font-weight: normal;
line-height: 20px;
max-height: 40px;
}
.info {
.clip;
padding: 8px 0;
color: #999;
font-size: 12px;
line-height: 16px;
max-height: 32px;
}
.price {
color: @theme;
font-size: 14px;
}
}
}
/* 购物车独有 */
&.goods-cart {
border-bottom: none;
a {
.van-image {
width: 100px;
height: 100px;
}
.cart-desc {
box-sizing: border-box;
padding: 10px;
min-width: 220px;
color: #555;
flex: 1 0;
.title {
.clip;
font-size: 14px;
font-weight: normal;
line-height: 20px;
max-height: 40px;
}
.info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
line-height: 28px;
span {
font-size: 14px;
color: #ee0a24;
}
}
}
}
}
}
</style>
购物车页操作
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
const hasCartStore = ref(false)
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
hasCartStore.value = true
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
list.value.find((item) => item.cartItemId == id).goodsCount = count
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
// 正常操作的情况下 应该是添加成功后,返回添加后的值
getShopCartList()
}
const badge = computed(() => {
// 暂时先计算出 我们当前选中的详情页的数量
const current = list.value.reduce((memo, current) => ((memo += current.goodsCount), memo), 0)
return current
})
return {
badge,
hasCartStore,
list,
getShopCartList,
setShopCartItem,
removeShopCartItem,
addShopCartItem
}
})
- src/views/Detail.vue
<script setup>
import { queryGoodsInfo } from '@/api'
import { processURL } from '../utils'
import { useCartStore } from '../stores/cart.js'
const route = useRoute()
const router = useRouter()
const currentGoods = ref(null)
onMounted(async () => {
const id = route.params.id
if (!id) {
return router.push('/')
}
currentGoods.value = await queryGoodsInfo(id)
})
// 购物车store
const store = useCartStore()
const { list } = storeToRefs(store) // 购物车的数据
function findCurrentItem() {
return list.value.find((item) => item.goodsId == route.params.id)
}
const { proxy } = getCurrentInstance()
function addCart() {
const current = findCurrentItem()
if (current) {
// 修改数量
if (current.goodsCount == 5) {
return proxy.$showToast('超出最大购买限制')
}
store.setShopCartItem(current.cartItemId, current.goodsCount + 1)
} else {
// 新增
store.addShopCartItem(route.params.id)
}
}
</script>
<template>
<div class="detail-box">
<!-- 导航 -->
<TopBar title="商品详情"></TopBar>
<!-- 商品详情 -->
<div class="info" v-if="currentGoods">
<van-image lazy-load :src="processURL(currentGoods.goodsCoverImg)" />
<div class="desc">
<h3 class="title">{{ currentGoods.goodsName }}</h3>
<p class="tag">{{ currentGoods.goodsIntro }}</p>
<p class="price">¥{{ currentGoods.sellingPrice }}</p>
</div>
<div class="tab">
<a href="javascript:;">概述</a>
<span>|</span>
<a href="javascript:;">参数</a>
<span>|</span>
<a href="javascript:;">安装服务</a>
<span>|</span>
<a href="javascript:;">常见问题</a>
</div>
<div class="content" v-html="currentGoods.goodsDetailContent"></div>
</div>
<!-- 相关操作 -->
<van-action-bar style="max-width: 540px">
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon
icon="cart-o"
text="购物车"
@click="$router.push('/cart')"
:badge="store.badge"
/>
<van-action-bar-button
color="linear-gradient(90deg,#6bd8d8,#1baeae)"
text="加入购物车"
@click="addCart"
/>
<van-action-bar-button color="linear-gradient(90deg,#0dc3c3,#098888)" text="立即购买" />
</van-action-bar>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.van-skeleton {
margin-top: 20px;
}
.detail-box {
padding-bottom: 50px;
.info {
padding: 0 10px;
.content {
:deep(img) {
max-width: 100%;
}
}
.van-image {
width: 100%;
min-height: 240px;
}
.desc {
.title {
font-size: 18px;
color: #555;
font-weight: normal;
}
.tag {
padding: 10px 0;
font-size: 14px;
color: #999;
}
.price {
font-size: 18px;
color: #f63515;
}
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
a,
span {
font-size: 15px;
color: #555;
}
a {
padding: 0 10px;
}
}
}
}
</style>
- src/views/Cart.vue
<script setup>
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'
const store = useCartStore()
const { list } = storeToRefs(store)
</script>
<template>
<div class="cart-box">
<!-- 头部导航 -->
<TopBar title="购物车"></TopBar>
<van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
<!-- 中间列表 -->
<div class="cart-list" v-else>
<van-swipe-cell v-for="item in list" :key="item.goodsId">
<div class="item">
<van-checkbox checked-color="#1baeae"></van-checkbox>
<GoodsItem :cart="true" :info="item" />
</div>
<template #right>
<van-button square type="danger" text="删除" />
</template>
</van-swipe-cell>
</div>
<!-- 底部结算 -->
<van-submit-bar :price="100" button-text="结算" button-type="primary">
<van-checkbox>全选</van-checkbox>
</van-submit-bar>
</div>
</template>
<style lang="less" scoped>
.van-submit-bar {
bottom: 60px;
}
.cart-box {
padding: 0 10px;
.cart-list {
height: calc(100vh - 46px - 110px);
overflow-y: auto;
.item {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
.van-checkbox {
flex: 0 0 auto;
width: 20px;
}
}
}
}
.van-button--square {
height: 100%;
}
:deep(.van-swipe-cell__right) {
right: -1px;
}
</style>
- src/components/NavBar.vue
<template>
<van-tabbar route v-if="props.visible">
<van-tabbar-item to="/home">
<template #icon>
<SvgIcon icon-class="home"></SvgIcon>
</template>
首页</van-tabbar-item
>
<van-tabbar-item to="/category">
<template #icon>
<SvgIcon icon-class="category"></SvgIcon>
</template>
分类</van-tabbar-item
>
<van-tabbar-item to="/cart" :badge="store.badge">
<template #icon>
<SvgIcon icon-class="cart"></SvgIcon>
</template>
购物车</van-tabbar-item
>
<van-tabbar-item to="/user">
<template #icon>
<SvgIcon icon-class="user"></SvgIcon>
</template>
我的</van-tabbar-item
>
</van-tabbar>
</template>
<script setup>
import { useCartStore } from '@/stores/cart.js'
const store = useCartStore()
const route = useRoute()
// 父组件告诉我要不要渲染,我自己判断如果需要渲染,告诉父亲
const props = defineProps(['visible'])
const emit = defineEmits(['update:visible'])
watchEffect(() => {
// 根据路由判断是否要渲染
const paths = ['/home', '/category', '/cart', '/user']
// 如果访问的是这几个页面
emit('update:visible', paths.includes(route.path))
// 通知父组件我的最新状态是多少
})
</script>
- src/components/GoodsItem.vue
<template>
<div :class="{ 'goods-item-box': true, 'goods-cart': cart }">
<router-link :to="`/detail/${info.goodsId}`">
<van-image :src="processURL(info.goodsCoverImg)" />
<div class="desc" v-if="!cart">
<h3 class="title">{{ info.goodsName }}</h3>
<p class="info">商品介绍</p>
<p class="price">{{ info.sellingPrice }}</p>
</div>
<div class="cart-desc" v-else>
<h3 class="title">{{ info.goodsName }}</h3>
<p class="info">
<span>{{ info.sellingPrice }}</span>
<van-stepper max="5" :modelValue="info.goodsCount" @change="handle" />
</p>
</div>
</router-link>
</div>
</template>
<script setup>
import { useCartStore } from '../stores/cart'
import { processURL } from '../utils'
const props = defineProps(['cart', 'info'])
const store = useCartStore()
function handle(value) {
store.setShopCartItem(props.info.cartItemId, value)
}
</script>
<style lang="less" scoped>
.clip {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.goods-item-box {
padding: 10px;
border-bottom: 1px solid #eee;
a {
display: flex;
.van-image {
margin-right: 10px;
width: 120px;
height: 120px;
flex: 0 0 auto;
}
.desc {
color: #555;
flex: 1 0;
.title {
.clip;
font-size: 14px;
font-weight: normal;
line-height: 20px;
max-height: 40px;
}
.info {
.clip;
padding: 8px 0;
color: #999;
font-size: 12px;
line-height: 16px;
max-height: 32px;
}
.price {
color: @theme;
font-size: 14px;
}
}
}
/* 购物车独有 */
&.goods-cart {
border-bottom: none;
a {
.van-image {
width: 100px;
height: 100px;
}
.cart-desc {
box-sizing: border-box;
padding: 10px;
min-width: 220px;
color: #555;
flex: 1 0;
.title {
.clip;
font-size: 14px;
font-weight: normal;
line-height: 20px;
max-height: 40px;
}
.info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
line-height: 28px;
span {
font-size: 14px;
color: #ee0a24;
}
}
}
}
}
}
</style>
购物车全选与删除操作
- src/views/Cart.vue
<script setup>
// @ts-ignore
import empty from '@/assets/images/empty-car.png'
import { useCartStore } from '../stores/cart'
import { storeToRefs } from 'pinia'
// @ts-ignore
const store = useCartStore()
const { list } = storeToRefs(store)
function remove(id) {
store.removeShopCartItem(id)
}
// @ts-ignore
</script>
<template>
<div class="cart-box">
<!-- 头部导航 -->
<TopBar title="购物车"></TopBar>
<van-empty description="购物车空空如也" :image="empty" v-if="list.length == 0" />
<!-- 中间列表 -->
<div class="cart-list" v-else>
<van-swipe-cell v-for="item in list" :key="item.goodsId">
<div class="item">
<van-checkbox checked-color="#1baeae" v-model="item.checked"></van-checkbox>
<GoodsItem :cart="true" :info="item" />
</div>
<template #right>
<van-button square type="danger" text="删除" @click="remove(item.cartItemId)" />
</template>
</van-swipe-cell>
</div>
<!-- 底部结算 -->
<van-submit-bar :price="store.total * 100" button-text="结算" button-type="primary">
<van-checkbox v-model="store.isCheckedAll">全选</van-checkbox>
</van-submit-bar>
</div>
</template>
<style lang="less" scoped>
.van-submit-bar {
bottom: 60px;
}
.cart-box {
padding: 0 10px;
.cart-list {
height: calc(100vh - 46px - 110px);
overflow-y: auto;
.item {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
.van-checkbox {
flex: 0 0 auto;
width: 20px;
}
}
}
}
.van-button--square {
height: 100%;
}
:deep(.van-swipe-cell__right) {
right: -1px;
}
</style>
- src/stores/cart.js
// 1.获取购物车列表queryShopCart
// 2.修改数量 哪个商品的数量是多少 setCartCount
// 3.删除购物车的某个商品 removeCart
// 4.新增只传递新增的id即可addNewCart
// @ts-ignore
import { queryShopCart, setCartCount, removeCart, addNewCart } from '@/api'
export const useCartStore = defineStore('cart', () => {
const list = ref([])
const hasCartStore = ref(false)
// 在pinia中缓存数据
async function getShopCartList() {
list.value = await queryShopCart()
hasCartStore.value = true
}
async function setShopCartItem(id, count) {
await setCartCount(id, count)
// 将列表中的数据也要更新,如果是修改也可以不发请求 通过id 获取到这一项,更改数量即可
list.value.find((item) => item.cartItemId == id).goodsCount = count
}
async function removeShopCartItem(id) {
await removeCart(id)
// 将列表中的数据也要更新
list.value = list.value.filter((item) => item.cartItemId != id)
}
async function addShopCartItem(id) {
await addNewCart(id)
// 将列表中的数据也要更新
// 正常操作的情况下 应该是添加成功后,返回添加后的值
getShopCartList()
}
const badge = computed(() => {
// 暂时先计算出 我们当前选中的详情页的数量
const current = list.value.reduce((memo, current) => ((memo += current.goodsCount), memo), 0)
return current
})
const total = computed(() => {
return list.value.reduce((memo, current) => {
if (current.checked) {
// 选中就累加
memo += current.goodsCount * current.sellingPrice
}
return memo
}, 0)
})
const isCheckedAll = computed({
get() {
return list.value.every((item) => item.checked)
},
set(newVal) {
list.value.forEach((item) => (item.checked = newVal))
}
})
return {
total,
isCheckedAll,
badge,
hasCartStore,
list,
getShopCartList,
setShopCartItem,
removeShopCartItem,
addShopCartItem
}
})
搜索操作
- src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { useCartStore } from '@/stores/cart'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home,
meta: { title: '首页' }
},
{
path: '/category',
meta: { title: '分类' },
name: 'category',
component: () => import('../views/Category.vue')
},
{
path: '/cart',
name: 'cart',
component: () => import('../views/Cart.vue'),
meta: {
needLogin: true // 需要登录才能访问的 我们需要配置这个属性
}
},
{
path: '/user',
name: 'user',
component: () => import('../views/User.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
{
path: '/detail/:id',
name: 'detail',
meta: {
needLogin: true
},
component: () => import('../views/Detail.vue')
},
{
path: '/search',
name: 'search',
meta: {
needLogin: true
},
component: () => import('../views/Search.vue')
}
]
})
// 鉴权看是否登录过
router.beforeEach(async (to) => {
let token = localStorage.getItem('token')
if (token) {
// 如果有token 说明登录成功,但是如果你访问的还是登录
if (to.name === 'login') {
return { path: '/' }
}
} else {
// 没有登录,跳转到登录页面
// /a/b -> ['/a','/a/b']
if (to.matched.some((item) => item.meta.needLogin)) {
// 此路由需要登录但是没有登录, 应该跳转到登录页面
return {
path: '/login',
query: {
redirect: to.path, // 跳转到登录页面,并且告诉登录页面稍后回调回来
...to.query // 当前页面的其他参数也添加进去
}
}
}
}
})
// 如果登陆过查询是否已经获取购物车信息
// 获取购物车的信息存储到pinia中
router.beforeEach(() => {
let token = localStorage.getItem('token')
if (token) {
const store = useCartStore()
if (!store.hasCartStore) {
// 如果当前没有获取过购物车信息
store.getShopCartList()
}
}
})
export default router
- src/stores/category.js
import { queryCategories } from '@/api'
export const useCategoryStore = defineStore('category', () => {
const list = ref([]) // 默认所有分类的信息
async function getCategories() {
// 获取分类将获取的数据保存到list变量中
list.value = await queryCategories()
}
return { list, getCategories }
})
- src/views/Search.vue
<script setup>
import { onMounted } from 'vue'
import { useCategoryStore } from '../stores/category'
const route = useRoute()
const goodsCategoryId = ref(parseInt(route.query.categoryId) || '')
const categoryOptions = ref([{ text: '全部分类', value: '' }])
const orderBy = ref('')
const sortOptions = [
{ text: '综合排序', value: '' },
{ text: '时间排序', value: 'new' },
{ text: '价格排序', value: 'price' }
]
const store = useCategoryStore()
const { list } = storeToRefs(store)
onMounted(async () => {
if (list.value.length == 0) {
await store.getCategories()
}
console.log(`list.value-->`, list.value)
const result = []
const stack = [...toRaw(list.value)] // 所以的数据
console.log(`stack-->`, JSON.parse(JSON.stringify(stack)))
// 树的遍历, 层序遍历
let index = 0 // 指针
let current
// 递归会出现爆栈的问题,避免用递归
while ((current = stack[index++]) != null) {
if (current.categoryLevel == 1) {
stack.push(...current.secondLevelCategoryVOS)
} else if (current.categoryLevel == 2) {
stack.push(...current.thirdLevelCategoryVOS)
} else {
result.push({ text: current.categoryName, value: current.categoryId })
}
}
categoryOptions.value.push(...result)
})
</script>
<template>
<div class="search-box">
<!-- 头部导航 -->
<TopBar>
<template #title>
<van-search />
</template>
<template #right>
<van-button type="primary" size="small"> 搜索 </van-button>
</template>
</TopBar>
<!-- 筛选 -->
<van-dropdown-menu>
<van-dropdown-item :options="categoryOptions" v-model="goodsCategoryId" />
<van-dropdown-item :options="sortOptions" v-model="orderBy" />
</van-dropdown-menu>
<!-- 内容 -->
<div class="content">
<van-empty description="赶快搜索你想要的产品吧" />
<van-list>
<!-- <GoodsItem /> -->
</van-list>
</div>
</div>
</template>
<style scoped>
.content {
height: calc(100vh - 94px);
overflow: scroll;
}
</style>
数组数据转化-扁平化
层序遍历
const stack = [
{
categoryId: 15,
categoryLevel: 1,
categoryName: '家电 数码 手机',
secondLevelCategoryVOS: [
{
categoryId: 17,
parentId: 15,
categoryLevel: 2,
categoryName: '家电',
thirdLevelCategoryVOS: [
{
categoryId: 20,
categoryLevel: 3,
categoryName: '生活电器'
},
{
categoryId: 110,
categoryLevel: 3,
categoryName: 'wer'
},
{
categoryId: 21,
categoryLevel: 3,
categoryName: '厨房电器'
},
{
categoryId: 22,
categoryLevel: 3,
categoryName: '扫地机器人'
},
{
categoryId: 31,
categoryLevel: 3,
categoryName: '空气净化器'
}
]
},
{
categoryId: 19,
parentId: 15,
categoryLevel: 2,
categoryName: '手机',
thirdLevelCategoryVOS: [
{
categoryId: 45,
categoryLevel: 3,
categoryName: '荣耀手机'
},
{
categoryId: 57,
categoryLevel: 3,
categoryName: 'vivo'
},
{
categoryId: 58,
categoryLevel: 3,
categoryName: '手机以旧换新'
}
]
}
]
},
{
categoryId: 16,
categoryLevel: 1,
categoryName: '女装 男装 穿搭',
secondLevelCategoryVOS: [
{
categoryId: 67,
parentId: 16,
categoryLevel: 2,
categoryName: '女装',
thirdLevelCategoryVOS: [
{
categoryId: 76,
categoryLevel: 3,
categoryName: '外套'
}
]
}
]
},
{
categoryId: 61,
categoryLevel: 1,
categoryName: '家具 家饰 家纺',
secondLevelCategoryVOS: [
{
categoryId: 70,
parentId: 61,
categoryLevel: 2,
categoryName: '家具',
thirdLevelCategoryVOS: [
{
categoryId: 77,
categoryLevel: 3,
categoryName: '沙发'
}
]
}
]
}
]
const result = []
console.log(`0. stack-->`, JSON.parse(JSON.stringify(stack)))
// 树的遍历, 层序遍历
let index = 0 // 指针
let current
// 递归会出现爆栈的问题,避免用递归
while ((current = stack[index++]) != null) {
if (current.categoryLevel == 1) {
stack.push(...current.secondLevelCategoryVOS)
console.log(`1. stack-->`, JSON.parse(JSON.stringify(stack)))
} else if (current.categoryLevel == 2) {
stack.push(...current.thirdLevelCategoryVOS)
console.log(`2. stack-->`, JSON.parse(JSON.stringify(stack)))
} else {
result.push({ text: current.categoryName, value: current.categoryId })
console.log(`3. stack-->`, JSON.parse(JSON.stringify(stack)))
}
}
console.log(`result-->`, result)
<script setup>
import { onMounted } from 'vue'
import { useCategoryStore } from '../stores/category'
const route = useRoute()
const goodsCategoryId = ref(parseInt(route.query.categoryId) || '')
const categoryOptions = ref([{ text: '全部分类', value: '' }])
const orderBy = ref('')
const sortOptions = [
{ text: '综合排序', value: '' },
{ text: '时间排序', value: 'new' },
{ text: '价格排序', value: 'price' }
]
const store = useCategoryStore()
const { list } = storeToRefs(store)
onMounted(async () => {
if (list.value.length == 0) {
await store.getCategories()
}
console.log(`list.value-->`, list.value)
const result = []
const stack = [...toRaw(list.value)] // 所以的数据
console.log(`stack-->`, JSON.parse(JSON.stringify(stack)))
// 树的遍历, 层序遍历
let index = 0 // 指针
let current
// 递归会出现爆栈的问题,避免用递归
while ((current = stack[index++]) != null) {
if (current.categoryLevel == 1) {
stack.push(...current.secondLevelCategoryVOS)
} else if (current.categoryLevel == 2) {
stack.push(...current.thirdLevelCategoryVOS)
} else {
result.push({ text: current.categoryName, value: current.categoryId })
}
}
categoryOptions.value.push(...result)
})
</script>