designer-wxwork-real-data-implementation.md 39 KB

设计师端(组员端)企业微信身份识别与真实数据接入实施方案

文档概述

本文档详细说明如何为设计师端(Designer/组员端)优化企业微信身份识别并接入真实数据库,包括任务管理、请假申请、个人数据等功能的完整实现。


一、现状分析

1.1 设计师端当前实现情况

企微认证 ✅

已实现内容

  • ✅ 路由守卫:WxworkAuthGuardapp.routes.ts 第64行)
  • ✅ 组件内认证:WxworkAuth 实例(dashboard.ts 第55-72行)
  • ✅ 认证流程:authenticateAndLoadData() 方法(第79-96行)
  • ✅ 降级机制:认证失败时使用模拟数据

代码位置

// 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参数

// 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组件的认证流程

// 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

// 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组件使用真实数据

// 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(设计师端使用)

// 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中添加请假申请功能

// 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)

<!-- 请假申请按钮 -->
<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读取真实技能标签

// 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优化
  • 错误处理完善
  • 编写使用文档

四、数据初始化

为现有设计师初始化数据

// 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查询优化

// 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 错误处理

// 统一错误处理
try {
  const tasks = await this.taskService.getMyTasks(designerId);
  // 处理成功逻辑
} catch (error) {
  console.error('加载任务失败:', error);
  // 降级到模拟数据或提示用户
  this.showErrorMessage('加载任务失败,请刷新重试');
}

5.3 数据缓存

// 缓存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() 方法自动排除周末:

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
维护人:开发团队