南通市网站建设_网站建设公司_全栈开发者_seo优化
2026/1/8 1:01:37 网站建设 项目流程

效果展示:

未超出:

超出:

代码:

<template> <div class="custom-tabs"> <!-- 左侧翻页按钮 --> <div class="scroll-btn left-btn" :class="{ disabled: !canScrollLeft }" @click="scrollLeft" v-show="showScrollButtons"> <span>&lt;</span> </div> <!-- 标签页容器 --> <div class="tabs-container" ref="containerRef"> <div class="tabs-wrap" ref="wrapRef" :style="{ transform: `translateX(${translateX}px)` }"> <!-- 标签页 --> <div v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: modelValue === tab.value }" @click="handleTabClick(tab.value)" ref="tabRefs" > {{ tab.label }} </div> <!-- 底部滑动条 --> <div class="active-bar" :style="activeBarStyle"></div> </div> </div> <!-- 右侧翻页按钮 --> <div class="scroll-btn right-btn" :class="{ disabled: !canScrollRight }" @click="scrollRight" v-show="showScrollButtons"> <span>&gt;</span> </div> </div> </template> <script setup> import { ref, computed, watch, onMounted, nextTick, onUnmounted } from "vue"; const props = defineProps({ modelValue: { type: [String, Number], required: true }, tabs: { type: Array, required: true, default: () => [] } }); const emit = defineEmits(["update:modelValue", "tab-click"]); // Refs const containerRef = ref(null); const wrapRef = ref(null); const tabRefs = ref([]); // 状态 const translateX = ref(0); const containerWidth = ref(0); const wrapWidth = ref(0); const activeTabIndex = ref(0); // 计算属性 const showScrollButtons = computed(() => { return wrapWidth.value > containerWidth.value; }); const canScrollLeft = computed(() => translateX.value < 0); const canScrollRight = computed(() => { const remainingWidth = wrapWidth.value + translateX.value; return remainingWidth > containerWidth.value; }); // 激活标签的滑动条样式 const activeBarStyle = computed(() => { if (!tabRefs.value.length || activeTabIndex.value >= tabRefs.value.length) { return { width: "0px", transform: "translateX(0px)" }; } const activeTab = tabRefs.value[activeTabIndex.value]; if (!activeTab) return { width: "0px", transform: "translateX(0px)" }; return { width: `${activeTab.offsetWidth}px`, transform: `translateX(${activeTab.offsetLeft}px)` }; }); // 更新容器和包裹层宽度 const updateDimensions = () => { if (!containerRef.value || !wrapRef.value) return; containerWidth.value = containerRef.value.offsetWidth; wrapWidth.value = wrapRef.value.scrollWidth; // 重置translateX为0,确保靠左对齐 if (wrapWidth.value <= containerWidth.value) { translateX.value = 0; } // 确保当前激活标签可见 ensureActiveTabVisible(); }; // 左右翻页 const scrollLeft = () => { if (!canScrollLeft.value) return; const scrollAmount = Math.min(containerWidth.value * 0.8, -translateX.value); translateX.value += scrollAmount; }; const scrollRight = () => { if (!canScrollRight.value) return; const remainingWidth = wrapWidth.value + translateX.value - containerWidth.value; const scrollAmount = Math.min(containerWidth.value * 0.8, remainingWidth); translateX.value -= scrollAmount; }; // 确保激活标签可见 const ensureActiveTabVisible = () => { // 如果内容没有溢出,直接返回 if (wrapWidth.value <= containerWidth.value) { translateX.value = 0; return; } nextTick(() => { if (!tabRefs.value.length || activeTabIndex.value >= tabRefs.value.length) return; const activeTab = tabRefs.value[activeTabIndex.value]; if (!activeTab) return; const tabLeft = activeTab.offsetLeft; const tabRight = tabLeft + activeTab.offsetWidth; // 如果标签在可视区域左侧 if (tabLeft < -translateX.value) { translateX.value = -tabLeft; } // 如果标签在可视区域右侧 else if (tabRight > -translateX.value + containerWidth.value) { translateX.value = -(tabRight - containerWidth.value); } // 限制边界 clampTranslateX(); }); }; // 限制平移范围 const clampTranslateX = () => { const maxTranslate = Math.min(0, containerWidth.value - wrapWidth.value); translateX.value = Math.max(maxTranslate, Math.min(0, translateX.value)); }; // 标签点击处理 const handleTabClick = value => { emit("update:modelValue", value); emit("tab-click", value); // 查找激活的索引 const index = props.tabs.findIndex(tab => tab.value === value); if (index !== -1) { activeTabIndex.value = index; ensureActiveTabVisible(); } }; // 监听值变化 watch( () => props.modelValue, newVal => { const index = props.tabs.findIndex(tab => tab.value === newVal); if (index !== -1) { activeTabIndex.value = index; ensureActiveTabVisible(); } } ); // 监听tabs变化 watch( () => props.tabs, () => { nextTick(() => { updateDimensions(); }); }, { deep: true } ); // 生命周期 onMounted(() => { updateDimensions(); // 监听窗口大小变化 window.addEventListener("resize", updateDimensions); // 使用ResizeObserver监听容器大小变化 const resizeObserver = new ResizeObserver(updateDimensions); if (containerRef.value) { resizeObserver.observe(containerRef.value); } // 清理 onUnmounted(() => { window.removeEventListener("resize", updateDimensions); resizeObserver.disconnect(); }); }); // 初始查找激活标签索引 const initActiveIndex = () => { const index = props.tabs.findIndex(tab => tab.value === props.modelValue); activeTabIndex.value = index !== -1 ? index : 0; }; initActiveIndex(); </script> <style scoped> .custom-tabs { display: flex; align-items: center; position: relative; width: 100%; height: 40px; border-bottom: 1px solid #e4e7ed; } .tabs-container { flex: 1; overflow: hidden; position: relative; height: 100%; } .tabs-wrap { display: inline-flex; position: relative; height: 100%; white-space: nowrap; transition: transform 0.3s ease; /* 确保内容不足时靠左对齐 */ min-width: 100%; } .tab-item { position: relative; display: inline-flex; align-items: center; justify-content: center; padding: 0 20px; height: 100%; font-size: 14px; color: #606266; cursor: pointer; user-select: none; transition: all 0.3s ease; flex-shrink: 0; } .tab-item:hover { color: #409eff; } .tab-item.active { color: #409eff; font-weight: 500; } .active-bar { position: absolute; bottom: 0; left: 0; height: 2px; background-color: #409eff; transition: all 0.3s ease; z-index: 1; } .scroll-btn { display: flex; align-items: center; justify-content: center; width: 32px; height: 100%; color: #606266; cursor: pointer; user-select: none; transition: color 0.3s; background: #fff; border-bottom: 1px solid #e4e7ed; z-index: 2; flex-shrink: 0; } .scroll-btn:hover { color: #409eff; } .scroll-btn.disabled { color: #c0c4cc; cursor: not-allowed; } .scroll-btn.left-btn { border-right: 1px solid #e4e7ed; } .scroll-btn.right-btn { border-left: 1px solid #e4e7ed; } .scroll-btn span { font-size: 14px; } </style>

