| | |
| | | <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="title">打卡签到 |
| | | </div> |
| | | <div class="sub-title">支持一键打卡,自动记录上下班时间</div> |
| | | </div> |
| | | <div class="attendance-actions"> |
| | |
| | | <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 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 :type="todayStatusTag" |
| | | size="small"> |
| | | {{ todayStatusText }} |
| | | </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 ?? '-' }} |
| | | </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.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 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 |
| | | 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" |
| | | > |
| | | <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 === 'normal'" |
| | | 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.statusText }} |
| | | <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" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | 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 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; |
| | | const { proxy } = getCurrentInstance(); |
| | | const tableLoading = ref(false); |
| | | // 分页参数 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | tableData.value = list; |
| | | }; |
| | | // 今日数据 |
| | | const todayRecord = ref({}); |
| | | |
| | | const handleQuery = () => { |
| | | recomputeTable(); |
| | | }; |
| | | // 部门选项 |
| | | const deptOptions = ref([]); |
| | | |
| | | const resetSearch = () => { |
| | | searchForm.dept = ""; |
| | | searchForm.date = ""; |
| | | recomputeTable(); |
| | | }; |
| | | // 查询表单 |
| | | const searchForm = reactive({ |
| | | deptId: "", |
| | | date: "", |
| | | }); |
| | | |
| | | // 导出(演示) |
| | | const handleExport = () => { |
| | | ElMessage.success("当前为演示页面,导出功能未对接实际接口"); |
| | | }; |
| | | // 表格数据 |
| | | const tableData = ref([]); |
| | | |
| | | // 打卡逻辑(仅前端模拟) |
| | | const handleCheckInOut = () => { |
| | | const [dateStr, timeStr] = nowTime.value.split(" "); |
| | | if (!dateStr || !timeStr) return; |
| | | // 当前时间展示 |
| | | const nowTime = ref(""); |
| | | let timer = null; |
| | | |
| | | // 上班打卡 |
| | | 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)); |
| | | 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}`; |
| | | }; |
| | | |
| | | // 早退判断:18:00 前离开视为早退(只示意) |
| | | if (timeStr < "18:00:00") { |
| | | todayRecord.value.status = "early"; |
| | | todayRecord.value.statusText = "早退"; |
| | | } else if (todayRecord.value.status === "normal") { |
| | | todayRecord.value.statusText = "正常"; |
| | | // 打卡按钮文本 |
| | | const checkInOutText = computed(() => { |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "上班打卡"; |
| | | } |
| | | ElMessage.success("下班打卡成功"); |
| | | } else { |
| | | ElMessage.info("今日已完成上下班打卡"); |
| | | 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; |
| | | }); |
| | | } |
| | | |
| | | recomputeTable(); |
| | | }; |
| | | // 查询 |
| | | 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; |
| | | }); |
| | | }; |
| | | |
| | | 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(); |
| | | }); |
| | | // 查询今日打卡信息 |
| | | const fetchTodayData = () => { |
| | | findTodayPersonalAttendanceRecord({}).then(res => { |
| | | todayRecord.value = res.data; |
| | | }); |
| | | }; |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }); |
| | | 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 getCurrentLocation = () => { |
| | | return new Promise((resolve, reject) => { |
| | | if (navigator.geolocation) { |
| | | navigator.geolocation.getCurrentPosition( |
| | | position => { |
| | | const { longitude, latitude } = position.coords; |
| | | resolve({ longitude, latitude }); |
| | | }, |
| | | error => { |
| | | console.log("获取位置失败:", error); |
| | | reject(error); |
| | | } |
| | | ); |
| | | } else { |
| | | reject(new Error("浏览器不支持地理定位")); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 打卡 |
| | | const handleCheckInOut = () => { |
| | | getCurrentLocation() |
| | | .then(location => { |
| | | createPersonalAttendanceRecord(location).then(res => { |
| | | fetchData(); |
| | | fetchTodayData(); |
| | | ElMessage.success("打卡成功!"); |
| | | }); |
| | | }) |
| | | .catch(error => { |
| | | // 获取位置失败时,仍允许打卡 |
| | | ElMessage.warning("获取位置失败,将使用默认位置打卡"); |
| | | 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> |
| | | |