Vue3+ts 封装一个 tab标签页组件

前言:

在写项目的时候,用到了element-plus,在这个项目中需要使用tabs组件,但是使用element-plus 的tab组件来满足项目中的tab样式,有的时候会有点麻烦,且自己也想动手试试封装一个tab组件,于是,开干!!

最终效果图:

这里面的选中下横线用的是绝对定位

 <div class="indicator" :style="indicatorStyle"></div>

 .indicator {

    position: absolute;

    bottom: 0;

    height: 2px;

    background-color: @themeColor;

    transition: transform 0.3s ease;

  }

其中还需要去计算他的宽度 width 和 移动量 transform 

/**
 *这里通过computed 来计算 
 * index :标记选中的是第几个

 * tabs :获取所有 tab的信息

 * translateX:横向移动的数据

 * width:单个tab的款宽度---注意:如果需要在选中tab的后面加一个标识,比如:“当前”,则  * width和translate 计算有误,后面会出兼容版本
 */

const indicatorStyle = computed(() => {

  const panelParent = myTabsRef.value

  if (panelParent) {

    let index: number

    if (dps.isIndex) {

      index = Number(dps.active)

    } else {

      index = dps.data.findIndex((r: any) => r[getDefaultProps.value.id] === dps.active)

    }

    const tabs = panelParent.querySelectorAll('.myTab__header_item')

    let translateX = 0, width = 0

    // 移动指示器到选中的标签

    if (tabs.length && index !== -1) {

      const selectedTab = tabs[index]

      const selectedTabRect = selectedTab.getBoundingClientRect()

      width = selectedTabRect.width

      const tabsHeaderRect = selectedTab.parentNode.getBoundingClientRect()

      translateX = selectedTabRect.left - tabsHeaderRect.left

    }

    return {

      transform: `translateX(${translateX}px)`,

      width: `${width}px`

    }

  }

  return {}

})

具体代码:

<script setup lang="ts">
import { ref, computed } from 'vue'

const emits = defineEmits(['update:active'])

interface Props {
  data: any // tab的数组数据
  active: string | number// 选中标识
  isIndex?: boolean
  props?: any// 对象数组的对象里面的属性对应
  padBottom:string // 高亮横线距离tab文字的距离 
}
const dps = withDefaults(defineProps<Props>(), {
  data: [],
  active: '',
  padBottom:'14px'
})

const getDefaultProps = computed(() => {
  return dps.props || {
    id: 'elementId',
    name: 'elementName'
  }
})

const myTabsRef = ref()
const indicatorStyle = computed(() => {
  const panelParent = myTabsRef.value
  if (panelParent) {
    let index: number
    if (dps.isIndex) {
      index = Number(dps.active)
    } else {
      index = dps.data.findIndex((r: any) => r[getDefaultProps.value.id] === dps.active)
    }
    const tabs = panelParent.querySelectorAll('.myTab__header_item')
    let translateX = 0, width = 0
    // 移动指示器到选中的标签
    if (tabs.length && index !== -1) {
      const selectedTab = tabs[index]
      const selectedTabRect = selectedTab.getBoundingClientRect()
      width = selectedTabRect.width
      const tabsHeaderRect = selectedTab.parentNode.getBoundingClientRect()
      translateX = selectedTabRect.left - tabsHeaderRect.left
    }
    return {
      transform: `translateX(${translateX}px)`,
      width: `${width}px`
    }
  }
  return {}
})

const onTabClick = (item: any, index: number) => {
  emits('update:active', { ...item, index: index })
}
</script>

<template>
  <div class="myTab tabs-container" ref="myTabsRef" v-if="data.length">
    <div class="myTab__header">
      <div class="indicator" :style="indicatorStyle"></div>
      <div class="myTab__header_box">
        <div class="myTab__header_item" :style="{paddingBottom:padBottom}" :class="{ 'active': active === (isIndex ? index : item[getDefaultProps.id]) }"
          v-for="(item, index) in data" :key="index" @click="onTabClick(item, index)">
          <slot name="label" :index="index" :row="item">{{ item[getDefaultProps.name] }}</slot>
        </div>
      </div>
    </div>
    <slot name="right"></slot>
  </div>
