- app.js init(): use native fetch (no Bearer) to call /api/install/status and /api/settings/settings, so site_name loads even before login state is confirmed; call Auth.reset() if logged but no site_name set - auth.js: add reset() method that clears TK/US without redirecting - app.js route(): remove early return for uninstalled; let install check run first so init() can fall through to normal routing when installed - app.js renderMain(): catch 401/login errors from page render, clear Auth state, show login prompt with button that redirects back after login (target hash preserved)
244 lines
12 KiB
JavaScript
244 lines
12 KiB
JavaScript
/*
|
|
* MC Report System
|
|
* Copyright (C) 2026 Sea Network Technology Studio
|
|
* Author: CangLan <admin@sea-studio.top>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
const App = {
|
|
navItems: [
|
|
{ id: 'dashboard', label: '控制台', icon: 'fa-chart-simple', roles: ['owner','admin','player'] },
|
|
{ id: 'tickets-create', label: '提交工单', icon: 'fa-plus-circle', roles: ['player'] },
|
|
{ id: 'bans', label: '封禁列表', icon: 'fa-ban', roles: ['owner','admin','player'] },
|
|
{ id: 'unclaimed', label: '待处理工单', icon: 'fa-inbox', roles: ['owner','admin'] },
|
|
{ id: 'users', label: '用户管理', icon: 'fa-users', roles: ['owner'] },
|
|
{ id: 'servers', label: '服务器管理', icon: 'fa-server', roles: ['owner'] },
|
|
{ id: 'polls', label: '投票', icon: 'fa-poll', roles: ['owner'] },
|
|
{ id: 'features', label: '更新内容', icon: 'fa-lightbulb', roles: ['owner'] },
|
|
{ id: 'notifications', label: '通知配置', icon: 'fa-bell', roles: ['owner'] },
|
|
{ id: 'external-api', label: '外部API', icon: 'fa-key', roles: ['owner'] },
|
|
{ id: 'sources', label: '来源管理', icon: 'fa-tags', roles: ['owner'] },
|
|
{ id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner'] },
|
|
{ id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner'] },
|
|
{ id: 'logs', label: '系统日志', icon: 'fa-clipboard-list', roles: ['owner','admin'] },
|
|
{ id: 'export', label: '数据导出', icon: 'fa-download', roles: ['owner'] },
|
|
],
|
|
|
|
async init() {
|
|
// 获取站点名称(无论是否安装),使用原生 fetch 避免携带 Bearer token
|
|
try {
|
|
const r = await fetch('/api/install/status');
|
|
const s = await r.json();
|
|
if (!s.installed) { location.hash = '#/install'; return; }
|
|
} catch { location.hash = '#/install'; return; }
|
|
try {
|
|
const nameRes = await fetch('/api/settings/settings');
|
|
if (nameRes.ok) {
|
|
const settings = await nameRes.json();
|
|
if (settings.site_name && settings.site_name.value) window.__SITE_NAME__ = settings.site_name.value;
|
|
}
|
|
} catch {}
|
|
// 确保登录态同步:若 cookie 还在但 token 已过期,清除
|
|
if (Auth.logged() && !window.__SITE_NAME__) Auth.reset();
|
|
if (window.__SITE_NAME__) {
|
|
document.getElementById('site-title').textContent = window.__SITE_NAME__;
|
|
document.getElementById('sidebar-title').textContent = window.__SITE_NAME__ || '举报系统';
|
|
}
|
|
this._footerLoaded = false;
|
|
window.addEventListener('hashchange', () => this.route());
|
|
window.onerror = (msg, url, line, col, err) => {
|
|
console.error('JS Error:', msg, url, line, col);
|
|
const el = document.getElementById('sidebar-nav');
|
|
if (el) el.insertAdjacentHTML('beforeend', '<div style="color:red;font-size:10px;padding:8px">Error: ' + String(msg).substring(0,80) + '</div>');
|
|
};
|
|
document.getElementById('modal-close-btn').onclick = () => this.closeModal();
|
|
document.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('[data-action]');
|
|
if (!btn) return;
|
|
const parts = btn.dataset.action.split(':');
|
|
const target = window[parts[0]];
|
|
if (target && target[parts[1]]) target[parts[1]](...parts.slice(2));
|
|
});
|
|
this.route();
|
|
},
|
|
|
|
async loadFooter() {
|
|
if (!Auth.isAdmin() || this._footerLoaded) return;
|
|
this._footerLoaded = true;
|
|
try {
|
|
const s = await API.get('/settings/settings');
|
|
const cr = document.getElementById('footer-copyright');
|
|
const icp = document.getElementById('footer-icp');
|
|
if (cr && s.copyright) cr.innerHTML = s.copyright.value;
|
|
if (icp && s.icp && s.icp.value) icp.innerHTML = `<a href="https://beian.miit.gov.cn" target="_blank">${s.icp.value}</a>`;
|
|
} catch {}
|
|
},
|
|
|
|
afterLogin() { location.hash = '#/dashboard'; },
|
|
logout() { Auth.logout(); },
|
|
closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); },
|
|
navigate(page, param) { location.hash = `#/${page}` + (param ? `/${param}` : ''); },
|
|
|
|
route() {
|
|
const hash = location.hash || '#/home';
|
|
const parts = hash.replace('#/', '').split('/');
|
|
const page = parts[0];
|
|
const param = parts.slice(1).join('/');
|
|
|
|
if (page === 'verify') {
|
|
this.showPublic('verify');
|
|
return;
|
|
}
|
|
|
|
if (page === 'install') { this.showPublic('install'); return; }
|
|
if (page === 'home') { this.showPublic('home', param); return; }
|
|
if (page === 'login' || page === 'register' || page === 'forgot' || page === 'reset') { this.showPublic(page); return; }
|
|
if (!Auth.logged()) { location.hash = '#/login'; return; }
|
|
|
|
this.showLayout();
|
|
const sidebarPage = (page === 'tickets' && param.startsWith('create')) ? 'tickets-create' : page;
|
|
this.renderSidebar(sidebarPage);
|
|
|
|
switch (page) {
|
|
case 'dashboard': this.renderMain('控制台', Dashboard, param); break;
|
|
case 'unclaimed': this.renderMain('待处理工单', UnclaimedPage, param); break;
|
|
case 'tickets-create': App.navigate('tickets','create'); break;
|
|
case 'tickets':
|
|
if (param.startsWith('create')) {
|
|
const q = new URLSearchParams((location.hash.split('?')[1] || ''));
|
|
document.getElementById('page-title').textContent = '创建工单';
|
|
document.getElementById('page-actions').innerHTML = '<button class="btn btn-o btn-sm" data-action="App:navigate:tickets"><i class="fas fa-arrow-left"></i> 返回</button>';
|
|
TicketCreate.render(document.getElementById('page-content'), { type: q.get('type') || 'report', prefill: { game_name: Auth.user()?.game_name, game_uid: Auth.user()?.game_uid }, backLink: "App.navigate('tickets')" });
|
|
} else this.renderMain('工单列表', TicketsPage, param);
|
|
break;
|
|
case 'ticket': this.renderMain('工单详情', TicketDetail, param); break;
|
|
case 'tracking': this.renderMain('追踪工单', TrackingPage, param); break;
|
|
case 'users': this.renderMain('用户管理', UsersPage, param); break;
|
|
case 'notifications': this.renderMain('通知配置', NotificationsPage, param); break;
|
|
case 'external-api': this.renderMain('外部API', ExternalApiPage, param); break;
|
|
case 'sources': this.renderMain('来源管理', SourcesPage, param); break;
|
|
case 'templates': this.renderMain('邮件模板', TemplatesPage, param); break;
|
|
case 'settings': this.renderMain('系统设置', SettingsPage, param); break;
|
|
case 'export': this.renderMain('数据导出', ExportPage, param); break;
|
|
case 'polls': this.renderMain('投票', PollsPage, param); break;
|
|
case 'features': this.renderMain('更新内容', FeaturesPage, param); break;
|
|
case 'feature': this.renderMain('更新详情', FeatureDetail, param); break;
|
|
case 'servers': this.renderMain('服务器管理', ServersPage, param); break;
|
|
case 'bans': this.renderMain('封禁列表', BansPage, param); break;
|
|
case 'logs': this.renderMain('系统日志', LogsPage, param); break;
|
|
default: this.renderMain('控制台', Dashboard, param);
|
|
}
|
|
},
|
|
|
|
showPublic(page, param) {
|
|
document.getElementById('main-layout').classList.add('hidden');
|
|
const pub = document.getElementById('public-page');
|
|
pub.classList.remove('hidden');
|
|
document.getElementById('top-bar').classList.remove('hidden');
|
|
this.updateTopAuth();
|
|
const comp = page === 'login' ? LoginPage : page === 'register' ? RegisterPage : page === 'install' ? InstallPage : page === 'forgot' ? ForgotPage : page === 'reset' ? ResetPage : page === 'verify' ? VerifyPage : HomePage;
|
|
comp.render(param).then(html => { pub.innerHTML = html; if (comp.mount) comp.mount(param); }).catch(() => {
|
|
pub.innerHTML = '<div class="pub-card"><div class="logo"><i class="fas fa-exclamation-triangle" style="color:var(--d)"></i><h2>加载失败</h2><p>请刷新页面重试</p></div></div>';
|
|
});
|
|
},
|
|
|
|
updateTopAuth() {
|
|
const el = document.getElementById('top-auth');
|
|
if (Auth.logged()) {
|
|
const u = Auth.user();
|
|
el.innerHTML = `<span class="tm ts">${U.esc(u.game_name||u.username)}</span>
|
|
${Auth.isAdmin() ? `<a href="#/dashboard" class="btn btn-p btn-sm"><i class="fas fa-chart-simple"></i> 控制台</a>` : ''}
|
|
${Auth.isAdmin() ? `<a href="#/unclaimed" class="btn btn-o btn-sm">待处理</a>` : ''}
|
|
${Auth.isOwner() ? `<a href="#/polls" class="btn btn-o btn-sm"><i class="fas fa-poll"></i></a>` : ''}
|
|
<button class="btn btn-o btn-sm" onclick="App.logout()"><i class="fas fa-sign-out-alt"></i> 退出</button>`;
|
|
} else {
|
|
el.innerHTML = `<a href="#/login" class="btn btn-o btn-sm"><i class="fas fa-sign-in-alt"></i> 登录</a><a href="#/register" class="btn btn-p btn-sm"><i class="fas fa-user-plus"></i> 注册</a>`;
|
|
el.style.display = 'flex';
|
|
}
|
|
},
|
|
|
|
showLayout() {
|
|
document.getElementById('top-bar').classList.add('hidden');
|
|
document.getElementById('public-page').classList.add('hidden');
|
|
document.getElementById('main-layout').classList.remove('hidden');
|
|
const u = Auth.user();
|
|
document.getElementById('user-info').innerHTML = `<div class="dt"><div class="nm">${U.esc(u.game_name||u.username)}</div></div>`;
|
|
document.getElementById('sidebar-logout').onclick = () => App.logout();
|
|
this.loadFooter();
|
|
},
|
|
|
|
renderSidebar(current) {
|
|
const u = Auth.user();
|
|
const nav = document.getElementById('sidebar-nav');
|
|
nav.innerHTML = this.navItems
|
|
.filter(i => i.roles.includes(u.role))
|
|
.map(i => `<div class="nav-item ${current===i.id?'active':''}" data-nav="${i.id}"><i class="fas ${i.icon}"></i><span>${i.label}</span></div>`).join('');
|
|
nav.querySelectorAll('.nav-item').forEach(el => {
|
|
el.addEventListener('click', () => App.navigate(el.dataset.nav));
|
|
});
|
|
},
|
|
|
|
async renderMain(title, page, param) {
|
|
document.getElementById('page-title').textContent = title;
|
|
document.getElementById('page-actions').innerHTML = '';
|
|
const ct = document.getElementById('page-content');
|
|
try {
|
|
ct.innerHTML = await page.render(param);
|
|
if (page.mount) await page.mount(param);
|
|
} catch (e) {
|
|
// 401 / 登录过期: 清除状态, 保留访问目标页 hash 提示重新登录
|
|
if (e && e.message && e.message.includes('登录')) {
|
|
Auth.reset();
|
|
const target = location.hash;
|
|
ct.innerHTML = `<div class="alert alert-e">登录已过期,请重新登录。</div>
|
|
<div class="mt-2"><button class="btn btn-p" onclick="location.hash='#/login';setTimeout(()=>location.hash='${target}',2000)">去登录</button></div>`;
|
|
} else throw e;
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => App.init());
|
|
window.App = App;
|
|
window.Dashboard = Dashboard;
|
|
window.TicketsPage = TicketsPage;
|
|
window.TicketDetail = TicketDetail;
|
|
window.TicketCreate = TicketCreate;
|
|
window.TrackingPage = TrackingPage;
|
|
window.UnclaimedPage = UnclaimedPage;
|
|
window.UsersPage = UsersPage;
|
|
window.TemplatesPage = TemplatesPage;
|
|
window.NotificationsPage = NotificationsPage;
|
|
window.ExternalApiPage = ExternalApiPage;
|
|
window.SourcesPage = SourcesPage;
|
|
window.SettingsPage = SettingsPage;
|
|
window.ExportPage = ExportPage;
|
|
window.PollsPage = PollsPage;
|
|
window.FeaturesPage = FeaturesPage;
|
|
window.FeatureDetail = FeatureDetail;
|
|
window.ServersPage = ServersPage;
|
|
window.BansPage = BansPage;
|
|
window.LogsPage = LogsPage;
|
|
window.LoginPage = LoginPage;
|
|
window.RegisterPage = RegisterPage;
|
|
window.HomePage = HomePage;
|
|
window.InstallPage = InstallPage;
|
|
window.ForgotPage = ForgotPage;
|
|
window.ResetPage = ResetPage;
|
|
window.VerifyPage = VerifyPage;
|
|
window.U = U;
|
|
window.Auth = Auth;
|
|
window.API = API;
|
|
window.App = App;
|