开篇
本文使用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>
后续可优化点
- 九个功能图标由后端动态返回,可动态展示用户最常用的九个功能或者最近使用的九个功能的快捷入口;
- 九个功能图标的样式和颜色,也可由后端返回,并增加对应的功能图标的样式配置页面;
- 可进一步优化由九点到图标的动画效果;
注
以上便是九点导航面板的全部实现代码,希望能对您有所抛砖引玉的作用~
感谢阅读!