-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
365 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,2 @@ | ||
export type TPullStatus = 'pulling' | 'canRelease' | 'refreshing' | 'complete'; | ||
|
||
export interface IPullStatus { | ||
[key: string]: TPullStatus; | ||
} | ||
export type TPullKey = 'PULLING' | 'CAN_RELEASE' | 'REFRESHING' | 'COMPLETE'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import InternalSwiper from './swiper'; | ||
import SwiperItem from './swiper-item'; | ||
|
||
export type { SwiperProps } from './swiper'; | ||
export type { SwiperItemProps } from './swiper-item'; | ||
|
||
type InternalSwiperType = typeof InternalSwiper; | ||
|
||
export interface SwiperInterface extends InternalSwiperType { | ||
Item: typeof SwiperItem; | ||
} | ||
|
||
const Swiper = InternalSwiper as SwiperInterface; | ||
|
||
Swiper.Item = SwiperItem; | ||
|
||
export default Swiper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.ygm-swiper-item { | ||
display: block; | ||
width: 100%; | ||
height: 100%; | ||
white-space: normal; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
.ygm-swiper-page-indicator { | ||
display: flex; | ||
width: auto; | ||
|
||
&-dot { | ||
width: 5px; | ||
height: 5px; | ||
border-radius: 50%; | ||
background-color: $ygm-color-weak; | ||
margin-right: 5px; | ||
|
||
&:last-child { | ||
margin-right: 0; | ||
} | ||
|
||
&-active { | ||
width: 13px; | ||
height: 5px; | ||
border-radius: 2px; | ||
background-color: $ygm-color-primary; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
.ygm-swiper { | ||
width: 100%; | ||
height: auto; | ||
position: relative; | ||
touch-action: pan-y; | ||
|
||
&-track { | ||
width: 100%; | ||
height: 100%; | ||
position: relative; | ||
flex-wrap: nowrap; | ||
display: flex; | ||
overflow: hidden; | ||
|
||
&-inner { | ||
width: 100%; | ||
height: 100%; | ||
position: relative; | ||
flex-wrap: nowrap; | ||
display: flex; | ||
overflow: hidden; | ||
} | ||
} | ||
|
||
&-slide { | ||
width: 100%; | ||
position: relative; | ||
display: block; | ||
flex-shrink: 0; | ||
white-space: unset; | ||
} | ||
|
||
&-indicator { | ||
position: absolute; | ||
bottom: 6px; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from 'react'; | ||
|
||
import './styles/swiper-item.scss'; | ||
|
||
export interface SwiperItemProps { | ||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; | ||
children?: React.ReactNode; | ||
} | ||
|
||
const SwiperItem: React.FC<SwiperItemProps> = React.memo((props) => { | ||
return ( | ||
<div className="ygm-swiper-item" onClick={props.onClick}> | ||
{props.children} | ||
</div> | ||
); | ||
}); | ||
|
||
SwiperItem.displayName = 'SwiperItem'; | ||
|
||
export default SwiperItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react'; | ||
import cx from 'classnames'; | ||
|
||
import './styles/swiper-page-indicator.scss'; | ||
|
||
export interface SwiperPageIndicatorProps { | ||
current: number; | ||
total: number; | ||
indicatorClassName?: string; | ||
} | ||
|
||
const classPrefix = 'ygm-swiper-page-indicator'; | ||
|
||
const SwiperPageIndicator: React.FC<SwiperPageIndicatorProps> = React.memo((props) => { | ||
const dots: React.ReactElement[] = React.useMemo(() => { | ||
return Array(props.total) | ||
.fill(0) | ||
.map((_, index) => ( | ||
<div | ||
key={index} | ||
className={cx(`${classPrefix}-dot`, { | ||
[`${classPrefix}-dot-active`]: props.current === index, | ||
})} | ||
/> | ||
)); | ||
}, [props]); | ||
|
||
return <div className={classPrefix}>{dots}</div>; | ||
}); | ||
|
||
SwiperPageIndicator.displayName = 'SwiperPageIndicator'; | ||
|
||
export default SwiperPageIndicator; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import React from 'react'; | ||
import SwiperPageIndicator from '@/swiper/swiper-page-indicator'; | ||
|
||
import { modulus } from './utils'; | ||
|
||
import './styles/swiper.scss'; | ||
|
||
export interface SwiperProps { | ||
autoplay?: boolean; | ||
defaultIndex?: number; | ||
autoplayInterval?: number; | ||
children: React.ReactElement | React.ReactElement[]; | ||
showIndicator?: boolean; | ||
indicatorClassName?: string; | ||
} | ||
|
||
const Swiper: React.FC<SwiperProps> = React.memo((props) => { | ||
const [currentIndex, setCurrentIndex] = React.useState<number>(props.defaultIndex || 0); | ||
const [dragging, setDragging] = React.useState<boolean>(false); | ||
|
||
const trackRef = React.useRef<HTMLDivElement>(null); | ||
const startRef = React.useRef<number>(0); | ||
const slideRatioRef = React.useRef<number>(0); | ||
const intervalRef = React.useRef<number>(0); | ||
const autoPlaying = React.useRef<boolean>(false); | ||
|
||
const count = React.useMemo(() => React.Children.count(props.children), [props.children]); | ||
|
||
const getTransition = React.useCallback( | ||
(position: number) => { | ||
if (dragging) { | ||
return ''; | ||
} else if (autoPlaying.current) { | ||
if (position === -100 || position === 0) { | ||
return 'transform 0.3s ease-out'; | ||
} else { | ||
return ''; | ||
} | ||
} | ||
return 'transform 0.3s ease-out'; | ||
}, | ||
[dragging] | ||
); | ||
|
||
const getFinalPosition = React.useCallback( | ||
(index: number) => { | ||
let finalPosition = -currentIndex * 100 + index * 100; | ||
const totalWidth = count * 100; | ||
const flagWidth = totalWidth / 2; | ||
|
||
finalPosition = modulus(finalPosition + flagWidth, totalWidth) - flagWidth; | ||
return finalPosition; | ||
}, | ||
[count, currentIndex] | ||
); | ||
|
||
const renderSwiperItem = React.useCallback(() => { | ||
return ( | ||
<div className="ygm-swiper-track-inner"> | ||
{React.Children.map(props.children, (child, index) => { | ||
const position = getFinalPosition(index); | ||
return ( | ||
<div | ||
className="ygm-swiper-slide" | ||
style={{ | ||
transform: `translate3d(${position}%, 0px, 0px)`, | ||
left: `-${index * 100}%`, | ||
transition: getTransition(position), | ||
}} | ||
> | ||
{child} | ||
</div> | ||
); | ||
})} | ||
</div> | ||
); | ||
}, [props.children, getFinalPosition, getTransition]); | ||
|
||
const swipeTo = React.useCallback( | ||
(index: number) => { | ||
const targetIndex = modulus(index, count); | ||
setCurrentIndex(targetIndex); | ||
}, | ||
[count] | ||
); | ||
|
||
const swipeNext = React.useCallback(() => { | ||
swipeTo(currentIndex + 1); | ||
}, [swipeTo, currentIndex]); | ||
|
||
const getSlideRatio = React.useCallback((diff: number) => { | ||
const element = trackRef.current; | ||
if (!element) return 0; | ||
return diff / element.offsetWidth; | ||
}, []); | ||
|
||
const onTouchMove = React.useCallback( | ||
(e: TouchEvent) => { | ||
const currentX = e.changedTouches[0].clientX; | ||
const diff = startRef.current - currentX; | ||
slideRatioRef.current = getSlideRatio(diff); | ||
|
||
setCurrentIndex(currentIndex + slideRatioRef.current); | ||
}, | ||
[currentIndex, getSlideRatio] | ||
); | ||
|
||
const onTouchEnd = React.useCallback(() => { | ||
const element = trackRef.current; | ||
if (!element) return; | ||
const index = Math.round(slideRatioRef.current); | ||
slideRatioRef.current = 0; | ||
swipeTo(currentIndex + index); | ||
setDragging(false); | ||
element.removeEventListener('touchmove', onTouchMove); | ||
element.removeEventListener('touchend', onTouchEnd); | ||
}, [currentIndex, onTouchMove, swipeTo]); | ||
|
||
const onTouchStart = React.useCallback( | ||
(e: React.TouchEvent<HTMLDivElement>) => { | ||
const element = trackRef.current; | ||
if (!element) return; | ||
|
||
startRef.current = e.changedTouches[0].clientX; | ||
setDragging(true); | ||
clearInterval(intervalRef.current); | ||
autoPlaying.current = false; | ||
element.addEventListener('touchmove', onTouchMove); | ||
element.addEventListener('touchend', onTouchEnd); | ||
}, | ||
[onTouchEnd, onTouchMove] | ||
); | ||
|
||
React.useEffect(() => { | ||
if (!props.autoplay || dragging) return; | ||
intervalRef.current = window.setInterval(() => { | ||
autoPlaying.current = true; | ||
swipeNext(); | ||
}, props.autoplayInterval); | ||
return () => { | ||
clearInterval(intervalRef.current); | ||
}; | ||
}, [dragging, props.autoplay, props.autoplayInterval, swipeNext]); | ||
|
||
return ( | ||
<div className="ygm-swiper"> | ||
<div className="ygm-swiper-track" ref={trackRef} onTouchStart={onTouchStart}> | ||
{renderSwiperItem()} | ||
</div> | ||
{props.showIndicator && ( | ||
<div className="ygm-swiper-indicator"> | ||
<SwiperPageIndicator | ||
total={count} | ||
current={slideRatioRef.current > 0 ? Math.floor(currentIndex) : Math.ceil(currentIndex)} | ||
indicatorClassName={props.indicatorClassName} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}); | ||
|
||
Swiper.defaultProps = { | ||
autoplay: false, | ||
defaultIndex: 0, | ||
autoplayInterval: 3000, | ||
showIndicator: true, | ||
}; | ||
|
||
Swiper.displayName = 'Swiper'; | ||
|
||
export default Swiper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const modulus = (value: number, division: number) => { | ||
const remainder = value % division; | ||
return remainder < 0 ? remainder + division : remainder; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
.swiper-demo { | ||
&-content { | ||
height: 120px; | ||
color: #ffffff; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
font-size: 48px; | ||
user-select: none; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
import { Meta } from '@storybook/react'; | ||
|
||
import Swiper from '@/swiper'; | ||
|
||
import './index.scss'; | ||
|
||
const SwiperStory: Meta = { | ||
title: '信息展示/Swiper 轮播图', | ||
component: Swiper, | ||
subcomponents: { 'Swiper.Item': Swiper.Item }, | ||
}; | ||
|
||
const colors = ['#ace0ff', '#bcffbd', '#e4fabd']; | ||
|
||
export const Basic = () => { | ||
return ( | ||
<div className="swiper-demo"> | ||
<div> | ||
<h3>基本用法</h3> | ||
<Swiper autoplay={false}> | ||
{colors.map((color, index) => ( | ||
<Swiper.Item key={index}> | ||
<div className="swiper-demo-content" style={{ background: color }}> | ||
{index + 1} | ||
</div> | ||
</Swiper.Item> | ||
))} | ||
</Swiper> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
Basic.storyName = '基本用法'; | ||
|
||
export default SwiperStory; |