Files
SenSu/static/web_panel/js/app.js
T
qinglong dc0bb79a7e feat: 全局文件选择器 + 项目管理路径选择按钮
- app.js新增window.pickPath(mode,callback)全局复用
- 项目管理「工作目录」+Git部署「目标目录」添加📁选择按钮
- Git部署不再硬编码data/projects,改用用户选择的目标目录
- files.js picker模式读取URL cb参数实现callback_id回传
- app.js版本号更新至v=0701

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:11:50 +08:00

256 lines
9.7 KiB
JavaScript

/* ── SenSu WebUI — Theme + Routing + Page Loader ── */
// ═══ 主题初始化 (必须在渲染前, 防闪白) ═══
(function(){
var s = localStorage.getItem("sensu-theme") || "dark";
document.documentElement.setAttribute("data-theme", s);
})();
window.toggleTheme = function(){
var t = document.documentElement.getAttribute("data-theme") === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", t);
localStorage.setItem("sensu-theme", t);
var btn = document.querySelector(".theme-toggle");
if(btn) btn.textContent = t === "light" ? "☀️" : "🌙";
};
// ═══ 路由系统 ═══
// Hash format: #/dashboard | #/files | #/plugins/example_plugin
// Built-in pages map to ./static/pages/{name}.html
// Plugin pages load from /plugin/{name}
var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files'];
var _pluginPagesCache = null; // {plugin_name: {path, title, icon}}
function currentRoute() {
var h = location.hash.replace('#', '') || '/dashboard';
return h.replace(/^\/+/, '');
}
function navigateTo(route) {
if (!route) route = 'dashboard';
location.hash = '#' + route; // triggers hashchange → routePage()
}
function routePage() {
var r = currentRoute();
if (r.startsWith('plugins/')) {
loadPluginPage(r.replace('plugins/', ''));
} else if (BUILTIN_PAGES.indexOf(r) >= 0) {
loadPage(r);
} else {
loadPage('dashboard');
location.hash = '#/dashboard';
}
}
// ═══ onload ═══
window.onload = async function() {
var btn = document.querySelector(".theme-toggle");
if(btn) btn.textContent = document.documentElement.getAttribute("data-theme") === "light" ? "☀️" : "🌙";
try {
var res = await fetch('./api/auth/status', {credentials:'include'});
if(res.status === 401 || !res.ok) {
sessionStorage.setItem('sensu_redirect_hash', location.hash);
window.location.href = './index.html'; return;
}
var data = await res.json();
if(!data.authenticated) {
sessionStorage.setItem('sensu_redirect_hash', location.hash);
window.location.href = './index.html'; return;
}
document.getElementById('uname').textContent = data.username || 'Admin';
} catch(e) { window.location.href = './index.html'; }
window.addEventListener('hashchange', routePage);
routePage(); // load from current hash (or default to dashboard)
};
// ═══ Page loader (built-in pages) ═══
async function loadPage(pageName) {
var content = document.getElementById('page-content');
var bar = document.getElementById('progress');
// Sidebar highlight
document.querySelectorAll('.nav-item').forEach(function(el){ el.classList.remove('active'); });
document.querySelectorAll('.nav-sub-item').forEach(function(el){ el.classList.remove('active'); });
var navItem = document.querySelector('.nav-item[data-page="'+pageName+'"]');
if(navItem) navItem.classList.add('active');
content.style.padding = '';
// Progress
bar.classList.add('active'); bar.style.width = '0%';
await new Promise(function(r){ requestAnimationFrame(function(){ bar.style.width='80%'; setTimeout(r,100); }); });
// Destroy previous module
var modName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
if(window[modName] && window[modName].destroy) { try { window[modName].destroy(); } catch(e){} }
try {
var resp = await fetch('./static/pages/'+pageName+'.html');
if(!resp.ok) throw new Error('404');
var html = await resp.text();
var scripts = [];
var cleanHtml = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
content.innerHTML = cleanHtml;
scripts.forEach(function(code){
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
catch(ex){ console.error('Inline script error:', ex); }
});
// External JS module
try {
var jsResp = await fetch('./static/pages/'+pageName+'.js?t='+Date.now());
if(jsResp.ok) {
var jsCode = await jsResp.text();
var s=document.createElement('script'); s.textContent=jsCode; document.head.appendChild(s); document.head.removeChild(s);
if(window[modName] && window[modName].init) window[modName].init();
}
} catch(e){ console.error('Page JS error:', pageName, e); }
bar.style.width = '100%';
setTimeout(function(){ bar.classList.remove('active'); }, 200);
} catch(e) {
content.innerHTML = '<div style="color:var(--error);text-align:center;margin-top:20vh">页面加载失败: '+e.message+'</div>';
bar.style.background='var(--error)';
setTimeout(function(){ bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); bar.style.background='var(--primary)'; },200); },100);
}
}
// ═══ Plugin page loader ═══
async function loadPluginPage(pluginName) {
var content = document.getElementById('page-content');
var bar = document.getElementById('progress');
// Highlight sub-item
document.querySelectorAll('.nav-item').forEach(function(el){ el.classList.remove('active'); });
document.querySelectorAll('.nav-sub-item').forEach(function(el){ el.classList.remove('active'); });
var sub = document.querySelector('.nav-sub-item[data-plugin="'+pluginName+'"]');
if(sub) sub.classList.add('active');
bar.classList.add('active'); bar.style.width='0%';
await new Promise(function(r){ requestAnimationFrame(function(){ bar.style.width='80%'; setTimeout(r,100); }); });
// Also load plugin pages list if not yet
if(!_pluginPagesCache) await fetchPluginPages();
try {
var resp = await fetch('/plugin/'+pluginName, {credentials:'include'});
if(!resp.ok) throw new Error(resp.status);
var html = await resp.text();
var styles = '';
html.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, function(m,code){ styles+=code; return ''; });
var bodyMatch = html.match(/<body\b[^>]*>([\s\S]*)<\/body>/i);
var clean = bodyMatch ? bodyMatch[1] : html.replace(/<html[^>]*>|<\/html>|<head[^>]*>[\s\S]*?<\/head>|<body[^>]*>|<\/body>/gi, '');
var scripts = [];
clean = clean.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
content.style.padding = '0';
content.innerHTML = '<style>'+styles+'</style>'+clean;
scripts.forEach(function(code){
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
catch(ex){ console.error('Plugin script error:', ex); }
});
bar.style.width='100%';
setTimeout(function(){ bar.classList.remove('active'); }, 200);
} catch(ex) {
content.innerHTML = '<div style="color:var(--error);text-align:center;margin-top:20vh">插件页面加载失败: '+ex.message+'</div>';
bar.style.background='var(--error)';
setTimeout(function(){ bar.style.width='100%'; setTimeout(function(){ bar.classList.remove('active'); bar.style.background='var(--primary)'; },200); },100);
}
}
// ═══ Global file/dir picker (reusable by any page) ═══
// Usage: pickPath('dir', function(path) { document.getElementById('my-input').value = path; })
window.pickPath = function(mode, callback) {
var id = 'picker_' + Date.now();
window['_pickCallback_' + id] = callback;
var url = './static/pages/files.html?picker=1&mode=' + (mode || 'dir') + '&cb=' + id;
var w = window.open(url, 'fm-picker', 'width=680,height=520');
if (!w) { alert('请允许弹窗以使用文件选择器'); return; }
// Listen for pick result
window.addEventListener('message', function handler(e) {
try {
var d = JSON.parse(e.data);
if (d.action === 'fm-picked' && d.callback_id === id) {
window.removeEventListener('message', handler);
if (d.path && callback) callback(d.path);
}
} catch(ex) {}
});
};
// ═══ Sidebar toggle ═══
function toggleSidebar() {
document.getElementById('app').classList.toggle('collapsed');
}
// ═══ Logout ═══
async function doLogout() {
await fetch('./api/logout', {method:'POST', credentials:'include'});
window.location.href = './index.html';
}
// ═══ Plugin pages expand/collapse ═══
async function fetchPluginPages() {
try {
var res = await fetch('./api/plugin-pages', {credentials:'include'});
var data = await res.json();
_pluginPagesCache = {};
(data.pages||[]).forEach(function(p){ _pluginPagesCache[p.plugin] = p; });
return _pluginPagesCache;
} catch(e) { return {}; }
}
async function togglePluginPages() {
var body = document.getElementById('nav-plugins-body');
var arrow = document.getElementById('nav-plugins-arrow');
var isOpen = body.style.display !== 'none';
if(isOpen){ body.style.display='none'; arrow.style.transform=''; return; }
body.style.display='block';
arrow.style.transform='rotate(180deg)';
if(!_pluginPagesCache) await fetchPluginPages();
var pages = _pluginPagesCache || {};
var names = Object.keys(pages);
if(names.length) {
var html = '';
names.forEach(function(name){
var p = pages[name];
html += '<div class="nav-sub-item" data-plugin="'+p.plugin+'" data-path="'+p.path+'">'+
'<span class="nav-icon">'+(p.icon||'P')+'</span><span>'+p.title+'</span></div>';
});
body.innerHTML = html;
}
}
// ═══ Click delegation ═══
document.addEventListener('click', function(e) {
// Built-in nav
var nav = e.target.closest('.nav-item');
if(nav && nav.dataset.page) {
navigateTo(nav.dataset.page);
e.preventDefault();
return;
}
// Plugin sub-item
var sub = e.target.closest('.nav-sub-item');
if(sub && sub.dataset.plugin) {
navigateTo('plugins/' + sub.dataset.plugin);
e.preventDefault();
}
});