You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
22 KiB

1 month ago
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>“光联万物 创芯未来”首届沪苏协同新兴产业科创大赛报名系统</title>
<link rel="stylesheet" href="./assets/bootstrap.min.css" />
<link rel="stylesheet" href="./assets/prototype.css" />
1 month ago
<script src="./assets/prototype-scale.js?v=20260819-hero"></script>
1 month ago
</head>
<body class="prototype-page layout-shell">
<header class="navbar navbar-expand-lg layout-header">
<div class="container-fluid px-4 layout-header-inner">
<div class="header-brand">
<span class="navbar-brand mb-0 prototype-title">首届沪苏协同新兴产业科创大赛报名系统</span>
</div>
<div class="header-meta">
<span class="header-status">报名进行中</span>
<div class="header-deadline" aria-label="报名截止时间">
<span>截至</span>
<time datetime="2026-12-31T23:59:00+08:00">2026-12-31 23:59</time>
</div>
<div class="header-audience-actions">
<button id="headerMyRegistrationsButton" class="header-my-registrations d-none" type="button" aria-haspopup="dialog" aria-controls="myRegistrationsModal">我的报名</button>
<div class="dropdown header-user-dropdown">
<button id="headerUserToggle" class="header-user dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false" aria-label="打开用户菜单">
<span id="roleBadge" class="header-user-name">王清萍</span>
</button>
<ul class="dropdown-menu dropdown-menu-end header-user-menu" aria-labelledby="headerUserToggle">
<li id="profileMenuItem">
<a id="profileMenuLink" class="dropdown-item" href="javascript:void(0)" data-bs-toggle="modal" data-bs-target="#profileModal">
<span id="profileMenuLabel">个人信息</span>
</a>
</li>
<li class="header-user-menu-separator">
<a class="dropdown-item dropdown-item-danger" href="./login.html">
<span>退出登录</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</header>
<div id="mobileNavSection" class="container-fluid px-4 pt-2 pb-1 d-lg-none">
<div class="mobile-nav-wrap" id="mobileNav">
<button class="mobile-nav-link" data-page="./pages/user-form.html" data-role="user">赛事报名</button>
<button class="mobile-nav-link" data-page="./pages/audience-form.html" data-role="audience">观众报名</button>
<button class="mobile-nav-link" data-page="./pages/admin-report.html" data-role="admin">综合看板</button>
<button class="mobile-nav-link" data-page="./pages/admin-list.html" data-role="admin">项目管理</button>
<button class="mobile-nav-link" data-page="./pages/admin-audience.html" data-role="admin">观众与核销</button>
<button class="mobile-nav-link" data-page="./pages/admin-reviewers.html" data-role="admin">评审员管理</button>
</div>
</div>
<div class="container-fluid px-4 py-3 layout-main">
<div class="row g-3">
<aside class="col-12 col-md-3 col-lg-2 layout-sider">
<div class="card">
<div class="card-body">
<div class="nav flex-column gap-2" id="sideNav">
<button class="nav-link text-start" data-page="./pages/user-form.html" data-role="user">赛事报名</button>
<button class="nav-link text-start" data-page="./pages/audience-form.html" data-role="audience">观众报名</button>
<button class="nav-link text-start" data-page="./pages/admin-report.html" data-role="admin">综合看板</button>
<button class="nav-link text-start" data-page="./pages/admin-list.html" data-role="admin">项目管理</button>
<button class="nav-link text-start" data-page="./pages/admin-audience.html" data-role="admin">观众与核销</button>
<button class="nav-link text-start" data-page="./pages/admin-reviewers.html" data-role="admin">评审员管理</button>
</div>
</div>
</div>
</aside>
<main class="col-12 col-md-9 col-lg-10">
<iframe class="main-iframe" id="mainFrame" title="主内容区"></iframe>
</main>
</div>
</div>
<footer id="layoutFooter" class="layout-footer">
<div class="layout-footer-inner">
<div class="layout-footer-brand">首届沪苏协同新兴产业科创大赛</div>
<div class="layout-footer-meta">
1 month ago
<span>© 苏州市人民政府驻上海联络处&nbsp;&nbsp;苏州恒泰</span>
1 month ago
</div>
</div>
</footer>
<div class="modal fade" id="profileModal" tabindex="-1" aria-labelledby="profileModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered profile-modal-dialog">
<div class="modal-content profile-modal-content">
<div class="modal-header profile-modal-header">
<h5 id="profileModalTitle" class="modal-title profile-modal-title">个人信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body profile-modal-body">
<div class="row g-3">
<div class="col-md-6">
<div class="profile-field">
<div class="profile-field-label">姓名</div>
<div class="profile-field-value">王清萍</div>
</div>
</div>
<div class="col-md-6">
<div class="profile-field">
<div class="profile-field-label">手机号</div>
<div class="profile-field-value">13800138000</div>
</div>
</div>
<div class="col-md-6">
<div class="profile-field">
<div class="profile-field-label">单位</div>
<div class="profile-field-value">首届沪苏协同新兴产业科创大赛组委会</div>
</div>
</div>
<div class="col-md-6">
<div class="profile-field">
<div class="profile-field-label">邮箱</div>
<div class="profile-field-value">wangqp@example.com</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="myRegistrationsModal" tabindex="-1" aria-labelledby="myRegistrationsModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered my-registrations-modal-dialog">
<div class="modal-content profile-modal-content">
<div class="modal-header profile-modal-header">
<div>
<h5 id="myRegistrationsModalTitle" class="modal-title profile-modal-title">我的报名</h5>
<p class="my-registrations-modal-subtitle">查看已提交的活动报名信息</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div id="myRegistrationsModalBody" class="modal-body profile-modal-body my-registrations-modal-body"></div>
</div>
</div>
</div>
<div class="modal fade" id="myTicketModal" tabindex="-1" aria-labelledby="myTicketModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm"><div class="modal-content audience-ticket-card">
<div class="modal-header border-0"><h5 id="myTicketModalTitle" class="modal-title">电子入场票</h5><button class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button></div>
<div class="modal-body text-center pt-0"><span id="myTicketStatus" class="my-ticket-status"></span><h6 id="myTicketSession" class="mt-3 mb-1"></h6><p id="myTicketHolder" class="text-secondary small"></p><div id="myTicketQr" class="ticket-qr"></div><code id="myTicketCode" class="ticket-code"></code><p class="small text-secondary mt-3 mb-0">入场时请出示此码</p></div>
</div></div>
</div>
<script src="./assets/bootstrap.bundle.min.js"></script>
1 month ago
<script src="./assets/registration-i18n.js?v=20260819-4"></script>
1 month ago
<script>
const navButtons = Array.from(
document.querySelectorAll("#sideNav .nav-link, #mobileNav .mobile-nav-link")
);
const frame = document.getElementById("mainFrame");
const layoutParams = new URLSearchParams(window.location.search);
const role = layoutParams.get("role") || "user";
const isEnglishRegistration = layoutParams.get("lang") === "en" && (role === "user" || role === "audience");
const roleBadge = document.getElementById("roleBadge");
const isAdmin = role === "admin";
const isReviewer = role === "reviewer";
const isAudience = role === "audience";
const isManager = isAdmin || isReviewer;
const registrationLanguageQuery = isEnglishRegistration ? "?lang=en" : "";
const defaultPage = isReviewer ? "./pages/admin-list.html?role=reviewer" : (isAdmin ? "./pages/admin-report.html" : (isAudience ? "./pages/audience-form.html" + registrationLanguageQuery : "./pages/user-form.html" + registrationLanguageQuery));
const headerStatus = document.querySelector(".header-status");
const headerDeadline = document.querySelector(".header-deadline");
const headerMyRegistrationsButton = document.getElementById("headerMyRegistrationsButton");
const headerUserToggle = document.getElementById("headerUserToggle");
const profileMenuItem = document.getElementById("profileMenuItem");
const profileMenuLink = document.getElementById("profileMenuLink");
const profileMenuLabel = document.getElementById("profileMenuLabel");
const logoutMenuItem = document.querySelector(".header-user-menu-separator");
const myRegistrationsModalElement = document.getElementById("myRegistrationsModal");
const myRegistrationsModalBody = document.getElementById("myRegistrationsModalBody");
const verifiedMobile = localStorage.getItem("verifiedMobile") || "13800138000";
const demoAudienceRegistrations = [
{
sessionId: "shanghai-preliminary",
registeredAt: "2026年8月12日 10:26"
}
];
let frameResizeObserver;
const audienceSessionDetails = {
"shanghai-preliminary": {
name: "上海预赛路演",
date: "2026年9月18日(星期五)",
address: "上海市浦东新区张江科学会堂"
},
"suzhou-preliminary": {
name: "苏州预赛路演",
date: "2026年10月30日(星期五)",
address: "苏州市工业园区苏州国际博览中心"
},
"shenzhen-preliminary": {
name: "深圳预赛路演",
date: "2026年12月25日(星期五)",
address: "深圳市南山区深圳湾科技生态园"
},
"suzhou-final": {
name: "苏州总决赛",
date: "2027年3月19日(星期五)",
address: "苏州市工业园区金鸡湖国际会议中心"
}
};
function escapeHtml(value) {
return String(value ?? "").replace(/[&<>'"]/g, function (character) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" }[character];
});
}
function ticketCodeFor(registration) {
if (registration.ticketCode) return registration.ticketCode;
const prefix = { "shanghai-preliminary":"SH", "suzhou-preliminary":"SZ", "shenzhen-preliminary":"SZH", "suzhou-final":"FIN" }[registration.sessionId] || "EV";
let hash = 0;
(registration.sessionId + verifiedMobile).split("").forEach(function (char) { hash = (hash * 31 + char.charCodeAt(0)) % 1000000; });
return "AUD-" + prefix + "-2026-" + String(hash).padStart(6, "0");
}
function ticketQr(seed) {
let bits = "";
for (let i = 0; i < 225; i += 1) bits += ((seed.charCodeAt(i % seed.length) * 7 + i * 13) % 11 < 5) ? "<i></i>" : "<b></b>";
return bits;
}
function renderMyRegistrationsModal() {
let registrations = [];
try {
const storedRegistrations = localStorage.getItem("audienceRegistrations:" + verifiedMobile);
registrations = storedRegistrations === null
? demoAudienceRegistrations
: (JSON.parse(storedRegistrations) || []);
} catch (error) {
registrations = demoAudienceRegistrations;
}
const validRegistrations = registrations.filter(function (registration) {
return audienceSessionDetails[registration.sessionId];
});
if (validRegistrations.length === 0) {
myRegistrationsModalBody.innerHTML =
'<div class="my-registrations-modal-empty">' +
'<span class="my-registrations-modal-empty-icon" aria-hidden="true">暂无</span>' +
'<strong>暂无报名记录</strong>' +
'<p>选择可报名赛事并提交后,报名信息将在这里展示。</p>' +
'</div>';
return;
}
myRegistrationsModalBody.innerHTML =
'<div class="my-registrations-modal-summary"><span>已报名场次</span><strong>' + validRegistrations.length + '<small>场</small></strong></div>' +
'<div class="my-registrations-modal-list">' +
validRegistrations.map(function (registration) {
const session = audienceSessionDetails[registration.sessionId];
return '<article class="my-registration-modal-card">' +
'<div class="my-registration-modal-card-head">' +
'<h6>' + escapeHtml(session.name) + '</h6>' +
'<span><i aria-hidden="true"></i>报名成功</span>' +
'</div>' +
'<dl>' +
'<div><dt>活动时间</dt><dd>' + escapeHtml(session.date) + '</dd></div>' +
'<div><dt>活动地点</dt><dd>' + escapeHtml(session.address) + '</dd></div>' +
'<div><dt>报名时间</dt><dd>' + escapeHtml(registration.registeredAt || "—") + '</dd></div>' +
'</dl>' +
'<button class="btn btn-outline-primary btn-sm my-ticket-button" type="button" data-session-id="' + escapeHtml(registration.sessionId) + '">出示电子票</button>' +
'</article>';
}).join('') +
'</div>';
myRegistrationsModalBody.querySelectorAll('.my-ticket-button').forEach(function (button) {
button.addEventListener('click', function () {
const registration = validRegistrations.find(function (item) { return item.sessionId === button.dataset.sessionId; });
const session = audienceSessionDetails[registration.sessionId];
const code = ticketCodeFor(registration);
const checked = localStorage.getItem('audienceTicketChecked:' + code) === 'true' || registration.checkedIn;
document.getElementById('myTicketSession').textContent = session.name;
document.getElementById('myTicketHolder').textContent = (registration.holderName || verifiedMobile) + ' · ' + verifiedMobile;
document.getElementById('myTicketCode').textContent = code;
document.getElementById('myTicketQr').innerHTML = ticketQr(code);
const status = document.getElementById('myTicketStatus');
status.textContent = checked ? '已核销入场' : '待入场';
status.className = 'my-ticket-status ' + (checked ? 'is-checked' : 'is-pending');
bootstrap.Modal.getOrCreateInstance(myRegistrationsModalElement).hide();
bootstrap.Modal.getOrCreateInstance(document.getElementById('myTicketModal')).show();
});
});
}
roleBadge.textContent = isAdmin ? "管理员" : (isReviewer ? "评审员" : (isAudience ? verifiedMobile : "王清萍"));
if (isReviewer) {
document.body.classList.add("reviewer-mode");
headerStatus?.classList.add("d-none");
headerDeadline?.classList.add("d-none");
profileMenuItem?.remove();
logoutMenuItem?.classList.remove("header-user-menu-separator");
}
if (isAdmin) {
document.body.classList.add("admin-mode");
headerStatus?.classList.add("d-none");
headerDeadline?.classList.add("d-none");
profileMenuItem?.remove();
logoutMenuItem?.classList.remove("header-user-menu-separator");
}
if (isAudience) {
headerStatus?.classList.add("d-none");
headerDeadline?.classList.add("d-none");
headerMyRegistrationsButton?.classList.remove("d-none");
profileMenuItem?.remove();
logoutMenuItem?.classList.remove("header-user-menu-separator");
headerMyRegistrationsButton?.addEventListener("click", function () {
renderMyRegistrationsModal();
bootstrap.Modal.getOrCreateInstance(myRegistrationsModalElement).show();
});
}
if (!isManager && !isAudience) {
profileMenuItem?.remove();
logoutMenuItem?.classList.remove("header-user-menu-separator");
}
if (!isManager) {
document.body.classList.add("user-mobile-no-menu");
const sideMenu = document.querySelector(".layout-sider");
const mainContent = document.querySelector(".layout-main main");
sideMenu?.classList.add("d-none");
mainContent?.classList.remove("col-md-9", "col-lg-10");
}
function closeHeaderUserMenu() {
if (!headerUserToggle || headerUserToggle.getAttribute("aria-expanded") !== "true") return;
bootstrap.Dropdown.getOrCreateInstance(headerUserToggle).hide();
}
function resizeFrameToContent() {
try {
const doc = frame.contentDocument;
if (!doc?.documentElement || !doc.body) return;
doc.documentElement.style.overflow = "visible";
doc.body.style.overflow = "visible";
const contentHeight = Math.ceil(
Array.from(doc.body.children).reduce((bottom, element) => {
const styles = doc.defaultView?.getComputedStyle(element);
if (!styles || styles.display === "none" || styles.position === "fixed") return bottom;
return Math.max(bottom, element.getBoundingClientRect().bottom);
}, 0)
);
const headerHeight = document.querySelector(".layout-header")?.offsetHeight || 0;
const mobileNavHeight = document.getElementById("mobileNavSection")?.offsetHeight || 0;
const footerHeight = document.getElementById("layoutFooter")?.offsetHeight || 0;
const main = document.querySelector(".layout-main");
const mainStyles = main ? getComputedStyle(main) : null;
const mainVerticalPadding = mainStyles
? parseFloat(mainStyles.paddingTop) + parseFloat(mainStyles.paddingBottom)
: 0;
const viewportHeight = window.visualViewport?.height || window.innerHeight;
const availableHeight = viewportHeight
- headerHeight
- mobileNavHeight
- footerHeight
- mainVerticalPadding;
frame.style.height = Math.max(360, contentHeight, availableHeight) + "px";
} catch (error) {
// The prototype pages are same-origin; safely ignore if that changes later.
}
}
function bindFrameDocument() {
try {
const doc = frame.contentDocument;
if (!doc?.documentElement || !doc.body) {
requestAnimationFrame(bindFrameDocument);
return;
}
doc.addEventListener("pointerdown", closeHeaderUserMenu);
frameResizeObserver?.disconnect();
frameResizeObserver = new doc.defaultView.ResizeObserver(resizeFrameToContent);
frameResizeObserver.observe(doc.documentElement);
frameResizeObserver.observe(doc.body);
resizeFrameToContent();
if (frame.contentWindow.location.hash === "#my-registrations") {
requestAnimationFrame(function () {
const target = doc.getElementById("my-registrations");
if (!target) return;
target.focus({ preventScroll: true });
window.scrollTo({
top: frame.getBoundingClientRect().top + window.scrollY + target.offsetTop - 16,
behavior: "smooth"
});
});
}
} catch (error) {
// The prototype pages are same-origin; safely ignore if that changes later.
}
}
frame.addEventListener("load", bindFrameDocument);
frame.src = defaultPage;
if (isReviewer) {
navButtons.forEach((btn) => {
if (btn.dataset.page === "./pages/admin-list.html") {
btn.textContent = "项目评审";
}
});
}
function setActive(page) {
navButtons.forEach((btn) => {
btn.classList.toggle("active", btn.dataset.page === page && !btn.classList.contains("d-none"));
});
}
navButtons.forEach((btn) => {
const forUser = btn.dataset.role === "user";
const forAdmin = btn.dataset.role === "admin";
const forAudience = btn.dataset.role === "audience";
const isReportEntry = btn.dataset.page === "./pages/admin-report.html";
const isReviewerManageEntry = btn.dataset.page === "./pages/admin-reviewers.html";
const isAudienceCheckinEntry = btn.dataset.page === "./pages/admin-audience.html";
if ((isManager || isAudience) && forUser) btn.classList.add("d-none");
if (!isManager && forAdmin) btn.classList.add("d-none");
if (!isAudience && forAudience) btn.classList.add("d-none");
if (isReviewer && (isReportEntry || isReviewerManageEntry || isAudienceCheckinEntry)) btn.classList.add("d-none");
btn.addEventListener("click", () => {
const isRegistrationPage = btn.dataset.page === "./pages/user-form.html" || btn.dataset.page === "./pages/audience-form.html";
const targetPage = isReviewer && forAdmin
? (btn.dataset.page + "?role=reviewer")
: (btn.dataset.page + (isEnglishRegistration && isRegistrationPage ? "?lang=en" : ""));
frame.src = targetPage;
setActive(btn.dataset.page);
});
});
setActive(defaultPage);
window.addEventListener("resize", resizeFrameToContent);
window.addEventListener("orientationchange", resizeFrameToContent);
</script>
</body>
</html>