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 @@
- - - - - - - - - - - - - - 搜索 - 预生成所选PDF - 预生成当前批次全部 - 导出 +
+ + + + + + + + + + + + + + 搜索 +
+
+ 导出 + 预生成当前批次 + 预生成所选 PDF +
@@ -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 @@ 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 @@ 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 @@