启动器新增游戏设置页(频道开关+活动开关),窗口标题改名

- 服务端新增活动开关运行时配置与 GM 接口(GET/POST /dnf/gm/activity-switches),
  持久化到 Settings,接入月签到/周签到/PC房↔累计在线的实时下发
- 服务端新增频道开关 GM 接口(channel-switches),目录生成时过滤禁用频道
- channelcatalog/channelinfo 支持按禁用频道 ID 过滤目录与客户端脚本
- 启动器新增「游戏设置」页面:频道开关 Tab + 活动开关 Tab
- native.py/server.py 新增对应转发 API
- 窗口标题/页面 title/侧边栏「90轮回」统一改为「DNF90 灵狐小竹整合单机版」
This commit is contained in:
hanfuit committed 2026-11-04 20:08:50 +08:00
1 parent 977a4a1196
commit 0ced464168
15 files changed
+1022 -14

No files matched your search

+169 -2
View File
@@ -1,13 +1,13 @@
/* 90轮回启动器 · 单窗口页面与工具整合 */
(function () {
const $ = (id) => document.getElementById(id);
const pages = ['start','gm','loadout','settings','changelog'];
const pages = ['start','gm','loadout','gamesettings','settings','changelog'];
let shellGmInit = false;
let loConfig = null, loPage = 0, loScheme = -1;
let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null;
let hotkeyCapture = false;
const crumb = {start:'开始 / 本地服务端',gm:'GM工具',loadout:'工具 / 一键换装',settings:'设置',changelog:'更新日志'};
const crumb = {start:'开始 / 本地服务端',gm:'GM工具',loadout:'工具 / 一键换装',gamesettings:'工具 / 游戏设置',settings:'设置',changelog:'更新日志'};
function shellGo(page) {
if (!pages.includes(page)) page = 'start';
@@ -16,6 +16,7 @@
$('shellCrumb').textContent = crumb[page] || page;
if (page === 'start') loadStart();
if (page === 'loadout') loadLoadout();
if (page === 'gamesettings') loadGameSettings();
if (page === 'settings') loadSettings();
if (page === 'changelog') loadChangelog();
if (page === 'gm') {
@@ -488,6 +489,172 @@
}
$('setRefresh').addEventListener('click', loadSettings);
// ==================== 游戏设置(频道开关 / 活动开关) ====================
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) => {