project-space-deliverable.service.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import { Injectable } from '@angular/core';
  2. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  3. import { ProjectFileService } from './project-file.service';
  4. const Parse = FmodeParse.with('nova');
  5. /**
  6. * 空间交付物统计信息
  7. */
  8. export interface SpaceDeliverableInfo {
  9. /** 空间ID(Product ID) */
  10. spaceId: string;
  11. /** 空间名称 */
  12. spaceName: string;
  13. /** 空间类型 */
  14. spaceType: string;
  15. /** 交付物类型统计 */
  16. deliverableTypes: {
  17. /** 白模文件数量 */
  18. whiteModel: number;
  19. /** 软装文件数量 */
  20. softDecor: number;
  21. /** 渲染文件数量 */
  22. rendering: number;
  23. /** 后期文件数量 */
  24. postProcess: number;
  25. };
  26. /** 总文件数 */
  27. totalFiles: number;
  28. /** 是否已上传交付物 */
  29. hasDeliverables: boolean;
  30. /** 完成度(0-100) */
  31. completionRate: number;
  32. }
  33. /**
  34. * 🆕 阶段进度信息
  35. */
  36. export interface PhaseProgressInfo {
  37. /** 阶段名称 */
  38. phaseName: 'modeling' | 'softDecor' | 'rendering' | 'postProcessing';
  39. /** 阶段中文标签 */
  40. phaseLabel: string;
  41. /** 应完成空间数(有此类型交付物要求的空间数) */
  42. requiredSpaces: number;
  43. /** 已完成空间数(已上传此类型交付物的空间数) */
  44. completedSpaces: number;
  45. /** 完成率(0-100) */
  46. completionRate: number;
  47. /** 总文件数 */
  48. totalFiles: number;
  49. /** 未完成空间列表 */
  50. incompleteSpaces: Array<{
  51. spaceId: string;
  52. spaceName: string;
  53. assignee?: string; // 负责人
  54. }>;
  55. }
  56. /**
  57. * 项目空间与交付物统计信息
  58. */
  59. export interface ProjectSpaceDeliverableSummary {
  60. /** 项目ID */
  61. projectId: string;
  62. /** 项目名称 */
  63. projectName: string;
  64. /** 空间总数 */
  65. totalSpaces: number;
  66. /** 已上传交付物的空间数 */
  67. spacesWithDeliverables: number;
  68. /** 空间详细列表 */
  69. spaces: SpaceDeliverableInfo[];
  70. /** 总交付文件数 */
  71. totalDeliverableFiles: number;
  72. /** 各类型总计 */
  73. totalByType: {
  74. whiteModel: number;
  75. softDecor: number;
  76. rendering: number;
  77. postProcess: number;
  78. };
  79. /** 整体完成率(0-100) */
  80. overallCompletionRate: number;
  81. /** 🆕 各阶段进度详情 */
  82. phaseProgress: {
  83. modeling: PhaseProgressInfo;
  84. softDecor: PhaseProgressInfo;
  85. rendering: PhaseProgressInfo;
  86. postProcessing: PhaseProgressInfo;
  87. };
  88. }
  89. /**
  90. * 项目空间与交付物统计服务
  91. *
  92. * 功能:
  93. * 1. 计算项目中有多少个空间(基于Product表)
  94. * 2. 统计每个空间对应的交付物上传情况(基于ProjectFile表)
  95. * 3. 提供详细的统计数据,方便在不同地方使用(如时间轴、看板等)
  96. */
  97. @Injectable({
  98. providedIn: 'root'
  99. })
  100. export class ProjectSpaceDeliverableService {
  101. // ✅ 新增:内存缓存
  102. private summaryCache = new Map<string, ProjectSpaceDeliverableSummary>();
  103. constructor(
  104. private projectFileService: ProjectFileService
  105. ) {}
  106. /**
  107. * 🆕 注入统计数据(用于云函数预加载)
  108. */
  109. injectSummary(projectId: string, summary: ProjectSpaceDeliverableSummary): void {
  110. this.summaryCache.set(projectId, summary);
  111. }
  112. /**
  113. * 🆕 清除缓存
  114. */
  115. clearCache(projectId?: string): void {
  116. if (projectId) {
  117. this.summaryCache.delete(projectId);
  118. } else {
  119. this.summaryCache.clear();
  120. }
  121. }
  122. /**
  123. * 获取项目的空间与交付物统计信息
  124. *
  125. * @param projectId 项目ID
  126. * @param forceRefresh 是否强制刷新(忽略缓存)
  127. * @returns 项目空间与交付物统计摘要
  128. */
  129. async getProjectSpaceDeliverableSummary(
  130. projectId: string,
  131. forceRefresh: boolean = false
  132. ): Promise<ProjectSpaceDeliverableSummary> {
  133. // ✅ 检查缓存
  134. if (!forceRefresh && this.summaryCache.has(projectId)) {
  135. return this.summaryCache.get(projectId)!;
  136. }
  137. try {
  138. // 1. 获取项目信息
  139. const projectQuery = new Parse.Query('Project');
  140. const project = await projectQuery.get(projectId);
  141. const projectName = project.get('title') || project.get('name') || '未命名项目';
  142. // ✅ 应用方案:获取项目的所有空间(Product),包含负责人信息
  143. let products: FmodeObject[] = [];
  144. try {
  145. const productQuery = new Parse.Query('Product');
  146. productQuery.equalTo('project', project.toPointer());
  147. productQuery.ascending('createdAt');
  148. productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
  149. // ✅ 优化:如果 profile 是 Pointer,需要 include profile 的 name 字段
  150. // 注意:Parse 的 include 会自动包含关联对象的基本字段,但为了确保 name 字段可用,我们显式 include
  151. products = await productQuery.find();
  152. // ✅ 调试:检查 profile 字段是否正确加载
  153. // console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
  154. // products.forEach((product, index) => {
  155. // const profile = product.get('profile');
  156. // const productName = product.get('productName') || '未命名';
  157. // if (profile) {
  158. // const profileName = profile.get?.('name') || profile.name || '未知';
  159. // console.log(` ${index + 1}. ${productName}: profile = ${profileName}`);
  160. // } else {
  161. // console.log(` ${index + 1}. ${productName}: 无 profile`);
  162. // }
  163. // });
  164. // console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
  165. } catch (productError: any) {
  166. // ✅ 容错处理:Product 查询失败时,记录警告但继续处理
  167. console.warn(`⚠️ Product 查询失败,将使用空的空间列表:`, productError.message || productError.toString());
  168. // console.warn(`⚠️ Product 错误详情:`, productError);
  169. products = []; // 查询失败时使用空数组
  170. }
  171. // 3. 去重:按空间名称去重(忽略大小写和首尾空格)
  172. const uniqueProducts = this.deduplicateProducts(products);
  173. // console.log(`📊 去重后空间数:${uniqueProducts.length}`);
  174. // 4. 统计每个空间的交付物
  175. const spaceInfos: SpaceDeliverableInfo[] = [];
  176. let totalDeliverableFiles = 0;
  177. let spacesWithDeliverables = 0;
  178. const totalByType = {
  179. whiteModel: 0,
  180. softDecor: 0,
  181. rendering: 0,
  182. postProcess: 0
  183. };
  184. // ✅ 应用方案:优化性能,一次性查询所有交付文件
  185. const allDeliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
  186. stage: 'delivery'
  187. });
  188. // console.log(`📊 项目 ${projectName} 共有 ${allDeliveryFiles.length} 个交付文件`);
  189. for (const product of uniqueProducts) {
  190. // ✅ 应用方案:传入所有文件列表,避免重复查询
  191. const spaceInfo = await this.getSpaceDeliverableInfo(projectId, product, allDeliveryFiles);
  192. spaceInfos.push(spaceInfo);
  193. // 累加统计
  194. totalDeliverableFiles += spaceInfo.totalFiles;
  195. if (spaceInfo.hasDeliverables) {
  196. spacesWithDeliverables++;
  197. }
  198. totalByType.whiteModel += spaceInfo.deliverableTypes.whiteModel;
  199. totalByType.softDecor += spaceInfo.deliverableTypes.softDecor;
  200. totalByType.rendering += spaceInfo.deliverableTypes.rendering;
  201. totalByType.postProcess += spaceInfo.deliverableTypes.postProcess;
  202. }
  203. // 5. 计算整体完成率
  204. const overallCompletionRate = this.calculateOverallCompletionRate(spaceInfos);
  205. // ✅ 应用方案:计算各阶段进度详情,传入 products 以获取负责人信息
  206. const phaseProgress = this.calculatePhaseProgress(spaceInfos, project, uniqueProducts);
  207. const result = {
  208. projectId,
  209. projectName,
  210. totalSpaces: uniqueProducts.length,
  211. spacesWithDeliverables,
  212. spaces: spaceInfos,
  213. totalDeliverableFiles,
  214. totalByType,
  215. overallCompletionRate,
  216. phaseProgress
  217. };
  218. // ✅ 存入缓存
  219. this.summaryCache.set(projectId, result);
  220. return result;
  221. } catch (error) {
  222. console.error('获取项目空间交付物统计失败:', error);
  223. throw error;
  224. }
  225. }
  226. /**
  227. * 获取单个空间的交付物信息
  228. *
  229. * @param projectId 项目ID
  230. * @param product Product对象
  231. * @param allDeliveryFiles 所有交付文件列表(可选,用于性能优化)
  232. * @returns 空间交付物信息
  233. */
  234. private async getSpaceDeliverableInfo(
  235. projectId: string,
  236. product: FmodeObject,
  237. allDeliveryFiles?: FmodeObject[]
  238. ): Promise<SpaceDeliverableInfo> {
  239. const spaceId = product.id!;
  240. const spaceName = product.get('productName') || '未命名空间';
  241. const spaceType = product.get('productType') || 'other';
  242. // 定义交付物类型映射
  243. const deliveryTypeMap = {
  244. whiteModel: 'delivery_white_model',
  245. softDecor: 'delivery_soft_decor',
  246. rendering: 'delivery_rendering',
  247. postProcess: 'delivery_post_process'
  248. };
  249. // 统计各类型文件数量
  250. const deliverableTypes = {
  251. whiteModel: 0,
  252. softDecor: 0,
  253. rendering: 0,
  254. postProcess: 0
  255. };
  256. // ✅ 应用方案:优化文件查询逻辑,支持 deliveryType 字段
  257. // 如果已传入文件列表,直接使用;否则查询
  258. let deliveryFiles: FmodeObject[];
  259. if (allDeliveryFiles) {
  260. deliveryFiles = allDeliveryFiles;
  261. } else {
  262. deliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
  263. stage: 'delivery'
  264. });
  265. }
  266. // 定义交付类型映射(支持多种字段格式)
  267. const deliveryTypeMappings = {
  268. whiteModel: {
  269. fileType: 'delivery_white_model',
  270. deliveryType: ['white_model', 'delivery_white_model', 'whiteModel']
  271. },
  272. softDecor: {
  273. fileType: 'delivery_soft_decor',
  274. deliveryType: ['soft_decor', 'delivery_soft_decor', 'softDecor']
  275. },
  276. rendering: {
  277. fileType: 'delivery_rendering',
  278. deliveryType: ['rendering', 'delivery_rendering']
  279. },
  280. postProcess: {
  281. fileType: 'delivery_post_process',
  282. deliveryType: ['post_process', 'delivery_post_process', 'postProcess']
  283. }
  284. };
  285. // 统计各类型文件数量
  286. for (const [key, mapping] of Object.entries(deliveryTypeMappings)) {
  287. // 过滤当前空间的该类型文件
  288. const spaceFiles = deliveryFiles.filter(file => {
  289. const data = file.get('data') || {};
  290. const fileType = file.get('fileType') || '';
  291. const deliveryType = data.deliveryType || '';
  292. const uploadStage = data.uploadStage || '';
  293. // ✅ 应用方案:优先使用 data.spaceId,其次使用 data.productId
  294. const isSpaceMatch = data.spaceId === spaceId || data.productId === spaceId;
  295. // ✅ 应用方案:支持多种字段格式匹配
  296. const isTypeMatch =
  297. fileType === mapping.fileType ||
  298. mapping.deliveryType.includes(deliveryType) ||
  299. mapping.deliveryType.includes(fileType);
  300. // ✅ 应用方案:确认是交付阶段文件(推荐检查 uploadStage)
  301. const isDeliveryStage = uploadStage === 'delivery' || !uploadStage || fileType.includes('delivery');
  302. return isSpaceMatch && isTypeMatch && isDeliveryStage;
  303. });
  304. deliverableTypes[key as keyof typeof deliverableTypes] = spaceFiles.length;
  305. }
  306. // 计算总文件数
  307. const totalFiles = Object.values(deliverableTypes).reduce((sum, count) => sum + count, 0);
  308. // 判断是否已上传交付物
  309. const hasDeliverables = totalFiles > 0;
  310. // 计算完成度(假设每种类型至少需要1个文件才算完成)
  311. const completedTypes = Object.values(deliverableTypes).filter(count => count > 0).length;
  312. const completionRate = Math.round((completedTypes / 4) * 100);
  313. return {
  314. spaceId,
  315. spaceName,
  316. spaceType,
  317. deliverableTypes,
  318. totalFiles,
  319. hasDeliverables,
  320. completionRate
  321. };
  322. }
  323. /**
  324. * 去重Product列表(按名称去重)
  325. *
  326. * @param products Product对象数组
  327. * @returns 去重后的Product数组
  328. */
  329. private deduplicateProducts(products: FmodeObject[]): FmodeObject[] {
  330. const seen = new Set<string>();
  331. const unique: FmodeObject[] = [];
  332. for (const product of products) {
  333. const name = (product.get('productName') || '').trim().toLowerCase();
  334. if (!seen.has(name) && name) {
  335. seen.add(name);
  336. unique.push(product);
  337. }
  338. }
  339. return unique;
  340. }
  341. /**
  342. * 计算整体完成率
  343. *
  344. * @param spaceInfos 空间信息列表
  345. * @returns 完成率(0-100)
  346. */
  347. private calculateOverallCompletionRate(spaceInfos: SpaceDeliverableInfo[]): number {
  348. if (spaceInfos.length === 0) return 0;
  349. const totalCompletionRate = spaceInfos.reduce(
  350. (sum, space) => sum + space.completionRate,
  351. 0
  352. );
  353. return Math.round(totalCompletionRate / spaceInfos.length);
  354. }
  355. /**
  356. * ✅ 应用方案:计算各阶段进度详情
  357. *
  358. * @param spaceInfos 空间信息列表
  359. * @param project 项目对象
  360. * @param products Product对象数组(可选,用于获取空间负责人)
  361. * @returns 各阶段进度信息
  362. */
  363. private calculatePhaseProgress(
  364. spaceInfos: SpaceDeliverableInfo[],
  365. project: FmodeObject,
  366. products?: FmodeObject[]
  367. ): ProjectSpaceDeliverableSummary['phaseProgress'] {
  368. // 获取项目阶段截止信息中的负责人
  369. const projectData = project.get('data') || {};
  370. const phaseDeadlines = projectData.phaseDeadlines || {};
  371. // ✅ 🆕 获取 designerAssignmentStats 统计数据(主要数据源)
  372. const projectDate = project.get('date') || {};
  373. const designerAssignmentStats = projectDate.designerAssignmentStats || {};
  374. const projectLeader = designerAssignmentStats.projectLeader || null;
  375. const teamMembers = designerAssignmentStats.teamMembers || [];
  376. const crossTeamCollaborators = designerAssignmentStats.crossTeamCollaborators || [];
  377. // 阶段映射:交付物类型 -> 阶段名称
  378. const phaseMap = {
  379. modeling: {
  380. key: 'whiteModel' as const,
  381. label: '建模',
  382. phaseName: 'modeling' as const
  383. },
  384. softDecor: {
  385. key: 'softDecor' as const,
  386. label: '软装',
  387. phaseName: 'softDecor' as const
  388. },
  389. rendering: {
  390. key: 'rendering' as const,
  391. label: '渲染',
  392. phaseName: 'rendering' as const
  393. },
  394. postProcessing: {
  395. key: 'postProcess' as const,
  396. label: '后期',
  397. phaseName: 'postProcessing' as const
  398. }
  399. };
  400. const result: any = {};
  401. // ✅ 🆕 构建空间ID到负责人姓名的映射(优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee)
  402. const spaceAssigneeMap = new Map<string, string>();
  403. // 优先级1:从 designerAssignmentStats 获取空间分配信息(最准确)
  404. // 1.1 项目负责人的空间分配
  405. if (projectLeader && projectLeader.assignedSpaces && Array.isArray(projectLeader.assignedSpaces)) {
  406. projectLeader.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
  407. if (space.id && projectLeader.name) {
  408. spaceAssigneeMap.set(space.id, projectLeader.name);
  409. // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${projectLeader.name} (项目负责人)`);
  410. }
  411. });
  412. }
  413. // 1.2 团队成员的空间分配
  414. teamMembers.forEach((member: { id: string; name: string; isProjectLeader?: boolean; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
  415. if (member.assignedSpaces && Array.isArray(member.assignedSpaces) && member.name) {
  416. member.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
  417. if (space.id && !spaceAssigneeMap.has(space.id)) {
  418. // 如果该空间还没有分配负责人,则使用当前成员
  419. spaceAssigneeMap.set(space.id, member.name);
  420. // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${member.name}${member.isProjectLeader ? ' (项目负责人)' : ''}`);
  421. }
  422. });
  423. }
  424. });
  425. // 1.3 跨组合作者的空间分配
  426. crossTeamCollaborators.forEach((collaborator: { id: string; name: string; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
  427. if (collaborator.assignedSpaces && Array.isArray(collaborator.assignedSpaces) && collaborator.name) {
  428. collaborator.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
  429. if (space.id && !spaceAssigneeMap.has(space.id)) {
  430. spaceAssigneeMap.set(space.id, collaborator.name);
  431. // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${collaborator.name} (跨组合作)`);
  432. }
  433. });
  434. }
  435. });
  436. // 优先级2:从 Product.profile 获取空间负责人(如果 designerAssignmentStats 中没有)
  437. if (products) {
  438. products.forEach(product => {
  439. const spaceId = product.id!;
  440. // 如果该空间还没有分配负责人,才尝试使用 Product.profile
  441. if (!spaceAssigneeMap.has(spaceId)) {
  442. const profile = product.get('profile');
  443. if (profile) {
  444. // ✅ 优化:支持多种方式获取设计师姓名
  445. let profileName: string | undefined;
  446. // 方式1:profile 是 Parse Object,使用 get('name')
  447. if (profile.get && typeof profile.get === 'function') {
  448. profileName = profile.get('name') || profile.get('username') || '';
  449. }
  450. // 方式2:profile 是普通对象,直接访问 name 属性
  451. else if (profile.name) {
  452. profileName = profile.name;
  453. }
  454. // 方式3:profile 是字符串(ID),需要查询(暂时跳过,性能考虑)
  455. else if (typeof profile === 'string') {
  456. // 可以后续实现查询逻辑
  457. profileName = undefined;
  458. }
  459. if (profileName) {
  460. spaceAssigneeMap.set(spaceId, profileName);
  461. // console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
  462. } else {
  463. // console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
  464. }
  465. }
  466. }
  467. });
  468. }
  469. // 计算每个阶段的进度
  470. Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
  471. const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
  472. let completedSpaces = 0;
  473. let totalFiles = 0;
  474. const incompleteSpaces: Array<{
  475. spaceId: string;
  476. spaceName: string;
  477. assignee?: string;
  478. }> = [];
  479. // ✅ 优先级3:获取阶段负责人信息(作为最后备选)
  480. const phaseInfo = phaseDeadlines[phaseKey];
  481. const assignee = phaseInfo?.assignee;
  482. let phaseAssigneeName: string | undefined;
  483. if (assignee) {
  484. // 如果 assignee 是对象(Parse Pointer)
  485. if (assignee.objectId) {
  486. // 为了性能,暂时不实时查询,但保留接口
  487. phaseAssigneeName = undefined; // 可以后续实现查询逻辑
  488. }
  489. // 如果 assignee 是字符串(ID)
  490. else if (typeof assignee === 'string') {
  491. phaseAssigneeName = undefined; // 可以后续实现查询逻辑
  492. }
  493. // 如果 assignee 是对象且包含 name 字段
  494. else if (assignee.name) {
  495. phaseAssigneeName = assignee.name;
  496. }
  497. }
  498. spaceInfos.forEach(space => {
  499. const fileCount = space.deliverableTypes[phaseConfig.key];
  500. totalFiles += fileCount;
  501. if (fileCount > 0) {
  502. completedSpaces++;
  503. } else {
  504. // ✅ 应用方案:未完成空间列表,按优先级获取负责人
  505. // 优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee
  506. let spaceAssignee = spaceAssigneeMap.get(space.spaceId);
  507. // 如果找不到,尝试使用阶段负责人
  508. if (!spaceAssignee) {
  509. spaceAssignee = phaseAssigneeName;
  510. }
  511. // 如果还是没有,设置为"未分配"
  512. if (!spaceAssignee) {
  513. spaceAssignee = '未分配';
  514. }
  515. // console.log(`📊 未完成空间: ${space.spaceName} (ID: ${space.spaceId}), 负责人: ${spaceAssignee}`);
  516. incompleteSpaces.push({
  517. spaceId: space.spaceId,
  518. spaceName: space.spaceName,
  519. assignee: spaceAssignee
  520. });
  521. }
  522. });
  523. const completionRate = requiredSpaces > 0
  524. ? Math.round((completedSpaces / requiredSpaces) * 100)
  525. : 0;
  526. result[phaseKey] = {
  527. phaseName: phaseConfig.phaseName,
  528. phaseLabel: phaseConfig.label,
  529. requiredSpaces,
  530. completedSpaces,
  531. completionRate,
  532. totalFiles,
  533. incompleteSpaces
  534. };
  535. });
  536. return result;
  537. }
  538. /**
  539. * 检查项目是否所有空间都已上传交付物
  540. *
  541. * @param projectId 项目ID
  542. * @returns 是否全部完成
  543. */
  544. async isAllSpacesDelivered(projectId: string): Promise<boolean> {
  545. try {
  546. const summary = await this.getProjectSpaceDeliverableSummary(projectId);
  547. return summary.spacesWithDeliverables === summary.totalSpaces && summary.totalSpaces > 0;
  548. } catch (error) {
  549. console.error('检查项目交付完成状态失败:', error);
  550. return false;
  551. }
  552. }
  553. /**
  554. * 获取项目未完成空间列表
  555. *
  556. * @param projectId 项目ID
  557. * @returns 未完成空间的名称列表
  558. */
  559. async getIncompleteSpaces(projectId: string): Promise<string[]> {
  560. try {
  561. const summary = await this.getProjectSpaceDeliverableSummary(projectId);
  562. return summary.spaces
  563. .filter(space => !space.hasDeliverables)
  564. .map(space => space.spaceName);
  565. } catch (error) {
  566. console.error('获取未完成空间列表失败:', error);
  567. return [];
  568. }
  569. }
  570. /**
  571. * 获取项目交付进度百分比
  572. *
  573. * @param projectId 项目ID
  574. * @returns 进度百分比(0-100)
  575. */
  576. async getProjectDeliveryProgress(projectId: string): Promise<number> {
  577. try {
  578. const summary = await this.getProjectSpaceDeliverableSummary(projectId);
  579. return summary.overallCompletionRate;
  580. } catch (error) {
  581. console.error('获取项目交付进度失败:', error);
  582. return 0;
  583. }
  584. }
  585. /**
  586. * 获取空间类型显示名称
  587. *
  588. * @param spaceType 空间类型
  589. * @returns 显示名称
  590. */
  591. getSpaceTypeName(spaceType: string): string {
  592. const nameMap: Record<string, string> = {
  593. 'living_room': '客厅',
  594. 'bedroom': '卧室',
  595. 'kitchen': '厨房',
  596. 'bathroom': '卫生间',
  597. 'dining_room': '餐厅',
  598. 'study': '书房',
  599. 'balcony': '阳台',
  600. 'corridor': '走廊',
  601. 'storage': '储物间',
  602. 'entrance': '玄关',
  603. 'other': '其他'
  604. };
  605. return nameMap[spaceType] || '其他';
  606. }
  607. /**
  608. * 格式化统计摘要为文本
  609. *
  610. * @param summary 统计摘要
  611. * @returns 格式化的文本
  612. */
  613. formatSummaryText(summary: ProjectSpaceDeliverableSummary): string {
  614. const lines = [
  615. `项目:${summary.projectName}`,
  616. `空间总数:${summary.totalSpaces}`,
  617. `已完成空间:${summary.spacesWithDeliverables}/${summary.totalSpaces}`,
  618. `总文件数:${summary.totalDeliverableFiles}`,
  619. ` - 白模:${summary.totalByType.whiteModel}`,
  620. ` - 软装:${summary.totalByType.softDecor}`,
  621. ` - 渲染:${summary.totalByType.rendering}`,
  622. ` - 后期:${summary.totalByType.postProcess}`,
  623. `完成率:${summary.overallCompletionRate}%`
  624. ];
  625. return lines.join('\n');
  626. }
  627. /**
  628. * 获取项目交付状态标签
  629. *
  630. * @param completionRate 完成率
  631. * @returns 状态标签
  632. */
  633. getDeliveryStatusLabel(completionRate: number): string {
  634. if (completionRate === 0) return '未开始';
  635. if (completionRate < 25) return '刚开始';
  636. if (completionRate < 50) return '进行中';
  637. if (completionRate < 75) return '接近完成';
  638. if (completionRate < 100) return '即将完成';
  639. return '已完成';
  640. }
  641. /**
  642. * 获取项目交付状态颜色
  643. *
  644. * @param completionRate 完成率
  645. * @returns 颜色类名或颜色值
  646. */
  647. getDeliveryStatusColor(completionRate: number): string {
  648. if (completionRate === 0) return '#94a3b8'; // 灰色
  649. if (completionRate < 25) return '#fbbf24'; // 黄色
  650. if (completionRate < 50) return '#fb923c'; // 橙色
  651. if (completionRate < 75) return '#60a5fa'; // 蓝色
  652. if (completionRate < 100) return '#818cf8'; // 紫色
  653. return '#34d399'; // 绿色
  654. }
  655. }