使用:

<template> <div class="app"> <h2>仿 Element Plus Tabs 组件</h2> <CustomTabs v-model="activeTab" :tabs="tabs" @tab-click="handleTabClick" /> <div class="tab-content"> <div v-if="activeTab === 'tab1'">内容 1</div> <div v-if="activeTab === 'tab2'">内容 2</div> <div v-if="activeTab === 'tab3'">内容 3</div> <div v-if="activeTab === 'tab4'">内容 4</div> </div> </div> </template> <script setup> import { ref } from "vue"; import CustomTabs from "@/components/customTabs/index.vue"; const activeTab = ref("tab1"); const tabs = ref([ { label: "标签页 1", value: "tab1" }, { label: "标签页 2", value: "tab2" }, { label: "标签页 3", value: "tab3" }, { label: "标签页 4", value: "tab4" }, { label: "标签页 5(很长很长的标签)", value: "tab5" }, { label: "标签页 6", value: "tab6" }, { label: "标签页 7", value: "tab7" }, { label: "标签页 8", value: "tab8" }, { label: "标签页 9", value: "tab9" }, { label: "标签页 10", value: "tab10" }, { label: "标签页 11", value: "tab11" }, { label: "标签页 12", value: "tab12" }, { label: "标签页 13", value: "tab13" }, { label: "标签页 14", value: "tab14" }, { label: "标签页 15", value: "tab15" }, { label: "标签页 16", value: "tab16" }, { label: "标签页 17", value: "tab17" }, { label: "标签页 18", value: "tab18" }, { label: "标签页 19", value: "tab19" }, { label: "标签页 20", value: "tab20" }, { label: "标签页 21", value: "tab21" }, { label: "标签页 22", value: "tab22" }, { label: "标签页 23", value: "tab23" }, { label: "标签页 24", value: "tab24" }, { label: "标签页 25", value: "tab25" }, { label: "标签页 26", value: "tab26" }, { label: "标签页 27", value: "tab27" } ]); const handleTabClick = value => { console.log("标签被点击:", value); }; </script> <style scoped> .app { width: 80%; margin: 0 auto; padding: 20px; } .tab-content { padding: 20px; background: #f5f7fa; margin-top: 20px; border-radius: 4px; } </style>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询