COMPONENT-REUSE-ANALYSIS.md 31 KB

🔍 员工详情组件复用分析报告

📋 分析目标

分析 @employee-detail-panel 组件的数据流和样式设计,找出为什么在 @employee-info-panel 中复用后显示不一致的原因。


🎯 一、组长端 employee-detail-panel 组件分析

1.1 组件设计架构

// 核心设计理念:纯展示组件(Presentational Component)
@Component({
  selector: 'app-employee-detail-panel',
  standalone: true,
  imports: [CommonModule, DesignerCalendarComponent],
  templateUrl: './employee-detail-panel.html',
  styleUrls: ['./employee-detail-panel.scss']
})
export class EmployeeDetailPanelComponent implements OnInit {
  // ⭐ 关键设计:所有数据通过 @Input 接收,组件本身不负责数据获取
  @Input() visible: boolean = false;
  @Input() employeeDetail: EmployeeDetail | null = null;
  @Input() embedMode: boolean = false;
  
  // ⭐ 关键设计:所有交互通过 @Output 向外发射,由父组件处理
  @Output() close = new EventEmitter<void>();
  @Output() calendarMonthChange = new EventEmitter<number>();
  @Output() calendarDayClick = new EventEmitter<EmployeeCalendarDay>();
  @Output() projectClick = new EventEmitter<string>();
  @Output() refreshSurvey = new EventEmitter<void>();
}

关键特点:

  • 职责单一:仅负责数据展示,不负责数据获取和业务逻辑
  • 数据驱动:完全依赖 @Input() employeeDetail 的数据结构
  • 事件委托:所有用户交互通过 @Output 事件向父组件汇报
  • 样式封装:样式通过 SCSS 完全封装,不依赖外部样式

1.2 数据接口结构

export interface EmployeeDetail {
  name: string;
  currentProjects: number;           // ⭐ 当前项目数
  projectNames: string[];            // 项目名称列表
  projectData: Array<{               // ⭐ 项目完整数据(含ID)
    id: string; 
    name: string;
  }>;
  leaveRecords: LeaveRecord[];       // 请假记录
  redMarkExplanation: string;        // 红色标记说明
  calendarData?: EmployeeCalendarData; // ⭐ 日历数据
  surveyCompleted?: boolean;         // 问卷完成状态
  surveyData?: any;                  // 问卷数据
  profileId?: string;                // Profile ID
}

export interface EmployeeCalendarData {
  currentMonth: Date;                // ⭐ 当前显示月份
  days: EmployeeCalendarDay[];       // ⭐ 日历日期数组
}

export interface EmployeeCalendarDay {
  date: Date;                        // ⭐ 日期对象
  projectCount: number;              // ⭐ 当天项目数
  projects: Array<{                  // ⭐ 当天项目列表
    id: string; 
    name: string; 
    deadline?: Date;
  }>;
  isToday: boolean;                  // 是否今天
  isCurrentMonth: boolean;           // 是否当前月
}

数据特征:

  • ✅ 接口定义清晰,字段明确
  • ✅ 包含所有必需的展示数据
  • ✅ 数据结构扁平,易于传递

1.3 数据准备流程(组长端 Dashboard)

// 📍 位置:team-leader/dashboard/dashboard.ts

// 第1步:加载设计师工作负载到内存
async loadDesignerWorkload(): Promise<void> {
  // 从 ProjectTeam 表查询项目分配关系
  const projectTeams = await projectTeamQuery.find();
  
  // ⭐ 关键:使用 Map 按员工名称聚合项目
  this.designerWorkloadMap = new Map<string, any[]>();
  
  for (const team of projectTeams) {
    const memberName = team.get('memberName');
    const project = team.get('project');
    
    if (!this.designerWorkloadMap.has(memberName)) {
      this.designerWorkloadMap.set(memberName, []);
    }
    this.designerWorkloadMap.get(memberName)!.push({
      id: project.id,
      name: project.get('name'),
      deadline: project.get('deadline'),
      createdAt: project.get('createdAt')
      // ... 其他项目字段
    });
  }
}

