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 修饰类 | boolean | false |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 选中项变化(点击可切换的项时) | (value: string | number) |
Slots
| 插槽名 | 说明 | 作用域 |
|---|---|---|
| default | 每一项的内容(外层 tab 容器由组件渲染) | { item: T, active: boolean, disabled: boolean }(T 与 data 元素类型一致) |
| left-extra | 左侧扩展区 | - |
| right-extra | 右侧扩展区 | - |
泛型与扩展字段
GiNavTabs 为泛型组件,data 除 label、value、disabled? 外可携带业务字段,默认插槽 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 | 根容器,支持选择器或 HTMLElement | string | HTMLElement | null | - |
| tabScrollEl | 滚动容器,支持类名(如 tab__scroll)或选择器 | string | HTMLElement | null | - |
| tabItemClassName | 标签项 class 名 | string | - |
| activeValue | 当前选中值,变化时自动居中 | string | number | - |
| wheelSpeed | 滚轮换算系数 | number | 1.5 |
TIP
tabScrollEl 传入 tab__scroll 时会自动补全为 .tab__scroll;tabScrollEl 会优先在 tabEl 内部查找,避免多实例误匹配。
返回值
| 名称 | 说明 | 类型 |
|---|---|---|
| scrollToActive | 手动滚动到当前选中项并居中 | (behavior?: ScrollBehavior) => void |
| getScrollEl | 获取解析后的滚动容器 | () => HTMLElement | null |
引入
ts
import { useNavTabs } from 'gi-component'