From 40f2807b7cee7ae8e528f64a91937303199a30e1 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期四, 10 九月 2026 14:49:36 +0800
Subject: [PATCH] feat(erp/mes/hrm): 新增采购订单/销售订单/计量器具导入弹窗;导入完成自动关闭

---
 src/api/mes/dv/metering/index.ts                        |   20 +
 src/views/erp/purchase/order/index.vue                  |   19 +
 src/views/erp/sale/order/modules/import-form.vue        |  155 ++++++++++++++
 src/api/erp/sale/order/index.ts                         |   20 +
 src/views/erp/sale/order/index.vue                      |   19 +
 src/views/hrm/employee/modules/import-form.vue          |   16 +
 src/views/wls/materialstock/components/import-form.vue  |   16 +
 src/views/mes/dv/metering/modules/import-form.vue       |  155 ++++++++++++++
 src/views/erp/purchase/order/modules/import-form.vue    |  167 +++++++++++++++
 src/views/hrm/employee/contract/modules/import-form.vue |   16 +
 src/api/erp/purchase/order/index.ts                     |   23 ++
 src/views/mes/dv/metering/index.vue                     |   19 +
 12 files changed, 639 insertions(+), 6 deletions(-)

diff --git a/src/api/erp/purchase/order/index.ts b/src/api/erp/purchase/order/index.ts
index 798d3b9..4f0c6d6 100644
--- a/src/api/erp/purchase/order/index.ts
+++ b/src/api/erp/purchase/order/index.ts
@@ -112,6 +112,16 @@
   });
 }
 
+/** 閲囪喘璁㈠崟瀵煎叆缁撴灉 */
+export interface ImportResult {
+  /** 鍒涘缓鎴愬姛鐨勯噰璐崟缂栧彿 */
+  createList?: string[];
+  /** 鏇存柊鎴愬姛鐨勯噰璐崟缂栧彿 */
+  updateList?: string[];
+  /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪噰璐崟缂栧彿鎴栬鍙凤紝value 涓哄け璐ュ師鍥� */
+  failureList?: Record<string, string>;
+}
+
 /** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃鍝嶅簲 */
 export interface ProcessDefinition {
   id: string; // 娴佺▼瀹氫箟ID
@@ -126,3 +136,16 @@
     params: { id },
   });
 }
+
+/** 涓嬭浇閲囪喘璁㈠崟瀵煎叆妯℃澘 */
+export function importPurchaseOrderTemplate() {
+  return requestClient.download('/erp/purchase-order/get-import-template');
+}
+
+/** 瀵煎叆閲囪喘璁㈠崟 */
+export function importPurchaseOrder(file: File, directApprove: boolean) {
+  return requestClient.upload<ImportResult>(
+    '/erp/purchase-order/import',
+    { file, directApprove },
+  );
+}
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 3961aca..069fc90 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -65,6 +65,16 @@
   }
 }
 
+/** 閿�鍞鍗曞鍏ョ粨鏋� */
+export interface ImportResult {
+  /** 鍒涘缓鎴愬姛鐨勯攢鍞崟缂栧彿 */
+  createList?: string[];
+  /** 鏇存柊鎴愬姛鐨勯攢鍞崟缂栧彿 */
+  updateList?: string[];
+  /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪攢鍞崟缂栧彿鎴栬鍙凤紝value 涓哄け璐ュ師鍥� */
+  failureList?: Record<string, string>;
+}
+
 /** 鏌ヨ閿�鍞鍗曞垎椤� */
 export function getSaleOrderPage(params: PageParam) {
   return requestClient.get<PageResult<ErpSaleOrderApi.SaleOrder>>(
@@ -136,3 +146,13 @@
     params: { id },
   });
 }
