Bläddra i källkod

还原 BottomDock组件为线性滚动模式,移除椭圆旋转功能,添加呼吸和波浪跳动效果;修改Main页面的BottomDock引用参数;

画安 3 veckor sedan
förälder
incheckning
49292e65ee
2 ändrade filer med 133 tillägg och 236 borttagningar
  1. 132 235
      src/components/ui/BottomDock.vue
  2. 1 1
      src/views/Main.vue

+ 132 - 235
src/components/ui/BottomDock.vue

@@ -9,32 +9,21 @@
          :style="dockStyles"
          @mouseleave="handleDockLeave">
          
-        <div class="nav-arrow left-arrow" 
-            :class="{ 'is-disabled': mode === 'linear' && !canScrollLeft, 'is-active': mode === 'ellipse' || canScrollLeft }"
-            @click="mode === 'ellipse' ? rotateMenu(-1) : scrollList(-1)"
-            @mouseenter="pauseAutoRotate"
-            @mouseleave="resumeAutoRotate">
-            <img v-if="mode === 'ellipse' || canScrollLeft" src="@/assets/main/main-right.png" class="arrow-img left-facing" draggable="false" />
-            <img v-else src="@/assets/main/main-left.png" class="arrow-img" draggable="false" />
+        <div class="nav-arrow left-arrow" :class="{ 'is-disabled': !canScrollLeft, 'is-active': canScrollLeft }"
+            @click="scrollList(-1)">
+            <img v-if="canScrollLeft" src="@/assets/main/main-right.png" class="arrow-img left-facing" />
+            <img v-else src="@/assets/main/main-left.png" class="arrow-img" />
         </div>
 
-        <div class="dock-list-container" 
-             :class="{ 'is-ellipse-mode': mode === 'ellipse' }"
-             ref="listContainer" 
-             @scroll="checkScrollState"
-             @mousedown.prevent="handleDragStart"
-             @touchstart.passive="handleDragStart"
-             @mouseenter="pauseAutoRotate"
-             @mouseleave="resumeAutoRotate">
-             
-            <div class="dock-list" :class="{ 'is-ellipse-mode': mode === 'ellipse' }">
+        <div class="dock-list-container" ref="listContainer" @scroll="checkScrollState">
+            <div class="dock-list">
                 <div v-for="(item, index) in dockItems" :key="index" class="dock-item"
                     :class="{ 
                         'is-active': activeIndex === index, 
                         [`theme-${item.theme}`]: item.theme,
-                        'is-front': mode === 'ellipse' && frontIndex === index 
+                        'is-breathing': waveAnimation
                     }"
-                    :style="mode === 'ellipse' ? getEllipseStyle(index) : {}"
+                    :style="waveAnimation ? { animationDelay: `${index * 0.15}s` } : {}"
                     @click="handleSelect(index, item)"
                     @mouseenter="hoverIndex = index"
                     @mouseleave="hoverIndex = null"
@@ -42,7 +31,8 @@
                     <div class="item-icon">
                         <img v-if="item.imgUrl" 
                         :src="(activeIndex === index || hoverIndex === index) && item.activeImgUrl ? item.activeImgUrl : item.imgUrl" 
-                        class="custom-icon" draggable="false" />
+                        class="custom-icon" />
+
                         <i v-else :class="item.iconClass"></i>
                     </div>
                     <div class="item-label">{{ item.label }}</div>
@@ -50,13 +40,10 @@
             </div>
         </div>
 
-        <div class="nav-arrow right-arrow" 
-            :class="{ 'is-disabled': mode === 'linear' && !canScrollRight, 'is-active': mode === 'ellipse' || canScrollRight }"
-            @click="mode === 'ellipse' ? rotateMenu(1) : scrollList(1)"
-            @mouseenter="pauseAutoRotate"
-            @mouseleave="resumeAutoRotate">
-            <img v-if="mode === 'ellipse' || canScrollRight" src="@/assets/main/main-right.png" class="arrow-img" draggable="false" />
-            <img v-else src="@/assets/main/main-left.png" class="arrow-img left-facing" draggable="false" />
+        <div class="nav-arrow right-arrow" :class="{ 'is-disabled': !canScrollRight, 'is-active': canScrollRight }"
+            @click="scrollList(1)">
+            <img v-if="canScrollRight" src="@/assets/main/main-right.png" class="arrow-img" />
+            <img v-else src="@/assets/main/main-left.png" class="arrow-img left-facing" />
         </div>
     </div>
 </template>
