fix: 静态文件no-cache + app.js重命名防缓存
This commit is contained in:
@@ -63,6 +63,11 @@ class WebPanelManager:
|
||||
# URL 前缀: /SenSu/static/ -> 物理路径: .../static/web_panel/
|
||||
static_dir = self.project_root / "static" / "web_panel"
|
||||
if static_dir.exists():
|
||||
# 静态文件添加 no-cache
|
||||
async def _static_handler(request):
|
||||
resp = await static_handler(request)
|
||||
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
return resp
|
||||
app.router.add_static(f'{self.base_path}/static/', path=str(static_dir))
|
||||
logger.info(f"📂 静态资源已挂载: {self.base_path}/static/")
|
||||
else:
|
||||
|
||||
@@ -79,6 +79,6 @@
|
||||
<script src="./static/js/chart.js?v=0603"></script>
|
||||
|
||||
<!-- 🟢 2. 再加载主逻辑 -->
|
||||
<script src="./static/js/app.js?v=0712"></script>
|
||||
<script src="./static/js/app_v2.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
/* ── SenSu WebUI — Theme + Routing + Page Loader ── */
|
||||
|
||||
// ═══ 主题初始化 (必须在渲染前, 防闪白) ═══
|
||||
(function(){
|
||||
var s = localStorage.getItem("sensu-theme") || "dark";
|
||||
document.documentElement.setAttribute("data-theme", s);
|
||||
})();
|
||||
|
||||
// ═══ 工具函数 ═══
|
||||
function getCookie(name) {
|
||||
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? match[2] : '';
|
||||
}
|
||||
|
||||
// ── 顶部版本检查 ──
|
||||
async function checkTopUpdate() {
|
||||
var btn = document.getElementById("top-ver-btn");
|
||||
try {
|
||||
var r = await fetch("./api/updates"), d = await r.json();
|
||||
var ver = d.current_version || d.latest_version || "";
|
||||
btn.textContent = ver ? ver : "v?";
|
||||
if (d.update_available) {
|
||||
btn.textContent = "↻ " + d.latest_version + " 可用";
|
||||
btn.style.color = "var(--success)";
|
||||
btn.title = "新版本可用! " + d.latest_version;
|
||||
} else if (ver) {
|
||||
btn.style.color = "";
|
||||
btn.title = "已是最新 " + ver;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
setTimeout(checkTopUpdate, 3000);
|
||||
setInterval(checkTopUpdate, 600000); // 10min
|
||||
|
||||
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','settings'];
|
||||
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?t='+Date.now());
|
||||
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+'?t='+Date.now(), {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 ''; });
|
||||
|
||||
// Scope plugin styles: replace body selector with .plugin-page-root to avoid
|
||||
// styling the main page's <body> element
|
||||
styles = styles.replace(/body\s*\{/gi, '.plugin-page-root{').replace(/body\b/gi, '.plugin-page-root');
|
||||
|
||||
content.style.padding = '0';
|
||||
content.innerHTML = '<div class="plugin-page-root"><style>'+styles+'</style>'+clean+'</div>';
|
||||
|
||||
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 (in-page overlay, no iframe) ═══
|
||||
window.pickPath = function(mode, callback) {
|
||||
var id = 'picker_' + Date.now();
|
||||
|
||||
// Create modal overlay
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'fm-picker-overlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center';
|
||||
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = 'background:var(--md-sys-color-surface-container-high);border-radius:var(--shape-lg);width:720px;height:520px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--md-sys-elevation-5)';
|
||||
|
||||
// Header with close button
|
||||
var header = document.createElement('div');
|
||||
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--outline);flex-shrink:0';
|
||||
var headerTitle = document.createElement('span');
|
||||
headerTitle.style.cssText = 'font-weight:500;color:var(--primary)';
|
||||
headerTitle.textContent = '📂 选择' + (mode==='file'?'文件':'目录');
|
||||
header.appendChild(headerTitle);
|
||||
var closeBtn = document.createElement('span');
|
||||
closeBtn.style.cssText = 'cursor:pointer;font-size:20px;line-height:1;padding:4px 8px;border-radius:4px;color:var(--text-dim)';
|
||||
closeBtn.textContent = '✕';
|
||||
closeBtn.onclick = function(){ closePicker(); };
|
||||
header.appendChild(closeBtn);
|
||||
box.appendChild(header);
|
||||
|
||||
// Content area — load file manager via fetch
|
||||
var contentArea = document.createElement('div');
|
||||
contentArea.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column';
|
||||
box.appendChild(contentArea);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Close on backdrop click
|
||||
overlay.addEventListener('click', function(e){ if(e.target===overlay) closePicker(); });
|
||||
|
||||
function closePicker() {
|
||||
if(overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
|
||||
// Load file manager HTML into content area
|
||||
fetch('./static/pages/files.html?t=' + Date.now())
|
||||
.then(function(r){ return r.text(); })
|
||||
.then(function(html){
|
||||
// Strip meta and CSS link (already available in main page)
|
||||
html = html.replace(/<meta[^>]*>/gi, '').replace(/<link[^>]*>/gi, '');
|
||||
// Extract inline scripts
|
||||
var scripts = [];
|
||||
var clean = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, function(m,code){ scripts.push(code.trim()); return ''; });
|
||||
contentArea.innerHTML = clean;
|
||||
// Execute scripts
|
||||
scripts.forEach(function(code){
|
||||
try { var s=document.createElement('script'); s.textContent=code; document.head.appendChild(s); document.head.removeChild(s); }
|
||||
catch(ex){}
|
||||
});
|
||||
// Load files.js if not already loaded
|
||||
// Fix container height for overlay
|
||||
var fmContainer = contentArea.querySelector('.fm-container');
|
||||
if(fmContainer) fmContainer.style.height = '100%';
|
||||
function doInit(){
|
||||
window.FilesModule._apiBase = '';
|
||||
window.FilesModule.currentPath = '';
|
||||
window.FilesModule.pickerMode = mode || 'dir';
|
||||
window.FilesModule.pickerCallback = id;
|
||||
window.FilesModule._scope = contentArea;
|
||||
window.FilesModule._getEl = function(sel){ return contentArea.querySelector(sel); };
|
||||
// Override _pickResult to close overlay instead of alert
|
||||
window.FilesModule._pickResult = function(path){
|
||||
closePicker();
|
||||
if(callback) callback(path || window.FilesModule.currentPath || '/');
|
||||
};
|
||||
if(window.FilesModule.destroy) window.FilesModule.destroy();
|
||||
if(window.FilesModule.init) window.FilesModule.init();
|
||||
}
|
||||
// Override picker actions to close overlay and call callback
|
||||
window._fmPick = function(){
|
||||
var p = window.FilesModule && window.FilesModule.currentPath || '/';
|
||||
closePicker();
|
||||
if(callback) callback(p);
|
||||
};
|
||||
window._fmCancel = function(){ closePicker(); };
|
||||
|
||||
if(window.FilesModule){ doInit(); }
|
||||
else {
|
||||
var s=document.createElement('script');
|
||||
s.src='./static/pages/files.js?v=0603';
|
||||
s.onload=doInit;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})
|
||||
.catch(function(e){
|
||||
contentArea.innerHTML = '<div style="color:var(--error);padding:40px;text-align:center">加载失败: '+e.message+'</div>';
|
||||
});
|
||||
};
|
||||
|
||||
// ═══ 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user