gaoluyang
3 天以前 8fa692c8c6693d9f8833c58ce84f6b61f6b6610e
feat: 导入
添加导入按钮
已修改13个文件
已添加2个文件
606 ■■■■■ 文件已修改
src/api/crm/invoice/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/ipqc/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/iqc/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/oqc/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/rqc/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/modules/import-form.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/components/import-form-modal.vue 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/invoice/index.ts
@@ -120,3 +120,23 @@
    `/crm/invoice/simple-list?contractId=${contractId}`,
  );
}
/** å¼€ç¥¨å¯¼å…¥ç»“æžœ */
export interface InvoiceImportResult {
  /** åˆ›å»ºæˆåŠŸçš„å¼€ç¥¨ç¼–å· */
  createList?: string[];
  /** å¯¼å…¥å¤±è´¥çš„集合:key ä¸ºã€Œå‘票号码 xxx」或「第 N è¡Œã€ */
  failureList?: Record<string, string>;
}
/** ä¸‹è½½å¼€ç¥¨å¯¼å…¥æ¨¡æ¿ */
export function importInvoiceTemplate() {
  return requestClient.download('/crm/invoice/get-import-template');
}
/** å¯¼å…¥å¼€ç¥¨ */
export function importInvoice(file: File) {
  return requestClient.upload<InvoiceImportResult>('/crm/invoice/import', {
    file,
  });
}
src/api/mes/qc/ipqc/index.ts
@@ -33,11 +33,18 @@
    majorQuantity?: number; // ä¸¥é‡ç¼ºé™·æ•°é‡
    minorQuantity?: number; // è½»å¾®ç¼ºé™·æ•°é‡
    checkResult?: number; // æ£€æµ‹ç»“æžœ
    indicatorResultSummary?: string; // æ£€æµ‹ç»“果(检验项实测值汇总,如「温度:15,电阻:1」)
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
  /** è¿‡ç¨‹æ£€éªŒå•导入结果 */
  export interface ImportResult {
    createList?: string[]; // æ–°å¢žæˆåŠŸçš„æ£€éªŒå•ç¼–å·
    failureList?: Record<string, string>; // å¯¼å…¥å¤±è´¥æ˜Žç»†ï¼škey ä¸ºã€Œæ£€éªŒå•编号 X」或「第 N è¡Œã€
  }
}
@@ -77,3 +84,15 @@
export function exportIpqc(params: any) {
  return requestClient.download('/mes/qc/ipqc/export-excel', { params });
}
/** ä¸‹è½½è¿‡ç¨‹æ£€éªŒå•导入模板 */
export function importIpqcTemplate() {
  return requestClient.download('/mes/qc/ipqc/get-import-template');
}
/** å¯¼å…¥è¿‡ç¨‹æ£€éªŒå• */
export function importIpqc(file: File) {
  return requestClient.upload<MesQcIpqcApi.ImportResult>('/mes/qc/ipqc/import', {
    file,
  });
}
src/api/mes/qc/iqc/index.ts
@@ -32,6 +32,7 @@
    majorQuantity?: number; // ä¸¥é‡ç¼ºé™·æ•°é‡
    minorQuantity?: number; // è½»å¾®ç¼ºé™·æ•°é‡
    checkResult?: number; // æ£€æµ‹ç»“æžœ
    indicatorResultSummary?: string; // æ£€æµ‹ç»“果(检验项实测值汇总,如「温度:15,电阻:1」)
    receiveDate?: number; // æ¥æ–™æ—¥æœŸ
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspector?: string; // æ£€æµ‹äººå‘˜ï¼ˆæ˜µç§°ï¼‰
@@ -39,6 +40,12 @@
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
  /** æ¥æ–™æ£€éªŒå•导入结果 */
  export interface ImportResult {
    createList?: string[]; // æ–°å¢žæˆåŠŸçš„æ£€éªŒå•ç¼–å·
    failureList?: Record<string, string>; // å¯¼å…¥å¤±è´¥æ˜Žç»†ï¼škey ä¸ºã€Œæ£€éªŒå•编号 X」或「第 N è¡Œã€
  }
}
@@ -78,3 +85,15 @@
export function exportIqc(params: any) {
  return requestClient.download('/mes/qc/iqc/export-excel', { params });
}
/** ä¸‹è½½æ¥æ–™æ£€éªŒå•导入模板 */
export function importIqcTemplate() {
  return requestClient.download('/mes/qc/iqc/get-import-template');
}
/** å¯¼å…¥æ¥æ–™æ£€éªŒå• */
export function importIqc(file: File) {
  return requestClient.upload<MesQcIqcApi.ImportResult>('/mes/qc/iqc/import', {
    file,
  });
}
src/api/mes/qc/oqc/index.ts
@@ -34,12 +34,19 @@
    majorQuantity?: number; // ä¸¥é‡ç¼ºé™·æ•°é‡
    minorQuantity?: number; // è½»å¾®ç¼ºé™·æ•°é‡
    checkResult?: number; // æ£€æµ‹ç»“æžœ
    indicatorResultSummary?: string; // æ£€æµ‹ç»“果(检验项实测值汇总,如「温度:15,电阻:1」)
    outDate?: number; // å‡ºè´§æ—¥æœŸ
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
  /** å‡ºè´§æ£€éªŒå•导入结果 */
  export interface ImportResult {
    createList?: string[]; // æ–°å¢žæˆåŠŸçš„æ£€éªŒå•ç¼–å·
    failureList?: Record<string, string>; // å¯¼å…¥å¤±è´¥æ˜Žç»†ï¼škey ä¸ºã€Œæ£€éªŒå•编号 X」或「第 N è¡Œã€
  }
}
@@ -79,3 +86,15 @@
export function exportOqc(params: any) {
  return requestClient.download('/mes/qc/oqc/export-excel', { params });
}
/** ä¸‹è½½å‡ºè´§æ£€éªŒå•导入模板 */
export function importOqcTemplate() {
  return requestClient.download('/mes/qc/oqc/get-import-template');
}
/** å¯¼å…¥å‡ºè´§æ£€éªŒå• */
export function importOqc(file: File) {
  return requestClient.upload<MesQcOqcApi.ImportResult>('/mes/qc/oqc/import', {
    file,
  });
}
src/api/mes/qc/rqc/index.ts
@@ -24,6 +24,7 @@
    qualifiedQuantity?: number; // åˆæ ¼å“æ•°é‡
    unqualifiedQuantity?: number; // ä¸åˆæ ¼æ•°é‡
    checkResult?: number; // æ£€æµ‹ç»“æžœ
    indicatorResultSummary?: string; // æ£€æµ‹ç»“果(检验项实测值汇总,如「温度:15,电阻:1」)
    inspectDate?: number; // æ£€æµ‹æ—¥æœŸ
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
src/views/crm/invoice/index.vue
@@ -21,6 +21,7 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
const { push } = useRouter();
const sceneType = ref('1');
@@ -29,6 +30,16 @@
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
/** æ‰“开导入开票弹窗 */
function handleImport() {
  importModalApi.open();
}
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
@@ -149,6 +160,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid>
      <template #toolbar-actions>
        <Tabs class="w-full" @change="handleChangeSceneType">
