Files
check_in/templates/admin.html
jeremygan2021 c6bfc59faa
Some checks failed
Docker 构建与部署 / deploy (push) Has been cancelled
Docker 构建与部署 / build (push) Has been cancelled
new
2026-03-20 15:13:14 +08:00

957 lines
39 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>管理后台 - 云南AI共生大会</title>
<style>
:root {
--primary-color: #00f2ff;
--secondary-color: #0066ff;
--bg-color: #050814;
--card-bg: rgba(12, 24, 50, 0.5);
--text-color: #ffffff;
--text-muted: #b0c4de;
--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);
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;
padding: 20px;
}
.container {
width: 100%;
max-width: 800px;
position: relative;
z-index: 1;
}
h1 {
font-size: 2rem;
margin-bottom: 30px;
text-align: center;
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
text-shadow: 0 0 20px rgba(0, 242, 255, 0.4);
}
.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);
}
h2 {
color: #fff;
margin-bottom: 20px;
font-size: 1.2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 10px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
h2::after {
content: '▼';
font-size: 0.8em;
transition: transform 0.3s;
opacity: 0.7;
}
.card.collapsed h2::after {
transform: rotate(-90deg);
}
.card.collapsed h2 {
border-bottom: none;
margin-bottom: 0;
}
.card-content {
transition: all 0.3s ease-in-out;
max-height: 3000px; /* Ensure enough height for content */
opacity: 1;
overflow: hidden;
}
.card.collapsed .card-content {
max-height: 0;
opacity: 0;
margin: 0;
padding: 0;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--primary-color);
font-size: 0.9rem;
}
input[type="text"], input[type="password"], textarea, select, input[type="number"] {
width: 100%;
padding: 12px;
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;
}
input:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
background: rgba(0, 0, 0, 0.6);
}
input[type="color"] {
height: 40px;
width: 100px;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
}
button {
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
}
/* Button variants */
.btn-success {
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
box-shadow: 0 4px 15px rgba(32, 201, 151, 0.35);
}
.btn-danger,
.danger-btn {
background: linear-gradient(90deg, #cc0000 0%, #ff4d4d 100%);
box-shadow: 0 4px 15px rgba(255, 0, 0, 0.3);
}
.btn-block {
width: 100%;
}
.btn-lg {
padding: 16px 28px;
font-size: 1.2rem;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.4);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
box-shadow: none;
}
.danger-zone {
border: 1px solid rgba(255, 50, 50, 0.5);
background: rgba(50, 0, 0, 0.3);
}
.danger-zone h2 {
color: #ff6b6b;
border-color: rgba(255, 50, 50, 0.3);
}
.btn-danger:hover,
.danger-btn:hover {
box-shadow: 0 6px 20px rgba(255, 0, 0, 0.5);
}
.preview-img {
max-width: 100%;
height: auto;
margin-top: 10px;
border-radius: 8px;
border: 1px solid rgba(0, 242, 255, 0.3);
}
#message {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
color: white;
font-weight: bold;
display: none;
z-index: 1000;
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.success {
background: rgba(40, 167, 69, 0.9);
border: 1px solid #28a745;
}
.error {
background: rgba(220, 53, 69, 0.9);
border: 1px solid #dc3545;
}
/* Password Overlay */
#password-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-color);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-image: inherit;
}
.auth-card {
background: var(--card-bg);
padding: 40px;
border-radius: 16px;
text-align: center;
width: 90%;
max-width: 400px;
box-shadow: 0 0 40px rgba(0,0,0,0.5);
border: 1px solid rgba(0, 242, 255, 0.3);
backdrop-filter: blur(20px);
}
/* Password input group */
.input-group {
position: relative;
}
.input-group .toggle-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
padding: 6px 10px;
font-size: 0.9rem;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 6px;
box-shadow: none;
}
.input-group .toggle-btn:hover {
background: rgba(0,0,0,0.5);
}
.checkbox-wrapper {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.checkbox-wrapper input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 10px;
accent-color: var(--primary-color);
}
.checkbox-wrapper label {
margin-bottom: 0;
font-size: 1rem;
color: white;
cursor: pointer;
}
</style>
</head>
<body>
<!-- Password Overlay -->
<div id="password-overlay">
<div class="auth-card">
<h2 style="border:none; text-align:center; font-size:1.5rem;">请输入访问密钥</h2>
<div class="input-group" style="margin-bottom: 20px;">
<input type="password" id="secret-input" placeholder="输入密钥">
<button class="toggle-btn" aria-label="显示/隐藏密钥" data-target="secret-input" onclick="togglePasswordVisibility(this)">显示</button>
</div>
<button onclick="checkSecret()">确认进入</button>
<p id="secret-error" style="color: #ff6b6b; margin-top: 15px; display: none;">密钥错误</p>
</div>
</div>
<div class="container">
<h1>管理后台</h1>
<div style="text-align: center; margin-bottom: 25px;">
<a href="/wall" target="_blank" style="text-decoration: none;">
<button class="btn-lg" style="background: linear-gradient(90deg, #6a11cb 0%, #2575fc 100%); width: auto; padding: 15px 40px; box-shadow: 0 0 20px rgba(37, 117, 252, 0.5);">
📺 打开签到大屏 (Large Screen)
</button>
</a>
</div>
<div class="card">
<h2>基本信息设置</h2>
<div class="card-content">
<div class="form-group">
<label>活动标题</label>
<input type="text" id="event_title">
</div>
<div class="form-group">
<label>副标题</label>
<input type="text" id="event_sub_title">
</div>
<div class="form-group">
<label>活动时间</label>
<input type="text" id="event_time">
</div>
<div class="form-group">
<label>活动地点</label>
<input type="text" id="event_location">
</div>
<div class="form-group">
<label>活动内容</label>
<textarea id="event_content" rows="4"></textarea>
</div>
</div>
</div>
<div class="card collapsed">
<h2>签到设置</h2>
<div class="card-content">
<div class="checkbox-wrapper">
<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>
</div>
</div>
<div class="card collapsed">
<h2>座位设置</h2>
<div class="card-content">
<div class="checkbox-wrapper">
<input type="checkbox" id="enable_seating" onchange="toggleSeatingInputs()">
<label for="enable_seating">开启分桌功能</label>
</div>
<div id="seating_inputs">
<div class="form-group">
<label>总桌数</label>
<input type="number" id="total_tables" min="1">
</div>
<div class="form-group">
<label>每桌最大人数</label>
<input type="number" id="max_per_table" min="1">
</div>
</div>
</div>
</div>
<div class="card collapsed">
<h2>报名输入字段设置 (Registration Fields)</h2>
<div class="card-content">
<p style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 15px;">对应 gsdh_data 表 (ticket.html)</p>
<table style="width: 100%; border-collapse: collapse; color: white;">
<thead>
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); text-align: left;">
<th style="padding: 10px;">字段名称</th>
<th style="padding: 10px;">是否显示</th>
<th style="padding: 10px;">是否必填</th>
</tr>
</thead>
<tbody id="ticket_field_config_body">
<!-- Ticket Fields will be injected here -->
</tbody>
</table>
</div>
</div>
<div class="card collapsed">
<h2>签到/大屏字段设置 (Check-in Fields)</h2>
<div class="card-content">
<p style="font-size: 0.9em; color: var(--text-muted); margin-bottom: 15px;">对应 checkin_info 表 (wall/checkin)</p>
<table style="width: 100%; border-collapse: collapse; color: white;">
<thead>
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); text-align: left;">
<th style="padding: 10px;">字段名称</th>
<th style="padding: 10px;">是否显示</th>
<th style="padding: 10px;">是否必填</th>
</tr>
</thead>
<tbody id="checkin_field_config_body">
<!-- Checkin Fields will be injected here -->
</tbody>
</table>
</div>
</div>
<div class="card collapsed">
<h2>支付设置 (Payment)</h2>
<div class="card-content">
<div class="checkbox-wrapper">
<input type="checkbox" id="enable_payment">
<label for="enable_payment">开启付费报名 (Enable Paid Registration)</label>
</div>
<p style="font-size: 0.9em; color: var(--text-muted); margin-left: 30px; margin-bottom: 15px;">
开启后,<a href="/ticket" target="_blank" style="color: var(--primary-color);">/ticket</a> 页面将开放访问。
</p>
<div class="form-group">
<label>支付金额 (元)</label>
<input type="number" id="payment_amount" step="0.01" min="0.01">
</div>
<h3 style="color: #b0c4de; margin: 20px 0 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px; font-size: 1rem;">微信支付配置 (WeChat Pay V3)</h3>
<div class="form-group">
<label>AppID</label>
<input type="text" id="wx_appid" placeholder="wx...">
</div>
<div class="form-group">
<label>MchID (商户号)</label>
<input type="text" id="wx_mchid" placeholder="16...">
</div>
<div class="form-group">
<label>API V3 Key</label>
<div class="input-group">
<input type="password" id="wx_api_v3_key" placeholder="32位密钥">
<button class="toggle-btn" aria-label="显示/隐藏" data-target="wx_api_v3_key" onclick="togglePasswordVisibility(this)">显示</button>
</div>
</div>
<div class="form-group">
<label>Serial No (证书序列号)</label>
<input type="text" id="wx_serial_no" placeholder="证书序列号">
</div>
<div class="form-group">
<label>商户私钥路径 (Private Key Path)</label>
<input type="text" id="wx_private_key_path" placeholder="cert/apiclient_key.pem">
</div>
<div class="form-group">
<label>回调地址 (Notify URL)</label>
<input type="text" id="wx_notify_url" placeholder="https://your-domain.com/api/payment/notify">
</div>
</div>
</div>
<div class="card collapsed">
<h2>数据库设置</h2>
<div class="card-content">
<div class="form-group">
<label>数据库地址 (Host)</label>
<input type="text" id="db_host" placeholder="localhost">
</div>
<div class="form-group">
<label>端口 (Port)</label>
<input type="text" id="db_port" placeholder="5432">
</div>
<div class="form-group">
<label>用户名 (User)</label>
<input type="text" id="db_user">
</div>
<div class="form-group">
<label>密码 (Password)</label>
<div class="input-group">
<input type="password" id="db_password" placeholder="数据库密码">
<button class="toggle-btn" aria-label="显示/隐藏密码" data-target="db_password" onclick="togglePasswordVisibility(this)">显示</button>
</div>
</div>
<div class="form-group">
<label>数据库名 (Database Name)</label>
<input type="text" id="db_name">
</div>
<button class="btn-success" onclick="testDbConnection()">测试连接</button>
<button class="btn-lg" style="margin-left: 10px; background: linear-gradient(90deg, #17a2b8 0%, #138496 100%);" onclick="initDatabase()">初始化/迁移结构</button>
<p style="font-size: 0.9em; color: var(--text-muted); margin-top: 10px;">
提示:切换到新数据库后,点击“初始化/迁移结构”可自动创建表结构(不会删除现有数据)。
</p>
</div>
</div>
<div class="card collapsed">
<h2>大屏设置 (Wall Config)</h2>
<div class="card-content">
<div class="form-group">
<label>背景图片透明度 (0-1)</label>
<input type="number" id="wall_bg_opacity" step="0.1" min="0" max="1" value="0.3">
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_title"> <label for="wall_show_title">显示大屏标题</label>
</div>
<div class="form-group">
<label>了解我们 URL (手机端底部按钮)</label>
<input type="text" id="wall_learn_more_url" placeholder="https://...">
</div>
<div class="form-group">
<label>显示字段</label>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_name"> <label for="wall_show_name">姓名</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_company"> <label for="wall_show_company">单位名称</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_position"> <label for="wall_show_position">职务</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_vision"> <label for="wall_show_vision">愿景 (Vision)</label>
</div>
<div class="checkbox-wrapper">
<input type="checkbox" id="wall_show_scope"> <label for="wall_show_scope">业务范围 (弹幕)</label>
</div>
</div>
</div>
</div>
<div class="card collapsed">
<h2>主题设置</h2>
<div class="card-content">
<div class="form-group">
<label>主色调 (Primary Color)</label>
<input type="color" id="primary_color">
</div>
<div class="form-group">
<label>次色调 (Secondary Color)</label>
<input type="color" id="secondary_color">
</div>
<div class="form-group">
<label>背景色 (Background Color)</label>
<input type="color" id="bg_color">
</div>
</div>
</div>
<div class="card collapsed">
<h2>图片设置</h2>
<div class="card-content">
<div class="form-group">
<label>顶部头图</label>
<input type="file" id="header_image_file" accept="image/*" style="padding: 10px 0;">
<img id="image_preview" class="preview-img" src="">
</div>
<button onclick="uploadImage()">上传图片</button>
</div>
</div>
<div style="margin-bottom: 25px; text-align: center;">
<button onclick="saveConfig()" class="btn-block btn-lg">保存所有设置</button>
</div>
<div class="card danger-zone collapsed">
<h2>危险区域</h2>
<div class="card-content">
<p style="color: #ffcccc; margin-bottom: 15px;">此处操作不可逆,请谨慎操作。</p>
<div class="form-group">
<button class="btn-danger" onclick="resetDatabase()">重置/创建数据库</button>
<p style="font-size: 0.9em; color: #ffcccc; margin-top: 10px;">这将清空所有签到数据并重新创建表结构。</p>
</div>
</div>
</div>
<div id="message"></div>
</div>
<script>
// Collapsible Cards Logic
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.card h2').forEach(header => {
header.addEventListener('click', () => {
header.parentElement.classList.toggle('collapsed');
});
});
});
// Use secret from backend
const ACCESS_SECRET = "{{ secret }}";
// Password Check Logic
function checkSecret() {
const input = document.getElementById('secret-input');
const error = document.getElementById('secret-error');
const overlay = document.getElementById('password-overlay');
if (input.value === ACCESS_SECRET) {
overlay.style.opacity = '0';
setTimeout(() => {
overlay.style.display = 'none';
}, 500);
} else {
error.style.display = 'block';
input.value = '';
input.focus();
}
}
document.getElementById('secret-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
checkSecret();
}
});
/**
* 切换密码输入框的可见性
* @param {HTMLButtonElement} btn 触发按钮(需携带 data-target 指向 input 的 id
*/
function togglePasswordVisibility(btn) {
const targetId = btn.dataset.target;
const input = document.getElementById(targetId);
if (!input) return;
const isHidden = input.type === 'password';
input.type = isHidden ? 'text' : 'password';
btn.textContent = isHidden ? '隐藏' : '显示';
}
function toggleSeatingInputs() {
const enabled = document.getElementById('enable_seating').checked;
const inputs = document.getElementById('seating_inputs');
if (enabled) {
inputs.style.display = 'block';
} else {
inputs.style.display = 'none';
}
}
// Load config on startup
fetch('/api/admin/config')
.then(res => res.json())
.then(config => {
document.getElementById('event_title').value = config.event_title;
document.getElementById('event_sub_title').value = config.event_sub_title;
document.getElementById('event_time').value = config.event_time;
document.getElementById('event_location').value = config.event_location;
document.getElementById('event_content').value = config.event_content;
document.getElementById('primary_color').value = config.primary_color;
document.getElementById('secondary_color').value = config.secondary_color;
document.getElementById('bg_color').value = config.bg_color;
document.getElementById('image_preview').src = config.header_image;
// Checkin Config
document.getElementById('enable_ticket_validation').checked = config.enable_ticket_validation !== false;
document.getElementById('enable_sms_verification').checked = config.enable_sms_verification === true;
// Payment Config
document.getElementById('enable_payment').checked = config.enable_payment === true;
document.getElementById('payment_amount').value = config.payment_amount || 0.01;
const wx = config.wechat_pay_config || {};
document.getElementById('wx_appid').value = wx.appid || '';
document.getElementById('wx_mchid').value = wx.mchid || '';
document.getElementById('wx_api_v3_key').value = wx.api_v3_key || '';
document.getElementById('wx_serial_no').value = wx.serial_no || '';
document.getElementById('wx_private_key_path').value = wx.private_key_path || '';
document.getElementById('wx_notify_url').value = wx.notify_url || '';
// Seating Config
document.getElementById('enable_seating').checked = config.enable_seating !== false; // Default true
document.getElementById('total_tables').value = config.total_tables || 14;
document.getElementById('max_per_table').value = config.max_per_table || 10;
toggleSeatingInputs();
// Field Configs
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}
}, ["name", "phone", "industry_company"]);
renderFieldConfig('checkin_field_config_body', config.checkin_field_config || {
"name": {"label": "姓名", "show": true, "required": true},
"phone": {"label": "手机号码", "show": True, "required": True},
"company_name": {"label": "单位名称", "show": true, "required": false},
"position": {"label": "职务", "show": true, "required": false},
"business_scope": {"label": "公司主要经营 / 业务", "show": true, "required": false},
"vision_2026": {"label": "2026年业务愿景", "show": true, "required": false}
}, ["name", "phone", "company_name", "position", "business_scope", "vision_2026"]);
// DB Config
document.getElementById('db_host').value = config.db_host || '';
document.getElementById('db_port').value = config.db_port || '';
document.getElementById('db_user').value = config.db_user || '';
document.getElementById('db_password').value = config.db_password || '';
document.getElementById('db_name').value = config.db_name || '';
// Wall Config
const wc = config.wall_config || {};
const show = wc.show_fields || {};
document.getElementById('wall_bg_opacity').value = wc.bg_opacity !== undefined ? wc.bg_opacity : 0.3;
document.getElementById('wall_show_title').checked = wc.show_title !== false;
document.getElementById('wall_learn_more_url').value = wc.learn_more_url || '';
document.getElementById('wall_show_name').checked = show.name !== false;
document.getElementById('wall_show_position').checked = show.position !== false;
document.getElementById('wall_show_vision').checked = show.vision_2026 !== false;
document.getElementById('wall_show_scope').checked = show.business_scope !== false;
});
function renderFieldConfig(tbodyId, fieldConfig, orderedKeys) {
const tbody = document.getElementById(tbodyId);
tbody.innerHTML = '';
// If orderedKeys not provided, use default order or Object.keys
if (!orderedKeys) {
orderedKeys = Object.keys(fieldConfig);
}
orderedKeys.forEach(key => {
if (!fieldConfig[key]) return;
const field = fieldConfig[key];
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
// Name/Phone lock logic
const isLocked = (key === 'name' || key === 'phone');
tr.innerHTML = `
<td style="padding: 10px;">${field.label} (${key})</td>
<td style="padding: 10px;">
<input type="checkbox" class="field-show" data-key="${key}" ${field.show ? 'checked' : ''} ${isLocked ? 'disabled' : ''}>
</td>
<td style="padding: 10px;">
<input type="checkbox" class="field-required" data-key="${key}" ${field.required ? 'checked' : ''} ${isLocked ? 'disabled' : ''}>
</td>
`;
tbody.appendChild(tr);
});
}
function getFieldConfig(tbodyId) {
const config = {};
const rows = document.querySelectorAll(`#${tbodyId} tr`);
rows.forEach(row => {
const showCb = row.querySelector('.field-show');
const reqCb = row.querySelector('.field-required');
const key = showCb.dataset.key;
const labelText = row.cells[0].textContent.split(' (')[0];
config[key] = {
label: labelText,
show: showCb.checked,
required: reqCb.checked
};
});
// Ensure locked fields are true/true
if (config.name) { config.name.show = true; config.name.required = true; }
if (config.phone) { config.phone.show = true; config.phone.required = true; }
return config;
}
function showMessage(text, type) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.className = type;
msg.style.display = 'block';
setTimeout(() => msg.style.display = 'none', 3000);
}
async function saveConfig() {
const config = {
event_title: document.getElementById('event_title').value,
event_sub_title: document.getElementById('event_sub_title').value,
event_time: document.getElementById('event_time').value,
event_location: document.getElementById('event_location').value,
event_content: document.getElementById('event_content').value,
primary_color: document.getElementById('primary_color').value,
secondary_color: document.getElementById('secondary_color').value,
bg_color: document.getElementById('bg_color').value,
header_image: document.getElementById('image_preview').src.replace(window.location.origin, ''),
// 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,
payment_amount: parseFloat(document.getElementById('payment_amount').value),
wechat_pay_config: {
appid: document.getElementById('wx_appid').value,
mchid: document.getElementById('wx_mchid').value,
api_v3_key: document.getElementById('wx_api_v3_key').value,
serial_no: document.getElementById('wx_serial_no').value,
private_key_path: document.getElementById('wx_private_key_path').value,
notify_url: document.getElementById('wx_notify_url').value
},
// Seating Config
enable_seating: document.getElementById('enable_seating').checked,
total_tables: parseInt(document.getElementById('total_tables').value),
max_per_table: parseInt(document.getElementById('max_per_table').value),
// Field Config
ticket_field_config: getFieldConfig('ticket_field_config_body'),
checkin_field_config: getFieldConfig('checkin_field_config_body'),
// Wall Config
wall_config: {
bg_opacity: parseFloat(document.getElementById('wall_bg_opacity').value),
show_title: document.getElementById('wall_show_title').checked,
learn_more_url: document.getElementById('wall_learn_more_url').value,
show_fields: {
name: document.getElementById('wall_show_name').checked,
company_name: document.getElementById('wall_show_company').checked,
position: document.getElementById('wall_show_position').checked,
vision_2026: document.getElementById('wall_show_vision').checked,
business_scope: document.getElementById('wall_show_scope').checked
}
},
// DB Config
db_host: document.getElementById('db_host').value,
db_port: document.getElementById('db_port').value,
db_user: document.getElementById('db_user').value,
db_password: document.getElementById('db_password').value,
db_name: document.getElementById('db_name').value
};
try {
const res = await fetch('/api/admin/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
if (res.ok) {
showMessage('设置已保存', 'success');
} else {
showMessage('保存失败', 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
async function testDbConnection() {
const config = {
db_host: document.getElementById('db_host').value,
db_port: document.getElementById('db_port').value,
db_user: document.getElementById('db_user').value,
db_password: document.getElementById('db_password').value,
db_name: document.getElementById('db_name').value
};
try {
const res = await fetch('/api/admin/test-db', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
const data = await res.json();
if (res.ok) {
showMessage('连接成功', 'success');
} else {
showMessage('连接失败: ' + data.message, 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
async function uploadImage() {
const fileInput = document.getElementById('header_image_file');
if (!fileInput.files[0]) {
showMessage('请先选择图片', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
const res = await fetch('/api/admin/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
document.getElementById('image_preview').src = data.url;
showMessage('图片上传成功,请记得点击保存设置', 'success');
} else {
showMessage('上传失败: ' + data.message, 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
async function initDatabase() {
if (!confirm('确定要在当前配置的数据库中初始化/迁移表结构吗?\n此操作安全不会删除现有数据')) return;
try {
const res = await fetch('/api/admin/init-db', {
method: 'POST'
});
const data = await res.json();
if (res.ok) {
showMessage('数据库结构初始化/迁移成功', 'success');
} else {
showMessage('初始化失败: ' + data.message, 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
async function resetDatabase() {
if (!confirm('确定要重置数据库吗?所有数据将被清空!')) return;
try {
const res = await fetch('/api/admin/reset-db', {
method: 'POST'
});
const data = await res.json();
if (res.ok) {
showMessage('数据库已重置', 'success');
} else {
showMessage('重置失败: ' + data.message, 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
</script>
</body>
</html>