Files
90CNServer/launcher/shell.js
T
hanfuit c58168406b 启动器:游戏更新入口移到开始旁,移除设置页面
- 侧边栏顺序调整为 开始 → 游戏更新 → GM工具 → 一键换装 → 游戏设置
- 删除「设置」页面(侧边栏入口 + page-settings section)
- shell.js 移除 loadSettings 函数及 setRefresh 事件监听
- shell.css 清理 .settings-* 相关样式(明暗主题两套)
2026-11-04 20:38:22 +08:00

707 lines
29 KiB
JavaScript
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.
/* 90轮回启动器 · 单窗口页面与工具整合 */
(function () {
const $ = (id) => document.getElementById(id);
const pages = ['start','changelog','gm','loadout','gamesettings'];
let shellGmInit = false;
let loConfig = null, loPage = 0, loScheme = -1;
let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null;
let hotkeyCapture = false;
const crumb = {start:'开始 / 本地服务端',changelog:'游戏更新',gm:'GM工具',loadout:'工具 / 一键换装',gamesettings:'工具 / 游戏设置'};
function shellGo(page) {
if (!pages.includes(page)) page = 'start';
document.querySelectorAll('.shell-page').forEach(el => el.classList.toggle('active', el.id === 'page-' + page));
document.querySelectorAll('.shell-nav-item').forEach(el => el.classList.toggle('active', el.dataset.shellPage === page));
$('shellCrumb').textContent = crumb[page] || page;
if (page === 'start') loadStart();
if (page === 'loadout') loadLoadout();
if (page === 'gamesettings') loadGameSettings();
if (page === 'changelog') loadChangelog();
if (page === 'gm') {
// 进入 GM:GM 覆盖整个窗口(隐藏顶部导航/面包屑/底部状态栏),并铺满工作区
document.body.classList.add('gm-fullscreen');
const w = win();
if (w && w.enter_gm) w.enter_gm();
if (!shellGmInit && typeof initGm === 'function') {
shellGmInit = true;
initGm();
if (typeof renderGmPage === 'function') renderGmPage('dashboard');
} else if (typeof renderGmPage === 'function') {
renderGmPage(gmCurrentPage || 'dashboard');
}
} else {
// 离开 GM:移除全屏状态,并恢复窗口几何(若正处于全屏)
if (document.body.classList.contains('gm-fullscreen')) {
document.body.classList.remove('gm-fullscreen');
const w2 = win();
if (w2 && w2.restore_gm) w2.restore_gm();
}
}
}
window.shellGo = shellGo;
// 从 GM 返回启动器:恢复窗口几何 + 回到开始页
function shellExitGm() {
document.body.classList.remove('gm-fullscreen');
const w = win();
if (w && w.restore_gm) w.restore_gm();
shellGo('start');
}
window.shellExitGm = shellExitGm;
document.querySelectorAll('.shell-nav-item').forEach(el => {
el.addEventListener('click', () => shellGo(el.dataset.shellPage));
});
// 顶部窗口按钮
const win = () => (window.pywebview && window.pywebview.api) || null;
const bMin = $('shellWinMin'); if (bMin) bMin.addEventListener('click', () => { const w = win(); if (w && w.minimize) w.minimize(); });
const bClose = $('shellWinClose'); if (bClose) bClose.addEventListener('click', () => { const w = win(); if (w && w.close) w.close(); });
// 账号登录:打开原账号管理弹窗,可选择默认账号
$('shellAccount').addEventListener('click', () => {
if (typeof openAccounts === 'function') openAccounts();
else alert('账号管理未加载');
});
async function shellAddAccount() {
const user = $('newAccUser').value.trim();
const pass = $('newAccPass').value;
const toast = (msg, type) => {
if (typeof gmToast === 'function') gmToast(msg, type || 'info');
else alert(msg);
};
if (!user || !pass) { toast('请填写用户名和密码', 'err'); return; }
try {
const r = await post('/api/accounts-config', {op:'add', username:user, password:pass});
if (!r.ok) { toast('添加失败:' + (r.error || '未知错误'), 'err'); return; }
$('newAccUser').value = '';
$('newAccPass').value = '';
$('accountsModal').classList.add('hidden');
await loadAccountLabel();
toast('账号添加成功:' + user, 'ok');
} catch (e) { toast('添加失败:' + e.message, 'err'); }
}
// app.js 的 init 如果没有完成绑定,这里也保证添加按钮可用
const addBtn = $('btnAddAccount');
if (addBtn) {
const fresh = addBtn.cloneNode(true);
addBtn.replaceWith(fresh);
fresh.addEventListener('click', shellAddAccount);
}
// 开始页
let startState = 'idle'; // idle | starting | started
function setStartState(state) {
startState = state;
const btn = $('shellStart');
if (!btn) return;
btn.disabled = state === 'starting';
if (state === 'starting') btn.innerHTML = '<span>⏳</span> 游戏启动中…';
else if (state === 'started') btn.innerHTML = '<span>✔</span> 游戏已启动';
else btn.innerHTML = '<span>▶</span> 开始游戏';
}
$('shellStart').addEventListener('click', () => {
if (startState === 'starting') return;
setStartState('starting');
doAction('start');
});
$('shellStartMenu').addEventListener('click', () => $('shellStartPop').classList.toggle('hidden'));
document.querySelectorAll('#shellStartPop button').forEach(b => b.addEventListener('click', () => {
$('shellStartPop').classList.add('hidden');
if (b.dataset.shellAction === 'stop') setStartState('idle');
doAction(b.dataset.shellAction);
}));
$('shellStop').addEventListener('click', () => {
setStartState('idle');
doAction('stop');
});
async function doAction(action) {
const map = {start:'start', launch:'start', stop:'stop'};
const payload = {action: map[action] || action};
if (payload.action === 'start') {
payload.account = $('shellAccountSelect').value || '';
}
try {
const r = await post('/api/action', payload);
if (!r.ok) {
if (startState === 'starting') setStartState('idle');
alert(r.error || '操作失败');
}
setTimeout(loadStart, 900);
} catch (e) {
if (startState === 'starting') setStartState('idle');
alert('操作失败:' + e.message);
}
}
async function loadStart() {
try {
const s = await api('/api/status');
const running = !!s.server_running;
if (running && startState !== 'started') setStartState('started');
else if (!running && startState === 'started') setStartState('idle');
$('shellStatusDot').style.background = running ? '#3dd68b' : '#5a7568';
$('shellStatusText').textContent = running ? '运行中' : '未运行';
$('shellPid').textContent = running ? '服务端已启动' : 'PID —';
$('shellBottomStatus').textContent = running ? '运行中' : '未运行';
$('statClient').textContent = running ? '运行中' : '未运行';
$('statPorts').textContent = running ? '18111 / 7001' : '—';
$('statPvf').textContent = s.db_exists ? '已校验通过' : '未检测';
$('statClientDir').textContent = s.client_dir || '—';
$('statDb').textContent = s.db_exists ? 'SQLite 就绪' : '未创建';
$('statGmPort').textContent = '18111';
$('statApiPort').textContent = location.port || '19527';
$('shellBottomPort').textContent = running ? '18111 / 7001' : '—';
$('statState').textContent = s.control_exists && s.client_dir_exists ? '已就绪' : '不完整';
$('statClient').textContent = s.client_dir_exists ? (running ? '运行中' : '已就绪') : '缺失';
$('shellClock').textContent = '检查于 ' + new Date().toLocaleTimeString('zh-CN', {hour12:false});
$('statResource').textContent = s.admin ? '管理员' : '普通权限';
loadAccountLabel();
loadUpdateStatus();
} catch (e) { /* ignore */ }
}
async function loadAccountLabel() {
try {
const r = await api('/api/accounts-config');
const list = (r && r.data) || [];
const selected = list.find(a => a.is_default) || list[0];
$('shellAccountName').textContent = selected ? selected.username : '未配置账号';
const sel = $('shellAccountSelect');
if (sel && list.length) {
const current = sel.value;
sel.innerHTML = list.map(a => `<option value="${a.username}">${a.username}</option>`).join('');
sel.value = selected ? selected.username : (current || list[0].username);
}
} catch (e) { /* ignore */ }
}
// 检查更新:填充「版本」卡片里的「本地 / 远程最新 / 状态」
async function loadUpdateStatus() {
const remoteEl = $('statRemote');
const versionEl = $('statVersion');
try {
const r = await api('/api/update/check');
if (versionEl) versionEl.textContent = r.current || '—';
if (!remoteEl) return;
if (r.error) {
remoteEl.textContent = '检查失败';
remoteEl.className = '';
remoteEl.title = '无法连接更新服务器:' + r.error;
} else if (r.has_update && r.latest) {
remoteEl.textContent = r.latest;
remoteEl.className = 'green';
remoteEl.title = '发现新版本,点击查看';
remoteEl.style.cursor = 'pointer';
remoteEl.onclick = () => { window.open(r.latest_url, '_blank'); };
} else if (r.latest) {
remoteEl.textContent = r.latest;
remoteEl.className = 'green';
remoteEl.title = '已是最新版本';
} else {
remoteEl.textContent = '暂无发布';
remoteEl.className = '';
}
} catch (e) {
if (remoteEl) { remoteEl.textContent = '检查失败'; remoteEl.className = ''; }
}
}
// 更新日志页面:拉取所有 Release 的 changelog
async function loadChangelog() {
const box = $('changelogBody');
if (!box) return;
box.innerHTML = '<div class="empty-box">正在获取更新日志…</div>';
try {
const r = await api('/api/update/changelog');
if (!r.ok) {
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
return;
}
const rels = r.releases || [];
if (!rels.length) {
box.innerHTML = '<div class="empty-box">暂无发布记录。<br>在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。</div>';
return;
}
box.innerHTML = rels.map(rel => {
const badge = rel.prerelease ? '<span class="gm-tag gm-tag-gray">预发布</span>' : '';
const body = (rel.body || '').replace(/\r?\n/g, '<br>');
const assets = (rel.assets || []).map(a =>
`<a class="changelog-asset" href="${escapeHtml(a.download_url)}" target="_blank">⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)</a>`
).join('');
const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : '';
return `<div class="changelog-item">
<div class="changelog-head">
<span class="changelog-tag">${escapeHtml(rel.tag || rel.name || '')}</span>
<span class="changelog-name">${escapeHtml(rel.name || '')}</span>
${badge}
<span class="changelog-time">${escapeHtml(when)}</span>
</div>
<div class="changelog-body">${body}</div>
${assets ? '<div class="changelog-assets">' + assets + '</div>' : ''}
<a class="changelog-link" href="${escapeHtml(rel.html_url)}" target="_blank">在浏览器中查看 ↗</a>
</div>`;
}).join('');
} catch (e) {
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
}
}
$('shellAccountSelect').addEventListener('change', async () => {
const username = $('shellAccountSelect').value;
try {
const r = await post('/api/accounts-config', {op:'select', username});
if (r.ok) { $('shellAccountName').textContent = username; }
} catch (e) { /* ignore */ }
});
// ---------------- 一键换装 ----------------
function normalizeConfig(cfg) {
cfg = cfg || {};
if (!Array.isArray(cfg.Pages) || !cfg.Pages.length) cfg.Pages = [{Name:'默认页', Schemes:[]}];
cfg.Pages.forEach(p => { if (!Array.isArray(p.Schemes)) p.Schemes = []; });
cfg.ActivePage = Math.max(0, Math.min(Number(cfg.ActivePage) || 0, cfg.Pages.length - 1));
return cfg;
}
async function loadLoadout() {
try {
const r = await api('/api/loadout/config');
loConfig = normalizeConfig(r.data);
loPage = loConfig.ActivePage || 0;
renderLoadout();
} catch (e) { $('loStatus').textContent = '读取失败:' + e.message; }
}
function currentPage() { return loConfig.Pages[loPage]; }
function currentScheme() { return currentPage().Schemes[loScheme] || null; }
function loadoutCooldownRemaining() {
return Math.max(0, Math.ceil((loCooldownUntil - Date.now()) / 1000));
}
function updateLoadoutCooldown() {
const s = currentScheme();
const active = s && s.CharacterName === loCooldownCharacter;
const remain = active ? loadoutCooldownRemaining() : 0;
const el = $('loCooldown');
if (remain > 0) {
el.textContent = `换装冷却:${remain} 秒`;
el.classList.add('active');
$('loApply').disabled = true;
} else {
el.textContent = '';
el.classList.remove('active');
$('loApply').disabled = false;
if (loCooldownTimer) { clearInterval(loCooldownTimer); loCooldownTimer = null; }
}
}
function startLoadoutCooldown(character, seconds) {
loCooldownCharacter = character || '';
loCooldownUntil = Date.now() + Math.max(0, Number(seconds) || 0) * 1000;
if (loCooldownTimer) clearInterval(loCooldownTimer);
updateLoadoutCooldown();
loCooldownTimer = setInterval(updateLoadoutCooldown, 250);
}
function renderLoadout() {
const pages = $('loPages'); pages.innerHTML = '';
loConfig.Pages.forEach((p, i) => {
const b = document.createElement('button');
b.className = 'lo-page' + (i === loPage ? ' active' : '');
b.textContent = p.Name || ('页 ' + (i + 1));
b.onclick = async () => { loPage = i; loConfig.ActivePage = i; loScheme = -1; renderLoadout(); await saveLoadout(false); };
pages.appendChild(b);
});
const list = $('loList'); list.innerHTML = '';
currentPage().Schemes.forEach((s, i) => {
const d = document.createElement('div');
d.className = 'lo-item' + (i === loScheme ? ' active' : '');
d.textContent = `${s.Name || '方案'} [${s.CharacterName || '未绑定'}] ${hotkeyText(s.Modifiers, s.KeyCode)} (${(s.Slots||[]).length} 件)`;
d.onclick = () => { loScheme = i; renderLoadout(); };
list.appendChild(d);
});
const s = currentScheme();
$('loName').value = s ? (s.Name || '') : '';
$('loCharacter').value = s ? (s.CharacterName || '') : '';
$('loHotkey').value = s ? hotkeyText(s.Modifiers, s.KeyCode) : '未设置';
const slots = $('loSlots');
slots.innerHTML = '';
if (!s || !(s.Slots || []).length) {
slots.innerHTML = '<div class="empty-box">选择方案后显示装备快照;点击「保存当前装备」从服务端读取。</div>';
} else {
s.Slots.forEach(it => {
const d = document.createElement('div');
d.className = 'lo-slot';
d.innerHTML = `槽位 ${it.slot}<b>${it.item_id}</b>`;
slots.appendChild(d);
});
}
updateLoadoutCooldown();
}
function hotkeyText(mods, key) {
if (!key) return '未设置';
const p = [];
if (mods & 2) p.push('Ctrl');
if (mods & 1) p.push('Alt');
if (mods & 4) p.push('Shift');
p.push(keyName(key));
return p.join('+');
}
function keyName(code) {
if (code >= 112 && code <= 123) return 'F' + (code - 111);
if (code >= 65 && code <= 90) return String.fromCharCode(code);
if (code >= 48 && code <= 57) return String.fromCharCode(code);
return String(code);
}
$('loAddPage').addEventListener('click', async () => {
const name = prompt('换装页名称', '换装页' + (loConfig.Pages.length + 1));
if (!name) return;
loConfig.Pages.push({Name:name, Schemes:[]});
loPage = loConfig.Pages.length - 1; loConfig.ActivePage = loPage; loScheme = -1;
renderLoadout(); await saveLoadout();
});
$('loAddScheme').addEventListener('click', async () => {
const name = prompt('方案名称', '方案' + (currentPage().Schemes.length + 1));
if (!name) return;
const character = prompt('绑定角色游戏内名称(可稍后修改)', '') || '';
currentPage().Schemes.push({Name:name, CharacterName:character, Modifiers:0, KeyCode:0, Slots:[]});
loScheme = currentPage().Schemes.length - 1;
renderLoadout(); await saveLoadout();
});
$('loDeleteScheme').addEventListener('click', async () => {
if (loScheme < 0) return;
if (!confirm('删除当前方案?')) return;
currentPage().Schemes.splice(loScheme, 1);
loScheme = -1; renderLoadout(); await saveLoadout();
});
$('loSaveConfig').addEventListener('click', () => saveLoadout());
$('loName').addEventListener('change', () => { const s=currentScheme(); if(s){s.Name=$('loName').value; renderLoadout();} });
$('loCharacter').addEventListener('change', () => { const s=currentScheme(); if(s){s.CharacterName=$('loCharacter').value.trim(); renderLoadout();} });
async function saveLoadout(show = true) {
try {
if (loConfig) { loConfig.ActivePage = loPage; const s=currentScheme(); if(s){s.Name=$('loName').value; s.CharacterName=$('loCharacter').value.trim();} }
const r = await post('/api/loadout/config', {config: loConfig});
if (r.ok) { if (show) $('loStatus').textContent = '配置已保存 ' + new Date().toLocaleTimeString(); }
else $('loStatus').textContent = r.error || '保存失败';
} catch (e) { $('loStatus').textContent = '保存失败:' + e.message; }
}
$('loRefreshChars').addEventListener('click', async () => {
try {
const r = await api('/api/loadout/characters');
if (!r.ok) { $('loStatus').textContent = r.error || '读取失败'; return; }
const list = r.data || [];
$('loStatus').textContent = list.map(c => `${c.name}(${c.online?'在线':'离线'})`).join(' ') || '没有角色';
} catch (e) { $('loStatus').textContent = e.message; }
});
$('loSaveCurrent').addEventListener('click', async () => {
const s = currentScheme();
if (!s) { $('loStatus').textContent = '请先选择方案'; return; }
if (!s.CharacterName) { $('loStatus').textContent = '请先填写绑定角色'; return; }
try {
const r = await api('/api/loadout/equipment?character=' + encodeURIComponent(s.CharacterName));
if (!r.ok) { $('loStatus').textContent = r.error || '读取装备失败'; return; }
s.Slots = r.data.slots || [];
renderLoadout(); await saveLoadout();
$('loStatus').textContent = `已保存 ${s.Slots.length} 个部位`;
} catch (e) { $('loStatus').textContent = e.message; }
});
$('loApply').addEventListener('click', async () => {
const s = currentScheme();
if (!s) { $('loStatus').textContent = '请先选择方案'; return; }
if (s.CharacterName === loCooldownCharacter && loadoutCooldownRemaining() > 0) {
$('loStatus').textContent = `换装冷却中,还需 ${loadoutCooldownRemaining()} 秒`;
return;
}
try {
const r = await post('/api/loadout/apply', {character:s.CharacterName, slots:s.Slots || []});
if (r.ok) {
const d = r.data || {};
const applied = (d.applied || []).length;
const skipped = (d.skipped || []).length;
if (Number(d.cooldown_seconds) > 0) {
startLoadoutCooldown(s.CharacterName, Number(d.cooldown_seconds));
$('loStatus').textContent = `应用完成:成功 ${applied},跳过 ${skipped};换装冷却 ${d.cooldown_seconds} 秒`;
} else {
$('loStatus').textContent = `应用完成:成功 ${applied},跳过 ${skipped}`;
}
} else {
const msg = r.error || '应用失败';
const m = msg.match(/cooldown[^0-9]*(\d+)\s*seconds?/i);
if (m) {
startLoadoutCooldown(s.CharacterName, Number(m[1]));
$('loStatus').textContent = `换装冷却中,还需 ${m[1]} 秒才能再次换装`;
} else {
$('loStatus').textContent = msg;
}
}
} catch (e) { $('loStatus').textContent = e.message; }
});
$('loCaptureHotkey').addEventListener('click', () => {
if (!currentScheme()) { $('loStatus').textContent = '请先选择方案'; return; }
hotkeyCapture = true; $('loHotkey').value = '请按下组合键…'; $('loHotkey').focus();
});
$('loHotkey').addEventListener('keydown', (e) => {
if (!hotkeyCapture) return;
e.preventDefault();
let mods = 0;
if (e.ctrlKey) mods |= 2;
if (e.altKey) mods |= 1;
if (e.shiftKey) mods |= 4;
const key = e.keyCode;
if (!key || key === 17 || key === 18 || key === 16) return;
const s = currentScheme();
if (!s) return;
s.Modifiers = mods; s.KeyCode = key;
hotkeyCapture = false;
$('loHotkey').value = hotkeyText(mods, key);
saveLoadout();
});
// ==================== 游戏设置(频道开关 / 活动开关) ====================
let gsCurrentTab = 'channels';
const gsToast = (msg, type = 'info') => {
if (typeof gmToast === 'function') gmToast(msg, type);
else console.log(msg);
};
function gsSwitchTab(tab) {
gsCurrentTab = tab;
document.querySelectorAll('#gsTabs .st-tab').forEach(el =>
el.classList.toggle('active', el.dataset.gs === tab));
$('gsChannelsPane').style.display = tab === 'channels' ? '' : 'none';
$('gsActivitiesPane').style.display = tab === 'activities' ? '' : 'none';
}
function gsChannelTypeText(type) {
const t = Number(type);
if (t === 22) return '特殊区域';
if (t === 11) return '普通城镇';
if (t === 2 || t === 3) return '引导';
return '频道 ' + t;
}
async function gsLoadChannels() {
const grid = $('gsChannelGrid');
const summary = $('gsChannelSummary');
if (!grid) return;
grid.innerHTML = '<div class="empty-box">加载中…</div>';
try {
const r = await api('/api/game-settings/channel-switches');
if (!r.ok) {
grid.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
summary.textContent = '';
return;
}
const d = r.data || {};
const list = d.channels || [];
summary.textContent = '共 ' + (d.total_channels ?? list.length) + ' 个频道 · 已启用 ' +
(d.enabled_channels ?? '—') + ' · 已停用 ' + (d.disabled_channels ?? '—');
if (!list.length) {
grid.innerHTML = '<div class="empty-box">暂无频道数据。请确认服务端已启动。</div>';
return;
}
grid.innerHTML = list.map(ch => {
const on = !!ch.enabled;
return `<div class="gs-card ${on ? '' : 'gs-off'}">
<div class="gs-card-head">
<span class="gs-card-name">${escapeHtml(ch.name || ('频道 ' + ch.id))}</span>
<span class="gm-tag ${on ? 'gm-tag-green' : 'gm-tag-gray'}">${on ? '启用' : '停用'}</span>
</div>
<div class="gs-card-meta muted">ID ${ch.id} · ${gsChannelTypeText(ch.type)}${ch.group ? ' · ' + escapeHtml(ch.group) : ''}${ch.port ? ' · 端口 ' + ch.port : ''}</div>
<div class="gs-card-actions">
<button class="shell-btn ${on ? '' : 'primary'}" data-gs-channel="${ch.id}" data-gs-enable="${!on}">${on ? '停用' : '启用'}</button>
</div>
</div>`;
}).join('');
} catch (e) {
grid.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
}
}
async function gsLoadActivities() {
const list = $('gsActivityList');
const summary = $('gsActivitySummary');
if (!list) return;
list.innerHTML = '<div class="empty-box">加载中…</div>';
try {
const r = await api('/api/game-settings/activity-switches');
if (!r.ok) {
list.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
summary.textContent = '';
return;
}
const d = r.data || {};
const items = d.switches || [];
const onCount = items.filter(x => x.enabled).length;
summary.textContent = '共 ' + items.length + ' 个活动 · 已开启 ' + onCount;
if (!items.length) {
list.innerHTML = '<div class="empty-box">暂无活动数据。请确认服务端已启动。</div>';
return;
}
list.innerHTML = items.map(it => {
const on = !!it.enabled;
return `<div class="gs-row ${on ? '' : 'gs-off'}">
<div class="gs-row-main">
<div class="gs-row-name">${escapeHtml(it.title || it.key)}</div>
<div class="gs-row-desc muted">${escapeHtml(it.description || '')}</div>
</div>
<button class="gs-switch ${on ? 'on' : ''}" data-gs-activity="${escapeHtml(it.key)}" data-gs-enable="${!on}" role="switch" aria-checked="${on}">
<span class="gs-knob"></span>
</button>
</div>`;
}).join('');
} catch (e) {
list.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
}
}
async function loadGameSettings() {
gsSwitchTab(gsCurrentTab);
if (gsCurrentTab === 'channels') await gsLoadChannels();
else await gsLoadActivities();
}
// 刷新按钮
$('gsRefresh').addEventListener('click', () => {
if (gsCurrentTab === 'channels') gsLoadChannels();
else gsLoadActivities();
});
// Tab 切换
document.querySelectorAll('#gsTabs .st-tab').forEach(el => {
el.addEventListener('click', () => {
gsSwitchTab(el.dataset.gs);
if (gsCurrentTab === 'channels') gsLoadChannels();
else gsLoadActivities();
});
});
// 频道开关点击(事件委托)
$('gsChannelGrid').addEventListener('click', async (e) => {
const btn = e.target.closest('button[data-gs-channel]');
if (!btn) return;
const id = Number(btn.dataset.gsChannel);
const enable = btn.dataset.gsEnable === 'true';
btn.disabled = true;
btn.textContent = '处理中…';
try {
const r = await post('/api/game-settings/channel-switch', { channel_id: id, enabled: enable });
if (r.ok) {
gsToast(enable ? '频道已启用' : '频道已停用', 'success');
await gsLoadChannels();
} else {
gsToast('操作失败:' + (r.error || '未知错误'), 'error');
btn.disabled = false;
btn.textContent = enable ? '启用' : '停用';
}
} catch (e) {
gsToast('操作失败:' + (e && e.message ? e.message : e), 'error');
btn.disabled = false;
btn.textContent = enable ? '启用' : '停用';
}
});
// 活动开关点击(事件委托)
$('gsActivityList').addEventListener('click', async (e) => {
const sw = e.target.closest('button[data-gs-activity]');
if (!sw) return;
const key = sw.dataset.gsActivity;
const enable = sw.dataset.gsEnable === 'true';
sw.disabled = true;
try {
const r = await post('/api/game-settings/activity-switch', { key: key, enabled: enable });
if (r.ok) {
gsToast(enable ? '活动已开启' : '活动已关闭', 'success');
await gsLoadActivities();
} else {
gsToast('操作失败:' + (r.error || '未知错误'), 'error');
sw.disabled = false;
}
} catch (e) {
gsToast('操作失败:' + (e && e.message ? e.message : e), 'error');
sw.disabled = false;
}
});
// 顶栏拖动窗口(JS move 兜底,兼容 pywebview EdgeChromium)
// 同时支持第一行标题栏(.shell-topnav)与第二行面包屑栏(.shell-topbar)
['shell-topnav', 'shell-topbar'].forEach((sel) => {
const topbar = document.querySelector('.' + sel);
if (!topbar) return;
let dragState = null;
const onMove = (e) => {
if (!dragState) return;
const w = win();
if (w && w.move) w.move(dragState.x + e.screenX - dragState.sx, dragState.y + e.screenY - dragState.sy);
};
const onUp = () => {
dragState = null;
document.removeEventListener('mousemove', onMove, true);
document.removeEventListener('mouseup', onUp, true);
};
topbar.addEventListener('mousedown', (e) => {
if (e.target.closest('button,select,input,a')) return;
const w = win();
if (!w || !w.getPosition) return;
e.preventDefault();
const sx = e.screenX, sy = e.screenY;
w.getPosition().then((pos) => {
dragState = {x: Number(pos[0]) || 0, y: Number(pos[1]) || 0, sx, sy};
document.addEventListener('mousemove', onMove, true);
document.addEventListener('mouseup', onUp, true);
}).catch(() => {});
});
});
document.querySelectorAll('.win-resize').forEach(el => {
el.addEventListener('mousedown', (e) => {
e.preventDefault();
const edge = el.dataset.edge;
const w = win();
if (!w) return;
// 优先系统边缘缩放;右侧/底部再用 JS 尺寸兜底
if (['left','top','topleft','topright','bottomleft'].includes(edge) && w.start_resize) w.start_resize(edge);
if (['right','bottom','bottomright'].includes(edge) && w.resize_window) {
const sx = e.screenX, sy = e.screenY;
const sw = window.innerWidth, sh = window.innerHeight;
let rz = {edge, sx, sy, sw, sh};
const move = (ev) => {
let nw = sw, nh = sh;
if (edge.includes('right') || edge === 'right') nw = sw + ev.screenX - sx;
if (edge.includes('bottom') || edge === 'bottom') nh = sh + ev.screenY - sy;
w.resize_window(nw, nh);
};
const up = () => {
rz = null;
document.removeEventListener('mousemove', move, true);
document.removeEventListener('mouseup', up, true);
};
document.addEventListener('mousemove', move, true);
document.addEventListener('mouseup', up, true);
}
});
});
// 兼容旧 GM 页面里的返回
window.shellGo = shellGo;
loadStart();
setInterval(loadStart, 4000);
})();