UI组件库vant3(移动端用的比较多)

官网:vant-contrib.gitee.io/vant/v3/#/z...

1. 安装

npm i vant@latest-v3 安装vant组件库

npm i unplugin-vue-components -D 安装vant按需引入插件。vite.config.ts中引入集成

npm i postcss-pxtorem -D安装移动端适配插件,将px单位转化为rem单位。vite.config.ts中引入集成

npm i amfe-flexible -D安装移动端适配插件,适配不同屏幕尺寸。main.ts中引入

2. vite.config.ts中集成配置插件

javascript 复制代码
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite'//引入按需引入vant插件
import { VantResolver } from 'unplugin-vue-components/resolvers'//引入按需引入vant插件

import postCssPxToRem from 'postcss-pxtorem'//引入移动端适配插件

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],//集成按需引入vant插件
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css:{
    postcss:{
      plugins:[
        postCssPxToRem({//自适应,px转rem
          rootValue:75,//换算的基数(设计图750的根字体为75)
          propList:['*']//需要转换的属性,*代表全部
        })
      ]
    }
  },
  server:{
    proxy: {
      '/api': {
        target: 'http://124.71.63.13:8088/',
        changeOrigin: true,
        // rewrite: path => path.replace(/^\/api/, '')
      }
    }
  },
})

3. main.ts导入vant函数组件样式,引入适配插件(因为使用函数组件时,unplugin-vue-components无法自动引入对应的样式,需手动引入样式。)

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import 'normalize.css' // 重置样式

// 导入vant函数组件样式
import 'vant/es/toast/style'
import 'vant/es/dialog/style'
import 'vant/es/notify/style'
import 'vant/es/image-preview/style'

//引入amfe-flexible屏幕适配插件
import 'amfe-flexible'

const app = createApp(App)
app.use(store)
app.use(router)

app.mount('#app')

4. 解决引入vue组件ts报错(因为ts不识别.vue文件,需在env.d.ts中声明)

env.d.ts

typescript 复制代码
// <reference types="vite/client" />
declare module '*.vue' {
    import { DefineComponent } from 'vue'
    const component: DefineComponent<{}, {}, any>
    export default component
}

5. 项目中使用组件示例

按钮组件van-button、Toast轻提示、导航栏组件van-nav-bar、单元格组件van-cell、商品卡片van-card、

列表组件van-list(上拉加载更多)、下拉刷新van-pull-refresh、轮播van-swipe/van-swipe-item

分类选择van-tree-select、宫格组件van-grid/van-grid-item

标签栏van-tabbar/van-tabbar-item

滑动单元格van-swipe-cell、步进器van-stepper

示例一:

Home.vue

xml 复制代码
<template>
  <!-- 按钮组件 -->
  <van-button type="primary" @click="bindConfirm">主要按钮</van-button>

  <!-- nav-bar导航栏组件 -->
  <van-nav-bar title="首页" />

  <!-- list列表组件
  通过 loading 和 finished 两个变量控制加载状态
  滚动到底部时,会触发 load 事件。
  loading-true进行异步更新--loading-false更新完成
  数据全部加载完毕,则直接将 finished 设置成 true
  -->

  <!-- <van-list
      v-model:loading="loading"
      :finished="finished"
      @load="onLoad"
      finished-text="没有更多了"
  >
      <van-cell v-for="item in list" :key="item.id" :title="item.product" />
  </van-list> -->
  <van-card v-for="item in list" :key="item.id"
        :num="item.putaway"
        :price="item.price"
        :desc="item.detail"
        :title="item.product"
        :thumb="item.picture"
        :origin-price="item.oldprice"
      />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Toast } from 'vant'
import { RequestShopList } from '@/api/index'

//Toast轻提示
const bindConfirm = () =>{
  Toast('轻提示内容')
}

// 商品列表数据
const list = ref([])

