feat(review-meeting): 右下角展示导出打包进度与文案

pull/874/head
Codewoc 2026-04-11 17:40:37 +08:00
parent 0b140a145e
commit 06cd96067d
1 changed files with 109 additions and 1 deletions

View File

@ -217,6 +217,22 @@
:file-name="minutesPreviewFileName"
/>
<transition name="export-progress-fade">
<div v-if="exportProgressWidgetVisible" class="export-progress-widget">
<el-progress
:percentage="exportProgressPercentage"
:indeterminate="exportProgressIndeterminate"
:status="exportProgressStatusType"
:stroke-width="8"
:show-text="!exportProgressIndeterminate"
:striped="exportProgressStatus === 0 || exportProgressStatus === 1"
:striped-flow="exportProgressStatus === 0 || exportProgressStatus === 1"
duration="12"
/>
<div class="export-progress-label">{{ exportProgressText }}</div>
</div>
</transition>
<input
ref="minutesUploadInputRef"
type="file"
@ -228,7 +244,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import {
@ -295,6 +311,42 @@ const exportTaskMap = reactive<Record<number, ReviewMeetingMaterialExportTaskRes
{}
)
const exportPollingMap = new Map<number, ReturnType<typeof setTimeout>>()
const exportProgressMeetingId = ref<number>()
const exportProgressStatus = ref<number>()
const exportProgressTotal = ref(0)
const exportProgressProcessed = ref(0)
const exportProgressWidgetVisible = ref(false)
const exportProgressIndeterminate = computed(
() =>
(exportProgressStatus.value === 0 || exportProgressStatus.value === 1) &&
exportProgressTotal.value <= 0
)
const exportProgressPercentage = computed(() => {
if (exportProgressStatus.value === 2) {
return 100
}
if (exportProgressTotal.value <= 0) {
return 0
}
const percentage = Math.round((exportProgressProcessed.value / exportProgressTotal.value) * 100)
return Math.min(100, Math.max(0, percentage))
})
const exportProgressStatusType = computed<'success' | ''>(() => {
if (exportProgressStatus.value === 2) {
return 'success'
}
return ''
})
const exportProgressText = computed(() => {
if (exportProgressStatus.value === 0 || exportProgressStatus.value === 1) {
if (exportProgressTotal.value > 0) {
return `打包中 ${exportProgressProcessed.value}/${exportProgressTotal.value}`
}
return '打包中'
}
return ''
})
const syncActionColumnMode = () => {
useFixedActionColumn.value = window.innerWidth >= 1680
@ -574,6 +626,14 @@ const buildExportZipFileName = (meetingId: number) => {
return `${formatYYYYMMDD(date)}+${meetingName}+评审资料.zip`
}
const syncExportProgress = (meetingId: number, task: ReviewMeetingMaterialExportTaskRespVO) => {
exportProgressMeetingId.value = meetingId
exportProgressStatus.value = task.status
exportProgressTotal.value = task.totalProjectCount ?? 0
exportProgressProcessed.value = task.processedProjectCount ?? 0
exportProgressWidgetVisible.value = true
}
const triggerExportDownload = async (meetingId: number, taskId: number) => {
const blob = await downloadReviewMeetingMaterialExportTask(taskId)
download.zip(blob as Blob, buildExportZipFileName(meetingId))
@ -583,11 +643,17 @@ const pollExportTask = async (meetingId: number, taskId: number) => {
try {
const task = await getReviewMeetingMaterialExportTask(taskId)
exportTaskMap[meetingId] = task
syncExportProgress(meetingId, task)
if (task.status === 2 && task.downloadReady) {
clearExportPolling(meetingId)
try {
await triggerExportDownload(meetingId, taskId)
ElMessage.success('资料压缩包已生成并开始下载')
setTimeout(() => {
if (exportProgressMeetingId.value === meetingId && exportProgressStatus.value === 2) {
exportProgressWidgetVisible.value = false
}
}, 1200)
} catch {
ElMessage.error('资料压缩包下载失败')
} finally {
@ -597,6 +663,9 @@ const pollExportTask = async (meetingId: number, taskId: number) => {
}
if (task.status === 3) {
clearExportPolling(meetingId)
if (exportProgressMeetingId.value === meetingId) {
exportProgressWidgetVisible.value = false
}
ElMessage.error(task.errorMessage || '资料打包失败')
exportTaskMap[meetingId] = undefined
return
@ -604,6 +673,9 @@ const pollExportTask = async (meetingId: number, taskId: number) => {
} catch (error) {
clearExportPolling(meetingId)
exportTaskMap[meetingId] = undefined
if (exportProgressMeetingId.value === meetingId) {
exportProgressWidgetVisible.value = false
}
ElMessage.error('查询导出进度失败')
return
}
@ -618,10 +690,20 @@ const pollExportTask = async (meetingId: number, taskId: number) => {
const handleExportMaterials = async (row: ReviewMeetingRespVO) => {
if (isExporting(row.id)) {
const runningTask = exportTaskMap[row.id]
if (runningTask) {
syncExportProgress(row.id, runningTask)
}
return
}
exportProgressMeetingId.value = row.id
exportProgressStatus.value = 0
exportProgressTotal.value = 0
exportProgressProcessed.value = 0
exportProgressWidgetVisible.value = true
const task = await createReviewMeetingMaterialExportTask(row.id)
exportTaskMap[row.id] = task
syncExportProgress(row.id, task)
await pollExportTask(row.id, task.taskId)
}
@ -869,6 +951,32 @@ onBeforeUnmount(() => {
color: #c22e35;
background-color: rgba(252, 79, 84, 0.12);
}
.export-progress-widget {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 3000;
width: 280px;
padding: 10px 12px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
border: 1px solid rgba(148, 163, 184, 0.2);
}
.export-progress-label {
margin-top: 6px;
color: #475569;
font-size: 12px;
line-height: 1;
}
.export-progress-fade-enter-active,
.export-progress-fade-leave-active {
transition: opacity 0.2s ease;
}
.export-progress-fade-enter-from,
.export-progress-fade-leave-to {
opacity: 0;
}
/* ── 表格样式 ── */
:deep(.review-table .el-table__header-wrapper th) {