Преглед на файлове

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

徐福静0235668 преди 13 часа
родител
ревизия
f254d7bdaa

+ 378 - 0
CLOUD_FUNCTION_FINAL.js

@@ -0,0 +1,378 @@
+async function handler(request, response) {
+  console.log('🚀 执行高性能 SQL 统计 (完全体 - 最终修正版)...');
+  
+  try {
+    let companyId = 'cDL6R1hgSi';
+    if (request.company && request.company.id) companyId = request.company.id;
+    else if (request.params && request.params.companyId) companyId = request.params.companyId;
+    else if (request.body && request.body.companyId) companyId = request.body.companyId;
+
+    // --- SQL 定义 ---
+
+    const workloadSql = `
+      SELECT
+        u."objectId" as "id",
+        u."name",
+        COALESCE((u."data"->'tags'->'capacity'->>'weeklyProjects')::int, 3) as "weeklyCapacity",
+        COUNT(DISTINCT pt."project") as "projectCount",
+        COUNT(DISTINCT CASE WHEN p."deadline" < NOW() AND p."status" != '已完成' THEN p."objectId" END) as "overdueCount",
+        SUM(CASE 
+          WHEN p."status" = '已完成' THEN 0 
+          ELSE ((CASE WHEN p."data"->>'projectType' = 'hard' THEN 2.0 ELSE 1.0 END) * (CASE WHEN p."deadline" < NOW() THEN 1.5 ELSE 1.0 END)) 
+        END) as "weightedLoad"
+      FROM "Profile" u
+      LEFT JOIN "ProjectTeam" pt ON pt."profile" = u."objectId" AND pt."isDeleted" IS NOT TRUE
+      LEFT JOIN "Project" p ON pt."project" = p."objectId" AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+      WHERE u."company" = $1 AND u."roleName" = '组员' AND u."isDeleted" IS NOT TRUE
+      GROUP BY u."objectId", u."name", u."data"
+      ORDER BY "weightedLoad" DESC
+    `;
+
+    const projectsSql = `
+      SELECT
+        p."objectId" as "id",
+        p."title" as "name",
+        p."status",
+        p."currentStage",
+        p."deadline",
+        p."updatedAt",
+        p."createdAt",
+        p."data"->>'urgency' as "urgency",
+        p."data"->>'projectType' as "type",
+        p."data"->'phaseDeadlines' as "phaseDeadlines",
+        p."date" as "projectDate",
+        EXTRACT(DAY FROM (p."deadline" - NOW())) as "daysLeft",
+        (
+          SELECT string_agg(pr."name", ', ')
+          FROM "ProjectTeam" pt
+          JOIN "Profile" pr ON pt."profile" = pr."objectId"
+          WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
+        ) as "designerName",
+        (
+          SELECT array_agg(pt."profile")
+          FROM "ProjectTeam" pt
+          WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
+        ) as "designerIds"
+      FROM "Project" p
+      WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+      ORDER BY p."updatedAt" DESC
+      LIMIT 1000
+    `;
+
+    const spaceStatsSql = `
+      WITH ActiveProjects AS (
+          SELECT p."objectId" 
+          FROM "Project" p 
+          WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+          LIMIT 1000
+      ),
+      ProjectSpaces AS (
+          SELECT 
+              p."objectId" as "spaceId",
+              p."productName" as "spaceName",
+              p."productType" as "spaceType",
+              p."project" as "projectId"
+          FROM "Product" p
+          WHERE p."project" IN (SELECT "objectId" FROM ActiveProjects)
+            AND (p."isDeleted" IS NULL OR p."isDeleted" = false)
+      ),
+      Deliverables AS (
+          SELECT 
+              COALESCE(d."data"->>'spaceId', d."data"->>'productId') as "spaceId",
+              COUNT(*) as "fileCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_white_model' OR 
+                  d."data"->>'deliveryType' IN ('white_model', 'delivery_white_model') 
+                  THEN 1 ELSE 0 END) as "whiteModelCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_soft_decor' OR 
+                  d."data"->>'deliveryType' IN ('soft_decor', 'delivery_soft_decor') 
+                  THEN 1 ELSE 0 END) as "softDecorCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_rendering' OR 
+                  d."data"->>'deliveryType' IN ('rendering', 'delivery_rendering') 
+                  THEN 1 ELSE 0 END) as "renderingCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_post_process' OR 
+                  d."data"->>'deliveryType' IN ('post_process', 'delivery_post_process') 
+                  THEN 1 ELSE 0 END) as "postProcessCount"
+          FROM "ProjectFile" d
+          WHERE d."project" IN (SELECT "objectId" FROM ActiveProjects)
+            AND (d."isDeleted" IS NULL OR d."isDeleted" = false)
+            AND (
+                d."fileType" LIKE 'delivery_%' OR 
+                d."data"->>'uploadStage' = 'delivery'
+            )
+          GROUP BY COALESCE(d."data"->>'spaceId', d."data"->>'productId')
+      )
+      SELECT 
+          ps."projectId",
+          ps."spaceId",
+          ps."spaceName",
+          ps."spaceType",
+          COALESCE(d."fileCount", 0) as "totalFiles",
+          COALESCE(d."whiteModelCount", 0) as "whiteModel",
+          COALESCE(d."softDecorCount", 0) as "softDecor",
+          COALESCE(d."renderingCount", 0) as "rendering",
+          COALESCE(d."postProcessCount", 0) as "postProcess"
+      FROM ProjectSpaces ps
+      LEFT JOIN Deliverables d ON ps."spaceId" = d."spaceId"
+    `;
+
+    // 关键修正:通过 Project 表关联查询,获取更多字段
+    const issuesSql = `
+      SELECT
+        i."objectId" as "id",
+        i."title",
+        i."description",
+        i."priority",
+        i."issueType",
+        i."status",
+        i."dueDate",
+        i."createdAt",
+        i."updatedAt",
+        i."data",
+        p."objectId" as "projectId",
+        p."title" as "projectName",
+        c."name" as "creatorName",
+        a."name" as "assigneeName"
+      FROM "ProjectIssue" i
+      JOIN "Project" p ON i."project" = p."objectId"
+      LEFT JOIN "Profile" c ON i."creator" = c."objectId"
+      LEFT JOIN "Profile" a ON i."assignee" = a."objectId"
+      WHERE p."company" = $1 
+        AND (i."isDeleted" IS NULL OR i."isDeleted" = false)
+        AND i."status" IN ('待处理', '处理中')
+      ORDER BY i."updatedAt" DESC
+      LIMIT 50
+    `;
+
+    // --- 执行 SQL ---
+
+    const [workloadResult, projectsResult, spaceStatsResult, issuesResult] = await Promise.all([
+      Psql.query(workloadSql, [companyId]),
+      Psql.query(projectsSql, [companyId]),
+      Psql.query(spaceStatsSql, [companyId]),
+      Psql.query(issuesSql, [companyId])
+    ]);
+
+    // --- 格式化数据 ---
+
+    // 1. Workload
+    const workload = workloadResult.map(w => {
+      const capacity = w.weeklyCapacity || 3;
+      const load = parseFloat(w.weightedLoad || 0);
+      const loadRate = Math.round((load / capacity) * 100);
+      let status = 'idle';
+      if (loadRate > 80) status = 'overload';
+      else if (loadRate > 50) status = 'busy';
+      
+      return {
+        id: w.id,
+        name: w.name,
+        weeklyCapacity: capacity,
+        projectCount: parseInt(w.projectCount),
+        overdueCount: parseInt(w.overdueCount),
+        weightedLoad: load,
+        loadRate,
+        status
+      };
+    });
+
+    // 2. Projects
+    const spaceAssigneeMap = {};
+    const projects = projectsResult.map(p => {
+      if (p.projectDate && p.projectDate.designerAssignmentStats) {
+          const stats = p.projectDate.designerAssignmentStats;
+          if (stats.projectLeader && stats.projectLeader.assignedSpaces) {
+              stats.projectLeader.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = stats.projectLeader.name; });
+          }
+          if (Array.isArray(stats.teamMembers)) {
+              stats.teamMembers.forEach(member => {
+                  if (member.assignedSpaces && member.name) {
+                      member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
+                  }
+              });
+          }
+          if (Array.isArray(stats.crossTeamCollaborators)) {
+              stats.crossTeamCollaborators.forEach(member => {
+                  if (member.assignedSpaces && member.name) {
+                      member.assignedSpaces.forEach(s => { if (s.id) spaceAssigneeMap[s.id] = member.name; });
+                  }
+              });
+          }
+      }
+
+      let statusStr = 'normal';
+      const days = parseFloat(p.daysLeft);
+      if (days < 0) statusStr = 'overdue';
+      else if (days <= 3) statusStr = 'urgent';
+      
+      return {
+        id: p.id,
+        name: p.name,
+        status: p.status,
+        currentStage: p.currentStage,
+        deadline: p.deadline,
+        updatedAt: p.updatedAt,
+        createdAt: p.createdAt,
+        urgency: p.urgency,
+        type: p.type,
+        phaseDeadlines: p.phaseDeadlines || {},
+        daysLeft: Math.ceil(days), 
+        isOverdue: days < 0,
+        statusStr,
+        designerName: p.designerName || '待分配',
+        designerIds: p.designerIds || []
+      };
+    });
+
+    // 3. Space Stats (完全修复聚合逻辑)
+    const spaceStats = {};
+    
+    // 创建项目名称映射,确保能获取到 projectName
+    const projectNameMap = {};
+    projects.forEach(p => {
+      projectNameMap[p.id] = p.name;
+    });
+    
+    spaceStatsResult.forEach(row => {
+        if (!spaceStats[row.projectId]) {
+            spaceStats[row.projectId] = {
+                spaces: []
+            };
+        }
+        
+        // 计算单个空间的完成度
+        const hasFiles = parseInt(row.totalFiles) > 0;
+        let completion = 0;
+        if (hasFiles) {
+            if (parseInt(row.whiteModel) > 0) completion += 25;
+            if (parseInt(row.softDecor) > 0) completion += 25;
+            if (parseInt(row.rendering) > 0) completion += 25;
+            if (parseInt(row.postProcess) > 0) completion += 25;
+        }
+
+        const spaceInfo = {
+            spaceId: row.spaceId,
+            spaceName: row.spaceName,
+            spaceType: row.spaceType,
+            totalFiles: parseInt(row.totalFiles),
+            deliverableTypes: {
+                whiteModel: parseInt(row.whiteModel),
+                softDecor: parseInt(row.softDecor),
+                rendering: parseInt(row.rendering),
+                postProcess: parseInt(row.postProcess)
+            },
+            hasDeliverables: hasFiles,
+            completionRate: Math.min(100, completion)
+        };
+        
+        spaceStats[row.projectId].spaces.push(spaceInfo);
+    });
+
+    Object.keys(spaceStats).forEach(pid => {
+        const proj = spaceStats[pid];
+        const totalSpaces = proj.spaces.length;
+        
+        // 计算整体完成率
+        const sumCompletion = proj.spaces.reduce((sum, s) => sum + s.completionRate, 0);
+        const overallCompletionRate = totalSpaces > 0 ? Math.round(sumCompletion / totalSpaces) : 0;
+        
+        const calcPhaseDetails = (typeKey) => {
+            const spacesWithFile = proj.spaces.filter(s => s.deliverableTypes[typeKey] > 0);
+            const completedCount = spacesWithFile.length;
+            const rate = totalSpaces > 0 ? Math.round((completedCount / totalSpaces) * 100) : 0;
+            const fileCount = proj.spaces.reduce((sum, s) => sum + s.deliverableTypes[typeKey], 0);
+            
+            const incomplete = proj.spaces
+                .filter(s => s.deliverableTypes[typeKey] === 0)
+                .map(s => ({
+                    spaceName: s.spaceName,
+                    assignee: spaceAssigneeMap[s.spaceId] || '未分配',
+                    spaceId: s.spaceId
+                }));
+
+            return {
+                completionRate: rate,
+                completedSpaces: completedCount,
+                requiredSpaces: totalSpaces,
+                totalFiles: fileCount,
+                incompleteSpaces: incomplete
+            };
+        };
+
+        const phaseProgress = {
+            modeling: calcPhaseDetails('whiteModel'),
+            softDecor: calcPhaseDetails('softDecor'),
+            rendering: calcPhaseDetails('rendering'),
+            postProcessing: calcPhaseDetails('postProcess')
+        };
+        
+        // 关键:确保 projectName 和 totalByType 存在
+        spaceStats[pid] = {
+            projectId: pid,
+            projectName: projectNameMap[pid] || '未命名项目', 
+            totalSpaces,
+            spaces: proj.spaces,
+            totalDeliverableFiles: proj.spaces.reduce((sum, s) => sum + s.totalFiles, 0),
+            totalByType: {
+                whiteModel: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.whiteModel, 0),
+                softDecor: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.softDecor, 0),
+                rendering: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.rendering, 0),
+                postProcess: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.postProcess, 0)
+            },
+            overallCompletionRate,
+            phaseProgress
+        };
+    });
+
+    // 4. Issues (恢复完整字段)
+    const zh2enStatus = (status) => {
+      const map = {
+        '待处理': 'open',
+        '处理中': 'in_progress',
+        '已解决': 'resolved',
+        '已关闭': 'closed'
+      };
+      return map[status] || 'open';
+    };
+
+    const issues = issuesResult.map(row => ({
+      id: row.id,
+      title: row.title || (row.description ? row.description.slice(0, 40) : '未命名问题'),
+      description: row.description,
+      priority: row.priority || 'medium',
+      type: row.issueType || 'task',
+      status: zh2enStatus(row.status),
+      projectId: row.projectId || '',
+      projectName: row.projectName || '未知项目',
+      relatedSpace: row.data?.relatedSpace,
+      relatedStage: row.data?.relatedStage,
+      assigneeName: row.assigneeName || '未指派',
+      creatorName: row.creatorName || '未知',
+      createdAt: row.createdAt,
+      updatedAt: row.updatedAt,
+      dueDate: row.dueDate,
+      tags: row.data?.tags || []
+    }));
+
+    // 5. Stats
+    const stats = {
+      totalActive: projects.length,
+      overdueCount: projects.filter(p => p.isOverdue).length,
+      urgentCount: projects.filter(p => p.statusStr === 'urgent').length,
+      avgLoadRate: workload.length > 0 ? Math.round(workload.reduce((sum, w) => sum + w.loadRate, 0) / workload.length) : 0
+    };
+
+    response.json({ 
+      code: 200, 
+      success: true, 
+      data: { stats, workload, projects, spaceStats, issues } 
+    });
+
+  } catch (error) {
+    console.error('❌ SQL 执行失败:', error.message);
+    response.json({ code: 500, success: false, error: error.message });
+  }
+}