// 1.触发 load 事件,loading 自动设置成 true。
// 2.此时可以发起异步操作并更新数据
// 3.数据更新完毕后,将 loading 设置成 false
const loading = ref(false)

// 若数据已全部加载完毕,则直接将 finished 设置成 true 即可
const finished = ref(false)

// 当组件滚动到底部时,会触发 load 事件
const onLoad = () => {
      getProductList(1, 15)
}

const getProductList = async (pageNo: number, pageSize: number) => {
  const data = await RequestShopList(pageNo, pageSize)
  const { resultCode, resultInfo } = data
  if (resultCode === 1) {
    list.value = resultInfo.list
  } else {
      // 数据全部加载完成
      finished.value = true
  }
  // 加载状态结束
  loading.value = false
}
</script>
<style scoped></style>
示例二:(购物商城APP示例。ts类型定义,hooks组合式函数封装)
types文件夹→types.ts ts类型定义
typescript 复制代码
export interface IResponse {
    resultCode: number
    resultInfo: any
}

/**
 * 商品
 */
export interface IGoods {
    id: number
    shop: string
    picture: string
    product: string
    price: number
    oldprice: number
    putaway: number
    detail: string
    categoryname: string
    categoryId: number
}

/**
 * 轮播
 */
export interface IBanner {
    id: number
    url: string
    content: string
}

/**
 * 商品分类
 */
export interface ICategory {
    id: number
    name: string
    text: string
}

/**
 * 购物车商品
 */
export interface ICartGoods {
    id:number
    name:string // 商品名称
    price:number // 价格
    url:string //图片地址
    num:number //数量
    state:boolean //状态
}

/**
 * 账户
 */
export interface IAccount{
    id:string
    nick:string
    name?:string
    url:string
}
hooks文件夹→home.ts首页商品列表

hooks函数中调用引入并调用另一个hooks函数,会重复执行两次

解决1:定义一个全局开关变量,执行一次后关闭。但是会引起另一个问题,刷新数据会丢失。

解决2:不直接引入,通过vue文件传参形式传入并调用

typescript 复制代码
import { ref, type Ref, onMounted } from 'vue'
import { RequestShopList, RequestBanner } from '@/api/index'
import type { IGoods, IBanner } from '@/types/types'
import { useRouter } from 'vue-router'

//let isBanner = true//全局开关变量
/**
 * 轮播
 */
export const useBanner = () => {
    // 轮播列表数据
    const bannerList: Ref<Array<IBanner>> = ref([])
    /**
    * 轮播
    */
    const getBanner = async () => {
        const data = await RequestBanner()
        const { resultCode, resultInfo } = data
        if (resultCode === 1) {
            bannerList.value = resultInfo.list
        }
    }
    onMounted(() => {
       // if(isBanner){
            getBanner()
           // isBanner = false
       // }
    })
    return { bannerList, getBanner }
}

/**
 * 商品列表
 *   商品列表显示
 *   加载更多
 *   下拉刷新
 *   hooks组全式函数
 */
export const useGoods = (getBanner:Function) => {
    // 商品列表数据
    const list: Ref<Array<IGoods>> = ref([]) //ref()返回ref对象的数据类型是Ref
    // 加载数据状态,true表示加载中,false加载完成
    const loading: Ref<boolean> = ref(false)
    // 加载完成数据状态, 所有数据加载完成设置为true
const finished: Ref<boolean> = ref(false)

    // 下拉刷新状态
    const refreshLoading:Ref<boolean> = ref(false)

    // 当前页号
    let currentNo: number = 1

    /**
    * 1.第一次进入自动调用,loadding设置为true,
    * 2.触底自动调用
    */
    const onLoad = () => {
        getProductList(currentNo++, 10)
    }

    /**
     * 下拉刷新
     */
   // const { getBanner } = useBanner()
    const onRefresh = () => {
        // 重置
        currentNo = 1
        finished.value = false
        loading.value = false
        getProductList(currentNo, 10)
        currentNo++ //刷新后currentNo加一,加载更多从第二页开始
        getBanner()
    }

    /*
    *商品列表
    * @param pageNo
    * @param pageSize
    */
    const getProductList = async (pageNo: number, pageSize: number) => {
        const data = await RequestShopList(pageNo, pageSize)
        // 关闭下拉刷新, 手动下拉刷新时自动设置为true
        if( refreshLoading.value){
            list.value = []
            refreshLoading.value = false
        }

        const { resultCode, resultInfo } = data
        if (resultCode === 1) {
            list.value = [...list.value, ...resultInfo.list]
            // list.value = resultInfo.list
        }

        loading.value = false // loadding加载完成|关闭下拉刷新
        if (resultCode === -1) {
            finished.value = true //所有数据加载完成
        }
    }

    // 模板使用的数据方法
    return { list, loading, finished,refreshLoading, onLoad, onRefresh }
}

