Files
check_in/templates/index.html
2026-01-28 23:43:12 +08:00

1112 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ config.event_title }} - 签到</title>
<style>
:root {
--primary-color: {{ config.primary_color }};
--secondary-color: {{ config.secondary_color }};
--bg-color: {{ config.bg_color }};
--card-bg: rgba(12, 24, 50, 0.5); /* Glassy blue tint */
--text-color: #ffffff;
--text-muted: #b0c4de; /* Light blue-grey */
--accent-glow: rgba(0, 242, 255, 0.6);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
/* Deep radial gradient mimicking the poster's atmosphere */
background-image:
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
}
/* Add a subtle grid or tech-line overlay if desired, but keep it clean for now */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
z-index: -1;
}
.container {
width: 100%;
max-width: 600px;
padding: 30px 20px;
position: relative;
z-index: 1;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0, 242, 255, 0.2);
}
.header-img {
width: 100%;
max-height: 220px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.2);
border: 1px solid rgba(0, 242, 255, 0.3);
}
h1 {
font-size: 2.4rem;
margin-bottom: 15px;
/* Poster style: Bold white/cyan gradient text */
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
letter-spacing: 1px;
text-shadow: 0 0 30px rgba(0, 242, 255, 0.4);
position: relative;
}
.event-info {
font-size: 0.95rem;
color: #dbe4ff;
line-height: 1.7;
margin-top: 20px;
text-align: left;
background: rgba(0, 20, 60, 0.4);
padding: 20px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
backdrop-filter: blur(5px);
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 30px;
margin-bottom: 25px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0, 242, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 242, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.6), inset 0 0 20px rgba(0, 242, 255, 0.1);
}
h2 {
color: #fff;
text-align: center;
margin-bottom: 25px;
font-weight: 700;
letter-spacing: 1px;
}
.input-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 10px;
font-size: 0.95rem;
color: var(--primary-color);
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 14px 16px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 8px;
color: white;
font-size: 1rem;
outline: none;
transition: all 0.3s;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
}
input:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3), inset 0 2px 5px rgba(0,0,0,0.3);
background: rgba(0, 0, 0, 0.6);
}
/* Placeholder styling */
::placeholder {
color: rgba(255, 255, 255, 0.3);
}
button {
width: 100%;
padding: 16px;
border: none;
border-radius: 8px;
/* Gradient button */
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
color: #fff;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
button:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
background: linear-gradient(90deg, #0040cc 0%, #00d0dd 100%);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #2a3b55;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
}
.hidden {
display: none;
}
.error-msg {
color: #ff6b6b;
font-size: 0.9rem;
margin-top: 15px;
text-align: center;
background: rgba(255, 107, 107, 0.1);
padding: 10px;
border-radius: 6px;
border: 1px solid rgba(255, 107, 107, 0.3);
}
.success-msg {
color: #00ff88;
font-size: 1.8rem;
text-align: center;
margin: 20px 0 10px;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.seat-info {
font-size: 3rem;
font-weight: 800;
color: #fff;
margin: 30px 0;
text-shadow: 0 0 30px var(--primary-color);
padding: 30px;
border: 2px solid var(--primary-color);
border-radius: 16px;
background: rgba(0, 242, 255, 0.05);
box-shadow: 0 0 40px rgba(0, 242, 255, 0.15), inset 0 0 20px rgba(0, 242, 255, 0.1);
position: relative;
overflow: hidden;
}
/* Shine effect on seat info */
.seat-info::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shine 3s infinite;
}
@keyframes shine {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
}
.user-preview {
background: rgba(0, 81, 255, 0.15);
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
border: 1px solid rgba(0, 81, 255, 0.4);
text-align: center;
}
.user-preview h3 {
margin-bottom: 8px;
font-size: 1.3rem;
color: #fff;
}
.user-preview p {
color: #b0c4de;
font-size: 0.95rem;
}
/* Loading spinner */
.loader {
border: 3px solid rgba(255,255,255,0.1);
border-radius: 50%;
border-top: 3px solid var(--primary-color);
width: 20px;
height: 20px;
animation: spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.user-select-item {
padding: 15px;
background: rgba(255,255,255,0.05);
margin-bottom: 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.user-select-item:hover {
background: rgba(0, 242, 255, 0.1);
border-color: var(--primary-color);
}
/* Tablemates styling */
.tablemates-section {
margin-top: 25px;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 20px;
}
.tablemate-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 12px;
margin-bottom: 10px;
display: flex;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s;
cursor: pointer;
position: relative;
}
.tablemate-card:hover {
transform: translateX(5px);
background: rgba(255, 255, 255, 0.08);
border-color: var(--primary-color);
}
.tablemate-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
font-size: 1.2rem;
flex-shrink: 0;
}
.tablemate-info {
text-align: left;
flex-grow: 1;
}
.tablemate-name {
font-weight: bold;
color: #fff;
font-size: 1rem;
}
.tablemate-industry {
font-size: 0.8rem;
color: var(--text-muted);
}
.click-hint {
font-size: 0.7rem;
color: var(--primary-color);
opacity: 0.7;
margin-left: 10px;
}
/* Modal Styling */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.8);
backdrop-filter: blur(5px);
}
.modal-content {
background: var(--bg-color);
margin: 15% auto;
padding: 25px;
border: 1px solid var(--primary-color);
border-radius: 15px;
width: 90%;
max-width: 500px;
box-shadow: 0 0 30px rgba(0, 242, 255, 0.3);
position: relative;
animation: modalOpen 0.3s;
}
@keyframes modalOpen {
from {opacity: 0; transform: translateY(-50px);}
to {opacity: 1; transform: translateY(0);}
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: var(--primary-color);
text-decoration: none;
cursor: pointer;
}
.detail-item {
margin-bottom: 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 10px;
}
.detail-label {
color: var(--primary-color);
font-size: 0.85rem;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.detail-value {
color: #fff;
font-size: 1rem;
line-height: 1.5;
}
.important-warning {
color: #ff3333 !important;
font-size: 1.3rem !important;
font-weight: 900 !important;
margin-top: 20px;
text-align: center;
background: rgba(255, 0, 0, 0.2) !important;
padding: 25px !important;
border-radius: 12px !important;
border: 2px solid #ff0000 !important;
box-shadow: 0 0 20px rgba(255, 0, 0, 0.4) !important;
animation: pulse-red 2s infinite;
text-shadow: 0 0 5px rgba(0,0,0,0.5);
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
}
</style>
</head>
<body>
<div class="container">
<header>
<img src="{{ config.header_image }}" alt="{{ config.event_title }}" class="header-img" onerror="this.style.display='none'">
<div style="text-align: center; margin-bottom: 10px; color: var(--primary-color); font-size: 0.8rem; letter-spacing: 2px; font-weight: 600; opacity: 0.9;">
{{ config.event_sub_title }}
</div>
<h1>{{ config.event_title }}</h1>
<div class="event-info">
<p>📅 <strong>时间:</strong>{{ config.event_time }}</p>
<p>📍 <strong>地点:</strong>{{ config.event_location }}</p>
<p>💡 <strong>内容:</strong>{{ config.event_content }}</p>
</div>
</header>
<!-- Step 1: Search -->
<div id="step-search" class="card">
<h2 style="margin-bottom: 20px;">嘉宾签到</h2>
<div class="input-group">
<label>请输入手机号码或姓名</label>
<input type="text" id="search-input" placeholder="例如13800000000 或 张三">
</div>
<button onclick="searchUser()" id="search-btn">签到</button>
<div id="search-error" class="error-msg hidden"></div>
<div id="user-list" class="hidden" style="margin-top: 15px;"></div>
{% if config.enable_payment %}
<div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); text-align: center;">
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 15px;">尚未购票?</p>
<a href="/ticket" style="text-decoration: none;">
<button style="background: transparent; border: 1px solid var(--primary-color); color: var(--primary-color);">
购票报名 (¥{{ config.payment_amount }})
</button>
</a>
</div>
{% endif %}
</div>
<!-- Step 2: Fill Info -->
<div id="step-form" class="card hidden">
<div class="user-preview">
<h3 id="display-name"></h3>
<p id="display-phone"></p>
<p id="display-company" style="font-size: 0.8rem; margin-top: 5px;"></p>
</div>
<form id="checkin-form" onsubmit="submitCheckin(event)">
<input type="hidden" id="gsdh-id">
<div class="input-group">
<label>姓名</label>
<input type="text" id="form-name" required>
</div>
<div class="input-group">
<label>手机号码 (不公开)</label>
<input type="text" id="form-phone" required>
</div>
<div class="input-group hidden" id="sms-verification-group">
<label>验证码</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="form-verification-code" placeholder="输入验证码" style="flex: 1;">
<button type="button" id="send-checkin-code-btn" onclick="sendCheckinSms()" style="width: auto; padding: 14px; background: var(--secondary-color); margin-bottom: 0; border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: bold;">发送验证码</button>
</div>
</div>
<div class="input-group hidden" id="group-company">
<label id="label-company">单位名称</label>
<input type="text" id="form-company" placeholder="点此输入">
</div>
<div class="input-group hidden" id="group-position">
<label id="label-position">职务</label>
<input type="text" id="form-position" placeholder="点此输入">
</div>
<div class="input-group hidden" id="group-business">
<label id="label-business">公司主要经营 / 业务</label>
<textarea id="form-business" rows="2" placeholder="点此输入"></textarea>
</div>
<div class="input-group hidden" id="group-vision">
<label id="label-vision">2026年业务愿景</label>
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)"></textarea>
</div>
<input type="hidden" id="form-location" value="">
<button type="submit" id="submit-btn">确认签到</button>
</form>
<div id="submit-error" class="error-msg hidden"></div>
</div>
<!-- Step 3: Success -->
<div id="step-success" class="card hidden" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 4rem; margin-bottom: 20px;">🎉</div>
<h2 class="success-msg">签到成功!</h2>
<p id="success-seat-label" style="color: var(--text-muted); margin-bottom: 20px;">您的座位已分配</p>
<div id="seat-display" class="seat-info">
<!-- Seat number will be inserted here -->
</div>
<div id="tablemates-container" class="tablemates-section hidden">
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
<div id="tablemates-list">
<!-- Tablemates inserted here -->
</div>
</div>
<p style="margin-top: 20px;">请入座等待会议开始</p>
<button id="success-btn" onclick="window.location.href='/wall'" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">前往活动</button>
</div>
<!-- Step 4: Already Signed -->
<div id="step-already-signed" class="card hidden" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 4rem; margin-bottom: 20px;"></div>
<h2 class="success-msg" style="color: #00f2ff;">您已完成签到</h2>
<div class="user-preview" style="margin: 20px 0; background: rgba(255,255,255,0.05); border: none;">
<h3 id="signed-name" style="margin-bottom: 5px;"></h3>
<p id="signed-phone" style="opacity: 0.7;"></p>
</div>
<p id="signed-seat-label" style="color: var(--text-muted); margin-bottom: 20px;">您的座位是</p>
<div id="signed-seat-display" class="seat-info">
<!-- Seat number will be inserted here -->
</div>
<div id="signed-tablemates-container" class="tablemates-section hidden">
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
<div id="signed-tablemates-list">
<!-- Tablemates inserted here -->
</div>
</div>
<p style="margin-top: 20px;">请入座等待会议开始</p>
<button id="signed-btn" onclick="window.location.href='/wall'" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">前往活动</button>
</div>
</div>
<!-- Modal for details -->
<div id="detail-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<div style="text-align: center; margin-bottom: 20px;">
<div id="modal-avatar" style="width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; margin-bottom: 10px;"></div>
<h2 id="modal-name" style="color: #fff; margin-bottom: 5px;"></h2>
<p id="modal-industry" style="color: var(--primary-color); font-size: 0.9rem;"></p>
</div>
<div class="detail-item">
<div class="detail-label">单位名称</div>
<div id="modal-company" class="detail-value"></div>
</div>
<div class="detail-item">
<div class="detail-label">职位</div>
<div id="modal-position" class="detail-value"></div>
</div>
<div class="detail-item">
<div class="detail-label">公司主要经营 / 业务</div>
<div id="modal-business" class="detail-value"></div>
</div>
<div class="detail-item" style="border-bottom: none;">
<div class="detail-label">2026年业务愿景</div>
<div id="modal-vision" class="detail-value"></div>
</div>
</div>
</div>
<script>
// Store selected user data
let currentUser = null;
// Inject Config
const CONFIG = {{ config | tojson }};
let isCheckinCodeVerified = false;
async function refreshConfig() {
try {
const res = await fetch('/api/admin/config');
if (res.ok) {
const newConfig = await res.json();
// Update global CONFIG object
Object.assign(CONFIG, newConfig);
applyConfigUI();
}
} catch (e) {
console.error("Failed to refresh config", e);
}
}
function applyConfigUI() {
const smsGroup = document.getElementById('sms-verification-group');
if (smsGroup) {
if (CONFIG.enable_sms_verification) {
smsGroup.classList.remove('hidden');
} else {
smsGroup.classList.add('hidden');
}
}
// Apply field config (Show/Hide/Required)
const fc = CONFIG.checkin_field_config || {};
const fields = [
{ key: 'company_name', groupId: 'group-company', inputId: 'form-company', labelId: 'label-company' },
{ key: 'position', groupId: 'group-position', inputId: 'form-position', labelId: 'label-position' },
{ key: 'business_scope', groupId: 'group-business', inputId: 'form-business', labelId: 'label-business' },
{ key: 'vision_2026', groupId: 'group-vision', inputId: 'form-vision', labelId: 'label-vision' }
];
fields.forEach(field => {
const conf = fc[field.key] || { show: false, required: false, label: '' };
const group = document.getElementById(field.groupId);
const input = document.getElementById(field.inputId);
const label = document.getElementById(field.labelId);
if (group && input) {
// Visibility
if (conf.show) {
group.classList.remove('hidden');
} else {
group.classList.add('hidden');
}
// Required
input.required = conf.required === true;
// Label
if (label && conf.label) {
label.textContent = conf.label;
}
}
});
}
function updateCheckinButtonState() {
const btn = document.getElementById('submit-btn');
const codeInput = document.getElementById('form-verification-code');
// Check if loading (by checking loader class or text content)
if (btn.innerHTML.includes('loader')) return;
if (codeInput) {
if (isCheckinCodeVerified && codeInput.value.trim().length === 4) {
btn.disabled = false;
} else {
btn.disabled = true;
}
} else {
btn.disabled = false;
}
}
async function verifyCheckinCode() {
const codeInput = document.getElementById('form-verification-code');
const phoneInput = document.getElementById('form-phone');
const errorDiv = document.getElementById('submit-error');
if (!codeInput) return;
const code = codeInput.value.trim();
const phone = phoneInput ? phoneInput.value.trim() : '';
if (code.length === 4 && phone) {
try {
const res = await fetch('/api/verify-sms-code', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({phone: phone, code: code})
});
const data = await res.json();
if (data.success) {
isCheckinCodeVerified = true;
errorDiv.classList.add('hidden');
} else {
isCheckinCodeVerified = false;
errorDiv.textContent = "验证码错误";
errorDiv.classList.remove('hidden');
}
} catch(e) {
isCheckinCodeVerified = false;
}
} else {
isCheckinCodeVerified = false;
if (code.length < 4) errorDiv.classList.add('hidden');
}
updateCheckinButtonState();
}
window.addEventListener('DOMContentLoaded', () => {
applyConfigUI();
const codeInput = document.getElementById('form-verification-code');
const phoneInput = document.getElementById('form-phone');
if (codeInput) {
codeInput.addEventListener('input', verifyCheckinCode);
// Initial check
updateCheckinButtonState();
}
if (phoneInput) {
phoneInput.addEventListener('input', () => {
isCheckinCodeVerified = false;
updateCheckinButtonState();
});
}
});
async function searchUser() {
const query = document.getElementById('search-input').value.trim();
const btn = document.getElementById('search-btn');
const errorDiv = document.getElementById('search-error');
const listDiv = document.getElementById('user-list');
if (!query) {
errorDiv.textContent = "请输入查询内容";
errorDiv.classList.remove('hidden');
return;
}
// Reset UI
errorDiv.classList.add('hidden');
errorDiv.classList.remove('important-warning');
listDiv.classList.add('hidden');
listDiv.innerHTML = '';
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span> 查询中...';
try {
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`);
const data = await response.json();
if (response.ok) {
if (data.allow_create) {
await refreshConfig();
const u = data.user || {};
const tempId = 'TEMP_' + Date.now();
currentUser = { new_id: tempId };
document.getElementById('gsdh-id').value = tempId;
document.getElementById('form-name').value = u.name || (!/^\d+$/.test(query) ? query : '');
document.getElementById('form-phone').value = u.phone || (/^\d+$/.test(query) ? query : '');
document.getElementById('display-name').textContent = document.getElementById('form-name').value || '新嘉宾';
document.getElementById('display-phone').textContent = document.getElementById('form-phone').value;
document.getElementById('display-company').textContent = '现场注册';
document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-form').classList.remove('hidden');
return;
}
if (data.multiple) {
// Handle multiple results
listDiv.classList.remove('hidden');
listDiv.innerHTML = '<p style="margin-bottom:10px; color:var(--text-muted)">查询到多位嘉宾,请选择:</p>';
data.users.forEach(user => {
const item = document.createElement('div');
item.className = 'user-select-item';
item.innerHTML = `<strong>${user.name}</strong> <span style="font-size:0.8em; opacity:0.7">(${user.phone})</span>`;
item.onclick = () => selectUser(user);
listDiv.appendChild(item);
});
} else if (data.already_signed) {
// Show already signed screen
document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-already-signed').classList.remove('hidden');
document.getElementById('signed-name').textContent = data.user.name;
document.getElementById('signed-phone').textContent = data.user.phone;
if (CONFIG.enable_seating !== false) {
document.getElementById('signed-seat-display').textContent = data.seat || "自由席";
document.getElementById('signed-seat-display').style.display = 'block';
document.getElementById('signed-seat-label').textContent = "您的座位是";
document.getElementById('signed-seat-label').style.display = 'block';
// Show tablemates if available
if (data.tablemates && data.tablemates.length > 0) {
renderTablemates(data.tablemates, 'signed-tablemates-list', 'signed-tablemates-container');
} else {
document.getElementById('signed-tablemates-container').classList.add('hidden');
}
} else {
document.getElementById('signed-seat-display').style.display = 'none';
document.getElementById('signed-seat-label').textContent = "签到成功,请自由入座";
document.getElementById('signed-tablemates-container').classList.add('hidden');
}
} else {
// Single user found
await refreshConfig();
selectUser(data.user);
}
} else {
errorDiv.textContent = data.message || "查询失败";
errorDiv.classList.remove('hidden');
errorDiv.style.color = '#ff4d4d';
}
} catch (err) {
errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = '查询';
}
}
async function selectUser(user) {
// Check if fee is 0 (unpaid)
// Ensure fee is treated as number. If fee is missing or 0, block checkin.
// CONFIG.payment_amount is usually 0.01 or 26.
const fee = parseFloat(user.fee || 0);
if (fee <= 0) {
const errorDiv = document.getElementById('search-error');
const amount = CONFIG.payment_amount || 26;
errorDiv.textContent = `⚠️ 无法签到:请先前往签到处付费${amount}`;
errorDiv.classList.remove('hidden');
errorDiv.classList.add('important-warning');
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
currentUser = user;
// Populate form
document.getElementById('gsdh-id').value = user.new_id;
const setVal = (id, val) => {
const el = document.getElementById(id);
if (el) el.value = val || '';
};
setVal('form-name', user.name);
setVal('form-phone', user.phone);
setVal('form-company', user.industry_company);
// Clear other optional fields to avoid carrying over data if user switches
setVal('form-position', user.position); // Note: user.position might not exist in search result
setVal('form-business', user.business_scope);
setVal('form-vision', user.vision_2026);
// Display preview
if (document.getElementById('display-name')) document.getElementById('display-name').textContent = user.name;
if (document.getElementById('display-phone')) document.getElementById('display-phone').textContent = user.phone;
if (document.getElementById('display-company')) document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
// Ensure UI reflects current config (e.g. SMS button)
applyConfigUI();
// Switch steps
document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-form').classList.remove('hidden');
updateCheckinButtonState();
}
async function submitCheckin(e) {
e.preventDefault();
const btn = document.getElementById('submit-btn');
const errorDiv = document.getElementById('submit-error');
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span> 提交中...';
errorDiv.classList.add('hidden');
// Helper to safe get value
const getVal = (id) => {
const el = document.getElementById(id);
return el ? el.value : '';
};
const payload = {
gsdh_id: getVal('gsdh-id'),
name: getVal('form-name'),
phone: getVal('form-phone'),
verification_code: getVal('form-verification-code'),
company_name: getVal('form-company'),
position: getVal('form-position'),
business_scope: getVal('form-business'),
vision_2026: getVal('form-vision'),
location: getVal('form-location')
};
try {
const response = await fetch('/api/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
document.getElementById('success-btn').textContent = payload.name + '/点击进入活动大厅';
if (CONFIG.enable_seating !== false) {
// Show assigned seat
document.getElementById('seat-display').textContent = data.seat || "自由席";
document.getElementById('seat-display').style.display = 'block';
document.getElementById('success-seat-label').textContent = "您的座位已分配";
document.getElementById('success-seat-label').style.display = 'block';
// Show tablemates if available
if (data.tablemates && data.tablemates.length > 0) {
renderTablemates(data.tablemates, 'tablemates-list', 'tablemates-container');
} else {
document.getElementById('tablemates-container').classList.add('hidden');
}
} else {
document.getElementById('seat-display').style.display = 'none';
document.getElementById('success-seat-label').textContent = "请自由入座";
document.getElementById('tablemates-container').classList.add('hidden');
}
document.getElementById('step-form').classList.add('hidden');
document.getElementById('step-success').classList.remove('hidden');
} else {
errorDiv.textContent = data.message || "提交失败";
errorDiv.classList.remove('hidden');
}
} catch (err) {
errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden');
} finally {
btn.textContent = '确认签到';
updateCheckinButtonState();
}
}
function renderTablemates(tablemates, listId, containerId) {
const list = document.getElementById(listId);
list.innerHTML = '';
tablemates.forEach(mate => {
const initial = mate.name ? mate.name.charAt(0) : '?';
const div = document.createElement('div');
div.className = 'tablemate-card';
div.innerHTML = `
<div class="tablemate-avatar">${initial}</div>
<div class="tablemate-info">
<div class="tablemate-name">${mate.name} <span class="click-hint">点击详情</span></div>
<div class="tablemate-industry">${mate.industry}</div>
</div>
`;
// Add click event listener for details
div.onclick = () => showModal(mate);
list.appendChild(div);
});
document.getElementById(containerId).classList.remove('hidden');
}
function showModal(mate) {
const modal = document.getElementById('detail-modal');
const initial = mate.name ? mate.name.charAt(0) : '?';
document.getElementById('modal-avatar').textContent = initial;
document.getElementById('modal-name').textContent = mate.name;
document.getElementById('modal-industry').textContent = mate.industry;
document.getElementById('modal-company').textContent = mate.company_name || '未填写';
document.getElementById('modal-position').textContent = mate.position || '未填写';
document.getElementById('modal-business').textContent = mate.business_scope || '未填写';
document.getElementById('modal-vision').textContent = mate.vision_2026 || '未填写';
modal.style.display = "block";
}
function closeModal() {
document.getElementById('detail-modal').style.display = "none";
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('detail-modal');
if (event.target == modal) {
modal.style.display = "none";
}
}
function resetFlow() {
document.getElementById('step-success').classList.add('hidden');
document.getElementById('step-already-signed').classList.add('hidden');
document.getElementById('step-search').classList.remove('hidden');
document.getElementById('search-input').value = '';
document.getElementById('user-list').innerHTML = '';
document.getElementById('checkin-form').reset();
currentUser = null;
updateCheckinButtonState();
}
async function sendCheckinSms() {
const phone = document.getElementById('form-phone').value.trim();
const errorDiv = document.getElementById('submit-error');
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
errorDiv.textContent = '请输入有效的手机号码';
errorDiv.classList.remove('hidden');
return;
} else {
errorDiv.classList.add('hidden');
}
const btn = document.getElementById('send-checkin-code-btn');
if (!btn) return;
btn.disabled = true;
let count = 60;
const originalText = "发送验证码";
try {
const res = await fetch('/api/send-sms', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({phone: phone})
});
const data = await res.json();
if (data.success) {
btn.innerText = `${count}s`;
const timer = setInterval(() => {
count--;
btn.innerText = `${count}s`;
if (count <= 0) {
clearInterval(timer);
btn.disabled = false;
btn.innerText = originalText;
}
}, 1000);
} else {
errorDiv.textContent = data.message || '发送失败';
errorDiv.classList.remove('hidden');
btn.disabled = false;
}
} catch (e) {
errorDiv.textContent = '网络错误';
errorDiv.classList.remove('hidden');
btn.disabled = false;
}
}
</script>
</body>
</html>