<template>
|
<div class="app-container">
|
<!-- 员工打卡区 -->
|
<el-card shadow="never"
|
class="mb16">
|
<div class="attendance-header">
|
<div>
|
<div class="title">打卡签到</div>
|
<div class="sub-title">支持一键打卡,自动记录上下班时间</div>
|
</div>
|
<div class="attendance-actions">
|
<div class="time-block">
|
<div class="label">当前时间</div>
|
<div class="value">{{ nowTime }}</div>
|
</div>
|
<el-button type="primary"
|
size="large"
|
@click="handleCheckInOut"
|
:disabled="todayRecord.workEndAt">
|
{{ checkInOutText }}
|
</el-button>
|
</div>
|
</div>
|
<el-descriptions border
|
:column="4"
|
class="mt10">
|
<el-descriptions-item label="员工姓名">
|
{{ todayRecord.staffName }}
|
</el-descriptions-item>
|
<el-descriptions-item label="工号">
|
{{ todayRecord.staffNo }}
|
</el-descriptions-item>
|
<el-descriptions-item label="所属部门">
|
{{ todayRecord.deptName }}
|
</el-descriptions-item>
|
<el-descriptions-item label="今日状态">
|
<el-tag :type="todayStatusTag"
|
size="small">
|
{{ todayStatusText }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="上班时间">
|
{{ todayRecord?.workStartAt || '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="下班时间">
|
{{ todayRecord?.workEndAt || '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="工时(小时)">
|
{{ todayRecord?.workHours ?? '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="异常标记">
|
<span v-if="!todayRecord.id || todayRecord?.status === 0">-</span>
|
<el-tag v-else
|
type="danger"
|
size="small">
|
{{ todayRecord?.status ? getStatusText(todayRecord.status) : '-' }}
|
</el-tag>
|
</el-descriptions-item>
|
</el-descriptions>
|
</el-card>
|
<div class="attendance-operation">
|
<!-- 查询条件(管理员考勤日报) -->
|
<el-form :model="searchForm"
|
:inline="true"
|
class="search-form">
|
<el-form-item label="部门:"
|
prop="deptId">
|
<el-tree-select v-model="searchForm.deptId"
|
:data="deptOptions"
|
:props="{ value: 'id', label: 'label', children: 'children' }"
|
value-key="id"
|
placeholder="请选择部门"
|
check-strictly
|
style="width: 200px" />
|
</el-form-item>
|
<el-form-item label="日期:"
|
prop="date">
|
<el-date-picker v-model="searchForm.date"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择日期"
|
clearable />
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary"
|
@click="fetchData">
|
<el-icon>
|
<Search />
|
</el-icon>
|
搜索
|
</el-button>
|
<el-button @click="resetSearch">
|
<el-icon>
|
<Refresh />
|
</el-icon>
|
重置
|
</el-button>
|
</el-form-item>
|
</el-form>
|
<el-button icon="Download"
|
@click="handleExport">
|
导出考勤日报
|
</el-button>
|
</div>
|
<!-- 考勤日报表格 -->
|
<div class="table_list">
|
<el-table :data="tableData"
|
border
|
v-loading="tableLoading"
|
style="width: 100%"
|
height="calc(100vh - 24em)"
|
:header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
|
:row-class-name="rowClassName">
|
<el-table-column type="index"
|
label="序号"
|
width="60"
|
align="center" />
|
<el-table-column prop="date"
|
label="日期"
|
width="120" />
|
<el-table-column prop="deptName"
|
label="部门"
|
width="140" />
|
<el-table-column prop="staffName"
|
label="姓名"
|
width="120" />
|
<el-table-column prop="staffNo"
|
label="工号"
|
width="120" />
|
<el-table-column prop="workStartAt"
|
label="上班时间"
|
width="140" />
|
<el-table-column prop="workEndAt"
|
label="下班时间"
|
width="140" />
|
<el-table-column prop="workHours"
|
label="工时(小时)"
|
align="center" />
|
<el-table-column prop="status"
|
label="考勤状态"
|
align="center">
|
<template #default="scope">
|
<el-tag v-if="scope.row.status === 0"
|
type="success"
|
size="small">
|
正常
|
</el-tag>
|
<el-tag v-else
|
type="danger"
|
size="small">
|
{{ scope.row.status === 1 ? '迟到' : scope.row.status === 2 ? '早退' : '迟到、早退' }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="remark"
|
label="备注"
|
show-overflow-tooltip />
|
</el-table>
|
<pagination :total="total"
|
layout="total, sizes, prev, pager, next, jumper"
|
:page="page.current"
|
:limit="page.size"
|
@pagination="paginationChange" />
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import {
|
createPersonalAttendanceRecord,
|
findPersonalAttendanceRecords,
|
findTodayPersonalAttendanceRecord,
|
} from "@/api/personnelManagement/personalAttendanceRecords.js";
|
import Pagination from "@/components/Pagination/index.vue";
|
import { deptTreeSelect } from "@/api/system/user.js";
|
import { Refresh, Search } from "@element-plus/icons-vue";
|
|
const { proxy } = getCurrentInstance();
|
const tableLoading = ref(false);
|
// 分页参数
|
const page = reactive({
|
current: 1,
|
size: 10,
|
total: 0,
|
});
|
// 今日数据
|
const todayRecord = ref({});
|
|
// 部门选项
|
const deptOptions = ref([]);
|
|
// 查询表单
|
const searchForm = reactive({
|
deptId: "",
|
date: "",
|
});
|
|
// 表格数据
|
const tableData = ref([]);
|
|
// 当前时间展示
|
const nowTime = ref("");
|
let timer = null;
|
|
const updateNowTime = () => {
|
const now = new Date();
|
const Y = now.getFullYear();
|
const M = String(now.getMonth() + 1).padStart(2, "0");
|
const D = String(now.getDate()).padStart(2, "0");
|
const h = String(now.getHours()).padStart(2, "0");
|
const m = String(now.getMinutes()).padStart(2, "0");
|
const s = String(now.getSeconds()).padStart(2, "0");
|
nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
|
};
|
|
// 打卡按钮文本
|
const checkInOutText = computed(() => {
|
if (!todayRecord.value || !todayRecord.value.workStartAt) {
|
return "上班打卡";
|
}
|
if (!todayRecord.value.workEndAt) {
|
return "下班打卡";
|
}
|
return "今日已打卡完成";
|
});
|
|
// 今日状态展示
|
const todayStatusTag = computed(() => {
|
if (!todayRecord.value.id) return "info";
|
if (todayRecord.value.status === 0) return "success";
|
return "danger";
|
});
|
const getStatusText = status => {
|
switch (status) {
|
case 0:
|
return "正常";
|
case 1:
|
return "迟到";
|
case 2:
|
return "早退";
|
case 3:
|
return "迟到、早退";
|
case 4:
|
return "缺勤";
|
}
|
};
|
|
const todayStatusText = computed(() => {
|
if (!todayRecord.value.id) return "未打卡";
|
switch (todayRecord.value.status) {
|
case 0:
|
return "正常";
|
case 1:
|
return "迟到";
|
case 2:
|
return "早退";
|
case 3:
|
return "迟到、早退";
|
case 4:
|
return "缺勤";
|
}
|
});
|
|
// 行样式:异常高亮
|
const rowClassName = ({ row }) => {
|
if (row.status === 1 || row.status === 2) {
|
return "row-abnormal";
|
}
|
return "";
|
};
|
|
// 查询部门列表
|
const fetchDeptOptions = () => {
|
deptTreeSelect().then(response => {
|
deptOptions.value = filterDisabledDept(
|
JSON.parse(JSON.stringify(response.data))
|
);
|
});
|
};
|
|
/** 过滤禁用的部门 */
|
function filterDisabledDept(deptList) {
|
return deptList.filter(dept => {
|
if (dept.disabled) {
|
return false;
|
}
|
if (dept.children && dept.children.length) {
|
dept.children = filterDisabledDept(dept.children);
|
}
|
return true;
|
});
|
}
|
|
// 查询
|
const fetchData = () => {
|
tableLoading.value = true;
|
findPersonalAttendanceRecords({ ...page, ...searchForm })
|
.then(res => {
|
tableData.value = res.data.records;
|
page.value.total = res.data.total;
|
})
|
.finally(() => {
|
tableLoading.value = false;
|
});
|
};
|
|
// 查询今日打卡信息
|
const fetchTodayData = () => {
|
findTodayPersonalAttendanceRecord({}).then(res => {
|
todayRecord.value = res.data;
|
});
|
};
|
|
const paginationChange = pagination => {
|
page.current = pagination.page;
|
page.size = pagination.limit;
|
fetchData();
|
};
|
|
const resetSearch = () => {
|
searchForm.deptId = "";
|
searchForm.date = "";
|
fetchData();
|
};
|
|
const handleExport = () => {
|
ElMessageBox.confirm("是否确认导出?", "导出", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => {
|
proxy.download("/personalAttendanceRecords/export", {}, "考勤记录.xlsx");
|
})
|
.catch(() => {
|
proxy.$modal.msg("已取消");
|
});
|
};
|
|
// 打卡
|
const handleCheckInOut = () => {
|
createPersonalAttendanceRecord({}).then(res => {
|
fetchData();
|
fetchTodayData();
|
ElMessage.success("打卡成功!");
|
});
|
};
|
|
onMounted(() => {
|
updateNowTime();
|
timer = setInterval(updateNowTime, 1000);
|
// 默认展示当天数据
|
const today = new Date();
|
const Y = today.getFullYear();
|
const M = String(today.getMonth() + 1).padStart(2, "0");
|
const D = String(today.getDate()).padStart(2, "0");
|
searchForm.date = `${Y}-${M}-${D}`;
|
fetchData();
|
fetchTodayData();
|
fetchDeptOptions();
|
});
|
|
onBeforeUnmount(() => {
|
if (timer) {
|
clearInterval(timer);
|
}
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.mb16 {
|
margin-bottom: 16px;
|
}
|
|
.attendance-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.attendance-header .title {
|
font-size: 18px;
|
font-weight: 600;
|
margin-bottom: 4px;
|
}
|
|
.attendance-header .sub-title {
|
font-size: 13px;
|
color: #909399;
|
}
|
|
.attendance-actions {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
}
|
|
.time-block {
|
text-align: right;
|
}
|
|
.time-block .label {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.time-block .value {
|
font-size: 18px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
::v-deep(.row-abnormal) {
|
background-color: #fff5f5;
|
}
|
|
.attendance-operation {
|
display: flex;
|
justify-content: space-between;
|
}
|
</style>
|