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