Просмотр исходного кода

feat: add leave application feature to designer dashboard

- Introduced a modal for submitting leave applications, including form validation and submission logic.
- Enhanced the dashboard routing to support dynamic company IDs (cid).
- Updated the dashboard layout with a new header and leave application button.
- Implemented styles for the leave application modal and records display.
- Added functionality to load and display user's leave records.
0235711 2 дней назад
Родитель
Сommit
55ea1047bb

+ 24 - 1
src/app/app.routes.ts

@@ -58,7 +58,30 @@ export const routes: Routes = [
     ]
   },
 
-  // 设计师路由
+  // 设计师路由(支持cid参数)
+  {
+    path: ':cid/designer',
+    canActivate: [WxworkAuthGuard],
+    children: [
+      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
+      {
+        path: 'dashboard',
+        loadComponent: () => import('./pages/designer/dashboard/dashboard').then(m => m.Dashboard),
+        title: '设计师工作台'
+      },
+      {
+        path: 'project-detail/:id',
+        loadComponent: () => import('./pages/designer/project-detail/project-detail').then(m => m.ProjectDetail),
+        title: '项目详情'
+      },
+      {
+        path: 'personal-board',
+        loadComponent: () => import('./pages/designer/personal-board/personal-board').then(m => m.PersonalBoard),
+        title: '个人看板'
+      }
+    ]
+  },
+  // 兼容旧路由(不带cid)
   {
     path: 'designer',
     canActivate: [WxworkAuthGuard],

+ 99 - 1
src/app/pages/designer/dashboard/dashboard.html

@@ -1,6 +1,16 @@
 <div class="dashboard-container">
   <header class="dashboard-header">
-    <h1>设计师工作台</h1>
+    <div class="header-content">
+      <h1>设计师工作台</h1>
+      
+      <!-- 请假申请按钮 -->
+      <button class="leave-btn" (click)="openLeaveModal()" title="申请请假">
+        <svg viewBox="0 0 24 24" width="20" height="20">
+          <path fill="currentColor" 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>
+    </div>
     
     <!-- 顶部导航 -->
     <nav class="dashboard-nav">
@@ -219,4 +229,92 @@
       <button (click)="clearReminder()" class="btn-close">关闭</button>
     </div>
   </div>
+  
+  <!-- 请假申请弹窗 -->
+  @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>
+  }
+  
+  <!-- 我的请假记录(可选,显示在个人看板) -->
+  @if (activeDashboard === 'personal' && leaveApplications.length > 0) {
+    <div class="leave-records-section">
+      <h4>我的请假记录</h4>
+      <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>
+    </div>
+  }
 </div>

+ 333 - 1
src/app/pages/designer/dashboard/dashboard.scss

@@ -20,6 +20,8 @@
   padding: 20px;
   background-color: $ios-background;
   min-height: 100vh;
+  display: flex;
+  flex-direction: column;
 }
 
 .dashboard-header {
@@ -183,6 +185,28 @@
   grid-template-columns: 1fr;
   gap: 32px;
   padding: 0 4px; // 添加轻微的水平内边距
+  max-height: calc(100vh - 200px);
+  overflow-y: auto;
+  
+  // 滚动条样式
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: $ios-background-secondary;
+    border-radius: $ios-radius-full;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: $ios-radius-full;
+    border: 2px solid $ios-background-secondary;
+  }
+  
+  &::-webkit-scrollbar-thumb:hover {
+    background: rgba(0, 0, 0, 0.3);
+  }
 }
 
 /* 视图切换按钮样式 - 现代化升级 */
