Files
check_in/templates/index.html
jeremygan2021 84b243dc52 new
2026-03-27 13:27:47 +08:00

1246 lines
49 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); }
}
/* 入群二维码样式 */
.qrcode-section {
margin-top: 25px;
padding: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(0, 242, 255, 0.3);
text-align: center;
}
.qrcode-title {
color: var(--primary-color);
font-size: 0.9rem;
margin-bottom: 15px;
font-weight: 600;
}
.qrcode-image {
max-width: 180px;
width: 100%;
border-radius: 8px;
border: 1px solid rgba(0, 242, 255, 0.4);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.qrcode-image:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.3);
}
.qrcode-hint {
color: var(--text-muted);
font-size: 0.75rem;
margin-top: 10px;
opacity: 0.8;
}
</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>
<!-- 动态签到字段容器 - 根据 checkin_field_config 自动生成 -->
<div id="dynamic-checkin-fields"></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>
<div id="qrcode-container-success" class="qrcode-section hidden">
<p class="qrcode-title">📱 扫码加入社群</p>
<img id="qrcode-img-success" class="qrcode-image" src="" alt="入群二维码" oncontextmenu="handleQrcodeLongPress(event)">
<p class="qrcode-hint">长按二维码保存到相册</p>
</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>
<div id="qrcode-container-signed" class="qrcode-section hidden">
<p class="qrcode-title">📱 扫码加入社群</p>
<img id="qrcode-img-signed" class="qrcode-image" src="" alt="入群二维码" oncontextmenu="handleQrcodeLongPress(event)">
<p class="qrcode-hint">长按二维码保存到相册</p>
</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;
/**
* 获取座位单位名称
*/
function getSeatUnitName() {
return CONFIG.seat_unit_name || '桌';
}
/**
* 格式化座位号显示(替换"桌"为单位名称)
* @param {string} seatText 原始座位文字
* @returns {string} 格式化后的座位文字
*/
function formatSeatDisplay(seatText) {
if (!seatText) return '';
const unit = getSeatUnitName();
if (unit === '桌') return seatText;
return seatText.replace(/桌/g, unit);
}
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);
}
}
/**
* 动态渲染签到表单字段
* 根据 checkin_field_config 配置生成字段 HTML
*/
function renderDynamicCheckinFields() {
const container = document.getElementById('dynamic-checkin-fields');
if (!container) return;
const fc = CONFIG.checkin_field_config || {};
container.innerHTML = '';
// 定义字段显示顺序
const fieldOrder = ['company_name', 'position', 'business_scope', 'vision_2026'];
// 按顺序添加字段
fieldOrder.forEach(key => {
const field = fc[key];
if (!field) return;
const div = document.createElement('div');
div.className = 'input-group' + (field.show ? '' : ' hidden');
div.id = 'group-' + key;
const isTextarea = (key === 'business_scope' || key === 'vision_2026');
let placeholder = '点此输入';
if (key === 'business_scope') placeholder = '点此输入';
if (key === 'vision_2026') placeholder = '点此输入 (为了业务更好对接,最好表述清晰)';
if (isTextarea) {
const rows = key === 'vision_2026' ? 3 : 2;
div.innerHTML = `
<label id="label-${key}">${field.label || key}</label>
<textarea id="form-${key}" rows="${rows}" placeholder="${placeholder}"></textarea>
`;
} else {
div.innerHTML = `
<label id="label-${key}">${field.label || key}</label>
<input type="text" id="form-${key}" placeholder="${placeholder}">
`;
}
container.appendChild(div);
});
}
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');
}
}
// 动态渲染字段
renderDynamicCheckinFields();
}
function updateCheckinButtonState() {
const btn = document.getElementById('submit-btn');
const codeInput = document.getElementById('form-verification-code');
const smsGroup = document.getElementById('sms-verification-group');
// Check if loading (by checking loader class or text content)
if (btn.innerHTML.includes('loader')) return;
// Check if SMS verification is enabled (group is visible)
if (smsGroup && !smsGroup.classList.contains('hidden')) {
if (isCheckinCodeVerified && codeInput && 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');
applyConfigUI();
updateCheckinButtonState();
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 = formatSeatDisplay(data.seat) || ("自由" + getSeatUnitName());
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');
}
// 显示二维码(如果配置开启)
updateQrcodeDisplay();
} 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);
// 动态字段填充
const fc = CONFIG.checkin_field_config || {};
Object.keys(fc).forEach(key => {
if (key === 'name' || key === 'phone') return;
// 特殊映射company_name 字段填充 industry_company 的值
const fieldKey = (key === 'company_name') ? 'industry_company' : key;
setVal('form-' + key, user[fieldKey] || '');
});
// 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 : '';
};
// 构建 payload
const payload = {
gsdh_id: getVal('gsdh-id'),
name: getVal('form-name'),
phone: getVal('form-phone'),
verification_code: getVal('form-verification-code'),
location: getVal('form-location')
};
// 动态收集签到字段数据
const fc = CONFIG.checkin_field_config || {};
Object.keys(fc).forEach(key => {
if (key === 'name' || key === 'phone') return;
payload[key] = getVal('form-' + key);
});
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 = formatSeatDisplay(data.seat) || ("自由" + getSeatUnitName());
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');
}
// 显示二维码(如果配置开启)
updateQrcodeDisplay();
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();
}
/**
* 根据配置控制二维码显示
*/
function updateQrcodeDisplay() {
const enableQrcode = CONFIG.enable_qrcode === true;
const qrcodeImage = CONFIG.qrcode_image;
if (enableQrcode && qrcodeImage) {
// 签到成功页面的二维码
const successContainer = document.getElementById('qrcode-container-success');
const successImg = document.getElementById('qrcode-img-success');
if (successContainer) {
successImg.src = qrcodeImage;
successContainer.classList.remove('hidden');
}
// 已签到页面的二维码
const signedContainer = document.getElementById('qrcode-container-signed');
const signedImg = document.getElementById('qrcode-img-signed');
if (signedContainer) {
signedImg.src = qrcodeImage;
signedContainer.classList.remove('hidden');
}
} else {
document.getElementById('qrcode-container-success')?.classList.add('hidden');
document.getElementById('qrcode-container-signed')?.classList.add('hidden');
}
}
/**
* 处理二维码长按保存(在 PC 端右键保存)
*/
function handleQrcodeLongPress(event) {
event.preventDefault();
const img = event.target;
if (img.src) {
const link = document.createElement('a');
link.href = img.src;
link.download = '入群二维码';
link.click();
}
}
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 = "发送验证码";
// Show loading state
btn.innerHTML = '<span class="loader" style="width: 14px; height: 14px; border-width: 2px; margin-right: 5px;"></span> 发送中...';
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;
btn.innerText = originalText;
}
} catch (e) {
errorDiv.textContent = '网络错误';
errorDiv.classList.remove('hidden');
btn.disabled = false;
btn.innerText = originalText;
}
}
</script>
</body>
</html>