/**
 * 商品详情
 */
export const useGoodsDetail = ()=>{
    const router = useRouter()
    //跳转到商品详情页
    const bindGoodsDetail =(id:number)=>{
        router.push({path:'detail',query:{id}})
    }
    return{bindGoodsDetail}
}
Home.vue首页商品列表
xml 复制代码
<template>

  <van-nav-bar title="首页" />

<!-- <van-search

      v-model="value"

      shape="round"

      background="#f50718"

      placeholder="请输入搜索关键词"

  /> -->

  <SearchHome></SearchHome>

  <van-pull-refresh v-model="refreshLoading" @refresh="onRefresh">

    <!-- 轮播 -->

    <van-swipe :autoplay="3000" lazy-render>

        <van-swipe-item v-for="item in bannerList" :key="item.id">

            <img style="height: 150px; width: 100%" :src="item.url" />

        </van-swipe-item>

    </van-swipe>

    <!-- 商品列表 -->

    <van-list

      v-model:loading="loading"

      :finished="finished"

      finished-text="没有更多了"

      @load="onLoad"

    >

      <van-card v-for="item in list" :key="item.id"

        :num="item.putaway"

        :price="item.price"

        :desc="item.detail"

        :title="item.product"

        :thumb="item.picture"

        :origin-price="item.oldprice"

        @click="bindGoodsDetail(item.id)"

      />

    </van-list>

</van-pull-refresh>

</template>

<!-- ts编程,写代码时,变量指定数据类型,函数参数返回值类型,

 如果没有显示指定,ts类型推论 -->

<script setup lang="ts">

import { useBanner,useGoods,useGoodsDetail } from '@/hooks/home'

const { bannerList,getBanner } = useBanner()

const {goodsList,loading,finished,refreshLoading,onLoad,onRefresh} = useGoods(getBanner)

const {bindGoodsDetail} = useGoodsDetail()

</script>

<style scoped></style>
SearchHome.vue修改第三方组件
xml 复制代码
<template>

    <van-nav-bar>

        <template #left>

            <router-link to="/city">城市</router-link>

        </template>

       

        <template #title>

            <router-link to="/search">搜索</router-link>

        </template>

        <template #right> 

            <van-image v-if="accountStore.account.url" :src="accountStore.account.url" round type="contain" width="40px" height="40px" />

            <router-link v-else to="/login">登录</router-link>

        </template>

 

    </van-nav-bar>

</template>

<script setup lang="ts">

import { useAccountStore } from '@/stores/user'

const accountStore = useAccountStore()

console.log('url ',accountStore.account.url);

</script>

<style scoped lang="scss">

.van-nav-bar{

    background-color: #d30607;

    .van-nav-bar__left,.van-nav-bar__right{

        a{

            color: white;

            font-weight: bold;

        }

    }

    //样式穿透

    :deep(.van-nav-bar__title){

        background-color: white;

        width: 80%;

        border-radius: 30px;

        height: 35px;

        line-height: 35px;

        box-sizing: border-box;

        text-align: left;

        padding-left: 30px;

        font-size: 14px;

        a{

            color: gray;

        }

    }

}

