avatar-upload.tsx 3.6 KB

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