tuqin
4 天以前 5cd5a5baa076377f3136338db7a9336d0d570d33
feat(hrm): 前端同步新疆中亿装配的人力资源模块

配套后端提交 52feee3e。新增加班工资配置、合同附件、薪资计算表单等页面,
调整考勤/员工/薪资/离职/调岗等页面以匹配新接口与新字段
(员工类别、考勤规则生效区间、社保大病医疗/长期护理比例等)。
已修改42个文件
已添加6个文件
1436 ■■■■■ 文件已修改
src/api/hrm/attendance/holiday/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/rule/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/contract/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/resignation/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/overtime-wage/index.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/social-security-scheme/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/transfer/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/detail/approve/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/holiday/data.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/holiday/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/holiday/modules/form.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/modules/import-form.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/rule/data.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/index.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/modules/attachment-modal.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/detail.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/form.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/detail/approve/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/detail/approve/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/detail/approve/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/data.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/index.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/modules/form.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-social-security/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/data.ts 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/modules/form.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/data.ts 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/modules/form.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/data.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/detail/approve/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/modules/form.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/holiday/index.ts
@@ -7,10 +7,7 @@
  export interface Holiday {
    id?: number;
    name?: string;
    /** å¼€å§‹æ—¥æœŸ */
    startDate?: string;
    /** ç»“束日期 */
    endDate?: string;
    date?: string;
    type?: number;
    typeName?: string;
    isLegal?: number;
src/api/hrm/attendance/rule/index.ts
@@ -14,6 +14,8 @@
    lateAllowMinutes?: number;
    earlyAllowMinutes?: number;
    overtimeThreshold?: number;
    effectiveStartDate?: string;
    effectiveEndDate?: string;
    status?: number;
    remark?: string;
    createTime?: string;
src/api/hrm/employee/contract/index.ts
@@ -154,3 +154,28 @@
    { file, updateSupport },
  );
}
/** ç”Ÿæˆå¹¶ä¸‹è½½åˆåŒæ–‡æ¡£ï¼ˆdocx)
 * @param templateType 1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同
 */
export function generateContractDocx(id: number, templateType: number) {
  return requestClient.download(
    `/hrm/employee-contract/generate-docx?id=${id}&templateType=${templateType}`,
  );
}
/** ç”ŸæˆåˆåŒå¹¶å›žå­˜ä¸ºåˆåŒé™„件,返回 blobId */
export function generateContractArchive(id: number, templateType: number) {
  return requestClient.post<number>(
    `/hrm/employee-contract/generate?id=${id}&templateType=${templateType}`,
  );
}
/** ä¸‹è½½åˆåŒç©ºç™½æ¨¡æ¿
 * @param type 1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同 4-退休返聘合同
 */
export function downloadContractTemplate(type: number) {
  return requestClient.download(
    `/hrm/employee-contract/download-template?type=${type}`,
  );
}
src/api/hrm/employee/index.ts
@@ -27,6 +27,8 @@
    regularDate?: string;
    leaveDate?: string;
    employeeStatus?: number;
    /** å‘˜å·¥ç±»åˆ«ï¼š1-普通员工,2-技工,3-管理 */
    employeeCategory?: number;
    annualLeaveBalance?: number;
    sickLeaveBalance?: number;
    salaryStructureId?: number;
@@ -44,6 +46,10 @@
    workHistories?: WorkHistory[];
    /** ç´§æ€¥è”系人列表 */
    emergencyContacts?: EmergencyContact[];
    /** é™„ä»¶ blobId åˆ—表(保存时传入) */
    blobIds?: number[];
    /** é™„件列表(详情时返回) */
    attachmentList?: { id: number; name?: string; url?: string }[];
  }
  /** æ•™è‚²ç»åކ */
@@ -85,8 +91,13 @@
    id: number;
    employeeNo: string;
    name: string;
    /** éƒ¨é—¨ID */
    deptId?: number;
    deptName?: string;
    /** å²—位ID列表 */
    postIds?: number[];
    postName?: string;
    employeeStatus?: number;
  }
  /** å‘˜å·¥å¯¼å…¥ç»“æžœ */
@@ -169,3 +180,15 @@
export function deleteEmergencyContact(id: number) {
  return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
}
/** å‘˜å·¥è½¬æ­£ï¼ˆä»…劳动合同可转正) */
export function regularizeEmployee(id: number) {
  return requestClient.put(`/hrm/employee/regular?id=${id}`);
}
/** å¾…转正提醒列表(试用中、劳动合同、试用期即将到期) */
export function getRegularizeReminders(days = 7) {
  return requestClient.get<HrmEmployeeApi.Employee[]>(
    `/hrm/employee/regularize-reminders?days=${days}`,
  );
}
src/api/hrm/resignation/index.ts
@@ -58,7 +58,7 @@
/** æäº¤ç¦»èŒç”³è¯·ï¼ˆå‘起审批) */
export function submitResignation(id: number) {
  return requestClient.post(`/hrm/resignation/submit?id=${id}&processDefinitionKey=hrm_resignation_approve`);
  return requestClient.post(`/hrm/resignation/submit?id=${id}`);
}
/** æ’¤é”€ç¦»èŒç”³è¯· */
src/api/hrm/salary/overtime-wage/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
import { requestClient } from '#/api/request';
export namespace HrmOvertimeWageApi {
  /** å²—位加班工资配置 */
  export interface OvertimeWageConfig {
    id?: number;
    /** å²—位ID */
    postId?: number;
    /** å²—位名称 */
    postName?: string;
    /** åŠ ç­æ—¶è–ªï¼ˆå…ƒ/小时),默认 20 */
    hourlyRate?: number;
    /** æ¯æœˆæœ€å¤§åŠ ç­æ—¶é•¿ä¸Šé™ï¼ˆå°æ—¶ï¼‰ï¼Œä¸ºç©ºè¡¨ç¤ºä¸é™ */
    maxOvertimeHours?: number;
    /** çŠ¶æ€ï¼š0-启用,1-禁用 */
    status?: number;
    remark?: string;
    createTime?: string;
  }
  /** æŸ¥è¯¢å‚æ•° */
  export interface OvertimeWageConfigQueryParam {
    postId?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢å²—位加班工资配置列表 */
export function getOvertimeWageConfigList(
  params?: HrmOvertimeWageApi.OvertimeWageConfigQueryParam,
) {
  return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig[]>(
    '/hrm/salary/overtime-wage/list',
    { params },
  );
}
/** æŸ¥è¯¢å²—位加班工资配置详情 */
export function getOvertimeWageConfig(id: number) {
  return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig>(
    `/hrm/salary/overtime-wage/get?id=${id}`,
  );
}
/** æ–°å¢žå²—位加班工资配置 */
export function createOvertimeWageConfig(
  data: HrmOvertimeWageApi.OvertimeWageConfig,
) {
  return requestClient.post('/hrm/salary/overtime-wage/create', data);
}
/** æ›´æ–°å²—位加班工资配置 */
export function updateOvertimeWageConfig(
  data: HrmOvertimeWageApi.OvertimeWageConfig,
) {
  return requestClient.put('/hrm/salary/overtime-wage/update', data);
}
/** åˆ é™¤å²—位加班工资配置 */
export function deleteOvertimeWageConfig(id: number) {
  return requestClient.delete(`/hrm/salary/overtime-wage/delete?id=${id}`);
}
src/api/hrm/salary/social-security-scheme/index.ts
@@ -16,6 +16,14 @@
    unemploymentPersonalRatio?: number;
    workInjuryRatio?: number;
    maternityRatio?: number;
    /** å¤§ç—…医疗企业缴纳比例 */
    seriousIllnessCompanyRatio?: number;
    /** å¤§ç—…医疗个人缴纳比例 */
    seriousIllnessPersonalRatio?: number;
    /** é•¿æœŸæŠ¤ç†ä¼ä¸šç¼´çº³æ¯”例 */
    longTermCareCompanyRatio?: number;
    /** é•¿æœŸæŠ¤ç†ä¸ªäººç¼´çº³æ¯”例 */
    longTermCarePersonalRatio?: number;
    housingFundCompanyRatio?: number;
    housingFundPersonalRatio?: number;
    status?: number;
src/api/hrm/transfer/index.ts
@@ -7,8 +7,10 @@
  /** è°ƒå²—申请 */
  export interface Transfer {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    /** è°ƒå²—人(员工ID) */
    userId?: number;
    /** è°ƒå²—人姓名 */
    userName?: string;
    currentDeptId?: number;
    currentDeptName?: string;
    currentPostId?: number;
@@ -59,7 +61,7 @@
/** æäº¤è°ƒå²—申请(发起审批) */
export function submitTransfer(id: number) {
  return requestClient.post(`/hrm/transfer/submit?id=${id}&processDefinitionKey=hrm_transfer_approve`);
  return requestClient.post(`/hrm/transfer/submit?id=${id}`);
}
/** æ’¤é”€è°ƒå²—申请 */
src/views/hrm/attendance/exception/data.ts
@@ -129,7 +129,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/attendance/exception/detail/approve/index.vue
@@ -6,7 +6,7 @@
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime, formatDate } from '@vben/utils';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
@@ -49,7 +49,7 @@
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
src/views/hrm/attendance/exception/index.vue
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { automaticApproval } from '#/store/audit-policy';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
@@ -49,15 +48,10 @@
/** æäº¤è€ƒå‹¤å¼‚常申请 */
async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) {
  const res = automaticApproval.value ? [] : await getAttendanceExceptionApproveProcessList();
  const res = await getAttendanceExceptionApproveProcessList();
  processList.value = res;
  currentRow.value = row;
  selectedProcessKey.value = undefined;
  if (automaticApproval.value) {
    selectedProcessKey.value = 'hrm_attendance_exception';
    await confirmSubmit();
    return;
  }
  submitModalVisible.value = true;
}
@@ -70,7 +64,7 @@
  submitLoading.value = true;
  try {
    await submitAttendanceException(currentRow.value!.id!, selectedProcessKey.value);
    message.success(automaticApproval.value ? '已提交生效' : '提交审批成功');
    message.success('提交审批成功');
    submitModalVisible.value = false;
    handleRefresh();
  } finally {
src/views/hrm/attendance/holiday/data.ts
@@ -29,10 +29,11 @@
      rules: 'required',
    },
    {
      fieldName: 'dateRange',
      label: '日期区间',
      component: 'RangePicker',
      fieldName: 'date',
      label: '日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
@@ -125,14 +126,7 @@
export function useGridColumns(): VxeTableGridOptions<HrmAttendanceHolidayApi.Holiday>['columns'] {
  return [
    { field: 'name', title: '节假日名称', minWidth: 150 },
    {
      field: 'startDate',
      title: '日期区间',
      minWidth: 200,
      slots: {
        default: 'dateRange',
      },
    },
    { field: 'date', title: '日期', width: 120 },
    {
      field: 'type',
      title: '类型',
@@ -155,7 +149,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/attendance/holiday/index.vue
@@ -114,11 +114,6 @@
          ]"
        />
      </template>
      <template #dateRange="{ row }">
        <span v-if="!row.startDate && !row.endDate">-</span>
        <span v-else-if="row.startDate === row.endDate">{{ row.startDate }}</span>
        <span v-else>{{ row.startDate }} ~ {{ row.endDate }}</span>
      </template>
      <template #isLegal="{ row }">
        <Tag v-if="row.isLegal === 1" color="red">法定</Tag>
        <span v-else>-</span>
src/views/hrm/attendance/holiday/modules/form.vue
@@ -53,15 +53,7 @@
      return;
    }
    modalApi.lock();
    const values = (await formApi.getValues()) as HrmAttendanceHolidayApi.Holiday & {
      dateRange?: string[];
    };
    const { dateRange, ...rest } = values;
    const data: HrmAttendanceHolidayApi.Holiday = {
      ...rest,
      startDate: dateRange?.[0],
      endDate: dateRange?.[1],
    };
    const data = (await formApi.getValues()) as HrmAttendanceHolidayApi.Holiday;
    try {
      if (formType.value === 'update') {
        await updateAttendanceHoliday(data);
@@ -89,14 +81,7 @@
    }
    modalApi.lock();
    try {
      const detail = await getAttendanceHoliday(data.id);
      await formApi.setValues({
        ...detail,
        dateRange:
          detail.startDate && detail.endDate
            ? [detail.startDate, detail.endDate]
            : undefined,
      });
      await formApi.setValues(await getAttendanceHoliday(data.id));
    } finally {
      modalApi.unlock();
    }
src/views/hrm/attendance/record/modules/import-form.vue
@@ -31,8 +31,7 @@
      emit('success');
      // åŽç«¯è¿”回导入结果(含失败原因,例如“以下员工不存在...;需补充手机号...”)
      if (res && res.length > 0) {
        // å¤±è´¥æ—¶åŽç«¯ä¼šæ‹¼ä¸Šã€Œæœªèƒ½å¯¼å…¥ï¼šã€æ˜Žç»†æ®µï¼Œç”¨å®ƒåˆ¤å®šï¼Œé¿å…æˆåŠŸæ–‡æ¡ˆé‡Œå‡ºçŽ°ã€Œæœªã€å°±è¯¯æŠ¥è­¦å‘Š
        message[res.includes('未能导入') ? 'warning' : 'success'](res);
        message[res.includes('未') || res.includes('不完整') ? 'warning' : 'success'](res);
      } else {
        message.success('导入成功');
      }
src/views/hrm/attendance/rule/data.ts
@@ -106,6 +106,28 @@
      },
    },
    {
      fieldName: 'effectiveStartDate',
      label: '生效开始日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '不选表示不限',
        valueFormat: 'YYYY-MM-DD',
        format: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'effectiveEndDate',
      label: '生效结束日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '不选表示长期有效',
        valueFormat: 'YYYY-MM-DD',
        format: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
@@ -186,6 +208,13 @@
    { field: 'earlyAllowMinutes', title: '早退允许(分钟)', width: 120 },
    { field: 'overtimeThreshold', title: '加班起算(小时)', width: 120 },
    {
      field: 'effectiveStartDate',
      title: '生效区间',
      minWidth: 200,
      formatter: ({ row }) =>
        `${row.effectiveStartDate || '不限'} ~ ${row.effectiveEndDate || '不限'}`,
    },
    {
      field: 'status',
      title: '状态',
      width: 80,
src/views/hrm/employee/contract/data.ts
@@ -285,7 +285,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/employee/contract/index.vue
@@ -3,12 +3,17 @@
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Tag } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { Button, Dropdown, Menu, message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEmployeeContract,
  downloadContractTemplate,
  generateContractArchive,
  generateContractDocx,
  getEmployeeContractPage,
} from '#/api/hrm/employee/contract';
import { $t } from '#/locales';
@@ -17,6 +22,7 @@
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
import Terminate from './modules/terminate.vue';
import Attachment from './modules/attachment-modal.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -30,6 +36,11 @@
const [TerminateModal, terminateModalApi] = useVbenModal({
  connectedComponent: Terminate,
  destroyOnClose: true,
});
const [AttachmentModal, attachmentModalApi] = useVbenModal({
  connectedComponent: Attachment,
  destroyOnClose: true,
});
@@ -48,6 +59,21 @@
  importModalApi.open();
}
/** ä¸‹è½½åˆåŒç©ºç™½æ¨¡æ¿ï¼štype 1-劳动合同简版 2-劳动合同标准 3-劳务合同 */
const TEMPLATE_NAMES: Record<number, string> = {
  1: '劳动合同(简版).docx',
  2: '劳动合同(标准).docx',
  3: '劳务合同.doc',
};
async function handleDownloadTemplate(type: number) {
  const data = await downloadContractTemplate(type);
  downloadFileFromBlobPart({
    fileName: TEMPLATE_NAMES[type] || '合同模板',
    source: data,
  });
  message.success('模板已下载');
}
/** ç¼–辑合同 */
function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
@@ -61,6 +87,60 @@
/** è§£é™¤/终止合同 */
function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) {
  terminateModalApi.setData({ id: row.id }).open();
}
/** æŸ¥çœ‹åˆåŒé™„ä»¶ */
function handleAttachments(row: HrmEmployeeContractApi.EmployeeContract) {
  attachmentModalApi.setData({ id: row.id }).open();
}
/** ç”ŸæˆåˆåŒï¼ˆdocx):templateType 1-劳动合同简版 2-劳动合同标准 3-劳务合同 */
async function handleGenerate(
  row: HrmEmployeeContractApi.EmployeeContract,
  templateType: number,
) {
  // 1. ç”Ÿæˆå¹¶å›žå­˜ä¸ºåˆåŒé™„ä»¶
  await generateContractArchive(row.id!, templateType);
  // 2. åŒæ—¶ä¸‹è½½ä¸€ä»½
  const data = await generateContractDocx(row.id!, templateType);
  const genName =
    { 1: '劳动合同(简版).docx', 2: '劳动合同(标准).docx', 3: '劳务合同.docx' }[
      templateType
    ] || '合同.docx';
  downloadFileFromBlobPart({
    fileName: `${row.contractNo || row.id}-${genName}`,
    source: data,
  });
  message.success('合同已生成,并归档到该合同附件');
  handleRefresh();
}
/** â€œæ›´å¤šâ€èœå•:修改 / ç»­ç­¾ / è§£é™¤ç»ˆæ­¢ / åˆ é™¤ */
function handleMore({ key }: { key: number | string }, row: HrmEmployeeContractApi.EmployeeContract) {
  switch (key) {
    case 'edit': {
      handleEdit(row);
      break;
    }
    case 'renew': {
      handleRenew(row);
      break;
    }
    case 'terminate': {
      handleTerminate(row);
      break;
    }
    case 'delete': {
      Modal.confirm({
        title: `确认删除合同【${row.contractNo || row.id}】吗?`,
        okText: '删除',
        okType: 'danger',
        cancelText: '取消',
        onOk: () => handleDelete(row),
      });
      break;
    }
  }
}
/** åˆ é™¤åˆåŒ */
@@ -110,6 +190,7 @@
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <TerminateModal @success="handleRefresh" />
    <AttachmentModal />
    <Grid table-title="员工合同列表">
      <template #parentNo="{ row }">
        <span>{{ row.parentNo || '-' }}</span>
@@ -119,6 +200,19 @@
        <span v-else>-</span>
      </template>
      <template #toolbar-tools>
        <Dropdown>
          <Button class="mr-2" type="primary">
            ä¸‹è½½åˆåŒæ¨¡æ¿ <IconifyIcon class="size-3" icon="lucide:chevron-down" />
          </Button>
          <template #overlay>
            <Menu @click="({ key }) => handleDownloadTemplate(Number(key))">
              <Menu.Item key="1">劳动合同(简版)</Menu.Item>
              <Menu.Item key="2">劳动合同(标准)</Menu.Item>
              <Menu.Item key="3">劳务合同</Menu.Item>
              <Menu.Item key="4">退休返聘合同</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
        <TableAction
          :actions="[
            {
@@ -139,44 +233,29 @@
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: 'ç»­ç­¾',
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:employee-contract:create'],
              ifShow: isTerminable(row),
              onClick: handleRenew.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:employee-contract:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '解除/终止',
              type: 'link',
              icon: ACTION_ICON.CLOSE,
              auth: ['hrm:employee-contract:update'],
              ifShow: isTerminable(row),
              onClick: handleTerminate.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:employee-contract:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['员工合同']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
        <Dropdown>
          <a class="text-primary" @click.prevent>生成合同 <IconifyIcon class="size-3" icon="lucide:chevron-down" /></a>
          <template #overlay>
            <Menu @click="({ key }) => handleGenerate(row, Number(key))">
              <Menu.Item key="1">劳动合同(简版)</Menu.Item>
              <Menu.Item key="2">劳动合同(标准)</Menu.Item>
              <Menu.Item key="3">劳务合同</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
        <a class="ml-3 text-primary" @click="handleAttachments(row)">附件</a>
        <Dropdown>
          <a class="ml-3 text-primary" @click.prevent>更多 <IconifyIcon class="size-3" icon="lucide:chevron-down" /></a>
          <template #overlay>
            <Menu @click="(e) => handleMore(e, row)">
              <Menu.Item key="edit">修改</Menu.Item>
              <Menu.Item v-if="isTerminable(row)" key="renew">ç»­ç­¾</Menu.Item>
              <Menu.Item v-if="isTerminable(row)" key="terminate">解除/终止</Menu.Item>
              <Menu.Divider />
              <Menu.Item danger key="delete">删除</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
      </template>
    </Grid>
  </Page>
src/views/hrm/employee/contract/modules/attachment-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { getEmployeeContract } from '#/api/hrm/employee/contract';
const attachments = ref<HrmEmployeeContractApi.AttachmentItem[]>([]);
const loading = ref(false);
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ id?: number }>();
    if (!data?.id) {
      return;
    }
    loading.value = true;
    try {
      const detail = await getEmployeeContract(data.id);
      attachments.value = detail.attachmentList || [];
    } finally {
      loading.value = false;
    }
  },
});
</script>
<template>
  <Modal title="合同附件" class="w-[560px]">
    <div v-if="loading" class="py-6 text-center text-gray-400">加载中...</div>
    <div v-else-if="attachments.length === 0" class="py-6 text-center text-gray-400">
      æš‚无附件(可在「更多 â†’ ä¿®æ”¹ã€é‡Œä¸Šä¼ ï¼Œæˆ–用「生成合同」生成后自动归档)
    </div>
    <ul v-else class="max-h-[50vh] overflow-auto pl-5">
      <li v-for="f in attachments" :key="f.id" class="mb-2 list-disc">
        <a :href="f.url" target="_blank" rel="noopener" class="text-primary">
          {{ f.name || `附件${f.id}` }}
        </a>
      </li>
    </ul>
  </Modal>
