From 704ea58c2e4cfd33e7e6ac377228b12bc673b432 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 07 七月 2026 10:54:07 +0800
Subject: [PATCH] 银川 1.员工管理页面开发、联调 2.人事审批页面开发联调 3.考勤管理页面开发联调 4.薪酬管理页面开发联调

---
 src/views/hrm/employee/components/select.vue                      |  140 +
 src/api/hrm/salary/social-security-scheme/index.ts                |   65 
 src/views/hrm/employee/data.ts                                    |  379 +++
 src/views/hrm/salary/payment/index.vue                            |  132 +
 src/views/hrm/employee/modules/emergency-contact-list.vue         |  126 +
 src/api/hrm/employee/emergency-contact/index.ts                   |   25 
 src/views/hrm/resignation/modules/form.vue                        |   93 
 src/api/hrm/salary/payment/index.ts                               |   63 
 src/api/hrm/resignation/index.ts                                  |   75 
 src/views/hrm/employee/modules/form.vue                           |  177 +
 src/views/hrm/salary/employee-social-security/data.ts             |  178 +
 src/views/hrm/salary/employee-social-security/modules/history.vue |   64 
 src/views/hrm/leave/modules/form.vue                              |   93 
 src/views/hrm/employee/modules/social-security-list.vue           |  174 +
 src/views/hrm/salary/tax-rate-config/index.vue                    |  119 +
 src/views/hrm/attendance/exception/data.ts                        |  132 +
 src/views/hrm/resignation/data.ts                                 |  169 +
 src/views/hrm/salary/calculation/data.ts                          |  130 +
 src/views/hrm/salary/payment/data.ts                              |  118 +
 src/views/hrm/employee/modules/social-security-form.vue           |  162 +
 src/views/hrm/attendance/record/index.vue                         |   94 
 src/views/hrm/salary/tax-rate-config/modules/form.vue             |   87 
 src/api/hrm/attendance/exception/index.ts                         |   49 
 src/api/hrm/attendance/record/index.ts                            |   80 
 src/views/hrm/salary/social-security-scheme/modules/form.vue      |  102 
 src/api/hrm/employee/education/index.ts                           |   25 
 src/api/hrm/employee/work-history/index.ts                        |   25 
 src/views/hrm/employee/index.vue                                  |  146 +
 src/views/hrm/leave/index.vue                                     |  169 +
 src/views/hrm/employee/components/select-dialog.vue               |  209 +
 src/views/hrm/salary/employee-social-security/modules/form.vue    |   88 
 src/views/hrm/employee/modules/detail.vue                         |  102 
 src/packages/constants/src/dict-enum.ts                           |   16 
 src/views/hrm/salary/calculation/index.vue                        |  443 +++
 src/api/hrm/employee/index.ts                                     |  127 +
 src/views/hrm/salary/tax-rate-config/data.ts                      |  107 
 src/views/hrm/resignation/index.vue                               |  190 +
 src/api/hrm/salary/calculation/index.ts                           |  109 
 src/api/hrm/salary/employee-social-security/index.ts              |   77 
 src/views/hrm/attendance/record/modules/import-form.vue           |   75 
 src/api/hrm/salary/tax-rate-config/index.ts                       |   56 
 src/views/hrm/employee/modules/work-history-list.vue              |  156 +
 src/api/hrm/leave/index.ts                                        |   66 
 src/views/hrm/transfer/data.ts                                    |  215 +
 src/api/hrm/transfer/index.ts                                     |   71 
 src/views/hrm/salary/social-security-scheme/index.vue             |  141 +
 src/views/hrm/transfer/index.vue                                  |  169 +
 src/views/hrm/leave/data.ts                                       |  173 +
 src/views/hrm/salary/social-security-scheme/data.ts               |  328 ++
 src/views/hrm/employee/modules/education-list.vue                 |  174 +
 src/views/hrm/transfer/modules/form.vue                           |  109 
 src/views/hrm/attendance/record/data.ts                           |  116 
 src/views/hrm/employee/components/index.ts                        |    2 
 src/views/hrm/salary/employee-social-security/index.vue           |  141 +
 src/views/hrm/attendance/exception/index.vue                      |  110 
 src/views/hrm/attendance/exception/modules/form.vue               |   89 
 56 files changed, 7,050 insertions(+), 0 deletions(-)

