From 7c1ed5b09faa760292cdd47441ade8a16099d8b8 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期一, 14 九月 2026 15:28:39 +0800
Subject: [PATCH] feat: 售后工单按条绑定部门与原因类型;新增加班工资管理;员工附件留档

---
 src/api/hrm/employee/index.ts                       |    6 
 src/views/aftersales/ticket/data.ts                 |   95 +++++---
 src/views/hrm/salary/overtime-wage/modules/form.vue |   90 ++++++++
 src/views/hrm/salary/overtime-wage/index.vue        |  125 +++++++++++
 src/views/hrm/employee/data.ts                      |   42 +++
 src/views/aftersales/ticket/modules/form.vue        |   27 -
 src/api/hrm/salary/overtime-wage/index.ts           |   62 +++++
 src/views/hrm/employee/index.vue                    |    5 
 src/views/hrm/employee/modules/form.vue             |   13 +
 src/api/aftersales/ticket.ts                        |   10 
 src/views/hrm/employee/modules/detail.vue           |   14 +
 src/packages/constants/src/dict-enum.ts             |    2 
 src/views/hrm/salary/overtime-wage/data.ts          |  146 +++++++++++++
 13 files changed, 578 insertions(+), 59 deletions(-)

diff --git a/src/api/aftersales/ticket.ts b/src/api/aftersales/ticket.ts
index 36120d0..b79998b 100644
--- a/src/api/aftersales/ticket.ts
+++ b/src/api/aftersales/ticket.ts
@@ -7,6 +7,8 @@
   export interface TicketItem {
     id?: number;
     ticketId?: number;
+    /** 鏄庣粏鍞悗鍗曞彿锛堝伐鍗曞彿-搴忓彿锛� */
+    itemNo?: string;
     saleOrderItemId?: number;
     itemId?: number;
     itemCode?: string;
@@ -17,6 +19,14 @@
     returnQuantity?: number;
     needRepair?: boolean;
     needReturn?: boolean;
+    /** 閮ㄩ棬ID */
+    deptId?: number;
+    /** 閮ㄩ棬鍚嶇О */
+    deptName?: string;
+    /** 鍘熷洜绫诲瀷 */
+    reasonType?: number;
+    /** 鍏蜂綋鍘熷洜 */
+    reason?: string;
     remark?: string;
   }
 
diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
index 4c0360a..0afd4a7 100644
--- a/src/api/hrm/employee/index.ts
+++ b/src/api/hrm/employee/index.ts
@@ -27,6 +27,8 @@
     regularDate?: string;
     leaveDate?: string;
     employeeStatus?: number;
+    /** 鍛樺伐绫诲埆锛�1-鏅�氬憳宸ワ紝2-鎶�宸ワ紝3-绠$悊 */
+    employeeCategory?: number;
     annualLeaveBalance?: number;
     sickLeaveBalance?: number;
     salaryStructureId?: number;
@@ -44,6 +46,10 @@
     workHistories?: WorkHistory[];
     /** 绱ф�ヨ仈绯讳汉鍒楄〃 */
     emergencyContacts?: EmergencyContact[];
+    /** 闄勪欢 blobId 鍒楄〃锛堜繚瀛樻椂浼犲叆锛� */
+    blobIds?: number[];
+    /** 闄勪欢鍒楄〃锛堣鎯呮椂杩斿洖锛� */
+    attachmentList?: { id: number; name?: string; url?: string }[];
   }
 
   /** 鏁欒偛缁忓巻 */
