银川
1.员工管理页面开发、联调
2.人事审批页面开发联调
3.考勤管理页面开发联调
4.薪酬管理页面开发联调
已添加55个文件
已修改1个文件
7050 ■■■■■ 文件已修改
src/api/hrm/attendance/exception/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/record/index.ts 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/education/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/emergency-contact/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/work-history/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/leave/index.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/resignation/index.ts 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/calculation/index.ts 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/employee-social-security/index.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/payment/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/social-security-scheme/index.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/tax-rate-config/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/transfer/index.ts 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/data.ts 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/index.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/modules/form.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/data.ts 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/index.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/modules/import-form.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/components/select-dialog.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/components/select.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/index.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/detail.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/education-list.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/emergency-contact-list.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/form.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/social-security-form.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/social-security-list.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/work-history-list.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/data.ts 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/index.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/modules/form.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/data.ts 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/index.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/modules/form.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/data.ts 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/index.vue 443 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-social-security/data.ts 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-social-security/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-social-security/modules/form.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-social-security/modules/history.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/data.ts 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/data.ts 328 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/modules/form.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/tax-rate-config/data.ts 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/tax-rate-config/index.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/tax-rate-config/modules/form.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/data.ts 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/index.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/modules/form.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/exception/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmAttendanceExceptionApi {
  /** è€ƒå‹¤å¼‚常申请 */
  export interface AttendanceException {
    id?: number;
    userId?: number;
    userName?: string;
    deptName?: string;
    date?: string;
    exceptionType?: number;
    exceptionTypeName?: string;
    reason?: string;
    fileUrl?: string;
    status?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢è€ƒå‹¤å¼‚常申请分页列表 */
export function getAttendanceExceptionPage(params: PageParam) {
  return requestClient.get<PageResult<HrmAttendanceExceptionApi.AttendanceException>>(
    '/hrm/attendance/exception/page',
    { params },
  );
}
/** æŸ¥è¯¢è€ƒå‹¤å¼‚常申请详情 */
export function getAttendanceException(id: number) {
  return requestClient.get<HrmAttendanceExceptionApi.AttendanceException>(
    `/hrm/attendance/exception/get?id=${id}`,
  );
}
/** æ–°å¢žè€ƒå‹¤å¼‚常申请 */
export function createAttendanceException(
  data: HrmAttendanceExceptionApi.AttendanceException,
) {
  return requestClient.post('/hrm/attendance/exception/create', data);
}
/** æäº¤è€ƒå‹¤å¼‚常申请 */
export function submitAttendanceException(id: number) {
  return requestClient.post(`/hrm/attendance/exception/submit?id=${id}`);
}
src/api/hrm/attendance/record/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmAttendanceRecordApi {
  /** è€ƒå‹¤è®°å½• */
  export interface AttendanceRecord {
    id?: number;
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    date?: string;
    clockInTime?: string;
    clockOutTime?: string;
    clockInType?: number;
    clockInTypeName?: string;
    clockOutType?: number;
    clockOutTypeName?: string;
    workHours?: number;
    overtimeHours?: number;
    location?: string;
    dataSource?: number;
    dataSourceName?: string;
    remark?: string;
    createTime?: string;
  }
  /** è€ƒå‹¤ç»Ÿè®¡ */
  export interface AttendanceStatistics {
    userId?: number;
    userName?: string;
    deptName?: string;
    totalDays?: number;
    normalDays?: number;
    lateDays?: number;
    earlyDays?: number;
    absentDays?: number;
    overtimeHours?: number;
    totalWorkHours?: number;
  }
}
/** æŸ¥è¯¢è€ƒå‹¤è®°å½•分页列表 */
export function getAttendanceRecordPage(params: PageParam) {
  return requestClient.get<PageResult<HrmAttendanceRecordApi.AttendanceRecord>>(
    '/hrm/attendance/record/page',
    { params },
  );
}
/** æŸ¥è¯¢è€ƒå‹¤ç»Ÿè®¡ï¼ˆæŒ‰æœˆï¼‰ */
export function getAttendanceStatistics(params: {
  userId?: number;
  deptId?: number;
  month: string;
}) {
  return requestClient.get<HrmAttendanceRecordApi.AttendanceStatistics[]>(
    '/hrm/attendance/record/statistics',
    { params },
  );
}
/** å¯¼å…¥è€ƒå‹¤è®°å½• */
export function importAttendanceRecord(file: File, updateSupport: boolean) {
  return requestClient.upload('/hrm/attendance/record/import', {
    file,
    updateSupport,
  });
}
/** ä¸‹è½½è€ƒå‹¤å¯¼å…¥æ¨¡æ¿ */
export function importAttendanceTemplate() {
  return requestClient.download('/hrm/attendance/record/get-import-template');
}
/** å¯¼å‡ºè€ƒå‹¤è®°å½• */
export function exportAttendanceRecord(params: any) {
  return requestClient.download('/hrm/attendance/record/export-excel', { params });
}
src/api/hrm/employee/education/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import type { HrmEmployeeApi } from '../index';
import { requestClient } from '#/api/request';
/** æŸ¥è¯¢æ•™è‚²ç»åŽ†åˆ—è¡¨ */
export function getEducationListByEmployeeId(employeeId: number) {
  return requestClient.get<HrmEmployeeApi.Education[]>(
    `/hrm/employee/education/list?employeeId=${employeeId}`,
  );
}
/** æ–°å¢žæ•™è‚²ç»åކ */
export function createEducation(data: HrmEmployeeApi.Education) {
  return requestClient.post('/hrm/employee/education/create', data);
}
/** æ›´æ–°æ•™è‚²ç»åކ */
export function updateEducation(data: HrmEmployeeApi.Education) {
  return requestClient.put('/hrm/employee/education/update', data);
}
/** åˆ é™¤æ•™è‚²ç»åކ */
export function deleteEducation(id: number) {
  return requestClient.delete(`/hrm/employee/education/delete?id=${id}`);
}
src/api/hrm/employee/emergency-contact/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import type { HrmEmployeeApi } from '../index';
import { requestClient } from '#/api/request';
/** æŸ¥è¯¢ç´§æ€¥è”系人列表 */
export function getEmergencyContactListByEmployeeId(employeeId: number) {
  return requestClient.get<HrmEmployeeApi.EmergencyContact[]>(
    `/hrm/employee/emergency-contact/list?employeeId=${employeeId}`,
  );
}
/** æ–°å¢žç´§æ€¥è”系人 */
export function createEmergencyContact(data: HrmEmployeeApi.EmergencyContact) {
  return requestClient.post('/hrm/employee/emergency-contact/create', data);
}
/** æ›´æ–°ç´§æ€¥è”系人 */
export function updateEmergencyContact(data: HrmEmployeeApi.EmergencyContact) {
  return requestClient.put('/hrm/employee/emergency-contact/update', data);
}
/** åˆ é™¤ç´§æ€¥è”系人 */
export function deleteEmergencyContact(id: number) {
  return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
}
src/api/hrm/employee/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmEmployeeApi {
  /** å‘˜å·¥ä¿¡æ¯ */
  export interface Employee {
    id?: number;
    userId?: number;
    deptId?: number;
    deptName?: string;
    postId?: number;
    postName?: string;
    employeeNo?: string;
    name?: string;
    gender?: number;
    birthday?: string;
    phone?: string;
    email?: string;
    idCard?: string;
    hireDate?: string;
    regularDate?: string;
    leaveDate?: string;
    employeeStatus?: number;
    annualLeaveBalance?: number;
    sickLeaveBalance?: number;
    baseSalary?: number;
    roleIds?: number[];
    roleNames?: string[];
    bankName?: string;
    bankAccount?: string;
    address?: string;
    remark?: string;
    createTime?: string;
    /** æ•™è‚²ç»åŽ†åˆ—è¡¨ */
    educations?: Education[];
    /** å·¥ä½œç»åŽ†åˆ—è¡¨ */
    workHistories?: WorkHistory[];
    /** ç´§æ€¥è”系人列表 */
    emergencyContacts?: EmergencyContact[];
  }
  /** æ•™è‚²ç»åކ */
  export interface Education {
    id?: number;
    employeeId?: number;
    schoolName?: string;
    major?: string;
    degree?: number;
    startDate?: string;
    endDate?: string;
    certificateNo?: string;
  }
  /** å·¥ä½œç»åކ */
  export interface WorkHistory {
    id?: number;
    employeeId?: number;
    companyName?: string;
    position?: string;
    startDate?: string;
    endDate?: string;
    workContent?: string;
    leaveReason?: string;
  }
  /** ç´§æ€¥è”系人 */
  export interface EmergencyContact {
    id?: number;
    employeeId?: number;
    contactName?: string;
    relationship?: string;
    phone?: string;
    address?: string;
  }
  /** å‘˜å·¥ç²¾ç®€ä¿¡æ¯ */
  export interface EmployeeSimple {
    id: number;
    employeeNo: string;
    name: string;
    deptName?: string;
    postName?: string;
  }
}
/** æŸ¥è¯¢å‘˜å·¥åˆ†é¡µåˆ—表 */
export function getEmployeePage(params: PageParam) {
  return requestClient.get<PageResult<HrmEmployeeApi.Employee>>(
    '/hrm/employee/page',
    { params },
  );
}
/** æŸ¥è¯¢å‘˜å·¥è¯¦æƒ… */
export function getEmployee(id: number) {
  return requestClient.get<HrmEmployeeApi.Employee>(
    `/hrm/employee/get?id=${id}`,
  );
}
/** æŸ¥è¯¢å‘˜å·¥ç²¾ç®€åˆ—表 */
export function getEmployeeSimpleList() {
  return requestClient.get<HrmEmployeeApi.EmployeeSimple[]>(
    '/hrm/employee/simple-list',
  );
}
/** æ–°å¢žå‘˜å·¥ */
export function createEmployee(data: HrmEmployeeApi.Employee) {
  return requestClient.post('/hrm/employee/create', data);
}
/** æ›´æ–°å‘˜å·¥ */
export function updateEmployee(data: HrmEmployeeApi.Employee) {
  return requestClient.put('/hrm/employee/update', data);
}
/** åˆ é™¤å‘˜å·¥ */
export function deleteEmployee(id: number) {
  return requestClient.delete(`/hrm/employee/delete?id=${id}`);
}
/** å¯¼å‡ºå‘˜å·¥ */
export function exportEmployee(params: any) {
  return requestClient.download('/hrm/employee/export-excel', { params });
}
src/api/hrm/employee/work-history/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import type { HrmEmployeeApi } from '../index';
import { requestClient } from '#/api/request';
/** æŸ¥è¯¢å·¥ä½œç»åŽ†åˆ—è¡¨ */
export function getWorkHistoryListByEmployeeId(employeeId: number) {
  return requestClient.get<HrmEmployeeApi.WorkHistory[]>(
    `/hrm/employee/work-history/list?employeeId=${employeeId}`,
  );
}
/** æ–°å¢žå·¥ä½œç»åކ */
export function createWorkHistory(data: HrmEmployeeApi.WorkHistory) {
  return requestClient.post('/hrm/employee/work-history/create', data);
}
/** æ›´æ–°å·¥ä½œç»åކ */
export function updateWorkHistory(data: HrmEmployeeApi.WorkHistory) {
  return requestClient.put('/hrm/employee/work-history/update', data);
}
/** åˆ é™¤å·¥ä½œç»åކ */
export function deleteWorkHistory(id: number) {
  return requestClient.delete(`/hrm/employee/work-history/delete?id=${id}`);
}
src/api/hrm/leave/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmLeaveApi {
  /** è¯·å‡ç”³è¯· */
  export interface Leave {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    deptName?: string;
    leaveType?: number;
    startTime?: string;
    endTime?: string;
    duration?: number;
    reason?: string;
    fileUrl?: string;
    status?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢è¯·å‡ç”³è¯·åˆ†é¡µåˆ—表 */
export function getLeavePage(params: PageParam) {
  return requestClient.get<PageResult<HrmLeaveApi.Leave>>(
    '/hrm/leave/page',
    { params },
  );
}
/** æŸ¥è¯¢è¯·å‡ç”³è¯·è¯¦æƒ… */
export function getLeave(id: number) {
  return requestClient.get<HrmLeaveApi.Leave>(`/hrm/leave/get?id=${id}`);
}
/** æ–°å¢žè¯·å‡ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰ */
export function createLeave(data: HrmLeaveApi.Leave) {
  return requestClient.post('/hrm/leave/create', data);
}
/** æ›´æ–°è¯·å‡ç”³è¯· */
export function updateLeave(data: HrmLeaveApi.Leave) {
  return requestClient.put('/hrm/leave/update', data);
}
/** åˆ é™¤è¯·å‡ç”³è¯· */
export function deleteLeave(id: number) {
  return requestClient.delete(`/hrm/leave/delete?id=${id}`);
}
/** æäº¤è¯·å‡ç”³è¯·ï¼ˆå‘起审批) */
export function submitLeave(id: number) {
  return requestClient.post(`/hrm/leave/submit?id=${id}`);
}
/** æ’¤é”€è¯·å‡ç”³è¯· */
export function cancelLeave(id: number) {
  return requestClient.post(`/hrm/leave/cancel?id=${id}`);
}
/** èŽ·å–å¯ç”¨æµç¨‹å®šä¹‰åˆ—è¡¨ */
export function getLeaveApproveProcessDefinitionList() {
  return requestClient.get<any[]>('/hrm/leave/approve-process-definition-list');
}
src/api/hrm/resignation/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmResignationApi {
  /** ç¦»èŒç”³è¯· */
  export interface Resignation {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    deptName?: string;
    postName?: string;
    resignationType?: number;
    expectedDate?: string;
    reason?: string;
    handoverUserId?: number;
    handoverUserName?: string;
    handoverStatus?: number;
    fileUrl?: string;
    status?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢ç¦»èŒç”³è¯·åˆ†é¡µåˆ—表 */
export function getResignationPage(params: PageParam) {
  return requestClient.get<PageResult<HrmResignationApi.Resignation>>(
    '/hrm/resignation/page',
    { params },
  );
}
/** æŸ¥è¯¢ç¦»èŒç”³è¯·è¯¦æƒ… */
export function getResignation(id: number) {
  return requestClient.get<HrmResignationApi.Resignation>(
    `/hrm/resignation/get?id=${id}`,
  );
}
/** æ–°å¢žç¦»èŒç”³è¯·ï¼ˆè‰ç¨¿ï¼‰ */
export function createResignation(data: HrmResignationApi.Resignation) {
  return requestClient.post('/hrm/resignation/create', data);
}
/** æ›´æ–°ç¦»èŒç”³è¯· */
export function updateResignation(data: HrmResignationApi.Resignation) {
  return requestClient.put('/hrm/resignation/update', data);
}
/** åˆ é™¤ç¦»èŒç”³è¯· */
export function deleteResignation(id: number) {
  return requestClient.delete(`/hrm/resignation/delete?id=${id}`);
}
/** æäº¤ç¦»èŒç”³è¯·ï¼ˆå‘起审批) */
export function submitResignation(id: number) {
  return requestClient.post(`/hrm/resignation/submit?id=${id}`);
}
/** æ’¤é”€ç¦»èŒç”³è¯· */
export function cancelResignation(id: number) {
  return requestClient.post(`/hrm/resignation/cancel?id=${id}`);
}
/** æ›´æ–°äº¤æŽ¥çŠ¶æ€ */
export function updateHandoverStatus(id: number, handoverStatus: number) {
  return requestClient.put('/hrm/resignation/handover', { id, handoverStatus });
}
/** èŽ·å–å¯ç”¨æµç¨‹å®šä¹‰åˆ—è¡¨ */
export function getResignationApproveProcessDefinitionList() {
  return requestClient.get<any[]>('/hrm/resignation/approve-process-definition-list');
}
src/api/hrm/salary/calculation/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmSalaryCalculationApi {
  /** è–ªé…¬æ ¸ç®— */
  export interface SalaryCalculation {
    id?: number;
    no?: string;
    period?: string;
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    baseSalary?: number;
    performanceSalary?: number;
    overtimePay?: number;
    mealAllowance?: number;
    transportAllowance?: number;
    otherAllowance?: number;
    socialSecurityDeduction?: number;
    housingFundDeduction?: number;
    taxDeduction?: number;
    otherDeduction?: number;
    actualSalary?: number;
    workDays?: number;
    overtimeHours?: number;
    status?: number;
    statusName?: string;
    remark?: string;
    createTime?: string;
  }
  /** è–ªé…¬æ ¸ç®—查询参数 */
  export interface SalaryCalculationPageParam extends PageParam {
    period?: string;
    userId?: number;
    deptId?: number;
    status?: number;
  }
  /** è–ªé…¬æ ¸ç®—预览参数 */
  export interface SalaryPreviewParam {
    period: string;
    deptId?: number;
  }
  /** è–ªé…¬æ ¸ç®—保存参数 */
  export interface SalarySaveParam {
    period: string;
    list: SalaryCalculation[];
  }
  /** è–ªé…¬æ ¸ç®—执行参数 */
  export interface SalaryCalculateParam {
    period: string;
    deptId?: number;
  }
}
/** æŸ¥è¯¢è–ªé…¬æ ¸ç®—分页列表 */
export function getSalaryCalculationPage(
  params: HrmSalaryCalculationApi.SalaryCalculationPageParam,
) {
  return requestClient.get<PageResult<HrmSalaryCalculationApi.SalaryCalculation>>(
    '/hrm/salary/calculation/page',
    { params },
  );
}
/** æŸ¥è¯¢è–ªé…¬æ ¸ç®—详情 */
export function getSalaryCalculation(id: number) {
  return requestClient.get<HrmSalaryCalculationApi.SalaryCalculation>(
    `/hrm/salary/calculation/get?id=${id}`,
  );
}
/** æ‰§è¡Œè–ªé…¬æ ¸ç®—(自动计算) */
export function calculateSalary(params: HrmSalaryCalculationApi.SalaryCalculateParam) {
  return requestClient.post('/hrm/salary/calculation/calculate', null, { params });
}
/** é¢„览薪酬核算数据(根据周期和部门获取员工工资明细) */
export function previewSalaryCalculation(params: HrmSalaryCalculationApi.SalaryPreviewParam) {
  return requestClient.get<HrmSalaryCalculationApi.SalaryCalculation[]>(
    '/hrm/salary/calculation/preview',
    { params },
  );
}
/** æ‰¹é‡ä¿å­˜è–ªé…¬æ ¸ç®—记录 */
export function saveSalaryCalculation(data: HrmSalaryCalculationApi.SalarySaveParam) {
  return requestClient.post('/hrm/salary/calculation/save', data);
}
/** æ›´æ–°è–ªé…¬æ ¸ç®—记录 */
export function updateSalaryCalculation(data: HrmSalaryCalculationApi.SalaryCalculation) {
  return requestClient.put('/hrm/salary/calculation/update', data);
}
/** ç¡®è®¤è–ªé…¬æ ¸ç®— */
export function confirmSalary(ids: number[]) {
  return requestClient.post('/hrm/salary/calculation/confirm', { ids });
}
/** å¯¼å‡ºè–ªé…¬æ ¸ç®—表 */
export function exportSalaryCalculation(params: any) {
  return requestClient.download('/hrm/salary/calculation/export-excel', { params });
}
src/api/hrm/salary/employee-social-security/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmEmployeeSocialSecurityApi {
  /** å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘档案 */
  export interface EmployeeSocialSecurity {
    id?: number;
    userId?: number;
    userName?: string;
    deptName?: string;
    socialSecuritySchemeId?: number;
    schemeName?: string;
    socialSecurityBase?: number;
    housingFundBase?: number;
    effectiveDate?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  /** å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘档案查询参数 */
  export interface EmployeeSocialSecurityPageParam extends PageParam {
    userName?: string;
    deptId?: number;
  }
}
/** èŽ·å–å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘æ¡£æ¡ˆåˆ†é¡µåˆ—è¡¨ */
export function getEmployeeSocialSecurityPage(
  params: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurityPageParam,
) {
  return requestClient.get<PageResult<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>>(
    '/hrm/salary/employee-social-security/page',
    { params },
  );
}
/** èŽ·å–å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘æ¡£æ¡ˆè¯¦æƒ… */
export function getEmployeeSocialSecurity(id: number) {
  return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>(
    `/hrm/salary/employee-social-security/get?id=${id}`,
  );
}
/** èŽ·å–å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘æ¡£æ¡ˆåŽ†å²åˆ—è¡¨ */
export function getEmployeeSocialSecurityListByUser(userId: number) {
  return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>(
    `/hrm/salary/employee-social-security/list-by-user?userId=${userId}`,
  );
}
/** èŽ·å–å‘˜å·¥æœ€æ–°ç”Ÿæ•ˆçš„ç¤¾ä¿å…¬ç§¯é‡‘æ¡£æ¡ˆ */
export function getLatestEmployeeSocialSecurity(userId: number) {
  return requestClient.get<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>(
    `/hrm/salary/employee-social-security/get-latest?userId=${userId}`,
  );
}
/** åˆ›å»ºå‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘档案 */
export function createEmployeeSocialSecurity(
  data: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity,
) {
  return requestClient.post('/hrm/salary/employee-social-security/create', data);
}
/** æ›´æ–°å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘档案 */
export function updateEmployeeSocialSecurity(
  data: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity,
) {
  return requestClient.put('/hrm/salary/employee-social-security/update', data);
}
/** åˆ é™¤å‘˜å·¥ç¤¾ä¿å…¬ç§¯é‡‘档案 */
export function deleteEmployeeSocialSecurity(id: number) {
  return requestClient.delete(`/hrm/salary/employee-social-security/delete?id=${id}`);
}
src/api/hrm/salary/payment/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmSalaryPaymentApi {
  /** è–ªèµ„发放记录 */
  export interface SalaryPayment {
    id?: number;
    no?: string;
    period?: string;
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    actualSalary?: number;
    paymentMethod?: number;
    paymentTime?: string;
    paymentNo?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  /** è–ªèµ„发放查询参数 */
  export interface SalaryPaymentPageParam extends PageParam {
    period?: string;
    userId?: number;
    deptId?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢è–ªèµ„发放分页列表 */
export function getSalaryPaymentPage(
  params: HrmSalaryPaymentApi.SalaryPaymentPageParam,
) {
  return requestClient.get<PageResult<HrmSalaryPaymentApi.SalaryPayment>>(
    '/hrm/salary/payment/page',
    { params },
  );
}
/** æŸ¥è¯¢è–ªèµ„发放详情 */
export function getSalaryPayment(id: number) {
  return requestClient.get<HrmSalaryPaymentApi.SalaryPayment>(
    `/hrm/salary/payment/get?id=${id}`,
  );
}
/** æ‰§è¡Œè–ªèµ„发放 */
export function paySalary(ids: number[]) {
  return requestClient.post('/hrm/salary/payment/pay', { ids });
}
/** æ’¤é”€è–ªèµ„发放 */
export function revokeSalaryPayment(id: number) {
  return requestClient.post(`/hrm/salary/payment/revoke?id=${id}`);
}
/** å¯¼å‡ºè–ªèµ„发放表 */
export function exportSalaryPayment(params: any) {
  return requestClient.download('/hrm/salary/payment/export-excel', { params });
}
src/api/hrm/salary/social-security-scheme/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
import type { PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmSocialSecuritySchemeApi {
  /** ç¤¾ä¿å…¬ç§¯é‡‘方案 */
  export interface SocialSecurityScheme {
    id?: number;
    name?: string;
    effectiveDate?: string;
    pensionCompanyRatio?: number;
    pensionPersonalRatio?: number;
    medicalCompanyRatio?: number;
    medicalPersonalRatio?: number;
    unemploymentCompanyRatio?: number;
    unemploymentPersonalRatio?: number;
    workInjuryRatio?: number;
    maternityRatio?: number;
    housingFundCompanyRatio?: number;
    housingFundPersonalRatio?: number;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  /** ç¤¾ä¿å…¬ç§¯é‡‘方案查询参数 */
  export interface SocialSecuritySchemeQueryParam {
    name?: string;
    status?: number;
  }
}
/** èŽ·å–ç¤¾ä¿å…¬ç§¯é‡‘æ–¹æ¡ˆåˆ—è¡¨ */
export function getSocialSecuritySchemeList(params?: HrmSocialSecuritySchemeApi.SocialSecuritySchemeQueryParam) {
  return requestClient.get<HrmSocialSecuritySchemeApi.SocialSecurityScheme[]>(
    '/hrm/salary/social-security-scheme/list',
    { params },
  );
}
/** èŽ·å–ç¤¾ä¿å…¬ç§¯é‡‘æ–¹æ¡ˆè¯¦æƒ… */
export function getSocialSecurityScheme(id: number) {
  return requestClient.get<HrmSocialSecuritySchemeApi.SocialSecurityScheme>(
    `/hrm/salary/social-security-scheme/get?id=${id}`,
  );
}
/** åˆ›å»ºç¤¾ä¿å…¬ç§¯é‡‘方案 */
export function createSocialSecurityScheme(
  data: HrmSocialSecuritySchemeApi.SocialSecurityScheme,
) {
  return requestClient.post('/hrm/salary/social-security-scheme/create', data);
}
/** æ›´æ–°ç¤¾ä¿å…¬ç§¯é‡‘方案 */
export function updateSocialSecurityScheme(
  data: HrmSocialSecuritySchemeApi.SocialSecurityScheme,
) {
  return requestClient.put('/hrm/salary/social-security-scheme/update', data);
}
/** åˆ é™¤ç¤¾ä¿å…¬ç§¯é‡‘方案 */
export function deleteSocialSecurityScheme(id: number) {
  return requestClient.delete(`/hrm/salary/social-security-scheme/delete?id=${id}`);
}
src/api/hrm/salary/tax-rate-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
import type { PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmTaxRateConfigApi {
  /** ä¸ªç¨Žç¨ŽçŽ‡é…ç½® */
  export interface TaxRateConfig {
    id?: number;
    name?: string;
    effectiveDate?: string;
    level?: number;
    minAmount?: number;
    maxAmount?: number;
    taxRate?: number;
    quickDeduction?: number;
    createTime?: string;
  }
}
/** èŽ·å–ä¸ªç¨Žç¨ŽçŽ‡é…ç½®åˆ—è¡¨ */
export function getTaxRateConfigList() {
  return requestClient.get<HrmTaxRateConfigApi.TaxRateConfig[]>(
    '/hrm/salary/tax-rate-config/list',
  );
}
/** èŽ·å–ä¸ªç¨Žç¨ŽçŽ‡é…ç½®è¯¦æƒ… */
export function getTaxRateConfig(id: number) {
  return requestClient.get<HrmTaxRateConfigApi.TaxRateConfig>(
    `/hrm/salary/tax-rate-config/get?id=${id}`,
  );
}
/** èŽ·å–ç”Ÿæ•ˆçš„ä¸ªç¨Žç¨ŽçŽ‡é…ç½®åˆ—è¡¨ */
export function getEffectiveTaxRateConfigList() {
  return requestClient.get<HrmTaxRateConfigApi.TaxRateConfig[]>(
    '/hrm/salary/tax-rate-config/get-effective',
  );
}
/** æ‰¹é‡åˆ›å»ºä¸ªç¨Žç¨ŽçŽ‡é…ç½® */
export function createTaxRateConfigList(
  data: HrmTaxRateConfigApi.TaxRateConfig[],
) {
  return requestClient.post('/hrm/salary/tax-rate-config/create-list', data);
}
/** æ›´æ–°ä¸ªç¨Žç¨ŽçŽ‡é…ç½® */
export function updateTaxRateConfig(data: HrmTaxRateConfigApi.TaxRateConfig) {
  return requestClient.put('/hrm/salary/tax-rate-config/update', data);
}
/** åˆ é™¤ä¸ªç¨Žç¨ŽçŽ‡é…ç½® */
export function deleteTaxRateConfig(id: number) {
  return requestClient.delete(`/hrm/salary/tax-rate-config/delete?id=${id}`);
}
src/api/hrm/transfer/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmTransferApi {
  /** è°ƒå²—申请 */
  export interface Transfer {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    currentDeptId?: number;
    currentDeptName?: string;
    currentPostId?: number;
    currentPostName?: string;
    targetDeptId?: number;
    targetDeptName?: string;
    targetPostId?: number;
    targetPostName?: string;
    transferType?: number;
    effectiveDate?: string;
    reason?: string;
    fileUrl?: string;
    status?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢è°ƒå²—申请分页列表 */
export function getTransferPage(params: PageParam) {
  return requestClient.get<PageResult<HrmTransferApi.Transfer>>(
    '/hrm/transfer/page',
    { params },
  );
}
/** æŸ¥è¯¢è°ƒå²—申请详情 */
export function getTransfer(id: number) {
  return requestClient.get<HrmTransferApi.Transfer>(`/hrm/transfer/get?id=${id}`);
}
/** æ–°å¢žè°ƒå²—申请(草稿) */
export function createTransfer(data: HrmTransferApi.Transfer) {
  return requestClient.post('/hrm/transfer/create', data);
}
/** æ›´æ–°è°ƒå²—申请 */
export function updateTransfer(data: HrmTransferApi.Transfer) {
  return requestClient.put('/hrm/transfer/update', data);
}
/** åˆ é™¤è°ƒå²—申请 */
export function deleteTransfer(id: number) {
  return requestClient.delete(`/hrm/transfer/delete?id=${id}`);
}
/** æäº¤è°ƒå²—申请(发起审批) */
export function submitTransfer(id: number) {
  return requestClient.post(`/hrm/transfer/submit?id=${id}`);
}
/** æ’¤é”€è°ƒå²—申请 */
export function cancelTransfer(id: number) {
  return requestClient.post(`/hrm/transfer/cancel?id=${id}`);
}
/** èŽ·å–å¯ç”¨æµç¨‹å®šä¹‰åˆ—è¡¨ */
export function getTransferApproveProcessDefinitionList() {
  return requestClient.get<any[]>('/hrm/transfer/approve-process-definition-list');
}
src/packages/constants/src/dict-enum.ts
@@ -294,12 +294,28 @@
  STOCK_BIZ_TYPE: 'stock_biz_type', // åº“存业务类型
} as const;
/** ========== HRM - äººåŠ›èµ„æºæ¨¡å— ========== */
const HRM_DICT = {
  HRM_DEGREE: 'hrm_degree', // HRM å­¦åކ
  HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM å‘˜å·¥çŠ¶æ€
  HRM_GENDER: 'hrm_gender', // HRM æ€§åˆ«
  HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM å®¡æ‰¹çŠ¶æ€
  HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM è¯·å‡ç±»åž‹
  HRM_TRANSFER_TYPE: 'hrm_transfer_type', // HRM è°ƒå²—类型
  HRM_RESIGNATION_TYPE: 'hrm_resignation_type', // HRM ç¦»èŒç±»åž‹
  HRM_CLOCK_TYPE: 'hrm_clock_type', // HRM æ‰“卡类型
  HRM_SALARY_STATUS: 'hrm_salary_status', // HRM è–ªèµ„状态
  HRM_HANDOVER_STATUS: 'hrm_handover_status', // HRM äº¤æŽ¥çŠ¶æ€
  HRM_PAYMENT_METHOD: 'hrm_payment_method', // HRM å‘放方式
} as const;
/** å­—典类型枚举 - ç»Ÿä¸€å¯¼å‡º */
const DICT_TYPE = {
  ...AI_DICT,
  ...BPM_DICT,
  ...CRM_DICT,
  ...ERP_DICT,
  ...HRM_DICT,
  ...INFRA_DICT,
  ...IM_DICT,
  ...IOT_DICT,
src/views/hrm/attendance/exception/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail';
/** æ–°å¢žè€ƒå‹¤å¼‚常申请的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'date',
      label: '考勤日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择考勤日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'exceptionType',
      label: '异常类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择异常类型',
        options: [
          { label: '漏打卡', value: 1 },
          { label: '迟到', value: 2 },
          { label: '早退', value: 3 },
          { label: '其他', value: 4 },
        ],
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'reason',
      label: '异常原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入异常原因',
        rows: 3,
        maxlength: 500,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'date',
      label: '考勤日期',
      component: 'RangePicker',
      componentProps: {
        allowClear: true,
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmAttendanceExceptionApi.AttendanceException>['columns'] {
  return [
    { field: 'userName', title: '申请人', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'date', title: '考勤日期', width: 110 },
    { field: 'exceptionTypeName', title: '异常类型', width: 100 },
    { field: 'reason', title: '异常原因', minWidth: 200 },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 150,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/attendance/exception/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAttendanceExceptionPage, submitAttendanceException } from '#/api/hrm/attendance/exception';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè€ƒå‹¤å¼‚常申请 */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è€ƒå‹¤å¼‚常申请 */
function handleDetail(row: HrmAttendanceExceptionApi.AttendanceException) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** æäº¤è€ƒå‹¤å¼‚常申请 */
async function handleSubmit(row: HrmAttendanceExceptionApi.AttendanceException) {
  await submitAttendanceException(row.id!);
  message.success('提交成功');
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getAttendanceExceptionPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmAttendanceExceptionApi.AttendanceException>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="考勤异常申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['考勤异常申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:attendance-exception:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '查看',
              type: 'link',
              onClick: handleDetail.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['hrm:attendance-exception:submit'],
              ifShow: row.status === 0,
              onClick: handleSubmit.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/attendance/exception/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createAttendanceException, getAttendanceException } from '#/api/hrm/attendance/exception';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看考勤异常申请';
  }
  return '新增考勤异常申请';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmAttendanceExceptionApi.AttendanceException;
    try {
      await createAttendanceException(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;
    formApi.setState({ schema: useFormSchema(data.formType) });
    formApi.setDisabled(data.formType === 'detail');
    modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getAttendanceException(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/attendance/record/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceRecordApi } from '#/api/hrm/attendance/record';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getDeptList } from '#/api/system/dept';
import { handleTree } from '#/packages/utils/src';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'userName',
      label: '员工姓名',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工姓名',
      },
    },
    {
      fieldName: 'date',
      label: '考勤日期',
      component: 'RangePicker',
      componentProps: {
        allowClear: true,
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
    {
      fieldName: 'clockInType',
      label: '上班打卡类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CLOCK_TYPE, 'number'),
        placeholder: '请选择上班打卡类型',
      },
    },
    {
      fieldName: 'clockOutType',
      label: '下班打卡类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CLOCK_TYPE, 'number'),
        placeholder: '请选择下班打卡类型',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmAttendanceRecordApi.AttendanceRecord>['columns'] {
  return [
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'date', title: '考勤日期', width: 110 },
    { field: 'clockInTime', title: '上班打卡时间', width: 160 },
    {
      field: 'clockInType',
      title: '上班打卡类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CLOCK_TYPE },
      },
    },
    { field: 'clockOutTime', title: '下班打卡时间', width: 160 },
    {
      field: 'clockOutType',
      title: '下班打卡类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CLOCK_TYPE },
      },
    },
    { field: 'workHours', title: '工作时长(h)', width: 100 },
    { field: 'overtimeHours', title: '加班时长(h)', width: 100 },
    { field: 'location', title: '打卡地点', minWidth: 150 },
    {
      field: 'dataSource',
      title: '数据来源',
      width: 100,
    },
    {
      title: '操作',
      width: 120,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/attendance/record/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmAttendanceRecordApi } from '#/api/hrm/attendance/record';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message } 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 ImportForm from './modules/import-form.vue';
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å…¥è€ƒå‹¤è®°å½• */
function handleImport() {
  importModalApi.open();
}
/** å¯¼å‡ºè€ƒå‹¤è®°å½• */
async function handleExport() {
  const data = await exportAttendanceRecord(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '考勤记录.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getAttendanceRecordPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmAttendanceRecordApi.AttendanceRecord>,
});
</script>
<template>
  <Page auto-content-height>
    <ImportModal @success="handleRefresh" />
    <Grid table-title="考勤记录列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['hrm:attendance-record:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['hrm:attendance-record:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/attendance/record/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message, Switch, Upload } from 'ant-design-vue';
import { importAttendanceRecord, importAttendanceTemplate } from '#/api/hrm/attendance/record';
const emit = defineEmits(['success']);
const fileList = ref<FileType[]>([]);
const updateSupport = ref(false);
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (fileList.value.length === 0) {
      message.warning('请选择要导入的文件');
      return;
    }
    modalApi.lock();
    try {
      await importAttendanceRecord(fileList.value[0] as File, updateSupport.value);
      await modalApi.close();
      emit('success');
      message.success('导入成功');
    } finally {
      modalApi.unlock();
    }
  },
});
function beforeUpload(file: FileType) {
  fileList.value = [file];
  return false;
}
function handleRemove() {
  fileList.value = [];
}
async function handleDownloadTemplate() {
  const data = await importAttendanceTemplate();
  downloadFileFromBlobPart({ fileName: '考勤导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入考勤记录" class="w-[500px]">
    <div class="mx-4">
      <Upload
        :max-count="1"
        accept=".xls,.xlsx"
        :file-list="fileList"
        :before-upload="beforeUpload"
        @remove="handleRemove"
      >
        <Button type="primary">选择 Excel æ–‡ä»¶</Button>
      </Upload>
      <div class="mt-4">
        <span class="mr-2">是否覆盖已存在的数据:</span>
        <Switch v-model:checked="updateSupport" />
      </div>
      <div class="mt-2 text-gray-500">
        ä»…允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶
      </div>
    </div>
    <template #prepend-footer>
      <Button @click="handleDownloadTemplate">下载导入模板</Button>
    </template>
  </Modal>
</template>
src/views/hrm/employee/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as HrmEmployeeSelect } from './select.vue';
export { default as HrmEmployeeSelectDialog } from './select-dialog.vue';
src/views/hrm/employee/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getEmployeePage } from '#/api/hrm/employee';
import {
  useEmployeeSelectGridColumns,
  useEmployeeSelectGridFormSchema,
} from '../data';
defineOptions({ name: 'HrmEmployeeSelectDialog' });
const emit = defineEmits<{
  selected: [rows: HrmEmployeeApi.Employee[]];
}>();
const open = ref(false);
const multiple = ref(false);
const selectedRows = ref<HrmEmployeeApi.Employee[]>([]);
const preSelectedIds = ref<number[]>([]);
const filterDeptId = ref<number>();
/** èŽ·å–å½“å‰è¡¨æ ¼æ•°æ® */
function getTableRows() {
  return gridApi.grid.getTableData().fullData as HrmEmployeeApi.Employee[];
}
/** èŽ·å–å¤šé€‰è®°å½• */
function getMultipleSelectedRows() {
  const selectedMap = new Map<number, HrmEmployeeApi.Employee>();
  const records = [
    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
  ] as HrmEmployeeApi.Employee[];
  records.forEach((row) => {
    const rowId = row.id;
    if (rowId !== undefined && rowId !== null) {
      selectedMap.set(rowId, row);
    }
  });
  return [...selectedMap.values()];
}
/** å¤„理勾选变化 */
function handleCheckboxSelectChange() {
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理单选变化 */
function handleRadioChange(row: HrmEmployeeApi.Employee) {
  selectedRows.value = [row];
}
/** å¤šé€‰æ¨¡å¼ä¸‹åˆ‡æ¢è¡Œå‹¾é€‰ */
async function toggleMultipleRow(row: HrmEmployeeApi.Employee) {
  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
  await gridApi.grid.setCheckboxRow(row, !selected);
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理行双击 */
async function handleCellDblclick({ row }: { row: HrmEmployeeApi.Employee }) {
  if (multiple.value) {
    await toggleMultipleRow(row);
    return;
  }
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选员工 */
async function applyPreSelection() {
  if (preSelectedIds.value.length === 0) {
    return;
  }
  const rows = getTableRows();
  for (const row of rows) {
    if (row.id === undefined || row.id === null || !preSelectedIds.value.includes(row.id)) {
      continue;
    }
    if (multiple.value) {
      await gridApi.grid.setCheckboxRow(row, true);
    } else {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
  if (multiple.value) {
    selectedRows.value = getMultipleSelectedRows();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useEmployeeSelectGridFormSchema(),
  },
  gridOptions: {
    columns: useEmployeeSelectGridColumns(false),
    height: 400,
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      range: true,
      reserve: true,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getEmployeePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
            deptId: filterDeptId.value,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmEmployeeApi.Employee>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    checkboxAll: handleCheckboxSelectChange,
    checkboxChange: handleCheckboxSelectChange,
    radioChange: ({ row }: { row: HrmEmployeeApi.Employee }) => {
      handleRadioChange(row);
    },
  },
});
/** é‡ç½®æŸ¥è¯¢å’Œé€‰æ‹©çŠ¶æ€ */
async function resetQueryState() {
  selectedRows.value = [];
  filterDeptId.value = undefined;
  await gridApi.grid.clearCheckboxRow();
  await gridApi.grid.clearCheckboxReserve();
  await gridApi.grid.clearRadioRow();
  await gridApi.formApi.resetForm();
}
/** æ‰“开员工选择弹窗 */
async function openModal(
  selectedIds?: number[],
  options?: { multiple?: boolean; deptId?: number },
) {
  open.value = true;
  multiple.value = options?.multiple ?? false;
  filterDeptId.value = options?.deptId;
  preSelectedIds.value = selectedIds || [];
  await nextTick();
  gridApi.setGridOptions({
    columns: useEmployeeSelectGridColumns(multiple.value),
  });
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­å‘˜å·¥é€‰æ‹©å¼¹çª— */
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
/** ç¡®è®¤é€‰æ‹©å‘˜å·¥ */
function handleConfirm() {
  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
  if (rows.length === 0) {
    message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
    return;
  }
  emit('selected', multiple.value ? rows : [rows[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="员工选择"
    width="800px"
    :destroy-on-close="true"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <Grid table-title="员工列表" />
  </Modal>
</template>
src/views/hrm/employee/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
<script lang="ts" setup>
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '#/packages/icons/src';
import { Input, Tooltip } from 'ant-design-vue';
import { getEmployee } from '#/api/hrm/employee';
import HrmEmployeeSelectDialog from './select-dialog.vue';
defineOptions({ name: 'HrmEmployeeSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
    deptId?: number;
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择员工',
    deptId: undefined,
  },
);
const emit = defineEmits<{
  change: [item: HrmEmployeeApi.Employee | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof HrmEmployeeSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<HrmEmployeeApi.Employee>();
const displayLabel = computed(() => selectedItem.value?.name ?? '');
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== null,
);
/** æ ¹æ®å‘˜å·¥ID回显选择器 */
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.id === id) {
    return;
  }
  selectedItem.value = await getEmployee(id);
}
watch(
  () => props.modelValue,
  (value) => {
    resolveItemById(value);
  },
  { immediate: true },
);
/** æ¸…空已选员工 */
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
/** æ‰“开员工选择弹窗 */
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  const selectedIds = (props.modelValue === null ? [] : [props.modelValue]) as number[];
  dialogRef.value?.open(selectedIds, { multiple: false, deptId: props.deptId });
}
/** å›žå¡«é€‰ä¸­çš„员工 */
function handleSelected(rows: HrmEmployeeApi.Employee[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>员工编号:{{ selectedItem.employeeNo || '-' }}</div>
          <div>员工姓名:{{ selectedItem.name || '-' }}</div>
          <div v-if="selectedItem.deptName">部门:{{ selectedItem.deptName }}</div>
          <div v-if="selectedItem.postName">岗位:{{ selectedItem.postName }}</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <HrmEmployeeSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>
src/views/hrm/employee/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,379 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { h } from 'vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { getDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getSimpleRoleList } from '#/api/system/role';
import { handleTree } from '#/packages/utils/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改员工的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'userId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'employeeNo',
      label: '员工编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入员工编号',
      },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '员工姓名',
      component: 'Input',
      componentProps: {
        placeholder: '请输入员工姓名',
      },
      rules: 'required',
    },
    {
      fieldName: 'gender',
      label: '性别',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.HRM_GENDER, 'number'),
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'phone',
      label: '手机号(登录账号)',
      component: 'Input',
      componentProps: {
        placeholder: '请输入手机号码,将作为系统登录账号',
      },
      rules: 'required',
      help: '手机号将作为系统登录账号,默认密码与手机号相同',
    },
    {
      fieldName: 'deptId',
      label: '归属部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        placeholder: '请选择归属部门',
        treeDefaultExpandAll: true,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'postId',
      label: '岗位',
      component: 'ApiSelect',
      componentProps: {
        api: getSimplePostList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择岗位',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'roleIds',
      label: '用户角色',
      component: 'ApiSelect',
      componentProps: {
        api: getSimpleRoleList,
        labelField: 'name',
        valueField: 'id',
        mode: 'multiple',
        placeholder: '请选择角色',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'hireDate',
      label: '入职日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择入职日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'regularDate',
      label: '转正日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择转正日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'employeeStatus',
      label: '员工状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_STATUS, 'number'),
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'birthday',
      label: '出生日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择出生日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'email',
      label: '邮箱',
      component: 'Input',
      componentProps: {
        placeholder: '请输入邮箱',
      },
    },
    {
      fieldName: 'idCard',
      label: '身份证号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入身份证号',
      },
    },
    {
      fieldName: 'annualLeaveBalance',
      label: '年假余额',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入年假余额',
        min: 0,
        precision: 1,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'sickLeaveBalance',
      label: '病假余额',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入病假余额',
        min: 0,
        precision: 1,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'baseSalary',
      label: '基本工资',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入基本工资',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'bankName',
      label: '开户银行',
      component: 'Input',
      componentProps: {
        placeholder: '请输入开户银行',
      },
    },
    {
      fieldName: 'bankAccount',
      label: '银行账号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入银行账号',
      },
    },
    {
      fieldName: 'address',
      label: '居住地址',
      component: 'Input',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入居住地址',
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'employeeNo',
      label: '员工编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工编号',
      },
    },
    {
      fieldName: 'name',
      label: '员工姓名',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工姓名',
      },
    },
    {
      fieldName: 'deptId',
      label: '归属部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择归属部门',
        treeDefaultExpandAll: true,
      },
    },
    {
      fieldName: 'employeeStatus',
      label: '员工状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_STATUS, 'number'),
        placeholder: '请选择员工状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmEmployeeApi.Employee>['columns'] {
  return [
    {
      field: 'employeeNo',
      title: '员工编号',
      minWidth: 120,
      slots: {
        default: 'employeeNo',
      },
    },
    { field: 'name', title: '员工姓名', minWidth: 100 },
    {
      field: 'gender',
      title: '性别',
      width: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_GENDER },
      },
    },
    { field: 'phone', title: '手机号码', minWidth: 120 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'postName', title: '岗位', minWidth: 120 },
    {
      field: 'employeeStatus',
      title: '员工状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_EMPLOYEE_STATUS },
      },
    },
    { field: 'hireDate', title: '入职日期', width: 110 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 180,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
/** å‘˜å·¥é€‰æ‹©å¼¹çª—的搜索表单 */
export function useEmployeeSelectGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'employeeNo',
      label: '员工编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工编号',
      },
    },
    {
      fieldName: 'name',
      label: '员工姓名',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工姓名',
      },
    },
  ];
}
/** å‘˜å·¥é€‰æ‹©å¼¹çª—的列 */
export function useEmployeeSelectGridColumns(multiple: boolean): VxeTableGridOptions<HrmEmployeeApi.Employee>['columns'] {
  return [
    {
      type: multiple ? 'checkbox' : 'radio',
      width: 50,
    },
    { field: 'employeeNo', title: '员工编号', minWidth: 120 },
    { field: 'name', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'postName', title: '岗位', minWidth: 120 },
    { field: 'phone', title: '手机号码', minWidth: 120 },
  ];
}
src/views/hrm/employee/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,146 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteEmployee, exportEmployee, getEmployeePage } from '#/api/hrm/employee';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºå‘˜å·¥ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹å‘˜å·¥ */
function handleDetail(row: HrmEmployeeApi.Employee) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑员工 */
function handleEdit(row: HrmEmployeeApi.Employee) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤å‘˜å·¥ */
async function handleDelete(row: HrmEmployeeApi.Employee) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteEmployee(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å¯¼å‡ºå‘˜å·¥ */
async function handleExport() {
  const data = await exportEmployee(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '员工列表.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getEmployeePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmEmployeeApi.Employee>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="员工列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['员工']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:employee:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['hrm:employee:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #employeeNo="{ row }">
        <Button type="link" @click="handleDetail(row)">{{ row.employeeNo }}</Button>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:employee:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:employee:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/employee/modules/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<script lang="ts" setup>
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { computed } from 'vue';
import { Descriptions, DescriptionsItem, Tabs, TabPane, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import EducationList from './education-list.vue';
import WorkHistoryList from './work-history-list.vue';
import EmergencyContactList from './emergency-contact-list.vue';
import SocialSecurityList from './social-security-list.vue';
const props = defineProps<{
  employee: HrmEmployeeApi.Employee;
}>();
const genderLabel = computed(() =>
  getDictLabel(DICT_TYPE.HRM_GENDER, props.employee.gender),
);
const employeeStatusLabel = computed(() =>
  getDictLabel(DICT_TYPE.HRM_EMPLOYEE_STATUS, props.employee.employeeStatus),
);
const employeeStatusColor = computed(() => {
  const status = props.employee.employeeStatus;
  if (status === 1) return 'success';
  if (status === 2) return 'processing';
  if (status === 3) return 'default';
  return 'default';
});
</script>
<template>
  <div class="employee-detail">
    <!-- åŸºæœ¬ä¿¡æ¯ -->
    <Descriptions title="基本信息" bordered :column="2" class="mb-4">
      <DescriptionsItem label="员工编号">{{ employee.employeeNo }}</DescriptionsItem>
      <DescriptionsItem label="员工姓名">{{ employee.name }}</DescriptionsItem>
      <DescriptionsItem label="性别">{{ genderLabel }}</DescriptionsItem>
      <DescriptionsItem label="出生日期">{{ employee.birthday || '-' }}</DescriptionsItem>
      <DescriptionsItem label="手机号码">{{ employee.phone }}</DescriptionsItem>
      <DescriptionsItem label="邮箱">{{ employee.email || '-' }}</DescriptionsItem>
      <DescriptionsItem label="身份证号">{{ employee.idCard || '-' }}</DescriptionsItem>
      <DescriptionsItem label="员工状态">
        <Tag :color="employeeStatusColor">{{ employeeStatusLabel }}</Tag>
      </DescriptionsItem>
    </Descriptions>
    <!-- å·¥ä½œä¿¡æ¯ -->
    <Descriptions title="工作信息" bordered :column="2" class="mb-4">
      <DescriptionsItem label="归属部门">{{ employee.deptName || '-' }}</DescriptionsItem>
      <DescriptionsItem label="岗位">{{ employee.postName || '-' }}</DescriptionsItem>
      <DescriptionsItem label="入职日期">{{ employee.hireDate }}</DescriptionsItem>
      <DescriptionsItem label="转正日期">{{ employee.regularDate || '-' }}</DescriptionsItem>
      <DescriptionsItem label="离职日期">{{ employee.leaveDate || '-' }}</DescriptionsItem>
      <DescriptionsItem label="基本工资">{{ employee.baseSalary ? `Â¥${employee.baseSalary.toFixed(2)}` : '-' }}</DescriptionsItem>
      <DescriptionsItem label="用户角色" :span="2">
        {{ Array.isArray(employee.roleNames) ? employee.roleNames.join('、') : employee.roleNames || '-' }}
      </DescriptionsItem>
      <DescriptionsItem label="年假余额">{{ employee.annualLeaveBalance ?? 0 }} å¤©</DescriptionsItem>
      <DescriptionsItem label="病假余额">{{ employee.sickLeaveBalance ?? 0 }} å¤©</DescriptionsItem>
    </Descriptions>
    <!-- è´¢åŠ¡ä¿¡æ¯ -->
    <Descriptions title="财务信息" bordered :column="2" class="mb-4">
      <DescriptionsItem label="开户银行">{{ employee.bankName || '-' }}</DescriptionsItem>
      <DescriptionsItem label="银行账号">{{ employee.bankAccount || '-' }}</DescriptionsItem>
      <DescriptionsItem label="居住地址" :span="2">{{ employee.address || '-' }}</DescriptionsItem>
    </Descriptions>
    <!-- å¤‡æ³¨ -->
    <Descriptions title="其他信息" bordered :column="1" class="mb-4">
      <DescriptionsItem label="备注">{{ employee.remark || '-' }}</DescriptionsItem>
    </Descriptions>
    <!-- å­è¡¨ä¿¡æ¯ -->
    <Tabs v-if="employee.id" class="mt-4">
      <Tabs.TabPane key="education" tab="教育经历">
        <EducationList form-type="detail" :employee-id="employee.id" />
      </Tabs.TabPane>
      <Tabs.TabPane key="workHistory" tab="工作经历">
        <WorkHistoryList form-type="detail" :employee-id="employee.id" />
      </Tabs.TabPane>
      <Tabs.TabPane key="emergencyContact" tab="紧急联系人">
        <EmergencyContactList form-type="detail" :employee-id="employee.id" />
      </Tabs.TabPane>
      <Tabs.TabPane key="socialSecurity" tab="社保公积金档案">
        <SocialSecurityList form-type="detail" :employee-id="employee.id" :user-id="employee.userId" />
      </Tabs.TabPane>
    </Tabs>
  </div>
</template>
<style scoped>
.employee-detail {
  padding: 0 16px;
}
</style>
src/views/hrm/employee/modules/education-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,174 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import type { DefaultOptionType } from 'ant-design-vue/es/select';
import { computed, nextTick, watch } from 'vue';
import { IconifyIcon } from '#/packages/icons/src';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { Button, DatePicker, Input, Select } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getEducationListByEmployeeId } from '#/api/hrm/employee/education';
import { $t } from '#/locales';
const props = defineProps<{
  employeeId?: number;
  formType: FormType;
}>();
const isReadOnly = computed(() => props.formType === 'detail');
const degreeOptions = getDictOptions(DICT_TYPE.HRM_DEGREE, 'number') as DefaultOptionType[];
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: [
      { field: 'schoolName', title: '学校名称', minWidth: 150, slots: { default: 'schoolName' } },
      { field: 'major', title: '专业', minWidth: 120, slots: { default: 'major' } },
      {
        field: 'degree',
        title: '学历',
        minWidth: 100,
        slots: { default: 'degree' },
      },
      {
        field: 'startDate',
        title: '入学时间',
        minWidth: 120,
        slots: { default: 'startDate' },
      },
      {
        field: 'endDate',
        title: '毕业时间',
        minWidth: 120,
        slots: { default: 'endDate' },
      },
      { field: 'certificateNo', title: '证书编号', minWidth: 140, slots: { default: 'certificateNo' } },
      {
        title: '操作',
        width: 80,
        fixed: 'right',
        slots: { default: 'actions' },
        visible: !isReadOnly.value,
      },
    ],
    border: true,
    showOverflow: true,
    autoResize: true,
    keepSource: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<HrmEmployeeApi.Education>,
});
/** æ·»åŠ æ•™è‚²ç»åŽ† */
async function handleAdd() {
  await gridApi.grid.insertAt({} as HrmEmployeeApi.Education, -1);
}
/** åˆ é™¤æ•™è‚²ç»åކ */
async function handleDelete(row: HrmEmployeeApi.Education) {
  await gridApi.grid.remove(row);
}
/** æä¾›èŽ·å–è¡¨æ ¼æ•°æ®çš„æ–¹æ³•ä¾›çˆ¶ç»„ä»¶è°ƒç”¨ */
defineExpose({
  getData: (): HrmEmployeeApi.Education[] => {
    const data = gridApi.grid.getData() as HrmEmployeeApi.Education[];
    const removeRecords = gridApi.grid.getRemoveRecords() as HrmEmployeeApi.Education[];
    const insertRecords = gridApi.grid.getInsertRecords() as HrmEmployeeApi.Education[];
    return [
      ...data.filter((row) => !removeRecords.some((removed) => removed.id === row.id)),
      ...insertRecords.map((row: any) => ({ ...row, id: undefined })),
    ];
  },
});
/** ç›‘听员工ID变化,加载子表数据 */
watch(
  () => props.employeeId,
  async (val) => {
    if (!val) {
      return;
    }
    await nextTick();
    await gridApi.grid.loadData(await getEducationListByEmployeeId(props.employeeId!));
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full">
    <template #schoolName="{ row }">
      <Input v-model:value="row.schoolName" :disabled="isReadOnly" placeholder="请输入学校名称" />
    </template>
    <template #major="{ row }">
      <Input v-model:value="row.major" :disabled="isReadOnly" placeholder="请输入专业" />
    </template>
    <template #degree="{ row }">
      <Select
        v-model:value="row.degree"
        :disabled="isReadOnly"
        :options="degreeOptions"
        placeholder="请选择学历"
        allowClear
      />
    </template>
    <template #startDate="{ row }">
      <DatePicker
        v-model:value="row.startDate"
        :disabled="isReadOnly"
        placeholder="请选择入学时间"
        valueFormat="YYYY-MM-DD"
        style="width: 100%"
      />
    </template>
    <template #endDate="{ row }">
      <DatePicker
        v-model:value="row.endDate"
        :disabled="isReadOnly"
        placeholder="请选择毕业时间"
        valueFormat="YYYY-MM-DD"
        style="width: 100%"
      />
    </template>
    <template #certificateNo="{ row }">
      <Input v-model:value="row.certificateNo" :disabled="isReadOnly" placeholder="请输入证书编号" />
    </template>
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: $t('common.delete'),
            danger: true,
            type: 'link',
            icon: ACTION_ICON.DELETE,
            onClick: handleDelete.bind(null, row),
          },
        ]"
      />
    </template>
  </Grid>
  <div v-if="!isReadOnly" class="-mt-4 flex justify-center">
    <Button type="primary" ghost @click="handleAdd">
      <IconifyIcon icon="lucide:plus" />
      æ·»åŠ æ•™è‚²ç»åŽ†
    </Button>
  </div>
</template>
src/views/hrm/employee/modules/emergency-contact-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { computed, nextTick, watch } from 'vue';
import { IconifyIcon } from '#/packages/icons/src';
import { Button, Input } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getEmergencyContactListByEmployeeId } from '#/api/hrm/employee/emergency-contact';
import { $t } from '#/locales';
const props = defineProps<{
  employeeId?: number;
  formType: FormType;
}>();
const isReadOnly = computed(() => props.formType === 'detail');
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: [
      { field: 'contactName', title: '联系人姓名', minWidth: 120, slots: { default: 'contactName' } },
      { field: 'relationship', title: '关系', minWidth: 100, slots: { default: 'relationship' } },
      { field: 'phone', title: '联系电话', minWidth: 130, slots: { default: 'phone' } },
      { field: 'address', title: '地址', minWidth: 200, slots: { default: 'address' } },
      {
        title: '操作',
        width: 80,
        fixed: 'right',
        slots: { default: 'actions' },
        visible: !isReadOnly.value,
      },
    ],
    border: true,
    showOverflow: true,
    autoResize: true,
    keepSource: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<HrmEmployeeApi.EmergencyContact>,
});
/** æ·»åŠ ç´§æ€¥è”ç³»äºº */
async function handleAdd() {
  await gridApi.grid.insertAt({} as HrmEmployeeApi.EmergencyContact, -1);
}
/** åˆ é™¤ç´§æ€¥è”系人 */
async function handleDelete(row: HrmEmployeeApi.EmergencyContact) {
  await gridApi.grid.remove(row);
}
/** æä¾›èŽ·å–è¡¨æ ¼æ•°æ®çš„æ–¹æ³•ä¾›çˆ¶ç»„ä»¶è°ƒç”¨ */
defineExpose({
  getData: (): HrmEmployeeApi.EmergencyContact[] => {
    const data = gridApi.grid.getData() as HrmEmployeeApi.EmergencyContact[];
    const removeRecords = gridApi.grid.getRemoveRecords() as HrmEmployeeApi.EmergencyContact[];
    const insertRecords = gridApi.grid.getInsertRecords() as HrmEmployeeApi.EmergencyContact[];
    return [
      ...data.filter((row) => !removeRecords.some((removed) => removed.id === row.id)),
      ...insertRecords.map((row: any) => ({ ...row, id: undefined })),
    ];
  },
});
/** ç›‘听员工ID变化,加载子表数据 */
watch(
  () => props.employeeId,
  async (val) => {
    if (!val) {
      return;
    }
    await nextTick();
    await gridApi.grid.loadData(await getEmergencyContactListByEmployeeId(props.employeeId!));
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full">
    <template #contactName="{ row }">
      <Input v-model:value="row.contactName" :disabled="isReadOnly" placeholder="请输入联系人姓名" />
    </template>
    <template #relationship="{ row }">
      <Input v-model:value="row.relationship" :disabled="isReadOnly" placeholder="请输入关系" />
    </template>
    <template #phone="{ row }">
      <Input v-model:value="row.phone" :disabled="isReadOnly" placeholder="请输入联系电话" />
    </template>
    <template #address="{ row }">
      <Input v-model:value="row.address" :disabled="isReadOnly" placeholder="请输入地址" />
    </template>
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: $t('common.delete'),
            danger: true,
            type: 'link',
            icon: ACTION_ICON.DELETE,
            onClick: handleDelete.bind(null, row),
          },
        ]"
      />
    </template>
  </Grid>
  <div v-if="!isReadOnly" class="-mt-4 flex justify-center">
    <Button type="primary" ghost @click="handleAdd">
      <IconifyIcon icon="lucide:plus" />
      æ·»åŠ ç´§æ€¥è”ç³»äºº
    </Button>
  </div>
</template>
src/views/hrm/employee/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tabs, Input } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createEmployee, getEmployee, updateEmployee } from '#/api/hrm/employee';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import EducationList from './education-list.vue';
import WorkHistoryList from './work-history-list.vue';
import EmergencyContactList from './emergency-contact-list.vue';
import SocialSecurityList from './social-security-list.vue';
import EmployeeDetail from './detail.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const employeeId = ref<number>();
const employeeData = ref<HrmEmployeeApi.Employee>();
const subTabsName = ref('education');
const remark = ref<string>();
// å­è¡¨ç»„ä»¶ ref
const educationListRef = ref<InstanceType<typeof EducationList>>();
const workHistoryListRef = ref<InstanceType<typeof WorkHistoryList>>();
const emergencyContactListRef = ref<InstanceType<typeof EmergencyContactList>>();
const socialSecurityListRef = ref<InstanceType<typeof SocialSecurityList>>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看员工';
  }
  return formType.value === 'update' ? '修改员工' : '新增员工';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 150,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = (await formApi.getValues()) as HrmEmployeeApi.Employee;
    data.remark = remark.value;
    // èŽ·å–å­è¡¨æ•°æ®
    data.educations = educationListRef.value?.getData();
    data.workHistories = workHistoryListRef.value?.getData();
    data.emergencyContacts = emergencyContactListRef.value?.getData();
    try {
      if (data.id) {
        await updateEmployee(data);
      } else {
        await createEmployee(data);
      }
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      employeeId.value = undefined;
      employeeData.value = undefined;
      remark.value = undefined;
      return;
    }
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    if (data.formType !== 'detail') {
      formApi.setState({ schema: useFormSchema(data.formType) });
      formApi.setDisabled(false);
      modalApi.setState({ showConfirmButton: true });
    } else {
      modalApi.setState({ showConfirmButton: false });
    }
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      const formData = await getEmployee(data.id);
      employeeId.value = formData.id;
      employeeData.value = formData;
      remark.value = formData.remark;
      if (data.formType !== 'detail') {
        await formApi.setValues(formData);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[1100px]">
    <!-- è¯¦æƒ…模式:使用专门的详情展示组件 -->
    <EmployeeDetail v-if="isDetail && employeeData" :employee="employeeData" />
    <!-- æ–°å¢ž/编辑模式:使用表单 -->
    <template v-else>
      <Form class="mx-4" />
      <Tabs
        v-if="!isDetail || employeeId"
        v-model:active-key="subTabsName"
        class="mx-4 mt-4"
      >
        <Tabs.TabPane key="education" tab="教育经历">
          <EducationList
            ref="educationListRef"
            :form-type="formType"
            :employee-id="employeeId"
          />
        </Tabs.TabPane>
        <Tabs.TabPane key="workHistory" tab="工作经历">
          <WorkHistoryList
            ref="workHistoryListRef"
            :form-type="formType"
            :employee-id="employeeId"
          />
        </Tabs.TabPane>
        <Tabs.TabPane key="emergencyContact" tab="紧急联系人">
          <EmergencyContactList
            ref="emergencyContactListRef"
            :form-type="formType"
            :employee-id="employeeId"
          />
        </Tabs.TabPane>
        <Tabs.TabPane key="socialSecurity" tab="社保公积金档案" v-if="employeeId">
          <SocialSecurityList
            ref="socialSecurityListRef"
            :form-type="formType"
            :employee-id="employeeId"
            :user-id="employeeData?.userId"
          />
        </Tabs.TabPane>
      </Tabs>
      <div class="mx-4 mt-4">
        <div class="mb-2 font-medium">备注</div>
        <Input.TextArea
          v-model:value="remark"
          placeholder="请输入备注"
          :rows="3"
        />
      </div>
    </template>
  </Modal>
</template>
src/views/hrm/employee/modules/social-security-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
<script lang="ts" setup>
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import type { HrmSocialSecuritySchemeApi } from '#/api/hrm/salary/social-security-scheme';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Form, FormItem, Select, InputNumber, DatePicker, RadioGroup, Textarea } from 'ant-design-vue';
import {
  createEmployeeSocialSecurity,
  getEmployeeSocialSecurity,
  updateEmployeeSocialSecurity,
} from '#/api/hrm/salary/employee-social-security';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const formType = ref<'create' | 'update'>('create');
const userId = ref<number>();
const schemeList = ref<HrmSocialSecuritySchemeApi.SocialSecurityScheme[]>([]);
const formState = ref<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>({
  userId: undefined,
  socialSecuritySchemeId: undefined,
  socialSecurityBase: 0,
  housingFundBase: 0,
  effectiveDate: undefined,
  status: 0,
  remark: '',
});
const getTitle = computed(() =>
  formType.value === 'update' ? '修改社保公积金档案' : '新增社保公积金档案',
);
const rules = {
  socialSecuritySchemeId: [{ required: true, message: '请选择社保方案' }],
  socialSecurityBase: [{ required: true, message: '请输入社保基数' }],
  housingFundBase: [{ required: true, message: '请输入公积金基数' }],
  effectiveDate: [{ required: true, message: '请选择生效日期' }],
};
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    try {
      const data = { ...formState.value, userId: userId.value };
      if (formState.value.id) {
        await updateEmployeeSocialSecurity(data);
      } else {
        await createEmployeeSocialSecurity(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: 'create' | 'update';
      id?: number;
      userId?: number;
      schemeList?: HrmSocialSecuritySchemeApi.SocialSecurityScheme[];
    }>();
    formType.value = data.formType;
    userId.value = data.userId;
    schemeList.value = data.schemeList || [];
    // é‡ç½®è¡¨å•
    formState.value = {
      userId: data.userId,
      socialSecuritySchemeId: undefined,
      socialSecurityBase: 0,
      housingFundBase: 0,
      effectiveDate: undefined,
      status: 0,
      remark: '',
    };
    if (data?.id) {
      modalApi.lock();
      try {
        const detail = await getEmployeeSocialSecurity(data.id);
        formState.value = detail;
      } finally {
        modalApi.unlock();
      }
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[700px]">
    <Form
      :model="formState"
      :rules="rules"
      :label-col="{ style: { width: '100px' } }"
      layout="horizontal"
    >
      <FormItem label="社保方案" name="socialSecuritySchemeId">
        <Select
          v-model:value="formState.socialSecuritySchemeId"
          :options="schemeList.map(s => ({ label: s.name, value: s.id }))"
          placeholder="请选择社保公积金方案"
        />
      </FormItem>
      <FormItem label="社保基数" name="socialSecurityBase">
        <InputNumber
          v-model:value="formState.socialSecurityBase"
          :min="0"
          :precision="2"
          style="width: 100%"
          placeholder="请输入社保基数"
        />
      </FormItem>
      <FormItem label="公积金基数" name="housingFundBase">
        <InputNumber
          v-model:value="formState.housingFundBase"
          :min="0"
          :precision="2"
          style="width: 100%"
          placeholder="请输入公积金基数"
        />
      </FormItem>
      <FormItem label="生效日期" name="effectiveDate">
        <DatePicker
          v-model:value="formState.effectiveDate"
          value-format="YYYY-MM-DD"
          style="width: 100%"
          placeholder="请选择生效日期"
        />
      </FormItem>
      <FormItem label="状态" name="status">
        <RadioGroup
          v-model:value="formState.status"
          :options="[
            { label: '启用', value: 0 },
            { label: '禁用', value: 1 },
          ]"
        />
      </FormItem>
      <FormItem label="备注">
        <Textarea
          v-model:value="formState.remark"
          :rows="2"
          placeholder="请输入备注"
        />
      </FormItem>
    </Form>
  </Modal>
</template>
src/views/hrm/employee/modules/social-security-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,174 @@
<script lang="ts" setup>
import type { FormType } from './form.vue';
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import { ref, computed, watch } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Table, Tag, Button } from 'ant-design-vue';
import { ACTION_ICON, TableAction } from '#/adapter/vxe-table';
import {
  createEmployeeSocialSecurity,
  deleteEmployeeSocialSecurity,
  getEmployeeSocialSecurityListByUser,
  updateEmployeeSocialSecurity,
} from '#/api/hrm/salary/employee-social-security';
import { getSocialSecuritySchemeList } from '#/api/hrm/salary/social-security-scheme';
import { $t } from '#/locales';
import SocialSecurityForm from './social-security-form.vue';
const props = defineProps<{
  employeeId?: number;
  userId?: number;
  formType?: FormType;
}>();
const emit = defineEmits(['refresh']);
const list = ref<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>([]);
const schemeList = ref<any[]>([]);
const loading = ref(false);
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: SocialSecurityForm,
  destroyOnClose: true,
});
/** åˆ·æ–°åˆ—表 */
async function handleRefresh() {
  if (!props.userId) return;
  loading.value = true;
  try {
    list.value = await getEmployeeSocialSecurityListByUser(props.userId);
  } finally {
    loading.value = false;
  }
  emit('refresh');
}
/** æ–°å¢žæ¡£æ¡ˆ */
function handleCreate() {
  formModalApi.setData({
    formType: 'create',
    userId: props.userId,
    schemeList: schemeList.value,
  }).open();
}
/** ç¼–辑档案 */
function handleEdit(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
  formModalApi.setData({
    formType: 'update',
    id: row.id,
    userId: props.userId,
    schemeList: schemeList.value,
  }).open();
}
/** åˆ é™¤æ¡£æ¡ˆ */
async function handleDelete(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
  await deleteEmployeeSocialSecurity(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['社保公积金档案']));
  handleRefresh();
}
/** èŽ·å–æ–¹æ¡ˆåˆ—è¡¨ */
async function getSchemeList() {
  schemeList.value = await getSocialSecuritySchemeList({ status: 0 });
}
/** ç›‘听 userId å˜åŒ– */
watch(
  () => props.userId,
  (val) => {
    if (val) {
      handleRefresh();
      getSchemeList();
    }
  },
  { immediate: true },
);
const columns = [
  { title: '社保方案', dataIndex: 'schemeName', width: 180 },
  { title: '社保基数', dataIndex: 'socialSecurityBase', width: 100 },
  { title: '公积金基数', dataIndex: 'housingFundBase', width: 100 },
  { title: '生效日期', dataIndex: 'effectiveDate', width: 110 },
  {
    title: '状态',
    dataIndex: 'status',
    width: 80,
    key: 'status',
  },
  {
    title: '操作',
    key: 'action',
    width: 150,
    fixed: 'right',
  },
];
const isDetail = computed(() => props.formType === 'detail');
</script>
<template>
  <div class="social-security-list">
    <FormModal @success="handleRefresh" />
    <!-- å·¥å…·æ  -->
    <div v-if="!isDetail" class="mb-3">
      <Button type="primary" @click="handleCreate">
        æ–°å¢žç¤¾ä¿å…¬ç§¯é‡‘档案
      </Button>
    </div>
    <!-- è¡¨æ ¼ -->
    <Table
      :columns="columns"
      :data-source="list"
      :loading="loading"
      :pagination="false"
      size="small"
      row-key="id"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'status'">
          <Tag :color="record.status === 0 ? 'success' : 'default'">
            {{ record.status === 0 ? '启用' : '禁用' }}
          </Tag>
        </template>
        <template v-else-if="column.key === 'action' && !isDetail">
          <TableAction
            :actions="[
              {
                label: $t('common.edit'),
                type: 'link',
                icon: ACTION_ICON.EDIT,
                onClick: handleEdit.bind(null, record),
              },
              {
                label: $t('common.delete'),
                type: 'link',
                danger: true,
                icon: ACTION_ICON.DELETE,
                popConfirm: {
                  title: $t('ui.actionMessage.deleteConfirm', ['社保公积金档案']),
                  confirm: handleDelete.bind(null, record),
                },
              },
            ]"
          />
        </template>
      </template>
    </Table>
  </div>