diff --git a/src/api/hrm/attendance/exception/index.ts b/src/api/hrm/attendance/exception/index.ts
new file mode 100644
index 0000000..5bb1014
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/attendance/record/index.ts b/src/api/hrm/attendance/record/index.ts
new file mode 100644
index 0000000..7852fa8
--- /dev/null
+++ b/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 });
+}
\ No newline at end of file
diff --git a/src/api/hrm/employee/education/index.ts b/src/api/hrm/employee/education/index.ts
new file mode 100644
index 0000000..636665a
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/employee/emergency-contact/index.ts b/src/api/hrm/employee/emergency-contact/index.ts
new file mode 100644
index 0000000..3956f41
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
new file mode 100644
index 0000000..d5deea4
--- /dev/null
+++ b/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 });
+}
\ No newline at end of file
diff --git a/src/api/hrm/employee/work-history/index.ts b/src/api/hrm/employee/work-history/index.ts
new file mode 100644
index 0000000..4a216fb
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/leave/index.ts b/src/api/hrm/leave/index.ts
new file mode 100644
index 0000000..981d7ff
--- /dev/null
+++ b/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');
+}
\ No newline at end of file
diff --git a/src/api/hrm/resignation/index.ts b/src/api/hrm/resignation/index.ts
new file mode 100644
index 0000000..0e01e44
--- /dev/null
+++ b/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');
+}
\ No newline at end of file
diff --git a/src/api/hrm/salary/calculation/index.ts b/src/api/hrm/salary/calculation/index.ts
new file mode 100644
index 0000000..1e129ab
--- /dev/null
+++ b/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 });
+}
\ No newline at end of file
diff --git a/src/api/hrm/salary/employee-social-security/index.ts b/src/api/hrm/salary/employee-social-security/index.ts
new file mode 100644
index 0000000..18f5b3d
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/salary/payment/index.ts b/src/api/hrm/salary/payment/index.ts
new file mode 100644
index 0000000..c98af96
--- /dev/null
+++ b/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 });
+}
\ No newline at end of file
diff --git a/src/api/hrm/salary/social-security-scheme/index.ts b/src/api/hrm/salary/social-security-scheme/index.ts
new file mode 100644
index 0000000..4b1c69a
--- /dev/null
+++ b/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}`);
+}
diff --git a/src/api/hrm/salary/tax-rate-config/index.ts b/src/api/hrm/salary/tax-rate-config/index.ts
new file mode 100644
index 0000000..5812da0
--- /dev/null
+++ b/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}`);
+}
\ No newline at end of file
diff --git a/src/api/hrm/transfer/index.ts b/src/api/hrm/transfer/index.ts
new file mode 100644
index 0000000..23631c8
--- /dev/null
+++ b/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');
+}
\ No newline at end of file
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 65b0042..8162670 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/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,
diff --git a/src/views/hrm/attendance/exception/data.ts b/src/views/hrm/attendance/exception/data.ts
new file mode 100644
index 0000000..1521cf2
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/attendance/exception/index.vue b/src/views/hrm/attendance/exception/index.vue
new file mode 100644
index 0000000..ea9bbee
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/exception/modules/form.vue b/src/views/hrm/attendance/exception/modules/form.vue
new file mode 100644
index 0000000..abd26f9
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/record/data.ts b/src/views/hrm/attendance/record/data.ts
new file mode 100644
index 0000000..933798f
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/attendance/record/index.vue b/src/views/hrm/attendance/record/index.vue
new file mode 100644
index 0000000..50f3b9e
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/attendance/record/modules/import-form.vue b/src/views/hrm/attendance/record/modules/import-form.vue
new file mode 100644
index 0000000..486e53c
--- /dev/null
+++ b/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銆亁lsx 鏍煎紡鏂囦欢
+      </div>
+    </div>
+    <template #prepend-footer>
+      <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+    </template>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/hrm/employee/components/index.ts b/src/views/hrm/employee/components/index.ts
new file mode 100644
index 0000000..9664e32
--- /dev/null
+++ b/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';
\ No newline at end of file
diff --git a/src/views/hrm/employee/components/select-dialog.vue b/src/views/hrm/employee/components/select-dialog.vue
new file mode 100644
index 0000000..8f00d8e
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/components/select.vue b/src/views/hrm/employee/components/select.vue
new file mode 100644
index 0000000..bde15c3
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/data.ts b/src/views/hrm/employee/data.ts
new file mode 100644
index 0000000..532286b
--- /dev/null
+++ b/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 },
+  ];
+}
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
new file mode 100644
index 0000000..3cc7d6b
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/employee/modules/detail.vue b/src/views/hrm/employee/modules/detail.vue
new file mode 100644
index 0000000..ca20c47
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/education-list.vue b/src/views/hrm/employee/modules/education-list.vue
new file mode 100644
index 0000000..62a1d21
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/emergency-contact-list.vue b/src/views/hrm/employee/modules/emergency-contact-list.vue
new file mode 100644
index 0000000..839f94e
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/form.vue b/src/views/hrm/employee/modules/form.vue
new file mode 100644
index 0000000..0cd7c0d
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/social-security-form.vue b/src/views/hrm/employee/modules/social-security-form.vue
new file mode 100644
index 0000000..11a6203
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/social-security-list.vue b/src/views/hrm/employee/modules/social-security-list.vue
new file mode 100644
index 0000000..f3eb352
--- /dev/null
+++ b/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');
+}
+
+/** 鏂板妗f */
+function handleCreate() {
+  formModalApi.setData({
+    formType: 'create',
+    userId: props.userId,
+    schemeList: schemeList.value,
+  }).open();
+}
+
+/** 缂栬緫妗f */
+function handleEdit(row: HrmEmployeeSocialSecurityApi.EmployeeSocialSecurity) {
+  formModalApi.setData({
+    formType: 'update',
+    id: row.id,
+    userId: props.userId,
+    schemeList: schemeList.value,
+  }).open();
+}
+
+/** 鍒犻櫎妗f */
+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>
\ No newline at end of file
diff --git a/src/views/hrm/employee/modules/work-history-list.vue b/src/views/hrm/employee/modules/work-history-list.vue
new file mode 100644
index 0000000..45a6a71
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/leave/data.ts b/src/views/hrm/leave/data.ts
new file mode 100644
index 0000000..75786f4
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/leave/index.vue b/src/views/hrm/leave/index.vue
new file mode 100644
index 0000000..029edf5
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/leave/modules/form.vue b/src/views/hrm/leave/modules/form.vue
new file mode 100644
index 0000000..9e76099
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/resignation/data.ts b/src/views/hrm/resignation/data.ts
new file mode 100644
index 0000000..0ea0da4
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/resignation/index.vue b/src/views/hrm/resignation/index.vue
new file mode 100644
index 0000000..cdf72da
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/resignation/modules/form.vue b/src/views/hrm/resignation/modules/form.vue
new file mode 100644
index 0000000..8ba43bb
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/calculation/data.ts b/src/views/hrm/salary/calculation/data.ts
new file mode 100644
index 0000000..2aa50f5
--- /dev/null
+++ b/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: '绀句繚鎵f', width: 100, formatter: 'formatAmount2' },
+    { field: 'housingFundDeduction', title: '鍏Н閲戞墸娆�', width: 100, formatter: 'formatAmount2' },
+    { field: 'taxDeduction', title: '涓◣鎵f', width: 100, formatter: 'formatAmount2' },
+    { field: 'otherDeduction', title: '鍏朵粬鎵f', 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',
+      },
+    },
+  ];
+}
+
+/** 棰勮琛ㄦ牸鐨勫垪瀹氫箟锛堢敤浜嶢nt 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: '绀句繚鎵f', dataIndex: 'socialSecurityDeduction', width: 110 },
+    { title: '鍏Н閲戞墸娆�', dataIndex: 'housingFundDeduction', width: 110 },
+    { title: '涓◣鎵f', dataIndex: 'taxDeduction', width: 110 },
+    { title: '鍏朵粬鎵f', dataIndex: 'otherDeduction', width: 100 },
+    { title: '瀹炲彂宸ヨ祫', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
+    { title: '鍑哄嫟澶╂暟', dataIndex: 'workDays', width: 90 },
+    { title: '鍔犵彮鏃堕暱', dataIndex: 'overtimeHours', width: 100 },
+  ];
+}
diff --git a/src/views/hrm/salary/calculation/index.vue b/src/views/hrm/salary/calculation/index.vue
new file mode 100644
index 0000000..59551ab
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/employee-social-security/data.ts b/src/views/hrm/salary/employee-social-security/data.ts
new file mode 100644
index 0000000..8d0bcac
--- /dev/null
+++ b/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' },
+    },
+  ];
+}
diff --git a/src/views/hrm/salary/employee-social-security/index.vue b/src/views/hrm/salary/employee-social-security/index.vue
new file mode 100644
index 0000000..5d741d3
--- /dev/null
+++ b/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();
+}
+
+/** 鏂板妗f */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫妗f */
+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();
+}
+
+/** 鍒犻櫎妗f */
+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>
diff --git a/src/views/hrm/salary/employee-social-security/modules/form.vue b/src/views/hrm/salary/employee-social-security/modules/form.vue
new file mode 100644
index 0000000..e95859b
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/salary/employee-social-security/modules/history.vue b/src/views/hrm/salary/employee-social-security/modules/history.vue
new file mode 100644
index 0000000..1a5029e
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/salary/payment/data.ts b/src/views/hrm/salary/payment/data.ts
new file mode 100644
index 0000000..c93e181
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/salary/payment/index.vue b/src/views/hrm/salary/payment/index.vue
new file mode 100644
index 0000000..3933c41
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/social-security-scheme/data.ts b/src/views/hrm/salary/social-security-scheme/data.ts
new file mode 100644
index 0000000..2e363b8
--- /dev/null
+++ b/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' },
+    },
+  ];
+}
diff --git a/src/views/hrm/salary/social-security-scheme/index.vue b/src/views/hrm/salary/social-security-scheme/index.vue
new file mode 100644
index 0000000..65c71b5
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/social-security-scheme/modules/form.vue b/src/views/hrm/salary/social-security-scheme/modules/form.vue
new file mode 100644
index 0000000..9611251
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/tax-rate-config/data.ts b/src/views/hrm/salary/tax-rate-config/data.ts
new file mode 100644
index 0000000..2765700
--- /dev/null
+++ b/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: '閫熺畻鎵i櫎鏁�',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ�熺畻鎵i櫎鏁�',
+        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: '閫熺畻鎵i櫎鏁�', width: 120, formatter: 'formatAmount2' },
+    { field: 'effectiveDate', title: '鐢熸晥鏃ユ湡', width: 110 },
+    {
+      title: '鎿嶄綔',
+      width: 150,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/salary/tax-rate-config/index.vue b/src/views/hrm/salary/tax-rate-config/index.vue
new file mode 100644
index 0000000..55a2fde
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/salary/tax-rate-config/modules/form.vue b/src/views/hrm/salary/tax-rate-config/modules/form.vue
new file mode 100644
index 0000000..13e4f6b
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/transfer/data.ts b/src/views/hrm/transfer/data.ts
new file mode 100644
index 0000000..61cd895
--- /dev/null
+++ b/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',
+      },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/hrm/transfer/index.vue b/src/views/hrm/transfer/index.vue
new file mode 100644
index 0000000..36e331d
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/hrm/transfer/modules/form.vue b/src/views/hrm/transfer/modules/form.vue
new file mode 100644
index 0000000..5b07236
--- /dev/null
+++ b/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>
\ No newline at end of file

--
Gitblit v1.9.3