+
+/** 涓嬭浇閿�鍞鍗曞鍏ユā鏉� */
+export function importSaleOrderTemplate() {
+  return requestClient.download('/erp/sale-order/get-import-template');
+}
+
+/** 瀵煎叆閿�鍞鍗� */
+export function importSaleOrder(file: File) {
+  return requestClient.upload<ImportResult>('/erp/sale-order/import', { file });
+}
diff --git a/src/api/mes/dv/metering/index.ts b/src/api/mes/dv/metering/index.ts
index 55f0e7e..138dd73 100644
--- a/src/api/mes/dv/metering/index.ts
+++ b/src/api/mes/dv/metering/index.ts
@@ -29,6 +29,16 @@
   }
 }
 
+/** 璁¢噺鍣ㄥ叿瀵煎叆缁撴灉 */
+export interface ImportResult {
+  /** 鍒涘缓鎴愬姛鐨勮閲忓櫒鍏风紪鐮� */
+  createList?: string[];
+  /** 鏇存柊鎴愬姛鐨勮閲忓櫒鍏风紪鐮� */
+  updateList?: string[];
+  /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓鸿閲忓櫒鍏风紪鐮佹垨琛屽彿锛寁alue 涓哄け璐ュ師鍥� */
+  failureList?: Record<string, string>;
+}
+
 /** 鏌ヨ璁¢噺鍣ㄥ叿鍒嗛〉 */
 export function getMeteringPage(params: PageParam) {
   return requestClient.get<PageResult<MesDvMeteringApi.Metering>>(
@@ -70,3 +80,13 @@
     '/mes/dv/metering/simple-list',
   );
 }
+
+/** 涓嬭浇璁¢噺鍣ㄥ叿瀵煎叆妯℃澘 */
+export function importMeteringTemplate() {
+  return requestClient.download('/mes/dv/metering/get-import-template');
+}
+
+/** 瀵煎叆璁¢噺鍣ㄥ叿 */
+export function importMetering(file: File) {
+  return requestClient.upload<ImportResult>('/mes/dv/metering/import', { file });
+}
diff --git a/src/views/erp/purchase/order/index.vue b/src/views/erp/purchase/order/index.vue
index 79a6a6c..6189ea1 100644
--- a/src/views/erp/purchase/order/index.vue
+++ b/src/views/erp/purchase/order/index.vue
@@ -21,6 +21,7 @@
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
 
 /** ERP 閲囪喘璁㈠崟鍒楄〃 */
 defineOptions({ name: 'ErpPurchaseOrder' });
@@ -29,6 +30,11 @@
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+  connectedComponent: ImportForm,
   destroyOnClose: true,
 });
 
@@ -41,6 +47,11 @@
 async function handleExport() {
   const data = await exportPurchaseOrder(await gridApi.formApi.getValues());
   downloadFileFromBlobPart({ fileName: '閲囪喘璁㈠崟.xls', source: data });
+}
+
+/** 瀵煎叆閲囪喘璁㈠崟 */
+function handleImport() {
+  importModalApi.open();
 }
 
 /** 鏂板閲囪喘璁㈠崟 */