</template>

<style lang="less" scoped>
.myTab {
  display: flex;
  justify-content: space-between;
  column-gap: 12px;
}

.myTab__header {
  position: relative;
  border-bottom: 1px solid #F6F7FA;
  flex: 1;
  overflow-x: auto;

  &:hover::-webkit-scrollbar-thumb {
    background: #dddddd;
  }

  &_box {
    flex: 1;
    white-space: nowrap;
  }

  .indicator {
    position: absolute;
    bottom: 0;
    height: 2px;
    background-color: @themeColor;
    transition: transform 0.3s ease;
  }

  &_item {
    display: inline-block;
    // padding-bottom: 14px;
    min-width: 56px;
    margin-left: 24px;
    text-align: center;
    color: #666;
    cursor: pointer;

    &:first-child {
      margin-left: 0;
    }

    &.active {
      font-weight: 600;
    }

    &.active,
    &:hover {
      color: @themeColor;
    }
  }
}
</style>
 

以上就是完整代码了

单个tab的款宽度-需注意:如果需要在选中tab的后面加一个标识,比如:“当前”,则   width和translate 计算有误,后面会出兼容版本

后续会出兼容版本,尽情期待

<think>好的,用户想学习如何使用TypeScript封装一个通用的Vue路由跳转方法。首先,我需要回顾一下用户之前的对话历史。用户之前询问了Vue路由跳转方式大全,我给出了详细的回答,涵盖了声明式导航、编程式导航、动态路由匹配等内容,并提到了使用TypeScript封装axios的例子。现在用户希望将类似的封装思路应用到路由跳转上,用TypeScript来增强代码的可维护性和类型安全。 接下来,我需要分析用户的具体需求。用户需要的是一个通用的路由跳转方法,这意味着这个方法应该能够处理不同的跳转场景,比如路径跳转、命名路由、传递参数等,同时要符合TypeScript的类型要求。此外,用户可能需要这个方法简化重复代码,提高代码复用性,并且在封装过程中加入类型检查,避免运行时错误。 然后,我要考虑如何结合用户提供的引用内容。引用[1]提到Vue Router的路由守卫和TypeScript的类型注解,引用[3]讨论了使用TypeScript封装axios的经验,这些都是有用的参考。用户可能希望封装的跳转方法能像axios封装那样,提供清晰的类型定义和错误处理。同时,引用[4]提到路由守卫的应用,可能在封装中需要考虑导航守卫的集成,但用户当前的需求可能更侧重于基础封装,所以暂时可以不涉及守卫部分。 我需要确定封装的核心功能点。根据用户之前的回答,路由跳转主要涉及push、replace等方法,支持路径、命名路由、参数传递等。因此,封装的函数需要处理这些情况,并添加类型约束。例如,函数需要区分路径跳转和命名路由跳转,因为命名路由需要name属性,而路径跳转则需要path属性。TypeScript的类型系统可以帮助在编译时检查这些参数是否正确。 接下来要考虑TypeScript的类型定义。可能需要定义一个联合类型RouteTarget,包含路径跳转和命名路由跳转的参数类型。然后,封装一个函数,接受这个类型作为参数,并调用Vue Router的相应方法。同时,需要处理参数传递,比如params和query的区别,确保在命名路由中使用params,而在路径跳转中使用query,或者根据需要进行调整。 然后,需要处理错误情况。路由跳转可能会失败,比如路径不存在,或者参数缺失。因此,在封装函数中加入错误处理机制,比如try-catch块,或者返回Promise以便调用者处理异常。同时,利用TypeScript的类型断言或类型保护来确保传入的参数符合预期。 另外,用户可能希望这个封装的方法能集成到Vue的原型中,方便在组件中通过this.$router调用,或者作为一个独立的工具函数导出。需要根据Vue的插件系统,将封装的方法注入到Vue实例中,或者在Composition API中作为可组合函数使用。 还需要考虑示例代码的结构。用户需要看到具体的实现步骤,包括类型定义、函数封装、错误处理、使用示例等。代码需要符合TypeScript的最佳实践,比如使用枚举定义路由名称,使用接口定义参数类型,避免使用any类型,确保类型安全。 最后,验证示例代码的正确性。确保代码在Vue3和TypeScript环境下能够运行,处理各种跳转情况,并且类型检查无误。可能需要测试不同的跳转方式,如路径跳转、命名路由传参、query参数等,确保封装的方法覆盖这些场景。 总结来说,我需要将Vue Router的跳转方法进行类型封装,提供清晰的类型定义和错误处理,确保代码的健壮性和可维护性,同时给出实际的使用示例,帮助用户理解如何在实际项目中应用这个封装后的方法。</think>#### 基于 TypeScript 的路由跳转方法封装Vue3 实现) --- ### 一、基础类型定义 ```typescript // types/router.d.ts import { RouteLocationRaw } from &#39;vue-router&#39; /** 路由跳转目标类型 */ type RouteTarget = | string // 直接路径 | { path: string, query?: Record<string, string | number> } // 路径+查询参数 | { name: string, params?: Record<string, string | number> } // 命名路由+参数 /** 路由跳转配置选项 */ interface RouterOptions { replace?: boolean // 是否替换当前路由 newTab?: boolean // 是否新标签页打开 } ``` --- ### 二、核心封装实现 ```typescript // utils/router.ts import { Router } from &#39;vue-router&#39; /** 封装的路由跳转方法 */ export class RouterHelper { private router: Router constructor(router: Router) { this.router = router } /** 通用跳转方法 */ async navigate(target: RouteTarget, options: RouterOptions = {}): Promise<void> { try { const routeLocation = this.parseTarget(target) if (options.newTab) { return this.openNewTab(routeLocation) } if (options.replace) { await this.router.replace(routeLocation) } else { await this.router.push(routeLocation) } } catch (error) { console.error(&#39;[Router Error]&#39;, error) throw new Error(`路由跳转失败: ${(error as Error).message}`) } } /** 解析跳转目标 */ private parseTarget(target: RouteTarget): RouteLocationRaw { if (typeof target === &#39;string&#39;) { return { path: target } } // 类型保护检查 if (&#39;name&#39; in target && target.params) { return { name: target.name, params: target.params } } if (&#39;path&#39; in target && target.query) { return { path: target.path, query: target.query } } throw new Error(&#39;非法的路由跳转参数格式&#39;) } /** 新标签页打开 */ private openNewTab(location: RouteLocationRaw): void { const route = this.router.resolve(location) window.open(route.href, &#39;_blank&#39;) } } ``` --- ### 三、使用示例 ```typescript // main.ts 初始化 import { createApp } from &#39;vue&#39; import { createRouter } from &#39;vue-router&#39; import { RouterHelper } from &#39;./utils/router&#39; const app = createApp(App) const router = createRouter({ /* 路由配置 */ }) app.use(router) // 挂载到全局属性 app.config.globalProperties.$routerHelper = new RouterHelper(router) ``` --- ### 四、组件中使用 ```typescript // 在.vue组件中 import { defineComponent } from &#39;vue&#39; export default defineComponent({ methods: { // 跳转到用户详情(命名路由) goToUserDetail() { this.$routerHelper.navigate({ name: &#39;user-detail&#39;, params: { id: 123 } }) }, // 带查询参数的跳转 searchProducts() { this.$routerHelper.navigate({ path: &#39;/search&#39;, query: { keyword: &#39;TypeScript&#39; } }, { replace: true }) } } }) ``` --- ### 五、关键实现说明 1. **类型安全** - 通过`RouteTarget`联合类型确保只能传递合法参数 - 使用`RouteLocationRaw`继承Vue Router原生类型 - 通过类型保护检查确保参数有效性[^3] 2. **功能扩展** - 支持常规跳转/替换跳转 - 支持新标签页打开(自动处理路由解析) - 统一错误处理机制 3. **最佳实践** - 通过类封装保持状态隔离 - 使用async/await处理异步导航 - 兼容Vue3组合式API和选项式API[^2] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值