avatar-upload.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { useState } from 'react'
  2. import { View, Image } from '@tarojs/components'
  3. import Taro from '@tarojs/taro'
  4. import { cn } from '@/utils/cn'
  5. import { uploadFromSelect, type UploadResult } from '@/utils/minio'
  6. interface AvatarUploadProps {
  7. currentAvatar?: string
  8. onUploadSuccess?: (result: UploadResult) => void
  9. onUploadError?: (error: Error) => void
  10. size?: number
  11. editable?: boolean
  12. }
  13. export function AvatarUpload({
  14. currentAvatar,
  15. onUploadSuccess,
  16. onUploadError,
  17. size = 96,
  18. editable = true
  19. }: AvatarUploadProps) {
  20. const [uploading, setUploading] = useState(false)
  21. const [progress, setProgress] = useState(0)
  22. const handleChooseImage = async () => {
  23. if (!editable || uploading) return
  24. try {
  25. setUploading(true)
  26. setProgress(0)
  27. const result = await uploadFromSelect(
  28. 'avatars',
  29. {
  30. sourceType: ['album', 'camera'],
  31. count: 1
  32. },
  33. {
  34. onProgress: (event) => {
  35. setProgress(event.progress)
  36. if (event.stage === 'uploading') {
  37. Taro.showLoading({
  38. title: `上传中...${event.progress}%`
  39. })
  40. }
  41. },
  42. onComplete: () => {
  43. Taro.hideLoading()
  44. Taro.showToast({
  45. title: '上传成功',
  46. icon: 'success'
  47. })
  48. },
  49. onError: (error) => {
  50. Taro.hideLoading()
  51. onUploadError?.(error)
  52. Taro.showToast({
  53. title: '上传失败',
  54. icon: 'none'
  55. })
  56. }
  57. }
  58. )
  59. onUploadSuccess?.(result)
  60. } catch (error) {
  61. console.error('头像上传失败:', error)
  62. onUploadError?.(error as Error)
  63. } finally {
  64. setUploading(false)
  65. setProgress(0)
  66. }
  67. }
  68. const avatarSize = size
  69. return (
  70. <View
  71. className="relative inline-block"
  72. onClick={handleChooseImage}
  73. >
  74. <View
  75. className={cn(
  76. "relative overflow-hidden rounded-full",
  77. "border-4 border-white shadow-lg",
  78. editable && "cursor-pointer active:scale-95 transition-transform duration-150",
  79. uploading && "opacity-75"
  80. )}
  81. style={{ width: avatarSize, height: avatarSize }}
  82. >
  83. <Image
  84. src={currentAvatar || 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=160&h=160&fit=crop&crop=face'}
  85. mode="aspectFill"
  86. className="w-full h-full"
  87. />
  88. {uploading && (
  89. <View className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
  90. <View className="text-white text-xs">{progress}%</View>
  91. </View>
  92. )}
  93. </View>
  94. {editable && !uploading && (
  95. <View
  96. className={cn(
  97. "absolute -bottom-1 -right-1",
  98. "w-8 h-8 bg-blue-500 rounded-full",
  99. "flex items-center justify-center shadow-md",
  100. "border-2 border-white"
  101. )}
  102. >
  103. <View className="i-heroicons-camera-20-solid w-4 h-4 text-white" />
  104. </View>
  105. )}
  106. {uploading && (
  107. <View
  108. className={cn(
  109. "absolute -bottom-1 -right-1",
  110. "w-8 h-8 bg-gray-500 rounded-full",
  111. "flex items-center justify-center shadow-md",
  112. "border-2 border-white"
  113. )}
  114. >
  115. <View className="i-heroicons-arrow-path-20-solid w-4 h-4 text-white animate-spin" />
  116. </View>
  117. )}
  118. </View>
  119. )
  120. }