完成页面签到一致性以及手机验证

This commit is contained in:
jeremygan2021
2026-01-28 23:43:12 +08:00
parent 4597d6fe35
commit da2f7b34ea
6 changed files with 541 additions and 46 deletions

Binary file not shown.

View File

@@ -8,7 +8,8 @@
"secondary_color": "#0066ff", "secondary_color": "#0066ff",
"bg_color": "#050814", "bg_color": "#050814",
"header_image": "/static/image1.jpg", "header_image": "/static/image1.jpg",
"enable_ticket_validation": false, "enable_ticket_validation": true,
"enable_sms_verification": true,
"enable_payment": true, "enable_payment": true,
"payment_amount": 0.01, "payment_amount": 0.01,
"wechat_pay_config": { "wechat_pay_config": {
@@ -53,21 +54,21 @@
"company_name": { "company_name": {
"label": "单位名称", "label": "单位名称",
"show": true, "show": true,
"required": false "required": true
}, },
"position": { "position": {
"label": "职务", "label": "职务",
"show": true, "show": true,
"required": false "required": true
}, },
"business_scope": { "business_scope": {
"label": "公司主要经营 / 业务", "label": "公司主要经营 / 业务",
"show": true, "show": true,
"required": false "required": true
}, },
"vision_2026": { "vision_2026": {
"label": "2026年业务愿景", "label": "2026年业务愿景",
"show": true, "show": false,
"required": false "required": false
} }
}, },
@@ -77,7 +78,7 @@
"learn_more_url": "", "learn_more_url": "",
"show_fields": { "show_fields": {
"name": true, "name": true,
"company_name": true, "company_name": false,
"position": true, "position": true,
"vision_2026": true, "vision_2026": true,
"business_scope": true "business_scope": true
@@ -85,7 +86,7 @@
}, },
"db_host": "6.6.6.66", "db_host": "6.6.6.66",
"db_port": "5432", "db_port": "5432",
"db_user": "123", "db_user": "AI_event",
"db_password": "1231", "db_password": "123AI_event",
"db_name": "123" "db_name": "AI_event"
} }

154
main.py
View File

