image.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { View, Image as TaroImage, ImageProps as TaroImageProps } from '@tarojs/components'
  2. import { cn } from '@/utils/cn'
  3. import { useState } from 'react'
  4. export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
  5. /**
  6. * 图片地址
  7. */
  8. src: string
  9. /**
  10. * 替代文本
  11. */
  12. alt?: string
  13. /**
  14. * 图片模式
  15. * @default "aspectFill"
  16. */
  17. mode?: TaroImageProps['mode']
  18. /**
  19. * 是否懒加载
  20. * @default true
  21. */
  22. lazyLoad?: boolean
  23. /**
  24. * 是否显示加载占位
  25. * @default true
  26. */
  27. showLoading?: boolean
  28. /**
  29. * 是否显示错误占位
  30. * @default true
  31. */
  32. showError?: boolean
  33. /**
  34. * 圆角大小
  35. */
  36. rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
  37. /**
  38. * 自定义样式类
  39. */
  40. className?: string
  41. /**
  42. * 图片加载失败时的回调
  43. */
  44. onError?: () => void
  45. /**
  46. * 图片加载成功的回调
  47. */
  48. onLoad?: () => void
  49. }
  50. const roundedMap = {
  51. none: '',
  52. sm: 'rounded-sm',
  53. md: 'rounded-md',
  54. lg: 'rounded-lg',
  55. xl: 'rounded-xl',
  56. full: 'rounded-full'
  57. }
  58. export function Image({
  59. src,
  60. alt = '图片',
  61. mode = 'aspectFill',
  62. lazyLoad = true,
  63. showLoading = true,
  64. showError = true,
  65. rounded = 'none',
  66. className,
  67. onError,
  68. onLoad,
  69. ...props
  70. }: ImageProps) {
  71. const [loading, setLoading] = useState(true)
  72. const [error, setError] = useState(false)
  73. const handleLoad = () => {
  74. setLoading(false)
  75. setError(false)
  76. onLoad?.()
  77. }
  78. const handleError = () => {
  79. setLoading(false)
  80. setError(true)
  81. onError?.()
  82. }
  83. const renderPlaceholder = () => {
  84. if (loading && showLoading) {
  85. return (
  86. <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
  87. <View className="i-heroicons-photo-20-solid w-8 h-8 text-gray-400 animate-pulse" />
  88. </View>
  89. )
  90. }
  91. if (error && showError) {
  92. return (
  93. <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
  94. <View className="i-heroicons-exclamation-triangle-20-solid w-8 h-8 text-gray-400" />
  95. </View>
  96. )
  97. }
  98. return null
  99. }
  100. return (
  101. <View className={cn('relative overflow-hidden', roundedMap[rounded], className)}>
  102. <TaroImage
  103. src={src}
  104. mode={mode}
  105. lazyLoad={lazyLoad}
  106. onLoad={handleLoad}
  107. onError={handleError}
  108. className={cn(
  109. 'w-full h-full',
  110. loading && 'opacity-0',
  111. !loading && !error && 'opacity-100 transition-opacity duration-300'
  112. )}
  113. {...props}
  114. />
  115. {renderPlaceholder()}
  116. </View>
  117. )
  118. }