From 3a0f729669dfb5dbc37f2ed2e22958dbb35c908e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= <2178503051@qq.com>
Date: Fri, 29 May 2026 17:13:57 +0800
Subject: [PATCH] =?UTF-8?q?feat(attendanceCheck):=20=E6=96=B0=E5=A2=9E?=
=?UTF-8?q?=E5=91=98=E5=B7=A5=E8=80=83=E5=8B=A4=E8=AF=A6=E6=83=85=E5=BC=B9?=
=?UTF-8?q?=E7=AA=97=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 为员工列表列添加详情按钮,点击可查看个人考勤详情
2. 新增个人考勤汇总卡片,展示考勤统计数据
3. 支持表格和日历两种明细视图,根据天数自动切换
4. 添加考勤日历视图样式与状态标识
5. 补充相关计算属性与工具方法支撑新功能
---
.../wms/hrm/attendance/attendanceCheck.vue | 320 +++++++++++++++++-
1 file changed, 317 insertions(+), 3 deletions(-)
diff --git a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
index bbd29626..4bb16ad9 100644
--- a/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
+++ b/klp-ui/src/views/wms/hrm/attendance/attendanceCheck.vue
@@ -24,7 +24,12 @@
-
+
+
+ {{ scope.row.employeeName }}
+
+
+
+
+
+
+
+ {{ currentPersonData.employeeName }}
+ {{ personSummary.normal }}天
+ {{ personSummary.abnormal }}天
+ {{ personSummary.absentHalf }}天
+ {{ personSummary.absentFull }}天
+ {{ personSummary.noRecord }}天
+ {{ personSummary.totalLateMinutes }}分钟
+ {{ personSummary.totalEarlyMinutes }}分钟
+ ¥{{ personSummary.totalDeduct }}
+ {{ dateList.length }}天
+
+
+
考勤明细({{ dateList.length }}天)
+
+
+
+
+
+
+
+ {{ getStatusText(scope.row.overallStatus) }}
+
+
+
+ {{ formatTime(scope.row.p1FirstCheck) }}
+
+
+ {{ formatTime(scope.row.p1LastCheck) }}
+
+
+
+ {{ scope.row.p1LateMinutes }}
+ 0
+
+
+
+
+ {{ scope.row.p1EarlyMinutes }}
+ 0
+
+
+
+ ¥{{ scope.row.p1Deduct || '0.00' }}
+
+
+
+
+
+
+
+
{{ month.label }}
+
+
+
+ {{ cell.day }}
+ {{ getStatusText(cell.status) }}
+
+
+
+
+ 正常
+ 迟到/早退
+ 半天旷工
+ 全天旷工
+ 无记录
+
+
+
+
+
@@ -515,7 +595,12 @@ export default {
selectedUserIds: [],
detailRecordsLoading: false,
- detailRecordsList: []
+ detailRecordsList: [],
+
+ personDialogVisible: false,
+ personDialogTitle: '',
+ currentPersonData: null,
+ personLoading: false,
};
},
computed: {
@@ -533,7 +618,88 @@ export default {
key: String(emp.infoId),
label: emp.name + '(' + emp.dept + ')'
}))
- }
+ },
+ personSummary() {
+ if (!this.currentPersonData) return { normal: 0, abnormal: 0, absentHalf: 0, absentFull: 0, noRecord: 0, totalLateMinutes: 0, totalEarlyMinutes: 0, totalDeduct: '0.00' };
+ let normal = 0, abnormal = 0, absentHalf = 0, absentFull = 0, noRecord = 0;
+ let totalLate = 0, totalEarly = 0, totalDeduct = 0;
+ this.dateList.forEach(date => {
+ const record = this.currentPersonData[date];
+ if (!record) {
+ noRecord++;
+ return;
+ }
+ const status = record.overallStatus;
+ if (status === 'normal') normal++;
+ else if (status === 'absent_half') absentHalf++;
+ else if (status === 'absent_full') absentFull++;
+ else if (status === 'abnormal' || status === 'late_warn' || status === 'late_one' || status === 'late_two' || status === 'early_warn' || status === 'early_one' || status === 'early_two' || status === 'missed') abnormal++;
+ totalLate += Number(record.p1LateMinutes || 0) + Number(record.p2LateMinutes || 0);
+ totalEarly += Number(record.p1EarlyMinutes || 0) + Number(record.p2EarlyMinutes || 0);
+ totalDeduct += Number(record.p1Deduct || 0) + Number(record.p2Deduct || 0) + Number(record.totalDeduct || 0);
+ });
+ return { normal, abnormal, absentHalf, absentFull, noRecord, totalLateMinutes: totalLate, totalEarlyMinutes: totalEarly, totalDeduct: totalDeduct.toFixed(2) };
+ },
+ personViewMode() {
+ const days = this.dateList.length;
+ if (days <= 14) return 'table';
+ if (days <= 62) return 'calendar';
+ return 'table';
+ },
+ personDetailTableData() {
+ if (!this.currentPersonData) return [];
+ return this.dateList.map(date => {
+ const record = this.currentPersonData[date];
+ if (!record) {
+ return { workDate: date, shiftName: '-', overallStatus: null, p1FirstCheck: null, p1LastCheck: null, p2FirstCheck: null, p2LastCheck: null, p1LateMinutes: 0, p1EarlyMinutes: 0, p1Deduct: 0, p2LateMinutes: 0, p2EarlyMinutes: 0, p2Deduct: 0, totalDeduct: 0, remark: '' };
+ }
+ return {
+ workDate: date,
+ ...record,
+ p1LateMinutes: record.p1LateMinutes || 0,
+ p1EarlyMinutes: record.p1EarlyMinutes || 0,
+ };
+ });
+ },
+ calendarWeekDays() {
+ return ['日', '一', '二', '三', '四', '五', '六'];
+ },
+ personCalendarMonths() {
+ if (!this.dateList || this.dateList.length === 0) return [];
+ const months = {};
+ this.dateList.forEach(date => {
+ const d = new Date(date);
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
+ if (!months[key]) {
+ months[key] = { key, label: `${d.getFullYear()}年${d.getMonth() + 1}月`, dates: [] };
+ }
+ months[key].dates.push(date);
+ });
+ return Object.values(months).map(month => {
+ const firstDate = new Date(month.dates[0]);
+ const year = firstDate.getFullYear();
+ const monthNum = firstDate.getMonth();
+ const firstDay = new Date(year, monthNum, 1);
+ const lastDay = new Date(year, monthNum + 1, 0);
+ const startDayOfWeek = firstDay.getDay();
+ const totalDays = lastDay.getDate();
+ const cells = [];
+ for (let i = 0; i < startDayOfWeek; i++) {
+ cells.push({ day: '', date: null });
+ }
+ for (let day = 1; day <= totalDays; day++) {
+ const dateStr = `${year}-${String(monthNum + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
+ const record = this.currentPersonData ? this.currentPersonData[dateStr] : null;
+ cells.push({
+ day,
+ date: dateStr,
+ status: record ? record.overallStatus : null,
+ inRange: month.dates.includes(dateStr),
+ });
+ }
+ return { ...month, cells };
+ });
+ },
},
created() {
this.getAllEmployees().then(() => {
@@ -1049,6 +1215,30 @@ export default {
}).finally(() => {
this.loading = false
})
+ },
+ handlePersonDetail(row) {
+ this.currentPersonData = row
+ this.personDialogTitle = row.employeeName + ' - 考勤总览'
+ this.personDialogVisible = true
+ },
+ getCalendarCellClass(cell) {
+ if (!cell.date) return 'cal-empty'
+ if (!cell.status) return 'cal-no-record'
+ switch (cell.status) {
+ case 'normal': return 'cal-normal'
+ case 'absent_half': return 'cal-absent-half'
+ case 'absent_full': return 'cal-absent-full'
+ case 'abnormal':
+ case 'late_warn':
+ case 'late_one':
+ case 'late_two':
+ case 'early_warn':
+ case 'early_one':
+ case 'early_two':
+ case 'missed':
+ return 'cal-abnormal'
+ default: return 'cal-default'
+ }
}
}
};
@@ -1229,4 +1419,128 @@ export default {
margin-right: 10px;
white-space: nowrap;
}
+
+.early-info {
+ color: #f56c6c;
+}
+
+.calendar-view {
+ margin-top: 10px;
+}
+
+.calendar-month {
+ margin-bottom: 24px;
+}
+
+.calendar-month-title {
+ margin: 0 0 8px 0;
+ font-size: 14px;
+ color: #303133;
+}
+
+.calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 2px;
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.calendar-header {
+ text-align: center;
+ padding: 6px 4px;
+ background: #f5f7fa;
+ font-size: 12px;
+ color: #909399;
+ font-weight: bold;
+}
+
+.calendar-cell {
+ min-height: 50px;
+ padding: 4px;
+ background: #fff;
+ text-align: center;
+ font-size: 12px;
+ border-right: 1px solid #ebeef5;
+ border-bottom: 1px solid #ebeef5;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.calendar-day {
+ font-weight: bold;
+ color: #303133;
+ font-size: 13px;
+}
+
+.calendar-status {
+ font-size: 10px;
+ margin-top: 2px;
+}
+
+.cal-empty {
+ background: #f9f9f9;
+ color: #c0c4cc;
+}
+
+.cal-no-record {
+ background: #fdf6ec;
+ color: #e6a23c;
+}
+
+.cal-normal {
+ background: #f0f9eb;
+ color: #67c23a;
+}
+
+.cal-abnormal {
+ background: #fef0f0;
+ color: #f56c6c;
+}
+
+.cal-absent-half {
+ background: #f4f4f5;
+ color: #909399;
+}
+
+.cal-absent-full {
+ background: #e9e9eb;
+ color: #606266;
+}
+
+.cal-default {
+ background: #f5f7fa;
+ color: #c0c4cc;
+}
+
+.calendar-legend {
+ display: flex;
+ gap: 16px;
+ margin-top: 12px;
+ flex-wrap: wrap;
+ font-size: 12px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: #606266;
+}
+
+.legend-dot {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.legend-dot.normal { background: #f0f9eb; border: 1px solid #67c23a; }
+.legend-dot.late { background: #fef0f0; border: 1px solid #f56c6c; }
+.legend-dot.absent-half { background: #f4f4f5; border: 1px solid #909399; }
+.legend-dot.absent-full { background: #e9e9eb; border: 1px solid #606266; }
+.legend-dot.no-record { background: #fdf6ec; border: 1px solid #e6a23c; }