liu
2 天以前 5a322d24b59c4b70c08e792f21162be0f05d0199
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import type { ImportResult } from '#/api/erp/purchase/request';
 
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 {
  importPurchaseRequest,
  importPurchaseRequestTemplate,
} from '#/api/erp/purchase/request';
 
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 importPurchaseRequest(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 importPurchaseRequestTemplate();
  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 格式文件;必填列:产品编码、数量。“产品编码”按物料编码填写(“产品名称”列仅供对照),供应商可空(填了需与系统供应商一致),“需求日期”格式如 2026-01-01,“是否来料检验”填“是/否”。</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>