@@ -7,6 +7,8 @@ import psycopg2
from psycopg2 import pool from psycopg2 import pool
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from typing import Optional, List, Dict from typing import Optional, List, Dict
import datetime
from datetime import timedelta
import random import random
import uuid import uuid
import os import os
@@ -40,6 +42,11 @@ DEFAULT_CONFIG = {
"db_user": os.getenv("DB_USER", "gsdh"), "db_user": os.getenv("DB_USER", "gsdh"),
"db_password": os.getenv("DB_PASSWORD", "123gsdh"), "db_password": os.getenv("DB_PASSWORD", "123gsdh"),
"db_name": os.getenv("DB_NAME", "gsdh"), "db_name": os.getenv("DB_NAME", "gsdh"),
"enable_sms_verification": False,
"sms_verification_config": {
"template_code": "SMS_493295002",
"sign_name": "叠加态科技云南"
},
"enable_ticket_validation": True, "enable_ticket_validation": True,
"enable_seating": True, "enable_seating": True,
"total_tables": 14, "total_tables": 14,
@@ -47,7 +54,7 @@ DEFAULT_CONFIG = {
"ticket_field_config": { "ticket_field_config": {
"name": {"label": "姓名", "show": True, "required": True}, "name": {"label": "姓名", "show": True, "required": True},
"phone": {"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}
}, },
"checkin_field_config": { "checkin_field_config": {
"name": {"label": "姓名", "show": True, "required": True}, "name": {"label": "姓名", "show": True, "required": True},
@@ -107,6 +114,9 @@ def save_config(config):
CONFIG = load_config() CONFIG = load_config()
# SMS Verification Storage (In-memory)
SMS_CODES = {} # phone -> {code: str, expires_at: datetime}
app = FastAPI() app = FastAPI()
# 进程池全局变量 # 进程池全局变量
@@ -114,11 +124,29 @@ process_pool = None
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
import sys
print(f"DEBUG: Running from {__file__}")
print(f"DEBUG: Python executable: {sys.executable}")
global process_pool global process_pool
# RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算 # RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算
process_pool = ProcessPoolExecutor(max_workers=6) process_pool = ProcessPoolExecutor(max_workers=6)
print("ProcessPoolExecutor initialized with 6 workers") print("ProcessPoolExecutor initialized with 6 workers")
# Debug: Print all registered routes
print("=== Registered Routes ===")
for route in app.routes:
if hasattr(route, "methods"):
print(f"{route.path} {route.methods}")
else:
print(f"{route.path}")
print("=========================")
@app.post("/api/test-sms")
async def test_sms_simple():
return {"message": "Test SMS route is working"}
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
if process_pool: if process_pool:
@@ -306,6 +334,7 @@ class CheckinRequest(BaseModel):
gsdh_id: str gsdh_id: str
name: str name: str
phone: str phone: str
verification_code: Optional[str] = None
company_name: Optional[str] = None company_name: Optional[str] = None
position: Optional[str] = None position: Optional[str] = None
business_scope: Optional[str] = None business_scope: Optional[str] = None
@@ -315,10 +344,113 @@ class CheckinRequest(BaseModel):
class AddUserRequest(BaseModel): class AddUserRequest(BaseModel):
name: str name: str
phone: str phone: str
verification_code: Optional[str] = None
industry_company: Optional[str] = None industry_company: Optional[str] = None
fee: Optional[str] = None fee: Optional[str] = None
payment_channel: Optional[str] = None payment_channel: Optional[str] = None
class SendSMSRequest(BaseModel):
phone: str
def verify_sms_code(phone: str, code: str) -> bool:
if not CONFIG.get("enable_sms_verification", False):
return True
if not code:
return False
record = SMS_CODES.get(phone)
if not record:
return False
if datetime.datetime.now() > record["expires_at"]:
del SMS_CODES[phone]
return False
if record["code"] == code:
# Optional: Delete after successful verification to prevent replay
# del SMS_CODES[phone]
return True
return False
class VerifyCodeRequest(BaseModel):
phone: str
code: str
@app.post("/api/verify-sms-code")
def verify_sms_code_endpoint(req: VerifyCodeRequest):
if verify_sms_code(req.phone, req.code):
return JSONResponse({"success": True, "message": "验证成功"})
else:
return JSONResponse({"success": False, "message": "验证码错误或已过期"})
@app.api_route("/api/send-sms", methods=["GET", "POST"])
async def send_sms_endpoint(req: Request):
print(f"DEBUG: send_sms_endpoint called via {req.method}")
# Handle GET for testing
if req.method == "GET":
return JSONResponse({"message": "GET method works, please use POST"})
# Handle POST
try:
body = await req.json()
phone = body.get("phone")
except:
return JSONResponse({"success": False, "message": "Invalid JSON body"}, status_code=400)
print(f"DEBUG: Processing phone={phone}")
if not CONFIG.get("enable_sms_verification", False):
return JSONResponse({"success": False, "message": "短信验证未开启"})
if not phone or len(phone) != 11:
return JSONResponse({"success": False, "message": "手机号格式错误"})
# Generate 4 digit code
code = str(random.randint(1000, 9999))
# Store code (expires in 5 minutes)
SMS_CODES[phone] = {
"code": code,
"expires_at": datetime.datetime.now() + timedelta(minutes=5)
}
# Send SMS via external API
sms_config = CONFIG.get("sms_verification_config", {})
payload = {
"phone_number": phone,
"code": code,
"template_code": sms_config.get("template_code", "SMS_493295002"),
"sign_name": sms_config.get("sign_name", "叠加态科技云南")
}
try:
print(f"DEBUG: Sending SMS payload: {payload}")
# Using a timeout to prevent hanging
res = requests.post(
'https://data.tangledup-ai.com/api/send-sms',
json=payload,
headers={'Content-Type': 'application/json', 'accept': 'application/json'},
timeout=30
)
print(f"DEBUG: SMS API Response: {res.status_code} - {res.text}")
if res.status_code == 200:
data = res.json()
if data.get("status") == "success":
return JSONResponse({"success": True, "message": "验证码已发送"})
else:
return JSONResponse({"success": False, "message": data.get("message", "发送失败")})
else:
return JSONResponse({"success": False, "message": f"发送失败: {res.status_code}"})
except Exception as e:
print(f"SMS Send Error: {e}")
return JSONResponse({"success": False, "message": "发送验证码请求失败"})
def get_db_connection(): def get_db_connection():
max_retries = 5 max_retries = 5
for attempt in range(max_retries): for attempt in range(max_retries):
@@ -732,6 +864,15 @@ def search_user(query: str):
else: else:
user = users[0] user = users[0]
# Normalize fee to float for frontend consistency
if user.get('fee'):
try:
user['fee'] = float(user['fee'])
except:
user['fee'] = 0.0
else:
user['fee'] = 0.0
# Check if already signed # Check if already signed
if user.get('is_signed') == 'TRUE': if user.get('is_signed') == 'TRUE':
# Check if already signed # Check if already signed
@@ -776,6 +917,11 @@ def search_user(query: str):
@app.post("/api/checkin") @app.post("/api/checkin")
def checkin_user(checkin_data: CheckinRequest): def checkin_user(checkin_data: CheckinRequest):
# Verify SMS Code if enabled
if CONFIG.get("enable_sms_verification", False):
if not verify_sms_code(checkin_data.phone, checkin_data.verification_code):
return JSONResponse(content={"success": False, "message": "手机验证码错误或已过期"}, status_code=400)
try: try:
conn = get_db_connection() conn = get_db_connection()
cur = conn.cursor() cur = conn.cursor()
@@ -1518,6 +1664,7 @@ async def success_page(request: Request):
class TicketPaymentRequest(BaseModel): class TicketPaymentRequest(BaseModel):
name: str name: str
phone: str phone: str
verification_code: Optional[str] = None
company_name: Optional[str] = None company_name: Optional[str] = None
position: Optional[str] = None position: Optional[str] = None
business_scope: Optional[str] = None business_scope: Optional[str] = None
@@ -1628,6 +1775,11 @@ async def create_native_payment(req: TicketPaymentRequest, request: Request):
global CONFIG global CONFIG
CONFIG = load_config() CONFIG = load_config()
# Verify SMS Code if enabled
if CONFIG.get("enable_sms_verification", False):
if not verify_sms_code(req.phone, req.verification_code):
return JSONResponse(content={"success": False, "message": "手机验证码错误或已过期"}, status_code=400)
if not CONFIG.get("enable_payment", False): if not CONFIG.get("enable_payment", False):
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403) return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)

View File

@@ -363,6 +363,10 @@
<input type="checkbox" id="enable_ticket_validation"> <input type="checkbox" id="enable_ticket_validation">
<label for="enable_ticket_validation">开启验票验证 (核对数据库与付款记录)</label> <label for="enable_ticket_validation">开启验票验证 (核对数据库与付款记录)</label>
</div> </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 style="font-size: 0.9em; color: var(--text-muted); margin-left: 30px;">
关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。 关闭后将跳过数据库核查,直接允许签到(若用户不存在将自动创建)。
</p> </p>
@@ -690,7 +694,7 @@
renderFieldConfig('ticket_field_config_body', config.ticket_field_config || { renderFieldConfig('ticket_field_config_body', config.ticket_field_config || {
"name": {"label": "姓名", "show": true, "required": true}, "name": {"label": "姓名", "show": true, "required": true},
"phone": {"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"]); }, ["name", "phone", "industry_company"]);
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || { renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
@@ -797,6 +801,7 @@
// Checkin Config // Checkin Config
enable_ticket_validation: document.getElementById('enable_ticket_validation').checked, enable_ticket_validation: document.getElementById('enable_ticket_validation').checked,
enable_sms_verification: document.getElementById('enable_sms_verification').checked,
// Payment Config // Payment Config
enable_payment: document.getElementById('enable_payment').checked, enable_payment: document.getElementById('enable_payment').checked,

View File

@@ -524,35 +524,33 @@
<input type="text" id="form-phone" required> <input type="text" id="form-phone" required>
</div> </div>
{% set fc = config.field_config or {} %} <div class="input-group hidden" id="sms-verification-group">
<label>验证码</label>
{% if fc.company_name and fc.company_name.show %} <div style="display: flex; gap: 10px;">
<div class="input-group"> <input type="text" id="form-verification-code" placeholder="输入验证码" style="flex: 1;">
<label>{{ fc.company_name.label }}</label> <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>
<input type="text" id="form-company" placeholder="点此输入" {{ 'required' if fc.company_name.required else '' }}>
</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> </div>
{% endif %}
{% if fc.business_scope and fc.business_scope.show %} <div class="input-group hidden" id="group-company">
<div class="input-group"> <label id="label-company">单位名称</label>
<label>{{ fc.business_scope.label }}</label> <input type="text" id="form-company" placeholder="点此输入">
<textarea id="form-business" rows="2" placeholder="点此输入" {{ 'required' if fc.business_scope.required else '' }}></textarea>
</div> </div>
{% endif %}
{% if fc.vision_2026 and fc.vision_2026.show %} <div class="input-group hidden" id="group-position">
<div class="input-group"> <label id="label-position">职务</label>
<label>{{ fc.vision_2026.label }}</label> <input type="text" id="form-position" placeholder="点此输入">
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)" {{ 'required' if fc.vision_2026.required else '' }}></textarea> </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> </div>
{% endif %}
<input type="hidden" id="form-location" value=""> <input type="hidden" id="form-location" value="">
@@ -646,6 +644,138 @@
// Inject Config // Inject Config
const CONFIG = {{ config | tojson }}; 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() { async function searchUser() {
const query = document.getElementById('search-input').value.trim(); const query = document.getElementById('search-input').value.trim();
const btn = document.getElementById('search-btn'); const btn = document.getElementById('search-btn');
@@ -672,6 +802,7 @@
if (response.ok) { if (response.ok) {
if (data.allow_create) { if (data.allow_create) {
await refreshConfig();
const u = data.user || {}; const u = data.user || {};
const tempId = 'TEMP_' + Date.now(); const tempId = 'TEMP_' + Date.now();
currentUser = { new_id: tempId }; currentUser = { new_id: tempId };
@@ -727,6 +858,7 @@
} }
} else { } else {
// Single user found // Single user found
await refreshConfig();
selectUser(data.user); selectUser(data.user);
} }
} else { } else {
@@ -743,11 +875,15 @@
} }
} }
function selectUser(user) { async function selectUser(user) {
// Check if fee is 0 (unpaid) // 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'); const errorDiv = document.getElementById('search-error');
errorDiv.textContent = "⚠️ 无法签到请先前往签到处付费26元"; const amount = CONFIG.payment_amount || 26;
errorDiv.textContent = `⚠️ 无法签到:请先前往签到处付费${amount}`;
errorDiv.classList.remove('hidden'); errorDiv.classList.remove('hidden');
errorDiv.classList.add('important-warning'); errorDiv.classList.add('important-warning');
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
@@ -758,18 +894,32 @@
// Populate form // Populate form
document.getElementById('gsdh-id').value = user.new_id; document.getElementById('gsdh-id').value = user.new_id;
document.getElementById('form-name').value = user.name;
document.getElementById('form-phone').value = user.phone; const setVal = (id, val) => {
document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping 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 // Display preview
document.getElementById('display-name').textContent = user.name; if (document.getElementById('display-name')) document.getElementById('display-name').textContent = user.name;
document.getElementById('display-phone').textContent = user.phone; if (document.getElementById('display-phone')) document.getElementById('display-phone').textContent = user.phone;
document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息'; 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 // Switch steps
document.getElementById('step-search').classList.add('hidden'); document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-form').classList.remove('hidden'); document.getElementById('step-form').classList.remove('hidden');
updateCheckinButtonState();
} }
async function submitCheckin(e) { async function submitCheckin(e) {
@@ -791,6 +941,7 @@
gsdh_id: getVal('gsdh-id'), gsdh_id: getVal('gsdh-id'),
name: getVal('form-name'), name: getVal('form-name'),
phone: getVal('form-phone'), phone: getVal('form-phone'),
verification_code: getVal('form-verification-code'),
company_name: getVal('form-company'), company_name: getVal('form-company'),
position: getVal('form-position'), position: getVal('form-position'),
business_scope: getVal('form-business'), business_scope: getVal('form-business'),
@@ -840,8 +991,8 @@
errorDiv.textContent = "网络错误,请稍后重试"; errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden'); errorDiv.classList.remove('hidden');
} finally { } finally {
btn.disabled = false;
btn.textContent = '确认签到'; btn.textContent = '确认签到';
updateCheckinButtonState();
} }
} }
@@ -903,6 +1054,57 @@
document.getElementById('user-list').innerHTML = ''; document.getElementById('user-list').innerHTML = '';
document.getElementById('checkin-form').reset(); document.getElementById('checkin-form').reset();
currentUser = null; 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> </script>
</body> </body>

View File

@@ -269,6 +269,12 @@
<label>{{ field.label }}{% if field.required %} *{% endif %}</label> <label>{{ field.label }}{% if field.required %} *{% endif %}</label>
{% if key == 'phone' %} {% if key == 'phone' %}
<input type="tel" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}" pattern="[0-9]{11}"> <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 %} {% else %}
<input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}"> <input type="text" id="{{ key }}" {% if field.required %}required{% endif %} placeholder="请输入{{ field.label }}">
{% endif %} {% endif %}
@@ -301,6 +307,57 @@
<script> <script>
let pollTimer = null; 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() { function closeModal() {
document.getElementById('qrModal').style.display = 'none'; document.getElementById('qrModal').style.display = 'none';
@@ -309,10 +366,28 @@
// Reset button state // Reset button state
const btn = document.getElementById('submitBtn'); const btn = document.getElementById('submitBtn');
btn.disabled = false;
btn.innerHTML = '立即支付报名'; 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) { async function handlePayment(e) {
e.preventDefault(); e.preventDefault();
@@ -342,6 +417,7 @@
const data = { const data = {
name: name, name: name,
phone: phone, phone: phone,
verification_code: getVal('verification_code'),
company_name: getVal('industry_company') company_name: getVal('industry_company')
}; };
@@ -383,14 +459,14 @@
} }
} else { } else {
showMessage(result.message || '支付请求失败', 'error'); showMessage(result.message || '支付请求失败', 'error');
btn.disabled = false;
btn.innerHTML = originalText; btn.innerHTML = originalText;
updateSubmitButtonState();
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
showMessage(err.message || '网络请求错误,请重试', 'error'); showMessage(err.message || '网络请求错误,请重试', 'error');
btn.disabled = false;
btn.innerHTML = originalText; btn.innerHTML = originalText;
updateSubmitButtonState();
} }
} }
@@ -433,6 +509,65 @@
msg.className = type; msg.className = type;
msg.style.display = 'block'; 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> </script>
</body> </body>
</html> </html>