初次切换前端架构
This commit is contained in:
@@ -11,6 +11,8 @@ tests
|
|||||||
data/*
|
data/*
|
||||||
!data/.gitkeep
|
!data/.gitkeep
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
compose*.yml
|
compose*.yml
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ data/*.sqlite-wal
|
|||||||
**/obj/
|
**/obj/
|
||||||
.vs/
|
.vs/
|
||||||
artifacts/
|
artifacts/
|
||||||
|
**/node_modules/
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
|
FROM node:24-bookworm-slim AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /src/src/Eis.Web/ClientApp
|
||||||
|
COPY src/Eis.Web/ClientApp/package.json src/Eis.Web/ClientApp/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY src/Eis.Web/ClientApp/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-bookworm-slim AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0-bookworm-slim AS build
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
COPY --from=frontend-build /src/src/Eis.Web/wwwroot/vue-app ./src/Eis.Web/wwwroot/vue-app
|
||||||
RUN dotnet restore src/Eis.Web/Eis.Web.csproj \
|
RUN dotnet restore src/Eis.Web/Eis.Web.csproj \
|
||||||
&& dotnet restore src/Eis.Tools/Eis.Tools.csproj \
|
&& dotnet restore src/Eis.Tools/Eis.Tools.csproj \
|
||||||
&& dotnet publish src/Eis.Web/Eis.Web.csproj --configuration Release --output /app/web --no-restore \
|
&& dotnet publish src/Eis.Web/Eis.Web.csproj --configuration Release --output /app/web --no-restore \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 衡准 · 考试信息管理系统
|
# 衡准 · 考试信息管理系统
|
||||||
|
|
||||||
一个完整可运行的分级权限考试信息管理系统,后端使用 ASP.NET Core 10;本地开发采用 SQLite,生产环境支持 MySQL 8.4。
|
一个完整可运行的分级权限考试信息管理系统,前端使用 Vue 3 + Vite,后端使用 ASP.NET Core 10;本地开发采用 SQLite,生产环境支持 MySQL 8.4。
|
||||||
|
|
||||||
## 已实现功能
|
## 已实现功能
|
||||||
|
|
||||||
@@ -85,15 +85,28 @@
|
|||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
需要 .NET 10 SDK。
|
需要 .NET 10 SDK、Node.js 20.19+(推荐 Node.js 24)和 npm。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
Set-Location .\src\Eis.Web\ClientApp
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
Set-Location ..\..\..
|
||||||
dotnet restore .\Eis.slnx
|
dotnet restore .\Eis.slnx
|
||||||
dotnet run --project .\src\Eis.Web\Eis.Web.csproj
|
dotnet run --project .\src\Eis.Web\Eis.Web.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
打开 <http://127.0.0.1:4173>。
|
打开 <http://127.0.0.1:4173>。
|
||||||
|
|
||||||
|
日常开发主页时,可以让 ASP.NET Core 继续监听 `4173`,另开一个终端启动 Vite:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location .\src\Eis.Web\ClientApp
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
然后打开 <http://127.0.0.1:5173>。Vite 会把 API、旧业务模块、CKEditor 和基础样式代理到 ASP.NET Core。当前采用渐进式迁移:Vue 已接管应用入口与公开首页;考生中心、管理后台、招生学校端及其他公开子页继续通过兼容入口按需加载,业务行为保持不变。生产构建输出到 `src/Eis.Web/wwwroot/vue-app`,不得手工修改该目录中的文件。
|
||||||
|
|
||||||
账户可在“账户安全”中启用 TOTP 二次验证。生产环境必须设置至少 32 个字符的 `TOTP_ENCRYPTION_KEY`;该值用于加密 TOTP 密钥并保护恢复码哈希,部署后必须稳定保存,不能随意更换。本地开发未设置时会使用仅适合开发的稳定派生值。
|
账户可在“账户安全”中启用 TOTP 二次验证。生产环境必须设置至少 32 个字符的 `TOTP_ENCRYPTION_KEY`;该值用于加密 TOTP 密钥并保护恢复码哈希,部署后必须稳定保存,不能随意更换。本地开发未设置时会使用仅适合开发的稳定派生值。
|
||||||
|
|
||||||
生产环境还必须单独设置至少 32 个字符的 `DOCUMENT_VERIFICATION_SECRET`。系统用它为成绩单和录取通知书生成 HMAC 防伪查询码;更换该值会使此前下载文书的查询码失效,因此应独立生成、稳定保存且不得与 TOTP 密钥共用。
|
生产环境还必须单独设置至少 32 个字符的 `DOCUMENT_VERIFICATION_SECRET`。系统用它为成绩单和录取通知书生成 HMAC 防伪查询码;更换该值会使此前下载文书的查询码失效,因此应独立生成、稳定保存且不得与 TOTP 密钥共用。
|
||||||
@@ -373,7 +386,10 @@ src/Eis.Application 应用服务契约
|
|||||||
src/Eis.Infrastructure SQLite/MySQL、认证、缓存、Excel 与业务实现
|
src/Eis.Infrastructure SQLite/MySQL、认证、缓存、Excel 与业务实现
|
||||||
src/Eis.Tools 可独立发布的数据库初始化、重建和演示数据工具
|
src/Eis.Tools 可独立发布的数据库初始化、重建和演示数据工具
|
||||||
src/Eis.Web ASP.NET Core 宿主、端点和静态资源
|
src/Eis.Web ASP.NET Core 宿主、端点和静态资源
|
||||||
src/Eis.Web/wwwroot 浏览器入口、样式、前端模块及 CKEditor
|
src/Eis.Web/ClientApp Vue 3 单文件组件、Vite 配置与前端源码
|
||||||
|
src/Eis.Web/wwwroot/vue-app Vue 生产构建产物
|
||||||
|
src/Eis.Web/wwwroot/js 渐进迁移期间保留的业务模块
|
||||||
|
src/Eis.Web/wwwroot 浏览器入口、基础样式及 CKEditor
|
||||||
tests/Eis.Infrastructure.Tests xUnit 自动化测试
|
tests/Eis.Infrastructure.Tests xUnit 自动化测试
|
||||||
|
|
||||||
scripts/publish.ps1 Web 与数据库工具统一发布脚本
|
scripts/publish.ps1 Web 与数据库工具统一发布脚本
|
||||||
|
|||||||
@@ -12,6 +12,27 @@ $publishRoot = [IO.Path]::GetFullPath(
|
|||||||
else {
|
else {
|
||||||
Join-Path $repositoryRoot $OutputDirectory
|
Join-Path $repositoryRoot $OutputDirectory
|
||||||
}))
|
}))
|
||||||
|
$clientRoot = Join-Path $repositoryRoot 'src\Eis.Web\ClientApp'
|
||||||
|
|
||||||
|
Push-Location $clientRoot
|
||||||
|
try {
|
||||||
|
$npmInstallArgs = @('ci')
|
||||||
|
& npm @npmInstallArgs
|
||||||
|
$npmInstallExitCode = $LASTEXITCODE
|
||||||
|
if ($npmInstallExitCode -ne 0) {
|
||||||
|
throw "npm ci 失败,退出码:$npmInstallExitCode"
|
||||||
|
}
|
||||||
|
|
||||||
|
$npmBuildArgs = @('run', 'build')
|
||||||
|
& npm @npmBuildArgs
|
||||||
|
$npmBuildExitCode = $LASTEXITCODE
|
||||||
|
if ($npmBuildExitCode -ne 0) {
|
||||||
|
throw "Vue 生产构建失败,退出码:$npmBuildExitCode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
$projects = @(
|
$projects = @(
|
||||||
@{
|
@{
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0d2d54" />
|
||||||
|
<meta name="description" content="衡准考试信息管理系统——考试通知、考生报名、准考证与成绩查询一站式服务。" />
|
||||||
|
<title>衡准 · 考试信息管理系统</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<div id="modalRoot"></div>
|
||||||
|
<div class="toast" id="toast" role="status" aria-live="polite">
|
||||||
|
<span class="toast-icon">✓</span>
|
||||||
|
<div><strong>操作成功</strong><small>更改已保存</small></div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+1123
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "eis-web-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "3.5.40"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "6.0.8",
|
||||||
|
"vite": "8.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import HomePage from '@/components/HomePage.vue';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref('');
|
||||||
|
const publicData = ref({ organization: {}, notices: [], exams: [], stats: {} });
|
||||||
|
const session = ref({ user: null, profile: null, permissions: [] });
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const [home, currentSession] = await Promise.all([
|
||||||
|
api('/api/public/home'),
|
||||||
|
api('/api/auth/me').catch(requestError => {
|
||||||
|
if (requestError.status === 401) return { user: null, profile: null, permissions: [] };
|
||||||
|
throw requestError;
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
publicData.value = home;
|
||||||
|
session.value = currentSession;
|
||||||
|
} catch (requestError) {
|
||||||
|
error.value = requestError.message || '首页服务暂时不可用,请稍后重试。';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await api('/api/auth/logout', { method: 'POST' });
|
||||||
|
session.value = { user: null, profile: null, permissions: [] };
|
||||||
|
await load();
|
||||||
|
} catch (requestError) {
|
||||||
|
error.value = requestError.message || '退出未完成,请稍后重试。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="loading" class="hz-loading" role="status" aria-live="polite">
|
||||||
|
<span class="hz-loading__seal" aria-hidden="true">衡</span>
|
||||||
|
<strong>正在接入考试公共服务</strong>
|
||||||
|
<small>请稍候</small>
|
||||||
|
</div>
|
||||||
|
<main v-else-if="error" class="hz-failure">
|
||||||
|
<span>服务提示</span>
|
||||||
|
<h1>首页暂时无法加载</h1>
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
<button type="button" @click="load">重新加载</button>
|
||||||
|
</main>
|
||||||
|
<HomePage
|
||||||
|
v-else
|
||||||
|
:public-data="publicData"
|
||||||
|
:session="session"
|
||||||
|
@logout="logout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { dateRange, formatDate, registrationLabel, roleHome } from '@/lib/format';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
publicData: { type: Object, required: true },
|
||||||
|
session: { type: Object, required: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['logout']);
|
||||||
|
const mobileOpen = ref(false);
|
||||||
|
|
||||||
|
const notices = computed(() => props.publicData.notices || []);
|
||||||
|
const exams = computed(() => props.publicData.exams || []);
|
||||||
|
const stats = computed(() => props.publicData.stats || {});
|
||||||
|
const organization = computed(() => props.publicData.organization || {});
|
||||||
|
const siteCopy = computed(() => props.publicData.siteCopy || {});
|
||||||
|
const user = computed(() => props.session.user || null);
|
||||||
|
const featuredExam = computed(() => exams.value.find(exam => exam.registrationState === 'open') || exams.value[0] || null);
|
||||||
|
const topNotice = computed(() => notices.value[0] || null);
|
||||||
|
|
||||||
|
const portalLabel = computed(() => {
|
||||||
|
if (!user.value) return '登录';
|
||||||
|
if (user.value.role === 'candidate') return '进入考生中心';
|
||||||
|
if (user.value.role === 'admission_school') return '进入招生学校端';
|
||||||
|
return '进入管理后台';
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryRoute = computed(() => {
|
||||||
|
if (user.value) return roleHome(user.value);
|
||||||
|
return props.publicData.selfRegistrationEnabled ? 'register' : 'login';
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryLabel = computed(() => {
|
||||||
|
if (user.value) return portalLabel.value;
|
||||||
|
return props.publicData.selfRegistrationEnabled ? '申请固定报名号' : '使用报名号登录';
|
||||||
|
});
|
||||||
|
|
||||||
|
function go(route) {
|
||||||
|
mobileOpen.value = false;
|
||||||
|
location.hash = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToSection(id) {
|
||||||
|
mobileOpen.value = false;
|
||||||
|
document.querySelector(`#${id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExam() {
|
||||||
|
go(user.value?.role === 'candidate' ? 'candidate/exams' : 'login');
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(value) {
|
||||||
|
return new Intl.NumberFormat('zh-CN').format(Number(value || 0));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="hz-site">
|
||||||
|
<a class="hz-skip" href="#hz-main">跳到主要内容</a>
|
||||||
|
|
||||||
|
<div class="hz-service-bar">
|
||||||
|
<div class="hz-container hz-service-bar__inner">
|
||||||
|
<p><span aria-hidden="true"></span>考试信息公共服务平台</p>
|
||||||
|
<div>
|
||||||
|
<span v-if="organization.phone">咨询电话:{{ organization.phone }}</span>
|
||||||
|
<button type="button" @click="go('verify')">文书防伪查询</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="hz-header">
|
||||||
|
<div class="hz-container hz-header__inner">
|
||||||
|
<button class="hz-brand" type="button" aria-label="返回首页" @click="go('home')">
|
||||||
|
<span class="hz-brand__seal" aria-hidden="true">衡</span>
|
||||||
|
<span class="hz-brand__copy">
|
||||||
|
<strong>衡准考试服务</strong>
|
||||||
|
<small>EXAMINATION INFORMATION SERVICE</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav :class="['hz-nav', { 'is-open': mobileOpen }]" aria-label="主要导航">
|
||||||
|
<button type="button" class="is-current" @click="go('home')">首页</button>
|
||||||
|
<button type="button" @click="scrollToSection('hz-exams')">考试报名</button>
|
||||||
|
<button type="button" @click="go('notices')">通知公告</button>
|
||||||
|
<button type="button" @click="go('verify')">防伪查询</button>
|
||||||
|
<button type="button" @click="scrollToSection('hz-guide')">办事指南</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="hz-header__actions">
|
||||||
|
<button class="hz-account-link" type="button" @click="go(roleHome(user))">{{ portalLabel }}</button>
|
||||||
|
<button v-if="user" class="hz-exit-link" type="button" @click="emit('logout')">退出</button>
|
||||||
|
<button
|
||||||
|
class="hz-menu"
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="mobileOpen"
|
||||||
|
aria-label="打开导航"
|
||||||
|
@click="mobileOpen = !mobileOpen"
|
||||||
|
>
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="hz-main">
|
||||||
|
<section class="hz-hero">
|
||||||
|
<div class="hz-container">
|
||||||
|
<button v-if="topNotice" class="hz-latest" type="button" @click="go(`notice/${topNotice.id}`)">
|
||||||
|
<span>最新发布</span>
|
||||||
|
<strong>{{ topNotice.title }}</strong>
|
||||||
|
<time>{{ formatDate(topNotice.publishAt) }}</time>
|
||||||
|
<i aria-hidden="true">→</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="hz-hero__grid">
|
||||||
|
<div class="hz-hero__content">
|
||||||
|
<p class="hz-kicker">统一入口 · 规范办理 · 全程留痕</p>
|
||||||
|
<h1>
|
||||||
|
{{ siteCopy.heroTitle || '让每一次考试办理,' }}
|
||||||
|
<em>{{ siteCopy.heroHighlight || '都有清晰、可信的依据。' }}</em>
|
||||||
|
</h1>
|
||||||
|
<p class="hz-hero__lead">
|
||||||
|
{{ siteCopy.heroDescription || '面向考生、学校和考试管理机构,统一提供报名、准考证、成绩、录取与公开信息服务。' }}
|
||||||
|
</p>
|
||||||
|
<div class="hz-hero__actions">
|
||||||
|
<button class="hz-button hz-button--primary" type="button" @click="go(primaryRoute)">
|
||||||
|
{{ primaryLabel }}<span aria-hidden="true">→</span>
|
||||||
|
</button>
|
||||||
|
<button class="hz-button hz-button--secondary" type="button" @click="scrollToSection('hz-exams')">
|
||||||
|
查看已发布考试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<dl class="hz-trust-list">
|
||||||
|
<div><dt>账户原则</dt><dd>一个报名号长期使用</dd></div>
|
||||||
|
<div><dt>信息原则</dt><dd>以平台正式发布为准</dd></div>
|
||||||
|
<div><dt>安全原则</dt><dd>重要文书支持在线核验</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside v-if="featuredExam" class="hz-exam-docket" aria-label="重点考试">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span>重点考试</span>
|
||||||
|
<small>{{ featuredExam.code }}</small>
|
||||||
|
</div>
|
||||||
|
<em :class="`is-${featuredExam.registrationState}`">{{ registrationLabel(featuredExam.registrationState) }}</em>
|
||||||
|
</header>
|
||||||
|
<div class="hz-exam-docket__body">
|
||||||
|
<p>EXAMINATION NOTICE</p>
|
||||||
|
<h2>{{ featuredExam.name }}</h2>
|
||||||
|
<dl>
|
||||||
|
<div><dt>报名时间</dt><dd>{{ dateRange(featuredExam.registrationStart, featuredExam.registrationEnd) }}</dd></div>
|
||||||
|
<div><dt>考试时间</dt><dd>{{ dateRange(featuredExam.examStart, featuredExam.examEnd) }}</dd></div>
|
||||||
|
<div><dt>考试地点</dt><dd>{{ featuredExam.location || '以准考证公布为准' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
<div class="hz-subjects">
|
||||||
|
<span v-for="subject in featuredExam.subjects?.slice(0, 5)" :key="subject.id || subject.name">{{ subject.name }}</span>
|
||||||
|
<span v-if="featuredExam.subjects?.length > 5">+{{ featuredExam.subjects.length - 5 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<p><strong>{{ count(featuredExam.registrationCount) }}</strong><span>人已报名</span></p>
|
||||||
|
<button type="button" @click="openExam">{{ featuredExam.registrationState === 'open' ? '办理报名' : '查看考试' }} →</button>
|
||||||
|
</footer>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside v-else class="hz-exam-docket hz-exam-docket--empty">
|
||||||
|
<span>考试发布栏</span>
|
||||||
|
<h2>当前暂无已发布考试</h2>
|
||||||
|
<p>新考试发布后,将在此展示报名时间、考试安排和办理入口。</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="hz-entry-section" aria-labelledby="hz-entry-title">
|
||||||
|
<div class="hz-container">
|
||||||
|
<div class="hz-section-heading hz-section-heading--compact">
|
||||||
|
<div><p>ONLINE SERVICES</p><h2 id="hz-entry-title">常用服务</h2></div>
|
||||||
|
<span>按事项进入,减少查找和重复填写</span>
|
||||||
|
</div>
|
||||||
|
<div class="hz-service-grid">
|
||||||
|
<button type="button" @click="go(primaryRoute)">
|
||||||
|
<span class="hz-service-grid__index">01</span>
|
||||||
|
<strong>{{ user ? '个人业务中心' : '报名号登录' }}</strong>
|
||||||
|
<small>{{ user ? '继续办理当前账户下的考试事项' : '使用固定报名号进入考生服务' }}</small>
|
||||||
|
<i>进入服务 →</i>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="scrollToSection('hz-exams')">
|
||||||
|
<span class="hz-service-grid__index">02</span>
|
||||||
|
<strong>考试报名</strong>
|
||||||
|
<small>查看开放考试、报名日期与科目安排</small>
|
||||||
|
<i>查看考试 →</i>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="go(user?.role === 'candidate' ? 'candidate/results' : 'login')">
|
||||||
|
<span class="hz-service-grid__index">03</span>
|
||||||
|
<strong>成绩与准考证</strong>
|
||||||
|
<small>下载准考证,查询已正式发布的成绩</small>
|
||||||
|
<i>办理查询 →</i>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="go('verify')">
|
||||||
|
<span class="hz-service-grid__index">04</span>
|
||||||
|
<strong>文书防伪核验</strong>
|
||||||
|
<small>核对成绩单、录取通知书签发记录</small>
|
||||||
|
<i>立即核验 →</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="hz-public-records">
|
||||||
|
<div class="hz-container hz-records-grid">
|
||||||
|
<div class="hz-notices">
|
||||||
|
<div class="hz-section-heading">
|
||||||
|
<div><p>PUBLIC INFORMATION</p><h2>通知公告</h2></div>
|
||||||
|
<button type="button" @click="go('notices')">查看全部 →</button>
|
||||||
|
</div>
|
||||||
|
<article v-if="topNotice" class="hz-featured-notice">
|
||||||
|
<div>
|
||||||
|
<span>{{ topNotice.category || '通知公告' }}</span>
|
||||||
|
<time>{{ formatDate(topNotice.publishAt) }}</time>
|
||||||
|
</div>
|
||||||
|
<h3>{{ topNotice.title }}</h3>
|
||||||
|
<p>{{ topNotice.summary || '请进入公告正文查看完整内容和办理要求。' }}</p>
|
||||||
|
<button type="button" @click="go(`notice/${topNotice.id}`)">阅读全文 <span>→</span></button>
|
||||||
|
</article>
|
||||||
|
<div v-else class="hz-empty">当前暂无通知公告</div>
|
||||||
|
<div class="hz-notice-list">
|
||||||
|
<button v-for="notice in notices.slice(1, 5)" :key="notice.id" type="button" @click="go(`notice/${notice.id}`)">
|
||||||
|
<time><strong>{{ String(new Date(notice.publishAt).getDate()).padStart(2, '0') }}</strong><span>{{ new Date(notice.publishAt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit' }).replace('/', '.') }}</span></time>
|
||||||
|
<span><em>{{ notice.category || '通知' }}</em><strong>{{ notice.title }}</strong></span>
|
||||||
|
<i>→</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="hz-operation-board" aria-label="平台运行概况">
|
||||||
|
<header><span>服务概况</span><small>数据随业务实时更新</small></header>
|
||||||
|
<dl>
|
||||||
|
<div><dt>在册考生</dt><dd>{{ count(stats.candidates) }}<small>人</small></dd></div>
|
||||||
|
<div><dt>累计报名记录</dt><dd>{{ count(stats.registrations) }}<small>条</small></dd></div>
|
||||||
|
<div><dt>当前开放考试</dt><dd>{{ count(stats.exams) }}<small>项</small></dd></div>
|
||||||
|
</dl>
|
||||||
|
<section>
|
||||||
|
<strong>公开信息说明</strong>
|
||||||
|
<p>考试安排、录取公示及其他重要事项,以平台通知公告栏目正式发布内容为准。</p>
|
||||||
|
<button type="button" @click="go('notices')">进入公开信息目录</button>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="hz-exams" class="hz-exams">
|
||||||
|
<div class="hz-container">
|
||||||
|
<div class="hz-section-heading">
|
||||||
|
<div><p>EXAMINATIONS</p><h2>已发布考试</h2></div>
|
||||||
|
<span>登录后按考试要求选择科目并提交报名</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="exams.length" class="hz-exam-grid">
|
||||||
|
<article v-for="exam in exams" :key="exam.id" class="hz-exam-card">
|
||||||
|
<header>
|
||||||
|
<span>{{ exam.code }}</span>
|
||||||
|
<em :class="`is-${exam.registrationState}`">{{ registrationLabel(exam.registrationState) }}</em>
|
||||||
|
</header>
|
||||||
|
<h3>{{ exam.name }}</h3>
|
||||||
|
<p>{{ exam.description || '考试具体安排与报名要求请以正式通知为准。' }}</p>
|
||||||
|
<dl>
|
||||||
|
<div><dt>报名日期</dt><dd>{{ dateRange(exam.registrationStart, exam.registrationEnd) }}</dd></div>
|
||||||
|
<div><dt>考试日期</dt><dd>{{ dateRange(exam.examStart, exam.examEnd) }}</dd></div>
|
||||||
|
<div><dt>科目</dt><dd>{{ exam.subjects?.length || 0 }} 科 · 总分 {{ exam.totalScore || 0 }} 分</dd></div>
|
||||||
|
</dl>
|
||||||
|
<footer>
|
||||||
|
<span>{{ count(exam.registrationCount) }} 人已报名</span>
|
||||||
|
<button type="button" @click="openExam">{{ exam.registrationState === 'open' ? '选择科目报名' : '查看考试' }} →</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hz-empty hz-empty--large">当前没有已发布的考试,请留意通知公告。</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="hz-guide" class="hz-guide">
|
||||||
|
<div class="hz-container">
|
||||||
|
<div class="hz-guide__intro">
|
||||||
|
<p>办事指南</p>
|
||||||
|
<h2>一个报名号,贯穿完整考试服务</h2>
|
||||||
|
<span>报名号是考生的长期账户。每场考试新增报名记录,不重复创建个人账户。</span>
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<li><span>1</span><div><strong>领取报名号</strong><p>由学校创建,或在开放自主注册时在线申请。</p></div></li>
|
||||||
|
<li><span>2</span><div><strong>完善个人资料</strong><p>首次登录修改密码,并按要求提交真实资料。</p></div></li>
|
||||||
|
<li><span>3</span><div><strong>选择考试科目</strong><p>资料审核通过后,在开放期内完成报名。</p></div></li>
|
||||||
|
<li><span>4</span><div><strong>办理后续事项</strong><p>使用同一报名号下载准考证、查分和查看录取。</p></div></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="hz-footer">
|
||||||
|
<div class="hz-container hz-footer__main">
|
||||||
|
<div class="hz-footer__brand">
|
||||||
|
<span class="hz-brand__seal" aria-hidden="true">衡</span>
|
||||||
|
<div><strong>衡准考试服务</strong><small>规范 · 准确 · 可追溯</small></div>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
<div><dt>主管单位</dt><dd>{{ organization.name || '考试信息管理机构' }}</dd></div>
|
||||||
|
<div v-if="organization.phone"><dt>咨询电话</dt><dd>{{ organization.phone }}</dd></div>
|
||||||
|
<div v-if="organization.address"><dt>联系地址</dt><dd>{{ organization.address }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
<div class="hz-footer__links">
|
||||||
|
<button type="button" @click="go('notices')">通知公告</button>
|
||||||
|
<button type="button" @click="go('verify')">文书核验</button>
|
||||||
|
<button type="button" @click="go('login')">服务登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hz-footer__legal">
|
||||||
|
<div class="hz-container">
|
||||||
|
<span>{{ siteCopy.footerNotice || '公开信息以本平台正式发布内容为准' }}</span>
|
||||||
|
<span>请勿在非官方页面提交密码或验证码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const pendingReads = new Map();
|
||||||
|
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const binaryBody = options.body instanceof ArrayBuffer || options.body instanceof Blob || options.body instanceof FormData;
|
||||||
|
const response = await fetch(path, {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
...(options.body && !binaryBody ? { 'Content-Type': 'application/json' } : {}),
|
||||||
|
...options.headers
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
body: options.body && typeof options.body !== 'string' && !binaryBody
|
||||||
|
? JSON.stringify(options.body)
|
||||||
|
: options.body
|
||||||
|
});
|
||||||
|
|
||||||
|
const type = response.headers.get('content-type') || '';
|
||||||
|
const data = type.includes('application/json') ? await response.json() : await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(data?.message || '操作未完成,请稍后重试');
|
||||||
|
error.status = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function api(path, options = {}) {
|
||||||
|
const method = String(options.method || 'GET').toUpperCase();
|
||||||
|
if (method !== 'GET' || options.body != null || options.signal) return request(path, options);
|
||||||
|
|
||||||
|
const key = String(path);
|
||||||
|
if (pendingReads.has(key)) return pendingReads.get(key);
|
||||||
|
const loading = request(path, options).finally(() => {
|
||||||
|
if (pendingReads.get(key) === loading) pendingReads.delete(key);
|
||||||
|
});
|
||||||
|
pendingReads.set(key, loading);
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export function formatDate(value, withTime = false) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
...(withTime ? { hour: '2-digit', minute: '2-digit', hour12: false } : {})
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateRange(start, end) {
|
||||||
|
if (!start && !end) return '时间待发布';
|
||||||
|
const startDate = start ? new Date(start) : null;
|
||||||
|
const endDate = end ? new Date(end) : null;
|
||||||
|
if (!startDate || Number.isNaN(startDate.getTime())) return formatDate(end);
|
||||||
|
if (!endDate || Number.isNaN(endDate.getTime())) return formatDate(start);
|
||||||
|
|
||||||
|
const day = value => new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit'
|
||||||
|
}).format(value);
|
||||||
|
return `${day(startDate)} — ${day(endDate)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registrationLabel(state) {
|
||||||
|
return {
|
||||||
|
open: '报名开放',
|
||||||
|
upcoming: '即将开放',
|
||||||
|
closed: '报名结束'
|
||||||
|
}[state] || '已发布';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roleHome(user) {
|
||||||
|
if (!user) return 'login';
|
||||||
|
if (user.role === 'candidate') return 'candidate/dashboard';
|
||||||
|
if (user.role === 'admission_school') return 'admission_school/dashboard';
|
||||||
|
return 'admin/dashboard';
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import './styles/home.css';
|
||||||
|
|
||||||
|
const root = document.querySelector('#app');
|
||||||
|
let vueApp = null;
|
||||||
|
let legacyLoading = null;
|
||||||
|
|
||||||
|
window.__EIS_VUE_SHELL__ = true;
|
||||||
|
|
||||||
|
function currentRoute() {
|
||||||
|
return location.hash.slice(1) || 'home';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVueRoute(route) {
|
||||||
|
return route === 'home';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLegacyBoot() {
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="hz-loading" role="status" aria-live="polite">
|
||||||
|
<span class="hz-loading__seal" aria-hidden="true">衡</span>
|
||||||
|
<strong>正在进入业务中心</strong>
|
||||||
|
<small>请稍候</small>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLegacy() {
|
||||||
|
if (legacyLoading) return legacyLoading;
|
||||||
|
if (vueApp) {
|
||||||
|
vueApp.unmount();
|
||||||
|
vueApp = null;
|
||||||
|
}
|
||||||
|
showLegacyBoot();
|
||||||
|
legacyLoading = import(/* @vite-ignore */ '/js/app.js?v=20260723-vue').catch(error => {
|
||||||
|
console.error('Legacy application failed to load', error);
|
||||||
|
root.innerHTML = '<main class="hz-failure"><span>服务提示</span><h1>业务中心暂时无法加载</h1><p>请刷新页面后重试。</p><button type="button" onclick="location.reload()">刷新页面</button></main>';
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return legacyLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountVue() {
|
||||||
|
if (vueApp) return;
|
||||||
|
root.innerHTML = '';
|
||||||
|
vueApp = createApp(App);
|
||||||
|
vueApp.mount(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEntry() {
|
||||||
|
if (isVueRoute(currentRoute())) mountVue();
|
||||||
|
else loadLegacy();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
const route = currentRoute();
|
||||||
|
if (isVueRoute(route) && legacyLoading) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isVueRoute(route) && !legacyLoading) loadLegacy();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderEntry();
|
||||||
@@ -0,0 +1,712 @@
|
|||||||
|
:root {
|
||||||
|
--hz-navy-950: #071c35;
|
||||||
|
--hz-navy-900: #0d2d54;
|
||||||
|
--hz-navy-800: #113b6c;
|
||||||
|
--hz-blue-700: #17558f;
|
||||||
|
--hz-blue-100: #e8f0f8;
|
||||||
|
--hz-red-700: #9f3038;
|
||||||
|
--hz-red-100: #f8e9e9;
|
||||||
|
--hz-ink: #182536;
|
||||||
|
--hz-muted: #5d6c7d;
|
||||||
|
--hz-line: #d5dee8;
|
||||||
|
--hz-paper: #f4f7fa;
|
||||||
|
--hz-white: #ffffff;
|
||||||
|
--hz-title: "Noto Serif SC", "Source Han Serif SC", "Songti SC", STSong, SimSun, serif;
|
||||||
|
--hz-body: "Inter", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||||
|
color: var(--hz-ink);
|
||||||
|
font-family: var(--hz-body);
|
||||||
|
font-synthesis: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
background: var(--hz-white);
|
||||||
|
color: var(--hz-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-site,
|
||||||
|
.hz-site * { box-sizing: border-box; }
|
||||||
|
|
||||||
|
.hz-site {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--hz-white);
|
||||||
|
color: var(--hz-ink);
|
||||||
|
font-family: var(--hz-body);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-site button,
|
||||||
|
.hz-failure button {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-site button { cursor: pointer; }
|
||||||
|
|
||||||
|
.hz-site button:focus-visible,
|
||||||
|
.hz-site a:focus-visible,
|
||||||
|
.hz-failure button:focus-visible {
|
||||||
|
outline: 3px solid #e2a918;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-container {
|
||||||
|
width: min(1180px, calc(100% - 48px));
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-skip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--hz-white);
|
||||||
|
color: var(--hz-navy-900);
|
||||||
|
font-weight: 700;
|
||||||
|
transform: translateY(-160%);
|
||||||
|
transition: transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-skip:focus { transform: translateY(0); }
|
||||||
|
|
||||||
|
.hz-service-bar {
|
||||||
|
min-height: 38px;
|
||||||
|
background: var(--hz-navy-950);
|
||||||
|
color: #dbe7f4;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-service-bar__inner {
|
||||||
|
min-height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-service-bar p,
|
||||||
|
.hz-service-bar div { display: flex; align-items: center; gap: 18px; margin: 0; }
|
||||||
|
|
||||||
|
.hz-service-bar p span {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #5ba8e5;
|
||||||
|
box-shadow: 0 0 0 4px rgba(91, 168, 229, .14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-service-bar button {
|
||||||
|
padding: 0 0 0 18px;
|
||||||
|
border-left: 1px solid rgba(255,255,255,.2);
|
||||||
|
background: transparent;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-header {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 30;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 1px solid var(--hz-line);
|
||||||
|
background: rgba(255,255,255,.97);
|
||||||
|
box-shadow: 0 6px 22px rgba(9, 34, 62, .06);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-header__inner {
|
||||||
|
min-height: 78px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--hz-navy-900);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-brand__seal {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
flex: 0 0 44px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid var(--hz-red-700);
|
||||||
|
background: var(--hz-red-700);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--hz-title);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(255,255,255,.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-brand__copy { display: flex; flex-direction: column; line-height: 1.1; }
|
||||||
|
.hz-brand__copy strong { font-family: var(--hz-title); font-size: 20px; letter-spacing: .08em; }
|
||||||
|
.hz-brand__copy small { margin-top: 7px; color: #63758b; font-size: 8px; font-weight: 700; letter-spacing: .12em; }
|
||||||
|
|
||||||
|
.hz-nav {
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-nav button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 17px;
|
||||||
|
background: transparent;
|
||||||
|
color: #34465c;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-nav button::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 17px;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 17px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--hz-red-700);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition: transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-nav button:hover,
|
||||||
|
.hz-nav button.is-current { color: var(--hz-navy-900); }
|
||||||
|
.hz-nav button:hover::after,
|
||||||
|
.hz-nav button.is-current::after { transform: scaleX(1); }
|
||||||
|
|
||||||
|
.hz-header__actions { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
|
.hz-account-link {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid var(--hz-navy-800) !important;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--hz-navy-800);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-account-link:hover { background: var(--hz-navy-950); }
|
||||||
|
.hz-exit-link { padding: 9px 4px; background: transparent; color: var(--hz-muted); font-size: 13px !important; }
|
||||||
|
|
||||||
|
.hz-menu {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-menu span { width: 22px; height: 2px; background: var(--hz-navy-900); }
|
||||||
|
|
||||||
|
.hz-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--hz-line);
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(17,59,108,.045) 1px, transparent 1px) 0 0 / 62px 62px,
|
||||||
|
linear-gradient(rgba(17,59,108,.045) 1px, transparent 1px) 0 0 / 62px 62px,
|
||||||
|
linear-gradient(118deg, #f8fafc 0%, #f3f7fb 63%, #edf3f8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-hero::before {
|
||||||
|
content: "准";
|
||||||
|
position: absolute;
|
||||||
|
right: max(-30px, calc((100vw - 1340px) / 2));
|
||||||
|
bottom: -160px;
|
||||||
|
color: rgba(13,45,84,.035);
|
||||||
|
font-family: var(--hz-title);
|
||||||
|
font-size: 530px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-latest {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 46px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 22px;
|
||||||
|
padding: 0 18px 0 0;
|
||||||
|
border: 1px solid #cfd9e5 !important;
|
||||||
|
border-left: 0 !important;
|
||||||
|
background: rgba(255,255,255,.78);
|
||||||
|
color: var(--hz-ink);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-latest > span {
|
||||||
|
align-self: stretch;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: var(--hz-red-700);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-latest strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.hz-latest time { color: var(--hz-muted); font-size: 12px; }
|
||||||
|
.hz-latest i { color: var(--hz-red-700); font-style: normal; }
|
||||||
|
|
||||||
|
.hz-hero__grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.18fr) minmax(380px, .82fr);
|
||||||
|
gap: 74px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 74px 0 82px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-kicker,
|
||||||
|
.hz-section-heading p,
|
||||||
|
.hz-guide__intro > p {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
color: var(--hz-blue-700);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-kicker::before {
|
||||||
|
content: "";
|
||||||
|
width: 28px;
|
||||||
|
height: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: var(--hz-red-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-hero__content h1 {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--hz-navy-950);
|
||||||
|
font-family: var(--hz-title);
|
||||||
|
font-size: clamp(42px, 4.6vw, 67px);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.035em;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-hero__content h1 em { display: block; color: var(--hz-navy-800); font-style: normal; }
|
||||||
|
|
||||||
|
.hz-hero__lead {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 26px 0 0;
|
||||||
|
color: #52657a;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-hero__actions { display: flex; gap: 12px; margin-top: 34px; }
|
||||||
|
|
||||||
|
.hz-button {
|
||||||
|
min-height: 50px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 26px;
|
||||||
|
padding: 0 22px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 750 !important;
|
||||||
|
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-button:hover { transform: translateY(-2px); }
|
||||||
|
.hz-button--primary { background: var(--hz-navy-800); color: #fff; box-shadow: 0 12px 25px rgba(17,59,108,.18); }
|
||||||
|
.hz-button--primary:hover { background: var(--hz-navy-950); box-shadow: 0 15px 30px rgba(17,59,108,.25); }
|
||||||
|
.hz-button--secondary { border: 1px solid #b9c7d6 !important; background: rgba(255,255,255,.5); color: var(--hz-navy-900); }
|
||||||
|
.hz-button--secondary:hover { border-color: var(--hz-navy-800) !important; background: #fff; }
|
||||||
|
|
||||||
|
.hz-trust-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 28px;
|
||||||
|
margin: 34px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-trust-list div { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.hz-trust-list dt { color: var(--hz-red-700); font-size: 11px; font-weight: 800; }
|
||||||
|
.hz-trust-list dd { margin: 0; color: #53677c; font-size: 12px; }
|
||||||
|
|
||||||
|
.hz-exam-docket {
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #bdcad8;
|
||||||
|
box-shadow: 0 28px 65px rgba(14,45,78,.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-exam-docket::before,
|
||||||
|
.hz-exam-docket::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 17px;
|
||||||
|
height: 34px;
|
||||||
|
background: #eff4f8;
|
||||||
|
border: 1px solid #bdcad8;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-exam-docket::before { left: -1px; border-left: 0; border-radius: 0 24px 24px 0; }
|
||||||
|
.hz-exam-docket::after { right: -1px; border-right: 0; border-radius: 24px 0 0 24px; }
|
||||||
|
|
||||||
|
.hz-exam-docket > header {
|
||||||
|
min-height: 59px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-bottom: 1px dashed #c4cfdb;
|
||||||
|
background: #f9fbfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-exam-docket > header div { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.hz-exam-docket > header span { color: var(--hz-navy-900); font-weight: 800; }
|
||||||
|
.hz-exam-docket > header small { color: var(--hz-muted); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }
|
||||||
|
.hz-exam-docket em,
|
||||||
|
.hz-exam-card em {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--hz-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-exam-docket em::before,
|
||||||
|
.hz-exam-card em::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: #8392a2; }
|
||||||
|
.hz-exam-docket em.is-open,
|
||||||
|
.hz-exam-card em.is-open { color: #17673d; }
|
||||||
|
.hz-exam-docket em.is-open::before,
|
||||||
|
.hz-exam-card em.is-open::before { background: #259557; box-shadow: 0 0 0 4px #e3f4e9; }
|
||||||
|
.hz-exam-docket em.is-upcoming,
|
||||||
|
.hz-exam-card em.is-upcoming { color: #8a5714; }
|
||||||
|
.hz-exam-docket em.is-upcoming::before,
|
||||||
|
.hz-exam-card em.is-upcoming::before { background: #d99625; }
|
||||||
|
|
||||||
|
.hz-exam-docket__body { padding: 29px 30px 31px; }
|
||||||
|
.hz-exam-docket__body > p { margin: 0 0 6px; color: #8290a0; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 9px; letter-spacing: .18em; }
|
||||||
|
.hz-exam-docket__body h2 { margin: 0 0 26px; color: var(--hz-navy-950); font-family: var(--hz-title); font-size: 25px; line-height: 1.45; }
|
||||||
|
.hz-exam-docket__body dl { margin: 0; }
|
||||||
|
.hz-exam-docket__body dl div { display: grid; grid-template-columns: 72px 1fr; gap: 14px; padding: 9px 0; border-top: 1px solid #e6ebf0; }
|
||||||
|
.hz-exam-docket__body dt { color: #738296; font-size: 12px; }
|
||||||
|
.hz-exam-docket__body dd { margin: 0; color: #28384c; font-size: 12px; font-weight: 650; }
|
||||||
|
|
||||||
|
.hz-subjects { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 18px; }
|
||||||
|
.hz-subjects span { padding: 4px 8px; border: 1px solid #d4dde6; background: #f7f9fb; color: #506174; font-size: 11px; }
|
||||||
|
|
||||||
|
.hz-exam-docket > footer {
|
||||||
|
min-height: 73px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0 30px;
|
||||||
|
border-top: 1px dashed #c4cfdb;
|
||||||
|
background: var(--hz-navy-900);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-exam-docket > footer p { display: flex; align-items: baseline; gap: 7px; margin: 0; }
|
||||||
|
.hz-exam-docket > footer strong { font-family: var(--hz-title); font-size: 25px; }
|
||||||
|
.hz-exam-docket > footer span { color: #b8cce0; font-size: 11px; }
|
||||||
|
.hz-exam-docket > footer button { padding: 8px 0 8px 20px; background: transparent; color: #fff; font-size: 13px; font-weight: 700; }
|
||||||
|
.hz-exam-docket--empty { min-height: 330px; display: flex; justify-content: center; flex-direction: column; padding: 40px; }
|
||||||
|
.hz-exam-docket--empty::before,
|
||||||
|
.hz-exam-docket--empty::after { display: none; }
|
||||||
|
.hz-exam-docket--empty > span { color: var(--hz-red-700); font-size: 12px; font-weight: 800; }
|
||||||
|
.hz-exam-docket--empty h2 { margin: 12px 0; font-family: var(--hz-title); }
|
||||||
|
.hz-exam-docket--empty p { color: var(--hz-muted); }
|
||||||
|
|
||||||
|
.hz-entry-section { padding: 62px 0 72px; background: #fff; }
|
||||||
|
|
||||||
|
.hz-section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-section-heading p { margin-bottom: 7px; }
|
||||||
|
.hz-section-heading h2 { margin: 0; color: var(--hz-navy-950); font-family: var(--hz-title); font-size: 30px; letter-spacing: .01em; }
|
||||||
|
.hz-section-heading > span { max-width: 410px; color: var(--hz-muted); font-size: 13px; text-align: right; }
|
||||||
|
.hz-section-heading > button { padding: 8px 0; background: transparent; color: var(--hz-blue-700); font-size: 13px; font-weight: 750; }
|
||||||
|
.hz-section-heading--compact { margin-bottom: 20px; }
|
||||||
|
|
||||||
|
.hz-service-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
border: 1px solid var(--hz-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-service-grid > button {
|
||||||
|
min-height: 190px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 26px 22px;
|
||||||
|
border-right: 1px solid var(--hz-line);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--hz-ink);
|
||||||
|
text-align: left;
|
||||||
|
transition: background 180ms ease, transform 180ms ease, box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-service-grid > button:last-child { border-right: 0; }
|
||||||
|
.hz-service-grid > button:hover { z-index: 1; background: var(--hz-navy-900); color: #fff; box-shadow: 0 16px 35px rgba(13,45,84,.19); transform: translateY(-5px); }
|
||||||
|
.hz-service-grid__index { color: var(--hz-red-700); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; font-weight: 800; }
|
||||||
|
.hz-service-grid strong { margin-top: 20px; font-family: var(--hz-title); font-size: 18px; }
|
||||||
|
.hz-service-grid small { margin-top: 8px; color: var(--hz-muted); font-size: 12px; line-height: 1.7; }
|
||||||
|
.hz-service-grid i { margin-top: auto; color: var(--hz-blue-700); font-size: 12px; font-style: normal; font-weight: 750; }
|
||||||
|
.hz-service-grid > button:hover small { color: #c3d2e1; }
|
||||||
|
.hz-service-grid > button:hover i,
|
||||||
|
.hz-service-grid > button:hover .hz-service-grid__index { color: #fff; }
|
||||||
|
|
||||||
|
.hz-public-records { padding: 78px 0; background: var(--hz-paper); border-block: 1px solid var(--hz-line); }
|
||||||
|
.hz-records-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(290px, .5fr); gap: 52px; }
|
||||||
|
|
||||||
|
.hz-featured-notice {
|
||||||
|
padding: 30px 32px;
|
||||||
|
border-top: 3px solid var(--hz-navy-800);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 14px 30px rgba(15,42,70,.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-featured-notice > div { display: flex; justify-content: space-between; gap: 20px; }
|
||||||
|
.hz-featured-notice > div span { color: var(--hz-red-700); font-size: 12px; font-weight: 800; }
|
||||||
|
.hz-featured-notice time { color: var(--hz-muted); font-size: 12px; }
|
||||||
|
.hz-featured-notice h3 { margin: 18px 0 10px; color: var(--hz-navy-950); font-family: var(--hz-title); font-size: 24px; line-height: 1.5; }
|
||||||
|
.hz-featured-notice p { margin: 0; color: var(--hz-muted); font-size: 13px; line-height: 1.8; }
|
||||||
|
.hz-featured-notice > button { display: inline-flex; gap: 16px; margin-top: 22px; padding: 7px 0; background: transparent; color: var(--hz-blue-700); font-size: 12px; font-weight: 750; }
|
||||||
|
|
||||||
|
.hz-notice-list { margin-top: 12px; border-top: 1px solid var(--hz-line); }
|
||||||
|
.hz-notice-list > button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 85px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 62px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
border-bottom: 1px solid var(--hz-line);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--hz-ink);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-notice-list > button:hover { background: #fff; }
|
||||||
|
.hz-notice-list time { display: flex; align-items: center; flex-direction: column; border-right: 1px solid var(--hz-line); line-height: 1.1; }
|
||||||
|
.hz-notice-list time strong { color: var(--hz-navy-900); font-family: var(--hz-title); font-size: 22px; }
|
||||||
|
.hz-notice-list time span { margin-top: 5px; color: var(--hz-muted); font-size: 9px; }
|
||||||
|
.hz-notice-list > button > span { min-width: 0; display: flex; flex-direction: column; }
|
||||||
|
.hz-notice-list em { color: var(--hz-red-700); font-size: 10px; font-style: normal; }
|
||||||
|
.hz-notice-list > button > span strong { overflow: hidden; margin-top: 4px; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.hz-notice-list i { color: var(--hz-blue-700); font-style: normal; }
|
||||||
|
|
||||||
|
.hz-operation-board {
|
||||||
|
align-self: start;
|
||||||
|
border: 1px solid #c5d0dc;
|
||||||
|
background: var(--hz-navy-900);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-operation-board > header { display: flex; justify-content: space-between; padding: 20px 22px; border-bottom: 1px solid rgba(255,255,255,.16); }
|
||||||
|
.hz-operation-board > header span { font-family: var(--hz-title); font-size: 17px; font-weight: 800; }
|
||||||
|
.hz-operation-board > header small { color: #9db2c7; font-size: 10px; }
|
||||||
|
.hz-operation-board > dl { margin: 0; padding: 8px 22px; }
|
||||||
|
.hz-operation-board > dl div { display: flex; align-items: baseline; justify-content: space-between; padding: 18px 0; border-bottom: 1px solid rgba(255,255,255,.13); }
|
||||||
|
.hz-operation-board dt { color: #bdd0e1; font-size: 12px; }
|
||||||
|
.hz-operation-board dd { margin: 0; font-family: var(--hz-title); font-size: 29px; font-weight: 800; }
|
||||||
|
.hz-operation-board dd small { margin-left: 5px; color: #9db2c7; font-family: var(--hz-body); font-size: 10px; font-weight: 500; }
|
||||||
|
.hz-operation-board > section { margin: 14px; padding: 19px; background: rgba(255,255,255,.08); }
|
||||||
|
.hz-operation-board > section strong { font-size: 12px; }
|
||||||
|
.hz-operation-board > section p { margin: 8px 0 15px; color: #bdccdb; font-size: 11px; line-height: 1.8; }
|
||||||
|
.hz-operation-board > section button { padding: 6px 0; background: transparent; color: #fff; font-size: 11px; font-weight: 700; }
|
||||||
|
|
||||||
|
.hz-exams { padding: 82px 0 92px; background: #fff; scroll-margin-top: 78px; }
|
||||||
|
.hz-exam-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
|
||||||
|
.hz-exam-card { min-height: 390px; display: flex; flex-direction: column; padding: 25px; border: 1px solid var(--hz-line); background: #fff; transition: border 180ms ease, box-shadow 180ms ease, transform 180ms ease; }
|
||||||
|
.hz-exam-card:hover { border-color: #9fb2c5; box-shadow: 0 18px 38px rgba(12,44,78,.09); transform: translateY(-4px); }
|
||||||
|
.hz-exam-card > header { display: flex; justify-content: space-between; gap: 14px; }
|
||||||
|
.hz-exam-card > header > span { color: var(--hz-blue-700); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; font-weight: 800; }
|
||||||
|
.hz-exam-card h3 { margin: 25px 0 10px; color: var(--hz-navy-950); font-family: var(--hz-title); font-size: 21px; line-height: 1.5; }
|
||||||
|
.hz-exam-card > p { margin: 0; color: var(--hz-muted); font-size: 12px; line-height: 1.75; }
|
||||||
|
.hz-exam-card dl { margin: 23px 0; }
|
||||||
|
.hz-exam-card dl div { display: grid; grid-template-columns: 64px 1fr; gap: 12px; padding: 8px 0; border-top: 1px solid #e6ebf0; }
|
||||||
|
.hz-exam-card dt { color: #778597; font-size: 11px; }
|
||||||
|
.hz-exam-card dd { margin: 0; color: #34475b; font-size: 11px; font-weight: 650; }
|
||||||
|
.hz-exam-card > footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: auto; padding-top: 17px; border-top: 1px solid var(--hz-line); }
|
||||||
|
.hz-exam-card > footer span { color: var(--hz-muted); font-size: 11px; }
|
||||||
|
.hz-exam-card > footer button { padding: 6px 0; background: transparent; color: var(--hz-blue-700); font-size: 11px; font-weight: 800; }
|
||||||
|
|
||||||
|
.hz-guide { padding: 80px 0; background: var(--hz-navy-950); color: #fff; scroll-margin-top: 78px; }
|
||||||
|
.hz-guide .hz-container { display: grid; grid-template-columns: .72fr 1.28fr; gap: 78px; }
|
||||||
|
.hz-guide__intro > p { color: #7cb2de; }
|
||||||
|
.hz-guide__intro h2 { margin: 0; font-family: var(--hz-title); font-size: 32px; line-height: 1.45; }
|
||||||
|
.hz-guide__intro > span { display: block; margin-top: 18px; color: #a9bed3; font-size: 13px; line-height: 1.9; }
|
||||||
|
.hz-guide ol { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; margin: 0; padding: 0; list-style: none; border-top: 1px solid rgba(255,255,255,.16); border-left: 1px solid rgba(255,255,255,.16); }
|
||||||
|
.hz-guide li { min-height: 145px; display: flex; gap: 17px; padding: 25px; border-right: 1px solid rgba(255,255,255,.16); border-bottom: 1px solid rgba(255,255,255,.16); }
|
||||||
|
.hz-guide li > span { width: 28px; height: 28px; flex: 0 0 28px; display: grid; place-items: center; border: 1px solid #6e91b1; color: #9fc0dc; font-family: var(--hz-title); font-size: 13px; }
|
||||||
|
.hz-guide li strong { font-family: var(--hz-title); font-size: 16px; }
|
||||||
|
.hz-guide li p { margin: 8px 0 0; color: #a9bed3; font-size: 11px; line-height: 1.8; }
|
||||||
|
|
||||||
|
.hz-footer { background: #f0f3f6; border-top: 1px solid var(--hz-line); }
|
||||||
|
.hz-footer__main { min-height: 178px; display: grid; grid-template-columns: .85fr 1.35fr .5fr; align-items: center; gap: 55px; }
|
||||||
|
.hz-footer__brand { display: flex; align-items: center; gap: 14px; }
|
||||||
|
.hz-footer__brand > div { display: flex; flex-direction: column; }
|
||||||
|
.hz-footer__brand strong { color: var(--hz-navy-950); font-family: var(--hz-title); font-size: 18px; }
|
||||||
|
.hz-footer__brand small { margin-top: 6px; color: var(--hz-muted); font-size: 10px; letter-spacing: .14em; }
|
||||||
|
.hz-footer dl { margin: 0; padding-left: 30px; border-left: 1px solid var(--hz-line); }
|
||||||
|
.hz-footer dl div { display: grid; grid-template-columns: 72px 1fr; gap: 15px; padding: 3px 0; }
|
||||||
|
.hz-footer dt { color: var(--hz-muted); font-size: 11px; }
|
||||||
|
.hz-footer dd { margin: 0; color: #304155; font-size: 11px; }
|
||||||
|
.hz-footer__links { display: flex; align-items: flex-start; flex-direction: column; }
|
||||||
|
.hz-footer__links button { padding: 4px 0; background: transparent; color: #405469; font-size: 11px; }
|
||||||
|
.hz-footer__legal { min-height: 47px; display: flex; align-items: center; background: #e4e9ee; color: #657486; font-size: 10px; }
|
||||||
|
.hz-footer__legal .hz-container { display: flex; justify-content: space-between; gap: 24px; }
|
||||||
|
|
||||||
|
.hz-empty { padding: 30px; border: 1px dashed #bdc9d5; color: var(--hz-muted); font-size: 13px; text-align: center; }
|
||||||
|
.hz-empty--large { padding: 70px 30px; }
|
||||||
|
|
||||||
|
.hz-loading,
|
||||||
|
.hz-failure {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f3f6f9;
|
||||||
|
color: var(--hz-navy-900);
|
||||||
|
font-family: var(--hz-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hz-loading__seal { width: 54px; height: 54px; display: grid; place-items: center; margin-bottom: 18px; background: var(--hz-red-700); color: #fff; font-family: var(--hz-title); font-size: 28px; box-shadow: inset 0 0 0 4px rgba(255,255,255,.25); }
|
||||||
|
.hz-loading strong { font-family: var(--hz-title); letter-spacing: .08em; }
|
||||||
|
.hz-loading small { margin-top: 8px; color: var(--hz-muted); }
|
||||||
|
.hz-failure { padding: 30px; text-align: center; }
|
||||||
|
.hz-failure > span { color: var(--hz-red-700); font-size: 12px; font-weight: 800; }
|
||||||
|
.hz-failure h1 { margin: 12px 0 4px; font-family: var(--hz-title); }
|
||||||
|
.hz-failure p { color: var(--hz-muted); }
|
||||||
|
.hz-failure button { margin-top: 16px; padding: 11px 20px; background: var(--hz-navy-800); color: #fff; cursor: pointer; }
|
||||||
|
|
||||||
|
@media (max-width: 1060px) {
|
||||||
|
.hz-header__inner { gap: 16px; }
|
||||||
|
.hz-nav button { padding-inline: 10px; font-size: 13px; }
|
||||||
|
.hz-nav button::after { right: 10px; left: 10px; }
|
||||||
|
.hz-hero__grid { grid-template-columns: 1fr 370px; gap: 36px; }
|
||||||
|
.hz-hero__content h1 { font-size: 46px; }
|
||||||
|
.hz-service-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.hz-service-grid > button:nth-child(2) { border-right: 0; }
|
||||||
|
.hz-service-grid > button:nth-child(-n+2) { border-bottom: 1px solid var(--hz-line); }
|
||||||
|
.hz-records-grid { gap: 28px; }
|
||||||
|
.hz-exam-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.hz-footer__main { grid-template-columns: 1fr 1.4fr; }
|
||||||
|
.hz-footer__links { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.hz-container { width: min(100% - 32px, 700px); }
|
||||||
|
.hz-service-bar__inner > div span { display: none; }
|
||||||
|
.hz-header__inner { min-height: 70px; grid-template-columns: 1fr auto; }
|
||||||
|
.hz-brand__seal { width: 39px; height: 39px; flex-basis: 39px; font-size: 21px; }
|
||||||
|
.hz-brand__copy strong { font-size: 17px; }
|
||||||
|
.hz-brand__copy small { display: none; }
|
||||||
|
.hz-menu { display: flex; }
|
||||||
|
.hz-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
display: none;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 16px 16px;
|
||||||
|
border-bottom: 1px solid var(--hz-line);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 16px 28px rgba(10,35,64,.1);
|
||||||
|
}
|
||||||
|
.hz-nav.is-open { display: flex; }
|
||||||
|
.hz-nav button { min-height: 45px; text-align: left; }
|
||||||
|
.hz-nav button::after { display: none; }
|
||||||
|
.hz-hero__grid { grid-template-columns: 1fr; padding: 55px 0 62px; }
|
||||||
|
.hz-hero__content h1 { max-width: 650px; font-size: clamp(39px, 8vw, 54px); }
|
||||||
|
.hz-exam-docket { max-width: 560px; }
|
||||||
|
.hz-records-grid { grid-template-columns: 1fr; }
|
||||||
|
.hz-operation-board { max-width: none; }
|
||||||
|
.hz-guide .hz-container { grid-template-columns: 1fr; gap: 38px; }
|
||||||
|
.hz-footer__main { grid-template-columns: 1fr; gap: 24px; padding-block: 42px; }
|
||||||
|
.hz-footer dl { padding: 22px 0 0; border-top: 1px solid var(--hz-line); border-left: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 580px) {
|
||||||
|
.hz-container { width: calc(100% - 28px); }
|
||||||
|
.hz-service-bar__inner { justify-content: center; }
|
||||||
|
.hz-service-bar__inner > div { display: none; }
|
||||||
|
.hz-account-link { max-width: 132px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.hz-exit-link { display: none; }
|
||||||
|
.hz-latest { grid-template-columns: auto 1fr auto; gap: 10px; padding-right: 12px; }
|
||||||
|
.hz-latest time { display: none; }
|
||||||
|
.hz-latest > span { padding-inline: 10px; }
|
||||||
|
.hz-hero__grid { padding-top: 46px; }
|
||||||
|
.hz-hero__content h1 { font-size: 38px; line-height: 1.35; }
|
||||||
|
.hz-hero__lead { font-size: 14px; }
|
||||||
|
.hz-hero__actions { align-items: stretch; flex-direction: column; }
|
||||||
|
.hz-trust-list { align-items: flex-start; flex-direction: column; }
|
||||||
|
.hz-exam-docket__body { padding: 25px 22px; }
|
||||||
|
.hz-exam-docket > footer { padding-inline: 22px; }
|
||||||
|
.hz-section-heading { align-items: flex-start; flex-direction: column; }
|
||||||
|
.hz-section-heading > span { text-align: left; }
|
||||||
|
.hz-service-grid { grid-template-columns: 1fr; }
|
||||||
|
.hz-service-grid > button { min-height: 170px; border-right: 0; border-bottom: 1px solid var(--hz-line); }
|
||||||
|
.hz-service-grid > button:last-child { border-bottom: 0; }
|
||||||
|
.hz-records-grid { gap: 38px; }
|
||||||
|
.hz-featured-notice { padding: 25px 22px; }
|
||||||
|
.hz-featured-notice h3 { font-size: 20px; }
|
||||||
|
.hz-notice-list > button { grid-template-columns: 52px 1fr auto; gap: 10px; }
|
||||||
|
.hz-exam-grid { grid-template-columns: 1fr; }
|
||||||
|
.hz-exam-card { min-height: 360px; }
|
||||||
|
.hz-guide ol { grid-template-columns: 1fr; }
|
||||||
|
.hz-guide__intro h2 { font-size: 27px; }
|
||||||
|
.hz-footer__legal .hz-container { flex-direction: column; gap: 2px; padding-block: 10px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://127.0.0.1:4173',
|
||||||
|
'/health': 'http://127.0.0.1:4173',
|
||||||
|
'/js': 'http://127.0.0.1:4173',
|
||||||
|
'/vendor': 'http://127.0.0.1:4173',
|
||||||
|
'/styles.css': 'http://127.0.0.1:4173'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: '../wwwroot/vue-app',
|
||||||
|
emptyOutDir: true,
|
||||||
|
cssCodeSplit: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: fileURLToPath(new URL('./src/main.js', import.meta.url)),
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'app.js',
|
||||||
|
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||||
|
assetFileNames: assetInfo => assetInfo.names?.some(name => name.endsWith('.css')) ? 'app.css' : 'assets/[name]-[hash][extname]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#132451" />
|
<meta name="theme-color" content="#0d2d54" />
|
||||||
<meta name="description" content="衡准考试信息管理系统——考试通知、考生报名、准考证与成绩查询一站式服务。" />
|
<meta name="description" content="衡准考试信息管理系统——考试通知、考生报名、准考证与成绩查询一站式服务。" />
|
||||||
<title>衡准 · 考试信息管理系统</title>
|
<title>衡准 · 考试信息管理系统</title>
|
||||||
<link rel="stylesheet" href="/styles.css?v=20260723" />
|
<link rel="stylesheet" href="/styles.css?v=20260723" />
|
||||||
|
<link rel="stylesheet" href="/vue-app/app.css?v=20260723" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" aria-live="polite">
|
<div id="app" aria-live="polite">
|
||||||
@@ -21,6 +22,6 @@
|
|||||||
<span class="toast-icon">✓</span>
|
<span class="toast-icon">✓</span>
|
||||||
<div><strong>操作成功</strong><small>更改已保存</small></div>
|
<div><strong>操作成功</strong><small>更改已保存</small></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/js/app.js?v=20260723"></script>
|
<script type="module" src="/vue-app/app.js?v=20260723"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -169,7 +169,13 @@ async function renderRoute() {
|
|||||||
const route = location.hash.slice(1) || 'home';
|
const route = location.hash.slice(1) || 'home';
|
||||||
const [section, page = 'dashboard'] = route.split('/');
|
const [section, page = 'dashboard'] = route.split('/');
|
||||||
if (section !== 'login') state.authNotice = '';
|
if (section !== 'login') state.authNotice = '';
|
||||||
if (section === 'home') renderHome();
|
if (section === 'home') {
|
||||||
|
if (window.__EIS_VUE_SHELL__) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderHome();
|
||||||
|
}
|
||||||
else if (section === 'verify') {
|
else if (section === 'verify') {
|
||||||
if (!page || page === 'dashboard') renderVerification();
|
if (!page || page === 'dashboard') renderVerification();
|
||||||
else {
|
else {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user