当前位置: 首页 > article >正文

使用Vue3实现可拖拽的九点导航面板

开篇

本文使用Vue3实现了一个可拖拽的九宫导航面板。这个面板在我这里的应用场景是我个人网站的首页的位置,九宫导航对应的是用户最后使用或者最多使用的九个功能,正常应该是由后端接口返回的,不过这里为了简化,写的是固定的数组数据。

效果展示

截图

在这里插入图片描述在这里插入图片描述

视频

九点导航面板

功能概述

该导航面板初始状态为三行三列排布的九个圆点的集合,当鼠标放上去之后,九个圆点就会变成九个功能的图标。同时,该面板具有可拖拽功能,可以拖到浏览器上任何一个位置。

代码实现

<template>
  <div 
    class="nine-point-container"
    v-draggable
    :style="{ left: position.x + 'px', top: position.y + 'px' }"
    :class="{ 'expanded': isPanelHovered && !isDragging }"
  >
    <!-- 九点导航面板 -->
    <div 
      class="nine-point-panel"
      @mouseenter="handlePanelHover(true)"
      @mouseleave="handlePanelHover(false)"
      :class="{ 'panel-expanded': isPanelHovered && !isDragging }"
    >
      <!-- 背景遮罩,只在未展开状态显示 -->
      <div class="background-mask" :class="{ 'mask-hidden': isPanelHovered && !isDragging }"></div>
      
      <div 
        v-for="(item, index) in navigationItems" 
        :key="index"
        class="nav-item"
        :style="getItemStyle(index)"
      >
        <!-- 默认状态显示圆点 -->
        <div 
          class="dot" 
          :class="{ 'dot-hidden': isPanelHovered && !isDragging }"
          :style="getDotStyle(index)"
        ></div>
        
        <!-- 图标和文字 -->
        <div 
          class="hover-content" 
          :class="{ 'content-visible': isPanelHovered && !isDragging }"
          :style="getContentStyle(index)"
        >
          <el-icon><component :is="item.icon" /></el-icon>
          <span class="item-text">{{ item.text }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { 
  Clock,
  FullScreen,
  CirclePlus,
  Search,
  Message,
  Crop,
  Delete,
  Tools,
  PieChart
} from '@element-plus/icons-vue'

// 添加拖动状态
const isDragging = ref(false)

// 修改拖拽指令
const vDraggable = {
  mounted(el) {
    el.style.position = 'fixed'
    el.style.cursor = 'move'
    
    el.onmousedown = (e) => {
      isDragging.value = true
      const disX = e.clientX - el.offsetLeft
      const disY = e.clientY - el.offsetTop
      
      document.onmousemove = (e) => {
        let left = e.clientX - disX
        let top = e.clientY - disY
        
        // 防止拖出视口
        const maxX = window.innerWidth - el.offsetWidth
        const maxY = window.innerHeight - el.offsetHeight
        
        left = Math.min(maxX, Math.max(0, left))
        top = Math.min(maxY, Math.max(0, top))
        
        position.value.x = left
        position.value.y = top
      }
      
      document.onmouseup = () => {
        document.onmousemove = null
        document.onmouseup = null
        // 添加一个小延时,防止拖动结束后立即触发hover效果
        setTimeout(() => {
          isDragging.value = false
        }, 100)
      }
    }
  }
}

// 组件位置状态
const position = ref({
  x: 20,
  y: 20
})

// 面板悬停状态
const isPanelHovered = ref(false)

// 处理面板悬停
const handlePanelHover = (isHovered) => {
  isPanelHovered.value = isHovered
}

// 导航项配置 - 移除 isHovered 属性,因为现在统一控制
const navigationItems = ref([
  { icon: FullScreen, text: '全屏' },
  { icon: CirclePlus, text: '新建' },
  { icon: Clock, text: '时钟' },
  { icon: Search, text: '搜索' },
  { icon: Message, text: '消息' },
  { icon: Crop, text: '裁剪' },
  { icon: Delete, text: '删除' },
  { icon: Tools, text: '工具' },
  { icon: PieChart, text: '图表' }
])

// 计算每个项目的动画延迟
const getItemStyle = (index) => {
  // 计算项目在网格中的位置
  const row = Math.floor(index / 3)
  const col = index % 3
  
  // 计算到中心点的距离(用于径向动画)
  const centerRow = 1
  const centerCol = 1
  const distance = Math.sqrt(
    Math.pow(row - centerRow, 2) + 
    Math.pow(col - centerCol, 2)
  )
  
  // 基础延迟时间(ms)
  const baseDelay = distance * 50

  return {
    '--item-delay': `${baseDelay}ms`,
    '--item-row': row,
    '--item-col': col,
    '--item-distance': distance
  }
}

// 圆点的特定样式
const getDotStyle = (index) => {
  return {
    '--dot-delay': `${index * 30}ms`
  }
}

// 内容的特定样式
const getContentStyle = (index) => {
  return {
    '--content-delay': `${index * 30}ms`
  }
}
</script>

<style lang="scss" scoped>
.nine-point-container {
  z-index: 1000;
  user-select: none;
  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
  width: 120px; // 初始较小的尺寸
  height: 120px;
  
  &.expanded {
    width: 300px; // 展开后的较大尺寸
    height: 300px;
    transform: scale(1.1);
  }
}

.nine-point-panel {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px; // 初始较小的间距
  padding: 12px; // 初始较小的内边距
  border-radius: 16px;
  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
  
  // 背景遮罩
  .background-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(30, 30, 30, 0.9);
    backdrop-filter: blur(10px);
    border-radius: 16px;
    transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
    z-index: -1;
    
    &.mask-hidden {
      opacity: 0;
      transform: scale(1.2);
    }
  }
  
  &.panel-expanded {
    gap: 24px;
    padding: 24px;
  }
}

