云
3 天以前 fbc58d6280a811f3c36696c530961f44c6491bb2
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
<script lang="ts" setup>
  import type { QrExportTask } from '#/api/mes/pro/qrExport';
 
  import { onBeforeUnmount, onMounted, ref } from 'vue';
  import { Button, message, Modal, Progress } from 'ant-design-vue';
 
  import {
    cancelQrExportTask,
    getQrExportTask,
  } from '#/api/mes/pro/qrExport';
  import { downloadQrExportZip, pickQrExportSaveFile } from '../qrexport/download-qr-export';
 
  const storageKey = 'mes-pro-last-qr-export-task-id';
  const visible = ref(false);
  const task = ref<QrExportTask>();
  const downloading = ref(false);
  const downloadLoaded = ref(0);
  const downloadTotal = ref(0);
  let downloadController: AbortController | undefined;
  let timer: ReturnType<typeof setTimeout> | undefined;
 
  function stopPolling() {
    if (timer) clearTimeout(timer);
    timer = undefined;
  }
 
  async function refresh() {
    if (!task.value) return;
    stopPolling();
    try {
      task.value = await getQrExportTask(task.value.id);
      if (task.value.status === 10 || task.value.status === 20) {
        timer = setTimeout(refresh, 2000);
      }
    } catch {
      message.error('获取导出进度失败,请点击刷新重试');
    }
  }
 
  function show(value: QrExportTask) {
    task.value = value;
    localStorage.setItem(storageKey, String(value.id));
    visible.value = true;
    void refresh();
  }
 
  function showLast() {
    if (task.value) visible.value = true;
    else message.info('暂无后台导出任务');
  }
 
  async function cancel() {
    if (!task.value) return;
    await cancelQrExportTask(task.value.id);
    message.success('已申请取消导出任务');
    await refresh();
  }
 
  async function download() {
    if (!task.value?.downloadable || downloading.value) return;
    const currentTask = task.value;
    // 文件选择器必须由用户点击直接触发,不能等网络请求之后再调用。
    const saveHandle = await pickQrExportSaveFile(`${currentTask.taskName}.zip`);
    downloading.value = true;
    downloadLoaded.value = 0;
    downloadTotal.value = currentTask.fileSize ?? 0;
    const controller = new AbortController();
    downloadController = controller;
    try {
      await downloadQrExportZip(currentTask, {
        saveHandle,
        signal: controller.signal,
        onProgress(loaded, total) {
          downloadLoaded.value = loaded;
          downloadTotal.value = total;
        },
      });
      message.success('ZIP 下载完成');
    } catch (error: any) {
      if (!controller.signal.aborted) {
        message.error(error?.message ?? '下载失败,请重试');
      }
    } finally {
      downloading.value = false;
      if (downloadController === controller) downloadController = undefined;
    }
  }
 
  function formatSize(bytes: number) {
    return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  }
 
  onMounted(async () => {
    const id = Number(localStorage.getItem(storageKey));
    if (!Number.isSafeInteger(id) || id <= 0) return;
    try {
      const previous = await getQrExportTask(id);
      task.value = previous;
      if (previous.status === 10 || previous.status === 20 || previous.downloadable) {
        visible.value = true;
      }
      if (previous.status === 10 || previous.status === 20) timer = setTimeout(refresh, 2000);
    } catch {
      localStorage.removeItem(storageKey);
    }
  });
  onBeforeUnmount(() => {
    stopPolling();
    downloadController?.abort();
  });
 
  defineExpose({ show, showLast });
</script>
 
<template>
  <Modal v-model:open="visible" title="二维码后台导出" :footer="null" :mask-closable="false">
    <template v-if="task">
      <p>任务 #{{ task.id }} · {{ task.taskName }}</p>
      <p>已生成 {{ task.finishedCount }} / {{ task.totalCount }} 张,{{ task.statusName }}</p>
      <Progress :percent="task.progress" :status="task.status === 50 ? 'exception' : task.downloadable ? 'success' : 'active'" />
      <p v-if="downloading" class="mt-2 text-sm">
        正在下载 {{ formatSize(downloadLoaded) }} / {{ formatSize(downloadTotal) }}
      </p>
      <p v-if="task.errorMsg" class="text-red-600">{{ task.errorMsg }}</p>
      <div class="mt-4 flex justify-end gap-2">
        <Button @click="refresh">刷新</Button>
        <Button v-if="task.status === 10 || task.status === 20" danger @click="cancel">取消任务</Button>
        <Button v-if="task.downloadable" type="primary" :loading="downloading" @click="download">下载 ZIP</Button>
      </div>
    </template>
  </Modal>
</template>