|
|
<!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="icon" type="image/png" href="/favicon.png?v=20260829c">
|
|
|
<link rel="stylesheet" href="./assets/bootstrap.min.css" />
|
|
|
<link rel="stylesheet" href="./assets/prototype.css" />
|
|
|
</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-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-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">
|
|
|
<span>© 苏州市人民政府驻上海联络处 苏州恒泰控股集团有限公司</span>
|
|
|
</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>
|
|
|
|
|
|
<script src="./assets/bootstrap.bundle.min.js"></script>
|
|
|
<script>
|
|
|
const navButtons = Array.from(
|
|
|
document.querySelectorAll("#sideNav .nav-link, #mobileNav .mobile-nav-link")
|
|
|
);
|
|
|
const frame = document.getElementById("mainFrame");
|
|
|
const role = new URLSearchParams(window.location.search).get("role") || "user";
|
|
|
const roleBadge = document.getElementById("roleBadge");
|
|
|
const isAdmin = role === "admin";
|
|
|
const isReviewer = role === "reviewer";
|
|
|
const isAudience = role === "audience";
|
|
|
const isManager = isAdmin || isReviewer;
|
|
|
const defaultPage = isReviewer ? "./pages/admin-list.html?role=reviewer" : (isAdmin ? "./pages/admin-report.html" : (isAudience ? "./pages/audience-form.html" : "./pages/user-form.html"));
|
|
|
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: "2027年1月8日(星期五)",
|
|
|
address: "深圳市南山区深圳湾科技生态园"
|
|
|
},
|
|
|
"suzhou-final": {
|
|
|
name: "苏州总决赛",
|
|
|
date: "2027年3月19日(星期五)",
|
|
|
address: "苏州市工业园区金鸡湖国际会议中心"
|
|
|
}
|
|
|
};
|
|
|
|
|
|
function escapeHtml(value) {
|
|
|
return String(value ?? "").replace(/[&<>'"]/g, function (character) {
|
|
|
return { "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[character];
|
|
|
});
|
|
|
}
|
|
|
|
|
|
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>' +
|
|
|
'</article>';
|
|
|
}).join('') +
|
|
|
'</div>';
|
|
|
}
|
|
|
|
|
|
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";
|
|
|
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)) btn.classList.add("d-none");
|
|
|
btn.addEventListener("click", () => {
|
|
|
const targetPage = isReviewer && forAdmin ? (btn.dataset.page + "?role=reviewer") : btn.dataset.page;
|
|
|
frame.src = targetPage;
|
|
|
setActive(btn.dataset.page);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
setActive(defaultPage);
|
|
|
window.addEventListener("resize", resizeFrameToContent);
|
|
|
window.addEventListener("orientationchange", resizeFrameToContent);
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|