// 第2步:用户点击员工时生成详情数据
async onEmployeeClick(employeeName: string): Promise<void> {
  // ⭐ 关键:从内存中的 Map 获取该员工的所有项目
  const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
  
  // 生成员工详情数据
  this.selectedEmployeeDetail = await this.generateEmployeeDetail(employeeName);
  this.showEmployeeDetailPanel = true;
}

// 第3步:生成完整的员工详情数据
private async generateEmployeeDetail(employeeName: string): Promise<EmployeeDetail> {
  // ⭐ 关键:从 designerWorkloadMap 获取项目列表
  const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
  const currentProjects = employeeProjects.length;
  
  // ⭐ 关键:准备项目数据(最多显示3个)
  const projectData = employeeProjects.slice(0, 3).map(p => ({
    id: p.id,
    name: p.name
  }));
  
  // ⭐ 关键:生成日历数据
  const calendarData = this.generateEmployeeCalendar(employeeName, employeeProjects);
  
  // ⭐ 关键:查询问卷数据
  const profile = await this.findProfileByName(employeeName);
  const surveyData = await this.loadSurveyData(profile);
  
  // ⭐ 返回完整的 EmployeeDetail 对象
  return {
    name: employeeName,
    currentProjects,
    projectNames: projectData.map(p => p.name),
    projectData,                    // ⭐ 包含完整的项目数组
    leaveRecords: employeeLeaveRecords,
    redMarkExplanation,
    calendarData,                   // ⭐ 包含完整的日历数据
    surveyCompleted,
    surveyData,
    profileId
  };
}

// 第4步:生成日历数据
private generateEmployeeCalendar(
  employeeName: string, 
  employeeProjects: any[], 
  targetMonth?: Date
): EmployeeCalendarData {
  const currentMonth = targetMonth || new Date();
  const year = currentMonth.getFullYear();
  const month = currentMonth.getMonth();
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const days: EmployeeCalendarDay[] = [];
  
  // ⭐ 关键:遍历当月每一天
  for (let day = 1; day <= daysInMonth; day++) {
    const date = new Date(year, month, day);
    const dateStr = date.toISOString().split('T')[0];
    
    // ⭐ 关键:找出该日期相关的项目(基于项目的整个生命周期)
    const dayProjects = employeeProjects.filter(p => {
      const createdAt = this.parseDate(p.createdAt);
      const deadline = this.parseDate(p.deadline);
      
      if (!createdAt || !deadline) return false;
      
      // ⭐ 关键:项目在 [createdAt, deadline] 范围内的所有天都显示
      const dateTime = date.getTime();
      return dateTime >= createdAt.getTime() && dateTime <= deadline.getTime();
    });
    
    days.push({
      date,
      projectCount: dayProjects.length,
      projects: dayProjects.map(p => ({
        id: p.id,
        name: p.name,
        deadline: this.parseDate(p.deadline)
      })),
      isToday: this.isSameDay(date, new Date()),
      isCurrentMonth: true
    });
  }
  
  // ⭐ 填充上月和下月的日期(用于日历网格对齐)
  // ... 填充逻辑 ...
  
  return {
    currentMonth,
    days
  };
}

数据流特点:

  1. 预加载:Dashboard 启动时加载所有设计师的项目数据到 designerWorkloadMap
  2. 快速访问:点击员工时直接从内存 Map 中读取,无需再次查询数据库
  3. 完整性generateEmployeeDetail 返回的数据结构与 EmployeeDetail 接口完全匹配
  4. 日历算法:基于项目的 createdAtdeadline 填充整个生命周期的日期
  5. 问卷查询:异步查询 ProfileSurveyLog

1.4 样式设计特点

// 📍 位置:employee-detail-panel.scss

