<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>
|