upload.component.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { Component, EventEmitter, Input, Output, ViewChild, ElementRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { NovaStorage, NovaFile } from 'fmode-ng/core';
  4. export interface UploadResult {
  5. success: boolean;
  6. file?: NovaFile;
  7. url?: string;
  8. error?: string;
  9. }
  10. @Component({
  11. selector: 'app-upload-component',
  12. standalone: true,
  13. imports: [CommonModule],
  14. templateUrl: './upload.component.html',
  15. styleUrls: ['./upload.component.scss']
  16. })
  17. export class UploadComponent {
  18. @Input() accept: string = '*/*'; // 接受的文件类型
  19. @Input() multiple: boolean = false; // 是否支持多文件上传
  20. @Input() maxSize: number = 10; // 最大文件大小(MB)
  21. @Input() allowedTypes: string[] = []; // 允许的文件类型
  22. @Input() prefixKey: string = ''; // 文件存储前缀
  23. @Input() disabled: boolean = false; // 是否禁用
  24. @Input() showPreview: boolean = false; // 是否显示预览
  25. @Input() compressImages: boolean = true; // 是否压缩图片
  26. @Output() fileSelected = new EventEmitter<File[]>(); // 文件选择事件
  27. @Output() uploadStart = new EventEmitter<File[]>(); // 上传开始事件
  28. @Output() uploadProgressEvent = new EventEmitter<{ completed: number; total: number; currentFile: string }>(); // 上传进度事件
  29. @Output() uploadComplete = new EventEmitter<UploadResult[]>(); // 上传完成事件
  30. @Output() uploadError = new EventEmitter<string>(); // 上传错误事件
  31. @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
  32. isUploading: boolean = false;
  33. uploadProgress: number = 0;
  34. uploadedFiles: UploadResult[] = [];
  35. dragOver: boolean = false;
  36. private storage: NovaStorage | null = null;
  37. constructor() {
  38. this.initStorage();
  39. }
  40. // 初始化 NovaStorage
  41. private async initStorage(): Promise<void> {
  42. try {
  43. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  44. this.storage = await NovaStorage.withCid(cid);
  45. console.log('✅ NovaStorage 初始化成功, cid:', cid);
  46. } catch (error) {
  47. console.error('❌ NovaStorage 初始化失败:', error);
  48. }
  49. }
  50. /**
  51. * 触发文件选择
  52. */
  53. triggerFileSelect(): void {
  54. if (!this.disabled) {
  55. this.fileInput.nativeElement.click();
  56. }
  57. }
  58. /**
  59. * 处理文件选择
  60. */
  61. onFileSelect(event: Event): void {
  62. const target = event.target as HTMLInputElement;
  63. const files = Array.from(target.files || []);
  64. if (files.length > 0) {
  65. this.handleFiles(files);
  66. }
  67. // 清空input值,允许重复选择同一文件
  68. target.value = '';
  69. }
  70. /**
  71. * 处理拖拽进入
  72. */
  73. onDragOver(event: DragEvent): void {
  74. event.preventDefault();
  75. event.stopPropagation();
  76. if (!this.disabled) {
  77. this.dragOver = true;
  78. }
  79. }
  80. /**
  81. * 处理拖拽离开
  82. */
  83. onDragLeave(event: DragEvent): void {
  84. event.preventDefault();
  85. event.stopPropagation();
  86. this.dragOver = false;
  87. }
  88. /**
  89. * 处理文件拖拽放下
  90. */
  91. onDrop(event: DragEvent): void {
  92. event.preventDefault();
  93. event.stopPropagation();
  94. this.dragOver = false;
  95. if (this.disabled) {
  96. return;
  97. }
  98. const files = Array.from(event.dataTransfer?.files || []);
  99. if (files.length > 0) {
  100. this.handleFiles(files);
  101. }
  102. }
  103. /**
  104. * 处理文件(验证并上传)
  105. */
  106. private async handleFiles(files: File[]): Promise<void> {
  107. // 验证文件
  108. const validationError = this.validateFiles(files);
  109. if (validationError) {
  110. this.uploadError.emit(validationError);
  111. return;
  112. }
  113. this.fileSelected.emit(files);
  114. // 开始上传
  115. await this.uploadFiles(files);
  116. }
  117. /**
  118. * 验证文件
  119. */
  120. private validateFiles(files: File[]): string | null {
  121. if (files.length === 0) {
  122. return '请选择文件';
  123. }
  124. // 检查文件类型
  125. if (this.allowedTypes.length > 0) {
  126. const invalidFiles = files.filter(file =>
  127. !this.validateFileType(file, this.allowedTypes)
  128. );
  129. if (invalidFiles.length > 0) {
  130. return `不支持的文件类型: ${invalidFiles.map(f => f.name).join(', ')}`;
  131. }
  132. }
  133. // 检查文件大小
  134. const oversizedFiles = files.filter(file =>
  135. !this.validateFileSize(file, this.maxSize)
  136. );
  137. if (oversizedFiles.length > 0) {
  138. return `文件大小超过限制 (${this.maxSize}MB): ${oversizedFiles.map(f => f.name).join(', ')}`;
  139. }
  140. return null;
  141. }
  142. /**
  143. * 验证文件类型
  144. */
  145. private validateFileType(file: File, allowedTypes: string[]): boolean {
  146. return allowedTypes.some(type => file.type.includes(type));
  147. }
  148. /**
  149. * 验证文件大小
  150. */
  151. private validateFileSize(file: File, maxSizeInMB: number): boolean {
  152. const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
  153. return file.size <= maxSizeInBytes;
  154. }
  155. /**
  156. * 上传文件
  157. */
  158. private async uploadFiles(files: File[]): Promise<void> {
  159. if (!this.storage) {
  160. this.uploadError.emit('存储服务未初始化');
  161. return;
  162. }
  163. this.isUploading = true;
  164. this.uploadProgress = 0;
  165. this.uploadedFiles = [];
  166. this.uploadStart.emit(files);
  167. try {
  168. const results: UploadResult[] = [];
  169. for (let i = 0; i < files.length; i++) {
  170. const file = files[i];
  171. // 更新进度
  172. const progress = ((i + 1) / files.length) * 100;
  173. this.uploadProgress = progress;
  174. this.uploadProgressEvent.emit({
  175. completed: i + 1,
  176. total: files.length,
  177. currentFile: file.name
  178. });
  179. try {
  180. // 使用 NovaStorage 上传文件
  181. const uploaded: NovaFile = await this.storage.upload(file, {
  182. prefixKey: this.prefixKey,
  183. onProgress: (p) => {
  184. const fileProgress = (i / files.length) * 100 + (p.total.percent / files.length);
  185. this.uploadProgress = fileProgress;
  186. this.uploadProgressEvent.emit({
  187. completed: i,
  188. total: files.length,
  189. currentFile: file.name
  190. });
  191. }
  192. });
  193. const result: UploadResult = {
  194. success: true,
  195. file: uploaded,
  196. url: uploaded.url
  197. };
  198. results.push(result);
  199. console.log('✅ 文件上传成功:', uploaded.key, uploaded.url);
  200. } catch (error) {
  201. const result: UploadResult = {
  202. success: false,
  203. error: error instanceof Error ? error.message : '上传失败'
  204. };
  205. results.push(result);
  206. console.error('❌ 文件上传失败:', file.name, error);
  207. }
  208. }
  209. this.uploadedFiles = results;
  210. this.uploadComplete.emit(results);
  211. // 检查是否有失败的上传
  212. const failedUploads = results.filter(r => !r.success);
  213. if (failedUploads.length > 0) {
  214. const errorMessages = failedUploads.map(r => r.error).filter(Boolean);
  215. this.uploadError.emit(`部分文件上传失败: ${errorMessages.join(', ')}`);
  216. }
  217. } catch (error) {
  218. const errorMessage = error instanceof Error ? error.message : '上传过程中发生错误';
  219. this.uploadError.emit(errorMessage);
  220. } finally {
  221. this.isUploading = false;
  222. this.uploadProgress = 0;
  223. }
  224. }
  225. /**
  226. * 删除已上传的文件
  227. */
  228. removeUploadedFile(index: number): void {
  229. this.uploadedFiles.splice(index, 1);
  230. }
  231. /**
  232. * 格式化文件大小
  233. */
  234. formatFileSize(bytes: number): string {
  235. if (bytes === 0) return '0 Bytes';
  236. const k = 1024;
  237. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  238. const i = Math.floor(Math.log(bytes) / Math.log(k));
  239. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  240. }
  241. /**
  242. * 获取文件扩展名
  243. */
  244. getFileExtension(filename: string): string {
  245. return filename.split('.').pop()?.toLowerCase() || '';
  246. }
  247. /**
  248. * 检查是否为图片文件
  249. */
  250. isImageFile(file: File): boolean {
  251. return file.type.startsWith('image/');
  252. }
  253. /**
  254. * 生成预览URL
  255. */
  256. generatePreviewUrl(file: File): string {
  257. if (this.isImageFile(file)) {
  258. return URL.createObjectURL(file);
  259. }
  260. return '';
  261. }
  262. /**
  263. * 清空上传结果
  264. */
  265. clearResults(): void {
  266. this.uploadedFiles = [];
  267. this.uploadProgress = 0;
  268. }
  269. /**
  270. * 重置组件状态
  271. */
  272. reset(): void {
  273. this.isUploading = false;
  274. this.uploadProgress = 0;
  275. this.uploadedFiles = [];
  276. this.dragOver = false;
  277. if (this.fileInput) {
  278. this.fileInput.nativeElement.value = '';
  279. }
  280. }
  281. }