.nav-item {
  position: relative;
  width: 24px; // 初始较小的尺寸
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
  
  .dot {
    width: 6px; // 初始较小的圆点
    height: 6px;
    background: rgba(255, 255, 255, 0.5);
    border-radius: 50%;
    position: absolute;
    transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
    transition-delay: var(--dot-delay);
    transform-origin: center;
    box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
    
    &.dot-hidden {
      transform: scale(0) rotate(180deg);
      opacity: 0;
    }
  }
  
  .hover-content {
    position: absolute;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: #fff;
    opacity: 0;
    transform: scale(0.6) rotate(-45deg);
    transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
    transition-delay: var(--content-delay);
    filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.2));
    
    &.content-visible {
      opacity: 1;
      transform: scale(1) rotate(0deg);
      width: 60px;
      height: 60px;
      
      .el-icon {
        color: #409EFF; // 默认使用Element Plus的主题蓝色
      }
      
      .item-text {
        color: #E6E8EB; // 浅灰色文字
      }
    }
    
    .el-icon {
      font-size: 24px;
      margin-bottom: 8px;
      transition: all 0.3s ease;
      // 添加渐变色图标效果
      background: linear-gradient(120deg, #409EFF, #53C1FF);
      -webkit-background-clip: text;
      background-clip: text;
      -webkit-text-fill-color: transparent;
      filter: drop-shadow(0 0 8px rgba(64, 158, 255, 0.3));
    }
    
    .item-text {
      font-size: 12px;
      opacity: 0;
      transform: translateY(10px);
      transition: all 0.3s ease;
      transition-delay: calc(var(--content-delay) + 100ms);
      white-space: nowrap;
      text-shadow: 0 0 10px rgba(230, 232, 235, 0.3);
    }
  }
  
  &:hover {
    .hover-content {
      .el-icon {
        transform: scale(1.2);
        filter: drop-shadow(0 0 12px rgba(64, 158, 255, 0.5));
      }
      
      .item-text {
        opacity: 1;
        transform: translateY(0);
        color: #FFFFFF;
      }
    }
    
    background: transparent; // 移除悬停背景色
    border-radius: 12px;
    transform: translateZ(20px);
  }
}

