From 536c6fbd1f42ea335abdce561451a6ee87d71465 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 10 八月 2026 14:01:16 +0800
Subject: [PATCH] feat(hrm): 添加员工个人信息字段及导入导出功能

---
 src/api/hrm/employee/index.ts                  |   28 ++++
 src/views/hrm/employee/modules/import-form.vue |  149 ++++++++++++++++++++++++
 src/views/hrm/employee/index.vue               |   79 ++++++++++++
 src/views/hrm/employee/modules/detail.vue      |    5 
 src/views/hrm/employee/data.ts                 |   46 +++++++
 5 files changed, 304 insertions(+), 3 deletions(-)

diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
index 9aeea4d..0287f85 100644
--- a/src/api/hrm/employee/index.ts
+++ b/src/api/hrm/employee/index.ts
@@ -18,6 +18,11 @@
     phone?: string;
     email?: string;
     idCard?: string;
+    nation?: string;
+    marriageStatus?: string;
+    age?: number;
+    education?: string;
+    politicalStatus?: string;
     hireDate?: string;
     regularDate?: string;
     leaveDate?: string;
@@ -82,6 +87,16 @@
     deptName?: string;
     postName?: string;
   }
+
+  /** 鍛樺伐瀵煎叆缁撴灉 */
+  export interface ImportResult {
+    /** 鍒涘缓鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+    createNames?: string[];
+    /** 鏇存柊鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+    updateNames?: string[];
+    /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄憳宸ュ悕锛寁alue 涓哄け璐ュ師鍥� */
+    failureNames?: Record<string, string>;
+  }
 }
 
 /** 鏌ヨ鍛樺伐鍒嗛〉鍒楄〃 */
@@ -126,6 +141,19 @@
   return requestClient.download('/hrm/employee/export-excel', { params });
 }
 
+/** 涓嬭浇鍛樺伐瀵煎叆妯℃澘 */
+export function importEmployeeTemplate() {
+  return requestClient.download('/hrm/employee/import-template');
+}
+
+/** 瀵煎叆鍛樺伐 */
+export function importEmployee(file: File, updateSupport: boolean) {
+  return requestClient.upload<HrmEmployeeApi.ImportResult>(
+    '/hrm/employee/import',
+    { file, updateSupport },
+  );
+}
+
 /** 鍒犻櫎鏁欒偛缁忓巻 */
 export function deleteEducation(id: number) {
   return requestClient.delete(`/hrm/employee/education/delete?id=${id}`);
diff --git a/src/views/hrm/employee/data.ts b/src/views/hrm/employee/data.ts
index b9e0233..9affcfa 100644
--- a/src/views/hrm/employee/data.ts
+++ b/src/views/hrm/employee/data.ts
@@ -177,6 +177,49 @@
       },
     },
     {
+      fieldName: 'nation',
+      label: '姘戞棌',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ皯鏃�',
+      },
+    },
+    {
+      fieldName: 'marriageStatus',
+      label: '濠氬Щ鐘舵��',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ濮荤姸鎬�',
+      },
+    },
+    {
+      fieldName: 'age',
+      label: '骞撮緞',
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ勾榫�',
+        min: 0,
+        max: 150,
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'education',
+      label: '瀛﹀巻',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ鍘�',
+      },
+    },
+    {
+      fieldName: 'politicalStatus',
+      label: '鏀挎不闈㈣矊',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ斂娌婚潰璨�',
+      },
+    },
+    {
       fieldName: 'annualLeaveBalance',
       label: '骞村亣浣欓',
       component: 'InputNumber',
@@ -311,6 +354,9 @@
     { field: 'phone', title: '鎵嬫満鍙风爜', minWidth: 120 },
     { field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
     { field: 'postName', title: '宀椾綅', minWidth: 120 },
+    { field: 'nation', title: '姘戞棌', width: 90 },
+    { field: 'age', title: '骞撮緞', width: 70 },
+    { field: 'education', title: '瀛﹀巻', width: 90 },
     {
       field: 'employeeStatus',
       title: '鍛樺伐鐘舵��',
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
index 98a6237..639122d 100644
--- a/src/views/hrm/employee/index.vue
+++ b/src/views/hrm/employee/index.vue
@@ -5,7 +5,9 @@
 import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
 import { downloadFileFromBlobPart } from '#/packages/utils/src';
 
-import { Button, message, Tag } from 'ant-design-vue';
+import { Button, message, Progress, Tag } from 'ant-design-vue';
+
+import { ref } from 'vue';
 
 import { DICT_TYPE } from '#/packages/constants/src';
 import { getDictLabel } from '#/packages/effects/hooks/src';
@@ -16,9 +18,15 @@
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+  connectedComponent: ImportForm,
   destroyOnClose: true,
 });
 
@@ -57,10 +65,55 @@
   }
 }
 
