From 85a3578025c1fa732322613340cbb0bf2c31fdb3 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 02 九月 2026 16:56:39 +0800
Subject: [PATCH] feat(mes): 添加库存现有量导入功能

---
 src/views/wls/materialstock/components/import-form.vue |  165 +++++++++++++++++++++++++++++++++++++++++
 src/views/wls/materialstock/components/index.ts        |    1 
 src/api/mes/wm/materialstock/index.ts                  |   23 +++++
 src/views/wls/materialstock/index.vue                  |   29 +++++++
 4 files changed, 217 insertions(+), 1 deletions(-)

diff --git a/src/api/mes/wm/materialstock/index.ts b/src/api/mes/wm/materialstock/index.ts
index edbd6fe..06a2911 100644
--- a/src/api/mes/wm/materialstock/index.ts
+++ b/src/api/mes/wm/materialstock/index.ts
@@ -64,6 +64,29 @@
   });
 }
 
+/** 搴撳瓨鐜版湁閲忓鍏ョ粨鏋� */
+export interface ImportResult {
+  /** 鏂板缓搴撳瓨璁板綍鎴愬姛鐨勮锛屽厓绱犳牸寮忋�岀墿鏂欑紪鐮�-浠撳簱銆� */
+  createNames?: string[];
+  /** 宸插瓨鍦ㄥ簱瀛樿褰曡皟鏁存垚鍔熺殑琛岋紝鍏冪礌鏍煎紡銆岀墿鏂欑紪鐮�-浠撳簱銆� */
+  updateNames?: string[];
+  /** 瀵煎叆澶辫触鐨勮褰曪紝key 涓恒�岀墿鏂欑紪鐮�-浠撳簱銆嶏紝value 涓哄け璐ュ師鍥� */
+  failureNames?: Record<string, string>;
+}
+
+/** 涓嬭浇搴撳瓨鐜版湁閲忓鍏ユā鏉� */
+export function importMaterialStockTemplate() {
+  return requestClient.download('/mes/wm/material-stock/import-template');
+}
+
+/** 瀵煎叆搴撳瓨鐜版湁閲� */
+export function importMaterialStock(file: File) {
+  return requestClient.upload<MesWmMaterialStockApi.ImportResult>(
+    '/mes/wm/material-stock/import',
+    { file },
+  );
+}
+
 /** 搴撳瓨璋冩暣锛堣嚜瀹氫箟鍑哄叆搴擄級- 鍏ュ簱 */
 export interface MaterialStockAdjustInParams {
   itemId: number; // 鐗╂枡ID
diff --git a/src/views/wls/materialstock/components/import-form.vue b/src/views/wls/materialstock/components/import-form.vue
new file mode 100644
index 0000000..cbe0d00
--- /dev/null
+++ b/src/views/wls/materialstock/components/import-form.vue
@@ -0,0 +1,165 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import {
+  importMaterialStock,
+  importMaterialStockTemplate,
+} from '#/api/mes/wm/materialstock';
+
+defineOptions({ name: 'WmMaterialStockImportModal' });
+
+const emit = defineEmits<{ success: [] }>();
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<MesWmMaterialStockApi.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 importMaterialStock(fileList.value[0] as File);
+      // 瀹屾垚锛氳繘搴︽潯璧版弧
+      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 importMaterialStockTemplate();
+  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-3 rounded-md border border-gray-200 bg-gray-50 p-3 text-sm leading-6 text-gray-600"
+      >
+        妯℃澘鍒楋細鐗╂枡缂栫爜銆佷粨搴撱�佸簱鍖恒�佸簱浣嶃�佹壒娆″彿銆佷緵搴斿晢銆�<b>鎿嶄綔绫诲瀷</b>銆�<b>鍦ㄥ簱鏁伴噺</b>銆�<br />
+        鎿嶄綔绫诲瀷蹇呭~锛�
+        <b class="text-gray-800">澧炲姞</b> / <b class="text-gray-800">鍑忓皯</b>
+        鎸�"鍦ㄥ簱鏁伴噺"浣滀负鏈鍙樺姩閲忓姞鍑忓簱瀛橈紱
+        <b class="text-gray-800">瑕嗙洊</b>
+        鎶婅璁板綍鍦ㄥ簱鏁伴噺鐩存帴璁句负"鍦ㄥ簱鏁伴噺"锛堟湡鍒濆缓璐︽帹鑽愶級锛屽凡瀛樺湪璁板綍鑷姩鎸夊樊棰濊皟鏁淬��<br />
+        浠撳簱/搴撳尯/搴撲綅/渚涘簲鍟嗗彲濉紪鐮佹垨鍚嶇О锛涚墿鏂欏惎鐢ㄦ壒娆$鐞嗘椂鎵规鍙峰繀濉紱姣忚鐙珛鎵ц锛屽け璐ヨ涓嶅奖鍝嶅叾瀹冭锛涗粎鏀寔
+        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 flex-wrap 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="createCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-green-600">
+          <div v-for="name in importResult.createNames" :key="`c-${name}`">
+            {{ name }}
+          </div>
+        </div>
+        <div v-if="updateCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-blue-600">
+          <div v-for="name in importResult.updateNames" :key="`u-${name}`">
+            {{ name }}
+          </div>
+        </div>
+        <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+          <div
+            v-for="(reason, name) in importResult.failureNames"
+            :key="`f-${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>
\ No newline at end of file
diff --git a/src/views/wls/materialstock/components/index.ts b/src/views/wls/materialstock/components/index.ts
index 60d3bf1..7ecabaf 100644
--- a/src/views/wls/materialstock/components/index.ts
+++ b/src/views/wls/materialstock/components/index.ts
@@ -2,3 +2,4 @@
 export { default as WmMaterialStockSelect } from './select.vue';
 export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
 export { default as WmStockMoveModal } from './move-modal.vue';
+export { default as WmMaterialStockImportModal } from './import-form.vue';
diff --git a/src/views/wls/materialstock/index.vue b/src/views/wls/materialstock/index.vue
index 17e3625..91a398b 100644
--- a/src/views/wls/materialstock/index.vue
+++ b/src/views/wls/materialstock/index.vue
@@ -19,7 +19,11 @@
 import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
 import { WmBatchDetail } from '#/views/wls/batch/components';
 import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
-import { WmStockAdjustModal, WmStockMoveModal } from './components';
+import {
+  WmMaterialStockImportModal,
+  WmStockAdjustModal,
+  WmStockMoveModal,
+} from './components';
 
 import { useGridColumns, useGridFormSchema } from './data';
 
@@ -35,6 +39,11 @@
 
 const [StockMoveModal, stockMoveModalApi] = useVbenModal({
   connectedComponent: WmStockMoveModal,
+  destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+  connectedComponent: WmMaterialStockImportModal,
   destroyOnClose: true,
 });
 
@@ -72,6 +81,16 @@
 /** 鎵撳紑搴撳瓨璋冩暣寮圭獥 */
 function handleOpenStockAdjust() {
   stockAdjustModalApi.setData({}).open();
+}
+
+/** 鎵撳紑搴撳瓨鐜版湁閲忓鍏ュ脊绐� */
+function handleOpenImport() {
+  importModalApi.open();
+}
+
+/** 瀵煎叆鎴愬姛鍥炶皟 */
+function handleStockImportSuccess() {
+  gridApi.query();
 }
 
 /** 搴撳瓨璋冩暣鎴愬姛鍥炶皟 */
@@ -155,6 +174,7 @@
     <WmBatchDetail ref="batchDetailRef" />
     <StockAdjustModal @success="handleStockAdjustSuccess" />
     <StockMoveModal @success="handleStockMoveSuccess" />
+    <ImportModal @success="handleStockImportSuccess" />
 
     <div class="flex h-full w-full">
       <!-- 宸︿晶鐗╂枡鍒嗙被鏍� -->
@@ -175,6 +195,13 @@
                   onClick: handleOpenStockAdjust,
                 },
                 {
+                  label: '瀵煎叆',
+                  type: 'primary',
+                  icon: ACTION_ICON.UPLOAD,
+                  auth: ['mes:wm-material-stock:import'],
+                  onClick: handleOpenImport,
+                },
+                {
                   label: $t('ui.actionTitle.export'),
                   type: 'primary',
                   icon: ACTION_ICON.DOWNLOAD,

--
Gitblit v1.9.3