152 lines
3.9 KiB
TypeScript
152 lines
3.9 KiB
TypeScript
import React, { useState, useRef } from "react";
|
||
import { View as RNView, Dimensions, type ViewProps, type StyleProp, type ViewStyle } from "react-native";
|
||
import RNCarousel, { ICarouselInstance } from "react-native-reanimated-carousel";
|
||
import { cn } from "../../lib/utils";
|
||
|
||
// 扩展 View 以支持 className(NativeWind)
|
||
const View = RNView as React.ComponentType<ViewProps & { className?: string }>;
|
||
|
||
const { width: screenWidth } = Dimensions.get("window");
|
||
|
||
export type CarouselApi = ICarouselInstance;
|
||
|
||
type CarouselProps = {
|
||
orientation?: "horizontal" | "vertical";
|
||
setApi?: (api: CarouselApi) => void;
|
||
className?: string;
|
||
children: React.ReactNode;
|
||
width?: number;
|
||
height?: number;
|
||
autoPlay?: boolean;
|
||
autoPlayInterval?: number;
|
||
loop?: boolean;
|
||
};
|
||
|
||
type CarouselContextProps = {
|
||
api: CarouselApi | null;
|
||
orientation: "horizontal" | "vertical";
|
||
width: number;
|
||
height: number;
|
||
autoPlay: boolean;
|
||
autoPlayInterval: number;
|
||
loop: boolean;
|
||
};
|
||
|
||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||
|
||
function useCarousel() {
|
||
const context = React.useContext(CarouselContext);
|
||
if (!context) {
|
||
throw new Error("useCarousel must be used within a <Carousel />");
|
||
}
|
||
return context;
|
||
}
|
||
|
||
function Carousel({
|
||
orientation = "horizontal",
|
||
setApi,
|
||
className,
|
||
children,
|
||
width = screenWidth,
|
||
height = 410,
|
||
autoPlay = false,
|
||
autoPlayInterval = 3000,
|
||
loop = true,
|
||
onIndexChange,
|
||
}: CarouselProps & { onIndexChange?: (index: number) => void }) {
|
||
const [api, setInternalApi] = useState<CarouselApi | null>(null);
|
||
const carouselRef = useRef<ICarouselInstance>(null);
|
||
|
||
React.useEffect(() => {
|
||
if (carouselRef.current) {
|
||
const instance = carouselRef.current;
|
||
setInternalApi(instance);
|
||
setApi?.(instance);
|
||
}
|
||
}, [setApi]);
|
||
|
||
// 提取子元素到数组,过滤掉非 ReactElement 的内容
|
||
// 如果子元素是 CarouselContent,需要提取其内部的子元素
|
||
const childrenArray = React.Children.toArray(children);
|
||
const items = childrenArray.reduce<React.ReactElement[]>((acc, child) => {
|
||
if (React.isValidElement(child)) {
|
||
const props = child.props as { children?: React.ReactNode };
|
||
if (props.children) {
|
||
const innerChildren = React.Children.toArray(props.children).filter(
|
||
(innerChild): innerChild is React.ReactElement => React.isValidElement(innerChild)
|
||
);
|
||
return [...acc, ...innerChildren];
|
||
}
|
||
return [...acc, child];
|
||
}
|
||
return acc;
|
||
}, []);
|
||
|
||
return (
|
||
<CarouselContext.Provider
|
||
value={{
|
||
api,
|
||
orientation,
|
||
width,
|
||
height,
|
||
autoPlay,
|
||
autoPlayInterval,
|
||
loop,
|
||
}}
|
||
>
|
||
<View className={cn("relative", className)}>
|
||
<RNCarousel
|
||
ref={carouselRef}
|
||
loop={loop}
|
||
width={width}
|
||
height={height}
|
||
autoPlay={autoPlay}
|
||
autoPlayInterval={autoPlayInterval}
|
||
data={items}
|
||
scrollAnimationDuration={500}
|
||
vertical={orientation === "vertical"}
|
||
renderItem={({ item }) => item as React.ReactElement}
|
||
onSnapToItem={onIndexChange}
|
||
/>
|
||
</View>
|
||
</CarouselContext.Provider>
|
||
);
|
||
}
|
||
|
||
function CarouselContent({
|
||
children,
|
||
...props
|
||
}: {
|
||
className?: string;
|
||
children: React.ReactNode;
|
||
style?: StyleProp<ViewStyle>;
|
||
}) {
|
||
// CarouselContent 在新架构中主要用于包裹 CarouselItem
|
||
// 实际渲染由 Carousel 的 renderItem 处理
|
||
return <>{children}</>;
|
||
}
|
||
|
||
function CarouselItem({
|
||
className,
|
||
children,
|
||
...props
|
||
}: {
|
||
className?: string;
|
||
children: React.ReactNode;
|
||
style?: StyleProp<ViewStyle>;
|
||
}) {
|
||
const { width, height } = useCarousel();
|
||
|
||
return (
|
||
<View
|
||
className={cn("flex justify-center items-center", className)}
|
||
style={{ width, height }}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
export { Carousel, CarouselContent, CarouselItem, useCarousel };
|