// ⭐ 关键:使用固定类名的层级结构
.employee-detail-overlay {
  position: fixed;
  z-index: 1100;
  // ... 遮罩层样式
  
  .employee-detail-panel {
    background: #ffffff;
    border-radius: 16px;
    max-width: 600px;
    
    .panel-header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      // ... 头部样式
    }
    
    .panel-content {
      padding: 24px;
      
      .section {
        margin-bottom: 24px;
        
        .section-header {
          display: flex;
          align-items: center;
          gap: 8px;
          // ... 区块头部样式
        }
        
        // ⭐ 关键:各个数据区块的样式
        &.workload-section { /* ... */ }
        &.calendar-section { /* ... */ }
        &.leave-section { /* ... */ }
        &.explanation-section { /* ... */ }
        &.survey-section { /* ... */ }
      }
    }
  }
}

// ⭐ 关键:日历组件样式
.employee-calendar {
  .calendar-month-header { /* ... */ }
  .calendar-weekdays { /* ... */ }
  .calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    
    .calendar-day {
      aspect-ratio: 1;
      
      &.has-projects {
        background: #e0f2fe;
        border-color: #0284c7;
      }
      
      &.today {
        background: #fef3c7;
        border: 2px solid #f59e0b;
      }
      
      .day-badge {
        font-size: 11px;
        background: #3b82f6;
        color: white;
        // ... 项目徽章样式
      }
    }
  }
}

样式特点:

  • 层级清晰:通过嵌套 SCSS 保持样式层级与 HTML 结构一致
  • 命名规范:使用语义化的 BEM 风格类名
  • 封装性:所有样式都在 .employee-detail-overlay.employee-detail-panel
  • 响应式:使用 flexbox 和 grid 布局
  • 主题色:统一使用渐变色和品牌色

🔧 二、管理端 employee-info-panel 组件分析

2.1 组件设计架构

// 📍 位置:shared/components/employee-info-panel/employee-info-panel.component.ts

@Component({
  selector: 'app-employee-info-panel',
  standalone: true,
  imports: [CommonModule, FormsModule, DesignerCalendarComponent, EmployeeDetailPanelComponent],
  templateUrl: './employee-info-panel.component.html',
  styleUrls: ['./employee-info-panel.component.scss']
})
export class EmployeeInfoPanelComponent implements OnInit, OnChanges {
  Array = Array; // 暴露 Array 给模板
  
  // ⭐ 关键:接收的是 EmployeeFullInfo,不是 EmployeeDetail
  @Input() visible: boolean = false;
  @Input() employee: EmployeeFullInfo | null = null;
  
  // ⭐ 关键:通过 getter 转换数据格式
  get employeeDetailForTeamLeader(): TeamLeaderEmployeeDetail | null {
    if (!this.employee) return null;

    return {
      name: this.employee.realname || this.employee.name,
      currentProjects: this.employee.currentProjects || 0,
      projectNames: this.employee.projectNames || [],
      projectData: this.employee.projectData || [],
      leaveRecords: this.employee.leaveRecords || [],
      redMarkExplanation: this.employee.redMarkExplanation || '',
      calendarData: this.employee.calendarData,
      surveyCompleted: this.employee.surveyCompleted,
      surveyData: this.employee.surveyData,
      profileId: this.employee.profileId || this.employee.id
    };
  }
}

设计问题:

  • 数据转换层:需要通过 getter 将 EmployeeFullInfo 转换为 EmployeeDetail
  • 数据完整性依赖:依赖 employee 对象已经包含所有必需字段
  • 双重架构:既有自己的编辑模式,又复用展示组件

2.2 数据准备流程(管理端 Employees)

// 📍 位置:pages/admin/employees/employees.ts

