import { ref, computed, readonly } from 'vue' import { PresignUploadService } from '@/services' import type { PresignUploadStatus, PresignUploadMode, PresignInitResponse, PresignUploadOptions, PresignStatusResponse, UploadProgress, ExpireStyle, AlertType } from '@/types' import { getErrorMessage } from '@/utils/common' // 预签名上传状态常量 export const PRESIGN_UPLOAD_STATUS = { IDLE: 'idle', INITIALIZING: 'initializing', UPLOADING: 'uploading', CONFIRMING: 'confirming', SUCCESS: 'success', ERROR: 'error' } as const // 默认过期配置 const DEFAULT_EXPIRE_VALUE = 1 const DEFAULT_EXPIRE_STYLE: ExpireStyle = 'day' type ErrorWithResponse = { response?: { status?: number } } type PresignedUploadNotifier = (message: string, type: AlertType) => void type UsePresignedUploadOptions = { getMaxFileSize?: () => number notify?: PresignedUploadNotifier } /** * 预签名上传 Composable * 支持 S3 直传模式和服务器代理模式 */ export function usePresignedUpload(options: UsePresignedUploadOptions = {}) { // 状态管理 const presignStatus = ref(PRESIGN_UPLOAD_STATUS.IDLE) const uploadSession = ref(null) const uploadProgress = ref({ loaded: 0, total: 0, percentage: 0 }) const uploadedCode = ref('') const errorMessage = ref('') // 计算属性 const isInitializing = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.INITIALIZING) const isUploading = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.UPLOADING) const isConfirming = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.CONFIRMING) const isSuccess = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.SUCCESS) const isError = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.ERROR) const currentMode = computed(() => uploadSession.value?.mode ?? null) const notify: PresignedUploadNotifier = (message, type) => { options.notify?.(message, type) } /** * 文件大小验证 */ const validateFileSize = (file: File): boolean => { const maxFileSize = options.getMaxFileSize?.() if (!maxFileSize) { return true } if (file.size > maxFileSize) { const maxSizeMB = Math.round(maxFileSize / 1024 / 1024) errorMessage.value = `文件大小不能超过 ${maxSizeMB}MB` notify(errorMessage.value, 'error') return false } return true } /** * 进度追踪回调 */ const createProgressHandler = ( externalHandler?: (progress: UploadProgress) => void ): ((progress: UploadProgress) => void) => { return (progress: UploadProgress) => { // 确保进度值在有效范围内 uploadProgress.value = { loaded: Math.max(0, progress.loaded), total: Math.max(0, progress.total), percentage: Math.min(100, Math.max(0, progress.percentage)) } // 调用外部进度回调 if (externalHandler) { externalHandler(uploadProgress.value) } } } /** * 错误处理 */ const handleUploadError = (error: unknown): void => { presignStatus.value = PRESIGN_UPLOAD_STATUS.ERROR const status = (error as ErrorWithResponse)?.response?.status const fallback = status === 404 ? '上传会话不存在或已过期' : status === 500 ? '服务器错误,请稍后重试' : '上传失败,请重试' errorMessage.value = getErrorMessage(error, fallback) notify(errorMessage.value, 'error') } /** * 初始化上传会话 */ const initUploadSession = async ( file: File, options?: PresignUploadOptions ): Promise => { try { presignStatus.value = PRESIGN_UPLOAD_STATUS.INITIALIZING const response = await PresignUploadService.initUpload({ file_name: file.name, file_size: file.size, expire_value: options?.expireValue ?? DEFAULT_EXPIRE_VALUE, expire_style: options?.expireStyle ?? DEFAULT_EXPIRE_STYLE }) if (response.code === 200 && response.detail) { uploadSession.value = response.detail return response.detail } else { throw new Error(response.message || '初始化上传失败') } } catch (error) { handleUploadError(error) return null } } /** * 直传模式上传到 S3 */ const uploadDirect = async ( file: File, session: PresignInitResponse, options?: PresignUploadOptions ): Promise => { try { presignStatus.value = PRESIGN_UPLOAD_STATUS.UPLOADING // 上传到 S3 const progressHandler = createProgressHandler(options?.onProgress) const uploadSuccess = await PresignUploadService.directUploadToS3( session.upload_url, file, progressHandler ) if (!uploadSuccess) { throw new Error('S3 上传失败') } // 确认上传 presignStatus.value = PRESIGN_UPLOAD_STATUS.CONFIRMING const confirmResponse = await PresignUploadService.confirmUpload(session.upload_id, { expire_value: options?.expireValue ?? DEFAULT_EXPIRE_VALUE, expire_style: options?.expireStyle ?? DEFAULT_EXPIRE_STYLE }) if (confirmResponse.code === 200 && confirmResponse.detail?.code) { presignStatus.value = PRESIGN_UPLOAD_STATUS.SUCCESS uploadedCode.value = String(confirmResponse.detail.code) // 确保进度为 100% uploadProgress.value = { loaded: file.size, total: file.size, percentage: 100 } notify('文件上传成功!', 'success') return uploadedCode.value } else { throw new Error(confirmResponse.message || '确认上传失败') } } catch (error) { handleUploadError(error) return null } } /** * 代理模式上传 */ const uploadProxy = async ( file: File, session: PresignInitResponse, options?: PresignUploadOptions ): Promise => { try { presignStatus.value = PRESIGN_UPLOAD_STATUS.UPLOADING const progressHandler = createProgressHandler(options?.onProgress) const uploadUrl = session.proxy_upload_url || session.upload_url || session.legacy_proxy_upload_url const response = await PresignUploadService.proxyUpload( session.upload_id, file, progressHandler, uploadUrl ) if (response.code === 200 && response.detail?.code) { presignStatus.value = PRESIGN_UPLOAD_STATUS.SUCCESS uploadedCode.value = String(response.detail.code) // 确保进度为 100% uploadProgress.value = { loaded: file.size, total: file.size, percentage: 100 } notify('文件上传成功!', 'success') return uploadedCode.value } else { throw new Error(response.message || '代理上传失败') } } catch (error) { handleUploadError(error) return null } } const uploadFile = async (file: File, options?: PresignUploadOptions): Promise => { reset() if (!validateFileSize(file)) { presignStatus.value = PRESIGN_UPLOAD_STATUS.ERROR return null } const session = await initUploadSession(file, options) if (!session) { return null } if (session.mode === 'direct') { return await uploadDirect(file, session, options) } else { return await uploadProxy(file, session, options) } } /** * 取消上传 */ const cancelUpload = async (): Promise => { if (!uploadSession.value?.upload_id) { return } try { await PresignUploadService.cancelUpload(uploadSession.value.upload_id) notify('上传已取消', 'info') } catch (error) { // 取消失败时静默处理,因为会话可能已过期 console.warn('取消上传失败:', error) } finally { reset() } } /** * 获取上传状态 */ const getStatus = async (): Promise => { if (!uploadSession.value?.upload_id) { return null } try { const response = await PresignUploadService.getUploadStatus(uploadSession.value.upload_id) if (response.code === 200 && response.detail) { // 检查会话是否过期 if (response.detail.is_expired) { errorMessage.value = '上传会话已过期' notify(errorMessage.value, 'warning') } return response.detail } return null } catch (error) { handleUploadError(error) return null } } /** * 重置状态 */ const reset = (): void => { presignStatus.value = PRESIGN_UPLOAD_STATUS.IDLE uploadSession.value = null uploadProgress.value = { loaded: 0, total: 0, percentage: 0 } uploadedCode.value = '' errorMessage.value = '' } return { // 状态 (只读) presignStatus: readonly(presignStatus), uploadSession: readonly(uploadSession), uploadProgress: readonly(uploadProgress), uploadedCode: readonly(uploadedCode), errorMessage: readonly(errorMessage), // 计算属性 isInitializing, isUploading, isConfirming, isSuccess, isError, currentMode, // 方法 uploadFile, cancelUpload, getStatus, reset } }