ryan 20 часов назад
Родитель
Сommit
46b2fa8aa6

+ 378 - 0
cloud/functions/project-load.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 });
+  }
+}

+ 9 - 7
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.ts

@@ -15,18 +15,19 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
   @Input() designerWorkloadMap: Map<string, any[]> = new Map();
   @Input() realDesigners: any[] = [];
   @Input() filteredProjects: Project[] = [];
-  
+  @Input() baseDate: Date | null = null; // 🆕 时间基准日期
+
   @Output() employeeClick = new EventEmitter<string>();
 
   @ViewChild('workloadGanttContainer', { static: false }) workloadGanttContainer!: ElementRef<HTMLDivElement>;
-  
+
   private workloadGanttChart: any | null = null;
   workloadGanttScale: 'week' | 'month' = 'week';
 
   constructor(private cdr: ChangeDetectorRef) {}
 
   ngOnChanges(changes: SimpleChanges): void {
-    if (changes['designerWorkloadMap'] || changes['realDesigners'] || changes['filteredProjects']) {
+    if (changes['designerWorkloadMap'] || changes['realDesigners'] || changes['filteredProjects'] || changes['baseDate']) {
       this.updateWorkloadGantt();
     }
   }
@@ -63,13 +64,13 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
   private updateWorkloadGantt(): void {
     if (!this.workloadGanttContainer?.nativeElement) {
       // If called too early, retry slightly later
-      // setTimeout(() => this.updateWorkloadGantt(), 100); 
+      // setTimeout(() => this.updateWorkloadGantt(), 100);
       return;
     }
 
     if (!this.workloadGanttChart) {
       this.workloadGanttChart = echarts.init(this.workloadGanttContainer.nativeElement);
-      
+
       // Add click event listener
       this.workloadGanttChart.on('click', (params: any) => {
         // Handle workload block click
@@ -90,8 +91,9 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
     }
 
     const DAY = 24 * 60 * 60 * 1000;
-    const now = new Date();
-    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+    // 🆕 使用 baseDate 或当前日期作为基准
+    const baseDate = this.baseDate ? new Date(this.baseDate) : new Date();
+    const today = new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate());
     const todayTs = today.getTime();
 
     // Time range & Category Data

+ 18 - 1
src/app/pages/team-leader/dashboard/dashboard.html

@@ -41,6 +41,21 @@
     <div class="section-header">
       <h2>项目监控大盘</h2>
       <div class="section-actions">
+        <!-- 🆕 时间范围选择器 -->
+        <div class="time-range-selector">
+          <button class="time-nav-btn" (click)="goToPreviousMonth()" title="上个月">
+            <span class="nav-icon">‹</span>
+          </button>
+          <span class="time-display">{{ getCurrentMonthDisplay() }}</span>
+          <button class="time-nav-btn" (click)="goToNextMonth()" title="下个月">
+            <span class="nav-icon">›</span>
+          </button>
+          @if (!isCurrentMonth()) {
+            <button class="time-today-btn" (click)="goToCurrentMonth()" title="返回当前月">
+              返回本月
+            </button>
+          }
+        </div>
         @if (selectedStatus !== 'all') {
           <button class="btn-link" (click)="resetStatusFilter()">返回全部项目</button>
         }
@@ -52,6 +67,7 @@
       [designerWorkloadMap]="designerWorkloadMap"
       [realDesigners]="realDesigners"
       [filteredProjects]="filteredProjects"
+      [baseDate]="timeBaseDate"
       (employeeClick)="onEmployeeClick($event)">
     </app-workload-gantt>
     
@@ -71,9 +87,10 @@
     
     <!-- 项目负载时间轴(切换视图时显示) -->
     @if (showGanttView) {
-      <app-project-timeline 
+      <app-project-timeline
         [projects]="projectTimelineData"
         [companyId]="currentUser.name"
+        [baseDate]="timeBaseDate"
         (projectClick)="onProjectTimelineClick($event)"
         (refreshData)="loadDashboardData()">
       </app-project-timeline>

+ 71 - 0
src/app/pages/team-leader/dashboard/dashboard.scss

@@ -62,6 +62,77 @@
   }
 }
 
+/* 🆕 时间范围选择器样式 */
+.time-range-selector {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  padding: 4px 8px;
+
+  .time-nav-btn {
+    width: 28px;
+    height: 28px;
+    border: none;
+    background: transparent;
+    color: #64748b;
+    cursor: pointer;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 18px;
+    font-weight: bold;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: #e2e8f0;
+      color: #334155;
+    }
+
+    &:active {
+      background: #cbd5e1;
+    }
+
+    .nav-icon {
+      line-height: 1;
+    }
+  }
+
+  .time-display {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1e293b;
+    min-width: 90px;
+    text-align: center;
+    padding: 0 8px;
+  }
+
+  .time-today-btn {
+    padding: 4px 10px;
+    border: none;
+    background: linear-gradient(135deg, #3b82f6, #2563eb);
+    color: white;
+    cursor: pointer;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    transition: all 0.2s ease;
+    white-space: nowrap;
+
+    &:hover {
+      background: linear-gradient(135deg, #2563eb, #1d4ed8);
+      transform: translateY(-1px);
+    }
+
+    &:active {
+      transform: translateY(0);
+    }
+  }
+}
+
 .btn-toggle-view {
   padding: local.$ios-spacing-sm local.$ios-spacing-md;
   border-radius: local.$ios-radius-md;

+ 110 - 14
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -70,25 +70,25 @@ const Parse = FmodeParse.with('nova');
 export class Dashboard implements OnInit, OnDestroy {
   // 暴露 Array 给模板使用
   Array = Array;
-  
+
   projects: Project[] = [];
   filteredProjects: Project[] = [];
   urgentPinnedProjects: Project[] = [];
   showAlert: boolean = false;
   selectedProjectId: string = '';
-  
+
   // 待办任务数据(交给子组件处理显示)
   todoTasksFromIssues: TodoTaskFromIssue[] = [];
   loadingTodoTasks: boolean = false;
   todoTaskError: string = '';
   private todoTaskRefreshTimer: any;
-  
+
   // 紧急事件数据(交给子组件处理显示)
   urgentEvents: UrgentEvent[] = [];
   loadingUrgentEvents: boolean = false;
   handledUrgentEventIds: Set<string> = new Set();
   mutedUrgentEventIds: Set<string> = new Set();
-  
+
   // 新增:当前用户信息
   currentUser = {
     name: '组长',
@@ -96,6 +96,11 @@ export class Dashboard implements OnInit, OnDestroy {
     roleName: '组长'
   };
   currentDate = new Date();
+
+  // 🆕 时间范围选择相关
+  timeBaseDate: Date = new Date(); // 时间基准日期
+  timeRangeStart: Date | null = null; // 查询到的实际时间范围
+  timeRangeEnd: Date | null = null;
   
   // 真实设计师数据(从fmode-ng获取)
   realDesigners: any[] = [];
@@ -193,10 +198,13 @@ export class Dashboard implements OnInit, OnDestroy {
    */
   async loadDashboardData(): Promise<void> {
     console.log('🔄 开始加载看板数据...');
-    
+
+    // 构建时间参数
+    const timeParams = this.buildTimeParams();
+
     // 🔥 优先尝试从云函数加载数据
-    const cloudData = await this.dashboardDataService.getTeamLeaderDataFromCloud();
-    
+    const cloudData = await this.dashboardDataService.getTeamLeaderDataFromCloud(timeParams);
+
     if (cloudData) {
       // 如果云函数成功,使用云端聚合数据
       this.processCloudData(cloudData);
@@ -208,32 +216,113 @@ export class Dashboard implements OnInit, OnDestroy {
       // 加载待办任务(从问题板块)- 只有在云函数失败时才单独加载
       await this.loadTodoTasksFromIssues();
     }
-    
+
     // 🆕 计算紧急事件
     this.calculateUrgentEvents();
-    
+
     // 触发变更检测
     this.cdr.markForCheck();
   }
 
+  /**
+   * 🆕 构建时间参数
+   */
+  private buildTimeParams(): { baseDate: string } | undefined {
+    // 如果基准日期是当前月份,不传参数(使用默认行为)
+    const now = new Date();
+    const isCurrentMonth = this.timeBaseDate.getFullYear() === now.getFullYear() &&
+                          this.timeBaseDate.getMonth() === now.getMonth();
+
+    if (isCurrentMonth) {
+      return undefined; // 使用默认行为
+    }
+
+    // 否则传递 baseDate 参数
+    const year = this.timeBaseDate.getFullYear();
+    const month = String(this.timeBaseDate.getMonth() + 1).padStart(2, '0');
+    const day = String(this.timeBaseDate.getDate()).padStart(2, '0');
+
+    return {
+      baseDate: `${year}-${month}-${day}`
+    };
+  }
+
+  /**
+   * 🆕 获取当前显示的月份描述
+   */
+  getCurrentMonthDisplay(): string {
+    const year = this.timeBaseDate.getFullYear();
+    const month = this.timeBaseDate.getMonth() + 1;
+    return `${year}年${month}月`;
+  }
+
+  /**
+   * 🆕 切换到上个月
+   */
+  async goToPreviousMonth(): Promise<void> {
+    const newDate = new Date(this.timeBaseDate);
+    newDate.setMonth(newDate.getMonth() - 1);
+    this.timeBaseDate = newDate;
+
+    console.log('📅 切换到上个月:', this.getCurrentMonthDisplay());
+
+    // 重新加载数据
+    await this.loadDashboardData();
+  }
+
+  /**
+   * 🆕 切换到下个月
+   */
+  async goToNextMonth(): Promise<void> {
+    const newDate = new Date(this.timeBaseDate);
+    newDate.setMonth(newDate.getMonth() + 1);
+    this.timeBaseDate = newDate;
+
+    console.log('📅 切换到下个月:', this.getCurrentMonthDisplay());
+
+    // 重新加载数据
+    await this.loadDashboardData();
+  }
+
+  /**
+   * 🆕 返回当前月份
+   */
+  async goToCurrentMonth(): Promise<void> {
+    this.timeBaseDate = new Date();
+
+    console.log('📅 返回当前月份:', this.getCurrentMonthDisplay());
+
+    // 重新加载数据
+    await this.loadDashboardData();
+  }
+
+  /**
+   * 🆕 判断是否为当前月份
+   */
+  isCurrentMonth(): boolean {
+    const now = new Date();
+    return this.timeBaseDate.getFullYear() === now.getFullYear() &&
+           this.timeBaseDate.getMonth() === now.getMonth();
+  }
+
   /**
    * 处理云函数返回的数据
    */
   private processCloudData(data: any): void {
     try {
-      const { projects, workload, spaceStats, issues } = data;
-      
+      const { projects, workload, spaceStats, issues, timeRange } = 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;
@@ -244,6 +333,13 @@ export class Dashboard implements OnInit, OnDestroy {
         this.loadTodoTasksFromIssues();
       }
 
+      // 🆕 5. 更新时间范围信息
+      if (timeRange) {
+        this.timeRangeStart = new Date(timeRange.start);
+        this.timeRangeEnd = new Date(timeRange.end);
+        console.log(`📅 查询时间范围: ${this.timeRangeStart.toLocaleDateString()} ~ ${this.timeRangeEnd.toLocaleDateString()}`);
+      }
+
       console.log(`✅ 云端数据处理完成,共 ${this.projects.length} 个项目`);
     } catch (error) {
       console.error('❌ 处理云端数据失败:', error);

+ 13 - 11
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -62,29 +62,30 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   @Input() projects: ProjectTimeline[] = [];
   @Input() companyId: string = '';
   @Input() defaultDesigner: string = 'all'; // 🆕 默认选择的设计师
+  @Input() baseDate: Date | null = null; // 🆕 时间基准日期
   @Output() projectClick = new EventEmitter<string>();
   @Output() refreshData = new EventEmitter<void>(); // 🆕 请求父组件刷新数据
-  
+
   // 筛选状态
   selectedDesigner: string = 'all';
   selectedStatus: 'all' | 'normal' | 'warning' | 'urgent' | 'overdue' | 'stalled' = 'all';
   selectedPriority: 'all' | 'low' | 'medium' | 'high' | 'critical' = 'all';
   sortBy: 'priority' | 'time' = 'priority';
-  
+
   // 设计师统计
   designers: DesignerInfo[] = [];
   filteredProjects: ProjectTimeline[] = [];
-  
+
   // 时间轴相关
   timeRange: Date[] = [];
   timeRangeStart: Date = new Date();
   timeRangeEnd: Date = new Date();
   timelineScale: 'week' | 'month' = 'week'; // 默认周视图(7天)
-  
+
   // 🆕 实时时间相关
   currentTime: Date = new Date(); // 精确到分钟的当前时间
   private refreshTimer: any; // 自动刷新定时器
-  
+
   // 🆕 空间与交付物统计缓存
   spaceDeliverableCache: Map<string, ProjectSpaceDeliverableSummary> = new Map();
 
@@ -137,19 +138,20 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
    * 计算时间范围(周视图=7天,月视图=30天)
    */
   private calculateTimeRange(): void {
-    const now = new Date();
-    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-    
+    // 🆕 使用 baseDate 或当前日期作为基准
+    const baseDate = this.baseDate ? new Date(this.baseDate) : new Date();
+    const today = new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate());
+
     // 根据时间尺度计算范围
     const days = this.timelineScale === 'week' ? 7 : 30;
-    
+
     this.timeRangeStart = today;
     this.timeRangeEnd = new Date(today.getTime() + days * 24 * 60 * 60 * 1000);
-    
+
     // 生成时间刻度数组
     this.timeRange = [];
     const interval = this.timelineScale === 'week' ? 1 : 5; // 周视图每天一个刻度,月视图每5天
-    
+
     for (let i = 0; i <= days; i += interval) {
       const date = new Date(today.getTime() + i * 24 * 60 * 60 * 1000);
       this.timeRange.push(date);

+ 29 - 5
src/app/pages/team-leader/services/dashboard-data.service.ts

@@ -341,8 +341,16 @@ export class DashboardDataService {
 
   /**
    * 获取组长看板数据
+   * @param timeParams 时间参数(可选)
+   *   - baseDate: 基准日期,格式 YYYY-MM-DD
+   *   - startDate: 自定义开始日期
+   *   - endDate: 自定义结束日期
    */
-  async getTeamLeaderDataFromCloud(): Promise<any> {
+  async getTeamLeaderDataFromCloud(timeParams?: {
+    baseDate?: string;
+    startDate?: string;
+    endDate?: string;
+  }): Promise<any> {
     try {
       const { FmodeParse } = await import('fmode-ng/core');
 
@@ -354,11 +362,27 @@ export class DashboardDataService {
 
       const startTime = Date.now();
 
-      // 调用云函数 (ID: 8qJkylemKn)
-      const result = await Parse.Cloud.function({
-        id: '8qJkylemKn', 
+      // 构建请求参数
+      const functionParams: any = {
+        id: '8qJkylemKn',
         companyId: this.cid
-      });
+      };
+
+      // 添加时间参数
+      if (timeParams) {
+        if (timeParams.baseDate) {
+          functionParams.baseDate = timeParams.baseDate;
+        }
+        if (timeParams.startDate) {
+          functionParams.startDate = timeParams.startDate;
+        }
+        if (timeParams.endDate) {
+          functionParams.endDate = timeParams.endDate;
+        }
+      }
+
+      // 调用云函数 (ID: 8qJkylemKn)
+      const result = await Parse.Cloud.function(functionParams);
 
       console.log(`✅ 云函数数据加载成功,耗时 ${Date.now() - startTime}ms`);