601 lines
19 KiB
Vue
601 lines
19 KiB
Vue
<template>
|
||
<div class="attendance-reviewed-container">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="title">综合部复审列表</span>
|
||
<div class="header-right">
|
||
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
|
||
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
|
||
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
|
||
style="width: 200px; margin-right: 8px;">
|
||
<template #prefix>
|
||
<el-icon>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
|
||
:header-cell-style="{
|
||
background: '#f5f7fa',
|
||
color: '#606266',
|
||
fontWeight: 'bold',
|
||
textAlign: 'center',
|
||
padding: '12px 0'
|
||
}" :cell-style="{
|
||
padding: '8px 0'
|
||
}">
|
||
<el-table-column label="头像" width="80" align="center" fixed="left">
|
||
<template #default="scope">
|
||
<el-avatar :size="40" :src="scope.row.avatar" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
|
||
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
|
||
{{ scope.row.status === 0 ? '正式' : '试用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
|
||
show-overflow-tooltip />
|
||
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||
<template #default="scope">
|
||
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
|
||
详情
|
||
</el-button>
|
||
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
|
||
审批
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<!-- 异常考勤详情对话框 -->
|
||
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
|
||
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
|
||
<div class="date-header">{{ date }}</div>
|
||
<el-table :data="records" style="width: 100%" border stripe>
|
||
<el-table-column prop="type" label="异常类型" width="120">
|
||
<template #default="scope">
|
||
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
|
||
{{ getAbnormalTypeLabel(scope.row.type) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="异常详情" min-width="200">
|
||
<template #default="scope">
|
||
<template v-if="scope.row.type === 'abnormal'">
|
||
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
|
||
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
|
||
<div class="result-row">
|
||
考勤结果:
|
||
<el-select v-model="scope.row.realResult" size="small" style="width: 100px; margin: 0 8px;"
|
||
@change="handleUpdateAbnormal(scope.row)">
|
||
<el-option label="正常" value="Normal" />
|
||
<el-option label="早退" value="Early" />
|
||
<el-option label="迟到" value="Late" />
|
||
<el-option label="严重迟到" value="SeriousLate" />
|
||
<el-option label="旷工迟到" value="Absenteeism" />
|
||
<el-option label="未打卡" value="NotSigned" />
|
||
</el-select>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="scope.row.type === 'checkin'">
|
||
<div class="result-row">
|
||
缺卡次数:
|
||
<el-input-number v-model="scope.row.missedCount" size="small" style="width: 100px; margin: 0 8px;"
|
||
:min="0" :max="10" @change="handleUpdateAbnormal(scope.row)" />
|
||
次
|
||
</div>
|
||
</template>
|
||
<template v-else-if="scope.row.type === 'report'">
|
||
<div class="result-row">
|
||
日志状态:
|
||
<el-select v-model="scope.row.reportCount" size="small" style="width: 100px; margin: 0 8px;"
|
||
@change="handleUpdateAbnormal(scope.row)">
|
||
<el-option label="未写日志" :value="0" />
|
||
<el-option label="正常" :value="1" />
|
||
</el-select>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="修改记录" min-width="300">
|
||
<template #default="scope">
|
||
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
|
||
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
|
||
<div>修改人:{{ mod.modifiedBy }}</div>
|
||
<div>修改时间:{{ mod.modifiedAt }}</div>
|
||
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
|
||
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
|
||
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
|
||
</div>
|
||
</div>
|
||
<span v-else>无修改记录</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { Search } from '@element-plus/icons-vue';
|
||
import dayjs from 'dayjs';
|
||
import {
|
||
getReviewedAttendanceList,
|
||
updateAttendanceDetail,
|
||
reviewAttendance,
|
||
getAbnormalAttendanceDetail,
|
||
updateAbnormalAttendance,
|
||
} from "../../../../api/modules/index";
|
||
|
||
// 权限检查
|
||
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
|
||
|
||
|
||
// 列表数据
|
||
const attendanceList = ref([]);
|
||
const originalList = ref([]);
|
||
const searchKeyword = ref('');
|
||
|
||
// 日期范围
|
||
const dateRange = ref(null);
|
||
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
|
||
|
||
// 日期快捷选项
|
||
const dateShortcuts = [
|
||
{
|
||
text: '上个月',
|
||
value: () => {
|
||
const now = new Date();
|
||
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||
},
|
||
},
|
||
{
|
||
text: '本月',
|
||
value: () => {
|
||
const now = new Date();
|
||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
||
},
|
||
},
|
||
{
|
||
text: '下个月',
|
||
value: () => {
|
||
const now = new Date();
|
||
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
||
},
|
||
},
|
||
];
|
||
|
||
// 设置默认日期范围(上个月)
|
||
const setDefaultDateRange = () => {
|
||
const now = new Date();
|
||
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||
};
|
||
|
||
// 处理日期变化
|
||
const handleDateChange = (val) => {
|
||
if (val) {
|
||
fetchList();
|
||
}
|
||
};
|
||
|
||
// 获取列表数据
|
||
const fetchList = async () => {
|
||
try {
|
||
const params = {};
|
||
if (dateRange.value) {
|
||
params.year = dateRange.value.getFullYear();
|
||
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
|
||
}
|
||
const res = await getReviewedAttendanceList(params);
|
||
if (res.errcode === 0) {
|
||
originalList.value = res.result;
|
||
attendanceList.value = res.result;
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error("获取列表失败");
|
||
}
|
||
};
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
if (!searchKeyword.value) {
|
||
attendanceList.value = originalList.value;
|
||
return;
|
||
}
|
||
attendanceList.value = originalList.value.filter(item =>
|
||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||
);
|
||
};
|
||
|
||
// 获取审批状态标签
|
||
const getFlagLabel = (flag) => {
|
||
const flagMap = {
|
||
0: '待审批',
|
||
1: '已通过',
|
||
2: '已驳回'
|
||
};
|
||
return flagMap[flag] || '未知';
|
||
};
|
||
|
||
// 获取审批状态标签样式
|
||
const getFlagTagType = (flag) => {
|
||
const flagMap = {
|
||
0: 'warning',
|
||
1: 'success',
|
||
2: 'danger'
|
||
};
|
||
return flagMap[flag] || '';
|
||
};
|
||
|
||
// 审批
|
||
const handleApprove = (row) => {
|
||
ElMessageBox.confirm(
|
||
'确认审批通过该考勤记录?',
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
).then(async () => {
|
||
try {
|
||
const res = await reviewAttendance([row.id]);
|
||
if (res.errcode === 0) {
|
||
ElMessage.success("审批成功");
|
||
fetchList();
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error("审批失败");
|
||
}
|
||
}).catch(() => {
|
||
ElMessage.info('已取消审批');
|
||
});
|
||
};
|
||
|
||
// 异常考勤对话框
|
||
const abnormalDialogVisible = ref(false);
|
||
const abnormalList = ref([]);
|
||
|
||
// 在 data 中添加新的响应式变量
|
||
const currentAbnormalUser = ref({
|
||
id: '',
|
||
userid: ''
|
||
});
|
||
|
||
// 获取异常类型标签
|
||
const getAbnormalTypeLabel = (type) => {
|
||
const typeMap = {
|
||
'abnormal': '考勤异常',
|
||
'checkin': '签到签退',
|
||
'report': '日志异常'
|
||
};
|
||
return typeMap[type] || '未知';
|
||
};
|
||
|
||
// 获取异常类型标签样式
|
||
const getAbnormalTypeTag = (type) => {
|
||
const typeMap = {
|
||
'abnormal': 'danger',
|
||
'checkin': 'warning',
|
||
'report': 'info'
|
||
};
|
||
return typeMap[type] || '';
|
||
};
|
||
|
||
// 获取异常结果标签
|
||
const getAbnormalResultLabel = (result) => {
|
||
const resultMap = {
|
||
'Normal': '正常',
|
||
'Early': '早退',
|
||
'Late': '迟到',
|
||
'SeriousLate': '严重迟到',
|
||
'Absenteeism': '旷工迟到',
|
||
'NotSigned': '未打卡'
|
||
};
|
||
return resultMap[result] || '未知';
|
||
};
|
||
|
||
// 获取异常结果标签样式
|
||
const getAbnormalResultTag = (result) => {
|
||
const resultMap = {
|
||
'Normal': 'success',
|
||
'Early': 'warning',
|
||
'Late': 'warning',
|
||
'SeriousLate': 'danger',
|
||
'Absenteeism': 'danger',
|
||
'NotSigned': 'danger'
|
||
};
|
||
return resultMap[result] || '';
|
||
};
|
||
|
||
// 获取修改记录的值显示
|
||
const getModificationValue = (value, type) => {
|
||
if (type === 'abnormal') {
|
||
const resultMap = {
|
||
'Normal': '正常',
|
||
'Early': '早退',
|
||
'Late': '迟到',
|
||
'SeriousLate': '严重迟到',
|
||
'Absenteeism': '旷工迟到',
|
||
'NotSigned': '未打卡'
|
||
};
|
||
return resultMap[value] || value;
|
||
} else if (type === 'checkin') {
|
||
return `${value}次`;
|
||
} else if (type === 'report') {
|
||
return value == 0 ? '未写日志' : '正常';
|
||
}
|
||
return value;
|
||
};
|
||
|
||
// 修改查看异常考勤方法
|
||
const handleViewAbnormal = async (row) => {
|
||
try {
|
||
const year = dateRange.value.getFullYear();
|
||
const month = dateRange.value.getMonth() + 1;
|
||
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
|
||
if (res.errcode === 0) {
|
||
currentAbnormalUser.value = {
|
||
id: res.result.id,
|
||
userid: res.result.userid
|
||
};
|
||
abnormalList.value = res.result.data;
|
||
abnormalDialogVisible.value = true;
|
||
} else {
|
||
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error("获取异常考勤数据失败");
|
||
}
|
||
};
|
||
|
||
// 修改异常考勤
|
||
const handleUpdateAbnormal = async (record) => {
|
||
// 保存原始值
|
||
const originalValue = record.realResult;
|
||
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'确认修改该考勤记录?',
|
||
'提示',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
);
|
||
|
||
const year = dateRange.value.getFullYear();
|
||
const month = dateRange.value.getMonth() + 1;
|
||
const data = {
|
||
id: currentAbnormalUser.value.id,
|
||
userid: currentAbnormalUser.value.userid,
|
||
year,
|
||
month,
|
||
data: abnormalList.value
|
||
};
|
||
const res = await updateAbnormalAttendance(data);
|
||
if (res.errcode === 0) {
|
||
ElMessage.success("修改成功");
|
||
fetchList()
|
||
// 重新获取异常考勤数据
|
||
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
|
||
if (detailRes.errcode === 0) {
|
||
currentAbnormalUser.value = {
|
||
id: detailRes.result.id,
|
||
userid: detailRes.result.userid
|
||
};
|
||
abnormalList.value = detailRes.result.data;
|
||
}
|
||
} else {
|
||
// 如果修改失败,恢复原值
|
||
record.realResult = originalValue;
|
||
ElMessage.error(res.errmsg || "修改失败");
|
||
}
|
||
} catch (error) {
|
||
if (error === 'cancel') {
|
||
// 用户取消时恢复原值并刷新列表
|
||
record.realResult = originalValue;
|
||
const year = dateRange.value.getFullYear();
|
||
const month = dateRange.value.getMonth() + 1;
|
||
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
|
||
if (detailRes.errcode === 0) {
|
||
currentAbnormalUser.value = {
|
||
id: detailRes.result.id,
|
||
userid: detailRes.result.userid
|
||
};
|
||
abnormalList.value = detailRes.result.data;
|
||
}
|
||
ElMessage.info('已取消修改');
|
||
} else {
|
||
// 其他错误时也恢复原值
|
||
record.realResult = originalValue;
|
||
ElMessage.error("修改失败");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 页面加载时设置默认日期范围并获取数据
|
||
onMounted(() => {
|
||
setDefaultDateRange();
|
||
fetchList();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.attendance-reviewed-container {
|
||
height: calc(100vh - 120px);
|
||
padding: 20px;
|
||
background-color: var(--el-bg-color-page);
|
||
|
||
:deep(.el-card) {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-radius: 8px;
|
||
box-shadow: var(--el-box-shadow-light);
|
||
|
||
.el-card__body {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
flex: 1;
|
||
|
||
.el-table__body-wrapper {
|
||
height: 100%;
|
||
}
|
||
|
||
.el-table__cell {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.el-table__header {
|
||
th {
|
||
background-color: #f5f7fa;
|
||
font-weight: bold;
|
||
color: #606266;
|
||
height: 48px;
|
||
padding: 12px 0;
|
||
}
|
||
}
|
||
|
||
.el-table__row {
|
||
height: 56px;
|
||
}
|
||
|
||
.el-table__fixed-right {
|
||
height: 100% !important;
|
||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.el-table__fixed {
|
||
height: 100% !important;
|
||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--el-border-color-light);
|
||
background-color: var(--el-bg-color);
|
||
|
||
.title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding: 20px;
|
||
}
|
||
|
||
:deep(.el-dialog__header) {
|
||
margin: 0;
|
||
padding: 20px;
|
||
border-bottom: 1px solid var(--el-border-color-light);
|
||
background-color: var(--el-bg-color);
|
||
}
|
||
|
||
:deep(.el-dialog__footer) {
|
||
padding: 20px;
|
||
border-top: 1px solid var(--el-border-color-light);
|
||
background-color: var(--el-bg-color);
|
||
}
|
||
|
||
:deep(.el-dialog) {
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.abnormal-date-group {
|
||
margin-bottom: 20px;
|
||
|
||
.date-header {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
padding: 8px;
|
||
background-color: #f5f7fa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.modification-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
position: relative;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.finally-tag {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background-color: #67c23a;
|
||
color: white;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.result-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 4px;
|
||
}
|
||
</style> |