diff --git a/src/api/hrm/salary/overtime-wage/index.ts b/src/api/hrm/salary/overtime-wage/index.ts
new file mode 100644
index 0000000..daf9884
--- /dev/null
+++ b/src/api/hrm/salary/overtime-wage/index.ts
@@ -0,0 +1,62 @@
+import { requestClient } from '#/api/request';
+
+export namespace HrmOvertimeWageApi {
+  /** 宀椾綅鍔犵彮宸ヨ祫閰嶇疆 */
+  export interface OvertimeWageConfig {
+    id?: number;
+    /** 宀椾綅ID */
+    postId?: number;
+    /** 宀椾綅鍚嶇О */
+    postName?: string;
+    /** 鍔犵彮鏃惰柂锛堝厓/灏忔椂锛夛紝榛樿 20 */
+    hourlyRate?: number;
+    /** 姣忔湀鏈�澶у姞鐝椂闀夸笂闄愶紙灏忔椂锛夛紝涓虹┖琛ㄧず涓嶉檺 */
+    maxOvertimeHours?: number;
+    /** 鐘舵�侊細0-鍚敤锛�1-绂佺敤 */
+    status?: number;
+    remark?: string;
+    createTime?: string;
+  }
+
+  /** 鏌ヨ鍙傛暟 */
+  export interface OvertimeWageConfigQueryParam {
+    postId?: number;
+    status?: number;
+  }
+}
+
+/** 鏌ヨ宀椾綅鍔犵彮宸ヨ祫閰嶇疆鍒楄〃 */
+export function getOvertimeWageConfigList(
+  params?: HrmOvertimeWageApi.OvertimeWageConfigQueryParam,
+) {
+  return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig[]>(
+    '/hrm/salary/overtime-wage/list',
+    { params },
+  );
+}
+
+/** 鏌ヨ宀椾綅鍔犵彮宸ヨ祫閰嶇疆璇︽儏 */
+export function getOvertimeWageConfig(id: number) {
+  return requestClient.get<HrmOvertimeWageApi.OvertimeWageConfig>(
+    `/hrm/salary/overtime-wage/get?id=${id}`,
+  );
+}
+
+/** 鏂板宀椾綅鍔犵彮宸ヨ祫閰嶇疆 */
+export function createOvertimeWageConfig(
+  data: HrmOvertimeWageApi.OvertimeWageConfig,
+) {
+  return requestClient.post('/hrm/salary/overtime-wage/create', data);
+}
+
+/** 鏇存柊宀椾綅鍔犵彮宸ヨ祫閰嶇疆 */
+export function updateOvertimeWageConfig(
+  data: HrmOvertimeWageApi.OvertimeWageConfig,
+) {
+  return requestClient.put('/hrm/salary/overtime-wage/update', data);
+}
+
+/** 鍒犻櫎宀椾綅鍔犵彮宸ヨ祫閰嶇疆 */
+export function deleteOvertimeWageConfig(id: number) {
+  return requestClient.delete(`/hrm/salary/overtime-wage/delete?id=${id}`);
+}
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..c13f8c6 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -136,6 +136,7 @@
   AFTERSALE_ISSUE_TYPE: 'aftersale_issue_type', // 鍞悗闂绫诲瀷
   AFTERSALE_RETURN_TYPE: 'aftersale_return_type', // 鍞悗閫�璐х被鍨�
   AFTERSALE_SEVERITY_LEVEL: 'aftersale_severity_level', // 鍞悗涓ラ噸绋嬪害
+  AFTERSALE_REASON_TYPE: 'aftersale_reason_type', // 鍞悗鍘熷洜绫诲瀷
 } as const;
 
 /** ========== ERP - 浼佷笟璧勬簮璁″垝妯″潡 ========== */
@@ -317,6 +318,7 @@
 const HRM_DICT = {
   HRM_DEGREE: 'hrm_degree', // HRM 瀛﹀巻
   HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM 鍛樺伐鐘舵��
+  HRM_EMPLOYEE_CATEGORY: 'hrm_employee_category', // HRM 鍛樺伐绫诲埆锛堟櫘閫�/鎶�宸�/绠$悊锛�
   HRM_GENDER: 'hrm_gender', // HRM 鎬у埆
   HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM 瀹℃壒鐘舵��
   HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM 璇峰亣绫诲瀷
diff --git a/src/views/aftersales/ticket/data.ts b/src/views/aftersales/ticket/data.ts
index 15c83cc..0a27a78 100644
--- a/src/views/aftersales/ticket/data.ts
+++ b/src/views/aftersales/ticket/data.ts
@@ -12,10 +12,11 @@
 
 import { z } from '#/adapter/form';
 import CrmCustomerSelect from '#/components/crm-customer-select.vue';
-import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
 import { MdItemSelect } from '#/views/mes/md/item/components';
 import { WmBatchSelect } from '#/views/wls/batch/components';
 import { UserSelect } from '#/views/system/user/components';
+import { getDeptList } from '#/api/system/dept';
+import { handleTree } from '#/packages/utils/src';
 
 /** 琛ㄥ崟绫诲瀷 */
 export type TicketFormType = 'create' | 'detail' | 'update';
@@ -40,11 +41,7 @@
 }
 
 /** 宸ュ崟琛ㄥ崟 */