</style>
hooks文件夹→login.ts登录
typescript 复制代码
import { ref,type Ref } from 'vue'

import { RequestLogin } from '@/api/home'

import { useAccountStore } from '@/stores/user'

import type{ IAccount} from '@/types/types' 

import { Toast } from 'vant'

import { useRouter} from 'vue-router'

export const useAccount = ()=>{

    // const username = ref<string>('')

    const username:Ref<string> = ref('')

    const password:Ref<string> = ref('')

    const accountStore = useAccountStore()

    const router = useRouter()

    const bindBack = ()=>history.back()

    /**

     * 登录

     */

    const onSubmit = async()=>{

        const data = await RequestLogin(username.value,password.value)

        const {resultCode,resultInfo,token} = data

        if(resultCode === 1){

            // 保存用户信息,主界面登录按钮换成用户头像

            const account:IAccount = {id:resultInfo.id,nick:resultInfo.nick,url:resultInfo.headerimg}

            accountStore.saveAccount(account)

            // 保存token

            sessionStorage.setItem('TOKEN',token as string)

            // 登录成功提示,跳转到主界面

            Toast.success('登录成功!')

            router.replace('/')

        }else{

            Toast.fail('账户出错!')

        }

    }

    return {username,password, bindBack,onSubmit}

}
Login.vue登录
ini 复制代码
<template>

    <van-nav-bar

        title="登录界面"

        left-text="返回"

        left-arrow

        @click-left="bindBack"

    />

    <van-form @submit="onSubmit">

        <van-cell-group inset>

            <van-field

                v-model="username"

                name="username"

                label="用户名"

                placeholder="用户名"

                :rules="[{ required: true, message: '请填写用户名' }]"

            />

            <van-field

                v-model="password"

                type="password"

                name="password"

                label="密码"

                placeholder="密码"

                :rules="[{ required: true, message: '请填写密码' }]"

            />

        </van-cell-group>

        <div style="margin: 16px">

            <van-button round block type="primary" native-type="submit">

                确定

            </van-button>

        </div>

    </van-form>

</template>

<script setup lang="ts">
import { useAccount } from '@/hooks/login'
const {username,password ,bindBack,onSubmit } = useAccount()
</script>
<style scoped></style>
hooks文件夹→category.ts商品分类
typescript 复制代码
import { ref, type Ref,onMounted } from 'vue'
import { RequestCategory, RequestGoodsByCategoryId } from '@/api/Category'
import type { ICategory , IGoods} from '@/types/types'

/**

 * 商品分类

 */

export const useCategory = () =>{

    //右侧索引

    const activeId:Ref<number> = ref(1)

    //左侧索引

    const activeIndex:Ref<number> = ref(0)

    // 根据分类id获取的商品列表

    const goodsList:Ref<Array<IGoods>> = ref([])

    // 商品分类列表

    const categoryList: Ref<Array<ICategory>> = ref([])

    /**

     * 商品分类

     */

    const getCategory = async()=>{

        const data = await RequestCategory()

        const {resultCode,resultInfo} = data

        if(resultCode === 1){

            //左侧数据过滤

            //组件库中数据与接口中数据不同,因此要遍历返回一个新数组

            // categoryList.value = resultInfo.list

            categoryList.value = resultInfo.list.map((item:ICategory)=>{

                return {id:item.id,name:item.name,text:item.name}

            })

            // 加载第一件商品

            const categoryId = categoryList.value[0].id

            getGoodsByCategoryId(categoryId)

        }

    }

     /**

     * 根据分类id获取商品列表

     */

     const getGoodsByCategoryId = async (categoryId: number) => {

        const data = await RequestGoodsByCategoryId(categoryId)

        const { resultCode, resultInfo } = data

        if (resultCode === 1) {

            goodsList.value = resultInfo.list

        }

    }

    /**

     * 切换分类

     */

    const bindClickNav = (index:number) => {

        const categoryId = categoryList.value[index].id

        getGoodsByCategoryId(categoryId)

    }

    onMounted(()=>{

        getCategory()

    })

    return{activeId,activeIndex,goodsList,categoryList,bindClickNav}

}
Category.vue商品分类
xml 复制代码
<template>

    <van-nav-bar title="分类" />

    <van-tree-select 

    v-model:active-id="activeId"

    v-model:main-active-index="activeIndex"

    height="90%" 

    :items="categoryList"

    @click-nav="bindClickNav"

    >

    <template #content>

        <!-- <van-card

            v-for="item in goodsList"

            :key="item.id"

            :price="item.price"

            :title="item.product"

            :thumb="item.picture"

        /> -->

        <van-grid :border="false" :column-num="3">

            <van-grid-item v-for="item in goodsList" :key="item.id">

                <div class="g-goods">

                    <img :src="item.picture" :alt="item.product" />

                    <p>{{ item.product }}</p>

                </div>

            </van-grid-item>

        </van-grid>

    </template>

    </van-tree-select>

