tuqin
9 小时以前 55e155a169b63cea3262d95cf51705ef173a36c2
feat(hrm): 合同生成与模板下载、员工转正、导入导出字段对齐

- 合同列表:新增『生成合同』『附件』『下载合同模板』(含三种模板下拉)
- 员工列表:新增『转正』按钮与待转正提醒
- 员工表单/接口字段对齐
已修改4个文件
已添加1个文件
283 ■■■■■ 文件已修改
src/api/hrm/employee/contract/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/index.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/modules/attachment-modal.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/contract/index.ts
@@ -154,3 +154,28 @@
    { file, updateSupport },
  );
}
/** ç”Ÿæˆå¹¶ä¸‹è½½åˆåŒæ–‡æ¡£ï¼ˆdocx)
 * @param templateType 1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同
 */
export function generateContractDocx(id: number, templateType: number) {
  return requestClient.download(
    `/hrm/employee-contract/generate-docx?id=${id}&templateType=${templateType}`,
  );
}
/** ç”ŸæˆåˆåŒå¹¶å›žå­˜ä¸ºåˆåŒé™„件,返回 blobId */
export function generateContractArchive(id: number, templateType: number) {
  return requestClient.post<number>(
    `/hrm/employee-contract/generate?id=${id}&templateType=${templateType}`,
  );
}
/** ä¸‹è½½åˆåŒç©ºç™½æ¨¡æ¿
 * @param type 1-劳动合同(简版) 2-劳动合同(标准) 3-劳务合同
 */
export function downloadContractTemplate(type: number) {
  return requestClient.download(
    `/hrm/employee-contract/download-template?type=${type}`,
  );
}
src/api/hrm/employee/index.ts
@@ -175,3 +175,15 @@
export function deleteEmergencyContact(id: number) {
  return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
}
/** å‘˜å·¥è½¬æ­£ï¼ˆä»…劳动合同可转正) */
export function regularizeEmployee(id: number) {
  return requestClient.put(`/hrm/employee/regular?id=${id}`);
}
/** å¾…转正提醒列表(试用中、劳动合同、试用期即将到期) */
export function getRegularizeReminders(days = 7) {
  return requestClient.get<HrmEmployeeApi.Employee[]>(
    `/hrm/employee/regularize-reminders?days=${days}`,
  );
}
src/views/hrm/employee/contract/index.vue
@@ -3,12 +3,17 @@
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Tag } from 'ant-design-vue';
import { DownOutlined } from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEmployeeContract,
  downloadContractTemplate,
  generateContractArchive,
  generateContractDocx,
  getEmployeeContractPage,
} from '#/api/hrm/employee/contract';
import { $t } from '#/locales';
@@ -17,6 +22,7 @@
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
import Terminate from './modules/terminate.vue';
import Attachment from './modules/attachment-modal.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -30,6 +36,11 @@
const [TerminateModal, terminateModalApi] = useVbenModal({
  connectedComponent: Terminate,
  destroyOnClose: true,
});
const [AttachmentModal, attachmentModalApi] = useVbenModal({
  connectedComponent: Attachment,
  destroyOnClose: true,
});
@@ -48,6 +59,21 @@
  importModalApi.open();
}
/** ä¸‹è½½åˆåŒç©ºç™½æ¨¡æ¿ï¼štype 1-劳动合同简版 2-劳动合同标准 3-劳务合同 */
const TEMPLATE_NAMES: Record<number, string> = {
  1: '劳动合同(简版).docx',
  2: '劳动合同(标准).docx',
  3: '劳务合同.doc',
};
async function handleDownloadTemplate(type: number) {
  const data = await downloadContractTemplate(type);
  downloadFileFromBlobPart({
    fileName: TEMPLATE_NAMES[type] || '合同模板',
    source: data,
  });
  message.success('模板已下载');
}
/** ç¼–辑合同 */
function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
@@ -61,6 +87,60 @@
/** è§£é™¤/终止合同 */
function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) {
  terminateModalApi.setData({ id: row.id }).open();
}
/** æŸ¥çœ‹åˆåŒé™„ä»¶ */
function handleAttachments(row: HrmEmployeeContractApi.EmployeeContract) {
  attachmentModalApi.setData({ id: row.id }).open();
}
/** ç”ŸæˆåˆåŒï¼ˆdocx):templateType 1-劳动合同简版 2-劳动合同标准 3-劳务合同 */
async function handleGenerate(
  row: HrmEmployeeContractApi.EmployeeContract,
  templateType: number,
) {
  // 1. ç”Ÿæˆå¹¶å›žå­˜ä¸ºåˆåŒé™„ä»¶
  await generateContractArchive(row.id!, templateType);
  // 2. åŒæ—¶ä¸‹è½½ä¸€ä»½
  const data = await generateContractDocx(row.id!, templateType);
  const genName =
    { 1: '劳动合同(简版).docx', 2: '劳动合同(标准).docx', 3: '劳务合同.docx' }[
      templateType
    ] || '合同.docx';
  downloadFileFromBlobPart({
    fileName: `${row.contractNo || row.id}-${genName}`,
    source: data,
  });
  message.success('合同已生成,并归档到该合同附件');
  handleRefresh();
}
/** â€œæ›´å¤šâ€èœå•:修改 / ç»­ç­¾ / è§£é™¤ç»ˆæ­¢ / åˆ é™¤ */
function handleMore({ key }: { key: number | string }, row: HrmEmployeeContractApi.EmployeeContract) {
  switch (key) {
    case 'edit': {
      handleEdit(row);
      break;
    }
    case 'renew': {
      handleRenew(row);
      break;
    }
    case 'terminate': {
      handleTerminate(row);
      break;
    }
    case 'delete': {
      Modal.confirm({
        title: `确认删除合同【${row.contractNo || row.id}】吗?`,
        okText: '删除',
        okType: 'danger',
        cancelText: '取消',
        onOk: () => handleDelete(row),
      });
      break;
    }
  }
}
/** åˆ é™¤åˆåŒ */
@@ -110,6 +190,7 @@
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <TerminateModal @success="handleRefresh" />
    <AttachmentModal />
    <Grid table-title="员工合同列表">
      <template #parentNo="{ row }">
        <span>{{ row.parentNo || '-' }}</span>