@@ -284,7 +308,7 @@
   border-radius: 16px;
   border: 1px solid rgba(0, 0, 0, 0.06);
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05);
-  overflow: hidden;
+  overflow: visible;
   backdrop-filter: blur(10px);
 
   .list-header {
@@ -865,6 +889,28 @@
   padding: 24px;
   box-shadow: $ios-shadow-card;
   border: 1px solid $ios-border;
+  max-height: calc(100vh - 200px);
+  overflow-y: auto;
+  
+  // 滚动条样式
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: $ios-background-secondary;
+    border-radius: $ios-radius-full;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: $ios-radius-full;
+    border: 2px solid $ios-background-secondary;
+  }
+  
+  &::-webkit-scrollbar-thumb:hover {
+    background: rgba(0, 0, 0, 0.3);
+  }
 }
 
 /* 紧急任务区域样式 */
@@ -2681,3 +2727,289 @@
     }
   }
 }
+
+// 请假申请相关样式
+.header-content {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  
+  h1 {
+    margin: 0;
+  }
+}
+
+.leave-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 16px;
+  background: linear-gradient(135deg, #007aff 0%, #5ac8fa 100%);
+  color: white;
+  border: none;
+  border-radius: $ios-radius-lg;
+  font-size: 14px;
+  font-weight: $ios-font-weight-medium;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
+  
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
+  }
+  
+  &:active {
+    transform: translateY(0);
+  }
+  
+  svg {
+    width: 20px;
+    height: 20px;
+  }
+}
+
+// 请假弹窗样式
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  backdrop-filter: blur(4px);
+}
+
+.leave-modal {
+  width: 90%;
+  max-width: 500px;
+  background: white;
+  border-radius: $ios-radius-xl;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+  
+  .modal-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 20px 24px;
+    background: linear-gradient(135deg, #007aff 0%, #5ac8fa 100%);
+    color: white;
+    
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: $ios-font-weight-semibold;
+    }
+    
+    .close-btn {
+      width: 32px;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: rgba(255, 255, 255, 0.2);
+      border: none;
+      border-radius: 50%;
+      color: white;
+      font-size: 24px;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        background: rgba(255, 255, 255, 0.3);
+      }
+    }
+  }
+  
+  .modal-body {
+    padding: 24px;
+    
+    .form-group {
+      margin-bottom: 20px;
+      
+      &:last-child {
+        margin-bottom: 0;
+      }
+      
+      label {
+        display: block;
+        margin-bottom: 8px;
+        font-size: 14px;
+        font-weight: $ios-font-weight-medium;
+        color: $ios-text-primary;
+      }
+      
+      .form-control {
+        width: 100%;
+        padding: 12px;
+        border: 1px solid $ios-border;
+        border-radius: $ios-radius-md;
+        font-size: 14px;
+        font-family: $ios-font-family;
+        transition: all 0.2s ease;
+        
+        &:focus {
+          outline: none;
+          border-color: #007aff;
+          box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
+        }
+      }
+      
+      textarea.form-control {
+        resize: vertical;
+        min-height: 80px;
+      }
+    }
+  }
+  
+  .modal-footer {
+    padding: 16px 24px;
+    background: $ios-background-secondary;
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+    
+    .btn {
+      padding: 10px 24px;
+      border-radius: $ios-radius-md;
+      font-size: 14px;
+      font-weight: $ios-font-weight-medium;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      border: none;
+      
+      &.btn-cancel {
+        background: $ios-background;
+        color: $ios-text-secondary;
+        border: 1px solid $ios-border;
+        
+        &:hover {
+          background: $ios-background-secondary;
+        }
+      }
+      
+      &.btn-submit {
+        background: linear-gradient(135deg, #007aff 0%, #5ac8fa 100%);
+        color: white;
+        
+        &:hover {
+          transform: translateY(-1px);
+          box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
+        }
+      }
+    }
+  }
+}
+
+// 请假记录样式
+.leave-records-section {
+  margin-top: 32px;
+  padding: 24px;
+  background: white;
+  border-radius: $ios-radius-xl;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  
+  h4 {
+    margin: 0 0 20px 0;
+    font-size: 18px;
+    font-weight: $ios-font-weight-semibold;
+    color: $ios-text-primary;
+  }
+  
+  .records-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+  
+  .record-item {
+    padding: 16px;
+    background: $ios-background;
+    border-radius: $ios-radius-lg;
+    border: 1px solid $ios-border;
+    transition: all 0.2s ease;
+    
+    &:hover {
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+      transform: translateY(-2px);
+    }
+    
+    &.pending {
+      border-left: 4px solid #ff9500;
+    }
+    
+    &.approved {
+      border-left: 4px solid #34c759;
+    }
+    
+    &.rejected {
+      border-left: 4px solid #ff3b30;
+    }
+    
+    .record-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 12px;
+      
+      .record-type {
+        font-size: 14px;
+        font-weight: $ios-font-weight-semibold;
+        color: $ios-text-primary;
+      }
+      
+      .record-status {
+        padding: 4px 12px;
+        border-radius: $ios-radius-full;
+        font-size: 12px;
+        font-weight: $ios-font-weight-medium;
+        
+        &.pending {
+          background: rgba(255, 149, 0, 0.1);
+          color: #ff9500;
+        }
+        
+        &.approved {
+          background: rgba(52, 199, 89, 0.1);
+          color: #34c759;
+        }
+        
+        &.rejected {
+          background: rgba(255, 59, 48, 0.1);
+          color: #ff3b30;
+        }
+      }
+    }
+    
+    .record-body {
+      .record-date {
+        font-size: 14px;
+        color: $ios-text-primary;
+        margin-bottom: 8px;
+      }
+      
+      .record-reason {
+        font-size: 13px;
+        color: $ios-text-secondary;
+        line-height: 1.5;
+        margin: 0;
+      }
+    }
+    
+    .record-footer {
+      margin-top: 12px;
+      padding-top: 12px;
+      border-top: 1px solid $ios-border;
+      
+      .record-time {
+        font-size: 12px;
+        color: $ios-text-tertiary;
+      }
+    }
+  }
+}

