完整代码
//组件部分
// 轮播图
import {
forwardRef,
useEffect,
useState,
useRef,
} from 'react';
import StyleElement from '@/utils/addRulesIntoStyle';
import styles from './Carousel.less';
import propsInterface from './interface';
import { FaArrowRight,FaArrowLeft } from "react-icons/fa";
const Carousel = forwardRef((props: propsInterface, ref: any) => {
const {
children,
carouselStyle = {},
change = function () {},
click = function () {},
direction = 'horizontal',
autoplay = false,
interval = 3000,
arrow='hover',
dotsShow = true,
dotsType = 'dot',
dotsPlacements = 'bottom',
dotsStyles = {},
dotsMouseEnter = true,
dotBackground = '#8a8793',
active_dotBackground = '#faf3ef',
} = props;
//轮播图的真实长度
const carouselItemLenght = useRef(0);
const [carouselItem, setCarouselItem] = useState(() => {
if (Array.isArray(children)) {
//传入的dom数组扁平化,防止dom的map循环使传入的children存在多维嵌套
let new_children = children.flat();
//记录轮播图的真实长度
carouselItemLenght.current = new_children.length;
//复制一组保证轮播图循环播放
new_children = [...new_children, ...new_children];
return new_children;
} else {
return [children];
}
});
//当前展示index
const [currentCarouselIndex, set_currentCarouselIndex] = useState(() => {
return 0;
});
const _currentCarouselIndex = useRef(0);
const autoplayTimer = useRef<undefined | NodeJS.Timeout>(undefined); //自动播放的timer
let styleElement = useRef(new StyleElement('editStyleElement'));
let originStyles = useRef<string>(''); //保存carouselContent的原始样式,防止被asideBtn操作污染
//轮播图的播放方向
let transformDirection = useRef('translateX');
useEffect(() => {
//确定轮播图的播放方向
if (direction === 'vertical') {
transformDirection.current =
direction === 'vertical' ? 'translateY' : 'translateX';
let carouselContent = document.getElementById('carouselContent');
carouselContent?.setAttribute('style', 'display:block;');
originStyles.current = carouselContent?.getAttribute('style') ?? '';
}
//自动播放
openAutoplay();
//组件销毁时
return () => {
if (autoplayTimer.current) {
clearInterval(autoplayTimer.current);
}
};
}, []);
//鼠标移入事件
const mouseEnter = function (e: any) {
btnShow(true);
if (autoplayTimer.current) {
clearInterval(autoplayTimer.current);
}
};
//鼠标移出事件
const mouseLeave = function (e: any) {
btnShow(false);
openAutoplay();
};
//开启自动播放
const openAutoplay = function () {
if (autoplay) {
if (autoplayTimer.current) {
clearInterval(autoplayTimer.current);
}
autoplayTimer.current = setInterval(function () {
carouselChange('insert', 'autoplay');
}, interval);
}
};
/**
* 切换轮播图
* @param type insert向右滚动或者向下滚动(direction相关) ;decrease 向左滚动或者向上滚动
* @param mode 触发轮播图切换的方式 autoplay自动切换 click手动切换
* @param step 单次轮播图切换的步数
*/
const carouselChange = function (
type: 'insert' | 'decrease',
mode: 'autoplay' | 'click',
step: number = 1,
) {
if (step > carouselItemLenght.current) {
throw '单次轮播图切换步数不可大于轮播图成员数量!';
}
let carouselContent = document.getElementById('carouselContent');
let carouselContentStylesStr: string = originStyles.current;
//本次移动的keyframes
let keyframesStr = '';
//本次移动的开始位置,结束位置
let transformXFrom = 0;
let transformXTo = 0;
//改变index
if (type === 'insert') {
transformXFrom = -(_currentCarouselIndex.current * 100);
_currentCarouselIndex.current = _currentCarouselIndex.current + step;
} else if (type === 'decrease') {
transformXFrom =
-(carouselItemLenght.current + _currentCarouselIndex.current) * 100;
_currentCarouselIndex.current =
carouselItemLenght.current + _currentCarouselIndex.current - step;
}
transformXTo = -(_currentCarouselIndex.current * 100);
keyframesStr = `@keyframes carouselContentTransfrom_${_currentCarouselIndex.current}{
0%{
transform:${transformDirection.current}(${transformXFrom}%)
}
100%{
transform:${transformDirection.current}(${transformXTo}%)
}
}`;
//替换styles规则
styleElement.current.replaceRulesInStyle(
'carouselContentTransfrom_once',
keyframesStr,
);
//animation的keyframes名需要动态切换否则不生效
carouselContentStylesStr =
carouselContentStylesStr +
'animation:carouselContentTransfrom_' +
_currentCarouselIndex.current +
' 0.5s forwards linear;';
carouselContent?.setAttribute('style', carouselContentStylesStr);
//当展示的轮播图超过实际轮播图的时候重置
if (_currentCarouselIndex.current > carouselItemLenght.current - 1) {
_currentCarouselIndex.current =
_currentCarouselIndex.current - carouselItemLenght.current;
}
set_currentCarouselIndex(_currentCarouselIndex.current);
//轮播图变化回调
emitChange({
currentCarouselIndex,
mode,
type,
});
};
/**
* 轮播图按钮出现事件
* @param show 是否展示轮播图
*/
const btnShow = function(show:boolean){
if(arrow==='always'){
return
}
let asideLeft = document.getElementById("asideLeft");
let asideRight = document.getElementById("asideRight");
if(show){
asideLeft?.style.setProperty ('visibility','visible');
asideLeft?.style.setProperty ('transform','translateX(10px)');
asideLeft?.style.setProperty ('transition','0.3s');
asideRight?.style.setProperty ('visibility','visible');
asideRight?.style.setProperty ('transform','translateX(-10px)');
asideRight?.style.setProperty ('transition','0.3s');
}else{
asideLeft?.style.setProperty ('transform','translateX(-60px)');
asideLeft?.style.setProperty ('transition','0.3s');
asideLeft?.style.setProperty('visibility','hidden');
asideRight?.style.setProperty ('transform','translateX(60px)');
asideRight?.style.setProperty ('transition','0.3s');
asideRight?.style.setProperty('visibility','hidden');
}
};
/**
* 轮播图变化的emit
* @param data
*/
const emitChange = function (data: any) {
change(data);
};
/**
* 轮播图当前页点击
*/
const carouselClick = function () {
emitClick(currentCarouselIndex);
};
/**
* 发出点击事件
* @param data
*/
const emitClick = function (data: any) {
click(data);
};
/**
* 设置默认样式
*/
const returnCarouselStyle = function () {
let style = {
with: '100%',
height: '100%',
...carouselStyle,
};
return style;
};
/**
* arrow
*/
const arrowRender = function(){
if(arrow==='never'){
return
}
return <div>
<aside
className={`${styles.decreaseBtn} ${arrow==='hover'?styles.decreaseBtn_hover:''}`}
id='asideLeft'
onClick={() => {
carouselChange('decrease', 'click');
}}
>
<FaArrowLeft/>
</aside>
<aside
className={`${styles.insertBtn} ${arrow==='hover'?styles.insertBtn_hover:''}`}
id='asideRight'
onClick={() => {
carouselChange('insert', 'click');
}}
>
<FaArrowRight/>
</aside>
</div>
}
/**
* 指示点
*/
const dots = function () {
if (!dotsShow) {
return null;
}
let dotsArr = [];
for (let i = 0; i < carouselItemLenght.current; i++) {
dotsArr.push(
<li
key={i}
className={`${styles.dotItem} ${
dotsType === 'dot' ? styles.dotCicle : styles.dotLine
}`}
style={{
background:`${currentCarouselIndex===i?active_dotBackground:dotBackground}`
}}
onMouseEnter={(mouseEvent) => {
dotsMouseEnterFun(mouseEvent, i);
}}
></li>,
);
}
return dotsArr;
};
//dots鼠标移入事件
const dotsMouseEnterFun = function (
mouseEvent: React.MouseEvent,
dotsIndex: number,
) {
if (dotsMouseEnter) {
let steps: number = dotsIndex - _currentCarouselIndex.current;
if (steps > 0) {
carouselChange('insert', 'click', Math.abs(steps));
} else {
carouselChange('decrease', 'click', Math.abs(steps));
}
}
};
//指示点根据传入的dotsPlacements改变显示位置
const [dotsPlaceStyles, setdotsPlaceStyles] = useState(() => {
//默认bottom模式
if (dotsPlacements === 'bottom') {
return styles.dotsBottom;
} else if (dotsPlacements === 'top') {
return styles.dotsTop;
} else if (dotsPlacements === 'left') {
return styles.dotsLeft;
} else if (dotsPlacements === 'right') {
return styles.dotsRight;
}
});
const returnDotsStyles = function () {
return {
...dotsStyles,
};
};
return (
<article
className={`${styles.carousel}`}
style={returnCarouselStyle()}
onMouseEnter={mouseEnter}
onMouseLeave={mouseLeave}
>
{/* children传入参数 */}
<section className={styles.carouselWindow}>
<div className={styles.carouselContent} id="carouselContent">
{carouselItem.map(
(carouselItem_content: any, carouselItemIndex: any) => {
return (
<div
className={styles.carouselItem_content}
key={carouselItemIndex}
onClick={carouselClick}
>
{carouselItem_content}
</div>
);
},
)}
</div>
</section>
{/* 切换按钮 */}
{arrowRender()}
{/* 指示点 */}
<ul
className={`${dotsPlaceStyles} ${styles.dots}`}
style={returnDotsStyles()}
>
{dots()}
</ul>
</article>
);
});
export default Carousel;
interface propsInterface {
/**
* children中最多map两层dom
*/
children:Array<JSX.Element|Array<JSX.Element|Array<JSX.Element>>>|JSX.Element,
carouselStyle?:React.CSSProperties,
/**
* 轮播图切换事件
*/
change?:Function,
/**
* 轮播图当前也点击事件
*/
click?:Function,
/**
* 轮播图展示方式
* horizontal 水平
* vertical 垂直
*/
direction?:'horizontal'|'vertical',
/**
* 是否自动切换
*/
autoplay?:boolean,
interval?:number,//自动切换间隔
/**
* arrow
*/
arrow?:'hover'|'always'|'never'
/**
* 指示点
*/
dotsShow?:boolean,
dotsType?:'line'|'dot', //指示点样式 (线、点)
dotsPlacements?:'bottom'|'top'|'left'|'right',
dotsStyles?:React.CSSProperties,
dotsMouseEnter?:boolean, //鼠标移入dot是否自动切换
dotBackground?:string, //指示点颜色
active_dotBackground?:string, //指示点活动颜色
}
export default propsInterface
//style节点内容
class StyleElement{
//存放节点要添加的内容
innerHtml:{[key:string]:string} = {};
//存放节点
style:HTMLElement|null = null;
styleId:string;
constructor(styleId:string){
this.styleId = styleId
this.createStyleElement();
}
private createStyleElement(){
let createStyle = document.getElementById(this.styleId);
if(!createStyle){
createStyle = document.createElement('style');
createStyle.id = this.styleId;
createStyle.type = 'text/css';
document.getElementsByTagName('head')[0].appendChild(createStyle);
}
this.style = createStyle;
}
private innerHtmlAssemble(){
let innerHtml_new = '';
for(let i in this.innerHtml){
innerHtml_new = innerHtml_new + '\n' + this.innerHtml[i];
}
return innerHtml_new;
}
addRulesIntoStyle(key:string,newRules:string){
this.innerHtml[key] = newRules;
if(this.style){
this.style.innerHTML = this.innerHtmlAssemble();
}
}
replaceRulesInStyle(key:string,newRules:string){
this.innerHtml[key] = newRules;
if(this.style){
this.style.innerHTML = this.innerHtmlAssemble();
}
}
removeRulesInStyle(key:string){
delete this.innerHtml.key;
if(this.style){
this.style.innerHTML = this.innerHtmlAssemble();
}
}
clearRulesInStyle(){
this.innerHtml = {};
if(this.style){
this.style.innerHTML = this.innerHtmlAssemble();
}
}
}
export default StyleElement;
//less部分
.carousel {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.carouselWindow {
width: auto;
height: 100%;
overflow: hidden;
}
.carouselContent {
display: flex;
height: 100%;
}
.carouselItem_content {
min-width: 100%;
height: 100%;
}
.decreaseBtn,
.insertBtn {
position: absolute;
top: 45%;
text-align: center;
background: #bfc9d4;
width: 40px;
height: 40px;
border-radius: 10%;
z-index: 10;
display: flex;
align-items: center;
justify-content:center;
cursor: pointer;
}
.decreaseBtn{
left: 10px;
}
.insertBtn{
right:10px;
}
.decreaseBtn_hover {
visibility:hidden;
}
.insertBtn_hover {
visibility:hidden;
}
.dots{
position: absolute;
z-index: 10;
}
.dotsBottom{
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.dotsTop{
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
}
.dotsRight{
top: 50%;
left: 20px;
transform: translateY(-50%);
}
.dotItem{
width: 10px;
height: 10px;
margin: 2px;
}
.dotLine{
width:20px;
height: 5px;
border-radius: 4px;
}
.dotCicle{
border-radius: 50%;
}
<section className={styles.left}>
<Carousel
change={carouselChange}
click={carouselClick}
autoplay={true}
direction="horizontal"
carouselStyle={{
height: '50%',
borderRadius:'4px'
}}
>
{carouselItem.map(
(carouselList_item: string, carouselList_index: number) => {
return (
<img
key={carouselList_index}
className={styles.carouselItem_img}
src={carouselList_item}
></img>
);
},
)}
</Carousel>
</section>