@@ -119,6 +200,18 @@
        <span v-else>-</span>
      </template>
      <template #toolbar-tools>
        <Dropdown>
          <Button class="mr-2" type="primary">
            ä¸‹è½½åˆåŒæ¨¡æ¿ <DownOutlined />
          </Button>
          <template #overlay>
            <Menu @click="({ key }) => handleDownloadTemplate(Number(key))">
              <Menu.Item key="1">劳动合同(简版)</Menu.Item>
              <Menu.Item key="2">劳动合同(标准)</Menu.Item>
              <Menu.Item key="3">劳务合同</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
        <TableAction
          :actions="[
            {
@@ -139,44 +232,29 @@
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: 'ç»­ç­¾',
              type: 'link',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:employee-contract:create'],
              ifShow: isTerminable(row),
              onClick: handleRenew.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:employee-contract:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '解除/终止',
              type: 'link',
              icon: ACTION_ICON.CLOSE,
              auth: ['hrm:employee-contract:update'],
              ifShow: isTerminable(row),
              onClick: handleTerminate.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:employee-contract:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['员工合同']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
        <Dropdown>
          <a class="text-primary" @click.prevent>生成合同 <DownOutlined /></a>
          <template #overlay>
            <Menu @click="({ key }) => handleGenerate(row, Number(key))">
              <Menu.Item key="1">劳动合同(简版)</Menu.Item>
              <Menu.Item key="2">劳动合同(标准)</Menu.Item>
              <Menu.Item key="3">劳务合同</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
        <a class="ml-3 text-primary" @click="handleAttachments(row)">附件</a>
        <Dropdown>
          <a class="ml-3 text-primary" @click.prevent>更多 <DownOutlined /></a>
          <template #overlay>
            <Menu @click="(e) => handleMore(e, row)">
              <Menu.Item key="edit">修改</Menu.Item>
              <Menu.Item v-if="isTerminable(row)" key="renew">ç»­ç­¾</Menu.Item>
              <Menu.Item v-if="isTerminable(row)" key="terminate">解除/终止</Menu.Item>
              <Menu.Divider />
              <Menu.Item danger key="delete">删除</Menu.Item>
            </Menu>
          </template>
        </Dropdown>
      </template>
    </Grid>
  </Page>
src/views/hrm/employee/contract/modules/attachment-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { getEmployeeContract } from '#/api/hrm/employee/contract';
const attachments = ref<HrmEmployeeContractApi.AttachmentItem[]>([]);
const loading = ref(false);
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ id?: number }>();
    if (!data?.id) {
      return;
    }
    loading.value = true;
    try {
      const detail = await getEmployeeContract(data.id);
      attachments.value = detail.attachmentList || [];
    } finally {
      loading.value = false;
    }
  },
});
</script>
<template>
  <Modal title="合同附件" class="w-[560px]">
    <div v-if="loading" class="py-6 text-center text-gray-400">加载中...</div>
    <div v-else-if="attachments.length === 0" class="py-6 text-center text-gray-400">
      æš‚无附件(可在「更多 â†’ ä¿®æ”¹ã€é‡Œä¸Šä¼ ï¼Œæˆ–用「生成合同」生成后自动归档)
    </div>
    <ul v-else class="max-h-[50vh] overflow-auto pl-5">
      <li v-for="f in attachments" :key="f.id" class="mb-2 list-disc">
        <a :href="f.url" target="_blank" rel="noopener" class="text-primary">
          {{ f.name || `附件${f.id}` }}
        </a>
      </li>
    </ul>
  </Modal>
