feat(hrm/attendance): 新增考勤汇总页面并优化考勤打卡页面布局
1. 新增attendanceSummary.vue页面,实现员工考勤数据汇总统计,包含工时、迟到早退、加班、出勤休假等数据展示与统计 2. 调整attendanceCheck.vue页面,注释掉班次ID输入框并新增实际上下班打卡时间展示字段
This commit is contained in:
@@ -131,12 +131,12 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<!-- <el-col :span="8">
|
||||
<el-form-item label="班次ID">
|
||||
<el-input v-if="isEdit" v-model="editForm.shiftId" type="number" placeholder="请输入班次ID" />
|
||||
<span v-else>{{ currentDetail.shiftId || '-' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-col> -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="班次名称">
|
||||
<el-input v-if="isEdit" v-model="editForm.shiftName" placeholder="请输入班次名称" />
|
||||
@@ -209,6 +209,16 @@
|
||||
<span v-else>{{ formatTime(currentDetail.p1EndTime) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际上班">
|
||||
<span>{{ formatTime(currentDetail.p1FirstCheck) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际下班">
|
||||
<span>{{ formatTime(currentDetail.p1LastCheck) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
@@ -260,6 +270,16 @@
|
||||
<span v-else>{{ formatTime(currentDetail.p2EndTime) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际上班">
|
||||
<span>{{ formatTime(currentDetail.p2FirstCheck) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际下班">
|
||||
<span>{{ formatTime(currentDetail.p2LastCheck) }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentDetail.p2StartTime || isEdit">
|
||||
<el-col :span="6">
|
||||
|
||||
354
klp-ui/src/views/wms/hrm/attendance/attendanceSummary.vue
Normal file
354
klp-ui/src/views/wms/hrm/attendance/attendanceSummary.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="date-range-section">
|
||||
<TimeRangePicker v-model="dateRangeParams" startKey="startDate" endKey="endDate"
|
||||
:defaultStartTime="defaultStartTime" :defaultEndTime="defaultEndTime" format="yyyy-MM-dd" @change="handleDateRangeChange"
|
||||
@quick-select="getSummaryList" />
|
||||
</div>
|
||||
|
||||
<div class="operation-bar">
|
||||
<el-button type="primary" plain icon="el-icon-search" @click="handleQuery">搜索</el-button>
|
||||
<el-button type="warning" plain icon="el-icon-download" @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
|
||||
<div class="summary-table-wrapper">
|
||||
<el-table v-loading="loading" :data="summaryData" border stripe>
|
||||
<el-table-column prop="employeeId" label="工号" width="100" />
|
||||
<el-table-column prop="employeeName" label="姓名" width="100" />
|
||||
<el-table-column prop="departmentName" label="所属部门" width="120" />
|
||||
|
||||
<el-table-column label="工作时数" width="160">
|
||||
<el-table-column prop="standardWorkHours" label="标准" width="80" />
|
||||
<el-table-column prop="actualWorkHours" label="实际" width="80" />
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="迟到" width="120">
|
||||
<el-table-column prop="lateCount" label="次数" width="60" />
|
||||
<el-table-column prop="lateScore" label="分数" width="60" />
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="早退" width="120">
|
||||
<el-table-column prop="earlyCount" label="次数" width="60" />
|
||||
<el-table-column prop="earlyScore" label="分数" width="60" />
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="加班时数" width="160">
|
||||
<el-table-column prop="normalOvertime" label="正常" width="80" />
|
||||
<el-table-column prop="specialOvertime" label="特殊" width="80" />
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="attendanceDays" label="出勤天数(标准/实际)" width="140" />
|
||||
<el-table-column prop="businessTripDays" label="出差(天)" width="80" />
|
||||
<el-table-column prop="absentDays" label="旷工(天)" width="80" />
|
||||
<el-table-column prop="leaveDays" label="请假(天)" width="80" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="summary-footer">
|
||||
<div class="summary-totals">
|
||||
<span>总人数:{{ summaryData.length }}人</span>
|
||||
<span>平均出勤:{{ averageAttendance }}天</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listAttendanceCheck } from "@/api/wms/attendanceCheck";
|
||||
import { listLeaveRequest } from "@/api/wms/leaveRequest";
|
||||
import { listOutRequest } from "@/api/wms/outRequest";
|
||||
import TimeRangePicker from "@/views/wms/report/components/timeRangePicker";
|
||||
|
||||
export default {
|
||||
name: "AttendanceSummary",
|
||||
components: {
|
||||
TimeRangePicker
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
summaryData: [],
|
||||
dateRangeParams: {},
|
||||
defaultStartTime: '',
|
||||
defaultEndTime: '',
|
||||
leaveList: [],
|
||||
outList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
averageAttendance() {
|
||||
if (this.summaryData.length === 0) return '0.00';
|
||||
const total = this.summaryData.reduce((sum, item) => {
|
||||
const days = item.attendanceDays?.split('/')[1] || '0';
|
||||
return sum + parseFloat(days);
|
||||
}, 0);
|
||||
return (total / this.summaryData.length).toFixed(2);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initDateRange();
|
||||
},
|
||||
methods: {
|
||||
initDateRange() {
|
||||
const now = new Date()
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
||||
|
||||
this.defaultStartTime = this.formatDate(firstDay)
|
||||
this.defaultEndTime = this.formatDate(lastDay)
|
||||
|
||||
this.dateRangeParams = {
|
||||
startDate: this.defaultStartTime,
|
||||
endDate: this.defaultEndTime
|
||||
}
|
||||
|
||||
this.getSummaryList()
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
|
||||
handleDateRangeChange() {
|
||||
if (this.dateRangeParams.startDate && this.dateRangeParams.endDate) {
|
||||
this.getSummaryList()
|
||||
}
|
||||
},
|
||||
|
||||
getSummaryList() {
|
||||
this.loading = true
|
||||
Promise.all([
|
||||
this.fetchAttendanceData(),
|
||||
this.fetchLeaveData(),
|
||||
this.fetchOutData()
|
||||
]).then(([attendanceRows, leaveRows, outRows]) => {
|
||||
this.summaryData = this.calculateSummary(attendanceRows, leaveRows, outRows)
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
fetchAttendanceData() {
|
||||
return listAttendanceCheck(this.dateRangeParams).then(response => {
|
||||
return response.rows || []
|
||||
}).catch(() => [])
|
||||
},
|
||||
|
||||
fetchLeaveData() {
|
||||
const query = {
|
||||
startTime: this.dateRangeParams.startDate,
|
||||
endTime: this.dateRangeParams.endDate
|
||||
}
|
||||
return listLeaveRequest(query).then(response => {
|
||||
return response.rows || response.data || []
|
||||
}).catch(() => [])
|
||||
},
|
||||
|
||||
fetchOutData() {
|
||||
const query = {
|
||||
startTime: this.dateRangeParams.startDate,
|
||||
endTime: this.dateRangeParams.endDate
|
||||
}
|
||||
return listOutRequest(query).then(response => {
|
||||
return response.rows || response.data || []
|
||||
}).catch(() => [])
|
||||
},
|
||||
|
||||
calculateSummary(attendanceRows, leaveRows, outRows) {
|
||||
const summaryMap = {}
|
||||
const totalDays = this.calculateDaysInRange()
|
||||
|
||||
attendanceRows.forEach(record => {
|
||||
const employeeId = record.userId
|
||||
if (!summaryMap[employeeId]) {
|
||||
summaryMap[employeeId] = {
|
||||
employeeId: employeeId,
|
||||
employeeName: record.employeeName,
|
||||
departmentName: record.departmentName || '',
|
||||
standardWorkHours: '0.00',
|
||||
actualWorkHours: '0',
|
||||
lateCount: 0,
|
||||
lateScore: 0,
|
||||
earlyCount: 0,
|
||||
earlyScore: 0,
|
||||
normalOvertime: '0',
|
||||
specialOvertime: '0',
|
||||
attendanceDays: 0,
|
||||
businessTripDays: 0,
|
||||
absentDays: 0,
|
||||
leaveDays: 0
|
||||
}
|
||||
}
|
||||
|
||||
const summary = summaryMap[employeeId]
|
||||
|
||||
if (record.overallStatus === 'absent_full') {
|
||||
summary.absentDays += 1
|
||||
} else if (record.overallStatus === 'absent_half') {
|
||||
summary.absentDays += 0.5
|
||||
} else if (record.overallStatus !== 'absent_full' && record.overallStatus !== 'absent_half') {
|
||||
summary.attendanceDays += 1
|
||||
}
|
||||
|
||||
if (record.p1LateMinutes > 0 || record.p2LateMinutes > 0) {
|
||||
summary.lateCount += 1
|
||||
summary.lateScore += (record.p1LateMinutes || 0) + (record.p2LateMinutes || 0)
|
||||
}
|
||||
|
||||
if (record.p1EarlyMinutes > 0 || record.p2EarlyMinutes > 0) {
|
||||
summary.earlyCount += 1
|
||||
summary.earlyScore += (record.p1EarlyMinutes || 0) + (record.p2EarlyMinutes || 0)
|
||||
}
|
||||
|
||||
let actualHours = 0
|
||||
if (record.p1FirstCheck && record.p1LastCheck) {
|
||||
const p1Hours = this.calculateHours(record.p1FirstCheck, record.p1LastCheck)
|
||||
actualHours += p1Hours
|
||||
}
|
||||
if (record.p2FirstCheck && record.p2LastCheck) {
|
||||
const p2Hours = this.calculateHours(record.p2FirstCheck, record.p2LastCheck)
|
||||
actualHours += p2Hours
|
||||
}
|
||||
summary.actualWorkHours = this.formatHours(actualHours)
|
||||
})
|
||||
|
||||
leaveRows.forEach(leave => {
|
||||
const employeeId = leave.applicantId || leave.applicantName
|
||||
if (!summaryMap[employeeId]) {
|
||||
summaryMap[employeeId] = {
|
||||
employeeId: employeeId,
|
||||
employeeName: leave.applicantName,
|
||||
departmentName: leave.applicantDeptName || '',
|
||||
standardWorkHours: '0.00',
|
||||
actualWorkHours: '0',
|
||||
lateCount: 0,
|
||||
lateScore: 0,
|
||||
earlyCount: 0,
|
||||
earlyScore: 0,
|
||||
normalOvertime: '0',
|
||||
specialOvertime: '0',
|
||||
attendanceDays: 0,
|
||||
businessTripDays: 0,
|
||||
absentDays: 0,
|
||||
leaveDays: 0
|
||||
}
|
||||
}
|
||||
summaryMap[employeeId].leaveDays += parseFloat(leave.leaveDays || 0)
|
||||
})
|
||||
|
||||
outRows.forEach(out => {
|
||||
const employeeId = out.applicantId || out.applicantName
|
||||
if (!summaryMap[employeeId]) {
|
||||
summaryMap[employeeId] = {
|
||||
employeeId: employeeId,
|
||||
employeeName: out.applicantName,
|
||||
departmentName: out.applicantDeptName || '',
|
||||
standardWorkHours: '0.00',
|
||||
actualWorkHours: '0',
|
||||
lateCount: 0,
|
||||
lateScore: 0,
|
||||
earlyCount: 0,
|
||||
earlyScore: 0,
|
||||
normalOvertime: '0',
|
||||
specialOvertime: '0',
|
||||
attendanceDays: 0,
|
||||
businessTripDays: 0,
|
||||
absentDays: 0,
|
||||
leaveDays: 0
|
||||
}
|
||||
}
|
||||
summaryMap[employeeId].businessTripDays += parseFloat(out.outHours || 0) / 8
|
||||
})
|
||||
|
||||
const standardHours = totalDays * 9
|
||||
Object.values(summaryMap).forEach(summary => {
|
||||
summary.standardWorkHours = standardHours.toFixed(2)
|
||||
summary.attendanceDays = `${totalDays}/${summary.attendanceDays.toFixed(1)}`
|
||||
summary.businessTripDays = summary.businessTripDays.toFixed(1)
|
||||
summary.absentDays = summary.absentDays.toFixed(1)
|
||||
summary.leaveDays = summary.leaveDays.toFixed(1)
|
||||
summary.lateCount = String(summary.lateCount)
|
||||
summary.lateScore = String(summary.lateScore)
|
||||
summary.earlyCount = String(summary.earlyCount)
|
||||
summary.earlyScore = String(summary.earlyScore)
|
||||
})
|
||||
|
||||
return Object.values(summaryMap)
|
||||
},
|
||||
|
||||
calculateDaysInRange() {
|
||||
const start = new Date(this.dateRangeParams.startDate)
|
||||
const end = new Date(this.dateRangeParams.endDate)
|
||||
const diffTime = Math.abs(end - start)
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
|
||||
return diffDays
|
||||
},
|
||||
|
||||
calculateHours(startTime, endTime) {
|
||||
try {
|
||||
const start = new Date(`2000-01-01 ${startTime}`)
|
||||
const end = new Date(`2000-01-01 ${endTime}`)
|
||||
const diffMs = end - start
|
||||
return diffMs / (1000 * 60 * 60)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
|
||||
formatHours(hours) {
|
||||
const h = Math.floor(hours)
|
||||
const m = Math.round((hours - h) * 60)
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`
|
||||
},
|
||||
|
||||
handleQuery() {
|
||||
this.getSummaryList()
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
this.download('wms/attendanceSummary/export', { ...this.dateRangeParams }, `attendanceSummary_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.date-range-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-footer {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.summary-totals {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.summary-totals span {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user