|
|
@@ -132,28 +132,36 @@ export class ProjectSpaceDeliverableService {
|
|
|
const projectName = project.get('title') || project.get('name') || '未命名项目';
|
|
|
|
|
|
// ✅ 应用方案:获取项目的所有空间(Product),包含负责人信息
|
|
|
- const productQuery = new Parse.Query('Product');
|
|
|
- productQuery.equalTo('project', project.toPointer());
|
|
|
- productQuery.ascending('createdAt');
|
|
|
- productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
|
|
|
- // ✅ 优化:如果 profile 是 Pointer,需要 include profile 的 name 字段
|
|
|
- // 注意:Parse 的 include 会自动包含关联对象的基本字段,但为了确保 name 字段可用,我们显式 include
|
|
|
- const products = await productQuery.find();
|
|
|
-
|
|
|
- // ✅ 调试:检查 profile 字段是否正确加载
|
|
|
- console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
|
|
|
- products.forEach((product, index) => {
|
|
|
- const profile = product.get('profile');
|
|
|
- const productName = product.get('productName') || '未命名';
|
|
|
- if (profile) {
|
|
|
- const profileName = profile.get?.('name') || profile.name || '未知';
|
|
|
- console.log(` ${index + 1}. ${productName}: profile = ${profileName}`);
|
|
|
- } else {
|
|
|
- console.log(` ${index + 1}. ${productName}: 无 profile`);
|
|
|
- }
|
|
|
- });
|
|
|
+ let products: FmodeObject[] = [];
|
|
|
+ try {
|
|
|
+ const productQuery = new Parse.Query('Product');
|
|
|
+ productQuery.equalTo('project', project.toPointer());
|
|
|
+ productQuery.ascending('createdAt');
|
|
|
+ productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
|
|
|
+ // ✅ 优化:如果 profile 是 Pointer,需要 include profile 的 name 字段
|
|
|
+ // 注意:Parse 的 include 会自动包含关联对象的基本字段,但为了确保 name 字段可用,我们显式 include
|
|
|
+ products = await productQuery.find();
|
|
|
+
|
|
|
+ // ✅ 调试:检查 profile 字段是否正确加载
|
|
|
+ console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
|
|
|
+ products.forEach((product, index) => {
|
|
|
+ const profile = product.get('profile');
|
|
|
+ const productName = product.get('productName') || '未命名';
|
|
|
+ if (profile) {
|
|
|
+ const profileName = profile.get?.('name') || profile.name || '未知';
|
|
|
+ console.log(` ${index + 1}. ${productName}: profile = ${profileName}`);
|
|
|
+ } else {
|
|
|
+ console.log(` ${index + 1}. ${productName}: 无 profile`);
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
|
|
|
+ console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
|
|
|
+ } catch (productError: any) {
|
|
|
+ // ✅ 容错处理:Product 查询失败时,记录警告但继续处理
|
|
|
+ console.warn(`⚠️ Product 查询失败,将使用空的空间列表:`, productError.message || productError.toString());
|
|
|
+ console.warn(`⚠️ Product 错误详情:`, productError);
|
|
|
+ products = []; // 查询失败时使用空数组
|
|
|
+ }
|
|
|
|
|
|
// 3. 去重:按空间名称去重(忽略大小写和首尾空格)
|
|
|
const uniqueProducts = this.deduplicateProducts(products);
|
|
|
@@ -384,6 +392,13 @@ export class ProjectSpaceDeliverableService {
|
|
|
// 获取项目阶段截止信息中的负责人
|
|
|
const projectData = project.get('data') || {};
|
|
|
const phaseDeadlines = projectData.phaseDeadlines || {};
|
|
|
+
|
|
|
+ // ✅ 🆕 获取 designerAssignmentStats 统计数据(主要数据源)
|
|
|
+ const projectDate = project.get('date') || {};
|
|
|
+ const designerAssignmentStats = projectDate.designerAssignmentStats || {};
|
|
|
+ const projectLeader = designerAssignmentStats.projectLeader || null;
|
|
|
+ const teamMembers = designerAssignmentStats.teamMembers || [];
|
|
|
+ const crossTeamCollaborators = designerAssignmentStats.crossTeamCollaborators || [];
|
|
|
|
|
|
// 阶段映射:交付物类型 -> 阶段名称
|
|
|
const phaseMap = {
|
|
|
@@ -411,43 +426,51 @@ export class ProjectSpaceDeliverableService {
|
|
|
|
|
|
const result: any = {};
|
|
|
|
|
|
- // 计算每个阶段的进度
|
|
|
- Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
|
|
|
- const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
|
|
|
- let completedSpaces = 0;
|
|
|
- let totalFiles = 0;
|
|
|
- const incompleteSpaces: Array<{
|
|
|
- spaceId: string;
|
|
|
- spaceName: string;
|
|
|
- assignee?: string;
|
|
|
- }> = [];
|
|
|
-
|
|
|
- // ✅ 应用方案:获取阶段负责人信息
|
|
|
- const phaseInfo = phaseDeadlines[phaseKey];
|
|
|
- const assignee = phaseInfo?.assignee;
|
|
|
- let assigneeName: string | undefined;
|
|
|
-
|
|
|
- if (assignee) {
|
|
|
- // 如果 assignee 是对象(Parse Pointer)
|
|
|
- if (assignee.objectId) {
|
|
|
- // 为了性能,暂时不实时查询,但保留接口
|
|
|
- assigneeName = undefined; // 可以后续实现查询逻辑
|
|
|
- }
|
|
|
- // 如果 assignee 是字符串(ID)
|
|
|
- else if (typeof assignee === 'string') {
|
|
|
- assigneeName = undefined; // 可以后续实现查询逻辑
|
|
|
- }
|
|
|
- // 如果 assignee 是对象且包含 name 字段
|
|
|
- else if (assignee.name) {
|
|
|
- assigneeName = assignee.name;
|
|
|
+ // ✅ 🆕 构建空间ID到负责人姓名的映射(优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee)
|
|
|
+ const spaceAssigneeMap = new Map<string, string>();
|
|
|
+
|
|
|
+ // 优先级1:从 designerAssignmentStats 获取空间分配信息(最准确)
|
|
|
+ // 1.1 项目负责人的空间分配
|
|
|
+ if (projectLeader && projectLeader.assignedSpaces && Array.isArray(projectLeader.assignedSpaces)) {
|
|
|
+ projectLeader.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
|
|
|
+ if (space.id && projectLeader.name) {
|
|
|
+ spaceAssigneeMap.set(space.id, projectLeader.name);
|
|
|
+ console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${projectLeader.name} (项目负责人)`);
|
|
|
}
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1.2 团队成员的空间分配
|
|
|
+ teamMembers.forEach((member: { id: string; name: string; isProjectLeader?: boolean; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
|
|
|
+ if (member.assignedSpaces && Array.isArray(member.assignedSpaces) && member.name) {
|
|
|
+ member.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
|
|
|
+ if (space.id && !spaceAssigneeMap.has(space.id)) {
|
|
|
+ // 如果该空间还没有分配负责人,则使用当前成员
|
|
|
+ spaceAssigneeMap.set(space.id, member.name);
|
|
|
+ console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${member.name}${member.isProjectLeader ? ' (项目负责人)' : ''}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
-
|
|
|
- // ✅ 应用方案:为每个空间查找负责人
|
|
|
- // 优先使用空间负责人(Product.profile),其次使用阶段负责人
|
|
|
- const spaceAssigneeMap = new Map<string, string>();
|
|
|
- if (products) {
|
|
|
- products.forEach(product => {
|
|
|
+ });
|
|
|
+
|
|
|
+ // 1.3 跨组合作者的空间分配
|
|
|
+ crossTeamCollaborators.forEach((collaborator: { id: string; name: string; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
|
|
|
+ if (collaborator.assignedSpaces && Array.isArray(collaborator.assignedSpaces) && collaborator.name) {
|
|
|
+ collaborator.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
|
|
|
+ if (space.id && !spaceAssigneeMap.has(space.id)) {
|
|
|
+ spaceAssigneeMap.set(space.id, collaborator.name);
|
|
|
+ console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${collaborator.name} (跨组合作)`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 优先级2:从 Product.profile 获取空间负责人(如果 designerAssignmentStats 中没有)
|
|
|
+ if (products) {
|
|
|
+ products.forEach(product => {
|
|
|
+ const spaceId = product.id!;
|
|
|
+ // 如果该空间还没有分配负责人,才尝试使用 Product.profile
|
|
|
+ if (!spaceAssigneeMap.has(spaceId)) {
|
|
|
const profile = product.get('profile');
|
|
|
if (profile) {
|
|
|
// ✅ 优化:支持多种方式获取设计师姓名
|
|
|
@@ -468,13 +491,46 @@ export class ProjectSpaceDeliverableService {
|
|
|
}
|
|
|
|
|
|
if (profileName) {
|
|
|
- spaceAssigneeMap.set(product.id!, profileName);
|
|
|
- console.log(`📊 空间 ${product.get('productName')} 的负责人: ${profileName}`);
|
|
|
+ spaceAssigneeMap.set(spaceId, profileName);
|
|
|
+ console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
|
|
|
} else {
|
|
|
- console.warn(`⚠️ 空间 ${product.get('productName')} 的负责人信息无法获取`, profile);
|
|
|
+ console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算每个阶段的进度
|
|
|
+ Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
|
|
|
+ const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
|
|
|
+ let completedSpaces = 0;
|
|
|
+ let totalFiles = 0;
|
|
|
+ const incompleteSpaces: Array<{
|
|
|
+ spaceId: string;
|
|
|
+ spaceName: string;
|
|
|
+ assignee?: string;
|
|
|
+ }> = [];
|
|
|
+
|
|
|
+ // ✅ 优先级3:获取阶段负责人信息(作为最后备选)
|
|
|
+ const phaseInfo = phaseDeadlines[phaseKey];
|
|
|
+ const assignee = phaseInfo?.assignee;
|
|
|
+ let phaseAssigneeName: string | undefined;
|
|
|
+
|
|
|
+ if (assignee) {
|
|
|
+ // 如果 assignee 是对象(Parse Pointer)
|
|
|
+ if (assignee.objectId) {
|
|
|
+ // 为了性能,暂时不实时查询,但保留接口
|
|
|
+ phaseAssigneeName = undefined; // 可以后续实现查询逻辑
|
|
|
+ }
|
|
|
+ // 如果 assignee 是字符串(ID)
|
|
|
+ else if (typeof assignee === 'string') {
|
|
|
+ phaseAssigneeName = undefined; // 可以后续实现查询逻辑
|
|
|
+ }
|
|
|
+ // 如果 assignee 是对象且包含 name 字段
|
|
|
+ else if (assignee.name) {
|
|
|
+ phaseAssigneeName = assignee.name;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
spaceInfos.forEach(space => {
|
|
|
@@ -484,13 +540,13 @@ export class ProjectSpaceDeliverableService {
|
|
|
if (fileCount > 0) {
|
|
|
completedSpaces++;
|
|
|
} else {
|
|
|
- // ✅ 应用方案:未完成空间列表,优先使用空间负责人,其次使用阶段负责人
|
|
|
- // 注意:space.spaceId 应该是 product.id,需要确保匹配
|
|
|
+ // ✅ 应用方案:未完成空间列表,按优先级获取负责人
|
|
|
+ // 优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee
|
|
|
let spaceAssignee = spaceAssigneeMap.get(space.spaceId);
|
|
|
|
|
|
// 如果找不到,尝试使用阶段负责人
|
|
|
if (!spaceAssignee) {
|
|
|
- spaceAssignee = assigneeName;
|
|
|
+ spaceAssignee = phaseAssigneeName;
|
|
|
}
|
|
|
|
|
|
// 如果还是没有,设置为"未分配"
|