</template>
<style scoped>
.social-security-list {
  padding: 0;
}
</style>
src/views/hrm/employee/modules/work-history-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import { computed, nextTick, watch } from 'vue';
import { IconifyIcon } from '#/packages/icons/src';
import { Button, DatePicker, Input } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getWorkHistoryListByEmployeeId } from '#/api/hrm/employee/work-history';
import { $t } from '#/locales';
const props = defineProps<{
  employeeId?: number;
  formType: FormType;
}>();
const isReadOnly = computed(() => props.formType === 'detail');
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: [
      { field: 'companyName', title: '公司名称', minWidth: 150, slots: { default: 'companyName' } },
      { field: 'position', title: '职位', minWidth: 120, slots: { default: 'position' } },
      {
        field: 'startDate',
        title: '开始时间',
        minWidth: 120,
        slots: { default: 'startDate' },
      },
      {
        field: 'endDate',
        title: '结束时间',
        minWidth: 120,
        slots: { default: 'endDate' },
      },
      { field: 'workContent', title: '工作内容', minWidth: 200, slots: { default: 'workContent' } },
      { field: 'leaveReason', title: '离职原因', minWidth: 150, slots: { default: 'leaveReason' } },
      {
        title: '操作',
        width: 80,
        fixed: 'right',
        slots: { default: 'actions' },
        visible: !isReadOnly.value,
      },
    ],
    border: true,
    showOverflow: true,
    autoResize: true,
    keepSource: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<HrmEmployeeApi.WorkHistory>,
});
/** æ·»åŠ å·¥ä½œç»åŽ† */
async function handleAdd() {
  await gridApi.grid.insertAt({} as HrmEmployeeApi.WorkHistory, -1);
}
/** åˆ é™¤å·¥ä½œç»åކ */
async function handleDelete(row: HrmEmployeeApi.WorkHistory) {
  await gridApi.grid.remove(row);
}
/** æä¾›èŽ·å–è¡¨æ ¼æ•°æ®çš„æ–¹æ³•ä¾›çˆ¶ç»„ä»¶è°ƒç”¨ */
defineExpose({
  getData: (): HrmEmployeeApi.WorkHistory[] => {
    const data = gridApi.grid.getData() as HrmEmployeeApi.WorkHistory[];
    const removeRecords = gridApi.grid.getRemoveRecords() as HrmEmployeeApi.WorkHistory[];
    const insertRecords = gridApi.grid.getInsertRecords() as HrmEmployeeApi.WorkHistory[];
    return [
      ...data.filter((row) => !removeRecords.some((removed) => removed.id === row.id)),
      ...insertRecords.map((row: any) => ({ ...row, id: undefined })),
    ];
  },
});
/** ç›‘听员工ID变化,加载子表数据 */
watch(
  () => props.employeeId,
  async (val) => {
    if (!val) {
      return;
    }
    await nextTick();
    await gridApi.grid.loadData(await getWorkHistoryListByEmployeeId(props.employeeId!));
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full">
    <template #companyName="{ row }">
      <Input v-model:value="row.companyName" :disabled="isReadOnly" placeholder="请输入公司名称" />
    </template>
    <template #position="{ row }">
      <Input v-model:value="row.position" :disabled="isReadOnly" placeholder="请输入职位" />
    </template>
    <template #startDate="{ row }">
      <DatePicker
        v-model:value="row.startDate"
        :disabled="isReadOnly"
        placeholder="请选择开始时间"
        valueFormat="YYYY-MM-DD"
        style="width: 100%"
      />
    </template>
    <template #endDate="{ row }">
      <DatePicker
        v-model:value="row.endDate"
        :disabled="isReadOnly"
        placeholder="请选择结束时间"
        valueFormat="YYYY-MM-DD"
        style="width: 100%"
      />
    </template>
    <template #workContent="{ row }">
      <Input v-model:value="row.workContent" :disabled="isReadOnly" placeholder="请输入工作内容" />
    </template>
    <template #leaveReason="{ row }">
      <Input v-model:value="row.leaveReason" :disabled="isReadOnly" placeholder="请输入离职原因" />
    </template>
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: $t('common.delete'),
            danger: true,
            type: 'link',
            icon: ACTION_ICON.DELETE,
            onClick: handleDelete.bind(null, row),
          },
        ]"
      />
    </template>
  </Grid>
  <div v-if="!isReadOnly" class="-mt-4 flex justify-center">
    <Button type="primary" ghost @click="handleAdd">
      <IconifyIcon icon="lucide:plus" />
      æ·»åŠ å·¥ä½œç»åŽ†
    </Button>
  </div>