+ 385 - 0
CLOUD_FUNCTION_MIGRATION_GUIDE.md

@@ -0,0 +1,385 @@
+# 组长看板性能优化:云函数迁移指南 (终极修复版)
+
+## 更新说明
+- **修复数据冗余**:只查询 `phaseDeadlines`,不再返回巨大的 `data` 对象。
+- **修复前端报错**:补充了 `phaseProgress` 计算逻辑,确保前端进度条能正常显示。
+
+---
+
+## 云函数代码实现 (Node.js)
+
+请将以下代码完整覆盖到云函数 (ID: 8qJkylemKn) 中:
+
+```javascript
+async function handler(request, response) {
+  console.log('🚀 执行高性能 SQL 统计 (含空间交付物 V2)...');
+  
+  try {
+    // 1. 获取 companyId
+    let companyId = 'cDL6R1hgSi';
+    if (request.company && request.company.id) companyId = request.company.id;
+    else if (request.params && request.params.companyId) companyId = request.params.companyId;
+    else if (request.body && request.body.companyId) companyId = request.body.companyId;
+
+    // 2.1 定义 Workload SQL
+    const workloadSql = `
+      SELECT
+        u."objectId" as "id",
+        u."name",
+        COALESCE((u."data"->'tags'->'capacity'->>'weeklyProjects')::int, 3) as "weeklyCapacity",
+        COUNT(DISTINCT pt."project") as "projectCount",
+        COUNT(DISTINCT CASE WHEN p."deadline" < NOW() AND p."status" != '已完成' THEN p."objectId" END) as "overdueCount",
+        SUM(CASE 
+          WHEN p."status" = '已完成' THEN 0 
+          ELSE ((CASE WHEN p."data"->>'projectType' = 'hard' THEN 2.0 ELSE 1.0 END) * (CASE WHEN p."deadline" < NOW() THEN 1.5 ELSE 1.0 END)) 
+        END) as "weightedLoad"
+      FROM "Profile" u
+      LEFT JOIN "ProjectTeam" pt ON pt."profile" = u."objectId" AND pt."isDeleted" IS NOT TRUE
+      LEFT JOIN "Project" p ON pt."project" = p."objectId" AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+      WHERE u."company" = $1 AND u."roleName" = '组员' AND u."isDeleted" IS NOT TRUE
+      GROUP BY u."objectId", u."name", u."data"
+      ORDER BY "weightedLoad" DESC
+    `;
+
+    // 2.2 定义 Projects SQL (✅ 优化:获取 phaseDeadlines 和 date 用于计算负责人)
+    const projectsSql = `
+      SELECT
+        p."objectId" as "id",
+        p."title" as "name",
+        p."status",
+        p."currentStage",
+        p."deadline",
+        p."updatedAt",
+        p."createdAt",
+        p."data"->>'urgency' as "urgency",
+        p."data"->>'projectType' as "type",
+        p."data"->'phaseDeadlines' as "phaseDeadlines",
+        p."date" as "projectDate", -- ✅ 获取 date 字段以解析 designerAssignmentStats
+        EXTRACT(DAY FROM (p."deadline" - NOW())) as "daysLeft",
+        (
+          SELECT string_agg(pr."name", ', ')
+          FROM "ProjectTeam" pt
+          JOIN "Profile" pr ON pt."profile" = pr."objectId"
+          WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
+        ) as "designerName",
+        (
+          SELECT array_agg(pt."profile")
+          FROM "ProjectTeam" pt
+          WHERE pt."project" = p."objectId" AND pt."isDeleted" IS NOT TRUE
+        ) as "designerIds"
+      FROM "Project" p
+      WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+      ORDER BY p."updatedAt" DESC
+      LIMIT 1000
+    `;
+
+    // 2.3 定义 Space Stats SQL
+    const spaceStatsSql = `
+      WITH ActiveProjects AS (
+          SELECT p."objectId" 
+          FROM "Project" p 
+          WHERE p."company" = $1 AND p."isDeleted" IS NOT TRUE AND p."status" != '已完成'
+          LIMIT 1000
+      ),
+      ProjectSpaces AS (
+          SELECT 
+              p."objectId" as "spaceId",
+              p."productName" as "spaceName",
+              p."productType" as "spaceType",
+              p."project" as "projectId"
+          FROM "Product" p
+          WHERE p."project" IN (SELECT "objectId" FROM ActiveProjects)
+            AND (p."isDeleted" IS NULL OR p."isDeleted" = false)
+      ),
+      Deliverables AS (
+          SELECT 
+              COALESCE(d."data"->>'spaceId', d."data"->>'productId') as "spaceId",
+              COUNT(*) as "fileCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_white_model' OR 
+                  d."data"->>'deliveryType' IN ('white_model', 'delivery_white_model') 
+                  THEN 1 ELSE 0 END) as "whiteModelCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_soft_decor' OR 
+                  d."data"->>'deliveryType' IN ('soft_decor', 'delivery_soft_decor') 
+                  THEN 1 ELSE 0 END) as "softDecorCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_rendering' OR 
+                  d."data"->>'deliveryType' IN ('rendering', 'delivery_rendering') 
+                  THEN 1 ELSE 0 END) as "renderingCount",
+              SUM(CASE WHEN 
+                  d."fileType" = 'delivery_post_process' OR 
+                  d."data"->>'deliveryType' IN ('post_process', 'delivery_post_process') 
+                  THEN 1 ELSE 0 END) as "postProcessCount"
+          FROM "ProjectFile" d
+          WHERE d."project" IN (SELECT "objectId" FROM ActiveProjects)
+            AND (d."isDeleted" IS NULL OR d."isDeleted" = false)
+            AND (
+                d."fileType" LIKE 'delivery_%' OR 
+                d."data"->>'uploadStage' = 'delivery'
+            )
+          GROUP BY COALESCE(d."data"->>'spaceId', d."data"->>'productId')
+      )
+      SELECT 
+          ps."projectId",
+          ps."spaceId",
+          ps."spaceName",
+          ps."spaceType",
+          COALESCE(d."fileCount", 0) as "totalFiles",
+          COALESCE(d."whiteModelCount", 0) as "whiteModel",
+          COALESCE(d."softDecorCount", 0) as "softDecor",
+          COALESCE(d."renderingCount", 0) as "rendering",
+          COALESCE(d."postProcessCount", 0) as "postProcess"
+      FROM ProjectSpaces ps
+      LEFT JOIN Deliverables d ON ps."spaceId" = d."spaceId"
+    `;
+
+    // 2.4 定义 Issues SQL (🆕 新增:查询待办事项)
+    const issuesSql = `
+      SELECT
+        i."objectId" as "id",
+        i."title",
+        i."description",
+        i."priority",
+        i."issueType",
+        i."status",
+        i."dueDate",
+        i."createdAt",
+        i."updatedAt",
+        i."data",
+        p."objectId" as "projectId",
+        p."title" as "projectName",
+        c."name" as "creatorName",
+        a."name" as "assigneeName"
+      FROM "ProjectIssue" i
+      LEFT JOIN "Project" p ON i."project" = p."objectId"
+      LEFT JOIN "Profile" c ON i."creator" = c."objectId"
+      LEFT JOIN "Profile" a ON i."assignee" = a."objectId"
+      WHERE i."company" = $1 
+        AND (i."isDeleted" IS NULL OR i."isDeleted" = false)
+        AND i."status" IN ('待处理', '处理中')
+      ORDER BY i."updatedAt" DESC
+      LIMIT 50
+    `;
+
+    // 3. 并行执行 SQL
+    const [workloadResult, projectsResult, spaceStatsResult, issuesResult] = await Promise.all([
+      Psql.query(workloadSql, [companyId]),
+      Psql.query(projectsSql, [companyId]),
+      Psql.query(spaceStatsSql, [companyId]),
+      Psql.query(issuesSql, [companyId])
+    ]);
+
+    console.log(`✅ SQL 完成: ${workloadResult.length} 人, ${projectsResult.length} 项目, ${spaceStatsResult.length} 空间记录, ${issuesResult.length} 待办`);
+
+    // ... (4.1 和 4.2 保持不变)
+    
+    // 4.4 格式化 Issues
+    const zh2enStatus = (status) => {
+      const map = {
+        '待处理': 'open',
+        '处理中': 'in_progress',
+        '已解决': 'resolved',
+        '已关闭': 'closed'
+      };
+      return map[status] || 'open';
+    };
+
+    const issues = issuesResult.map(row => ({
+      id: row.id,
+      title: row.title || (row.description ? row.description.slice(0, 40) : '未命名问题'),
+      description: row.description,
+      priority: row.priority || 'medium',
+      type: row.issueType || 'task',
+      status: zh2enStatus(row.status),
+      projectId: row.projectId || '',
+      projectName: row.projectName || '未知项目',
+      relatedSpace: row.data?.relatedSpace,
+      relatedStage: row.data?.relatedStage,
+      assigneeName: row.assigneeName || '未指派',
+      creatorName: row.creatorName || '未知',
+      createdAt: row.createdAt,
+    }));
+
+    // 4.2 格式化 Projects & 构建负责人映射
+    const spaceAssigneeMap = {}; // spaceId -> designerName
+
+    const projects = projectsResult.map(p => {
+      // ... (前面的格式化逻辑)
+      
+      // 解析 designerAssignmentStats 构建空间负责人映射
+      // ✅ 解析 designerAssignmentStats 构建空间负责人映射
+      if (p.projectDate && p.projectDate.designerAssignmentStats) {
+          const stats = p.projectDate.designerAssignmentStats;
+          
+          // 1. 项目负责人
+          if (stats.projectLeader && stats.projectLeader.assignedSpaces) {
+              stats.projectLeader.assignedSpaces.forEach(s => {
+                  if (s.id) spaceAssigneeMap[s.id] = stats.projectLeader.name;
+              });
+          }
+          
+          // 2. 团队成员
+          if (Array.isArray(stats.teamMembers)) {
+              stats.teamMembers.forEach(member => {
+                  if (member.assignedSpaces && member.name) {
+                      member.assignedSpaces.forEach(s => {
+                          if (s.id) spaceAssigneeMap[s.id] = member.name;
+                      });
+                  }
+              });
+          }
+          
+          // 3. 跨组协作
+          if (Array.isArray(stats.crossTeamCollaborators)) {
+              stats.crossTeamCollaborators.forEach(member => {
+                  if (member.assignedSpaces && member.name) {
+                      member.assignedSpaces.forEach(s => {
+                          if (s.id) spaceAssigneeMap[s.id] = member.name;
+                      });
+                  }
+              });
+          }
+      }
+
+      let statusStr = 'normal';
+      // ... (保持原有的 return 逻辑)
+      return {
+          // ...
+          id: p.id,
+          name: p.name,
+          status: p.status,
+          currentStage: p.currentStage,
+          deadline: p.deadline,
+          updatedAt: p.updatedAt,
+          createdAt: p.createdAt,
+          urgency: p.urgency,
+          type: p.type,
+          phaseDeadlines: p.phaseDeadlines || {},
+          daysLeft: Math.ceil(parseFloat(p.daysLeft)),
+          isOverdue: parseFloat(p.daysLeft) < 0,
+          statusStr,
+          designerName: p.designerName || '待分配',
+          designerIds: p.designerIds || []
+      };
+    });
+
+    // 4.3 聚合 Space Stats (✅ 增加 phaseProgress 计算)
+    const spaceStats = {};
+    
+    // 创建 projectId 到 projectName 的映射
+    const projectNameMap = {};
+    projects.forEach(p => {
+      projectNameMap[p.id] = p.name;
+    });
+    
+    spaceStatsResult.forEach(row => {
+        if (!spaceStats[row.projectId]) {
+            spaceStats[row.projectId] = {
+                spaces: []
+            };
+        }
+        
+        const spaceInfo = {
+            spaceId: row.spaceId,
+            spaceName: row.spaceName,
+            spaceType: row.spaceType,
+            totalFiles: parseInt(row.totalFiles),
+            deliverableTypes: {
+                whiteModel: parseInt(row.whiteModel),
+                softDecor: parseInt(row.softDecor),
+                rendering: parseInt(row.rendering),
+                postProcess: parseInt(row.postProcess)
+            },
+            hasDeliverables: parseInt(row.totalFiles) > 0,
+            completionRate: parseInt(row.totalFiles) > 0 ? 
+                Math.min(100, (
+                    (parseInt(row.whiteModel) > 0 ? 25 : 0) +
+                    (parseInt(row.softDecor) > 0 ? 25 : 0) +
+                    (parseInt(row.rendering) > 0 ? 25 : 0) +
+                    (parseInt(row.postProcess) > 0 ? 25 : 0)
+                )) : 0
+        };
+        
+        spaceStats[row.projectId].spaces.push(spaceInfo);
+    });
+
+    Object.keys(spaceStats).forEach(pid => {
+        const proj = spaceStats[pid];
+        const totalSpaces = proj.spaces.length;
+        
+        // 计算整体完成率
+        const sumCompletion = proj.spaces.reduce((sum, s) => sum + s.completionRate, 0);
+        const overallCompletionRate = totalSpaces > 0 ? Math.round(sumCompletion / totalSpaces) : 0;
+        
+        // ✅ 关键修复:计算各阶段详细进度 (M/S/R/P)
+        const calcPhaseDetails = (typeKey) => {
+            const spacesWithFile = proj.spaces.filter(s => s.deliverableTypes[typeKey] > 0);
+            const completedCount = spacesWithFile.length;
+            const rate = totalSpaces > 0 ? Math.round((completedCount / totalSpaces) * 100) : 0;
+            const fileCount = proj.spaces.reduce((sum, s) => sum + s.deliverableTypes[typeKey], 0);
+            
+            // 获取未完成空间
+            const incomplete = proj.spaces
+                .filter(s => s.deliverableTypes[typeKey] === 0)
+                .map(s => ({
+                    spaceName: s.spaceName,
+                    // ✅ 使用映射获取负责人,如果没有则显示未分配
+                    assignee: spaceAssigneeMap[s.spaceId] || '未分配'
+                }));
+
+            return {
+                completionRate: rate,
+                completedSpaces: completedCount,
+                requiredSpaces: totalSpaces,
+                totalFiles: fileCount,
+                incompleteSpaces: incomplete
+            };
+        };
+
+        const phaseProgress = {
+            modeling: calcPhaseDetails('whiteModel'),
+            softDecor: calcPhaseDetails('softDecor'),
+            rendering: calcPhaseDetails('rendering'),
+            postProcessing: calcPhaseDetails('postProcess')
+        };
+        
+        spaceStats[pid] = {
+            projectId: pid,
+            projectName: projectNameMap[pid] || '未命名项目', 
+            totalSpaces,
+            spaces: proj.spaces,
+            totalDeliverableFiles: proj.spaces.reduce((sum, s) => sum + s.totalFiles, 0),
+            totalByType: proj.totalByType,
+            overallCompletionRate,
+            phaseProgress // ✅ 现在包含完整详情
+        };
+        
+        // 补充 totalByType 到 spaceStats[pid]
+        spaceStats[pid].totalByType = {
+            whiteModel: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.whiteModel, 0),
+            softDecor: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.softDecor, 0),
+            rendering: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.rendering, 0),
+            postProcess: proj.spaces.reduce((sum, s) => sum + s.deliverableTypes.postProcess, 0)
+        };
+    });
+
+    const stats = {
+      totalActive: projects.length,
+      overdueCount: projects.filter(p => p.isOverdue).length,
+      urgentCount: projects.filter(p => p.statusStr === 'urgent').length,
+      avgLoadRate: workload.length > 0 ? Math.round(workload.reduce((sum, w) => sum + w.loadRate, 0) / workload.length) : 0
+    };
+
+    response.json({ 
+      code: 200, 
+      success: true, 
+      data: { stats, workload, projects, spaceStats } 
+    });
+
+  } catch (error) {
+    console.error('❌ SQL 执行失败:', error.message);
+    response.json({ code: 500, success: false, error: error.message });
+  }
+}
+```

