import { Component, OnInit, signal, computed, Inject, ViewChild, ElementRef, OnDestroy, NgZone, ChangeDetectorRef, AfterViewInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatTabsModule } from '@angular/material/tabs'; import { MatIconModule } from '@angular/material/icon'; import { MatTableModule } from '@angular/material/table'; import { MatCardModule } from '@angular/material/card'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Attendance as AttendanceModel, Employee } from '../../../models/hr.model'; import * as echarts from 'echarts'; // 补卡申请对话框组件 @Component({ selector: 'app-attendance-dialog', standalone: true, imports: [ CommonModule, FormsModule, MatButtonModule, MatIconModule ], template: `

补卡申请

{{ attendanceData.employeeName }}
{{ formatDate(attendanceData.date) }}
{{ attendanceData.status }}
`, styles: [` .dialog-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #e5e7eb; } .close-btn { background: none; border: none; cursor: pointer; color: #6b7280; padding: 4px; } .dialog-content { max-width: 500px; } .info-item { display: flex; justify-content: space-between; margin-bottom: 12px; padding: 8px 0; border-bottom: 1px solid #f3f4f6; } .form-group { margin-top: 20px; } label { display: block; margin-bottom: 4px; font-weight: 500; color: #374151; } .reason-input { width: 100%; padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 6px; font-size: 14px; resize: vertical; } .dialog-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } `] }) class AttendanceDialog { attendanceData: any; reason = ''; constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any ) { this.attendanceData = data; } formatDate(date: Date | string): string { if (!date) return ''; const d = new Date(date); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } submit() { if (!this.reason.trim()) { alert('请输入补卡原因'); return; } this.dialogRef.close({ reason: this.reason }); } } // 生成模拟考勤数据 const generateMockAttendanceData = (): AttendanceModel[] => { const statuses: Array<'正常' | '迟到' | '早退' | '旷工' | '请假'> = ['正常', '迟到', '早退', '旷工', '请假']; const attendanceList: AttendanceModel[] = []; const today = new Date(); const projectIds = ['proj-001', 'proj-002', 'proj-003', 'proj-004']; const projectNames = ['现代风格客厅设计', '欧式厨房改造', '极简卧室设计', '办公室规划']; // 生成最近30天的考勤数据 for (let i = 29; i >= 0; i--) { const date = new Date(); date.setDate(today.getDate() - i); // 为每个部门生成3-5个员工的考勤数据 const departmentCount = Math.floor(Math.random() * 3) + 3; for (let j = 1; j <= departmentCount; j++) { const statusIndex = Math.floor(Math.random() * 10); let status: typeof statuses[0]; // 80%概率正常,10%概率迟到,5%概率早退,3%概率请假,2%概率旷工 if (statusIndex < 8) { status = '正常'; } else if (statusIndex < 9) { status = '迟到'; } else if (statusIndex < 9.5) { status = '早退'; } else if (statusIndex < 9.8) { status = '请假'; } else { status = '旷工'; } // 随机选择一个项目 const projectIndex = Math.floor(Math.random() * projectIds.length); const projectId = projectIds[projectIndex]; const projectName = projectNames[projectIndex]; // 生成打卡时间 let checkInTime: Date | undefined; let checkOutTime: Date | undefined; let workHours = 0; if (status !== '旷工' && status !== '请假') { checkInTime = new Date(date); checkInTime.setHours(9 + (status === '迟到' ? Math.floor(Math.random() * 2) : 0)); checkInTime.setMinutes(Math.floor(Math.random() * 60)); checkOutTime = new Date(date); checkOutTime.setHours(18 - (status === '早退' ? Math.floor(Math.random() * 2) : 0)); checkOutTime.setMinutes(Math.floor(Math.random() * 60)); // 计算工作时长(小时) workHours = Math.round((checkOutTime.getTime() - checkInTime.getTime()) / (1000 * 60 * 60) * 10) / 10; } attendanceList.push({ id: `att-${date.getTime()}-${j}`, employeeId: `emp-${j}`, date, checkInTime, checkOutTime, status, workHours, projectId, projectName }); } } return attendanceList; }; // 生成模拟员工数据 const generateMockEmployees = (): Employee[] => { const employees: Employee[] = []; const departments = ['设计部', '客户服务部', '财务部', '人力资源部']; const positions = ['设计师', '客服专员', '财务专员', '人事专员', '经理', '助理']; const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']; for (let i = 1; i <= 20; i++) { employees.push({ id: `emp-${i}`, name: names[i % names.length] + i, department: departments[Math.floor(Math.random() * departments.length)], position: positions[Math.floor(Math.random() * positions.length)], employeeId: `EMP2023${String(i).padStart(3, '0')}`, phone: `138${Math.floor(Math.random() * 100000000)}`, email: `employee${i}@example.com`, gender: i % 2 === 0 ? '女' : '男', birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1), hireDate: new Date(2020 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1), status: '在职', avatar: `https://via.placeholder.com/40?text=${i}` }); } return employees; }; // 主组件 @Component({ selector: 'app-attendance', standalone: true, imports: [ CommonModule, FormsModule, MatButtonModule, MatTabsModule, MatIconModule, MatTableModule, MatCardModule, MatTooltipModule, MatDialogModule ], templateUrl: './attendance.html', styleUrl: './attendance.scss' }) export class Attendance implements OnDestroy, OnInit, AfterViewInit { @ViewChild('ganttChartRef') ganttChartRef!: ElementRef; private ganttChart: any = null; private onResize = () => { if (this.ganttChart) { this.ganttChart.resize(); } }; private onChartClick = (params: any) => { this.zone.run(() => { const v = params?.value; if (!v) return; const start = new Date(v[1]); const end = new Date(v[2]); const employee = v[3]; const progress = v[4]; alert(`任务:${params.name}\n负责人:${employee}\n进度:${progress}%\n起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`); }); }; showGanttView = signal(false); ganttScale = signal<'day' | 'week' | 'month'>('week'); ganttMode = signal<'attendance' | 'workload'>('attendance'); selectedDepartment = 'all'; // 数据 attendanceData = signal(generateMockAttendanceData()); employees = signal(generateMockEmployees()); // 计算属性 departments = computed(() => { const depts = new Set(this.employees().map(emp => emp.department)); return Array.from(depts).sort(); }); filteredEmployees = computed(() => { if (this.selectedDepartment === 'all') { return this.employees(); } return this.employees().filter(emp => emp.department === this.selectedDepartment); }); selectedView = signal<'day' | 'week' | 'month'>('month'); selectedDate = signal(new Date()); selectedEmployeeId = signal(''); selectedProjectId = signal(''); // 通过computed缓存当月日历,避免模板每次变更检测都创建新数组 calendarDays = computed(() => this.computeCalendarDays()); // 获取日期tooltip信息 getDayTooltip(day: any): string { if (!day.attendance) { return '休息日'; } const status = day.attendance.status; let tooltip = `状态: ${status}`; if (day.attendance.workHours) { tooltip += `\n工作时长: ${day.attendance.workHours}小时`; } if (day.attendance.checkInTime) { const checkIn = new Date(day.attendance.checkInTime); tooltip += `\n签到: ${checkIn.getHours().toString().padStart(2, '0')}:${checkIn.getMinutes().toString().padStart(2, '0')}`; } if (day.attendance.checkOutTime) { const checkOut = new Date(day.attendance.checkOutTime); tooltip += `\n签退: ${checkOut.getHours().toString().padStart(2, '0')}:${checkOut.getMinutes().toString().padStart(2, '0')}`; } if (day.attendance.projectName) { tooltip += `\n项目: ${day.attendance.projectName}`; } return tooltip; } // 检查日期是否为今天 isToday(date: Date): boolean { const today = new Date(); const checkDate = new Date(date); return today.toDateString() === checkDate.toDateString(); } // 计算属性 filteredAttendance = computed(() => { let filtered = this.attendanceData(); // 按员工筛选 if (this.selectedEmployeeId()) { filtered = filtered.filter(item => item.employeeId === this.selectedEmployeeId()); } // 按项目筛选 if (this.selectedProjectId()) { filtered = filtered.filter(item => item.projectId === this.selectedProjectId()); } // 按视图筛选(日/周/月) const selectedDate = this.selectedDate(); if (this.selectedView() === 'day') { filtered = filtered.filter(item => { const itemDate = new Date(item.date); return itemDate.getDate() === selectedDate.getDate() && itemDate.getMonth() === selectedDate.getMonth() && itemDate.getFullYear() === selectedDate.getFullYear(); }); } else if (this.selectedView() === 'week') { // 获取本周的起止日期 const startOfWeek = new Date(selectedDate); const day = startOfWeek.getDay() || 7; // 调整周日为7 startOfWeek.setDate(startOfWeek.getDate() - day + 1); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(endOfWeek.getDate() + 6); filtered = filtered.filter(item => { const itemDate = new Date(item.date); return itemDate >= startOfWeek && itemDate <= endOfWeek; }); } else if (this.selectedView() === 'month') { filtered = filtered.filter(item => { const itemDate = new Date(item.date); return itemDate.getMonth() === selectedDate.getMonth() && itemDate.getFullYear() === selectedDate.getFullYear(); }); } return filtered; }); // 异常考勤列表 exceptionAttendance = computed(() => { return this.filteredAttendance().filter(item => item.status === '迟到' || item.status === '早退' || item.status === '旷工' ).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); // 考勤统计 attendanceStats = computed(() => { const data = this.filteredAttendance(); const totalDays = data.length; const normalDays = data.filter(item => item.status === '正常').length; const lateDays = data.filter(item => item.status === '迟到').length; const earlyLeaveDays = data.filter(item => item.status === '早退').length; const absentDays = data.filter(item => item.status === '旷工').length; const leaveDays = data.filter(item => item.status === '请假').length; const totalWorkHours = data.reduce((sum, item) => sum + item.workHours, 0); const avgWorkHours = totalDays > 0 ? (totalWorkHours / (totalDays - leaveDays - absentDays || 1)).toFixed(1) : '0.0'; return { totalDays, normalDays, lateDays, earlyLeaveDays, absentDays, leaveDays, totalWorkHours: totalWorkHours.toFixed(1), avgWorkHours, complianceRate: totalDays > 0 ? Math.round((normalDays / totalDays) * 100) : 0 }; }); // 部门考勤对比数据 departmentAttendanceData = computed(() => { const departmentMap = new Map(); this.attendanceData().forEach(item => { const employee = this.employees().find(emp => emp.id === item.employeeId); if (employee) { const dept = employee.department; const current = departmentMap.get(dept) || { total: 0, compliant: 0 }; current.total++; if (item.status === '正常') { current.compliant++; } departmentMap.set(dept, current); } }); const result: { department: string; complianceRate: number; total: number; compliant: number }[] = []; departmentMap.forEach((value, key) => { result.push({ department: key, complianceRate: Math.round((value.compliant / value.total) * 100), total: value.total, compliant: value.compliant }); }); return result.sort((a, b) => b.complianceRate - a.complianceRate); }); // 显示的表格列 displayedColumns = ['date', 'employeeName', 'status', 'workHours', 'projectName', 'actions']; constructor(private dialog: MatDialog, private zone: NgZone, private cdr: ChangeDetectorRef) {} ngOnInit() { // 已在字段声明处初始化模拟数据,避免首次变更检测期间的状态跃迁导致 ExpressionChanged } ngAfterViewInit() { // 稳定首次变更检测,避免控制流指令在初始化过程中值变化触发 NG0100 this.cdr.detectChanges(); } ngOnDestroy() { if (this.ganttChart) { this.ganttChart.off('click', this.onChartClick); this.ganttChart.dispose(); this.ganttChart = null; } window.removeEventListener('resize', this.onResize); } // 移除 ngAfterViewInit 钩子 // 切换视图(日/周/月) switchView(view: 'day' | 'week' | 'month') { this.selectedView.set(view); // 重置到当前日期 this.selectedDate.set(new Date()); } // 切换视图(考勤/任务) toggleView(target?: 'attendance' | 'task') { try { const next = target ? (target === 'task') : !this.showGanttView(); this.showGanttView.set(next); if (next) { // 等待DOM渲染后初始化图表 setTimeout(() => { try { this.initOrUpdateGantt(); } catch (error) { console.error('甘特图初始化失败:', error); } }, 100); } else { this.cleanupGanttChart(); } } catch (error) { console.error('视图切换失败:', error); } } // 清理甘特图资源 private cleanupGanttChart() { try { if (this.ganttChart) { this.ganttChart.off('click', this.onChartClick); this.ganttChart.dispose(); this.ganttChart = null; window.removeEventListener('resize', this.onResize); } } catch (error) { console.error('甘特图清理失败:', error); } } // 设置甘特时间尺度 setGanttScale(scale: 'day' | 'week' | 'month') { if (this.ganttScale() !== scale) { this.ganttScale.set(scale); this.updateGantt(); } } // 设置甘特图显示模式 setGanttMode(mode: 'attendance' | 'workload') { if (this.ganttMode() !== mode) { this.ganttMode.set(mode); this.updateGantt(); } } // 部门筛选变化 onDepartmentChange() { this.updateGantt(); } // 获取时间范围文本 getTimeRangeText(): string { const date = this.selectedDate(); const scale = this.ganttScale(); if (scale === 'day') { return date.toLocaleDateString('zh-CN'); } else if (scale === 'week') { const startOfWeek = new Date(date); const day = startOfWeek.getDay(); const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1); startOfWeek.setDate(diff); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6); return `${startOfWeek.toLocaleDateString('zh-CN')} - ${endOfWeek.toLocaleDateString('zh-CN')}`; } else { return `${date.getFullYear()}年${date.getMonth() + 1}月`; } } // 获取平均出勤率 getAverageAttendanceRate(): number { const employees = this.filteredEmployees(); if (employees.length === 0) return 0; const totalRate = employees.reduce((sum, emp) => { const empAttendance = this.attendanceData().filter(att => att.employeeId === emp.id); const normalDays = empAttendance.filter(att => att.status === '正常').length; const rate = empAttendance.length > 0 ? (normalDays / empAttendance.length) * 100 : 0; return sum + rate; }, 0); return Math.round(totalRate / employees.length); } // 获取平均工时 getAverageWorkHours(): number { const employees = this.filteredEmployees(); if (employees.length === 0) return 0; const totalHours = employees.reduce((sum, emp) => { const empAttendance = this.attendanceData().filter(att => att.employeeId === emp.id); const avgHours = empAttendance.reduce((h, att) => h + (att.workHours || 0), 0) / empAttendance.length; return sum + (avgHours || 0); }, 0); return Math.round((totalHours / employees.length) * 10) / 10; } // 获取员工姓名 getEmployeeName(employeeId: string): string { const employee = this.employees().find(emp => emp.id === employeeId); return employee ? employee.name : '未知员工'; } // 格式化日期 formatDate(date: Date | string): string { if (!date) return ''; const d = new Date(date); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } // 格式化时间 formatTime(date: Date | string): string { if (!date) return ''; const d = new Date(date); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } // 获取状态样式类 getStatusClass(status: string): string { switch (status) { case '正常': return 'status-normal'; case '迟到': return 'status-late'; case '早退': return 'status-early'; case '旷工': return 'status-absent'; case '请假': return 'status-leave'; default: return ''; } } // 切换日期 navigateDate(direction: 'prev' | 'next') { const newDate = new Date(this.selectedDate()); if (this.selectedView() === 'day') { newDate.setDate(newDate.getDate() + (direction === 'prev' ? -1 : 1)); } else if (this.selectedView() === 'week') { newDate.setDate(newDate.getDate() + (direction === 'prev' ? -7 : 7)); } else if (this.selectedView() === 'month') { newDate.setMonth(newDate.getMonth() + (direction === 'prev' ? -1 : 1)); } this.selectedDate.set(newDate); } private isDialogOpening = false; // 打开补卡申请对话框 openAttendanceDialog(attendance: AttendanceModel) { // 防止重复点击 if (this.isDialogOpening) { return; } try { this.isDialogOpening = true; const employee = this.employees().find(emp => emp.id === attendance.employeeId); const dialogRef = this.dialog.open(AttendanceDialog, { width: '500px', maxWidth: '90vw', disableClose: true, panelClass: 'hr-dialog', backdropClass: 'hr-dialog-backdrop', data: { ...attendance, employeeName: employee ? employee.name : '未知员工' } }); dialogRef.afterClosed().subscribe({ next: (result) => { this.isDialogOpening = false; if (result) { // 在实际应用中,这里会提交补卡申请到服务器 alert('补卡申请已提交,等待审核'); } }, error: (error) => { this.isDialogOpening = false; console.error('对话框关闭时出错:', error); } }); } catch (error) { this.isDialogOpening = false; console.error('打开补卡对话框失败:', error); alert('打开对话框失败,请重试'); } } // 导出考勤数据 exportAttendanceData(): void { try { const data = this.filteredAttendance(); if (data.length === 0) { alert('暂无数据可导出'); return; } const csvContent = this.convertToCSV(data); this.downloadCSV(csvContent, '考勤数据.csv'); // 显示成功提示 setTimeout(() => { alert(`成功导出 ${data.length} 条考勤记录`); }, 100); } catch (error) { console.error('导出数据失败:', error); alert('导出失败,请重试'); } } // 将数据转换为CSV格式 private convertToCSV(data: any[]): string { if (data.length === 0) return ''; const headers = ['员工ID', '员工姓名', '日期', '上班时间', '下班时间', '状态', '工作时长', '所属项目']; const rows = data.map(item => { const employee = this.employees().find(emp => emp.id === item.employeeId); return [ item.employeeId, employee ? employee.name : '未知员工', this.formatDate(item.date), item.checkInTime ? this.formatTime(item.checkInTime) : '', item.checkOutTime ? this.formatTime(item.checkOutTime) : '', item.status, item.workHours || '', item.projectName ]; }); return [headers, ...rows].map(row => row.map(field => `"${field}"`).join(',')).join('\n'); } // 下载CSV文件 private downloadCSV(csvContent: string, filename: string): void { const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 获取日历数据 getCalendarDays() { return this.calendarDays(); } // 实际计算日历数据 private computeCalendarDays() { const year = this.selectedDate().getFullYear(); const month = this.selectedDate().getMonth(); // 获取当月第一天和最后一天 const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); // 获取当月第一天是星期几 const firstDayIndex = firstDay.getDay(); const days: any[] = []; // 添加上月的最后几天 for (let i = firstDayIndex; i > 0; i--) { const day = new Date(year, month, -i + 1); days.push({ date: day, dayOfMonth: day.getDate(), currentMonth: false, attendance: null }); } // 添加当月的天数 for (let i = 1; i <= lastDay.getDate(); i++) { const day = new Date(year, month, i); const dayAttendance = this.attendanceData().filter(item => { const itemDate = new Date(item.date); return itemDate.getDate() === i && itemDate.getMonth() === month && itemDate.getFullYear() === year && (!this.selectedEmployeeId() || item.employeeId === this.selectedEmployeeId()); }); days.push({ date: day, dayOfMonth: i, currentMonth: true, attendance: dayAttendance.length > 0 ? dayAttendance[0] : null }); } // 添加下月的前几天,凑够42天(6行7列) const remainingDays = 42 - days.length; for (let i = 1; i <= remainingDays; i++) { const day = new Date(year, month + 1, i); days.push({ date: day, dayOfMonth: i, currentMonth: false, attendance: null }); } return days; } // 获取星期几的中文名称 getWeekdayName(index: number): string { const weekdays = ['日', '一', '二', '三', '四', '五', '六']; return weekdays[index]; } private initOrUpdateGantt(): void { if (!this.ganttChartRef?.nativeElement) { console.warn('甘特图容器未找到'); return; } const el = this.ganttChartRef.nativeElement; this.zone.runOutsideAngular(() => { try { if (!this.ganttChart) { // 检查容器尺寸 if (el.offsetWidth === 0 || el.offsetHeight === 0) { console.warn('甘特图容器尺寸为0,延迟初始化'); setTimeout(() => this.initOrUpdateGantt(), 200); return; } this.ganttChart = echarts.init(el); window.addEventListener('resize', this.onResize); } this.updateGantt(); } catch (error) { console.error('甘特图初始化失败:', error); } }); } private updateGantt(): void { if (!this.ganttChart) { console.warn('甘特图实例不存在'); return; } try { // 工作负荷模式和考勤状态模式使用相同的数据结构,只是颜色映射不同 // 考勤状态甘特图 const employees = this.filteredEmployees(); const categories = employees.map(emp => emp.name); // 考勤状态颜色映射 const statusColorMap: Record = { '正常': '#22c55e', '迟到': '#f59e0b', '早退': '#f97316', '旷工': '#ef4444', '请假': '#8b5cf6', '加班': '#06b6d4' }; const DAY = 24 * 60 * 60 * 1000; const now = new Date(); const selectedDate = this.selectedDate(); // 计算时间范围 let xMin: number; let xMax: number; let xSplitNumber: number; let xLabelFormatter: (value: number) => string; if (this.ganttScale() === 'day') { // 日视图:显示当天的小时 const startOfDay = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate()); const endOfDay = new Date(startOfDay.getTime() + DAY - 1); xMin = startOfDay.getTime(); xMax = endOfDay.getTime(); xSplitNumber = 24; xLabelFormatter = (val) => `${new Date(val).getHours()}:00`; } else if (this.ganttScale() === 'week') { // 周视图:显示一周的天数 const day = selectedDate.getDay(); const diffToMonday = (day === 0 ? 6 : day - 1); const startOfWeek = new Date(selectedDate.getTime() - diffToMonday * DAY); const endOfWeek = new Date(startOfWeek.getTime() + 7 * DAY - 1); xMin = startOfWeek.getTime(); xMax = endOfWeek.getTime(); xSplitNumber = 7; const WEEK_LABELS = ['周日','周一','周二','周三','周四','周五','周六']; xLabelFormatter = (val) => WEEK_LABELS[new Date(val).getDay()]; } else { // 月视图:显示一个月的周数 const startOfMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1); const endOfMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, 0, 23, 59, 59, 999); xMin = startOfMonth.getTime(); xMax = endOfMonth.getTime(); xSplitNumber = 4; xLabelFormatter = (val) => `第${Math.ceil(new Date(val).getDate() / 7)}周`; } // 生成考勤数据 const data: any[] = []; employees.forEach((emp, empIndex) => { const empAttendance = this.attendanceData().filter(att => att.employeeId === emp.id); if (this.ganttScale() === 'day') { // 日视图:显示工作时间段 const dayAttendance = empAttendance.find(att => { const attDate = new Date(att.date); return attDate.toDateString() === selectedDate.toDateString(); }); if (dayAttendance && dayAttendance.checkInTime && dayAttendance.checkOutTime) { const checkIn = new Date(`${selectedDate.toDateString()} ${dayAttendance.checkInTime}`); const checkOut = new Date(`${selectedDate.toDateString()} ${dayAttendance.checkOutTime}`); data.push({ name: emp.name, value: [empIndex, checkIn.getTime(), checkOut.getTime(), dayAttendance.status, dayAttendance.workHours], itemStyle: { color: statusColorMap[dayAttendance.status] || '#94a3b8' } }); } } else { // 周视图和月视图:显示每天的考勤状态 const timeRange = this.ganttScale() === 'week' ? 7 : 30; const startTime = this.ganttScale() === 'week' ? new Date(selectedDate.getTime() - ((selectedDate.getDay() === 0 ? 6 : selectedDate.getDay() - 1) * DAY)) : new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1); for (let i = 0; i < timeRange; i++) { const currentDay = new Date(startTime.getTime() + i * DAY); const dayAttendance = empAttendance.find(att => { const attDate = new Date(att.date); return attDate.toDateString() === currentDay.toDateString(); }); if (dayAttendance) { const dayStart = new Date(currentDay.getFullYear(), currentDay.getMonth(), currentDay.getDate()); const dayEnd = new Date(dayStart.getTime() + DAY - 1); data.push({ name: emp.name, value: [empIndex, dayStart.getTime(), dayEnd.getTime(), dayAttendance.status, dayAttendance.workHours], itemStyle: { color: statusColorMap[dayAttendance.status] || '#94a3b8' } }); } } } }); // 计算默认可视区域 const total = categories.length; const visible = Math.min(total, 15); const defaultEndPercent = total > 0 ? Math.min(100, (visible / total) * 100) : 100; const todayTs = now.getTime(); const option = { backgroundColor: 'transparent', tooltip: { trigger: 'item', formatter: (params: any) => { const v = params.value; const start = new Date(v[1]); const end = new Date(v[2]); const status = v[3]; const workHours = v[4]; if (this.ganttScale() === 'day') { return `员工:${params.name}
状态:${status}
工作时间:${start.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'})} - ${end.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'})}
工时:${workHours || 0}小时`; } else { return `员工:${params.name}
日期:${start.toLocaleDateString('zh-CN')}
状态:${status}
工时:${workHours || 0}小时`; } } }, grid: { left: 120, right: 64, top: 30, bottom: 30 }, xAxis: { type: 'time', min: xMin, max: xMax, splitNumber: xSplitNumber, axisLine: { lineStyle: { color: '#e5e7eb' } }, axisLabel: { color: '#6b7280', formatter: xLabelFormatter }, splitLine: { lineStyle: { color: '#f1f5f9' } } }, yAxis: { type: 'category', data: categories, inverse: true, axisLabel: { color: '#374151', margin: 8, formatter: (val: string) => { const text = val.length > 12 ? val.slice(0, 12) + '…' : val; return text; } }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#e5e7eb' } } }, dataZoom: [ { type: 'slider', yAxisIndex: 0, orient: 'vertical', right: 6, width: 14, start: 0, end: defaultEndPercent, zoomLock: false }, { type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true } ], series: [ { type: 'custom', renderItem: (params: any, api: any) => { const categoryIndex = api.value(0); const start = api.coord([api.value(1), categoryIndex]); const end = api.coord([api.value(2), categoryIndex]); const height = Math.max(api.size([0, 1])[1] * 0.6, 12); // 考勤状态条 const rectShape = echarts.graphic.clipRectByRect({ x: start[0], y: start[1] - height / 2, width: Math.max(end[0] - start[0], 2), height }, { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }); // 今日标记线 const isToday = this.ganttScale() === 'day' || (api.value(1) <= todayTs && api.value(2) >= todayTs); const elements = [ { type: 'rect', shape: rectShape, style: { ...api.style(), stroke: isToday ? '#3b82f6' : 'transparent', strokeWidth: isToday ? 2 : 0 } } ]; return { type: 'group', children: elements }; }, encode: { x: [1, 2], y: 0 }, data, itemStyle: { borderRadius: 6 }, emphasis: { focus: 'self' } } ] }; this.ganttChart.setOption(option, true); // 绑定点击事件,避免重复绑定 this.ganttChart.off('click', this.onChartClick); this.ganttChart.on('click', this.onChartClick); this.ganttChart.resize(); } catch (error) { console.error('甘特图更新失败:', error); } } }