Initial certificate system

This commit is contained in:
nelso
2026-06-06 19:32:24 +08:00
commit ce4ed6213b
105 changed files with 9133 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
<template>
<section>
<header class="page-head">
<div>
<h2>Excel 导入</h2>
<p>上传后先校验确认导入后才会正式写入学员和证书数据</p>
</div>
<el-button @click="downloadTemplate">下载模板</el-button>
</header>
<el-upload
class="upload"
drag
:auto-upload="false"
:show-file-list="true"
:limit="1"
accept=".xlsx"
@change="pickFile"
@remove="selectedFile = null"
>
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div> Excel 文件拖到这里或点击选择文件</div>
</el-upload>
<el-button type="primary" :disabled="!selectedFile" :loading="uploading" @click="uploadFile">上传并校验</el-button>
<el-card class="panel" shadow="never">
<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="120">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="total_rows" label="总行数" width="100" />
<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">
<template #default="{ row }">
<el-button size="small" @click="downloadSource(row)">下载原文件</el-button>
<el-button
v-if="row.status === 'validated'"
size="small"
type="primary"
:loading="confirmingId === row.id"
@click="confirmBatch(row)"
>
确认导入
</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>
</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 { http, type ImportBatch } from "../api";
import { downloadFile } from "../download";
const batches = ref<ImportBatch[]>([]);
const selectedFile = ref<File | null>(null);
const uploading = ref(false);
const confirmingId = ref<number | null>(null);
function statusText(status: string) {
const labels: Record<string, string> = {
uploaded: "已上传",
validated: "已校验",
imported: "已导入",
failed: "校验失败",
};
return labels[status] || status;
}
function pickFile(file: UploadFile) {
selectedFile.value = file.raw || null;
}
function downloadTemplate() {
downloadFile("/admin/import-batches/template", "certificate-import-template.xlsx");
}
async function loadBatches() {
const { data } = await http.get<ImportBatch[]>("/admin/import-batches");
batches.value = data;
}
async function uploadFile() {
if (!selectedFile.value) return;
uploading.value = true;
try {
const body = new FormData();
body.append("file", selectedFile.value);
await http.post("/admin/import-batches", body);
ElMessage.success("文件已上传并完成校验,请检查结果后点击确认导入");
selectedFile.value = null;
await loadBatches();
} finally {
uploading.value = false;
}
}
async function confirmBatch(row: ImportBatch) {
await ElMessageBox.confirm(`确认把批次 ${row.id}${row.valid_rows} 行校验通过的数据写入系统?`, "确认导入", { type: "warning" });
confirmingId.value = row.id;
try {
const { data } = await http.post<ImportBatch>(`/admin/import-batches/${row.id}/confirm`);
row.status = data.status;
ElMessage.success(`导入完成,当前状态:${statusText(data.status)}`);
await loadBatches();
} finally {
confirmingId.value = null;
}
}
function downloadSource(row: ImportBatch) {
downloadFile(`/admin/import-batches/${row.id}/file`, row.filename);
}
function downloadErrorReport(row: ImportBatch) {
downloadFile(`/admin/import-batches/${row.id}/error-report`, `import-errors-${row.id}.xlsx`);
}
async function deleteBatch(row: ImportBatch) {
await ElMessageBox.confirm(`确认删除导入批次 ${row.id}?这只删除上传文件和导入记录,不会删除已经生成的学员和证书。`, "删除导入记录", { type: "warning" });
await http.delete(`/admin/import-batches/${row.id}`);
ElMessage.success("导入记录已删除");
await loadBatches();
}
onMounted(loadBatches);
</script>
<style scoped>
.page-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.page-head p {
margin: 6px 0 0;
color: #64748b;
}
.upload {
margin-bottom: 12px;
}
.upload-icon {
font-size: 36px;
color: #208a87;
}
.panel {
margin-top: 20px;
border-radius: 8px;
}
</style>