fix(review-meeting): 修复导出下载与命名及按钮顺序
parent
8e37690ef6
commit
c413c5cc56
|
|
@ -87,6 +87,17 @@ export interface ReviewMeetingAgendaAttachmentRespVO {
|
||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReviewMeetingMaterialExportTaskRespVO {
|
||||||
|
taskId: number
|
||||||
|
reviewMeetingId: number
|
||||||
|
status: number
|
||||||
|
statusName: string
|
||||||
|
totalProjectCount: number
|
||||||
|
processedProjectCount: number
|
||||||
|
errorMessage?: string
|
||||||
|
downloadReady: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReviewMeetingAgendaGenerateReqVO {
|
export interface ReviewMeetingAgendaGenerateReqVO {
|
||||||
name: string
|
name: string
|
||||||
organizationUnit: string
|
organizationUnit: string
|
||||||
|
|
@ -219,6 +230,27 @@ export const uploadMinutesAttachment = (
|
||||||
export const getImportTemplate = () =>
|
export const getImportTemplate = () =>
|
||||||
request.download({ url: '/project/review-meeting/get-import-template' })
|
request.download({ url: '/project/review-meeting/get-import-template' })
|
||||||
|
|
||||||
|
/** 创建会议资料导出任务 */
|
||||||
|
export const createReviewMeetingMaterialExportTask = (reviewMeetingId: number) =>
|
||||||
|
request.post<ReviewMeetingMaterialExportTaskRespVO>({
|
||||||
|
url: '/project/review-meeting/material-export-task/create',
|
||||||
|
params: { reviewMeetingId }
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 查询会议资料导出任务 */
|
||||||
|
export const getReviewMeetingMaterialExportTask = (taskId: number) =>
|
||||||
|
request.get<ReviewMeetingMaterialExportTaskRespVO>({
|
||||||
|
url: '/project/review-meeting/material-export-task/get',
|
||||||
|
params: { taskId }
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 下载会议资料导出结果 */
|
||||||
|
export const downloadReviewMeetingMaterialExportTask = (taskId: number) =>
|
||||||
|
request.download<Blob>({
|
||||||
|
url: '/project/review-meeting/material-export-task/download',
|
||||||
|
params: { taskId }
|
||||||
|
})
|
||||||
|
|
||||||
const uploadMeetingAttachmentByPresignedUrl = async (
|
const uploadMeetingAttachmentByPresignedUrl = async (
|
||||||
file: File
|
file: File
|
||||||
): Promise<ReviewMeetingAgendaAttachmentRespVO> => {
|
): Promise<ReviewMeetingAgendaAttachmentRespVO> => {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
<span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span>
|
<span :class="`status-text status-${row.status}`">{{ STATUS_LABEL[row.status] }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="360" align="center" fixed="right">
|
<el-table-column label="操作" width="430" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row.status === 0">
|
<template v-if="row.status === 0">
|
||||||
<div class="op-group">
|
<div class="op-group">
|
||||||
|
|
@ -75,6 +75,14 @@
|
||||||
<a v-if="row.mailSent" v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="openMailStatus(row)">邮件状态</a>
|
<a v-if="row.mailSent" v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="openMailStatus(row)">邮件状态</a>
|
||||||
<a v-else v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="handleSendMail(row)">发送议程</a>
|
<a v-else v-hasPermi="['review:meeting:send-mail']" class="op-link" @click="handleSendMail(row)">发送议程</a>
|
||||||
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="triggerUploadMinutes(row)">上传纪要</a>
|
<a v-hasPermi="['review:meeting:update']" class="op-link" @click="triggerUploadMinutes(row)">上传纪要</a>
|
||||||
|
<a
|
||||||
|
v-hasPermi="['review:meeting:export']"
|
||||||
|
class="op-link"
|
||||||
|
:class="{ disabled: isExporting(row.id) }"
|
||||||
|
@click="handleExportMaterials(row)"
|
||||||
|
>
|
||||||
|
{{ getExportText(row.id) }}
|
||||||
|
</a>
|
||||||
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)">结束</a>
|
<a v-hasPermi="['review:meeting:finish']" class="op-link" @click="handleFinish(row)">结束</a>
|
||||||
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)">取消</a>
|
<a v-hasPermi="['review:meeting:cancel']" class="op-link op-danger" @click="handleCancel(row)">取消</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -109,7 +117,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
|
|
@ -119,12 +127,17 @@ import {
|
||||||
sendSmsInvitation,
|
sendSmsInvitation,
|
||||||
sendMailInvitation,
|
sendMailInvitation,
|
||||||
uploadMinutesAttachment,
|
uploadMinutesAttachment,
|
||||||
|
createReviewMeetingMaterialExportTask,
|
||||||
|
getReviewMeetingMaterialExportTask,
|
||||||
|
downloadReviewMeetingMaterialExportTask,
|
||||||
type ReviewMeetingRespVO,
|
type ReviewMeetingRespVO,
|
||||||
type ReviewMeetingPageReqVO
|
type ReviewMeetingPageReqVO,
|
||||||
|
type ReviewMeetingMaterialExportTaskRespVO
|
||||||
} from '@/api/review/meeting'
|
} from '@/api/review/meeting'
|
||||||
import SmsStatusDialog from './SmsStatusDialog.vue'
|
import SmsStatusDialog from './SmsStatusDialog.vue'
|
||||||
import MailStatusDialog from './MailStatusDialog.vue'
|
import MailStatusDialog from './MailStatusDialog.vue'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
import download from '@/utils/download'
|
||||||
|
|
||||||
defineOptions({ name: 'ReviewMeeting' })
|
defineOptions({ name: 'ReviewMeeting' })
|
||||||
|
|
||||||
|
|
@ -153,6 +166,8 @@ const smsStatusRef = ref()
|
||||||
const mailStatusRef = ref()
|
const mailStatusRef = ref()
|
||||||
const minutesUploadInputRef = ref<HTMLInputElement>()
|
const minutesUploadInputRef = ref<HTMLInputElement>()
|
||||||
const pendingMinutesMeeting = ref<ReviewMeetingRespVO>()
|
const pendingMinutesMeeting = ref<ReviewMeetingRespVO>()
|
||||||
|
const exportTaskMap = reactive<Record<number, ReviewMeetingMaterialExportTaskRespVO | undefined>>({})
|
||||||
|
const exportPollingMap = new Map<number, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
@ -264,7 +279,111 @@ const openMailStatus = (row: ReviewMeetingRespVO) => {
|
||||||
mailStatusRef.value?.open(row.id, row.name)
|
mailStatusRef.value?.open(row.id, row.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExporting = (meetingId: number) => {
|
||||||
|
const task = exportTaskMap[meetingId]
|
||||||
|
return task?.status === 0 || task?.status === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExportText = (meetingId: number) => {
|
||||||
|
const task = exportTaskMap[meetingId]
|
||||||
|
if (!task) return '导出资料'
|
||||||
|
if (task.status === 0 || task.status === 1) {
|
||||||
|
const total = task.totalProjectCount ?? 0
|
||||||
|
const processed = task.processedProjectCount ?? 0
|
||||||
|
return total > 0 ? `打包中 ${processed}/${total}` : '打包中'
|
||||||
|
}
|
||||||
|
return '导出资料'
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearExportPolling = (meetingId?: number) => {
|
||||||
|
if (meetingId !== undefined) {
|
||||||
|
const timer = exportPollingMap.get(meetingId)
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
exportPollingMap.delete(meetingId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exportPollingMap.forEach((timer) => clearTimeout(timer))
|
||||||
|
exportPollingMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeFileName = (name: string) => name.replace(/[\\/:*?"<>|]/g, '_').trim()
|
||||||
|
|
||||||
|
const resolveMeetingDate = (value?: string) => {
|
||||||
|
if (!value) return undefined
|
||||||
|
const normalized = value.includes(' ') ? value.replace(' ', 'T') : value
|
||||||
|
const date = new Date(normalized)
|
||||||
|
return Number.isNaN(date.getTime()) ? undefined : date
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatYYYYMMDD = (date: Date) => {
|
||||||
|
const year = String(date.getFullYear())
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}${month}${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildExportZipFileName = (meetingId: number) => {
|
||||||
|
const meeting = list.value.find((item) => item.id === meetingId)
|
||||||
|
const date = resolveMeetingDate(meeting?.startTime) || new Date()
|
||||||
|
const meetingName = sanitizeFileName(meeting?.name || `会议${meetingId}`)
|
||||||
|
return `${formatYYYYMMDD(date)}+${meetingName}+评审资料.zip`
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerExportDownload = async (meetingId: number, taskId: number) => {
|
||||||
|
const blob = await downloadReviewMeetingMaterialExportTask(taskId)
|
||||||
|
download.zip(blob as Blob, buildExportZipFileName(meetingId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollExportTask = async (meetingId: number, taskId: number) => {
|
||||||
|
try {
|
||||||
|
const task = await getReviewMeetingMaterialExportTask(taskId)
|
||||||
|
exportTaskMap[meetingId] = task
|
||||||
|
if (task.status === 2 && task.downloadReady) {
|
||||||
|
clearExportPolling(meetingId)
|
||||||
|
try {
|
||||||
|
await triggerExportDownload(meetingId, taskId)
|
||||||
|
ElMessage.success('资料压缩包已生成并开始下载')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('资料压缩包下载失败')
|
||||||
|
} finally {
|
||||||
|
exportTaskMap[meetingId] = undefined
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (task.status === 3) {
|
||||||
|
clearExportPolling(meetingId)
|
||||||
|
ElMessage.error(task.errorMessage || '资料打包失败')
|
||||||
|
exportTaskMap[meetingId] = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearExportPolling(meetingId)
|
||||||
|
exportTaskMap[meetingId] = undefined
|
||||||
|
ElMessage.error('查询导出进度失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearExportPolling(meetingId)
|
||||||
|
exportPollingMap.set(
|
||||||
|
meetingId,
|
||||||
|
setTimeout(() => {
|
||||||
|
pollExportTask(meetingId, taskId)
|
||||||
|
}, 2000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportMaterials = async (row: ReviewMeetingRespVO) => {
|
||||||
|
if (isExporting(row.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const task = await createReviewMeetingMaterialExportTask(row.id)
|
||||||
|
exportTaskMap[row.id] = task
|
||||||
|
await pollExportTask(row.id, task.taskId)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => getList())
|
onMounted(() => getList())
|
||||||
|
onBeforeUnmount(() => clearExportPolling())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue