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