|  | @@ -0,0 +1,1413 @@
 | 
	
		
			
				|  |  | +# 设计师端(组员端)企业微信身份识别与真实数据接入实施方案
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 文档概述
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +本文档详细说明如何为**设计师端(Designer/组员端)**优化企业微信身份识别并接入真实数据库,包括任务管理、请假申请、个人数据等功能的完整实现。
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 一、现状分析
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 1.1 设计师端当前实现情况
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 企微认证 ✅
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**已实现内容**:
 | 
	
		
			
				|  |  | +- ✅ 路由守卫:`WxworkAuthGuard`(`app.routes.ts` 第64行)
 | 
	
		
			
				|  |  | +- ✅ 组件内认证:`WxworkAuth` 实例(`dashboard.ts` 第55-72行)
 | 
	
		
			
				|  |  | +- ✅ 认证流程:`authenticateAndLoadData()` 方法(第79-96行)
 | 
	
		
			
				|  |  | +- ✅ 降级机制:认证失败时使用模拟数据
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**代码位置**:
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/pages/designer/dashboard/dashboard.ts
 | 
	
		
			
				|  |  | +export class Dashboard implements OnInit {
 | 
	
		
			
				|  |  | +  private wxAuth: WxworkAuth | null = null;
 | 
	
		
			
				|  |  | +  private currentUser: FmodeUser | null = null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor(private projectService: ProjectService) {
 | 
	
		
			
				|  |  | +    this.initAuth();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private initAuth(): void {
 | 
	
		
			
				|  |  | +    this.wxAuth = new WxworkAuth({
 | 
	
		
			
				|  |  | +      cid: 'cDL6R1hgSi'  // 公司帐套ID
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  async ngOnInit(): Promise<void> {
 | 
	
		
			
				|  |  | +    await this.authenticateAndLoadData();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 数据接入现状 ❌
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +| 功能模块 | 当前状态 | 数据来源 | 问题 |
 | 
	
		
			
				|  |  | +|---------|---------|---------|------|
 | 
	
		
			
				|  |  | +| **任务列表** | ❌ 模拟数据 | `ProjectService.getTasks()` | 返回的是硬编码的模拟数据 |
 | 
	
		
			
				|  |  | +| **待处理反馈** | ❌ 模拟数据 | `loadPendingFeedbacks()` | 使用 mockFeedbacks 数组 |
 | 
	
		
			
				|  |  | +| **代班任务** | ❌ 模拟数据 | `loadShiftTasks()` | 使用 mockShiftTasks |
 | 
	
		
			
				|  |  | +| **工作量计算** | ❌ 模拟数据 | `calculateWorkloadPercentage()` | 基于模拟任务计算 |
 | 
	
		
			
				|  |  | +| **项目时间线** | ❌ 模拟数据 | `loadProjectTimeline()` | 使用 mockTimeline |
 | 
	
		
			
				|  |  | +| **技能标签** | ❌ 模拟数据 | `PersonalBoard.loadSkillTags()` | 从 ProjectService 获取模拟数据 |
 | 
	
		
			
				|  |  | +| **绩效数据** | ❌ 模拟数据 | `PersonalBoard.loadPerformanceData()` | 从 ProjectService 获取模拟数据 |
 | 
	
		
			
				|  |  | +| **请假申请** | ❌ 未实现 | 无 | 设计师无法申请或查看请假 |
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 1.2 核心问题
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +1. **数据真实性问题**:所有数据都是模拟的,无法支持真实业务
 | 
	
		
			
				|  |  | +2. **功能缺失问题**:缺少请假申请、个人信息管理等关键功能
 | 
	
		
			
				|  |  | +3. **角色识别问题**:虽有企微认证,但未验证是否为"组员"角色
 | 
	
		
			
				|  |  | +4. **数据同步问题**:设计师的个人信息(技能、绩效)无法更新
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 二、技术方案设计
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 2.1 优化企微身份识别
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 当前问题
 | 
	
		
			
				|  |  | +- ❌ cid 硬编码在代码中(`'cDL6R1hgSi'`)
 | 
	
		
			
				|  |  | +- ❌ 未从URL参数获取cid
 | 
	
		
			
				|  |  | +- ❌ 未验证用户的"组员"角色
 | 
	
		
			
				|  |  | +- ❌ 未获取当前登录的Profile信息
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 优化方案
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**第1步:修改路由配置,支持cid参数**
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/app.routes.ts
 | 
	
		
			
				|  |  | +{
 | 
	
		
			
				|  |  | +  path: ':cid/designer',  // 添加 cid 参数
 | 
	
		
			
				|  |  | +  canActivate: [WxworkAuthGuard],
 | 
	
		
			
				|  |  | +  children: [
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +      path: 'dashboard',
 | 
	
		
			
				|  |  | +      loadComponent: () => import('./pages/designer/dashboard/dashboard')
 | 
	
		
			
				|  |  | +        .then(m => m.Dashboard),
 | 
	
		
			
				|  |  | +      title: '设计师工作台'
 | 
	
		
			
				|  |  | +    },
 | 
	
		
			
				|  |  | +    // ... 其他子路由
 | 
	
		
			
				|  |  | +  ]
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**第2步:优化Dashboard组件的认证流程**
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/pages/designer/dashboard/dashboard.ts
 | 
	
		
			
				|  |  | +import { ActivatedRoute } from '@angular/router';
 | 
	
		
			
				|  |  | +import { ProfileService } from '../../../services/profile.service';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export class Dashboard implements OnInit {
 | 
	
		
			
				|  |  | +  private wxAuth: WxworkAuth | null = null;
 | 
	
		
			
				|  |  | +  private currentUser: FmodeUser | null = null;
 | 
	
		
			
				|  |  | +  private currentProfile: FmodeObject | null = null; // 新增:当前Profile
 | 
	
		
			
				|  |  | +  private cid: string = '';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor(
 | 
	
		
			
				|  |  | +    private projectService: ProjectService,
 | 
	
		
			
				|  |  | +    private route: ActivatedRoute,  // 新增
 | 
	
		
			
				|  |  | +    private router: Router,         // 新增
 | 
	
		
			
				|  |  | +    private profileService: ProfileService  // 新增
 | 
	
		
			
				|  |  | +  ) {}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  async ngOnInit(): Promise<void> {
 | 
	
		
			
				|  |  | +    // 1. 从URL获取cid
 | 
	
		
			
				|  |  | +    this.route.paramMap.subscribe(async params => {
 | 
	
		
			
				|  |  | +      this.cid = params.get('cid') || localStorage.getItem('company') || '';
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      if (!this.cid) {
 | 
	
		
			
				|  |  | +        console.error('❌ 未找到公司ID');
 | 
	
		
			
				|  |  | +        alert('缺少公司信息,请联系管理员');
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 2. 初始化企微认证
 | 
	
		
			
				|  |  | +      this.initAuth();
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 3. 执行认证并加载数据
 | 
	
		
			
				|  |  | +      await this.authenticateAndLoadData();
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 初始化企业微信认证(修改版)
 | 
	
		
			
				|  |  | +  private initAuth(): void {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      this.wxAuth = new WxworkAuth({
 | 
	
		
			
				|  |  | +        cid: this.cid,  // 使用动态获取的cid
 | 
	
		
			
				|  |  | +        appId: 'crm'
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      console.log('✅ 设计师端企微认证初始化成功');
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 设计师端企微认证初始化失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 认证并加载数据(优化版)
 | 
	
		
			
				|  |  | +  private async authenticateAndLoadData(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      // 执行企业微信认证和登录
 | 
	
		
			
				|  |  | +      const { user, profile } = await this.wxAuth!.authenticateAndLogin();
 | 
	
		
			
				|  |  | +      this.currentUser = user;
 | 
	
		
			
				|  |  | +      this.currentProfile = profile;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (!user || !profile) {
 | 
	
		
			
				|  |  | +        console.error('❌ 设计师登录失败');
 | 
	
		
			
				|  |  | +        this.loadMockData();
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      console.log('✅ 设计师登录成功:', user.get('username'));
 | 
	
		
			
				|  |  | +      console.log('✅ Profile ID:', profile.id);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 验证角色是否为"组员"
 | 
	
		
			
				|  |  | +      if (!await this.validateDesignerRole()) {
 | 
	
		
			
				|  |  | +        alert('您不是设计师,无权访问此页面');
 | 
	
		
			
				|  |  | +        this.router.navigate(['/']);
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 缓存Profile ID
 | 
	
		
			
				|  |  | +      localStorage.setItem('Parse/ProfileId', profile.id);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 加载真实数据
 | 
	
		
			
				|  |  | +      await this.loadDashboardData();
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 设计师认证过程出错:', error);
 | 
	
		
			
				|  |  | +      // 降级到模拟数据
 | 
	
		
			
				|  |  | +      this.loadMockData();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 验证设计师(组员)角色
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async validateDesignerRole(): Promise<boolean> {
 | 
	
		
			
				|  |  | +    if (!this.currentProfile) {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    const roleName = this.currentProfile.get('roleName');
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    if (roleName !== '组员') {
 | 
	
		
			
				|  |  | +      console.warn(`⚠️ 用户角色为"${roleName}",不是"组员"`);
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    console.log('✅ 角色验证通过:组员(设计师)');
 | 
	
		
			
				|  |  | +    return true;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 2.2 任务数据真实接入
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 创建DesignerTaskService
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/services/designer-task.service.ts
 | 
	
		
			
				|  |  | +import { Injectable } from '@angular/core';
 | 
	
		
			
				|  |  | +import { FmodeParse } from 'fmode-ng/parse';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export interface DesignerTask {
 | 
	
		
			
				|  |  | +  id: string;
 | 
	
		
			
				|  |  | +  projectId: string;
 | 
	
		
			
				|  |  | +  projectName: string;
 | 
	
		
			
				|  |  | +  stage: string;
 | 
	
		
			
				|  |  | +  deadline: Date;
 | 
	
		
			
				|  |  | +  isOverdue: boolean;
 | 
	
		
			
				|  |  | +  priority: 'high' | 'medium' | 'low';
 | 
	
		
			
				|  |  | +  customerName: string;
 | 
	
		
			
				|  |  | +  space?: string; // 空间名称(如"主卧")
 | 
	
		
			
				|  |  | +  productId?: string; // 关联的Product ID
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +@Injectable({
 | 
	
		
			
				|  |  | +  providedIn: 'root'
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +export class DesignerTaskService {
 | 
	
		
			
				|  |  | +  private Parse: any = null;
 | 
	
		
			
				|  |  | +  private cid: string = '';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor() {
 | 
	
		
			
				|  |  | +    this.initParse();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private async initParse(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const { FmodeParse } = await import('fmode-ng/parse');
 | 
	
		
			
				|  |  | +      this.Parse = FmodeParse.with('nova');
 | 
	
		
			
				|  |  | +      this.cid = localStorage.getItem('company') || '';
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('DesignerTaskService: Parse初始化失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 获取当前设计师的任务列表
 | 
	
		
			
				|  |  | +   * @param designerId Profile的objectId
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  async getMyTasks(designerId: string): Promise<DesignerTask[]> {
 | 
	
		
			
				|  |  | +    if (!this.Parse) await this.initParse();
 | 
	
		
			
				|  |  | +    if (!this.Parse || !this.cid) return [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      // 方案1:从ProjectTeam表查询(设计师实际负责的项目)
 | 
	
		
			
				|  |  | +      const teamQuery = new this.Parse.Query('ProjectTeam');
 | 
	
		
			
				|  |  | +      teamQuery.equalTo('profile', this.Parse.Object.extend('Profile').createWithoutData(designerId));
 | 
	
		
			
				|  |  | +      teamQuery.notEqualTo('isDeleted', true);
 | 
	
		
			
				|  |  | +      teamQuery.include('project');
 | 
	
		
			
				|  |  | +      teamQuery.include('project.contact');
 | 
	
		
			
				|  |  | +      teamQuery.limit(1000);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const teamRecords = await teamQuery.find();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (teamRecords.length === 0) {
 | 
	
		
			
				|  |  | +        console.warn('⚠️ 未找到分配给该设计师的项目');
 | 
	
		
			
				|  |  | +        return [];
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const tasks: DesignerTask[] = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      for (const teamRecord of teamRecords) {
 | 
	
		
			
				|  |  | +        const project = teamRecord.get('project');
 | 
	
		
			
				|  |  | +        if (!project) continue;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const projectId = project.id;
 | 
	
		
			
				|  |  | +        const projectName = project.get('title') || '未命名项目';
 | 
	
		
			
				|  |  | +        const currentStage = project.get('currentStage') || '未知';
 | 
	
		
			
				|  |  | +        const deadline = project.get('deadline') || new Date();
 | 
	
		
			
				|  |  | +        const contact = project.get('contact');
 | 
	
		
			
				|  |  | +        const customerName = contact?.get('name') || '未知客户';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 查询该项目下该设计师负责的Product(空间设计产品)
 | 
	
		
			
				|  |  | +        const productQuery = new this.Parse.Query('Product');
 | 
	
		
			
				|  |  | +        productQuery.equalTo('project', project);
 | 
	
		
			
				|  |  | +        productQuery.equalTo('profile', this.Parse.Object.extend('Profile').createWithoutData(designerId));
 | 
	
		
			
				|  |  | +        productQuery.notEqualTo('isDeleted', true);
 | 
	
		
			
				|  |  | +        productQuery.containedIn('status', ['in_progress', 'awaiting_review']);
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        const products = await productQuery.find();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if (products.length === 0) {
 | 
	
		
			
				|  |  | +          // 如果没有具体的Product,创建项目级任务
 | 
	
		
			
				|  |  | +          tasks.push({
 | 
	
		
			
				|  |  | +            id: projectId,
 | 
	
		
			
				|  |  | +            projectId,
 | 
	
		
			
				|  |  | +            projectName,
 | 
	
		
			
				|  |  | +            stage: currentStage,
 | 
	
		
			
				|  |  | +            deadline: new Date(deadline),
 | 
	
		
			
				|  |  | +            isOverdue: new Date(deadline) < new Date(),
 | 
	
		
			
				|  |  | +            priority: this.calculatePriority(deadline, currentStage),
 | 
	
		
			
				|  |  | +            customerName
 | 
	
		
			
				|  |  | +          });
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +          // 如果有Product,为每个Product创建任务
 | 
	
		
			
				|  |  | +          products.forEach((product: any) => {
 | 
	
		
			
				|  |  | +            const productName = product.get('productName') || '未命名空间';
 | 
	
		
			
				|  |  | +            const productStage = product.get('stage') || currentStage;
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  | +            tasks.push({
 | 
	
		
			
				|  |  | +              id: `${projectId}-${product.id}`,
 | 
	
		
			
				|  |  | +              projectId,
 | 
	
		
			
				|  |  | +              projectName: `${projectName} - ${productName}`,
 | 
	
		
			
				|  |  | +              stage: productStage,
 | 
	
		
			
				|  |  | +              deadline: new Date(deadline),
 | 
	
		
			
				|  |  | +              isOverdue: new Date(deadline) < new Date(),
 | 
	
		
			
				|  |  | +              priority: this.calculatePriority(deadline, productStage),
 | 
	
		
			
				|  |  | +              customerName,
 | 
	
		
			
				|  |  | +              space: productName,
 | 
	
		
			
				|  |  | +              productId: product.id
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  | +          });
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 按截止日期排序
 | 
	
		
			
				|  |  | +      tasks.sort((a, b) => a.deadline.getTime() - b.deadline.getTime());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      console.log(`✅ 成功加载 ${tasks.length} 个任务`);
 | 
	
		
			
				|  |  | +      return tasks;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('获取设计师任务失败:', error);
 | 
	
		
			
				|  |  | +      return [];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 计算任务优先级
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private calculatePriority(deadline: Date, stage: string): 'high' | 'medium' | 'low' {
 | 
	
		
			
				|  |  | +    const now = new Date();
 | 
	
		
			
				|  |  | +    const daysLeft = Math.ceil((new Date(deadline).getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // 超期或临期(3天内)
 | 
	
		
			
				|  |  | +    if (daysLeft < 0 || daysLeft <= 3) {
 | 
	
		
			
				|  |  | +      return 'high';
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // 渲染阶段优先级高
 | 
	
		
			
				|  |  | +    if (stage === 'rendering' || stage === '渲染') {
 | 
	
		
			
				|  |  | +      return 'high';
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // 7天内
 | 
	
		
			
				|  |  | +    if (daysLeft <= 7) {
 | 
	
		
			
				|  |  | +      return 'medium';
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    return 'low';
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 方案2:从Project.assignee查询(如果不使用ProjectTeam表)
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  async getMyTasksFromAssignee(designerId: string): Promise<DesignerTask[]> {
 | 
	
		
			
				|  |  | +    if (!this.Parse) await this.initParse();
 | 
	
		
			
				|  |  | +    if (!this.Parse || !this.cid) return [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const query = new this.Parse.Query('Project');
 | 
	
		
			
				|  |  | +      query.equalTo('assignee', this.Parse.Object.extend('Profile').createWithoutData(designerId));
 | 
	
		
			
				|  |  | +      query.equalTo('company', this.cid);
 | 
	
		
			
				|  |  | +      query.containedIn('status', ['进行中', '待审核']);
 | 
	
		
			
				|  |  | +      query.notEqualTo('isDeleted', true);
 | 
	
		
			
				|  |  | +      query.include('contact');
 | 
	
		
			
				|  |  | +      query.ascending('deadline');
 | 
	
		
			
				|  |  | +      query.limit(1000);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const projects = await query.find();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      return projects.map((project: any) => {
 | 
	
		
			
				|  |  | +        const deadline = project.get('deadline') || new Date();
 | 
	
		
			
				|  |  | +        const currentStage = project.get('currentStage') || '未知';
 | 
	
		
			
				|  |  | +        const contact = project.get('contact');
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return {
 | 
	
		
			
				|  |  | +          id: project.id,
 | 
	
		
			
				|  |  | +          projectId: project.id,
 | 
	
		
			
				|  |  | +          projectName: project.get('title') || '未命名项目',
 | 
	
		
			
				|  |  | +          stage: currentStage,
 | 
	
		
			
				|  |  | +          deadline: new Date(deadline),
 | 
	
		
			
				|  |  | +          isOverdue: new Date(deadline) < new Date(),
 | 
	
		
			
				|  |  | +          priority: this.calculatePriority(deadline, currentStage),
 | 
	
		
			
				|  |  | +          customerName: contact?.get('name') || '未知客户'
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('从Project.assignee获取任务失败:', error);
 | 
	
		
			
				|  |  | +      return [];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 修改Dashboard组件使用真实数据
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/pages/designer/dashboard/dashboard.ts
 | 
	
		
			
				|  |  | +import { DesignerTaskService } from '../../../services/designer-task.service';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export class Dashboard implements OnInit {
 | 
	
		
			
				|  |  | +  constructor(
 | 
	
		
			
				|  |  | +    private projectService: ProjectService,
 | 
	
		
			
				|  |  | +    private route: ActivatedRoute,
 | 
	
		
			
				|  |  | +    private router: Router,
 | 
	
		
			
				|  |  | +    private profileService: ProfileService,
 | 
	
		
			
				|  |  | +    private taskService: DesignerTaskService  // 新增
 | 
	
		
			
				|  |  | +  ) {}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 加载仪表板数据(修改版)
 | 
	
		
			
				|  |  | +  private async loadDashboardData(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      if (!this.currentProfile) {
 | 
	
		
			
				|  |  | +        throw new Error('未找到当前Profile');
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      await Promise.all([
 | 
	
		
			
				|  |  | +        this.loadRealTasks(),      // 使用真实数据
 | 
	
		
			
				|  |  | +        this.loadShiftTasks(),
 | 
	
		
			
				|  |  | +        this.calculateWorkloadPercentage(),
 | 
	
		
			
				|  |  | +        this.loadProjectTimeline()
 | 
	
		
			
				|  |  | +      ]);
 | 
	
		
			
				|  |  | +      console.log('✅ 设计师仪表板数据加载完成');
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 设计师仪表板数据加载失败:', error);
 | 
	
		
			
				|  |  | +      throw error;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 加载真实任务数据
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadRealTasks(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const designerTasks = await this.taskService.getMyTasks(this.currentProfile!.id);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 转换为组件所需格式
 | 
	
		
			
				|  |  | +      this.tasks = designerTasks.map(task => ({
 | 
	
		
			
				|  |  | +        id: task.id,
 | 
	
		
			
				|  |  | +        projectId: task.projectId,
 | 
	
		
			
				|  |  | +        name: task.projectName,
 | 
	
		
			
				|  |  | +        stage: task.stage,
 | 
	
		
			
				|  |  | +        deadline: task.deadline,
 | 
	
		
			
				|  |  | +        isOverdue: task.isOverdue,
 | 
	
		
			
				|  |  | +        priority: task.priority,
 | 
	
		
			
				|  |  | +        customerName: task.customerName
 | 
	
		
			
				|  |  | +      }));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 筛选超期任务
 | 
	
		
			
				|  |  | +      this.overdueTasks = this.tasks.filter(task => task.isOverdue);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 筛选紧急任务
 | 
	
		
			
				|  |  | +      this.urgentTasks = this.tasks.filter(task => {
 | 
	
		
			
				|  |  | +        const now = new Date();
 | 
	
		
			
				|  |  | +        const diffHours = (task.deadline.getTime() - now.getTime()) / (1000 * 60 * 60);
 | 
	
		
			
				|  |  | +        return diffHours <= 3 && diffHours > 0 && task.stage === '渲染';
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 加载待处理反馈
 | 
	
		
			
				|  |  | +      await this.loadRealPendingFeedbacks();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 启动倒计时
 | 
	
		
			
				|  |  | +      this.startCountdowns();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      console.log(`✅ 成功加载 ${this.tasks.length} 个真实任务`);
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载真实任务失败:', error);
 | 
	
		
			
				|  |  | +      throw error;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 加载真实待处理反馈
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadRealPendingFeedbacks(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 查询该设计师相关项目的待处理反馈
 | 
	
		
			
				|  |  | +      const projectIds = this.tasks.map(t => t.projectId);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      const query = new Parse.Query('ProjectFeedback');
 | 
	
		
			
				|  |  | +      query.containedIn('project', projectIds.map(id => 
 | 
	
		
			
				|  |  | +        Parse.Object.extend('Project').createWithoutData(id)
 | 
	
		
			
				|  |  | +      ));
 | 
	
		
			
				|  |  | +      query.equalTo('status', '待处理');
 | 
	
		
			
				|  |  | +      query.notEqualTo('isDeleted', true);
 | 
	
		
			
				|  |  | +      query.include('project');
 | 
	
		
			
				|  |  | +      query.include('contact');
 | 
	
		
			
				|  |  | +      query.descending('createdAt');
 | 
	
		
			
				|  |  | +      query.limit(100);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      const feedbacks = await query.find();
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      this.pendingFeedbacks = feedbacks.map((feedback: any) => {
 | 
	
		
			
				|  |  | +        const project = feedback.get('project');
 | 
	
		
			
				|  |  | +        const task = this.tasks.find(t => t.projectId === project?.id);
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        return {
 | 
	
		
			
				|  |  | +          task: task || {
 | 
	
		
			
				|  |  | +            id: project?.id || '',
 | 
	
		
			
				|  |  | +            projectId: project?.id || '',
 | 
	
		
			
				|  |  | +            name: project?.get('title') || '未知项目',
 | 
	
		
			
				|  |  | +            stage: '反馈处理',
 | 
	
		
			
				|  |  | +            deadline: new Date(),
 | 
	
		
			
				|  |  | +            isOverdue: false
 | 
	
		
			
				|  |  | +          },
 | 
	
		
			
				|  |  | +          feedback: {
 | 
	
		
			
				|  |  | +            id: feedback.id,
 | 
	
		
			
				|  |  | +            content: feedback.get('content') || '',
 | 
	
		
			
				|  |  | +            rating: feedback.get('rating'),
 | 
	
		
			
				|  |  | +            createdAt: feedback.get('createdAt')
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      console.log(`✅ 成功加载 ${this.pendingFeedbacks.length} 个待处理反馈`);
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载待处理反馈失败:', error);
 | 
	
		
			
				|  |  | +      this.pendingFeedbacks = [];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 2.3 请假申请功能实现
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 创建LeaveService(设计师端使用)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/services/leave.service.ts
 | 
	
		
			
				|  |  | +import { Injectable } from '@angular/core';
 | 
	
		
			
				|  |  | +import { FmodeParse } from 'fmode-ng/parse';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export interface LeaveApplication {
 | 
	
		
			
				|  |  | +  id?: string;
 | 
	
		
			
				|  |  | +  startDate: Date;
 | 
	
		
			
				|  |  | +  endDate: Date;
 | 
	
		
			
				|  |  | +  type: 'annual' | 'sick' | 'personal' | 'other';
 | 
	
		
			
				|  |  | +  reason: string;
 | 
	
		
			
				|  |  | +  status: 'pending' | 'approved' | 'rejected';
 | 
	
		
			
				|  |  | +  days: number;
 | 
	
		
			
				|  |  | +  createdAt?: Date;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +@Injectable({
 | 
	
		
			
				|  |  | +  providedIn: 'root'
 | 
	
		
			
				|  |  | +})
 | 
	
		
			
				|  |  | +export class LeaveService {
 | 
	
		
			
				|  |  | +  private Parse: any = null;
 | 
	
		
			
				|  |  | +  private cid: string = '';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor() {
 | 
	
		
			
				|  |  | +    this.initParse();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private async initParse(): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const { FmodeParse } = await import('fmode-ng/parse');
 | 
	
		
			
				|  |  | +      this.Parse = FmodeParse.with('nova');
 | 
	
		
			
				|  |  | +      this.cid = localStorage.getItem('company') || '';
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('LeaveService: Parse初始化失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 提交请假申请
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  async submitLeaveApplication(
 | 
	
		
			
				|  |  | +    designerId: string,
 | 
	
		
			
				|  |  | +    application: Omit<LeaveApplication, 'id' | 'status' | 'createdAt'>
 | 
	
		
			
				|  |  | +  ): Promise<boolean> {
 | 
	
		
			
				|  |  | +    if (!this.Parse) await this.initParse();
 | 
	
		
			
				|  |  | +    if (!this.Parse || !this.cid) return false;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      // 方案1:添加到Profile.data.leave.records
 | 
	
		
			
				|  |  | +      const query = new this.Parse.Query('Profile');
 | 
	
		
			
				|  |  | +      const profile = await query.get(designerId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const data = profile.get('data') || {};
 | 
	
		
			
				|  |  | +      const leaveData = data.leave || { records: [], statistics: {} };
 | 
	
		
			
				|  |  | +      const records = leaveData.records || [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 生成请假日期列表
 | 
	
		
			
				|  |  | +      const leaveDates: string[] = [];
 | 
	
		
			
				|  |  | +      const currentDate = new Date(application.startDate);
 | 
	
		
			
				|  |  | +      const endDate = new Date(application.endDate);
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      while (currentDate <= endDate) {
 | 
	
		
			
				|  |  | +        leaveDates.push(currentDate.toISOString().split('T')[0]);
 | 
	
		
			
				|  |  | +        currentDate.setDate(currentDate.getDate() + 1);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 添加每一天的请假记录
 | 
	
		
			
				|  |  | +      leaveDates.forEach(date => {
 | 
	
		
			
				|  |  | +        records.push({
 | 
	
		
			
				|  |  | +          id: `leave-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
 | 
	
		
			
				|  |  | +          date,
 | 
	
		
			
				|  |  | +          type: application.type,
 | 
	
		
			
				|  |  | +          status: 'pending', // 待审批
 | 
	
		
			
				|  |  | +          reason: application.reason,
 | 
	
		
			
				|  |  | +          createdAt: new Date().toISOString(),
 | 
	
		
			
				|  |  | +          days: application.days
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      leaveData.records = records;
 | 
	
		
			
				|  |  | +      data.leave = leaveData;
 | 
	
		
			
				|  |  | +      profile.set('data', data);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      await profile.save();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      console.log('✅ 请假申请提交成功');
 | 
	
		
			
				|  |  | +      return true;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 提交请假申请失败:', error);
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 获取我的请假记录
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  async getMyLeaveRecords(designerId: string): Promise<LeaveApplication[]> {
 | 
	
		
			
				|  |  | +    if (!this.Parse) await this.initParse();
 | 
	
		
			
				|  |  | +    if (!this.Parse) return [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const query = new this.Parse.Query('Profile');
 | 
	
		
			
				|  |  | +      const profile = await query.get(designerId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const data = profile.get('data') || {};
 | 
	
		
			
				|  |  | +      const leaveData = data.leave || { records: [] };
 | 
	
		
			
				|  |  | +      const records = leaveData.records || [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 按日期聚合成请假申请
 | 
	
		
			
				|  |  | +      const applications = new Map<string, LeaveApplication>();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      records.forEach((record: any) => {
 | 
	
		
			
				|  |  | +        const key = `${record.type}-${record.reason}-${record.status}`;
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        if (!applications.has(key)) {
 | 
	
		
			
				|  |  | +          applications.set(key, {
 | 
	
		
			
				|  |  | +            id: record.id,
 | 
	
		
			
				|  |  | +            startDate: new Date(record.date),
 | 
	
		
			
				|  |  | +            endDate: new Date(record.date),
 | 
	
		
			
				|  |  | +            type: record.type,
 | 
	
		
			
				|  |  | +            reason: record.reason,
 | 
	
		
			
				|  |  | +            status: record.status,
 | 
	
		
			
				|  |  | +            days: 1,
 | 
	
		
			
				|  |  | +            createdAt: new Date(record.createdAt)
 | 
	
		
			
				|  |  | +          });
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +          const app = applications.get(key)!;
 | 
	
		
			
				|  |  | +          const recordDate = new Date(record.date);
 | 
	
		
			
				|  |  | +          
 | 
	
		
			
				|  |  | +          if (recordDate < app.startDate) {
 | 
	
		
			
				|  |  | +            app.startDate = recordDate;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +          if (recordDate > app.endDate) {
 | 
	
		
			
				|  |  | +            app.endDate = recordDate;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +          app.days++;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      return Array.from(applications.values())
 | 
	
		
			
				|  |  | +        .sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 获取请假记录失败:', error);
 | 
	
		
			
				|  |  | +      return [];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 计算请假天数(排除周末)
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  calculateLeaveDays(startDate: Date, endDate: Date): number {
 | 
	
		
			
				|  |  | +    let days = 0;
 | 
	
		
			
				|  |  | +    const currentDate = new Date(startDate);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    while (currentDate <= endDate) {
 | 
	
		
			
				|  |  | +      const dayOfWeek = currentDate.getDay();
 | 
	
		
			
				|  |  | +      // 排除周六(6)和周日(0)
 | 
	
		
			
				|  |  | +      if (dayOfWeek !== 0 && dayOfWeek !== 6) {
 | 
	
		
			
				|  |  | +        days++;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      currentDate.setDate(currentDate.getDate() + 1);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return days;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 在Dashboard中添加请假申请功能
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/pages/designer/dashboard/dashboard.ts
 | 
	
		
			
				|  |  | +import { LeaveService, LeaveApplication } from '../../../services/leave.service';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +export class Dashboard implements OnInit {
 | 
	
		
			
				|  |  | +  // 请假相关
 | 
	
		
			
				|  |  | +  showLeaveModal: boolean = false;
 | 
	
		
			
				|  |  | +  leaveApplications: LeaveApplication[] = [];
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  // 请假表单
 | 
	
		
			
				|  |  | +  leaveForm = {
 | 
	
		
			
				|  |  | +    startDate: '',
 | 
	
		
			
				|  |  | +    endDate: '',
 | 
	
		
			
				|  |  | +    type: 'personal' as 'annual' | 'sick' | 'personal' | 'other',
 | 
	
		
			
				|  |  | +    reason: ''
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor(
 | 
	
		
			
				|  |  | +    private projectService: ProjectService,
 | 
	
		
			
				|  |  | +    private route: ActivatedRoute,
 | 
	
		
			
				|  |  | +    private router: Router,
 | 
	
		
			
				|  |  | +    private profileService: ProfileService,
 | 
	
		
			
				|  |  | +    private taskService: DesignerTaskService,
 | 
	
		
			
				|  |  | +    private leaveService: LeaveService  // 新增
 | 
	
		
			
				|  |  | +  ) {}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 打开请假申请弹窗
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  openLeaveModal(): void {
 | 
	
		
			
				|  |  | +    this.showLeaveModal = true;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 关闭请假申请弹窗
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  closeLeaveModal(): void {
 | 
	
		
			
				|  |  | +    this.showLeaveModal = false;
 | 
	
		
			
				|  |  | +    this.resetLeaveForm();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 提交请假申请
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  async submitLeaveApplication(): Promise<void> {
 | 
	
		
			
				|  |  | +    if (!this.currentProfile) {
 | 
	
		
			
				|  |  | +      alert('未找到当前用户信息');
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 验证表单
 | 
	
		
			
				|  |  | +    if (!this.leaveForm.startDate || !this.leaveForm.endDate) {
 | 
	
		
			
				|  |  | +      alert('请选择请假日期');
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (!this.leaveForm.reason.trim()) {
 | 
	
		
			
				|  |  | +      alert('请输入请假原因');
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const startDate = new Date(this.leaveForm.startDate);
 | 
	
		
			
				|  |  | +    const endDate = new Date(this.leaveForm.endDate);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (startDate > endDate) {
 | 
	
		
			
				|  |  | +      alert('结束日期不能早于开始日期');
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 计算请假天数
 | 
	
		
			
				|  |  | +    const days = this.leaveService.calculateLeaveDays(startDate, endDate);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (days === 0) {
 | 
	
		
			
				|  |  | +      alert('请假天数必须大于0(周末不计入)');
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const success = await this.leaveService.submitLeaveApplication(
 | 
	
		
			
				|  |  | +        this.currentProfile.id,
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +          startDate,
 | 
	
		
			
				|  |  | +          endDate,
 | 
	
		
			
				|  |  | +          type: this.leaveForm.type,
 | 
	
		
			
				|  |  | +          reason: this.leaveForm.reason,
 | 
	
		
			
				|  |  | +          days
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (success) {
 | 
	
		
			
				|  |  | +        alert(`请假申请已提交!共${days}天(已排除周末)`);
 | 
	
		
			
				|  |  | +        this.closeLeaveModal();
 | 
	
		
			
				|  |  | +        await this.loadMyLeaveRecords();
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        alert('请假申请提交失败,请重试');
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('提交请假申请失败:', error);
 | 
	
		
			
				|  |  | +      alert('请假申请提交失败,请重试');
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 加载我的请假记录
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadMyLeaveRecords(): Promise<void> {
 | 
	
		
			
				|  |  | +    if (!this.currentProfile) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      this.leaveApplications = await this.leaveService.getMyLeaveRecords(
 | 
	
		
			
				|  |  | +        this.currentProfile.id
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +      console.log(`✅ 成功加载 ${this.leaveApplications.length} 条请假记录`);
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载请假记录失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 重置请假表单
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private resetLeaveForm(): void {
 | 
	
		
			
				|  |  | +    this.leaveForm = {
 | 
	
		
			
				|  |  | +      startDate: '',
 | 
	
		
			
				|  |  | +      endDate: '',
 | 
	
		
			
				|  |  | +      type: 'personal',
 | 
	
		
			
				|  |  | +      reason: ''
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 请假申请UI(添加到dashboard.html)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```html
 | 
	
		
			
				|  |  | +<!-- 请假申请按钮 -->
 | 
	
		
			
				|  |  | +<button class="leave-btn" (click)="openLeaveModal()">
 | 
	
		
			
				|  |  | +  <svg viewBox="0 0 24 24">
 | 
	
		
			
				|  |  | +    <path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/>
 | 
	
		
			
				|  |  | +  </svg>
 | 
	
		
			
				|  |  | +  申请请假
 | 
	
		
			
				|  |  | +</button>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<!-- 请假申请弹窗 -->
 | 
	
		
			
				|  |  | +@if (showLeaveModal) {
 | 
	
		
			
				|  |  | +  <div class="modal-overlay" (click)="closeLeaveModal()">
 | 
	
		
			
				|  |  | +    <div class="modal-content leave-modal" (click)="$event.stopPropagation()">
 | 
	
		
			
				|  |  | +      <div class="modal-header">
 | 
	
		
			
				|  |  | +        <h3>申请请假</h3>
 | 
	
		
			
				|  |  | +        <button class="close-btn" (click)="closeLeaveModal()">×</button>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      <div class="modal-body">
 | 
	
		
			
				|  |  | +        <div class="form-group">
 | 
	
		
			
				|  |  | +          <label>请假类型</label>
 | 
	
		
			
				|  |  | +          <select [(ngModel)]="leaveForm.type" class="form-control">
 | 
	
		
			
				|  |  | +            <option value="annual">年假</option>
 | 
	
		
			
				|  |  | +            <option value="sick">病假</option>
 | 
	
		
			
				|  |  | +            <option value="personal">事假</option>
 | 
	
		
			
				|  |  | +            <option value="other">其他</option>
 | 
	
		
			
				|  |  | +          </select>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        <div class="form-group">
 | 
	
		
			
				|  |  | +          <label>开始日期</label>
 | 
	
		
			
				|  |  | +          <input 
 | 
	
		
			
				|  |  | +            type="date" 
 | 
	
		
			
				|  |  | +            [(ngModel)]="leaveForm.startDate" 
 | 
	
		
			
				|  |  | +            class="form-control"
 | 
	
		
			
				|  |  | +            [min]="today"
 | 
	
		
			
				|  |  | +          >
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        <div class="form-group">
 | 
	
		
			
				|  |  | +          <label>结束日期</label>
 | 
	
		
			
				|  |  | +          <input 
 | 
	
		
			
				|  |  | +            type="date" 
 | 
	
		
			
				|  |  | +            [(ngModel)]="leaveForm.endDate" 
 | 
	
		
			
				|  |  | +            class="form-control"
 | 
	
		
			
				|  |  | +            [min]="leaveForm.startDate || today"
 | 
	
		
			
				|  |  | +          >
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        <div class="form-group">
 | 
	
		
			
				|  |  | +          <label>请假原因</label>
 | 
	
		
			
				|  |  | +          <textarea 
 | 
	
		
			
				|  |  | +            [(ngModel)]="leaveForm.reason" 
 | 
	
		
			
				|  |  | +            class="form-control" 
 | 
	
		
			
				|  |  | +            rows="4"
 | 
	
		
			
				|  |  | +            placeholder="请输入请假原因..."
 | 
	
		
			
				|  |  | +          ></textarea>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      <div class="modal-footer">
 | 
	
		
			
				|  |  | +        <button class="btn btn-cancel" (click)="closeLeaveModal()">取消</button>
 | 
	
		
			
				|  |  | +        <button class="btn btn-submit" (click)="submitLeaveApplication()">提交申请</button>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +    </div>
 | 
	
		
			
				|  |  | +  </div>
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<!-- 我的请假记录 -->
 | 
	
		
			
				|  |  | +<div class="leave-records">
 | 
	
		
			
				|  |  | +  <h4>我的请假记录</h4>
 | 
	
		
			
				|  |  | +  @if (leaveApplications.length > 0) {
 | 
	
		
			
				|  |  | +    <div class="records-list">
 | 
	
		
			
				|  |  | +      @for (leave of leaveApplications; track leave.id) {
 | 
	
		
			
				|  |  | +        <div class="record-item" [class]="leave.status">
 | 
	
		
			
				|  |  | +          <div class="record-header">
 | 
	
		
			
				|  |  | +            <span class="record-type">{{ getLeaveTypeText(leave.type) }}</span>
 | 
	
		
			
				|  |  | +            <span class="record-status" [class]="leave.status">
 | 
	
		
			
				|  |  | +              {{ getLeaveStatusText(leave.status) }}
 | 
	
		
			
				|  |  | +            </span>
 | 
	
		
			
				|  |  | +          </div>
 | 
	
		
			
				|  |  | +          <div class="record-body">
 | 
	
		
			
				|  |  | +            <p class="record-date">
 | 
	
		
			
				|  |  | +              {{ leave.startDate | date:'yyyy-MM-dd' }} 至 {{ leave.endDate | date:'yyyy-MM-dd' }}
 | 
	
		
			
				|  |  | +              (共{{ leave.days }}天)
 | 
	
		
			
				|  |  | +            </p>
 | 
	
		
			
				|  |  | +            <p class="record-reason">{{ leave.reason }}</p>
 | 
	
		
			
				|  |  | +          </div>
 | 
	
		
			
				|  |  | +          <div class="record-footer">
 | 
	
		
			
				|  |  | +            <span class="record-time">{{ leave.createdAt | date:'yyyy-MM-dd HH:mm' }}</span>
 | 
	
		
			
				|  |  | +          </div>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    </div>
 | 
	
		
			
				|  |  | +  } @else {
 | 
	
		
			
				|  |  | +    <p class="no-records">暂无请假记录</p>
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +</div>
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 2.4 个人数据(技能标签、绩效)真实接入
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#### 从Profile.data读取真实技能标签
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// src/app/pages/designer/personal-board/personal-board.ts
 | 
	
		
			
				|  |  | +export class PersonalBoard implements OnInit {
 | 
	
		
			
				|  |  | +  private currentProfile: FmodeObject | null = null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  async ngOnInit(): Promise<void> {
 | 
	
		
			
				|  |  | +    // 获取当前Profile
 | 
	
		
			
				|  |  | +    const profileId = localStorage.getItem('Parse/ProfileId');
 | 
	
		
			
				|  |  | +    if (profileId) {
 | 
	
		
			
				|  |  | +      await this.loadCurrentProfile(profileId);
 | 
	
		
			
				|  |  | +      await this.loadRealSkillTags();
 | 
	
		
			
				|  |  | +      await this.loadRealPerformanceData();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      // 降级到模拟数据
 | 
	
		
			
				|  |  | +      this.loadSkillTags();
 | 
	
		
			
				|  |  | +      this.loadPerformanceData();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 加载当前Profile
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadCurrentProfile(profileId: string): Promise<void> {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
 | 
	
		
			
				|  |  | +      const query = new Parse.Query('Profile');
 | 
	
		
			
				|  |  | +      this.currentProfile = await query.get(profileId);
 | 
	
		
			
				|  |  | +      console.log('✅ 成功加载Profile');
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载Profile失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 从Profile.data.tags读取真实技能标签
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadRealSkillTags(): Promise<void> {
 | 
	
		
			
				|  |  | +    if (!this.currentProfile) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const data = this.currentProfile.get('data') || {};
 | 
	
		
			
				|  |  | +      const tags = data.tags || {};
 | 
	
		
			
				|  |  | +      const expertise = tags.expertise || {};
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 转换为组件所需格式
 | 
	
		
			
				|  |  | +      this.skillTags = [];
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 添加擅长风格
 | 
	
		
			
				|  |  | +      (expertise.styles || []).forEach((style: string) => {
 | 
	
		
			
				|  |  | +        this.skillTags.push({
 | 
	
		
			
				|  |  | +          name: style,
 | 
	
		
			
				|  |  | +          level: 85, // 可以从数据中读取实际等级
 | 
	
		
			
				|  |  | +          category: '风格'
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 添加专业技能
 | 
	
		
			
				|  |  | +      (expertise.skills || []).forEach((skill: string) => {
 | 
	
		
			
				|  |  | +        this.skillTags.push({
 | 
	
		
			
				|  |  | +          name: skill,
 | 
	
		
			
				|  |  | +          level: 80,
 | 
	
		
			
				|  |  | +          category: '技能'
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      // 添加擅长空间
 | 
	
		
			
				|  |  | +      (expertise.spaceTypes || []).forEach((space: string) => {
 | 
	
		
			
				|  |  | +        this.skillTags.push({
 | 
	
		
			
				|  |  | +          name: space,
 | 
	
		
			
				|  |  | +          level: 75,
 | 
	
		
			
				|  |  | +          category: '空间'
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      console.log(`✅ 成功加载 ${this.skillTags.length} 个技能标签`);
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载技能标签失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * 从Profile.data.tags.history读取真实绩效数据
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private async loadRealPerformanceData(): Promise<void> {
 | 
	
		
			
				|  |  | +    if (!this.currentProfile) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const data = this.currentProfile.get('data') || {};
 | 
	
		
			
				|  |  | +      const tags = data.tags || {};
 | 
	
		
			
				|  |  | +      const history = tags.history || {};
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      this.performanceData = {
 | 
	
		
			
				|  |  | +        totalProjects: history.totalProjects || 0,
 | 
	
		
			
				|  |  | +        completionRate: history.completionRate || 0,
 | 
	
		
			
				|  |  | +        onTimeRate: history.onTimeRate || 0,
 | 
	
		
			
				|  |  | +        excellentRate: history.excellentCount 
 | 
	
		
			
				|  |  | +          ? (history.excellentCount / history.totalProjects * 100) 
 | 
	
		
			
				|  |  | +          : 0,
 | 
	
		
			
				|  |  | +        avgRating: history.avgRating || 0
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      console.log('✅ 成功加载绩效数据');
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('❌ 加载绩效数据失败:', error);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 三、实施步骤
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 阶段1:优化企微认证(第1天上午)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**任务清单**:
 | 
	
		
			
				|  |  | +- [ ] 修改路由配置,添加 `:cid` 参数
 | 
	
		
			
				|  |  | +- [ ] 优化Dashboard的 `initAuth()` 方法,支持动态cid
 | 
	
		
			
				|  |  | +- [ ] 实现 `validateDesignerRole()` 角色验证
 | 
	
		
			
				|  |  | +- [ ] 测试企微授权流程
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**验收标准**:
 | 
	
		
			
				|  |  | +- ✅ 访问设计师端自动触发企微授权
 | 
	
		
			
				|  |  | +- ✅ 非组员角色无法访问
 | 
	
		
			
				|  |  | +- ✅ 可以正确获取当前Profile信息
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 阶段2:任务数据真实接入(第1天下午)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**任务清单**:
 | 
	
		
			
				|  |  | +- [ ] 创建 `DesignerTaskService`
 | 
	
		
			
				|  |  | +- [ ] 实现 `getMyTasks()` 方法
 | 
	
		
			
				|  |  | +- [ ] 修改Dashboard的 `loadRealTasks()` 方法
 | 
	
		
			
				|  |  | +- [ ] 实现 `loadRealPendingFeedbacks()` 方法
 | 
	
		
			
				|  |  | +- [ ] 测试任务数据加载
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**验收标准**:
 | 
	
		
			
				|  |  | +- ✅ 可以看到真实分配的任务
 | 
	
		
			
				|  |  | +- ✅ 超期任务、紧急任务正确标识
 | 
	
		
			
				|  |  | +- ✅ 待处理反馈正确显示
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 阶段3:请假申请功能(第2天上午)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**任务清单**:
 | 
	
		
			
				|  |  | +- [ ] 创建 `LeaveService`
 | 
	
		
			
				|  |  | +- [ ] 实现请假申请表单UI
 | 
	
		
			
				|  |  | +- [ ] 实现 `submitLeaveApplication()` 方法
 | 
	
		
			
				|  |  | +- [ ] 实现请假记录查看功能
 | 
	
		
			
				|  |  | +- [ ] 测试请假申请流程
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**验收标准**:
 | 
	
		
			
				|  |  | +- ✅ 设计师可以提交请假申请
 | 
	
		
			
				|  |  | +- ✅ 请假记录正确保存
 | 
	
		
			
				|  |  | +- ✅ 可以查看自己的请假历史
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 阶段4:个人数据真实接入(第2天下午)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**任务清单**:
 | 
	
		
			
				|  |  | +- [ ] 修改PersonalBoard的数据加载逻辑
 | 
	
		
			
				|  |  | +- [ ] 实现 `loadRealSkillTags()` 方法
 | 
	
		
			
				|  |  | +- [ ] 实现 `loadRealPerformanceData()` 方法
 | 
	
		
			
				|  |  | +- [ ] 测试个人看板数据显示
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**验收标准**:
 | 
	
		
			
				|  |  | +- ✅ 技能标签从Profile.data读取
 | 
	
		
			
				|  |  | +- ✅ 绩效数据正确显示
 | 
	
		
			
				|  |  | +- ✅ 数据更新后立即反映
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 阶段5:测试与优化(第3天)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**任务清单**:
 | 
	
		
			
				|  |  | +- [ ] 全流程测试
 | 
	
		
			
				|  |  | +- [ ] 性能优化(缓存、懒加载)
 | 
	
		
			
				|  |  | +- [ ] UI/UX优化
 | 
	
		
			
				|  |  | +- [ ] 错误处理完善
 | 
	
		
			
				|  |  | +- [ ] 编写使用文档
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 四、数据初始化
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 为现有设计师初始化数据
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// scripts/init-designer-data.ts
 | 
	
		
			
				|  |  | +import { FmodeParse } from 'fmode-ng/parse';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async function initDesignerData() {
 | 
	
		
			
				|  |  | +  const Parse = FmodeParse.with('nova');
 | 
	
		
			
				|  |  | +  const cid = 'cDL6R1hgSi'; // 公司ID
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 查询所有组员
 | 
	
		
			
				|  |  | +  const query = new Parse.Query('Profile');
 | 
	
		
			
				|  |  | +  query.equalTo('company', cid);
 | 
	
		
			
				|  |  | +  query.equalTo('roleName', '组员');
 | 
	
		
			
				|  |  | +  query.notEqualTo('isDeleted', true);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const designers = await query.find();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  for (const designer of designers) {
 | 
	
		
			
				|  |  | +    const data = designer.get('data') || {};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 初始化tags结构(如果不存在)
 | 
	
		
			
				|  |  | +    if (!data.tags) {
 | 
	
		
			
				|  |  | +      data.tags = {
 | 
	
		
			
				|  |  | +        expertise: {
 | 
	
		
			
				|  |  | +          styles: ['现代简约', '北欧风格'],
 | 
	
		
			
				|  |  | +          skills: ['3D建模', '效果图渲染'],
 | 
	
		
			
				|  |  | +          spaceTypes: ['客厅', '卧室']
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        capacity: {
 | 
	
		
			
				|  |  | +          weeklyProjects: 3,
 | 
	
		
			
				|  |  | +          maxConcurrent: 5,
 | 
	
		
			
				|  |  | +          avgDaysPerProject: 10
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        emergency: {
 | 
	
		
			
				|  |  | +          willing: false,
 | 
	
		
			
				|  |  | +          premium: 0,
 | 
	
		
			
				|  |  | +          maxPerWeek: 0
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        history: {
 | 
	
		
			
				|  |  | +          totalProjects: 0,
 | 
	
		
			
				|  |  | +          completionRate: 0,
 | 
	
		
			
				|  |  | +          avgRating: 0,
 | 
	
		
			
				|  |  | +          onTimeRate: 0,
 | 
	
		
			
				|  |  | +          excellentCount: 0
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +        portfolio: []
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 初始化leave结构(如果不存在)
 | 
	
		
			
				|  |  | +    if (!data.leave) {
 | 
	
		
			
				|  |  | +      data.leave = {
 | 
	
		
			
				|  |  | +        records: [],
 | 
	
		
			
				|  |  | +        statistics: {
 | 
	
		
			
				|  |  | +          annualTotal: 10,
 | 
	
		
			
				|  |  | +          annualUsed: 0,
 | 
	
		
			
				|  |  | +          sickUsed: 0,
 | 
	
		
			
				|  |  | +          personalUsed: 0
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    designer.set('data', data);
 | 
	
		
			
				|  |  | +    await designer.save();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    console.log(`✅ 为 ${designer.get('name')} 初始化数据`);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  console.log('✅ 所有设计师数据初始化完成');
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 执行
 | 
	
		
			
				|  |  | +initDesignerData().catch(console.error);
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 五、关键技术要点
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 5.1 Parse查询优化
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// 1. 批量查询减少请求
 | 
	
		
			
				|  |  | +await Promise.all([
 | 
	
		
			
				|  |  | +  query1.find(),
 | 
	
		
			
				|  |  | +  query2.find(),
 | 
	
		
			
				|  |  | +  query3.find()
 | 
	
		
			
				|  |  | +]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 2. 使用include减少请求次数
 | 
	
		
			
				|  |  | +query.include('project', 'project.contact', 'profile');
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 3. 限制返回字段
 | 
	
		
			
				|  |  | +query.select('title', 'deadline', 'status');
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 4. 添加索引字段
 | 
	
		
			
				|  |  | +query.equalTo('status', 'in_progress'); // status字段需要索引
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 5.2 错误处理
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// 统一错误处理
 | 
	
		
			
				|  |  | +try {
 | 
	
		
			
				|  |  | +  const tasks = await this.taskService.getMyTasks(designerId);
 | 
	
		
			
				|  |  | +  // 处理成功逻辑
 | 
	
		
			
				|  |  | +} catch (error) {
 | 
	
		
			
				|  |  | +  console.error('加载任务失败:', error);
 | 
	
		
			
				|  |  | +  // 降级到模拟数据或提示用户
 | 
	
		
			
				|  |  | +  this.showErrorMessage('加载任务失败,请刷新重试');
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 5.3 数据缓存
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +// 缓存Profile信息
 | 
	
		
			
				|  |  | +if (profile?.id) {
 | 
	
		
			
				|  |  | +  localStorage.setItem('Parse/ProfileId', profile.id);
 | 
	
		
			
				|  |  | +  // 可以考虑缓存整个profile数据
 | 
	
		
			
				|  |  | +  localStorage.setItem('Parse/ProfileData', JSON.stringify(profile.toJSON()));
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 读取缓存
 | 
	
		
			
				|  |  | +const cachedProfileData = localStorage.getItem('Parse/ProfileData');
 | 
	
		
			
				|  |  | +if (cachedProfileData) {
 | 
	
		
			
				|  |  | +  this.currentProfile = JSON.parse(cachedProfileData);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 六、测试方案
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 6.1 企微认证测试
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +| 场景 | 操作 | 预期结果 |
 | 
	
		
			
				|  |  | +|------|------|----------|
 | 
	
		
			
				|  |  | +| 首次访问 | 访问设计师端 | 跳转企微授权 |
 | 
	
		
			
				|  |  | +| 授权成功 | 完成企微授权 | 自动登录并进入工作台 |
 | 
	
		
			
				|  |  | +| 角色不匹配 | 用其他角色访问 | 提示"您不是设计师" |
 | 
	
		
			
				|  |  | +| 已登录 | 再次访问 | 直接进入(无需重复授权) |
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 6.2 任务数据测试
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +| 场景 | 操作 | 预期结果 |
 | 
	
		
			
				|  |  | +|------|------|----------|
 | 
	
		
			
				|  |  | +| 有任务 | 加载工作台 | 显示所有分配的任务 |
 | 
	
		
			
				|  |  | +| 无任务 | 加载工作台 | 显示"暂无任务" |
 | 
	
		
			
				|  |  | +| 超期任务 | 查看任务列表 | 超期任务标红显示 |
 | 
	
		
			
				|  |  | +| 紧急任务 | 查看任务列表 | 紧急任务优先显示 |
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 6.3 请假申请测试
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +| 场景 | 操作 | 预期结果 |
 | 
	
		
			
				|  |  | +|------|------|----------|
 | 
	
		
			
				|  |  | +| 提交申请 | 填写并提交 | 成功提示,数据保存 |
 | 
	
		
			
				|  |  | +| 查看记录 | 打开请假记录 | 显示所有历史申请 |
 | 
	
		
			
				|  |  | +| 日期验证 | 选择错误日期 | 提示错误并阻止提交 |
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 七、FAQ
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### Q1: 为什么要优化企微认证?
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**A**: 虽然设计师端已有企微认证,但:
 | 
	
		
			
				|  |  | +- cid硬编码,无法支持多公司
 | 
	
		
			
				|  |  | +- 未验证"组员"角色
 | 
	
		
			
				|  |  | +- 未缓存Profile信息
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### Q2: 任务数据从哪个表查询?
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**A**: 推荐使用 `ProjectTeam` 表,因为:
 | 
	
		
			
				|  |  | +- ✅ 更准确:反映实际执行人
 | 
	
		
			
				|  |  | +- ✅ 支持多人协作
 | 
	
		
			
				|  |  | +- ✅ 可按Product(空间)粒度管理
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +降级方案:从 `Project.assignee` 查询
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### Q3: 请假数据存在哪里?
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**A**: 推荐存在 `Profile.data.leave`:
 | 
	
		
			
				|  |  | +- ✅ 实施快速
 | 
	
		
			
				|  |  | +- ✅ 无需新建表
 | 
	
		
			
				|  |  | +- ✅ 查询简单
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +如需复杂审批流程,可创建独立的 `Leave` 表
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### Q4: 如何处理周末请假?
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**A**: 使用 `calculateLeaveDays()` 方法自动排除周末:
 | 
	
		
			
				|  |  | +```typescript
 | 
	
		
			
				|  |  | +calculateLeaveDays(startDate, endDate); // 自动排除周六日
 | 
	
		
			
				|  |  | +```
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 八、后续优化建议
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 8.1 短期(1-2周)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +1. **工作负载可视化**
 | 
	
		
			
				|  |  | +   - 添加个人工作量甘特图
 | 
	
		
			
				|  |  | +   - 显示任务优先级排序
 | 
	
		
			
				|  |  | +   - 支持任务拖拽调整
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +2. **协作功能**
 | 
	
		
			
				|  |  | +   - 支持任务转交
 | 
	
		
			
				|  |  | +   - 支持请求协助
 | 
	
		
			
				|  |  | +   - 团队消息通知
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +3. **移动端优化**
 | 
	
		
			
				|  |  | +   - 响应式布局优化
 | 
	
		
			
				|  |  | +   - 手势操作支持
 | 
	
		
			
				|  |  | +   - 离线数据缓存
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 8.2 长期(1-3月)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +1. **智能助手**
 | 
	
		
			
				|  |  | +   - AI推荐最优任务顺序
 | 
	
		
			
				|  |  | +   - 自动预警风险任务
 | 
	
		
			
				|  |  | +   - 智能工作量评估
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +2. **成长体系**
 | 
	
		
			
				|  |  | +   - 技能成长追踪
 | 
	
		
			
				|  |  | +   - 绩效趋势分析
 | 
	
		
			
				|  |  | +   - 个人成就系统
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +3. **系统集成**
 | 
	
		
			
				|  |  | +   - 对接项目管理系统
 | 
	
		
			
				|  |  | +   - 对接设计软件(3DMax、Photoshop)
 | 
	
		
			
				|  |  | +   - 对接企业微信群聊
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 九、参考文档
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 9.1 内部文档
 | 
	
		
			
				|  |  | +- `rules/wxwork/auth.md` - 企微认证API
 | 
	
		
			
				|  |  | +- `rules/schemas.md` - 数据表结构
 | 
	
		
			
				|  |  | +- `src/app/services/project.service.ts` - 原有服务参考
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +### 9.2 已实现功能参考
 | 
	
		
			
				|  |  | +- 设计师端认证:`src/app/pages/designer/dashboard/dashboard.ts`
 | 
	
		
			
				|  |  | +- 组长端数据服务:`src/app/pages/team-leader/services/designer.service.ts`
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +## 十、总结
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +本方案通过以下步骤实现设计师端的完整功能:
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +1. **优化企微认证**:支持动态cid、角色验证、Profile缓存
 | 
	
		
			
				|  |  | +2. **任务数据接入**:创建DesignerTaskService,从ProjectTeam/Product表查询真实任务
 | 
	
		
			
				|  |  | +3. **请假申请功能**:创建LeaveService,支持申请和查看请假记录
 | 
	
		
			
				|  |  | +4. **个人数据接入**:从Profile.data读取技能标签和绩效数据
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**预计工作量**:3个工作日  
 | 
	
		
			
				|  |  | +**实施难度**:中等  
 | 
	
		
			
				|  |  | +**风险等级**:低
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +实施完成后,设计师端将具备:
 | 
	
		
			
				|  |  | +- ✅ 完整的企微身份识别
 | 
	
		
			
				|  |  | +- ✅ 真实的任务数据展示
 | 
	
		
			
				|  |  | +- ✅ 便捷的请假申请流程
 | 
	
		
			
				|  |  | +- ✅ 准确的个人数据展示
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +为设计师提供高效、智能的工作管理平台!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +---
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +**文档版本**:v1.0  
 | 
	
		
			
				|  |  | +**创建日期**:2024-12-24  
 | 
	
		
			
				|  |  | +**最后更新**:2024-12-24  
 | 
	
		
			
				|  |  | +**维护人**:开发团队
 | 
	
		
			
				|  |  | +
 |