2026-09-02 85a3578025c1fa732322613340cbb0bf2c31fdb3
feat(mes): 添加库存现有量导入功能

- 新增导入结果接口定义,包含创建、更新、失败记录统计
- 添加下载导入模板和导入库存现有量API方法
- 创建库存现有量导入弹窗组件,支持文件上传和进度显示
- 在库存管理页面集成导入功能按钮和模态框
- 实现导入成功后的数据刷新回调机制
- 添加导入模板下载功能和导入结果展示界面
已添加1个文件
已修改3个文件
218 ■■■■■ 文件已修改
src/api/mes/wm/materialstock/index.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/import-form.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
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、xlsx æ–‡ä»¶
      </div>
      <!-- å¯¼å…¥è¿›åº¦æ¡ -->
      <div v-if="importing" class="mt-4">
        <div class="mb-2 text-sm text-gray-600">正在导入库存数据,请稍候...</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>
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';
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,