支持mouse、touch事件
<CircleCarousel
v-model="index"
:items="list"
:radius="180"
:selectedAngle="-90"
@change="onChange"
/>
可设置selectedAngle调整选中项角度所在
<template>
<div
ref="container"
class="circle-carousel"
@mousedown="startDrag"
@touchstart="startDrag"
>
<div
v-for="(item, i) in items"
:key="i"
class="circle-item"
:class="{ active: i === activeIndex }"
:style="getItemStyle(i)"
>
<slot :item="item" :index="i">
{{ item }}
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
interface Props {
items: any[]
radius?: number
selectedAngle?: number
modelValue?: number
}
const props = withDefaults(defineProps<Props>(), {
radius: 200,
selectedAngle: 0,
modelValue: 0
})
const emit = defineEmits(["update:modelValue", "change"])
const container = ref<HTMLDivElement>()
const rotation = ref(0)
const activeIndex = ref(props.modelValue)
const dragging = ref(false)
let centerX = 0
let centerY = 0
let lastAngle = 0
const stepAngle = computed(() => 360 / props.items.length)
function getItemStyle(index: number) {
const angle = index * stepAngle.value + rotation.value
const rad = (angle * Math.PI) / 180
const x = Math.cos(rad) * props.radius
const y = Math.sin(rad) * props.radius
return {
transform: `translate(${x}px, ${y}px)`
}
}
function getPoint(e: MouseEvent | TouchEvent) {
return "touches" in e ? e.touches[0] : e
}
function getAngle(x: number, y: number) {
return Math.atan2(y - centerY, x - centerX) * 180 / Math.PI
}
function startDrag(e: MouseEvent | TouchEvent) {
if (!container.value) return
dragging.value = true
const rect = container.value.getBoundingClientRect()
centerX = rect.left + rect.width / 2
centerY = rect.top + rect.height / 2
const p = getPoint(e)
lastAngle = getAngle(p.clientX, p.clientY)
window.addEventListener("mousemove", moveDrag)
window.addEventListener("touchmove", moveDrag)
window.addEventListener("mouseup", endDrag)
window.addEventListener("touchend", endDrag)
}
function moveDrag(e: MouseEvent | TouchEvent) {
if (!dragging.value) return
const p = getPoint(e)
const angle = getAngle(p.clientX, p.clientY)
let delta = angle - lastAngle
if (delta > 180) delta -= 360
if (delta < -180) delta += 360
rotation.value += delta
lastAngle = angle
updateActive()
}
function endDrag() {
dragging.value = false
snapToNearest()
window.removeEventListener("mousemove", moveDrag)
window.removeEventListener("touchmove", moveDrag)
window.removeEventListener("mouseup", endDrag)
window.removeEventListener("touchend", endDrag)
}
function updateActive() {
const step = stepAngle.value
const index =
Math.round((props.selectedAngle - rotation.value) / step) %
props.items.length
const fixed = (index + props.items.length) % props.items.length
if (fixed !== activeIndex.value) {
activeIndex.value = fixed
emit("update:modelValue", fixed)
emit("change", fixed)
}
}
function snapToNearest() {
const step = stepAngle.value
const target =
Math.round((props.selectedAngle - rotation.value) / step) * step
animateTo(props.selectedAngle - target)
}
function animateTo(target: number) {
const start = rotation.value
const diff = target - start
const duration = 300
const startTime = performance.now()
function frame(now: number) {
const t = Math.min((now - startTime) / duration, 1)
const ease = 1 - Math.pow(1 - t, 3)
rotation.value = start + diff * ease
updateActive()
if (t < 1) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}
onMounted(() => {
rotation.value = -props.modelValue * stepAngle.value
updateActive()
})
</script>
<style scoped>
.circle-carousel {
width: 500px;
height: 500px;
margin: auto;
position: relative;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
cursor: grab;
}
.circle-item {
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #eee;
transition: transform 0.2s;
}
.circle-item.active {
background: #409eff;
color: white;
transform: scale(1.3);
}
</style>
使用示例
<template>
<CircleCarousel
v-model="index"
:items="list"
:radius="180"
:selectedAngle="0"
@change="onChange"
/>
</template>
<script setup lang="ts">
import { ref } from "vue"
import CircleCarousel from "./components/CircleCarousel.vue"
const index = ref(0)
const list = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
]
function onChange(i: number) {
console.log("选中:", i)
}
</script>