Skip to content

NavTabs 导航页签

主要用于后台管理系统页签栏,由于el-tabs功能不足

基础用法

内容超出可视宽度时,会自动出现左右滚动按钮。下方 Demo 支持拖动滑块调整宽度以查看效果。

页签1
页签2
页签3
页签4
页签5
页签6

当前选中:1

查看代码
vue
<template>
  <div class="demo">
    <el-slider v-model="width" :min="200" :max="500" />
    <div class="demo-nav-tabs-wrap" :style="{ width: `${width}px` }">
      <gi-nav-tabs v-model="activeValue" :data="tabList"></gi-nav-tabs>
    </div>
    <p class="demo-nav-tabs-wrap__tip">当前选中:{{ activeValue }}</p>
  </div>
</template>

<script setup lang="ts">
import type { NavTabItem } from 'gi-component'
import { ref } from 'vue'

const width = ref(360)
const activeValue = ref('1')

const tabList = ref<NavTabItem[]>([
  { label: '页签1', value: '1' },
  { label: '页签2', value: '2', disabled: true },
  { label: '页签3', value: '3' },
  { label: '页签4', value: '4' },
  { label: '页签5', value: '5' },
  { label: '页签6', value: '6' }
])
</script>

<style lang="scss" scoped>
.demo {
  margin-top: 20px;
}

.demo-nav-tabs-wrap {
  border: 1px solid var(--el-border-color);
}

.demo-nav-tabs-wrap__tip {
  margin-top: 8px;
  font-size: 12px;
  color: var(--el-text-color-secondary);
}
</style>

自定义默认插槽

页签1
页签2
页签3
页签4
页签5
页签6
页签7
页签8
页签9
页签10
页签11
页签12
查看代码
vue
<template>
  <div class="demo">
    <gi-nav-tabs v-model="activeValue" :data="tabList">
      <template #left-extra>
        <el-button size="small" type="primary">左插槽</el-button>
      </template>
      <template #default="{ item }">
        <el-space :size="4">
          <span>{{ item.label }}</span>
          <el-tag v-if="item.badge" size="small" type="warning">{{ item.badge }}</el-tag>
        </el-space>
      </template>
      <template #right-extra>
        <el-button size="small" type="primary">右插槽</el-button>
      </template>
    </gi-nav-tabs>
  </div>
</template>

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

/** 扩展 NavTabBase,演示默认插槽 item 类型推导 */
interface DemoTabItem {
  label: string
  value: string
  disabled?: boolean
  badge?: string
}

const activeValue = ref('1')

const tabList = ref<DemoTabItem[]>([
  { label: '页签1', value: '1' },
  { label: '页签2', value: '2' },
  { label: '页签3', value: '3', badge: '新' },
  { label: '页签4', value: '4' },
  { label: '页签5', value: '5' },
  { label: '页签6', value: '6' },
  { label: '页签7', value: '7' },
  { label: '页签8', value: '8' },
  { label: '页签9', value: '9' },
  { label: '页签10', value: '10' },
  { label: '页签11', value: '11' },
  { label: '页签12', value: '12' }
])
</script>

<style lang="scss" scoped>
.demo {
  width: 100%;
  border: 1px solid var(--el-border-color);
  padding: 0 10px;
  box-sizing: border-box;
  margin-top: 20px;
}

.demo__active-tip {
  font-size: 12px;
  color: var(--el-color-primary);
}
</style>

自定义默认插槽2(右键菜单)

查看代码
vue
<template>
  <div class="demo">
    <gi-nav-tabs v-model="activeValue" :data="tabList" custom>
      <template #left-extra>
        <el-button size="small" type="primary">左</el-button>
      </template>
      <template #default="{ item, active, disabled }">
        <el-dropdown :ref="(el) => setDropdownRef(item.value, el)" trigger="contextmenu" :disabled="disabled"
          @visible-change="(visible) => handleContextMenuVisible(visible, item.value)">
          <gi-tag :type="active ? 'dark' : 'light-outline'" :color="active ? 'primary' : 'info'" size="large"
            :closable="item.value !== '1'" style="height: 26px;">
            {{ item.label }}
          </gi-tag>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>关闭左侧</el-dropdown-item>
              <el-dropdown-item>关闭右侧</el-dropdown-item>
              <el-dropdown-item>关闭其他</el-dropdown-item>
              <el-dropdown-item>关闭所有</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </template>
      <template #right-extra>
        <el-button size="small" type="primary">右</el-button>
      </template>
    </gi-nav-tabs>
  </div>
