xiaoyi
5 天以前 0c27ffce47e533c2c6a66f4db4c467e8c456803f
feat(mes): 采购入库单 / 销售出库单 前端新增导入入口

1. 两个页面工具栏新增「导入」按钮(权限 mes:wm-item-receipt:import / mes:wm-product-sales:import)

2. 新增通用导入弹窗组件 wm-doc-import,支持下载模板、上传 Excel、按成功/失败分类提示

3. api 补充 importXxxTemplate / importXxx 方法
已添加1个文件
已修改4个文件
228 ■■■■■ 文件已修改
src/api/mes/wm/itemreceipt/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productsales/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/wm-doc-import/index.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/itemreceipt/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/itemreceipt/index.ts
@@ -26,6 +26,12 @@
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** å•据导入结果 */
  export interface WmDocImportResult {
    createCodes?: string[]; // åˆ›å»ºæˆåŠŸçš„å•æ®ç¼–ç 
    failureCodes?: Record<string, string>; // å¤±è´¥çš„编码及原因
  }
}
/** æŸ¥è¯¢é‡‡è´­å…¥åº“单分页 */
@@ -85,6 +91,19 @@
  });
}
/** ä¸‹è½½é‡‡è´­å…¥åº“单导入模板 */
export function importItemReceiptTemplate() {
  return requestClient.download('/mes/wm/item-receipt/get-import-template');
}
/** å¯¼å…¥é‡‡è´­å…¥åº“单 */
export function importItemReceipt(file: File) {
  return requestClient.upload<WmDocImportResult>(
    '/mes/wm/item-receipt/import',
    { file },
  );
}
/** æ ¹æ®åˆ°è´§é€šçŸ¥å•生成采购入库单 */
export function generateItemReceiptFromArrivalNotice(arrivalNoticeId: number) {
  return requestClient.post<number>(
src/api/mes/wm/productsales/index.ts
@@ -24,6 +24,12 @@
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** å•据导入结果 */
  export interface WmDocImportResult {
    createCodes?: string[]; // åˆ›å»ºæˆåŠŸçš„å•æ®ç¼–ç 
    failureCodes?: Record<string, string>; // å¤±è´¥çš„编码及原因
  }
}
/** æŸ¥è¯¢é”€å”®å‡ºåº“单分页 */
@@ -88,6 +94,19 @@
  );
}
/** ä¸‹è½½é”€å”®å‡ºåº“单导入模板 */
export function importProductSalesTemplate() {
  return requestClient.download('/mes/wm/product-sales/get-import-template');
}
/** å¯¼å…¥é”€å”®å‡ºåº“单 */
export function importProductSales(file: File) {
  return requestClient.upload<MesWmProductSalesApi.WmDocImportResult>(
    '/mes/wm/product-sales/import',
    { file },
  );
}
/** å¯¼å‡ºé”€å”®å‡ºåº“单 */
export function exportProductSales(params: any) {
  return requestClient.download('/mes/wm/product-sales/export-excel', {
src/components/wm-doc-import/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
defineOptions({ name: 'WmDocImportForm' });
const props = withDefaults(
  defineProps<{
    /** ä¸‹è½½æ¨¡æ¿æŽ¥å£ */
    templateApi: () => Promise<Blob | BlobPart | any>;
    /** å¯¼å…¥æŽ¥å£ */
    importApi: (file: File) => Promise<any>;
    /** å•据名称,用于提示与文件名 */
    subject: string;
    /** ä¸Šä¼ æ¡†æ ‡ç­¾ */
    fileLabel?: string;
  }>(),
  {
    fileLabel: 'Excel æ•°æ®',
  },
);
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'file',
      label: '',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ï¼›åŒä¸€å•据编码的多行会归并为一张单据的明细',
    },
  ],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    try {
      const result = (await props.importApi(data.file as File)) as any;
      const createCodes: string[] = result?.createCodes ?? [];
      const failureCodes: Record<string, string> = result?.failureCodes ?? {};
      const failCount = Object.keys(failureCodes).length;
      await modalApi.close();
      if (createCodes.length > 0) {
        emit('success');
      }
      // ç»“果提示:成功/失败分别汇总,失败明细最多展示 5 æ¡
      const lines: string[] = [];
      if (createCodes.length > 0) {
        lines.push(`成功导入 ${createCodes.length} å¼ ${props.subject}`);
      }
      if (failCount > 0) {
        const detail = Object.entries(failureCodes)
          .slice(0, 5)
          .map(([key, reason]) => `${key}:${reason}`)
          .join(';');
        lines.push(
          `${failCount} è¡Œ/张未导入——${detail}${failCount > 5 ? '(其余略)' : ''}`,
        );
      }
      if (lines.length === 0) {
        message.warning('没有可导入的数据,请检查文件内容');
        return;
      }
      if (createCodes.length > 0 && failCount === 0) {
        message.success(lines[0]);
      } else if (failCount > 0 && createCodes.length === 0) {
        message.error(lines.join(';'));
      } else {
        message.warning(lines.join(';'));
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await props.templateApi();
  downloadFileFromBlobPart({
    fileName: `${props.subject}导入模板.xls`,
    source: data,
  });
}
</script>
<template>
  <Modal :title="`导入${subject}`" class="w-2/5">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload :before-upload="beforeUpload" :max-count="1" accept=".xls,.xlsx">
            <Button type="primary">选择 Excel æ–‡ä»¶</Button>
          </Upload>
        </div>
      </template>
    </Form>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload">下载导入模板</Button>
      </div>
    </template>
  </Modal>
</template>
src/views/wls/itemreceipt/index.vue
@@ -15,8 +15,11 @@
  exportItemReceipt,
  generateItemReceiptFromArrivalNotice,
  getItemReceiptPage,
  importItemReceipt,
  importItemReceiptTemplate,
} from '#/api/mes/wm/itemreceipt';
import { $t } from '#/locales';
import WmDocImportForm from '#/components/wm-doc-import/index.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -29,6 +32,11 @@
const [AnSelectModal, anSelectModalApi] = useVbenModal({
  connectedComponent: AnSelectDialog,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: WmDocImportForm,
  destroyOnClose: true,
});
@@ -106,6 +114,17 @@
  }
}
/** å¯¼å…¥é‡‡è´­å…¥åº“单 */
function handleImport() {
  importModalApi
    .setData({
      subject: '采购入库单',
      templateApi: importItemReceiptTemplate,
      importApi: importItemReceipt,
    })
    .open();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportItemReceipt(await gridApi.formApi.getValues());
@@ -147,6 +166,7 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <AnSelectModal @selected="handleArrivalNoticeSelected" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="采购入库单列表">
      <template #toolbar-tools>
@@ -167,6 +187,13 @@
              onClick: handleGenerateFromAN,
            },
            {
              label: '导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:wm-item-receipt:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
src/views/wls/productsales/index.vue
@@ -14,14 +14,22 @@
  deleteProductSales,
  exportProductSales,
  getProductSalesPage,
  importProductSales,
  importProductSalesTemplate,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
import WmDocImportForm from '#/components/wm-doc-import/index.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: WmDocImportForm,
  destroyOnClose: true,
});
@@ -82,6 +90,17 @@
  handleRefresh();
}
/** å¯¼å…¥é”€å”®å‡ºåº“单 */
function handleImport() {
  importModalApi
    .setData({
      subject: '销售出库单',
      templateApi: importProductSalesTemplate,
      importApi: importProductSales,
    })
    .open();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportProductSales(await gridApi.formApi.getValues());
@@ -120,7 +139,9 @@
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="销售出库单列表">
      <template #toolbar-tools>
@@ -134,6 +155,13 @@
              onClick: handleCreate,
            },
            {
              label: '导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:wm-product-sales:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,