小兔鲜----商城项目

配置路由:

一级路由:

首页和登入页

二级路由:

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

sku是库存管理中最小的常用单位

为了让用户能够选择商品的规格,选择后选中状态要更新

渲染组件

点击规格更新选中状态

新增selected属性,JS允许直接增加对象没有的属性

当前如果是已经激活的就改成未激活,

如果未激活就先把同一行的状态都改成未激活,再把当前的改成激活。

再在组件中使用:class动态给样式

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数组,库存大于0即为有效字段。

得到有效的SKU数组以后对其forEach遍历出所有sku的最底层的每个选项的 名称,如黑色

对最底层的每个选项使用PowerSet算法得到所有子集,

根据自己生成路径字典对象。先设置一个pathMap对象,遍历子集并转换成字符串作为pathMap 的键名,对每一个sku遍历的时候检查其是否有此键名,有点话追加sku.id,没有则直接赋值。

// 创建生成路径字典对象函数
const getpathMap = (goods) => {
  const pathMap = {}
  // 1. 得到所有有效的Sku集合 
  const effectiveSkus = goods.skus.filter(sku => sku.inventory > 0)
  // 2. 根据有效的Sku集合使用powerSet算法得到所有子集 [1,2] => [[1], [2], [1,2]]
  effectiveSkus.forEach(sku => {
    console.log(sku);
    
    // 2.1 获取可选规格值数组
    const selectedValArr = sku.specs.map(val => val.valueName)
    // 2.2 获取可选值数组的子集
    const valueArrPowerSet = powerSet(selectedValArr)
    // 3. 根据子集生成路径字典对象
    // 3.1 遍历子集 往pathMap中插入数据
    valueArrPowerSet.forEach(arr => {
      // 根据Arr得到字符串的key,约定使用-分割 ['蓝色','美国'] => '蓝色-美国'
      const key = arr.join('-')
      // 给pathMap设置数据
      if (pathMap[key]) {
        pathMap[key].push(sku.id)
      } else {
        pathMap[key] = [sku.id]
      }
    })
  })
  
  return pathMap
}

初始化规格禁用

遍历每一个规格,用specs的name作为key在pathMap中做匹配,匹配不上就禁用。

点击选择了一些规格后,某个规格禁用

每个规格有确定的位置,从上往下

1.先写一个函数来收集所有的规格字段,对每一行做遍历,被选中的就加上其规格name,否则是undefined。

// 获取选中匹配数组 ['黑色',undefined,undefined]
const getSelectedValues = (specs) => {
  const arr = []
  specs.forEach(spec => {
    const selectedVal = spec.values.find(value => value.selected)
    arr.push(selectedVal ? selectedVal.name : undefined)
  })
  return arr
}

2.

const updateDisabledState = (specs, pathMap) => {
  // 约定:每一个按钮的状态由自身的disabled进行控制
  specs.forEach((spec,index)=>{
    const selectedValues=getSelectedValues(specs)
    spec.values.forEach(val=>{
      selectedValues[index]=val.name
      const key=selectedValues.filter(value=>value).join('-')
      if(pathMap[key]){
        val.disabled=false
      }else{
        val.disabled=true
      }
    })
  })
}

产出数据,

判断数组中是否有undefined,有的话说明数据不完整,不能产出,否则产出

const changeSku = (item, val) => {
  // 省略...
  // 产出SKU对象数据
  const index = getSelectedValues(goods.value.specs).findIndex(item => item === undefined)
  if (index > -1) {
    console.log('找到了,信息不完整')
  } else {
    console.log('没有找到,信息完整,可以产出')
    // 获取sku对象
    const key = getSelectedValues(goods.value.specs).join('-')
    const skuIds = pathMap[key]
    console.log(skuIds)
    // 以skuId作为匹配项去goods.value.skus数组中找
    const skuObj = goods.value.skus.find(item => item.id === skuIds[0])
    console.log('sku对象为', skuObj)
  }
}

直接用现成的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()

}

拓展-SKU组件

### Vue3 前端项目 小兔 案例教程 Vue3 是一个现代化的前端框架,具有许多新特性和改进,使得开发更加高效和灵活。以下是关于 Vue3 前端项目中“小兔”案例的相关信息。 #### 1. 热门品牌推荐栏目实现 在“小兔项目的热门品牌推荐栏目中,开发者需要根据业务需求设计组件结构并实现数据展示功能。此部分可以通过组合式 API 来完成,例如使用 `ref` 和 `reactive` 来管理状态。以下是一个简单的代码示例: ```vue <script setup> import { ref } from 'vue' const brands = ref([ { id: 1, name: '品牌A', logo: 'logo-a.png' }, { id: 2, name: '品牌B', logo: 'logo-b.png' }, { id: 3, name: '品牌C', logo: 'logo-c.png' } ]) </script> <template> <div class="brand-list"> <div v-for="brand in brands" :key="brand.id" class="brand-item"> <img :src="brand.logo" :alt="brand.name" /> <span>{{ brand.name }}</span> </div> </div> </template> <style scoped> .brand-list { display: flex; gap: 16px; } .brand-item { text-align: center; } </style> ``` 这段代码展示了如何通过 Vue3 的组合式 API 创建一个品牌列表,并动态渲染品牌信息[^1]。 #### 2. 路由配置与页面布局 在 Vue3 中,路由的配置通常通过 `vue-router` 实现。以下是“小兔项目中 App.vue 的基本结构: ```vue <script setup> </script> <template> <div id="app"> <!-- 一级路由出口 --> <router-view></router-view> </div> </template> <style scoped> </style> ``` 此代码片段展示了如何设置一个基础的路由出口,用于加载不同页面的内容[^2]。 #### 3. 组件交互与状态管理 在 Vue3 中,状态管理可以使用组合式 API 或 Pinia 等工具。以下是一个简单的按钮计数器示例,展示了如何使用 `ref` 来管理组件内部的状态: ```vue <script setup> import { ref } from 'vue' const count = ref(0) const addCount = () => count.value++ </script> <template> <button @click="addCount">{{ count }}</button> </template> <style scoped> </style> ``` 这段代码展示了如何通过 Vue3 的组合式 API 实现一个简单的按钮计数器功能[^3]。 #### 4. 项目实战建议 - **模块化开发**:将项目拆分为多个模块,每个模块负责特定的功能。 - **组件复用**:设计通用组件以提高代码复用性。 - **状态管理**:对于复杂的状态管理,可以考虑使用 Pinia 或 Vuex。 - **性能优化**:利用 Vue3 的新特性(如 Fragment、Teleport 和 Suspense)来优化性能。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值