import { Component, EventEmitter, Input, Output, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NovaStorage, NovaFile } from 'fmode-ng/core'; export interface UploadResult { success: boolean; file?: NovaFile; url?: string; error?: string; } @Component({ selector: 'app-upload-component', standalone: true, imports: [CommonModule], templateUrl: './upload.component.html', styleUrls: ['./upload.component.scss'] }) export class UploadComponent { @Input() accept: string = '*/*'; // 接受的文件类型 @Input() multiple: boolean = false; // 是否支持多文件上传 @Input() maxSize: number = 10; // 最大文件大小(MB) @Input() allowedTypes: string[] = []; // 允许的文件类型 @Input() prefixKey: string = ''; // 文件存储前缀 @Input() disabled: boolean = false; // 是否禁用 @Input() showPreview: boolean = false; // 是否显示预览 @Input() compressImages: boolean = true; // 是否压缩图片 @Output() fileSelected = new EventEmitter(); // 文件选择事件 @Output() uploadStart = new EventEmitter(); // 上传开始事件 @Output() uploadProgressEvent = new EventEmitter<{ completed: number; total: number; currentFile: string }>(); // 上传进度事件 @Output() uploadComplete = new EventEmitter(); // 上传完成事件 @Output() uploadError = new EventEmitter(); // 上传错误事件 @ViewChild('fileInput') fileInput!: ElementRef; isUploading: boolean = false; uploadProgress: number = 0; uploadedFiles: UploadResult[] = []; dragOver: boolean = false; private storage: NovaStorage | null = null; constructor() { this.initStorage(); } // 初始化 NovaStorage private async initStorage(): Promise { try { const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; this.storage = await NovaStorage.withCid(cid); console.log('✅ NovaStorage 初始化成功, cid:', cid); } catch (error) { console.error('❌ NovaStorage 初始化失败:', error); } } /** * 触发文件选择 */ triggerFileSelect(): void { if (!this.disabled) { this.fileInput.nativeElement.click(); } } /** * 处理文件选择 */ onFileSelect(event: Event): void { const target = event.target as HTMLInputElement; const files = Array.from(target.files || []); if (files.length > 0) { this.handleFiles(files); } // 清空input值,允许重复选择同一文件 target.value = ''; } /** * 处理拖拽进入 */ onDragOver(event: DragEvent): void { event.preventDefault(); event.stopPropagation(); if (!this.disabled) { this.dragOver = true; } } /** * 处理拖拽离开 */ onDragLeave(event: DragEvent): void { event.preventDefault(); event.stopPropagation(); this.dragOver = false; } /** * 处理文件拖拽放下 */ onDrop(event: DragEvent): void { event.preventDefault(); event.stopPropagation(); this.dragOver = false; if (this.disabled) { return; } const files = Array.from(event.dataTransfer?.files || []); if (files.length > 0) { this.handleFiles(files); } } /** * 处理文件(验证并上传) */ private async handleFiles(files: File[]): Promise { // 验证文件 const validationError = this.validateFiles(files); if (validationError) { this.uploadError.emit(validationError); return; } this.fileSelected.emit(files); // 开始上传 await this.uploadFiles(files); } /** * 验证文件 */ private validateFiles(files: File[]): string | null { if (files.length === 0) { return '请选择文件'; } // 检查文件类型 if (this.allowedTypes.length > 0) { const invalidFiles = files.filter(file => !this.validateFileType(file, this.allowedTypes) ); if (invalidFiles.length > 0) { return `不支持的文件类型: ${invalidFiles.map(f => f.name).join(', ')}`; } } // 检查文件大小 const oversizedFiles = files.filter(file => !this.validateFileSize(file, this.maxSize) ); if (oversizedFiles.length > 0) { return `文件大小超过限制 (${this.maxSize}MB): ${oversizedFiles.map(f => f.name).join(', ')}`; } return null; } /** * 验证文件类型 */ private validateFileType(file: File, allowedTypes: string[]): boolean { return allowedTypes.some(type => file.type.includes(type)); } /** * 验证文件大小 */ private validateFileSize(file: File, maxSizeInMB: number): boolean { const maxSizeInBytes = maxSizeInMB * 1024 * 1024; return file.size <= maxSizeInBytes; } /** * 上传文件 */ private async uploadFiles(files: File[]): Promise { if (!this.storage) { this.uploadError.emit('存储服务未初始化'); return; } this.isUploading = true; this.uploadProgress = 0; this.uploadedFiles = []; this.uploadStart.emit(files); try { const results: UploadResult[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; // 更新进度 const progress = ((i + 1) / files.length) * 100; this.uploadProgress = progress; this.uploadProgressEvent.emit({ completed: i + 1, total: files.length, currentFile: file.name }); try { // 使用 NovaStorage 上传文件 const uploaded: NovaFile = await this.storage.upload(file, { prefixKey: this.prefixKey, onProgress: (p) => { const fileProgress = (i / files.length) * 100 + (p.total.percent / files.length); this.uploadProgress = fileProgress; this.uploadProgressEvent.emit({ completed: i, total: files.length, currentFile: file.name }); } }); const result: UploadResult = { success: true, file: uploaded, url: uploaded.url }; results.push(result); console.log('✅ 文件上传成功:', uploaded.key, uploaded.url); } catch (error) { const result: UploadResult = { success: false, error: error instanceof Error ? error.message : '上传失败' }; results.push(result); console.error('❌ 文件上传失败:', file.name, error); } } this.uploadedFiles = results; this.uploadComplete.emit(results); // 检查是否有失败的上传 const failedUploads = results.filter(r => !r.success); if (failedUploads.length > 0) { const errorMessages = failedUploads.map(r => r.error).filter(Boolean); this.uploadError.emit(`部分文件上传失败: ${errorMessages.join(', ')}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '上传过程中发生错误'; this.uploadError.emit(errorMessage); } finally { this.isUploading = false; this.uploadProgress = 0; } } /** * 删除已上传的文件 */ removeUploadedFile(index: number): void { this.uploadedFiles.splice(index, 1); } /** * 格式化文件大小 */ formatFileSize(bytes: number): string { 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)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * 获取文件扩展名 */ getFileExtension(filename: string): string { return filename.split('.').pop()?.toLowerCase() || ''; } /** * 检查是否为图片文件 */ isImageFile(file: File): boolean { return file.type.startsWith('image/'); } /** * 生成预览URL */ generatePreviewUrl(file: File): string { if (this.isImageFile(file)) { return URL.createObjectURL(file); } return ''; } /** * 清空上传结果 */ clearResults(): void { this.uploadedFiles = []; this.uploadProgress = 0; } /** * 重置组件状态 */ reset(): void { this.isUploading = false; this.uploadProgress = 0; this.uploadedFiles = []; this.dragOver = false; if (this.fileInput) { this.fileInput.nativeElement.value = ''; } } }