gaoluyang
17 小时以前 0db4bf5a865d183a83dc991d2ef53f820dca8c77
src/views/basicData/mdm/modules/import-form.vue
@@ -1,18 +1,28 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h } from 'vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, notification, Upload } from 'ant-design-vue';
import { Button, message, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importItem, importItemTemplate } from '#/api/mdm/item';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
/** 导入结果,对应后端 MdmItemImportRespVO */
interface ImportResult {
  /** 新增成功的物料 */
  createList?: string[];
  /** 更新成功的物料 */
  updateList?: string[];
  /** 失败明细:key 为「物料编码 名称」,value 为失败原因 */
  failureList?: Record<string, string>;
}
const emit = defineEmits(['success']);
@@ -26,6 +36,17 @@
  showDefaultActions: false,
});
const importResult = ref<ImportResult>({});
const createCount = computed(() => importResult.value.createList?.length ?? 0);
const updateCount = computed(() => importResult.value.updateList?.length ?? 0);
const failureEntries = computed(() =>
  Object.entries(importResult.value.failureList ?? {}),
);
const failureCount = computed(() => failureEntries.value.length);
// 注意:本组件里的多个 useVbenModal 有先后顺序要求——
// 父页面用 useVbenModal({ connectedComponent: ImportForm }) 通过 provide/inject 绑定,
// 只有第一个调用会接管这个绑定。所以导入窗口必须声明在最前,其余弹框(如结果弹框)排在后面。
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
@@ -37,29 +58,45 @@
    const data = await formApi.getValues();
    try {
      const response = (await importItem(data.file, data.updateSupport)) as any;
      // 关闭并提示
      await modalApi.close();
      const resultData = (response?.data || response || {}) as ImportResult;
      const hasFailure = Object.keys(resultData.failureList ?? {}).length > 0;
      emit('success');
      const resultData = response?.data || response || {};
      const failureList = resultData.failureList || {};
      if (Object.keys(failureList).length > 0) {
        const errorNodes = Object.entries(failureList).map(([name, reason]) => {
          return h('div', `${name}: ${reason}`);
        });
        notification.warning({
          message: '导入完成,部分物料导入失败',
          description: () => h('div', errorNodes),
          duration: null,
        });
      if (hasFailure) {
        // 不能在这里先关导入窗口:父页面配置了 destroyOnClose,
        // 关窗会把本组件整个重建,刚打开的结果弹框会被连带卸载(闪一下就消失)。
        // 改为把结果弹框叠在上面,等它关闭后再关导入窗口(见下面 onClosed)。
        importResult.value = resultData;
        resultModalApi.open();
      } else {
        importResult.value = {};
        message.success($t('ui.actionMessage.operationSuccess'));
        await modalApi.close();
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** 导入结果弹框:失败明细可能很长,弹框内固定高度滚动,避免像通知条那样撑出屏幕 */
const [ResultModal, resultModalApi] = useVbenModal({
  onOpenChange(isOpen: boolean) {
    if (isOpen) {
      resultModalApi.setState({
        confirmText: '知道了',
        showCancelButton: false,
      });
    }
  },
  // 必须显式提供:ModalApi.onConfirm 是 this.api.onConfirm?.() 的可选调用,不传则点按钮毫无反应
  onConfirm() {
    resultModalApi.close();
  },
  // 结果弹框关闭后再关导入窗口(此时本组件被销毁是安全的)
  onClosed() {
    modalApi.close();
  },
});
@@ -102,4 +139,46 @@
      </div>
    </template>
  </Modal>
  <ResultModal title="导入结果" class="w-[760px]">
    <div class="mx-4">
      <div class="mb-3 flex flex-wrap gap-x-6 gap-y-1 text-sm">
        <span>
          新增:<span class="font-medium text-green-600">{{ createCount }}</span>
          条
        </span>
        <span>
          更新:<span class="font-medium text-blue-600">{{ updateCount }}</span>
          条
        </span>
        <span>
          失败:<span class="font-medium text-red-600">{{ failureCount }}</span>
          条
        </span>
      </div>
      <div
        v-if="failureCount > 0"
        class="overflow-hidden rounded-md border border-red-100"
      >
        <div
          class="border-b border-red-100 bg-red-50 px-3 py-2 text-sm font-medium text-red-600"
        >
          失败明细(共 {{ failureCount }} 条,下方区域可滚动查看)
        </div>
        <div class="max-h-[60vh] overflow-y-auto">
          <div
            v-for="[itemKey, itemReason] in failureEntries"
            :key="itemKey"
            class="flex gap-2 border-b border-gray-100 px-3 py-2 text-sm last:border-b-0"
          >
            <span class="w-56 shrink-0 break-all text-gray-700">
              {{ itemKey }}
            </span>
            <span class="break-all text-red-500">{{ itemReason }}</span>
          </div>
        </div>
      </div>
    </div>
  </ResultModal>
</template>