528 lines
19 KiB
Vue
528 lines
19 KiB
Vue
<template>
|
||
<div class="hrm-page attendance-page">
|
||
<section class="panel-grid">
|
||
<el-card class="metal-panel flat" shadow="never">
|
||
<div class="card-toolbar">
|
||
<div class="actions-inline">
|
||
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="openShiftDialog()">新增</el-button>
|
||
<el-button size="mini" plain icon="el-icon-refresh" @click="loadShift">刷新</el-button>
|
||
</div>
|
||
</div>
|
||
<el-table :data="shiftList" v-loading="shiftLoading" height="320" stripe>
|
||
<el-table-column label="名称" prop="shiftName" min-width="140" />
|
||
<el-table-column label="时间段" min-width="180">
|
||
<template slot-scope="scope">{{ scope.row.startTime }} - {{ scope.row.endTime }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="夜班" prop="nightShift" min-width="80">
|
||
<template slot-scope="scope">
|
||
<el-tag size="mini" :type="scope.row.nightShift ? 'warning' : 'info'">
|
||
{{ scope.row.nightShift ? '是' : '否' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="休息" prop="restMinutes" min-width="100" />
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="text" @click.stop="openShiftDialog(scope.row)">编辑</el-button>
|
||
<el-button size="mini" type="text" @click.stop="delShift(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-card class="metal-panel flat" shadow="never">
|
||
<div class="card-toolbar">
|
||
<div class="actions-inline">
|
||
<el-date-picker
|
||
v-model="scheduleQuery.date"
|
||
type="date"
|
||
placeholder="日期"
|
||
size="mini"
|
||
style="width: 140px"
|
||
@change="loadSchedule"
|
||
/>
|
||
<el-button size="mini" type="primary" plain @click="loadSchedule">查询</el-button>
|
||
<el-button size="mini" plain icon="el-icon-plus" @click="openScheduleDialog()">新增</el-button>
|
||
</div>
|
||
</div>
|
||
<el-table :data="scheduleList" v-loading="scheduleLoading" height="320" stripe>
|
||
<el-table-column label="员工" prop="empId" min-width="120">
|
||
<template slot-scope="scope">
|
||
{{ renderEmp(scope.row.empId) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="日期" prop="workDate" min-width="120" />
|
||
<el-table-column label="班次" prop="shiftId" min-width="140">
|
||
<template slot-scope="scope">
|
||
{{ renderShift(scope.row.shiftId) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="text" @click.stop="openScheduleDialog(scope.row)">编辑</el-button>
|
||
<el-button size="mini" type="text" @click.stop="delSchedule(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-card class="metal-panel flat wide" shadow="never">
|
||
<div class="card-toolbar">
|
||
<div class="actions-inline">
|
||
<el-date-picker
|
||
v-model="punchQuery.range"
|
||
type="daterange"
|
||
start-placeholder="开始"
|
||
end-placeholder="结束"
|
||
size="mini"
|
||
value-format="yyyy-MM-dd"
|
||
@change="loadPunchAndAttend"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="dual-tables">
|
||
<div class="table-half">
|
||
<div class="table-title">打卡记录</div>
|
||
<el-table :data="punchList" v-loading="punchLoading" height="230" stripe>
|
||
<el-table-column label="时间" prop="punchTime" min-width="150">
|
||
<template slot-scope="scope">{{ formatDate(scope.row.punchTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="员工" prop="empId" min-width="120">
|
||
<template slot-scope="scope">
|
||
{{ renderEmp(scope.row.empId) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="来源" prop="source" min-width="100" />
|
||
<el-table-column label="定位/设备" prop="location" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="120" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="text" @click.stop="openPunchDialog(scope.row)">编辑</el-button>
|
||
<el-button size="mini" type="text" @click.stop="delPunch(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div class="table-half">
|
||
<div class="table-title">考勤结果</div>
|
||
<el-table :data="attendList" v-loading="attendLoading" height="230" stripe>
|
||
<el-table-column label="日期" prop="workDate" min-width="120" />
|
||
<el-table-column label="出勤分钟" prop="attendMinutes" min-width="110" />
|
||
<el-table-column label="加班分钟" prop="overtimeMinutes" min-width="110" />
|
||
<el-table-column label="请假分钟" prop="leaveMinutes" min-width="110" />
|
||
<el-table-column label="异常" prop="exceptionMsg" min-width="140" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span :class="['exception-tag', scope.row.exceptionMsg ? 'is-error' : '']">
|
||
{{ scope.row.exceptionMsg || '正常' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</section>
|
||
|
||
<!-- 班次弹窗 -->
|
||
<el-dialog :visible.sync="shiftDialogVisible" title="班次" width="420px">
|
||
<el-form ref="shiftForm" :model="shiftForm" :rules="shiftRules" label-width="90px" size="small">
|
||
<el-form-item label="名称" prop="shiftName">
|
||
<el-input v-model="shiftForm.shiftName" placeholder="班次名称" />
|
||
</el-form-item>
|
||
<el-form-item label="开始时间" prop="startTime">
|
||
<el-time-picker v-model="shiftForm.startTime" placeholder="开始时间" style="width: 100%" value-format="HH:mm" />
|
||
</el-form-item>
|
||
<el-form-item label="结束时间" prop="endTime">
|
||
<el-time-picker v-model="shiftForm.endTime" placeholder="结束时间" style="width: 100%" value-format="HH:mm" />
|
||
</el-form-item>
|
||
<el-form-item label="夜班" prop="nightShift">
|
||
<el-switch v-model="shiftForm.nightShift" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
<el-form-item label="休息分钟" prop="restMinutes">
|
||
<el-input-number v-model="shiftForm.restMinutes" :min="0" :max="600" controls-position="right" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="shiftForm.remark" type="textarea" :rows="2" placeholder="备注" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer">
|
||
<el-button @click="shiftDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="shiftSubmitting" @click="submitShift">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 排班弹窗 -->
|
||
<el-dialog :visible.sync="scheduleDialogVisible" title="排班" width="420px">
|
||
<el-form ref="scheduleForm" :model="scheduleForm" :rules="scheduleRules" label-width="90px" size="small">
|
||
<el-form-item label="日期" prop="workDate">
|
||
<el-date-picker v-model="scheduleForm.workDate" type="date" placeholder="选择日期" style="width: 100%" value-format="yyyy-MM-dd" />
|
||
</el-form-item>
|
||
<el-form-item label="员工" prop="empId">
|
||
<el-select v-model="scheduleForm.empId" placeholder="选择员工" filterable style="width: 100%">
|
||
<el-option v-for="emp in employeeOptions" :key="emp.empId" :label="`${emp.empName} (${emp.empCode || emp.empId})`" :value="emp.empId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="班次" prop="shiftId">
|
||
<el-select v-model="scheduleForm.shiftId" placeholder="选择班次" filterable style="width: 100%">
|
||
<el-option v-for="shift in shiftList" :key="shift.shiftId" :label="`${shift.shiftName} ${shift.startTime || ''}-${shift.endTime || ''}`" :value="shift.shiftId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="scheduleForm.remark" type="textarea" :rows="2" placeholder="备注" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer">
|
||
<el-button @click="scheduleDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="scheduleSubmitting" @click="submitSchedule">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 打卡弹窗 -->
|
||
<el-dialog :visible.sync="punchDialogVisible" title="打卡" width="420px">
|
||
<el-form ref="punchForm" :model="punchForm" :rules="punchRules" label-width="90px" size="small">
|
||
<el-form-item label="员工" prop="empId">
|
||
<el-select v-model="punchForm.empId" placeholder="选择员工" filterable style="width: 100%">
|
||
<el-option v-for="emp in employeeOptions" :key="emp.empId" :label="`${emp.empName} (${emp.empCode || emp.empId})`" :value="emp.empId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="时间" prop="punchTime">
|
||
<el-date-picker
|
||
v-model="punchForm.punchTime"
|
||
type="datetime"
|
||
placeholder="选择时间"
|
||
style="width: 100%"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="来源" prop="source">
|
||
<el-select v-model="punchForm.source" placeholder="选择来源" style="width: 100%">
|
||
<el-option label="手工" value="manual" />
|
||
<el-option label="设备" value="device" />
|
||
<el-option label="移动" value="mobile" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="定位/设备" prop="location">
|
||
<el-input v-model="punchForm.location" placeholder="如:门禁A / GPS坐标" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="punchForm.remark" type="textarea" :rows="2" placeholder="备注" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer">
|
||
<el-button @click="punchDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="punchSubmitting" @click="submitPunch">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
listEmployee,
|
||
listShift,
|
||
listSchedule,
|
||
listPunch,
|
||
listAttendCalc,
|
||
addShift,
|
||
updateShift,
|
||
delShift,
|
||
addSchedule,
|
||
updateSchedule,
|
||
delSchedule,
|
||
addPunch,
|
||
updatePunch,
|
||
delPunch
|
||
} from '@/api/hrm'
|
||
|
||
export default {
|
||
name: 'HrmAttendance',
|
||
data() {
|
||
return {
|
||
employeeOptions: [],
|
||
shiftList: [],
|
||
shiftLoading: false,
|
||
scheduleList: [],
|
||
scheduleLoading: false,
|
||
scheduleQuery: { date: '' },
|
||
punchList: [],
|
||
punchLoading: false,
|
||
punchQuery: { range: [] },
|
||
attendList: [],
|
||
attendLoading: false,
|
||
shiftDialogVisible: false,
|
||
shiftForm: {},
|
||
shiftRules: {
|
||
shiftName: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
||
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
|
||
},
|
||
shiftSubmitting: false,
|
||
scheduleDialogVisible: false,
|
||
scheduleForm: {},
|
||
scheduleRules: {
|
||
workDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||
empId: [{ required: true, message: '请选择员工', trigger: 'change' }],
|
||
shiftId: [{ required: true, message: '请选择班次', trigger: 'change' }]
|
||
},
|
||
scheduleSubmitting: false,
|
||
punchDialogVisible: false,
|
||
punchForm: {},
|
||
punchRules: {
|
||
empId: [{ required: true, message: '请选择员工', trigger: 'change' }],
|
||
punchTime: [{ required: true, message: '请选择时间', trigger: 'change' }],
|
||
source: [{ required: true, message: '请选择来源', trigger: 'change' }],
|
||
location: [{ required: true, message: '请输入定位/设备', trigger: 'blur' }]
|
||
},
|
||
punchSubmitting: false
|
||
}
|
||
},
|
||
created() {
|
||
this.loadEmployees()
|
||
this.loadShift()
|
||
this.loadSchedule()
|
||
this.loadPunchAndAttend()
|
||
},
|
||
methods: {
|
||
formatDate(val) {
|
||
if (!val) return ''
|
||
const date = new Date(val)
|
||
const pad = n => (n < 10 ? `0${n}` : n)
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||
},
|
||
loadEmployees() {
|
||
listEmployee({ pageNum: 1, pageSize: 500 })
|
||
.then(res => {
|
||
this.employeeOptions = res.rows || []
|
||
})
|
||
},
|
||
loadShift() {
|
||
this.shiftLoading = true
|
||
listShift({ pageNum: 1, pageSize: 200 })
|
||
.then(res => {
|
||
this.shiftList = res.rows || []
|
||
})
|
||
.finally(() => {
|
||
this.shiftLoading = false
|
||
})
|
||
},
|
||
loadSchedule() {
|
||
this.scheduleLoading = true
|
||
listSchedule({ ...this.scheduleQuery, pageNum: 1, pageSize: 200 })
|
||
.then(res => {
|
||
this.scheduleList = res.rows || []
|
||
})
|
||
.finally(() => {
|
||
this.scheduleLoading = false
|
||
})
|
||
},
|
||
loadPunchAndAttend() {
|
||
this.punchLoading = true
|
||
this.attendLoading = true
|
||
const [startTime, endTime] = this.punchQuery.range || []
|
||
listPunch({ pageNum: 1, pageSize: 200, startTime, endTime })
|
||
.then(res => {
|
||
this.punchList = res.rows || []
|
||
})
|
||
.finally(() => {
|
||
this.punchLoading = false
|
||
})
|
||
listAttendCalc({ pageNum: 1, pageSize: 200 })
|
||
.then(res => {
|
||
this.attendList = res.rows || []
|
||
})
|
||
.finally(() => {
|
||
this.attendLoading = false
|
||
})
|
||
},
|
||
renderEmp(empId) {
|
||
const emp = this.employeeOptions.find(e => e.empId === empId)
|
||
if (!emp) return empId || ''
|
||
return `${emp.empName}${emp.empCode ? ` (${emp.empCode})` : ''}`
|
||
},
|
||
renderShift(shiftId) {
|
||
const shift = this.shiftList.find(s => s.shiftId === shiftId)
|
||
if (!shift) return shiftId || ''
|
||
return `${shift.shiftName}${shift.startTime ? ` ${shift.startTime}-${shift.endTime}` : ''}`
|
||
},
|
||
// 班次
|
||
openShiftDialog(row) {
|
||
this.shiftForm = row
|
||
? { ...row }
|
||
: { shiftName: '', startTime: '', endTime: '', nightShift: 0, restMinutes: 0, remark: '' }
|
||
this.shiftDialogVisible = true
|
||
this.$nextTick(() => this.$refs.shiftForm && this.$refs.shiftForm.clearValidate())
|
||
},
|
||
submitShift() {
|
||
this.$refs.shiftForm.validate(valid => {
|
||
if (!valid) return
|
||
this.shiftSubmitting = true
|
||
const api = this.shiftForm.shiftId ? updateShift : addShift
|
||
api(this.shiftForm)
|
||
.then(() => {
|
||
this.$message.success('已保存')
|
||
this.shiftDialogVisible = false
|
||
this.loadShift()
|
||
})
|
||
.finally(() => {
|
||
this.shiftSubmitting = false
|
||
})
|
||
})
|
||
},
|
||
delShift(row) {
|
||
this.$confirm('确认删除该班次吗?', '提示', { type: 'warning' }).then(() => {
|
||
delShift(row.shiftId).then(() => {
|
||
this.$message.success('已删除')
|
||
this.loadShift()
|
||
})
|
||
})
|
||
},
|
||
// 排班
|
||
openScheduleDialog(row) {
|
||
this.scheduleForm = row
|
||
? { ...row }
|
||
: { workDate: '', empId: '', shiftId: '', remark: '' }
|
||
this.scheduleDialogVisible = true
|
||
this.$nextTick(() => this.$refs.scheduleForm && this.$refs.scheduleForm.clearValidate())
|
||
},
|
||
submitSchedule() {
|
||
this.$refs.scheduleForm.validate(valid => {
|
||
if (!valid) return
|
||
this.scheduleSubmitting = true
|
||
const api = this.scheduleForm.scheduleId ? updateSchedule : addSchedule
|
||
api(this.scheduleForm)
|
||
.then(() => {
|
||
this.$message.success('已保存')
|
||
this.scheduleDialogVisible = false
|
||
this.loadSchedule()
|
||
})
|
||
.finally(() => {
|
||
this.scheduleSubmitting = false
|
||
})
|
||
})
|
||
},
|
||
delSchedule(row) {
|
||
this.$confirm('确认删除该排班吗?', '提示', { type: 'warning' }).then(() => {
|
||
delSchedule(row.scheduleId).then(() => {
|
||
this.$message.success('已删除')
|
||
this.loadSchedule()
|
||
})
|
||
})
|
||
},
|
||
// 打卡
|
||
openPunchDialog(row) {
|
||
this.punchForm = row
|
||
? { ...row }
|
||
: { empId: '', punchTime: '', source: 'manual', location: '', remark: '' }
|
||
this.punchDialogVisible = true
|
||
this.$nextTick(() => this.$refs.punchForm && this.$refs.punchForm.clearValidate())
|
||
},
|
||
submitPunch() {
|
||
this.$refs.punchForm.validate(valid => {
|
||
if (!valid) return
|
||
this.punchSubmitting = true
|
||
const api = this.punchForm.punchId ? updatePunch : addPunch
|
||
api(this.punchForm)
|
||
.then(() => {
|
||
this.$message.success('已保存')
|
||
this.punchDialogVisible = false
|
||
this.loadPunchAndAttend()
|
||
})
|
||
.finally(() => {
|
||
this.punchSubmitting = false
|
||
})
|
||
})
|
||
},
|
||
delPunch(row) {
|
||
this.$confirm('确认删除该打卡记录吗?', '提示', { type: 'warning' }).then(() => {
|
||
delPunch(row.punchId).then(() => {
|
||
this.$message.success('已删除')
|
||
this.loadPunchAndAttend()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.attendance-page {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.panel-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
align-items: start;
|
||
}
|
||
.metal-panel {
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
.metal-panel :deep(.el-card__body) {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.card-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 4px 0 8px;
|
||
}
|
||
.actions-inline {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.flat :deep(.el-card__body) {
|
||
padding: 0;
|
||
}
|
||
.flat {
|
||
padding: 0;
|
||
}
|
||
.wide {
|
||
grid-column: span 2;
|
||
}
|
||
.flat :deep(.el-table),
|
||
.flat :deep(.el-table__body-wrapper),
|
||
.flat :deep(.el-table__header-wrapper),
|
||
.flat :deep(.el-table__empty-block) {
|
||
background: transparent;
|
||
}
|
||
.dual-tables {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
.table-half {
|
||
border: none;
|
||
border-radius: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.table-title {
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
color: #1f2937;
|
||
}
|
||
.exception-tag {
|
||
color: #409eff;
|
||
&.is-error {
|
||
color: #e76f51;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
@media (max-width: 1200px) {
|
||
.panel-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.wide {
|
||
grid-column: span 1;
|
||
}
|
||
.dual-tables {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|