+ 343 - 19
src/app/pages/designer/dashboard/dashboard.ts

@@ -1,12 +1,15 @@
 import { Component, OnInit, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { RouterModule } from '@angular/router';
+import { RouterModule, ActivatedRoute, Router } from '@angular/router';
 import { ProjectService } from '../../../services/project.service';
 import { Task } from '../../../models/project.model';
 import { SkillRadarComponent } from './skill-radar/skill-radar.component';
 import { PersonalBoard } from '../personal-board/personal-board';
 import { FmodeQuery, FmodeObject, FmodeUser } from 'fmode-ng/core';
 import { WxworkAuth } from 'fmode-ng/core';
+import { DesignerTaskService } from '../../../services/designer-task.service';
+import { LeaveService, LeaveApplication } from '../../../services/leave.service';
+import { FormsModule } from '@angular/forms';
 
 interface ShiftTask {
   id: string;
@@ -28,7 +31,7 @@ interface ProjectTimelineItem {
 @Component({
   selector: 'app-dashboard',
   standalone: true,
-  imports: [CommonModule, RouterModule, SkillRadarComponent, PersonalBoard],
+  imports: [CommonModule, RouterModule, FormsModule, SkillRadarComponent, PersonalBoard],
   templateUrl: './dashboard.html',
   styleUrl: './dashboard.scss'
 })
@@ -54,52 +57,122 @@ export class Dashboard implements OnInit {
   projectTimeline: ProjectTimelineItem[] = [];
   private wxAuth: WxworkAuth | null = null;
   private currentUser: FmodeUser | null = null;
+  private currentProfile: FmodeObject | null = null; // 当前Profile
+  private cid: string = '';
+  
+  // 请假相关
+  showLeaveModal: boolean = false;
+  leaveApplications: LeaveApplication[] = [];
+  today: string = new Date().toISOString().split('T')[0];
+  
+  // 请假表单
+  leaveForm = {
+    startDate: '',
+    endDate: '',
+    type: 'personal' as 'annual' | 'sick' | 'personal' | 'other',
+    reason: ''
+  };
 
-  constructor(private projectService: ProjectService) {
-    this.initAuth();
+  constructor(
+    private projectService: ProjectService,
+    private route: ActivatedRoute,
+    private router: Router,
+    private taskService: DesignerTaskService,
+    private leaveService: LeaveService
+  ) {}
+
+  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.warn('⚠️ 未找到公司ID,尝试使用默认值');
+        this.cid = 'cDL6R1hgSi'; // 默认公司ID
+      }
+      
+      // 2. 初始化企微认证
+      this.initAuth();
+      
+      // 3. 执行认证并加载数据
+      await this.authenticateAndLoadData();
+    });
   }
 
-  // 初始化企业微信认证
+  // 初始化企业微信认证(优化版)
   private initAuth(): void {
     try {
       this.wxAuth = new WxworkAuth({
-        cid: 'cDL6R1hgSi'  // 公司帐套ID
+        cid: this.cid,  // 使用动态获取的cid
+        appId: 'crm'
       });
-      console.log('✅ 设计师仪表板企业微信认证初始化成功');
+      console.log('✅ 设计师端企微认证初始化成功,CID:', this.cid);
     } catch (error) {
-      console.error('❌ 设计师仪表板企业微信认证初始化失败:', error);
+      console.error('❌ 设计师端企微认证初始化失败:', error);
     }
   }
 
-  async ngOnInit(): Promise<void> {
-    await this.authenticateAndLoadData();
-  }
-
-  // 认证并加载数据
+  // 认证并加载数据(优化版)
   private async authenticateAndLoadData(): Promise<void> {
     try {
       // 执行企业微信认证和登录
-      const { user } = await this.wxAuth!.authenticateAndLogin();
+      const { user, profile } = await this.wxAuth!.authenticateAndLogin();
       this.currentUser = user;
+      this.currentProfile = profile;
 
-      if (user) {
-        console.log('✅ 设计师登录成功:', user.get('username'));
-        await this.loadDashboardData();
-      } else {
+      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;
+  }
 
   // 加载仪表板数据
   private async loadDashboardData(): Promise<void> {
     try {
       await Promise.all([
-        this.loadTasks(),
+        this.loadRealTasks(),      // 使用真实数据
         this.loadShiftTasks(),
         this.calculateWorkloadPercentage(),
         this.loadProjectTimeline()
@@ -111,6 +184,127 @@ export class Dashboard implements OnInit {
     }
   }
 
+  /**
+   * 加载真实任务数据
+   */
+  private async loadRealTasks(): Promise<void> {
+    try {
+      if (!this.currentProfile) {
+        throw new Error('未找到当前Profile');
+      }
+
+      const designerTasks = await this.taskService.getMyTasks(this.currentProfile.id);
+      
+      // 转换为组件所需格式
+      this.tasks = designerTasks.map(task => ({
+        id: task.id,
+        projectId: task.projectId,
+        projectName: task.projectName,
+        title: task.projectName,
+        stage: task.stage,
+        deadline: task.deadline,
+        isOverdue: task.isOverdue,
+        priority: task.priority,
+        isCompleted: false,
+        assignee: null,
+        description: `${task.stage} - ${task.customerName}`
+      })) as Task[];
+
+      // 筛选超期任务
+      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 === '渲染';
+      });
+
+      // 设置反馈项目ID
+      if (this.overdueTasks.length > 0) {
+        this.feedbackProjectId = this.overdueTasks[0].projectId;
+      }
+
+      // 加载待处理反馈
+      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);
+      
+      if (projectIds.length === 0) {
+        this.pendingFeedbacks = [];
+        return;
+      }
+      
+      const query = new Parse.Query('ProjectFeedback');
+      const ProjectClass = Parse.Object.extend('Project');
+      query.containedIn('project', projectIds.map(id => {
+        const obj = new ProjectClass();
+        obj.id = id;
+        return obj;
+      }));
+      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);
+        
+        const fallbackTask: Task = {
+          id: project?.id || '',
+          projectId: project?.id || '',
+          projectName: project?.get('title') || '未知项目',
+          title: project?.get('title') || '未知项目',
+          stage: '投诉处理',
+          deadline: new Date(),
+          isOverdue: false,
+          isCompleted: false,
+          priority: 'high',
+          assignee: null,
+          description: '待处理反馈'
+        };
+        
+        return {
+          task: task || fallbackTask,
+          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 = [];
+    }
+  }
+
   // 降级到模拟数据
   private loadMockData(): void {
     console.warn('⚠️ 使用模拟数据');
@@ -511,5 +705,135 @@ export class Dashboard implements OnInit {
       }
     ].sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime());
   }
+  
+  /**
+   * 打开请假申请弹窗
+   */
+  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: ''
+    };
+  }
+  
+  /**
+   * 获取请假类型文本
+   */
+  getLeaveTypeText(type: string): string {
+    const typeMap: Record<string, string> = {
+      'annual': '年假',
+      'sick': '病假',
+      'personal': '事假',
+      'other': '其他'
+    };
+    return typeMap[type] || type;
+  }
+
+  /**
+   * 获取请假状态文本
+   */
+  getLeaveStatusText(status: string): string {
+    const statusMap: Record<string, string> = {
+      'pending': '待审批',
+      'approved': '已批准',
+      'rejected': '已拒绝'
+    };
+    return statusMap[status] || status;
+  }
 
 }