-export function useTicketFormSchema(
-  formType: TicketFormType,
-  formApi?: VbenFormApi,
-  onSaleOrderChange?: (order: Record<string, unknown> | undefined) => void,
-): VbenFormSchema[] {
+export function useTicketFormSchema(formType: TicketFormType): VbenFormSchema[] {
   const readonly = isHeaderReadonly(formType);
   return [
     {
@@ -71,32 +68,6 @@
         placeholder: '璇疯緭鍏ュ伐鍗曚富棰�',
       },
       rules: 'required',
-    },
-    {
-      fieldName: 'saleOrderId',
-      label: '閿�鍞鍗曞彿',
-      component: markRaw(ErpSaleOrderSelect),
-      componentProps: {
-        placeholder: '璇烽�夋嫨閿�鍞鍗�',
-        outStatus: 3,
-        onChange: (item: Record<string, unknown> | undefined) => {
-          if (formApi) {
-            formApi.setFieldValue('saleOrderNo', item?.no ?? '');
-            if (item?.customerId) {
-              formApi.setFieldValue('customerId', item.customerId);
-            }
-          }
-          onSaleOrderChange?.(item);
-        },
-      },
-    },
-    {
-      fieldName: 'saleOrderNo',
-      component: 'Input',
-      dependencies: {
-        triggerFields: [''],
-        show: () => false,
-      },
     },
     {
       fieldName: 'customerId',
@@ -269,11 +240,6 @@
       minWidth: 150,
     },
     {
-      field: 'saleOrderNo',
-      title: '閿�鍞鍗曞彿',
-      minWidth: 150,
-    },
-    {
       field: 'issueType',
       title: '闂绫诲瀷',
       width: 100,
@@ -322,6 +288,11 @@
 ): VxeTableGridOptions<AftersalesTicketApi.TicketItem>['columns'] {
   return [
     {
+      field: 'itemNo',
+      title: '鏄庣粏鍗曞彿',
+      minWidth: 170,
+    },
+    {
       field: 'itemCode',
       title: '鐗╂枡缂栫爜',
       minWidth: 120,
@@ -357,6 +328,22 @@
       title: '闇�瑕侀��璐�',
       width: 90,
       slots: { default: 'needReturn' },
+    },
+    {
+      field: 'deptName',
+      title: '閮ㄩ棬',
+      minWidth: 120,
+    },
+    {
+      field: 'reasonType',
+      title: '鍘熷洜绫诲瀷',
+      width: 120,
+      slots: { default: 'reasonType' },
+    },
+    {
+      field: 'reason',
+      title: '鍏蜂綋鍘熷洜',
+      minWidth: 160,
     },
     ...(editable
       ? [
@@ -488,6 +475,40 @@
       rules: z.boolean().default(false),
     },
     {
+      fieldName: 'deptId',
+      label: '閮ㄩ棬',
+      component: 'ApiTreeSelect',
+      componentProps: {
+        api: async () => handleTree(await getDeptList()),
+        labelField: 'name',
+        valueField: 'id',
+        childrenField: 'children',
+        placeholder: '璇烽�夋嫨閮ㄩ棬',
+        treeDefaultExpandAll: true,
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'reasonType',
+      label: '鍘熷洜绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.AFTERSALE_REASON_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨鍘熷洜绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'reason',
+      label: '鍏蜂綋鍘熷洜',
+      component: 'Textarea',
+      formItemClass: 'col-span-3',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ叿浣撳師鍥�',
+        rows: 2,
+      },
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
diff --git a/src/views/aftersales/ticket/modules/form.vue b/src/views/aftersales/ticket/modules/form.vue
index bdb3fab..47d677b 100644
--- a/src/views/aftersales/ticket/modules/form.vue
+++ b/src/views/aftersales/ticket/modules/form.vue
@@ -2,14 +2,13 @@
 import type { TicketFormType } from '../data';
 
 import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 
 import { computed, nextTick, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { useVbenModal } from '@vben/common-ui';
-import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants';
+import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum, DICT_TYPE } from '@vben/constants';
 
 import { Button, Divider, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue';
 
@@ -24,7 +23,7 @@
   resolveTicket,
   updateTicket,
 } from '#/api/aftersales/ticket';
-import { getSaleOrder } from '#/api/erp/sale/order';
+import { getDictLabel } from '#/packages/effects/hooks/src';
 import { $t } from '#/locales';
 
 import { useTicketFormSchema, useTicketItemFormSchema, useTicketItemGridColumns } from '../data';
@@ -75,17 +74,6 @@
   return $t('ui.actionTitle.create', ['鍞悗宸ュ崟']);
 });
 
-// 閿�鍞鍗曠墿鏂欒繃婊�
-const orderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]);
-
-function handleSaleOrderChange(item: Record<string, unknown> | undefined) {
-  if (!item?.id) {
-    orderItems.value = [];
-    return;
-  }
-  orderItems.value = (item.items as ErpSaleOrderApi.SaleOrderItem[]) || [];
-}
-
 // 涓昏〃鍗�
 const [Form, formApi] = useVbenForm({
   commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 110 },
@@ -131,11 +119,11 @@
 // Modal锛堝伐鍗曡〃鍗曞脊妗嗭級蹇呴』鍦� ItemModal 涔嬪墠锛岀‘淇� connectedComponent 娉ㄥ叆鍒版纭殑寮规
 const [Modal, modalApi] = useVbenModal({
   async onOpenChange(isOpen: boolean) {
-    if (!isOpen) { formData.value = undefined; items.value = []; orderItems.value = []; showClassify.value = false; return; }
+    if (!isOpen) { formData.value = undefined; items.value = []; showClassify.value = false; return; }
     const data = modalApi.getData<{ formType: TicketFormType; id?: number }>();
     formType.value = data.formType;
     itemsGridApi.setGridOptions({ columns: useTicketItemGridColumns(isEdit.value) });
-    formApi.setState({ schema: useTicketFormSchema(formType.value, formApi, handleSaleOrderChange) });
+    formApi.setState({ schema: useTicketFormSchema(formType.value) });
     formApi.setDisabled(!isEdit.value);
     modalApi.setState({ showConfirmButton: false, showCancelButton: false });
     if (data?.id) {
@@ -152,10 +140,6 @@
             status: 'done', id: item.id,
           }));
           await formApi.setFieldValue('blobIds', blobIds);
-        }
-        if (formData.value?.saleOrderId) {
-          const order = await getSaleOrder(formData.value.saleOrderId);
-          orderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
         }
       } finally { modalApi.unlock(); }
     } else {
@@ -183,7 +167,7 @@
   onOpenChange(isOpen: boolean) {
     if (!isOpen) { editingItemIndex.value = null; return; }
     const d = itemModalApi.getData<AftersalesTicketApi.TicketItem | null>();
-    itemFormApi.setState({ schema: useTicketItemFormSchema(formData.value?.customerId, formData.value?.saleOrderNo, orderItems.value, itemFormApi) });
+    itemFormApi.setState({ schema: useTicketItemFormSchema(formData.value?.customerId, undefined, [], itemFormApi) });
     if (d) { itemFormApi.setValues(d as Record<string, unknown>); } else { itemFormApi.resetForm(); }
   },
 });