// 第1步:点击员工时准备初始数据
async openEmployeeInfoPanel(emp: EmployeeFullInfo): Promise<void> {
  // ⭐ 问题:初始数据不完整,只有基础字段
  this.selectedEmployeeForPanel = {
    ...emp,
    currentProjects: 0,              // ⚠️ 初始值为 0
    projectData: [],                 // ⚠️ 初始值为空数组
    calendarData: undefined          // ⚠️ 初始值为 undefined
  };
  
  console.log(`📦 [Employees] 初始面板数据:`, {
    currentProjects: this.selectedEmployeeForPanel.currentProjects,
    projectData: this.selectedEmployeeForPanel.projectData
  });
  
  this.showEmployeeInfoPanel = true;

  // 第2步:异步加载项目数据(⚠️ 问题:延迟加载)
  if (emp.roleName === '组员' || emp.roleName === '组长') {
    try {
      console.log(`🔄 [Employees] 开始异步加载员工 ${emp.id} 的项目数据...`);
      const wl = await this.employeeService.getEmployeeWorkload(emp.id);
      
      console.log(`✅ [Employees] 查询到项目数据:`, {
        currentProjects: wl.currentProjects,
        ongoingProjects数量: wl.ongoingProjects.length,
        ongoingProjects列表: wl.ongoingProjects.map(p => p.name)
      });
      
      // 第3步:生成日历数据
      const calendarData = this.buildCalendarData(wl.ongoingProjects || []);
      
      // 第4步:更新面板数据
      this.selectedEmployeeForPanel = {
        ...this.selectedEmployeeForPanel!,
        currentProjects: wl.currentProjects || 0,
        projectData: coreProjects,
        calendarData: calendarData
      };
      
      console.log(`🎯 [Employees] 面板数据已更新:`, {
        currentProjects: this.selectedEmployeeForPanel.currentProjects,
        projectData数量: this.selectedEmployeeForPanel.projectData?.length,
        calendarData: this.selectedEmployeeForPanel.calendarData ? '已生成' : '未生成'
      });
    } catch (err) {
      console.error(`❌ [Employees] 刷新员工项目数据失败:`, err);
    }
  }
}

// 日历数据构建(⚠️ 问题:算法与组长端不一致)
private buildCalendarData(projects: Array<any>): { currentMonth: Date; days: any[] } {
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth();
  
  // ⚠️ 问题:只基于 deadline 填充日期,没有考虑项目整个生命周期
  const dayMap = new Map<string, Array<any>>();
  for (const p of projects) {
    const dd = toDate(p.deadline);
    if (!dd) continue;
    const key = normalizeDateKey(new Date(dd.getFullYear(), dd.getMonth(), dd.getDate()));
    if (!dayMap.has(key)) dayMap.set(key, []);
    dayMap.get(key)!.push({ id: p.id, name: p.name, deadline: dd });
  }
  
  // ⚠️ 问题:未填充上月和下月日期,日历网格可能不对齐
  const days = [];
  for (let day = 1; day <= daysInMonth; day++) {
    const date = new Date(year, month, day);
    const key = normalizeDateKey(date);
    const dayProjects = dayMap.get(key) || [];
    
    days.push({
      date,
      projectCount: dayProjects.length,
      projects: dayProjects,
      isToday: this.isSameDay(date, now),
      isCurrentMonth: true
    });
  }
  
  return { currentMonth: now, days };
}

数据流问题:

  1. 延迟加载:面板先显示初始空数据,然后异步更新(用户会看到闪烁)
  2. 算法不一致:日历生成算法与组长端不同(只基于 deadline,而非整个生命周期)
  3. 缺少填充:没有填充上月/下月日期,导致日历网格可能不对齐
  4. 错误处理不足:异步加载失败时,用户看到的是空数据,没有提示

2.3 HTML 模板复用方式

<!-- 📍 位置:employee-info-panel.component.html -->

<!-- ⭐ 当前方式:直接在 HTML 中复制粘贴组长端的结构 -->
@if (activeTab === 'workload') {
  <div class="tab-content workload-tab">
    @if (employeeDetailForTeamLeader) {
      <!-- 🎯 严格复用组长端组件的内容部分 -->
      <div class="embedded-panel-content">
        
        <!-- 负载概况栏 -->
        <div class="section workload-section">
          <div class="section-header">
            <svg>...</svg>
            <h4>负载概况</h4>
          </div>
          <div class="workload-info">
            <div class="workload-stat">
              <span class="stat-label">当前负责项目数:</span>
              <span class="stat-value" [class]="employeeDetailForTeamLeader.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
                {{ employeeDetailForTeamLeader.currentProjects }} 个
              </span>
            </div>
            @if (employeeDetailForTeamLeader.projectData && employeeDetailForTeamLeader.projectData.length > 0) {
              <!-- ⚠️ 问题:直接复制了所有的 HTML 结构,长达 400+ 行 -->
              <!-- ... 项目列表、日历、请假记录、问卷等 ... -->
            }
          </div>
        </div>
        
        <!-- 日历、请假、问卷等区块 ... (全部复制粘贴) -->
        
      </div>
    }
  </div>
}

