配置路由:

一级路由:

首页和登入页

二级路由:

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

}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