/** * 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 => { // 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, } }