feat(erp/mes/hrm): 新增采购订单/销售订单/计量器具导入弹窗;导入完成自动关闭

- 新增 采购订单/销售订单/计量器具 导入弹窗(下载模板、上传、进度条、导入结果)
- 采购订单导入支持"是否审核"开关;销售订单、计量器具无开关
- 修复导入弹窗导入成功后不自动关闭的问题(员工合同/员工档案/库存现有量一并修复)
已添加3个文件
已修改9个文件
645 ■■■■■ 文件已修改
src/api/erp/purchase/order/index.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/sale/order/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/metering/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/import-form.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/import-form.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/contract/modules/import-form.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/import-form.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/metering/modules/import-form.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/import-form.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts
@@ -112,6 +112,16 @@
  });
}
/** é‡‡è´­è®¢å•导入结果 */
export interface ImportResult {
  /** åˆ›å»ºæˆåŠŸçš„é‡‡è´­å•ç¼–å· */
  createList?: string[];
  /** æ›´æ–°æˆåŠŸçš„é‡‡è´­å•ç¼–å· */
  updateList?: string[];
  /** å¯¼å…¥å¤±è´¥é›†åˆï¼Œkey ä¸ºé‡‡è´­å•编号或行号,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 },
  );
}
src/api/erp/sale/order/index.ts
@@ -65,6 +65,16 @@
  }
}
/** é”€å”®è®¢å•导入结果 */
export interface ImportResult {
  /** åˆ›å»ºæˆåŠŸçš„é”€å”®å•ç¼–å· */
  createList?: string[];
  /** æ›´æ–°æˆåŠŸçš„é”€å”®å•ç¼–å· */
  updateList?: string[];
  /** å¯¼å…¥å¤±è´¥é›†åˆï¼Œkey ä¸ºé”€å”®å•编号或行号,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 });
}
src/api/mes/dv/metering/index.ts
@@ -29,6 +29,16 @@
  }
}
/** è®¡é‡å™¨å…·å¯¼å…¥ç»“æžœ */
export interface ImportResult {
  /** åˆ›å»ºæˆåŠŸçš„è®¡é‡å™¨å…·ç¼–ç  */
  createList?: string[];
  /** æ›´æ–°æˆåŠŸçš„è®¡é‡å™¨å…·ç¼–ç  */
  updateList?: string[];
  /** å¯¼å…¥å¤±è´¥é›†åˆï¼Œkey ä¸ºè®¡é‡å™¨å…·ç¼–码或行号,value ä¸ºå¤±è´¥åŽŸå›  */
  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 });
}
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,
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、xlsx æ ¼å¼æ–‡ä»¶ï¼›æ¨¡æ¿ä¸­â€œä¾›åº”商名称”按名称(或编码)填写,需与系统内供应商一致;“产品编码”按物料编码填写(“产品名称”列仅供对照,匹配以产品编码为准);“采购时间”格式如 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">正在导入采购订单数据,请稍候...</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>
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,
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、xlsx æ ¼å¼æ–‡ä»¶ï¼›æ¨¡æ¿ä¸­â€œå®¢æˆ·åç§°â€æŒ‰åç§°å¡«å†™ï¼Œéœ€ä¸Ž 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">正在导入销售订单数据,请稍候...</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>
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();
      }
    }
  },
});
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();
      }
    }
  },
});
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>
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、xlsx æ ¼å¼æ–‡ä»¶ã€‚必填列:计量器具编码、计量器具名称、器具类别、状态;“器具类别 / çŠ¶æ€â€å¡«å­—å…¸ä¸­æ–‡ï¼ˆå¦‚ å¡å°º/其他、在用/停用/报废)。</p>
        <p>“使用部门 / è´£ä»»äººâ€æŒ‰ç³»ç»Ÿå†…的部门名称 / ç”¨æˆ·æ˜µç§°åŒ¹é…ï¼ŒåŒ¹é…ä¸åˆ°è¯¥è¡Œä¼šå¤±è´¥ï¼›â€œæ ¡å‡†å‘¨æœŸ(月)”留空默认 12;“投入使用日期”格式如 2026-01-01。</p>
        <p>计量器具编码已存在时会导入失败(仅新增,不覆盖)。</p>
      </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 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>
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();
      }
    }
  },
});