@@ -267,6 +251,7 @@
         <ItemsGrid>
           <template #needRepair="{ row }"><Tag :color="row.needRepair ? 'blue' : 'default'">{{ row.needRepair ? '鏄�' : '鍚�' }}</Tag></template>
           <template #needReturn="{ row }"><Tag :color="row.needReturn ? 'orange' : 'default'">{{ row.needReturn ? '鏄�' : '鍚�' }}</Tag></template>
+          <template #reasonType="{ row }">{{ getDictLabel(DICT_TYPE.AFTERSALE_REASON_TYPE, row.reasonType) }}</template>
           <template v-if="isEdit" #itemActions="{ row, rowIndex }">
             <TableAction :actions="[
               { label: $t('common.edit'), type: 'link', icon: ACTION_ICON.EDIT, onClick: () => editItem(rowIndex) },
diff --git a/src/views/hrm/employee/data.ts b/src/views/hrm/employee/data.ts
index 7474ca7..1307eea 100644
--- a/src/views/hrm/employee/data.ts
+++ b/src/views/hrm/employee/data.ts
@@ -153,6 +153,18 @@
       rules: z.number().default(1),
     },
     {
+      fieldName: 'employeeCategory',
+      label: '鍛樺伐绫诲埆',
+      component: 'RadioGroup',
+      componentProps: {
+        buttonStyle: 'solid',
+        optionType: 'button',
+        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'),
+      },
+      rules: z.number().default(1),
+      help: '鏅�氬憳宸ユ寜宀椾綅鍔犵彮鏃惰柂锛涙妧宸ュ姞鐝寜 1:1锛涚鐞嗕笉璁″姞鐝伐璧�',
+    },
+    {
       fieldName: 'birthday',
       label: '鍑虹敓鏃ユ湡',
       component: 'DatePicker',
@@ -292,6 +304,18 @@
         placeholder: '璇疯緭鍏ュ眳浣忓湴鍧�',
       },
     },
