xiaoyi
2026-10-30 a024b819ddb6999874faa77aacbb26a50b5f8999
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 个复选框勾选后存不进去、不回显)
已修改6个文件
258 ■■■■■ 文件已修改
src/api/hrm/attendance/record/index.ts 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/batchConfig/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/data.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/modules/clock-panel.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/modules/item-batch-config-form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/record/index.ts
@@ -43,8 +43,65 @@
    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;
    };
  }
  /** 考勤统计 */
@@ -87,6 +144,18 @@
  );
}
/**
 * 查询当前登录人的考勤状态
 *
 * @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;
src/api/mes/md/item/batchConfig/index.ts
@@ -1,4 +1,4 @@
import { requestClient } from '#/api/request';
import { requestClient } from '#/api/request';
export namespace MesMdItemBatchConfigApi {
  /** MES 物料批次属性配置 */
@@ -12,7 +12,7 @@
    clientFlag?: boolean; // 批次属性-客户
    salesOrderCodeFlag?: boolean; // 批次属性-销售订单编号
    purchaseOrderCodeFlag?: boolean; // 批次属性-采购订单编号
    workorderFlag?: boolean; // 批次属性-生产订单
    workOrderFlag?: boolean; // 批次属性-生产工单
    taskFlag?: boolean; // 批次属性-生产任务
    workstationFlag?: boolean; // 批次属性-工作站
    toolFlag?: boolean; // 批次属性-工具
src/views/hrm/attendance/record/data.ts
@@ -3,10 +3,44 @@
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[] {
@@ -67,6 +101,19 @@
        placeholder: '请选择下班打卡类型',
      },
    },
    {
      fieldName: 'leaveFlag',
      label: '是否请假',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '已请假', value: true },
          { label: '未请假', value: false },
        ],
        placeholder: '请选择是否请假',
      },
    },
  ];
}
@@ -97,6 +144,14 @@
        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 },
src/views/hrm/attendance/record/index.vue
@@ -5,13 +5,13 @@
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';
@@ -108,6 +108,15 @@
          ]"
        />
      </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>
src/views/hrm/attendance/record/modules/clock-panel.vue
@@ -1,13 +1,13 @@
<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']);
@@ -16,6 +16,77 @@
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 交给后端校验。
@@ -72,9 +143,12 @@
      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) {
    // 接口错误已由请求拦截器统一提示,这里只兜底本地异常,避免重复弹出
@@ -102,7 +176,12 @@
const [Modal] = useVbenModal({
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
    if (isOpen) {
      // 每次打开都拉一次今日状态,避免展示过期的请假信息
      myStatus.value = undefined;
      lastClockRecord.value = undefined;
      void loadMyStatus();
    } else {
      location.value = DEFAULT_LOCATION;
    }
  },
@@ -112,6 +191,28 @@
<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
src/views/mes/md/item/modules/item-batch-config-form.vue
@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesMdItemBatchConfigApi } from '#/api/mes/md/item/batchConfig';
@@ -41,7 +41,7 @@
    clientFlag: false,
    salesOrderCodeFlag: false,
    purchaseOrderCodeFlag: false,
    workorderFlag: false,
    workOrderFlag: false,
    taskFlag: false,
    workstationFlag: false,
    toolFlag: false,
@@ -155,7 +155,7 @@
          销售订单编号
        </Checkbox>
        <Checkbox
          v-model:checked="formData.workorderFlag"
          v-model:checked="formData.workOrderFlag"
          :disabled="isReadOnly"
        >
          生产订单