新增多证书模板管理流程

This commit is contained in:
Certificate System
2026-08-13 18:06:05 +08:00
parent eac19ea593
commit abbefc4b48
27 changed files with 774 additions and 61 deletions

View File

@@ -15,6 +15,17 @@
title="课程开始日期、课程结束日期和发证日期均为必填项,请统一使用 YYYY-MM-DD 格式,例如 2026-06-01。"
/>
<div class="batch-template">
<div>
<strong>本批次证书模板</strong>
<span>确认导入后整批证书都会固化为所选模板</span>
</div>
<el-select v-model="templateCode" placeholder="选择证书模板">
<el-option v-for="item in templates" :key="item.code" :label="item.name" :value="item.code" />
</el-select>
<img v-if="selectedTemplate" :src="selectedTemplate.preview_url" :alt="selectedTemplate.name" />
</div>
<el-upload
class="upload"
drag
@@ -34,6 +45,9 @@
<el-table :data="batches" border>
<el-table-column prop="id" label="批次ID" width="90" />
<el-table-column prop="filename" label="文件名" min-width="220" />
<el-table-column label="证书模板" width="170">
<template #default="{ row }">{{ templateName(row.template_code) }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
@@ -99,10 +113,12 @@ import { UploadFilled } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox, type UploadFile } from "element-plus";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { http, type ImportBatch, type PdfPregenerationJob } from "../api";
import { http, type CertificateTemplate, type ImportBatch, type PdfPregenerationJob } from "../api";
import { apiErrorMessage, downloadFile } from "../download";
const batches = ref<ImportBatch[]>([]);
const templates = ref<CertificateTemplate[]>([]);
const templateCode = ref("classic");
const selectedFile = ref<File | null>(null);
const uploading = ref(false);
const confirmingId = ref<number | null>(null);
@@ -112,6 +128,11 @@ const pregenerationJob = ref<PdfPregenerationJob | null>(null);
let pregenerationTimer: number | null = null;
const pregenerationDone = computed(() => ["completed", "completed_with_errors"].includes(pregenerationJob.value?.status || ""));
const selectedTemplate = computed(() => templates.value.find((item) => item.code === templateCode.value));
function templateName(code: string) {
return templates.value.find((item) => item.code === code)?.name || code;
}
function statusText(status: string) {
const labels: Record<string, string> = {
@@ -136,12 +157,18 @@ async function loadBatches() {
batches.value = data;
}
async function loadTemplates() {
const { data } = await http.get<CertificateTemplate[]>("/admin/certificate-templates");
templates.value = data;
}
async function uploadFile() {
if (!selectedFile.value) return;
uploading.value = true;
try {
const body = new FormData();
body.append("file", selectedFile.value);
body.append("template_code", templateCode.value);
await http.post("/admin/import-batches", body);
ElMessage.success("文件已上传并完成校验,请检查结果后点击确认导入");
selectedFile.value = null;
@@ -213,7 +240,7 @@ function stopPregenerationPolling() {
}
}
onMounted(loadBatches);
onMounted(() => Promise.all([loadBatches(), loadTemplates()]));
onUnmounted(stopPregenerationPolling);
</script>
@@ -235,6 +262,35 @@ onUnmounted(stopPregenerationPolling);
margin-bottom: 12px;
}
.batch-template {
display: grid;
grid-template-columns: minmax(260px, 1fr) 240px 180px;
align-items: center;
gap: 18px;
margin-top: 16px;
padding: 14px 16px;
border: 1px solid var(--app-border);
border-radius: 8px;
background: #fff;
}
.batch-template div {
display: grid;
gap: 5px;
}
.batch-template span {
color: var(--app-muted);
font-size: 13px;
}
.batch-template img {
width: 180px;
aspect-ratio: 1.36;
object-fit: contain;
border: 1px solid #edf2f4;
}
.import-tip {
margin-bottom: 4px;
}
@@ -289,4 +345,14 @@ onUnmounted(stopPregenerationPolling);
color: #b91c1c;
line-height: 1.8;
}
@media (max-width: 900px) {
.batch-template {
grid-template-columns: 1fr;
}
.batch-template img {
width: min(100%, 260px);
}
}
</style>