</template>

<script setup lang="ts">
import type { DropdownInstance } from 'element-plus'
import type { NavTabItem } from 'gi-component'
import { ref } from 'vue'

const activeValue = ref('1')

const dropdownRefMap = new Map<string | number, DropdownInstance>()

function setDropdownRef(value: string | number, el: unknown) {
  if (el) {
    dropdownRefMap.set(value, el as DropdownInstance)
  } else {
    dropdownRefMap.delete(value)
  }
}

/** 新开页签右键菜单时,关闭其它页签已打开的菜单 */
function handleContextMenuVisible(visible: boolean, value: string | number) {
  if (!visible) {
    return
  }
  dropdownRefMap.forEach((inst, key) => {
    if (key !== value) {
      inst.handleClose()
    }
  })
}

const tabList = ref<NavTabItem[]>([
  { label: '工作台', value: '1' },
  { label: '分析页', value: '2' },
  { label: '图表页', value: '3' },
  { label: '配置页', value: '4' },
  { label: '个人中心', value: '5' },
  { label: '系统日志', value: '6' },
  { label: '帮助文档', value: '7' },
  { label: '关于我们', value: '8' },
  { label: '联系我们', value: '9' },
  { label: '反馈建议', value: '10' },
  { label: '隐私政策', value: '11' },
  { label: '用户协议', value: '12' },
  { label: '条款说明', value: '13' },
  { label: '常见问题', value: '14' },
  { label: '使用指南', value: '15' },
  { label: '更新日志', value: '16' },
  { label: '版本历史', value: '17' },
  { label: '系统设置', value: '18' },
  { label: '用户管理', value: '19' },
  { label: '权限管理', value: '20' },
  { label: '角色管理', value: '21' },
  { label: '日志管理', value: '22' }
])
</script>

<style lang="scss" scoped>
.demo {
  width: 100%;
  border: 1px solid var(--el-border-color);
  padding: 0 10px;
  box-sizing: border-box;
  margin-top: 20px;
}
</style>

Props

参数说明类型默认值
v-model当前选中值string | number-
data标签数据,支持扩展字段,类型由数组元素推导T[]T extends NavTabBase[]
custom自定义项样式:无 padding,不应用 --active / --disabled 修饰类booleanfalse

Events

事件名说明回调参数
change选中项变化(点击可切换的项时)(value: string | number)

Slots

插槽名说明作用域
default每一项的内容(外层 tab 容器由组件渲染){ item: T, active: boolean, disabled: boolean }Tdata 元素类型一致)
left-extra左侧扩展区-
right-extra右侧扩展区-

泛型与扩展字段

GiNavTabs 为泛型组件,datalabelvaluedisabled? 外可携带业务字段,默认插槽 item 会随 data 推导类型:

ts
import type { NavTabBase } from 'gi-component'

interface MyTab extends NavTabBase {
  badge?: string
}

const tabList = ref<MyTab[]>([
  { label: '页签1', value: '1' },
  { label: '页签2', value: '2', badge: '新' }
])
vue
<gi-nav-tabs v-model="active" :data="tabList">
  <template #default="{ item }">
    {{ item.label }}
    <el-tag v-if="item.badge">{{ item.badge }}</el-tag>
  </template>
</gi-nav-tabs>

无扩展字段时,NavTabItem(即 NavTabBase)仍可直接使用。


useNavTabs 组合函数

useNavTabs 是一个用于水平标签导航的组合函数,提供鼠标滚轮横向滚动与选中项自动居中能力。适用于完全自定义标签头 DOM 结构。

源码位于 packages/hooks/useNavTabs.ts

组合函数基础用法

在自定义标签头模板中调用 useNavTabs,传入根容器、滚动容器、标签项 class 以及当前选中值即可。

页签1
页签2
页签3
页签4
页签5
页签6
页签7
页签8
页签9
页签10
页签11
页签12

在标签区域使用鼠标滚轮可横向滚动,切换选中项后会自动居中。

