
- 要实现能转起来连接,其实就是要展示的数据复制一份,下半部分的⚪隐藏即可
- 通过
getItemPosition
计算每个元素在圆上的位置
- 让大转盘进行旋转,为了里面的元素是正向展示的,则同时每个元素进行反方向同角度进行旋转
- 由于UI图是椭圆的,则Y轴进行压缩
transform: scaleY(0.7);
,每个子元素为了正常展示scaleY(1.42)
- 按照需求可添加鼠标移动进入是否暂停动画
handleAnimation
<template>
<div class="circular-menu">
<div class="center-button" @click="handleCenterClick">
<button class="default-center-btn">加入我们</button>
</div>
<div class="circular-menu-container" @mouseenter="loopClose()" @mouseleave="loopStart()">
<div class="circular-wheel" ref="wheel" @mouseenter="pauseRotation" @mouseleave="resumeRotation">
<div ref="whellOrigin" class="wheel-origin">
<div v-for="(item, index) in menuItems" :key="index" class="wheel-item"
:class="{ 'active': activeIndex === index }" :style="getItemPosition(index)"
@click="handleItemClick(index)">
<img :src="getIconPath(item.icon)" :alt="item.title" class="item-icon" />
<p class="item-title">{{ item.title }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CircularMenu',
props: {
items: {
type: Array,
default: () => [
{ title: "XXX", icon: "h" },
{ title: "XXX", icon: "hv" },
{ title: "XXX", icon: "hk" },
{ title: "XXX", icon: "net" },
{ title: "XXX", icon: "hua" },
{ title: "XXX", icon: "shu" },
{ title: "XXX", icon: "m" },
{ title: "XXX", icon: "ev" },
{ title: "XXX", icon: "rjgf" },
{ title: "XXX", icon: "sr" },
{ title: "XXX", icon: "h" },
{ title: "XXX", icon: "hv" },
{ title: "XXX", icon: "hk" },
{ title: "XXX", icon: "net" },
{ title: "XXX", icon: "hua" },
{ title: "XXX", icon: "shu" },
{ title: "XXX", icon: "m" },
{ title: "XXX", icon: "ev" },
{ title: "XXX", icon: "rjgf" },
{ title: "XXX", icon: "sr" },
]
},
startAngle: {
type: Number,
default: 0
}
},
data() {
return {
currentRotation: this.startAngle,
activeIndex: 0,
radius: 0,
isPaused: false,
animationFrame: null
}
},
computed: {
menuItems() {
return this.items
}
},
mounted() {
this.initWheel()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
cancelAnimationFrame(this.animationFrame)
window.removeEventListener('resize', this.handleResize)
},
methods: {
initWheel() {
this.$nextTick(() => {
const wheel = this.$refs.wheel
this.radius = wheel ? wheel.offsetWidth / 2 : 0
})
},
loopClose() {
this.handleAnimation("paused");
},
loopStart() {
this.handleAnimation("running");
},
handleAnimation(params) {
this.$refs.whellOrigin.style.animationPlayState = params;
let cirulaItem = document.querySelectorAll(".wheel-item");
for (let i = 0; i < cirulaItem.length; i++) {
cirulaItem[i].style.animationPlayState = params;
}
},
getItemPosition(index) {
const angle = (index * (360 / this.menuItems.length)) - 90
const rad = (angle * Math.PI) / 180
const distance = this.radius * 0.85
return {
left: `${this.radius + distance * Math.cos(rad)}px`,
top: `${this.radius + distance * Math.sin(rad)}px`,
}
},
getIconPath(icon) {
return require(`@/assets/images/ui-demo/portal/custom/${icon}.png`)
},
pauseRotation() {
this.isPaused = true
},
resumeRotation() {
this.isPaused = false
},
handleItemClick(index) {
this.activeIndex = index
this.$emit('item-click', this.menuItems[index])
},
handleCenterClick() {
this.$emit('center-click')
},
handleResize() {
this.initWheel()
}
}
}
</script>
<style lang="scss" scoped>
.circular-menu {
aspect-ratio: 2 / 1;
position: relative;
width: 100vw;
max-width: 80vw;
overflow: hidden;
margin: auto;
margin-top: calc(-0.3 * 40vw);
background: url(~@/assets/images/ui-demo/portal/custom-bg.png) center bottom /100% auto no-repeat;
&::after {
display: inline-block;
width: 100%;
height: 30%;
background: linear-gradient(0, rgba(244, 246, 250, 1) 0%, rgba(244, 246, 250, 0) 100%);
content: ' ';
position: absolute;
bottom: -4px;
z-index: 10;
max-height: 265px;
}
.center-button {
z-index: 13;
position: absolute;
left: 50%;
transform: translate(-50%, 0%);
bottom: 108px;
width: 120px;
height: 48px;
background: linear-gradient(135deg, #1364DF, #0396FF);
font-size: 18px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
overflow: hidden;
&::before {
content: "";
width: 100%;
height: 100%;
background: #1364DF;
top: 0;
left: -100%;
position: absolute;
transition: all 0.6s cubic-bezier(0.32, 0.94, 0.6, 1);
}
&:hover {
&::before {
left: 0;
}
}
.default-center-btn {
background: transparent;
border: none;
color: white;
font-size: 18px;
font-weight: bold;
cursor: pointer;
z-index: 10;
line-height: 48px;
width: 100%;
text-align: center;
position: relative;
}
}
}
.circular-menu-container {
position: relative;
width: 100vw;
max-width: 80vw;
margin: 0 auto;
aspect-ratio: 1;
transform: scaleY(0.7);
.circular-wheel {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
.wheel-origin {
position: absolute;
width: 100%;
height: 100%;
transform-origin: center;
animation: rotate 45s infinite linear;
.wheel-item {
position: absolute;
animation: rotate2 45s infinite linear;
width: 80px;
height: 80px;
margin-left: -40px;
margin-top: -40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
transform-origin: center;
&:hover,
&.active {
transform: scale(1.1) rotate(v-bind('currentRotation * -1deg'));
.item-title {
color: #176AFD;
font-weight: bold;
}
}
.item-icon {
width: 160px;
height: 80px;
border-radius: 4px;
margin-bottom: 6px;
object-fit: contain;
transition: transform 0.3s ease;
}
.item-title {
margin: 0;
color: #15161A;
font-size: 20px;
color: #333;
white-space: nowrap;
transition: color 0.3s ease;
}
}
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes rotate2 {
from {
transform: rotate(0deg) scaleY(1.42);
}
to {
transform: rotate(-360deg) scaleY(1.42);
}
}
@media (max-width: 1500px) {
.circular-menu-container {
.wheel-item {
width: 70px !important;
height: 70px !important;
margin-left: -35px !important;
margin-top: -35px !important;
.item-icon {
width: 128px !important;
height: 64px !important;
}
.item-title {
font-size: 16px !important;
}
}
}
}
@media (max-width: 1200px) {
.circular-menu-container {
.wheel-item {
width: 60px !important;
height: 60px !important;
margin-left: -30px !important;
margin-top: -30px !important;
.item-icon {
width: 96px !important;
height: 48px !important;
}
.item-title {
font-size: 16px !important;
}
}
}
}
</style>