huminmin
4 天以前 6240f8d054b8dc53d81f352648d82533495cad0d
加班申请
已修改2个文件
已添加9个文件
1311 ■■■■■ 文件已修改
src/api/hrm/employee/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/overtime/index.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/structure/index.ts 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/data.ts 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/detail/approve/index.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/index.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/overtime/modules/form.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/data.ts 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/index.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts
@@ -29,6 +29,7 @@
    employeeStatus?: number;
    annualLeaveBalance?: number;
    sickLeaveBalance?: number;
    salaryStructureId?: number;
    baseSalary?: number;
    roleIds?: number[];
    roleNames?: string[];
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}`,
  );
}
/** èŽ·å–å½“å‰ç”³è¯·äººå…³è”çš„å‘˜å·¥æ¡£æ¡ˆ */
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',
  );
}
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}`);
}
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',
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: '扣除用餐时间',
      component: 'Switch',
      componentProps: {
        checkedChildren: '扣除',
        unCheckedChildren: '不扣除',
      },
      defaultValue: false,
    },
    {
      fieldName: 'mealDeductionHours',
      label: '用餐扣除时长',
      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' },
    },
  ];
}
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: '用餐扣除', field: 'mealDeduction', render: (value) => value ? '是' : '否' },
    { label: '扣除时长', 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>
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: '正在删除加班申请...', 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>
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' ? '修改加班申请' : '新增加班申请';
});
/** æ ¹æ®æ—¶æ®µã€è·¨å¤©æ ‡è¯†å’Œç”¨é¤æ‰£é™¤è®¡ç®—加班时长,并按 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 å°æ—¶ï¼Œè¯·æ£€æŸ¥æ—¶æ®µæˆ–用餐扣除');
      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>
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,
    },
  ];
}
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>
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 æ—¶ï¼Œä»…该类型不计算加班工资,审批通过的加班时长仍正常统计。"
      message="加班工资计算说明"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
  </Modal>
</template>