lty/qy-lty-admin/lib/api/upload.ts
2026-03-17 13:17:02 +08:00

362 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ApiResponse<UploadResponse>> => {
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': 'multipart/form-data',
},
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 / 1024 / 1024).toFixed(2)}MB)`);
if (onProgress) {
onProgress({
loaded,
total,
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<ApiResponse<UploadResponse[]>> => {
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': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
const percentage = total ? Math.round((loaded * 100) / total) : 0;
console.log(`📊 批量上传进度: ${percentage}%`);
if (onProgress) {
onProgress({
loaded,
total,
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<ApiResponse<UploadResponse>> => {
// 验证文件类型
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<ApiResponse<UploadResponse>> => {
// 验证文件类型
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<ApiResponse<UploadResponse>> => {
// 验证文件类型
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<ApiResponse<UploadResponse>> => {
// 验证文件类型
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<ApiResponse<UploadResponse>> => {
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<ApiResponse<{ message: string }>> => {
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<ApiResponse<UploadResponse>> => {
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];
};