优化系统界面并接入正式品牌标识
This commit is contained in:
@@ -7,13 +7,13 @@ from app.api.deps import require_roles
|
||||
from app.core.paths import data_path
|
||||
from app.db.session import get_db
|
||||
from app.models import AdminUser, OperationLog
|
||||
from app.schemas.log import OperationLogOut
|
||||
from app.schemas.log import OperationLogPage
|
||||
from app.services.logs import HIGH_RISK_LOG_RETENTION_DAYS, LOG_RETENTION_DAYS, cleanup_expired_logs, format_log
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[OperationLogOut])
|
||||
@router.get("", response_model=OperationLogPage)
|
||||
def list_logs(
|
||||
action: str | None = Query(default=None),
|
||||
object_type: str | None = Query(default=None),
|
||||
|
||||
@@ -19,3 +19,11 @@ class OperationLogOut(BaseModel):
|
||||
summary: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class OperationLogPage(BaseModel):
|
||||
items: list[OperationLogOut]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
@@ -208,7 +208,7 @@ def render_practice_camp_certificate_image(certificate: Certificate, learner: Le
|
||||
(end_month, 1058),
|
||||
]:
|
||||
draw.text(
|
||||
(x * x_scale, 648 * y_scale),
|
||||
(x * x_scale, 658 * y_scale),
|
||||
value,
|
||||
fill="#202020",
|
||||
font=font(41, "song", False, text_scale),
|
||||
@@ -217,7 +217,7 @@ def render_practice_camp_certificate_image(certificate: Certificate, learner: Le
|
||||
|
||||
issue_date_text = f"{issue_year}年{issue_month}月{issue_day}日"
|
||||
issue_date_font = font(36, "song", False, text_scale)
|
||||
issue_date_position = (568 * x_scale, 1140 * y_scale)
|
||||
issue_date_position = (568 * x_scale, 1149 * y_scale)
|
||||
draw.rectangle(
|
||||
(
|
||||
442 * x_scale,
|
||||
@@ -362,7 +362,7 @@ def draw_issue_date_numbers(
|
||||
y_scale: float,
|
||||
) -> None:
|
||||
text_font = font(13, "song", False, scale)
|
||||
y = 682 * y_scale
|
||||
y = 685 * y_scale
|
||||
for value, x in [(issue_year, 500), (issue_month, 535), (issue_day, 566)]:
|
||||
draw.text((x * x_scale, y), value, fill="#111111", font=text_font, anchor="mm")
|
||||
|
||||
|
||||
8
backend/tests/test_admin_logs.py
Normal file
8
backend/tests/test_admin_logs.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from app.api.routes.admin_logs import router
|
||||
from app.schemas.log import OperationLogPage
|
||||
|
||||
|
||||
def test_operation_log_list_uses_paginated_response_model():
|
||||
list_route = next(route for route in router.routes if route.path == "")
|
||||
|
||||
assert list_route.response_model is OperationLogPage
|
||||
@@ -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>
|
||||
|
||||
BIN
frontend/public/brand/huiyu-logo.png
Normal file
BIN
frontend/public/brand/huiyu-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
98
frontend/src/components/PublicServiceHeader.vue
Normal file
98
frontend/src/components/PublicServiceHeader.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +81,7 @@
|
||||
|
||||
<el-card class="panel" shadow="never">
|
||||
<div class="toolbar">
|
||||
<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" />
|
||||
@@ -94,10 +95,13 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
<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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,30 +1,57 @@
|
||||
<template>
|
||||
<main class="admin-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand-row">
|
||||
<div class="brand">
|
||||
<h1>证书管理</h1>
|
||||
<p>培训结业证书后台</p>
|
||||
<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>
|
||||
|
||||
@@ -1,45 +1,73 @@
|
||||
<template>
|
||||
<main class="public-page">
|
||||
<section class="query-panel">
|
||||
<div class="heading">
|
||||
<p>电子证书查询</p>
|
||||
<h1>培训结业证书查询</h1>
|
||||
<main class="public-query-page">
|
||||
<PublicServiceHeader />
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user