<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">
|
{{ checkInOutText }}
|
</el-button>
|
</div>
|
</div>
|
<el-descriptions border :column="4" class="mt10">
|
<el-descriptions-item label="员工姓名">
|
{{ currentUser.name }}
|
</el-descriptions-item>
|
<el-descriptions-item label="工号">
|
{{ currentUser.no }}
|
</el-descriptions-item>
|
<el-descriptions-item label="所属部门">
|
{{ currentUser.dept }}
|
</el-descriptions-item>
|
<el-descriptions-item label="今日状态">
|
<el-tag :type="todayStatusTag" size="small">
|
{{ todayStatusText }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="上班时间">
|
{{ todayRecord?.checkInTime || '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="下班时间">
|
{{ todayRecord?.checkOutTime || '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="工时(小时)">
|
{{ todayRecord?.workHours ?? '-' }}
|
</el-descriptions-item>
|
<el-descriptions-item label="异常标记">
|
<span v-if="todayRecord?.status === 'normal'">-</span>
|
<el-tag v-else type="danger" size="small">
|
{{ todayRecord?.statusText }}
|
</el-tag>
|
</el-descriptions-item>
|
</el-descriptions>
|
</el-card>
|
|
<!-- 查询条件(管理员考勤日报) -->
|
<div class="search_form">
|
<div>
|
<span class="search_title">部门:</span>
|
<el-select
|
v-model="searchForm.dept"
|
placeholder="请选择部门"
|
style="width: 180px"
|
clearable
|
>
|
<el-option
|
v-for="item in deptOptions"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value"
|
/>
|
</el-select>
|
|
<span class="search_title ml10">日期:</span>
|
<el-date-picker
|
v-model="searchForm.date"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择日期"
|
clearable
|
/>
|
|
<el-button type="primary" @click="handleQuery" style="margin-left: 10px">
|
搜索
|
</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
</div>
|
<div>
|
<el-button icon="Download" @click="handleExport">
|
导出考勤日报
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 考勤日报表格 -->
|
<div class="table_list">
|
<el-table
|
:data="tableData"
|
border
|
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="dept"
|
label="部门"
|
width="140"
|
/>
|
<el-table-column
|
prop="name"
|
label="姓名"
|
width="120"
|
/>
|
<el-table-column
|
prop="no"
|
label="工号"
|
width="120"
|
/>
|
<el-table-column
|
prop="checkInTime"
|
label="上班时间"
|
width="140"
|
/>
|
<el-table-column
|
prop="checkOutTime"
|
label="下班时间"
|
width="140"
|
/>
|
<el-table-column
|
prop="workHours"
|
label="工时(小时)"
|
width="110"
|
align="center"
|
/>
|
<el-table-column
|
prop="statusText"
|
label="考勤状态"
|
width="120"
|
align="center"
|
>
|
<template #default="scope">
|
<el-tag
|
v-if="scope.row.status === 'normal'"
|
type="success"
|
size="small"
|
>
|
正常
|
</el-tag>
|
<el-tag
|
v-else
|
type="danger"
|
size="small"
|
>
|
{{ scope.row.statusText }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column
|
prop="remark"
|
label="备注"
|
show-overflow-tooltip
|
/>
|
</el-table>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
|
import { ElMessage } from "element-plus";
|
|
// 模拟当前登录员工
|
const currentUser = reactive({
|
id: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
});
|
|
// 部门选项
|
const deptOptions = [
|
{ label: "生产一部", value: "生产一部" },
|
{ label: "生产二部", value: "生产二部" },
|
{ label: "设备维护部", value: "设备维护部" },
|
{ label: "质检部", value: "质检部" },
|
];
|
|
// 模拟考勤原始数据
|
const rawAttendance = ref([
|
{
|
id: 1,
|
date: "2024-12-01",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:58",
|
checkOutTime: "18:10",
|
workHours: 9.2,
|
status: "normal",
|
statusText: "正常",
|
remark: "",
|
},
|
{
|
id: 2,
|
date: "2024-12-01",
|
userId: 2,
|
name: "李四",
|
no: "E10002",
|
dept: "生产一部",
|
checkInTime: "09:15",
|
checkOutTime: "18:05",
|
workHours: 8.8,
|
status: "late",
|
statusText: "迟到",
|
remark: "因交通拥堵迟到",
|
},
|
{
|
id: 3,
|
date: "2024-12-01",
|
userId: 3,
|
name: "王五",
|
no: "E20001",
|
dept: "设备维护部",
|
checkInTime: "08:50",
|
checkOutTime: "17:20",
|
workHours: 8.5,
|
status: "early",
|
statusText: "早退",
|
remark: "外出处理紧急故障",
|
},
|
{
|
id: 4,
|
date: "2024-12-02",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:45",
|
checkOutTime: "18:30",
|
workHours: 9.7,
|
status: "normal",
|
statusText: "正常",
|
remark: "加班0.5小时",
|
},
|
]);
|
|
// 查询表单
|
const searchForm = reactive({
|
dept: "",
|
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 todayStr = computed(() => nowTime.value.slice(0, 10));
|
|
// 当日当前员工考勤记录
|
const todayRecord = computed(() =>
|
rawAttendance.value.find(
|
(item) =>
|
item.userId === currentUser.id && item.date === todayStr.value
|
)
|
);
|
|
// 打卡按钮文本
|
const checkInOutText = computed(() => {
|
if (!todayRecord.value || !todayRecord.value.checkInTime) {
|
return "上班打卡";
|
}
|
if (!todayRecord.value.checkOutTime) {
|
return "下班打卡";
|
}
|
return "今日已打卡完成";
|
});
|
|
// 今日状态展示
|
const todayStatusTag = computed(() => {
|
if (!todayRecord.value) return "info";
|
if (todayRecord.value.status === "normal") return "success";
|
return "danger";
|
});
|
|
const todayStatusText = computed(() => {
|
if (!todayRecord.value) return "未打卡";
|
return todayRecord.value.statusText || "正常";
|
});
|
|
// 行样式:异常高亮
|
const rowClassName = ({ row }) => {
|
if (row.status === "late" || row.status === "early") {
|
return "row-abnormal";
|
}
|
return "";
|
};
|
|
// 查询
|
const recomputeTable = () => {
|
const list = rawAttendance.value.filter((item) => {
|
if (searchForm.dept && item.dept !== searchForm.dept) {
|
return false;
|
}
|
if (searchForm.date && item.date !== searchForm.date) {
|
return false;
|
}
|
return true;
|
});
|
tableData.value = list;
|
};
|
|
const handleQuery = () => {
|
recomputeTable();
|
};
|
|
const resetSearch = () => {
|
searchForm.dept = "";
|
searchForm.date = "";
|
recomputeTable();
|
};
|
|
// 导出(演示)
|
const handleExport = () => {
|
ElMessage.success("当前为演示页面,导出功能未对接实际接口");
|
};
|
|
// 打卡逻辑(仅前端模拟)
|
const handleCheckInOut = () => {
|
const [dateStr, timeStr] = nowTime.value.split(" ");
|
if (!dateStr || !timeStr) return;
|
|
// 上班打卡
|
if (!todayRecord.value) {
|
const newId = rawAttendance.value.length
|
? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
|
: 1;
|
const status =
|
timeStr > "09:00:00" ? "late" : "normal";
|
const statusText = status === "late" ? "迟到" : "正常";
|
rawAttendance.value.push({
|
id: newId,
|
date: dateStr,
|
userId: currentUser.id,
|
name: currentUser.name,
|
no: currentUser.no,
|
dept: currentUser.dept,
|
checkInTime: timeStr.slice(0, 5),
|
checkOutTime: "",
|
workHours: null,
|
status,
|
statusText,
|
remark: "",
|
});
|
ElMessage.success("上班打卡成功");
|
} else if (!todayRecord.value.checkOutTime) {
|
// 下班打卡
|
todayRecord.value.checkOutTime = timeStr.slice(0, 5);
|
// 简单按 9:00-18:00 计算工时
|
const start = todayRecord.value.checkInTime || "09:00";
|
const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
|
const [eh, em] = todayRecord.value.checkOutTime
|
.split(":")
|
.map((v) => parseInt(v, 10));
|
const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
|
todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
|
|
// 早退判断:18:00 前离开视为早退(只示意)
|
if (timeStr < "18:00:00") {
|
todayRecord.value.status = "early";
|
todayRecord.value.statusText = "早退";
|
} else if (todayRecord.value.status === "normal") {
|
todayRecord.value.statusText = "正常";
|
}
|
ElMessage.success("下班打卡成功");
|
} else {
|
ElMessage.info("今日已完成上下班打卡");
|
}
|
|
recomputeTable();
|
};
|
|
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}`;
|
recomputeTable();
|
});
|
|
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;
|
}
|
</style>
|