+    {
+      fieldName: 'blobIds',
+      label: '闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        help: '鍙笂浼犺韩浠借瘉銆佸鍘嗚瘉涔︺�佷綋妫�鎶ュ憡绛夎祫鏂欎綔涓虹暀妗�',
+      },
+      formItemClass: 'col-span-2',
+    },
   ];
 }
 
@@ -343,6 +367,16 @@
         placeholder: '璇烽�夋嫨鍛樺伐鐘舵��',
       },
     },
+    {
+      fieldName: 'employeeCategory',
+      label: '鍛樺伐绫诲埆',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨鍛樺伐绫诲埆',
+      },
+    },
   ];
 }
 
@@ -380,6 +414,14 @@
         default: 'employeeStatus',
       },
     },
+    {
+      field: 'employeeCategory',
+      title: '鍛樺伐绫诲埆',
+      width: 100,
+      slots: {
+        default: 'employeeCategory',
+      },
+    },
     { field: 'hireDate', title: '鍏ヨ亴鏃ユ湡', width: 110 },
     {
       field: 'createTime',
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
index 9f5cb70..b1fba92 100644
--- a/src/views/hrm/employee/index.vue
+++ b/src/views/hrm/employee/index.vue
@@ -191,6 +191,11 @@
           {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_STATUS, row.employeeStatus) }}
         </Tag>
       </template>
+      <template #employeeCategory="{ row }">
+        <Tag :color="row.employeeCategory === 2 ? 'orange' : row.employeeCategory === 3 ? 'purple' : 'blue'">
+          {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_CATEGORY, row.employeeCategory) }}
+        </Tag>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
diff --git a/src/views/hrm/employee/modules/detail.vue b/src/views/hrm/employee/modules/detail.vue
index ccd7122..9cd2df2 100644
--- a/src/views/hrm/employee/modules/detail.vue
+++ b/src/views/hrm/employee/modules/detail.vue
@@ -78,6 +78,20 @@
       <DescriptionsItem label="灞呬綇鍦板潃" :span="2">{{ employee.address || '-' }}</DescriptionsItem>
     </Descriptions>
 
+    <!-- 闄勪欢鐣欐。 -->
+    <Descriptions title="闄勪欢鐣欐。" bordered :column="1" class="mb-4">
+      <DescriptionsItem label="闄勪欢">
+        <template v-if="employee.attachmentList && employee.attachmentList.length">
+          <div v-for="file in employee.attachmentList" :key="file.id" class="mb-1">
+            <a :href="file.url" target="_blank" rel="noopener">
+              {{ file.name || `闄勪欢${file.id}` }}
+            </a>
+          </div>
+        </template>
+        <span v-else>-</span>
+      </DescriptionsItem>
+    </Descriptions>
+
     <!-- 澶囨敞 -->
     <Descriptions title="鍏朵粬淇℃伅" bordered :column="1" class="mb-4">
       <DescriptionsItem label="澶囨敞">{{ employee.remark || '-' }}</DescriptionsItem>
diff --git a/src/views/hrm/employee/modules/form.vue b/src/views/hrm/employee/modules/form.vue
index e24e6ce..dbae5ef 100644
--- a/src/views/hrm/employee/modules/form.vue
+++ b/src/views/hrm/employee/modules/form.vue
@@ -66,6 +66,10 @@
     // 鎻愪氦琛ㄥ崟
     const data = (await formApi.getValues()) as HrmEmployeeApi.Employee;
     data.remark = remark.value;
+    // 闄勪欢锛氭妸 FileUpload 鐨勫�艰浆鎴� blobId 鏁扮粍
+    if (Array.isArray(data.blobIds)) {
+      data.blobIds = (data.blobIds as any[]).map((item) => item?.id ?? item);
+    }
     // 鑾峰彇瀛愯〃鏁版嵁
     data.educations = educationListRef.value?.getData();
     data.workHistories = workHistoryListRef.value?.getData();
