|
@@ -0,0 +1,127 @@
|
|
|
|
|
+import { View, Image as TaroImage, ImageProps as TaroImageProps } from '@tarojs/components'
|
|
|
|
|
+import { cn } from '@/utils/cn'
|
|
|
|
|
+import { useState } from 'react'
|
|
|
|
|
+
|
|
|
|
|
+export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 图片地址
|
|
|
|
|
+ */
|
|
|
|
|
+ src: string
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 替代文本
|
|
|
|
|
+ */
|
|
|
|
|
+ alt?: string
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 图片模式
|
|
|
|
|
+ * @default "aspectFill"
|
|
|
|
|
+ */
|
|
|
|
|
+ mode?: TaroImageProps['mode']
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否懒加载
|
|
|
|
|
+ * @default true
|
|
|
|
|
+ */
|
|
|
|
|
+ lazyLoad?: boolean
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否显示加载占位
|
|
|
|
|
+ * @default true
|
|
|
|
|
+ */
|
|
|
|
|
+ showLoading?: boolean
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否显示错误占位
|
|
|
|
|
+ * @default true
|
|
|
|
|
+ */
|
|
|
|
|
+ showError?: boolean
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 圆角大小
|
|
|
|
|
+ */
|
|
|
|
|
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义样式类
|
|
|
|
|
+ */
|
|
|
|
|
+ className?: string
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 图片加载失败时的回调
|
|
|
|
|
+ */
|
|
|
|
|
+ onError?: () => void
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 图片加载成功的回调
|
|
|
|
|
+ */
|
|
|
|
|
+ onLoad?: () => void
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const roundedMap = {
|
|
|
|
|
+ none: '',
|
|
|
|
|
+ sm: 'rounded-sm',
|
|
|
|
|
+ md: 'rounded-md',
|
|
|
|
|
+ lg: 'rounded-lg',
|
|
|
|
|
+ xl: 'rounded-xl',
|
|
|
|
|
+ full: 'rounded-full'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function Image({
|
|
|
|
|
+ src,
|
|
|
|
|
+ alt = '图片',
|
|
|
|
|
+ mode = 'aspectFill',
|
|
|
|
|
+ lazyLoad = true,
|
|
|
|
|
+ showLoading = true,
|
|
|
|
|
+ showError = true,
|
|
|
|
|
+ rounded = 'none',
|
|
|
|
|
+ className,
|
|
|
|
|
+ onError,
|
|
|
|
|
+ onLoad,
|
|
|
|
|
+ ...props
|
|
|
|
|
+}: ImageProps) {
|
|
|
|
|
+ const [loading, setLoading] = useState(true)
|
|
|
|
|
+ const [error, setError] = useState(false)
|
|
|
|
|
+
|
|
|
|
|
+ const handleLoad = () => {
|
|
|
|
|
+ setLoading(false)
|
|
|
|
|
+ setError(false)
|
|
|
|
|
+ onLoad?.()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleError = () => {
|
|
|
|
|
+ setLoading(false)
|
|
|
|
|
+ setError(true)
|
|
|
|
|
+ onError?.()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const renderPlaceholder = () => {
|
|
|
|
|
+ if (loading && showLoading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
|
|
|
|
+ <View className="i-heroicons-photo-20-solid w-8 h-8 text-gray-400 animate-pulse" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (error && showError) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
|
|
|
|
+ <View className="i-heroicons-exclamation-triangle-20-solid w-8 h-8 text-gray-400" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View className={cn('relative overflow-hidden', roundedMap[rounded], className)}>
|
|
|
|
|
+ <TaroImage
|
|
|
|
|
+ src={src}
|
|
|
|
|
+ mode={mode}
|
|
|
|
|
+ lazyLoad={lazyLoad}
|
|
|
|
|
+ onLoad={handleLoad}
|
|
|
|
|
+ onError={handleError}
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ 'w-full h-full',
|
|
|
|
|
+ loading && 'opacity-0',
|
|
|
|
|
+ !loading && !error && 'opacity-100 transition-opacity duration-300'
|
|
|
|
|
+ )}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ {renderPlaceholder()}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|