模板问题:

  • 代码重复:将 employee-detail-panel.html 的内容完全复制粘贴到 employee-info-panel.component.html
  • 维护困难:如果组长端组件更新,需要同步修改两处
  • 不是真正的复用:没有使用 <app-employee-detail-panel> 组件,而是复制其模板

正确的复用方式应该是:

<!-- ❌ 错误方式:复制粘贴 HTML -->
<div class="embedded-panel-content">
  <!-- 400+ 行复制的代码 -->
</div>

<!-- ✅ 正确方式:使用组件 -->
<app-employee-detail-panel
  [visible]="true"
  [employeeDetail]="employeeDetailForTeamLeader"
  [embedMode]="true"
  (projectClick)="onProjectClick($event)"
  (calendarMonthChange)="onChangeMonth($event)">
</app-employee-detail-panel>

2.4 样式复用方式

// 📍 位置:employee-info-panel.component.scss

// ⭐ 当前方式:通过 @import 引入组长端样式
@import '../../../pages/team-leader/employee-detail-panel/employee-detail-panel.scss';

// 🎯 嵌入内容样式适配
.embedded-panel-content {
  width: 100%;
  padding: 0;
  
  // ⚠️ 问题:重新定义了 .section 等类的样式,与引入的样式冲突
  .section {
    margin-bottom: 20px;
    background: #fff;
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    // ... 其他样式
  }
  
  .section-header {
    display: flex;
    align-items: center;
    // ... 重复定义
  }
}

样式问题:

  • 样式冲突:既引入了 employee-detail-panel.scss,又在本文件中重新定义相同的类
  • 优先级问题:本地定义的样式可能覆盖引入的样式,导致显示不一致
  • 维护困难:需要同时维护两份样式代码
  • 路径依赖:直接引入其他模块的 SCSS 文件,破坏了模块封装性

🚨 三、问题根本原因分析

3.1 数据流问题

问题 组长端 管理端 影响
数据加载时机 ✅ 预加载到 Map,点击时立即显示 ❌ 点击后异步加载,先显示空数据 用户看到数据闪烁
数据完整性 generateEmployeeDetail 返回完整数据 ⚠️ 初始数据为空,异步更新 初始渲染不完整
日历算法 ✅ 基于项目整个生命周期(createdAt ~ deadline) ❌ 只基于 deadline 单日 日历显示不完整
日历填充 ✅ 填充上月/下月日期对齐网格 ❌ 只有当月日期 日历网格可能错位
错误处理 ✅ try-catch + 默认值 ⚠️ catch 后无提示 用户看不到错误

3.2 组件复用问题

问题 当前实现 预期实现 影响
复用方式 ❌ 复制粘贴 HTML(400+ 行) ✅ 使用 <app-employee-detail-panel> 代码重复,难以维护
数据转换 ⚠️ getter 转换 ✅ 数据准备阶段转换 getter 每次调用都计算
样式复用 @import + 重新定义 ✅ 组件自带样式 样式冲突和不一致
事件处理 ⚠️ 手动绑定到复制的 HTML ✅ 通过 @Output 自动处理 事件逻辑重复

3.3 数据结构对比

// ⭐ 组长端:数据准备完整
const employeeDetail: EmployeeDetail = {
  name: '张三',
  currentProjects: 3,
  projectData: [
    { id: 'p1', name: '项目A' },
    { id: 'p2', name: '项目B' },
    { id: 'p3', name: '项目C' }
  ],
  calendarData: {
    currentMonth: new Date(2025, 10, 1),
    days: [
      {
        date: new Date(2025, 10, 1),
        projectCount: 2,
        projects: [/* ... */],
        isToday: false,
        isCurrentMonth: true
      },
      // ... 完整的 30+ 天数据
    ]
  },
  surveyData: { /* 完整问卷数据 */ }
};