</template>

<script lang="ts" setup>

import {useCategory} from '@/hooks/Category'

const {activeId,activeIndex,goodsList,categoryList, bindClickNav} = useCategory()

</script>

<style lang="scss" scoped>

.g-goods {

    img {

        width: 60px;

        height: 60px;

    }

    p {

        max-width: 60px;

        white-space: nowrap;

        overflow: hidden; //文本超出隐藏

        text-overflow: ellipsis; //文本超出省略号替代

    }

}

</style>
hooks文件夹→detail.ts首页商品列表点击跳转到商品详情
typescript 复制代码
import {ref, type Ref, onMounted} from 'vue'

import type {ICartGoods,IGoods} from '@/types/types'

import {RequestDetail} from '@/api/Detail'

import { useCartStore } from '@/stores/cart'

import { useRoute } from 'vue-router'

import { Toast } from 'vant'

/**

 * 商品详情

 */

export const useDetail = ()=>{

   

    const cartStore = useCartStore()

    //路由信息对象接受参数id

    const route = useRoute()

    const id: any = route.query.id

    //商品详情数据

    const goods:Ref<IGoods> = ref({

        id: 0,

        shop: '',

        picture: '',

        product: '',

        price: 0,

        oldprice: 0,

        putaway: 0,

        detail: '',

        categoryname: '',

    })

    //返回上一页

    const bindBack = ()=>history.back()

    //商品详情接口,调用接口渲染数据

    const getDetail = async()=>{

        const data = await RequestDetail(id)

        const { resultCode, resultInfo } = data

        if (resultCode === 1) {

            goods.value = resultInfo

        }

    }

    /**

     * 加入购物车

     */

    const bindAddCart = ()=>{

        //购物车列表数据

        const cartGoods:ICartGoods = {

            id:goods.value.id,

            name: goods.value.product,

            price: goods.value.price,

            url: goods.value.picture,

            num: 1,

            state: false,

        }

        //存储到pinia中(stores-cart.ts)

        cartStore.saveGoods(cartGoods)

        Toast.success('加入购物车成功!')

    }

    onMounted(()=>{

        getDetail()

    })

    return {bindBack,goods,bindAddCart}

}
Detail.vue首页商品列表点击跳转到商品详情
xml 复制代码
<template>

    <van-nav-bar title="商品详情" 

    left-text="返回"

    left-arrow

    @click-left="bindBack"/>

    <div>

        <img :src="goods.picture" style="width:100%;height:280px;" :alt="goods.picture">

        <h2>{{goods.product}}</h2>

        <van-cell>

            <template #title>

              <span>商品价格:{{goods.price}}</span>

            </template>

            <van-button class="" round type="primary" size="small" @click="bindAddCart">加入购物车</van-button>

        </van-cell>

        <p>: </p>

       

    </div>

    <van-tabbar v-model="active">

        <van-button round  type="success" size="large" to="Cart">购物车</van-button>

    </van-tabbar>