</template>
src/views/hrm/employee/index.vue
@@ -5,15 +5,15 @@
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message, Progress, Tag } from 'ant-design-vue';
import { Button, message, Progress, Tag, Alert } from 'ant-design-vue';
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteEmployee, exportEmployee, getEmployeePage } from '#/api/hrm/employee';
import { deleteEmployee, exportEmployee, getEmployeePage, getRegularizeReminders, regularizeEmployee } from '#/api/hrm/employee';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -116,6 +116,26 @@
  importModalApi.open();
}
/** å¾…转正提醒数 */
const regularizeCount = ref(0);
async function refreshRegularizeReminders() {
  try {
    const list = await getRegularizeReminders(7);
    regularizeCount.value = list?.length ?? 0;
  } catch {
    regularizeCount.value = 0;
  }
}
onMounted(refreshRegularizeReminders);
/** å‘˜å·¥è½¬æ­£ï¼ˆä»…劳动合同可转正) */
async function handleRegularize(row: HrmEmployeeApi.Employee) {
  await regularizeEmployee(row.id!);
  message.success(`【${row.name}】已转正`);
  handleRefresh();
  refreshRegularizeReminders();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
@@ -150,6 +170,13 @@
  <Page auto-content-height>
    <ImportModal @success="handleRefresh" />
    <FormModal @success="handleRefresh" />
    <Alert
      v-if="regularizeCount > 0"
      class="mb-3"
      :message="`待转正提醒:有 ${regularizeCount} åå‘˜å·¥è¯•用期即将到期,请及时转正`"
      show-icon
      type="warning"
    />
    <Grid table-title="员工列表">
      <template #toolbar-tools>
        <TableAction
@@ -207,6 +234,16 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '转正',
              type: 'link',
              auth: ['hrm:employee:update'],
              ifShow: () => row.employeeStatus === 2,
              popConfirm: {
                title: `确认将【${row.name}】转正吗?(仅劳动合同可转正,不含调岗)`,
                confirm: handleRegularize.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,