吴羡
11 小时以前 5620ab475b200182a4d990ee04b029396f1db2dd
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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>