重构学员证书签发与批量导入流程

This commit is contained in:
Certificate System
2026-08-14 11:37:09 +08:00
parent 522f529c6a
commit 5e33657268
16 changed files with 1047 additions and 370 deletions

View File

@@ -5,25 +5,26 @@
<h2>Excel 导入</h2>
<p>上传后先校验确认导入后才会正式写入学员和证书数据</p>
</div>
<el-button @click="downloadTemplate">下载模板</el-button>
<el-button :disabled="!selectedTemplate" @click="downloadTemplate">下载所选模板</el-button>
</header>
<el-alert
class="import-tip"
type="info"
:closable="false"
title="课程开始日期、课程结束日期和发证日期均为必填项,请统一使用 YYYY-MM-DD 格式,例如 2026-06-01。"
title="请先选择证书模板,再下载对应 Excel。不同证书模板的列和必填项不同不要混用文件。"
/>
<div class="batch-template">
<div>
<strong>本批次证书模板</strong>
<span>确认导入后整批证书都会固化为所选模板</span>
<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" />
<p v-if="selectedTemplate" class="template-fields">Excel 字段{{ selectedImportColumns.join("") }}</p>
</div>
<el-upload
@@ -129,6 +130,12 @@ 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));
const selectedImportColumns = computed(() => [
"姓名",
"手机号",
"项目代码",
...(selectedTemplate.value?.dynamic_fields.filter((field) => field !== "姓名") || []),
]);
function templateName(code: string) {
return templates.value.find((item) => item.code === code)?.name || code;
@@ -149,7 +156,11 @@ function pickFile(file: UploadFile) {
}
function downloadTemplate() {
downloadFile("/admin/import-batches/template", "certificate-import-template.xlsx");
if (!selectedTemplate.value) return;
downloadFile(
`/admin/import-batches/template?template_code=${encodeURIComponent(templateCode.value)}`,
`certificate-import-${templateCode.value}.xlsx`,
);
}
async function loadBatches() {
@@ -291,6 +302,13 @@ onUnmounted(stopPregenerationPolling);
border: 1px solid #edf2f4;
}
.template-fields {
grid-column: 1 / -1;
margin: 0;
color: var(--app-muted);
font-size: 13px;
}
.import-tip {
margin-bottom: 4px;
}