From 6240f8d054b8dc53d81f352648d82533495cad0d Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期四, 20 八月 2026 10:30:19 +0800
Subject: [PATCH] 加班申请

---
 src/views/hrm/overtime/index.vue                |  200 +++++++++
 src/api/hrm/employee/index.ts                   |    3 
 src/views/hrm/salary/structure/modules/form.vue |   95 ++++
 src/views/hrm/overtime/detail/approve/index.vue |   83 +++
 src/views/hrm/overtime/modules/form.vue         |  159 +++++++
 src/views/hrm/salary/structure/index.vue        |  137 ++++++
 src/api/hrm/overtime/index.ts                   |   89 ++++
 src/api/hrm/salary/structure/index.ts           |   50 ++
 src/views/hrm/salary/structure/data.ts          |  163 +++++++
 src/views/hrm/overtime/data.ts                  |  320 ++++++++++++++
 src/views/hrm/employee/data.ts                  |   14 
 11 files changed, 1,312 insertions(+), 1 deletions(-)

diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
index 0287f85..6a659ab 100644
--- a/src/api/hrm/employee/index.ts
+++ b/src/api/hrm/employee/index.ts
@@ -29,6 +29,7 @@
     employeeStatus?: number;
     annualLeaveBalance?: number;
     sickLeaveBalance?: number;
+    salaryStructureId?: number;
     baseSalary?: number;
     roleIds?: number[];
     roleNames?: string[];
@@ -167,4 +168,4 @@
 /** 鍒犻櫎绱ф�ヨ仈绯讳汉 */
 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/overtime/index.ts b/src/api/hrm/overtime/index.ts
