feat(hrm): 考勤页面展示请假状态,打卡面板提示已请假
配套后端「请假 → 考勤」打通,展示与交互层改动:
1. api/hrm/attendance/record/index.ts
- AttendanceRecord 新增 leaveFlag / leaveHours / leaveTypes
- 新增 AttendanceMyStatus 接口与 getAttendanceMyStatus(date?)
2. views/hrm/attendance/record/data.ts
- 新增 getLeaveTypeLabel / formatLeaveTypeNames("1,2" -> "事假、病假")
- 列表新增「请假」列,搜索新增「是否请假」
- 打卡类型 4 处均用 DICT_TYPE.HRM_CLOCK_TYPE,无硬编码,无需改动
3. views/hrm/attendance/record/index.vue
- #leave slot:请假时渲染蓝色 Tag「事假 3.50h」,否则显示 -
4. views/hrm/attendance/record/modules/clock-panel.vue
- 打开弹窗拉 my-status;请假时 Alert 提示「今日已请假,如需上班可正常打卡」
- 打卡按钮保持可用(业务要求请假当天允许打卡)
- 成功后刷新状态;接口失败静默降级不阻断打卡
顺带修正 MES 批次属性字段名 workorderFlag -> workOrderFlag
(与后端 DO 的 workOrderFlag、数据库列 work_order_flag 对齐,
原先大小写不一致导致产品侧 7 个复选框勾选后存不进去、不回显)
| | |
| | | clockOutAddress?: string; |
| | | dataSource?: number; |
| | | dataSourceName?: string; |
| | | /** 当天是否请假(存在审批通过且与工作时段重叠的请假单) */ |
| | | leaveFlag?: boolean; |
| | | /** 当天请假工时(小时),未请假为 0 */ |
| | | leaveHours?: number; |
| | | /** 当天请假类型,逗号分隔的 HrmLeaveTypeEnum.type,如 "1,2" */ |
| | | leaveTypes?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** 我的考勤状态(/hrm/attendance/record/my-status) */ |
| | | export interface AttendanceMyStatus { |
| | | /** 员工姓名 */ |
| | | employeeName?: string; |
| | | userName?: string; |
| | | deptName?: string; |
| | | /** 考勤日期 */ |
| | | date?: string; |
| | | /** 服务器时间 */ |
| | | serverTime?: string; |
| | | /** 是否工作日 */ |
| | | workDay?: boolean; |
| | | /** 休息日原因,如:国庆节、周六 */ |
| | | restDayReason?: string; |
| | | /** 打卡地点名称 */ |
| | | locationName?: string; |
| | | locationAddress?: string; |
| | | /** 是否启用定位范围校验 */ |
| | | locationRequired?: boolean; |
| | | /** 允许打卡半径,单位米 */ |
| | | radiusMeters?: number; |
| | | /** 当日考勤记录,未打卡时为 null */ |
| | | record?: AttendanceRecord; |
| | | /** 今日记录ID */ |
| | | recordId?: number; |
| | | clockInTime?: string; |
| | | clockOutTime?: string; |
| | | clockInType?: number; |
| | | clockOutType?: number; |
| | | clockInDone?: boolean; |
| | | clockOutDone?: boolean; |
| | | workHours?: number; |
| | | overtimeHours?: number; |
| | | location?: string; |
| | | /** 当天是否请假 */ |
| | | onLeave?: boolean; |
| | | /** 当天请假工时(小时),未请假为 0 */ |
| | | leaveHours?: number; |
| | | /** 当天请假类型名称,多段用顿号分隔,如 "事假、病假" */ |
| | | leaveTypeNames?: string; |
| | | /** 当前可打卡状态 */ |
| | | clockable?: { |
| | | clockIn?: boolean; |
| | | clockOut?: boolean; |
| | | /** 下一步动作:in-上班打卡,out-下班打卡,none-无需打卡 */ |
| | | nextAction?: 'in' | 'none' | 'out' | string; |
| | | /** 提示文案 */ |
| | | message?: string; |
| | | }; |
| | | } |
| | | |
| | | /** 考勤统计 */ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 查询当前登录人的考勤状态 |
| | | * |
| | | * @param date 考勤日期,格式 yyyy-MM-dd,不传取服务器当天 |
| | | */ |
| | | export function getAttendanceMyStatus(date?: string) { |
| | | return requestClient.get<HrmAttendanceRecordApi.AttendanceMyStatus>( |
| | | '/hrm/attendance/record/my-status', |
| | | date ? { params: { date } } : undefined, |
| | | ); |
| | | } |
| | | |
| | | /** 查询考勤统计(按月) */ |
| | | export function getAttendanceStatistics(params: { |
| | | userId?: number; |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesMdItemBatchConfigApi { |
| | | /** MES 物料批次属性配置 */ |
| | |
| | | clientFlag?: boolean; // 批次属性-客户 |
| | | salesOrderCodeFlag?: boolean; // 批次属性-销售订单编号 |
| | | purchaseOrderCodeFlag?: boolean; // 批次属性-采购订单编号 |
| | | workorderFlag?: boolean; // 批次属性-生产订单 |
| | | workOrderFlag?: boolean; // 批次属性-生产工单 |
| | | taskFlag?: boolean; // 批次属性-生产任务 |
| | | workstationFlag?: boolean; // 批次属性-工作站 |
| | | toolFlag?: boolean; // 批次属性-工具 |
| | |
| | | import type { HrmAttendanceRecordApi } from '#/api/hrm/attendance/record'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | import { getDictLabel, getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getDeptList } from '#/api/system/dept'; |
| | | import { handleTree } from '#/packages/utils/src'; |
| | | |
| | | /** 请假类型的兜底名称,与后端 HrmLeaveTypeEnum 保持一致 */ |
| | | const LEAVE_TYPE_FALLBACK_LABELS: Record<number, string> = { |
| | | 1: '事假', |
| | | 2: '病假', |
| | | 3: '年假', |
| | | 4: '婚假', |
| | | 5: '产假', |
| | | 6: '丧假', |
| | | }; |
| | | |
| | | /** |
| | | * 请假类型的字典标签 |
| | | * |
| | | * 字典 hrm_leave_type 未同步时返回空串,由调用方决定兜底, |
| | | * 避免这类环境把请假类型显示成 "-" 而让人误以为没有请假。 |
| | | */ |
| | | export function getLeaveTypeLabel(type: number | string) { |
| | | const label = getDictLabel(DICT_TYPE.HRM_LEAVE_TYPE, String(type)); |
| | | return label || LEAVE_TYPE_FALLBACK_LABELS[Number(type)] || ''; |
| | | } |
| | | |
| | | /** 把 leaveTypes(如 "1,2")翻译成请假类型名称,多段用顿号分隔 */ |
| | | export function formatLeaveTypeNames(leaveTypes?: string) { |
| | | if (!leaveTypes) { |
| | | return ''; |
| | | } |
| | | return leaveTypes |
| | | .split(',') |
| | | .map((type) => type.trim()) |
| | | .filter(Boolean) |
| | | .map((type) => getLeaveTypeLabel(type) || type) |
| | | .join('、'); |
| | | } |
| | | |
| | | /** 列表的搜索表单 */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | |
| | | placeholder: '请选择下班打卡类型', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'leaveFlag', |
| | | label: '是否请假', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: [ |
| | | { label: '已请假', value: true }, |
| | | { label: '未请假', value: false }, |
| | | ], |
| | | placeholder: '请选择是否请假', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | props: { type: DICT_TYPE.HRM_CLOCK_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'leaveFlag', |
| | | title: '请假', |
| | | width: 160, |
| | | slots: { |
| | | default: 'leave', |
| | | }, |
| | | }, |
| | | { field: 'workHours', title: '工作时长(h)', width: 100 }, |
| | | { field: 'overtimeHours', title: '加班时长(h)', width: 100 }, |
| | | { field: 'location', title: '打卡地点', minWidth: 150 }, |
| | |
| | | import { Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { exportAttendanceRecord, getAttendanceRecordPage } from '#/api/hrm/attendance/record'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import { formatLeaveTypeNames, useGridColumns, useGridFormSchema } from './data'; |
| | | import ClockPanel from './modules/clock-panel.vue'; |
| | | import ImportForm from './modules/import-form.vue'; |
| | | |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #leave="{ row }"> |
| | | <Tag v-if="row.leaveFlag" color="blue"> |
| | | {{ formatLeaveTypeNames(row.leaveTypes) || '请假' }} |
| | | <span v-if="Number(row.leaveHours) > 0"> |
| | | {{ Number(row.leaveHours).toFixed(2) }}h |
| | | </span> |
| | | </Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { AttendanceClockLocation, AttendanceClockResponse } from '#/api/hrm/attendance/record'; |
| | | import type { AttendanceClockLocation, AttendanceClockResponse, HrmAttendanceRecordApi } from '#/api/hrm/attendance/record'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { clockIn, clockOut } from '#/api/hrm/attendance/record'; |
| | | import { clockIn, clockOut, getAttendanceMyStatus } from '#/api/hrm/attendance/record'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | |
| | | const location = ref(DEFAULT_LOCATION); |
| | | const loading = ref(false); |
| | | const locating = ref(false); |
| | | /** 今日考勤状态(含请假信息),加载失败时为 undefined */ |
| | | const myStatus = ref<HrmAttendanceRecordApi.AttendanceMyStatus>(); |
| | | const statusLoading = ref(false); |
| | | /** 当前打卡返回的当天记录,用于打卡后即时刷新请假信息 */ |
| | | const lastClockRecord = ref<HrmAttendanceRecordApi.AttendanceRecord>(); |
| | | |
| | | /** 当天是否请假:优先用 my-status 的 onLeave,其次用当天记录上的 leaveFlag */ |
| | | const onLeave = computed( |
| | | () => |
| | | myStatus.value?.onLeave === true || |
| | | myStatus.value?.record?.leaveFlag === true || |
| | | lastClockRecord.value?.leaveFlag === true, |
| | | ); |
| | | |
| | | /** 请假类型名称,如 "事假、病假" */ |
| | | const leaveTypeNames = computed( |
| | | () => |
| | | myStatus.value?.leaveTypeNames || |
| | | formatLeaveTypes( |
| | | myStatus.value?.record?.leaveTypes || |
| | | lastClockRecord.value?.leaveTypes, |
| | | ), |
| | | ); |
| | | |
| | | /** 请假工时(小时) */ |
| | | const leaveHours = computed( |
| | | () => |
| | | Number( |
| | | myStatus.value?.leaveHours ?? |
| | | myStatus.value?.record?.leaveHours ?? |
| | | lastClockRecord.value?.leaveHours ?? |
| | | 0, |
| | | ) || 0, |
| | | ); |
| | | |
| | | /** 请假类型兜底名称,与后端 HrmLeaveTypeEnum 保持一致 */ |
| | | const LEAVE_TYPE_NAMES: Record<number, string> = { |
| | | 1: '事假', |
| | | 2: '病假', |
| | | 3: '年假', |
| | | 4: '婚假', |
| | | 5: '产假', |
| | | 6: '丧假', |
| | | }; |
| | | |
| | | /** 把 "1,2" 翻译成 "事假、病假" */ |
| | | function formatLeaveTypes(leaveTypes?: string) { |
| | | if (!leaveTypes) { |
| | | return ''; |
| | | } |
| | | return leaveTypes |
| | | .split(',') |
| | | .map((type) => type.trim()) |
| | | .filter(Boolean) |
| | | .map((type) => LEAVE_TYPE_NAMES[Number(type)] ?? '') |
| | | .filter(Boolean) |
| | | .join('、'); |
| | | } |
| | | |
| | | /** 加载当前登录人的考勤状态;失败时不阻断打卡 */ |
| | | async function loadMyStatus() { |
| | | statusLoading.value = true; |
| | | try { |
| | | myStatus.value = await getAttendanceMyStatus(); |
| | | } catch { |
| | | // 接口错误已由请求拦截器统一提示,这里静默降级,不影响打卡主流程 |
| | | myStatus.value = undefined; |
| | | } finally { |
| | | statusLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取浏览器定位并统一为 WGS84 交给后端校验。 |
| | |
| | | action === 'in' |
| | | ? await clockIn(position, locationText) |
| | | : await clockOut(position, locationText); |
| | | lastClockRecord.value = result?.record; |
| | | message.success( |
| | | buildSuccessMessage(action === 'in' ? '上班打卡' : '下班打卡', result), |
| | | ); |
| | | // 打卡后刷新今日状态(含请假信息),保证面板上的提示与后端一致 |
| | | await loadMyStatus(); |
| | | emit('success'); |
| | | } catch (error: any) { |
| | | // 接口错误已由请求拦截器统一提示,这里只兜底本地异常,避免重复弹出 |
| | |
| | | |
| | | const [Modal] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | if (isOpen) { |
| | | // 每次打开都拉一次今日状态,避免展示过期的请假信息 |
| | | myStatus.value = undefined; |
| | | lastClockRecord.value = undefined; |
| | | void loadMyStatus(); |
| | | } else { |
| | | location.value = DEFAULT_LOCATION; |
| | | } |
| | | }, |
| | |
| | | <template> |
| | | <Modal title="打卡" class="w-[420px]"> |
| | | <div class="p-4"> |
| | | <a-alert |
| | | v-if="onLeave" |
| | | class="mb-4" |
| | | type="info" |
| | | show-icon |
| | | message="今日已请假,如需上班可正常打卡" |
| | | > |
| | | <template #description> |
| | | <div class="text-xs"> |
| | | <span v-if="leaveTypeNames">请假类型:{{ leaveTypeNames }}</span> |
| | | <span v-if="leaveHours > 0" class="ml-2"> |
| | | 请假 {{ leaveHours.toFixed(2) }} 小时 |
| | | </span> |
| | | <div class="mt-1 text-gray-500"> |
| | | 请假当天打卡不计迟到/早退,也不计缺卡。 |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </a-alert> |
| | | <div v-else-if="statusLoading" class="mb-4 text-xs text-gray-400"> |
| | | 正在加载今日考勤状态… |
| | | </div> |
| | | <div class="mb-4"> |
| | | <label class="mr-2">打卡地点:</label> |
| | | <a-input |
| | |
| | | <script lang="ts" setup> |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesMdItemBatchConfigApi } from '#/api/mes/md/item/batchConfig'; |
| | |
| | | clientFlag: false, |
| | | salesOrderCodeFlag: false, |
| | | purchaseOrderCodeFlag: false, |
| | | workorderFlag: false, |
| | | workOrderFlag: false, |
| | | taskFlag: false, |
| | | workstationFlag: false, |
| | | toolFlag: false, |
| | |
| | | 销售订单编号 |
| | | </Checkbox> |
| | | <Checkbox |
| | | v-model:checked="formData.workorderFlag" |
| | | v-model:checked="formData.workOrderFlag" |
| | | :disabled="isReadOnly" |
| | | > |
| | | 生产订单 |