@@ -168,6 +180,13 @@
              onClick: handleCreate,
            },
            {
              label: '导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['crm:invoice:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
src/views/crm/invoice/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import type { InvoiceImportResult } from '#/api/crm/invoice';
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 { importInvoice, importInvoiceTemplate } from '#/api/crm/invoice';
const emit = defineEmits(['success']);
const fileList = ref<FileType[]>([]);
const importResult = ref<InvoiceImportResult>();
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 importInvoice(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 importInvoiceTemplate();
  downloadFileFromBlobPart({ fileName: '开票导入模板.xls', source: data });
}
const createCount = computed(() => importResult.value?.createList?.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 æ ¼å¼æ–‡ä»¶ï¼›å¿…填列:合同编号、发票类型、开票金额(元)。“合同编号”需与系统中已审核通过的合同编号一致(编号需完全一致),客户由合同自动带入,无需填写;“发票类型”按字典名称填写(增值税专用发票 / å¢žå€¼ç¨Žæ™®é€šå‘票 / ç”µå­å‘票);“开票金额(元)”需大于 0 ä¸”不超过该合同剩余可开票金额。</p>
        <p>“开票编号”留空则由系统自动生成,填写时不可与系统中已有开票编号重复;“开票日期”格式为 2026-01-01 10:00:00 æˆ– 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-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/mes/qc/components/import-form-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
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';
/** å¯¼å…¥ç»“果(IQC/IPQC/OQC ä¸‰ä¸ªå¯¼å…¥æŽ¥å£è¿”回结构一致) */
interface ImportResult {
  createList?: string[];
  failureList?: Record<string, string>;
}
interface ImportModalData {
  /** å¼¹çª—标题 */
  title?: string;
  /** æ¨¡æ¿ä¸‹è½½å‡½æ•°ï¼šrequestClient.download(...) è¿”回 Blob */
  templateApi: () => Promise<Blob>;
  /** å¯¼å…¥å‡½æ•°ï¼ˆmultipart ä¸Šä¼  Excel) */
  importApi: (file: File) => Promise<ImportResult>;
  /** ä¸‹è½½çš„æ¨¡æ¿æ–‡ä»¶å */
  fileName: string;
  /** ç°è‰²æç¤ºåŒºçš„说明段落 */
  hints?: string[];
}
defineOptions({ name: 'QcImportFormModal' });
const emit = defineEmits(['success']);
const modalData = ref<ImportModalData>();
const title = ref('批量导入');
const hints = ref<string[]>([]);
const fileList = ref<FileType[]>([]);
const importResult = ref<ImportResult>();
const importing = ref(false);
const progressPercent = ref(0);
let progressTimer: ReturnType<typeof setInterval> | undefined;
const createCount = computed(() => importResult.value?.createList?.length ?? 0);
const failureCount = computed(
  () => Object.keys(importResult.value?.failureList ?? {}).length,
);
/** æ¨¡æ‹Ÿè¿›åº¦æ¡ï¼šè¯·æ±‚期间从 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));
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const data = modalData.value;
    if (!data) {
      return;
    }
    if (fileList.value.length === 0) {
      message.warning('请选择要导入的文件');
      return;
    }
    if (importing.value) {
      return;
    }
    modalApi.lock();
    importing.value = true;
    startMockProgress();
    let shouldClose = false;
    try {
      importResult.value = await data.importApi(fileList.value[0] as File);
      stopMockProgress();
      progressPercent.value = 100;
      await delay(300);
      emit('success');
      if (failureCount.value > 0) {
        message.warning(
          `导入完成:新增 ${createCount.value} æ¡ï¼Œå¤±è´¥ ${failureCount.value} æ¡ï¼Œè¯·æŸ¥çœ‹ä¸‹æ–¹å¤±è´¥æ˜Žç»†`,
        );
      } else {
        message.success('导入完成');
        shouldClose = true;
      }
    } finally {
      stopMockProgress();
      importing.value = false;
      modalApi.unlock();
      if (shouldClose) {
        await modalApi.close();
      }
    }
  },
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      modalData.value = undefined;
      fileList.value = [];
      importResult.value = undefined;
      importing.value = false;
      progressPercent.value = 0;
      return;
    }
    const data = modalApi.getData<ImportModalData>();
    modalData.value = data;
    title.value = data?.title ?? '批量导入';
    hints.value = data?.hints ?? [];
    modalApi.setState({ confirmText: '导入' });
  },
});
/** ä¸Šä¼ å‰ï¼šåªä¿ç•™ä¸€ä¸ªæ–‡ä»¶ï¼Œäº¤ç”± onConfirm ç»Ÿä¸€æäº¤ */
function beforeUpload(file: FileType) {
  fileList.value = [file];
  importResult.value = undefined;
  return false;
}
function handleRemove() {
  fileList.value = [];
  importResult.value = undefined;
}
async function handleDownloadTemplate() {
  const data = modalData.value;
  if (!data) {
    return;
  }
  const blob = await data.templateApi();
  downloadFileFromBlobPart({ fileName: data.fileName, source: blob });
}
</script>
<template>
  <Modal :title="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
        v-if="hints.length > 0"
        class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"
      >
        <p v-for="(hint, index) in hints" :key="index">{{ hint }}</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-red-600">失败:{{ failureCount }} æ¡</span>
        </div>
        <div v-if="createCount > 0" class="text-sm text-gray-600">
          æ–°å¢žæ£€éªŒå•为「待检验」状态,请在列表中编辑并录入检测结果
        </div>
        <div v-if="failureCount > 0" class="mt-2 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/mes/qc/ipqc/data.ts
@@ -468,6 +468,12 @@
      },
    },
    {
      field: 'indicatorResultSummary',
      title: '检测结果(检验项)',
      minWidth: 200,
      showOverflow: 'tooltip',
    },
    {
      field: 'inspectDate',
      title: '检测日期',
      width: 120,
src/views/mes/qc/ipqc/index.vue
@@ -8,9 +8,16 @@
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteIpqc, exportIpqc, getIpqcPage } from '#/api/mes/qc/ipqc';
import {
  deleteIpqc,
  exportIpqc,
  getIpqcPage,
  importIpqc,
  importIpqcTemplate,
} from '#/api/mes/qc/ipqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcImportFormModal from '#/views/mes/qc/components/import-form-modal.vue';
import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
@@ -34,9 +41,34 @@
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: QcImportFormModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰¹é‡å¯¼å…¥è¿‡ç¨‹æ£€éªŒå• */
function handleImport() {
  importModalApi
    .setData({
      title: '批量导入过程检验单',
      templateApi: importIpqcTemplate,
      importApi: importIpqc,
      fileName: '过程检验单导入模板.xls',
      hints: [
        '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ï¼›å¿…填列:检验单名称、产品物料编码、批号、合格品数量、不合格品数量。',
        '「产品物料编码」需与「物料管理」中的编码完全一致;「批号」为必填,不允许为空。',
        '「检验类型」按字典名称填写(自制工序检验 / é¦–检 / å·¡æ£€ / è‡ªæ£€ / æˆå“æ£€éªŒï¼‰ï¼›ã€Œæ£€æµ‹ç»“果」按字典名称填写(校验通过 / æ ¡éªŒä¸é€šè¿‡ï¼‰ã€‚',
        '合格品数量 + ä¸åˆæ ¼å“æ•°é‡ å¿…须等于「检测数量」(留空时自动取两者之和);工废 + æ–™åºŸ + å…¶ä»–废品 ä¹‹å’Œä¸å¾—超过不合格品数量。',
        '「质检方案」填方案名称,留空则按物料自动匹配 IPQC æ–¹æ¡ˆï¼›ã€Œæ£€æµ‹äººå‘˜ã€ç•™ç©ºå–当前登录用户;「检测日期」可留空。',
        '逐行导入,失败行不影响成功行;导入后的检验单为「待检验」状态,可在列表中继续编辑、录入检测结果。',
      ],
    })
    .open();
}
/** åˆ›å»ºè¿‡ç¨‹æ£€éªŒå• */
@@ -123,6 +155,7 @@
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <ReportModal />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="过程检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -135,6 +168,13 @@
              onClick: handleCreate,
            },
            {
              label: '批量导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:qc-ipqc:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
src/views/mes/qc/iqc/data.ts
@@ -427,6 +427,12 @@
      },
    },
    {
      field: 'indicatorResultSummary',
      title: '检测结果(检验项)',
      minWidth: 200,
      showOverflow: 'tooltip',
    },
    {
      field: 'receiveDate',
      title: '来料日期',
      width: 120,
src/views/mes/qc/iqc/index.vue
@@ -11,9 +11,16 @@
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteIqc, exportIqc, getIqcPage } from '#/api/mes/qc/iqc';
import {
  deleteIqc,
  exportIqc,
  getIqcPage,
  importIqc,
  importIqcTemplate,
} from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcImportFormModal from '#/views/mes/qc/components/import-form-modal.vue';
import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
@@ -41,9 +48,33 @@
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: QcImportFormModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰¹é‡å¯¼å…¥æ¥æ–™æ£€éªŒå• */
function handleImport() {
  importModalApi
    .setData({
      title: '批量导入来料检验单',
      templateApi: importIqcTemplate,
      importApi: importIqc,
      fileName: '来料检验单导入模板.xls',
      hints: [
        '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ï¼›å¿…填列:检验单名称、产品物料编码、合格品数量、不合格品数量、检测日期、来料日期。',
        '「产品物料编码」需与「物料管理」中的编码完全一致;「供应商名称」需与「供应商管理」中的名称一致,留空则该单无供应商。',
        '合格品数量 + ä¸åˆæ ¼å“æ•°é‡ å¿…须等于「本次接收数量」(留空时自动取两者之和);「检测结果」按字典名称填写(校验通过 / æ ¡éªŒä¸é€šè¿‡ï¼‰ã€‚',
        '「质检方案」填方案名称,留空则按物料自动匹配 IQC æ–¹æ¡ˆï¼›ã€Œæ£€æµ‹äººå‘˜ã€ç•™ç©ºå–当前登录用户;日期支持 2026-01-01 10:00:00 æˆ– 2026-01-01。',
        '逐行导入,失败行不影响成功行;导入后的检验单为「待检验」状态,可在列表中继续编辑、录入检测结果。',
      ],
    })
    .open();
}
/** åˆ›å»ºæ¥æ–™æ£€éªŒå• */
@@ -137,6 +168,7 @@
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <ReportModal />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="来料检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -149,6 +181,13 @@
              onClick: handleCreate,
            },
            {
              label: '批量导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:qc-iqc:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
src/views/mes/qc/oqc/data.ts
@@ -425,6 +425,12 @@
      },
    },
    {
      field: 'indicatorResultSummary',
      title: '检测结果(检验项)',
      minWidth: 200,
      showOverflow: 'tooltip',
    },
    {
      field: 'inspectDate',
      title: '检测日期',
      width: 120,
src/views/mes/qc/oqc/index.vue
@@ -8,9 +8,16 @@
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import {
  deleteOqc,
  exportOqc,
  getOqcPage,
  importOqc,
  importOqcTemplate,
} from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import QcImportFormModal from '#/views/mes/qc/components/import-form-modal.vue';
import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
@@ -34,9 +41,33 @@
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: QcImportFormModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰¹é‡å¯¼å…¥å‡ºè´§æ£€éªŒå• */
function handleImport() {
  importModalApi
    .setData({
      title: '批量导入出货检验单',
      templateApi: importOqcTemplate,
      importApi: importOqc,
      fileName: '出货检验单导入模板.xls',
      hints: [
        '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ï¼›å¿…填列:检验单名称、产品物料编码、客户名称、本次出货数量、合格品数量、不合格品数量、检测日期、出货日期。',
        '「产品物料编码」需与「物料管理」中的编码完全一致;「客户名称」需与「CRM å®¢æˆ·ç®¡ç†ã€ä¸­çš„名称一致,且必须在当前登录用户的客户数据权限范围内。',
        '合格品数量 + ä¸åˆæ ¼å“æ•°é‡ å¿…须等于「检测数量」(留空时自动取两者之和);「检测结果」按字典名称填写(校验通过 / æ ¡éªŒä¸é€šè¿‡ï¼‰ã€‚',
        '「质检方案」填方案名称,留空则按物料自动匹配 OQC æ–¹æ¡ˆï¼›ã€Œæ£€æµ‹äººå‘˜ã€ç•™ç©ºå–当前登录用户;日期支持 2026-01-01 10:00:00 æˆ– 2026-01-01。',
        '逐行导入,失败行不影响成功行;导入后的检验单为「待检验」状态,可在列表中继续编辑、录入检测结果。',
      ],
    })
    .open();
}
/** åˆ›å»ºå‡ºè´§æ£€éªŒå• */
@@ -123,6 +154,7 @@
    <FormModal @success="handleRefresh" />
    <ExportRangeModal />
    <ReportModal />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="出货检验单列表">
      <template #toolbar-tools>
        <TableAction
@@ -135,6 +167,13 @@
              onClick: handleCreate,
            },
            {
              label: '批量导入',
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:qc-oqc:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
src/views/mes/qc/rqc/data.ts
@@ -414,6 +414,12 @@
      },
    },
    {
      field: 'indicatorResultSummary',
      title: '检测结果(检验项)',
      minWidth: 200,
      showOverflow: 'tooltip',
    },
    {
      field: 'inspectDate',
      title: '检测日期',
      width: 160,