project-file.service.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { Injectable } from '@angular/core';
  2. import { NovaStorage, NovaFile } from 'fmode-ng/core';
  3. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  4. const Parse = FmodeParse.with('nova');
  5. @Injectable({
  6. providedIn: 'root'
  7. })
  8. export class ProjectFileService {
  9. /**
  10. * 上传项目文件并保存到ProjectFile表
  11. * @param file 要上传的文件
  12. * @param projectId 项目ID
  13. * @param fileType 文件类型
  14. * @param spaceId 空间ID(可选)
  15. * @param stage 项目阶段(可选)
  16. * @param additionalMetadata 额外元数据(可选)
  17. * @param onProgress 上传进度回调
  18. * @returns 上传后的NovaFile对象
  19. */
  20. async uploadProjectFile(
  21. file: File,
  22. projectId: string,
  23. fileType: string,
  24. spaceId?: string,
  25. stage?: string,
  26. additionalMetadata?: any,
  27. onProgress?: (progress: number) => void
  28. ): Promise<NovaFile> {
  29. try {
  30. // 获取公司ID
  31. const cid = localStorage.getItem('company');
  32. if (!cid) {
  33. throw new Error('公司ID未找到');
  34. }
  35. // 初始化存储
  36. const storage = await NovaStorage.withCid(cid);
  37. // 构建prefixKey
  38. let prefixKey = `project/${projectId}`;
  39. if (spaceId) {
  40. prefixKey += `/space/${spaceId}`;
  41. }
  42. if (stage) {
  43. prefixKey += `/stage/${stage}`;
  44. }
  45. // 上传文件
  46. const uploadedFile: NovaFile = await storage.upload(file, {
  47. prefixKey,
  48. onProgress: (progress: { total: { percent: number } }) => {
  49. if (onProgress) {
  50. onProgress(progress.total.percent);
  51. }
  52. }
  53. });
  54. // 保存到Attachment表
  55. await this.saveToAttachmentTable(uploadedFile, projectId, fileType, spaceId, stage, additionalMetadata);
  56. return uploadedFile;
  57. } catch (error) {
  58. console.error('项目文件上传失败:', error);
  59. throw error;
  60. }
  61. }
  62. /**
  63. * 保存文件信息到Attachment表
  64. */
  65. private async saveToAttachmentTable(
  66. file: NovaFile,
  67. projectId: string,
  68. fileType: string,
  69. spaceId?: string,
  70. stage?: string,
  71. additionalMetadata?: any
  72. ): Promise<FmodeObject> {
  73. const attachment = new Parse.Object('Attachment');
  74. // 设置基本字段
  75. attachment.set('size', file.size);
  76. attachment.set('url', file.url);
  77. attachment.set('name', file.name);
  78. attachment.set('mime', file.type);
  79. attachment.set('md5', file.md5);
  80. attachment.set('metadata', {
  81. ...file.metadata,
  82. projectId,
  83. fileType,
  84. spaceId,
  85. stage,
  86. ...additionalMetadata
  87. });
  88. // 设置关联关系
  89. const cid = localStorage.getItem('company');
  90. if (cid) {
  91. let company = new Parse.Object('Company');
  92. company.id = cid
  93. if (company) {
  94. attachment.set('company', company.toPointer());
  95. }
  96. }
  97. // 设置当前用户
  98. const currentUser = Parse.User.current();
  99. if (currentUser) {
  100. attachment.set('user', currentUser);
  101. }
  102. const savedAttachment = await attachment.save();
  103. return savedAttachment;
  104. }
  105. /**
  106. * 保存到ProjectFile表
  107. */
  108. async saveToProjectFile(
  109. attachment: FmodeObject,
  110. projectId: string,
  111. fileType: string,
  112. spaceId?: string,
  113. stage?: string
  114. ): Promise<FmodeObject> {
  115. const projectFile = new Parse.Object('ProjectFile');
  116. // 获取项目
  117. const projectQuery = new Parse.Query("Project");
  118. const project = await projectQuery.get(projectId);
  119. // 设置字段
  120. projectFile.set('project', project);
  121. projectFile.set('attach', attachment);
  122. projectFile.set('fileType', fileType);
  123. projectFile.set('fileUrl', attachment.get('url'));
  124. projectFile.set('fileName', attachment.get('name'));
  125. projectFile.set('fileSize', attachment.get('size'));
  126. if (stage) {
  127. projectFile.set('stage', stage);
  128. }
  129. // ✨ 增强:完整保存元数据到 ProjectFile.data,包括审批状态等
  130. const attachmentMetadata = attachment.get('metadata') || {};
  131. const deliverableId = attachmentMetadata.deliverableId;
  132. const data = {
  133. spaceId,
  134. uploadedAt: new Date(),
  135. fileType,
  136. deliverableId,
  137. // ✨ 保存所有元数据(包含 approvalStatus, uploadedByName, uploadedById 等)
  138. ...attachmentMetadata
  139. };
  140. projectFile.set('data', data);
  141. // 设置上传者
  142. const currentUser = Parse.User.current();
  143. if (currentUser) {
  144. projectFile.set('uploadedBy', currentUser);
  145. }
  146. const savedProjectFile = await projectFile.save();
  147. console.log('✅ ProjectFile已保存,data字段:', savedProjectFile.get('data'));
  148. return savedProjectFile;
  149. }
  150. /**
  151. * 删除项目文件
  152. */
  153. async deleteProjectFile(projectFileId: string): Promise<void> {
  154. try {
  155. // 删除ProjectFile记录
  156. const ProjectFile = new Parse.Object('ProjectFile');
  157. const query = new Parse.Query("ProjectFile");
  158. const projectFile = await query.get(projectFileId);
  159. // 删除Attachment记录
  160. const attachment = projectFile.get('attach');
  161. if (attachment) {
  162. await attachment.destroy();
  163. }
  164. // 删除ProjectFile记录
  165. await projectFile.destroy();
  166. } catch (error) {
  167. console.error('删除项目文件失败:', error);
  168. throw error;
  169. }
  170. }
  171. /**
  172. * 获取项目文件列表
  173. */
  174. async getProjectFiles(
  175. projectId: string,
  176. filters?: {
  177. fileType?: string;
  178. spaceId?: string;
  179. stage?: string;
  180. }
  181. ): Promise<FmodeObject[]> {
  182. try {
  183. const ProjectFile = new Parse.Object('ProjectFile');
  184. const query = new Parse.Query("ProjectFile");
  185. // 关联项目查询
  186. const Project = new Parse.Object('Project');
  187. const projectQuery = new Parse.Query("Project");
  188. projectQuery.equalTo('objectId', projectId);
  189. query.matchesQuery('project', projectQuery);
  190. query.include('attach', 'uploadedBy');
  191. query.descending('createdAt');
  192. // 应用过滤器
  193. if (filters?.fileType) {
  194. query.equalTo('fileType', filters.fileType);
  195. }
  196. if (filters?.stage) {
  197. query.equalTo('stage', filters.stage);
  198. }
  199. const results = await query.find();
  200. // 如果有空间ID过滤,从data中筛选
  201. if (filters?.spaceId) {
  202. return results.filter(result => {
  203. const data = result.get('data');
  204. return data?.spaceId === filters.spaceId;
  205. });
  206. }
  207. return results;
  208. } catch (error) {
  209. console.error('获取项目文件列表失败:', error);
  210. throw error;
  211. }
  212. }
  213. /**
  214. * 批量上传文件
  215. */
  216. async uploadMultipleFiles(
  217. files: File[],
  218. projectId: string,
  219. fileType: string,
  220. spaceId?: string,
  221. stage?: string,
  222. onProgress?: (fileIndex: number, progress: number) => void
  223. ): Promise<NovaFile[]> {
  224. const results: NovaFile[] = [];
  225. for (let i = 0; i < files.length; i++) {
  226. const file = files[i];
  227. try {
  228. const uploadedFile = await this.uploadProjectFile(
  229. file,
  230. projectId,
  231. fileType,
  232. spaceId,
  233. stage,
  234. undefined,
  235. (progress) => {
  236. if (onProgress) {
  237. onProgress(i, progress);
  238. }
  239. }
  240. );
  241. results.push(uploadedFile);
  242. } catch (error) {
  243. console.error(`文件 ${file.name} 上传失败:`, error);
  244. // 继续上传其他文件
  245. }
  246. }
  247. return results;
  248. }
  249. /**
  250. * 验证文件
  251. */
  252. validateFile(file: File, maxSize: number = 50 * 1024 * 1024, allowedTypes?: string[]): boolean {
  253. // 检查文件大小
  254. if (file.size > maxSize) {
  255. return false;
  256. }
  257. // 检查文件类型
  258. if (allowedTypes && !allowedTypes.includes(file.type)) {
  259. return false;
  260. }
  261. return true;
  262. }
  263. /**
  264. * 获取文件类型标签
  265. */
  266. getFileTypeLabel(fileType: string): string {
  267. const typeMap: Record<string, string> = {
  268. 'image/jpeg': '图片',
  269. 'image/png': '图片',
  270. 'image/gif': '图片',
  271. 'image/webp': '图片',
  272. 'video/mp4': '视频',
  273. 'video/mov': '视频',
  274. 'video/avi': '视频',
  275. 'application/pdf': 'PDF',
  276. 'application/msword': 'Word',
  277. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word',
  278. 'application/vnd.ms-excel': 'Excel',
  279. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel',
  280. 'application/vnd.ms-powerpoint': 'PPT',
  281. 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPT'
  282. };
  283. return typeMap[fileType] || '其他';
  284. }
  285. /**
  286. * 格式化文件大小
  287. */
  288. formatFileSize(bytes: number): string {
  289. if (bytes === 0) return '0 B';
  290. const k = 1024;
  291. const sizes = ['B', 'KB', 'MB', 'GB'];
  292. const i = Math.floor(Math.log(bytes) / Math.log(k));
  293. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  294. }
  295. /**
  296. * 上传文件并创建 Attachment 与 ProjectFile 记录,返回 ProjectFile
  297. */
  298. async uploadProjectFileWithRecord(
  299. file: File,
  300. projectId: string,
  301. fileType: string,
  302. spaceId?: string,
  303. stage?: string,
  304. additionalMetadata?: any,
  305. onProgress?: (progress: number) => void
  306. ): Promise<FmodeObject> {
  307. try {
  308. const cid = localStorage.getItem('company');
  309. if (!cid) {
  310. throw new Error('公司ID未找到');
  311. }
  312. const storage = await NovaStorage.withCid(cid);
  313. let prefixKey = `project/${projectId}`;
  314. if (spaceId) {
  315. prefixKey += `/space/${spaceId}`;
  316. }
  317. if (stage) {
  318. prefixKey += `/stage/${stage}`;
  319. }
  320. const uploadedFile = await storage.upload(file, {
  321. prefixKey,
  322. onProgress: (progress: { total: { percent: number } }) => {
  323. if (onProgress) {
  324. onProgress(progress.total.percent);
  325. }
  326. }
  327. });
  328. const attachment = await this.saveToAttachmentTable(
  329. uploadedFile,
  330. projectId,
  331. fileType,
  332. spaceId,
  333. stage,
  334. additionalMetadata
  335. );
  336. const projectFile = await this.saveToProjectFile(
  337. attachment,
  338. projectId,
  339. fileType,
  340. spaceId,
  341. stage
  342. );
  343. return projectFile;
  344. } catch (error) {
  345. console.error('上传并创建ProjectFile失败:', error);
  346. throw error;
  347. }
  348. }
  349. }