From 5dbd5f73288a1dd739de5d2383678e19ba2aece9 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 02 九月 2026 15:00:01 +0800
Subject: [PATCH] feat(hrm): 添加员工合同导入功能

---
 vite.config.ts                                          |    2 
 src/views/hrm/employee/contract/index.vue               |   19 ++++
 src/views/hrm/employee/index.vue                        |   23 +++--
 src/views/hrm/employee/contract/modules/import-form.vue |  153 ++++++++++++++++++++++++++++++++++++++
 src/api/hrm/employee/contract/index.ts                  |   26 ++++++
 5 files changed, 211 insertions(+), 12 deletions(-)

diff --git a/src/api/hrm/employee/contract/index.ts b/src/api/hrm/employee/contract/index.ts
index 38f1d73..a252c3e 100644
--- a/src/api/hrm/employee/contract/index.ts
+++ b/src/api/hrm/employee/contract/index.ts
@@ -85,6 +85,16 @@
     terminateDate: string;
     terminateReason: string;
   }
+
+  /** 鍚堝悓瀵煎叆缁撴灉 */
+  export interface ImportResult {
+    /** 鏂板鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+    createNames?: string[];
+    /** 瑕嗙洊鏇存柊鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+    updateNames?: string[];
+    /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄憳宸ュ悕锛寁alue 涓哄け璐ュ師鍥� */
+    failureNames?: Record<string, string>;
+  }
 }
 
 /** 鑾峰彇鍛樺伐鍚堝悓鍒嗛〉鍒楄〃 */
@@ -128,3 +138,19 @@
 export function getExpiringContractCount() {
   return requestClient.get<number>('/hrm/employee-contract/expiring-count');
 }
+
+/** 涓嬭浇鍛樺伐鍚堝悓瀵煎叆妯℃澘 */
+export function importEmployeeContractTemplate() {
+  return requestClient.download('/hrm/employee-contract/import-template');
+}
+
+/** 瀵煎叆鍛樺伐鍚堝悓 */
+export function importEmployeeContract(
+  file: File,
+  updateSupport: boolean,
+) {
+  return requestClient.upload<HrmEmployeeContractApi.ImportResult>(
+    '/hrm/employee-contract/import',
+    { file, updateSupport },
+  );
+}
diff --git a/src/views/hrm/employee/contract/index.vue b/src/views/hrm/employee/contract/index.vue
index 8f69373..02aa33c 100644
--- a/src/views/hrm/employee/contract/index.vue
+++ b/src/views/hrm/employee/contract/index.vue
@@ -15,10 +15,16 @@
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
 import Terminate from './modules/terminate.vue';
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+  connectedComponent: ImportForm,
   destroyOnClose: true,
 });
 
@@ -35,6 +41,11 @@
 /** 鏂板鍚堝悓 */
 function handleCreate() {
   formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 瀵煎叆鍚堝悓 */
+function handleImport() {
+  importModalApi.open();
 }
 
 /** 缂栬緫鍚堝悓 */
@@ -97,6 +108,7 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <ImportModal @success="handleRefresh" />
     <TerminateModal @success="handleRefresh" />
     <Grid table-title="鍛樺伐鍚堝悓鍒楄〃">
       <template #parentNo="{ row }">
@@ -116,6 +128,13 @@
               auth: ['hrm:employee-contract:create'],
               onClick: handleCreate,
             },
+            {
+              label: '瀵煎叆',
+              type: 'primary',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['hrm:employee-contract:import'],
+              onClick: handleImport,
+            },
           ]"
         />
       </template>