@@ -142,6 +153,7 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <ImportModal @success="handleRefresh" />
     <Grid table-title="閲囪喘璁㈠崟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -161,6 +173,13 @@
               onClick: handleExport,
             },
             {
+              label: '瀵煎叆',
+              type: 'primary',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['erp:purchase-order:import'],
+              onClick: handleImport,
+            },
+            {
               label: '鎵归噺鍒犻櫎',
               type: 'primary',
               danger: true,
diff --git a/src/views/erp/purchase/order/modules/import-form.vue b/src/views/erp/purchase/order/modules/import-form.vue
new file mode 100644
index 0000000..027d268
--- /dev/null
+++ b/src/views/erp/purchase/order/modules/import-form.vue
@@ -0,0 +1,167 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/erp/purchase/order';
+
+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 {
+  importPurchaseOrder,
+  importPurchaseOrderTemplate,
+} from '#/api/erp/purchase/order';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+/** 鏄惁灏嗗鍏ョ殑璁㈠崟鐩存帴缃负鈥滃凡瀹℃牳鈥� */
+const directApprove = ref(false);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    let shouldClose = false;
+    if (fileList.value.length === 0) {
+      message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+      return;
+    }
+    if (importing.value) return;
+    modalApi.lock();
+    importing.value = true;
+    startMockProgress();
+    try {
+      importResult.value = await importPurchaseOrder(
+        fileList.value[0] as File,
+        directApprove.value,
+      );
+      // 瀹屾垚锛氳繘搴︽潯璧版弧
+      stopMockProgress();
+      progressPercent.value = 100;
+      await delay(300);
+      emit('success');
+      const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
+    } finally {
+      stopMockProgress();
+      importing.value = false;
+      modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
+    }
+  },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 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 importPurchaseOrderTemplate();
+  downloadFileFromBlobPart({ fileName: '閲囪喘璁㈠崟瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+  () => Object.keys(importResult.value?.failureList ?? {}).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="directApprove" />
+      </div>
+      <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+        <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滀緵搴斿晢鍚嶇О鈥濇寜鍚嶇О锛堟垨缂栫爜锛夊~鍐欙紝闇�涓庣郴缁熷唴渚涘簲鍟嗕竴鑷达紱鈥滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛屽尮閰嶄互浜у搧缂栫爜涓哄噯锛夛紱鈥滈噰璐椂闂粹�濇牸寮忓 2026-01-01 10:00:00锛岀暀绌哄彇褰撳墠鏃堕棿銆�</p>
+        <p>涓�寮犻噰璐崟鍙惈澶氭潯鏄庣粏锛氱浉鍚屸�滈噰璐崟缂栧彿鈥濈殑澶氳浼氬悎骞朵负涓�寮犺鍗曪紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滈噰璐崟缂栧彿鈥濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊璁㈠崟銆傞噰璐崟缂栧彿宸插瓨鍦ㄦ椂浼氬鍏ュけ璐ャ��</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.failureList"
+            :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/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 925be2a..5d32a61 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -25,6 +25,7 @@
 
 import { useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
 import SaleOrderPrint from './modules/print.vue';
 import SalesNoticeForm from '#/views/wls/salesnotice/modules/form.vue';
 
@@ -36,6 +37,11 @@
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+  connectedComponent: ImportForm,
   destroyOnClose: true,
 });
 
@@ -53,6 +59,11 @@
 async function handleExport() {
   const data = await exportSaleOrder(await gridApi.formApi.getValues());
   downloadFileFromBlobPart({ fileName: '閿�鍞鍗�.xls', source: data });
+}
+
+/** 瀵煎叆閿�鍞鍗� */
+function handleImport() {
+  importModalApi.open();
 }
 
 /** 鏂板閿�鍞鍗� */
@@ -240,6 +251,7 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <ImportModal @success="handleRefresh" />
     <SalesNoticeFormModal />
     <SaleOrderPrint ref="printRef" />
     <Grid table-title="閿�鍞鍗曞垪琛�">
@@ -261,6 +273,13 @@
               onClick: handleExport,
             },
             {
+              label: '瀵煎叆',
+              type: 'primary',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['erp:sale-order:import'],
+              onClick: handleImport,
+            },
+            {
               label: '鎵归噺鍒犻櫎',
               type: 'primary',
               danger: true,
diff --git a/src/views/erp/sale/order/modules/import-form.vue b/src/views/erp/sale/order/modules/import-form.vue
new file mode 100644
index 0000000..09e4167
--- /dev/null
+++ b/src/views/erp/sale/order/modules/import-form.vue
@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/erp/sale/order';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import { importSaleOrder, importSaleOrderTemplate } from '#/api/erp/sale/order';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    let shouldClose = false;
+    if (fileList.value.length === 0) {
+      message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+      return;
+    }
+    if (importing.value) return;
+    modalApi.lock();
+    importing.value = true;
+    startMockProgress();
+    try {
+      importResult.value = await importSaleOrder(fileList.value[0] as File);
+      // 瀹屾垚锛氳繘搴︽潯璧版弧
+      stopMockProgress();
+      progressPercent.value = 100;
+      await delay(300);
+      emit('success');
+      const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
+    } finally {
+      stopMockProgress();
+      importing.value = false;
+      modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
+    }
+  },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 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 importSaleOrderTemplate();
+  downloadFileFromBlobPart({ fileName: '閿�鍞鍗曞鍏ユā鏉�.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+  () => Object.keys(importResult.value?.failureList ?? {}).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-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+        <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滃鎴峰悕绉扳�濇寜鍚嶇О濉啓锛岄渶涓� CRM 瀹㈡埛涓�鑷达紱鈥滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛屽尮閰嶄互浜у搧缂栫爜涓哄噯锛夛紱鈥滀笅鍗曟椂闂粹�濇牸寮忓 2026-01-01 10:00:00锛岀暀绌哄彇褰撳墠鏃堕棿锛涒�滃崟浠封�濈暀绌哄彇鐗╂枡閿�鍞环銆�</p>
+        <p>涓�寮犻攢鍞崟鍙惈澶氭潯鏄庣粏锛氱浉鍚屸�滈攢鍞崟缂栧彿鈥濈殑澶氳浼氬悎骞朵负涓�寮犺鍗曪紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滈攢鍞崟缂栧彿鈥濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊璁㈠崟銆傞攢鍞崟缂栧彿宸插瓨鍦ㄦ椂浼氬鍏ュけ璐ャ��</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.failureList"
+            :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/contract/modules/import-form.vue b/src/views/hrm/employee/contract/modules/import-form.vue
index a9a1dca..78987ea 100644
--- a/src/views/hrm/employee/contract/modules/import-form.vue
+++ b/src/views/hrm/employee/contract/modules/import-form.vue
@@ -25,6 +25,7 @@
 
 const [Modal, modalApi] = useVbenModal({
   async onConfirm() {
+    let shouldClose = false;
     if (fileList.value.length === 0) {
       message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
       return;
@@ -41,13 +42,24 @@
       // 瀹屾垚锛氳繘搴︽潯璧版弧
       stopMockProgress();
       progressPercent.value = 100;
-      await delay(500);
-      message.success('瀵煎叆瀹屾垚');
+      await delay(300);
       emit('success');
+      const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝瑕嗙洊 ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
     } finally {
       stopMockProgress();
       importing.value = false;
       modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
     }
   },
 });
diff --git a/src/views/hrm/employee/modules/import-form.vue b/src/views/hrm/employee/modules/import-form.vue
index 790d0da..b9f256e 100644
--- a/src/views/hrm/employee/modules/import-form.vue
+++ b/src/views/hrm/employee/modules/import-form.vue
@@ -22,6 +22,7 @@
 
 const [Modal, modalApi] = useVbenModal({
   async onConfirm() {
+    let shouldClose = false;
     if (fileList.value.length === 0) {
       message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
       return;
@@ -38,13 +39,24 @@
       // 瀹屾垚锛氳繘搴︽潯璧版弧
       stopMockProgress();
       progressPercent.value = 100;
-      await delay(500);
-      message.success('瀵煎叆瀹屾垚');
+      await delay(300);
       emit('success');
+      const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭垚鍔� ${createCount.value} 鏉°�佽鐩� ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
     } finally {
       stopMockProgress();
       importing.value = false;
       modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
     }
   },
 });
diff --git a/src/views/mes/dv/metering/index.vue b/src/views/mes/dv/metering/index.vue
index 27cbd3c..eddc3a1 100644
--- a/src/views/mes/dv/metering/index.vue
+++ b/src/views/mes/dv/metering/index.vue
@@ -21,15 +21,26 @@
   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,
+});
+
 /** 鍒锋柊琛ㄦ牸 */
 function handleRefresh() {
   gridApi.query();
+}
+
+/** 瀵煎叆璁¢噺鍣ㄥ叿 */
+function handleImport() {
+  importModalApi.open();
 }
 
 /** 鍒涘缓璁¢噺鍣ㄥ叿 */