// 修改图标颜色样式
.nav-item {
  &:nth-child(1) .hover-content .el-icon { color: #FF6B6B; }
  &:nth-child(2) .hover-content .el-icon { color: #4ECDC4; }
  &:nth-child(3) .hover-content .el-icon { color: #96E6A1; }
  &:nth-child(4) .hover-content .el-icon { color: #A18CD1; }
  &:nth-child(5) .hover-content .el-icon { color: #FF9A9E; }
  &:nth-child(6) .hover-content .el-icon { color: #84FAB0; }
  &:nth-child(7) .hover-content .el-icon { color: #FF9A9E; }
  &:nth-child(8) .hover-content .el-icon { color: #43E97B; }
  &:nth-child(9) .hover-content .el-icon { color: #FA709A; }

  .hover-content {
    .el-icon {
      font-size: 24px;
      margin-bottom: 8px;
      transition: all 0.3s ease;
      // 移除渐变背景
      filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
    }
    
    &.content-visible .el-icon {
      filter: drop-shadow(0 0 12px rgba(255, 255, 255, 0.5));
    }
  }
  
  &:hover .hover-content .el-icon {
    filter: drop-shadow(0 0 15px currentColor);
  }
}

// 优化展开动画
.panel-expanded .nav-item .hover-content {
  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  
  .el-icon {
    transition: all 0.3s ease, filter 0.5s ease;
  }
}

// 添加拖动时的样式
.nine-point-container.dragging {
  cursor: grabbing;
  
  .nav-item {
    pointer-events: none;
  }
}
</style>

后续可优化点

  • 九个功能图标由后端动态返回,可动态展示用户最常用的九个功能或者最近使用的九个功能的快捷入口;
  • 九个功能图标的样式和颜色,也可由后端返回,并增加对应的功能图标的样式配置页面;
  • 可进一步优化由九点到图标的动画效果;

以上便是九点导航面板的全部实现代码,希望能对您有所抛砖引玉的作用~
感谢阅读!


http://www.kler.cn/a/524869.html

相关文章:

  • 51单片机开发:串口通信
  • FireFox | Google Chrome | Microsoft Edge 禁用更新 final版
  • 四.3 Redis 五大数据类型/结构的详细说明/详细使用( hash 哈希表数据类型详解和使用)
  • 1.27补题 回训练营
  • 1.26学习
  • Autogen_core 测试代码:test_cache_store.py
  • Kafka的消息协议
  • Linux学习笔记——磁盘管理命令
  • ECMAScript 6语法
  • 【某大厂一面】ThreadLocal如何实现主子线程之间的数据同步
  • HTB--Administrator
  • hunyuan 混元学习
  • Codeforces Round 990 (Div. 2) 题解 A ~ D
  • PySalsa:灵活强大的Python库,专为网络数据分析设计
  • 租车骑绿岛
  • 【解决方案】VMware虚拟机adb连接宿主机夜神模拟器
  • 006 LocalStorage和SessionStorage
  • 1.五子棋对弈python解法——2024年省赛蓝桥杯真题
  • 春晚舞台上的人形机器人:科技与文化的奇妙融合
  • Elasticsearch有哪些应用场景?
  • P4681 [THUSC 2015] 平方运算 Solution
  • 2025_1_29 C语言学习中关于指针
  • 前端拖拽相关功能详解,一篇文章总结前端关于拖拽的应用场景和实现方式(含源码)
  • 【AI论文】Omni-RGPT:通过标记令牌统一图像和视频的区域级理解
  • 单机伪分布Hadoop详细配置
  • 萌新学 Python 之数值处理函数 round 四舍五入、abs 绝对值、pow 幂次方、divmod 元组商和余数