|
@@ -16,11 +16,13 @@ import { MatTableModule } from '@angular/material/table';
|
|
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
|
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
|
|
|
+import { MatDialogModule, MatDialog } from '@angular/material/dialog';
|
|
|
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';
|
|
|
import { DoubaoAiService, ResumeAnalysisRequest, ResumeAnalysisResponse } from '../../../services/doubao-ai.service';
|
|
|
import { ResignationDetailPanelComponent, DetailAnalysis, ImprovementPlan } from './resignation-detail-panel.component';
|
|
|
+import { AddComparisonDialogComponent, ComparisonItemData } from './add-comparison-dialog.component';
|
|
|
|
|
|
Chart.register(...registerables);
|
|
|
// 数据模型定义
|
|
@@ -160,6 +162,7 @@ export interface PerformanceMetric {
|
|
|
MatButtonToggleModule,
|
|
|
MatProgressSpinnerModule,
|
|
|
MatSnackBarModule,
|
|
|
+ MatDialogModule,
|
|
|
ResignationDetailPanelComponent
|
|
|
],
|
|
|
templateUrl: './dashboard.html',
|
|
@@ -181,10 +184,12 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
@ViewChild('lineChart', { static: false }) lineChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
@ViewChild('radarChart', { static: false }) radarChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
@ViewChild('resignationChart', { static: false }) resignationChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
+ @ViewChild('comparisonChart', { static: false }) comparisonChartRef!: ElementRef<HTMLCanvasElement>;
|
|
|
|
|
|
constructor(
|
|
|
private doubaoAiService: DoubaoAiService,
|
|
|
- private snackBar: MatSnackBar
|
|
|
+ private snackBar: MatSnackBar,
|
|
|
+ private dialog: MatDialog
|
|
|
) {
|
|
|
Chart.register(...registerables);
|
|
|
}
|
|
@@ -193,9 +198,13 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
private lineChart!: Chart;
|
|
|
private radarChart!: Chart;
|
|
|
private resignationChart!: Chart;
|
|
|
+ private comparisonChart!: Chart;
|
|
|
// 当前激活的标签页
|
|
|
activeTab: 'visualization' | 'recruitment' | 'performance' | 'onboarding' = 'visualization';
|
|
|
|
|
|
+ // 对比图表类型
|
|
|
+ chartType: 'bar' | 'line' | 'radar' = 'line';
|
|
|
+
|
|
|
// 待办事项是否展开
|
|
|
isTodoExpanded = false;
|
|
|
|
|
@@ -525,7 +534,7 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
comparisonMode: 'horizontal' | 'vertical' = 'horizontal';
|
|
|
selectedComparisonDimension: string = 'department';
|
|
|
selectedComparisonMetric: string[] = ['completion', 'quality'];
|
|
|
- chartType: 'bar' | 'line' | 'radar' = 'bar';
|
|
|
+ comparisonChartType: 'bar' | 'line' | 'radar' = 'bar';
|
|
|
|
|
|
horizontalComparisonData: any[] = [
|
|
|
{
|
|
@@ -962,24 +971,134 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
|
|
|
// 招聘阶段相关方法
|
|
|
refreshRecruitmentData(): void {
|
|
|
- // 刷新招聘数据
|
|
|
- console.log('刷新招聘数据');
|
|
|
- // 这里可以调用API刷新数据
|
|
|
+ // 显示加载状态
|
|
|
+ this.snackBar.open('正在刷新招聘数据...', '', {
|
|
|
+ duration: 1000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 模拟API调用刷新数据
|
|
|
+ setTimeout(() => {
|
|
|
+ // 更新招聘阶段数据
|
|
|
+ this.recruitmentStages.forEach(stage => {
|
|
|
+ stage.lastUpdate = new Date();
|
|
|
+ // 随机更新一些数据以模拟真实变化
|
|
|
+ if (Math.random() > 0.5) {
|
|
|
+ stage.candidateCount += Math.floor(Math.random() * 3);
|
|
|
+ stage.passRate = Math.min(100, stage.passRate + Math.floor(Math.random() * 5));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.snackBar.open('招聘数据已更新', '关闭', {
|
|
|
+ duration: 3000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['success-snackbar']
|
|
|
+ });
|
|
|
+ }, 1000);
|
|
|
}
|
|
|
|
|
|
openStageDetails(stage: RecruitmentStage): void {
|
|
|
- console.log('打开阶段详情:', stage);
|
|
|
- // 这里可以打开详情弹窗或导航到详情页面
|
|
|
+ // 显示阶段详情信息
|
|
|
+ const stageInfo = this.getStageDetailInfo(stage);
|
|
|
+
|
|
|
+ this.snackBar.open(`${stage.title} - ${stageInfo}`, '查看详情', {
|
|
|
+ duration: 5000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['info-snackbar']
|
|
|
+ }).onAction().subscribe(() => {
|
|
|
+ // 这里可以打开详情弹窗或导航到详情页面
|
|
|
+ console.log('导航到详情页面:', stage);
|
|
|
+ this.showStageDetailDialog(stage);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private getStageDetailInfo(stage: RecruitmentStage): string {
|
|
|
+ switch (stage.status) {
|
|
|
+ case 'completed':
|
|
|
+ return `已完成,通过率${stage.passRate}%`;
|
|
|
+ case 'active':
|
|
|
+ return `进行中,当前${stage.candidateCount}人`;
|
|
|
+ case 'pending':
|
|
|
+ return `待开始,预计${stage.candidateCount}人`;
|
|
|
+ case 'blocked':
|
|
|
+ return `已暂停,需要处理`;
|
|
|
+ default:
|
|
|
+ return '状态未知';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private showStageDetailDialog(stage: RecruitmentStage): void {
|
|
|
+ // 显示详细的阶段信息弹窗
|
|
|
+ const detailMessage = `
|
|
|
+ 阶段:${stage.title}
|
|
|
+ 状态:${stage.statusText}
|
|
|
+ 候选人数量:${stage.candidateCount}人
|
|
|
+ 通过率:${stage.passRate}%
|
|
|
+ 评估人:${stage.evaluator || '待分配'}
|
|
|
+ 最近更新:${stage.lastUpdate.toLocaleString()}
|
|
|
+ 下一步行动:${stage.nextAction || '无'}
|
|
|
+ `;
|
|
|
+
|
|
|
+ this.snackBar.open(detailMessage, '关闭', {
|
|
|
+ duration: 8000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['detail-snackbar']
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
navigateToOnboarding(): void {
|
|
|
- console.log('导航到新人跟进模块');
|
|
|
- // 这里可以使用Router导航到新人跟进页面
|
|
|
+ this.snackBar.open('正在跳转到新人跟进模块...', '', {
|
|
|
+ duration: 2000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 切换到入职跟进标签页
|
|
|
+ setTimeout(() => {
|
|
|
+ this.switchTab('onboarding');
|
|
|
+ this.snackBar.open('已切换到新人跟进模块', '关闭', {
|
|
|
+ duration: 3000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['success-snackbar']
|
|
|
+ });
|
|
|
+ }, 1000);
|
|
|
}
|
|
|
|
|
|
viewProbationReports(): void {
|
|
|
- console.log('查看试用期报告');
|
|
|
- // 这里可以打开试用期报告页面
|
|
|
+ // 显示试用期报告信息
|
|
|
+ const reportSummary = `
|
|
|
+ 试用期跟踪报告:
|
|
|
+ - 当前试用期员工:${this.recruitmentStages.find(s => s.id === 'probation-tracking')?.candidateCount || 0}人
|
|
|
+ - 通过率:${this.recruitmentStages.find(s => s.id === 'probation-tracking')?.passRate || 0}%
|
|
|
+ - 本月评估:3人待评估
|
|
|
+ - 转正推荐:2人
|
|
|
+ `;
|
|
|
+
|
|
|
+ this.snackBar.open(reportSummary, '查看详细报告', {
|
|
|
+ duration: 6000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['info-snackbar']
|
|
|
+ }).onAction().subscribe(() => {
|
|
|
+ // 这里可以打开详细的试用期报告页面
|
|
|
+ console.log('打开试用期详细报告');
|
|
|
+ this.showProbationDetailReport();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private showProbationDetailReport(): void {
|
|
|
+ // 显示详细的试用期报告
|
|
|
+ this.snackBar.open('试用期详细报告功能开发中,敬请期待!', '关闭', {
|
|
|
+ duration: 3000,
|
|
|
+ horizontalPosition: 'center',
|
|
|
+ verticalPosition: 'top',
|
|
|
+ panelClass: ['warning-snackbar']
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// 滑动功能相关属性和方法
|
|
@@ -1267,6 +1386,8 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
private initCharts() {
|
|
|
// 初始化离职原因图表
|
|
|
this.initResignationChart();
|
|
|
+ // 初始化对比图表
|
|
|
+ this.initComparisonChart();
|
|
|
}
|
|
|
|
|
|
// 初始化离职原因图表
|
|
@@ -1486,6 +1607,273 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
}, 50);
|
|
|
}
|
|
|
|
|
|
+ // 初始化对比图表
|
|
|
+ private initComparisonChart() {
|
|
|
+ if (!this.comparisonChartRef?.nativeElement) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ctx = this.comparisonChartRef.nativeElement.getContext('2d');
|
|
|
+ if (!ctx) return;
|
|
|
+
|
|
|
+ // 销毁现有图表
|
|
|
+ if (this.comparisonChart) {
|
|
|
+ this.comparisonChart.destroy();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据图表类型创建不同的配置
|
|
|
+ let config: ChartConfiguration;
|
|
|
+
|
|
|
+ if (this.comparisonChartType === 'bar') {
|
|
|
+ config = {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'UI设计部',
|
|
|
+ data: [85, 88, 92, 89, 91, 94],
|
|
|
+ backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
|
|
+ borderColor: 'rgba(76, 175, 80, 1)',
|
|
|
+ borderWidth: 1
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '3D建模部',
|
|
|
+ data: [78, 82, 85, 87, 84, 89],
|
|
|
+ backgroundColor: 'rgba(33, 150, 243, 0.8)',
|
|
|
+ borderColor: 'rgba(33, 150, 243, 1)',
|
|
|
+ borderWidth: 1
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '前端开发部',
|
|
|
+ data: [82, 85, 88, 86, 90, 92],
|
|
|
+ backgroundColor: 'rgba(255, 152, 0, 0.8)',
|
|
|
+ borderColor: 'rgba(255, 152, 0, 1)',
|
|
|
+ borderWidth: 1
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ callbacks: {
|
|
|
+ label: (context) => {
|
|
|
+ return `${context.dataset.label}: ${context.parsed.y}%`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: {
|
|
|
+ display: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ max: 100,
|
|
|
+ ticks: {
|
|
|
+ callback: (value) => `${value}%`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else if (this.comparisonChartType === 'line') {
|
|
|
+ config = {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'UI设计部',
|
|
|
+ data: [85, 88, 92, 89, 91, 94],
|
|
|
+ borderColor: 'rgba(76, 175, 80, 1)',
|
|
|
+ backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
|
+ borderWidth: 3,
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4,
|
|
|
+ pointBackgroundColor: 'rgba(76, 175, 80, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2,
|
|
|
+ pointRadius: 6
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '3D建模部',
|
|
|
+ data: [78, 82, 85, 87, 84, 89],
|
|
|
+ borderColor: 'rgba(33, 150, 243, 1)',
|
|
|
+ backgroundColor: 'rgba(33, 150, 243, 0.1)',
|
|
|
+ borderWidth: 3,
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4,
|
|
|
+ pointBackgroundColor: 'rgba(33, 150, 243, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2,
|
|
|
+ pointRadius: 6
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '前端开发部',
|
|
|
+ data: [82, 85, 88, 86, 90, 92],
|
|
|
+ borderColor: 'rgba(255, 152, 0, 1)',
|
|
|
+ backgroundColor: 'rgba(255, 152, 0, 0.1)',
|
|
|
+ borderWidth: 3,
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4,
|
|
|
+ pointBackgroundColor: 'rgba(255, 152, 0, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2,
|
|
|
+ pointRadius: 6
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ callbacks: {
|
|
|
+ label: (context) => {
|
|
|
+ return `${context.dataset.label}: ${context.parsed.y}%`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: {
|
|
|
+ display: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ max: 100,
|
|
|
+ ticks: {
|
|
|
+ callback: (value) => `${value}%`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ interaction: {
|
|
|
+ mode: 'nearest',
|
|
|
+ axis: 'x',
|
|
|
+ intersect: false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else { // radar
|
|
|
+ config = {
|
|
|
+ type: 'radar',
|
|
|
+ data: {
|
|
|
+ labels: ['完成率', '优秀率', '满意度', '按时率', '创新度', '协作度'],
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'UI设计部',
|
|
|
+ data: [92, 78, 88, 92, 85, 90],
|
|
|
+ borderColor: 'rgba(76, 175, 80, 1)',
|
|
|
+ backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
|
|
+ borderWidth: 2,
|
|
|
+ pointBackgroundColor: 'rgba(76, 175, 80, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '3D建模部',
|
|
|
+ data: [85, 82, 90, 88, 92, 85],
|
|
|
+ borderColor: 'rgba(33, 150, 243, 1)',
|
|
|
+ backgroundColor: 'rgba(33, 150, 243, 0.2)',
|
|
|
+ borderWidth: 2,
|
|
|
+ pointBackgroundColor: 'rgba(33, 150, 243, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '前端开发部',
|
|
|
+ data: [88, 75, 85, 88, 88, 92],
|
|
|
+ borderColor: 'rgba(255, 152, 0, 1)',
|
|
|
+ backgroundColor: 'rgba(255, 152, 0, 0.2)',
|
|
|
+ borderWidth: 2,
|
|
|
+ pointBackgroundColor: 'rgba(255, 152, 0, 1)',
|
|
|
+ pointBorderColor: '#fff',
|
|
|
+ pointBorderWidth: 2
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ r: {
|
|
|
+ beginAtZero: true,
|
|
|
+ max: 100,
|
|
|
+ ticks: {
|
|
|
+ stepSize: 20,
|
|
|
+ callback: (value) => `${value}%`
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ },
|
|
|
+ angleLines: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ this.comparisonChart = new Chart(ctx, config);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换对比图表类型
|
|
|
+ onComparisonChartTypeChange() {
|
|
|
+ // 添加加载状态
|
|
|
+ const chartContainer = this.comparisonChartRef?.nativeElement?.parentElement;
|
|
|
+ if (chartContainer) {
|
|
|
+ chartContainer.style.opacity = '0.7';
|
|
|
+ chartContainer.style.transition = 'opacity 0.3s ease';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用 setTimeout 确保 UI 更新
|
|
|
+ setTimeout(() => {
|
|
|
+ this.initComparisonChart();
|
|
|
+
|
|
|
+ // 恢复透明度
|
|
|
+ if (chartContainer) {
|
|
|
+ setTimeout(() => {
|
|
|
+ chartContainer.style.opacity = '1';
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ }, 50);
|
|
|
+ }
|
|
|
+
|
|
|
// 拖拽排序
|
|
|
drop(event: CdkDragDrop<TodoItem[]>) {
|
|
|
moveItemInArray(this.todoItems, event.previousIndex, event.currentIndex);
|
|
@@ -2074,8 +2462,7 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
console.log('查看指标详情:', metricId);
|
|
|
const metric = this.performanceMetrics.find(m => m.id === metricId);
|
|
|
if (metric) {
|
|
|
- // 这里可以打开详情对话框或导航到详情页面
|
|
|
- console.log('指标详情:', metric);
|
|
|
+ this.showMetricDetailsFeedback(metric);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -2083,11 +2470,312 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
console.log('查看指标趋势:', metricId);
|
|
|
const metric = this.performanceMetrics.find(m => m.id === metricId);
|
|
|
if (metric) {
|
|
|
- // 这里可以打开趋势图表对话框
|
|
|
- console.log('指标趋势:', metric);
|
|
|
+ this.showMetricTrendFeedback(metric);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private showMetricDetailsFeedback(metric: PerformanceMetric) {
|
|
|
+ const feedback = document.createElement('div');
|
|
|
+ feedback.className = 'metric-details-feedback';
|
|
|
+ feedback.innerHTML = `
|
|
|
+ <div class="feedback-header">
|
|
|
+ <mat-icon>${metric.icon}</mat-icon>
|
|
|
+ <span>查看${metric.title}详情</span>
|
|
|
+ </div>
|
|
|
+ <div class="feedback-content">
|
|
|
+ <div class="metric-info">
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="label">当前值:</span>
|
|
|
+ <span class="value">${metric.value}${metric.unit}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="label">目标值:</span>
|
|
|
+ <span class="value">${metric.target}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="label">完成率:</span>
|
|
|
+ <span class="value ${metric.achievementClass}">${metric.achievement}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="label">趋势:</span>
|
|
|
+ <span class="value trend-${metric.trend.type}">
|
|
|
+ <mat-icon>${metric.trend.icon}</mat-icon>
|
|
|
+ ${metric.trend.value} ${metric.trend.label}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ feedback.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
|
|
+ padding: 24px;
|
|
|
+ z-index: 10000;
|
|
|
+ min-width: 400px;
|
|
|
+ max-width: 500px;
|
|
|
+ animation: slideInScale 0.3s ease-out;
|
|
|
+ `;
|
|
|
+
|
|
|
+ const style = document.createElement('style');
|
|
|
+ style.textContent = `
|
|
|
+ @keyframes slideInScale {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translate(-50%, -50%) scale(0.9);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translate(-50%, -50%) scale(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .metric-details-feedback .feedback-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .feedback-header mat-icon {
|
|
|
+ color: #6366f1;
|
|
|
+ font-size: 24px;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .metric-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .info-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .info-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .label {
|
|
|
+ color: #666;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .value {
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .value.excellent {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .value.good {
|
|
|
+ color: #3b82f6;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .value.warning {
|
|
|
+ color: #f59e0b;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .value.poor {
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .trend-positive {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .trend-negative {
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+ .metric-details-feedback .trend-neutral {
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ document.head.appendChild(style);
|
|
|
+
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
+ z-index: 9999;
|
|
|
+ animation: fadeIn 0.3s ease-out;
|
|
|
+ `;
|
|
|
+ overlay.addEventListener('click', () => {
|
|
|
+ document.body.removeChild(overlay);
|
|
|
+ document.body.removeChild(feedback);
|
|
|
+ document.head.removeChild(style);
|
|
|
+ });
|
|
|
+
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+ document.body.appendChild(feedback);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (document.body.contains(overlay)) {
|
|
|
+ document.body.removeChild(overlay);
|
|
|
+ document.body.removeChild(feedback);
|
|
|
+ document.head.removeChild(style);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private showMetricTrendFeedback(metric: PerformanceMetric) {
|
|
|
+ const feedback = document.createElement('div');
|
|
|
+ feedback.className = 'metric-trend-feedback';
|
|
|
+ feedback.innerHTML = `
|
|
|
+ <div class="feedback-header">
|
|
|
+ <mat-icon>trending_up</mat-icon>
|
|
|
+ <span>${metric.title}趋势分析</span>
|
|
|
+ </div>
|
|
|
+ <div class="feedback-content">
|
|
|
+ <div class="trend-chart-placeholder">
|
|
|
+ <mat-icon>show_chart</mat-icon>
|
|
|
+ <p>趋势图表正在加载...</p>
|
|
|
+ </div>
|
|
|
+ <div class="trend-summary">
|
|
|
+ <div class="summary-item">
|
|
|
+ <span class="label">当前趋势:</span>
|
|
|
+ <span class="value trend-${metric.trend.type}">
|
|
|
+ <mat-icon>${metric.trend.icon}</mat-icon>
|
|
|
+ ${metric.trend.type === 'positive' ? '上升' : metric.trend.type === 'negative' ? '下降' : '平稳'}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span class="label">变化幅度:</span>
|
|
|
+ <span class="value">${metric.trend.value}</span>
|
|
|
+ </div>
|
|
|
+ <div class="summary-item">
|
|
|
+ <span class="label">对比周期:</span>
|
|
|
+ <span class="value">${metric.trend.label}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ feedback.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
|
|
+ padding: 24px;
|
|
|
+ z-index: 10000;
|
|
|
+ min-width: 450px;
|
|
|
+ max-width: 550px;
|
|
|
+ animation: slideInScale 0.3s ease-out;
|
|
|
+ `;
|
|
|
+
|
|
|
+ const style = document.createElement('style');
|
|
|
+ style.textContent = `
|
|
|
+ .metric-trend-feedback .feedback-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .feedback-header mat-icon {
|
|
|
+ color: #6366f1;
|
|
|
+ font-size: 24px;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-chart-placeholder {
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 2px dashed #cbd5e1;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 40px;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-chart-placeholder mat-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-chart-placeholder p {
|
|
|
+ color: #64748b;
|
|
|
+ margin: 0;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-summary {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .summary-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .summary-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .label {
|
|
|
+ color: #666;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .value {
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-positive {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-negative {
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+ .metric-trend-feedback .trend-neutral {
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ document.head.appendChild(style);
|
|
|
+
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
+ z-index: 9999;
|
|
|
+ animation: fadeIn 0.3s ease-out;
|
|
|
+ `;
|
|
|
+ overlay.addEventListener('click', () => {
|
|
|
+ document.body.removeChild(overlay);
|
|
|
+ document.body.removeChild(feedback);
|
|
|
+ document.head.removeChild(style);
|
|
|
+ });
|
|
|
+
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+ document.body.appendChild(feedback);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (document.body.contains(overlay)) {
|
|
|
+ document.body.removeChild(overlay);
|
|
|
+ document.body.removeChild(feedback);
|
|
|
+ document.head.removeChild(style);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
private showMetricsRefreshFeedback() {
|
|
|
const feedback = document.createElement('div');
|
|
|
feedback.className = 'metrics-feedback';
|
|
@@ -2322,29 +3010,49 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
}
|
|
|
|
|
|
addComparisonItem(): void {
|
|
|
- // 模拟添加对比项
|
|
|
- const newId = Math.max(...this.horizontalComparisonData.map(item => item.id)) + 1;
|
|
|
- const newItem = {
|
|
|
- id: newId,
|
|
|
- name: `新增项目${newId}`,
|
|
|
- icon: 'add_circle',
|
|
|
- iconClass: 'new-icon',
|
|
|
- completion: '85%',
|
|
|
- quality: '80%',
|
|
|
- efficiency: '82%',
|
|
|
- satisfaction: '85%',
|
|
|
- innovation: '78%'
|
|
|
- };
|
|
|
-
|
|
|
- this.horizontalComparisonData.push(newItem);
|
|
|
- this.verticalComparisonData.push({
|
|
|
- ...newItem,
|
|
|
- category: '新增项目',
|
|
|
- overallScore: 82,
|
|
|
- rank: this.verticalComparisonData.length + 1
|
|
|
+ const dialogRef = this.dialog.open(AddComparisonDialogComponent, {
|
|
|
+ width: '600px',
|
|
|
+ data: {
|
|
|
+ dimension: this.selectedComparisonDimension,
|
|
|
+ availableMetrics: this.selectedComparisonMetric
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
- this.showAddItemFeedback();
|
|
|
+ dialogRef.afterClosed().subscribe((result: ComparisonItemData) => {
|
|
|
+ if (result) {
|
|
|
+ const newId = Math.max(...this.horizontalComparisonData.map(item => item.id)) + 1;
|
|
|
+
|
|
|
+ // 计算综合评分
|
|
|
+ const scores = Object.values(result.metrics).map(value =>
|
|
|
+ parseInt(value.replace('%', ''))
|
|
|
+ );
|
|
|
+ const overallScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length);
|
|
|
+
|
|
|
+ const newItem = {
|
|
|
+ id: newId,
|
|
|
+ name: result.name,
|
|
|
+ icon: result.icon,
|
|
|
+ iconClass: result.iconClass,
|
|
|
+ ...result.metrics
|
|
|
+ };
|
|
|
+
|
|
|
+ this.horizontalComparisonData.push(newItem);
|
|
|
+ this.verticalComparisonData.push({
|
|
|
+ ...newItem,
|
|
|
+ category: result.category,
|
|
|
+ overallScore: overallScore,
|
|
|
+ rank: this.verticalComparisonData.length + 1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重新排序纵向对比数据
|
|
|
+ this.verticalComparisonData.sort((a, b) => b.overallScore - a.overallScore);
|
|
|
+ this.verticalComparisonData.forEach((item, index) => {
|
|
|
+ item.rank = index + 1;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.showAddItemFeedback(result.name);
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
removeComparisonItem(id: number): void {
|
|
@@ -2666,13 +3374,13 @@ export class Dashboard implements OnInit, AfterViewInit {
|
|
|
return 'score-poor';
|
|
|
}
|
|
|
|
|
|
- private showAddItemFeedback(): void {
|
|
|
+ private showAddItemFeedback(itemName: string): void {
|
|
|
const feedback = document.createElement('div');
|
|
|
feedback.className = 'add-item-feedback';
|
|
|
feedback.innerHTML = `
|
|
|
<div class="feedback-content">
|
|
|
<mat-icon>add_circle</mat-icon>
|
|
|
- <span>对比项添加成功!</span>
|
|
|
+ <span>对比项"${itemName}"添加成功!</span>
|
|
|
</div>
|
|
|
`;
|
|
|
feedback.style.cssText = `
|