</template>
src/views/hrm/leave/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,173 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveApi } from '#/api/hrm/leave';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改请假申请的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'leaveType',
      label: '请假类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择请假类型',
        options: getDictOptions(DICT_TYPE.HRM_LEAVE_TYPE, 'number'),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'startTime',
      label: '开始时间',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择开始时间',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'endTime',
      label: '结束时间',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择结束时间',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'duration',
      label: '请假时长',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入请假时长',
        min: 0.5,
        precision: 2,
        step: 0.5,
        style: { width: '100%' },
      },
      rules: 'required',
      suffix: '天',
    },
    {
      fieldName: 'reason',
      label: '请假原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入请假原因',
        rows: 3,
        maxlength: 500,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'fileUrl',
      label: '附件',
      component: 'Input',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请上传附件',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'leaveType',
      label: '请假类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_LEAVE_TYPE, 'number'),
        placeholder: '请选择请假类型',
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmLeaveApi.Leave>['columns'] {
  return [
    { field: 'id', title: '申请编号', width: 80 },
    { field: 'employeeName', title: '申请人', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    {
      field: 'leaveType',
      title: '请假类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_LEAVE_TYPE },
      },
    },
    { field: 'startTime', title: '开始时间', width: 160 },
    { field: 'endTime', title: '结束时间', width: 160 },
    { field: 'duration', title: '时长(天)', width: 90 },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/leave/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveApi } from '#/api/hrm/leave';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelLeave,
  deleteLeave,
  getLeavePage,
  submitLeave,
} from '#/api/hrm/leave';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè¯·å‡ç”³è¯· */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è¯·å‡ç”³è¯· */
function handleDetail(row: HrmLeaveApi.Leave) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑请假申请 */
function handleEdit(row: HrmLeaveApi.Leave) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è¯·å‡ç”³è¯· */
async function handleDelete(row: HrmLeaveApi.Leave) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', ['请假申请']),
    duration: 0,
  });
  try {
    await deleteLeave(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', ['请假申请']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤è¯·å‡ç”³è¯· */
async function handleSubmit(row: HrmLeaveApi.Leave) {
  await submitLeave(row.id!);
  message.success('提交成功');
  handleRefresh();
}
/** æ’¤é”€è¯·å‡ç”³è¯· */
async function handleCancel(row: HrmLeaveApi.Leave) {
  await cancelLeave(row.id!);
  message.success('撤销成功');
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getLeavePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmLeaveApi.Leave>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="请假申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['请假申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:leave:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '查看',
              type: 'link',
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:leave:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['hrm:leave:submit'],
              ifShow: row.status === 0,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '撤销',
              type: 'link',
              auth: ['hrm:leave:cancel'],
              ifShow: row.status === 10,
              onClick: handleCancel.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:leave:delete'],
              ifShow: row.status === 0 || row.status === 40,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['请假申请']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/leave/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmLeaveApi } from '#/api/hrm/leave';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createLeave, getLeave, updateLeave } from '#/api/hrm/leave';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看请假申请';
  }
  return formType.value === 'update' ? '修改请假申请' : '新增请假申请';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmLeaveApi.Leave;
    try {
      if (data.id) {
        await updateLeave(data);
      } else {
        await createLeave(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;
    formApi.setState({ schema: useFormSchema(data.formType) });
    formApi.setDisabled(data.formType === 'detail');
    modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getLeave(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/resignation/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmResignationApi } from '#/api/hrm/resignation';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改离职申请的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'resignationType',
      label: '离职类型',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.HRM_RESIGNATION_TYPE, 'number'),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'expectedDate',
      label: '预计离职日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择预计离职日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'handoverUserId',
      label: '交接人',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择交接人',
      },
    },
    {
      fieldName: 'reason',
      label: '离职原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入离职原因',
        rows: 3,
        maxlength: 500,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'fileUrl',
      label: '附件',
      component: 'Input',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请上传附件',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'resignationType',
      label: '离职类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_RESIGNATION_TYPE, 'number'),
        placeholder: '请选择离职类型',
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmResignationApi.Resignation>['columns'] {
  return [
    { field: 'id', title: '申请编号', width: 80 },
    { field: 'employeeName', title: '申请人', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'postName', title: '岗位', minWidth: 120 },
    {
      field: 'resignationType',
      title: '离职类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_RESIGNATION_TYPE },
      },
    },
    { field: 'expectedDate', title: '预计离职日期', width: 120 },
    { field: 'handoverUserName', title: '交接人', minWidth: 100 },
    {
      field: 'handoverStatus',
      title: '交接状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_HANDOVER_STATUS },
      },
    },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/resignation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmResignationApi } from '#/api/hrm/resignation';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelResignation,
  deleteResignation,
  getResignationPage,
  submitResignation,
  updateHandoverStatus,
} from '#/api/hrm/resignation';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºç¦»èŒç”³è¯· */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹ç¦»èŒç”³è¯· */
function handleDetail(row: HrmResignationApi.Resignation) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑离职申请 */
function handleEdit(row: HrmResignationApi.Resignation) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤ç¦»èŒç”³è¯· */
async function handleDelete(row: HrmResignationApi.Resignation) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', ['离职申请']),
    duration: 0,
  });
  try {
    await deleteResignation(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', ['离职申请']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤ç¦»èŒç”³è¯· */
async function handleSubmit(row: HrmResignationApi.Resignation) {
  await submitResignation(row.id!);
  message.success('提交成功');
  handleRefresh();
}
/** æ’¤é”€ç¦»èŒç”³è¯· */
async function handleCancel(row: HrmResignationApi.Resignation) {
  await cancelResignation(row.id!);
  message.success('撤销成功');
  handleRefresh();
}
/** æ›´æ–°äº¤æŽ¥çŠ¶æ€ */
async function handleUpdateHandover(row: HrmResignationApi.Resignation) {
  // äº¤æŽ¥çŠ¶æ€ï¼š0-未开始,10-进行中,20-已完成
  const nextStatus = row.handoverStatus === 0 ? 10 : row.handoverStatus === 10 ? 20 : 0;
  const statusText = nextStatus === 10 ? '进行中' : nextStatus === 20 ? '已完成' : '未开始';
  await updateHandoverStatus(row.id!, nextStatus);
  message.success(`交接状态已更新为:${statusText}`);
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getResignationPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmResignationApi.Resignation>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="离职申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['离职申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:resignation:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '查看',
              type: 'link',
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:resignation:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['hrm:resignation:submit'],
              ifShow: row.status === 0,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '撤销',
              type: 'link',
              auth: ['hrm:resignation:cancel'],
              ifShow: row.status === 10,
              onClick: handleCancel.bind(null, row),
            },
            {
              label: '交接',
              type: 'link',
              auth: ['hrm:resignation:handover'],
              ifShow: row.status === 20 && row.handoverStatus !== 20,
              popConfirm: {
                title: row.handoverStatus === 0 ? '确认开始交接吗?' : '确认完成交接吗?',
                confirm: handleUpdateHandover.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:resignation:delete'],
              ifShow: row.status === 0 || row.status === 40,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['离职申请']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/resignation/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmResignationApi } from '#/api/hrm/resignation';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createResignation, getResignation, updateResignation } from '#/api/hrm/resignation';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看离职申请';
  }
  return 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: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmResignationApi.Resignation;
    try {
      if (data.id) {
        await updateResignation(data);
      } else {
        await createResignation(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;
    formApi.setState({ schema: useFormSchema(data.formType) });
    formApi.setDisabled(data.formType === 'detail');
    modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getResignation(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/calculation/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getDeptList } from '#/api/system/dept';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { handleTree } from '#/packages/utils/src';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'period',
      label: '核算周期',
      component: 'DatePicker',
      componentProps: {
        picker: 'month',
        allowClear: true,
        format: 'YYYY-MM',
        valueFormat: 'YYYY-MM',
        style: { width: '100%' },
        placeholder: '请选择核算周期',
      },
    },
    {
      fieldName: 'userId',
      label: '员工',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择员工',
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_SALARY_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmSalaryCalculationApi.SalaryCalculation>['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { field: 'no', title: '核算单号', minWidth: 140 },
    { 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: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 80 },
    { field: 'overtimeHours', title: '加班时长(h)', width: 100 },
    {
      field: 'status',
      title: '状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_SALARY_STATUS },
      },
    },
    {
      title: '操作',
      width: 120,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
/** é¢„览表格的列定义(用于Ant Design Table) */
export function usePreviewColumns() {
  return [
    { title: '员工姓名', dataIndex: 'userName', width: 100, fixed: 'left' },
    { title: '部门', dataIndex: 'deptName', width: 120 },
    { title: '基本工资', dataIndex: 'baseSalary', width: 110 },
    { title: '绩效工资', dataIndex: 'performanceSalary', width: 110 },
    { title: '加班费', dataIndex: 'overtimePay', width: 110 },
    { title: '餐补', dataIndex: 'mealAllowance', width: 100 },
    { title: '交通补贴', dataIndex: 'transportAllowance', width: 100 },
    { title: '其他补贴', dataIndex: 'otherAllowance', width: 100 },
    { title: '社保扣款', dataIndex: 'socialSecurityDeduction', width: 110 },
    { title: '公积金扣款', dataIndex: 'housingFundDeduction', width: 110 },
    { title: '个税扣款', dataIndex: 'taxDeduction', width: 110 },
    { title: '其他扣款', dataIndex: 'otherDeduction', width: 100 },
    { title: '实发工资', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
    { title: '出勤天数', dataIndex: 'workDays', width: 90 },
    { title: '加班时长', dataIndex: 'overtimeHours', width: 100 },
  ];
}
src/views/hrm/salary/calculation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,443 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryCalculationApi } from '#/api/hrm/salary/calculation';
import { ref } from 'vue';
import { Page } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  calculateSalary,
  confirmSalary,
  exportSalaryCalculation,
  getSalaryCalculationPage,
  previewSalaryCalculation,
  saveSalaryCalculation,
} 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';
// æ ¸ç®—弹窗相关
const calculateVisible = ref(false);
const calculatePeriod = ref<string>();
const calculateDeptId = ref<number>();
const deptTreeData = ref<any[]>([]);
const previewLoading = ref(false);
const previewData = ref<HrmSalaryCalculationApi.SalaryCalculation[]>([]);
const submitLoading = ref(false);
const calculateLoading = ref(false);
// æ‰“开核算弹窗
async function openCalculateModal() {
  const formValues = await gridApi.formApi.getValues();
  calculatePeriod.value = formValues.period;
  calculateDeptId.value = formValues.deptId;
  previewData.value = [];
  // åŠ è½½éƒ¨é—¨æ ‘
  const data = await getDeptList();
  deptTreeData.value = handleTree(data);
  calculateVisible.value = true;
}
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰§è¡Œè–ªé…¬æ ¸ç®—(自动计算并保存) */
async function handleCalculate() {
  if (!calculatePeriod.value) {
    message.warning('请选择核算周期');
    return;
  }
  calculateLoading.value = true;
  try {
    await calculateSalary({
      period: calculatePeriod.value,
      deptId: calculateDeptId.value,
    });
    message.success('薪酬核算执行成功');
    calculateVisible.value = false;
    handleRefresh();
  } finally {
    calculateLoading.value = false;
  }
}
/** é¢„览核算数据 */
async function handlePreview() {
  if (!calculatePeriod.value) {
    message.warning('请选择核算周期');
    return;
  }
  previewLoading.value = true;
  try {
    previewData.value = await previewSalaryCalculation({
      period: calculatePeriod.value,
      deptId: calculateDeptId.value,
    });
    if (previewData.value.length === 0) {
      message.info('该条件下没有需要核算的员工');
    }
  } finally {
    previewLoading.value = false;
  }
}
/** è®¡ç®—实发工资 */
function calculateActualSalary(record: HrmSalaryCalculationApi.SalaryCalculation) {
  const income = (record.baseSalary || 0) +
    (record.performanceSalary || 0) +
    (record.overtimePay || 0) +
    (record.mealAllowance || 0) +
    (record.transportAllowance || 0) +
    (record.otherAllowance || 0);
  const deduction = (record.socialSecurityDeduction || 0) +
    (record.housingFundDeduction || 0) +
    (record.taxDeduction || 0) +
    (record.otherDeduction || 0);
  return income - deduction;
}
/** æ›´æ–°è¡Œæ•°æ®å¹¶è®¡ç®—实发工资 */
function updateFieldValue(record: HrmSalaryCalculationApi.SalaryCalculation, field: keyof HrmSalaryCalculationApi.SalaryCalculation, value: number) {
  (record as any)[field] = value;
  record.actualSalary = calculateActualSalary(record);
}
/** æäº¤è–ªé…¬æ ¸ç®—(手动调整后保存) */
async function handleSubmit() {
  if (!calculatePeriod.value) {
    message.warning('请选择核算周期');
    return;
  }
  if (previewData.value.length === 0) {
    message.warning('请先预览核算数据');
    return;
  }
  submitLoading.value = true;
  try {
    await saveSalaryCalculation({
      period: calculatePeriod.value,
      list: previewData.value,
    });
    message.success('核算保存成功');
    calculateVisible.value = false;
    handleRefresh();
  } finally {
    submitLoading.value = false;
  }
}
/** å–消弹窗 */
function handleCancel() {
  calculateVisible.value = false;
  previewData.value = [];
}
/** ç¡®è®¤è–ªé…¬æ ¸ç®— */
async function handleConfirm(row: HrmSalaryCalculationApi.SalaryCalculation) {
  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 ids = records.map((r) => r.id!);
  await confirmSalary(ids);
  message.success('确认成功');
  handleRefresh();
}
/** å¯¼å‡ºè–ªé…¬æ ¸ç®— */
async function handleExport() {
  const data = await exportSalaryCalculation(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '薪酬核算表.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      reserve: true,
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getSalaryCalculationPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmSalaryCalculationApi.SalaryCalculation>,
});
</script>
<template>
  <Page auto-content-height>
    <!-- æ ¸ç®—弹窗 -->
    <Modal
      v-model:open="calculateVisible"
      title="薪酬核算"
      width="1200px"
      :footer="null"
      :maskClosable="false"
    >
      <Alert
        message="两种核算方式:1. æ‰§è¡Œæ ¸ç®— - è‡ªåŠ¨è®¡ç®—å¹¶ä¿å­˜ï¼›2. é¢„览数据 - æ‰‹åŠ¨è°ƒæ•´åŽä¿å­˜"
        type="info"
        show-icon
        style="margin-bottom: 16px"
      />
      <Form layout="inline" class="mb-4">
        <FormItem label="核算周期" required>
          <DatePicker
            v-model:value="calculatePeriod"
            picker="month"
            format="YYYY-MM"
            valueFormat="YYYY-MM"
            placeholder="请选择核算周期"
            style="width: 180px"
          />
        </FormItem>
        <FormItem label="核算部门">
          <TreeSelect
            v-model:value="calculateDeptId"
            :tree-data="deptTreeData"
            :field-names="{ label: 'name', value: 'id', children: 'children' }"
            placeholder="全部部门"
            allow-clear
            tree-default-expand-all
            style="width: 200px"
          />
        </FormItem>
        <FormItem>
          <a-button type="primary" :loading="calculateLoading" @click="handleCalculate">
            æ‰§è¡Œæ ¸ç®—
          </a-button>
        </FormItem>
        <FormItem>
          <a-button :loading="previewLoading" @click="handlePreview">
            é¢„览数据
          </a-button>
        </FormItem>
      </Form>
      <Table
        v-if="previewData.length > 0"
        :columns="usePreviewColumns()"
        :data-source="previewData"
        :pagination="false"
        :scroll="{ x: 1800, y: 400 }"
        size="small"
        row-key="userId"
        bordered
        class="mb-4"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'baseSalary'">
            <InputNumber
              v-model:value="record.baseSalary"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'baseSalary', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'performanceSalary'">
            <InputNumber
              v-model:value="record.performanceSalary"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'performanceSalary', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'overtimePay'">
            <InputNumber
              v-model:value="record.overtimePay"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'overtimePay', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'mealAllowance'">
            <InputNumber
              v-model:value="record.mealAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'mealAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'transportAllowance'">
            <InputNumber
              v-model:value="record.transportAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'transportAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'otherAllowance'">
            <InputNumber
              v-model:value="record.otherAllowance"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'otherAllowance', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'socialSecurityDeduction'">
            <InputNumber
              v-model:value="record.socialSecurityDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'socialSecurityDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'housingFundDeduction'">
            <InputNumber
              v-model:value="record.housingFundDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'housingFundDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'taxDeduction'">
            <InputNumber
              v-model:value="record.taxDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'taxDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'otherDeduction'">
            <InputNumber
              v-model:value="record.otherDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 80px"
              @change="(val: number) => updateFieldValue(record, 'otherDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'actualSalary'">
            <span class="font-bold text-green-600">{{ record.actualSalary?.toFixed(2) }}</span>
          </template>
          <template v-else-if="column.dataIndex === 'workDays'">
            <InputNumber
              v-model:value="record.workDays"
              :min="0"
              :max="31"
              :precision="0"
              size="small"
              style="width: 70px"
            />
          </template>
          <template v-else-if="column.dataIndex === 'overtimeHours'">
            <InputNumber
              v-model:value="record.overtimeHours"
              :min="0"
              :precision="1"
              size="small"
              style="width: 80px"
            />
          </template>
        </template>
      </Table>
      <div class="flex justify-end gap-2" v-if="previewData.length > 0">
        <a-button @click="handleCancel">取消</a-button>
        <a-button type="primary" :loading="submitLoading" @click="handleSubmit">
          ä¿å­˜æ ¸ç®—
        </a-button>
      </div>
    </Modal>
    <Grid table-title="薪酬核算列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '核算',
              type: 'primary',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:salary-calculation:calculate'],
              onClick: openCalculateModal,
            },
            {
              label: '批量确认',
              type: 'primary',
              auth: ['hrm:salary-calculation:confirm'],
              onClick: handleBatchConfirm,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['hrm:salary-calculation:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '确认',
              type: 'link',
              auth: ['hrm:salary-calculation:confirm'],
              ifShow: row.status === 0,
              onClick: handleConfirm.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/employee-social-security/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import { getDeptList } from '#/api/system/dept';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { getSocialSecuritySchemeList } from '#/api/hrm/salary/social-security-scheme';
import { handleTree } from '#/packages/utils/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'update';
/** æ–°å¢ž/修改员工社保公积金档案的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      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()),
        placeholder: '请选择员工',
      },
      rules: 'selectRequired',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'socialSecuritySchemeId',
      label: '社保方案',
      component: 'ApiSelect',
      componentProps: {
        api: async () => {
          const list = await getSocialSecuritySchemeList({ status: 0 });
          return list;
        },
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择社保公积金方案',
      },
      rules: 'selectRequired',
      formItemClass: 'col-span-2',
    },
    // ç¼´çº³åŸºæ•°
    {
      fieldName: 'socialSecurityBase',
      label: '社保基数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入社保基数',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'housingFundBase',
      label: '公积金基数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入公积金基数',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      rules: 'required',
    },
    // ç”Ÿæ•ˆä¿¡æ¯
    {
      fieldName: 'effectiveDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择生效日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: [
          { label: '启用', value: 0 },
          { label: '禁用', value: 1 },
        ],
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'userName',
      label: '员工姓名',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入员工姓名',
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>['columns'] {
  return [
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'schemeName', title: '社保方案', minWidth: 180 },
    { field: 'socialSecurityBase', title: '社保基数', width: 100, formatter: 'formatAmount2' },
    { field: 'housingFundBase', title: '公积金基数', width: 100, formatter: 'formatAmount2' },
    { field: 'effectiveDate', title: '生效日期', width: 110 },
    {
      field: 'status',
      title: '状态',
      width: 80,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/salary/employee-social-security/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEmployeeSocialSecurity,
  getEmployeeSocialSecurityPage,
} from '#/api/hrm/salary/employee-social-security';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import History from './modules/history.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [HistoryModal, historyModalApi] = useVbenModal({
  connectedComponent: History,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žæ¡£æ¡ˆ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑档案 */
function handleEdit(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** æŸ¥çœ‹åŽ†å² */
function handleHistory(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
  historyModalApi.setData({ userId: row.userId, userName: row.userName }).open();
}
/** åˆ é™¤æ¡£æ¡ˆ */
async function handleDelete(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
  await deleteEmployeeSocialSecurity(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['员工社保公积金档案']));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getEmployeeSocialSecurityPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <HistoryModal />
    <Grid table-title="员工社保公积金档案列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['员工社保公积金档案']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:employee-social-security:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </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:employee-social-security:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '历史',
              type: 'link',
              onClick: handleHistory.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:employee-social-security:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['员工社保公积金档案']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/employee-social-security/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEmployeeSocialSecurity,
  getEmployeeSocialSecurity,
  updateEmployeeSocialSecurity,
} from '#/api/hrm/salary/employee-social-security';
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: 100,
  },
  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();
    const data =
      (await formApi.getValues()) as HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity;
    try {
      if (data.id) {
        await updateEmployeeSocialSecurity(data);
      } else {
        await createEmployeeSocialSecurity(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;
    if (!data?.id) {
      await formApi.resetForm();
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getEmployeeSocialSecurity(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[700px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/employee-social-security/modules/history.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
<script lang="ts" setup>
import type { HrmEmployeeSocialSecurityApi } from '#/api/hrm/salary/employee-social-security';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { Table, Tag } from 'ant-design-vue';
import { getEmployeeSocialSecurityListByUser } from '#/api/hrm/salary/employee-social-security';
const historyList = ref<HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity[]>([]);
const userName = ref<string>('');
const columns = [
  { title: '社保方案', dataIndex: 'schemeName', width: 180 },
  { title: '社保基数', dataIndex: 'socialSecurityBase', width: 100 },
  { title: '公积金基数', dataIndex: 'housingFundBase', width: 100 },
  { title: '生效日期', dataIndex: 'effectiveDate', width: 110 },
  {
    title: '状态',
    dataIndex: 'status',
    width: 80,
    key: 'status',
  },
];
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      historyList.value = [];
      return;
    }
    const data = modalApi.getData<{ userId: number; userName: string }>();
    userName.value = data.userName;
    modalApi.lock();
    try {
      historyList.value = await getEmployeeSocialSecurityListByUser(data.userId);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="`${userName} - ç¤¾ä¿å…¬ç§¯é‡‘历史记录`" class="w-[600px]" :footer="null">
    <Table
      :columns="columns"
      :data-source="historyList"
      :pagination="false"
      size="small"
      row-key="id"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'status'">
          <Tag :color="record.status === 0 ? 'success' : 'default'">
            {{ record.status === 0 ? '启用' : '禁用' }}
          </Tag>
        </template>
      </template>
    </Table>
  </Modal>
</template>
src/views/hrm/salary/payment/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryPaymentApi } from '#/api/hrm/salary/payment';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getDeptList } from '#/api/system/dept';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { handleTree } from '#/packages/utils/src';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'period',
      label: '发放周期',
      component: 'DatePicker',
      componentProps: {
        picker: 'month',
        allowClear: true,
        format: 'YYYY-MM',
        valueFormat: 'YYYY-MM',
        style: { width: '100%' },
        placeholder: '请选择发放周期',
      },
    },
    {
      fieldName: 'userId',
      label: '员工',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择员工',
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
    {
      fieldName: 'status',
      label: '发放状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '待发放', value: 10 },
          { label: '已发放', value: 20 },
          { label: '已撤销', value: 30 },
        ],
        placeholder: '请选择发放状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmSalaryPaymentApi.SalaryPayment>['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { field: 'no', title: '发放单号', minWidth: 140 },
    { field: 'period', title: '发放周期', width: 100 },
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    {
      field: 'paymentMethod',
      title: '发放方式',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_PAYMENT_METHOD },
      },
    },
    { field: 'paymentTime', title: '发放时间', width: 160 },
    { field: 'paymentNo', title: '发放流水号', minWidth: 140 },
    {
      field: 'status',
      title: '发放状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_SALARY_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 150,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/salary/payment/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryPaymentApi } from '#/api/hrm/salary/payment';
import { Page } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  exportSalaryPayment,
  getSalaryPaymentPage,
  paySalary,
  revokeSalaryPayment,
} from '#/api/hrm/salary/payment';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰§è¡Œè–ªèµ„发放 */
async function handlePay() {
  const records = gridApi.grid.getCheckboxRecords() as HrmSalaryPaymentApi.SalaryPayment[];
  if (records.length === 0) {
    message.warning('请选择要发放的记录');
    return;
  }
  const unpaidRecords = records.filter((r) => r.status === 10);
  if (unpaidRecords.length === 0) {
    message.warning('所选记录中没有待发放状态的记录');
    return;
  }
  const ids = unpaidRecords.map((r) => r.id!);
  await paySalary(ids);
  message.success('发放成功');
  handleRefresh();
}
/** æ’¤é”€è–ªèµ„发放 */
async function handleRevoke(row: HrmSalaryPaymentApi.SalaryPayment) {
  await revokeSalaryPayment(row.id!);
  message.success('撤销成功');
  handleRefresh();
}
/** å¯¼å‡ºè–ªèµ„发放 */
async function handleExport() {
  const data = await exportSalaryPayment(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '薪资发放表.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      reserve: true,
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getSalaryPaymentPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmSalaryPaymentApi.SalaryPayment>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="薪资发放列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '发放',
              type: 'primary',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:salary-payment:pay'],
              onClick: handlePay,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['hrm:salary-payment:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '撤销',
              type: 'link',
              auth: ['hrm:salary-payment:revoke'],
              ifShow: row.status === 20,
              popConfirm: {
                title: '确认撤销该发放记录吗?',
                confirm: handleRevoke.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/social-security-scheme/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,328 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSocialSecuritySchemeApi } from '#/api/hrm/salary/social-security-scheme';
/** è¡¨å•类型 */
export type FormType = 'create' | 'update';
/** æ ¼å¼åŒ–比例输入 */
function createRatioFormatter(step: number = 0.01) {
  return {
    formatter: (value: number) => value !== undefined && value !== null ? `${(value * 100).toFixed(step === 0.001 ? 1 : 0)}%` : '',
    parser: (value: string) => {
      const num = parseFloat(value.replace('%', ''));
      return isNaN(num) ? 0 : num / 100;
    },
  };
}
/** æ–°å¢ž/修改社保公积金方案的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'name',
      label: '方案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入方案名称,如:2024年南通市标准',
      },
      rules: 'required',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'effectiveDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择生效日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: [
          { label: '启用', value: 0 },
          { label: '禁用', value: 1 },
        ],
      },
      rules: 'required',
    },
    // å…»è€ä¿é™©
    {
      fieldName: 'divider_pension',
      label: '养老保险',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'pensionCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    {
      fieldName: 'pensionPersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    // åŒ»ç–—保险
    {
      fieldName: 'divider_medical',
      label: '医疗保险',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'medicalCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    {
      fieldName: 'medicalPersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    // å¤±ä¸šä¿é™©
    {
      fieldName: 'divider_unemployment',
      label: '失业保险',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'unemploymentCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
      },
    },
    {
      fieldName: 'unemploymentPersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
      },
    },
    // å·¥ä¼¤/生育保险
    {
      fieldName: 'divider_other',
      label: '工伤/生育保险',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'workInjuryRatio',
      label: '工伤保险',
      component: 'InputNumber',
      componentProps: {
        placeholder: '工伤保险比例',
        min: 0,
        max: 1,
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
      },
    },
    {
      fieldName: 'maternityRatio',
      label: '生育保险',
      component: 'InputNumber',
      componentProps: {
        placeholder: '生育保险比例',
        min: 0,
        max: 1,
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
      },
    },
    // å…¬ç§¯é‡‘
    {
      fieldName: 'divider_housing',
      label: '公积金',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'housingFundCompanyRatio',
      label: '企业比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '企业缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    {
      fieldName: 'housingFundPersonalRatio',
      label: '个人比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '个人缴纳比例',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    // å¤‡æ³¨
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'name',
      label: '方案名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入方案名称',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '启用', value: 0 },
          { label: '禁用', value: 1 },
        ],
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmSocialSecuritySchemeApi.SocialSecurityScheme>['columns'] {
  return [
    { field: 'name', title: '方案名称', minWidth: 180 },
    { field: 'effectiveDate', title: '生效日期', width: 110 },
    {
      field: 'pensionCompanyRatio',
      title: '养老(企业)',
      width: 100,
      slots: { default: 'pensionCompanyRatio' },
    },
    {
      field: 'pensionPersonalRatio',
      title: '养老(个人)',
      width: 100,
      slots: { default: 'pensionPersonalRatio' },
    },
    {
      field: 'medicalPersonalRatio',
      title: '医疗(个人)',
      width: 100,
      slots: { default: 'medicalPersonalRatio' },
    },
    {
      field: 'housingFundPersonalRatio',
      title: '公积金(个人)',
      width: 100,
      slots: { default: 'housingFundPersonalRatio' },
    },
    {
      field: 'status',
      title: '状态',
      width: 80,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 150,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/salary/social-security-scheme/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSocialSecuritySchemeApi } from '#/api/hrm/salary/social-security-scheme';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteSocialSecurityScheme,
  getSocialSecuritySchemeList,
} from '#/api/hrm/salary/social-security-scheme';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žæ–¹æ¡ˆ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑方案 */
function handleEdit(row: HrmSocialSecuritySchemeApi.SocialSecurityScheme) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤æ–¹æ¡ˆ */
async function handleDelete(row: HrmSocialSecuritySchemeApi.SocialSecurityScheme) {
  await deleteSocialSecurityScheme(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['社保公积金方案']));
  handleRefresh();
}
/** æ ¼å¼åŒ–比例显示 */
function formatRatio(ratio: number | undefined) {
  if (ratio === undefined || ratio === null) return '-';
  return `${(ratio * 100).toFixed(1)}%`;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async (_params, formValues) =>
          await getSocialSecuritySchemeList(formValues),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmSocialSecuritySchemeApi.SocialSecurityScheme>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="社保公积金方案列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['社保公积金方案']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:social-security-scheme:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #pensionCompanyRatio="{ row }">
        {{ formatRatio(row.pensionCompanyRatio) }}
      </template>
      <template #pensionPersonalRatio="{ row }">
        {{ formatRatio(row.pensionPersonalRatio) }}
      </template>
      <template #medicalPersonalRatio="{ row }">
        {{ formatRatio(row.medicalPersonalRatio) }}
      </template>
      <template #housingFundPersonalRatio="{ row }">
        {{ formatRatio(row.housingFundPersonalRatio) }}
      </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:social-security-scheme:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:social-security-scheme:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['社保公积金方案']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/social-security-scheme/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmSocialSecuritySchemeApi } from '#/api/hrm/salary/social-security-scheme';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createSocialSecurityScheme,
  getSocialSecurityScheme,
  updateSocialSecurityScheme,
} from '#/api/hrm/salary/social-security-scheme';
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: 100,
  },
  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();
    const data =
      (await formApi.getValues()) as HrmSocialSecuritySchemeApi.SocialSecurityScheme;
    try {
      if (data.id) {
        await updateSocialSecurityScheme(data);
      } else {
        await createSocialSecurityScheme(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;
    if (!data?.id) {
      await formApi.resetForm();
      // è®¾ç½®é»˜è®¤å€¼
      await formApi.setValues({
        pensionCompanyRatio: 0.16,
        pensionPersonalRatio: 0.08,
        medicalCompanyRatio: 0.095,
        medicalPersonalRatio: 0.02,
        unemploymentCompanyRatio: 0.005,
        unemploymentPersonalRatio: 0.005,
        workInjuryRatio: 0.005,
        maternityRatio: 0.005,
        housingFundCompanyRatio: 0.12,
        housingFundPersonalRatio: 0.12,
        status: 0,
      });
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getSocialSecurityScheme(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[700px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/tax-rate-config/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTaxRateConfigApi } from '#/api/hrm/salary/tax-rate-config';
/** è¡¨å•类型 */
export type FormType = 'create' | 'update';
/** æ–°å¢ž/修改个税税率配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'level',
      label: '税率级数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入税率级数',
        min: 1,
        max: 7,
        precision: 0,
      },
      rules: 'required',
    },
    {
      fieldName: 'minAmount',
      label: '应纳税所得额下限',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入下限(含)',
        min: 0,
        precision: 2,
      },
      rules: 'required',
    },
    {
      fieldName: 'maxAmount',
      label: '应纳税所得额上限',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入上限(不含),0表示无上限',
        min: 0,
        precision: 2,
      },
      help: '上限不含,0表示无上限',
    },
    {
      fieldName: 'taxRate',
      label: '税率',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入税率',
        min: 0,
        max: 1,
        step: 0.01,
        precision: 4,
        formatter: (value: number) => `${(value * 100).toFixed(0)}%`,
        parser: (value: string) => parseFloat(value.replace('%', '')) / 100,
      },
      rules: 'required',
    },
    {
      fieldName: 'quickDeduction',
      label: '速算扣除数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入速算扣除数',
        min: 0,
        precision: 2,
      },
      rules: 'required',
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmTaxRateConfigApi.TaxRateConfig>['columns'] {
  return [
    { field: 'level', title: '级数', width: 80 },
    {
      field: 'amountRange',
      title: '应纳税所得额',
      minWidth: 200,
      slots: { default: 'amountRange' },
    },
    {
      field: 'taxRate',
      title: '税率',
      width: 100,
      slots: { default: 'taxRate' },
    },
    { field: 'quickDeduction', title: '速算扣除数', width: 120, formatter: 'formatAmount2' },
    { field: 'effectiveDate', title: '生效日期', width: 110 },
    {
      title: '操作',
      width: 150,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/salary/tax-rate-config/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTaxRateConfigApi } from '#/api/hrm/salary/tax-rate-config';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteTaxRateConfig,
  getTaxRateConfigList,
} from '#/api/hrm/salary/tax-rate-config';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ–°å¢žé…ç½® */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑配置 */
function handleEdit(row: HrmTaxRateConfigApi.TaxRateConfig) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤é…ç½® */
async function handleDelete(row: HrmTaxRateConfigApi.TaxRateConfig) {
  await deleteTaxRateConfig(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['个税税率配置']));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async () => await getTaxRateConfigList(),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      refresh: true,
    },
  } as VxeTableGridOptions<HrmTaxRateConfigApi.TaxRateConfig>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="个税税率配置列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['个税税率配置']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:tax-rate-config:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #amountRange="{ row }">
        {{ row.minAmount }} ~ {{ row.maxAmount === 0 ? '∞' : row.maxAmount }}
      </template>
      <template #taxRate="{ row }">
        {{ (row.taxRate * 100).toFixed(0) }}%
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:tax-rate-config:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:tax-rate-config:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['个税税率配置']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/tax-rate-config/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmTaxRateConfigApi } from '#/api/hrm/salary/tax-rate-config';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createTaxRateConfigList,
  getTaxRateConfig,
  updateTaxRateConfig,
} from '#/api/hrm/salary/tax-rate-config';
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: 150,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmTaxRateConfigApi.TaxRateConfig;
    try {
      if (data.id) {
        await updateTaxRateConfig(data);
      } else {
        await createTaxRateConfigList([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;
    if (!data?.id) {
      await formApi.resetForm();
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getTaxRateConfig(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/transfer/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,215 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTransferApi } from '#/api/hrm/transfer';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { handleTree } from '#/packages/utils/src';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改调岗申请的表单 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'currentDeptId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'currentPostId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'currentDeptName',
      label: '原部门',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '自动获取',
      },
    },
    {
      fieldName: 'currentPostName',
      label: '原岗位',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '自动获取',
      },
    },
    {
      fieldName: 'targetDeptId',
      label: '目标部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        placeholder: '请选择目标部门',
        treeDefaultExpandAll: true,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'targetPostId',
      label: '目标岗位',
      component: 'ApiSelect',
      componentProps: {
        api: getSimplePostList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择目标岗位',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'transferType',
      label: '调岗类型',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.HRM_TRANSFER_TYPE, 'number'),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'effectiveDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择生效日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'reason',
      label: '调岗原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入调岗原因',
        rows: 3,
        maxlength: 500,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'fileUrl',
      label: '附件',
      component: 'Input',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请上传附件',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'transferType',
      label: '调岗类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_TRANSFER_TYPE, 'number'),
        placeholder: '请选择调岗类型',
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmTransferApi.Transfer>['columns'] {
  return [
    { field: 'id', title: '申请编号', width: 80 },
    { field: 'employeeName', title: '申请人', minWidth: 100 },
    { field: 'currentDeptName', title: '原部门', minWidth: 120 },
    { field: 'currentPostName', title: '原岗位', minWidth: 120 },
    { field: 'targetDeptName', title: '目标部门', minWidth: 120 },
    { field: 'targetPostName', title: '目标岗位', minWidth: 120 },
    {
      field: 'transferType',
      title: '调岗类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_TRANSFER_TYPE },
      },
    },
    { field: 'effectiveDate', title: '生效日期', width: 110 },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/transfer/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTransferApi } from '#/api/hrm/transfer';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelTransfer,
  deleteTransfer,
  getTransferPage,
  submitTransfer,
} from '#/api/hrm/transfer';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè°ƒå²—申请 */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è°ƒå²—申请 */
function handleDetail(row: HrmTransferApi.Transfer) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑调岗申请 */
function handleEdit(row: HrmTransferApi.Transfer) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è°ƒå²—申请 */
async function handleDelete(row: HrmTransferApi.Transfer) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', ['调岗申请']),
    duration: 0,
  });
  try {
    await deleteTransfer(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', ['调岗申请']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤è°ƒå²—申请 */
async function handleSubmit(row: HrmTransferApi.Transfer) {
  await submitTransfer(row.id!);
  message.success('提交成功');
  handleRefresh();
}
/** æ’¤é”€è°ƒå²—申请 */
async function handleCancel(row: HrmTransferApi.Transfer) {
  await cancelTransfer(row.id!);
  message.success('撤销成功');
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getTransferPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmTransferApi.Transfer>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="调岗申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['调岗申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:transfer:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '查看',
              type: 'link',
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:transfer:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['hrm:transfer:submit'],
              ifShow: row.status === 0,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '撤销',
              type: 'link',
              auth: ['hrm:transfer:cancel'],
              ifShow: row.status === 10,
              onClick: handleCancel.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:transfer:delete'],
              ifShow: row.status === 0 || row.status === 40,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['调岗申请']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/transfer/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmTransferApi } from '#/api/hrm/transfer';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTransfer, getTransfer, updateTransfer } from '#/api/hrm/transfer';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const currentEmployeeId = ref<number>();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看调岗申请';
  }
  return formType.value === 'update' ? '修改调岗申请' : '新增调岗申请';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmTransferApi.Transfer;
    data.employeeId = currentEmployeeId.value;
    try {
      if (data.id) {
        await updateTransfer(data);
      } else {
        await createTransfer(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;
    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,
        });
      }
      return;
    }
    modalApi.lock();
    try {
      const transfer = await getTransfer(data.id);
      currentEmployeeId.value = transfer.employeeId;
      await formApi.setValues(transfer);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>