diff --git a/backend/app/api/routes/admin_logs.py b/backend/app/api/routes/admin_logs.py
index e263e03..6a35acc 100644
--- a/backend/app/api/routes/admin_logs.py
+++ b/backend/app/api/routes/admin_logs.py
@@ -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),
diff --git a/backend/app/schemas/log.py b/backend/app/schemas/log.py
index 5862ec8..e4e2ac3 100644
--- a/backend/app/schemas/log.py
+++ b/backend/app/schemas/log.py
@@ -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
diff --git a/backend/app/services/pdf.py b/backend/app/services/pdf.py
index 9546848..eba0799 100644
--- a/backend/app/services/pdf.py
+++ b/backend/app/services/pdf.py
@@ -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")
diff --git a/backend/tests/test_admin_logs.py b/backend/tests/test_admin_logs.py
new file mode 100644
index 0000000..95b097c
--- /dev/null
+++ b/backend/tests/test_admin_logs.py
@@ -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
diff --git a/frontend/index.html b/frontend/index.html
index 5ca5b8f..165c160 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,6 +3,7 @@
+
培训结业证书管理与查询系统
diff --git a/frontend/public/brand/huiyu-logo.png b/frontend/public/brand/huiyu-logo.png
new file mode 100644
index 0000000..6a1d0ba
Binary files /dev/null and b/frontend/public/brand/huiyu-logo.png differ
diff --git a/frontend/src/components/PublicServiceHeader.vue b/frontend/src/components/PublicServiceHeader.vue
new file mode 100644
index 0000000..0ebd6b4
--- /dev/null
+++ b/frontend/src/components/PublicServiceHeader.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/frontend/src/design.css b/frontend/src/design.css
index 3e1c494..bcaade5 100644
--- a/frontend/src/design.css
+++ b/frontend/src/design.css
@@ -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;
+ }
}
diff --git a/frontend/src/views/AdminCertificates.vue b/frontend/src/views/AdminCertificates.vue
index 480dace..1ddc9ca 100644
--- a/frontend/src/views/AdminCertificates.vue
+++ b/frontend/src/views/AdminCertificates.vue
@@ -5,7 +5,7 @@
证书管理
支持单张新增、预览、下载、作废、链接重置和批量预生成PDF。
- 创建证书
+ 创建证书
@@ -81,23 +81,27 @@
@@ -183,6 +187,7 @@
diff --git a/frontend/src/views/AdminLogin.vue b/frontend/src/views/AdminLogin.vue
index 99c3e4c..f81c7fd 100644
--- a/frontend/src/views/AdminLogin.vue
+++ b/frontend/src/views/AdminLogin.vue
@@ -1,15 +1,26 @@
- 后台登录
-
+
+
+
+
+ 请使用管理员账号登录
+
-
+
+
+
-
+
+
+
- 登录
+ 登录后台
@@ -17,6 +28,7 @@
diff --git a/frontend/src/views/CertificateQuery.vue b/frontend/src/views/CertificateQuery.vue
index b0cbc1a..2dec50b 100644
--- a/frontend/src/views/CertificateQuery.vue
+++ b/frontend/src/views/CertificateQuery.vue
@@ -1,45 +1,73 @@
-
-
-
+
+
-
+
+
+ CERTIFICATE VERIFICATION
+ 证书查询与核验
+ 查询慧遇书院已签发的培训结业证书
+
+ 签发记录可追溯
+ 查询信息安全保护
+
+
+
+
+
+
+
查询证书
+
请选择证书编号或短信验证码查询
+
+
官方查询
+
+
+
-
+
-
+
+
+
-
+
+
+
-
-
-
+
+
+
-
-
+
+
+
+
- 刷新
+ 刷新
-
+
+
+
- 查询
+ 查询证书
+
@@ -89,14 +118,13 @@
发证日期
{{ item.issue_date }}
-
-
-
预览
+
预览
-
- 本证书仅用于证明学员完成相关培训课程/阶段学习,不代表国家学历、学位、职业资格或职业技能等级认证。
-
+
+
@@ -148,12 +178,26 @@
diff --git a/frontend/src/views/CertificateView.vue b/frontend/src/views/CertificateView.vue
index a72b55e..e9578e4 100644
--- a/frontend/src/views/CertificateView.vue
+++ b/frontend/src/views/CertificateView.vue
@@ -1,11 +1,18 @@
+
- 电子证书
- 证书公开展示
+
+ DIGITAL CERTIFICATE
+ 电子证书
+ 查看证书签发信息并下载正式 PDF 文件
+
+
+
+
{{ statusText(certificate.status) }}
{{ certificate.certificate_name }}
{{ certificate.learner_name }}
@@ -25,24 +32,25 @@
发证日期
{{ certificate.issue_date }}
-
-
-
+
下载 PDF 证书
+