+

+ 0 - 1
src/app/pages/designer/dashboard/skill-radar/skill-radar.component.scss

@@ -5,7 +5,6 @@
   border-radius: $ios-radius-lg;
   padding: $ios-spacing-xl;
   box-shadow: $ios-shadow-card;
-  margin-bottom: $ios-spacing-xl;
   border: 1px solid $ios-border;
 }
 .skill-radar-header {

+ 1 - 1
src/app/pages/designer/dashboard/skill-radar/skill-radar.component.ts

@@ -181,7 +181,7 @@ export class SkillRadarComponent implements OnInit, AfterViewInit, OnDestroy {
   // 清理图表资源
   ngOnDestroy(): void {
     if (this.radarChart) {
-      this.radarChart.destroy();
+      this.radarChart.dispose();
     }
   }
 }

+ 0 - 1
src/app/pages/designer/personal-board/personal-board.scss

@@ -6,7 +6,6 @@
   margin: 0 auto;
   padding: 20px;
   background-color: $ios-background;
-  min-height: 100vh;
 }
 
 .personal-board-header {

+ 207 - 0
src/app/services/designer-task.service.ts

@@ -0,0 +1,207 @@
+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 ProfileClass = this.Parse.Object.extend('Profile');
+      const profilePointer = new ProfileClass();
+      profilePointer.id = designerId;
+      
+      const teamQuery = new this.Parse.Query('ProjectTeam');
+      teamQuery.equalTo('profile', profilePointer);
+      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 await this.getMyTasksFromAssignee(designerId);
+      }
+
+      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') || project.get('deliveryDate') || project.get('expectedDeliveryDate') || 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查询(降级方案)
+   */
+  async getMyTasksFromAssignee(designerId: string): Promise<DesignerTask[]> {
+    if (!this.Parse) await this.initParse();
+    if (!this.Parse || !this.cid) return [];
+
+    try {
+      const ProfileClass = this.Parse.Object.extend('Profile');
+      const profilePointer = new ProfileClass();
+      profilePointer.id = designerId;
+      
+      const query = new this.Parse.Query('Project');
+      query.equalTo('assignee', profilePointer);
+      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();
+
+      console.log(`✅ [降级方案] 从Project.assignee加载 ${projects.length} 个任务`);
+
+      return projects.map((project: any) => {
+        const deadline = project.get('deadline') || project.get('deliveryDate') || project.get('expectedDeliveryDate') || 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 [];
+    }
+  }
+}
+

+ 169 - 0
src/app/services/leave.service.ts

@@ -0,0 +1,169 @@
+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;
+  }
+}
+
+
+