@@ -105,6 +116,7 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <ImportModal @success="handleRefresh" />
     <Grid table-title="璁¢噺鍣ㄥ叿鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -123,6 +135,13 @@
               auth: ['mes:dv-metering:export'],
               onClick: handleExport,
             },
+            {
+              label: '瀵煎叆',
+              type: 'primary',
+              icon: ACTION_ICON.UPLOAD,
+              auth: ['mes:dv-metering:import'],
+              onClick: handleImport,
+            },
           ]"
         />
       </template>
diff --git a/src/views/mes/dv/metering/modules/import-form.vue b/src/views/mes/dv/metering/modules/import-form.vue
new file mode 100644
index 0000000..a9e91bc
--- /dev/null
+++ b/src/views/mes/dv/metering/modules/import-form.vue
@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/mes/dv/metering';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import { importMetering, importMeteringTemplate } from '#/api/mes/dv/metering';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    let shouldClose = false;
+    if (fileList.value.length === 0) {
+      message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+      return;
+    }
+    if (importing.value) return;
+    modalApi.lock();
+    importing.value = true;
+    startMockProgress();
+    try {
+      importResult.value = await importMetering(fileList.value[0] as File);
+      // 瀹屾垚锛氳繘搴︽潯璧版弧
+      stopMockProgress();
+      progressPercent.value = 100;
+      await delay(300);
+      emit('success');
+      const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
+    } finally {
+      stopMockProgress();
+      importing.value = false;
+      modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
+    }
+  },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 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 importMeteringTemplate();
+  downloadFileFromBlobPart({ fileName: '璁¢噺鍣ㄥ叿瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+  () => Object.keys(importResult.value?.failureList ?? {}).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-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+        <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆傚繀濉垪锛氳閲忓櫒鍏风紪鐮併�佽閲忓櫒鍏峰悕绉般�佸櫒鍏风被鍒�佺姸鎬侊紱鈥滃櫒鍏风被鍒� / 鐘舵�佲�濆~瀛楀吀涓枃锛堝 鍗″昂/鍏朵粬銆佸湪鐢�/鍋滅敤/鎶ュ簾锛夈��</p>
+        <p>鈥滀娇鐢ㄩ儴闂� / 璐d换浜衡�濇寜绯荤粺鍐呯殑閮ㄩ棬鍚嶇О / 鐢ㄦ埛鏄电О鍖归厤锛屽尮閰嶄笉鍒拌琛屼細澶辫触锛涒�滄牎鍑嗗懆鏈�(鏈�)鈥濈暀绌洪粯璁� 12锛涒�滄姇鍏ヤ娇鐢ㄦ棩鏈熲�濇牸寮忓 2026-01-01銆�</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.failureList"
+            :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/wls/materialstock/components/import-form.vue b/src/views/wls/materialstock/components/import-form.vue
index cbe0d00..b5518a7 100644
--- a/src/views/wls/materialstock/components/import-form.vue
+++ b/src/views/wls/materialstock/components/import-form.vue
@@ -26,6 +26,7 @@
 
 const [Modal, modalApi] = useVbenModal({
   async onConfirm() {
+    let shouldClose = false;
     if (fileList.value.length === 0) {
       message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
       return;
@@ -39,13 +40,24 @@
       // 瀹屾垚锛氳繘搴︽潯璧版弧
       stopMockProgress();
       progressPercent.value = 100;
-      await delay(500);
-      message.success('瀵煎叆瀹屾垚');
+      await delay(300);
       emit('success');
+      const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+      if (failedCount > 0) {
+        message.warning(
+          `瀵煎叆瀹屾垚锛氭垚鍔� ${createCount.value} 鏉°�佽鐩� ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+        );
+      } else {
+        message.success('瀵煎叆瀹屾垚');
+        shouldClose = true;
+      }
     } finally {
       stopMockProgress();
       importing.value = false;
       modalApi.unlock();
+      if (shouldClose) {
+        await modalApi.close();
+      }
     }
   },
 });

--
Gitblit v1.9.3