- Drop 'like' event from rules, handler and web UI (Bilibili like messages are not reliably delivered as INTERACT_WORD_V2 msg_type=6) - Keep enter_room / follow / interact_other - event_rules.json rewritten without like key - build.py keeps BUILD_DIR = dist/portable
648 lines
22 KiB
HTML
648 lines
22 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>B站直播 → ゆっくり TTS</title>
|
|
<style>
|
|
:root {
|
|
--bg: #1a1a2e;
|
|
--panel: #16213e;
|
|
--border: #0f3460;
|
|
--accent: #e94560;
|
|
--accent-hover: #ff6b81;
|
|
--green: #4ade80;
|
|
--text: #e2e8f0;
|
|
--text-dim: #94a3b8;
|
|
--input-bg: #0f1629;
|
|
--radius: 8px;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
display: flex; justify-content: center; min-height: 100vh;
|
|
padding: 24px;
|
|
}
|
|
.container {
|
|
width: 100%; max-width: 680px;
|
|
display: flex; flex-direction: column; gap: 16px;
|
|
}
|
|
h1 {
|
|
font-size: 1.4rem; font-weight: 700;
|
|
text-align: center; color: var(--accent);
|
|
padding-bottom: 8px;
|
|
}
|
|
.card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
}
|
|
.card h2 {
|
|
font-size: 0.95rem; color: var(--text-dim);
|
|
text-transform: uppercase; letter-spacing: 1px;
|
|
margin-bottom: 14px;
|
|
}
|
|
.form-row {
|
|
display: flex; gap: 12px; margin-bottom: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.form-group {
|
|
display: flex; flex-direction: column; gap: 4px;
|
|
flex: 1; min-width: 140px;
|
|
}
|
|
.form-group label {
|
|
font-size: 0.8rem; color: var(--text-dim);
|
|
}
|
|
.form-group input, .form-group select {
|
|
padding: 8px 12px; border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--input-bg); color: var(--text);
|
|
font-size: 0.9rem; outline: none;
|
|
}
|
|
.form-group input:focus, .form-group select:focus {
|
|
border-color: var(--accent);
|
|
}
|
|
input[type="range"] {
|
|
accent-color: var(--accent);
|
|
border: none; background: transparent; padding: 0;
|
|
}
|
|
.btn-row {
|
|
display: flex; gap: 10px; margin-top: 4px;
|
|
}
|
|
.btn {
|
|
padding: 10px 24px; border: none; border-radius: 6px;
|
|
font-size: 0.9rem; font-weight: 600; cursor: pointer;
|
|
transition: background 0.15s, opacity 0.15s;
|
|
}
|
|
.btn-start {
|
|
background: var(--green); color: #1a1a2e;
|
|
flex: 1;
|
|
}
|
|
.btn-start:hover { opacity: 0.9; }
|
|
.btn-stop {
|
|
background: var(--accent); color: white;
|
|
flex: 1;
|
|
}
|
|
.btn-stop:hover { background: var(--accent-hover); }
|
|
.btn:disabled {
|
|
opacity: 0.4; cursor: not-allowed;
|
|
}
|
|
.btn-login {
|
|
background: transparent; color: var(--text);
|
|
border: 1px solid var(--border);
|
|
padding: 6px 16px; font-size: 0.85rem; cursor: pointer;
|
|
border-radius: 6px; transition: background 0.15s;
|
|
}
|
|
.btn-login:hover { background: var(--input-bg); }
|
|
.btn-login.ready {
|
|
border-color: var(--green); color: var(--green);
|
|
}
|
|
|
|
/* QR Container */
|
|
.login-area {
|
|
display: flex; align-items: flex-start; gap: 20px;
|
|
margin-top: 4px; flex-wrap: wrap;
|
|
}
|
|
.qr-box {
|
|
background: white; border-radius: 8px;
|
|
padding: 12px; width: 180px; height: 180px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.qr-box img {
|
|
width: 156px; height: 156px; image-rendering: pixelated;
|
|
}
|
|
.qr-box .qr-placeholder {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
justify-content: center; color: #94a3b8; font-size: 0.82rem;
|
|
text-align: center; gap: 8px;
|
|
}
|
|
.qr-box .qr-placeholder svg {
|
|
width: 48px; height: 48px; opacity: 0.3;
|
|
}
|
|
.qr-overlay {
|
|
position: absolute; inset: 0;
|
|
background: rgba(15,22,41,0.92);
|
|
display: flex; align-items: center; justify-content: center;
|
|
border-radius: 8px; flex-direction: column; gap: 6px;
|
|
font-size: 0.85rem; text-align: center;
|
|
}
|
|
.qr-overlay .btn-refresh {
|
|
margin-top: 8px; padding: 6px 18px; border-radius: 6px;
|
|
border: 1px solid var(--accent); background: transparent;
|
|
color: var(--accent); font-size: 0.82rem; cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
.qr-overlay .btn-refresh:hover { background: var(--accent); color: white; }
|
|
.qr-info {
|
|
display: flex; flex-direction: column; gap: 6px;
|
|
flex: 1; min-width: 180px;
|
|
}
|
|
.qr-info .status-text {
|
|
font-size: 0.88rem; line-height: 1.5;
|
|
}
|
|
.qr-info .user-info {
|
|
display: flex; align-items: center; gap: 10px;
|
|
font-size: 0.9rem; color: var(--green);
|
|
}
|
|
.qr-info .user-info .avatar {
|
|
width: 32px; height: 32px; border-radius: 50%;
|
|
border: 2px solid var(--green);
|
|
}
|
|
.logged-in-badge {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 8px 0; font-size: 0.85rem;
|
|
}
|
|
.logged-in-badge .dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
background: var(--green); flex-shrink: 0;
|
|
}
|
|
|
|
/* Status bar */
|
|
.status-bar {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 8px 0; font-size: 0.85rem;
|
|
}
|
|
.status-dot {
|
|
width: 10px; height: 10px; border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
.dot-off { background: #64748b; }
|
|
.dot-on { background: var(--green); animation: pulse 1.5s infinite; }
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
.stats {
|
|
display: flex; gap: 20px; flex-wrap: wrap;
|
|
font-size: 0.82rem; color: var(--text-dim);
|
|
}
|
|
.stats span { color: var(--text); font-weight: 600; }
|
|
#log {
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
height: 320px; overflow-y: auto;
|
|
padding: 12px;
|
|
font-family: "Consolas", "Courier New", monospace;
|
|
font-size: 0.82rem; line-height: 1.5;
|
|
white-space: pre-wrap; word-break: break-all;
|
|
}
|
|
#log .msg { color: var(--text-dim); }
|
|
#log .msg:last-child { color: var(--text); }
|
|
|
|
/* spinner */
|
|
.spinner {
|
|
width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.2);
|
|
border-top-color: var(--accent); border-radius: 50%;
|
|
animation: spin 0.8s linear infinite; margin: 0 auto;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* Event rules */
|
|
.rule-row {
|
|
display: flex; gap: 10px; align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
.rule-row .rule-name {
|
|
width: 110px; font-size: 0.82rem; color: var(--text-dim);
|
|
flex-shrink: 0;
|
|
}
|
|
.rule-row input[type="checkbox"] {
|
|
accent-color: var(--accent); width: 15px; height: 15px;
|
|
flex-shrink: 0;
|
|
}
|
|
.rule-row input[type="text"] {
|
|
flex: 1; padding: 6px 10px; border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--input-bg); color: var(--text);
|
|
font-size: 0.85rem; outline: none;
|
|
}
|
|
.rule-row input[type="text"]:focus { border-color: var(--accent); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>B站直播 → ゆっくり TTS</h1>
|
|
|
|
<!-- Login Card -->
|
|
<div class="card" id="login-card">
|
|
<h2>登录</h2>
|
|
<div class="login-area">
|
|
<div class="qr-box" id="qr-box">
|
|
<div class="qr-placeholder" id="qr-placeholder">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="3" y="3" width="18" height="18" rx="3"/>
|
|
<rect x="6" y="6" width="2" height="2"/><rect x="10" y="6" width="2" height="2"/>
|
|
<rect x="14" y="6" width="2" height="2"/><rect x="16" y="6" width="2" height="2"/>
|
|
<rect x="6" y="10" width="2" height="2"/>
|
|
<rect x="16" y="10" width="2" height="2"/>
|
|
<rect x="6" y="14" width="2" height="2"/>
|
|
<rect x="10" y="14" width="2" height="2"/>
|
|
<rect x="6" y="16" width="2" height="2"/>
|
|
<rect x="14" y="16" width="2" height="2"/>
|
|
</svg>
|
|
<span>点击"登录"<br/>获取二维码</span>
|
|
</div>
|
|
<img id="qr-img" src="" alt="QR Code" style="display:none">
|
|
<div id="qr-overlay" style="display:none"></div>
|
|
</div>
|
|
<div class="qr-info">
|
|
<div id="login-status" class="status-text">
|
|
未登录 — 匿名弹幕的用户名可能被隐藏。
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-login" id="btn-login" onclick="startLogin()">扫码登录</button>
|
|
<button class="btn btn-login" id="btn-logout" onclick="doLogout()" style="display:none">退出登录</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Config Card -->
|
|
<div class="card">
|
|
<h2>配置</h2>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="room-id">房间号</label>
|
|
<input type="number" id="room-id" placeholder="例如 12235923" value="">
|
|
</div>
|
|
<div class="form-group" style="max-width:120px">
|
|
<label for="voice">音色</label>
|
|
<select id="voice">
|
|
<option value="f1">f1 (灵梦)</option>
|
|
<option value="f2">f2 (魔理沙)</option>
|
|
<option value="m1">m1 (男声1)</option>
|
|
<option value="m2">m2 (男声2)</option>
|
|
<option value="r1">r1 (机器人)</option>
|
|
<option value="dvd">dvd</option>
|
|
<option value="imd1">imd1</option>
|
|
<option value="jgr">jgr</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" style="max-width:140px">
|
|
<label for="speed">语速 (<span id="speed-val">100</span>)</label>
|
|
<input type="range" id="speed" min="50" max="300" value="100"
|
|
oninput="document.getElementById('speed-val').textContent=this.value">
|
|
</div>
|
|
<div class="form-group" style="max-width:140px">
|
|
<label for="volume">音量 (<span id="volume-val">100</span>%)</label>
|
|
<input type="range" id="volume" min="0" max="200" value="100"
|
|
oninput="document.getElementById('volume-val').textContent=this.value">
|
|
</div>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-start" id="btn-start" onclick="startTTS()">开始</button>
|
|
<button class="btn btn-stop" id="btn-stop" onclick="stopTTS()" disabled>停止</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Voice Rules Card -->
|
|
<div class="card" id="rules-card">
|
|
<h2>事件语音规则</h2>
|
|
<div class="rule-list" id="rule-list"></div>
|
|
<div class="form-row" style="margin-top:10px; align-items:center">
|
|
<label class="chk-label"><input type="checkbox" id="rule-skip-free"> 跳过免费礼物</label>
|
|
<span style="flex:1"></span>
|
|
<button class="btn btn-login" onclick="saveRules()">保存规则</button>
|
|
</div>
|
|
<div class="rule-hint" style="margin-top:8px; font-size:0.78rem; color:var(--text-dim); line-height:1.6">
|
|
可用变量:{uname} {msg} | {viewer_name} {gift_name} {gift_num} {blind_box_gift} |
|
|
{ships_viewer_name} {ships_level} {ships_num} {guard_days} | {super_chat_content} {super_chat_price} |
|
|
{interact_type} {user_uid} — 模板留空 = 该事件静音。
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Card -->
|
|
<div class="card">
|
|
<div class="status-bar">
|
|
<span class="status-dot dot-off" id="status-dot"></span>
|
|
<span id="status-text">已停止</span>
|
|
</div>
|
|
<div class="stats">
|
|
<div>消息数: <span id="stat-msgs">0</span></div>
|
|
<div>运行时长: <span id="stat-uptime">00:00</span></div>
|
|
</div>
|
|
<div id="log" style="margin-top:12px">
|
|
<span class="msg">就绪。可选扫码登录,填写房间号后点击开始。</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let pollTimer = null;
|
|
let qrPollTimer = null;
|
|
let running = false;
|
|
let loginPollKey = null;
|
|
|
|
function fmtUptime(sec) {
|
|
const m = Math.floor(sec / 60), s = sec % 60;
|
|
return String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
|
|
}
|
|
|
|
// ── QR Login ────────────────────────────────────────────────────────────
|
|
|
|
async function startLogin() {
|
|
document.getElementById('btn-login').disabled = true;
|
|
document.getElementById('login-status').innerHTML = '<div class="spinner"></div> 正在生成...';
|
|
|
|
try {
|
|
const resp = await fetch('/api/qr/generate');
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
document.getElementById('login-status').textContent = '错误: ' + data.error;
|
|
document.getElementById('btn-login').disabled = false;
|
|
return;
|
|
}
|
|
|
|
document.getElementById('qr-placeholder').style.display = 'none';
|
|
document.getElementById('qr-img').src = 'https://api.qrserver.com/v1/create-qr-code/?size=156x156&data=' + encodeURIComponent(data.url);
|
|
document.getElementById('qr-img').style.display = 'block';
|
|
document.getElementById('qr-overlay').style.display = 'none';
|
|
document.getElementById('login-status').textContent = '请使用Bilibili客户端扫码';
|
|
document.getElementById('btn-login').disabled = false;
|
|
document.getElementById('btn-login').textContent = '刷新二维码';
|
|
|
|
loginPollKey = data.qrcode_key;
|
|
if (qrPollTimer) clearInterval(qrPollTimer);
|
|
qrPollTimer = setInterval(pollLoginStatus, 2000);
|
|
} catch (e) {
|
|
document.getElementById('login-status').textContent = '网络错误: ' + e.message;
|
|
document.getElementById('btn-login').disabled = false;
|
|
}
|
|
}
|
|
|
|
async function pollLoginStatus() {
|
|
if (!loginPollKey) return;
|
|
try {
|
|
const resp = await fetch('/api/qr/poll?key=' + loginPollKey);
|
|
const data = await resp.json();
|
|
const st = parseInt(data.status);
|
|
|
|
if (st === 0) {
|
|
// SUCCESS
|
|
clearInterval(qrPollTimer); qrPollTimer = null;
|
|
loginPollKey = null;
|
|
document.getElementById('qr-img').style.display = 'none';
|
|
document.getElementById('qr-placeholder').style.display = 'none';
|
|
showOverlay(false, '', '');
|
|
document.getElementById('login-status').innerHTML =
|
|
'<div class="user-info"><span class="status-dot dot-on"></span> 已登录: ' + escapeHtml(data.username || '未知') + '</div>';
|
|
document.getElementById('btn-login').style.display = 'none';
|
|
document.getElementById('btn-logout').style.display = 'inline-block';
|
|
document.getElementById('btn-login').disabled = false;
|
|
document.getElementById('btn-login').textContent = '扫码登录';
|
|
updateLoginStatusCard(true, data.username || '');
|
|
} else if (st === 86090) {
|
|
// Scanned, waiting
|
|
document.getElementById('login-status').textContent = '已扫码,请在手机上确认登录';
|
|
showOverlay(false, '', '');
|
|
} else if (st === 86101) {
|
|
// Not scanned
|
|
document.getElementById('login-status').textContent = '请使用Bilibili客户端扫码';
|
|
showOverlay(false, '', '');
|
|
} else if (st === 86038) {
|
|
// Expired
|
|
clearInterval(qrPollTimer); qrPollTimer = null;
|
|
loginPollKey = null;
|
|
showOverlay(true, 'expired', '二维码已过期');
|
|
document.getElementById('login-status').textContent = '二维码已过期,请点击刷新';
|
|
document.getElementById('btn-login').textContent = '刷新二维码';
|
|
} else {
|
|
document.getElementById('login-status').textContent = data.message || ('状态: ' + st);
|
|
}
|
|
} catch (e) {
|
|
// ignore network errors during polling
|
|
}
|
|
}
|
|
|
|
function showOverlay(show, cls, text) {
|
|
const overlay = document.getElementById('qr-overlay');
|
|
if (show) {
|
|
overlay.style.display = 'flex';
|
|
overlay.innerHTML = '<span>' + text + '</span>' +
|
|
'<button class="btn-refresh" onclick="startLogin()">刷新二维码</button>';
|
|
} else {
|
|
overlay.style.display = 'none';
|
|
overlay.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
async function doLogout() {
|
|
try {
|
|
await fetch('/api/logout');
|
|
} catch(e) {}
|
|
loginPollKey = null;
|
|
if (qrPollTimer) { clearInterval(qrPollTimer); qrPollTimer = null; }
|
|
document.getElementById('qr-img').style.display = 'none';
|
|
document.getElementById('qr-img').src = '';
|
|
document.getElementById('qr-placeholder').style.display = 'flex';
|
|
document.getElementById('qr-overlay').style.display = 'none';
|
|
document.getElementById('login-status').textContent = '未登录 — 匿名弹幕的用户名可能被隐藏。';
|
|
document.getElementById('btn-login').style.display = 'inline-block';
|
|
document.getElementById('btn-logout').style.display = 'none';
|
|
document.getElementById('btn-login').textContent = '扫码登录';
|
|
}
|
|
|
|
function updateLoginStatusCard(loggedIn, username) {
|
|
// Visual indicator in login card already handled above.
|
|
}
|
|
|
|
// ── TTS Control ──────────────────────────────────────────────────────────
|
|
|
|
async function startTTS() {
|
|
const roomId = document.getElementById('room-id').value;
|
|
if (!roomId) { alert('请输入房间号'); return; }
|
|
|
|
const config = {
|
|
room_id: parseInt(roomId),
|
|
voice: document.getElementById('voice').value,
|
|
speed: parseInt(document.getElementById('speed').value),
|
|
volume: parseInt(document.getElementById('volume').value),
|
|
};
|
|
|
|
try {
|
|
const resp = await fetch('/api/start', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
const data = await resp.json();
|
|
if (data.error) { alert('错误: ' + data.error); return; }
|
|
|
|
running = true;
|
|
updateButtons();
|
|
startPolling();
|
|
} catch (e) {
|
|
alert('连接失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function stopTTS() {
|
|
try {
|
|
await fetch('/api/stop', { method: 'POST' });
|
|
} catch(e) {}
|
|
|
|
running = false;
|
|
updateButtons();
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
document.getElementById('status-dot').className = 'status-dot dot-off';
|
|
document.getElementById('status-text').textContent = '已停止';
|
|
}
|
|
|
|
function updateButtons() {
|
|
document.getElementById('btn-start').disabled = running;
|
|
document.getElementById('btn-stop').disabled = !running;
|
|
for (const el of ['room-id', 'voice', 'speed', 'volume']) {
|
|
document.getElementById(el).disabled = running;
|
|
}
|
|
document.getElementById('btn-login').disabled = running;
|
|
document.getElementById('btn-logout').disabled = running;
|
|
}
|
|
|
|
async function pollStatus() {
|
|
try {
|
|
const resp = await fetch('/api/status');
|
|
const s = await resp.json();
|
|
|
|
if (!s.running && running) {
|
|
running = false;
|
|
updateButtons();
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
}
|
|
|
|
const dot = document.getElementById('status-dot');
|
|
const txt = document.getElementById('status-text');
|
|
if (s.running) {
|
|
dot.className = 'status-dot dot-on';
|
|
txt.textContent = '运行中';
|
|
} else {
|
|
dot.className = 'status-dot dot-off';
|
|
txt.textContent = '已停止';
|
|
}
|
|
|
|
document.getElementById('stat-msgs').textContent = s.messages_processed;
|
|
document.getElementById('stat-uptime').textContent = fmtUptime(s.uptime);
|
|
|
|
const log = document.getElementById('log');
|
|
const msgs = s.recent_messages || [];
|
|
log.innerHTML = msgs.length > 0
|
|
? msgs.map(m => '<span class="msg">' + escapeHtml(m) + '</span>').join('\n')
|
|
: '<span class="msg">等待弹幕...</span>';
|
|
log.scrollTop = log.scrollHeight;
|
|
} catch(e) {
|
|
// Server may have stopped
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function startPolling() {
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
pollTimer = setInterval(pollStatus, 400);
|
|
pollStatus();
|
|
}
|
|
|
|
// ── Init: check existing login ──────────────────────────────────────────
|
|
|
|
async function checkExistingLogin() {
|
|
try {
|
|
const resp = await fetch('/api/login/status');
|
|
const data = await resp.json();
|
|
if (data.logged_in) {
|
|
document.getElementById('login-status').innerHTML =
|
|
'<div class="user-info"><span class="status-dot dot-on"></span> 已登录: ' + escapeHtml(data.username) + '</div>';
|
|
document.getElementById('btn-login').style.display = 'none';
|
|
document.getElementById('btn-logout').style.display = 'inline-block';
|
|
document.getElementById('qr-placeholder').style.display = 'none';
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ── Event voice rules ────────────────────────────────────────────────────
|
|
|
|
const RULE_LABELS = {
|
|
danmaku: '弹幕', gift: '礼物', blind_box: '盲盒礼物',
|
|
enter_room: '进入房间', follow: '关注',
|
|
interact_other: '其他互动', buy_guard: '上舰',
|
|
guard_toast: '上舰提示', super_chat: '醒目留言',
|
|
};
|
|
|
|
async function loadRules() {
|
|
try {
|
|
const resp = await fetch('/api/rules');
|
|
const rules = await resp.json();
|
|
renderRules(rules);
|
|
} catch(e) {}
|
|
}
|
|
|
|
function renderRules(rules) {
|
|
const list = document.getElementById('rule-list');
|
|
list.innerHTML = '';
|
|
for (const key of Object.keys(RULE_LABELS)) {
|
|
const row = document.createElement('div');
|
|
row.className = 'rule-row';
|
|
const name = document.createElement('span');
|
|
name.className = 'rule-name';
|
|
name.textContent = RULE_LABELS[key];
|
|
const chk = document.createElement('input');
|
|
chk.type = 'checkbox';
|
|
chk.dataset.key = key;
|
|
chk.checked = !!(rules.enabled && rules.enabled[key]);
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.dataset.key = key;
|
|
input.placeholder = '留空 = 静音';
|
|
input.value = (rules.templates && rules.templates[key]) || '';
|
|
row.appendChild(name);
|
|
row.appendChild(chk);
|
|
row.appendChild(input);
|
|
list.appendChild(row);
|
|
}
|
|
const skip = document.getElementById('rule-skip-free');
|
|
skip.checked = !!rules.skip_free_gift;
|
|
}
|
|
|
|
async function saveRules() {
|
|
const enabled = {};
|
|
const templates = {};
|
|
document.querySelectorAll('#rule-list input[type="checkbox"]').forEach(c => {
|
|
enabled[c.dataset.key] = c.checked;
|
|
});
|
|
document.querySelectorAll('#rule-list input[type="text"]').forEach(i => {
|
|
templates[i.dataset.key] = i.value;
|
|
});
|
|
const payload = {
|
|
enabled,
|
|
templates,
|
|
skip_free_gift: document.getElementById('rule-skip-free').checked,
|
|
};
|
|
try {
|
|
const resp = await fetch('/api/rules', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = await resp.json();
|
|
if (data.error) { alert('保存失败: ' + data.error); return; }
|
|
renderRules(data.rules);
|
|
alert('规则已保存并即时生效。');
|
|
} catch(e) {
|
|
alert('保存失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
checkExistingLogin();
|
|
loadRules();
|
|
</script>
|
|
</body>
|
|
</html>
|