样式
This commit is contained in:
@@ -10,6 +10,18 @@ INITIAL_ADMIN_USERNAME=admin
|
|||||||
INITIAL_ADMIN_PASSWORD=Admin123!
|
INITIAL_ADMIN_PASSWORD=Admin123!
|
||||||
INITIAL_ADMIN_DISPLAY_NAME=系统管理员
|
INITIAL_ADMIN_DISPLAY_NAME=系统管理员
|
||||||
|
|
||||||
|
# 公开首页文案与联系方式(修改后重启应用生效)
|
||||||
|
PUBLIC_SITE_NAME=海州市教育考试中心
|
||||||
|
PUBLIC_SITE_CODE=HZ-EDU-032
|
||||||
|
PUBLIC_SITE_PHONE=0518-8602 3158
|
||||||
|
PUBLIC_SITE_ADDRESS=江苏省连云港市海州区文教路 18 号
|
||||||
|
PUBLIC_SITE_EMAIL=
|
||||||
|
PUBLIC_SITE_HERO_EYEBROW=HAIZHOU EXAMINATION SERVICE
|
||||||
|
PUBLIC_SITE_HERO_TITLE=一个报名号,
|
||||||
|
PUBLIC_SITE_HERO_HIGHLIGHT=贯穿每一次考试。
|
||||||
|
PUBLIC_SITE_HERO_DESCRIPTION=使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。
|
||||||
|
PUBLIC_SITE_FOOTER_NOTICE=本平台展示数据仅用于系统演示
|
||||||
|
|
||||||
# MySQL 8.4 生产环境:将 DATABASE_CLIENT 改为 mysql,并配置以下变量。
|
# MySQL 8.4 生产环境:将 DATABASE_CLIENT 改为 mysql,并配置以下变量。
|
||||||
# NODE_ENV=production
|
# NODE_ENV=production
|
||||||
# DATABASE_CLIENT=mysql
|
# DATABASE_CLIENT=mysql
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ npm start
|
|||||||
|
|
||||||
应用启动时会自动读取项目根目录的 `.env`,可先运行 `Copy-Item .env.example .env` 创建配置文件。命令行或部署平台已经注入的进程环境变量优先于 `.env`。应用根据 `DATABASE_CLIENT` 使用不同数据库;未设置时,开发/测试环境默认 `sqlite`,`NODE_ENV=production` 默认 `mysql`。
|
应用启动时会自动读取项目根目录的 `.env`,可先运行 `Copy-Item .env.example .env` 创建配置文件。命令行或部署平台已经注入的进程环境变量优先于 `.env`。应用根据 `DATABASE_CLIENT` 使用不同数据库;未设置时,开发/测试环境默认 `sqlite`,`NODE_ENV=production` 默认 `mysql`。
|
||||||
|
|
||||||
|
公开首页的机构名称、机构代码、电话、地址、邮箱、主标语和页脚提示分别由 `PUBLIC_SITE_NAME`、`PUBLIC_SITE_CODE`、`PUBLIC_SITE_PHONE`、`PUBLIC_SITE_ADDRESS`、`PUBLIC_SITE_EMAIL`、`PUBLIC_SITE_HERO_*`、`PUBLIC_SITE_FOOTER_NOTICE` 配置。修改 `.env` 后需要重启应用;这些配置会覆盖数据库中的演示机构信息,且只通过公开首页接口返回非敏感展示字段。
|
||||||
|
|
||||||
### 本地 SQLite
|
### 本地 SQLite
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
+15
@@ -22,6 +22,20 @@ if (existsSync(envPath)) loadEnvFile(envPath);
|
|||||||
|
|
||||||
const port = Number(process.env.PORT || 4173);
|
const port = Number(process.env.PORT || 4173);
|
||||||
const host = process.env.HOST || '127.0.0.1';
|
const host = process.env.HOST || '127.0.0.1';
|
||||||
|
const publicSiteConfig = Object.freeze({
|
||||||
|
organization: {
|
||||||
|
name: process.env.PUBLIC_SITE_NAME || '考试服务平台',
|
||||||
|
code: process.env.PUBLIC_SITE_CODE || 'EXAM-SERVICE',
|
||||||
|
phone: process.env.PUBLIC_SITE_PHONE || '',
|
||||||
|
address: process.env.PUBLIC_SITE_ADDRESS || '',
|
||||||
|
email: process.env.PUBLIC_SITE_EMAIL || ''
|
||||||
|
},
|
||||||
|
heroEyebrow: process.env.PUBLIC_SITE_HERO_EYEBROW || 'EXAMINATION SERVICE',
|
||||||
|
heroTitle: process.env.PUBLIC_SITE_HERO_TITLE || '一个报名号,',
|
||||||
|
heroHighlight: process.env.PUBLIC_SITE_HERO_HIGHLIGHT || '贯穿每一次考试。',
|
||||||
|
heroDescription: process.env.PUBLIC_SITE_HERO_DESCRIPTION || '使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。',
|
||||||
|
footerNotice: process.env.PUBLIC_SITE_FOOTER_NOTICE || ''
|
||||||
|
});
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
const staticFiles = new Set([
|
const staticFiles = new Set([
|
||||||
'/index.html',
|
'/index.html',
|
||||||
@@ -812,6 +826,7 @@ function admitCardsHtml(db, registrations, title) {
|
|||||||
const routeContext = {
|
const routeContext = {
|
||||||
database,
|
database,
|
||||||
readDb,
|
readDb,
|
||||||
|
publicSiteConfig,
|
||||||
sendJson,
|
sendJson,
|
||||||
sendError,
|
sendError,
|
||||||
readJson,
|
readJson,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function createAdminViews(context) {
|
|||||||
|
|
||||||
async function renderAdmin(page) {
|
async function renderAdmin(page) {
|
||||||
if (state.user?.role !== 'admin') return navigate('login');
|
if (state.user?.role !== 'admin') return navigate('login');
|
||||||
|
app.classList.add('admin-readable');
|
||||||
const meta = {
|
const meta = {
|
||||||
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
|
dashboard: ['考务工作台', '掌握当前报名、审核和发布任务。'], candidates: ['考生资料审核', '核验考生实名、学籍与联系信息。'],
|
||||||
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
|
registrations: ['考试报名审核', '确认考生所报考试、科目与缴费状态。'], exams: ['考试与科目', '创建考试、配置报名时间与考试科目。'],
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export function createCandidateViews(context) {
|
|||||||
|
|
||||||
async function renderCandidate(page) {
|
async function renderCandidate(page) {
|
||||||
if (state.user?.role !== 'candidate') return navigate('login');
|
if (state.user?.role !== 'candidate') return navigate('login');
|
||||||
|
app.classList.remove('admin-readable');
|
||||||
if (state.user.mustChangePassword) {
|
if (state.user.mustChangePassword) {
|
||||||
app.innerHTML = onboardingShell('password', passwordOnboardingForm());
|
app.innerHTML = onboardingShell('password', passwordOnboardingForm());
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -24,20 +24,22 @@ export function createPublicViews(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderHome() {
|
function renderHome() {
|
||||||
|
app.classList.remove('admin-readable');
|
||||||
const { notices, exams, stats, organization } = state.publicData;
|
const { notices, exams, stats, organization } = state.publicData;
|
||||||
|
const siteCopy = state.publicData.siteCopy || {};
|
||||||
const featured = exams.find(exam => exam.registrationState === 'open') || exams[0];
|
const featured = exams.find(exam => exam.registrationState === 'open') || exams[0];
|
||||||
const topNotice = notices[0];
|
const topNotice = notices[0];
|
||||||
app.innerHTML = `${publicHeader()}<main class="public-main">
|
app.innerHTML = `${publicHeader()}<main class="public-main">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-grid">
|
<div class="hero-grid">
|
||||||
<div class="hero-copy"><div class="notice-ticker"><span>最新</span><button data-action="open-notice" data-id="${h(topNotice?.id)}">${h(topNotice?.title || '欢迎使用衡准考试服务平台')}</button></div><p class="overline">HAIZHOU EXAMINATION SERVICE</p><h1>一个报名号,<br><em>贯穿每一次考试。</em></h1><p class="hero-lead">使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。</p><div class="hero-actions">${state.user?.role === 'candidate' ? `<button class="solid-button large" data-route="candidate/dashboard">进入考生中心 ${icons.arrow}</button>` : state.publicData.selfRegistrationEnabled ? `<button class="solid-button large" data-route="register">申请报名号 ${icons.arrow}</button>` : `<button class="solid-button large" data-route="login">使用报名号登录 ${icons.arrow}</button>`}<button class="ghost-button large" data-action="scroll-to" data-target="home-exams">查看开放考试</button></div><div class="hero-stats"><div><strong>${h(stats.candidates || 0)}</strong><span>在册考生</span></div><div><strong>${h(stats.registrations || 0)}</strong><span>报名记录</span></div><div><strong>${h(stats.exams || 0)}</strong><span>开放考试</span></div></div></div>
|
<div class="hero-copy"><div class="notice-ticker"><span>最新</span><button data-action="open-notice" data-id="${h(topNotice?.id)}">${h(topNotice?.title || '欢迎使用衡准考试服务平台')}</button></div><p class="overline">${h(siteCopy.heroEyebrow || 'EXAMINATION SERVICE')}</p><h1>${h(siteCopy.heroTitle || '一个报名号,')}<br><em>${h(siteCopy.heroHighlight || '贯穿每一次考试。')}</em></h1><p class="hero-lead">${h(siteCopy.heroDescription || '')}</p><div class="hero-actions">${state.user?.role === 'candidate' ? `<button class="solid-button large" data-route="candidate/dashboard">进入考生中心 ${icons.arrow}</button>` : state.publicData.selfRegistrationEnabled ? `<button class="solid-button large" data-route="register">申请报名号 ${icons.arrow}</button>` : `<button class="solid-button large" data-route="login">使用报名号登录 ${icons.arrow}</button>`}<button class="ghost-button large" data-action="scroll-to" data-target="home-exams">查看开放考试</button></div><div class="hero-stats"><div><strong>${h(stats.candidates || 0)}</strong><span>在册考生</span></div><div><strong>${h(stats.registrations || 0)}</strong><span>报名记录</span></div><div><strong>${h(stats.exams || 0)}</strong><span>开放考试</span></div></div></div>
|
||||||
${featured ? renderHeroTicket(featured) : '<div class="hero-ticket empty-state">暂无开放考试</div>'}
|
${featured ? renderHeroTicket(featured) : '<div class="hero-ticket empty-state">暂无开放考试</div>'}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="content-section" id="home-notices"><div class="section-heading"><div><p class="overline">NOTICE BOARD</p><h2>通知公告</h2></div><p>报名、考试、准考证与成绩发布的重要时间,请以平台通知为准。</p></div><div class="notice-layout"><article class="featured-notice">${topNotice ? `<span>${h(topNotice.category)}</span><h3>${h(topNotice.title)}</h3><p>${h(topNotice.summary)}</p><footer><time>${formatDate(topNotice.publishAt)}</time><button data-action="open-notice" data-id="${h(topNotice.id)}">阅读通知 ${icons.arrow}</button></footer>` : '<p>暂无通知</p>'}</article><div class="notice-list">${notices.slice(1, 5).map(renderNoticeRow).join('') || '<div class="empty-state">暂无更多通知</div>'}</div></div></section>
|
<section class="content-section" id="home-notices"><div class="section-heading"><div><p class="overline">NOTICE BOARD</p><h2>通知公告</h2></div><p>报名、考试、准考证与成绩发布的重要时间,请以平台通知为准。</p></div><div class="notice-layout"><article class="featured-notice">${topNotice ? `<span>${h(topNotice.category)}</span><h3>${h(topNotice.title)}</h3><p>${h(topNotice.summary)}</p><footer><time>${formatDate(topNotice.publishAt)}</time><button data-action="open-notice" data-id="${h(topNotice.id)}">阅读通知 ${icons.arrow}</button></footer>` : '<p>暂无通知</p>'}</article><div class="notice-list">${notices.slice(1, 5).map(renderNoticeRow).join('') || '<div class="empty-state">暂无更多通知</div>'}</div></div></section>
|
||||||
<section class="content-section exam-section" id="home-exams"><div class="section-heading"><div><p class="overline">OPEN EXAMINATIONS</p><h2>考试报名</h2></div><p>登录后选择考试,并按实际需要勾选报考科目。</p></div><div class="public-exam-grid">${exams.map(renderPublicExam).join('') || '<div class="empty-state">当前没有已发布的考试</div>'}</div></section>
|
<section class="content-section exam-section" id="home-exams"><div class="section-heading"><div><p class="overline">OPEN EXAMINATIONS</p><h2>考试报名</h2></div><p>登录后选择考试,并按实际需要勾选报考科目。</p></div><div class="public-exam-grid">${exams.map(renderPublicExam).join('') || '<div class="empty-state">当前没有已发布的考试</div>'}</div></section>
|
||||||
<section class="service-flow" id="service-flow"><div class="section-heading light"><div><p class="overline">SERVICE FLOW</p><h2>报名号是唯一账户</h2></div><p>报名号不会随考试改变,每场考试只新增一条报名记录。</p></div><div class="flow-track">${[['01','领取报名号','学校创建账户并下发初始密码。'],['02','修改初始密码','首次登录必须设置自己的新密码。'],['03','补全个人信息','填写籍贯、住址、手机、邮箱和班级等资料。'],['04','选择考试科目','资料审核通过后自主选择考试。'],['05','下载准考证与查分','继续使用同一报名号办理后续事项。']].map(item => `<article><span>${item[0]}</span><h3>${item[1]}</h3><p>${item[2]}</p></article>`).join('')}</div></section>
|
<section class="service-flow" id="service-flow"><div class="section-heading light"><div><p class="overline">SERVICE FLOW</p><h2>报名号是唯一账户</h2></div><p>报名号不会随考试改变,每场考试只新增一条报名记录。</p></div><div class="flow-track">${[['01','领取报名号','学校创建账户并下发初始密码。'],['02','修改初始密码','首次登录必须设置自己的新密码。'],['03','补全个人信息','填写籍贯、住址、手机、邮箱和班级等资料。'],['04','选择考试科目','资料审核通过后自主选择考试。'],['05','下载准考证与查分','继续使用同一报名号办理后续事项。']].map(item => `<article><span>${item[0]}</span><h3>${item[1]}</h3><p>${item[2]}</p></article>`).join('')}</div></section>
|
||||||
</main><footer class="public-footer"><div>${brand()}<p>${h(organization.name || '海州市教育考试中心')} · ${h(organization.phone || '')}</p></div><span>本平台展示数据仅用于系统演示</span></footer>`;
|
</main><footer class="public-footer"><div>${brand()}<p>${[organization.name, organization.phone].filter(Boolean).map(h).join(' · ')}</p>${organization.address || organization.email ? `<p class="public-contact-detail">${[organization.address, organization.email].filter(Boolean).map(h).join(' · ')}</p>` : ''}</div><span>${h(siteCopy.footerNotice || '')}</span></footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHeroTicket(exam) {
|
function renderHeroTicket(exam) {
|
||||||
@@ -54,6 +56,7 @@ export function createPublicViews(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAuth(kind) {
|
function renderAuth(kind) {
|
||||||
|
app.classList.remove('admin-readable');
|
||||||
const login = kind === 'login';
|
const login = kind === 'login';
|
||||||
const selfRegistration = state.publicData.selfRegistrationEnabled;
|
const selfRegistration = state.publicData.selfRegistrationEnabled;
|
||||||
app.innerHTML = `<main class="auth-page"><section class="auth-story"><div>${brand()}<p class="overline">CANDIDATE SERVICE</p><h1>${login ? '凭一个号码,' : '自主申请,'}<br><em>${login ? '办理每一次考试。' : '领取固定报名号。'}</em></h1><p>报名号就是考生账户,不因考试、科目或年度报名而改变。</p></div><div class="auth-quote"><span>首次登录顺序</span><p>修改初始密码 → 补全个人信息 → 等待资料审核。</p></div></section><section class="auth-panel"><button class="back-link" data-route="home">← 返回首页</button><div class="auth-card"><p class="overline">${login ? 'ACCOUNT LOGIN' : 'CANDIDATE NUMBER'}</p><h2>${login ? '报名号登录' : '自主申请报名号'}</h2><p>${login ? '考生填写报名号和密码;管理员继续使用管理账号。' : selfRegistration ? '提交基础学籍范围后,系统生成一个长期使用的报名号。' : '当前未开放自主注册,请联系学校领取报名号和初始密码。'}</p>${login ? loginForm() : selfRegistration ? registerForm() : '<div class="registration-closed"><strong>自主注册已关闭</strong><span>学校管理员会为考生创建账户并下发初始密码。</span><button class="solid-button" data-route="login">返回报名号登录</button></div>'}${login && selfRegistration ? `<div class="auth-switch">还没有报名号?<button data-route="register">自主申请</button></div>` : !login ? '<div class="auth-switch">已经有报名号?<button data-route="login">返回登录</button></div>' : ''}</div></section></main>`;
|
app.innerHTML = `<main class="auth-page"><section class="auth-story"><div>${brand()}<p class="overline">CANDIDATE SERVICE</p><h1>${login ? '凭一个号码,' : '自主申请,'}<br><em>${login ? '办理每一次考试。' : '领取固定报名号。'}</em></h1><p>报名号就是考生账户,不因考试、科目或年度报名而改变。</p></div><div class="auth-quote"><span>首次登录顺序</span><p>修改初始密码 → 补全个人信息 → 等待资料审核。</p></div></section><section class="auth-panel"><button class="back-link" data-route="home">← 返回首页</button><div class="auth-card"><p class="overline">${login ? 'ACCOUNT LOGIN' : 'CANDIDATE NUMBER'}</p><h2>${login ? '报名号登录' : '自主申请报名号'}</h2><p>${login ? '考生填写报名号和密码;管理员继续使用管理账号。' : selfRegistration ? '提交基础学籍范围后,系统生成一个长期使用的报名号。' : '当前未开放自主注册,请联系学校领取报名号和初始密码。'}</p>${login ? loginForm() : selfRegistration ? registerForm() : '<div class="registration-closed"><strong>自主注册已关闭</strong><span>学校管理员会为考生创建账户并下发初始密码。</span><button class="solid-button" data-route="login">返回报名号登录</button></div>'}${login && selfRegistration ? `<div class="auth-switch">还没有报名号?<button data-route="register">自主申请</button></div>` : !login ? '<div class="auth-switch">已经有报名号?<button data-route="login">返回登录</button></div>' : ''}</div></section></main>`;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export function createPublicRoutes(context) {
|
|||||||
const {
|
const {
|
||||||
database,
|
database,
|
||||||
readDb,
|
readDb,
|
||||||
|
publicSiteConfig,
|
||||||
sendJson,
|
sendJson,
|
||||||
sendError,
|
sendError,
|
||||||
readJson,
|
readJson,
|
||||||
@@ -54,7 +55,7 @@ export function createPublicRoutes(context) {
|
|||||||
if (pathname === '/api/public/home') {
|
if (pathname === '/api/public/home') {
|
||||||
const publishedNotices = db.notices.filter(item => item.status === 'published').sort((a, b) => Number(b.pinned) - Number(a.pinned) || new Date(b.publishAt) - new Date(a.publishAt)).map(noticeForClient);
|
const publishedNotices = db.notices.filter(item => item.status === 'published').sort((a, b) => Number(b.pinned) - Number(a.pinned) || new Date(b.publishAt) - new Date(a.publishAt)).map(noticeForClient);
|
||||||
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
||||||
return sendJson(response, 200, { ok: true, organization: db.organization, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: exams.length, registrations: db.registrations.length } });
|
return sendJson(response, 200, { ok: true, organization: publicSiteConfig.organization, siteCopy: { heroEyebrow: publicSiteConfig.heroEyebrow, heroTitle: publicSiteConfig.heroTitle, heroHighlight: publicSiteConfig.heroHighlight, heroDescription: publicSiteConfig.heroDescription, footerNotice: publicSiteConfig.footerNotice }, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: exams.length, registrations: db.registrations.length } });
|
||||||
}
|
}
|
||||||
const noticeMatch = pathname.match(/^\/api\/public\/notices\/([^/]+)$/);
|
const noticeMatch = pathname.match(/^\/api\/public\/notices\/([^/]+)$/);
|
||||||
if (noticeMatch) {
|
if (noticeMatch) {
|
||||||
|
|||||||
+16
-1
@@ -230,7 +230,7 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
|||||||
.service-flow { padding: 85px max(24px,calc((100% - 1180px)/2)); color: #fff; background: var(--navy); }
|
.service-flow { padding: 85px max(24px,calc((100% - 1180px)/2)); color: #fff; background: var(--navy); }
|
||||||
.section-heading.light .overline { color: #7182b2; }.section-heading.light h2 { color: #fff; }.section-heading.light > p { color: #95a2c5; }
|
.section-heading.light .overline { color: #7182b2; }.section-heading.light h2 { color: #fff; }.section-heading.light > p { color: #95a2c5; }
|
||||||
.flow-track { position: relative; display: grid; grid-template-columns: repeat(5,1fr); gap: 25px; }.flow-track::before { content:""; position:absolute; top:20px; left:20px; right:20px; height:1px; background:rgba(255,255,255,.13); }.flow-track article { position: relative; }.flow-track article span { width: 41px; height: 41px; display: grid; place-items: center; border: 1px solid #5d6e9e; border-radius: 50%; color: #fff; background: var(--navy); font-family: Georgia,serif; font-size: 11px; }.flow-track article:nth-child(3) span { border-color: var(--red); background: var(--red); box-shadow: 0 0 0 7px rgba(200,71,61,.12); }.flow-track h3 { margin: 19px 0 7px; font-size: 12px; }.flow-track p { margin: 0; color: #8e9abe; font-size: 9px; line-height: 1.7; }
|
.flow-track { position: relative; display: grid; grid-template-columns: repeat(5,1fr); gap: 25px; }.flow-track::before { content:""; position:absolute; top:20px; left:20px; right:20px; height:1px; background:rgba(255,255,255,.13); }.flow-track article { position: relative; }.flow-track article span { width: 41px; height: 41px; display: grid; place-items: center; border: 1px solid #5d6e9e; border-radius: 50%; color: #fff; background: var(--navy); font-family: Georgia,serif; font-size: 11px; }.flow-track article:nth-child(3) span { border-color: var(--red); background: var(--red); box-shadow: 0 0 0 7px rgba(200,71,61,.12); }.flow-track h3 { margin: 19px 0 7px; font-size: 12px; }.flow-track p { margin: 0; color: #8e9abe; font-size: 9px; line-height: 1.7; }
|
||||||
.public-footer { display: flex; align-items: flex-end; justify-content: space-between; padding: 38px max(24px,calc((100% - 1180px)/2)); border-top: 1px solid var(--line); background: #fff; }.public-footer > div { display: grid; gap: 13px; }.public-footer p,.public-footer > span { margin:0; color:#858d9e; font-size:9px; }
|
.public-footer { display: flex; align-items: flex-end; justify-content: space-between; padding: 38px max(24px,calc((100% - 1180px)/2)); border-top: 1px solid var(--line); background: #fff; }.public-footer > div { display: grid; gap: 13px; }.public-footer p,.public-footer > span { margin:0; color:#858d9e; font-size:9px; }.public-footer .public-contact-detail { color:#9aa1af; }
|
||||||
|
|
||||||
/* Authentication */
|
/* Authentication */
|
||||||
.auth-page { min-height: 100vh; display: grid; grid-template-columns: .9fr 1.1fr; background: #fff; }
|
.auth-page { min-height: 100vh; display: grid; grid-template-columns: .9fr 1.1fr; background: #fff; }
|
||||||
@@ -529,6 +529,21 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
|||||||
.candidate-archive-console { display:flex; align-items:center; justify-content:space-between; gap:26px; margin-bottom:15px; padding:20px 22px; background:linear-gradient(110deg,#fff,#f3f6fb); }.archive-controls { min-width:420px; display:grid; grid-template-columns:minmax(180px,1fr) auto auto; gap:9px; }.archive-controls select { min-height:40px; padding:8px 10px; border:1px solid var(--line); border-radius:8px; background:#fff; font-size:9px; }.archive-button { background:#9f463f; }.account-archived-row { opacity:.7; background:#f5f6f8; }.candidate-account-actions { display:flex; flex-wrap:wrap; gap:6px; }
|
.candidate-archive-console { display:flex; align-items:center; justify-content:space-between; gap:26px; margin-bottom:15px; padding:20px 22px; background:linear-gradient(110deg,#fff,#f3f6fb); }.archive-controls { min-width:420px; display:grid; grid-template-columns:minmax(180px,1fr) auto auto; gap:9px; }.archive-controls select { min-height:40px; padding:8px 10px; border:1px solid var(--line); border-radius:8px; background:#fff; font-size:9px; }.archive-button { background:#9f463f; }.account-archived-row { opacity:.7; background:#f5f6f8; }.candidate-account-actions { display:flex; flex-wrap:wrap; gap:6px; }
|
||||||
.reset-warning { padding:14px; border-left:3px solid var(--red); border-radius:8px; background:#fceceb; }.reset-warning strong { font-size:9px; }.reset-warning p { margin:5px 0 0; color:#875b58; font-size:8px; line-height:1.7; }
|
.reset-warning { padding:14px; border-left:3px solid var(--red); border-radius:8px; background:#fceceb; }.reset-warning strong { font-size:9px; }.reset-warning p { margin:5px 0 0; color:#875b58; font-size:8px; line-height:1.7; }
|
||||||
|
|
||||||
|
/* Admin readability: retain the information-dense layout while lifting microcopy to a readable floor. */
|
||||||
|
.admin-readable .portal-role { font-size:11px; }
|
||||||
|
.admin-readable .portal-sidebar nav button { font-size:13px; }
|
||||||
|
.admin-readable .portal-sidebar nav button em { font-size:10px; }
|
||||||
|
.admin-readable .sidebar-help span,.admin-readable .sidebar-help small { font-size:11px; }
|
||||||
|
.admin-readable .sidebar-help strong { font-size:13px; }
|
||||||
|
.admin-readable .portal-topbar > div:first-of-type,.admin-readable .portal-user > span strong { font-size:12px; }
|
||||||
|
.admin-readable .portal-user > span small { font-size:11px; }
|
||||||
|
.admin-readable .portal-heading > div > p:last-child,.admin-readable .heading-status { font-size:13px; line-height:1.65; }
|
||||||
|
.admin-readable .portal-content { line-height:1.55; }
|
||||||
|
.admin-readable .portal-content :where(p,small,time,dt,dd,label > span,th,td,button,input,select,textarea,em) { font-size:12px !important; }
|
||||||
|
.admin-readable .portal-content span:not(:has(svg)):not(.status):not(.user-avatar) { font-size:12px !important; }
|
||||||
|
.admin-readable .portal-content :where(td strong,button strong,p strong) { font-size:12px !important; }
|
||||||
|
.admin-readable .portal-content .status,.admin-readable .portal-content .overline { font-size:11px !important; }
|
||||||
|
|
||||||
@media (max-width: 1000px) { .candidate-archive-console { align-items:flex-start; flex-direction:column; }.archive-controls { width:100%; min-width:0; } }
|
@media (max-width: 1000px) { .candidate-archive-console { align-items:flex-start; flex-direction:column; }.archive-controls { width:100%; min-width:0; } }
|
||||||
@media (max-width: 620px) { .account-security-panel { grid-template-columns:1fr; padding:18px; }.archive-controls { grid-template-columns:1fr; } }
|
@media (max-width: 620px) { .account-security-panel { grid-template-columns:1fr; padding:18px; }.archive-controls { grid-template-columns:1fr; } }
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ await seededTestDatabase.close();
|
|||||||
|
|
||||||
const server = spawn(process.execPath, ['server.mjs'], {
|
const server = spawn(process.execPath, ['server.mjs'], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: { ...process.env, NODE_ENV: 'test', DATABASE_CLIENT: 'sqlite', PORT: String(port), SQLITE_PATH: testDb },
|
env: { ...process.env, NODE_ENV: 'test', DATABASE_CLIENT: 'sqlite', PORT: String(port), SQLITE_PATH: testDb, PUBLIC_SITE_NAME: '环境变量考试中心', PUBLIC_SITE_CODE: 'ENV-TEST', PUBLIC_SITE_PHONE: '0518-1234 5678', PUBLIC_SITE_ADDRESS: '测试地址 1 号', PUBLIC_SITE_EMAIL: 'service@example.test', PUBLIC_SITE_HERO_TITLE: '一次配置,', PUBLIC_SITE_HERO_HIGHLIGHT: '统一首页文案。', PUBLIC_SITE_FOOTER_NOTICE: '环境变量页脚提示' },
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,6 +207,10 @@ try {
|
|||||||
assert.ok(publicHome.data.notices.length >= 3, '公开首页应返回通知');
|
assert.ok(publicHome.data.notices.length >= 3, '公开首页应返回通知');
|
||||||
assert.ok(publicHome.data.exams.some(exam => exam.subjects.length > 1), '公开考试应包含多个科目');
|
assert.ok(publicHome.data.exams.some(exam => exam.subjects.length > 1), '公开考试应包含多个科目');
|
||||||
assert.equal(publicHome.data.selfRegistrationEnabled, false, '自主注册默认应关闭');
|
assert.equal(publicHome.data.selfRegistrationEnabled, false, '自主注册默认应关闭');
|
||||||
|
assert.deepEqual(publicHome.data.organization, { name: '环境变量考试中心', code: 'ENV-TEST', phone: '0518-1234 5678', address: '测试地址 1 号', email: 'service@example.test' }, '公开机构名称及联系方式应从环境变量读取');
|
||||||
|
assert.equal(publicHome.data.siteCopy.heroTitle, '一次配置,', '首页主标语应从环境变量读取');
|
||||||
|
assert.equal(publicHome.data.siteCopy.heroHighlight, '统一首页文案。', '首页强调标语应从环境变量读取');
|
||||||
|
assert.equal(publicHome.data.siteCopy.footerNotice, '环境变量页脚提示', '首页页脚提示应从环境变量读取');
|
||||||
const ckeditorAsset = await anonymous.request('/vendor/ckeditor5/ckeditor5.js');
|
const ckeditorAsset = await anonymous.request('/vendor/ckeditor5/ckeditor5.js');
|
||||||
assert.equal(ckeditorAsset.response.status, 200, '服务端应提供自托管 CKEditor 浏览器包');
|
assert.equal(ckeditorAsset.response.status, 200, '服务端应提供自托管 CKEditor 浏览器包');
|
||||||
assert.match(ckeditorAsset.data, /ImageInsertViaUrl/, '自托管 CKEditor 应包含图片 URL 插件');
|
assert.match(ckeditorAsset.data, /ImageInsertViaUrl/, '自托管 CKEditor 应包含图片 URL 插件');
|
||||||
|
|||||||
Reference in New Issue
Block a user