画安 3 giorni fa
parent
commit
a34dcdd773
3 ha cambiato i file con 338 aggiunte e 21 eliminazioni
  1. 40 0
      src/components/ui/TechTabPane.vue
  2. 259 0
      src/components/ui/TechTabs.vue
  3. 39 21
      src/views/MainWatch.vue

+ 40 - 0
src/components/ui/TechTabPane.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="tech-tab-pane" v-show="active">
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TechTabPane',
+  props: {
+    label: { type: String, required: true }, // 头部显示的文字
+    name: { type: [String, Number], required: true } // 唯一标识符
+  },
+  computed: {
+    // 核心:动态判断自己是否被选中
+    active() {
+      // 通过 this.$parent 拿到 TechTabs 组件绑定的 v-model (即 value 属性)
+      return this.$parent.value === this.name;
+    }
+  },
+  created() {
+    // 组件创建时,把自己注册到父组件的 panes 数组中去,让父组件能渲染出头部
+    if (this.$parent && this.$parent.addPane) {
+      this.$parent.addPane(this);
+    }
+  },
+  beforeDestroy() {
+    if (this.$parent && this.$parent.removePane) {
+      this.$parent.removePane(this);
+    }
+  }
+};
+</script>
+
+<style scoped>
+.tech-tab-pane {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 259 - 0
src/components/ui/TechTabs.vue

@@ -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>

+ 39 - 21
src/views/MainWatch.vue

@@ -32,25 +32,10 @@
     <main class="grid">
       <!-- Left -->
       <section class="col left top-tabs-wrapper">
-        <!-- 左侧Tab菜单栏 -->
-        <div class="top-tabs">
-          <div 
-            v-for="tab in tabs" 
-            :key="tab.id"
-            class="tab-item"
-            :class="{ 'active': currentTab === tab.id }"
-            @click="switchTab(tab.id)"
-          >
-            {{ tab.name }}
-          </div>
-        </div>
 
-        <div class="left-panel hide-scrollbar">
-          <div v-if="isLoading" class="loading-state">
-            正在加载 {{ currentTabName }} 数据...
-          </div>
-          
-          <div v-else class="menu-tree">
+        <!-- 左侧Tab菜单栏 -->
+        <TechTabs v-model="activeOuterTab" type="underline">
+          <TechTabPane label="总览" name="overview">
             <MenuItem 
               v-for="item in menuData" 
               :key="item.id" 
@@ -58,9 +43,36 @@
               :level="0"
               @node-click="handleMenuClick"
             />
-          </div>
-        </div>
-
+          </TechTabPane>
+          <TechTabPane label="路口" name="crossing">
+            <MenuItem 
+              v-for="item in menuData" 
+              :key="item.id" 
+              :model="item" 
+              :level="0"
+              @node-click="handleMenuClick"
+            />
+          </TechTabPane>
+          <TechTabPane label="干线" name="trunkLine">
+            <MenuItem 
+              v-for="item in menuData" 
+              :key="item.id" 
+              :model="item" 
+              :level="0"
+              @node-click="handleMenuClick"
+            />
+          </TechTabPane>
+          <TechTabPane label="特勤" name="specialDuty">
+            <MenuItem 
+              v-for="item in menuData" 
+              :key="item.id" 
+              :model="item" 
+              :level="0"
+              @node-click="handleMenuClick"
+            />
+          </TechTabPane>
+        </TechTabs>
+       
       </section>
 
       <!-- Middle -->
@@ -233,6 +245,8 @@ import TrafficTimeSpace from '@/components/ui/TrafficTimeSpace.vue';
 import IntersectionSignalMonitoring from '@/components/IntersectionSignalMonitoring.vue';
 import SegmentedRadio from '@/components/SegmentedRadio.vue';
 import DropdownSelect from '@/components/DropdownSelect.vue';
+import TechTabs from '@/components/ui/TechTabs.vue';
+import TechTabPane from '@/components/ui/TechTabPane.vue';
 
 import { menuData, makeTrafficTimeSpaceData} from '@/mock/data';
 
@@ -245,9 +259,13 @@ export default {
     IntersectionSignalMonitoring,
     SegmentedRadio,
     DropdownSelect,
+    TechTabs,
+    TechTabPane
   },
   data() {
     return {
+      activeOuterTab: 'overview',
+      activeOuterTab1: '',
       isNavVisible: true,
       baseW: 1920,
       baseH: 1080,