expo-popcore-app/components/ui/Img.tsx

153 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { forwardRef, memo, useEffect, useMemo, useState } from 'react'
import { Image as ExpoImage, ImageProps as ExpoImageProps } from 'expo-image'
import { TouchableOpacity, TouchableOpacityProps, View } from 'react-native'
interface ImgProps extends ExpoImageProps {
onClick?: () => void
touchProps?: TouchableOpacityProps
className?: string
src?: string | number
errorSource?: string | number
/** 图片宽度,用于 CDN 压缩 */
width?: number
/** 是否使用 WebP 格式(默认 true */
isWebP?: boolean
/** 是否启用 CDN 压缩(默认 false */
isCompression?: boolean
/** 自定义缓存键,用于需要重定向的 URL */
cacheKey?: string
/** 占位图 URL在真实图片加载完成前显示 */
placeholderSrc?: string
}
const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
const {
onClick,
touchProps = {},
className = '',
style,
src,
errorSource,
source: propSource,
width = 256,
isWebP = true,
isCompression = false,
cacheKey,
placeholderSrc,
onLoad,
...reset
} = props
const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => {
setIsLoaded(false)
}, [src])
// 判断是否为网络图片
const isNetworkImage = (uri: string | number): boolean => {
if (typeof uri === 'number') return false
return uri.startsWith('http://') || uri.startsWith('https://')
}
// CDN 压缩 URL使用 Cloudflare 图片优化服务)
const compressionUrl = useMemo((): string | undefined => {
if (!src || typeof src !== 'string' || !isNetworkImage(src)) return undefined
const format = isWebP ? 'webp' : 'jpg'
return `https://bowong.cc/cdn-cgi/image/width=${width},quality=75,format=${format}/${src}`
}, [width, isWebP, src])
// 构建图片源
const imageSource = useMemo(() => {
// 如果提供了source属性优先使用
if (propSource) return propSource
if (!src) return undefined
if (typeof src === 'number') {
// 本地图片资源require导入的资源ID
return src
} else {
// 网络图片或本地文件路径
if (isNetworkImage(src)) {
const finalUrl = (isCompression && compressionUrl) ? compressionUrl : src
return {
uri: finalUrl,
cacheKey: cacheKey || finalUrl,
}
} else {
// 本地文件路径
return { uri: src }
}
}
}, [src, propSource, cacheKey, isCompression, compressionUrl])
const handleLoad = (e: any) => {
setIsLoaded(true)
onLoad?.(e)
}
const handlePress = () => {
onClick && onClick()
}
// 渲染图片内容
const renderImage = () => {
// 无占位图时直接返回原图
if (!placeholderSrc) {
return (
<ExpoImage
ref={ref}
style={style}
source={imageSource}
cachePolicy="disk"
recyclingKey={typeof src === 'string' ? src : undefined}
transition={{ duration: 200, effect: 'cross-dissolve' }}
onLoad={onLoad}
{...reset}
/>
)
}
// 有占位图时使用层叠布局
return (
<View style={[style, { overflow: 'hidden' }]}>
{/* 占位图层 - 加载完成后隐藏 */}
<ExpoImage
style={{ position: 'absolute', width: '100%', height: '100%' }}
source={{ uri: placeholderSrc }}
cachePolicy="disk"
contentFit={reset.contentFit || 'cover'}
transition={{ duration: 100, effect: 'curl-up' }}
/>
{/* 真实图片层 */}
<ExpoImage
ref={ref}
style={{ width: '100%', height: '100%', opacity: isLoaded ? 1 : 0 }}
source={imageSource}
cachePolicy="disk"
recyclingKey={typeof src === 'string' ? src : undefined}
transition={{ duration: 300, effect: 'cross-dissolve' }}
onLoad={handleLoad}
contentFit={reset.contentFit || 'cover'}
{...reset}
/>
</View>
)
}
if (onClick) {
return (
<TouchableOpacity onPress={handlePress} {...touchProps}>
{renderImage()}
</TouchableOpacity>
)
}
return renderImage()
})
Img.displayName = 'Img'
export default memo(Img)