|
@@ -1,84 +1,72 @@
|
|
|
-import { Component, OnInit, AfterViewInit, OnDestroy, signal } from '@angular/core';
|
|
|
+import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { RouterModule } from '@angular/router';
|
|
|
import { MatCardModule } from '@angular/material/card';
|
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
|
-import { MatIconModule } from '@angular/material/icon';
|
|
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
|
-import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
|
+import { MatIconModule } from '@angular/material/icon';
|
|
|
+import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
|
import { MatChipsModule } from '@angular/material/chips';
|
|
|
-import { MatSelectModule } from '@angular/material/select';
|
|
|
-import { FormsModule } from '@angular/forms';
|
|
|
-
|
|
|
-// 模拟数据接口
|
|
|
-interface EmployeeMovement {
|
|
|
- month: string;
|
|
|
- hired: number;
|
|
|
- resigned: number;
|
|
|
- turnoverRate: number;
|
|
|
-}
|
|
|
-
|
|
|
-interface DepartmentData {
|
|
|
- name: string;
|
|
|
- hired: number;
|
|
|
- resigned: number;
|
|
|
- turnoverRate: number;
|
|
|
-}
|
|
|
+import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
|
+import { MatBadgeModule } from '@angular/material/badge';
|
|
|
+import { DragDropModule, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
|
|
+import { Chart, ChartConfiguration, registerables } from 'chart.js';
|
|
|
+import { trigger, state, style, transition, animate } from '@angular/animations';
|
|
|
|
|
|
-interface PendingProcess {
|
|
|
- type: 'hire' | 'resign';
|
|
|
- count: number;
|
|
|
- progress: number;
|
|
|
+// 数据模型定义
|
|
|
+export interface TodoItem {
|
|
|
+ id: number;
|
|
|
+ title: string;
|
|
|
+ description: string;
|
|
|
+ priority: 'high' | 'medium' | 'low';
|
|
|
+ status: 'pending' | 'completed' | 'in_progress';
|
|
|
+ type: 'resume' | 'onboarding' | 'promotion' | 'resignation';
|
|
|
}
|
|
|
|
|
|
-interface RiskAlert {
|
|
|
- type: string;
|
|
|
- employees: {name: string; position: string; department: string; daysLeft?: number}[];
|
|
|
- count: number;
|
|
|
+export interface RankDistribution {
|
|
|
+ junior: number;
|
|
|
+ intermediate: number;
|
|
|
+ senior: number;
|
|
|
}
|
|
|
|
|
|
-interface PerformanceMetric {
|
|
|
- name: string;
|
|
|
- target: number;
|
|
|
- actual: number;
|
|
|
- unit: string;
|
|
|
+export interface MonthlyData {
|
|
|
+ month: string;
|
|
|
+ hired: number;
|
|
|
+ left: number;
|
|
|
+ notes?: string;
|
|
|
}
|
|
|
|
|
|
-interface DepartmentPerformance {
|
|
|
- name: string;
|
|
|
+export interface DepartmentPerformance {
|
|
|
+ department: string;
|
|
|
+ completionRate: number;
|
|
|
excellentWorkRate: number;
|
|
|
- deliveryOnTimeRate: number;
|
|
|
- customerSatisfaction: number;
|
|
|
+ satisfactionRate: number;
|
|
|
+ overdueRate: number;
|
|
|
}
|
|
|
|
|
|
-interface PerformanceTier {
|
|
|
- name: string;
|
|
|
- percentage: number;
|
|
|
- count: number;
|
|
|
+export interface PromotionRule {
|
|
|
+ id: number;
|
|
|
+ title: string;
|
|
|
+ description: string;
|
|
|
+ conditions: string[];
|
|
|
}
|
|
|
|
|
|
-interface PenaltyWarning {
|
|
|
- issue: string;
|
|
|
- departments: {name: string; count: number}[];
|
|
|
- employees: {name: string; department: string; count: number}[];
|
|
|
+export interface PromotionSuggestion {
|
|
|
+ employeeId: number;
|
|
|
+ employeeName: string;
|
|
|
+ currentRank: string;
|
|
|
+ suggestedRank: string;
|
|
|
+ reasons: string[];
|
|
|
+ status: 'pending' | 'approved' | 'rejected';
|
|
|
}
|
|
|
|
|
|
-interface EmployeeStructure {
|
|
|
- category: string;
|
|
|
- data: {name: string; count: number; percentage: number}[];
|
|
|
-}
|
|
|
-
|
|
|
-interface MovementTrend {
|
|
|
- month: string;
|
|
|
- hired: number;
|
|
|
- resigned: number;
|
|
|
-}
|
|
|
-
|
|
|
-interface TodoItem {
|
|
|
- task: string;
|
|
|
+export interface OnboardingCheckpoint {
|
|
|
+ id: number;
|
|
|
+ title: string;
|
|
|
+ description: string;
|
|
|
dueDate: Date;
|
|
|
- priority: 'high' | 'medium' | 'low';
|
|
|
- type: string;
|
|
|
+ completed: boolean;
|
|
|
+ interviewTemplate: string[];
|
|
|
}
|
|
|
|
|
|
@Component({
|
|
@@ -89,293 +77,574 @@ interface TodoItem {
|
|
|
RouterModule,
|
|
|
MatCardModule,
|
|
|
MatButtonModule,
|
|
|
- MatIconModule,
|
|
|
MatTabsModule,
|
|
|
- MatProgressBarModule,
|
|
|
+ MatIconModule,
|
|
|
+ MatCheckboxModule,
|
|
|
MatChipsModule,
|
|
|
- MatSelectModule,
|
|
|
- FormsModule
|
|
|
+ MatProgressBarModule,
|
|
|
+ MatBadgeModule,
|
|
|
+ DragDropModule
|
|
|
],
|
|
|
templateUrl: './dashboard.html',
|
|
|
- styleUrls: ['./dashboard.scss']
|
|
|
+ styleUrls: ['./dashboard.scss'],
|
|
|
+ animations: [
|
|
|
+ trigger('slideInOut', [
|
|
|
+ transition(':enter', [
|
|
|
+ style({ height: '0', opacity: 0, overflow: 'hidden' }),
|
|
|
+ animate('300ms ease-in-out', style({ height: '*', opacity: 1 }))
|
|
|
+ ]),
|
|
|
+ transition(':leave', [
|
|
|
+ animate('300ms ease-in-out', style({ height: '0', opacity: 0, overflow: 'hidden' }))
|
|
|
+ ])
|
|
|
+ ])
|
|
|
+ ]
|
|
|
})
|
|
|
-export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
|
|
- // 时间筛选
|
|
|
- timeFilter = signal('month');
|
|
|
-
|
|
|
- // 图表实例
|
|
|
- private movementChart: any | null = null;
|
|
|
- private performanceChart: any | null = null;
|
|
|
- private trendChart: any | null = null;
|
|
|
- // 入职离职数据
|
|
|
- employeeMovements = signal<EmployeeMovement[]>([
|
|
|
- { month: '1月', hired: 12, resigned: 5, turnoverRate: 3.2 },
|
|
|
- { month: '2月', hired: 8, resigned: 4, turnoverRate: 2.5 },
|
|
|
- { month: '3月', hired: 15, resigned: 6, turnoverRate: 3.8 },
|
|
|
- { month: '4月', hired: 10, resigned: 8, turnoverRate: 5.1 },
|
|
|
- { month: '5月', hired: 14, resigned: 7, turnoverRate: 4.2 },
|
|
|
- { month: '6月', hired: 18, resigned: 5, turnoverRate: 2.9 }
|
|
|
- ]);
|
|
|
+export class Dashboard implements OnInit, AfterViewInit {
|
|
|
+ @ViewChild('pieChart', { static: false }) pieChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
+ @ViewChild('lineChart', { static: false }) lineChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
+ @ViewChild('radarChart', { static: false }) radarChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
|
|
|
- departmentData = signal<DepartmentData[]>([
|
|
|
- { name: '设计部', hired: 8, resigned: 3, turnoverRate: 4.2 },
|
|
|
- { name: '客服部', hired: 5, resigned: 2, turnoverRate: 3.1 },
|
|
|
- { name: '技术部', hired: 12, resigned: 4, turnoverRate: 5.3 },
|
|
|
- { name: '行政部', hired: 3, resigned: 1, turnoverRate: 2.8 }
|
|
|
- ]);
|
|
|
+ private pieChart!: Chart;
|
|
|
+ private lineChart!: Chart;
|
|
|
+ private radarChart!: Chart;
|
|
|
+ // 当前激活的标签页
|
|
|
+ activeTab: 'visualization' | 'promotion' | 'onboarding' = 'visualization';
|
|
|
|
|
|
- pendingProcesses = signal<PendingProcess[]>([
|
|
|
- { type: 'hire', count: 8, progress: 65 },
|
|
|
- { type: 'resign', count: 3, progress: 40 }
|
|
|
- ]);
|
|
|
+ // 待办事项是否展开
|
|
|
+ isTodoExpanded = false;
|
|
|
|
|
|
- riskAlerts = signal<RiskAlert[]>([
|
|
|
- {
|
|
|
- type: '试用期即将到期',
|
|
|
- employees: [
|
|
|
- { name: '张三', position: '设计师', department: '设计部', daysLeft: 7 },
|
|
|
- { name: '李四', position: '客服专员', department: '客服部', daysLeft: 10 },
|
|
|
- { name: '王五', position: '前端开发', department: '技术部', daysLeft: 15 }
|
|
|
- ],
|
|
|
- count: 3
|
|
|
+ // 当前页面
|
|
|
+ currentPage: string = 'dashboard';
|
|
|
+
|
|
|
+ // 待办事项展开状态
|
|
|
+ showTodoList: boolean = false;
|
|
|
+
|
|
|
+ // 待办事项列表(用于右侧展示)
|
|
|
+ todoList = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ title: '简历初筛',
|
|
|
+ description: '筛选新收到的设计师简历',
|
|
|
+ priority: 'high',
|
|
|
+ dueDate: '2024-01-25'
|
|
|
},
|
|
|
- {
|
|
|
- type: '离职关键岗位',
|
|
|
- employees: [
|
|
|
- { name: '赵六', position: '高级设计师', department: '设计部' },
|
|
|
- { name: '钱七', position: '技术组长', department: '技术部' }
|
|
|
- ],
|
|
|
- count: 2
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ title: '入职评定',
|
|
|
+ description: '完成新员工入职评定表',
|
|
|
+ priority: 'medium',
|
|
|
+ dueDate: '2024-01-28'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ title: '晋升审核',
|
|
|
+ description: '审核中级设计师晋升申请',
|
|
|
+ priority: 'high',
|
|
|
+ dueDate: '2024-01-30'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ title: '离职面谈',
|
|
|
+ description: '安排资深设计师离职面谈',
|
|
|
+ priority: 'medium',
|
|
|
+ dueDate: '2024-02-02'
|
|
|
}
|
|
|
- ]);
|
|
|
-
|
|
|
- // 绩效数据
|
|
|
- performanceMetrics = signal<PerformanceMetric[]>([
|
|
|
- { name: '优秀作品率', target: 80, actual: 76, unit: '%' },
|
|
|
- { name: '项目交付准时率', target: 95, actual: 92, unit: '%' },
|
|
|
- { name: '客户满意度评分', target: 4.5, actual: 4.3, unit: '分' }
|
|
|
- ]);
|
|
|
-
|
|
|
- departmentPerformance = signal<DepartmentPerformance[]>([
|
|
|
- { name: '设计部', excellentWorkRate: 78, deliveryOnTimeRate: 94, customerSatisfaction: 4.4 },
|
|
|
- { name: '客服部', excellentWorkRate: 82, deliveryOnTimeRate: 96, customerSatisfaction: 4.6 },
|
|
|
- { name: '技术部', excellentWorkRate: 75, deliveryOnTimeRate: 90, customerSatisfaction: 4.2 },
|
|
|
- { name: '行政部', excellentWorkRate: 80, deliveryOnTimeRate: 95, customerSatisfaction: 4.5 }
|
|
|
- ]);
|
|
|
+ ];
|
|
|
|
|
|
- performanceTiers = signal<PerformanceTier[]>([
|
|
|
- { name: '头部20%', percentage: 20, count: 24 },
|
|
|
- { name: '中部70%', percentage: 70, count: 84 },
|
|
|
- { name: '尾部10%', percentage: 10, count: 12 }
|
|
|
- ]);
|
|
|
-
|
|
|
- penaltyWarnings = signal<PenaltyWarning[]>([
|
|
|
- {
|
|
|
- issue: '项目延期',
|
|
|
- departments: [
|
|
|
- { name: '设计部', count: 5 },
|
|
|
- { name: '技术部', count: 3 }
|
|
|
- ],
|
|
|
- employees: [
|
|
|
- { name: '张三', department: '设计部', count: 2 },
|
|
|
- { name: '李四', department: '技术部', count: 2 }
|
|
|
+ // 模拟数据
|
|
|
+ todoItems: TodoItem[] = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ title: '简历初筛',
|
|
|
+ description: '筛选新收到的设计师简历',
|
|
|
+ priority: 'high',
|
|
|
+ status: 'pending',
|
|
|
+ type: 'resume'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ title: '入职评定',
|
|
|
+ description: '完成新员工入职评定表',
|
|
|
+ priority: 'medium',
|
|
|
+ status: 'pending',
|
|
|
+ type: 'onboarding'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ title: '晋升审核',
|
|
|
+ description: '审核中级设计师晋升申请',
|
|
|
+ priority: 'high',
|
|
|
+ status: 'pending',
|
|
|
+ type: 'promotion'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ title: '离职面谈',
|
|
|
+ description: '安排资深设计师离职面谈',
|
|
|
+ priority: 'medium',
|
|
|
+ status: 'pending',
|
|
|
+ type: 'resignation'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 职级分布数据
|
|
|
+ rankDistribution = [
|
|
|
+ { level: '初级设计师', count: 15, percentage: 45, color: '#4CAF50' },
|
|
|
+ { level: '中级设计师', count: 12, percentage: 36, color: '#FF9800' },
|
|
|
+ { level: '高级设计师', count: 6, percentage: 19, color: '#2196F3' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ monthlyHireData: MonthlyData[] = [
|
|
|
+ { month: '10月', hired: 8, left: 3, notes: '秋季招聘高峰' },
|
|
|
+ { month: '11月', hired: 5, left: 2, notes: '' },
|
|
|
+ { month: '12月', hired: 3, left: 4, notes: '年底离职高峰' },
|
|
|
+ { month: '1月', hired: 10, left: 1, notes: '新年新气象' },
|
|
|
+ { month: '2月', hired: 6, left: 2, notes: '' },
|
|
|
+ { month: '3月', hired: 12, left: 3, notes: '春季招聘启动' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 关键节点数据
|
|
|
+ keyNotes = [
|
|
|
+ { month: '3月', description: '入职10人:因春季招聘' },
|
|
|
+ { month: '5月', description: '离职5人:因项目不饱和' },
|
|
|
+ { month: '8月', description: '入职15人:暑期实习转正' },
|
|
|
+ { month: '12月', description: '离职8人:年底跳槽高峰' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 关键岗位空缺数据
|
|
|
+ keyVacancies = [
|
|
|
+ { position: '高级UI设计师', count: 2, priority: 'high', duration: 45 },
|
|
|
+ { position: '3D建模师', count: 1, priority: 'medium', duration: 20 },
|
|
|
+ { position: '前端开发工程师', count: 1, priority: 'medium', duration: 15 },
|
|
|
+ { position: '产品经理', count: 1, priority: 'high', duration: 60 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 新人列表数据
|
|
|
+ newbieList = [
|
|
|
+ { id: 1, name: '张三', position: 'UI设计师', joinDate: '2024-01-15', progress: 75 },
|
|
|
+ { id: 2, name: '李四', position: '前端开发', joinDate: '2024-01-20', progress: 60 },
|
|
|
+ { id: 3, name: '王五', position: '3D建模师', joinDate: '2024-02-01', progress: 40 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ departmentPerformance: DepartmentPerformance[] = [
|
|
|
+ {
|
|
|
+ department: 'UI设计部',
|
|
|
+ completionRate: 92,
|
|
|
+ excellentWorkRate: 78,
|
|
|
+ satisfactionRate: 88,
|
|
|
+ overdueRate: 8
|
|
|
+ },
|
|
|
+ {
|
|
|
+ department: '3D建模部',
|
|
|
+ completionRate: 85,
|
|
|
+ excellentWorkRate: 82,
|
|
|
+ satisfactionRate: 90,
|
|
|
+ overdueRate: 15
|
|
|
+ },
|
|
|
+ {
|
|
|
+ department: '前端开发部',
|
|
|
+ completionRate: 88,
|
|
|
+ excellentWorkRate: 75,
|
|
|
+ satisfactionRate: 85,
|
|
|
+ overdueRate: 12
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ promotionRules: PromotionRule[] = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ title: '初级→中级晋升标准',
|
|
|
+ description: '设计师晋升中级标准',
|
|
|
+ conditions: [
|
|
|
+ '连续3个月优秀作品率超过80%',
|
|
|
+ '逾期率低于5%',
|
|
|
+ '客户满意度评分4.5以上',
|
|
|
+ '至少完成12个主要项目'
|
|
|
]
|
|
|
},
|
|
|
- {
|
|
|
- issue: '投诉差评',
|
|
|
- departments: [
|
|
|
- { name: '客服部', count: 4 },
|
|
|
- { name: '设计部', count: 2 }
|
|
|
- ],
|
|
|
- employees: [
|
|
|
- { name: '王五', department: '客服部', count: 3 },
|
|
|
- { name: '赵六', department: '设计部', count: 1 }
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ title: '中级→高级晋升标准',
|
|
|
+ description: '设计师晋升高级标准',
|
|
|
+ conditions: [
|
|
|
+ '连续6个月优秀作品率超过85%',
|
|
|
+ '逾期率低于3%',
|
|
|
+ '客户满意度评分4.8以上',
|
|
|
+ '带领过至少2个大型项目',
|
|
|
+ '有 mentorship 经验'
|
|
|
]
|
|
|
}
|
|
|
- ]);
|
|
|
-
|
|
|
- // 关键数据
|
|
|
- employeeStructures = signal<EmployeeStructure[]>([
|
|
|
+ ];
|
|
|
+
|
|
|
+ promotionSuggestions: PromotionSuggestion[] = [
|
|
|
{
|
|
|
- category: '技术等级',
|
|
|
- data: [
|
|
|
- { name: '初级', count: 45, percentage: 37.5 },
|
|
|
- { name: '中级', count: 55, percentage: 45.8 },
|
|
|
- { name: '高级', count: 20, percentage: 16.7 }
|
|
|
+ employeeId: 1001,
|
|
|
+ employeeName: '张三',
|
|
|
+ currentRank: '初级设计师',
|
|
|
+ suggestedRank: '中级设计师',
|
|
|
+ reasons: [
|
|
|
+ '连续3个月优秀作品率85%',
|
|
|
+ '逾期率仅2%',
|
|
|
+ '客户满意度4.7分',
|
|
|
+ '已完成15个主要项目'
|
|
|
+ ],
|
|
|
+ status: 'pending'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ employeeId: 1002,
|
|
|
+ employeeName: '李四',
|
|
|
+ currentRank: '中级设计师',
|
|
|
+ suggestedRank: '高级设计师',
|
|
|
+ reasons: [
|
|
|
+ '连续6个月优秀作品率88%',
|
|
|
+ '逾期率1.5%',
|
|
|
+ '客户满意度4.9分',
|
|
|
+ '成功带领3个大型项目'
|
|
|
+ ],
|
|
|
+ status: 'pending'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ onboardingCheckpoints: OnboardingCheckpoint[] = [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ title: '入职1周面谈',
|
|
|
+ description: '了解新人工作适应情况',
|
|
|
+ dueDate: new Date(),
|
|
|
+ completed: false,
|
|
|
+ interviewTemplate: [
|
|
|
+ '工作环境是否适应?',
|
|
|
+ '是否有不清楚的工作流程?',
|
|
|
+ '团队沟通是否顺畅?',
|
|
|
+ '是否需要额外的工作工具?'
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
- category: '岗位类型',
|
|
|
- data: [
|
|
|
- { name: '设计师', count: 60, percentage: 50 },
|
|
|
- { name: '客服', count: 30, percentage: 25 },
|
|
|
- { name: '技术组长', count: 15, percentage: 12.5 },
|
|
|
- { name: '其他', count: 15, percentage: 12.5 }
|
|
|
+ id: 2,
|
|
|
+ title: '入职1个月评估',
|
|
|
+ description: '评估新人技能掌握情况',
|
|
|
+ dueDate: new Date(Date.now() + 21 * 24 * 60 * 60 * 1000),
|
|
|
+ completed: false,
|
|
|
+ interviewTemplate: [
|
|
|
+ '主要工作技能掌握程度?',
|
|
|
+ '遇到的最大挑战是什么?',
|
|
|
+ '对团队文化的感受?',
|
|
|
+ '个人职业发展期望?'
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
- category: '部门',
|
|
|
- data: [
|
|
|
- { name: '设计部', count: 65, percentage: 54.2 },
|
|
|
- { name: '客服部', count: 35, percentage: 29.2 },
|
|
|
- { name: '技术部', count: 20, percentage: 16.6 }
|
|
|
+ id: 3,
|
|
|
+ title: '入职3个月总结',
|
|
|
+ description: '全面评估新人表现',
|
|
|
+ dueDate: new Date(Date.now() + 83 * 24 * 60 * 60 * 1000),
|
|
|
+ completed: false,
|
|
|
+ interviewTemplate: [
|
|
|
+ '工作成果总体评价',
|
|
|
+ '需要改进的技能领域',
|
|
|
+ '长期职业规划讨论',
|
|
|
+ '转正评估准备'
|
|
|
]
|
|
|
}
|
|
|
- ]);
|
|
|
-
|
|
|
- movementTrends = signal<MovementTrend[]>([
|
|
|
- { month: '1月', hired: 12, resigned: 5 },
|
|
|
- { month: '2月', hired: 8, resigned: 4 },
|
|
|
- { month: '3月', hired: 15, resigned: 6 },
|
|
|
- { month: '4月', hired: 10, resigned: 8 },
|
|
|
- { month: '5月', hired: 14, resigned: 7 },
|
|
|
- { month: '6月', hired: 18, resigned: 5 }
|
|
|
- ]);
|
|
|
-
|
|
|
- todoItems = signal<TodoItem[]>([
|
|
|
- { task: '审核张三绩效表', dueDate: new Date('2023-06-15'), priority: 'high', type: '绩效审核' },
|
|
|
- { task: '签署李四离职协议', dueDate: new Date('2023-06-12'), priority: 'high', type: '离职手续' },
|
|
|
- { task: '跟进王五试用期评估', dueDate: new Date('2023-06-20'), priority: 'medium', type: '试用期评估' },
|
|
|
- { task: '审核新员工入职材料', dueDate: new Date('2023-06-18'), priority: 'medium', type: '入职手续' },
|
|
|
- { task: '更新部门人员架构图', dueDate: new Date('2023-06-25'), priority: 'low', type: '文档更新' }
|
|
|
- ]);
|
|
|
-
|
|
|
+ ];
|
|
|
+
|
|
|
ngOnInit() {
|
|
|
- // 实际项目中这里会调用服务获取数据
|
|
|
- }
|
|
|
-
|
|
|
- ngAfterViewInit(): void {
|
|
|
+ // 注册Chart.js组件
|
|
|
+ Chart.register(...registerables);
|
|
|
+ // 初始化图表数据
|
|
|
this.initCharts();
|
|
|
- window.addEventListener('resize', this.handleResize);
|
|
|
}
|
|
|
-
|
|
|
- ngOnDestroy(): void {
|
|
|
- window.removeEventListener('resize', this.handleResize);
|
|
|
- this.disposeCharts();
|
|
|
+
|
|
|
+ ngAfterViewInit() {
|
|
|
+ // 延迟初始化图表,确保DOM已渲染
|
|
|
+ setTimeout(() => {
|
|
|
+ this.initializeCharts();
|
|
|
+ }, 100);
|
|
|
}
|
|
|
-
|
|
|
- // 切换时间维度
|
|
|
- changeTimeFilter(filter: string) {
|
|
|
- this.timeFilter.set(filter);
|
|
|
- // 实际项目中这里会根据筛选条件重新获取数据
|
|
|
- // 刷新图表
|
|
|
- this.initCharts();
|
|
|
+
|
|
|
+ // 切换标签页
|
|
|
+ switchTab(tab: 'visualization' | 'promotion' | 'onboarding') {
|
|
|
+ this.activeTab = tab;
|
|
|
}
|
|
|
-
|
|
|
- // 跳转到对应处理页面
|
|
|
- navigateToProcess(type: 'hire' | 'resign') {
|
|
|
- // 实际项目中这里会导航到对应页面
|
|
|
- console.log(`Navigate to ${type} process page`);
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 初始化图表(这里需要后续集成ECharts)
|
|
|
+ private initCharts() {
|
|
|
+ // 图表初始化逻辑将在后续实现
|
|
|
+ console.log('初始化图表数据');
|
|
|
}
|
|
|
-
|
|
|
- // 获取进度条颜色
|
|
|
- getProgressColor(value: number, target: number): string {
|
|
|
- const percentage = (value / target) * 100;
|
|
|
- if (percentage >= 90) return 'primary';
|
|
|
- if (percentage >= 70) return 'accent';
|
|
|
- return 'warn';
|
|
|
+
|
|
|
+ // 拖拽排序
|
|
|
+ drop(event: CdkDragDrop<TodoItem[]>) {
|
|
|
+ moveItemInArray(this.todoItems, event.previousIndex, event.currentIndex);
|
|
|
}
|
|
|
-
|
|
|
- // 获取优先级样式类
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 获取优先级颜色
|
|
|
+ getPriorityColor(priority: string): string {
|
|
|
+ switch (priority) {
|
|
|
+ case 'high': return '#ff4757';
|
|
|
+ case 'medium': return '#ffa502';
|
|
|
+ case 'low': return '#2ed573';
|
|
|
+ default: return '#a4b0be';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取空缺岗位图标
|
|
|
+ getVacancyIcon(urgency: string): string {
|
|
|
+ switch (urgency) {
|
|
|
+ case 'urgent': return 'warning';
|
|
|
+ case 'normal': return 'info';
|
|
|
+ default: return 'help';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取部门颜色
|
|
|
+ getDepartmentColor(department: string): string {
|
|
|
+ const colors: { [key: string]: string } = {
|
|
|
+ 'UI设计部': '#2196F3',
|
|
|
+ '3D建模部': '#4CAF50',
|
|
|
+ '前端开发部': '#FF9800',
|
|
|
+ '产品部': '#9C27B0'
|
|
|
+ };
|
|
|
+ return colors[department] || '#757575';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取优先级类名
|
|
|
getPriorityClass(priority: string): string {
|
|
|
+ return `priority-${priority}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleTodoList(): void {
|
|
|
+ this.showTodoList = !this.showTodoList;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取甜甜圈图表特定选项
|
|
|
+ private getDoughnutOptions(): any {
|
|
|
+ return {
|
|
|
+ cutout: '60%'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化所有图表
|
|
|
+ private initializeCharts() {
|
|
|
+ this.initPieChart();
|
|
|
+ this.initLineChart();
|
|
|
+ this.initRadarChart();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化职级分布饼图
|
|
|
+ private initPieChart() {
|
|
|
+ if (!this.pieChartRef?.nativeElement) return;
|
|
|
+
|
|
|
+ const ctx = this.pieChartRef.nativeElement.getContext('2d');
|
|
|
+ if (!ctx) return;
|
|
|
+
|
|
|
+ const config: ChartConfiguration = {
|
|
|
+ type: 'doughnut',
|
|
|
+ data: {
|
|
|
+ labels: this.rankDistribution.map(item => item.level),
|
|
|
+ datasets: [{
|
|
|
+ data: this.rankDistribution.map(item => item.percentage),
|
|
|
+ backgroundColor: this.rankDistribution.map(item => item.color),
|
|
|
+ borderWidth: 0,
|
|
|
+ hoverBorderWidth: 2,
|
|
|
+ hoverBorderColor: '#fff'
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: false
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ callbacks: {
|
|
|
+ label: (context) => {
|
|
|
+ const label = context.label || '';
|
|
|
+ const value = context.parsed;
|
|
|
+ const count = this.rankDistribution[context.dataIndex].count;
|
|
|
+ return `${label}: ${value}% (${count}人)`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ...(this.getDoughnutOptions())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ this.pieChart = new Chart(ctx, config);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化入职离职趋势折线图
|
|
|
+ private initLineChart() {
|
|
|
+ if (!this.lineChartRef?.nativeElement) return;
|
|
|
+
|
|
|
+ const ctx = this.lineChartRef.nativeElement.getContext('2d');
|
|
|
+ if (!ctx) return;
|
|
|
+
|
|
|
+ const config: ChartConfiguration = {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: this.monthlyHireData.map(item => item.month),
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: '入职人数',
|
|
|
+ data: this.monthlyHireData.map(item => item.hired),
|
|
|
+ borderColor: '#4CAF50',
|
|
|
+ backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
|
+ tension: 0.4,
|
|
|
+ fill: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '离职人数',
|
|
|
+ data: this.monthlyHireData.map(item => item.left),
|
|
|
+ borderColor: '#f44336',
|
|
|
+ backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
|
|
+ tension: 0.4,
|
|
|
+ fill: true
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top'
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ interaction: {
|
|
|
+ mode: 'nearest',
|
|
|
+ axis: 'x',
|
|
|
+ intersect: false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ this.lineChart = new Chart(ctx, config);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化绩效总览雷达图
|
|
|
+ private initRadarChart() {
|
|
|
+ if (!this.radarChartRef?.nativeElement) return;
|
|
|
+
|
|
|
+ const ctx = this.radarChartRef.nativeElement.getContext('2d');
|
|
|
+ if (!ctx) return;
|
|
|
+
|
|
|
+ const config: ChartConfiguration = {
|
|
|
+ type: 'radar',
|
|
|
+ data: {
|
|
|
+ labels: ['项目完成率', '优秀作品率', '客户满意度', '逾期率'],
|
|
|
+ datasets: this.departmentPerformance.map((dept, index) => ({
|
|
|
+ label: dept.department,
|
|
|
+ data: [
|
|
|
+ dept.completionRate,
|
|
|
+ dept.excellentWorkRate,
|
|
|
+ dept.satisfactionRate,
|
|
|
+ 100 - dept.overdueRate // 逾期率转换为正向指标
|
|
|
+ ],
|
|
|
+ borderColor: this.getDepartmentColor(dept.department),
|
|
|
+ backgroundColor: this.getDepartmentColor(dept.department) + '20',
|
|
|
+ pointBackgroundColor: this.getDepartmentColor(dept.department),
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointHoverBackgroundColor: '#fff',
|
|
|
+ pointHoverBorderColor: this.getDepartmentColor(dept.department)
|
|
|
+ }))
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'bottom'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ r: {
|
|
|
+ beginAtZero: true,
|
|
|
+ max: 100,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ },
|
|
|
+ angleLines: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ },
|
|
|
+ pointLabels: {
|
|
|
+ font: {
|
|
|
+ size: 12
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ this.radarChart = new Chart(ctx, config);
|
|
|
+ }
|
|
|
+
|
|
|
+ getPriorityLabel(priority: string): string {
|
|
|
switch (priority) {
|
|
|
- case 'high': return 'priority-high';
|
|
|
- case 'medium': return 'priority-medium';
|
|
|
- case 'low': return 'priority-low';
|
|
|
- default: return '';
|
|
|
+ case 'high': return '紧急';
|
|
|
+ case 'medium': return '重要';
|
|
|
+ case 'low': return '一般';
|
|
|
+ default: return '未知';
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // ====== 图表相关 ======
|
|
|
- private initCharts(): void {
|
|
|
- this.initMovementChart();
|
|
|
- this.initPerformanceDistributionChart();
|
|
|
- this.initTrendChart();
|
|
|
+
|
|
|
+ getTypeLabel(type: string): string {
|
|
|
+ switch (type) {
|
|
|
+ case 'resume': return '简历初筛';
|
|
|
+ case 'onboarding': return '入职评定';
|
|
|
+ case 'promotion': return '晋升审核';
|
|
|
+ case 'resignation': return '离职面谈';
|
|
|
+ default: return '其他';
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- private disposeCharts(): void {
|
|
|
- if (this.movementChart) { this.movementChart.dispose(); this.movementChart = null; }
|
|
|
- if (this.performanceChart) { this.performanceChart.dispose(); this.performanceChart = null; }
|
|
|
- if (this.trendChart) { this.trendChart.dispose(); this.trendChart = null; }
|
|
|
+
|
|
|
+ getStatusLabel(status: string): string {
|
|
|
+ const statusMap: { [key: string]: string } = {
|
|
|
+ 'pending': '待处理',
|
|
|
+ 'in_progress': '进行中',
|
|
|
+ 'completed': '已完成',
|
|
|
+ 'urgent': '紧急',
|
|
|
+ 'normal': '普通'
|
|
|
+ };
|
|
|
+ return statusMap[status] || status;
|
|
|
}
|
|
|
-
|
|
|
- private initMovementChart(): void {
|
|
|
- const el = document.getElementById('hrMovementChart');
|
|
|
- if (!el) return;
|
|
|
- this.movementChart?.dispose();
|
|
|
- // 使用全局 echarts 变量
|
|
|
- // @ts-ignore
|
|
|
- this.movementChart = echarts.init(el);
|
|
|
-
|
|
|
- const data = this.employeeMovements();
|
|
|
- const months = data.map(d => d.month);
|
|
|
- const hired = data.map(d => d.hired);
|
|
|
- const resigned = data.map(d => d.resigned);
|
|
|
-
|
|
|
- this.movementChart.setOption({
|
|
|
- title: { text: '月度入职/离职', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
- legend: { data: ['入职', '离职'] },
|
|
|
- grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
|
- xAxis: { type: 'category', data: months },
|
|
|
- yAxis: { type: 'value' },
|
|
|
- series: [
|
|
|
- { name: '入职', type: 'bar', data: hired, itemStyle: { color: '#165DFF' }, barGap: 0 },
|
|
|
- { name: '离职', type: 'bar', data: resigned, itemStyle: { color: '#F53F3F' } }
|
|
|
- ]
|
|
|
- });
|
|
|
+
|
|
|
+ // 拖拽排序功能
|
|
|
+ onTodoDrop(event: CdkDragDrop<TodoItem[]>): void {
|
|
|
+ moveItemInArray(this.todoItems, event.previousIndex, event.currentIndex);
|
|
|
}
|
|
|
-
|
|
|
- private initPerformanceDistributionChart(): void {
|
|
|
- const el = document.getElementById('hrPerformanceDistributionChart');
|
|
|
- if (!el) return;
|
|
|
- this.performanceChart?.dispose();
|
|
|
- // @ts-ignore
|
|
|
- this.performanceChart = echarts.init(el);
|
|
|
-
|
|
|
- const tiers = this.performanceTiers();
|
|
|
- const data = tiers.map(t => ({ value: t.count, name: `${t.name}(${t.count}人)` }));
|
|
|
-
|
|
|
- this.performanceChart.setOption({
|
|
|
- title: { text: '绩效等级分布', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
- tooltip: { trigger: 'item', formatter: '{b}: {c} 人 ({d}%)' },
|
|
|
- legend: { orient: 'horizontal', bottom: 0 },
|
|
|
- series: [{
|
|
|
- type: 'pie', radius: '65%', center: ['50%', '50%'],
|
|
|
- data,
|
|
|
- emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.3)' } }
|
|
|
- }]
|
|
|
- });
|
|
|
+
|
|
|
+ // 更新待办事项状态
|
|
|
+ updateTodoStatus(todo: TodoItem, status: 'pending' | 'completed' | 'in_progress'): void {
|
|
|
+ todo.status = status;
|
|
|
}
|
|
|
-
|
|
|
- private initTrendChart(): void {
|
|
|
- const el = document.getElementById('hrTrendChart');
|
|
|
- if (!el) return;
|
|
|
- this.trendChart?.dispose();
|
|
|
- // @ts-ignore
|
|
|
- this.trendChart = echarts.init(el);
|
|
|
-
|
|
|
- const trend = this.movementTrends();
|
|
|
- const months = trend.map(t => t.month);
|
|
|
- const hired = trend.map(t => t.hired);
|
|
|
- const resigned = trend.map(t => t.resigned);
|
|
|
-
|
|
|
- this.trendChart.setOption({
|
|
|
- title: { text: '异动趋势', left: 'center', textStyle: { fontSize: 16 } },
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
- legend: { data: ['入职', '离职'] },
|
|
|
- xAxis: { type: 'category', data: months },
|
|
|
- yAxis: { type: 'value' },
|
|
|
- series: [
|
|
|
- { name: '入职', type: 'line', data: hired, smooth: true, lineStyle: { color: '#00B42A' }, itemStyle: { color: '#00B42A' } },
|
|
|
- { name: '离职', type: 'line', data: resigned, smooth: true, lineStyle: { color: '#F53F3F' }, itemStyle: { color: '#F53F3F' } }
|
|
|
- ]
|
|
|
- });
|
|
|
+
|
|
|
+ // 处理按钮按压效果
|
|
|
+ isButtonPressed = false;
|
|
|
+ handleButtonPress(action: 'press' | 'release') {
|
|
|
+ this.isButtonPressed = action === 'press';
|
|
|
}
|
|
|
-
|
|
|
- private handleResize = (): void => {
|
|
|
- this.movementChart?.resize();
|
|
|
- this.performanceChart?.resize();
|
|
|
- this.trendChart?.resize();
|
|
|
- };
|
|
|
}
|