feat: 售后工单按条绑定部门与原因类型;新增加班工资管理;员工附件留档

- 售后工单明细:独立单号/部门/原因类型/具体原因,销售订单改为可选
- 新增加班工资管理页面与员工类别字段
- 员工表单附件上传 + 详情附件留档展示
已添加4个文件
已修改9个文件
637 ■■■■■ 文件已修改
src/api/aftersales/ticket.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/overtime-wage/index.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/data.ts 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/modules/form.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/detail.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/form.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/data.ts 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/overtime-wage/modules/form.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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;
  }
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 }[];
  }
  /** æ•™è‚²ç»åކ */
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}`);
}
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 è¯·å‡ç±»åž‹
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',
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) },
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',
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="[
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>
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();
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,
    },
  ];
}
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>
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>