</template>

<script setup>

import {useDetail} from '@/hooks/Detail'

const {bindBack,goods,bindAddCart} = useDetail()

</script>

<style lang="scss" scoped>

span{

    font-size: 20px;

}

</style>
hooks文件夹→cart.ts购物车
typescript 复制代码
import { useCartStore } from '@/stores/cart'
import { useRouter } from 'vue-router'

export const useCart = ()=>{

    const cartStore = useCartStore()

    const router = useRouter()

const bindBack = ()=> router.replace({path:'/'})

// const bindBack = ()=> history.back()

 

    /**

     * 删除商品

     */

    const bindDelete = (id:number) =>{

        cartStore.deleteGoods(id)

    }

    /**

     * 提交订单

     */

    const onSubmit =()=>{

    }

   

    return {cartStore,bindBack,bindDelete,onSubmit}

}
Cart.vue
xml 复制代码
<template>

    <!-- <van-nav-bar title="购物车" /> -->

    <van-nav-bar

        title="购物车"

        left-text="返回"

        left-arrow

        @click-left="bindBack"

    />

    <!-- SwipeCell 滑动单元格 -->

    <van-swipe-cell v-for="item in cartStore.list"  :key="item.id">

        <van-card

            :num="item.num"

            :price="item.price"

            :title="item.name"

            class="goods-card"

        >

            <template #thumb>

                <div class="g-thumb">

                    <van-checkbox :checked="item.state" @click="cartStore.singleCheck(item.id)"></van-checkbox>

                    <van-image :src="item.url" width="70%"></van-image>

                </div>

            </template>

            <template #footer>

                <!-- Stepper 步进器 -->

                <van-stepper

                    v-model="item.num"

                    theme="round"

                    button-size="22"

                    disable-input

                />

            </template>

        </van-card>

        <template #right>

            <van-button

                square

                text="删除"

                type="danger"

                class="delete-button"

                @click="bindDelete(item.id)"

            />

        </template>

    </van-swipe-cell>

    <van-submit-bar :price="cartStore.totalPrice" button-text="提交订单" @submit="onSubmit">

      <van-checkbox :checked="cartStore.stateAll" @click="cartStore.checkAll">全选</van-checkbox>

    </van-submit-bar>

</template>

<script setup lang="ts">

import { useCart } from '@/hooks/cart'

const { cartStore,bindBack,bindDelete,onSubmit } = useCart()

</script>

<style lang="scss" scoped>

.g-thumb {

    display: flex;

}

.delete-button{

    height:100%;

}

</style>
Layout.vue
xml 复制代码
<template>

    <!-- 子路由输出 -->

    <router-view ></router-view>

    <van-tabbar route class="g-footer" active-color="#ee0a24" inactive-color="#000">

        <van-tabbar-item replace to="/home" icon="home-o">首页</van-tabbar-item>

        <van-tabbar-item replace to="/category"  icon="search">分类</van-tabbar-item>

        <van-tabbar-item replace to="/cart"  icon="shopping-cart-o">购物车</van-tabbar-item>

        <van-tabbar-item replace to="/my" icon="friends-o">我的</van-tabbar-item>

    </van-tabbar>

</template>

<script lang="ts" setup>

</script>

<style lang="scss" scoped>

#app {

    display: flex;

    flex-direction: column;

    .g-footer {

        height: 60px;

    }

    .g-container {

        flex: 1;

    }

}

</style>
api文件夹→index.ts接口定义
typescript 复制代码
import axiosInstance from '@/utils/request'
import type{IResponse} from '@/types/types'

/**

 * 产品列表

 *   shopKey: 店铺名称

 *   productKey: 产品名称

 * @returns 

 */

export const RequestShopList = (pageNo:number,pageSize:number):Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/shop',

        params:{

            pageSize,

            pageNo,

        }

    })

}

/**

 * 轮播

 */

