feat: add image picker utility and integrate image upload in DynamicForm component

This commit is contained in:
imeepos
2026-01-26 13:04:14 +08:00
parent 3fd445bb6e
commit 01865d94c2
5 changed files with 194 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
import React, { forwardRef, useMemo } from 'react'
import React, { forwardRef, memo, useEffect, useMemo, useState } from 'react'
import { Image as ExpoImage, ImageProps as ExpoImageProps } from 'expo-image'
import { TouchableOpacity, TouchableOpacityProps } from 'react-native'
import { TouchableOpacity, TouchableOpacityProps, View } from 'react-native'
interface ImgProps extends ExpoImageProps {
onClick?: () => void
@@ -8,6 +8,16 @@ interface ImgProps extends ExpoImageProps {
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) => {
@@ -19,15 +29,34 @@ const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
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属性优先使用
@@ -41,40 +70,83 @@ const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
} else {
// 网络图片或本地文件路径
if (isNetworkImage(src)) {
const finalUrl = (isCompression && compressionUrl) ? compressionUrl : src
return {
uri: src,
cache: 'immutable', // 使用expo-image的缓存机制
uri: finalUrl,
cacheKey: cacheKey || finalUrl,
}
} else {
// 本地文件路径
return { uri: src }
}
}
}, [src, propSource])
}, [src, propSource, cacheKey, isCompression, compressionUrl])
const imgProps = {
style,
className,
ref,
source: imageSource,
errorSource,
...reset,
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}>
<ExpoImage {...imgProps} />
{renderImage()}
</TouchableOpacity>
)
}
return <ExpoImage {...imgProps} />
return renderImage()
})
Img.displayName = 'Img'
export default Img
export default memo(Img)