+ 22 - 4
package-lock.json

@@ -56,7 +56,7 @@
         "echarts": "^6.0.0",
         "esdk-obs-browserjs": "^3.25.6",
         "eventemitter3": "^5.0.1",
-        "fmode-ng": "^0.0.227",
+        "fmode-ng": "^0.0.237",
         "highlight.js": "^11.11.1",
         "ionicons": "^8.0.13",
         "jquery": "^3.7.1",
@@ -8271,6 +8271,16 @@
       "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
       "license": "MIT"
     },
+    "node_modules/dingtalk-jsapi": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/dingtalk-jsapi/-/dingtalk-jsapi-3.2.2.tgz",
+      "integrity": "sha512-FglWzvdMJosOkX8k2MVavX3MiDIE6vAmAHqVE0bvXRfh3El4DVqACQRw1UIyKi3UloUS07XQXGRAG6S12eehgg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "promise-polyfill": "^7.1.0"
+      }
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -9565,9 +9575,9 @@
       "license": "ISC"
     },
     "node_modules/fmode-ng": {
-      "version": "0.0.227",
-      "resolved": "https://registry.npmjs.org/fmode-ng/-/fmode-ng-0.0.227.tgz",
-      "integrity": "sha512-6ehhqUpcYuSIRifeZ7rO1biNNd3sfvWQAvpFhse0rhj9iXxQ2hFUFZT6RnfzbWTDqNvvlY2s9IGY7zsNwLEmlg==",
+      "version": "0.0.237",
+      "resolved": "https://registry.npmjs.org/fmode-ng/-/fmode-ng-0.0.237.tgz",
+      "integrity": "sha512-pwNdCVQWNZ3/iyuPdejcPtHxkQk3pHlVdAPeKrbIT0lQyrnDY6oblOoJte/WFOTHyNWOPPleMvx7HJvAmLwPzg==",
       "license": "COPYRIGHT © 未来飞马 未来全栈 www.fmode.cn All RIGHTS RESERVED",
       "dependencies": {
         "tslib": "^2.3.0"
@@ -9611,6 +9621,7 @@
         "@types/spark-md5": "^3.0.4",
         "@wecom/jssdk": "^2.2.4",
         "codemirror": "^6.0.2",
+        "dingtalk-jsapi": "^3.2.0",
         "esdk-obs-browserjs": "^3.23.5",
         "eventemitter3": "^5.0.1",
         "highlight.js": "^11.0.0",
@@ -13785,6 +13796,13 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/promise-polyfill": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-7.1.2.tgz",
+      "integrity": "sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/promise-retry": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",

+ 1 - 1
package.json

@@ -74,7 +74,7 @@
     "echarts": "^6.0.0",
     "esdk-obs-browserjs": "^3.25.6",
     "eventemitter3": "^5.0.1",
-    "fmode-ng": "^0.0.227",
+    "fmode-ng": "^0.0.237",
     "highlight.js": "^11.11.1",
     "ionicons": "^8.0.13",
     "jquery": "^3.7.1",

+ 2 - 1
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.ts

@@ -283,7 +283,8 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
           
           if (projects && projects.length > 0) {
             listHtml = projects.slice(0, 6).map((p: any) => {
-              const stage = p.currentStage || '进行中';
+              // ✅ 修复:优先使用 stageName (中文),否则使用 currentStage (可能为英文Key)
+              const stage = p.stageName || p.currentStage || '进行中';
               return `
                <div style="display:flex;justify-content:space-between;font-size:12px;margin-top:4px;color:#64748b;">
                  <span style="max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${p.name}</span>

+ 226 - 5
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -24,6 +24,7 @@ import { DesignerWorkloadService } from '../services/designer-workload.service';
 import { DashboardNavigationHelper } from '../services/dashboard-navigation.helper';
 import { TodoTaskService } from '../services/todo-task.service';
 import { DashboardFilterService } from '../services/dashboard-filter.service';
+import { DashboardDataService } from '../services/dashboard-data.service';
 import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model';
 import { PROJECT_STAGES, CORE_PHASES } from './dashboard.constants';
 
@@ -172,24 +173,244 @@ export class Dashboard implements OnInit, OnDestroy {
     private designerWorkloadService: DesignerWorkloadService,
     private navigationHelper: DashboardNavigationHelper,
     private todoTaskService: TodoTaskService,
-    private dashboardFilterService: DashboardFilterService
+    private dashboardFilterService: DashboardFilterService,
+    private dashboardDataService: DashboardDataService
   ) {}
 
   async ngOnInit(): Promise<void> {
     // 新增:加载用户Profile信息
     await this.loadUserProfile();
     
-    await this.loadProjects();
-    await this.loadDesigners();
+    // 🔥 优先尝试从云函数加载数据
+    const cloudData = await this.dashboardDataService.getTeamLeaderDataFromCloud();
+    
+    if (cloudData) {
+      // 如果云函数成功,使用云端聚合数据
+      this.processCloudData(cloudData);
+    } else {
+      // 降级方案:使用原有慢速加载
+      console.warn('⚠️ 云函数加载失败或无数据,回退到本地聚合模式');
+      await this.loadProjects();
+      await this.loadDesigners();
+      // 加载待办任务(从问题板块)- 只有在云函数失败时才单独加载
+      await this.loadTodoTasksFromIssues();
+    }
     
-    // 加载待办任务(从问题板块)
-    await this.loadTodoTasksFromIssues();
     // 🆕 计算紧急事件
     this.calculateUrgentEvents();
     // 启动自动刷新
     this.startAutoRefresh();
   }
 
+  /**
+   * 处理云函数返回的数据
+   */
+  private processCloudData(data: any): void {
+    try {
+      const { projects, workload, spaceStats, issues } = data;
+      
+      // 1. 转换项目数据
+      this.projects = projects.map((p: any) => this.transformCloudProject(p));
+      
+      // 2. 更新设计师工作量数据
+      // 注意:为了兼容现有逻辑,我们同时更新 designerWorkloadMap 和 realDesigners
+      this.updateDesignerDataFromCloud(workload, projects, spaceStats);
+      
+      // 3. 应用筛选和构建索引
+      this.buildSearchIndexes();
+      this.applyFilters();
+      
+      // 4. 处理待办任务 (Issues)
+      if (issues && Array.isArray(issues)) {
+        this.todoTasksFromIssues = issues;
+        this.loadingTodoTasks = false;
+        console.log(`✅ 云端待办事项加载成功,共 ${issues.length} 条`);
+      } else {
+        // 如果云端没有返回 issues,尝试单独加载
+        this.loadTodoTasksFromIssues();
+      }
+
+      console.log(`✅ 云端数据处理完成,共 ${this.projects.length} 个项目`);
+    } catch (error) {
+      console.error('❌ 处理云端数据失败:', error);
+      // 发生错误时回退
+      this.loadProjects();
+      this.loadTodoTasksFromIssues();
+    }
+  }
+
+  /**
+   * 将云函数返回的项目对象转换为本地 Project 模型
+   */
+  private transformCloudProject(p: any): Project {
+    const deadline = p.deadline ? new Date(p.deadline.iso || p.deadline) : new Date();
+    const createdAt = p.createdAt ? new Date(p.createdAt.iso || p.createdAt) : undefined;
+    const updatedAt = p.updatedAt ? new Date(p.updatedAt.iso || p.updatedAt) : undefined;
+    
+    // 状态判断
+    const now = new Date();
+    const daysLeft = p.daysLeft !== undefined ? p.daysLeft : Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+    const isOverdue = p.isOverdue || daysLeft < 0;
+    const dueSoon = !isOverdue && daysLeft <= 3;
+    
+    return {
+      id: p.id,
+      name: p.name,
+      status: p.status,
+      currentStage: p.currentStage || '订单分配',
+      createdAt: createdAt,
+      updatedAt: updatedAt,
+      deadline: deadline,
+      
+      designerName: p.designerName || '未分配',
+      designerId: p.designerIds?.[0] || '', // 云函数需要返回 designerIds 数组,或者暂时留空
+      designerIds: [], // 云函数暂未返回此字段,后续可优化
+      
+      type: (p.type === 'hard') ? 'hard' : 'soft',
+      memberType: 'normal',
+      urgency: (p.urgency === 'high' || p.urgency === 'medium') ? p.urgency : 'medium',
+      phases: [],
+      expectedEndDate: deadline,
+      
+      isStalled: false, // 暂无数据
+      isModification: false, // 暂无数据
+      
+      isOverdue: isOverdue,
+      overdueDays: isOverdue ? Math.abs(daysLeft) : 0,
+      dueSoon: dueSoon,
+      searchIndex: `${p.name}|${p.designerName}`.toLowerCase(),
+      
+      data: p.data || {},
+      contact: null,
+      customer: ''
+    } as Project;
+  }
+
+  /**
+   * 从云端数据更新设计师信息
+   */
+  private updateDesignerDataFromCloud(workload: any[], projects: any[], spaceStats?: any): void {
+    // 1. 关键修复:利用 workload 更新 realDesigners,确保甘特图能获取到完整设计师列表
+    this.realDesigners = workload.map((w: any) => ({
+      id: w.id,
+      name: w.name,
+      tags: {
+        capacity: { weeklyProjects: w.weeklyCapacity || 3 }
+      }
+    }));
+
+    // 2. 更新设计师列表(用于筛选下拉框)
+    this.designers = this.realDesigners.map(d => ({
+      id: d.id,
+      name: d.name
+    }));
+    
+    // 3. 更新 designerProfiles(用于兼容性)
+    this.designerProfiles = workload.map((w: any) => ({
+      id: w.id,
+      name: w.name,
+      skills: [],
+      workload: w.loadRate,
+      avgRating: 0,
+      experience: 0
+    }));
+    
+    // 4. 重建 designerWorkloadMap(用于甘特图)
+    this.designerWorkloadMap.clear();
+    
+    // 遍历所有项目,按设计师分组
+    projects.forEach((p: any) => {
+      // ✅ 过滤已完成的项目,防止虚高负载
+      if (p.status === '已完成' || p.status === '已交付') {
+        return;
+      }
+
+      const designerNames = (p.designerName || '未分配').split(',').map((n: string) => n.trim());
+      
+      designerNames.forEach((name: string) => {
+        if (!name) return;
+        
+        if (!this.designerWorkloadMap.has(name)) {
+          this.designerWorkloadMap.set(name, []);
+        }
+        
+        // 转换为甘特图需要的格式,并传递 spaceStats
+        const timelineItem = this.transformCloudProjectToTimelineItem(p, name, spaceStats);
+        this.designerWorkloadMap.get(name)?.push(timelineItem);
+      });
+    });
+    
+    // 5. 直接生成 timeline 数据
+    const timelineData: ProjectTimeline[] = [];
+    this.designerWorkloadMap.forEach((items) => {
+      timelineData.push(...items);
+    });
+    
+    this.projectTimelineData = timelineData;
+    this.timelineDataCache = timelineData;
+  }
+
+  /**
+   * 将云端项目转换为时间轴项
+   */
+  private transformCloudProjectToTimelineItem(p: any, designerName: string, spaceStats?: any): any {
+    // 这里复用 transformCloudProject 的逻辑,但增加时间轴特有字段
+    const project = this.transformCloudProject(p);
+    const now = new Date();
+    
+    // 复用 DesignerWorkloadService 中的状态逻辑
+    let status: 'normal' | 'warning' | 'urgent' | 'overdue' = 'normal';
+    if (project.isOverdue) status = 'overdue';
+    else if (project.dueSoon) status = 'urgent';
+    
+    // 阶段映射:将中文阶段名映射为 Gantt 组件支持的英文 key
+    const stageMap: Record<string, string> = {
+      '订单分配': 'plan',
+      '方案深化': 'model',
+      '硬装': 'model',
+      '软装': 'decoration',
+      '渲染': 'render',
+      '交付执行': 'delivery',
+      '售后归档': 'aftercare'
+    };
+    
+    // 如果没有匹配到,默认使用 'model' 或保留原值
+    const currentStageKey = stageMap[project.currentStage] || 'model';
+
+    // ✅ 获取该项目的空间统计数据
+    const summary = spaceStats ? spaceStats[project.id] : null;
+
+    return {
+      id: project.id, // ✅ 修复:添加 id 属性以兼容 EmployeeDetailPanel
+      projectId: project.id,
+      projectName: project.name,
+      name: project.name, // ✅ 修复:Gantt Tooltip 需要 name 字段
+      designerName: designerName,
+      designerId: project.designerId,
+      
+      startDate: project.createdAt || new Date(), 
+      endDate: project.deadline,
+      deadline: project.deadline, // ✅ 修复:传递 deadline 给详情面板
+      deliveryDate: project.deadline,
+      reviewDate: project.deadline, 
+      
+      currentStage: currentStageKey, // ✅ 修复:映射为英文 key
+      stageName: project.currentStage, // 保留中文名称用于显示
+      stageProgress: 50,
+      
+      status: status,
+      isStalled: false,
+      isModification: false,
+      
+      priority: project.urgency === 'high' ? 'high' : 'medium',
+      spaceName: '',
+      customerName: '',
+      phaseDeadlines: undefined,
+      data: project.data,
+      spaceDeliverableSummary: summary // ✅ 传递空间统计数据
+    };
+  }
+
   /**
    * 加载项目列表
    */

+ 4 - 2
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts

@@ -631,8 +631,10 @@ export class EmployeeDetailPanelComponent implements OnInit, OnChanges {
       event.stopPropagation();
     }
 
-    // 🔥 关闭日历项目列表弹窗,避免与进度弹窗叠加显示
-    this.closeCalendarProjectList();
+    if (!projectId) {
+      console.warn('⚠️ 无法查看项目进度:项目ID无效');
+      return;
+    }
 
     this.selectedProjectForProgress = projectId;
     this.loadingProgress = true;

+ 16 - 1
src/app/pages/team-leader/project-timeline/project-progress-modal.ts

@@ -147,6 +147,20 @@ export class ProjectProgressModalComponent implements OnInit {
     return this.getPhaseStatusLabel(this.summary.overallCompletionRate);
   }
 
+  /**
+   * 🆕 获取阶段显示名称
+   */
+  getPhaseLabel(phaseKey: string): string {
+    const map: Record<string, string> = {
+      'modeling': '白模',
+      'softDecor': '软装',
+      'rendering': '渲染',
+      'postProcessing': '后期',
+      'whiteModel': '白模' // 兼容性
+    };
+    return map[phaseKey] || phaseKey;
+  }
+
   /**
    * ✅ 应用方案:获取当前项目的所有未完成的任务,按设计师分组
    * 注意:这里只显示当前项目(this.summary)的未完成任务,不是所有项目的汇总
@@ -182,7 +196,8 @@ export class ProjectProgressModalComponent implements OnInit {
 
     // 遍历当前项目的所有阶段
     Object.entries(this.summary.phaseProgress).forEach(([phaseKey, phaseInfo]: [string, any]) => {
-      const phaseLabel = phaseInfo.phaseLabel || phaseKey;
+      // ✅ 修复:使用 getPhaseLabel 获取中文名称
+      const phaseLabel = this.getPhaseLabel(phaseKey);
       
       // 遍历当前项目当前阶段的未完成空间
       if (phaseInfo.incompleteSpaces && Array.isArray(phaseInfo.incompleteSpaces)) {

+ 8 - 8
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -174,17 +174,17 @@
                 <!-- 4段式进度胶囊 -->
                 <div class="progress-pills">
                   <div class="pill type-model" 
-                       [style.--fill-percent.%]="summary.phaseProgress.modeling.completionRate"
-                       [title]="'建模: ' + summary.phaseProgress.modeling.completionRate + '%'"></div>
+                       [style.--fill-percent.%]="summary.phaseProgress?.modeling?.completionRate || 0"
+                       [title]="'建模: ' + (summary.phaseProgress?.modeling?.completionRate || 0) + '%'"></div>
                   <div class="pill type-soft" 
-                       [style.--fill-percent.%]="summary.phaseProgress.softDecor.completionRate"
-                       [title]="'软装: ' + summary.phaseProgress.softDecor.completionRate + '%'"></div>
+                       [style.--fill-percent.%]="summary.phaseProgress?.softDecor?.completionRate || 0"
+                       [title]="'软装: ' + (summary.phaseProgress?.softDecor?.completionRate || 0) + '%'"></div>
                   <div class="pill type-render" 
-                       [style.--fill-percent.%]="summary.phaseProgress.rendering.completionRate"
-                       [title]="'渲染: ' + summary.phaseProgress.rendering.completionRate + '%'"></div>
+                       [style.--fill-percent.%]="summary.phaseProgress?.rendering?.completionRate || 0"
+                       [title]="'渲染: ' + (summary.phaseProgress?.rendering?.completionRate || 0) + '%'"></div>
                   <div class="pill type-post" 
-                       [style.--fill-percent.%]="summary.phaseProgress.postProcessing.completionRate"
-                       [title]="'后期: ' + summary.phaseProgress.postProcessing.completionRate + '%'"></div>
+                       [style.--fill-percent.%]="summary.phaseProgress?.postProcessing?.completionRate || 0"
+                       [title]="'后期: ' + (summary.phaseProgress?.postProcessing?.completionRate || 0) + '%'"></div>
                 </div>
                 
                 <div class="pill-labels">

+ 19 - 21
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -882,13 +882,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     
     // 每10分钟刷新一次(600000毫秒)
     this.refreshTimer = setInterval(() => {
-      console.log('🔄 项目时间轴:10分钟自动刷新触发');
       this.updateCurrentTime();
       this.initializeData(); // 重新加载数据和过滤
       this.cdr.markForCheck(); // 触发变更检测
     }, 600000); // 10分钟 = 10 * 60 * 1000 = 600000ms
-    
-    console.log('⏰ 项目时间轴:已启动10分钟自动刷新');
   }
   
   /**
@@ -896,7 +893,6 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
    */
   private updateCurrentTime(): void {
     this.currentTime = new Date();
-    console.log('⏰ 当前精确时间已更新:', this.currentTime.toLocaleString('zh-CN'));
   }
   
   /**
@@ -920,30 +916,32 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
    * 🆕 加载项目的空间与交付物统计数据
    */
   private async loadSpaceDeliverableData(): Promise<void> {
-    console.log('📊 开始加载项目空间与交付物统计数据');
+    console.log(' 开始加载项目空间与交付物统计数据');
     
     // 异步加载每个项目的统计数据(不阻塞主流程)
     for (const project of this.projects) {
+      if (!project.projectId) {
+        continue;
+      }
+      
+      // 优化:如果项目已经有了统计数据(从云函数获取),直接使用,不重复请求
+      if (project.spaceDeliverableSummary) {
+        this.spaceDeliverableCache.set(project.projectId, project.spaceDeliverableSummary);
+        continue;
+      }
+      
+      // 检查缓存
+      if (this.spaceDeliverableCache.has(project.projectId)) {
+        continue;
+      }
+      
       try {
-        const summary = await this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(
-          project.projectId
-        );
-        
-        // 缓存统计数据
+        const summary = await this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(project.projectId);
         this.spaceDeliverableCache.set(project.projectId, summary);
-        
-        // 将统计数据附加到项目对象上
+        // 将统计数据附加到项目对象上,以便后续使用
         project.spaceDeliverableSummary = summary;
-        
-        console.log(`✅ 项目 ${project.projectName} 统计完成:`, {
-          空间数: summary.totalSpaces,
-          已完成空间: summary.spacesWithDeliverables,
-          总文件数: summary.totalDeliverableFiles,
-          完成率: `${summary.overallCompletionRate}%`
-        });
-        
       } catch (error) {
-        console.warn(`⚠️ 加载项目 ${project.projectName} 统计数据失败:`, error);
+        console.error('获取项目统计数据失败:', project.projectName, error);
       }
     }
     

+ 77 - 4
src/app/pages/team-leader/services/dashboard-data.service.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@angular/core';
+import { ProjectSpaceDeliverableService } from '../../../../modules/project/services/project-space-deliverable.service';
 
 /**
  * 仪表盘数据服务
@@ -11,7 +12,9 @@ export class DashboardDataService {
   private cid: string = '';
   private Parse: any = null;
   
-  constructor() {
+  constructor(
+    private projectSpaceDeliverableService: ProjectSpaceDeliverableService
+  ) {
     this.cid = localStorage.getItem('company') || '';
     console.log('🏢 DashboardDataService初始化,当前公司ID:', this.cid || '(未设置)');
     this.initParse();
@@ -22,11 +25,19 @@ export class DashboardDataService {
    */
   private async initParse(): Promise<void> {
     try {
-      const { FmodeParse } = await import('fmode-ng/parse');
+      // 🔥 尝试从 core 导入以获取完整功能的 FmodeParse
+      const { FmodeParse } = await import('fmode-ng/core');
       this.Parse = FmodeParse.with("nova");
-      console.log('✅ DashboardDataService: FmodeParse 初始化成功');
+      console.log('✅ DashboardDataService: FmodeParse (core) 初始化成功');
     } catch (error) {
-      console.error('❌ DashboardDataService: FmodeParse 初始化失败:', error);
+      console.warn('⚠️ 从 fmode-ng/core 导入失败,尝试 fmode-ng/parse', error);
+      try {
+        const { FmodeParse } = await import('fmode-ng/parse');
+        this.Parse = FmodeParse.with("nova");
+        console.log('✅ DashboardDataService: FmodeParse (parse) 初始化成功');
+      } catch (err) {
+        console.error('❌ DashboardDataService: FmodeParse 初始化失败:', err);
+      }
     }
   }
   
@@ -327,6 +338,68 @@ export class DashboardDataService {
       return [];
     }
   }
+
+  /**
+   * 获取组长看板数据
+   */
+  async getTeamLeaderDataFromCloud(): Promise<any> {
+    try {
+      const { FmodeParse } = await import('fmode-ng/core');
+
+      const Parse: any = FmodeParse.initialize({
+        appId: "ncloudmaster",
+        serverURL: "https://server.fmode.cn/parse",
+        appName: 'NovaCloud'
+      } as any);
+
+      const startTime = Date.now();
+
+      // 调用云函数 (ID: 8qJkylemKn)
+      const result = await Parse.Cloud.function({
+        id: '8qJkylemKn', 
+        companyId: this.cid
+      });
+
+      console.log(`✅ 云函数数据加载成功,耗时 ${Date.now() - startTime}ms`);
+      
+      // 🆕 处理空间统计数据注入(预加载缓存)
+      if (result && result.data && result.data.spaceStats) {
+        const spaceStats = result.data.spaceStats;
+        const count = Object.keys(spaceStats).length;
+        console.log(`📊 预加载 ${count} 个项目的空间统计数据`);
+        
+        Object.keys(spaceStats).forEach(projectId => {
+          this.projectSpaceDeliverableService.injectSummary(projectId, spaceStats[projectId]);
+        });
+      } else if (result && result.spaceStats) {
+        // 兼容直接返回的情况
+        const spaceStats = result.spaceStats;
+        Object.keys(spaceStats).forEach(projectId => {
+          this.projectSpaceDeliverableService.injectSummary(projectId, spaceStats[projectId]);
+        });
+      }
+      
+      // 兼容返回格式
+      if (result && (result.stats || result.workload)) return result;
+      if (result && result.data) return result.data;
+      if (result && result.result) return result.result;
+      
+      return result;
+
+    } catch (error) {
+      console.error('❌ 云函数调用失败:', error);
+      return null;
+    }
+  }
+
+  // 辅助方法:统一处理返回结果格式
+  private normalizeResult(result: any) {
+    if (!result) return null;
+    if (result.stats || result.workload) return result;
+    if (result.data) return result.data;
+    if (result.result) return result.result;
+    return result;
+  }
   
   /**
    * 获取默认KPI统计(无数据时)

+ 10 - 4
src/modules/project/components/project-progress-modal/project-progress-modal.component.html

@@ -13,6 +13,12 @@
       </button>
     </div>
 
+    <!-- 加载中状态 -->
+    <div class="loading-state" *ngIf="!summary">
+      <div class="spinner"></div>
+      <p>正在加载项目数据...</p>
+    </div>
+
     <!-- 项目基本信息 -->
     <div class="project-info" *ngIf="summary">
       <div class="info-row">
@@ -68,22 +74,22 @@
         <div class="stat-item">
           <span class="stat-icon">🏗️</span>
           <span class="stat-label">白模</span>
-          <span class="stat-value">{{ summary.totalByType.whiteModel }}</span>
+          <span class="stat-value">{{ summary.totalByType?.whiteModel || 0 }}</span>
         </div>
         <div class="stat-item">
           <span class="stat-icon">🎨</span>
           <span class="stat-label">软装</span>
-          <span class="stat-value">{{ summary.totalByType.softDecor }}</span>
+          <span class="stat-value">{{ summary.totalByType?.softDecor || 0 }}</span>
         </div>
         <div class="stat-item">
           <span class="stat-icon">🖼️</span>
           <span class="stat-label">渲染</span>
-          <span class="stat-value">{{ summary.totalByType.rendering }}</span>
+          <span class="stat-value">{{ summary.totalByType?.rendering || 0 }}</span>
         </div>
         <div class="stat-item">
           <span class="stat-icon">✨</span>
           <span class="stat-label">后期</span>
-          <span class="stat-value">{{ summary.totalByType.postProcess }}</span>
+          <span class="stat-value">{{ summary.totalByType?.postProcess || 0 }}</span>
         </div>
       </div>
       <div class="stats-total">

+ 29 - 0
src/modules/project/components/project-progress-modal/project-progress-modal.component.scss

@@ -109,6 +109,35 @@
   }
 }
 
+// 加载状态
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+  
+  .spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #f3f3f3;
+    border-top: 4px solid #667eea;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 16px;
+  }
+  
+  p {
+    color: #666;
+    font-size: 14px;
+  }
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
 // 项目基本信息
 .project-info {
   padding: 20px 24px;

+ 14 - 0
src/modules/project/components/project-progress-modal/project-progress-modal.component.ts

@@ -150,5 +150,19 @@ export class ProjectProgressModalComponent implements OnInit {
   canNavigateToProject(): boolean {
     return !!(this.summary?.projectId && this.companyId);
   }
+
+  /**
+   * 🆕 获取阶段显示名称
+   */
+  getPhaseLabel(phaseKey: string): string {
+    const map: Record<string, string> = {
+      'modeling': '白模',
+      'softDecor': '软装',
+      'rendering': '渲染',
+      'postProcessing': '后期',
+      'whiteModel': '白模' // 兼容性
+    };
+    return map[phaseKey] || phaseKey;
+  }
 }
 

+ 56 - 32
src/modules/project/services/project-space-deliverable.service.ts

@@ -103,28 +103,47 @@ export interface ProjectSpaceDeliverableSummary {
   providedIn: 'root'
 })
 export class ProjectSpaceDeliverableService {
+  // ✅ 新增:内存缓存
+  private summaryCache = new Map<string, ProjectSpaceDeliverableSummary>();
 
   constructor(
     private projectFileService: ProjectFileService
   ) {}
 
+  /**
+   * 🆕 注入统计数据(用于云函数预加载)
+   */
+  injectSummary(projectId: string, summary: ProjectSpaceDeliverableSummary): void {
+    this.summaryCache.set(projectId, summary);
+  }
+
+  /**
+   * 🆕 清除缓存
+   */
+  clearCache(projectId?: string): void {
+    if (projectId) {
+      this.summaryCache.delete(projectId);
+    } else {
+      this.summaryCache.clear();
+    }
+  }
+
   /**
    * 获取项目的空间与交付物统计信息
    * 
    * @param projectId 项目ID
+   * @param forceRefresh 是否强制刷新(忽略缓存)
    * @returns 项目空间与交付物统计摘要
-   * 
-   * @example
-   * ```typescript
-   * const summary = await this.service.getProjectSpaceDeliverableSummary('project123');
-   * console.log(`项目有 ${summary.totalSpaces} 个空间`);
-   * console.log(`已上传交付物的空间:${summary.spacesWithDeliverables} 个`);
-   * console.log(`总完成率:${summary.overallCompletionRate}%`);
-   * ```
    */
   async getProjectSpaceDeliverableSummary(
-    projectId: string
+    projectId: string,
+    forceRefresh: boolean = false
   ): Promise<ProjectSpaceDeliverableSummary> {
+    // ✅ 检查缓存
+    if (!forceRefresh && this.summaryCache.has(projectId)) {
+      return this.summaryCache.get(projectId)!;
+    }
+
     try {
       // 1. 获取项目信息
       const projectQuery = new Parse.Query('Project');
@@ -143,29 +162,29 @@ export class ProjectSpaceDeliverableService {
         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(`📊 查询到 ${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)`);
       } catch (productError: any) {
         // ✅ 容错处理:Product 查询失败时,记录警告但继续处理
         console.warn(`⚠️ Product 查询失败,将使用空的空间列表:`, productError.message || productError.toString());
-        console.warn(`⚠️ Product 错误详情:`, productError);
+        // console.warn(`⚠️ Product 错误详情:`, productError);
         products = []; // 查询失败时使用空数组
       }
 
       // 3. 去重:按空间名称去重(忽略大小写和首尾空格)
       const uniqueProducts = this.deduplicateProducts(products);
-      console.log(`📊 去重后空间数:${uniqueProducts.length}`);
+      // console.log(`📊 去重后空间数:${uniqueProducts.length}`);
 
       // 4. 统计每个空间的交付物
       const spaceInfos: SpaceDeliverableInfo[] = [];
@@ -183,7 +202,7 @@ export class ProjectSpaceDeliverableService {
       const allDeliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
         stage: 'delivery'
       });
-      console.log(`📊 项目 ${projectName} 共有 ${allDeliveryFiles.length} 个交付文件`);
+      // console.log(`📊 项目 ${projectName} 共有 ${allDeliveryFiles.length} 个交付文件`);
 
       for (const product of uniqueProducts) {
         // ✅ 应用方案:传入所有文件列表,避免重复查询
@@ -208,7 +227,7 @@ export class ProjectSpaceDeliverableService {
       // ✅ 应用方案:计算各阶段进度详情,传入 products 以获取负责人信息
       const phaseProgress = this.calculatePhaseProgress(spaceInfos, project, uniqueProducts);
 
-      return {
+      const result = {
         projectId,
         projectName,
         totalSpaces: uniqueProducts.length,
@@ -220,6 +239,11 @@ export class ProjectSpaceDeliverableService {
         phaseProgress
       };
 
+      // ✅ 存入缓存
+      this.summaryCache.set(projectId, result);
+
+      return result;
+
     } catch (error) {
       console.error('获取项目空间交付物统计失败:', error);
       throw error;
@@ -435,7 +459,7 @@ export class ProjectSpaceDeliverableService {
       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} (项目负责人)`);
+          // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${projectLeader.name} (项目负责人)`);
         }
       });
     }
@@ -447,7 +471,7 @@ export class ProjectSpaceDeliverableService {
           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 ? ' (项目负责人)' : ''}`);
+            // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${member.name}${member.isProjectLeader ? ' (项目负责人)' : ''}`);
           }
         });
       }
@@ -459,7 +483,7 @@ export class ProjectSpaceDeliverableService {
         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} (跨组合作)`);
+            // console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${collaborator.name} (跨组合作)`);
           }
         });
       }
@@ -492,9 +516,9 @@ export class ProjectSpaceDeliverableService {
             
             if (profileName) {
               spaceAssigneeMap.set(spaceId, profileName);
-              console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
+              // console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
             } else {
-              console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
+              // console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
             }
           }
         }
@@ -554,7 +578,7 @@ export class ProjectSpaceDeliverableService {
             spaceAssignee = '未分配';
           }
           
-          console.log(`📊 未完成空间: ${space.spaceName} (ID: ${space.spaceId}), 负责人: ${spaceAssignee}`);
+          // console.log(`📊 未完成空间: ${space.spaceName} (ID: ${space.spaceId}), 负责人: ${spaceAssignee}`);
           
           incompleteSpaces.push({
             spaceId: space.spaceId,