export const RequestBanner = ():Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/banner'

    })

}
api文件夹→category.ts接口定义
typescript 复制代码
import axiosInstance from '@/utils/request'
import type{IResponse} from '@/types/types'


/**

 * 商品分类

 */

export const RequestCategory = ():Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/category'

    })

}

/**

 * 根据分类id获取商品列表

 */

export const RequestGoodsByCategoryId = (categoryId:number):Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/shop/list',

        params:{

            categoryId

        }

    })

}
api文件夹→detail.ts接口定义
typescript 复制代码
import axiosInstance from '@/utils/request'
import type{IResponse} from '@/types/types'

/**

 * 商品详情

 */

export const RequestDetail = (id:number):Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/shop/find',

        params:{

            id

        }

    })

}
api文件夹→cart.ts接口定义
typescript 复制代码
import axiosInstance from '@/utils/request'
import type{IResponse} from '@/types/types'

/**

 * 删除商品

 */

export const RequestDeleteGoods = (id:number):Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/shop/delete',

        params:{

            id

        }

    })

}
utils文件夹→request.ts(创建axios实例,封装请求/响应拦截器)
javascript 复制代码
import axios from 'axios'
import { Toast } from 'vant';

// 服务根地址

export const baseURL = 'http://10.7.163.165:8089'

/**

 * 创建axios实例

 *   封装baseURL

 */

const axiosInstance = axios.create({

    baseURL, // 服务根地址

    timeout: 3000, // 超时时间

})

/**

 * 请求拦截器

 */

axiosInstance.interceptors.request.use(

    config => {

        const token = localStorage.getItem('TOKEN')

        if(token){

            config.headers['Authorization'] = token

        }

        return config

    },

    error => {

        return Promise.reject(error)

    }

)

/**

 * 响应拦截器

 */

axiosInstance.interceptors.response.use(

    response => {

        return response.data

    },

    error => {

        const { response } = error

        if (response) {

            const status = response.status

            switch (status) {

                case 404:

                    Toast('资源不存在 404')

                    break

                case 401:

                    Toast('Unauthorized 身份验证凭证缺失!')

                    break

                case 403:

                    Toast('403 Forbidden - 拒绝访问!')

                    break

                case 500:

                    Toast('服务器出错')

                    break

                default:

                    Toast('出现异想不到的错误!')

                    break

            }

        }else {

            // 说明服务器连结果都没有返回,可能的原因有两种:

            /**

             * 1. 服务器崩掉了

             * 2. 前端客户端断网状态

             */

            if (!window.navigator.onLine) {

                // 判断为断网,可以跳转到断网页面

                Toast('网络不可用,请检查您的网络连接!')

                return

            } else {

                Toast('连接服务端出错!' + error?.message)

                return Promise.reject(error)

            }

        }

        return Promise.reject(error)

    }

)

export default axiosInstance
router文件夹→index.ts路由封装
php 复制代码
import { createRouter, createWebHistory } from 'vue-router'

import Home from '@/views/Home.vue'

import Layout from '@/views/Layout.vue'

import Category from '@/views/Category.vue'

import Cart from '@/views/Cart.vue'

import My from '@/views/My.vue'

const router = createRouter({

  history: createWebHistory(import.meta.env.BASE_URL),

  routes: [

    // {

    //   path: '/',

    //   component: Home,

    // },

    // {

    //   path: '/home',

    //   name: 'home',

    //   component: Home

    // },

    {

      path: '/',

      component: Layout,

      redirect: 'home',

      children: [

        {

          path: '/home',

          name: 'home',

          component: Home

        },

        {

          path: '/category',

          name: 'category',

          component: Category,

        },

        {

          path: '/cart',

          name: 'cart',

          component: Cart,

        },

        {

          path: '/my',

          name: 'my',

          component: My,

        },

      ]

    },

    {

      path:'/detail',

      name:'detail',

      component: ()=>import('@/views/Detail.vue')

    },

    {

      path: '/login',

      name: 'login',

      component: () => import('@/views/Login.vue'),

  },

  ]

})

