| | |
| | | <div class="label">当前时间</div> |
| | | <div class="value">{{ nowTime }}</div> |
| | | </div> |
| | | <el-button type="primary" size="large" @click="handleCheckInOut"> |
| | | <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="员工姓名"> |
| | | {{ currentUser.name }} |
| | | {{ todayRecord.staffName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="工号"> |
| | | {{ currentUser.no }} |
| | | {{ todayRecord.staffNo }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="所属部门"> |
| | | {{ currentUser.dept }} |
| | | {{ todayRecord.deptName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="今日状态"> |
| | | <el-tag :type="todayStatusTag" size="small"> |
| | |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="上班时间"> |
| | | {{ todayRecord?.checkInTime || '-' }} |
| | | {{ todayRecord?.workStartAt || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="下班时间"> |
| | | {{ todayRecord?.checkOutTime || '-' }} |
| | | {{ todayRecord?.workEndAt || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="工时(小时)"> |
| | | {{ todayRecord?.workHours ?? '-' }} |
| | |
| | | <div> |
| | | <span class="search_title">部门:</span> |
| | | <el-select |
| | | v-model="searchForm.dept" |
| | | v-model="searchForm.deptId" |
| | | placeholder="请选择部门" |
| | | style="width: 180px" |
| | | clearable |
| | |
| | | clearable |
| | | /> |
| | | |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> |
| | | <el-button type="primary" @click="fetchData" style="margin-left: 10px"> |
| | | 搜索 |
| | | </el-button> |
| | | <el-button @click="resetSearch">重置</el-button> |
| | |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%" |
| | | height="calc(100vh - 24em)" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="dept" |
| | | prop="deptName" |
| | | label="部门" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="name" |
| | | prop="staffName" |
| | | label="姓名" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="no" |
| | | prop="staffNo" |
| | | label="工号" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="checkInTime" |
| | | prop="workStartAt" |
| | | label="上班时间" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="checkOutTime" |
| | | prop="workEndAt" |
| | | label="下班时间" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="workHours" |
| | | label="工时(小时)" |
| | | width="110" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="statusText" |
| | | prop="status" |
| | | label="考勤状态" |
| | | width="120" |
| | | align="center" |
| | | > |
| | | <template #default="scope"> |
| | | <el-tag |
| | | v-if="scope.row.status === 'normal'" |
| | | v-if="scope.row.status === 0" |
| | | type="success" |
| | | size="small" |
| | | > |
| | |
| | | type="danger" |
| | | size="small" |
| | | > |
| | | {{ scope.row.statusText }} |
| | | {{ scope.row.status === 1 ? '迟到' : '早退' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | 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 } from "element-plus"; |
| | | import { |
| | | createPersonalAttendanceRecord, |
| | | findPersonalAttendanceRecords, findTodayPersonalAttendanceRecord |
| | | } from "@/api/personnelManagement/personalAttendanceRecords.js"; |
| | | import Pagination from "@/components/Pagination/index.vue"; |
| | | |
| | | // 模拟当前登录员工 |
| | | const currentUser = reactive({ |
| | | id: 1, |
| | | name: "张三", |
| | | no: "E10001", |
| | | dept: "生产一部", |
| | | }); |
| | | const tableLoading = ref(false) |
| | | // 分页参数 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0 |
| | | }) |
| | | // 今日数据 |
| | | const todayRecord = ref({}) |
| | | |
| | | // 部门选项 |
| | | const deptOptions = [ |
| | |
| | | { 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: "", |
| | | deptId: "", |
| | | date: "", |
| | | }); |
| | | |
| | |
| | | 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) { |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "上班打卡"; |
| | | } |
| | | if (!todayRecord.value.checkOutTime) { |
| | | if (!todayRecord.value.workEndAt) { |
| | | return "下班打卡"; |
| | | } |
| | | return "今日已打卡完成"; |
| | |
| | | |
| | | // 今日状态展示 |
| | | const todayStatusTag = computed(() => { |
| | | if (!todayRecord.value) return "info"; |
| | | if (todayRecord.value.status === "normal") return "success"; |
| | | if (!todayRecord.value.id) return "info"; |
| | | if (todayRecord.value.status === 0) return "success"; |
| | | return "danger"; |
| | | }); |
| | | |
| | | const todayStatusText = computed(() => { |
| | | if (!todayRecord.value) return "未打卡"; |
| | | return todayRecord.value.statusText || "正常"; |
| | | if (!todayRecord.value.id) return "未打卡"; |
| | | switch (todayRecord.value.status) { |
| | | case 0: |
| | | return "正常" |
| | | case 1: |
| | | return "迟到" |
| | | case 2: |
| | | return "早退" |
| | | } |
| | | }); |
| | | |
| | | // 行样式:异常高亮 |
| | | const rowClassName = ({ row }) => { |
| | | if (row.status === "late" || row.status === "early") { |
| | | if (row.status === 1 || row.status === 2) { |
| | | 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; |
| | | 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; |
| | | }); |
| | | tableData.value = list; |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | recomputeTable(); |
| | | // 查询今日打卡信息 |
| | | const fetchTodayData = () => { |
| | | findTodayPersonalAttendanceRecord({}).then((res) => { |
| | | todayRecord.value = res.data; |
| | | }) |
| | | }; |
| | | |
| | | const paginationChange = (pagination) => { |
| | | page.current = pagination.page; |
| | | page.size = pagination.limit; |
| | | fetchData(); |
| | | } |
| | | |
| | | const resetSearch = () => { |
| | | searchForm.dept = ""; |
| | | searchForm.deptId = ""; |
| | | searchForm.date = ""; |
| | | recomputeTable(); |
| | | fetchData(); |
| | | }; |
| | | |
| | | // 导出(演示) |
| | |
| | | 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(); |
| | | createPersonalAttendanceRecord({}).then((res) => { |
| | | fetchData() |
| | | ElMessage.success("打卡成功!"); |
| | | }) |
| | | }; |
| | | |
| | | onMounted(() => { |
| | |
| | | const M = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const D = String(today.getDate()).padStart(2, "0"); |
| | | searchForm.date = `${Y}-${M}-${D}`; |
| | | recomputeTable(); |
| | | fetchData(); |
| | | fetchTodayData() |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |