import { apiClient } from "./client"; import type { ApiResponse } from "./types"; // 上传文件响应类型 export interface UploadResponse { url: string; filename: string; size: number; mimeType: string; } // 上传进度回调类型 export type UploadProgressCallback = (progressEvent: { loaded: number; total: number; percentage: number; }) => void; /** * 上传单个文件 * @param file 要上传的文件 * @param onProgress 上传进度回调函数 * @returns 上传结果 */ export const uploadFile = async ( file: File, onProgress?: UploadProgressCallback ): Promise> => { try { const formData = new FormData(); formData.append('file', file); console.log('📤 开始上传文件:', file.name, `(${(file.size / 1024 / 1024).toFixed(2)}MB)`); const response = await apiClient.post('/common/upload/', formData, { headers: { 'Content-Type': undefined as any, // Let axios/browser set multipart boundary automatically }, onUploadProgress: (progressEvent) => { const { loaded, total } = progressEvent; const percentage = total ? Math.round((loaded * 100) / total) : 0; console.log(`📊 上传进度: ${percentage}% (${(loaded / 1024 / 1024).toFixed(2)}MB / ${((total || 0) / 1024 / 1024).toFixed(2)}MB)`); if (onProgress) { onProgress({ loaded, total: total || 0, percentage, }); } }, }); if (!response.data.success) { throw new Error(response.data.message || '文件上传失败'); } console.log('✅ 文件上传成功:', response.data.data); return response.data; } catch (error) { console.error("文件上传失败:", error); throw error; } }; /** * 上传多个文件 * @param files 要上传的文件数组 * @param onProgress 上传进度回调函数 * @returns 上传结果数组 */ export const uploadFiles = async ( files: File[], onProgress?: UploadProgressCallback ): Promise> => { try { const formData = new FormData(); files.forEach((file, index) => { formData.append(`files`, file); }); console.log('📤 开始批量上传文件:', files.length, '个文件'); const response = await apiClient.post('/common/upload/', formData, { headers: { 'Content-Type': undefined as any, // Let axios/browser set multipart boundary automatically }, onUploadProgress: (progressEvent) => { const { loaded, total } = progressEvent; const percentage = total ? Math.round((loaded * 100) / total) : 0; console.log(`📊 批量上传进度: ${percentage}%`); if (onProgress) { onProgress({ loaded, total: total || 0, percentage, }); } }, }); if (!response.data.success) { throw new Error(response.data.message || '文件批量上传失败'); } console.log('✅ 文件批量上传成功:', response.data.data); return response.data; } catch (error) { console.error("文件批量上传失败:", error); throw error; } }; /** * 上传图片文件(带图片格式验证) * @param file 要上传的图片文件 * @param onProgress 上传进度回调函数 * @returns 上传结果 */ export const uploadImage = async ( file: File, onProgress?: UploadProgressCallback ): Promise> => { // 验证文件类型 const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { throw new Error('只支持 JPEG、PNG、GIF、WebP 格式的图片'); } // 验证文件大小(限制10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { throw new Error('图片文件大小不能超过 10MB'); } return uploadFile(file, onProgress); }; /** * 上传头像图片(带特殊限制) * @param file 要上传的头像图片 * @param onProgress 上传进度回调函数 * @returns 上传结果 */ export const uploadAvatar = async ( file: File, onProgress?: UploadProgressCallback ): Promise> => { // 验证文件类型 const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png']; if (!allowedTypes.includes(file.type)) { throw new Error('头像只支持 JPEG、PNG 格式'); } // 验证文件大小(限制2MB) const maxSize = 2 * 1024 * 1024; // 2MB if (file.size > maxSize) { throw new Error('头像文件大小不能超过 2MB'); } return uploadFile(file, onProgress); }; /** * 上传动画文件 * @param file 要上传的动画文件 * @param onProgress 上传进度回调函数 * @returns 上传结果 */ export const uploadAnimation = async ( file: File, onProgress?: UploadProgressCallback ): Promise> => { // 验证文件类型 const allowedTypes = [ 'video/mp4', 'video/avi', 'video/mov', 'video/wmv', 'video/flv', 'image/gif', // GIF动画 'application/x-lottie', // Lottie动画 'text/json', // Lottie JSON格式 ]; const isValidType = allowedTypes.includes(file.type) || file.name.toLowerCase().endsWith('.json') || file.name.toLowerCase().endsWith('.lottie'); if (!isValidType) { throw new Error('只支持 MP4、AVI、MOV、WMV、FLV、GIF、Lottie 格式的动画文件'); } // 验证文件大小(限制50MB) const maxSize = 50 * 1024 * 1024; // 50MB if (file.size > maxSize) { throw new Error('动画文件大小不能超过 50MB'); } console.log('📹 开始上传动画文件:', file.name, `(${(file.size / 1024 / 1024).toFixed(2)}MB)`); return uploadFile(file, onProgress); }; /** * 上传音频文件 * @param file 要上传的音频文件 * @param onProgress 上传进度回调函数 * @returns 上传结果 */ export const uploadAudio = async ( file: File, onProgress?: UploadProgressCallback ): Promise> => { // 验证文件类型 const allowedTypes = [ 'audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/aac', 'audio/flac', 'audio/wma', 'audio/m4a' ]; const isValidType = allowedTypes.includes(file.type) || /\.(mp3|wav|ogg|aac|flac|wma|m4a)$/i.test(file.name); if (!isValidType) { throw new Error('只支持 MP3、WAV、OGG、AAC、FLAC、WMA、M4A 格式的音频文件'); } // 验证文件大小(限制20MB) const maxSize = 20 * 1024 * 1024; // 20MB if (file.size > maxSize) { throw new Error('音频文件大小不能超过 20MB'); } console.log('🎵 开始上传音频文件:', file.name, `(${(file.size / 1024 / 1024).toFixed(2)}MB)`); return uploadFile(file, onProgress); }; /** * 通过URL上传文件 * @param url 文件URL * @param filename 文件名 * @returns 上传结果 */ export const uploadFromUrl = async ( url: string, filename?: string ): Promise> => { try { console.log('📤 开始从URL上传文件:', url); const response = await apiClient.post('/common/upload/', { url, filename, }); if (!response.data.success) { throw new Error(response.data.message || 'URL文件上传失败'); } console.log('✅ URL文件上传成功:', response.data.data); return response.data; } catch (error) { console.error("URL文件上传失败:", error); throw error; } }; /** * 删除上传的文件 * @param url 文件URL或文件ID * @returns 删除结果 */ export const deleteUploadedFile = async (url: string): Promise> => { try { console.log('🗑️ 删除上传文件:', url); const response = await apiClient.delete('/common/upload/', { data: { url } }); if (!response.data.success) { throw new Error(response.data.message || '文件删除失败'); } console.log('✅ 文件删除成功'); return response.data; } catch (error) { console.error("文件删除失败:", error); throw error; } }; /** * 获取文件信息 * @param url 文件URL * @returns 文件信息 */ export const getFileInfo = async (url: string): Promise> => { try { const response = await apiClient.get(`/common/upload/info/?url=${encodeURIComponent(url)}`); if (!response.data.success) { throw new Error(response.data.message || '获取文件信息失败'); } return response.data; } catch (error) { console.error("获取文件信息失败:", error); throw error; } }; // 文件类型检查工具函数 export const isImageFile = (file: File): boolean => { if (!file || !file.type) return false; return file.type.startsWith('image/'); }; export const isVideoFile = (file: File): boolean => { if (!file || !file.type) return false; return file.type.startsWith('video/'); }; export const isAudioFile = (file: File): boolean => { if (!file) return false; const hasAudioType = file.type && file.type.startsWith('audio/'); const hasAudioExtension = file.name && /\.(mp3|wav|ogg|aac|flac|wma|m4a)$/i.test(file.name); return Boolean(hasAudioType || hasAudioExtension); }; export const isAnimationFile = (file: File): boolean => { if (!file) return false; const hasVideoType = file.type && file.type.startsWith('video/'); const hasGifType = file.type && file.type === 'image/gif'; const hasLottieType = file.type && file.type === 'application/x-lottie'; const hasJsonName = file.name && file.name.toLowerCase().endsWith('.json'); const hasLottieName = file.name && file.name.toLowerCase().endsWith('.lottie'); return Boolean(hasVideoType || hasGifType || hasLottieType || hasJsonName || hasLottieName); }; export const isPdfFile = (file: File): boolean => { if (!file || !file.type) return false; return file.type === 'application/pdf'; }; // 文件大小格式化工具函数 export const formatFileSize = (bytes: number): string => { // 处理无效值 if (typeof bytes !== 'number' || isNaN(bytes) || bytes < 0) { return '未知大小'; } if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); // 确保索引在有效范围内 const sizeIndex = Math.min(i, sizes.length - 1); return parseFloat((bytes / Math.pow(k, sizeIndex)).toFixed(2)) + ' ' + sizes[sizeIndex]; };