小兔鲜----商城项目
在main.ts中//el:指令绑定的元素img// binding:binding.value 指令等于号后面绑定的表达式的值url})路由缓存问题是 Vue Router 在单页应用(SPA)中常见的一个问题,主要发生在组件实例被复用时,导致数据不更新或生命周期钩子不触发。Vue Router 为了性能优化,会对相同组件的路由切换进行复用:/user/1 → /user/2// 复用 User
配置路由:
一级路由:
首页和登入页
二级路由:
Home和category
默认二级路由path设置为空
scss变量自动导入
在项目中有一些组件共享的色值,将其存放在var.scss中,那么如果组件中要使用色值就要引入此文件。
将其自动导入可以免去手动导入
在vite.config.ts文件中加入
@use "@/style/var.scss" as *;
Layout页面
就近维护,在Layout里面新建components文件夹存放相关组件
搭建相关静态页面
LayoutNav.vue, LayoutHeader.vue, Layoutfotter.vue
引入字体图标
使用阿里巴巴图标库
在index.html中加入
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
获取接口数据,渲染一级导航
实现导航吸顶效果
在导航上下滚动过程中,如果导航距离顶部超过78px,就让导航吸顶显示,否则隐藏
新建LayoutFixed.vue组件
获取组件距离顶部的距离进行判断 。使用vueUse插件,使用useScroll方法得到距离
import { useScroll } from '@vueuse/core'
//是位于顶部的,所以可以直接使用Window的距离即可
const { y } = useScroll(window)
:class对象用法:对象的键是类名,值是布尔值。当值为 true 时,对应的类名会被添加到元素上
显示隐藏通过show类名控制
为其加上:class
<div class="app-header-sticky" :class="{show:y>78}">
pinia优化重复请求
因为LayoutFixed.vue组件和 LayoutHeader.vue组件都要请求相同的数据来渲染,所以将其封装在pinia中,减少浪费。
在stores文件夹中新建category.ts页面
使用pinia
Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字:
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/apis/layout';
import { ref } from 'vue';
// `defineStore()` 的返回值的命名是自由的
// 但最好含有 store 的名字,且以 `use` 开头,以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useCategoryStore = defineStore('category', ()=>{
const categoryList = ref([])
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.data.result
// console.log(category.value);
}
return{categoryList,getCategory}
})
将使用发送请求的代码放在index.vue 里,这样就只用发送一次请求,在剩余两个组件中直接使用数据即可。
注意使用TS要给categoryList注明类型,否则使用不了
定义接口
export interface CategoryItem {
id: number | string
name: string
picture: string
children: CategoryItem[] // 递归定义子分类
// 商品数组
}
const categoryList = ref<Array<CategoryItem>>([])
Home页面
搭建HomeCategory静态页面。
使用pinia中是数据渲染
二级渲染只要前两项,用数组的slice方法截取
<RouterLink v-for="i in item.children.slice(0,2)" :key="i.id" to="/">{{i.name}}</RouterLink>
搭建HomeBanner静态页面。
请求数据渲染
面板组件封装
新鲜好物和人气推荐结构类似,只是内容不同,封装组件实现复用结构
搭建HomePanel.vue静态页面,可变部分,复杂的用插槽slot,不复杂的用props
实现图片懒加载
因为电商网站的首页很长,用户不一定能访问到靠下的图片,所以让图片进入视口区域时才发送图片请求
使用自定义指令
定义全局指令
在main.ts中
app.directive('img-lazy',{
mounted(el,binding){
//el:指令绑定的元素 img
// binding:binding.value 指令等于号后面绑定的表达式的值 url
console.log(el,binding);
}
})
判断图片是否进入视口
使用vueuse里面的useIntersectionObserver方法
完整懒加载指令
app.directive('img-lazy',{
mounted(el,binding){
//el:指令绑定的元素 img
// binding:binding.value 指令等于号后面绑定的表达式的值 url
console.log(el,binding);
const { stop } = useIntersectionObserver(
el,
([entry]) => {
// 安全的类型检查
if (entry?.isIntersecting) {
el.src = binding.value
stop() // 加载完成后停止观察
}
},
)
}
})
stop()解决重复监听问题,只要加载了完一次后就停止监听
在组件中使用
v-img-lazy="item.picture"
优化图片懒加载
将其直接写在main.ts入口文件,是不合理的,所以将其封装成一个插件
这样main.ts就只要注册此插件即可
在directives里面新增index.ts封装此插件
import { type App, type DirectiveBinding } from 'vue'; // 导入Vue类型[citation:7]
import { useIntersectionObserver } from '@vueuse/core';
export const lazyPlugin={
install(app: App){
app.directive('img-lazy', {
mounted(el: HTMLImageElement, binding: DirectiveBinding<string>) {
//el:指令绑定的元素 img
// binding:binding.value 指令等于号后面绑定的表达式的值 url
// console.log(el,binding);
const { stop } = useIntersectionObserver(
el,
([entry]) => {
// 安全的类型检查
if (entry?.isIntersecting) {
el.src = binding.value
stop() // 加载完成后停止观察
}
},
)
}
})
}
}
在main.ts中引入
import { lazyPlugin } from './directives'
app.use(lazyPlugin)
编写product
静态+渲染+图片懒加载
封装GoodsItem组件
将product里类似的封装成组件
给header的导航加路由实现跳转
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 导航菜单、明确的链接 | <RouterLink> |
语义化,SEO友好,性能优化 |
| 按钮操作、条件跳转 | router.push() |
灵活控制跳转逻辑 |
| 列表项点击跳转 | 两者都可 | 根据复杂度选择 |
使用params路由参数
path:'category/:id',
组件中使用
<RouterLink :to="`/category/${item.id}`">{{item.name}}</RouterLink>
面包屑导航渲染
用useRouter获取路由参数
请求时需要带上路由参数传递
const res=await getTopCategoryAPI(route.params.id as string)
一级导航轮播图
用HomeBanner的请求加上参数,代码类似
激活导航栏
实现点击哪个哪个就拥有样式
active-class="active"
渲染下面的分类详情
只要把数据类型定好就行
解决路由缓存问题
什么是路由缓存问题?
路由缓存问题是 Vue Router 在单页应用(SPA)中常见的一个问题,主要发生在组件实例被复用时,导致数据不更新或生命周期钩子不触发。
Vue Router 为了性能优化,会对相同组件的路由切换进行复用:
/user/1 → /user/2 // 复用 User 组件实例
解决方案
1. 使用 key 属性(不在意性能时使用)
太粗暴,把整个实例都摧毁重建了
组件里面所有的请求都会重新发送,存在浪费
<router-view :key="$route.fullPath"></router-view>
2.使用路由守卫(在意性能时使用)
能精确控制需要销毁重建的部分
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to) => {
getTopCategory((to.params as { id: string }).id)
})
基于函数拆分业务
把同一个组件中独立的业务代码通过函数做封装处理,提升代码的可维护性。
把category里面的TS逻辑,抽象成useCategory和useBanner
抽象useCategory时,注意路由要作为参数传递,否则刚开始的时候route是undefined
二级分类功能
新建文件夹SubCategory
编写静态页面
配置路由规则
二级分类面包屑导航
请求数据+渲染
二级商品列表实现
请求数据,注意传参
使用之前封装的GoodItems组件进行渲染
添加筛选实现筛选
切换筛选参数,重新发送请求,获取不同数据进行渲染
使用Element Plus的tabs组件
为其绑定v-model,使用tab-change方法来重新调数据
<el-tabs v-model="reqData.sortField" @tab-change="handleChange">
列表无限加载功能
使用Element Plus提供的v-infinite-scroll指令
<el-tabs v-model="reqData.sortField" @tab-change="handleChange" :infinite-scroll-disabled="disabled">
监听是否触底条件,满足条件时让页数参数+1,来获取下一页的数据,再让它们拼接起来,加载完毕后停止监听
const disabled=ref(false)
const load=async()=>{
const res=await getSubCategoryAPI(reqData)
reqData.page++
goodsList.value=[...goodsList.value,...res.data.result.items]
// 加载完毕后停止监听
if(res.data.result.items.length===0){
disabled.value=true
}
}
定制路由滚动行为
再不同路由切换的时候,可以自动的滚动到页面的顶部,而不是停留在原来的位置。
配置路由
// 路由滚动行为定制
scrollBehavior(){
return{
top:0
}
}
详情页
view里面新增文件夹Detail
配置路由,有params参数,传id
{
path:'detail/:id',
component:Detail
},
首页新鲜好物模块配置路由跳转,点击后跳转到详情页
<RouterLink :to="`detail/${item.id}`">
请求数据,渲染
面包屑导航、右侧基础数据、左侧统计数据、商品详情都是用的同一个接口,先渲染这些部分
渲染面包屑导航的时候,因为一开始可能没有数据,所以使用可选链的语法
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/${detailList.categories[1]?.id}` }">{{detailList.categories[1]?.name}}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/sub/${detailList.categories[0]?.id}` }">{{detailList.categories[0]?.name}}
遇到多层访问属性报错时,可以考虑加上可选链,可能就解决了
详情块 热榜区
两块热榜结构一直,封装组件,再props不一样的地方
详情页-图片预览组件封装
通过小图换大图
设置一个图片数组列表,鼠标划入小图时记录当前小图下标值,通过下标值在数组中取出相应图片 ,放到大图位置。
编写静态页面
图片数组在父组件中使用的时候将其作为props参数传递
给小图添加鼠标移入函数,获取当前图片的下标值,并存储到activeIndex
const activeIndex=ref()
const handleEnter=(i:number)=>{
activeIndex.value=i
}
再将其渲染到大图位置
<img :src="imageList[activeIndex]" alt="" />
再为鼠标滑过的小图添加激活类名,显示当前选中
:class="{active:i===activeIndex}"
放大镜功能
获取鼠标相对位置
使用vueuse里的useMouseInElement
定义target绑定大盒子,鼠标是相对大盒子的距离,isOutside判断鼠标是否在盒子外部
const target=ref(null)
const {elementX,elementY,isOutside}=useMouseInElement(target)
让滑块跟着鼠标移动
用watch监听,计算滑块滑动有效范围
渲染到页面
const left=ref(0)
const top=ref(0)
watch([elementX,elementY],()=>{
if(isOutside.value){return}
// console.log(111);
if(elementX.value>100&&elementX.value<300){
left.value=elementX.value-100
}
if(elementY.value>100&&elementY.value<300){
top.value=elementY.value-100
}
if(elementX.value>300){left.value=200}
if(elementX.value<100){left.value=0}
if(elementY.value>300){top.value=200}
if(elementY.value<100){top.value=0}
})
滑块移动时,显示放大的图
放大的图的移动距离是原来的两倍,移动方向与滑块移动方向相反
控制滑块和放大图的显示和隐藏
v-show="!(isOutside)"
详情页-SKU组件(没写完,听不懂)
库存管理中最小的常用单位
为了让用户能够选择商品的规格
渲染组件
点击规格更新选中状态
const changeSlected=(item:goodsItem,val:valueItem)=>{
if(val.selected){
val.selected=false
}
else{
item.values.forEach(valItem=>valItem.selected=false)
val.selected=true
}
}
更新禁用状态
生成有效路径字典
当前规格的sku,或者组合起来的sku,在sku数组里对应项库存为0 时,当前规格会禁用。生成路径字典是为了协助和简化这个过程
直接用现成的SKU
也就是一个别人写的第三方组件,先看props和emit
选择规格后,sku组件的emit会传递是否有库存,有库存就传递一个对象,否则是一个空对象。
根据其传递是否为空,作为判断条件。
正常产出数据就可以加入购物车
详情页-通用组件统一注册为全局组件
通过注册插件的方式
import type { App } from "vue";
import ImageView from "./ImageView/index.vue";
import skuItem from "./XtxSku/index.vue";
export const componentsPlugin={
install(app:App){
app.component('ImageView', ImageView)
app.component('skuItem', skuItem)
}
}
再将其在main.ts中注册
import { componentsPlugin } from './components'
app.use(componentsPlugin)
登入
路由配置,静态页面
表单校验
账号和密码都是普通校验
是否同意隐私条款是自定义校验规则
agree:[
{validator:(rule:string, value: boolean, callback:(error?: string | Error) => void)=>{
if(value){
callback()
}
else{
callback(new Error('请勾选协议'))
}
}}
]
登入
点击登入时进行统一校验
登入账号:xiaotuxian001
密码:123456
登入成功跳转至首页
登入失败显示失败原因,放在拦截器里面,统一处理
用pinia管理用户数据
把获取用户的代码放到pinia中
组件使用则直接调用即可
pinia用户数据持久化
为了保持token不丢失,保持登入状态
操作state时自动把用户数据在本地的localStorage也存一份,刷新时先从本地取
使用pinia的pinia-plugin-persistedstate插件
安装插件包
在main.ts中注册
在需要持久化的pinia中配置
在请求拦截器中携带token
token作为用户标识,需要在多处使用。为了统一控制,采用请求拦截器携带
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 先获取token
const {userInfo}=useUserStore()
const token=userInfo.token
// 按照后端要求拼接
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config;
}
退出登入功能
退出时清空本地存储,并跳转到登入页
用户的数据和操作用户数据的方法都放在pinia里
token失效 401拦截
401错误表示未授权。请求需要用户身份验证,但提供的凭据无效、过期或缺失
token保持一定时间的有效性,如果用户一段时间不做任何操作,token就会失效。
使用失效的token再去请求一些接口,接口就会报401错误
在响应拦截器中对处理401错误
清除本地数据并跳转到登入页
// 401token失效处理
const { clearInfo } = useUserStore()
if (error.response.status === 401) {
clearInfo()
router.replace('/login')
}
购物车功能
放在pinia中,也需要持久化
加入购物车时,先判断购物车是否已经有这个物品,有的话对其count++
没有就push进数组中
在给pinia里的函数传递参数时,需要重建数据,因为异步获取,否则接收的都是undefined和空
cart.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore=defineStore('cart',()=>{
const cartList=ref<Array<dataItems>>([])
interface dataItems {
name: string
price: string
mainPictures: string | undefined
id: string
count: number
skuId: string | undefined
attrsText: string | undefined
selected: boolean
}
const addCart=(data:dataItems)=>{
// console.log(111);
// console.log(data);
const items:dataItems|undefined = cartList.value.find((item:dataItems)=>item.skuId===data.skuId)
if(items){
items.count++
// console.log(items.skuId);
}else{
cartList.value.push(data)
}
// console.log(cartList.value);
}
return {
cartList,
addCart
}
},
{
persist: true,
},
)
头部购物车列表渲染
静态模版
得到pinia里的数据渲染
添加删除功能
在pinia里面新增删除action,组件里面调用
购物车结算统计价钱
使用数组的reduce方法
本地购物车-列表购物车
在view下新建CartList文件夹
然后建index.vue
复制模版
和头部购物车一样从pinia里面拿数据渲染
单选功能!!!!
把checkBox的状态和pinia的selected关联起来
因为后续还要调接口所以对checkBox使用:model-value和@change
使用@change时要得到所点击对象的skuId,所以采用箭头函数同时传递skuId和selected
为了在其默认参数的基础上再添加一个参数可以使用这种方式,但这里不需要
:model-value="i.selected" @change="(selected:boolean)=>handlerChange(i.skuId,selected)"
直接传递skuId即可
<el-checkbox :model-value="i.selected" @change="handlerChange(i.skuId)"/>
在pinia中添加单选功能,
根据组件传递的skuId使用数组的find方法找到点击的对象,修改其selected
组件中调用
全选功能
在pinia中先计算是否所有子项都被选中,记得使用计算属性
然后再组件中对全选框绑定:model-value
再根据全选框是否被选中,在pinia中用数组的foeEach方法,将每一个子项的状态改成全选框的状态。
计算选中的数量和单价
一定记得用计算属性!!!!
先用filter筛选出已选中的,再用reduce计算总量
接口购物车
添加商品
判断用户是否登入,已登入就调用接口购物车
没登入就用本地的购物车。使用token判断是否登入
在api中新建cart.ts
封装接口
pinia中调用接口
删除商品
同上,调用删除接口
退出登入后,清空购物车
在pinia中新增clearCart函数,把cartList数组清空
合并购物车
封装接口
// 合并购物车
export const merfeCartAPI = (data: meItemParams[])=>{
return request({
url:"/member/cart/merge",
method:'POST',
data
})
}
在user的pinia中获取用户数据的时候合并购物车
结算
在views新建checkout文件夹
配置路由
封装接口-获取数据-渲染页面
默认让isDefaul为0的那项作为默认地址
实现切换地址
复制弹框组件到对应位置,v-model双向绑定showDialog
点击按钮时让showDialog为true
地址激活交互实现
通过函数传参,得到当前点击的项,
在模版中使用:class来动态匹配active类名
获得的参数再赋值给当前显示的对象,即可实现动态切换。
顺便把接收参数的对象置空,并关闭弹框
结算
在views新建Pay文件夹
封装接口在checkout.ts
注意调用接口时的参数传递
路由跳转使用的query参数
跳转之后清空购物车
支付模块
两个关键数据,一个是左侧倒计时,超过倒计时时间就释放库存。一个是要支付的金额。
封装获取订单详情
接口获取动态id,使用接口时传递
const getOrder=async()=>{
const res=await getOrderAPI(route.query.id as string)
console.log(res);
payInfo.value=res.data.result
}
渲染
实现支付功能
跳转第三方支付宝支付
// 支付地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const backURL = 'http://127.0.0.1:5173/paycallback'
const redirectUrl = encodeURIComponent(backURL)
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`
onMounted(()=>getOrder())
支付结果展示
静态页面+配置路由
根据支付结果适配支付状态
封装倒计时函数
格式化使用dayjs插件
import dayjs from "dayjs";
import { computed, ref } from "vue";
export const useCountDown=()=>{
const time=ref(0)
const fomatTime=computed(()=>dayjs.unix(time.value).format('mm分ss秒'))
const start=(currentTime:number)=>{
time.value=currentTime
setInterval(() => {
time.value--
}, 1000);
}
return{
fomatTime,
start
}
}
优化
定时器有可能出现内存溢出,所以要对定时器做处理
在组件销毁时把定时器清掉
// 组件销毁时清除定时器
onUnmounted(()=>{
if(timer.value){
clearInterval(timer.value)
}
})
会员中心
views新建member文件夹
静态模版+路由配置
它自身还有三级路由
新建components文件夹
再建UserInfo.vue(个人信息)
封装接口+渲染
和UserOrder.vue(我的订单)
封装接口+渲染
tab栏切换
给tab栏绑定tab-change事件,并获取激活时返回的值
<el-tabs @tab-change="tabChange">
修改发请求时的参数orderState
// tab栏切换
const tabChange=(type:number)=>{
// console.log(type);
params.value.orderState=type
getUserOrder()
}
分页
得到所有条数并渲染,绑定一些属性,添加切换函数
<el-pagination :total="total" :page-size="params.pageSize" @current-change="pageChange" background layout="prev, pager, next" />
组件中回调函数pageChange里面修改参数的page,再重新拉数据
// 页数切换
const pageChange=(page:number)=>{
console.log(page);
params.value.page=page
getUserOrder()
}
更多推荐


所有评论(0)