| | |
| | | <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']); |
| | | |
| | |
| | | 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(); |
| | |
| | | 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(); |
| | | }, |
| | | }); |
| | | |
| | |
| | | </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> |