| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- <template>
- <div class="dock-wrapper"
- ref="dockWrapper"
- :class="[
- autoHide ? 'is-auto-hide' : 'is-always-show',
- customClass,
- { 'is-expanded': dockExpanded }
- ]"
- :style="dockStyles"
- @mouseleave="handleDockLeave">
-
- <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" 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-breathing': waveAnimation
- }"
- :style="waveAnimation ? { animationDelay: `${index * 0.15}s` } : {}"
- @click="handleSelect(index, item)"
- @mouseenter="hoverIndex = index"
- @mouseleave="hoverIndex = null"
- >
- <div class="item-icon">
- <img v-if="item.imgUrl"
- :src="(activeIndex === index || hoverIndex === index) && item.activeImgUrl ? item.activeImgUrl : item.imgUrl"
- class="custom-icon" />
- <i v-else :class="item.iconClass"></i>
- </div>
- <div class="item-label">{{ item.label }}</div>
- </div>
- </div>
- </div>
- <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>
- <script>
- export default {
- name: 'BottomDock',
- props: {
- // 是否自动隐藏 (默认 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 {
- activeIndex: -1,
- hoverIndex: null,
- dockExpanded: false,
- canScrollLeft: false,
- canScrollRight: true,
- dockItems: [
- {
- label: '首页',
- imgUrl: require('@/assets/main/dock-home.png'),
- activeImgUrl: require('@/assets/main/dock-home-hover.png'),
- route: '/home',
- theme: 'blue',
- },
- {
- label: '状态监控',
- imgUrl: require('@/assets/main/dock-surve.png'),
- activeImgUrl: require('@/assets/main/dock-surve-hover.png'),
- route: '/surve',
- theme: 'blue',
- },
- {
- label: '勤务管理',
- imgUrl: require('@/assets/main/dock-security.png'),
- activeImgUrl: require('@/assets/main/dock-security-hover.png'),
- route: '/security',
- theme: 'gold',
- },
- {
- label: '干线协调',
- imgUrl: require('@/assets/main/dock-coor.png'),
- activeImgUrl: require('@/assets/main/dock-coor-hover.png'),
- route: '/trunk',
- theme: 'blue',
- },
- {
- label: '数据分析',
- imgUrl: require('@/assets/main/dock-watch.png'),
- activeImgUrl: require('@/assets/main/dock-watch-hover.png'),
- route: '/watch',
- theme: 'blue',
- },
- {
- label: '系统设置',
- imgUrl: require('@/assets/main/dock-setting.png'),
- activeImgUrl: require('@/assets/main/dock-setting-hover.png'),
- route: '/setting',
- theme: 'blue',
- },
- {
- label: '测试1',
- imgUrl: require('@/assets/main/dock-home.png'),
- theme: 'blue',
- },
- {
- label: '测试2',
- imgUrl: require('@/assets/main/dock-surve.png'),
- theme: 'blue',
- },
- {
- label: '测试3',
- imgUrl: require('@/assets/main/dock-security.png'),
- theme: 'blue',
- },
- ]
- };
- },
- computed: {
- // 动态计算 CSS 变量和合并自定义样式
- dockStyles() {
- // 统一只传一个基础 bottom 偏移量,动画交由 CSS transform 处理
- return {
- '--dock-bottom': `${this.bottomOffset}px`,
- ...this.customStyle
- };
- }
- },
- watch: {
- $route() {
- this.updateActiveIndexByRoute();
- }
- },
- created() {
- this.updateActiveIndexByRoute();
- },
- mounted() {
- this.$nextTick(() => {
- this.checkScrollState();
- window.addEventListener('resize', this.checkScrollState);
- if (this.autoHide) {
- document.addEventListener('mousemove', this.handleGlobalMouseMove);
- }
- });
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.checkScrollState);
- document.removeEventListener('mousemove', this.handleGlobalMouseMove);
- },
- methods: {
- updateActiveIndexByRoute() {
- const currentPath = this.$route?.path || '';
- const matchIndex = this.dockItems.findIndex(item => {
- return item.route && currentPath.startsWith(item.route);
- });
- if (matchIndex !== -1) {
- this.activeIndex = matchIndex;
- }
- },
- handleSelect(index, item) {
- if (this.activeIndex === index) return;
- this.activeIndex = index;
- if (item.route && this.$router) {
- this.$router.push(item.route).catch(err => {
- if (err.name !== 'NavigationDuplicated') {
- console.error('路由跳转失败:', err);
- }
- });
- }
- this.$emit('change', item);
- },
- checkScrollState() {
- const container = this.$refs.listContainer;
- if (!container) return;
- this.canScrollLeft = container.scrollLeft > 0;
- this.canScrollRight = Math.ceil(container.scrollLeft + container.clientWidth) < container.scrollWidth;
- },
- handleGlobalMouseMove(e) {
- if (!this.autoHide || this.dockExpanded) return;
- 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;
- }
- },
- handleDockLeave() {
- this.dockExpanded = false;
- },
- scrollList(direction) {
- if (direction === -1 && !this.canScrollLeft) return;
- if (direction === 1 && !this.canScrollRight) return;
- const container = this.$refs.listContainer;
- const scrollAmount = 130;
- if (container) {
- container.scrollBy({
- left: direction * scrollAmount,
- behavior: 'smooth'
- });
- }
- }
- }
- };
- </script>
- <style scoped>
- /* ================= 整体容器布局 ================= */
- .dock-wrapper {
- display: flex !important;
- flex-direction: row !important;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 160px;
- position: fixed;
- left: 0;
- bottom: var(--dock-bottom, 0px);
- z-index: 9999;
- transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
- background: linear-gradient(to top, rgba(0, 20, 30, 0.8) 0%, rgba(0, 20, 30, 0.01) 100%);
- pointer-events: auto !important;
- }
- /* === 状态 A:自动隐藏 === */
- .dock-wrapper.is-auto-hide {
- transform: translateY(calc(100% - clamp(15px, 4vh, 20px)));
- pointer-events: none;
- }
- .dock-wrapper.is-auto-hide.is-expanded {
- transform: translateY(0);
- pointer-events: auto;
- }
- /* 中间触发热区:只覆盖发光线范围,pointer-events 始终开启 */
- .dock-wrapper.is-auto-hide::after {
- content: '';
- position: absolute;
- top: -30px;
- left: 50%;
- transform: translateX(-50%);
- width: clamp(150px, 20vw, 250px);
- height: 60px;
- background: transparent;
- 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);
- box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
- border-radius: 4px;
- transition: opacity 0.3s;
- pointer-events: auto;
- }
- .dock-wrapper.is-auto-hide.is-expanded::before {
- opacity: 0;
- }
- /* === 状态 B:常驻显示 === */
- .dock-wrapper.is-always-show {
- transform: translateY(0);
- }
- .dock-wrapper.is-always-show::before,
- .dock-wrapper.is-always-show::after {
- display: none;
- }
- /* ================= 内部列表与滚动容器 ================= */
- .dock-list-container {
- width: 750px;
- height: 160px;
- overflow-x: auto;
- overflow-y: hidden;
- white-space: nowrap;
- -ms-overflow-style: none;
- scrollbar-width: none;
- }
- .dock-list-container::-webkit-scrollbar {
- display: none;
- }
- .dock-list {
- display: flex !important;
- flex-direction: row !important;
- flex-wrap: nowrap !important;
- align-items: flex-end;
- height: 100%;
- width: max-content;
- min-width: 100%;
- padding-bottom: 20px;
- gap: 30px;
- }
- /* ================= 左右控制箭头 ================= */
- .nav-arrow {
- flex-shrink: 0;
- width: 40px;
- height: 40px;
- display: flex;
- justify-content: center;
- align-items: center;
- transition: all 0.3s;
- margin: 0 15px;
- background: transparent !important;
- border: none !important;
- }
- .arrow-img {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- .left-facing {
- transform: rotate(180deg);
- }
- .nav-arrow.is-active {
- cursor: pointer;
- }
- .nav-arrow.is-active:hover .arrow-img {
- transform: scale(1.1);
- filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
- }
- .nav-arrow.left-arrow.is-active:hover .arrow-img.left-facing {
- transform: rotate(180deg) scale(1.1);
- }
- .nav-arrow.is-disabled {
- cursor: not-allowed;
- opacity: 0.6;
- }
- /* ================= 单个导航项 ================= */
- .dock-item {
- flex-shrink: 0;
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-end;
- cursor: pointer;
- width: 100px;
- height: 90px;
- transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
- }
- .item-icon {
- font-size: 32px;
- color: #a0cfff;
- z-index: 3;
- margin-bottom: 5px;
- transition: all 0.3s;
- }
- .item-label {
- color: #c0c4cc;
- font-size: 14px;
- text-align: center;
- transition: all 0.3s;
- letter-spacing: 1px;
- }
- /* ================= 交互状态:悬浮与选中 ================= */
- .dock-item:hover {
- transform: translateY(-15px) scale(1.15);
- }
- .dock-item:hover .item-icon,
- .dock-item.is-active .item-icon {
- color: #ffffff;
- text-shadow: 0 0 15px #00e5ff;
- }
- .dock-item:hover .item-label,
- .dock-item.is-active .item-label {
- color: #ffffff;
- font-weight: bold;
- text-shadow: 0 0 8px #00e5ff;
- }
- .dock-item:hover .top-solid,
- .dock-item.is-active .top-solid {
- background: linear-gradient(135deg, rgba(0, 229, 255, 0.9), rgba(0, 115, 255, 0.9));
- box-shadow: 0 0 20px rgba(0, 229, 255, 0.6);
- }
- .dock-item.theme-gold.is-active .item-icon {
- color: #ffd700;
- text-shadow: 0 0 15px #ffaa00;
- }
- .dock-item.theme-gold.is-active .item-label {
- color: #ffd700;
- text-shadow: 0 0 8px #ffaa00;
- }
- .item-icon {
- z-index: 3;
- margin-bottom: 5px;
- transition: transform 0.3s ease, filter 0.3s ease;
- }
- .custom-icon {
- width: 88px;
- height: 64px;
- object-fit: contain;
- }
- .dock-item:hover .custom-icon,
- .dock-item.is-active .custom-icon {
- filter: drop-shadow(0 0 8px rgba(0, 229, 255, 0.8));
- }
- /* ================= 呼吸波浪动态效果 ================= */
- @keyframes breathing-wave {
- 0%, 100% {
- transform: translateY(0) scale(1);
- }
- 50% {
- transform: translateY(-8px) scale(1.08); /* 呼吸时轻微上浮和放大 */
- }
- }
- .dock-item.is-breathing {
- /* 动画周期设为 2.5s,无限循环,平滑过渡 */
- animation: breathing-wave 2.5s infinite ease-in-out;
- }
- /* 覆盖动画冲突:当鼠标悬浮或该项处于激活状态时,停止呼吸动画,采用原有的高亮放大效果 */
- .dock-item:hover,
- .dock-item.is-active {
- animation: none !important; /* 强制停止动画 */
- transform: translateY(-15px) scale(1.15) !important;
- }
- </style>
|