Spaces:
Paused
Paused
| {% extends "base.html" %} | |
| {% block title %}设置 - tgState{% endblock %} | |
| {% block content %} | |
| <div style="max-width: 720px; margin: 0 auto;"> | |
| <div style="margin-bottom: 32px;"> | |
| <h1 style="font-size: 26px; font-weight: 800; margin-bottom: 6px;">系统设置</h1> | |
| <p style="color: var(--text-secondary); font-size: 15px;">配置 Telegram 机器人与其他系统参数。</p> | |
| </div> | |
| <form id="settings-form" onsubmit="return false;"> | |
| <!-- 账号安全 --> | |
| <div class="card" style="padding: 28px;"> | |
| <h2 style="font-size: 15px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary);"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> | |
| 账号安全 | |
| </h2> | |
| <div class="input-group" style="margin-bottom: 0;"> | |
| <label class="input-label">管理员密码</label> | |
| <div style="position: relative;"> | |
| <input type="password" class="input-field" name="PASS_WORD" id="PASS_WORD" placeholder="留空则不修改"> | |
| <button type="button" onclick="togglePwd('PASS_WORD')" style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); padding: 4px;"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Telegram 配置 --> | |
| <div class="card" style="padding: 28px;"> | |
| <h2 style="font-size: 15px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary);"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg> | |
| Telegram 配置 | |
| </h2> | |
| <div class="input-group"> | |
| <label class="input-label">Bot Token <span id="bot-token-badge" class="badge badge-success" style="display:none; margin-left: 8px; font-size: 11px;">已配置</span></label> | |
| <input type="text" class="input-field" name="BOT_TOKEN" id="BOT_TOKEN" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"> | |
| <div class="text-sm text-muted mt-4">从 <a href="https://t.me/BotFather" target="_blank" style="color: var(--primary-color);">@BotFather</a> 获取。留空则保持当前配置。</div> | |
| </div> | |
| <div class="input-group" style="margin-bottom: 0;"> | |
| <label class="input-label">Channel Name</label> | |
| <input type="text" class="input-field" name="CHANNEL_NAME" id="CHANNEL_NAME" placeholder="@my_channel 或 -100xxxxxxxx"> | |
| <div class="text-sm text-muted mt-4">机器人必须是该频道的管理员。</div> | |
| </div> | |
| </div> | |
| <!-- 连接设置 --> | |
| <div class="card" style="padding: 28px;"> | |
| <h2 style="font-size: 15px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary);"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> | |
| 连接设置 | |
| </h2> | |
| <div class="input-group"> | |
| <label class="input-label">Base URL</label> | |
| <div style="display: flex; gap: 8px;"> | |
| <input type="text" class="input-field" name="BASE_URL" id="BASE_URL" placeholder="http://your-domain.com"> | |
| <button type="button" class="btn btn-secondary" onclick="fillOrigin()" style="flex-shrink: 0;">使用当前</button> | |
| </div> | |
| <div class="text-sm text-muted mt-4">用于生成分享链接和图片地址。</div> | |
| </div> | |
| <div class="input-group" style="margin-bottom: 0;"> | |
| <label class="input-label">PicGo API Key <span id="picgo-key-badge" class="badge badge-success" style="display:none; margin-left: 8px; font-size: 11px;">已配置</span></label> | |
| <div style="display: flex; gap: 8px;"> | |
| <input type="text" class="input-field" name="PICGO_API_KEY" id="PICGO_API_KEY" placeholder="留空则保持当前配置"> | |
| <button type="button" class="btn btn-secondary" onclick="genKey()" style="flex-shrink: 0;">生成</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 操作按钮 --> | |
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 24px;"> | |
| <button type="button" class="btn btn-ghost" onclick="toggleAdvanced()" style="color: var(--text-tertiary);"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82V9"></path></svg> | |
| 高级选项 | |
| </button> | |
| <div style="display: flex; gap: 12px;"> | |
| <button type="button" class="btn btn-secondary" onclick="saveConfig(false)" id="btn-save">仅保存</button> | |
| <button type="button" class="btn btn-primary" onclick="saveConfig(true)" id="btn-finish">保存并应用</button> | |
| </div> | |
| </div> | |
| <!-- 高级选项面板 --> | |
| <div id="advanced-panel" class="hidden" style="margin-bottom: 48px;"> | |
| <div class="card" style="padding: 28px;"> | |
| <h3 style="font-size: 14px; font-weight: 700; margin-bottom: 16px;">连接测试</h3> | |
| <div style="display: flex; gap: 12px; margin-bottom: 24px;"> | |
| <button type="button" class="btn btn-secondary btn-sm" onclick="testBot()" id="btn-test-bot">测试 Bot Token</button> | |
| <button type="button" class="btn btn-secondary btn-sm" onclick="testChannel()" id="btn-test-ch">测试频道</button> | |
| </div> | |
| <div style="border-top: 1px solid var(--border-color); padding-top: 20px; margin-top: 20px;"> | |
| <h3 style="font-size: 14px; font-weight: 700; color: var(--danger-color); margin-bottom: 12px;">危险区域</h3> | |
| <button type="button" class="btn" style="color: var(--danger-color); border: 1px solid var(--danger-color);" onclick="resetConfig()">重置所有配置</button> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| <script> | |
| let configState = {}; // Store loaded config state | |
| function initData() { | |
| fetch('/api/app-config') | |
| .then(r => r.json()) | |
| .then(data => { | |
| configState = data.cfg || {}; | |
| // Populate fields | |
| const channelEl = document.getElementsByName('CHANNEL_NAME')[0]; | |
| if (channelEl) channelEl.value = configState.CHANNEL_NAME || ''; | |
| const baseUrlEl = document.getElementsByName('BASE_URL')[0]; | |
| if (baseUrlEl) baseUrlEl.value = configState.BASE_URL || ''; | |
| // Show badges for set-but-hidden fields | |
| if (configState.BOT_TOKEN_SET) { | |
| document.getElementById('bot-token-badge').style.display = 'inline-flex'; | |
| document.getElementById('BOT_TOKEN').placeholder = '已配置,留空保持不变'; | |
| } | |
| if (configState.PICGO_API_KEY_SET) { | |
| document.getElementById('picgo-key-badge').style.display = 'inline-flex'; | |
| document.getElementById('PICGO_API_KEY').placeholder = '已配置,留空保持不变'; | |
| } | |
| }); | |
| } | |
| function togglePwd(id) { | |
| const el = document.getElementById(id); | |
| el.type = el.type === 'password' ? 'text' : 'password'; | |
| } | |
| function fillOrigin() { | |
| document.getElementById('BASE_URL').value = window.location.origin; | |
| } | |
| function genKey() { | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
| let r = ''; | |
| for (let i = 0; i < 32; i++) r += chars.charAt(Math.floor(Math.random() * chars.length)); | |
| document.getElementById('PICGO_API_KEY').value = r; | |
| } | |
| function toggleAdvanced() { | |
| document.getElementById('advanced-panel').classList.toggle('hidden'); | |
| } | |
| async function saveConfig(apply = false) { | |
| const btn = document.getElementById(apply ? 'btn-finish' : 'btn-save'); | |
| Utils.setLoading(btn, true); | |
| const form = document.getElementById('settings-form'); | |
| const data = Object.fromEntries(new FormData(form)); | |
| // Don't send empty sensitive fields — keeps existing values in DB | |
| if (!data.PASS_WORD) delete data.PASS_WORD; | |
| if (!data.BOT_TOKEN) delete data.BOT_TOKEN; | |
| if (!data.PICGO_API_KEY) delete data.PICGO_API_KEY; | |
| try { | |
| const url = apply ? '/api/app-config/apply' : '/api/app-config/save'; | |
| const res = await fetch(url, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(data) | |
| }); | |
| const json = await res.json(); | |
| if (res.ok) { | |
| Toast.show(apply ? '配置已保存并应用' : '配置已保存'); | |
| if (apply) setTimeout(() => location.reload(), 1000); | |
| } else { | |
| Toast.show(json.detail?.message || '保存失败', 'error'); | |
| } | |
| } catch (e) { | |
| Toast.show('网络错误', 'error'); | |
| } finally { | |
| Utils.setLoading(btn, false); | |
| } | |
| } | |
| async function testBot() { | |
| const btn = document.getElementById('btn-test-bot'); | |
| Utils.setLoading(btn, true); | |
| const token = document.getElementsByName('BOT_TOKEN')[0].value; | |
| try { | |
| const res = await fetch('/api/verify/bot', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ BOT_TOKEN: token || undefined }) | |
| }); | |
| const json = await res.json(); | |
| if (json.ok) { | |
| Toast.show('Bot 验证成功: @' + json.result.username); | |
| } else { | |
| Toast.show(json.message || '验证失败', 'error'); | |
| } | |
| } catch (e) { | |
| Toast.show('请求失败', 'error'); | |
| } finally { Utils.setLoading(btn, false); } | |
| } | |
| async function testChannel() { | |
| const btn = document.getElementById('btn-test-ch'); | |
| Utils.setLoading(btn, true); | |
| const token = document.getElementsByName('BOT_TOKEN')[0].value; | |
| const channel = document.getElementsByName('CHANNEL_NAME')[0].value; | |
| try { | |
| const res = await fetch('/api/verify/channel', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ BOT_TOKEN: token || undefined, CHANNEL_NAME: channel || undefined }) | |
| }); | |
| const json = await res.json(); | |
| if (json.available) { | |
| Toast.show('频道验证成功'); | |
| } else { | |
| Toast.show(json.message || '频道不可用', 'error'); | |
| } | |
| } catch (e) { | |
| Toast.show('请求失败', 'error'); | |
| } finally { Utils.setLoading(btn, false); } | |
| } | |
| async function resetConfig() { | |
| const confirmed = await Modal.confirm('重置配置', '确定要重置所有配置吗?此操作不可撤销,并将强制退出登录。'); | |
| if (!confirmed) return; | |
| fetch('/api/reset-config', { method: 'POST' }) | |
| .then(r => r.json()) | |
| .then(() => { | |
| Toast.show('重置完成,正在跳转...'); | |
| setTimeout(() => window.location.href = '/welcome', 1000); | |
| }); | |
| } | |
| document.addEventListener('DOMContentLoaded', initData); | |
| </script> | |
| {% endblock %} | |