feat(attendanceCheck): 新增员工考勤详情弹窗功能
1. 为员工列表列添加详情按钮,点击可查看个人考勤详情 2. 新增个人考勤汇总卡片,展示考勤统计数据 3. 支持表格和日历两种明细视图,根据天数自动切换 4. 添加考勤日历视图样式与状态标识 5. 补充相关计算属性与工具方法支撑新功能
This commit is contained in:
@@ -24,7 +24,12 @@
|
||||
|
||||
<div class="schedule-table-wrapper">
|
||||
<el-table v-loading="loading" :data="attendanceData" border stripe>
|
||||
<el-table-column prop="employeeName" label="员工" width="120" fixed="left" />
|
||||
<el-table-column label="员工" width="150" fixed="left">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.employeeName }}</span>
|
||||
<el-button type="text" size="mini" icon="el-icon-view" @click="handlePersonDetail(scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="date in dateList" :key="date" :label="formatDateLabel(date)" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div class="attendance-cell" :class="getAttendanceStatusClass(scope.row[date])"
|
||||
@@ -385,6 +390,81 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 个人考勤总览 -->
|
||||
<el-dialog :title="personDialogTitle" :visible.sync="personDialogVisible" width="900px" append-to-body>
|
||||
<div v-if="currentPersonData" v-loading="personLoading">
|
||||
<el-descriptions title="考勤汇总" :column="5" border style="margin-bottom: 20px;">
|
||||
<el-descriptions-item label="姓名">{{ currentPersonData.employeeName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="正常">{{ personSummary.normal }}天</el-descriptions-item>
|
||||
<el-descriptions-item label="迟到/早退">{{ personSummary.abnormal }}天</el-descriptions-item>
|
||||
<el-descriptions-item label="半天旷工">{{ personSummary.absentHalf }}天</el-descriptions-item>
|
||||
<el-descriptions-item label="全天旷工">{{ personSummary.absentFull }}天</el-descriptions-item>
|
||||
<el-descriptions-item label="无记录">{{ personSummary.noRecord }}天</el-descriptions-item>
|
||||
<el-descriptions-item label="迟到合计">{{ personSummary.totalLateMinutes }}分钟</el-descriptions-item>
|
||||
<el-descriptions-item label="早退合计">{{ personSummary.totalEarlyMinutes }}分钟</el-descriptions-item>
|
||||
<el-descriptions-item label="总扣款">¥{{ personSummary.totalDeduct }}</el-descriptions-item>
|
||||
<el-descriptions-item label="统计天数">{{ dateList.length }}天</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">考勤明细({{ dateList.length }}天)</el-divider>
|
||||
|
||||
<!-- 表格视图:小数据量(≤14天) 或 特大数据量(>62天) -->
|
||||
<el-table v-if="personViewMode === 'table'" :data="personDetailTableData" border stripe size="small" max-height="400">
|
||||
<el-table-column prop="workDate" label="日期" width="120" fixed="left" />
|
||||
<el-table-column prop="shiftName" label="班次" width="120" />
|
||||
<el-table-column prop="overallStatus" label="总体状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span :class="getStatusClass(scope.row.overallStatus)">{{ getStatusText(scope.row.overallStatus) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="p1FirstCheck" label="上班打卡" width="160">
|
||||
<template slot-scope="scope">{{ formatTime(scope.row.p1FirstCheck) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="p1LastCheck" label="下班打卡" width="160">
|
||||
<template slot-scope="scope">{{ formatTime(scope.row.p1LastCheck) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="p1LateMinutes" label="迟到(分)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.p1LateMinutes > 0" class="late-info">{{ scope.row.p1LateMinutes }}</span>
|
||||
<span v-else>0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="p1EarlyMinutes" label="早退(分)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.p1EarlyMinutes > 0" class="early-info">{{ scope.row.p1EarlyMinutes }}</span>
|
||||
<span v-else>0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="p1Deduct" label="扣款" width="80" align="center">
|
||||
<template slot-scope="scope">¥{{ scope.row.p1Deduct || '0.00' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 日历视图:中等数据量(15~62天) -->
|
||||
<div v-if="personViewMode === 'calendar'" class="calendar-view">
|
||||
<div v-for="month in personCalendarMonths" :key="month.key" class="calendar-month">
|
||||
<h4 class="calendar-month-title">{{ month.label }}</h4>
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-header" v-for="w in calendarWeekDays" :key="w">{{ w }}</div>
|
||||
<div v-for="(cell, idx) in month.cells" :key="idx"
|
||||
class="calendar-cell" :class="getCalendarCellClass(cell)">
|
||||
<span class="calendar-day">{{ cell.day }}</span>
|
||||
<span v-if="cell.date && cell.status" class="calendar-status">{{ getStatusText(cell.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="legend-dot normal"></span>正常</span>
|
||||
<span class="legend-item"><span class="legend-dot late"></span>迟到/早退</span>
|
||||
<span class="legend-item"><span class="legend-dot absent-half"></span>半天旷工</span>
|
||||
<span class="legend-item"><span class="legend-dot absent-full"></span>全天旷工</span>
|
||||
<span class="legend-item"><span class="legend-dot no-record"></span>无记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="考勤比对" :visible.sync="checkOpen" width="700px" append-to-body>
|
||||
<el-form ref="checkForm" :model="checkForm" :rules="checkRules" label-width="80px">
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
@@ -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; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user