export default router
stores文件夹→index.js(创建pinia根存储,集成插件)
javascript 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'//引入pinia持久化存储插件

const storeRoot = createPinia()
storeRoot.use(piniaPluginPersist)//集成插件
export default storeRoot
stores→cart.ts
typescript 复制代码
import { defineStore } from 'pinia'
import type { ICartGoods } from '@/types/types'

export const useCartStore = defineStore('cart', {

    state() {

        return {

            list: [] as Array<ICartGoods>,

            stateAll: false, // 全选状态

        }

    },

    actions: {

        /**

         * 全选

         */

        checkAll() {

            this.stateAll = !this.stateAll

            this.list.forEach(item => (item.state = this.stateAll))

        },

        /**

         * 单选

         */

        singleCheck(id: number) {

            const goods: ICartGoods | undefined = this.list.find(

                item => item.id === id

            )

            if (goods) {

                goods.state = !goods.state

            }

            this.stateAll = this.list.every(item => item.state)

        },

        /**

         * 保存

         */

        saveGoods(goods: ICartGoods) {

            // 相同商品数量加一

            // 判断购物车是否有此要添加商品,如果有数量加,如果没有,作为新商品添加

            const oldGoods:ICartGoods|undefined = this.list.find(item=>item.id === goods.id)

            if(oldGoods){

                oldGoods.num++

            }else{

                // 新加商品,全选状态设置false

                this.stateAll = false

                this.list.push(goods)

            }

           

        },

        /**

        * 删除商品

        */

        deleteGoods(id:number){

            const index = this.list.findIndex(item=>item.id === id)

            this.list.splice(index,1)

        },

    },

    getters: {

        totalPrice(): number {

            const sum = this.list.reduce(

                (preventvalue, currentvalue) =>

                    preventvalue +

                    (currentvalue.state

                        ? currentvalue.price * currentvalue.num

                        : 0),

                0

            )

            return Number(sum.toFixed(2))*100

        },

    },

    persist: {//持久化存储

        enabled: true,

        strategies: [

            {

                key: 'cart',

                storage: localStorage,

                paths: ['list', 'stateAll'],

            },

        ],

    },

})
stores→user.ts
php 复制代码
import { defineStore } from 'pinia'
import type{ IAccount} from '@/types/types'

export const useAccountStore = defineStore('account',{

    state(){

        return {

            account:{id:'',nick:'',url:''}

        }

    },

    actions:{

        saveAccount(account:IAccount){

            this.account = account

        }

    },

    getters:{

 

    },

    persist: {

        enabled: true,

        strategies: [

            {

                key: 'account',

                storage: localStorage,

                paths: ['account'],

            },

        ],

    },

})
相关推荐
本末倒置1832 小时前
Vue 3 开发者转型 React 指南:保姆级教程
前端·javascript·vue.js
卜凡.4 小时前
Vue是对HTML、CSS、JS的标准化、组件化和响应式的上层抽象与增强
javascript·vue.js·html
Ww.xh5 小时前
鸿蒙系统中HTML与Vue集成方案
vue.js·html·harmonyos
萧曵 丶7 小时前
Vue3组件通信全方案
前端·javascript·vue.js·typescript·vue3
前端那点事7 小时前
双Token无感刷新:Vue3 + Axios 企业级完整实现
前端·vue.js
前端那点事7 小时前
Vue Token鉴权避坑指南|5步完整实现(从生成到失效全解析)
前端·vue.js
前端那点事8 小时前
企业级Vue前端鉴权方案全解析|从Token到OAuth2.0,覆盖多端适配+权限管控
前端·vue.js
亲亲小宝宝鸭8 小时前
从Vben-Admin里面学习hooks
前端·vue.js
天蓝色的鱼鱼8 小时前
当AI开始替我写代码,我还要纠结选Vue还是React吗?
vue.js·react.js·ai编程