feat: 添加 Vercel 部署支持(Serverless API + KV 存储)

This commit is contained in:
jacky
2026-06-11 19:03:31 +08:00
parent 03a3645dff
commit ac3f2b0451
13 changed files with 247 additions and 1 deletions

View File

@@ -0,0 +1,529 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>报名管理后台 | 北滘校区</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--orange: #FF6B35;
--orange-dark: #E85A24;
--blue: #4A6CF7;
--dark: #1A1A2E;
--text: #333;
--text-light: #666;
--bg: #F5F7FA;
--white: #fff;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* ====== Header ====== */
.header {
background: linear-gradient(135deg, var(--dark) 0%, #2D2D5E 100%);
color: white;
padding: 24px;
text-align: center;
}
.header h1 { font-size: 22px; font-weight: 700; }
.header p { font-size: 13px; opacity: 0.6; margin-top: 4px; }
/* ====== Login ====== */
.login-section {
max-width: 400px;
margin: 60px auto;
background: var(--white);
border-radius: 20px;
padding: 40px 32px;
box-shadow: var(--shadow);
text-align: center;
}
.login-section h2 { font-size: 20px; color: var(--dark); margin-bottom: 8px; }
.login-section p { font-size: 14px; color: var(--text-light); margin-bottom: 24px; }
.login-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e8e8e8;
border-radius: 12px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
text-align: center;
box-sizing: border-box;
}
.login-input:focus { border-color: var(--orange); }
.login-btn {
width: 100%;
padding: 14px;
background: var(--orange);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
margin-top: 16px;
transition: background 0.2s;
}
.login-btn:hover { background: var(--orange-dark); }
.login-error {
color: #FF4444;
font-size: 13px;
margin-top: 10px;
display: none;
}
/* ====== Dashboard ====== */
.dashboard { display: none; }
.dashboard.active { display: block; }
/* Stats Bar */
.stats-bar {
display: flex;
gap: 16px;
padding: 24px;
max-width: 900px;
margin: 0 auto;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 140px;
background: var(--white);
border-radius: 14px;
padding: 20px;
box-shadow: var(--shadow);
text-align: center;
}
.stat-card .stat-num {
font-size: 32px;
font-weight: 800;
color: var(--orange);
}
.stat-card .stat-label {
font-size: 13px;
color: var(--text-light);
margin-top: 4px;
}
/* Controls */
.controls {
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.controls select, .controls input {
padding: 10px 14px;
border: 2px solid #e8e8e8;
border-radius: 10px;
font-size: 14px;
outline: none;
background: white;
}
.controls select:focus, .controls input:focus {
border-color: var(--orange);
}
.ctrl-btn {
padding: 10px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.ctrl-btn:hover { transform: scale(1.03); }
.ctrl-btn.orange { background: var(--orange); color: white; }
.ctrl-btn.blue { background: var(--blue); color: white; }
.ctrl-btn.outline { background: transparent; border: 2px solid #ddd; color: var(--text-light); }
.ctrl-btn.outline:hover { border-color: var(--orange); color: var(--orange); }
.ctrl-btn.danger { background: #FF4444; color: white; }
.spacer { flex: 1; }
/* Table */
.table-section {
max-width: 900px;
margin: 20px auto;
padding: 0 24px 40px;
}
.table-wrap {
background: var(--white);
border-radius: 16px;
box-shadow: var(--shadow);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: #f8f9fa;
padding: 14px 16px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: var(--text-light);
border-bottom: 2px solid #eee;
}
td {
padding: 14px 16px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: #FAFAFA; }
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
}
.badge-orange { background: #FFF0E8; color: var(--orange); }
.badge-blue { background: #EEF2FF; color: var(--blue); }
.del-btn {
background: none;
border: none;
color: #FF4444;
font-size: 13px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
}
.del-btn:hover { opacity: 1; }
/* Empty State */
.empty {
text-align: center;
padding: 48px 20px;
color: #bbb;
}
.empty-icon { font-size: 48px; margin-bottom: 12px; }
.empty p { font-size: 15px; }
/* Confirm Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.show { display: flex; }
.modal {
background: white;
border-radius: 20px;
padding: 32px;
width: 90%;
max-width: 360px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.modal h3 { font-size: 18px; color: var(--dark); margin-bottom: 8px; }
.modal p { font-size: 14px; color: var(--text-light); margin-bottom: 20px; }
.modal-btns { display: flex; gap: 12px; }
.modal-btns button {
flex: 1;
padding: 12px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.modal-btns .cancel-btn { background: #f0f0f0; color: var(--text-light); }
.modal-btns .confirm-btn { background: #FF4444; color: white; }
@media (max-width: 640px) {
.stats-bar { padding: 16px; gap: 10px; }
.stat-card { min-width: 100px; padding: 14px; }
.stat-card .stat-num { font-size: 24px; }
.controls { padding: 0 16px; }
.table-section { padding: 0 16px 24px; }
th, td { padding: 10px 12px; font-size: 13px; }
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<h1>📋 报名管理后台</h1>
<p>北滘校区 · NCT 集训报名数据管理</p>
</div>
<!-- Login -->
<div class="login-section" id="loginSection">
<h2>🔐 管理员登录</h2>
<p>请输入管理员密码查看报名数据</p>
<input class="login-input" type="password" id="adminPassword" placeholder="输入管理员密码" onkeydown="if(event.key==='Enter') doLogin()">
<button class="login-btn" onclick="doLogin()">登 录</button>
<div class="login-error" id="loginError">密码错误,请重试</div>
</div>
<!-- Dashboard -->
<div class="dashboard" id="dashboard">
<!-- Stats -->
<div class="stats-bar" id="statsBar">
<div class="stat-card">
<div class="stat-num" id="statTotal">0</div>
<div class="stat-label">总报名人数</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statP1">0</div>
<div class="stat-label">第一期</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statP2">0</div>
<div class="stat-label">第二期</div>
</div>
<div class="stat-card">
<div class="stat-num" id="statP3">0</div>
<div class="stat-label">第三期</div>
</div>
</div>
<!-- Controls -->
<div class="controls">
<select id="filterPeriod" onchange="loadData()">
<option value="">全部期数</option>
<option value="第一期">第一期</option>
<option value="第二期">第二期</option>
<option value="第三期">第三期</option>
</select>
<select id="filterTeacher" onchange="loadData()">
<option value="">全部老师</option>
</select>
<input type="text" id="filterName" placeholder="搜索姓名" oninput="loadData()">
<div class="spacer"></div>
<button class="ctrl-btn outline" onclick="loadData()">🔄 刷新</button>
<button class="ctrl-btn blue" onclick="exportCSV()">📥 导出 CSV</button>
<button class="ctrl-btn outline" onclick="doLogout()">退出登录</button>
</div>
<!-- Table -->
<div class="table-section">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>期数</th>
<th>老师</th>
<th>报名时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tableBody">
</tbody>
</table>
<div class="empty" id="emptyState" style="display:none">
<div class="empty-icon">📭</div>
<p>暂无报名记录</p>
</div>
</div>
</div>
</div>
<!-- Delete Confirm Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<h3>确认删除</h3>
<p id="deleteMsg">确定要删除这条报名记录吗?此操作不可撤销。</p>
<div class="modal-btns">
<button class="cancel-btn" onclick="closeDeleteModal()">取消</button>
<button class="confirm-btn" onclick="confirmDelete()">确认删除</button>
</div>
</div>
</div>
<script>
var adminPwd = '';
var allData = [];
var deleteId = null;
function doLogin() {
adminPwd = document.getElementById('adminPassword').value;
if (!adminPwd) return;
fetch('api/stats?password=' + encodeURIComponent(adminPwd))
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
document.getElementById('loginSection').style.display = 'none';
document.getElementById('dashboard').classList.add('active');
updateStats(data.data);
loadData();
} else {
document.getElementById('loginError').style.display = 'block';
}
})
.catch(function() {
document.getElementById('loginError').style.display = 'block';
});
}
function doLogout() {
adminPwd = '';
document.getElementById('loginSection').style.display = '';
document.getElementById('dashboard').classList.remove('active');
document.getElementById('adminPassword').value = '';
document.getElementById('loginError').style.display = 'none';
}
function updateStats(stats) {
document.getElementById('statTotal').textContent = stats.total || 0;
var periods = stats.byPeriod || {};
document.getElementById('statP1').textContent = periods['第一期7月13日7月25日'] || 0;
document.getElementById('statP2').textContent = periods['第二期7月27日8月8日'] || 0;
document.getElementById('statP3').textContent = periods['第三期8月10日8月22日'] || 0;
}
function loadData() {
fetch('api/registrations?password=' + encodeURIComponent(adminPwd))
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) return;
allData = data.data;
applyFilters();
updateTeacherFilter(allData);
});
}
function updateTeacherFilter(records) {
var sel = document.getElementById('filterTeacher');
var current = sel.value;
var teachers = {};
records.forEach(function(r) { teachers[r.teacher] = true; });
sel.innerHTML = '<option value="">全部老师</option>';
Object.keys(teachers).sort().forEach(function(t) {
var opt = document.createElement('option');
opt.value = t;
opt.textContent = t;
if (t === current) opt.selected = true;
sel.appendChild(opt);
});
}
function applyFilters() {
var periodFilter = document.getElementById('filterPeriod').value;
var teacherFilter = document.getElementById('filterTeacher').value;
var nameFilter = document.getElementById('filterName').value.trim().toLowerCase();
var filtered = allData.filter(function(r) {
if (periodFilter && !r.period.includes(periodFilter)) return false;
if (teacherFilter && r.teacher !== teacherFilter) return false;
if (nameFilter && !r.name.toLowerCase().includes(nameFilter)) return false;
return true;
});
renderTable(filtered);
}
function renderTable(records) {
var tbody = document.getElementById('tableBody');
var empty = document.getElementById('emptyState');
if (records.length === 0) {
tbody.innerHTML = '';
empty.style.display = '';
return;
}
empty.style.display = 'none';
var html = '';
records.forEach(function(r, i) {
var badgeClass = r.period.includes('第二期') ? 'badge-blue' : 'badge-orange';
var periodShort = r.period;
html += '<tr>' +
'<td>' + (i + 1) + '</td>' +
'<td><strong>' + escHtml(r.name) + '</strong></td>' +
'<td><span class="badge ' + badgeClass + '">' + escHtml(periodShort) + '</span></td>' +
'<td>' + escHtml(r.teacher) + '</td>' +
'<td>' + escHtml(r.createdAt || '-') + '</td>' +
'<td><button class="del-btn" onclick="showDeleteModal(' + r.id + ', \'' + escHtml(r.name) + '\')">删除</button></td>' +
'</tr>';
});
tbody.innerHTML = html;
}
function escHtml(str) {
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function showDeleteModal(id, name) {
deleteId = id;
document.getElementById('deleteMsg').textContent = '确定要删除「' + name + '」的报名记录吗?此操作不可撤销。';
document.getElementById('deleteModal').classList.add('show');
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.remove('show');
deleteId = null;
}
function confirmDelete() {
if (!deleteId) return;
fetch('api/registrations/' + deleteId + '?password=' + encodeURIComponent(adminPwd), {
method: 'DELETE'
})
.then(function(r) { return r.json(); })
.then(function(data) {
closeDeleteModal();
if (data.ok) {
loadData();
fetch('api/stats?password=' + encodeURIComponent(adminPwd))
.then(function(r) { return r.json(); })
.then(function(s) { if (s.ok) updateStats(s.data); });
}
});
}
function exportCSV() {
var periodFilter = document.getElementById('filterPeriod').value;
var teacherFilter = document.getElementById('filterTeacher').value;
var nameFilter = document.getElementById('filterName').value.trim().toLowerCase();
var filtered = allData.filter(function(r) {
if (periodFilter && !r.period.includes(periodFilter)) return false;
if (teacherFilter && r.teacher !== teacherFilter) return false;
if (nameFilter && !r.name.toLowerCase().includes(nameFilter)) return false;
return true;
});
var csv = '序号,姓名,期数,老师,报名时间\n';
filtered.forEach(function(r, i) {
csv += (i + 1) + ',' + r.name + ',' + r.period + ',' + r.teacher + ',' + (r.createdAt || '') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = '北滘校区_报名数据_' + new Date().toISOString().slice(0, 10) + '.csv';
a.click();
URL.revokeObjectURL(url);
}
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) closeDeleteModal();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,704 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--orange: #FF6B35;
--orange-light: #FF8F5E;
--orange-dark: #E85A24;
--blue: #4A6CF7;
--blue-light: #6B8CFF;
--purple: #7C5CFC;
--dark: #1A1A2E;
--text: #333;
--text-light: #666;
--bg: #F5F7FA;
--white: #fff;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.12);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* ====== Hero ====== */
.hero {
background: linear-gradient(135deg, #FF6B35 0%, #FF8F5E 40%, #FFB347 100%);
color: white;
padding: 60px 24px 80px;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.08) 1px, transparent 1px);
background-size: 30px 30px;
animation: heroPattern 20s linear infinite;
}
@keyframes heroPattern {
from { transform: translate(0, 0); }
to { transform: translate(30px, 30px); }
}
.hero-content {
position: relative;
z-index: 2;
max-width: 700px;
margin: 0 auto;
}
.hero-badge {
display: inline-block;
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
padding: 6px 20px;
border-radius: 20px;
font-size: 14px;
margin-bottom: 20px;
letter-spacing: 1px;
}
.hero h1 {
font-size: 32px;
font-weight: 800;
margin-bottom: 12px;
text-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.hero .subtitle {
font-size: 18px;
opacity: 0.95;
margin-bottom: 28px;
}
.hero-stats {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
}
.hero-stat {
text-align: center;
}
.hero-stat .num {
font-size: 36px;
font-weight: 800;
}
.hero-stat .label {
font-size: 13px;
opacity: 0.85;
margin-top: 2px;
}
/* ====== Sections ====== */
.section {
max-width: 800px;
margin: 0 auto;
padding: 48px 20px;
}
.section-header {
text-align: center;
margin-bottom: 36px;
}
.section-header h2 {
font-size: 26px;
font-weight: 700;
color: var(--dark);
margin-bottom: 8px;
}
.section-header p {
color: var(--text-light);
font-size: 15px;
}
/* ====== Info Cards ====== */
.info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.info-card {
background: var(--white);
border-radius: 16px;
padding: 24px;
box-shadow: var(--shadow);
display: flex;
gap: 16px;
align-items: flex-start;
transition: transform 0.3s;
}
.info-card:hover {
transform: translateY(-2px);
}
.info-icon {
width: 52px;
height: 52px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
flex-shrink: 0;
}
.info-icon.orange { background: #FFF0E8; }
.info-icon.blue { background: #EEF2FF; }
.info-icon.purple { background: #F3EEFF; }
.info-icon.green { background: #E8F9EF; }
.info-card h3 {
font-size: 17px;
font-weight: 600;
color: var(--dark);
margin-bottom: 6px;
}
.info-card p {
font-size: 14px;
color: var(--text-light);
line-height: 1.7;
}
/* ====== Timeline ====== */
.period-section {
background: var(--white);
border-radius: 16px;
padding: 24px;
box-shadow: var(--shadow);
margin-bottom: 24px;
}
.period-badge {
display: inline-block;
font-size: 12px;
font-weight: 600;
padding: 3px 12px;
border-radius: 10px;
margin-bottom: 10px;
}
.period-badge.orange { background: #FFF0E8; color: var(--orange); }
.period-badge.blue { background: #EEF2FF; color: var(--blue); }
.period-section h3 {
font-size: 20px;
font-weight: 700;
color: var(--dark);
margin-bottom: 6px;
}
.period-info {
font-size: 14px;
color: var(--text-light);
margin-bottom: 16px;
line-height: 1.7;
}
/* ====== Registration Input ====== */
.reg-input-row {
display: flex;
gap: 10px;
align-items: center;
margin-top: 12px;
}
.reg-input {
flex: 1;
padding: 10px 14px;
border: 2px solid #e8e8e8;
border-radius: 10px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
}
.reg-input:focus {
border-color: var(--orange);
}
.reg-input::placeholder {
color: #bbb;
}
.reg-btn {
padding: 10px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
color: white;
cursor: pointer;
white-space: nowrap;
transition: transform 0.2s, opacity 0.2s;
}
.reg-btn:hover { transform: scale(1.03); }
.reg-btn:active { transform: scale(0.97); }
.reg-btn.orange { background: var(--orange); }
.reg-btn.blue { background: var(--blue); }
/* ====== Exam Info ====== */
.exam-card {
background: linear-gradient(135deg, var(--dark) 0%, #2D2D5E 100%);
color: white;
border-radius: 20px;
padding: 32px 28px;
text-align: center;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.exam-card::after {
content: '';
position: absolute;
top: -40%;
right: -20%;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255,107,53,0.15), transparent);
border-radius: 50%;
}
.exam-card h3 {
font-size: 15px;
opacity: 0.7;
margin-bottom: 8px;
letter-spacing: 2px;
position: relative;
z-index: 2;
}
.exam-date {
font-size: 38px;
font-weight: 800;
margin-bottom: 6px;
position: relative;
z-index: 2;
background: linear-gradient(135deg, #FF6B35, #FFB347);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.exam-note {
font-size: 14px;
opacity: 0.6;
position: relative;
z-index: 2;
}
/* ====== Value Cards ====== */
.value-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.value-card {
background: var(--white);
border-radius: 16px;
padding: 22px 18px;
text-align: center;
box-shadow: var(--shadow);
transition: transform 0.3s;
}
.value-card:hover {
transform: translateY(-3px);
}
.value-emoji {
font-size: 36px;
margin-bottom: 10px;
}
.value-card h3 {
font-size: 16px;
font-weight: 600;
color: var(--dark);
margin-bottom: 6px;
}
.value-card p {
font-size: 13px;
color: var(--text-light);
line-height: 1.6;
}
/* ====== Highlights ====== */
.highlight-strip {
background: linear-gradient(135deg, #FFF8E1, #FFF3E0);
border-radius: 16px;
padding: 20px 24px;
border-left: 5px solid #FFB347;
margin: 20px 0;
}
.highlight-strip p {
font-size: 14px;
color: #8B6914;
line-height: 1.7;
}
.highlight-strip strong {
color: #E85A24;
}
/* ====== Fee Section ====== */
.fee-table {
width: 100%;
background: var(--white);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow);
}
.fee-row {
display: flex;
align-items: center;
padding: 18px 24px;
border-bottom: 1px solid #f0f0f0;
}
.fee-row:last-child {
border-bottom: none;
}
.fee-label {
flex: 1;
font-size: 15px;
color: var(--text);
}
.fee-value {
font-size: 16px;
font-weight: 700;
color: var(--orange);
}
/* ====== FAQ ====== */
.faq-item {
background: var(--white);
border-radius: 14px;
margin-bottom: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
overflow: hidden;
}
.faq-q {
padding: 18px 24px;
font-size: 15px;
font-weight: 600;
color: var(--dark);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
transition: background 0.2s;
}
.faq-q:hover {
background: #f8f9fa;
}
.faq-q .arrow {
font-size: 12px;
color: #999;
transition: transform 0.3s;
}
.faq-item.open .faq-q .arrow {
transform: rotate(180deg);
}
.faq-a {
display: none;
padding: 0 24px 18px;
font-size: 14px;
color: var(--text-light);
line-height: 1.8;
}
.faq-item.open .faq-a {
display: block;
}
/* ====== CTA ====== */
.cta-section {
text-align: center;
padding: 48px 20px 24px;
max-width: 800px;
margin: 0 auto;
}
.cta-box {
background: linear-gradient(135deg, #FF6B35, #FF8F5E, #FFB347);
border-radius: 20px;
padding: 40px 28px;
color: white;
box-shadow: 0 8px 32px rgba(255,107,53,0.3);
}
.cta-box h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 12px;
}
.cta-box p {
font-size: 15px;
opacity: 0.95;
margin-bottom: 24px;
line-height: 1.7;
}
.cta-btn {
display: inline-block;
background: white;
color: var(--orange);
padding: 14px 40px;
border-radius: 30px;
font-size: 16px;
font-weight: 700;
text-decoration: none;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
transition: transform 0.3s, box-shadow 0.3s;
}
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
/* ====== Countdown ====== */
.countdown-section {
background: var(--dark);
color: white;
padding: 36px 20px;
text-align: center;
}
.countdown-title {
font-size: 14px;
opacity: 0.6;
letter-spacing: 3px;
margin-bottom: 16px;
}
.countdown-boxes {
display: flex;
justify-content: center;
gap: 12px;
}
.countdown-box {
background: rgba(255,255,255,0.08);
border-radius: 12px;
padding: 16px 20px;
min-width: 72px;
}
.countdown-box .num {
font-size: 32px;
font-weight: 800;
color: var(--orange-light);
}
.countdown-box .unit {
font-size: 12px;
opacity: 0.5;
margin-top: 2px;
}
/* ====== Footer ====== */
.footer {
text-align: center;
padding: 32px 20px;
color: #aaa;
font-size: 13px;
}
/* ====== Modal ====== */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.show {
display: flex;
}
.modal {
background: white;
border-radius: 20px;
padding: 32px;
width: 90%;
max-width: 380px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.modal h3 {
font-size: 20px;
margin-bottom: 6px;
color: var(--dark);
}
.modal .modal-period {
font-size: 14px;
color: var(--orange);
font-weight: 600;
margin-bottom: 20px;
}
.modal-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e8e8e8;
border-radius: 12px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
box-sizing: border-box;
text-align: center;
}
.modal-input:focus {
border-color: var(--orange);
}
.modal-select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e8e8e8;
border-radius: 12px;
font-size: 16px;
outline: none;
background: white;
color: #999;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23999' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
cursor: pointer;
box-sizing: border-box;
}
.modal-select:focus {
border-color: var(--orange);
}
.modal-select.has-value {
color: #333;
}
.modal-submit {
width: 100%;
padding: 14px;
background: var(--orange);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
margin-top: 14px;
transition: background 0.2s;
}
.modal-submit:hover { background: var(--orange-dark); }
.modal-submit:disabled { background: #ccc; cursor: default; }
.modal-cancel {
background: none;
border: none;
color: #999;
font-size: 14px;
margin-top: 12px;
cursor: pointer;
}
.modal-cancel:hover { color: #666; }
/* ====== Toast ====== */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.85);
background: rgba(0,0,0,0.82);
color: #fff;
padding: 28px 36px;
border-radius: 18px;
font-size: 15px;
line-height: 1.8;
text-align: center;
z-index: 2000;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
max-width: 340px;
box-shadow: 0 12px 40px rgba(0,0,0,0.25);
pointer-events: none;
white-space: pre-line;
}
.toast.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* ====== Animations ====== */
.fade-up {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-up.visible {
opacity: 1;
transform: translateY(0);
}
/* ====== Mobile ====== */
@media (max-width: 640px) {
.hero { padding: 48px 20px 64px; }
.hero h1 { font-size: 26px; }
.hero .subtitle { font-size: 15px; }
.hero-stats { gap: 20px; }
.hero-stat .num { font-size: 28px; }
.section { padding: 36px 16px; }
.section-header h2 { font-size: 22px; }
.value-grid { grid-template-columns: 1fr; }
.exam-date { font-size: 30px; }
.countdown-boxes { gap: 8px; }
.countdown-box { padding: 12px 16px; min-width: 60px; }
.countdown-box .num { font-size: 26px; }
}

View File

@@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2026 暑假 Kitten 图形化编程等级考试集训 | 北滘校区</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- Hero -->
<div class="hero">
<div class="hero-content">
<span class="hero-badge">🎯 2026 暑假集训</span>
<h1>Kitten 图形化编程<br>等级考试集训</h1>
<p class="subtitle">NCT 青少年编程能力等级测试 · 北滘校区</p>
<div class="hero-stats">
<div class="hero-stat">
<div class="num">100%</div>
<div class="label">集训学员通过率</div>
</div>
<div class="hero-stat">
<div class="num">75W+</div>
<div class="label">全国考生累计</div>
</div>
<div class="hero-stat">
<div class="num">8人</div>
<div class="label">小班精品教学</div>
</div>
</div>
</div>
</div>
<!-- 倒计时 -->
<div class="countdown-section">
<div class="countdown-title">距离 NCT 考试还有</div>
<div class="countdown-boxes">
<div class="countdown-box">
<div class="num" id="cd-days">--</div>
<div class="unit"></div>
</div>
<div class="countdown-box">
<div class="num" id="cd-hours">--</div>
<div class="unit"></div>
</div>
<div class="countdown-box">
<div class="num" id="cd-mins">--</div>
<div class="unit"></div>
</div>
<div class="countdown-box">
<div class="num" id="cd-secs">--</div>
<div class="unit"></div>
</div>
</div>
</div>
<!-- NCT 是什么 -->
<div class="section fade-up">
<div class="section-header">
<h2>什么是 NCT</h2>
<p>国内最权威的青少年编程能力等级测试</p>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-icon orange">🏛️</div>
<div>
<h3>中国软件行业协会主办</h3>
<p>NCT 由<strong>中国软件行业协会</strong>盖章颁发等级证书,是国家民政部认可的首批 AAA 级行业协会,唯一代表中国计算机软件产业界的全国性一级行业组织。</p>
</div>
</div>
<div class="info-card">
<div class="info-icon blue">🌐</div>
<div>
<h3>国内国际双认证</h3>
<p>通过<strong>教育部 CELTSC</strong> 标准符合性认证,同时通过国际教育技术协会 <strong>ISTE</strong> 认证,是国内首家获得 ISTE Seal of Alignment 的编程测评项目。</p>
</div>
</div>
<div class="info-card">
<div class="info-icon purple">🎓</div>
<div>
<h3>联合国教科文组织合作项目</h3>
<p>2021 年成为<strong>联合国教科文组织亚非地区青少年编程教育合作项目</strong>AAYC指定测评服务平台获得国际认可。</p>
</div>
</div>
<div class="info-card">
<div class="info-icon green">👨‍🎓</div>
<div>
<h3>全国超过 75 万考生参与</h3>
<p>覆盖中国 34 个省级行政区、全球 25+ 国家及地区3000+ 所学校和机构参与,新东方、编程猫、童程童美等知名品牌合作。</p>
</div>
</div>
</div>
</div>
<!-- 证书价值 -->
<div class="section fade-up">
<div class="section-header">
<h2>考级证书的价值</h2>
<p>不只是一张证书,更是孩子成长的里程碑</p>
</div>
<div class="value-grid">
<div class="value-card">
<div class="value-emoji">🏅</div>
<h3>权威认证</h3>
<p>中国软件行业协会盖章颁发,含金量高</p>
</div>
<div class="value-card">
<div class="value-emoji">📋</div>
<h3>科技特长</h3>
<p>可作为信息科技特长参考依据,综合素质评价加分</p>
</div>
<div class="value-card">
<div class="value-emoji">🧠</div>
<h3>能力提升</h3>
<p>锻炼逻辑思维、拆题能力与问题解决能力</p>
</div>
<div class="value-card">
<div class="value-emoji">🎯</div>
<h3>阶段目标</h3>
<p>清晰的等级体系,给孩子学习锚定方向与动力</p>
</div>
</div>
</div>
<!-- 考试日期 -->
<div class="section fade-up" style="padding-top: 0;">
<div class="exam-card">
<h3>NCT 考试时间</h3>
<div class="exam-date">2026 年 8 月 22 23 日</div>
<p class="exam-note">具体考试场次 6 月上旬公布</p>
</div>
</div>
<!-- 集训时间安排 -->
<div class="section fade-up">
<div class="section-header">
<h2>集训时间安排</h2>
<p>三期可选,内容一致,根据时间灵活选择</p>
</div>
<div class="period-section">
<span class="period-badge orange">第一期</span>
<h3>7 月集训 · 第一期</h3>
<p class="period-info">2 周 · 周一至周六 · 每周日休息 · 每节课 2.5 小时</p>
<div style="font-size:18px;font-weight:700;color:#1A1A2E;margin:12px 0 8px">7 月 13 日 7 月 25 日</div>
<div class="reg-input-row">
<input class="reg-input" type="text" placeholder="填写孩子姓名" maxlength="20">
<button class="reg-btn orange" onclick="register(this, '第一期7月13日7月25日')">我要报名</button>
</div>
</div>
<div class="period-section">
<span class="period-badge blue">第二期</span>
<h3>78 月集训 · 第二期</h3>
<p class="period-info">2 周 · 周一至周六 · 每周日休息 · 每节课 2.5 小时</p>
<div style="font-size:18px;font-weight:700;color:#1A1A2E;margin:12px 0 8px">7 月 27 日 8 月 8 日</div>
<div class="reg-input-row">
<input class="reg-input" type="text" placeholder="填写孩子姓名" maxlength="20">
<button class="reg-btn blue" onclick="register(this, '第二期7月27日8月8日')">我要报名</button>
</div>
</div>
<div class="period-section">
<span class="period-badge orange">第三期</span>
<h3>8 月集训 · 第三期</h3>
<p class="period-info">2 周 · 周一至周六 · 每周日休息 · 每节课 2.5 小时</p>
<div style="font-size:18px;font-weight:700;color:#1A1A2E;margin:12px 0 8px">8 月 10 日 8 月 22 日</div>
<div class="reg-input-row">
<input class="reg-input" type="text" placeholder="填写孩子姓名" maxlength="20">
<button class="reg-btn orange" onclick="register(this, '第三期8月10日8月22日')">我要报名</button>
</div>
</div>
<div class="highlight-strip">
<p>💡 三期集训内容完全一致,家长可根据孩子时间安排自由选择。每周日休息。<br>填写孩子姓名后点击<strong>「我要报名」</strong>即可提交,老师会主动联系您确认。</p>
</div>
</div>
<!-- 报名门槛 -->
<div class="section fade-up" style="padding-top: 0;">
<div class="section-header">
<h2>报名条件</h2>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-icon orange">📐</div>
<div>
<h3>K2 阶段学员</h3>
<p>报考 <strong>NCT 一级</strong>,夯实基础,建立信心</p>
</div>
</div>
<div class="info-card">
<div class="info-icon blue">🔬</div>
<div>
<h3>K4 阶段学员</h3>
<p>报考 <strong>NCT 二级</strong>,挑战进阶,证明实力</p>
</div>
</div>
</div>
<div class="highlight-strip" style="margin-top: 16px;">
<p>📌 未考过一级的学生需先考一级,不可跳级。所有在读 Kitten 学员均可报名。</p>
</div>
</div>
<!-- 费用说明 -->
<div class="section fade-up" style="padding-top: 0;">
<div class="section-header">
<h2>费用说明</h2>
</div>
<div class="fee-table">
<div class="fee-row">
<div class="fee-label">📝 NCT 官方报名费</div>
<div class="fee-value">300 元 / 人</div>
</div>
<div class="fee-row">
<div class="fee-label">🎒 集训费用</div>
<div class="fee-value">正常课时扣费</div>
</div>
<div class="fee-row">
<div class="fee-label">⏱️ 课时规则</div>
<div class="fee-value">每节扣 2 课时</div>
</div>
</div>
<div class="highlight-strip" style="margin-top: 16px;">
<p>🏆 <strong>奖学金激励:</strong>成绩优异的学员可获得校区颁发的奖学金,既是荣誉也是激励!</p>
</div>
</div>
<!-- 常见问题 -->
<div class="section fade-up" style="padding-top: 0;">
<div class="section-header">
<h2>常见问题</h2>
</div>
<div class="faq-item">
<div class="faq-q">
<span>孩子零基础可以参加吗?</span>
<span class="arrow"></span>
</div>
<div class="faq-a">
集训面向在读 Kitten 学员。K2 阶段报一级、K4 阶段报二级,难度与孩子当前学习进度匹配,老师全程指导,可以放心参加。
</div>
</div>
<div class="faq-item">
<div class="faq-q">
<span>考试形式是什么样的?</span>
<span class="arrow"></span>
</div>
<div class="faq-a">
NCT 采用线上考试形式支持居家和线下考点在线考试。AI 智能监考系统保证考试公正、专业,足不出户即可参加。
</div>
</div>
<div class="faq-item">
<div class="faq-q">
<span>集训会不会影响暑假出行?</span>
<span class="arrow"></span>
</div>
<div class="faq-a">
集训每期内容一样,选一期参加即可。每周日休息,三期覆盖整个暑假,合理安排完全不会影响家庭出行计划。
</div>
</div>
<div class="faq-item">
<div class="faq-q">
<span>通过率怎么样?</span>
<span class="arrow"></span>
</div>
<div class="faq-a">
参加集训的学员通过率基本达到 <strong>100%</strong>。集训内容紧扣考级知识点,系统刷题 + 模拟实操,帮助孩子充分准备。
</div>
</div>
<div class="faq-item">
<div class="faq-q">
<span>证书长什么样?有含金量吗?</span>
<span class="arrow"></span>
</div>
<div class="faq-a">
新版证书采用 PU 皮革材质、烫金凹印技术、圆角裁剪工艺,精美大气。由中国软件行业协会盖章颁发,通过顺丰快递寄送。该证书通过教育部 CELTSC 标准认证和国际 ISTE 认证,是目前国内外双认证的权威编程等级证书。
</div>
</div>
</div>
<!-- 报名入口 -->
<div class="cta-section">
<div class="cta-box">
<h2>🚀 暑假集训,助孩子高效拿证</h2>
<p>小班精品教学 · 系统备考方案 · 权威等级证书<br>名额有限,每班仅限 8 人,先到先得!</p>
<a href="#" class="cta-btn" onclick="showToast('请往上滑,填写孩子姓名\n并点击「我要报名」提交', 3000); return false;">立即咨询报名</a>
</div>
</div>
<!-- 底部 -->
<div class="footer">
<p>穹狼科创 · 北滘校区 · 2026 年暑假</p>
<p style="margin-top: 6px;">NCT 青少年编程能力等级测试</p>
</div>
<!-- Toast 提示 -->
<div class="toast" id="toast"></div>
<!-- 报名确认弹窗 -->
<div class="modal-overlay" id="regModal">
<div class="modal">
<h3>确认报名</h3>
<div class="modal-period" id="modalPeriod"></div>
<input class="modal-input" id="modalName" type="text" placeholder="请输入孩子姓名" maxlength="20" style="margin-bottom:10px">
<select class="modal-select" id="modalTeacher" onchange="this.classList.toggle('has-value', this.value)">
<option value="" disabled selected>选择负责老师</option>
<option value="东东老师">东东老师</option>
<option value="橙子老师">橙子老师</option>
<option value="福福老师">福福老师</option>
<option value="坦克老师">坦克老师</option>
<option value="菠萝老师">菠萝老师</option>
</select>
<button class="modal-submit" id="modalSubmit" onclick="submitReg()">确认提交</button>
<br>
<button class="modal-cancel" onclick="closeModal()">取消</button>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>

126
public/beijiaonct/js/app.js Normal file
View File

@@ -0,0 +1,126 @@
// ============ 倒计时 ============
function updateCountdown() {
var examDate = new Date('2026-08-22T09:00:00');
var now = new Date();
var diff = examDate - now;
if (diff <= 0) {
document.getElementById('cd-days').textContent = '0';
document.getElementById('cd-hours').textContent = '0';
document.getElementById('cd-mins').textContent = '0';
document.getElementById('cd-secs').textContent = '0';
return;
}
document.getElementById('cd-days').textContent = Math.floor(diff / 864e5);
document.getElementById('cd-hours').textContent = Math.floor((diff % 864e5) / 36e5);
document.getElementById('cd-mins').textContent = Math.floor((diff % 36e5) / 6e4);
document.getElementById('cd-secs').textContent = Math.floor((diff % 6e4) / 1e3);
}
updateCountdown();
setInterval(updateCountdown, 1000);
// ============ FAQ 折叠 ============
document.querySelectorAll('.faq-q').forEach(function(q) {
q.addEventListener('click', function() {
this.parentElement.classList.toggle('open');
});
});
// ============ 滚动渐入 ============
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) entry.target.classList.add('visible');
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-up').forEach(function(el) {
observer.observe(el);
});
// ============ Toast 提示 ============
function showToast(msg, duration) {
duration = duration || 2500;
var el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(el._timer);
el._timer = setTimeout(function() {
el.classList.remove('show');
}, duration);
}
// ============ 报名流程 ============
var pendingPeriod = '';
function register(btn, period) {
var row = btn.closest('.reg-input-row');
var name = row.querySelector('.reg-input').value.trim();
if (!name) {
row.querySelector('.reg-input').focus();
row.querySelector('.reg-input').style.borderColor = '#FF4444';
setTimeout(function() {
row.querySelector('.reg-input').style.borderColor = '';
}, 1500);
return;
}
pendingPeriod = period;
document.getElementById('modalPeriod').textContent = period;
document.getElementById('modalName').value = name;
document.getElementById('regModal').classList.add('show');
document.getElementById('modalName').focus();
}
function closeModal() {
document.getElementById('regModal').classList.remove('show');
pendingPeriod = '';
}
document.getElementById('regModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
function submitReg() {
var name = document.getElementById('modalName').value.trim();
var teacher = document.getElementById('modalTeacher').value;
var btn = document.getElementById('modalSubmit');
if (!name) {
document.getElementById('modalName').focus();
return;
}
if (!teacher) {
document.getElementById('modalTeacher').style.borderColor = '#FF4444';
setTimeout(function() {
document.getElementById('modalTeacher').style.borderColor = '';
}, 1500);
return;
}
btn.disabled = true;
btn.textContent = '提交中...';
fetch('api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'name=' + encodeURIComponent(name) +
'&period=' + encodeURIComponent(pendingPeriod) +
'&teacher=' + encodeURIComponent(teacher)
})
.then(function(res) { return res.json(); })
.then(function(data) {
closeModal();
if (data.ok) {
showToast('✅ 报名成功!\n\n' + name + '\n' + pendingPeriod + '\n' + teacher, 3000);
document.querySelectorAll('.reg-input').forEach(function(el) { el.value = ''; });
document.getElementById('modalTeacher').selectedIndex = 0;
document.getElementById('modalTeacher').classList.remove('has-value');
} else {
showToast('❌ ' + (data.msg || '提交失败,请稍后再试'));
}
})
.catch(function() {
showToast('❌ 网络错误,请稍后再试');
})
.finally(function() {
btn.disabled = false;
btn.textContent = '确认提交';
});
}