新增PDF批量预生成

This commit is contained in:
2026-06-23 13:59:56 +08:00
parent b6be8b4593
commit 4742c175c3
18 changed files with 725 additions and 24 deletions

View File

@@ -34,7 +34,7 @@
<el-table-column prop="valid_rows" label="可导入" width="100" />
<el-table-column prop="failed_rows" label="失败" width="100" />
<el-table-column prop="created_at" label="上传时间" width="190" />
<el-table-column label="操作" width="380">
<el-table-column label="操作" width="470">
<template #default="{ row }">
<el-button size="small" @click="downloadSource(row)">下载原文件</el-button>
<el-button
@@ -46,27 +46,65 @@
>
确认导入
</el-button>
<el-button
v-if="row.status === 'imported'"
size="small"
:loading="preGeneratingId === row.id"
@click="startPregeneration(row)"
>
预生成PDF
</el-button>
<el-button v-if="row.error_report_path" size="small" @click="downloadErrorReport(row)">错误报告</el-button>
<el-button size="small" type="danger" @click="deleteBatch(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="pregenerationVisible" title="批量预生成PDF" width="560px" @closed="stopPregenerationPolling">
<div v-if="pregenerationJob" class="pregeneration-panel">
<el-progress :percentage="pregenerationJob.percent" />
<dl class="pregeneration-stats">
<div><dt>总数</dt><dd>{{ pregenerationJob.total }}</dd></div>
<div><dt>已完成</dt><dd>{{ pregenerationJob.completed }}</dd></div>
<div><dt>新生成</dt><dd>{{ pregenerationJob.generated }}</dd></div>
<div><dt>已缓存</dt><dd>{{ pregenerationJob.cached }}</dd></div>
<div><dt>跳过</dt><dd>{{ pregenerationJob.skipped }}</dd></div>
<div><dt>失败</dt><dd>{{ pregenerationJob.failed }}</dd></div>
</dl>
<p class="progress-note">当前任务按系统设置的最大并发数执行{{ pregenerationJob.concurrency_limit }}</p>
<el-alert
v-if="pregenerationDone"
:type="pregenerationJob.failed ? 'warning' : 'success'"
:closable="false"
:title="pregenerationJob.failed ? '预生成已完成,但有部分失败。' : '预生成已完成。'"
/>
<ul v-if="pregenerationJob.errors.length" class="error-list">
<li v-for="item in pregenerationJob.errors" :key="item">{{ item }}</li>
</ul>
</div>
</el-dialog>
</section>
</template>
<script setup lang="ts">
import { UploadFilled } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox, type UploadFile } from "element-plus";
import { onMounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { http, type ImportBatch } from "../api";
import { downloadFile } from "../download";
import { http, type ImportBatch, type PdfPregenerationJob } from "../api";
import { apiErrorMessage, downloadFile } from "../download";
const batches = ref<ImportBatch[]>([]);
const selectedFile = ref<File | null>(null);
const uploading = ref(false);
const confirmingId = ref<number | null>(null);
const preGeneratingId = ref<number | null>(null);
const pregenerationVisible = ref(false);
const pregenerationJob = ref<PdfPregenerationJob | null>(null);
let pregenerationTimer: number | null = null;
const pregenerationDone = computed(() => ["completed", "completed_with_errors"].includes(pregenerationJob.value?.status || ""));
function statusText(status: string) {
const labels: Record<string, string> = {
@@ -134,7 +172,42 @@ async function deleteBatch(row: ImportBatch) {
await loadBatches();
}
async function startPregeneration(row: ImportBatch) {
preGeneratingId.value = row.id;
try {
const { data } = await http.post<PdfPregenerationJob>("/admin/certificates/pdf-pregeneration-jobs", {
import_batch_id: row.id,
});
pregenerationJob.value = data;
pregenerationVisible.value = true;
startPregenerationPolling(data.id);
} catch (error: any) {
ElMessage.error(await apiErrorMessage(error, "启动PDF预生成失败"));
} finally {
preGeneratingId.value = null;
}
}
function startPregenerationPolling(jobId: string) {
stopPregenerationPolling();
pregenerationTimer = window.setInterval(async () => {
const { data } = await http.get<PdfPregenerationJob>(`/admin/certificates/pdf-pregeneration-jobs/${jobId}`);
pregenerationJob.value = data;
if (["completed", "completed_with_errors"].includes(data.status)) {
stopPregenerationPolling();
}
}, 1000);
}
function stopPregenerationPolling() {
if (pregenerationTimer) {
clearInterval(pregenerationTimer);
pregenerationTimer = null;
}
}
onMounted(loadBatches);
onUnmounted(stopPregenerationPolling);
</script>
<style scoped>
@@ -163,4 +236,45 @@ onMounted(loadBatches);
margin-top: 20px;
border-radius: 8px;
}
.pregeneration-panel {
display: grid;
gap: 16px;
}
.pregeneration-stats {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
margin: 0;
}
.pregeneration-stats div {
border: 1px solid #e2edf0;
border-radius: 8px;
padding: 10px;
text-align: center;
}
.pregeneration-stats dt {
color: #64748b;
font-size: 12px;
}
.pregeneration-stats dd {
margin: 4px 0 0;
font-size: 18px;
font-weight: 700;
}
.progress-note {
margin: 0;
color: #64748b;
}
.error-list {
margin: 0;
color: #b91c1c;
line-height: 1.8;
}
</style>