完成页面签到一致性以及手机验证
This commit is contained in:
@@ -363,6 +363,10 @@
|
||||
<input type="checkbox" id="enable_ticket_validation">
|
||||
<label for="enable_ticket_validation">开启验票验证 (核对数据库与付款记录)</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox" id="enable_sms_verification">
|
||||
<label for="enable_sms_verification">开启手机号短信验证 (SMS Verification)</label>
|
||||
</div>
|
||||
<p style="font-size: 0.9em; color: var(--text-muted); margin-left: 30px;">
|
||||
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
|
||||
</p>
|
||||
@@ -690,7 +694,7 @@
|
||||
renderFieldConfig('ticket_field_config_body', config.ticket_field_config || {
|
||||
"name": {"label": "姓名", "show": true, "required": true},
|
||||
"phone": {"label": "手机号码", "show": true, "required": true},
|
||||
"industry_company": {"label": "单位名称", "show": true, "required": true}
|
||||
"industry_company": {"label": "行业名称/单位名称", "show": true, "required": true}
|
||||
}, ["name", "phone", "industry_company"]);
|
||||
|
||||
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
|
||||
@@ -797,6 +801,7 @@
|
||||
|
||||
// Checkin Config
|
||||
enable_ticket_validation: document.getElementById('enable_ticket_validation').checked,
|
||||
enable_sms_verification: document.getElementById('enable_sms_verification').checked,
|
||||
|
||||
// Payment Config
|
||||
enable_payment: document.getElementById('enable_payment').checked,
|
||||
|
||||
@@ -524,35 +524,33 @@
|
||||
<input type="text" id="form-phone" required>
|
||||
</div>
|
||||
|
||||
{% set fc = config.field_config or {} %}
|
||||
|
||||
{% if fc.company_name and fc.company_name.show %}
|
||||
<div class="input-group">
|
||||
<label>{{ fc.company_name.label }}</label>
|
||||
<input type="text" id="form-company" placeholder="点此输入" {{ 'required' if fc.company_name.required else '' }}>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{% if fc.position and fc.position.show %}
|
||||
<div class="input-group">
|
||||
<label>{{ fc.position.label }}</label>
|
||||
<input type="text" id="form-position" placeholder="点此输入" {{ 'required' if fc.position.required else '' }}>
|
||||
<div class="input-group hidden" id="group-company">
|
||||
<label id="label-company">单位名称</label>
|
||||
<input type="text" id="form-company" placeholder="点此输入">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if fc.business_scope and fc.business_scope.show %}
|
||||
<div class="input-group">
|
||||
<label>{{ fc.business_scope.label }}</label>
|
||||
<textarea id="form-business" rows="2" placeholder="点此输入" {{ 'required' if fc.business_scope.required else '' }}></textarea>
|
||||
<div class="input-group hidden" id="group-position">
|
||||
<label id="label-position">职务</label>
|
||||
<input type="text" id="form-position" placeholder="点此输入">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if fc.vision_2026 and fc.vision_2026.show %}
|
||||
<div class="input-group">
|
||||
<label>{{ fc.vision_2026.label }}</label>
|
||||
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)" {{ 'required' if fc.vision_2026.required else '' }}></textarea>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" id="form-location" value="">
|
||||
|
||||
@@ -646,6 +644,138 @@
|
||||
// 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');
|
||||
@@ -672,6 +802,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
if (data.allow_create) {
|
||||
await refreshConfig();
|
||||
const u = data.user || {};
|
||||
const tempId = 'TEMP_' + Date.now();
|
||||
currentUser = { new_id: tempId };
|
||||
@@ -727,6 +858,7 @@
|
||||
}
|
||||
} else {
|
||||
// Single user found
|
||||
await refreshConfig();
|
||||
selectUser(data.user);
|
||||
}
|
||||
} else {
|
||||
@@ -743,11 +875,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function selectUser(user) {
|
||||
async function selectUser(user) {
|
||||
// Check if fee is 0 (unpaid)
|
||||
if (user.fee == 0) {
|
||||
// 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');
|
||||
errorDiv.textContent = "⚠️ 无法签到:请先前往签到处付费26元";
|
||||
const amount = CONFIG.payment_amount || 26;
|
||||
errorDiv.textContent = `⚠️ 无法签到:请先前往签到处付费${amount}元`;
|
||||
errorDiv.classList.remove('hidden');
|
||||
errorDiv.classList.add('important-warning');
|
||||
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
@@ -758,18 +894,32 @@
|
||||
|
||||
// Populate form
|
||||
document.getElementById('gsdh-id').value = user.new_id;
|
||||
document.getElementById('form-name').value = user.name;
|
||||
document.getElementById('form-phone').value = user.phone;
|
||||
document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping
|
||||
|
||||
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
|
||||
document.getElementById('display-name').textContent = user.name;
|
||||
document.getElementById('display-phone').textContent = user.phone;
|
||||
document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
|
||||
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) {
|
||||
@@ -791,6 +941,7 @@
|
||||
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'),
|
||||
@@ -840,8 +991,8 @@
|
||||
errorDiv.textContent = "网络错误,请稍后重试";
|
||||
errorDiv.classList.remove('hidden');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '确认签到';
|
||||
updateCheckinButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,6 +1054,57 @@
|
||||
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>
|
||||
|
||||
@@ -269,6 +269,12 @@
|
||||
<label>{{ field.label }}{% if field.required %} *{% endif %}</label>
|
||||
{% if key == 'phone' %}
|
||||
<input type="tel" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}" pattern="[0-9]{11}">
|
||||
{% if config.enable_sms_verification %}
|
||||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||||
<input type="text" id="verification_code" placeholder="请先发送验证码" style="flex: 1;" disabled>
|
||||
<button type="button" id="send-code-btn" onclick="sendSmsCode()" style="width: auto; padding: 10px; font-size: 0.9rem; background: var(--secondary-color); margin-top: 0; border: none; border-radius: 8px; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; min-width: 100px;">发送验证码</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}">
|
||||
{% endif %}
|
||||
@@ -301,6 +307,57 @@
|
||||
|
||||
<script>
|
||||
let pollTimer = null;
|
||||
let isCodeVerified = false;
|
||||
|
||||
function updateSubmitButtonState() {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const codeInput = document.getElementById('verification_code');
|
||||
// Only update if not in loading state (checking innerHTML for loading spinner)
|
||||
if (btn.innerHTML.includes('loading')) return;
|
||||
|
||||
if (codeInput) {
|
||||
if (isCodeVerified && codeInput.value.trim().length === 4) {
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
}
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCode() {
|
||||
const codeInput = document.getElementById('verification_code');
|
||||
const phoneInput = document.getElementById('phone');
|
||||
|
||||
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) {
|
||||
isCodeVerified = true;
|
||||
showMessage('验证码正确', 'success');
|
||||
} else {
|
||||
isCodeVerified = false;
|
||||
showMessage('验证码错误', 'error');
|
||||
}
|
||||
} catch(e) {
|
||||
isCodeVerified = false;
|
||||
}
|
||||
} else {
|
||||
isCodeVerified = false;
|
||||
}
|
||||
updateSubmitButtonState();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('qrModal').style.display = 'none';
|
||||
@@ -309,10 +366,28 @@
|
||||
|
||||
// Reset button state
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '立即支付报名';
|
||||
updateSubmitButtonState();
|
||||
}
|
||||
|
||||
// Initialize button state on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const codeInput = document.getElementById('verification_code');
|
||||
const phoneInput = document.getElementById('phone');
|
||||
|
||||
if (codeInput) {
|
||||
updateSubmitButtonState();
|
||||
codeInput.addEventListener('input', verifyCode);
|
||||
}
|
||||
|
||||
if (phoneInput) {
|
||||
phoneInput.addEventListener('input', () => {
|
||||
isCodeVerified = false;
|
||||
updateSubmitButtonState();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function handlePayment(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -342,6 +417,7 @@
|
||||
const data = {
|
||||
name: name,
|
||||
phone: phone,
|
||||
verification_code: getVal('verification_code'),
|
||||
company_name: getVal('industry_company')
|
||||
};
|
||||
|
||||
@@ -383,14 +459,14 @@
|
||||
}
|
||||
} else {
|
||||
showMessage(result.message || '支付请求失败', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
updateSubmitButtonState();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showMessage(err.message || '网络请求错误,请重试', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
updateSubmitButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +509,65 @@
|
||||
msg.className = type;
|
||||
msg.style.display = 'block';
|
||||
}
|
||||
|
||||
async function sendSmsCode() {
|
||||
const phone = document.getElementById('phone').value.trim();
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
showMessage('请输入有效的手机号码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('send-code-btn');
|
||||
const codeInput = document.getElementById('verification_code');
|
||||
if (!btn) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const originalText = "发送验证码";
|
||||
// 添加loading效果
|
||||
btn.innerHTML = '<span class="loading" style="width: 14px; height: 14px; border-width: 2px; margin-right: 6px;"></span>发送中';
|
||||
|
||||
let count = 60;
|
||||
|
||||
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) {
|
||||
showMessage('验证码已发送', 'success');
|
||||
|
||||
// 成功后才允许输入验证码
|
||||
if (codeInput) {
|
||||
codeInput.disabled = false;
|
||||
codeInput.placeholder = "请输入验证码";
|
||||
codeInput.focus();
|
||||
}
|
||||
|
||||
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 {
|
||||
showMessage(data.message || '发送失败', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerText = originalText;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showMessage('网络错误', 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerText = originalText;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user