| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- /**
- * Composable for handling image uploads to S3
- */
- import { ref } from 'vue'
- import { showToast } from 'vant'
- import { uploadApi } from '~/api/upload'
- export interface UseImageUploadOptions {
- /**
- * Upload type (1=avatar, 2=identity card, etc.)
- */
- type: number
- /**
- * Max file size in MB
- */
- maxSizeMB?: number
- /**
- * Custom toast messages
- */
- messages?: {
- selectImage?: string
- onlyImage?: string
- tooLarge?: string
- signFailed?: string
- uploadUrlInvalid?: string
- uploadFailed?: string
- uploadSuccess?: string
- }
- }
- export function useImageUpload(options: UseImageUploadOptions) {
- const { t } = useI18n()
- const uploading = ref(false)
- const defaultMessages = {
- selectImage: t('editProfile.toast.selectImage'),
- onlyImage: t('editProfile.toast.onlyImage'),
- tooLarge: t('editProfile.toast.tooLarge'),
- signFailed: t('editProfile.toast.signFailed'),
- uploadUrlInvalid: t('editProfile.toast.uploadUrlInvalid'),
- uploadFailed: t('editProfile.toast.uploadFailed'),
- uploadSuccess: t('editProfile.toast.uploadSuccess'),
- }
- const messages = { ...defaultMessages, ...options.messages }
- const maxSizeMB = options.maxSizeMB || 5
- /**
- * Upload a file to S3 and return the file URL
- * @param file - The file to upload
- * @returns The uploaded file URL, or null if upload failed
- */
- const uploadFile = async (file: File): Promise<string | null> => {
- // Validate file type
- if (!file.type || !file.type.startsWith('image/')) {
- showToast(messages.onlyImage)
- return null
- }
- // Validate file size
- const isWithinSize = file.size / 1024 / 1024 < maxSizeMB
- if (!isWithinSize) {
- showToast(messages.tooLarge)
- return null
- }
- // Get file suffix
- const dotIndex = file.name.lastIndexOf('.')
- const suffix = dotIndex >= 0 ? file.name.slice(dotIndex + 1) : ''
- try {
- uploading.value = true
- // Get S3 pre-sign info
- const signResult = await uploadApi.getS3PreSign({
- type: options.type,
- suffix,
- })
- if (!signResult) {
- showToast(messages.signFailed)
- return null
- }
- const { preSignUrl, fileUrl } = signResult
- if (!preSignUrl) {
- showToast(messages.uploadUrlInvalid)
- return null
- }
- // Upload to S3 using native fetch PUT
- const res = await fetch(preSignUrl, {
- method: 'PUT',
- headers: {
- 'Content-Type': file.type || 'application/octet-stream',
- },
- body: file,
- })
- if (!res.ok) {
- console.error('S3 upload failed with status:', res.status)
- showToast(messages.uploadFailed)
- return null
- }
- // Return the file URL from backend, fallback to pre-signed URL without query params
- const uploadedUrl = fileUrl || preSignUrl.split('?')[0] || ''
- showToast(messages.uploadSuccess)
- return uploadedUrl
- }
- catch (error) {
- console.error('Upload file error:', error)
- showToast(messages.uploadFailed)
- return null
- }
- finally {
- uploading.value = false
- }
- }
- return {
- uploading,
- uploadFile,
- }
- }
|