diff --git a/src/views/hrm/employee/contract/modules/import-form.vue b/src/views/hrm/employee/contract/modules/import-form.vue
new file mode 100644
index 0000000..a9a1dca
--- /dev/null
+++ b/src/views/hrm/employee/contract/modules/import-form.vue
@@ -0,0 +1,153 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Switch, Upload } from 'ant-design-vue';
+
+import {
+  importEmployeeContract,
+  importEmployeeContractTemplate,
+} from '#/api/hrm/employee/contract';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const updateSupport = ref(false);
+const importResult = ref<HrmEmployeeContractApi.ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (fileList.value.length === 0) {
+      message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+      return;
+    }
+    if (importing.value) return;
+    modalApi.lock();
+    importing.value = true;
+    startMockProgress();
+    try {
+      importResult.value = await importEmployeeContract(
+        fileList.value[0] as File,
+        updateSupport.value,
+      );
+      // 瀹屾垚锛氳繘搴︽潯璧版弧
+      stopMockProgress();
+      progressPercent.value = 100;
+      await delay(500);
+      message.success('瀵煎叆瀹屾垚');
+      emit('success');
+    } finally {
+      stopMockProgress();
+      importing.value = false;
+      modalApi.unlock();
+    }
+  },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+  progressPercent.value = 0;
+  progressTimer = setInterval(() => {
+    if (progressPercent.value < 90) {
+      progressPercent.value = Math.min(
+        90,
+        progressPercent.value + Math.random() * 12,
+      );
+    }
+  }, 300);
+}
+
+function stopMockProgress() {
+  if (progressTimer) {
+    clearInterval(progressTimer);
+    progressTimer = undefined;
+  }
+}
+
+function delay(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+  fileList.value = [file];
+  importResult.value = undefined;
+  return false;
+}
+
+function handleRemove() {
+  fileList.value = [];
+  importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+  const data = await importEmployeeContractTemplate();
+  downloadFileFromBlobPart({ fileName: '鍛樺伐鍚堝悓瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createNames?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0);
+const failureCount = computed(
+  () => Object.keys(importResult.value?.failureNames ?? {}).length,
+);
+</script>
+
+<template>
+  <Modal title="瀵煎叆鍛樺伐鍚堝悓" class="w-[720px]">
+    <div class="mx-4">
+      <Upload
+        :max-count="1"
+        accept=".xls,.xlsx"
+        :file-list="fileList"
+        :before-upload="beforeUpload"
+        @remove="handleRemove"
+      >
+        <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+      </Upload>
+      <div class="mt-4">
+        <span class="mr-2">鍚屽憳宸ャ�佸悓鍚堝悓绫诲瀷銆佸悓寮�濮嬫棩鏈熺殑鍚堝悓宸插瓨鍦ㄦ椂瑕嗙洊鏇存柊璇ヨ锛�</span>
+        <Switch v-model:checked="updateSupport" />
+      </div>
+      <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+        <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滃憳宸ュ鍚�/鍚堝悓绫诲瀷/鍚堝悓鏈熼檺绫诲瀷鈥濇寜鍚嶇О鎴栦腑鏂囧~鍐欙紙鍚堝悓绫诲瀷濡傦細鍔冲姩鍚堝悓銆佸姵鍔″悎鍚岋級銆�</p>
+        <p>瀵煎叆鍒ゅ畾锛氭寜鈥滃憳宸ュ鍚� + 鍚堝悓绫诲瀷 + 鍚堝悓寮�濮嬫棩鏈熲�濊瘑鍒�傛湭鍕鹃�夆�滃凡瀛樺湪鏃舵洿鏂扳�濇椂锛屽懡涓凡瀛樺湪鍚堝悓鐨勮浼氬鍏ュけ璐ュ苟鎻愮ず锛涘嬀閫夊悗鍒欒鐩栨洿鏂拌琛岋紙淇濈暀鍘熷悎鍚岀紪鍙凤級锛屾湭鍛戒腑鐨勮涓�寰嬫柊澧炪��</p>
+      </div>
+      <!-- 瀵煎叆杩涘害鏉� -->
+      <div v-if="importing" class="mt-4">
+        <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆鍛樺伐鍚堝悓鏁版嵁锛岃绋嶅��...</div>
+        <Progress :percent="progressPercent" status="active" />
+      </div>
+      <!-- 瀵煎叆缁撴灉 -->
+      <div
+        v-if="importResult"
+        class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+      >
+        <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+        <div class="mb-2 flex gap-4">
+          <span class="text-green-600">鏂板锛歿{ createCount }} 浠�</span>
+          <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 浠�</span>
+          <span class="text-red-600">澶辫触锛歿{ failureCount }} 浠�</span>
+        </div>
+        <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+          <div
+            v-for="(reason, name) in importResult.failureNames"
+            :key="name"
+            class="text-sm leading-6 text-red-500"
+          >
+            {{ name }}锛歿{ reason }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <template #prepend-footer>
+      <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
index 639122d..9f5cb70 100644
--- a/src/views/hrm/employee/index.vue
+++ b/src/views/hrm/employee/index.vue
@@ -216,17 +216,18 @@
         />
       </template>
     </Grid>
-  </Page>
-  <!-- 瀵煎嚭杩涘害鏉¢伄缃� -->
-  <div
-    v-if="exporting"
-    class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40"
-  >
-    <div class="w-80 rounded-lg bg-white p-6 shadow-lg">
-      <div class="mb-3 text-center text-sm text-gray-600">
-        姝e湪瀵煎嚭鍛樺伐鍒楄〃...
+
+    <!-- 瀵煎嚭杩涘害鏉¢伄缃� -->
+    <div
+      v-if="exporting"
+      class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40"
+    >
+      <div class="w-80 rounded-lg bg-white p-6 shadow-lg">
+        <div class="mb-3 text-center text-sm text-gray-600">
+          姝e湪瀵煎嚭鍛樺伐鍒楄〃...
+        </div>
+        <Progress :percent="exportPercent" status="active" />
       </div>
-      <Progress :percent="exportPercent" status="active" />
     </div>
-  </div>
+  </Page>
 </template>
diff --git a/vite.config.ts b/vite.config.ts
index b9e690e..2d42c23 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -83,7 +83,7 @@
         proxy: {
           "/admin-api": {
             changeOrigin: true,
-            target: "http://36.213.90.123:9500",
+            target: "http://192.168.0.10:48080",
             ws: true,
           },
         },

--
Gitblit v1.9.3