| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 员工打卡区 --> |
| | | <el-card shadow="never" class="mb16"> |
| | | <el-card shadow="never" |
| | | class="mb16"> |
| | | <div class="attendance-header"> |
| | | <div> |
| | | <div class="title">打卡签到</div> |
| | |
| | | <div class="label">当前时间</div> |
| | | <div class="value">{{ nowTime }}</div> |
| | | </div> |
| | | <el-button type="primary" size="large" @click="handleCheckInOut" :disabled="todayRecord.workEndAt"> |
| | | <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 border |
| | | :column="4" |
| | | class="mt10"> |
| | | <el-descriptions-item label="员工姓名"> |
| | | {{ todayRecord.staffName }} |
| | | </el-descriptions-item> |
| | |
| | | {{ todayRecord.deptName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="今日状态"> |
| | | <el-tag :type="todayStatusTag" size="small"> |
| | | <el-tag :type="todayStatusTag" |
| | | size="small"> |
| | | {{ todayStatusText }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | |
| | | {{ 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 }} |
| | | <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="search_form"> |
| | | <div> |
| | | <span class="search_title">部门:</span> |
| | | <el-select |
| | | v-model="searchForm.deptId" |
| | | 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="fetchData" style="margin-left: 10px"> |
| | | 搜索 |
| | | </el-button> |
| | | <el-button @click="resetSearch">重置</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button icon="Download" @click="handleExport"> |
| | | 导出考勤日报 |
| | | </el-button> |
| | | </div> |
| | | <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" |
| | | > |
| | | <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 v-if="scope.row.status === 0" |
| | | type="success" |
| | | size="small"> |
| | | 正常 |
| | | </el-tag> |
| | | <el-tag |
| | | v-else |
| | | type="danger" |
| | | size="small" |
| | | > |
| | | {{ scope.row.status === 1 ? '迟到' : '早退' }} |
| | | <el-tag v-else |
| | | type="danger" |
| | | size="small"> |
| | | <!-- {{ scope.row.status === 1 ? '迟到' : scope.row.status === 2 ? '早退' : '迟到、早退' }} --> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="remark" |
| | | label="备注" |
| | | show-overflow-tooltip |
| | | /> |
| | | <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" /> |
| | | <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"; |
| | | 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 tableLoading = ref(false) |
| | | // 分页参数 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0 |
| | | }) |
| | | // 今日数据 |
| | | const todayRecord = ref({}) |
| | | |
| | | // 部门选项 |
| | | const deptOptions = [ |
| | | { label: "生产一部", value: "生产一部" }, |
| | | { label: "生产二部", value: "生产二部" }, |
| | | { label: "设备维护部", value: "设备维护部" }, |
| | | { label: "质检部", value: "质检部" }, |
| | | ]; |
| | | |
| | | // 查询表单 |
| | | 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 todayStatusText = computed(() => { |
| | | if (!todayRecord.value.id) return "未打卡"; |
| | | switch (todayRecord.value.status) { |
| | | case 0: |
| | | return "正常" |
| | | case 1: |
| | | return "迟到" |
| | | case 2: |
| | | return "早退" |
| | | } |
| | | }); |
| | | |
| | | // 行样式:异常高亮 |
| | | const rowClassName = ({ row }) => { |
| | | if (row.status === 1 || row.status === 2) { |
| | | return "row-abnormal"; |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | // 查询 |
| | | 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 { proxy } = getCurrentInstance(); |
| | | const tableLoading = ref(false); |
| | | // 分页参数 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | }; |
| | | // 今日数据 |
| | | const todayRecord = ref({}); |
| | | |
| | | // 查询今日打卡信息 |
| | | const fetchTodayData = () => { |
| | | findTodayPersonalAttendanceRecord({}).then((res) => { |
| | | todayRecord.value = res.data; |
| | | }) |
| | | }; |
| | | // 部门选项 |
| | | const deptOptions = ref([]); |
| | | |
| | | const paginationChange = (pagination) => { |
| | | page.current = pagination.page; |
| | | page.size = pagination.limit; |
| | | fetchData(); |
| | | } |
| | | // 查询表单 |
| | | const searchForm = reactive({ |
| | | deptId: "", |
| | | date: "", |
| | | }); |
| | | |
| | | const resetSearch = () => { |
| | | searchForm.deptId = ""; |
| | | searchForm.date = ""; |
| | | fetchData(); |
| | | }; |
| | | // 表格数据 |
| | | const tableData = ref([]); |
| | | |
| | | // 导出(演示) |
| | | const handleExport = () => { |
| | | ElMessage.success("当前为演示页面,导出功能未对接实际接口"); |
| | | }; |
| | | // 当前时间展示 |
| | | const nowTime = ref(""); |
| | | let timer = null; |
| | | |
| | | // 打卡 |
| | | const handleCheckInOut = () => { |
| | | createPersonalAttendanceRecord({}).then((res) => { |
| | | fetchData() |
| | | fetchTodayData() |
| | | ElMessage.success("打卡成功!"); |
| | | }) |
| | | }; |
| | | 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}`; |
| | | }; |
| | | |
| | | 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() |
| | | }); |
| | | // 打卡按钮文本 |
| | | const checkInOutText = computed(() => { |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "上班打卡"; |
| | | } |
| | | if (!todayRecord.value.workEndAt) { |
| | | return "下班打卡"; |
| | | } |
| | | return "今日已打卡完成"; |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | // 今日状态展示 |
| | | 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; |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .attendance-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | .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 .title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .attendance-header .sub-title { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | .attendance-header .sub-title { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .attendance-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | .attendance-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .time-block { |
| | | text-align: right; |
| | | } |
| | | .time-block { |
| | | text-align: right; |
| | | } |
| | | |
| | | .time-block .label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | .time-block .label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .time-block .value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .time-block .value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | ::v-deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | ::v-deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | |
| | | .attendance-operation { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | </style> |
| | | |