css动画实现大转盘(js优化扇形)

396 阅读8分钟

分析

  1. 首先我们需要通过css的clip-path,来裁剪扇形,具体情况下面的方法drawing或者案例代码
    • 或者通过skew扭曲成扇形
    • translate(50%,-50%) rotate(${(index * rotate) + (-90+180/lengthNum)}deg) skew(${(360 - (lengthNum * 90))/list.length}deg);transform-origin:0% 100% 进行先旋转角度,然后扭曲扇形
    • 最后通过transform-origin: center bottom;position: absolute;width: 50%;height:50%;bottom:0;top:revert;transform: translate(-50%, 0) skew(${0 - ((360 - (lengthNum * 90))/list.length)}deg) rotate(${(180 - rotate)/2}deg); ,先扭曲负的,然后旋转将里面的内容回正
    • 具体情况下面的方法drawing1或者案例代码
  2. 通过css动画实现大转盘的旋转
  3. 通过animationiteration监听动画重复播放时触发
  4. 最后获取到了需要旋转的选项,计算出需要旋转的角度,然后停止动画,通过transition过度动画旋转到选项的角度
  • html:

    <div id='app' v-cloak>
                           <div class="whellMain">
                                   <div class="whell" ref="whell" :class="[stop?'whellStop':'']" @animationiteration='animationiteration' @click="zhuanFn">
                                           <div class="whellKuang" v-for="(item,index) in list" :key="index" :ref="`whell_${index}`" :style="{'background-color':item.color}">
                                                   <!-- 文字显示的地方 -->
                                                   <div class="textItem" :ref="`textItem_${index}`">{{item.title}}</div>
                                           </div>
                                   </div>
                                   <div class="btnDian"></div>
                           </div>
    
                   </div>
    
  • css:

          [v-cloak]{
                            display: none;
                          }
                          .whellMain{
                                  position: relative;
                                  width: 400px;
                                  height: 400px;
                          }
                          .btnDian{
                                  width: 100px;
                                  height: 100px;
                                  position: absolute;
                                  top: 50%;
                                  left: 50%;
                                  transform: translate(-50%,-50%);
                                  border-radius: 50%;
                                  box-shadow: 5px 5px 10 5px rgba(0,0,0,0.5);
                                  background-color: #fff;
                                  pointer-events: none;
    
                          }
                          .btnDian::before{
                                  content: ' ';
                                  width: 30px;
                                  height: 20px;
                                  background-color: #fff;
                                  position: absolute;
                                  top: 0;
                                  left: 50%;
                                  transform: translate(-50%,-90%);
                                  clip-path:polygon(0 100%,50% 0,100% 100%);
                                  -webkit-clip-path:polygon(0 100%,50% 0,100% 100%);
    
                          }
                          .whell{
                                  width: 100%;
                                  height: 100%;
                                  border-radius: 50%;
                                  border: 1px solid;
                                  box-sizing: border-box;
                                  position: relative;
                                  animation: rotateAm 0.5s infinite linear;
                                  transform-origin: center center;
                          }
    
                          .whellStop{
                                  animation-play-state: paused;
                          }
                          .whellKuang{
                                  width: 100%;
                                  height: 100%;
                                  border-radius: 50%;
                                  position: absolute;
                                  top: 0;
                                  left: 0;
                                  overflow: hidden;
                          }
                          .textItem{
                                  height: 50%;
                                  transform-origin: center bottom;
                                  position: absolute;
                                  top: 0;
                                  display: flex;
                                  align-items: center;
                                  justify-content: center;
                                  color: #fff;
                                  font-size: 36px;
                          }
                          @keyframes  rotateAm{
                                  0%{
                                          transform:rotate(0deg)
                                  }
                                  100%{
                                          transform:rotate(360deg)
                                  }
                          }
    
    • js:

      1. 通过drawing画扇形
           // 方法1(裁剪,计算出扇形,最少两个)
           drawing(rotate,dom,width,cb){
        	var mask = dom
        	var r = width/2
        	var backgroundColor=mask.style['background-color'] || null
        	if(rotate<90){
        		var rad = rotate * Math.PI/180;
        		var x = r*Math.tan(rad)+r
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,${x}px 0,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,${x}px 0,50% 50%)`
        	}else if(rotate==90){
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 50%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 50%,50% 50%)`
        	}else if(rotate<180){
        		var rad1 = (rotate-90) * Math.PI/180;
        		var y = r*Math.tan(rad1)+r
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% ${y}px,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% ${y}px,50% 50%)`
        	}else if(rotate==180){
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,50% 100%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,50% 100%,50% 50%)`
        	}else if(rotate<270){
        		var rad2 = (rotate-180) * Math.PI/180;
        		var x1 = r-r*Math.tan(rad2)
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,${x1}px 100%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,${x1}px 100%,50% 50%)`
        	}else if(rotate==270){
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 50%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 50%,50% 50%)`
        	}else if(rotate<360){
        		var rad3 = (rotate-270) * Math.PI/180;
        		var y2 = r-r*Math.tan(rad3)
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% ${y2}px,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% ${y2}px,50% 50%)`
        	}else{
        		mask.style=`background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 0%,50% 0%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 0%,50% 0%)`
        	}
        	cb && cb()
        }
      
      1. 通过animationiteration监听动画,并执行最后的转动
      animationiteration(){
       	if(this.checkObject.du>0){
       		this.stop=true
       		let time=this.checkObject.du/360*0.5
       		this.$refs.whell.style.rotate=`${this.checkObject.du}deg`
       		this.$refs.whell.style.transition = `rotate ${time}s ease-out`
       		// 动画停止,传递参数
       		setTimeout(()=>{
       			this.endFn(true)
       		},time*1000)
       	}
       },
                   
                   // 方法2(使用变形,扭曲成扇形,最少三个)
                   drawing1 (rotate, domObject, index, cb){
                       //转盘扇形dom 和 内部元素 dom,具体看案例
                       const {whellDom,textItemDom} = domObject
                       var lengthNum = 360 / rotate
                       const skewDeg = (360 - (lengthNum * 90))/lengthNum
                       const deffAddRotate = (rotate/2)+(-90+180/lengthNum)
                       // 旋转角度,使用skew变形
                       whellDom.style.transform=`translate(50%,-50%)  rotate(${(index * rotate) +deffAddRotate}deg) skew(${skewDeg}deg)`
                       whellDom.style.transformOrigin = '0% 100%'
                       whellDom.style.borderRadius = 'revert'
                       // 文字旋转,并恢复变形
                       textItemDom.style=`transform-origin: center bottom;position: absolute;width: 50%;height:50%;bottom:0;top:revert;transform: translate(-50%, 0) skew(${0 - skewDeg}deg) rotate(${(180 - rotate)/2}deg);`
                       cb && cb();
                   }
      
      1. 案例:

      抽离成组件:

      使用:

      <template>
         <div @click="selectClick">
              <turntable class="customizeWrap" ref="turntableDom" @end='whellEnd'>
                <turntableItem v-for="item in list" :key="item.id">
                  <template #left>
                    <div style="width: 1px;height: 100%;background-color: #f0c55b;"></div>
                  </template>
                  <div class="textMain">
                     <div class="textItem">
                        <div class="title">{{ item.title }}</div>
                     </div>
                  </div>
                  <template #right>
                    <div style="width: 1px;height: 100%;background-color: #f0c55b;"></div>
                  </template>
                </turntableItem>
                 <template #btn>
                  <div class="">123</div>
                </template>
            </turntable>
          </div>
      </template>
      
      <script setup lang="ts">
        interface TurntableInstance {
          turntableStart: (callback: (stopValue: number) => void) => void;
          checkSelect: (selectedIndex: number,type?:'start' | 'end' | 'reset') => void;
        }
        const turntableDom = ref<TurntableInstance|null>(null);
        
        const list = ref<any[]>([
              {
                id: 1,
                title: "111111"
              },
              {
                id: 2,
                title: "222"
              },
              {
                id: 3,
                title: "333"
              },
              {
                id: 4,
                title: "444"
              },
              {
                id: 5,
                title: "555"
              },
              {
                id: 6,
                title: "666"
              }
         ]);
      
        const selectClick=()=>{
          //先旋转,后接口
          turntableDom.value?.turntableStart((stop:any)=>{
            setTimeout(()=>{
              // stop(3)
              turntableDom.value?.checkSelect(2)
            },3000)
          })
          
          // 成功的但是指向线(有动画)
          // turntableDom.value?.checkSelect(-1)
      
          // 失败的,突然停止(0:初始位置,-1:线)
          // turntableDom.value?.checkSelect(-1, 'reset');
        }
        const whellEnd=(index:number)=>{
          console.log(index)
        }
      </script>
      

      turntable

        <template>
            <div class="whellMain" :style="{ '--skew':(360 - (childComponents.length * 90))/childComponents.length}">
                <transition appear name="fadeWhellBg">
                    <div class="whellBg" v-if="isPlayRotate"></div>
                </transition>
                <div @animationiteration="animationiteration"
                    :style="{ '--amTimeS': amTimeS, '--anEs': optionsData.anEs, transform: `rotate(${optionsData.rotateNum}deg) translate3d(0,0,0)`, transition: optionsData.transitionString }"
                    class="whell" ref="whell" :class="[stop ? 'whellStop' : 'whellPlay']">
                    <slot></slot>
                    <div class="whellMainSlotBg">
                      <slot name="bg"></slot>
                    </div>
                </div>
                <div class="btnMain">
                    <slot name="btn">
                        <div class="btnDian"></div>
                    </slot>
                </div>
      
            </div>
        </template>
        <script setup lang="ts">
        import { ref,watch,  provide, nextTick, onMounted, computed, defineExpose,defineEmits,withDefaults,defineProps } from 'vue'
      
        const props = withDefaults(defineProps<{
            whellTime?:number // 秒
            isCenter?:boolean // 是否居中到第一个
            isOffset?:boolean // 是否偏移
        }>(), { 
            whellTime:0.8,
            isCenter:false,
            isOffset:true
        })
      
        const emit=defineEmits(['start','end'])
        const whell = ref<any>(null)
        const isPlayRotate=ref<boolean>(false)
        // 管理子组件传递参数
        const childComponents = ref<any[]>([]);
        // 提供注册方法给子组件
        provide('parentSetChild', {
            registerChild: (child: any) => {
               const index = childComponents.value.findIndex(n=>n.uuid === child.uuid)
               if(index===-1){
                  childComponents.value.push(child);
               }
            },
            removeChild: (child: any) => {
              const index = childComponents.value.findIndex(n=>n.uuid === child.uuid)
              if(index!==1){
                childComponents.value.splice(index,1)
              }
            }
      
        });
      
      
        onMounted(() => {
            console.log('父组件已挂载,childComponents:', childComponents.value);
        });
      
        // 方法1(裁剪,计算出扇形,最少两个)
        const drawing = (rotate: number, dom: HTMLElement, width: number, cb: any) => {
            var mask = dom;
            var r = width / 2;
            var backgroundColor = (mask.style?.backgroundColor as any) || null;
            if (rotate < 90) {
                var rad = (rotate * Math.PI) / 180;
                var x = r * Math.tan(rad) + r;
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,${x}px 0,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,${x}px 0,50% 50%)`;
            } else if (rotate == 90) {
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 50%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 50%,50% 50%)`;
            } else if (rotate < 180) {
                var rad1 = ((rotate - 90) * Math.PI) / 180;
                var y = r * Math.tan(rad1) + r;
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% ${y}px,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% ${y}px,50% 50%)`;
            } else if (rotate == 180) {
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,50% 100%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,50% 100%,50% 50%)`;
            } else if (rotate < 270) {
                var rad2 = ((rotate - 180) * Math.PI) / 180;
                var x1 = r - r * Math.tan(rad2);
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,${x1}px 100%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,${x1}px 100%,50% 50%)`;
            } else if (rotate == 270) {
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 50%,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 50%,50% 50%)`;
            } else if (rotate < 360) {
                var rad3 = ((rotate - 270) * Math.PI) / 180;
                var y2 = r - r * Math.tan(rad3);
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% ${y2}px,50% 50%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% ${y2}px,50% 50%)`;
            } else {
                mask.style = `background-color:${backgroundColor};clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 0%,50% 0%);-webkit-clip-path:polygon(50% 50%,50% 0%,100% 0,100% 100%,0% 100%,0% 0%,50% 0%)`;
            }
            cb && cb();
        }
        // 方法2(使用变形,扭曲成扇形,最少三个)
        const drawing1 = (rotate: number, domObject: any, index: number, cb?: any) =>{
            const {whellDom,textItemDom} = domObject
            var lengthNum = 360 / rotate
            const skewDeg = (360 - (lengthNum * 90))/lengthNum
            const deffAddRotate = ( props.isCenter?0:rotate/2)+(-90+180/lengthNum)
            // 旋转角度,使用skew变形(.whellKuang样式)
            whellDom.style=`position: absolute;top:-50%;left:50%;border-radius:revert;transform-origin:0% 100%;width:100%;height:100%;transform:rotate(${(index * rotate) +deffAddRotate}deg) skew(${skewDeg}deg);`
            // 文字旋转,并恢复变形(.textItemMain 样式)
            textItemDom.style=`transform-origin: center bottom;position: absolute;width: 50%;height:50%;bottom:0;top:revert;transform: translate(-50%, 0) skew(${0 - skewDeg}deg) rotate(${(180 - rotate)/2}deg);`
            cb && cb();
        }
        const whellInit = (objectData: any[]) => {
            nextTick(() => {
                const width = whell.value.offsetWidth;
                const rotate = 360 / objectData.length;
                const mainWidth = Math.sin(((rotate / 2) * Math.PI) / 180) * (width / 2) * 2;
                objectData.forEach((item, index) => {
                    let whellDom = item.whellItem
                    let textItemDom = item.textItemMain
                    drawing(rotate, whellDom, width, () => {
                        whellDom.style["transform"] = `rotate(${index * rotate+ (props.isCenter?(-rotate/2):0)}deg)`;
                        textItemDom.style = `width:${mainWidth}px;transform:rotate(${rotate / 2}deg);left:calc(50% - ${mainWidth / 2}px)`; // 设置内容框的位置
                        // 设置边框角度
                        const bianRotate = (180 - rotate) / 2;
                        if (item.leftBian) {
                            item.leftBian.style = `position: absolute;bottom: 0;right: 50%;height:100%;transform-origin: 50% 100%;-webkit-transform-origin: 50% 100%;transform: rotate(-${90 - bianRotate}deg);-webkit-transform:rotate(-${90 - bianRotate}deg);z-index:999`;
                        }
                        if (item.rightBian) {
                            item.rightBian.style = `position: absolute;bottom: 0;right: 50%;height:100%;transform-origin: 50% 100%;-webkit-transform-origin: 50% 100%;transform: rotate(${90 - bianRotate}deg);-webkit-transform:rotate(${90 - bianRotate}deg);z-index:999`;
                        }
                    })
                    // drawing1(rotate, {
                    //     whellDom,
                    //     textItemDom
                    // },index,()=>{
                    //     textItemDom.style.width=mainWidth+'px'
                    //     // 设置边框角度
                    //     const bianRotate = (180 - rotate) / 2;
                    //     if (item.leftBian) {
                    //         item.leftBian.style = `position: absolute;bottom: 0;right: 50%;height:100%;transform-origin: 50% 100%;-webkit-transform-origin: 50% 100%;transform: rotate(-${90 - bianRotate}deg);-webkit-transform:rotate(-${90 - bianRotate}deg);z-index:999`;
                    //     }
                    //     if (item.rightBian) {
                    //         item.rightBian.style = `position: absolute;bottom: 0;right: 50%;height:100%;transform-origin: 50% 100%;-webkit-transform-origin: 50% 100%;transform: rotate(${90 - bianRotate}deg);-webkit-transform:rotate(${90 - bianRotate}deg);z-index:999`;
                    //     }
                    // })
                })
            })
        }
      
      
      
        const resizeWheelInit = () =>{
          whellInit(childComponents.value)
        }
      
        watch(() => childComponents.value, (value) => {
            if (value.length > 0) {
                whellInit(value)
                window.addEventListener('resize', resizeWheelInit,false);
      
            }
        }, { immediate: true, deep: true })
        
      
        onUnmounted(()=>{
          window.removeEventListener('resize', resizeWheelInit,false);
        })
      
      
        const isClick = ref<boolean>(false)
        const stop = ref<boolean>(true)
      
        const ORANGE_DATA = {
          rotateNum: 0,
          amTime: props.whellTime,
          anEs: 'linear',
          isNextDel: true,
          transitionString: '',
          checkObject: {
            index: -1,
            du: 0,
          },
        };
        const optionsData = ref<Record<string, any>>(JSON.parse(JSON.stringify(ORANGE_DATA)))
      
        const setTimeOutArray = ref<any>([])
      
      
        const amTimeS = computed(() => {
            return optionsData.value.amTime + "s";
        })
      
        // 大转盘停止了
        const handlerMessage = () => turntableEnd('message');
        const handlerNoMessage = () => turntableEnd('noMessage');
        const turntableEnd = (endType='message') => {
            const rotate = 360 / childComponents.value.length;
            if(endType=='message')emit('end',optionsData.value.checkObject.index)
            isPlayRotate.value=false
            isClick.value = false;
            if(endType=='message'){
                let finalDu = optionsData.value.checkObject.du % 360;
                if(optionsData.value.checkObject.du<0){
                    finalDu = (props.isCenter? rotate/2:0) + 360
                }
                optionsData.value.checkObject.du = finalDu;
                optionsData.value.rotateNum = finalDu;
                optionsData.value.transitionString = `transform 0s `;
            }
            whell.value?.removeEventListener("webkitTransitionEnd", handlerMessage, false);
            whell.value?.removeEventListener("mozTransitionEnd", handlerMessage, false);
            whell.value?.removeEventListener("MSTransitionEnd", handlerMessage, false);
            whell.value?.removeEventListener("otransitionend", handlerMessage, false);
            whell.value?.removeEventListener("transitionend", handlerMessage, false);
        }
        // 选择哪一个index
        const checkSelect = (index: number,type?:'start' | 'end'| 'reset') => {
            const modeType = type || 'end'
            const rotate = 360 / childComponents.value.length;
            const selecthandelr = (selectType='') => {
                let itemIndex = index;
                optionsData.value.checkObject.index = index;
                if (itemIndex != -1) {
      
                    const deffRotate = rotate/2
                    optionsData.value.checkObject.du = (360 - (itemIndex * rotate + (props.isCenter?0:rotate / 2))) % 360;
                    let deleteRotate=0
                    if(deffRotate - 5 >0 && props.isOffset){
                        const deffRotate2 =  Math.random()*(deffRotate - 5 )
                        deleteRotate = deffRotate2 * (Math.random() < 0.5?-1:1)
                    }
                    optionsData.value.checkObject.du = optionsData.value.checkObject.du - deleteRotate
                    if(selectType==='judeg' &&  optionsData.value.checkObject.du<180){
                        optionsData.value.checkObject.du+=360
                    }
                }else{
                    optionsData.value.checkObject.du =-1;
                }
            }
            if(modeType==='start'){
                if (isClick.value) return false;
                emit('start',true)
                isClick.value = true;
                isPlayRotate.value=true
                selecthandelr()
                optionsData.value.rotateNum = 0;
                optionsData.value.transitionString = `transform 0s`;
                setTimeout(()=>{
                    let turntableRotate = optionsData.value.checkObject.du>0? optionsData.value.checkObject.du:0;
                    optionsData.value.transitionString = `transform ${optionsData.value.amTime*2}s ease-out`;
                    requestAnimationFrame(() => {
                        optionsData.value.rotateNum = turntableRotate+360;
                        whell.value.offsetHeight; 
                    });
                    whell.value.addEventListener("webkitTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("mozTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("MSTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("otransitionend", handlerMessage, false);
                    whell.value.addEventListener("transitionend", handlerMessage, false);
                })
            }else if(modeType==='reset'){
                isPlayRotate.value=false
                isClick.value = false;
                stop.value = true; 
                optionsData.value = JSON.parse(JSON.stringify(ORANGE_DATA));
                Object.assign(optionsData.value.checkObject,{index: -1,du: 0})
                optionsData.value.transitionString = `transform 0s `;
                setTimeOutArray.value.map((item:any)=>{
                    item && clearTimeout(item)
                })
                setTimeout(()=>{
                    if(index<0){
                      optionsData.value.rotateNum = rotate/2;
                    }else{
                      optionsData.value.rotateNum = 0;
                    }
                    whell.value?.offsetHeight; 
                    setTimeOutArray.value=[]
                    handlerNoMessage();
                })
            }else{
                selecthandelr('judeg')
            }
        }
        // 开始旋转
        const turntableStart = (cb?:any) => {
            if (isClick.value) return false;
            emit('start',true)
            optionsData.value.amTime = ORANGE_DATA.amTime
            optionsData.value.anEs = ORANGE_DATA.anEs
            optionsData.value.isNextDel = ORANGE_DATA.isNextDel
            isClick.value = true;
            isPlayRotate.value=true
            if (optionsData.value.checkObject.du == 0) {
                //度数为0,直接开始动画
                stop.value = true; // 暂停动画
                stop.value = false; // 立即重启以应用新时间
            } else {
                // 不为0,需要先转满最后一圈
                let zhuanDu = 360 - (optionsData.value.checkObject.du % 360);
                const time = (zhuanDu / 360) * optionsData.value.amTime;
                optionsData.value.rotateNum = optionsData.value.checkObject.du + zhuanDu;
                optionsData.value.transitionString = `transform ${time}s ${optionsData.value.anEs}`;
                // 再次复制为0
                optionsData.value.checkObject.du = 0;
                optionsData.value.checkObject.index = -1;
                // 并在旋转结束将whell的度数清零
                setTimeOutArray.value.push(setTimeout(() => {
                    // 开始执行动画
                    stop.value = false;
                    optionsData.value.anEs = "linear";
                    // 清除过渡角度的
                    optionsData.value.rotateNum = 0;
                    optionsData.value.transitionString = `transform 0s`;
                }, time * 1000))
                
                
                // 直接重置为0,不需要再次旋转
                // optionsData.value.checkObject.du = 0;
                // optionsData.value.checkObject.index = -1;
                // stop.value = true; // 暂停动画
                // stop.value = false; // 立即重启以应用新时间
                // requestAnimationFrame(() => {
                //   whell.value.offsetHeight;
                // });
                // setTimeout(()=>{
                //   optionsData.value.rotateNum = 0;
                //   optionsData.value.transitionString = `transform 0s`;
                // },0)
            }
            if(cb){
                cb((index:number)=>{
                    checkSelect(index)
                })  
            }
        }
      
      
        // 监听动画一个循环结束,发现最后需要旋转的度数大于0,就停止动画,然后通过transition旋转最后的度数
        const animationiteration = () => {
            optionsData.value.anEs = "linear";
            if (optionsData.value.checkObject.du != 0) {
                isPlayRotate.value=false
                const rotate = 360 / childComponents.value.length;
                const offsetRotate = (props.isCenter? rotate/2:0) + 360
                const turntableRotate = optionsData.value.checkObject.du>0? optionsData.value.checkObject.du:offsetRotate;
                const time =  (turntableRotate<180?turntableRotate+180:turntableRotate)/360 * optionsData.value.amTime * 1.5
                optionsData.value.transitionString = `transform ${time}s cubic-bezier(.4,.6,0.8,0.85)`;
                // 动画不执行了
                optionsData.value.amTime=0
                requestAnimationFrame(() => {
                    optionsData.value.rotateNum = turntableRotate;
                    whell.value.offsetHeight; 
                });
                setTimeOutArray.value.push(setTimeout(() => {
                   stop.value = true; 
                }, optionsData.value.checkObject.du<0?0:time*0.2*1000))
      
      
                // 最后选中的如果传入-1,表示没有选中,会回到最初位置
                if(optionsData.value.checkObject.du<0 && offsetRotate===0){
                    handlerMessage()
                }else{
                    // 过渡结束后执行回调
                    whell.value.addEventListener("webkitTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("mozTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("MSTransitionEnd", handlerMessage, false);
                    whell.value.addEventListener("otransitionend", handlerMessage, false);
                    whell.value.addEventListener("transitionend", handlerMessage, false);
                }
      
            } else {
                // 越来越慢
                if (optionsData.value.amTime >ORANGE_DATA.amTime/4  && optionsData.value.isNextDel) {
                    optionsData.value.amTime -= ORANGE_DATA.amTime * 0.05;
                } else {
                    // 最多慢两倍
                    optionsData.value.isNextDel = false;
                    if (optionsData.value.amTime < ORANGE_DATA.amTime * 1.5) {
                        optionsData.value.amTime += ORANGE_DATA.amTime * 0.05;
                    }
                }
            }
        }
      
        defineExpose({ turntableStart, checkSelect })
      
      
        </script>
      
        <style lang="scss" scoped>
        .whellMain {
            overflow: hidden;
            position: relative;
            width: 300px;
            height: 300px;
        }
        
        .whellMainSlotBg{
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: 50;
          pointer-events: none;
        }
      
        .btnMain {
            z-index: 999;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
      
        .btnDian {
            width: 50px;
            height: 50px;
            border-radius: 50%;
            box-shadow: 5px 5px 10 5px rgba(0, 0, 0, 0.5);
            background-color: #fff;
            pointer-events: none;
        }
      
        .btnDian::before {
            content: " ";
            width: 30px;
            height: 20px;
            background-color: #fff;
            position: absolute;
            top: 8px;
            left: 50%;
            transform: translate(-50%, -90%);
            clip-path: polygon(0 100%, 50% 0, 100% 100%);
            -webkit-clip-path: polygon(0 100%, 50% 0, 100% 100%);
        }
      
        .whell {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            box-sizing: border-box;
            position: relative;
            z-index: 99;
            transform: rotate(0deg) translate3d(0,0,0);
            transform-origin: center center;
        }
      
        .whellPlay {
            animation: rotateAm var(--amTimeS) infinite var(--anEs);
        }
      
        .whellStop {
            animation-play-state: paused !important;
            animation: none !important;
        }
      
      
        @keyframes rotateAm {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .whellBg{
            width: 100%;
            height: 100%;
            position: absolute;
            border-radius: 50%;
            top: 0;
            left: 0;
            z-index: 998;
            background: rgba(0,0,0,0.01); // 或背景图片
            -webkit-backdrop-filter: blur(3px);
            backdrop-filter: blur(3px);
        }
      
        /* 进入和离开过渡的公共样式 */
        .fadeWhellBg-enter-active,
        .fadeWhellBg-leave-active {
          transition: all 0.2s ease;
        }
      
        /* 进入动画 */
        .fadeWhellBg-enter-from {
          opacity: 0;
        }
      
        .fadeWhellBg-enter-to {
          opacity: 1;
        }
      
        /* 离开动画 */
        .fadeWhellBg-leave-from {
          opacity: 1;
        }
      
        .fadeWhellBg-leave-to {
          opacity: 0;
        }
        </style>
      
      

      turntableItem

      <template>
          <div class="whellKuang" ref="whellItem">
            <div class="textItemMain" ref="textItemMain">
               <!-- 左边框 -->
              <div class="leftBian" ref="leftBian">
                <slot name="left" />
              </div>
              <slot/>
              <!-- 右边框 -->
              <div class="rightBian"  ref="rightBian">
                <slot name="right" />
              </div>
            </div>
         </div>
      
      </template>
      <script setup lang="ts">
      import { ref, onMounted, onUnmounted,inject } from 'vue';
      
      type domType = HTMLElement | Element | null;
      
      const whellItem = ref<domType>(null);
      const textItemMain = ref<domType>(null);
      const leftBian = ref<domType>(null);
      const rightBian = ref<domType>(null);
      
      interface ParentInterface {
      registerChild: (child: {
        whellItem: domType;
        textItemMain: domType;
        leftBian: domType;
        rightBian: domType;
        uuid: number;
      }) => void;
      removeChild: (child: {
        whellItem: domType;
        textItemMain: domType;
        leftBian: domType;
        rightBian: domType;
        uuid: number;
      }) => void;
      }
      
      const parent = inject<ParentInterface>('parentSetChild');
      const componentInstance = getCurrentInstance();
      
      let isRegistered = false;
      const thisChildData = ref<Record<any, any>>({})
      
      onMounted(() => {
      if (parent && componentInstance && !isRegistered) {
        const childData = {
          whellItem: whellItem.value,
          textItemMain: textItemMain.value,
          leftBian: leftBian.value,
          rightBian: rightBian.value,
          uuid: componentInstance.uid // 使用组件唯一 uid 避免重复注册
        };
        thisChildData.value =childData
        if (childData.whellItem && childData.textItemMain) {
          parent.registerChild(childData);
          isRegistered = true;
        }
      }
      });
      
      onUnmounted(() => {
      isRegistered = false;
      if(Object.keys(thisChildData.value).length && parent && componentInstance ){
        parent.removeChild(thisChildData.value as any);
      }
      });
      </script>
      
      <style lang="scss" scoped>
      .whellKuang {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        position: absolute;
        top: 0;
        left: 0;
        z-index: 60;
        overflow: hidden;
        pointer-events: none;
      }
      
      .textItemMain {
        height: 50%;
        pointer-events: all;
        transform-origin: center bottom;
        position: absolute;
        top: 0;
        display: flex;
        justify-content: center;
        overflow: hidden;
      }
      </style>