// ⚠️ 管理端:初始数据不完整
const employeeFullInfo: EmployeeFullInfo = {
  id: 'e1',
  name: '张三',
  realname: '张三',
  // ⚠️ 以下字段在初始时为空
  currentProjects: 0,              // ❌ 初始值
  projectData: [],                 // ❌ 初始值
  calendarData: undefined,         // ❌ 初始值
  surveyCompleted: undefined,      // ❌ 未查询
  surveyData: undefined            // ❌ 未查询
};

// 异步加载后更新(用户会看到数据变化)
setTimeout(async () => {
  employeeFullInfo.currentProjects = 3;
  employeeFullInfo.projectData = [/* ... */];
  employeeFullInfo.calendarData = { /* ... */ };
}, 1000);

💡 四、解决方案建议

方案 A:完全复用组件(推荐)⭐

实现步骤:

  1. 修改 employee-info-panel.component.html

    <!-- ❌ 删除:400+ 行复制的 HTML -->
    <div class="embedded-panel-content">
    <!-- ... 所有复制的代码 ... -->
    </div>
    
    <!-- ✅ 改为:直接使用组件 -->
    @if (activeTab === 'workload') {
    <div class="tab-content workload-tab">
    @if (employeeDetailForTeamLeader) {
      <app-employee-detail-panel
        [visible]="true"
        [employeeDetail]="employeeDetailForTeamLeader"
        [embedMode]="true"
        (close)="onClose()"
        (projectClick)="onProjectClick($event)"
        (calendarMonthChange)="onChangeMonth($event)"
        (calendarDayClick)="onCalendarDayClick($event)"
        (refreshSurvey)="onRefreshSurvey()">
      </app-employee-detail-panel>
    } @else {
      <div class="loading-state">加载中...</div>
    }
    </div>
    }
    
  2. 修改 employee-info-panel.component.scss

    // ❌ 删除:所有重复的样式定义
    .embedded-panel-content { /* ... */ }
    .section { /* ... */ }
    .section-header { /* ... */ }
    // ... 删除所有与 employee-detail-panel 重复的样式
    
    // ✅ 保留:仅保留面板框架样式
    .employee-info-panel {
    .panel-header { /* ... */ }
    .panel-tabs { /* ... */ }
      
    .tab-content.workload-tab {
    padding: 0; // 让嵌入的组件自己控制内边距
        
    // 🎯 使用 ::ng-deep 覆盖嵌入模式下的特定样式
    ::ng-deep app-employee-detail-panel {
      .employee-detail-panel {
        box-shadow: none; // 移除阴影,因为已经在父容器中
        border-radius: 0; // 移除圆角
      }
          
      .panel-header {
        display: none; // 隐藏头部,使用父组件的头部
      }
    }
    }
    }
    
  3. 优化数据加载(关键)

    // 📍 位置:employees.ts
    
    async openEmployeeInfoPanel(emp: EmployeeFullInfo): Promise<void> {
    // ⭐ 方案1:预加载数据后再显示面板(推荐)
    if (emp.roleName === '组员' || emp.roleName === '组长') {
    try {
      // ⭐ 先加载数据
      const wl = await this.employeeService.getEmployeeWorkload(emp.id);
      const calendarData = this.buildCalendarData(wl.ongoingProjects || []);
          
      // ⭐ 查询问卷数据
      const surveyInfo = await this.loadEmployeeSurvey(emp.id);
          
      // ⭐ 准备完整数据后再显示面板
      this.selectedEmployeeForPanel = {
        ...emp,
        currentProjects: wl.currentProjects || 0,
        projectData: (wl.ongoingProjects || []).slice(0, 3).map(p => ({ id: p.id, name: p.name })),
        calendarData: calendarData,
        surveyCompleted: surveyInfo.completed,
        surveyData: surveyInfo.data
      };
          
      this.showEmployeeInfoPanel = true;
          
    } catch (err) {
      console.error(`❌ 加载员工数据失败:`, err);
      alert('加载员工数据失败,请稍后重试');
    }
    } else {
    // 非设计师角色,直接显示基础信息
    this.selectedEmployeeForPanel = { ...emp };
    this.showEmployeeInfoPanel = true;
    }
    }
    
    // ⭐ 新增:修复日历生成算法,与组长端一致
    private buildCalendarData(projects: Array<any>): EmployeeCalendarData {
    const now = new Date();
    const year = now.getFullYear();
    const month = now.getMonth();
    const daysInMonth = new Date(year, month + 1, 0).getDate();
    const firstWeekday = new Date(year, month, 1).getDay();
      
    const days: EmployeeCalendarDay[] = [];
      
    // ⭐ 关键修复:基于项目整个生命周期填充日历
    for (let day = 1; day <= daysInMonth; day++) {
    const date = new Date(year, month, day);
    const dateTime = date.getTime();
        
    // 找出该日期相关的项目
    const dayProjects = projects.filter(p => {
      const createdAt = this.parseDate(p.createdAt);
      const deadline = this.parseDate(p.deadline);
          
      if (!createdAt || !deadline) return false;
          
      // ⭐ 关键:项目在 [createdAt, deadline] 范围内的所有天都显示
      return dateTime >= createdAt.getTime() && dateTime <= deadline.getTime();
    });
        
    days.push({
      date,
      projectCount: dayProjects.length,
      projects: dayProjects.map(p => ({
        id: p.id,
        name: p.name,
        deadline: this.parseDate(p.deadline)
      })),
      isToday: this.isSameDay(date, now),
      isCurrentMonth: true
    });
    }
      
    // ⭐ 关键:填充上月和下月日期(对齐日历网格)
    const prevMonthDays: EmployeeCalendarDay[] = [];
    for (let i = 0; i < firstWeekday; i++) {
    const date = new Date(year, month, -i);
    prevMonthDays.unshift({
      date,
      projectCount: 0,
      projects: [],
      isToday: false,
      isCurrentMonth: false
    });
    }
      
    const nextMonthDays: EmployeeCalendarDay[] = [];
    const totalCells = 42; // 6 rows × 7 days
    const remainingCells = totalCells - prevMonthDays.length - days.length;
    for (let i = 1; i <= remainingCells; i++) {
    const date = new Date(year, month + 1, i);
    nextMonthDays.push({
      date,
      projectCount: 0,
      projects: [],
      isToday: false,
      isCurrentMonth: false
    });
    }
      
    return {
    currentMonth: now,
    days: [...prevMonthDays, ...days, ...nextMonthDays]
    };
    }
    
    // ⭐ 新增:加载问卷数据
    private async loadEmployeeSurvey(employeeId: string): Promise<{ completed: boolean; data: any }> {
    try {
    const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
        
    // 通过员工 ID 查找 Profile
    const profileQuery = new Parse.Query('Profile');
    profileQuery.equalTo('objectId', employeeId);
    const profile = await profileQuery.first();
        
    if (!profile) {
      return { completed: false, data: null };
    }
        
    const surveyCompleted = profile.get('surveyCompleted') || false;
        
    if (!surveyCompleted) {
      return { completed: false, data: null };
    }
        
    // 查询问卷数据
    const surveyQuery = new Parse.Query('SurveyLog');
    surveyQuery.equalTo('profile', profile.toPointer());
    surveyQuery.equalTo('type', 'survey-profile');
    surveyQuery.descending('createdAt');
    surveyQuery.limit(1);
        
    const surveyResults = await surveyQuery.find();
        
    if (surveyResults.length > 0) {
      const survey = surveyResults[0];
      return {
        completed: true,
        data: {
          answers: survey.get('answers') || [],
          createdAt: survey.get('createdAt'),
          updatedAt: survey.get('updatedAt')
        }
      };
    }
        
    return { completed: false, data: null };
    } catch (error) {
    console.error('❌ 加载问卷数据失败:', error);
    return { completed: false, data: null };
    }
    }
    

