<template>
|
<div class="attendance-page">
|
<div class="search">
|
<div class="search_thing">
|
<div class="search_input">
|
<el-date-picker
|
v-model="dateRange"
|
type="datetimerange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
:default-time="['00:00:00', '23:59:59']"
|
value-format="yyyy-MM-dd HH:mm:ss"
|
style="width: 340px"
|
size="small"
|
@change="refreshTable"
|
></el-date-picker>
|
</div>
|
</div>
|
<div class="search_thing">
|
<div class="search_input">
|
<el-input
|
v-model="queryParams.keyword"
|
placeholder="工号或员工名称"
|
size="small"
|
style="width: 200px"
|
clearable
|
@keyup.enter.native="refreshTable"
|
></el-input>
|
</div>
|
</div>
|
<div class="search_thing">
|
<el-button size="mini" type="primary" @click="refreshTable()"
|
>查 询</el-button
|
>
|
<el-button size="mini" @click="resetQuery">重置</el-button>
|
<el-button size="mini" type="primary" @click="openAddAttendanceDialog()">手动新增</el-button>
|
<el-button size="mini" type="success" @click="openSyncAttendanceDialog()">同步考勤记录</el-button>
|
</div>
|
</div>
|
<div class="container">
|
<div v-loading="loading">
|
<el-table
|
:data="tableData"
|
border
|
:height="tableHeight"
|
style="width: 100%"
|
:header-cell-style="{textAlign:'center'}"
|
:cell-style="{textAlign:'center'}"
|
>
|
<el-table-column type="index" label="序号" width="60"></el-table-column>
|
<el-table-column prop="personCode" label="工号" min-width="120" width="120"></el-table-column>
|
<el-table-column prop="personName" label="姓名" min-width="120" width="120"></el-table-column>
|
<el-table-column prop="deptName" label="部门名称" min-width="120" width="120"></el-table-column>
|
<el-table-column prop="shiftId" label="班次类型" min-width="120" width="120">
|
<template slot-scope="scope">
|
<el-tag :type="getDictTypeByShift(scope.row.shiftId)">{{ getShiftByDic(scope.row.shiftId) }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="result" label="考勤结果" min-width="120" width="120">
|
<template slot-scope="scope">
|
<el-tag v-if="getResultTag(scope.row.result)" :type="getResultTag(scope.row.result).type">{{getResultTag(scope.row.result).label}}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="swingDate" label="考勤时间" min-width="150" width="150"></el-table-column>
|
<el-table-column label="签入/签出情况">
|
<el-table-column prop="workDateTime" label="上班时间" min-width="160" width="160">
|
<template slot-scope="scope">
|
<el-tag type="success" v-if="scope.row.workDateTime && scope.row.workClockInState===1">{{ scope.row.workDateTime }}</el-tag>
|
<el-tag type="danger" v-else-if="scope.row.workDateTime && scope.row.workClockInState===0">{{ scope.row.workDateTime }}</el-tag>
|
<span v-else>{{ scope.row.workDateTime }}</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="offWorkDateTime" label="下班时间" min-width="160" width="160">
|
<template slot-scope="scope">
|
<el-tag type="success" v-if="scope.row.offWorkDateTime && scope.row.offClockInState===1">{{ scope.row.offWorkDateTime }}</el-tag>
|
<el-tag type="danger" v-else-if="scope.row.offWorkDateTime && scope.row.offClockInState===0">{{ scope.row.offWorkDateTime }}</el-tag>
|
<span v-else>{{ scope.row.offWorkDateTime }}</span>
|
</template>
|
</el-table-column>
|
</el-table-column>
|
<el-table-column label="考勤时长(h)">
|
<el-table-column prop="plannedWorkHours" label="应勤时长" min-width="80" width="80"></el-table-column>
|
<el-table-column prop="actualWorkHours" label="实际时长" min-width="80" width="80"></el-table-column>
|
<el-table-column prop="absenceWorkHours" label="缺勤时长" min-width="80" width="80"></el-table-column>
|
</el-table-column>
|
<el-table-column prop="isSync" label="数据来源" min-width="120">
|
<template slot-scope="scope">
|
<el-tag v-if="scope.row.isSync===0" type="success">ICC同步</el-tag>
|
<el-tag v-else-if="scope.row.isSync===1" type="info">手动新增</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="createUser" label="创建人" min-width="150" width="150" :formatter="(row)=>formatterUserName(row.createUser)"></el-table-column>
|
<el-table-column prop="createTime" label="创建时间" min-width="180" width="180"></el-table-column>
|
<el-table-column prop="updateUser" label="更新人" min-width="150" width="150" :formatter="(row)=>formatterUserName(row.updateUser)"></el-table-column>
|
<el-table-column prop="updateTime" label="更新时间" min-width="180" width="180"></el-table-column>
|
<el-table-column fixed="right" width="180" label="操作">
|
<template slot-scope="scope">
|
<el-button type="text" @click="openClockInDialog(scope.row)">进出记录</el-button>
|
<el-button :disabled="scope.row.isSync===0" type="text" @click="openAddAttendanceDialog(scope.row)">编辑</el-button>
|
<el-button :disabled="scope.row.isSync===0" type="text" class="remove-btn" @click="confirmRemoveRecord(scope.row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<pagination
|
:total="pagination.total"
|
:page.sync="pagination.current"
|
:limit.sync="pagination.size"
|
@pagination="handlePageChange"
|
/>
|
</div>
|
</div>
|
<el-dialog
|
title="进出记录"
|
:visible.sync="dialogVisible"
|
width="60%">
|
<staff-clock-in-record :key="Math.random()" :query-params="clockInQueryParams" ></staff-clock-in-record>
|
</el-dialog>
|
<el-dialog
|
:title="attendanceForm.id?'编辑考勤记录':'新增考勤记录'"
|
:visible.sync="addAttendanceVisible"
|
@closed="closeAttendanceDialog"
|
width="50%">
|
<el-form ref="attendanceForm" style="margin-top:20px" :model="attendanceForm" :rules="attendanceRule" label-position="right" label-width="100px">
|
<div>
|
<el-divider content-position="left">基本信息</el-divider>
|
<el-row >
|
<el-col :span="12">
|
<el-form-item label="工号" prop="personCode">
|
<el-input size="small" v-model="attendanceForm.personCode" disabled></el-input>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="姓名" prop="personName">
|
<el-select size="small" style="width:100%" v-model="attendanceForm.personName" placeholder="请选择员工姓名" @change="selectedPersonName" clearable>
|
<el-option v-for="(item,index) in userList" :key="index" :label="item.name" :value="item.name"></el-option>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row >
|
<el-col :span="12">
|
<el-form-item label="部门名称" prop="deptName">
|
<el-input size="small" v-model="attendanceForm.deptName" placeholder="请输入部门名称" clearable></el-input>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="考勤时间" prop="swingDate">
|
<el-date-picker size="small" clearable style="width:100%" type="date" placeholder="请选择日期" value-format="yyyy-MM-dd" v-model="attendanceForm.swingDate" @change="checkDutyDate"></el-date-picker>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row >
|
<el-col :span="12">
|
<el-form-item label="班次类型" prop="shiftId">
|
<el-select disabled size="small" style="width:100%" v-model="attendanceForm.shiftId" placeholder="请选择班次类型" clearable>
|
<el-option v-for="(item,index) in dailyTypeList" :key="index" :label="item.dictLabel" :value="item.dictValue"></el-option>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<!-- <el-form-item label="考勤结果" prop="result">-->
|
<!-- <el-select size="small" style="width:100%" clearable v-model="attendanceForm.result" placeholder="请选择考勤结果">-->
|
<!-- <el-option v-for="(item,index) in resultList" :key="index" :label="item.label" :value="item.value"/>-->
|
<!-- </el-select>-->
|
<!-- </el-form-item>-->
|
</el-col>
|
</el-row>
|
<el-divider content-position="left">签入/签出情况</el-divider>
|
<el-row>
|
<el-col :span="12">
|
<el-form-item label="上班时间" prop="workTime">
|
<el-time-picker
|
style="width:100%"
|
value-format="HH:mm"
|
v-model="attendanceForm.workDateTime"
|
format="HH:mm"
|
placeholder="上班时间">
|
</el-time-picker>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="下班时间" prop="offWorkTime">
|
<el-time-picker
|
style="width:100%"
|
value-format="HH:mm"
|
format="HH:mm"
|
v-model="attendanceForm.offWorkDateTime"
|
placeholder="下班时间">
|
</el-time-picker>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</div>
|
</el-form>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="addAttendanceVisible = false">取 消</el-button>
|
<el-button type="primary" @click="confirmAddAttendance()">确 定</el-button>
|
</span>
|
</el-dialog>
|
<el-dialog
|
title="同步考勤记录"
|
:visible.sync="syncAttendanceVisible"
|
@close="()=>{syncDateRange=[]}"
|
width="25%">
|
<el-date-picker
|
style="width:100%"
|
v-model="syncDateRange"
|
type="datetimerange"
|
value-format="yyyy-MM-dd HH:mm:ss"
|
:default-time="['00:00:00', '23:59:59']"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期">
|
</el-date-picker>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="syncAttendanceVisible = false">取 消</el-button>
|
<el-button type="primary" @click="confirmSyncAttendance">确 定</el-button>
|
</span>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import StaffClockInRecord from "./components/staffClockInRecord.vue";
|
import {selectAllUser} from '@/api/system/user'
|
import {
|
pageAttendanceRecord,
|
checkDutyDate,
|
saveOrUpdateStaffAttendanceTrackingRecord,
|
removeStaffAttendanceTrackingRecord,
|
syncAttendanceRecord
|
} from '@/api/performance/attendance'
|
import {getDicts} from "@/api/system/dict/data";
|
import dayjs from 'dayjs';
|
export default {
|
name: "Attendance",
|
components: {
|
StaffClockInRecord
|
},
|
data() {
|
return {
|
syncDateRange:[],
|
syncAttendanceVisible: false,
|
attendanceForm:{
|
workDataId: null,
|
offWorkDataId: null,
|
personCode: null,
|
personName: null,
|
deptName: "质量部",
|
shiftId: null,
|
swingDate: null,
|
workDateTime: null,
|
offWorkDateTime: null,
|
result: null,
|
},
|
attendanceRule:{
|
personName:{required:true,message:'请选择员工姓名',trigger:'change'},
|
deptName:{required:true,message:'请输入部门名称',trigger:'blur'},
|
dailyName:{required:true,message:'请输入班次名称',trigger:'blur'},
|
shiftId:{required:true,message:'请选择班次类型',trigger:'change'},
|
swingDate:{required:true,message:'请选择考勤时间',trigger:'change'},
|
workDateTime:{required:true,message:'请选择上班时间',trigger:'change'},
|
offWorkDateTime:{required:true,message:'请选择下班时间',trigger:'change'},
|
result:{required:true,message:'请选择考勤结果',trigger:'change'},
|
},
|
addAttendanceVisible:false,
|
dialogVisible:false,
|
dateRange: [],
|
queryParams: {
|
startDate: "",
|
endDate: "",
|
keyword: "",
|
},
|
userList:[],
|
loading: false,
|
tableData: [],
|
pagination: {
|
current: 1,
|
size: 20,
|
total: 0,
|
},
|
tableHeight:0,
|
resizeHandler: null, // 防抖函数引用
|
clockInQueryParams:{
|
personCode:null,//人员编号
|
swingDate:null,//考勤时间
|
shiftId:null//班次id
|
},
|
dailyTypeList:[],
|
resultList:[
|
{
|
label:"正常",
|
value:1,
|
type:"success"
|
},
|
{
|
label:"异常",
|
value:0,
|
type:"danger"
|
},
|
],
|
};
|
},
|
created() {
|
this.selectEnumByCategory()
|
this.getUserList()
|
this.getTableHeight();
|
this.dateRange = this.getTimeRange()
|
this.resizeHandler = this.debounce(() => {
|
this.getTableHeight();
|
}, 200);
|
this.refreshTable();
|
},
|
mounted(){
|
this.getTableHeight();
|
window.addEventListener("resize",this.resizeHandler)
|
},
|
beforeDestroy(){
|
window.removeEventListener("resize",this.resizeHandler)
|
},
|
methods: {
|
getShiftByDic(e) {
|
let obj = this.dailyTypeList.find((m) => m.dictValue == e);
|
if (obj) {
|
return obj.dictLabel;
|
}
|
return "无";
|
},
|
getDictTypeByShift(e) {
|
let obj = this.dailyTypeList.find((m) => m.dictValue == e);
|
if (obj) {
|
return obj.listClass;
|
}
|
return "";
|
},
|
selectEnumByCategory() {
|
getDicts("sys_class_type").then((response) => {
|
this.dailyTypeList = response.data;
|
});
|
},
|
/**
|
* 初始化默认日期范围:近一个月(当前日期 - 30天 至 当前日期)
|
*/
|
getTimeRange(format = 'YYYY-MM-DD HH:mm:ss') {
|
// 获取当前时间
|
const now = dayjs();
|
// 获取当前日期的「日」(1-31)
|
const currentDate = now.date();
|
|
let startTime, endTime;
|
|
// 核心逻辑:判断当前日期是否大于25号
|
if (currentDate > 25) {
|
// ✅ 情况1:当前日>25 → 当月26号 ~ 次月25号
|
startTime = now.startOf('month').add(25, 'day'); // 当月1号 +25天 = 26号
|
endTime = startTime.add(1, 'month').date(25).hour(23)
|
.minute(59)
|
.second(59); // 次月25号(dayjs自动处理跨年)
|
} else {
|
// ✅ 情况2:当前日≤25 → 上月26号 ~ 当月25号
|
startTime = now.subtract(1, 'month').startOf('month').add(25, 'day'); // 上月26号
|
endTime = now.date(25).hour(23)
|
.minute(59)
|
.second(59); // 当月25号
|
}
|
|
// 返回格式化后的时间数组
|
return [startTime.format(format), endTime.format(format)];
|
},
|
//同步考勤记录
|
confirmSyncAttendance(){
|
if(!this.syncDateRange || this.syncDateRange.length<2){
|
this.$message.warning("请选择同步日期");
|
return
|
}
|
syncAttendanceRecord({
|
startDate:this.syncDateRange[0],
|
endDate:this.syncDateRange[1]
|
}).then(res=>{
|
if(res.code===200){
|
this.$message.success("后台同步考勤记录中,请刷新表格查看")
|
this.refreshTable()
|
}
|
}).catch(error=>{
|
console.error(error)
|
})
|
this.$nextTick(()=>{
|
this.syncAttendanceVisible = false
|
})
|
},
|
//打开同步考勤记录弹框
|
openSyncAttendanceDialog(){
|
this.syncAttendanceVisible = true
|
},
|
//删除考勤记录
|
confirmRemoveRecord(row){
|
if(!row){
|
this.$message.warning("请选择一条记录")
|
return
|
}
|
this.$confirm('是否确认删除该条考勤记录', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(() => {
|
let ids = [row.workDataId,row.offWorkDataId]
|
removeStaffAttendanceTrackingRecord(ids).then(res=>{
|
if(res.code===200){
|
this.$message.success("删除成功")
|
this.refreshTable()
|
}
|
}).catch(error=>{
|
console.error(error)
|
})
|
}).catch(() => { });
|
},
|
//关闭新增/编辑考勤记录弹框
|
closeAttendanceDialog(){
|
this.attendanceForm.id = null
|
this.$refs.attendanceForm.resetFields()
|
},
|
//重置考勤记录弹框表单
|
resetAttendanceForm(){
|
this.attendanceForm = {
|
workDataId: null,
|
offWorkDataId: null,
|
personCode: null,
|
personName: null,
|
deptName: "质量部",
|
shiftId: null,
|
swingDate: null,
|
workDateTime: null,
|
offWorkDateTime: null,
|
result: null,
|
}
|
this.$refs.attendanceForm.resetFields()
|
this.$nextTick(()=>{
|
this.addAttendanceVisible = false
|
})
|
},
|
//确认保存考勤记录
|
confirmAddAttendance(){
|
this.$refs.attendanceForm.validate(valid=>{
|
if(valid){
|
saveOrUpdateStaffAttendanceTrackingRecord(this.attendanceForm).then(res=>{
|
if(res.code===200){
|
this.$message.success('保存成功')
|
this.refreshTable()
|
this.resetAttendanceForm()
|
}
|
}).catch(error=>{
|
console.error(error)
|
})
|
}
|
})
|
},
|
//校验所选考勤日期是否存在该人员的考勤记录
|
checkDutyDate(val){
|
if(!this.attendanceForm.personCode){
|
this.$message.warning("请先选择人员名称")
|
this.attendanceForm.dutyDate = null
|
return
|
}
|
let params = {
|
personCode:this.attendanceForm.personCode,
|
swingDate: val
|
}
|
checkDutyDate(params).then(res=>{
|
if(res.code===200&&res.data){
|
this.attendanceForm.result = 1//默认正常
|
this.attendanceForm.shiftId = res.data.shift
|
this.attendanceForm.workDateTime = res.data.startTime
|
this.attendanceForm.offWorkDateTime = res.data.endTime
|
}
|
}).catch(error=>{
|
console.error(error)
|
this.attendanceForm.swingDate = null
|
})
|
},
|
//选择人员名称带出工号
|
selectedPersonName(val){
|
let account = ""
|
this.userList.forEach(item=>{
|
if(item.name===val){
|
account = item.account
|
}
|
})
|
this.attendanceForm.personCode = account
|
},
|
//打开新增/编辑考勤记录弹框
|
openAddAttendanceDialog(row){
|
if(row){
|
//处理上/下班时间格式
|
let workTime = row.workDateTime&&row.workDateTime.length>8?row.workDateTime.substring(11,16):row.workDateTime
|
let offWorkTime = row.offWorkDateTime&&row.offWorkDateTime.length>8?row.offWorkDateTime.substring(11,16):row.offWorkDateTime
|
row.workDateTime = workTime
|
row.offWorkDateTime = offWorkTime
|
this.attendanceForm = {...row}
|
}
|
this.addAttendanceVisible = true
|
},
|
//打开查看打卡记录弹框
|
openClockInDialog(row){
|
if(row){
|
this.clockInQueryParams.personCode = row.personCode
|
this.clockInQueryParams.shiftId = row.shiftId
|
this.clockInQueryParams.swingDate = row.swingDate
|
}
|
this.$nextTick(()=>{
|
this.dialogVisible = true
|
})
|
},
|
//格式化人员名称
|
formatterUserName(value){
|
let userName = ""
|
this.userList.forEach(item=>{
|
if(item.id===value){
|
userName = item.name
|
}
|
})
|
return userName;
|
},
|
//格式化考勤结果
|
getResultTag(type) {
|
let typeItem = null
|
this.resultList.forEach(item=>{
|
if(item.value===type){
|
typeItem = item
|
}
|
})
|
return typeItem;
|
},
|
//防抖函数
|
debounce(fn, delay) {
|
let timer = null;
|
return (...args) => {
|
clearTimeout(timer);
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
};
|
},
|
//动态获取表格高度
|
getTableHeight(){
|
const innerHeight = window.innerHeight
|
const naviHeight = 96
|
const otherHeight = 128
|
this.tableHeight = innerHeight - naviHeight - otherHeight
|
},
|
//刷新表格
|
refreshTable() {
|
this.loading = true;
|
if (this.dateRange && this.dateRange.length === 2) {
|
this.queryParams.startDate = this.dateRange[0];
|
this.queryParams.endDate = this.dateRange[1];
|
} else {
|
this.queryParams.startDate = "";
|
this.queryParams.endDate = "";
|
}
|
pageAttendanceRecord({...this.queryParams, ...this.pagination}).then(res => {
|
this.tableData = res.data.records;
|
this.pagination.total = res.data.total;
|
this.pagination.current = res.data.current;
|
this.loading = false;
|
}).catch(() => {
|
this.loading = false;
|
});
|
},
|
//分页切换
|
handlePageChange(page) {
|
this.pagination.current = page.page;
|
this.pagination.size = page.limit;
|
this.refreshTable();
|
},
|
//查询用户列表
|
getUserList(){
|
selectAllUser().then(res=>{
|
this.userList = res.data
|
}).catch(error=>{
|
console.error(error)
|
})
|
},
|
//重置按钮
|
resetQuery() {
|
this.dateRange = this.getTimeRange();
|
this.queryParams = {
|
startDate: "",
|
endDate: "",
|
keyword: "",
|
};
|
this.$nextTick(()=>{
|
this.refreshTable()
|
})
|
},
|
},
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.attendance-page {
|
padding: 10px;
|
}
|
.remove-btn.el-button--text{
|
color:#ff4949
|
}
|
.search {
|
height: 50px;
|
display: flex;
|
align-items: center;
|
background-color: #fff;
|
margin-left:16px;
|
}
|
|
.search_thing {
|
display: flex;
|
align-items: center;
|
height: 50px;
|
margin-right: 30px
|
}
|
|
.search_label {
|
width: 90px;
|
font-size: 14px;
|
text-align: right;
|
}
|
|
.search_input {
|
display: flex;
|
align-items: center;
|
}
|
|
.container {
|
background-color: #fff;
|
padding: 0 16px;
|
min-height: calc(100vh - 180px);
|
}
|
|
::v-deep .el-tabs__header {
|
margin-bottom: 16px;
|
}
|
|
::v-deep .el-tabs__content {
|
padding: 0;
|
}
|
</style>
|