avatar-upload.tsx 3.6 KB

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