upload.service.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import { Injectable } from '@angular/core';
  2. import { NovaUploadService as FmodeUploadService } from 'fmode-ng/storage';
  3. /**
  4. * 文件上传服务
  5. * 用于处理图片、视频、文档等文件的上传
  6. * 基于 fmode-ng 的 NovaUploadService
  7. */
  8. @Injectable({
  9. providedIn: 'root'
  10. })
  11. export class ProjectUploadService {
  12. constructor(private uploadServ: FmodeUploadService) {}
  13. /**
  14. * 上传单个文件
  15. * @param file File对象
  16. * @param options 上传选项
  17. * @returns 上传后的文件URL
  18. */
  19. async uploadFile(
  20. file: File,
  21. options?: {
  22. onProgress?: (progress: number) => void;
  23. compress?: boolean;
  24. maxWidth?: number;
  25. maxHeight?: number;
  26. }
  27. ): Promise<string> {
  28. try {
  29. let fileToUpload = file;
  30. // 如果是图片且需要压缩
  31. if (options?.compress && file.type.startsWith('image/')) {
  32. fileToUpload = await this.compressImage(file, {
  33. maxWidth: options.maxWidth || 1920,
  34. maxHeight: options.maxHeight || 1920,
  35. quality: 0.8
  36. });
  37. }
  38. // 使用 fmode-ng 的上传服务
  39. const fileResult = await this.uploadServ.upload(fileToUpload, (progress: any) => {
  40. if (options?.onProgress && progress?.percent) {
  41. options.onProgress(progress.percent);
  42. }
  43. });
  44. // 返回文件URL
  45. return fileResult.url || '';
  46. } catch (error: any) {
  47. console.error('文件上传失败:', error);
  48. throw new Error('文件上传失败: ' + (error?.message || '未知错误'));
  49. }
  50. }
  51. /**
  52. * 批量上传文件
  53. * @param files File数组
  54. * @param options 上传选项
  55. * @returns 上传后的文件URL数组
  56. */
  57. async uploadFiles(
  58. files: File[],
  59. options?: {
  60. onProgress?: (current: number, total: number) => void;
  61. compress?: boolean;
  62. }
  63. ): Promise<string[]> {
  64. const urls: string[] = [];
  65. const total = files.length;
  66. for (let i = 0; i < files.length; i++) {
  67. const url = await this.uploadFile(files[i], {
  68. compress: options?.compress
  69. });
  70. urls.push(url);
  71. options?.onProgress?.(i + 1, total);
  72. }
  73. return urls;
  74. }
  75. /**
  76. * 压缩图片
  77. * @param file 原始文件
  78. * @param options 压缩选项
  79. * @returns 压缩后的File对象
  80. */
  81. private compressImage(
  82. file: File,
  83. options: {
  84. maxWidth: number;
  85. maxHeight: number;
  86. quality: number;
  87. }
  88. ): Promise<File> {
  89. return new Promise((resolve, reject) => {
  90. const reader = new FileReader();
  91. reader.onload = (e) => {
  92. const img = new Image();
  93. img.onload = () => {
  94. const canvas = document.createElement('canvas');
  95. let { width, height } = img;
  96. // 计算压缩后的尺寸
  97. if (width > options.maxWidth || height > options.maxHeight) {
  98. const ratio = Math.min(
  99. options.maxWidth / width,
  100. options.maxHeight / height
  101. );
  102. width = width * ratio;
  103. height = height * ratio;
  104. }
  105. canvas.width = width;
  106. canvas.height = height;
  107. const ctx = canvas.getContext('2d');
  108. ctx?.drawImage(img, 0, 0, width, height);
  109. canvas.toBlob(
  110. (blob) => {
  111. if (blob) {
  112. const compressedFile = new File([blob], file.name, {
  113. type: file.type,
  114. lastModified: Date.now()
  115. });
  116. resolve(compressedFile);
  117. } else {
  118. reject(new Error('图片压缩失败'));
  119. }
  120. },
  121. file.type,
  122. options.quality
  123. );
  124. };
  125. img.onerror = () => reject(new Error('图片加载失败'));
  126. img.src = e.target?.result as string;
  127. };
  128. reader.onerror = () => reject(new Error('文件读取失败'));
  129. reader.readAsDataURL(file);
  130. });
  131. }
  132. /**
  133. * 生成缩略图
  134. * @param file 原始图片文件
  135. * @param size 缩略图尺寸
  136. * @returns 缩略图URL
  137. */
  138. async generateThumbnail(file: File, size: number = 200): Promise<string> {
  139. const thumbnailFile = await this.compressImage(file, {
  140. maxWidth: size,
  141. maxHeight: size,
  142. quality: 0.7
  143. });
  144. return await this.uploadFile(thumbnailFile);
  145. }
  146. /**
  147. * 从URL下载文件
  148. * @param url 文件URL
  149. * @param filename 保存的文件名
  150. */
  151. async downloadFile(url: string, filename: string): Promise<void> {
  152. try {
  153. const response = await fetch(url);
  154. const blob = await response.blob();
  155. const link = document.createElement('a');
  156. link.href = URL.createObjectURL(blob);
  157. link.download = filename;
  158. link.click();
  159. URL.revokeObjectURL(link.href);
  160. } catch (error) {
  161. console.error('文件下载失败:', error);
  162. throw new Error('文件下载失败');
  163. }
  164. }
  165. /**
  166. * 验证文件类型
  167. * @param file 文件
  168. * @param allowedTypes 允许的MIME类型数组
  169. * @returns 是否合法
  170. */
  171. validateFileType(file: File, allowedTypes: string[]): boolean {
  172. return allowedTypes.some(type => {
  173. if (type.endsWith('/*')) {
  174. const prefix = type.replace('/*', '');
  175. return file.type.startsWith(prefix);
  176. }
  177. return file.type === type;
  178. });
  179. }
  180. /**
  181. * 验证文件大小
  182. * @param file 文件
  183. * @param maxSizeMB 最大大小(MB)
  184. * @returns 是否合法
  185. */
  186. validateFileSize(file: File, maxSizeMB: number): boolean {
  187. const maxSizeBytes = maxSizeMB * 1024 * 1024;
  188. return file.size <= maxSizeBytes;
  189. }
  190. /**
  191. * 格式化文件大小
  192. * @param bytes 字节数
  193. * @returns 格式化后的字符串
  194. */
  195. formatFileSize(bytes: number): string {
  196. if (bytes < 1024) return bytes + ' B';
  197. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
  198. if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  199. return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
  200. }
  201. }