| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- 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<File[]>(); // 文件选择事件
- @Output() uploadStart = new EventEmitter<File[]>(); // 上传开始事件
- @Output() uploadProgressEvent = new EventEmitter<{ completed: number; total: number; currentFile: string }>(); // 上传进度事件
- @Output() uploadComplete = new EventEmitter<UploadResult[]>(); // 上传完成事件
- @Output() uploadError = new EventEmitter<string>(); // 上传错误事件
- @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
- isUploading: boolean = false;
- uploadProgress: number = 0;
- uploadedFiles: UploadResult[] = [];
- dragOver: boolean = false;
- private storage: NovaStorage | null = null;
- constructor() {
- this.initStorage();
- }
- // 初始化 NovaStorage
- private async initStorage(): Promise<void> {
- 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<void> {
- // 验证文件
- 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<void> {
- 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 = '';
- }
- }
- }
|