@@ -65,16 +52,31 @@
 export default {
     name: 'BottomDock',
     props: {
-        autoHide: { type: Boolean, default: true },
-        bottomOffset: { type: Number, default: 0 },
-        customStyle: { type: Object, default: () => ({}) },
-        customClass: { type: [String, Array, Object], default: '' },
-        
-        mode: { type: String, default: 'linear' },         
-        autoRotate: { type: Boolean, default: true },      
-        autoRotateSpeed: { type: Number, default: 2500 },  
-        radiusX: { type: Number, default: 380 },           
-        radiusY: { type: Number, default: 60 },            
+        // 是否自动隐藏 (默认 true: 悬浮升起; false: 常驻显示)
+        autoHide: {
+            type: Boolean,
+            default: true
+        },
+        // 距离屏幕底部的偏移量 (单位 px,正数代表往上抬高)
+        bottomOffset: {
+            type: Number,
+            default: 0
+        },
+        // 允许外部传入的自定义容器样式 (如背景色、宽度等)
+        customStyle: {
+            type: Object,
+            default: () => ({})
+        },
+        // 允许外部传入自定义 class (支持字符串、数组、对象)
+        customClass: {
+            type: [String, Array, Object],
+            default: ''
+        },
+        // 是否开启波浪呼吸动画
+        waveAnimation: {
+            type: Boolean,
+            default: false
+        }
     },
     data() {
         return {
@@ -83,34 +85,71 @@ export default {
             dockExpanded: false,
             canScrollLeft: false,
             canScrollRight: true,
-            
-            ellipseRotation: Math.PI / 2, 
-            frontIndex: 0,
-            
-            // --- 拖拽交互状态 ---
-            isDragging: false,
-            hasDragged: false,
-            startX: 0,
-            currentX: 0,         // 记录实时拖动位置
-            startRotation: 0,
-            startFrontIndex: 0,  // 记录拖拽开始时的正前方项目
-            rotateTimer: null,
-
             dockItems: [
-                { label: '首页', imgUrl: require('@/assets/main/main-home.png'), activeImgUrl: require('@/assets/main/main-home-hover.png'), route: '/home', theme: 'blue' },
-                { label: '状态监控', imgUrl: require('@/assets/main/main-surve.png'), activeImgUrl: require('@/assets/main/main-surve-hover.png'), route: '/surve', theme: 'blue' },
-                { label: '勤务管理', imgUrl: require('@/assets/main/main-security.png'), activeImgUrl: require('@/assets/main/main-security-hover.png'), route: '/security', theme: 'gold' },
-                { label: '干线协调', imgUrl: require('@/assets/main/main-coor.png'), activeImgUrl: require('@/assets/main/main-coor-hover.png'), route: '/coor', theme: 'blue' },
-                { label: '数据分析', imgUrl: require('@/assets/main/main-watch.png'), activeImgUrl: require('@/assets/main/main-watch-hover.png'), route: '/watch', theme: 'blue' },
-                { label: '系统设置', imgUrl: require('@/assets/main/main-setting.png'), activeImgUrl: require('@/assets/main/main-setting-hover.png'), route: '/setting', theme: 'blue' },
-                // { label: '测试1', imgUrl: require('@/assets/main/main-home.png'), activeImgUrl: require('@/assets/main/main-home-hover.png'), theme: 'blue' },
-                // { label: '测试2', imgUrl: require('@/assets/main/main-surve.png'), activeImgUrl: require('@/assets/main/main-surve-hover.png'), theme: 'blue' },
-                // { label: '测试3', imgUrl: require('@/assets/main/main-security.png'), activeImgUrl: require('@/assets/main/main-security-hover.png'), theme: 'blue' },
+                {
+                    label: '首页',
+                    imgUrl: require('@/assets/main/main-home.png'),
+                    activeImgUrl: require('@/assets/main/main-home-hover.png'),
+                    route: '/home',
+                    theme: 'blue',
+                },
+                {
+                    label: '状态监控',
+                    imgUrl: require('@/assets/main/main-surve.png'),
+                    activeImgUrl: require('@/assets/main/main-surve-hover.png'),
+                    route: '/surve',
+                    theme: 'blue',
+                },
+                {
+                    label: '勤务管理',
+                    imgUrl: require('@/assets/main/main-security.png'),
+                    activeImgUrl: require('@/assets/main/main-security-hover.png'),
+                    route: '/security',
+                    theme: 'gold',
+                },
+                {
+                    label: '干线协调',
+                    imgUrl: require('@/assets/main/main-coor.png'),
+                    activeImgUrl: require('@/assets/main/main-coor-hover.png'),
+                    route: '/coor',
+                    theme: 'blue',
+                },
+                {
+                    label: '数据分析',
+                    imgUrl: require('@/assets/main/main-watch.png'),
+                    activeImgUrl: require('@/assets/main/main-watch-hover.png'),
+                    route: '/watch',
+                    theme: 'blue',
+                },
+                {
+                    label: '系统设置',
+                    imgUrl: require('@/assets/main/main-setting.png'),
+                    activeImgUrl: require('@/assets/main/main-setting-hover.png'),
+                    route: '/setting',
+                    theme: 'blue',
+                },
+                {
+                    label: '测试1',
+                    imgUrl: require('@/assets/main/main-home.png'),
+                    theme: 'blue',
+                },
+                {
+                    label: '测试2',
+                    imgUrl: require('@/assets/main/main-surve.png'),
+                    theme: 'blue',
+                },
+                {
+                    label: '测试3',
+                    imgUrl: require('@/assets/main/main-security.png'),
+                    theme: 'blue',
+                },
             ]
         };
     },
     computed: {
+        // 动态计算 CSS 变量和合并自定义样式
         dockStyles() {
+            // 统一只传一个基础 bottom 偏移量,动画交由 CSS transform 处理
             return {
                 '--dock-bottom': `${this.bottomOffset}px`,
                 ...this.customStyle 
@@ -127,69 +166,43 @@ export default {
     },
     mounted() {
         this.$nextTick(() => {
-            if (this.mode === 'linear') {
-                this.checkScrollState();
-                window.addEventListener('resize', this.checkScrollState);
-            } else {
-                this.resumeAutoRotate();
-            }
+            this.checkScrollState();
+            window.addEventListener('resize', this.checkScrollState);
             if (this.autoHide) {
                 document.addEventListener('mousemove', this.handleGlobalMouseMove);
             }
-            window.addEventListener('mousemove', this.handleDragging);
-            window.addEventListener('mouseup', this.handleDragEnd);
-            window.addEventListener('touchmove', this.handleDragging, { passive: false });
-            window.addEventListener('touchend', this.handleDragEnd);
         });
     },
     beforeDestroy() {
         window.removeEventListener('resize', this.checkScrollState);
         document.removeEventListener('mousemove', this.handleGlobalMouseMove);
-        window.removeEventListener('mousemove', this.handleDragging);
-        window.removeEventListener('mouseup', this.handleDragEnd);
-        window.removeEventListener('touchmove', this.handleDragging);
-        window.removeEventListener('touchend', this.handleDragEnd);
-        this.pauseAutoRotate();
     },
     methods: {
         updateActiveIndexByRoute() {
-            const currentPath = this.$route.path;
+            const currentPath = this.$route?.path || ''; 
             const matchIndex = this.dockItems.findIndex(item => {
                 return item.route && currentPath.startsWith(item.route);
             });
 
             if (matchIndex !== -1) {
                 this.activeIndex = matchIndex;
-                if (this.mode === 'ellipse') {
-                    this.rotateTo(matchIndex);
-                }
             }
         },
         handleSelect(index, item) {
-            if (this.hasDragged) {
-                this.hasDragged = false;
-                return;
-            }
-
-            if (this.mode === 'ellipse') {
-                this.rotateTo(index);
-            } else if (this.activeIndex === index) {
-                return;
-            }
-
+            if (this.activeIndex === index) return;
             this.activeIndex = index;
 
-            if (item.route) {
+            if (item.route && this.$router) {
                 this.$router.push(item.route).catch(err => {
                     if (err.name !== 'NavigationDuplicated') {
                         console.error('路由跳转失败:', err);
                     }
                 });
             }
+
             this.$emit('change', item);
         },
         checkScrollState() {
-            if (this.mode !== 'linear') return;
             const container = this.$refs.listContainer;
             if (!container) return;
 
@@ -201,10 +214,12 @@ export default {
             const el = this.$refs.dockWrapper;
             if (!el) return;
             const rect = el.getBoundingClientRect();
+            // 发光线区域:居中,宽度与 CSS clamp(150px, 20vw, 250px) 一致
             const lineWidth = Math.min(250, Math.max(150, window.innerWidth * 0.2));
             const centerX = window.innerWidth / 2;
             const left = centerX - lineWidth / 2;
             const right = centerX + lineWidth / 2;
+            // 垂直:dock 可见顶部往上延伸 30px(与 ::after 热区一致)
             if (e.clientX >= left && e.clientX <= right && e.clientY >= rect.top - 30) {
                 this.dockExpanded = true;
             }
@@ -225,125 +240,13 @@ export default {
                     behavior: 'smooth'
                 });
             }
-        },
-        getEllipseStyle(index) {
-            const total = this.dockItems.length;
-            const baseAngle = (index / total) * Math.PI * 2;
-            const finalAngle = baseAngle + this.ellipseRotation;
-
-            const x = Math.cos(finalAngle) * this.radiusX;
-            const y = Math.sin(finalAngle) * this.radiusY;
-
-            const sinVal = Math.sin(finalAngle);
-            const normalizedDepth = (sinVal + 1) / 2; 
-
-            let scale = 0.5 + (normalizedDepth * 0.7);
-            if (this.hoverIndex === index) scale *= 1.2;
-
-            const opacity = 0.3 + (normalizedDepth * 0.7);
-            const zIndex = Math.round(normalizedDepth * 100);
-
-            return {
-                position: 'absolute',
-                left: '50%',
-                top: '50%',
-                transform: `translate(-50%, -50%) translate(${x}px, ${y}px) scale(${scale})`,
-                opacity: opacity,
-                zIndex: zIndex,
-                transition: this.isDragging ? 'none' : 'transform 0.6s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.6s'
-            };
-        },
-        rotateMenu(direction) {
-            const total = this.dockItems.length;
-            const stepAngle = (Math.PI * 2) / total;
-            this.ellipseRotation -= direction * stepAngle;
-            this.updateFrontIndex();
-        },
-        rotateTo(index) {
-            const total = this.dockItems.length;
-            let diff = this.frontIndex - index;
-            if (diff > total / 2) diff -= total;
-            if (diff < -total / 2) diff += total;
-
-            const stepAngle = (Math.PI * 2) / total;
-            this.ellipseRotation += diff * stepAngle;
-            this.frontIndex = index;
-        },
-        updateFrontIndex() {
-            const total = this.dockItems.length;
-            const stepAngle = (Math.PI * 2) / total;
-            const currentRot = ((this.ellipseRotation % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
-            let targetIndex = Math.round((Math.PI / 2 - currentRot) / stepAngle);
-            this.frontIndex = ((targetIndex % total) + total) % total;
-        },
-        
-        // ================= 拖拽逻辑核心修改区 =================
-        handleDragStart(e) {
-            if (this.mode !== 'ellipse') return;
-            this.isDragging = true;
-            this.hasDragged = false;
-            this.pauseAutoRotate();
-            this.startX = e.clientX || (e.touches && e.touches[0].clientX);
-            this.currentX = this.startX;
-            
-            // 记录拖拽前的完美状态
-            this.startRotation = this.ellipseRotation;
-            this.startFrontIndex = this.frontIndex;
-        },
-        handleDragging(e) {
-            if (!this.isDragging) return;
-            this.currentX = e.clientX || (e.touches && e.touches[0].clientX);
-            const diffX = this.currentX - this.startX;
-            if (Math.abs(diffX) > 5) this.hasDragged = true; 
-            
-            // 拖动时,视觉上跟随鼠标
-            this.ellipseRotation = this.startRotation - (diffX / 350);
-            this.updateFrontIndex(); // 让中间的图标实时发光
-        },
-        handleDragEnd() {
-            if (!this.isDragging) return;
-            this.isDragging = false;
-            
-            const diffX = this.currentX - this.startX;
-            const threshold = 40; // 触发切换的距离阈值(滑动超过 40px 就切换)
-
-            // 【关键】无论拖动多远,先把底层状态恢复到起点
-            // 配合 isDragging = false 时的 transition 过渡,这能保证完美的滑动动画
-            this.ellipseRotation = this.startRotation;
-            this.frontIndex = this.startFrontIndex;
-
-            if (diffX < -threshold) {
-                // 向左滑:精准切换到“下一个”
-                this.rotateMenu(1);
-            } else if (diffX > threshold) {
-                // 向右滑:精准切换到“上一个”
-                this.rotateMenu(-1);
-            } else {
-                // 滑动距离不够,原地吸附回正(无操作,因为上面已经还原了 startRotation)
-            }
-
-            this.resumeAutoRotate();
-        },
-        // ======================================================
-
-        resumeAutoRotate() {
-            if (this.mode !== 'ellipse' || !this.autoRotate || this.rotateTimer) return;
-            this.rotateTimer = setInterval(() => {
-                this.rotateMenu(-1);
-            }, this.autoRotateSpeed);
-        },
-        pauseAutoRotate() {
-            if (this.rotateTimer) {
-                clearInterval(this.rotateTimer);
-                this.rotateTimer = null;
-            }
         }
     }
 };
 </script>
 
 <style scoped>
-/* ================= 以下是你的原版 CSS (原封不动) ================= */
+/* ================= 整体容器布局 ================= */
 .dock-wrapper {
     display: flex !important;
     flex-direction: row !important;
@@ -360,6 +263,7 @@ export default {
     pointer-events: auto !important;
 }
 
+/* === 状态 A:自动隐藏 === */
 .dock-wrapper.is-auto-hide {
     transform: translateY(calc(100% - clamp(15px, 4vh, 20px)));
     pointer-events: none;
@@ -369,6 +273,7 @@ export default {
     pointer-events: auto;
 }
 
+/* 中间触发热区:只覆盖发光线范围,pointer-events 始终开启 */
 .dock-wrapper.is-auto-hide::after {
     content: '';
     position: absolute;
@@ -381,12 +286,14 @@ export default {
     pointer-events: auto;
 }
 
+/* 发光的指示线自适应 */
 .dock-wrapper.is-auto-hide::before {
     content: '';
     position: absolute;
     top: 0;
     left: 50%;
     transform: translateX(-50%);
+    /* 宽度和厚度也做一点自适应,小屏自动变短点 */
     width: clamp(150px, 20vw, 250px);
     height: clamp(3px, 0.5vh, 5px); 
     background: rgba(0, 229, 255, 0.6);
@@ -400,6 +307,7 @@ export default {
     opacity: 0;
 }
 
+/* === 状态 B:常驻显示 === */
 .dock-wrapper.is-always-show {
     transform: translateY(0);
 }
@@ -408,6 +316,7 @@ export default {
     display: none;
 }
 
+/* ================= 内部列表与滚动容器 ================= */
 .dock-list-container {
     width: 750px;
     height: 160px;
@@ -434,6 +343,7 @@ export default {
     gap: 30px;
 }
 
+/* ================= 左右控制箭头 ================= */
 .nav-arrow {
     flex-shrink: 0;
     width: 40px;
@@ -474,6 +384,7 @@ export default {
   opacity: 0.6; 
 }
 
+/* ================= 单个导航项 ================= */
 .dock-item {
     flex-shrink: 0;
     position: relative;
@@ -503,6 +414,7 @@ export default {
     letter-spacing: 1px;
 }
 
+/* ================= 交互状态:悬浮与选中 ================= */
 .dock-item:hover {
     transform: translateY(-15px) scale(1.15);
 }
@@ -553,40 +465,25 @@ export default {
     filter: drop-shadow(0 0 8px rgba(0, 229, 255, 0.8));
 }
 
-/* ================= 针对 3D 椭圆模式的样式覆盖 ================= */
-.dock-list-container.is-ellipse-mode {
-    overflow: visible !important;
-    cursor: grab;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-}
-.dock-list-container.is-ellipse-mode:active {
-    cursor: grabbing;
+/* ================= 呼吸波浪动态效果 ================= */
+@keyframes breathing-wave {
+    0%, 100% {
+        transform: translateY(0) scale(1);
+    }
+    50% {
+        transform: translateY(-8px) scale(1.08); /* 呼吸时轻微上浮和放大 */
+    }
 }
 
-.dock-list.is-ellipse-mode {
-    position: relative !important;
-    width: 0 !important;
-    height: 0 !important;
-    min-width: 0 !important;
-    padding-bottom: 0 !important;
-    gap: 0 !important;
+.dock-item.is-breathing {
+    /* 动画周期设为 2.5s,无限循环,平滑过渡 */
+    animation: breathing-wave 2.5s infinite ease-in-out;
 }
 
-.dock-list.is-ellipse-mode .dock-item {
-    position: absolute;
-}
-.dock-list.is-ellipse-mode .dock-item:hover {
-    transform: none; 
-}
-
-.dock-list.is-ellipse-mode .dock-item.is-front .item-label {
-    color: #ffffff;
-    font-weight: bold;
-    text-shadow: 0 0 10px #00e5ff;
-}
-.dock-list.is-ellipse-mode .dock-item.is-front .custom-icon {
-    filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
+/* 覆盖动画冲突:当鼠标悬浮或该项处于激活状态时,停止呼吸动画,采用原有的高亮放大效果 */
+.dock-item:hover,
+.dock-item.is-active {
+    animation: none !important; /* 强制停止动画 */
+    transform: translateY(-15px) scale(1.15) !important;
 }
 </style>

+ 1 - 1
src/views/Main.vue

@@ -11,7 +11,7 @@
     </template>
 
     <template #main>
-      <BottomDock mode="ellipse" :auto-hide="false" :custom-class="['dock-style']" :auto-rotate="true"></BottomDock>
+      <BottomDock :auto-hide="false" :custom-class="['dock-style']" :wave-animation="true"></BottomDock>
     </template>
 
   </LoginLayout>