liu
3 天以前 ee7ac8df8bce38add1f75baf876b49de94b9e00a
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
<script lang="ts" setup>
  import { computed } from 'vue';
 
  import { Button, Progress } from 'ant-design-vue';
 
  import { useQrExport } from './use-qr-export';
 
  defineOptions({ name: 'QrExportProgress' });
 
  const { state, cancel, minimize, expand, close } = useQrExport();
 
  /** 面板是否展示(idle 表示没有任何任务) */
  const visible = computed(() => state.phase !== 'idle');
 
  /** 进行中的阶段才允许取消 */
  const cancelable = computed(() =>
    ['counting', 'downloading', 'generating'].includes(state.phase),
  );
 
  const percent = computed(() => {
    switch (state.phase) {
      case 'counting': {
        return 0;
      }
      case 'done': {
        return 100;
      }
      case 'downloading': {
        if (state.downloadTotal > 0) {
          return Math.min(
            100,
            Math.round((state.downloadLoaded / state.downloadTotal) * 100),
          );
        }
        return 0;
      }
      case 'generating': {
        return state.progress;
      }
      default: {
        return state.progress;
      }
    }
  });
 
  const progressStatus = computed(() => {
    if (state.phase === 'failed') return 'exception';
    if (state.phase === 'done') return 'success';
    return 'active';
  });
 
  /** 生成阶段与下载阶段都不可预知时长,用不确定态进度条 */
  const indeterminate = computed(
    () => state.phase === 'counting' || state.phase === 'sync',
  );
 
  const title = computed(() => {
    switch (state.phase) {
      case 'canceled': {
        return '导出已取消';
      }
      case 'counting': {
        return '正在统计数量';
      }
      case 'done': {
        return '导出完成';
      }
      case 'downloading': {
        return '正在下载';
      }
      case 'failed': {
        return '导出失败';
      }
      case 'generating': {
        return '正在生成二维码';
      }
      case 'sync': {
        return '正在导出';
      }
      default: {
        return '二维码导出';
      }
    }
  });
 
  const detail = computed(() => {
    if (state.phase === 'failed') {
      return state.errorMsg || '导出失败';
    }
    if (state.phase === 'downloading') {
      if (state.downloadTotal > 0) {
        return `${formatSize(state.downloadLoaded)} / ${formatSize(state.downloadTotal)}`;
      }
      return `已下载 ${formatSize(state.downloadLoaded)}`;
    }
    return state.message;
  });
 
  /** 收起后浮标上仍要能看到进度,避免用户以为任务已经停了 */
  const pillText = computed(() =>
    state.phase === 'generating' ? `${percent.value}%` : title.value,
  );
 
  function formatSize(bytes: number) {
    if (!bytes) return '0 B';
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
    if (bytes < 1024 * 1024 * 1024) {
      return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
    }
    return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
  }
</script>
 
<template>
  <!-- 收起态:小浮标,点击可重新展开 -->
  <div
    v-if="visible && state.minimized"
    class="fixed right-6 bottom-6 z-[2000] flex cursor-pointer items-center gap-2 rounded-full bg-white px-4 py-2 shadow-lg ring-1 ring-gray-200 hover:shadow-xl dark:bg-gray-800 dark:ring-gray-700"
    @click="expand"
  >
    <span
      class="inline-block h-2 w-2 rounded-full"
      :class="{
        'bg-blue-500': state.phase === 'generating' || state.phase === 'downloading',
        'bg-green-500': state.phase === 'done',
        'bg-red-500': state.phase === 'failed',
        'bg-gray-400': state.phase === 'canceled',
      }"
    ></span>
    <span class="text-sm text-gray-700 dark:text-gray-200">
      二维码导出 {{ pillText }}
    </span>
  </div>
 
  <!-- 展开态:进度面板 -->
  <div
    v-else-if="visible"
    class="fixed right-6 bottom-6 z-[2000] w-80 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-200 dark:bg-gray-800 dark:ring-gray-700"
  >
    <div class="mb-2 flex items-center justify-between">
      <span class="text-sm font-medium text-gray-800 dark:text-gray-100">
        {{ title }}
      </span>
      <div class="flex items-center gap-1">
        <!-- 进行中提供「隐藏」,任务转入后台继续,不会被中断 -->
        <Button
          v-if="state.phase !== 'done' && state.phase !== 'failed' && state.phase !== 'canceled'"
          type="link"
          size="small"
          @click="minimize"
        >
          隐藏
        </Button>
        <Button
          v-if="cancelable"
          type="link"
          size="small"
          danger
          @click="cancel"
        >
          取消
        </Button>
        <Button
          v-if="!cancelable"
          type="link"
          size="small"
          @click="close"
        >
          关闭
        </Button>
      </div>
    </div>
 
    <Progress
      :percent="percent"
      :status="progressStatus"
      :show-info="!indeterminate"
      size="small"
    />
 
    <div class="mt-2 truncate text-xs text-gray-500 dark:text-gray-400">
      {{ state.taskName }}
    </div>
    <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
      {{ detail }}
    </div>
  </div>
</template>