查看代码
vue
<template>
  <div class="demo-nav-tabs">
    <div class="tab">
      <div class="tab__scroll">
        <div v-for="item in tabList" :key="item.value" class="tab-item" :class="{
          'tab-item--active': activeValue === item.value,
          'tab-item--disabled': item.disabled,
        }" :data-value="item.value" @click="handleTabClick(item)">
          {{ item.label }}
        </div>
      </div>
    </div>
    <div class="demo-nav-tabs__actions">
      <el-button size="small" @click="activeValue = '1'">选中页签1</el-button>
      <el-button size="small" @click="activeValue = '5'">选中页签5</el-button>
      <el-button size="small" @click="activeValue = '10'">选中页签10</el-button>
    </div>
    <p class="demo-nav-tabs__tip">在标签区域使用鼠标滚轮可横向滚动,切换选中项后会自动居中。</p>
  </div>
</template>

<script setup lang="ts">
import type { NavTabItem } from 'gi-component'
import { useNavTabs } from 'gi-component'
import { ref } from 'vue'

const activeValue = ref<string | number>('1')

const tabList = ref<NavTabItem[]>([
  { label: '页签1', value: '1' },
  { label: '页签2', value: '2', disabled: true },
  { label: '页签3', value: '3' },
  { label: '页签4', value: '4' },
  { label: '页签5', value: '5' },
  { label: '页签6', value: '6' },
  { label: '页签7', value: '7' },
  { label: '页签8', value: '8' },
  { label: '页签9', value: '9' },
  { label: '页签10', value: '10' },
  { label: '页签11', value: '11' },
  { label: '页签12', value: '12' }
])

useNavTabs({
  tabEl: '.tab',
  tabScrollEl: 'tab__scroll',
  tabItemClassName: 'tab-item',
  activeValue
})

function handleTabClick(item: NavTabItem) {
  if (item.disabled) {
    return
  }
  activeValue.value = item.value
}
</script>

<style scoped>
.demo-nav-tabs {
  width: 360px;
}

.tab {
  border: 1px solid var(--el-border-color);
  border-radius: 4px;
  background: var(--el-bg-color);
  font-size: 14px;
}

.tab__scroll {
  display: flex;
  overflow-x: auto;
  scrollbar-width: none;
}

.tab__scroll::-webkit-scrollbar {
  display: none;
}

.tab-item {
  flex-shrink: 0;
  padding: 0 16px;
  height: 40px;
  line-height: 40px;
  cursor: pointer;
  color: var(--el-text-color-regular);
  position: relative;
  user-select: none;
}

.tab-item--active {
  color: var(--el-color-primary);
  font-weight: 500;
}

.tab-item--active::after {
  content: '';
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 0;
  height: 2px;
  background: var(--el-color-primary);
}

.tab-item--disabled {
  color: var(--el-text-color-disabled);
  cursor: not-allowed;
}

.demo-nav-tabs__actions {
  margin-top: 12px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.demo-nav-tabs__tip {
  margin-top: 8px;
  font-size: 12px;
  color: var(--el-text-color-secondary);
}
</style>

为什么用组合函数而不是构造函数

维度组合函数构造函数
生命周期自动在 onMounted / onUnmounted 绑定与清理事件需手动 init() / destroy()
响应式watch(activeValue) 自动居中需额外回调或轮询 DOM
使用方式<script setup> 中与 ref 配合更适合纯 JS 环境

API

Options

参数说明类型默认值
tabEl根容器,支持选择器或 HTMLElementstring | HTMLElement | null-
tabScrollEl滚动容器,支持类名(如 tab__scroll)或选择器string | HTMLElement | null-
tabItemClassName标签项 class 名string-
activeValue当前选中值,变化时自动居中string | number-
wheelSpeed滚轮换算系数number1.5

TIP

tabScrollEl 传入 tab__scroll 时会自动补全为 .tab__scrolltabScrollEl 会优先在 tabEl 内部查找,避免多实例误匹配。

返回值

名称说明类型
scrollToActive手动滚动到当前选中项并居中(behavior?: ScrollBehavior) => void
getScrollEl获取解析后的滚动容器() => HTMLElement | null

引入

ts
import { useNavTabs } from 'gi-component'