951 lines
39 KiB
HTML
951 lines
39 KiB
HTML
<!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>
|
||
<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;
|
||
|
||
// 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,
|
||
|
||
// 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>
|