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

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

154
main.py
View File

@@ -7,6 +7,8 @@ import psycopg2
from psycopg2 import pool
from psycopg2.extras import RealDictCursor
from typing import Optional, List, Dict
import datetime
from datetime import timedelta
import random
import uuid
import os
@@ -40,6 +42,11 @@ DEFAULT_CONFIG = {
"db_user": os.getenv("DB_USER", "gsdh"),
"db_password": os.getenv("DB_PASSWORD", "123gsdh"),
"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_seating": True,
"total_tables": 14,
@@ -47,7 +54,7 @@ DEFAULT_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}
},
"checkin_field_config": {
"name": {"label": "姓名", "show": True, "required": True},
@@ -107,6 +114,9 @@ def save_config(config):
CONFIG = load_config()
# SMS Verification Storage (In-memory)
SMS_CODES = {} # phone -> {code: str, expires_at: datetime}
app = FastAPI()
# 进程池全局变量
@@ -114,11 +124,29 @@ process_pool = None
@app.on_event("startup")
async def startup_event():
import sys
print(f"DEBUG: Running from {__file__}")
print(f"DEBUG: Python executable: {sys.executable}")
global process_pool
# RK3588 有 8 个核心,预留一些给数据库和系统,使用 6 个核心进行计算
process_pool = ProcessPoolExecutor(max_workers=6)
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")
async def shutdown_event():
if process_pool:
@@ -306,6 +334,7 @@ class CheckinRequest(BaseModel):
gsdh_id: str
name: str
phone: str
verification_code: Optional[str] = None
company_name: Optional[str] = None
position: Optional[str] = None
business_scope: Optional[str] = None
@@ -315,10 +344,113 @@ class CheckinRequest(BaseModel):
class AddUserRequest(BaseModel):
name: str
phone: str
verification_code: Optional[str] = None
industry_company: Optional[str] = None
fee: 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():
max_retries = 5
for attempt in range(max_retries):
@@ -732,6 +864,15 @@ def search_user(query: str):
else:
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
if user.get('is_signed') == 'TRUE':
# Check if already signed
@@ -776,6 +917,11 @@ def search_user(query: str):
@app.post("/api/checkin")
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:
conn = get_db_connection()
cur = conn.cursor()
@@ -1518,6 +1664,7 @@ async def success_page(request: Request):
class TicketPaymentRequest(BaseModel):
name: str
phone: str
verification_code: Optional[str] = None
company_name: Optional[str] = None
position: Optional[str] = None
business_scope: Optional[str] = None
@@ -1628,6 +1775,11 @@ async def create_native_payment(req: TicketPaymentRequest, request: Request):
global 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):
return JSONResponse(content={"success": False, "message": "支付未开启"}, status_code=403)

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>