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.

291 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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" />
<script src="../assets/prototype-scale.js?v=20260819-hero"></script>
</head>
<body class="prototype-page audience-page">
<div class="container-fluid audience-page-shell">
<div class="mb-3 form-page-header">
<h5 class="mb-0 form-page-title">观众报名</h5>
</div>
<section class="audience-session-section" aria-label="赛事场次">
<div id="sessionOverview" class="row g-3"></div>
</section>
<div class="audience-form-panel">
<div class="audience-form-body">
<form id="audienceForm" class="row g-3" novalidate>
<div class="col-12 col-md-6 audience-form-fifth">
<label class="form-label" for="audienceName">个人姓名 <span class="text-danger">*</span></label>
<input id="audienceName" class="form-control audience-entry-field" autocomplete="name" placeholder="请先选择赛事" required disabled />
<div class="invalid-feedback">请输入个人姓名</div>
</div>
<div class="col-12 col-md-6 audience-form-fifth">
<label class="form-label" for="audiencePhone">联系电话 <span class="text-danger">*</span></label>
<input id="audiencePhone" class="form-control audience-entry-field" type="tel" inputmode="numeric" autocomplete="tel" pattern="^1[3-9]\d{9}$" placeholder="请先选择赛事" required disabled />
<div class="invalid-feedback">请输入正确的11位手机号</div>
</div>
<div class="col-12 col-md-6 audience-form-fifth">
<label class="form-label" for="audienceCompany">所属单位</label>
<input id="audienceCompany" class="form-control audience-entry-field" autocomplete="organization" placeholder="请先选择赛事" disabled />
</div>
<div class="col-12 col-md-6 audience-form-fifth">
<label class="form-label" for="audienceJob">个人职务</label>
<input id="audienceJob" class="form-control audience-entry-field" autocomplete="organization-title" placeholder="请先选择赛事" disabled />
</div>
<div class="col-12 col-md-6 audience-form-fifth">
<label class="form-label" for="audiencePurpose">观赛目的 <span class="text-danger">*</span></label>
<select id="audiencePurpose" class="form-select audience-entry-field" required disabled><option value="">请先选择赛事</option><option>项目交流</option><option>产业合作</option><option>投资对接</option><option>学习参访</option><option>媒体报道</option><option>其他</option></select>
<div class="invalid-feedback">请选择观赛目的</div>
</div>
<div class="col-12 audience-form-actions">
<button id="audienceSubmit" class="btn btn-primary audience-submit-btn" type="submit" disabled>请先选择赛事</button>
</div>
</form>
<div id="audienceSuccess" class="alert alert-success audience-success d-none" role="status"></div>
</div>
</div>
</div>
<div class="modal fade" id="audienceTicketModal" tabindex="-1" aria-labelledby="audienceTicketTitle" 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="audienceTicketTitle" class="modal-title">报名成功</h5><button class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button></div>
<div class="modal-body text-center pt-0">
<div class="audience-ticket-success-mark">✓</div><p class="audience-ticket-lead">电子入场票已生成</p>
<span class="ticket-event-badge">沪苏科创大赛</span><h6 id="audienceTicketSession" class="mt-3 mb-1"></h6><p id="audienceTicketHolder" class="text-secondary small mb-0"></p>
<div id="audienceTicketQr" class="ticket-qr" aria-label="电子票二维码"></div><code id="audienceTicketCode" class="ticket-code"></code>
<p class="small text-secondary mt-3 mb-3">入场时请出示此码,每张票仅可核销一次</p>
<button class="btn btn-primary w-100" type="button" data-bs-dismiss="modal">完成</button>
</div>
</div></div>
</div>
<script src="../assets/bootstrap.bundle.min.js"></script>
<script src="../assets/registration-i18n.js?v=20260819-4"></script>
<script>
const audienceSessions = [
{
id: 'shanghai-preliminary',
name: '上海预赛路演',
signupStart: '2026-08-10',
signupEnd: '2026-09-10',
date: '2026年9月18日(星期五)',
address: '上海市浦东新区张江科学会堂',
capacity: 300,
registered: 214
},
{
id: 'suzhou-preliminary',
name: '苏州预赛路演',
signupStart: '2026-09-15',
signupEnd: '2026-10-20',
date: '2026年10月30日(星期五)',
address: '苏州市工业园区苏州国际博览中心',
capacity: 260,
registered: 198
},
{
id: 'shenzhen-preliminary',
name: '深圳预赛路演',
signupStart: '2026-11-01',
signupEnd: '2026-12-18',
date: '2026年12月25日(星期五)',
address: '深圳市南山区深圳湾科技生态园',
capacity: 200,
registered: 126
},
{
id: 'suzhou-final',
name: '苏州总决赛',
signupStart: '2027-02-01',
signupEnd: '2027-03-10',
date: '2027年3月19日(星期五)',
address: '苏州市工业园区金鸡湖国际会议中心',
capacity: 500,
registered: 347
}
];
const sessionOverview = document.getElementById('sessionOverview');
const entryFields = Array.from(document.querySelectorAll('.audience-entry-field'));
const submitButton = document.getElementById('audienceSubmit');
const isEnglishAudience = document.documentElement.lang === 'en';
const fieldPlaceholders = isEnglishAudience
? ['Enter your full name', 'Enter an 11-digit mobile number', 'Enter your organization', 'Enter your job title']
: ['请输入姓名', '请输入11位手机号', '请输入单位名称', '请输入职务'];
const verifiedMobile = localStorage.getItem('verifiedMobile') || '13800138000';
const registrationKey = 'audienceRegistrations:' + verifiedMobile;
const quotaKey = 'audienceSessionRegisteredCounts';
function createTicketCode(sessionId, mobile) {
const prefix = { 'shanghai-preliminary':'SH', 'suzhou-preliminary':'SZ', 'shenzhen-preliminary':'SZH', 'suzhou-final':'FIN' }[sessionId] || 'EV';
let hash = 0;
(sessionId + mobile).split('').forEach(function (char) { hash = (hash * 31 + char.charCodeAt(0)) % 1000000; });
return 'AUD-' + prefix + '-2026-' + String(hash).padStart(6, '0');
}
function makeTicketQr(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 showTicket(registration, session) {
document.getElementById('audienceTicketSession').textContent = session.name;
document.getElementById('audienceTicketHolder').textContent = document.getElementById('audienceName').value + ' · ' + verifiedMobile;
document.getElementById('audienceTicketCode').textContent = registration.ticketCode;
document.getElementById('audienceTicketQr').innerHTML = makeTicketQr(registration.ticketCode);
bootstrap.Modal.getOrCreateInstance(document.getElementById('audienceTicketModal')).show();
}
function getRegistrations() {
try {
return JSON.parse(localStorage.getItem(registrationKey)) || [];
} catch (error) {
return [];
}
}
function getRegisteredCounts() {
try {
return JSON.parse(localStorage.getItem(quotaKey)) || {};
} catch (error) {
return {};
}
}
function remainingCount(session) {
const counts = getRegisteredCounts();
return Math.max(0, session.capacity - (counts[session.id] ?? session.registered));
}
function getShanghaiDate() {
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(new Date());
const values = {};
parts.forEach(function (part) {
values[part.type] = part.value;
});
return values.year + '-' + values.month + '-' + values.day;
}
function shortDate(date) {
return date.slice(5).replace('-', '.');
}
const today = getShanghaiDate();
let selectedSessionId = '';
function getSessionState(session) {
const isNotStarted = today < session.signupStart;
const isExpired = today > session.signupEnd;
const isFull = remainingCount(session) === 0;
return {
isNotStarted: isNotStarted,
isExpired: isExpired,
isFull: isFull,
isAvailable: !isNotStarted && !isExpired && !isFull
};
}
function renderSessionOverview() {
const registrations = getRegistrations();
sessionOverview.innerHTML = audienceSessions.map(function (session) {
const state = getSessionState(session);
const isRegistered = registrations.some(function (item) { return item.sessionId === session.id; });
const remaining = remainingCount(session);
const isFinal = session.id === 'suzhou-final';
const statusText = isRegistered ? '已报名' : (state.isFull ? '已满额' : (state.isExpired ? '已截止' : (state.isNotStarted ? shortDate(session.signupStart) + ' 开放' : '报名中')));
const statusClass = isRegistered ? 'is-registered' : (state.isAvailable ? 'is-open' : (state.isFull ? 'is-full' : ''));
return '<div class="col-12 col-md-6 col-xl-3">' +
'<article class="audience-session-card' + (isFinal ? ' is-final' : '') + (isRegistered ? ' is-registered' : '') + '" role="radio" tabindex="0" aria-checked="' + (selectedSessionId === session.id) + '" aria-disabled="' + isRegistered + '" data-session-id="' + session.id + '">' +
'<div class="audience-session-card-head"><div class="audience-session-name-wrap"><h6>' + session.name + '</h6></div><span class="audience-status-badge ' + statusClass + '">' + statusText + '</span></div>' +
'<div class="audience-quota"><strong>' + remaining + '</strong><span>人</span><small>剩余可报名</small></div>' +
'<div class="audience-session-meta"><span>' + session.date + '</span><span>' + session.address + '</span></div>' +
'<div class="audience-session-selected">✓ 已选择</div>' +
'</article></div>';
}).join('');
}
function selectSession(sessionId) {
const session = audienceSessions.find(function (item) { return item.id === sessionId; });
const isRegistered = getRegistrations().some(function (item) { return item.sessionId === sessionId; });
if (!session || isRegistered || !getSessionState(session).isAvailable) return;
selectedSessionId = sessionId;
entryFields.forEach(function (field, index) {
field.disabled = false;
if (field.tagName === 'SELECT') {
field.options[0].textContent = isEnglishAudience ? 'Please select a purpose' : '请选择观赛目的';
} else {
field.placeholder = fieldPlaceholders[index];
}
});
submitButton.disabled = false;
submitButton.textContent = isEnglishAudience ? 'Submit Registration' : '提交观众报名';
renderSessionOverview();
document.getElementById('audienceName').focus({ preventScroll: true });
}
renderSessionOverview();
sessionOverview.addEventListener('click', function (event) {
const card = event.target.closest('.audience-session-card');
if (card) selectSession(card.dataset.sessionId);
});
sessionOverview.addEventListener('keydown', function (event) {
const card = event.target.closest('.audience-session-card');
if (card && (event.key === 'Enter' || event.key === ' ')) {
event.preventDefault();
selectSession(card.dataset.sessionId);
}
});
document.getElementById('audienceForm').addEventListener('submit', function (event) {
event.preventDefault();
this.classList.add('was-validated');
if (this.checkValidity()) {
const session = audienceSessions.find(function (item) { return item.id === selectedSessionId; });
if (!session || remainingCount(session) === 0) return;
const registrations = getRegistrations();
const isAlreadyRegistered = registrations.some(function (item) { return item.sessionId === session.id; });
let currentRegistration = registrations.find(function (item) { return item.sessionId === session.id; });
if (!isAlreadyRegistered) {
currentRegistration = {
sessionId: session.id,
registeredAt: new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', dateStyle: 'medium', timeStyle: 'short' }).format(new Date()),
ticketCode: createTicketCode(session.id, verifiedMobile),
holderName: document.getElementById('audienceName').value,
phone: verifiedMobile,
checkedIn: false
};
registrations.unshift(currentRegistration);
localStorage.setItem(registrationKey, JSON.stringify(registrations));
const counts = getRegisteredCounts();
counts[session.id] = (counts[session.id] ?? session.registered) + 1;
localStorage.setItem(quotaKey, JSON.stringify(counts));
}
const success = document.getElementById('audienceSuccess');
success.textContent = isAlreadyRegistered
? '您已报名“' + session.name + '”。'
: '报名成功!“' + session.name + '”的场次信息将通过手机号发送。';
success.classList.remove('d-none');
renderSessionOverview();
if (!currentRegistration.ticketCode) {
currentRegistration.ticketCode = createTicketCode(session.id, verifiedMobile);
localStorage.setItem(registrationKey, JSON.stringify(registrations));
}
showTicket(currentRegistration, session);
}
});
</script>
</body>
</html>