|
@@ -1,224 +0,0 @@
|
|
-import { Injectable } from '@angular/core';
|
|
|
|
-import { NovaUploadService as FmodeUploadService } from 'fmode-ng/storage';
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * 文件上传服务
|
|
|
|
- * 用于处理图片、视频、文档等文件的上传
|
|
|
|
- * 基于 fmode-ng 的 NovaUploadService
|
|
|
|
- */
|
|
|
|
-@Injectable({
|
|
|
|
- providedIn: 'root'
|
|
|
|
-})
|
|
|
|
-export class ProjectUploadService {
|
|
|
|
- constructor(private uploadServ: FmodeUploadService) {}
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 上传单个文件
|
|
|
|
- * @param file File对象
|
|
|
|
- * @param options 上传选项
|
|
|
|
- * @returns 上传后的文件URL
|
|
|
|
- */
|
|
|
|
- async uploadFile(
|
|
|
|
- file: File,
|
|
|
|
- options?: {
|
|
|
|
- onProgress?: (progress: number) => void;
|
|
|
|
- compress?: boolean;
|
|
|
|
- maxWidth?: number;
|
|
|
|
- maxHeight?: number;
|
|
|
|
- }
|
|
|
|
- ): Promise<string> {
|
|
|
|
- try {
|
|
|
|
- let fileToUpload = file;
|
|
|
|
-
|
|
|
|
- // 如果是图片且需要压缩
|
|
|
|
- if (options?.compress && file.type.startsWith('image/')) {
|
|
|
|
- fileToUpload = await this.compressImage(file, {
|
|
|
|
- maxWidth: options.maxWidth || 1920,
|
|
|
|
- maxHeight: options.maxHeight || 1920,
|
|
|
|
- quality: 0.8
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // 使用 fmode-ng 的上传服务
|
|
|
|
- const fileResult = await this.uploadServ.upload(fileToUpload, (progress: any) => {
|
|
|
|
- if (options?.onProgress && progress?.percent) {
|
|
|
|
- options.onProgress(progress.percent);
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- // 返回文件URL
|
|
|
|
- return fileResult.url || '';
|
|
|
|
- } catch (error: any) {
|
|
|
|
- console.error('文件上传失败:', error);
|
|
|
|
- throw new Error('文件上传失败: ' + (error?.message || '未知错误'));
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 批量上传文件
|
|
|
|
- * @param files File数组
|
|
|
|
- * @param options 上传选项
|
|
|
|
- * @returns 上传后的文件URL数组
|
|
|
|
- */
|
|
|
|
- async uploadFiles(
|
|
|
|
- files: File[],
|
|
|
|
- options?: {
|
|
|
|
- onProgress?: (current: number, total: number) => void;
|
|
|
|
- compress?: boolean;
|
|
|
|
- }
|
|
|
|
- ): Promise<string[]> {
|
|
|
|
- const urls: string[] = [];
|
|
|
|
- const total = files.length;
|
|
|
|
-
|
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
|
- const url = await this.uploadFile(files[i], {
|
|
|
|
- compress: options?.compress
|
|
|
|
- });
|
|
|
|
- urls.push(url);
|
|
|
|
- options?.onProgress?.(i + 1, total);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return urls;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 压缩图片
|
|
|
|
- * @param file 原始文件
|
|
|
|
- * @param options 压缩选项
|
|
|
|
- * @returns 压缩后的File对象
|
|
|
|
- */
|
|
|
|
- private compressImage(
|
|
|
|
- file: File,
|
|
|
|
- options: {
|
|
|
|
- maxWidth: number;
|
|
|
|
- maxHeight: number;
|
|
|
|
- quality: number;
|
|
|
|
- }
|
|
|
|
- ): Promise<File> {
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
- const reader = new FileReader();
|
|
|
|
-
|
|
|
|
- reader.onload = (e) => {
|
|
|
|
- const img = new Image();
|
|
|
|
-
|
|
|
|
- img.onload = () => {
|
|
|
|
- const canvas = document.createElement('canvas');
|
|
|
|
- let { width, height } = img;
|
|
|
|
-
|
|
|
|
- // 计算压缩后的尺寸
|
|
|
|
- if (width > options.maxWidth || height > options.maxHeight) {
|
|
|
|
- const ratio = Math.min(
|
|
|
|
- options.maxWidth / width,
|
|
|
|
- options.maxHeight / height
|
|
|
|
- );
|
|
|
|
- width = width * ratio;
|
|
|
|
- height = height * ratio;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- canvas.width = width;
|
|
|
|
- canvas.height = height;
|
|
|
|
-
|
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
|
- ctx?.drawImage(img, 0, 0, width, height);
|
|
|
|
-
|
|
|
|
- canvas.toBlob(
|
|
|
|
- (blob) => {
|
|
|
|
- if (blob) {
|
|
|
|
- const compressedFile = new File([blob], file.name, {
|
|
|
|
- type: file.type,
|
|
|
|
- lastModified: Date.now()
|
|
|
|
- });
|
|
|
|
- resolve(compressedFile);
|
|
|
|
- } else {
|
|
|
|
- reject(new Error('图片压缩失败'));
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
- file.type,
|
|
|
|
- options.quality
|
|
|
|
- );
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- img.onerror = () => reject(new Error('图片加载失败'));
|
|
|
|
- img.src = e.target?.result as string;
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- reader.onerror = () => reject(new Error('文件读取失败'));
|
|
|
|
- reader.readAsDataURL(file);
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 生成缩略图
|
|
|
|
- * @param file 原始图片文件
|
|
|
|
- * @param size 缩略图尺寸
|
|
|
|
- * @returns 缩略图URL
|
|
|
|
- */
|
|
|
|
- async generateThumbnail(file: File, size: number = 200): Promise<string> {
|
|
|
|
- const thumbnailFile = await this.compressImage(file, {
|
|
|
|
- maxWidth: size,
|
|
|
|
- maxHeight: size,
|
|
|
|
- quality: 0.7
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- return await this.uploadFile(thumbnailFile);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 从URL下载文件
|
|
|
|
- * @param url 文件URL
|
|
|
|
- * @param filename 保存的文件名
|
|
|
|
- */
|
|
|
|
- async downloadFile(url: string, filename: string): Promise<void> {
|
|
|
|
- try {
|
|
|
|
- const response = await fetch(url);
|
|
|
|
- const blob = await response.blob();
|
|
|
|
- const link = document.createElement('a');
|
|
|
|
- link.href = URL.createObjectURL(blob);
|
|
|
|
- link.download = filename;
|
|
|
|
- link.click();
|
|
|
|
- URL.revokeObjectURL(link.href);
|
|
|
|
- } catch (error) {
|
|
|
|
- console.error('文件下载失败:', error);
|
|
|
|
- throw new Error('文件下载失败');
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 验证文件类型
|
|
|
|
- * @param file 文件
|
|
|
|
- * @param allowedTypes 允许的MIME类型数组
|
|
|
|
- * @returns 是否合法
|
|
|
|
- */
|
|
|
|
- validateFileType(file: File, allowedTypes: string[]): boolean {
|
|
|
|
- return allowedTypes.some(type => {
|
|
|
|
- if (type.endsWith('/*')) {
|
|
|
|
- const prefix = type.replace('/*', '');
|
|
|
|
- return file.type.startsWith(prefix);
|
|
|
|
- }
|
|
|
|
- return file.type === type;
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 验证文件大小
|
|
|
|
- * @param file 文件
|
|
|
|
- * @param maxSizeMB 最大大小(MB)
|
|
|
|
- * @returns 是否合法
|
|
|
|
- */
|
|
|
|
- validateFileSize(file: File, maxSizeMB: number): boolean {
|
|
|
|
- const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
|
|
- return file.size <= maxSizeBytes;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /**
|
|
|
|
- * 格式化文件大小
|
|
|
|
- * @param bytes 字节数
|
|
|
|
- * @returns 格式化后的字符串
|
|
|
|
- */
|
|
|
|
- formatFileSize(bytes: number): string {
|
|
|
|
- if (bytes < 1024) return bytes + ' B';
|
|
|
|
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
|
|
- if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
|
|
- return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
|
|
- }
|
|
|
|
-}
|
|
|