@@ -742,143 +742,147 @@ function initDemoRotator(){
742742}
743743
744744window . addEventListener ( 'load' , ( ) => { try { initDemoRotator ( ) ; } catch ( e ) { } } ) ;
745- // 1. 初始化变量
746- // 1. 获取元素
747- const track = document . getElementById ( 'track' ) ;
748- const originalCards = document . querySelectorAll ( '.carousel-card' ) ;
749- const prevBtn = document . getElementById ( 'prevBtn' ) ;
750- const nextBtn = document . getElementById ( 'nextBtn' ) ;
751- const gapPx = 15 ; // 必须和 CSS 里的 gap 保持一致
752-
753- // 2. 动态获取当前是从 60% 还是 75% 渲染
754- function getCardWidthPercent ( ) {
755- return window . innerWidth < 768 ? 75 : 60 ;
756- }
757-
758- // 3. 克隆首尾 (Clone)
759- const firstClone = originalCards [ 0 ] . cloneNode ( true ) ;
760- const lastClone = originalCards [ originalCards . length - 1 ] . cloneNode ( true ) ;
761- track . appendChild ( firstClone ) ;
762- track . insertBefore ( lastClone , originalCards [ 0 ] ) ;
763-
764- let allCards = document . querySelectorAll ( '.carousel-card' ) ;
765- let currentIndex = 1 ;
766- let isTransitioning = false ;
767-
768- // 4. 核心:更新位置 (使用纯 CSS calc,稳如老狗)
769- function updateTrack ( animate ) {
770- if ( animate ) {
771- track . style . transition = 'transform 0.4s cubic-bezier(0.25, 1, 0.5, 1)' ;
772- } else {
773- track . style . transition = 'none' ;
745+ /* =========================================
746+ 无缝轮播 (修复回滚时的卡片缩放闪烁问题)
747+ ========================================= */
748+ function initCarousel ( ) {
749+ const track = document . getElementById ( 'track' ) ;
750+ if ( ! track ) return ;
751+
752+ const originalCards = document . querySelectorAll ( '.carousel-card' ) ;
753+ const prevBtn = document . getElementById ( 'prevBtn' ) ;
754+ const nextBtn = document . getElementById ( 'nextBtn' ) ;
755+ const gapPx = 15 ;
756+
757+ // 1. 克隆首尾
758+ const firstClone = originalCards [ 0 ] . cloneNode ( true ) ;
759+ const lastClone = originalCards [ originalCards . length - 1 ] . cloneNode ( true ) ;
760+ track . appendChild ( firstClone ) ;
761+ track . insertBefore ( lastClone , originalCards [ 0 ] ) ;
762+
763+ let allCards = document . querySelectorAll ( '.carousel-card' ) ;
764+ let currentIndex = 1 ;
765+ let isTransitioning = false ;
766+
767+ // 2. 动态获取宽度
768+ function getCardWidthPercent ( ) {
769+ return window . innerWidth < 768 ? 75 : 60 ;
774770 }
775771
776- const wPercent = getCardWidthPercent ( ) ;
777-
778- // 公式解释:
779- // 居中偏移量 = (100% - 卡片宽度%) / 2
780- // 移动距离 = 居中偏移量 - (当前索引 * (卡片宽度% + 间距px))
781- const centerOffset = ( 100 - wPercent ) / 2 ;
782- const val = `calc(${ centerOffset } % - ${ currentIndex } * (${ wPercent } % + ${ gapPx } px))` ;
783-
784- track . style . transform = `translateX(${ val } )` ;
785- }
786-
787- // 5. 切换逻辑
788- function switchSlide ( direction ) {
789- if ( isTransitioning ) return ;
790-
791- // 防止快速点击越界
792- if ( direction === 1 && currentIndex >= allCards . length - 1 ) return ;
793- if ( direction === - 1 && currentIndex <= 0 ) return ;
794-
795- currentIndex += direction ;
796- isTransitioning = true ;
797- updateTrack ( true ) ;
798- updateActive ( ) ;
799- }
800-
801- // 6. 事件绑定 (暴力绑定 click 和 touchend)
772+ // 3. 更新轨道位置
773+ function updateTrack ( animate ) {
774+ if ( animate ) {
775+ track . style . transition = 'transform 0.4s cubic-bezier(0.25, 1, 0.5, 1)' ;
776+ } else {
777+ track . style . transition = 'none' ;
778+ }
779+ const wPercent = getCardWidthPercent ( ) ;
780+ const centerOffset = ( 100 - wPercent ) / 2 ;
781+ const val = `calc(${ centerOffset } % - ${ currentIndex } * (${ wPercent } % + ${ gapPx } px))` ;
782+ track . style . transform = `translateX(${ val } )` ;
783+ }
802784
803- // 电脑端点击
804- nextBtn . addEventListener ( 'click' , ( e ) => { e . preventDefault ( ) ; switchSlide ( 1 ) ; } ) ;
805- prevBtn . addEventListener ( 'click' , ( e ) => { e . preventDefault ( ) ; switchSlide ( - 1 ) ; } ) ;
785+ // 4. 更新激活状态 (控制缩放和透明度)
786+ function updateActive ( ) {
787+ allCards . forEach ( ( card , index ) => {
788+ const video = card . querySelector ( 'video' ) ;
789+
790+ if ( index === currentIndex ) {
791+ // 当前激活卡片
792+ card . classList . add ( 'is-active' ) ;
793+ // 直接操作样式,保证优先级
794+ card . style . opacity = '1' ;
795+ card . style . transform = 'scale(1)' ;
796+ card . style . filter = 'blur(0)' ;
797+ card . style . zIndex = '10' ;
798+ if ( video ) video . play ( ) . catch ( ( ) => { } ) ;
799+ } else {
800+ // 非激活卡片
801+ card . classList . remove ( 'is-active' ) ;
802+ card . style . opacity = '0.4' ;
803+ card . style . transform = 'scale(0.9)' ;
804+ card . style . filter = 'blur(1px)' ;
805+ card . style . zIndex = '1' ;
806+ if ( video ) video . pause ( ) ;
807+ }
808+ } ) ;
809+ }
806810
807- // 手机端触摸 (加锁防止双重触发)
808- let touchLock = false ;
809- function handleTouch ( e , dir ) {
810- if ( touchLock ) return ;
811- e . preventDefault ( ) ; e . stopPropagation ( ) ; // 阻止冒泡
812- switchSlide ( dir ) ;
813- touchLock = true ;
814- setTimeout ( ( ) => touchLock = false , 300 ) ;
815- }
816- nextBtn . addEventListener ( 'touchend' , ( e ) => handleTouch ( e , 1 ) ) ;
817- prevBtn . addEventListener ( 'touchend' , ( e ) => handleTouch ( e , - 1 ) ) ;
818-
819- // 7. 手机滑动支持 (Swipe)
820- let touchStartX = 0 ;
821- track . addEventListener ( 'touchstart' , e => { touchStartX = e . changedTouches [ 0 ] . screenX ; } , { passive : true } ) ;
822- track . addEventListener ( 'touchend' , e => {
823- const touchEndX = e . changedTouches [ 0 ] . screenX ;
824- if ( touchEndX < touchStartX - 50 ) switchSlide ( 1 ) ; // 左滑 -> 下一张
825- if ( touchEndX > touchStartX + 50 ) switchSlide ( - 1 ) ; // 右滑 -> 上一张
826- } , { passive : true } ) ;
827-
828- // 8. 无缝循环 & 视频同步
829- track . addEventListener ( 'transitionend' , ( ) => {
830- isTransitioning = false ;
831- let targetIndex = - 1 ;
832-
833- // 到了克隆的最后一张 -> 跳回真实第1张
834- if ( currentIndex === allCards . length - 1 ) targetIndex = 1 ;
835- // 到了克隆的第一张 -> 跳回真实最后一张
836- else if ( currentIndex === 0 ) targetIndex = allCards . length - 2 ;
837-
838- if ( targetIndex !== - 1 ) {
839- // 视频同步
840- const v1 = allCards [ currentIndex ] . querySelector ( 'video' ) ;
841- const v2 = allCards [ targetIndex ] . querySelector ( 'video' ) ;
842- if ( v1 && v2 ) v2 . currentTime = v1 . currentTime ;
843-
844- // 瞬移
845- track . classList . add ( 'no-transition' ) ;
846- allCards . forEach ( c => c . classList . add ( 'no-transition' ) ) ;
811+ // 5. 切换逻辑
812+ function switchSlide ( direction ) {
813+ if ( isTransitioning ) return ;
814+ currentIndex += direction ;
815+ isTransitioning = true ;
847816
848- currentIndex = targetIndex ;
849- updateTrack ( false ) ; // 重新定位
850- updateActive ( ) ;
851-
852- void track . offsetHeight ; // 强制重绘
853- requestAnimationFrame ( ( ) => {
854- track . classList . remove ( 'no-transition' ) ;
855- allCards . forEach ( c => c . classList . remove ( 'no-transition' ) ) ;
856- } ) ;
817+ updateActive ( ) ; // 先触发缩放动画
818+ updateTrack ( true ) ; // 再触发位移动画
857819 }
858- } ) ;
859820
860- // 9. 激活状态 (只播中间,两边暂停)
861- function updateActive ( ) {
862- allCards . forEach ( ( card , index ) => {
863- const v = card . querySelector ( 'video' ) ;
864- if ( index === currentIndex ) {
865- card . classList . add ( 'is-active' ) ;
866- if ( v ) {
867- v . muted = true ;
868- v . playsInline = true ;
869- // 必须加 catch 防止报错
870- v . play ( ) . catch ( ( ) => { } ) ;
821+ // 6. 监听动画结束 (处理瞬间回滚 + 防止缩放闪烁)
822+ track . addEventListener ( 'transitionend' , ( ) => {
823+ if ( ! isTransitioning ) return ;
824+ isTransitioning = false ;
825+
826+ let targetIndex = - 1 ;
827+ // 检测是否到了边界
828+ if ( currentIndex === allCards . length - 1 ) targetIndex = 1 ;
829+ else if ( currentIndex === 0 ) targetIndex = allCards . length - 2 ;
830+
831+ if ( targetIndex !== - 1 ) {
832+ // === 核心修复开始 ===
833+
834+ // 1. 给所有卡片加上禁止动画类
835+ // 这步至关重要:防止从 scale(0.9) 变成 scale(1) 时播放动画
836+ allCards . forEach ( c => c . classList . add ( 'no-transition' ) ) ;
837+ track . style . transition = 'none' ;
838+
839+ // 2. 视频同步 (防止黑屏)
840+ const currentVideo = allCards [ currentIndex ] . querySelector ( 'video' ) ;
841+ const targetVideo = allCards [ targetIndex ] . querySelector ( 'video' ) ;
842+ if ( currentVideo && targetVideo ) {
843+ targetVideo . currentTime = currentVideo . currentTime ;
844+ targetVideo . play ( ) . catch ( ( ) => { } ) ;
871845 }
872- } else {
873- card . classList . remove ( 'is-active' ) ;
874- if ( v ) v . pause ( ) ;
846+
847+ // 3. 瞬间瞬移位置
848+ currentIndex = targetIndex ;
849+ updateTrack ( false ) ;
850+
851+ // 4. 瞬间强制应用激活样式 (因为加了 no-transition,这里会瞬间变大,不会有动画)
852+ updateActive ( ) ;
853+
854+ // 5. 强制浏览器重绘 (Force Reflow)
855+ // 告诉浏览器:“立刻把上面的变化渲染出来,不要等”
856+ void track . offsetHeight ;
857+
858+ // 6. 恢复动画能力 (下一帧再移除锁,确保万无一失)
859+ requestAnimationFrame ( ( ) => {
860+ allCards . forEach ( c => c . classList . remove ( 'no-transition' ) ) ;
861+ track . style . transition = '' ;
862+ } ) ;
863+ // === 核心修复结束 ===
875864 }
876865 } ) ;
877- }
878866
879- // 初始化
880- updateTrack ( false ) ;
881- updateActive ( ) ;
867+ // 7. 事件绑定
868+ nextBtn . onclick = ( e ) => { e . preventDefault ( ) ; switchSlide ( 1 ) ; } ;
869+ prevBtn . onclick = ( e ) => { e . preventDefault ( ) ; switchSlide ( - 1 ) ; } ;
870+
871+ // 手机滑动
872+ let touchStartX = 0 ;
873+ track . addEventListener ( 'touchstart' , e => { touchStartX = e . changedTouches [ 0 ] . screenX ; } , { passive : true } ) ;
874+ track . addEventListener ( 'touchend' , e => {
875+ const diff = e . changedTouches [ 0 ] . screenX - touchStartX ;
876+ if ( diff < - 50 ) switchSlide ( 1 ) ;
877+ if ( diff > 50 ) switchSlide ( - 1 ) ;
878+ } , { passive : true } ) ;
879+
880+ // 初始化
881+ updateTrack ( false ) ;
882+ updateActive ( ) ;
883+ window . addEventListener ( 'resize' , ( ) => updateTrack ( false ) ) ;
884+ }
882885
883- // 监听窗口大小变化(自动适应横竖屏)
884- window . addEventListener ( 'resize' , ( ) => updateTrack ( false ) ) ;
886+ window . addEventListener ( 'load' , ( ) => {
887+ try { initCarousel ( ) ; } catch ( e ) { console . error ( e ) ; }
888+ } ) ;
0 commit comments