|
|
@@ -0,0 +1,259 @@
|
|
|
+<template>
|
|
|
+ <div class="tech-tabs-container" :class="`is-${type}`" @mouseenter="pauseTimer" @mouseleave="resumeTimer">
|
|
|
+
|
|
|
+ <div class="tabs-header">
|
|
|
+ <div v-if="type === 'segmented'" class="segmented-slider" :style="sliderStyle"></div>
|
|
|
+ <div
|
|
|
+ v-for="(pane, index) in panes"
|
|
|
+ :key="index"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ 'is-active': value === pane.name }"
|
|
|
+ @click="handleTabClick(pane.name)"
|
|
|
+ >
|
|
|
+ {{ pane.label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tabs-content">
|
|
|
+ <slot></slot>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'TechTabs',
|
|
|
+ props: {
|
|
|
+ // 绑定的当前选中的 tab name (v-model)
|
|
|
+ value: { type: [String, Number], required: true },
|
|
|
+ // 风格类型:'underline' (下划线大Tab) 或 'segmented' (分段胶囊小Tab)
|
|
|
+ type: { type: String, default: 'underline' },
|
|
|
+ // 是否开启自动轮播
|
|
|
+ autoPlay: { type: Boolean, default: false },
|
|
|
+ // 轮播间隔时间 (毫秒),默认 3 秒
|
|
|
+ interval: { type: Number, default: 3000 }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ panes: [], // 自动收集的所有子面板实例
|
|
|
+ timer: null, // 定时器实例
|
|
|
+ isHovering: false // 记录鼠标是否悬浮在组件上
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 计算当前选中项的索引
|
|
|
+ activeIndex() {
|
|
|
+ const index = this.panes.findIndex(pane => pane.name === this.value);
|
|
|
+ return index === -1 ? 0 : index;
|
|
|
+ },
|
|
|
+ // 动态计算滑块的宽度和偏移量
|
|
|
+ sliderStyle() {
|
|
|
+ const total = this.panes.length || 1;
|
|
|
+ return {
|
|
|
+ // 宽度平分
|
|
|
+ width: `${100 / total}%`,
|
|
|
+ // 根据索引移动自己的宽度倍数 (100% 就是移动一个滑块的距离)
|
|
|
+ transform: `translateX(${this.activeIndex * 100}%)`
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 监听 autoPlay 的变化,支持动态开启/关闭
|
|
|
+ autoPlay(newVal) {
|
|
|
+ newVal ? this.startTimer() : this.stopTimer();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 等待所有的 TechTabPane 子组件注册完毕后,启动定时器
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.autoPlay) {
|
|
|
+ this.startTimer();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 销毁时务必清理定时器,防止内存泄漏
|
|
|
+ this.stopTimer();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleTabClick(name) {
|
|
|
+ if (this.value !== name) {
|
|
|
+ this.$emit('input', name);
|
|
|
+ this.$emit('tab-click', name);
|
|
|
+ // 用户手动干预后,重置定时器,重新开始倒计时
|
|
|
+ if (this.autoPlay && !this.isHovering) {
|
|
|
+ this.startTimer();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 切换到下一个
|
|
|
+ switchToNext() {
|
|
|
+ if (this.panes.length <= 1) return;
|
|
|
+
|
|
|
+ // 找到当前选中项的索引
|
|
|
+ const currentIndex = this.panes.findIndex(pane => pane.name === this.value);
|
|
|
+
|
|
|
+ // 计算下一个索引 (到了最后一个就回到第一个)
|
|
|
+ let nextIndex = currentIndex + 1;
|
|
|
+ if (nextIndex >= this.panes.length) {
|
|
|
+ nextIndex = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拿到下一个的 name 并触发更新
|
|
|
+ const nextName = this.panes[nextIndex].name;
|
|
|
+ this.$emit('input', nextName);
|
|
|
+ this.$emit('tab-click', nextName);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 启动定时器
|
|
|
+ startTimer() {
|
|
|
+ this.stopTimer(); // 启动前先清空旧的,防止多开定时器飙车
|
|
|
+ if (this.panes.length > 1) {
|
|
|
+ this.timer = setInterval(() => {
|
|
|
+ this.switchToNext();
|
|
|
+ }, this.interval);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 停止定时器
|
|
|
+ stopTimer() {
|
|
|
+ if (this.timer) {
|
|
|
+ clearInterval(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 鼠标悬浮时暂停自动切换,方便用户阅读数据
|
|
|
+ pauseTimer() {
|
|
|
+ this.isHovering = true;
|
|
|
+ if (this.autoPlay) this.stopTimer();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 鼠标离开后恢复自动切换
|
|
|
+ resumeTimer() {
|
|
|
+ this.isHovering = false;
|
|
|
+ if (this.autoPlay) this.startTimer();
|
|
|
+ },
|
|
|
+ // 供子组件 TechTabPane 注册自己
|
|
|
+ addPane(pane) {
|
|
|
+ this.panes.push(pane);
|
|
|
+ },
|
|
|
+ // 供子组件销毁时移除自己
|
|
|
+ removePane(pane) {
|
|
|
+ const index = this.panes.indexOf(pane);
|
|
|
+ if (index !== -1) this.panes.splice(index, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.tech-tabs-container {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.tabs-content {
|
|
|
+ flex: 1;
|
|
|
+ padding-top: 15px; /* 内容与头部的间距 */
|
|
|
+}
|
|
|
+
|
|
|
+/* ================== 风格 1:下划线类型 (is-underline) ================== */
|
|
|
+.is-underline .tabs-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ padding: 0 10px;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.is-underline .tab-item {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 25px;
|
|
|
+ font-weight: bold;
|
|
|
+ padding: 10px 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ transition: all 0.3s;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.is-underline>.tabs-header .tab-item:hover { color: #e2e8f0; }
|
|
|
+.is-underline>.tabs-header .tab-item.is-active {
|
|
|
+ color: #1FCEFB;
|
|
|
+ text-shadow: 0 0 8px rgba(0, 229, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+/* 选中态的底部发光下划线 */
|
|
|
+.is-underline>.tabs-header .tab-item::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ bottom: -1px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 0;
|
|
|
+ height: 2px;
|
|
|
+ background-color: #1FCEFB;
|
|
|
+ box-shadow: 0 0 8px #1FCEFB;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+}
|
|
|
+.is-underline>.tabs-header .tab-item.is-active::after { width: 100%; }
|
|
|
+
|
|
|
+/* ================== 风格 2:分段胶囊类型 (is-segmented) ================== */
|
|
|
+.is-segmented .tabs-header {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(119,161,255,0.14);
|
|
|
+ border: 1px solid rgba(119,161,255,0.2);
|
|
|
+ overflow: hidden;
|
|
|
+ height: 28px;
|
|
|
+ gap: 0;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+/* 独立的高亮滑块 */
|
|
|
+.segmented-slider {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient( 180deg, rgba(119,161,255,0) 0%, #77A1FF 100%);
|
|
|
+ border: 1px solid rgba(161,190,255,0.7);
|
|
|
+ border-radius: 2px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ z-index: 0;
|
|
|
+ transition: transform 0.35s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
|
+}
|
|
|
+
|
|
|
+.is-segmented .tab-item {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 100%;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ border-right: 1px solid rgba(119,161,255,0.2);
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+.is-segmented .tab-item:last-child { border-right: none; }
|
|
|
+
|
|
|
+.is-segmented .tab-item:hover:not(.is-active) {
|
|
|
+ background: rgba(119,161,255,0.1);
|
|
|
+ /* color: #ffffff; */
|
|
|
+}
|
|
|
+.is-segmented .tab-item.is-active {
|
|
|
+ color: #ffffff;
|
|
|
+ font-weight: bold;
|
|
|
+ border-right-color: transparent;
|
|
|
+}
|
|
|
+</style>
|