@@ -111,7 +115,14 @@
       employeeData.value = formData;
       remark.value = formData.remark;
       if (data.formType !== 'detail') {
-        await formApi.setValues(formData);
+        const blobIds = (formData.attachmentList || []).map((item) => ({
+          uid: String(item.id),
+          name: item.name || '',
+          url: item.url || '',
+          status: 'done',
+          id: item.id,
+        }));
+        await formApi.setValues({ ...formData, blobIds });
       }
     } finally {
       modalApi.unlock();
diff --git a/src/views/hrm/salary/overtime-wage/data.ts b/src/views/hrm/salary/overtime-wage/data.ts
new file mode 100644
index 0000000..e4c9bb4
--- /dev/null
+++ b/src/views/hrm/salary/overtime-wage/data.ts
@@ -0,0 +1,146 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
+
+import { getSimplePostList } from '#/api/system/post';
+
+export type FormType = 'create' | 'update';
+
+const statusOptions = [
+  { label: '鍚敤', value: 0 },
+  { label: '绂佺敤', value: 1 },
+];
+
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: { triggerFields: [''], show: () => false },
+    },
+    {
+      fieldName: 'postId',
+      label: '宀椾綅',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getSimplePostList,
+        labelField: 'name',
+        valueField: 'id',
+        placeholder: '璇烽�夋嫨宀椾綅锛堝伐绉嶏級',
+      },
+      rules: 'selectRequired',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'hourlyRate',
+      label: '鍔犵彮鏃惰柂',
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '鍏�/灏忔椂',
+        min: 0,
+        precision: 2,
+        step: 1,
+        placeholder: '榛樿 20锛屾寜宀椾綅鍙笉鍚�',
+        style: { width: '100%' },
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'maxOvertimeHours',
+      label: '鏈�澶у姞鐝椂闀�',
+      component: 'InputNumber',
+      componentProps: {
+        addonAfter: '灏忔椂/鏈�',
+        min: 0,
+        precision: 1,
+        step: 1,
+        placeholder: '鐣欑┖琛ㄧず涓嶉檺',
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: { options: statusOptions },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        maxlength: 500,
+        placeholder: '鍙~鍐欓�傜敤璇存槑锛屽銆屼粎鏅�氬憳宸ャ�嶃�屾妧宸ユ寜 1:1銆嶇瓑',
+        rows: 3,
+        showCount: true,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'postId',
+      label: '宀椾綅',
+      component: 'ApiSelect',
+      componentProps: {
+        api: getSimplePostList,
+        labelField: 'name',
+        valueField: 'id',
+        allowClear: true,
+        placeholder: '璇烽�夋嫨宀椾綅',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: statusOptions,
+        placeholder: '璇烽�夋嫨鐘舵��',
+      },
+    },
+  ];
+}
+
+export function useGridColumns(): VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>['columns'] {
+  return [
+    { field: 'postName', title: '宀椾綅锛堝伐绉嶏級', minWidth: 160 },
+    {
+      field: 'hourlyRate',
+      title: '鍔犵彮鏃惰柂',
+      width: 130,
+      slots: { default: 'hourlyRate' },
+    },
+    {
+      field: 'maxOvertimeHours',
+      title: '鏈�澶у姞鐝椂闀�',
+      width: 140,
+      slots: { default: 'maxOvertimeHours' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      width: 90,
+      slots: { default: 'status' },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 180 },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 180,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'actions',
+      title: '鎿嶄綔',
+      fixed: 'right',
+      slots: { default: 'actions' },
+      width: 150,
+    },
+  ];
+}
diff --git a/src/views/hrm/salary/overtime-wage/index.vue b/src/views/hrm/salary/overtime-wage/index.vue
new file mode 100644
index 0000000..043c6e2
--- /dev/null
+++ b/src/views/hrm/salary/overtime-wage/index.vue
@@ -0,0 +1,125 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteOvertimeWageConfig,
+  getOvertimeWageConfigList,
+} from '#/api/hrm/salary/overtime-wage';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+defineOptions({ name: 'HrmOvertimeWage' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmOvertimeWageApi.OvertimeWageConfig) {
+  formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+async function handleDelete(row: HrmOvertimeWageApi.OvertimeWageConfig) {
+  await deleteOvertimeWageConfig(row.id!);
+  message.success($t('ui.actionMessage.deleteSuccess', ['鍔犵彮宸ヨ祫閰嶇疆']));
+  handleRefresh();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: { schema: useGridFormSchema() },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: { enabled: false },
+    proxyConfig: {
+      ajax: {
+        query: async (_params, formValues) =>
+          await getOvertimeWageConfigList(formValues),
+      },
+    },
+    rowConfig: { keyField: 'id', isHover: true },
+    toolbarConfig: { refresh: true, search: true },
+  } as VxeTableGridOptions<HrmOvertimeWageApi.OvertimeWageConfig>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Alert
+      class="mb-4"
+      description="鎸夈�屽矖浣嶏紙宸ョ锛夈�嶉厤缃姞鐝椂钖細鏅�氬憳宸ユ寜姝ゅ鏃惰柂璁″姞鐝伐璧勶紝鍙缃瘡鏈堟渶澶у姞鐝椂闀夸笂闄愶紱鎶�宸ユ寜骞虫椂鏃惰柂 1:1锛岀鐞嗕笉璁″姞鐝伐璧勩�傚姞鐝伐璧勫嚟瀹℃壒閫氳繃鐨勫姞鐝崟鏍哥畻銆�"
+      message="鍔犵彮宸ヨ祫鎸夊矖浣嶅垝鍒�"
+      show-icon
+      type="info"
+    />
+    <Grid table-title="鍔犵彮宸ヨ祫绠$悊">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['鍔犵彮宸ヨ祫']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['hrm:overtime-wage:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #hourlyRate="{ row }">
+        <Tag color="blue">{{ row.hourlyRate ?? 0 }} 鍏�/灏忔椂</Tag>
+      </template>
+      <template #maxOvertimeHours="{ row }">
+        <span v-if="row.maxOvertimeHours === null || row.maxOvertimeHours === undefined">涓嶉檺</span>
+        <span v-else>{{ row.maxOvertimeHours }} 灏忔椂/鏈�</span>
+      </template>
+      <template #status="{ row }">
+        <Tag :color="row.status === 0 ? 'success' : 'default'">
+          {{ row.status === 0 ? '鍚敤' : '绂佺敤' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['hrm:overtime-wage:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['hrm:overtime-wage:delete'],
+              popConfirm: {
+                title: '纭鍒犻櫎璇ュ矖浣嶇殑鍔犵彮宸ヨ祫閰嶇疆鍚楋紵',
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/hrm/salary/overtime-wage/modules/form.vue b/src/views/hrm/salary/overtime-wage/modules/form.vue
new file mode 100644
index 0000000..46db733
--- /dev/null
+++ b/src/views/hrm/salary/overtime-wage/modules/form.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { HrmOvertimeWageApi } from '#/api/hrm/salary/overtime-wage';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createOvertimeWageConfig,
+  getOvertimeWageConfig,
+  updateOvertimeWageConfig,
+} from '#/api/hrm/salary/overtime-wage';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() =>
+  formType.value === 'update' ? '淇敼鍔犵彮宸ヨ祫' : '鏂板鍔犵彮宸ヨ祫',
+);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: { class: 'w-full' },
+    formItemClass: 'col-span-1',
+    labelWidth: 110,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'horizontal',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) return;
+    modalApi.lock();
+    try {
+      const data =
+        (await formApi.getValues()) as HrmOvertimeWageApi.OvertimeWageConfig;
+      if (data.id) {
+        await updateOvertimeWageConfig(data);
+      } else {
+        await createOvertimeWageConfig(data);
+      }
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) return;
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    await formApi.resetForm();
+    if (!data.id) {
+      await formApi.setValues({ hourlyRate: 20, status: 0 });
+      return;
+    }
+    modalApi.lock();
+    try {
+      await formApi.setValues(await getOvertimeWageConfig(data.id));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-[640px]">
+    <Alert
+      class="mx-4 mb-4"
+      description="鏅�氬憳宸ワ細鍔犵彮宸ヨ祫 = 鏈矖浣嶉厤缃殑鏃惰柂 脳 鍔犵彮鏃堕暱锛堣秴杩囨渶澶ф椂闀夸笂闄愮殑閮ㄥ垎涓嶈锛夛紱鎶�宸ユ寜骞虫椂鏃惰柂 1:1锛涚鐞嗕笉璁″姞鐝伐璧勩��"
+      message="鍔犵彮宸ヨ祫璁$畻璇存槑"
+      show-icon
+      type="info"
+    />
+    <Form class="mx-4" />
+  </Modal>
+</template>

--
Gitblit v1.9.3