效果

实现
<template>
<div class="tabs-wrapper">
<div v-show="showLeftArrow" class="nav-arrow left-arrow" @click="scrollList('left')">
<slot name="left-arrow">
<i class="el-icon-arrow-left"><</i>
</slot>
</div>
<div class="scroll-container" ref="scrollContainer" @scroll="handleScroll">
<div class="tab-list" ref="tabList">
<div v-for="(item, index) in tabList" :key="index" :ref="'tabItem' + index" class="tab-item"
:class="{ active: activeIndex === index }" @click="handleTabClick(item, index)">
{{ item.label }}
</div>
</div>
</div>
<div v-show="showRightArrow" class="nav-arrow right-arrow" @click="scrollList('right')">
<slot name="right-arrow">
<i class="el-icon-arrow-right">></i>
</slot>
</div>
</div>
</template>
<script>
export default {
name: 'HorizontalTabs',
props: {
// 数据列表,需包含 label 字段,或自行修改模板
tabList: {
type: Array,
required: true,
default: () => [
{ "label": "首页", "value": "home" },
{ "label": "很长很长很长的标签A", "value": "long_label_a" },
{ "label": "产品", "value": "products" },
{ "label": "一个超级无敌长的标签B", "value": "super_long_label_b" },
{ "label": "解决方案", "value": "solutions" },
{ "label": "短", "value": "short" },
{ "label": "中等长度的标签", "value": "medium_label" },
{ "label": "This is an English Label", "value": "english_label" },
{ "label": "标签", "value": "tag" },
{ "label": "最后的一个非常非常非常长的标签", "value": "last_very_long_label" },
{ "label": "首页🔥", "value": "home" },
{ "label": "产品&服务", "value": "product_service" },
{ "label": "价格¥999", "value": "price_999" },
{ "label": "FAQ?", "value": "faq" },
{ "label": "关于|我们", "value": "about_us" },
{ "label": "Contact✈️", "value": "contact" },
{ "label": "技术📚文档", "value": "tech_docs" },
{ "label": "解决方案#1", "value": "solution_1" },
{ "label": "解决方案#2", "value": "solution_2" },
{ "label": "测试&验证", "value": "test_verify" },
{ "label": "用户@反馈", "value": "user_feedback" },
{ "label": "活动🎉中心", "value": "activity_center" },
{ "label": "公告📢", "value": "notice" },
{ "label": "帮助💡", "value": "help" },
{ "label": "设置⚙️", "value": "setting" },
{ "label": "数据📊分析", "value": "data_analysis" },
{ "label": "营销📈推广", "value": "marketing" },
{ "label": "运营⚡管理", "value": "operation" },
{ "label": "财务💰报表", "value": "finance" },
{ "label": "人事💼管理", "value": "hr" }
]
},
// 默认选中的索引
defaultActiveIndex: {
type: Number,
default: 0
},
// 点击箭头每次滚动的距离,默认为 null,会自动计算为 2 个 tab 的宽度
scrollStep: {
type: Number,
default: null
}
},
data() {
return {
activeIndex: this.defaultActiveIndex,
showLeftArrow: false,
showRightArrow: false,
};
},
mounted() {
// 初始化检查箭头状态
this.$nextTick(() => {
this.checkArrows();
// 如果默认选中的不是第一个,需要初始化居中
if (this.activeIndex > 0) {
this.centerActiveTab(this.activeIndex, false); // false 表示不需要平滑滚动初始化
}
});
// 监听窗口大小变化,重新计算箭头显隐
window.addEventListener('resize', this.checkArrows);
},
beforeDestroy() {
window.removeEventListener('resize', this.checkArrows);
},
methods: {
/**
* 点击 Tab 处理
*/
handleTabClick(item, index) {
this.activeIndex = index;
this.$emit('change', item, index);
this.centerActiveTab(index);
},
/**
* 核心逻辑:将选中的 Tab 移动到视图中间
*/
centerActiveTab(index, smooth = true) {
const container = this.$refs.scrollContainer;
// 获取对应的 tab DOM 元素(注意:Vue2 中 v-for 的 ref 是一个数组)
const tabElement = this.$refs['tabItem' + index] && this.$refs['tabItem' + index][0];
if (container && tabElement) {
// 计算公式:滚动距离 = (Tab的左偏移 + Tab宽度/2) - (容器宽度/2)
const targetLeft = tabElement.offsetLeft + tabElement.offsetWidth / 2 - container.offsetWidth / 2;
container.scrollTo({
left: targetLeft,
behavior: smooth ? 'smooth' : 'auto'
});
}
},
/**
* 监听滚动事件,控制箭头显隐
*/
handleScroll() {
// 使用 requestAnimationFrame 或简单的节流可以优化性能,这里为了简洁直接调用
this.checkArrows();
},
/**
* 计算箭头是否需要显示
*/
checkArrows() {
const container = this.$refs.scrollContainer;
if (!container) return;
const { scrollLeft, scrollWidth, clientWidth } = container;
// 容差值,避免小数精度问题
const tolerance = 1;
// 左侧是否有隐藏内容
this.showLeftArrow = scrollLeft > tolerance;
// 右侧是否有隐藏内容 (滚动条位置 + 可视宽度 < 总滚动宽度)
this.showRightArrow = scrollLeft + clientWidth < scrollWidth - tolerance;
},
/**
* 点击箭头滚动
*/
scrollList(direction) {
const container = this.$refs.scrollContainer;
if (!container) return;
// 如果未传入具体步长,尝试获取第一个 tab 的宽度 * 2,如果没有 tab 则默认 200
let step = this.scrollStep;
if (!step) {
const firstTab = this.$refs['tabItem0'] && this.$refs['tabItem0'][0];
step = firstTab ? firstTab.offsetWidth * 2 : 200;
}
const currentScroll = container.scrollLeft;
const targetScroll = direction === 'left' ? currentScroll - step : currentScroll + step;
container.scrollTo({
left: targetScroll,
behavior: 'smooth'
});
}
}
};
</script>
<style scoped>
.tabs-wrapper {
position: relative;
width: 100%;
height: 50px;
/* 可根据需求调整 */
display: flex;
align-items: center;
background-color: #f5f7fa;
overflow: hidden;
/* 防止溢出 */
}
/* 箭头通用样式 */
.nav-arrow {
position: absolute;
top: 0;
bottom: 0;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
cursor: pointer;
z-index: 10;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
font-weight: bold;
user-select: none;
}
.left-arrow {
left: 0;
}
.right-arrow {
right: 0;
}
/* 滚动区域 */
.scroll-container {
flex: 1;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
/* 隐藏滚动条 */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE 10+ */
scroll-behavior: smooth;
}
.scroll-container::-webkit-scrollbar {
display: none;
/* Chrome Safari */
}
.tab-list {
display: inline-flex;
height: 100%;
align-items: center;
padding: 0 10px;
/* 给两端留一点空隙 */
}
/* 单个 Tab 样式 */
.tab-item {
display: inline-block;
padding: 8px 20px;
margin: 0 5px;
cursor: pointer;
border-radius: 4px;
color: #606266;
transition: all 0.3s;
font-size: 14px;
}
.tab-item:hover {
color: #409EFF;
}
.tab-item.active {
color: #fff;
background-color: #409EFF;
font-weight: bold;
}
</style>