方案优势:

  • ✅ 真正的组件复用,代码量大幅减少
  • ✅ 样式自动一致,无需手动同步
  • ✅ 功能自动同步(组长端更新后自动生效)
  • ✅ 数据预加载,无闪烁
  • ✅ 日历算法一致
  • ✅ 维护成本低

方案 B:改进当前实现(次选)

如果不想修改太多代码,只修复数据流问题:

  1. 修复数据加载时机(同方案 A 的第3步)
  2. 修复日历算法(同方案 A 的 buildCalendarData
  3. 添加加载状态提示

    @if (activeTab === 'workload') {
    <div class="tab-content workload-tab">
    @if (!employeeDetailForTeamLeader) {
      <!-- ⭐ 添加加载状态 -->
      <div class="loading-state">
        <div class="spinner"></div>
        <p>正在加载员工项目数据...</p>
      </div>
    } @else {
      <div class="embedded-panel-content">
        <!-- 现有的复制代码 -->
      </div>
    }
    </div>
    }
    

📊 五、方案对比

维度 方案 A(完全复用) 方案 B(改进当前) 当前实现
代码行数 ~50 行(HTML) ~400 行(HTML) ~400 行
样式代码 0(复用) ~500 行 ~500 行
维护成本 ⭐⭐⭐⭐⭐ 极低 ⭐⭐⭐ 中等 ⭐ 极高
一致性保证 ⭐⭐⭐⭐⭐ 自动一致 ⭐⭐ 需手动同步 ⭐ 经常不一致
性能 ⭐⭐⭐⭐⭐ 数据预加载 ⭐⭐⭐⭐ 数据预加载 ⭐⭐ 异步加载闪烁
功能完整性 ⭐⭐⭐⭐⭐ 完全继承 ⭐⭐⭐ 手动实现 ⭐⭐ 部分缺失
可扩展性 ⭐⭐⭐⭐⭐ 自动扩展 ⭐⭐ 需手动扩展 ⭐ 难以扩展

🎯 六、推荐实施步骤

Step 1:修复数据流(优先级:🔴 高)

  • 实现 loadEmployeeSurvey 方法查询问卷数据
  • 修改 buildCalendarData 算法,基于项目整个生命周期
  • 修改 openEmployeeInfoPanel,数据预加载后再显示面板

Step 2:真正复用组件(优先级:🔴 高)

  • 删除 employee-info-panel.component.html 中复制的 400+ 行代码
  • 使用 <app-employee-detail-panel [embedMode]="true"> 替代
  • 删除 employee-info-panel.component.scss 中重复的样式定义

Step 3:测试验证(优先级:🟡 中)

  • 测试数据是否完整显示
  • 测试日历是否正确填充
  • 测试问卷是否正确加载
  • 对比组长端和管理端显示是否一致

Step 4:性能优化(优先级:🟢 低)

  • 添加数据缓存(避免重复查询)
  • 添加骨架屏(优化加载体验)
  • 添加错误边界(优化错误处理)

📝 七、总结

核心问题

  1. 不是真正的组件复用:复制粘贴 HTML 和样式,而非使用 <app-employee-detail-panel>
  2. 数据流不一致:组长端预加载数据,管理端异步加载导致闪烁
  3. 日历算法不一致:组长端基于项目生命周期,管理端只基于 deadline
  4. 样式冲突:同时引入和重新定义样式,导致显示不一致

解决方案

  • ✅ 使用 <app-employee-detail-panel [embedMode]="true"> 真正复用组件
  • ✅ 修改数据加载流程,预加载数据后再显示面板
  • ✅ 统一日历算法,基于项目整个生命周期填充日期
  • ✅ 删除重复的 HTML 和 CSS 代码,依赖组件自带样式

预期效果

  • ⭐ 代码量减少 80%(400+ 行 → ~50 行)
  • ⭐ 维护成本降低 90%(组长端更新自动同步)
  • ⭐ 显示 100% 一致(使用同一组件)
  • ⭐ 用户体验提升(无数据闪烁,加载更快)

📌 建议:立即实施方案 A(完全复用组件),长期收益最大!