new file mode 100644
index 0000000..37f5b15
--- /dev/null
+++ b/src/api/hrm/overtime/index.ts
@@ -0,0 +1,89 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmOvertimeApi {
+  /** 鍔犵彮鐢宠 */
+  export interface Overtime {
+    id?: number;
+    employeeId?: number;
+    employeeName?: string;
+    employeeNo?: string;
+    deptName?: string;
+    postName?: string;
+    overtimeDate?: string;
+    startTime?: string;
+    endTime?: string;
+    duration?: number;
+    overtimeType?: number;
+    crossDay?: boolean;
+    mealDeduction?: boolean;
+    mealDeductionHours?: number;
+    reason?: string;
+    relatedType?: number;
+    relatedCode?: string;
+    workContent?: string;
+    status?: number;
+    processInstanceId?: string;
+    createTime?: string;
+  }
+
+  export interface ApproveProcess {
+    key: string;
+    name: string;
+  }
+}
+
+/** 鏌ヨ鍔犵彮鐢宠鍒嗛〉鍒楄〃 */
+export function getOvertimePage(params: PageParam) {
+  return requestClient.get<PageResult<HrmOvertimeApi.Overtime>>(
+    '/hrm/overtime/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍔犵彮鐢宠璇︽儏 */
+export function getOvertime(id: number) {
+  return requestClient.get<HrmOvertimeApi.Overtime>(
+    `/hrm/overtime/get?id=${id}`,
+  );
+}
+
+/** 鑾峰彇褰撳墠鐢宠浜哄叧鑱旂殑鍛樺伐妗f */
+export function getCurrentOvertimeApplicant() {
+  return requestClient.get<HrmOvertimeApi.Overtime>('/hrm/overtime/current-applicant');
+}
+
+/** 鏂板鍔犵彮鐢宠锛堣崏绋匡級 */
+export function createOvertime(data: HrmOvertimeApi.Overtime) {
+  return requestClient.post('/hrm/overtime/create', data);
+}
+
+/** 鏇存柊鍔犵彮鐢宠 */
+export function updateOvertime(data: HrmOvertimeApi.Overtime) {
+  return requestClient.put('/hrm/overtime/update', data);
+}
+
+/** 鍒犻櫎鍔犵彮鐢宠 */
+export function deleteOvertime(id: number) {
+  return requestClient.delete(`/hrm/overtime/delete?id=${id}`);
+}
+
+/** 鎻愪氦鍔犵彮鐢宠锛堝彂璧峰鎵癸級 */
+export function submitOvertime(id: number, processDefinitionKey: string) {
+  return requestClient.post('/hrm/overtime/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 鎾ら攢鍔犵彮鐢宠 */
+export function cancelOvertime(id: number) {
+  return requestClient.post(`/hrm/overtime/cancel?id=${id}`);
+}
+
+/** 鑾峰彇鍗忓悓鍔炲叕涓殑鍙敤鍔犵彮瀹℃壒娴佺▼ */
+export function getOvertimeApproveProcessDefinitionList() {
+  return requestClient.get<HrmOvertimeApi.ApproveProcess[]>(
+    '/hrm/overtime/approve-process-definition-list',
+  );
+}
diff --git a/src/api/hrm/salary/structure/index.ts b/src/api/hrm/salary/structure/index.ts
new file mode 100644
index 0000000..20a25b8
--- /dev/null
+++ b/src/api/hrm/salary/structure/index.ts
@@ -0,0 +1,50 @@
+import { requestClient } from '#/api/request';
+
+export namespace HrmSalaryStructureApi {
+  export interface SalaryStructure {
+    id?: number;
+    name?: string;
+    overtimePayRatio?: number;
+    restDayOvertimePayRatio?: number;
+    legalHolidayOvertimePayRatio?: number;
+    status?: number;
+    remark?: string;
+    createTime?: string;
+  }
+
+  export interface SalaryStructureQueryParam {
+    name?: string;
+    status?: number;
+  }
+}
+
+export function getSalaryStructureList(
+  params?: HrmSalaryStructureApi.SalaryStructureQueryParam,
+) {
+  return requestClient.get<HrmSalaryStructureApi.SalaryStructure[]>(
+    '/hrm/salary/structure/list',
+    { params },
+  );
+}
+
+export function getSalaryStructure(id: number) {
+  return requestClient.get<HrmSalaryStructureApi.SalaryStructure>(
+    `/hrm/salary/structure/get?id=${id}`,
+  );
+}
+
+export function createSalaryStructure(
+  data: HrmSalaryStructureApi.SalaryStructure,
+) {
+  return requestClient.post('/hrm/salary/structure/create', data);
+}
+
+export function updateSalaryStructure(
+  data: HrmSalaryStructureApi.SalaryStructure,
+) {
+  return requestClient.put('/hrm/salary/structure/update', data);
+}
+
+export function deleteSalaryStructure(id: number) {
+  return requestClient.delete(`/hrm/salary/structure/delete?id=${id}`);
+}
diff --git a/src/views/hrm/employee/data.ts b/src/views/hrm/employee/data.ts
index 9affcfa..5e9404f 100644
--- a/src/views/hrm/employee/data.ts
+++ b/src/views/hrm/employee/data.ts
@@ -10,6 +10,7 @@
 import { Button } from 'ant-design-vue';
 
 import { z } from '#/adapter/form';
+import { getSalaryStructureList } from '#/api/hrm/salary/structure';
 import { getDeptList } from '#/api/system/dept';
 import { getSimplePostList } from '#/api/system/post';
 import { getSimpleRoleList } from '#/api/system/role';
@@ -242,6 +243,19 @@
       },
     },
     {
+      fieldName: 'salaryStructureId',
+      label: '钖叕缁撴瀯',
+      component: 'ApiSelect',
+      componentProps: {
+        api: () => getSalaryStructureList({ status: 0 }),
+        labelField: 'name',
+        valueField: 'id',
+        allowClear: true,
+        placeholder: '璇烽�夋嫨钖叕缁撴瀯',
+      },
+      help: '鍐冲畾宸ヤ綔鏃ュ姞鐝�嶇巼锛涙湭閫夋嫨鏃朵娇鐢ㄧ郴缁熼粯璁� 1.5 鍊�',
+    },
+    {
       fieldName: 'baseSalary',
       label: '鍩烘湰宸ヨ祫',
       component: 'InputNumber',
diff --git a/src/views/hrm/overtime/data.ts b/src/views/hrm/overtime/data.ts
new file mode 100644
index 0000000..a4985b5
--- /dev/null
+++ b/src/views/hrm/overtime/data.ts
@@ -0,0 +1,320 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+export const OVERTIME_TYPE_OPTIONS = [
+  { label: '宸ヤ綔鏃ュ姞鐝�', value: 1 },
+  { label: '浼戞伅鏃ュ姞鐝�', value: 2 },
+  { label: '娉曞畾鑺傚亣鏃ュ姞鐝�', value: 3 },
+];
+
+export const RELATED_TYPE_OPTIONS = [
+  { label: '椤圭洰', value: 1 },
+  { label: '宸ュ崟', value: 2 },
+];
+
+export function getOvertimeTypeLabel(type?: number) {
+  return OVERTIME_TYPE_OPTIONS.find((item) => item.value === type)?.label || '-';
+}
+
+/** 鏂板/淇敼鍔犵彮鐢宠鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'employeeId',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'baseInfoDivider',
+      label: '鐢宠浜哄熀纭�淇℃伅',
+      component: 'Divider',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'employeeName',
+      label: '鐢宠浜�',
+      component: 'Input',
+      componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+    },
+    {
+      fieldName: 'employeeNo',
+      label: '宸ュ彿',
+      component: 'Input',
+      componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+    },
+    {
+      fieldName: 'deptName',
+      label: '鎵�灞為儴闂�',
+      component: 'Input',
+      componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+    },
+    {
+      fieldName: 'postName',
+      label: '宀椾綅/鑱岀骇',
+      component: 'Input',
+      componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+    },
+    {
+      fieldName: 'overtimeInfoDivider',
+      label: '鍔犵彮鏍稿績淇℃伅',
+      component: 'Divider',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'overtimeDate',
+      label: '鍔犵彮鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍔犵彮鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'overtimeType',
+      label: '鍔犵彮绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: OVERTIME_TYPE_OPTIONS,
+        placeholder: '璇烽�夋嫨鍔犵彮绫诲瀷',
+      },
+      help: '涓嶅悓绫诲瀷瀵瑰簲涓嶅悓鐨勮柂璧勮绠楄鍒�',
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'startTime',
+      label: '寮�濮嬫椂闂�',
+      component: 'TimePicker',
+      componentProps: {
+        format: 'HH:mm',
+        minuteStep: 30,
+        placeholder: '璇烽�夋嫨寮�濮嬫椂闂�',
+        valueFormat: 'HH:mm:ss',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'endTime',
+      label: '缁撴潫鏃堕棿',
+      component: 'TimePicker',
+      componentProps: {
+        format: 'HH:mm',
+        minuteStep: 30,
+        placeholder: '璇烽�夋嫨缁撴潫鏃堕棿',
+        valueFormat: 'HH:mm:ss',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'crossDay',
+      label: '鏄惁璺ㄥぉ',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鏄�',
+        disabled: true,
+        unCheckedChildren: '鍚�',
+      },
+      defaultValue: false,
+      help: '缁撴潫鏃堕棿鏃╀簬鎴栫瓑浜庡紑濮嬫椂闂存椂锛岀郴缁熻嚜鍔ㄦ爣璁颁负璺ㄥぉ',
+    },
+    {
+      fieldName: 'mealDeduction',
+      label: '鎵i櫎鐢ㄩ鏃堕棿',
+      component: 'Switch',
+      componentProps: {
+        checkedChildren: '鎵i櫎',
+        unCheckedChildren: '涓嶆墸闄�',
+      },
+      defaultValue: false,
+    },
+    {
+      fieldName: 'mealDeductionHours',
+      label: '鐢ㄩ鎵i櫎鏃堕暱',
+      component: 'InputNumber',
+      componentProps: {
+        min: 0.5,
+        max: 4,
+        precision: 1,
+        step: 0.5,
+        style: { width: '100%' },
+      },
+      defaultValue: 1,
+      suffix: '灏忔椂',
+      dependencies: {
+        triggerFields: ['mealDeduction'],
+        show: (values) => Boolean(values.mealDeduction),
+      },
+    },
+    {
+      fieldName: 'duration',
+      label: '鍔犵彮鏃堕暱',
+      component: 'InputNumber',
+      componentProps: {
+        disabled: true,
+        min: 0,
+        precision: 1,
+        step: 0.5,
+        style: { width: '100%' },
+      },
+      help: '鎸夊疄闄呮椂娈垫墸闄ょ敤椁愭椂闂村悗锛岃嚜鍔ㄥ彇鏁村埌 0.5 灏忔椂',
+      suffix: '灏忔椂',
+      rules: 'required',
+    },
+    {
+      fieldName: 'reasonInfoDivider',
+      label: '鍔犵彮浜嬬敱',
+      component: 'Divider',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'reason',
+      label: '鍔犵彮鍘熷洜',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxlength: 500,
+        placeholder: '璇疯鏄庡姞鐝殑蹇呰鎬у拰鍘熷洜',
+        rows: 3,
+        showCount: true,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'relatedType',
+      label: '鍏宠仈绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: RELATED_TYPE_OPTIONS,
+        placeholder: '鍙�夛細椤圭洰鎴栧伐鍗�',
+      },
+    },
+    {
+      fieldName: 'relatedCode',
+      label: '椤圭洰/宸ュ崟缂栧彿',
+      component: 'Input',
+      componentProps: {
+        allowClear: true,
+        maxlength: 100,
+        placeholder: '璇疯緭鍏ュ叧鑱旈」鐩垨宸ュ崟缂栧彿',
+      },
+      dependencies: {
+        triggerFields: ['relatedType'],
+        componentProps: (values) => ({
+          allowClear: true,
+          maxlength: 100,
+          placeholder: values.relatedType === 1
+            ? '璇疯緭鍏ラ」鐩紪鍙�'
+            : values.relatedType === 2
+              ? '璇疯緭鍏ュ伐鍗曠紪鍙�'
+              : '璇峰厛閫夋嫨鍏宠仈绫诲瀷',
+          disabled: !values.relatedType,
+        }),
+      },
+    },
+    {
+      fieldName: 'workContent',
+      label: '宸ヤ綔鍐呭绠�杩�',
+      component: 'Textarea',
+      formItemClass: 'col-span-2',
+      componentProps: {
+        maxlength: 1000,
+        placeholder: '璇风畝瑕佹弿杩板姞鐝湡闂磋鍒掑畬鎴愮殑鍏蜂綋宸ヤ綔',
+        rows: 4,
+        showCount: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'overtimeDate',
+      label: '鍔犵彮鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨鍔犵彮鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+      },
+    },
+    {
+      fieldName: 'overtimeType',
+      label: '鍔犵彮绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: OVERTIME_TYPE_OPTIONS,
+        placeholder: '璇烽�夋嫨鍔犵彮绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '瀹℃壒鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃瀛楁 */
+export function useGridColumns(): VxeTableGridOptions<HrmOvertimeApi.Overtime>['columns'] {
+  return [
+    { field: 'id', title: '鐢宠缂栧彿', width: 90 },
+    { field: 'employeeName', title: '鐢宠浜�', minWidth: 100 },
+    { field: 'employeeNo', title: '宸ュ彿', minWidth: 110 },
+    { field: 'deptName', title: '鎵�灞為儴闂�', minWidth: 120 },
+    { field: 'overtimeDate', title: '鍔犵彮鏃ユ湡', width: 120 },
+    {
+      field: 'overtimeType',
+      title: '鍔犵彮绫诲瀷',
+      width: 130,
+      slots: { default: 'overtimeType' },
+    },
+    { field: 'startTime', title: '寮�濮嬫椂闂�', width: 100 },
+    { field: 'endTime', title: '缁撴潫鏃堕棿', width: 100 },
+    { field: 'duration', title: '鏃堕暱(灏忔椂)', width: 100 },
+    {
+      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' },
+    },
+  ];
+}
diff --git a/src/views/hrm/overtime/detail/approve/index.vue b/src/views/hrm/overtime/detail/approve/index.vue
new file mode 100644
index 0000000..bfab940
--- /dev/null
+++ b/src/views/hrm/overtime/detail/approve/index.vue
@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { getOvertime } from '#/api/hrm/overtime';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+import { getOvertimeTypeLabel } from '#/views/hrm/overtime/data';
+
+defineOptions({ name: 'HrmOvertimeApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+const loading = ref(false);
+const formData = ref<HrmOvertimeApi.Overtime>();
+
+function textRender(value: unknown) {
+  return value === 0 || value ? String(value) : '-';
+}
+
+const [Descriptions] = useDescription({
+  bordered: true,
+  column: 2,
+  schema: [
+    { label: '鐢宠浜�', field: 'employeeName', render: textRender },
+    { label: '宸ュ彿', field: 'employeeNo', render: textRender },
+    { label: '鎵�灞為儴闂�', field: 'deptName', render: textRender },
+    { label: '宀椾綅/鑱岀骇', field: 'postName', render: textRender },
+    { label: '鍔犵彮鏃ユ湡', field: 'overtimeDate', render: textRender },
+    {
+      label: '鍔犵彮绫诲瀷',
+      field: 'overtimeType',
+      render: (value) => h(Tag, { color: value === 3 ? 'red' : value === 2 ? 'orange' : 'blue' }, () => getOvertimeTypeLabel(Number(value))),
+    },
+    { label: '寮�濮嬫椂闂�', field: 'startTime', render: textRender },
+    { label: '缁撴潫鏃堕棿', field: 'endTime', render: textRender },
+    { label: '鍔犵彮鏃堕暱', field: 'duration', render: (value) => `${textRender(value)} 灏忔椂` },
+    { label: '鏄惁璺ㄥぉ', field: 'crossDay', render: (value) => value ? '鏄�' : '鍚�' },
+    { label: '鐢ㄩ鎵i櫎', field: 'mealDeduction', render: (value) => value ? '鏄�' : '鍚�' },
+    { label: '鎵i櫎鏃堕暱', field: 'mealDeductionHours', render: (value) => value ? `${value} 灏忔椂` : '-' },
+    { label: '鍔犵彮鍘熷洜', field: 'reason', render: textRender },
+    { label: '鍏宠仈绫诲瀷', field: 'relatedType', render: (value) => value === 1 ? '椤圭洰' : value === 2 ? '宸ュ崟' : '-' },
+    { label: '椤圭洰/宸ュ崟缂栧彿', field: 'relatedCode', render: textRender },
+    { label: '宸ヤ綔鍐呭绠�杩�', field: 'workContent', render: textRender },
+    {
+      label: '瀹℃壒鐘舵��',
+      field: 'status',
+      render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }),
+    },
+    { label: '鍒涘缓鏃堕棿', field: 'createTime', render: (value) => formatDateTime(value) as string },
+  ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+  loading.value = true;
+  try {
+    formData.value = await getOvertime(Number(props.id));
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<template>
+  <ContentWrap class="m-2">
+    <Spin :spinning="loading" tip="鍔犺浇涓�...">
+      <div class="rounded-lg border border-border bg-background p-4">
+        <div class="mb-4 flex items-center gap-2">
+          <span class="inline-block h-4 w-1 rounded bg-primary" />
+          <span class="text-base font-semibold">鍔犵彮鐢宠淇℃伅</span>
+        </div>
+        <Descriptions :data="formData" />
+      </div>
+    </Spin>
+  </ContentWrap>
+</template>
diff --git a/src/views/hrm/overtime/index.vue b/src/views/hrm/overtime/index.vue
new file mode 100644
index 0000000..9b58111
--- /dev/null
+++ b/src/views/hrm/overtime/index.vue
@@ -0,0 +1,200 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message, Modal, Select, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  cancelOvertime,
+  deleteOvertime,
+  getOvertimeApproveProcessDefinitionList,
+  getOvertimePage,
+  submitOvertime,
+} from '#/api/hrm/overtime';
+import { $t } from '#/locales';
+
+import {
+  getOvertimeTypeLabel,
+  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: HrmOvertimeApi.Overtime) {
+  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+function handleEdit(row: HrmOvertimeApi.Overtime) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+async function handleDelete(row: HrmOvertimeApi.Overtime) {
+  const hideLoading = message.loading({ content: '姝e湪鍒犻櫎鍔犵彮鐢宠...', duration: 0 });
+  try {
+    await deleteOvertime(row.id!);
+    message.success('鍔犵彮鐢宠鍒犻櫎鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const submitModalVisible = ref(false);
+const currentRow = ref<HrmOvertimeApi.Overtime>();
+const processList = ref<HrmOvertimeApi.ApproveProcess[]>([]);
+const selectedProcessKey = ref<string>();
+const submitLoading = ref(false);
+
+async function handleSubmit(row: HrmOvertimeApi.Overtime) {
+  processList.value = await getOvertimeApproveProcessDefinitionList();
+  currentRow.value = row;
+  selectedProcessKey.value = undefined;
+  submitModalVisible.value = true;
+}
+
+async function confirmSubmit() {
+  if (!selectedProcessKey.value) {
+    message.warning('璇烽�夋嫨鍗忓悓鍔炲叕瀹℃壒娴佺▼');
+    return;
+  }
+  submitLoading.value = true;
+  try {
+    await submitOvertime(currentRow.value!.id!, selectedProcessKey.value);
+    message.success('宸叉彁浜よ嚦鍗忓悓鍔炲叕瀹℃壒');
+    submitModalVisible.value = false;
+    handleRefresh();
+  } finally {
+    submitLoading.value = false;
+  }
+}
+
+async function handleCancel(row: HrmOvertimeApi.Overtime) {
+  await cancelOvertime(row.id!);
+  message.success('鎾ら攢鎴愬姛');
+  handleRefresh();
+}
+
+function getOvertimeTypeColor(type?: number) {
+  return type === 1 ? 'blue' : type === 2 ? 'orange' : type === 3 ? 'red' : 'default';
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useGridFormSchema() },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) =>
+          await getOvertimePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          }),
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<HrmOvertimeApi.Overtime>,
+});
+</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:overtime:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #overtimeType="{ row }">
+        <Tag :color="getOvertimeTypeColor(row.overtimeType)">
+          {{ getOvertimeTypeLabel(row.overtimeType) }}
+        </Tag>
+      </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:overtime:update'],
+              ifShow: row.status === 0,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦',
+              type: 'link',
+              auth: ['hrm:overtime:submit'],
+              ifShow: row.status === 0,
+              onClick: handleSubmit.bind(null, row),
+            },
+            {
+              label: '鎾ら攢',
+              type: 'link',
+              auth: ['hrm:overtime:cancel'],
+              ifShow: row.status === 10,
+              onClick: handleCancel.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:overtime:delete'],
+              ifShow: row.status === 0 || row.status === 40,
+              popConfirm: {
+                title: '纭鍒犻櫎璇ュ姞鐝敵璇峰悧锛�',
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+    <Modal
+      v-model:open="submitModalVisible"
+      title="鎻愪氦鑷冲崗鍚屽姙鍏�"
+      width="420px"
+      :confirm-loading="submitLoading"
+      @ok="confirmSubmit"
+    >
+      <Select
+        v-model:value="selectedProcessKey"
+        class="w-full"
+        placeholder="璇烽�夋嫨鍔犵彮瀹℃壒娴佺▼"
+        :options="processList.map((item) => ({ label: item.name, value: item.key }))"
+      />
+    </Modal>
+  </Page>
+</template>
diff --git a/src/views/hrm/overtime/modules/form.vue b/src/views/hrm/overtime/modules/form.vue
new file mode 100644
index 0000000..4613de8
--- /dev/null
+++ b/src/views/hrm/overtime/modules/form.vue
@@ -0,0 +1,159 @@
+<script lang="ts" setup>
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createOvertime,
+  getCurrentOvertimeApplicant,
+  getOvertime,
+  updateOvertime,
+} from '#/api/hrm/overtime';
+import { $t } from '#/locales';
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+  if (isDetail.value) return '鏌ョ湅鍔犵彮鐢宠';
+  return formType.value === 'update' ? '淇敼鍔犵彮鐢宠' : '鏂板鍔犵彮鐢宠';
+});
+
+/** 鏍规嵁鏃舵銆佽法澶╂爣璇嗗拰鐢ㄩ鎵i櫎璁$畻鍔犵彮鏃堕暱锛屽苟鎸� 0.5 灏忔椂鍙栨暣 */
+function calculateDuration(values: Record<string, any>) {
+  const { startTime, endTime } = values;
+  if (!startTime || !endTime) return { crossDay: false, duration: 0 };
+
+  const toMinutes = (time: string) => {
+    const [hour = 0, minute = 0] = time.split(':').map(Number);
+    return hour * 60 + minute;
+  };
+  const startMinutes = toMinutes(startTime);
+  let endMinutes = toMinutes(endTime);
+  const crossDay = endMinutes <= startMinutes;
+  if (crossDay) endMinutes += 24 * 60;
+
+  const deductionMinutes = values.mealDeduction
+    ? Number(values.mealDeductionHours || 0) * 60
+    : 0;
+  const actualHours = Math.max(0, (endMinutes - startMinutes - deductionMinutes) / 60);
+  return {
+    crossDay,
+    duration: Math.round(actualHours * 2) / 2,
+  };
+}
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: [],
+  scrollToFirstError: true,
+  showDefaultActions: false,
+  handleValuesChange: (values, changedFields) => {
+    const durationFields = [
+      'startTime',
+      'endTime',
+      'mealDeduction',
+      'mealDeductionHours',
+    ];
+    if (changedFields.some((field) => durationFields.includes(field))) {
+      const result = calculateDuration(values);
+      void formApi.setValues(result);
+    }
+    if (changedFields.includes('relatedType') && !values.relatedType) {
+      void formApi.setFieldValue('relatedCode', undefined);
+    }
+  },
+});
+
+async function setCurrentEmployee() {
+  const employee = await getCurrentOvertimeApplicant();
+  await formApi.setValues({
+    employeeId: employee.employeeId,
+    employeeName: employee.employeeName,
+    employeeNo: employee.employeeNo,
+    deptName: employee.deptName,
+    postName: employee.postName,
+    mealDeduction: false,
+    mealDeductionHours: 1,
+    crossDay: false,
+    duration: 0,
+  });
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (isDetail.value) {
+      await modalApi.close();
+      return;
+    }
+    const { valid } = await formApi.validate();
+    if (!valid) return;
+
+    const data = (await formApi.getValues()) as HrmOvertimeApi.Overtime;
+    if (!data.employeeId) {
+      message.warning('褰撳墠璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏆傛椂鏃犳硶鎻愪氦鍔犵彮鐢宠');
+      return;
+    }
+    if (!data.duration || data.duration <= 0) {
+      message.warning('鍔犵彮鏃堕暱蹇呴』澶т簬 0 灏忔椂锛岃妫�鏌ユ椂娈垫垨鐢ㄩ鎵i櫎');
+      return;
+    }
+    if (data.relatedType && !data.relatedCode?.trim()) {
+      message.warning('璇烽�夋嫨鍏宠仈绫诲瀷鍚庡~鍐欏搴旂殑椤圭洰鎴栧伐鍗曠紪鍙�');
+      return;
+    }
+
+    modalApi.lock();
+    try {
+      if (data.id) await updateOvertime(data);
+      else await createOvertime(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() });
+    formApi.setDisabled(isDetail.value);
+    modalApi.setState({ showConfirmButton: !isDetail.value });
+
+    if (!data.id) {
+      await setCurrentEmployee();
+      return;
+    }
+
+    modalApi.lock();
+    try {
+      await formApi.setValues(await getOvertime(data.id));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-[900px]">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/hrm/salary/structure/data.ts b/src/views/hrm/salary/structure/data.ts
new file mode 100644
index 0000000..68d1ddf
--- /dev/null
+++ b/src/views/hrm/salary/structure/data.ts
@@ -0,0 +1,163 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+
+export type FormType = 'create' | 'update';
+
+const statusOptions = [
+  { label: '鍚敤', value: 0 },
+  { label: '绂佺敤', value: 1 },
+];
+
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'name',
+      label: '缁撴瀯鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        maxlength: 100,
+        placeholder: '渚嬪锛氭爣鍑嗗憳宸ヨ柂閰�佺鐞嗗眰钖叕',
+        showCount: true,
+      },
+      rules: 'required',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'overtimePayRatio',
+      label: '宸ヤ綔鏃ュ�嶇巼',
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '鍊�',
+        max: 10,
+        min: 0,
+        precision: 2,
+        step: 0.1,
+        placeholder: '0 琛ㄧず涓嶈鍔犵彮宸ヨ祫',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'restDayOvertimePayRatio',
+      label: '浼戞伅鏃ュ�嶇巼',
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '鍊�',
+        max: 10,
+        min: 0,
+        precision: 2,
+        step: 0.1,
+        placeholder: '0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'legalHolidayOvertimePayRatio',
+      label: '娉曞畾鑺傚亣鏃ュ�嶇巼',
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '鍊�',
+        max: 10,
+        min: 0,
+        precision: 2,
+        step: 0.1,
+        placeholder: '0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: { options: statusOptions },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        maxlength: 500,
+        placeholder: '濉啓閫傜敤宀椾綅銆佽亴绾ф垨鐗规畩璁¤柂璇存槑',
+        rows: 3,
+        showCount: true,
+      },
+      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: statusOptions,
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+export function useGridColumns(): VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>['columns'] {
+  return [
+    { field: 'name', title: '缁撴瀯鍚嶇О', minWidth: 180 },
+    {
+      field: 'overtimePayRatio',
+      title: '宸ヤ綔鏃ュ姞鐝�嶇巼',
+      width: 145,
+      slots: { default: 'overtimePayRatio' },
+    },
+    {
+      field: 'restDayOvertimePayRatio',
+      title: '浼戞伅鏃ュ姞鐝�嶇巼',
+      width: 145,
+      slots: { default: 'restDayOvertimePayRatio' },
+    },
+    {
+      field: 'legalHolidayOvertimePayRatio',
+      title: '娉曞畾鑺傚亣鏃ュ�嶇巼',
+      width: 165,
+      slots: { default: 'legalHolidayOvertimePayRatio' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 90,
+      slots: { default: 'status' },
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'actions',
+      title: '鎿嶄綔',
+      fixed: 'right',
+      slots: { default: 'actions' },
+      width: 150,
+    },
+  ];
+}
diff --git a/src/views/hrm/salary/structure/index.vue b/src/views/hrm/salary/structure/index.vue
new file mode 100644
index 0000000..4427175
--- /dev/null
+++ b/src/views/hrm/salary/structure/index.vue
@@ -0,0 +1,137 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteSalaryStructure,
+  getSalaryStructureList,
+} from '#/api/hrm/salary/structure';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+defineOptions({ name: 'HrmSalaryStructure' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmSalaryStructureApi.SalaryStructure) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+async function handleDelete(row: HrmSalaryStructureApi.SalaryStructure) {
+  await deleteSalaryStructure(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['钖叕缁撴瀯']));
+  handleRefresh();
+}
+
+function formatRatio(value?: number) {
+  return value === undefined || value === null ? '-' : `${value.toFixed(2)} 鍊峘;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useGridFormSchema() },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: { enabled: false },
+    proxyConfig: {
+      ajax: {
+        query: async (_params, formValues) =>
+          await getSalaryStructureList(formValues),
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Alert
+      class="mb-4"
+      description="鍩烘湰宸ヨ祫鍙栧憳宸ユ。妗堬紱绀句繚鍜屽叕绉噾鍩烘暟鍙栧憳宸ョぞ淇濇。妗堛�傝繖閲屼粎缁存姢宸ヤ綔鏃ャ�佷紤鎭棩鍜屾硶瀹氳妭鍋囨棩鍔犵彮鍊嶇巼銆�"
+      message="钖叕缁撴瀯缁存姢鍔犵彮璁¤柂瑙勫垯"
+      show-icon
+      type="info"
+    />
+    <Grid table-title="钖叕缁撴瀯鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['钖叕缁撴瀯']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['hrm:salary-structure:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #overtimePayRatio="{ row }">
+        <Tag :color="(row.overtimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+          {{ (row.overtimePayRatio ?? 0) > 0 ? formatRatio(row.overtimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+        </Tag>
+      </template>
+      <template #restDayOvertimePayRatio="{ row }">
+        <Tag :color="(row.restDayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+          {{ (row.restDayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.restDayOvertimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+        </Tag>
+      </template>
+      <template #legalHolidayOvertimePayRatio="{ row }">
+        <Tag :color="(row.legalHolidayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+          {{ (row.legalHolidayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.legalHolidayOvertimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+        </Tag>
+      </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:salary-structure:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:salary-structure:delete'],
+              popConfirm: {
+                title: '纭鍒犻櫎璇ヨ柂閰粨鏋勫悧锛熷凡琚憳宸ヤ娇鐢ㄧ殑缁撴瀯涓嶅彲鍒犻櫎銆�',
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/hrm/salary/structure/modules/form.vue b/src/views/hrm/salary/structure/modules/form.vue
new file mode 100644
index 0000000..0330821
--- /dev/null
+++ b/src/views/hrm/salary/structure/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createSalaryStructure,
+  getSalaryStructure,
+  updateSalaryStructure,
+} from '#/api/hrm/salary/structure';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() =>
+  formType.value === 'update' ? '淇敼钖叕缁撴瀯' : '鏂板钖叕缁撴瀯',
+);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) return;
+    modalApi.lock();
+    try {
+      const data =
+        (await formApi.getValues()) as HrmSalaryStructureApi.SalaryStructure;
+      if (data.id) {
+        await updateSalaryStructure(data);
+      } else {
+        await createSalaryStructure(data);
+      }
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) return;
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    await formApi.resetForm();
+    if (!data.id) {
+      await formApi.setValues({
+        legalHolidayOvertimePayRatio: 3,
+        overtimePayRatio: 1.5,
+        restDayOvertimePayRatio: 2,
+        status: 0,
+      });
+      return;
+    }
+    modalApi.lock();
+    try {
+      await formApi.setValues(await getSalaryStructure(data.id));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-[680px]">
+    <Alert
+      class="mx-4 mb-4"
+      description="涓夌鍔犵彮绫诲瀷鍒嗗埆璁剧疆鍊嶇巼锛涙煇涓�椤硅涓� 0 鏃讹紝浠呰绫诲瀷涓嶈绠楀姞鐝伐璧勶紝瀹℃壒閫氳繃鐨勫姞鐝椂闀夸粛姝e父缁熻銆�"
+      message="鍔犵彮宸ヨ祫璁$畻璇存槑"
+      show-icon
+      type="info"
+    />
+    <Form class="mx-4" />
+  </Modal>
+</template>

--
Gitblit v1.9.3