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

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 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>
1 month ago
<script src="../assets/registration-i18n.js?v=20260819-4"></script>
1 month ago
<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>