|
|
<!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>
|