mini-app-swiper
Version:
在小程序环境内无限轮播效果,并且不满足外层容器宽度时不轮播, 自定义内部slide
201 lines (175 loc) • 7.58 kB
JSX
import { useRef, useState, useEffect, memo, useCallback } from "react";
import Taro, { useDidShow, useReady, useLoad } from '@tarojs/taro';
import { View } from '@tarojs/components';
import IndexStyle from './index.module.less';
import { useMemo } from "react";
const MAX_RETRY = 5; // 最大重试次数
const RETRY_INTERVAL = 30; // 重试间隔(ms)
const THISCOMNAME = "Hourse-", THISCOMNAMEVIEW = 'HourseSwiper-'
const SlideItem = memo(({ dom, id, index, onGetDom }) => {
const timerRef = useRef(null);
useEffect(() => {
let retryCount = 0;
const getDom = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
Taro.nextTick(() => {
const query = Taro.createSelectorQuery();
query.select(`#${THISCOMNAME}${id}-${index}`).boundingClientRect(res => {
if (res && res.height > 0 && res.width > 0) {
onGetDom(res);
return;
}
if (retryCount < MAX_RETRY) {
retryCount++;
console.log(`DOM获取失败,第${retryCount}次重试...`);
timerRef.current = setTimeout(getDom, RETRY_INTERVAL);
} else {
console.error(`无法获取 #${id}${index} 的DOM元素`);
}
}).exec();
});
};
getDom();
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return (
<View id={`${THISCOMNAME}${id}-${index}`} className={IndexStyle.slideItem}>
{dom}
</View>
)
});
const Carousel = ({ className, width, speed, data, children }) => {
const uuid = useCallback(() => {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
}, [])
const thisComponentUUid = useRef(uuid())
const [propsOriginalSlides, setPropsOriginalSlides] = useState(data);
const [extendedSlides, setextendedSlides] = useState([...propsOriginalSlides, ...propsOriginalSlides]);
const [extendedSlidesDOM, setextendedSlidesDOM] = useState([...children, ...children]);
// 使用 ref 存储状态
const stateRef = useRef({
slideWidths: [],
totalWidth: 0,
realContentWidth: 0,
lastTimestamp: 0,
duration: speed * data.length * 2, // 15秒完成一轮
animationFrameId: null
});
const [translateX, setTranslateX] = useState(0);
const [isMeasuring, setIsMeasuring] = useState(true);
const ins = useMemo(() => { return Taro.getCurrentInstance()?.page }, []);
const childDom = useRef([]);
const [childDomProxy, setChildDomProxy] = useState([])
// 动态测量Slide宽度
useEffect(() => {
if (childDom.current.length === extendedSlides.length) {
(async () => {
const measureSlides = (contentWidth) => {
try {
const measurements = childDom.current.map(item => item?.width)
stateRef.current.slideWidths = measurements;
stateRef.current.totalWidth = measurements.reduce((sum, width) => sum + width, 0);
stateRef.current.realContentWidth = measurements
.slice(0, measurements.length / 2)
.reduce((sum, width) => sum + width, 0);
if (stateRef.current.realContentWidth > contentWidth) {
setIsMeasuring(false);
} else {
/**
*
* 去除多余的数据
*
*/
setextendedSlides(propsOriginalSlides)
}
} catch (error) {
console.error('Error measuring slides:', error);
}
};
const getContentDomView = () => {
return new Promise((resolve, rej) => {
// 获取容器宽度
const query = Taro.createSelectorQuery();
query.select(`#${THISCOMNAMEVIEW}${thisComponentUUid.current}`).boundingClientRect(function (res) {
resolve(res)
}).exec();
});
}
const data = await getContentDomView();
measureSlides(data?.width);
})()
}
}, [childDomProxy]);
// 自动滚动逻辑 - 使用requestAnimationFrame但只更新状态
useEffect(() => {
if (isMeasuring || stateRef.current.slideWidths.length === 0) return;
// 防抖
const animate = (timestamp) => {
if (!stateRef.current.lastTimestamp) {
stateRef.current.lastTimestamp = timestamp;
}
const elapsed = timestamp - stateRef.current.lastTimestamp;
const progress = elapsed / stateRef.current.duration;
// 计算当前位移
let newTranslateX = -progress * stateRef.current.totalWidth;
// 边界检测
if (-newTranslateX >= stateRef.current.realContentWidth) {
// 无缝跳转
newTranslateX = -stateRef.current.realContentWidth;
// 重置时间基准
stateRef.current.lastTimestamp = timestamp;
}
// 更新状态
setTranslateX(newTranslateX);
// 继续动画循环
stateRef.current.animationFrameId = requestAnimationFrame(animate);
};
// 启动动画
stateRef.current.animationFrameId = requestAnimationFrame(animate);
return () => {
if (stateRef.current.animationFrameId) {
cancelAnimationFrame(stateRef.current.animationFrameId);
}
};
}, [isMeasuring]);
const onGetDom = (e) => {
let copyArray = [...childDom.current];
copyArray.push(e)
childDom.current = copyArray;
if (childDom.current.length === extendedSlides.length) {
setChildDomProxy(childDom.current)
}
}
return (
<View id={THISCOMNAMEVIEW + thisComponentUUid.current} style={{ width }} className={[IndexStyle.carouselContainer, className].join(" ")}>
<View
className={IndexStyle.slidesWrapper}
style={{
transform: `translateX(${translateX}px)`,
transition: 'none',
}}
>
{extendedSlides.map((slide, index) => (
<SlideItem onGetDom={onGetDom} dom={extendedSlidesDOM[index]} id={thisComponentUUid.current} key={`${thisComponentUUid.current}-${index}`} index={index} />
))}
</View>
</View>
);
};
export default Carousel