+const exporting = ref(false);
+const exportPercent = ref(0);
+let exportTimer: ReturnType<typeof setInterval> | undefined;
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startExportProgress() {
+  exportPercent.value = 0;
+  exportTimer = setInterval(() => {
+    if (exportPercent.value < 90) {
+      exportPercent.value = Math.min(
+        90,
+        exportPercent.value + Math.random() * 12,
+      );
+    }
+  }, 300);
+}
+
+function stopExportProgress() {
+  if (exportTimer) {
+    clearInterval(exportTimer);
+    exportTimer = undefined;
+  }
+}
+
+function delay(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
 /** 瀵煎嚭鍛樺伐 */
 async function handleExport() {
-  const data = await exportEmployee(await gridApi.formApi.getValues());
-  downloadFileFromBlobPart({ fileName: '鍛樺伐鍒楄〃.xls', source: data });
+  if (exporting.value) return;
+  exporting.value = true;
+  startExportProgress();
+  try {
+    const data = await exportEmployee(await gridApi.formApi.getValues());
+    // 瀹屾垚锛氳繘搴︽潯璧版弧鍚庤Е鍙戜笅杞�
+    stopExportProgress();
+    exportPercent.value = 100;
+    await delay(500);
+    downloadFileFromBlobPart({ fileName: '鍛樺伐鍒楄〃.xls', source: data });
+  } finally {
+    stopExportProgress();
+    exporting.value = false;
+  }
+}
+
+/** 瀵煎叆鍛樺伐 */
+function handleImport() {
+  importModalApi.open();
 }
 
 const [Grid, gridApi] = useVbenVxeGrid({
@@ -95,6 +148,7 @@
 
 <template>
   <Page auto-content-height>
+    <ImportModal @success="handleRefresh" />
     <FormModal @success="handleRefresh" />
     <Grid table-title="鍛樺伐鍒楄〃">
       <template #toolbar-tools>
@@ -106,6 +160,13 @@
               icon: ACTION_ICON.ADD,
               auth: ['hrm:employee:create'],
               onClick: handleCreate,
+            },
+            {
+              label: '瀵煎叆',
+              type: 'primary',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['hrm:employee:import'],
+              onClick: handleImport,
             },
             {
               label: $t('ui.actionTitle.export'),
@@ -156,4 +217,16 @@
       </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>
+      <Progress :percent="exportPercent" status="active" />
+    </div>
+  </div>
 </template>
diff --git a/src/views/hrm/employee/modules/detail.vue b/src/views/hrm/employee/modules/detail.vue
index ca20c47..4f30a8b 100644
--- a/src/views/hrm/employee/modules/detail.vue
+++ b/src/views/hrm/employee/modules/detail.vue
@@ -48,6 +48,11 @@
       <DescriptionsItem label="鍛樺伐鐘舵��">
         <Tag :color="employeeStatusColor">{{ employeeStatusLabel }}</Tag>
       </DescriptionsItem>
+      <DescriptionsItem label="姘戞棌">{{ employee.nation || '-' }}</DescriptionsItem>
+      <DescriptionsItem label="濠氬Щ鐘舵��">{{ employee.marriageStatus || '-' }}</DescriptionsItem>
+      <DescriptionsItem label="骞撮緞">{{ employee.age ?? '-' }}</DescriptionsItem>
+      <DescriptionsItem label="瀛﹀巻">{{ employee.education || '-' }}</DescriptionsItem>
+      <DescriptionsItem label="鏀挎不闈㈣矊">{{ employee.politicalStatus || '-' }}</DescriptionsItem>
     </Descriptions>
 
     <!-- 宸ヤ綔淇℃伅 -->
diff --git a/src/views/hrm/employee/modules/import-form.vue b/src/views/hrm/employee/modules/import-form.vue
new file mode 100644
index 0000000..1940064
--- /dev/null
+++ b/src/views/hrm/employee/modules/import-form.vue
@@ -0,0 +1,149 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { HrmEmployeeApi } from '#/api/hrm/employee';
+
+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 { importEmployee, importEmployeeTemplate } from '#/api/hrm/employee';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const updateSupport = ref(false);
+const importResult = ref<HrmEmployeeApi.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 importEmployee(
+        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 importEmployeeTemplate();
+  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-[640px]">
+    <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 text-gray-500">
+        浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑"閮ㄩ棬鍚嶇О/宀椾綅鍚嶇О/鐢ㄦ埛瑙掕壊"濉啓鍚嶇О锛岃鑹插涓敤閫楀彿鍒嗛殧
+      </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>

--
Gitblit v1.9.3