优化系统界面并接入正式品牌标识

This commit is contained in:
Certificate System
2026-08-13 19:12:52 +08:00
parent 7d08f74580
commit 522f529c6a
17 changed files with 1092 additions and 313 deletions

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/brand/huiyu-logo.png" />
<title>培训结业证书管理与查询系统</title>
</head>
<body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,98 @@
<template>
<header class="service-header">
<div class="service-header-inner">
<router-link class="service-brand" to="/query" aria-label="返回证书查询首页">
<span class="brand-mark"><img src="/brand/huiyu-logo.png" alt="" /></span>
<span>
<strong>慧遇书院</strong>
<small>电子证书服务</small>
</span>
</router-link>
<span class="service-state"><i></i>证书查询与核验</span>
</div>
</header>
</template>
<style scoped>
.service-header {
width: 100%;
border-bottom: 1px solid #e2e7e9;
background: rgba(255, 255, 255, 0.96);
}
.service-header-inner {
width: min(1120px, calc(100% - 40px));
min-height: 76px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.service-brand {
display: flex;
align-items: center;
gap: 12px;
color: #18232c;
}
.service-brand:hover {
text-decoration: none;
}
.brand-mark {
width: 38px;
height: 38px;
display: grid;
place-items: center;
}
.brand-mark img {
width: 36px;
height: 32px;
object-fit: contain;
}
.service-brand > span:last-child {
display: grid;
gap: 2px;
}
.service-brand strong {
font-size: 16px;
letter-spacing: 0;
}
.service-brand small {
color: #75818a;
font-size: 11px;
}
.service-state {
display: inline-flex;
align-items: center;
gap: 8px;
color: #5d6d78;
font-size: 13px;
}
.service-state i {
width: 7px;
height: 7px;
border-radius: 50%;
background: #2f8a69;
box-shadow: 0 0 0 4px #e6f2ed;
}
@media (max-width: 640px) {
.service-header-inner {
width: calc(100% - 28px);
min-height: 66px;
}
.service-state {
font-size: 0;
}
}
</style>

View File

@@ -1,34 +1,39 @@
:root {
--app-bg: #f5f8fa;
--app-bg: #f3f5f7;
--app-surface: #ffffff;
--app-surface-soft: #f9fbfc;
--app-border: #dbe6ea;
--app-border-strong: #c8d7dd;
--app-text: #172033;
--app-muted: #64748b;
--app-primary: #16817e;
--app-primary-strong: #0f6f6b;
--app-primary-soft: #e6f5f3;
--app-blue-soft: #eef5fb;
--app-gold: #b88a2d;
--app-danger: #c9413a;
--app-warning: #b7791f;
--app-success: #16817e;
--app-shadow: 0 14px 36px rgba(42, 64, 78, 0.08);
--app-shadow-soft: 0 8px 22px rgba(42, 64, 78, 0.055);
--app-surface-soft: #f7f9fa;
--app-border: #dce3e7;
--app-border-strong: #c7d1d7;
--app-text: #17212b;
--app-muted: #647381;
--app-primary: #176f69;
--app-primary-strong: #105b57;
--app-primary-soft: #e8f3f1;
--app-gold: #b28a43;
--app-gold-soft: #f6f0e5;
--app-danger: #b83b36;
--app-warning: #a76518;
--app-success: #176f69;
--app-shadow: 0 12px 32px rgba(27, 43, 53, 0.08);
--app-shadow-soft: 0 4px 14px rgba(27, 43, 53, 0.05);
}
* {
box-sizing: border-box;
}
html,
body,
#app {
min-height: 100%;
}
body {
margin: 0;
color: var(--app-text);
background:
linear-gradient(180deg, #f7fafb 0%, #eef4f6 100%);
font-family:
Inter, "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif;
background: var(--app-bg);
font-family: Inter, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
button,
@@ -38,25 +43,36 @@ select {
font: inherit;
}
a {
color: var(--app-primary-strong);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 18px;
margin-bottom: 18px;
align-items: center;
gap: 20px;
margin-bottom: 22px;
}
.page-head h2 {
margin: 0;
color: var(--app-text);
font-size: 24px;
font-weight: 750;
font-size: 26px;
font-weight: 720;
letter-spacing: 0;
}
.page-head p {
max-width: 760px;
margin: 7px 0 0;
color: var(--app-muted);
font-size: 14px;
line-height: 1.6;
}
@@ -64,21 +80,62 @@ select {
.el-card.panel,
.el-card.stat {
border: 1px solid var(--app-border);
border-radius: 10px;
border-radius: 7px;
background: var(--app-surface);
box-shadow: var(--app-shadow-soft);
}
.panel + .panel {
margin-top: 18px;
}
.panel .el-card__body {
padding: 22px;
}
.panel-head,
.form-section-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 20px;
}
.panel-head h3,
.form-section-head h3 {
margin: 0;
font-size: 17px;
font-weight: 700;
}
.panel-head p,
.form-section-head p {
margin: 5px 0 0;
color: var(--app-muted);
font-size: 13px;
line-height: 1.55;
}
.el-card {
--el-card-border-color: var(--app-border);
--el-card-border-radius: 10px;
--el-card-border-radius: 7px;
}
.el-button {
--el-button-border-radius: 7px;
--el-button-border-radius: 6px;
min-height: 38px;
padding-right: 16px;
padding-left: 16px;
font-weight: 600;
}
.el-button--small {
min-height: 30px;
padding-right: 11px;
padding-left: 11px;
}
.el-button--primary {
--el-button-bg-color: var(--app-primary);
--el-button-border-color: var(--app-primary);
@@ -91,7 +148,8 @@ select {
.el-input__wrapper,
.el-select__wrapper,
.el-textarea__inner {
border-radius: 8px;
min-height: 40px;
border-radius: 6px;
box-shadow: 0 0 0 1px var(--app-border) inset;
}
@@ -104,75 +162,98 @@ select {
.el-input__wrapper.is-focus,
.el-select__wrapper.is-focused,
.el-textarea__inner:focus {
box-shadow: 0 0 0 1px var(--app-primary) inset, 0 0 0 3px rgba(22, 129, 126, 0.12);
box-shadow: 0 0 0 1px var(--app-primary) inset, 0 0 0 3px rgba(23, 111, 105, 0.1);
}
.el-form-item__label {
color: #415466;
color: #3f505e;
font-weight: 650;
}
.el-table {
--el-table-border-color: #e5edf0;
--el-table-header-bg-color: #f6fafb;
--el-table-header-text-color: #52677a;
--el-table-row-hover-bg-color: #f8fcfc;
border-radius: 8px;
--el-table-border-color: #e3e9ec;
--el-table-header-bg-color: #f5f7f8;
--el-table-header-text-color: #536572;
--el-table-row-hover-bg-color: #f6faf9;
border-radius: 6px;
overflow: hidden;
}
.el-table th.el-table__cell {
height: 44px;
font-weight: 700;
}
.el-table td.el-table__cell {
height: 48px;
}
.el-tag {
border-radius: 999px;
font-weight: 650;
}
.el-dialog {
border-radius: 12px;
border-radius: 8px;
box-shadow: 0 28px 72px rgba(20, 35, 45, 0.2);
}
.el-dialog__header {
padding: 22px 24px 12px;
padding: 22px 24px 14px;
margin: 0;
border-bottom: 1px solid #edf1f3;
}
.el-dialog__body {
padding: 14px 24px 22px;
padding: 20px 24px 24px;
}
.el-dialog__footer {
padding: 14px 24px 20px;
border-top: 1px solid #edf1f3;
}
.el-dialog__title {
color: var(--app-text);
font-size: 20px;
font-weight: 750;
font-size: 19px;
font-weight: 720;
}
.el-segmented {
--el-segmented-item-selected-bg-color: var(--app-primary);
--el-segmented-item-selected-color: #ffffff;
--el-segmented-item-selected-bg-color: #ffffff;
--el-segmented-item-selected-color: var(--app-primary-strong);
border: 1px solid var(--app-border);
background: #f4f8fa;
border-radius: 7px;
background: #eef2f3;
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px 18px;
}
.form-grid .el-form-item {
margin-bottom: 0;
}
.form-actions {
display: flex;
grid-column: 1 / -1;
justify-content: flex-end;
gap: 10px;
margin-top: 4px;
padding-top: 18px;
border-top: 1px solid #edf1f3;
}
.toolbar,
.filters {
align-items: center;
}
.pager {
border-top: 1px solid #edf2f4;
border-top: 1px solid #edf1f3;
padding-top: 12px;
}
@@ -190,15 +271,6 @@ select {
transform: translateY(-1px);
}
a {
color: var(--app-primary-strong);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 1100px) {
.form-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -211,7 +283,25 @@ a:hover {
align-items: start;
}
.page-head h2 {
font-size: 23px;
}
.form-grid {
grid-template-columns: 1fr;
}
.form-actions {
display: grid;
grid-template-columns: 1fr 1fr;
}
.form-actions .el-button {
width: 100%;
margin: 0;
}
.el-dialog {
width: calc(100% - 24px) !important;
}
}

View File

@@ -5,7 +5,7 @@
<h2>证书管理</h2>
<p>支持单张新增预览下载作废链接重置和批量预生成PDF</p>
</div>
<el-button type="primary" @click="openCreateDialog">创建证书</el-button>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">创建证书</el-button>
</header>
<el-dialog v-model="createVisible" title="创建证书" width="920px" destroy-on-close>
@@ -81,23 +81,27 @@
<el-card class="panel" shadow="never">
<div class="toolbar">
<el-input v-model.trim="keyword" placeholder="按证书编号、名称、项目代码搜索" clearable @keyup.enter="loadCertificates" />
<el-select v-model="statusValue" clearable placeholder="状态">
<el-option label="有效" value="valid" />
<el-option label="已作废" value="voided" />
</el-select>
<el-select v-model="pdfStatusValue" clearable placeholder="PDF状态">
<el-option label="未生成" value="not_generated" />
<el-option label="生成" value="generated" />
<el-option label="已清理" value="cleaned" />
</el-select>
<el-select v-model="importBatchId" clearable filterable placeholder="导入批次">
<el-option v-for="item in importBatches" :key="item.id" :label="`#${item.id} ${item.filename}`" :value="item.id" />
</el-select>
<el-button @click="loadCertificates">搜索</el-button>
<el-button type="primary" :disabled="!selectedCertificates.length" :loading="pregenerationStarting" @click="startPregenerationSelected">预生成所选PDF</el-button>
<el-button :disabled="!importBatchId" :loading="pregenerationStarting" @click="startPregenerationBatch">预生成当前批次全部</el-button>
<el-button @click="exportCertificates">导出</el-button>
<div class="filter-controls">
<el-input v-model.trim="keyword" placeholder="按证书编号、名称、项目代码搜索" clearable @keyup.enter="loadCertificates" />
<el-select v-model="statusValue" clearable placeholder="状态">
<el-option label="有效" value="valid" />
<el-option label="已作废" value="voided" />
</el-select>
<el-select v-model="pdfStatusValue" clearable placeholder="PDF状态">
<el-option label="生成" value="not_generated" />
<el-option label="已生成" value="generated" />
<el-option label="已清理" value="cleaned" />
</el-select>
<el-select v-model="importBatchId" clearable filterable placeholder="导入批次">
<el-option v-for="item in importBatches" :key="item.id" :label="`#${item.id} ${item.filename}`" :value="item.id" />
</el-select>
<el-button :icon="Search" @click="loadCertificates">搜索</el-button>
</div>
<div class="bulk-actions">
<el-button :icon="Download" @click="exportCertificates">导出</el-button>
<el-button :icon="Files" :disabled="!importBatchId" :loading="pregenerationStarting" @click="startPregenerationBatch">预生成当前批次</el-button>
<el-button type="primary" :icon="MagicStick" :disabled="!selectedCertificates.length" :loading="pregenerationStarting" @click="startPregenerationSelected">预生成所选 PDF</el-button>
</div>
</div>
<el-table :data="certificates" border @selection-change="handleSelectionChange">
@@ -183,6 +187,7 @@
</template>
<script setup lang="ts">
import { Download, Files, MagicStick, Plus, Search } from "@element-plus/icons-vue";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
@@ -530,14 +535,35 @@ onUnmounted(stopPregenerationPolling);
}
.toolbar {
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
justify-content: space-between;
padding-bottom: 16px;
border-bottom: 1px solid #edf1f3;
}
.toolbar .el-input {
.filter-controls,
.bulk-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.filter-controls {
flex: 1 1 660px;
}
.bulk-actions {
justify-content: flex-end;
}
.filter-controls .el-input {
flex: 1 1 260px;
max-width: 340px;
}
.toolbar .el-select {
.filter-controls .el-select {
width: 170px;
}
@@ -604,5 +630,18 @@ onUnmounted(stopPregenerationPolling);
.create-grid {
grid-template-columns: 1fr;
}
.filter-controls,
.bulk-actions {
width: 100%;
}
.filter-controls .el-input,
.filter-controls .el-select,
.bulk-actions .el-button {
width: 100%;
max-width: none;
margin: 0;
}
}
</style>

View File

@@ -2,22 +2,27 @@
<section>
<header class="page-head">
<div>
<h2>一期工作台</h2>
<p>快速查看证书导入和状态数据</p>
<h2>数据概览</h2>
<p>查看学员证书和近期导入的核心数据</p>
</div>
<el-button type="primary" @click="downloadTemplate">下载导入模板</el-button>
<el-button type="primary" :icon="Download" @click="downloadTemplate">下载导入模板</el-button>
</header>
<div class="stats">
<el-card v-for="item in cards" :key="item.label" class="stat" shadow="never">
<span>{{ item.label }}</span>
<div class="stat-head">
<span>{{ item.label }}</span>
<el-icon :class="item.tone"><component :is="item.icon" /></el-icon>
</div>
<strong>{{ item.value }}</strong>
<small>{{ item.note }}</small>
</el-card>
</div>
</section>
</template>
<script setup lang="ts">
import { CircleCheck, DataBoard, Download, UploadFilled, User, Warning } from "@element-plus/icons-vue";
import { computed, onMounted, reactive } from "vue";
import { useRouter } from "vue-router";
@@ -34,11 +39,11 @@ const summary = reactive({
});
const cards = computed(() => [
{ label: "学员总数", value: summary.learner_count },
{ label: "证书总数", value: summary.certificate_count },
{ label: "有效证书", value: summary.valid_certificate_count },
{ label: "作废证书", value: summary.voided_certificate_count },
{ label: "近期导入", value: summary.recent_import_count },
{ label: "学员总数", value: summary.learner_count, note: "系统学员档案", icon: User, tone: "teal" },
{ label: "证书总数", value: summary.certificate_count, note: "累计签发记录", icon: DataBoard, tone: "blue" },
{ label: "有效证书", value: summary.valid_certificate_count, note: "当前有效", icon: CircleCheck, tone: "green" },
{ label: "作废证书", value: summary.voided_certificate_count, note: "历史作废", icon: Warning, tone: "red" },
{ label: "近期导入", value: summary.recent_import_count, note: "最近导入批次", icon: UploadFilled, tone: "gold" },
]);
async function loadSummary() {
@@ -59,18 +64,6 @@ onMounted(loadSummary);
</script>
<style scoped>
.page-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.page-head p {
margin: 6px 0 0;
color: #64748b;
}
.stats {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
@@ -78,10 +71,13 @@ onMounted(loadSummary);
}
.stat {
border: 1px solid #dbe6ea;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff, #f8fbfc);
box-shadow: 0 8px 22px rgba(42, 64, 78, 0.055);
background: #ffffff;
}
.stat-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.stat span {
@@ -92,10 +88,45 @@ onMounted(loadSummary);
.stat strong {
display: block;
margin-top: 8px;
font-size: 30px;
font-weight: 800;
color: #172033;
margin-top: 16px;
color: #17212b;
font-size: 32px;
font-weight: 760;
}
.stat small {
display: block;
margin-top: 4px;
color: #8a959d;
font-size: 12px;
}
.stat .el-icon {
width: 34px;
height: 34px;
border-radius: 6px;
font-size: 18px;
}
.stat .teal,
.stat .green {
color: #176f69;
background: #e8f3f1;
}
.stat .blue {
color: #3e6789;
background: #ebf1f6;
}
.stat .red {
color: #a83a35;
background: #f8eaea;
}
.stat .gold {
color: #8d6b2f;
background: #f6f0e5;
}
@media (max-width: 1100px) {

View File

@@ -5,10 +5,16 @@
<h2>学员管理</h2>
<p>支持手动新增修改删除和查询学员也可以继续通过 Excel 批量导入</p>
</div>
<el-button type="primary" @click="saveLearner">{{ editingId ? "保存修改" : "新增学员" }}</el-button>
</header>
<el-card class="panel" shadow="never">
<div class="form-section-head">
<div>
<h3>{{ editingId ? "编辑学员" : "新增学员" }}</h3>
<p>{{ editingId ? `正在修改 ${form.current_name}` : "姓名和手机号用于证书归属及查询,请确认信息准确。" }}</p>
</div>
<el-tag v-if="editingId" type="warning">编辑中</el-tag>
</div>
<el-form class="form-grid" label-position="top">
<el-form-item label="姓名">
<el-input v-model.trim="form.current_name" />
@@ -28,9 +34,12 @@
<el-form-item label="备注" class="wide">
<el-input v-model.trim="form.remark" />
</el-form-item>
<el-form-item label=" ">
<el-button @click="resetForm">清空</el-button>
</el-form-item>
<div class="form-actions">
<el-button :icon="RefreshLeft" @click="resetForm">重置</el-button>
<el-button type="primary" :icon="editingId ? Check : Plus" @click="saveLearner">
{{ editingId ? "保存修改" : "新增学员" }}
</el-button>
</div>
</el-form>
</el-card>
@@ -62,6 +71,7 @@
</template>
<script setup lang="ts">
import { Check, Plus, RefreshLeft } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { onMounted, reactive, ref } from "vue";
@@ -124,7 +134,6 @@ onMounted(loadLearners);
</script>
<style scoped>
.page-head,
.toolbar {
display: flex;
justify-content: space-between;
@@ -133,24 +142,12 @@ onMounted(loadLearners);
margin-bottom: 16px;
}
.page-head p {
margin: 6px 0 0;
color: #64748b;
}
.panel {
margin-bottom: 16px;
border-radius: 8px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.wide {
grid-column: span 3;
grid-column: span 2;
}
.toolbar {
@@ -160,4 +157,10 @@ onMounted(loadLearners);
.toolbar .el-input {
max-width: 380px;
}
@media (max-width: 720px) {
.wide {
grid-column: 1;
}
}
</style>

View File

@@ -1,15 +1,26 @@
<template>
<main class="login-page">
<section class="login-box">
<h1>后台登录</h1>
<el-form label-position="top">
<header class="login-head">
<span class="login-mark"><img src="/brand/huiyu-logo.png" alt="" /></span>
<div>
<p>慧遇书院</p>
<h1>证书管理后台</h1>
</div>
</header>
<p class="login-copy">请使用管理员账号登录</p>
<el-form label-position="top" size="large">
<el-form-item label="账号">
<el-input v-model.trim="form.username" />
<el-input v-model.trim="form.username" placeholder="请输入账号">
<template #prefix><el-icon><User /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password @keyup.enter="login" />
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码" @keyup.enter="login">
<template #prefix><el-icon><Lock /></el-icon></template>
</el-input>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="login">登录</el-button>
<el-button type="primary" :icon="Right" class="submit" :loading="loading" @click="login">登录后台</el-button>
</el-form>
<el-alert v-if="message" class="message" :title="message" type="error" :closable="false" />
</section>
@@ -17,6 +28,7 @@
</template>
<script setup lang="ts">
import { Lock, Right, User } from "@element-plus/icons-vue";
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
@@ -50,29 +62,69 @@ async function login() {
min-height: 100vh;
display: grid;
place-items: center;
background: linear-gradient(180deg, #f7fafb 0%, #eef4f6 100%);
background: #f1f4f5;
padding: 24px;
}
.login-box {
width: min(440px, 100%);
padding: 32px;
border: 1px solid #dbe6ea;
border-radius: 14px;
padding: 34px;
border: 1px solid #dbe2e5;
border-top: 3px solid #b28a43;
border-radius: 7px;
background: #ffffff;
box-shadow: 0 18px 48px rgba(42, 64, 78, 0.09);
box-shadow: 0 18px 48px rgba(31, 47, 57, 0.1);
}
h1 {
margin: 0 0 22px;
color: #172033;
font-size: 26px;
font-weight: 800;
.login-head {
display: flex;
align-items: center;
gap: 13px;
}
.login-mark {
width: 42px;
height: 42px;
display: grid;
place-items: center;
}
.login-mark img {
width: 40px;
height: 35px;
object-fit: contain;
}
.login-head p,
.login-head h1 {
margin: 0;
}
.login-head p {
color: #9a7637;
font-size: 12px;
font-weight: 700;
}
.login-head h1 {
margin-top: 2px;
color: #17212b;
font-size: 23px;
font-weight: 750;
}
.login-copy {
margin: 22px 0 18px;
padding-bottom: 16px;
border-bottom: 1px solid #edf1f2;
color: #6b7982;
font-size: 13px;
}
.submit {
width: 100%;
height: 38px;
height: 42px;
margin-top: 4px;
font-weight: 700;
}

View File

@@ -5,10 +5,16 @@
<h2>项目管理</h2>
<p>维护项目代码默认证书模板和课程信息</p>
</div>
<el-button type="primary" @click="saveProject">{{ editingId ? "保存修改" : "新增项目" }}</el-button>
</header>
<el-card class="panel" shadow="never">
<div class="form-section-head">
<div>
<h3>{{ editingId ? "编辑项目" : "新增项目" }}</h3>
<p>{{ editingId ? `正在修改项目 ${form.code}` : "项目代码创建后不可修改,请使用清晰稳定的英文缩写。" }}</p>
</div>
<el-tag v-if="editingId" type="warning">编辑中</el-tag>
</div>
<el-form class="form-grid" label-position="top">
<el-form-item label="项目代码">
<el-input v-model.trim="form.code" :disabled="!!editingId" maxlength="16" />
@@ -33,9 +39,12 @@
<el-option label="停用" value="disabled" />
</el-select>
</el-form-item>
<el-form-item label=" ">
<el-button @click="resetForm">清空</el-button>
</el-form-item>
<div class="form-actions">
<el-button :icon="RefreshLeft" @click="resetForm">重置</el-button>
<el-button type="primary" :icon="editingId ? Check : Plus" @click="saveProject">
{{ editingId ? "保存修改" : "新增项目" }}
</el-button>
</div>
</el-form>
</el-card>
@@ -72,6 +81,7 @@
</template>
<script setup lang="ts">
import { Check, Plus, RefreshLeft } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { computed, onMounted, reactive, ref } from "vue";
@@ -182,7 +192,6 @@ onMounted(() => Promise.all([loadProjects(), loadTemplates()]));
</script>
<style scoped>
.page-head,
.toolbar {
display: flex;
justify-content: space-between;
@@ -190,20 +199,8 @@ onMounted(() => Promise.all([loadProjects(), loadTemplates()]));
margin-bottom: 16px;
}
.page-head p {
margin: 6px 0 0;
color: #64748b;
}
.panel {
margin-bottom: 16px;
border-radius: 8px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.toolbar .el-input {

View File

@@ -1,30 +1,57 @@
<template>
<main class="admin-shell">
<aside class="sidebar">
<div class="brand">
<h1>证书管理</h1>
<p>培训结业证书后台</p>
<div class="brand-row">
<div class="brand">
<span class="brand-mark"><img src="/brand/huiyu-logo.png" alt="" /></span>
<span>
<strong>证书管理</strong>
<small>慧遇书院管理后台</small>
</span>
</div>
<el-tooltip content="退出登录" placement="right">
<el-button class="logout" circle aria-label="退出登录" @click="logout"><el-icon><SwitchButton /></el-icon></el-button>
</el-tooltip>
</div>
<el-menu :default-active="route.path" router>
<el-menu-item index="/admin">数据概览</el-menu-item>
<el-menu-item index="/admin/projects">项目管理</el-menu-item>
<el-menu-item index="/admin/templates">证书模板</el-menu-item>
<el-menu-item index="/admin/learners">学员管理</el-menu-item>
<el-menu-item index="/admin/certificates">证书管理</el-menu-item>
<el-menu-item index="/admin/imports">Excel 导入</el-menu-item>
<el-menu-item index="/admin/logs">操作日志</el-menu-item>
<el-menu-item index="/admin/backups">备份管理</el-menu-item>
<el-menu-item index="/admin/settings">系统设置</el-menu-item>
<el-menu-item index="/admin"><el-icon><DataAnalysis /></el-icon><span>数据概览</span></el-menu-item>
<li class="menu-label">业务管理</li>
<el-menu-item index="/admin/projects"><el-icon><Collection /></el-icon><span>项目管理</span></el-menu-item>
<el-menu-item index="/admin/templates"><el-icon><Picture /></el-icon><span>证书模板</span></el-menu-item>
<el-menu-item index="/admin/learners"><el-icon><User /></el-icon><span>学员管理</span></el-menu-item>
<el-menu-item index="/admin/certificates"><el-icon><Postcard /></el-icon><span>证书管理</span></el-menu-item>
<el-menu-item index="/admin/imports"><el-icon><Upload /></el-icon><span>Excel 导入</span></el-menu-item>
<li class="menu-label">系统维护</li>
<el-menu-item index="/admin/logs"><el-icon><Document /></el-icon><span>操作日志</span></el-menu-item>
<el-menu-item index="/admin/backups"><el-icon><Coin /></el-icon><span>备份管理</span></el-menu-item>
<el-menu-item index="/admin/settings"><el-icon><Setting /></el-icon><span>系统设置</span></el-menu-item>
</el-menu>
<el-button class="logout" @click="logout">退出登录</el-button>
<div class="sidebar-foot">
<span class="avatar"></span>
<span><strong>系统管理员</strong><small>已安全登录</small></span>
</div>
</aside>
<section class="workspace">
<router-view />
<div class="workspace-inner"><router-view /></div>
</section>
</main>
</template>
<script setup lang="ts">
import {
Coin,
Collection,
DataAnalysis,
Document,
Picture,
Postcard,
Setting,
SwitchButton,
Upload,
User,
} from "@element-plus/icons-vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
@@ -40,77 +67,206 @@ function logout() {
.admin-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 248px 1fr;
background: linear-gradient(180deg, #f7fafb 0%, #eef4f6 100%);
color: #172033;
grid-template-columns: 232px minmax(0, 1fr);
background: var(--app-bg);
color: var(--app-text);
}
.sidebar {
position: sticky;
top: 0;
height: 100vh;
background: rgba(255, 255, 255, 0.94);
border-right: 1px solid #e1ebef;
padding: 24px 14px;
box-shadow: 8px 0 26px rgba(42, 64, 78, 0.045);
display: flex;
flex-direction: column;
padding: 20px 14px 16px;
border-right: 1px solid #dfe5e8;
background: #ffffff;
}
.brand-row,
.brand {
display: flex;
align-items: center;
}
.brand-row {
justify-content: space-between;
gap: 8px;
padding: 0 6px 18px;
border-bottom: 1px solid #e8edef;
}
.brand {
padding: 0 12px 20px;
border-bottom: 1px solid #e1ebef;
margin-bottom: 18px;
min-width: 0;
gap: 10px;
}
.brand h1 {
font-size: 21px;
font-weight: 800;
margin: 0;
letter-spacing: 0;
.brand-mark {
width: 34px;
height: 34px;
flex: 0 0 auto;
display: grid;
place-items: center;
}
.brand p {
margin: 7px 0 0;
color: #6b8795;
font-size: 13px;
.brand-mark img {
width: 32px;
height: 28px;
object-fit: contain;
}
:deep(.el-menu) {
border-right: 0;
.brand > span:last-child,
.sidebar-foot > span:last-child {
min-width: 0;
display: grid;
gap: 3px;
}
:deep(.el-menu-item) {
height: 42px;
margin: 4px 0;
border-radius: 8px;
color: #42576a;
font-weight: 650;
.brand strong {
font-size: 16px;
font-weight: 750;
}
:deep(.el-menu-item.is-active),
:deep(.el-menu-item:hover) {
background: #e6f5f3;
color: #116d6b;
.brand small,
.sidebar-foot small {
color: #7b8992;
font-size: 11px;
}
.logout {
width: calc(100% - 16px);
margin: 20px 8px 0;
flex: 0 0 auto;
min-height: 32px;
width: 32px;
height: 32px;
padding: 0;
}
:deep(.el-menu) {
flex: 1;
border-right: 0;
padding-top: 12px;
}
:deep(.el-menu-item) {
height: 40px;
margin: 3px 0;
border-radius: 6px;
color: #465965;
font-size: 14px;
font-weight: 600;
}
:deep(.el-menu-item .el-icon) {
font-size: 17px;
}
:deep(.el-menu-item.is-active) {
background: var(--app-primary-soft);
color: var(--app-primary-strong);
}
:deep(.el-menu-item:hover) {
background: #f3f6f7;
color: var(--app-text);
}
.menu-label {
list-style: none;
padding: 16px 14px 5px;
color: #99a4ab;
font-size: 11px;
font-weight: 700;
}
.sidebar-foot {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 8px 2px;
border-top: 1px solid #e8edef;
}
.sidebar-foot strong {
font-size: 13px;
}
.avatar {
width: 32px;
height: 32px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 50%;
color: #805f27;
background: var(--app-gold-soft);
font-size: 12px;
font-weight: 700;
}
.workspace {
min-width: 0;
padding: 28px;
overflow: auto;
padding: 30px 32px 48px;
}
.workspace-inner {
width: min(1480px, 100%);
margin: 0 auto;
}
@media (max-width: 900px) {
.admin-shell {
width: 100%;
min-width: 0;
grid-template-columns: 1fr;
overflow: hidden;
}
.sidebar {
position: static;
position: sticky;
z-index: 20;
height: auto;
width: 100%;
min-width: 0;
padding: 10px 14px 0;
overflow: hidden;
border-right: 0;
border-bottom: 1px solid #dfe5e8;
}
.brand-row {
padding: 0 2px 10px;
}
.brand small,
.sidebar-foot,
.menu-label {
display: none;
}
:deep(.el-menu) {
width: 100%;
min-width: 0;
display: flex;
overflow-x: auto;
gap: 4px;
padding: 0 0 8px;
}
:deep(.el-menu-item) {
flex: 0 0 auto;
padding: 0 12px;
}
.workspace {
width: 100%;
min-width: 0;
padding: 22px 16px 36px;
}
.workspace-inner {
width: 100%;
min-width: 0;
}
}
</style>

View File

@@ -1,45 +1,73 @@
<template>
<main class="public-page">
<section class="query-panel">
<div class="heading">
<p>电子证书查询</p>
<h1>培训结业证书查询</h1>
</div>
<main class="public-query-page">
<PublicServiceHeader />
<el-form label-position="top">
<section class="query-main">
<header class="query-intro">
<p class="eyebrow">CERTIFICATE VERIFICATION</p>
<h1>证书查询与核验</h1>
<p class="intro-copy">查询慧遇书院已签发的培训结业证书</p>
<div class="trust-row">
<span><el-icon><CircleCheck /></el-icon>签发记录可追溯</span>
<span><el-icon><Lock /></el-icon>查询信息安全保护</span>
</div>
</header>
<section class="query-card" aria-label="证书查询表单">
<div class="query-card-head">
<div>
<h2>查询证书</h2>
<p>请选择证书编号或短信验证码查询</p>
</div>
<span class="official-mark"><img src="/brand/huiyu-logo.png" alt="" />官方查询</span>
</div>
<el-form label-position="top" size="large">
<el-form-item label="查询方式">
<el-segmented v-model="queryMode" :options="queryOptions" />
<el-segmented v-model="queryMode" :options="queryOptions" class="query-mode" />
</el-form-item>
<el-form-item v-if="queryMode === 'certificate'" label="证书编号">
<el-input v-model.trim="form.certificateNo" placeholder="例如 PX2026-DBY-K7M9Q2R-83" />
<el-input v-model.trim="form.certificateNo" placeholder="例如 PX2026-DBY-K7M9Q2R-83">
<template #prefix><el-icon><Postcard /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item v-if="queryMode === 'certificate'" label="姓名">
<el-input v-model.trim="form.name" placeholder="请输入证书对应姓名" />
<el-input v-model.trim="form.name" placeholder="请输入证书对应姓名">
<template #prefix><el-icon><User /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item v-if="queryMode === 'certificate'" label="图形验证码">
<div class="captcha-row">
<el-input v-model.trim="form.captcha" placeholder="请输入验证码" />
<button class="captcha-button" type="button" @click="loadCaptcha">
<el-input v-model.trim="form.captcha" placeholder="请输入验证码">
<template #prefix><el-icon><Key /></el-icon></template>
</el-input>
<button class="captcha-button" type="button" title="点击刷新验证码" @click="loadCaptcha">
<img v-if="captchaImage" :src="captchaImage" alt="验证码" />
<span v-else>刷新</span>
<span v-else><el-icon><RefreshRight /></el-icon>刷新</span>
</button>
</div>
</el-form-item>
<el-form-item v-if="queryMode === 'sms'" label="手机号">
<el-input v-model.trim="form.phone" placeholder="请输入证书对应手机号" />
<el-input v-model.trim="form.phone" placeholder="请输入证书对应手机号">
<template #prefix><el-icon><Phone /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item v-if="queryMode === 'sms'" label="图形验证码">
<div class="captcha-row">
<el-input v-model.trim="form.smsCaptcha" placeholder="请输入验证码" />
<button class="captcha-button" type="button" @click="loadSmsCaptcha">
<el-input v-model.trim="form.smsCaptcha" placeholder="请输入验证码">
<template #prefix><el-icon><Key /></el-icon></template>
</el-input>
<button class="captcha-button" type="button" title="点击刷新验证码" @click="loadSmsCaptcha">
<img v-if="smsCaptchaImage" :src="smsCaptchaImage" alt="验证码" />
<span v-else>刷新</span>
<span v-else><el-icon><RefreshRight /></el-icon>刷新</span>
</button>
</div>
</el-form-item>
<el-form-item v-if="queryMode === 'sms'" label="短信验证码">
<div class="sms-row">
<el-input v-model.trim="form.smsCode" placeholder="请输入短信验证码" />
<el-input v-model.trim="form.smsCode" placeholder="请输入短信验证码">
<template #prefix><el-icon><Message /></el-icon></template>
</el-input>
<el-button
type="primary"
:disabled="smsCooldown > 0 || !form.phone"
@@ -50,10 +78,11 @@
</el-button>
</div>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="submitQuery">查询</el-button>
<el-button type="primary" class="submit" :icon="Search" :loading="loading" @click="submitQuery">查询证书</el-button>
</el-form>
<el-alert v-if="message" class="notice" :title="message" :type="messageType" :closable="false" />
</section>
<section v-if="results.length" class="results">
<div class="results-head">
@@ -89,14 +118,13 @@
<dt>发证日期</dt>
<dd>{{ item.issue_date }}</dd>
</div>
<div>
</div>
</dl>
<div class="card-actions">
<el-button @click="preview(item)">预览</el-button>
<el-button :icon="View" @click="preview(item)">预览</el-button>
<el-button
type="primary"
:icon="Download"
:disabled="!item.can_download_pdf"
:loading="downloadingCertificateNo === item.certificate_no"
@click="downloadPdf(item)"
@@ -107,11 +135,13 @@
</article>
</section>
<p class="disclaimer">
本证书仅用于证明学员完成相关培训课程/阶段学习不代表国家学历学位职业资格或职业技能等级认证
</p>
</section>
<footer class="public-footer">
<p>本证书仅用于证明学员完成相关培训课程或阶段学习不代表国家学历学位职业资格或职业技能等级认证</p>
<span>慧遇书院电子证书服务</span>
</footer>
<el-dialog v-model="previewVisible" title="证书预览" width="720px" @closed="clearPreviewUrl">
<div v-if="previewPdfUrl" class="pdf-frame">
<embed :src="previewPdfUrl" type="application/pdf" />
@@ -148,12 +178,26 @@
</template>
<script setup lang="ts">
import { Loading } from "@element-plus/icons-vue";
import {
CircleCheck,
Download,
Key,
Loading,
Lock,
Message,
Phone,
Postcard,
RefreshRight,
Search,
User,
View,
} from "@element-plus/icons-vue";
import { ElLoading, ElMessage } from "element-plus";
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { http, type Captcha, type PublicCertificate } from "../api";
import { apiErrorMessage, saveBlob } from "../download";
import PublicServiceHeader from "../components/PublicServiceHeader.vue";
interface SearchResponse {
items: PublicCertificate[];
@@ -364,34 +408,37 @@ onUnmounted(() => {
</script>
<style scoped>
.public-page {
.public-query-page {
min-height: 100vh;
display: grid;
place-items: start center;
background: linear-gradient(180deg, #f7fafb 0%, #eef4f6 100%);
padding: 42px 20px;
background: #f4f6f7;
}
.query-panel {
width: min(820px, 100%);
background: #ffffff;
border: 1px solid #dbe6ea;
border-radius: 14px;
padding: 32px;
box-shadow: 0 18px 48px rgba(42, 64, 78, 0.09);
.query-main {
width: min(880px, calc(100% - 40px));
margin: 0 auto;
padding: 62px 0 48px;
}
.heading {
margin-bottom: 22px;
.query-intro {
margin-bottom: 30px;
text-align: center;
}
.heading p {
margin: 0 0 6px;
color: #16817e;
font-size: 13px;
.eyebrow {
margin: 0 0 10px;
color: #9a7637;
font-family: Georgia, "Times New Roman", serif;
font-size: 12px;
font-weight: 700;
}
h1,
h2,
h3,
.query-intro p {
letter-spacing: 0;
}
h1,
h2,
h3 {
@@ -399,9 +446,91 @@ h3 {
}
h1 {
font-size: 30px;
color: #172033;
letter-spacing: 0;
color: #17212b;
font-size: 38px;
font-weight: 720;
}
.intro-copy {
margin: 12px 0 0;
color: #677681;
font-size: 15px;
}
.trust-row {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 18px;
color: #5e6d77;
font-size: 13px;
}
.trust-row span {
display: inline-flex;
align-items: center;
gap: 7px;
}
.trust-row .el-icon {
color: #27765f;
}
.query-card {
padding: 28px 30px 30px;
border: 1px solid #dce3e6;
border-top: 3px solid #b28a43;
border-radius: 7px;
background: #ffffff;
box-shadow: 0 14px 36px rgba(31, 47, 57, 0.08);
}
.query-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid #edf1f2;
}
.query-card-head h2 {
font-size: 20px;
}
.query-card-head p {
margin: 6px 0 0;
color: #75818a;
font-size: 13px;
}
.official-mark {
display: inline-flex;
align-items: center;
gap: 6px;
color: #87672f;
font-size: 12px;
font-weight: 650;
}
.official-mark img {
width: 18px;
height: 16px;
object-fit: contain;
}
.query-mode {
width: 100%;
}
:deep(.query-mode .el-segmented__group) {
width: 100%;
}
:deep(.query-mode .el-segmented__item) {
flex: 1;
min-height: 38px;
}
.captcha-row {
@@ -419,7 +548,7 @@ h1 {
}
.captcha-button {
height: 42px;
height: 40px;
padding: 0;
border: 1px solid #dbe6ea;
border-radius: 8px;
@@ -434,9 +563,19 @@ h1 {
display: block;
}
.captcha-button span {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: #5f6f79;
}
.submit {
width: 100%;
height: 38px;
height: 44px;
margin-top: 4px;
font-weight: 700;
}
@@ -452,6 +591,10 @@ h1 {
margin-bottom: 12px;
}
.results-head h2 {
font-size: 21px;
}
.results-head p {
margin: 4px 0 0;
color: #64748b;
@@ -459,12 +602,12 @@ h1 {
}
.certificate-card {
border: 1px solid #dbe6ea;
border-radius: 10px;
padding: 20px;
margin-bottom: 14px;
background: linear-gradient(180deg, #ffffff, #f8fbfc);
box-shadow: 0 8px 22px rgba(42, 64, 78, 0.05);
border: 1px solid #dce3e6;
border-radius: 7px;
padding: 22px;
margin-bottom: 12px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(31, 47, 57, 0.05);
}
.card-top {
@@ -534,27 +677,18 @@ h1 {
gap: 10px;
}
.disclaimer {
margin: 20px 0 0;
color: #6b7280;
font-size: 13px;
line-height: 1.6;
padding-top: 16px;
border-top: 1px solid #e8eef1;
}
.preview-card {
border: 1px solid #dfe9ec;
border-radius: 10px;
border-radius: 7px;
padding: 32px;
text-align: center;
background: linear-gradient(180deg, #ffffff, #f8fbfc);
background: #f8fafb;
}
.pdf-frame {
height: 70vh;
border: 1px solid #dfe9ec;
border-radius: 10px;
border-radius: 7px;
overflow: hidden;
background: #f8fafc;
}
@@ -606,9 +740,48 @@ h1 {
font-size: 13px;
}
.public-footer {
width: min(880px, calc(100% - 40px));
margin: 0 auto;
padding: 24px 0 32px;
border-top: 1px solid #dfe5e7;
color: #75818a;
font-size: 12px;
line-height: 1.7;
text-align: center;
}
.public-footer p {
margin: 0;
}
.public-footer span {
display: block;
margin-top: 8px;
color: #9a7637;
}
@media (max-width: 640px) {
.query-panel {
padding: 22px;
.query-main {
width: calc(100% - 28px);
padding: 38px 0 34px;
}
h1 {
font-size: 30px;
}
.trust-row {
display: grid;
gap: 8px;
}
.query-card {
padding: 22px 18px;
}
.query-card-head {
display: grid;
}
.card-top,
@@ -620,5 +793,15 @@ h1 {
.meta {
grid-template-columns: 1fr;
}
.captcha-row,
.sms-row {
grid-template-columns: minmax(0, 1fr) 126px;
}
.card-actions .el-button {
width: 100%;
margin: 0;
}
}
</style>

View File

@@ -1,11 +1,18 @@
<template>
<main class="public-page">
<PublicServiceHeader />
<section class="certificate-page">
<p class="kicker">电子证书</p>
<h1>证书公开展示</h1>
<header class="page-title">
<p class="kicker">DIGITAL CERTIFICATE</p>
<h1>电子证书</h1>
<p>查看证书签发信息并下载正式 PDF 文件</p>
</header>
<el-alert v-if="message" class="notice" :title="message" :type="messageType" :closable="false" />
<article v-if="certificate" class="certificate-card">
<span :class="['status-icon', certificate.status === 'valid' ? 'ok' : 'bad']">
<el-icon><CircleCheck v-if="certificate.status === 'valid'" /><Warning v-else /></el-icon>
</span>
<span :class="['status', certificate.status === 'valid' ? 'ok' : 'bad']">{{ statusText(certificate.status) }}</span>
<h2>{{ certificate.certificate_name }}</h2>
<p class="name">{{ certificate.learner_name }}</p>
@@ -25,24 +32,25 @@
<dt>发证日期</dt>
<dd>{{ certificate.issue_date }}</dd>
</div>
<div>
</div>
</dl>
<el-button v-if="certificate.can_download_pdf" type="primary" class="download" :loading="downloading" @click="downloadPdf">
<el-button v-if="certificate.can_download_pdf" type="primary" :icon="Download" class="download" :loading="downloading" @click="downloadPdf">
下载 PDF 证书
</el-button>
</article>
</section>
<footer class="page-foot">慧遇书院电子证书服务</footer>
</main>
</template>
<script setup lang="ts">
import { CircleCheck, Download, Warning } from "@element-plus/icons-vue";
import { ElLoading, ElMessage } from "element-plus";
import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { http, type PublicCertificate } from "../api";
import { apiErrorMessage, saveBlob } from "../download";
import PublicServiceHeader from "../components/PublicServiceHeader.vue";
const route = useRoute();
const token = String(route.params.token);
@@ -98,18 +106,17 @@ onMounted(loadCertificate);
<style scoped>
.public-page {
min-height: 100vh;
background: #f7fafc;
padding: 36px 20px;
background: #f4f6f7;
}
.certificate-page {
max-width: 760px;
max-width: 820px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #e1ebef;
border-radius: 12px;
padding: 30px;
box-shadow: 0 16px 46px rgba(76, 102, 118, 0.08);
padding: 54px 20px 36px;
}
.page-title {
text-align: center;
}
.kicker {
@@ -125,7 +132,13 @@ h2 {
}
h1 {
font-size: 28px;
font-size: 34px;
}
.page-title > p:last-child {
margin: 9px 0 0;
color: #6c7982;
font-size: 14px;
}
.notice {
@@ -133,12 +146,34 @@ h1 {
}
.certificate-card {
margin-top: 20px;
border: 1px solid #dfe9ec;
border-radius: 10px;
padding: 34px;
margin-top: 22px;
border: 1px solid #dce3e6;
border-top: 3px solid #b28a43;
border-radius: 7px;
padding: 38px;
text-align: center;
background: #fbfefe;
background: #ffffff;
box-shadow: 0 14px 36px rgba(31, 47, 57, 0.08);
}
.status-icon {
width: 46px;
height: 46px;
display: grid;
place-items: center;
margin: 0 auto 10px;
border-radius: 50%;
font-size: 25px;
}
.status-icon.ok {
color: #176f69;
background: #e8f3f1;
}
.status-icon.bad {
color: #a6312c;
background: #fae9e8;
}
.status {
@@ -176,11 +211,24 @@ h1 {
.meta {
display: grid;
gap: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
margin: 0;
border: 1px solid #e3e8ea;
border-radius: 6px;
color: #64748b;
}
.meta div {
min-width: 0;
padding: 14px;
border-right: 1px solid #e3e8ea;
}
.meta div:nth-child(3) {
border-right: 0;
}
.meta dt {
font-size: 12px;
}
@@ -192,5 +240,36 @@ h1 {
.download {
margin-top: 24px;
min-width: 180px;
}
.page-foot {
padding: 22px 20px 30px;
color: #8a949b;
font-size: 12px;
text-align: center;
}
@media (max-width: 640px) {
.certificate-page {
padding-top: 36px;
}
.certificate-card {
padding: 28px 18px;
}
.meta {
grid-template-columns: 1fr;
}
.meta div {
border-right: 0;
border-bottom: 1px solid #e3e8ea;
}
.meta div:last-child {
border-bottom: 0;
}
}
</style>

View File

@@ -1,11 +1,15 @@
<template>
<main class="public-page">
<PublicServiceHeader />
<section class="verify-page">
<h1>证书核验</h1>
<header class="verify-title">
<p>VERIFICATION RESULT</p>
<h1>证书核验结果</h1>
</header>
<el-alert :title="message" :type="messageType" :closable="false" />
<el-descriptions v-if="certificate" :column="1" border class="details">
<el-descriptions-item label="核验结果">{{ certificate.status === "valid" ? "该证书真实有效" : "该证书已作废" }}</el-descriptions-item>
<el-descriptions-item label="证书状态">{{ certificate.status }}</el-descriptions-item>
<el-descriptions-item label="证书状态">{{ certificate.status === "valid" ? "有效" : "已作废" }}</el-descriptions-item>
<el-descriptions-item label="证书编号">{{ certificate.certificate_no }}</el-descriptions-item>
<el-descriptions-item label="学员姓名">{{ certificate.learner_name }}</el-descriptions-item>
<el-descriptions-item label="证书名称">{{ certificate.certificate_name }}</el-descriptions-item>
@@ -13,6 +17,7 @@
<el-descriptions-item label="发证日期">{{ certificate.issue_date }}</el-descriptions-item>
</el-descriptions>
</section>
<footer class="page-foot">慧遇书院电子证书服务</footer>
</main>
</template>
@@ -21,6 +26,7 @@ import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { http, type PublicCertificate } from "../api";
import PublicServiceHeader from "../components/PublicServiceHeader.vue";
const route = useRoute();
const token = String(route.params.token);
@@ -52,25 +58,53 @@ onMounted(loadCertificate);
<style scoped>
.public-page {
min-height: 100vh;
background: #f7f8fb;
padding: 32px;
background: #f4f6f7;
}
.verify-page {
max-width: 680px;
max-width: 760px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 28px;
padding: 54px 20px 36px;
}
h1 {
margin: 0 0 20px;
font-size: 24px;
.verify-title {
margin-bottom: 22px;
text-align: center;
}
.verify-title p {
margin: 0 0 8px;
color: #9a7637;
font-family: Georgia, "Times New Roman", serif;
font-size: 12px;
font-weight: 700;
}
.verify-title h1 {
margin: 0;
font-size: 32px;
}
.verify-page > .el-alert,
.details {
background: #ffffff;
}
.verify-page > .el-alert {
border: 1px solid #dce3e6;
}
.details {
margin-top: 20px;
margin-top: 14px;
border-radius: 7px;
overflow: hidden;
box-shadow: 0 10px 28px rgba(31, 47, 57, 0.06);
}
.page-foot {
padding: 22px 20px 30px;
color: #8a949b;
font-size: 12px;
text-align: center;
}
</style>