</template>
src/views/hrm/employee/data.ts
@@ -153,6 +153,18 @@
      rules: z.number().default(1),
    },
    {
      fieldName: 'employeeCategory',
      label: '员工类别',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'),
      },
      rules: z.number().default(1),
      help: '普通员工按岗位加班时薪;技工加班按 1:1;管理不计加班工资',
    },
    {
      fieldName: 'birthday',
      label: '出生日期',
      component: 'DatePicker',
@@ -292,6 +304,18 @@
        placeholder: '请输入居住地址',
      },
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        help: '可上传身份证、学历证书、体检报告等资料作为留档',
      },
      formItemClass: 'col-span-2',
    },
  ];
}
@@ -343,6 +367,16 @@
        placeholder: '请选择员工状态',
      },
    },
    {
      fieldName: 'employeeCategory',
      label: '员工类别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'),
        placeholder: '请选择员工类别',
      },
    },
  ];
}
@@ -380,12 +414,20 @@
        default: 'employeeStatus',
      },
    },
    {
      field: 'employeeCategory',
      title: '员工类别',
      width: 100,
      slots: {
        default: 'employeeCategory',
      },
    },
    { field: 'hireDate', title: '入职日期', width: 110 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/employee/index.vue
@@ -5,15 +5,15 @@
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message, Progress, Tag } from 'ant-design-vue';
import { Button, message, Progress, Tag, Alert } from 'ant-design-vue';
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteEmployee, exportEmployee, getEmployeePage } from '#/api/hrm/employee';
import { deleteEmployee, exportEmployee, getEmployeePage, getRegularizeReminders, regularizeEmployee } from '#/api/hrm/employee';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -116,6 +116,26 @@
  importModalApi.open();
}
/** å¾…转正提醒数 */
const regularizeCount = ref(0);
async function refreshRegularizeReminders() {
  try {
    const list = await getRegularizeReminders(7);
    regularizeCount.value = list?.length ?? 0;
  } catch {
    regularizeCount.value = 0;
  }
}
onMounted(refreshRegularizeReminders);
/** å‘˜å·¥è½¬æ­£ï¼ˆä»…劳动合同可转正) */
async function handleRegularize(row: HrmEmployeeApi.Employee) {
  await regularizeEmployee(row.id!);
  message.success(`【${row.name}】已转正`);
  handleRefresh();
  refreshRegularizeReminders();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
@@ -150,6 +170,13 @@
  <Page auto-content-height>
    <ImportModal @success="handleRefresh" />
    <FormModal @success="handleRefresh" />
    <Alert
      v-if="regularizeCount > 0"
      class="mb-3"
      :message="`待转正提醒:有 ${regularizeCount} åå‘˜å·¥è¯•用期即将到期,请及时转正`"
      show-icon
      type="warning"
    />
    <Grid table-title="员工列表">
      <template #toolbar-tools>
        <TableAction
@@ -191,6 +218,11 @@
          {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_STATUS, row.employeeStatus) }}
        </Tag>
      </template>
      <template #employeeCategory="{ row }">
        <Tag :color="row.employeeCategory === 2 ? 'orange' : row.employeeCategory === 3 ? 'purple' : 'blue'">
          {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, row.employeeCategory) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -202,6 +234,16 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '转正',
              type: 'link',
              auth: ['hrm:employee:update'],
              ifShow: () => row.employeeStatus === 2,
              popConfirm: {
                title: `确认将【${row.name}】转正吗?(仅劳动合同可转正,不含调岗)`,
                confirm: handleRegularize.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
src/views/hrm/employee/modules/detail.vue
@@ -78,6 +78,20 @@
      <DescriptionsItem label="居住地址" :span="2">{{ employee.address || '-' }}</DescriptionsItem>
    </Descriptions>
    <!-- é™„件留档 -->
    <Descriptions title="附件留档" bordered :column="1" class="mb-4">
      <DescriptionsItem label="附件">
        <template v-if="employee.attachmentList && employee.attachmentList.length">
          <div v-for="file in employee.attachmentList" :key="file.id" class="mb-1">
            <a :href="file.url" target="_blank" rel="noopener">
              {{ file.name || `附件${file.id}` }}
            </a>
          </div>
        </template>
        <span v-else>-</span>
      </DescriptionsItem>
    </Descriptions>
    <!-- å¤‡æ³¨ -->
    <Descriptions title="其他信息" bordered :column="1" class="mb-4">
      <DescriptionsItem label="备注">{{ employee.remark || '-' }}</DescriptionsItem>
src/views/hrm/employee/modules/form.vue
@@ -66,6 +66,10 @@
    // æäº¤è¡¨å•
    const data = (await formApi.getValues()) as HrmEmployeeApi.Employee;
    data.remark = remark.value;
    // é™„件:把 FileUpload çš„值转成 blobId æ•°ç»„
    if (Array.isArray(data.blobIds)) {
      data.blobIds = (data.blobIds as any[]).map((item) => item?.id ?? item);
    }
    // èŽ·å–å­è¡¨æ•°æ®
    data.educations = educationListRef.value?.getData();
    data.workHistories = workHistoryListRef.value?.getData();
@@ -111,7 +115,14 @@
      employeeData.value = formData;
      remark.value = formData.remark;
      if (data.formType !== 'detail') {
        await formApi.setValues(formData);
        const blobIds = (formData.attachmentList || []).map((item) => ({
          uid: String(item.id),
          name: item.name || '',
          url: item.url || '',
          status: 'done',
          id: item.id,
        }));
        await formApi.setValues({ ...formData, blobIds });
      }
    } finally {
      modalApi.unlock();
src/views/hrm/leave/data.ts
@@ -173,7 +173,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/leave/detail/approve/index.vue
@@ -6,7 +6,7 @@
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime, formatDate } from '@vben/utils';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
@@ -60,7 +60,7 @@
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
src/views/hrm/leave/index.vue
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { automaticApproval } from '#/store/audit-policy';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveApi } from '#/api/hrm/leave';
@@ -73,14 +72,9 @@
/** æäº¤è¯·å‡ç”³è¯·ï¼ˆå…ˆé€‰æ‹©å®¡æ‰¹æµç¨‹ï¼‰ */
async function handleSubmit(row: HrmLeaveApi.Leave) {
  processList.value = automaticApproval.value ? [] : await getLeaveApproveProcessDefinitionList();
  processList.value = await getLeaveApproveProcessDefinitionList();
  currentRow.value = row;
  selectedProcessKey.value = undefined;
  if (automaticApproval.value) {
    selectedProcessKey.value = 'hrm_leave';
    await confirmSubmit();
    return;
  }
  submitModalVisible.value = true;
}
src/views/hrm/overtime/data.ts
@@ -308,7 +308,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/overtime/detail/approve/index.vue
@@ -6,7 +6,7 @@
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDate } from '@vben/utils';
import { formatDateTime } from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
@@ -54,7 +54,7 @@
      field: 'status',
      render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }),
    },
    { label: '创建时间', field: 'createTime', render: (value) => formatDate(value) as string },
    { label: '创建时间', field: 'createTime', render: (value) => formatDateTime(value) as string },
  ] as DescriptionItemSchema[],
});
src/views/hrm/overtime/index.vue
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import { automaticApproval } from '#/store/audit-policy';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmOvertimeApi } from '#/api/hrm/overtime';
@@ -65,14 +64,9 @@
const submitLoading = ref(false);
async function handleSubmit(row: HrmOvertimeApi.Overtime) {
  processList.value = automaticApproval.value ? [] : await getOvertimeApproveProcessDefinitionList();
  processList.value = await getOvertimeApproveProcessDefinitionList();
  currentRow.value = row;
  selectedProcessKey.value = undefined;
  if (automaticApproval.value) {
    selectedProcessKey.value = 'hrm_overtime';
    await confirmSubmit();
    return;
  }
  submitModalVisible.value = true;
}
@@ -84,7 +78,7 @@
  submitLoading.value = true;
  try {
    await submitOvertime(currentRow.value!.id!, selectedProcessKey.value);
    message.success(automaticApproval.value ? '已提交生效' : '已提交至协同办公审批');
    message.success('已提交至协同办公审批');
    submitModalVisible.value = false;
    handleRefresh();
  } finally {
src/views/hrm/resignation/data.ts
@@ -157,7 +157,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/resignation/detail/approve/index.vue
@@ -6,7 +6,7 @@
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDate } from '@vben/utils';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
@@ -42,7 +42,7 @@
    {
      label: '预计离职日期',
      field: 'expectedDate',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
    { label: '离职原因', field: 'reason', render: textRender },
    { label: '交接人', field: 'handoverUserName', render: textRender },
@@ -62,7 +62,7 @@
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
src/views/hrm/resignation/index.vue
@@ -163,7 +163,7 @@
            {
              label: '交接',
              type: 'link',
              auth: ['hrm:resignation:update'],
              auth: ['hrm:resignation:handover'],
              ifShow: row.status === 20 && row.handoverStatus !== 20,
              popConfirm: {
                title: row.handoverStatus === 0 ? '确认开始交接吗?' : '确认完成交接吗?',
src/views/hrm/salary/calculation/data.ts
@@ -75,21 +75,21 @@
    { field: 'period', title: '核算周期', width: 100 },
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'baseSalary', title: '基本工资', width: 100, formatter: 'formatAmount2' },
    { field: 'performanceSalary', title: '绩效工资', width: 100, formatter: 'formatAmount2' },
    { field: 'overtimePay', title: '加班费', width: 100, formatter: 'formatAmount2' },
    { field: 'mealAllowance', title: '餐补', width: 80, formatter: 'formatAmount2' },
    { field: 'transportAllowance', title: '交通补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'otherAllowance', title: '其他补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'socialSecurityDeduction', title: '社保扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'housingFundDeduction', title: '公积金扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'taxDeduction', title: '个税扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'otherDeduction', title: '其他扣款', width: 80, formatter: 'formatAmount2' },
    { field: 'leaveDeduction', title: '请假扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 80 },
    { field: 'overtimeHours', title: '加班时长(h)', width: 100 },
    { field: 'totalWorkHours', title: '排班工时(h)', width: 100 },
    { field: 'baseSalary', title: '基本工资', width: 120, slots: { default: 'editableCell' } },
    { field: 'performanceSalary', title: '绩效工资', width: 120, slots: { default: 'editableCell' } },
    { field: 'overtimePay', title: '加班费', width: 120, slots: { default: 'editableCell' } },
    { field: 'mealAllowance', title: '餐补', width: 110, slots: { default: 'editableCell' } },
    { field: 'transportAllowance', title: '交通补贴', width: 110, slots: { default: 'editableCell' } },
    { field: 'otherAllowance', title: '其他补贴', width: 110, slots: { default: 'editableCell' } },
    { field: 'socialSecurityDeduction', title: '社保扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'housingFundDeduction', title: '公积金扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'taxDeduction', title: '个税扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'otherDeduction', title: '其他扣款', width: 110, slots: { default: 'editableCell' } },
    { field: 'leaveDeduction', title: '请假扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'actualSalary', title: '实发工资', width: 110, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 110, slots: { default: 'editableCell' } },
    { field: 'overtimeHours', title: '加班时长(h)', width: 120, slots: { default: 'editableCell' } },
    { field: 'totalWorkHours', title: '排班工时(h)', width: 110 },
    {
      field: 'status',
      title: '状态',
@@ -101,7 +101,7 @@
    },
    {
      title: '操作',
      width: 120,
      width: 220,
      fixed: 'right',
      slots: {
        default: 'actions',
src/views/hrm/salary/calculation/index.vue
@@ -7,7 +7,7 @@
import { Page } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert, Tag } from 'ant-design-vue';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, Input, InputNumber, Alert, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
@@ -22,14 +22,18 @@
  previewSalaryCalculation,
  revokeSalaryCalculation,
  saveSalaryCalculation,
  updateSalaryCalculation,
} from '#/api/hrm/salary/calculation';
import { getDeptList } from '#/api/system/dept';
import { handleTree } from '#/packages/utils/src';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema, usePreviewColumns } from './data';
import FormModal from './modules/form.vue';
// æ ¸ç®—弹窗相关
const formModalRef = ref();
const saving = ref(false);
const calculateVisible = ref(false);
const calculatePeriod = ref<string>();
const calculateDeptId = ref<number>();
@@ -148,19 +152,35 @@
  previewData.value = [];
}
/** ç¡®è®¤è–ªé…¬æ ¸ç®— */
/** æŠŠè¡Œå†…编辑的文本值归一为数字后提交保存 */
async function saveRowEdits(row: any) {
  const payload: any = { ...row };
  EDITABLE_NUMBER_FIELDS.forEach((f) => {
    payload[f] = toNumberOrZero(payload[f]);
  });
  await updateSalaryCalculation(payload);
}
/** ç¡®è®¤è–ªé…¬æ ¸ç®—(方案A:确认前先保存当前行的修改,避免「改了没保存就确认」导致金额丢失) */
async function handleConfirm(row: HrmSalaryCalculationApi.SalaryCalculation) {
  if (row.status === 0) {
    await saveRowEdits(row);
  }
  await confirmSalary([row.id!]);
  message.success('确认成功');
  handleRefresh();
}
/** æ‰¹é‡ç¡®è®¤é€‰ä¸­çš„记录 */
/** æ‰¹é‡ç¡®è®¤é€‰ä¸­çš„记录(同样先保存勾选的待确认记录,再确认) */
async function handleBatchConfirm() {
  const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[];
  if (records.length === 0) {
    message.warning('请选择要确认的记录');
    return;
  }
  const pending = records.filter((r) => r.status === 0);
  for (const row of pending) {
    await saveRowEdits(row);
  }
  const ids = records.map((r) => r.id!);
  await confirmSalary(ids);
@@ -224,6 +244,55 @@
async function handleExport() {
  const data = await exportSalaryCalculation(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '薪酬核算表.xls', source: data });
}
/** åˆ—表可编辑的数值字段 */
const EDITABLE_NUMBER_FIELDS = [
  'baseSalary',
  'performanceSalary',
  'overtimePay',
  'mealAllowance',
  'transportAllowance',
  'otherAllowance',
  'socialSecurityDeduction',
  'housingFundDeduction',
  'taxDeduction',
  'otherDeduction',
  'leaveDeduction',
  'workDays',
  'overtimeHours',
];
/** æ–‡æœ¬è¾“入转数字(空值按 0) */
function toNumberOrZero(val: any) {
  if (val === '' || val === null || val === undefined) {
    return 0;
  }
  const num = Number(String(val).replace(/,/g, ''));
  return Number.isNaN(num) ? 0 : num;
}
/** ä¿å­˜åˆ—表内联修改(仅待确认状态) */
async function handleSaveEdits() {
  const rows = (gridApi.grid.getData() as any[]).filter((r) => r.status === 0);
  if (rows.length === 0) {
    message.warning('当前页没有待确认的记录可修改');
    return;
  }
  saving.value = true;
  try {
    for (const row of rows) {
      const payload: any = { ...row };
      EDITABLE_NUMBER_FIELDS.forEach((f) => {
        payload[f] = toNumberOrZero(payload[f]);
      });
      await updateSalaryCalculation(payload);
    }
    message.success(`已保存 ${rows.length} æ¡æ ¸ç®—记录`);
    handleRefresh();
  } finally {
    saving.value = false;
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -469,6 +538,7 @@
      </div>
    </Modal>
    <FormModal ref="formModalRef" @success="handleRefresh" />
    <Grid table-title="薪酬核算列表">
      <template #toolbar-tools>
        <TableAction
@@ -493,6 +563,13 @@
              onClick: handleBatchSave,
            },
            {
              label: '保存修改',
              type: 'primary',
              auth: ['hrm:salary-calculation:calculate'],
              loading: saving,
              onClick: handleSaveEdits,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
@@ -502,6 +579,15 @@
          ]"
        />
      </template>
      <template #editableCell="{ row, column }">
        <Input
          v-if="row.status === 0"
          v-model:value="row[column.field]"
          size="small"
          :style="{ width: '100%' }"
        />
        <span v-else>{{ row[column.field] ?? '' }}</span>
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'warning' : row.status === 10 ? 'success' : 'processing'">
          {{ getDictLabel(DICT_TYPE.HRM_SALARY_STATUS, row.status) }}
@@ -511,6 +597,13 @@
        <TableAction
          :actions="[
            {
              label: '修改',
              type: 'link',
              auth: ['hrm:salary-calculation:calculate'],
              ifShow: row.status === 0,
              onClick: () => formModalRef.value?.open(row),
            },
            {
              label: '确认',
              type: 'link',
              auth: ['hrm:salary-calculation:confirm'],
src/views/hrm/salary/calculation/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
<script lang="ts" setup>
import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation';
import { computed, ref } from 'vue';
import { message, Modal, Form, FormItem, Input, InputNumber } from 'ant-design-vue';
import { updateSalaryCalculation } from '#/api/hrm/salary/calculation';
const emit = defineEmits(['success']);
const visible = ref(false);
const loading = ref(false);
/** å½“前编辑的记录(含所有可编辑字段) */
const current = ref<Record<string, any>>({});
/** æ”¶å…¥é¡¹ */
const incomeFields = [
  { key: 'baseSalary', label: '基本工资' },
  { key: 'performanceSalary', label: '绩效工资' },
  { key: 'overtimePay', label: '加班费' },
  { key: 'mealAllowance', label: '餐补' },
  { key: 'transportAllowance', label: '交通补贴' },
  { key: 'otherAllowance', label: '其他补贴' },
];
/** æ‰£é™¤é¡¹ */
const deductionFields = [
  { key: 'socialSecurityDeduction', label: '社保扣款' },
  { key: 'housingFundDeduction', label: '公积金扣款' },
  { key: 'taxDeduction', label: '个税扣款' },
  { key: 'otherDeduction', label: '其他扣款' },
  { key: 'leaveDeduction', label: '请假扣款' },
];
/** å®žå‘工资(实时计算,与后端口径一致) */
const actualSalary = computed(() => {
  const num = (v: any) => (v === undefined || v === null || v === '' ? 0 : Number(v));
  const income = incomeFields.reduce((sum, f) => sum + num(current.value[f.key]), 0);
  const deduction = deductionFields.reduce((sum, f) => sum + num(current.value[f.key]), 0);
  return Number((income - deduction).toFixed(2));
});
function open(row: HrmSalaryCalculationApi.SalaryCalculation) {
  current.value = { ...row };
  visible.value = true;
}
async function handleOk() {
  loading.value = true;
  try {
    await updateSalaryCalculation({
      ...(current.value as HrmSalaryCalculationApi.SalaryCalculation),
      actualSalary: actualSalary.value,
    });
    message.success('修改成功');
    visible.value = false;
    emit('success');
  } finally {
    loading.value = false;
  }
}
defineExpose({ open });
</script>
<template>
  <Modal
    v-model:open="visible"
    :title="`修改薪酬核算${current.no ? ' - ' + current.no : ''}`"
    :width="760"
    :confirm-loading="loading"
    ok-text="保存"
    cancel-text="取消"
    @ok="handleOk"
  >
    <Form layout="vertical" class="mt-2">
      <div class="mb-2 text-sm font-semibold text-gray-700">收入项</div>
      <div class="grid grid-cols-3 gap-x-4">
        <FormItem v-for="f in incomeFields" :key="f.key" :label="f.label">
          <InputNumber
            v-model:value="current[f.key]"
            :min="0"
            :precision="2"
            style="width: 100%"
          />
        </FormItem>
      </div>
      <div class="mb-2 mt-2 text-sm font-semibold text-gray-700">扣除项</div>
      <div class="grid grid-cols-3 gap-x-4">
        <FormItem v-for="f in deductionFields" :key="f.key" :label="f.label">
          <InputNumber
            v-model:value="current[f.key]"
            :min="0"
            :precision="2"
            style="width: 100%"
          />
        </FormItem>
      </div>
      <div class="mb-2 mt-2 text-sm font-semibold text-gray-700">考勤 / å…¶ä»–</div>
      <div class="grid grid-cols-3 gap-x-4">
        <FormItem label="出勤天数">
          <InputNumber v-model:value="current.workDays" :min="0" :max="31" :precision="0" style="width: 100%" />
        </FormItem>
        <FormItem label="加班时长(h)">
          <InputNumber v-model:value="current.overtimeHours" :min="0" :precision="1" style="width: 100%" />
        </FormItem>
        <FormItem label="实发工资(自动计算)">
          <Input :value="actualSalary.toFixed(2)" disabled class="font-bold text-green-600" />
        </FormItem>
      </div>
      <FormItem label="备注">
        <Input.TextArea v-model:value="current.remark" :rows="2" placeholder="请输入备注" />
      </FormItem>
    </Form>
  </Modal>
</template>
src/views/hrm/salary/employee-social-security/data.ts
@@ -166,7 +166,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/salary/leave-type-config/data.ts
@@ -106,7 +106,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/salary/overtime-wage/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,146 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
import { getSimplePostList } from '#/api/system/post';
export type FormType = 'create' | 'update';
const statusOptions = [
  { label: '启用', value: 0 },
  { label: '禁用', value: 1 },
];
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'postId',
      label: '岗位',
      component: 'ApiSelect',
      componentProps: {
        api: getSimplePostList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择岗位(工种)',
      },
      rules: 'selectRequired',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'hourlyRate',
      label: '加班时薪',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '元/小时',
        min: 0,
        precision: 2,
        step: 1,
        placeholder: '默认 20,按岗位可不同',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'maxOvertimeHours',
      label: '最大加班时长',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '小时/月',
        min: 0,
        precision: 1,
        step: 1,
        placeholder: '留空表示不限',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: { options: statusOptions },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        maxlength: 500,
        placeholder: '可填写适用说明,如「仅普通员工」「技工按 1:1」等',
        rows: 3,
        showCount: true,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'postId',
      label: '岗位',
      component: 'ApiSelect',
      componentProps: {
        api: getSimplePostList,
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择岗位',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: statusOptions,
        placeholder: '请选择状态',
      },
    },
  ];
}
export function useGridColumns(): VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>['columns'] {
  return [
    { field: 'postName', title: '岗位(工种)', minWidth: 160 },
    {
      field: 'hourlyRate',
      title: '加班时薪',
      width: 130,
      slots: { default: 'hourlyRate' },
    },
    {
      field: 'maxOvertimeHours',
      title: '最大加班时长',
      width: 140,
      slots: { default: 'maxOvertimeHours' },
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    { field: 'remark', title: '备注', minWidth: 180 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      field: 'actions',
      title: '操作',
      fixed: 'right',
      slots: { default: 'actions' },
      width: 150,
    },
  ];
}
src/views/hrm/salary/overtime-wage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteOvertimeWageConfig,
  getOvertimeWageConfigList,
} from '#/api/hrm/salary/overtime-wage';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'HrmOvertimeWage' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: HrmOvertimeWageApi.OvertimeWageConfig) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
async function handleDelete(row: HrmOvertimeWageApi.OvertimeWageConfig) {
  await deleteOvertimeWageConfig(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['加班工资配置']));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    pagerConfig: { enabled: false },
    proxyConfig: {
      ajax: {
        query: async (_params, formValues) =>
          await getOvertimeWageConfigList(formValues),
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Alert
      class="mb-4"
      description="按「岗位(工种)」配置加班时薪:普通员工按此处时薪计加班工资,可设置每月最大加班时长上限;技工按平时时薪 1:1,管理不计加班工资。加班工资凭审批通过的加班单核算。"
      message="加班工资按岗位划分"
      show-icon
      type="info"
    />
    <Grid table-title="加班工资管理">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['加班工资']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:overtime-wage:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #hourlyRate="{ row }">
        <Tag color="blue">{{ row.hourlyRate ?? 0 }} å…ƒ/小时</Tag>
      </template>
      <template #maxOvertimeHours="{ row }">
        <span v-if="row.maxOvertimeHours === null || row.maxOvertimeHours === undefined">不限</span>
        <span v-else>{{ row.maxOvertimeHours }} å°æ—¶/月</span>
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'success' : 'default'">
          {{ row.status === 0 ? '启用' : '禁用' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:overtime-wage:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:overtime-wage:delete'],
              popConfirm: {
                title: '确认删除该岗位的加班工资配置吗?',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/overtime-wage/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
import type { FormType } from '../data';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createOvertimeWageConfig,
  getOvertimeWageConfig,
  updateOvertimeWageConfig,
} from '#/api/hrm/salary/overtime-wage';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const getTitle = computed(() =>
  formType.value === 'update' ? '修改加班工资' : '新增加班工资',
);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) return;
    modalApi.lock();
    try {
      const data =
        (await formApi.getValues()) as HrmOvertimeWageApi.OvertimeWageConfig;
      if (data.id) {
        await updateOvertimeWageConfig(data);
      } else {
        await createOvertimeWageConfig(data);
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    await formApi.resetForm();
    if (!data.id) {
      await formApi.setValues({ hourlyRate: 20, status: 0 });
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getOvertimeWageConfig(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[640px]">
    <Alert
      class="mx-4 mb-4"
      description="普通员工:加班工资 = æœ¬å²—位配置的时薪 Ã— åŠ ç­æ—¶é•¿ï¼ˆè¶…è¿‡æœ€å¤§æ—¶é•¿ä¸Šé™çš„éƒ¨åˆ†ä¸è®¡ï¼‰ï¼›æŠ€å·¥æŒ‰å¹³æ—¶æ—¶è–ª 1:1;管理不计加班工资。"
      message="加班工资计算说明"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/payment/data.ts
@@ -65,7 +65,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/salary/social-security-scheme/data.ts
@@ -5,10 +5,23 @@
/** è¡¨å•类型 */
export type FormType = 'create' | 'update';
/** æ ¼å¼åŒ–比例输入 */
/** æ ¼å¼åŒ–比例输入(统一显示两位小数百分比,如 8.25%) */
function createRatioFormatter(step: number = 0.01) {
  return {
    formatter: (value: number) => value !== undefined && value !== null ? `${(value * 100).toFixed(step === 0.001 ? 1 : 0)}%` : '',
    formatter: (value: number) =>
      value !== undefined && value !== null ? `${(value * 100).toFixed(2)}%` : '',
    parser: (value: string) => {
      const num = parseFloat(String(value).replace('%', ''));
      return isNaN(num) ? 0 : num / 100;
    },
  };
}
/** æ ¼å¼åŒ–比例输入(显示两位小数,如 0.50%) */
function createRatioFormatter2() {
  return {
    formatter: (value: number) =>
      value !== undefined && value !== null ? `${(value * 100).toFixed(2)}%` : '',
    parser: (value: string) => {
      const num = parseFloat(value.replace('%', ''));
      return isNaN(num) ? 0 : num / 100;
@@ -200,6 +213,76 @@
        ...createRatioFormatter(0.001),
      },
    },
    // å¤§ç—…医疗
    {
      fieldName: 'divider_serious_illness',
      label: '大病医疗',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'seriousIllnessCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.0001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter2(),
      },
    },
    {
      fieldName: 'seriousIllnessPersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.0001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter2(),
      },
    },
    // é•¿æœŸæŠ¤ç†
    {
      fieldName: 'divider_long_term_care',
      label: '长期护理',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'longTermCareCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.0001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter2(),
      },
    },
    {
      fieldName: 'longTermCarePersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.0001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter2(),
      },
    },
    // å…¬ç§¯é‡‘
    {
      fieldName: 'divider_housing',
@@ -307,6 +390,18 @@
      slots: { default: 'housingFundPersonalRatio' },
    },
    {
      field: 'seriousIllnessPersonalRatio',
      title: '大病医疗(个人)',
      width: 120,
      slots: { default: 'seriousIllnessPersonalRatio' },
    },
    {
      field: 'longTermCarePersonalRatio',
      title: '长期护理(个人)',
      width: 120,
      slots: { default: 'longTermCarePersonalRatio' },
    },
    {
      field: 'status',
      title: '状态',
      width: 80,
@@ -316,7 +411,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/salary/social-security-scheme/index.vue
@@ -43,10 +43,16 @@
  handleRefresh();
}
/** æ ¼å¼åŒ–比例显示 */
/** æ ¼å¼åŒ–比例显示(保留两位小数) */
function formatRatio(ratio: number | undefined) {
  if (ratio === undefined || ratio === null) return '-';
  return `${(ratio * 100).toFixed(1)}%`;
  return `${(ratio * 100).toFixed(2)}%`;
}
/** æ ¼å¼åŒ–比例显示(保留两位小数) */
function formatRatio2(ratio: number | undefined) {
  if (ratio === undefined || ratio === null) return '-';
  return `${(ratio * 100).toFixed(2)}%`;
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -107,6 +113,12 @@
      <template #housingFundPersonalRatio="{ row }">
        {{ formatRatio(row.housingFundPersonalRatio) }}
      </template>
      <template #seriousIllnessPersonalRatio="{ row }">
        {{ formatRatio2(row.seriousIllnessPersonalRatio) }}
      </template>
      <template #longTermCarePersonalRatio="{ row }">
        {{ formatRatio2(row.longTermCarePersonalRatio) }}
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'success' : 'default'">
          {{ row.status === 0 ? '启用' : '禁用' }}
src/views/hrm/salary/social-security-scheme/modules/form.vue
@@ -81,6 +81,10 @@
        maternityRatio: 0.005,
        housingFundCompanyRatio: 0.12,
        housingFundPersonalRatio: 0.12,
        seriousIllnessCompanyRatio: 0,
        seriousIllnessPersonalRatio: 0,
        longTermCareCompanyRatio: 0,
        longTermCarePersonalRatio: 0,
        status: 0,
      });
      return;
src/views/hrm/salary/structure/data.ts
@@ -150,7 +150,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      field: 'actions',
src/views/hrm/transfer/data.ts
@@ -7,6 +7,7 @@
import { getDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { handleTree } from '#/packages/utils/src';
/** è¡¨å•类型 */
@@ -40,12 +41,29 @@
      },
    },
    {
      fieldName: 'userId',
      label: '调岗人',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        showSearch: true,
        filterOption: (input: string, option: any) =>
          `${option?.label ?? ''}`.toLowerCase().includes(input.toLowerCase()),
        allowClear: true,
        placeholder: '请选择要调岗的员工(选择后自动带出原部门/原岗位)',
      },
      rules: 'selectRequired',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'currentDeptName',
      label: '原部门',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '自动获取',
        placeholder: '选择调岗人后自动带出',
      },
    },
    {
@@ -54,7 +72,7 @@
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '自动获取',
        placeholder: '选择调岗人后自动带出',
      },
    },
    {
@@ -175,7 +193,7 @@
export function useGridColumns(): VxeTableGridOptions<HrmTransferApi.Transfer>['columns'] {
  return [
    { field: 'id', title: '申请编号', width: 80 },
    { field: 'employeeName', title: '申请人', minWidth: 100 },
    { field: 'userName', title: '调岗人', minWidth: 100 },
    { field: 'currentDeptName', title: '原部门', minWidth: 120 },
    { field: 'currentPostName', title: '原岗位', minWidth: 120 },
    { field: 'targetDeptName', title: '目标部门', minWidth: 120 },
@@ -203,7 +221,7 @@
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
src/views/hrm/transfer/detail/approve/index.vue
@@ -6,7 +6,7 @@
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDate } from '@vben/utils';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
@@ -30,9 +30,9 @@
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '当前部门', field: 'currentDeptName', render: textRender },
    { label: '当前职位', field: 'currentPostName', render: textRender },
    { label: '调岗人', field: 'userName', render: textRender },
    { label: '原部门', field: 'currentDeptName', render: textRender },
    { label: '原岗位', field: 'currentPostName', render: textRender },
    { label: '目标部门', field: 'targetDeptName', render: textRender },
    { label: '目标职位', field: 'targetPostName', render: textRender },
    {
@@ -44,7 +44,7 @@
    {
      label: '生效日期',
      field: 'effectiveDate',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
    { label: '调岗原因', field: 'reason', render: textRender },
    {
@@ -57,7 +57,7 @@
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDate(val) as string,
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
src/views/hrm/transfer/modules/form.vue
@@ -17,7 +17,8 @@
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const currentEmployeeId = ref<number>();
/** å‘˜å·¥ç²¾ç®€åˆ—表缓存(选人时用于带出原部门 / åŽŸå²—ä½ï¼‰ */
let employeeCache: any[] = [];
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
@@ -26,6 +27,34 @@
  }
  return formType.value === 'update' ? '修改调岗申请' : '新增调岗申请';
});
/**
 * æ ¹æ®é€‰ä¸­çš„员工,自动带出原部门 / åŽŸå²—ä½ï¼ˆå« ID,便于后端落库)
 */
async function fillCurrentPosition(employeeId?: number) {
  if (!employeeId) {
    await formApi.setValues({
      currentDeptId: undefined,
      currentDeptName: undefined,
      currentPostId: undefined,
      currentPostName: undefined,
    });
    return;
  }
  if (employeeCache.length === 0) {
    employeeCache = (await getEmployeeSimpleList()) || [];
  }
  const emp = employeeCache.find((item) => item.id === employeeId);
  if (!emp) {
    return;
  }
  await formApi.setValues({
    currentDeptId: emp.deptId,
    currentDeptName: emp.deptName,
    currentPostId: emp.postIds?.[0],
    currentPostName: emp.postName,
  });
}
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -39,6 +68,12 @@
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  // é€‰æ‹©ã€Œè°ƒå²—人」后,自动带出其当前部门 / å²—位
  async handleValuesChange(values: any, changedFields: string[]) {
    if (changedFields?.includes('userId')) {
      await fillCurrentPosition(values?.userId);
    }
  },
});
const [Modal, modalApi] = useVbenModal({
@@ -53,7 +88,6 @@
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmTransferApi.Transfer;
    data.employeeId = currentEmployeeId.value;
    try {
      if (data.id) {
        await updateTransfer(data);
@@ -71,30 +105,31 @@
    if (!isOpen) {
      return;
    }
    employeeCache = [];
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(data.formType) });
    formApi.setDisabled(data.formType === 'detail');
    modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
    if (!data?.id) {
      // æ–°å¢žæ—¶ï¼Œè‡ªåŠ¨èŽ·å–å½“å‰ç™»å½•å‘˜å·¥çš„éƒ¨é—¨å’Œå²—ä½ä¿¡æ¯
      const employees = await getEmployeeSimpleList();
      // TODO: è¿™é‡Œåº”该根据登录用户获取对应的员工信息,暂时取第一个员工作为示例
      const currentEmployee = employees[0];
      if (currentEmployee) {
        currentEmployeeId.value = currentEmployee.id;
        await formApi.setValues({
          currentDeptName: currentEmployee.deptName,
          currentPostName: currentEmployee.postName,
        });
      }
      // æ–°å¢žï¼šæ¸…空表单,由用户选择调岗人
      await formApi.setValues({
        userId: undefined,
        currentDeptId: undefined,
        currentDeptName: undefined,
        currentPostId: undefined,
        currentPostName: undefined,
      });
      return;
    }
    modalApi.lock();
    try {
      const transfer = await getTransfer(data.id);
      currentEmployeeId.value = transfer.employeeId;
      await formApi.setValues(transfer);
      // åŽ†å²æ•°æ®å¯èƒ½æ²¡æœ‰åŽŸéƒ¨é—¨/岗位,按调岗人补齐
      if (transfer.userId && !transfer.currentDeptId) {
        await fillCurrentPosition(transfer.userId);
      }
      if (transfer.attachmentList?.length) {
        await formApi.setFieldValue('blobIds', transfer.attachmentList.map(item => ({
          uid: String(item.id),
@@ -112,7 +147,7 @@
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
  <Modal :title="getTitle" class="w-[700px]">
    <Form class="mx-4" />
  </Modal>
</template>
</template>