vue实现半圆转盘旋转(门户网页上)

发布于:2025-05-10 ⋅ 阅读:(15) ⋅ 点赞:(0)

在这里插入图片描述

  1. 要实现能转起来连接,其实就是要展示的数据复制一份,下半部分的⚪隐藏即可
  2. 通过getItemPosition计算每个元素在圆上的位置
  3. 让大转盘进行旋转,为了里面的元素是正向展示的,则同时每个元素进行反方向同角度进行旋转
  4. 由于UI图是椭圆的,则Y轴进行压缩transform: scaleY(0.7);,每个子元素为了正常展示scaleY(1.42)
  5. 按照需求可添加鼠标移动进入是否暂停动画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()
        //   this.startAutoRotation()
        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 // -90度从顶部开始
            const rad = (angle * Math.PI) / 180
            const distance = this.radius * 0.85 // 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>

网站公告

今日签到

点亮在社区的每一天
去签到