From 55e155a169b63cea3262d95cf51705ef173a36c2 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期一, 12 十月 2026 10:09:40 +0800
Subject: [PATCH] feat(hrm): 合同生成与模板下载、员工转正、导入导出字段对齐

---
 src/api/hrm/employee/index.ts                                |   12 ++
 src/views/hrm/employee/contract/index.vue                    |  156 +++++++++++++++++++++++-------
 src/views/hrm/employee/index.vue                             |   43 ++++++++
 src/views/hrm/employee/contract/modules/attachment-modal.vue |   47 +++++++++
 src/api/hrm/employee/contract/index.ts                       |   25 +++++
 5 files changed, 241 insertions(+), 42 deletions(-)

diff --git a/src/api/hrm/employee/contract/index.ts b/src/api/hrm/employee/contract/index.ts
index a252c3e..5b60ca2 100644
--- a/src/api/hrm/employee/contract/index.ts
+++ b/src/api/hrm/employee/contract/index.ts
@@ -154,3 +154,28 @@
     { file, updateSupport },
   );
 }
+
+/** 鐢熸垚骞朵笅杞藉悎鍚屾枃妗o紙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}`,
+  );
+}
diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
index 0afd4a7..7e1ae52 100644
--- a/src/api/hrm/employee/index.ts
+++ b/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}`);
 }
+
+/** 鍛樺伐杞锛堜粎鍔冲姩鍚堝悓鍙浆姝o級 */
+export function regularizeEmployee(id: number) {
+  return requestClient.put(`/hrm/employee/regular?id=${id}`);
+}
+
+/** 寰呰浆姝f彁閱掑垪琛紙璇曠敤涓�佸姵鍔ㄥ悎鍚屻�佽瘯鐢ㄦ湡鍗冲皢鍒版湡锛� */
+export function getRegularizeReminders(days = 7) {
+  return requestClient.get<HrmEmployeeApi.Employee[]>(
+    `/hrm/employee/regularize-reminders?days=${days}`,
+  );
+}
diff --git a/src/views/hrm/employee/contract/index.vue b/src/views/hrm/employee/contract/index.vue
index 02aa33c..c28ef57 100644
--- a/src/views/hrm/employee/contract/index.vue
+++ b/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();
 }
 
+/** 涓嬭浇鍚堝悓绌虹櫧妯℃澘锛歵ype 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 @@
 /** 瑙i櫎/缁堟鍚堝悓 */
 function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) {
   terminateModalApi.setData({ id: row.id }).open();
+}
+
+/** 鏌ョ湅鍚堝悓闄勪欢 */
+function handleAttachments(row: HrmEmployeeContractApi.EmployeeContract) {
+  attachmentModalApi.setData({ id: row.id }).open();
+}
+
+/** 鐢熸垚鍚堝悓锛坉ocx锛夛細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('鍚堝悓宸茬敓鎴愶紝骞跺綊妗e埌璇ュ悎鍚岄檮浠�');
+  handleRefresh();
+}
+
+/** 鈥滄洿澶氣�濊彍鍗曪細淇敼 / 缁 / 瑙i櫎缁堟 / 鍒犻櫎 */
+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: '瑙i櫎/缁堟',
-              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">瑙i櫎/缁堟</Menu.Item>
+              <Menu.Divider />
+              <Menu.Item danger key="delete">鍒犻櫎</Menu.Item>
+            </Menu>
+          </template>
+        </Dropdown>
       </template>
     </Grid>
   </Page>
diff --git a/src/views/hrm/employee/contract/modules/attachment-modal.vue b/src/views/hrm/employee/contract/modules/attachment-modal.vue
new file mode 100644
index 0000000..56817be
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
index b1fba92..57f2d62 100644
--- a/src/views/hrm/employee/index.vue
+++ b/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();
 }
 
+/** 寰呰浆姝f彁閱掓暟 */
+const regularizeCount = ref(0);
+async function refreshRegularizeReminders() {
+  try {
+    const list = await getRegularizeReminders(7);
+    regularizeCount.value = list?.length ?? 0;
+  } catch {
+    regularizeCount.value = 0;
+  }
+}
+onMounted(refreshRegularizeReminders);
+
+/** 鍛樺伐杞锛堜粎鍔冲姩鍚堝悓鍙浆姝o級 */
+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="`寰呰浆姝f彁閱掞細鏈� ${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}銆戣浆姝e悧锛燂紙浠呭姵鍔ㄥ悎鍚屽彲杞锛屼笉鍚皟宀楋級`,
+                confirm: handleRegularize.bind(null, row),
+              },
+            },
+            {
               label: $t('common.delete'),
               type: 'link',
               danger: true,

--
Gitblit v1.9.3