From 99c3e3e853a393e0fdc16fa3f0dd33b6d9c876a5 Mon Sep 17 00:00:00 2001 From: qinglong Date: Thu, 11 Jun 2026 20:56:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20hash=E8=B7=AF=E7=94=B1=20+=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E9=A1=B5=E9=9D=A2=E5=86=85=E8=81=94=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=20+=20=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=8F=92=E4=BB=B6=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=B1=95=E5=BC=80=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hash路由: #/dashboard #/files #/plugins/xxx 支持刷新保持/前进后退 - 登录过期保留hash,登录后自动跳回原页面 - 插件WebUI页面内联加载到page-container(不再弹新窗) - 插件页面HTML自动处理: style提取/body剥离/script执行/padding归零 - 侧边栏新增「插件面板」可折叠项,展开列出所有已注册插件页面 - 内存卡片显示 已用/总容量(百分比右侧) - 插件开发指南新增 2.6 WebUI页面开发章节 Co-Authored-By: Claude Opus 4.8 --- config/plugins/commands.yaml | 2 +- config/services/network_routes.yaml | 2 +- docs/SenSu 插件开发详细指南.md | 68 +++++++ services/web_panel/routes/plugins.py | 21 ++ static/web_panel/css/style.css | 10 + static/web_panel/home.html | 15 +- static/web_panel/index.html | 6 +- static/web_panel/js/app.js | 281 +++++++++++++++++++------- static/web_panel/pages/dashboard.html | 5 +- static/web_panel/pages/dashboard.js | 2 + 10 files changed, 330 insertions(+), 82 deletions(-) diff --git a/config/plugins/commands.yaml b/config/plugins/commands.yaml index 7d5e557..d27f62a 100644 --- a/config/plugins/commands.yaml +++ b/config/plugins/commands.yaml @@ -92,7 +92,7 @@ commands: permissions: - framework.command.test source: internal -last_updated: 291543.087298158 +last_updated: 297792.734060618 plugin_commands: example_plugin: echo: *id001 diff --git a/config/services/network_routes.yaml b/config/services/network_routes.yaml index 4292fc5..731fcc8 100644 --- a/config/services/network_routes.yaml +++ b/config/services/network_routes.yaml @@ -1,5 +1,5 @@ http_port: 4200 -last_updated: 291543.090646752 +last_updated: 297792.741311191 plugin_routes: example_plugin: - methods: diff --git a/docs/SenSu 插件开发详细指南.md b/docs/SenSu 插件开发详细指南.md index 7019466..7d0e4c7 100644 --- a/docs/SenSu 插件开发详细指南.md +++ b/docs/SenSu 插件开发详细指南.md @@ -5,6 +5,7 @@ - [SenSu 插件开发超详细指南](#sensu-插件开发超详细指南) - [一、插件系统架构深度解析](#一插件系统架构深度解析) - [二、插件开发完整方案](#二插件开发完整方案) + - [2.6 插件 WebUI 页面](#26-插件-webui-页面-v060) - [三、插件生命周期管理](#三插件生命周期管理) - [四、插件开发最佳实践](#四插件开发最佳实践) - [4.1 错误处理最佳实践](#41-错误处理最佳实践) @@ -3400,6 +3401,73 @@ class PluginResourceManager: ``` +### 2.6 插件 WebUI 页面 (v0.6.0) + +插件可以注册 WebUI 页面,自动出现在 SenSu 管理面板侧边栏的「插件面板」折叠项中。 + +#### 2.6.1 注册页面 + +```python +from sdk.plugin_web import PluginWebMixin + +class MyPlugin(PluginWebMixin): + async def initialize(self): + self.register_web_page( + path="mypanel", # 访问路径: /plugin/{plugin_name} + title="我的面板", # 侧边栏显示名称 + html_content="

Hello

", # 完整 HTML 页面 + icon="M" # 侧边栏图标 (单字符) + ) +``` + +#### 2.6.2 页面渲染机制 + +插件页面在 SenSu 主面板的 `page-container` 区域内联加载(不弹新窗口): + +1. 用户点击侧边栏「插件面板」→ 展开子项 → 点击插件页面 +2. 前端 fetch `/plugin/{plugin_name}` 获取完整 HTML +3. 渲染管线自动处理: + - 提取 ` +
+

插件面板标题

+ +
+ +``` + +#### 2.6.4 注意事项 + +| 事项 | 说明 | +|------|------| +| `body` 选择器 | CSS 中 `body { ... }` 不会生效,改用容器类 | +| 硬编码背景色 | 避免 `background: #000`,用 `var(--bg)` 自适应主题 | +| `onclick` 函数 | 函数需在 ` - + diff --git a/static/web_panel/index.html b/static/web_panel/index.html index 60f1417..c55659f 100644 --- a/static/web_panel/index.html +++ b/static/web_panel/index.html @@ -27,7 +27,11 @@ credentials: 'include', body: JSON.stringify({username: u, password: p}) }); const data = await res.json(); - if(res.ok && data.success) window.location.href = './home.html'; + if(res.ok && data.success) { + var hash = sessionStorage.getItem('sensu_redirect_hash') || ''; + sessionStorage.removeItem('sensu_redirect_hash'); + window.location.href = './home.html' + hash; + } else err.textContent = data.msg || "凭证错误"; } catch(e) { err.textContent = "网络异常"; } } diff --git a/static/web_panel/js/app.js b/static/web_panel/js/app.js index ede448e..6ea01ec 100644 --- a/static/web_panel/js/app.js +++ b/static/web_panel/js/app.js @@ -1,4 +1,6 @@ -// 主题初始化 (必须在渲染前执行, 防止闪白) +/* ── SenSu WebUI — Theme + Routing + Page Loader ── */ + +// ═══ 主题初始化 (必须在渲染前, 防闪白) ═══ (function(){ var s = localStorage.getItem("sensu-theme") || "dark"; document.documentElement.setAttribute("data-theme", s); @@ -12,97 +14,222 @@ window.toggleTheme = function(){ if(btn) btn.textContent = t === "light" ? "☀️" : "🌙"; }; -// 初始化检查 -window.onload = async () => { - // 设置按钮初始图标 + +// ═══ 路由系统 ═══ +// 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 { - const res = await fetch('./api/auth/status', { credentials: 'include' }); - if(res.status === 401) { window.location.href = './index.html'; return; } - const data = await res.json(); - if(!data.authenticated) { window.location.href = './index.html'; return; } - - document.getElementById('uname').textContent = data.username || 'Admin'; - loadPage('dashboard'); // 默认加载 - } catch(e) { window.location.href = './index.html'; } + + 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) { - const content = document.getElementById('page-content'); - const bar = document.getElementById('progress'); - - // 侧边栏高亮 - document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); - document.querySelector(`.nav-item[data-page="${pageName}"]`)?.classList.add('active'); - - // 进度条动画 - bar.classList.add('active'); bar.style.width = '0%'; - await new Promise(r => requestAnimationFrame(() => { bar.style.width = '80%'; setTimeout(r, 100); })); - + 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(/]*>([\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 { - const resp = await fetch(`./static/pages/${pageName}.html`); - if(!resp.ok) throw new Error('404'); - const html = await resp.text(); + 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); } - // Extract inline scripts before innerHTML (browsers skip them) - const scripts = []; - const cleanHtml = html.replace(/]*>([\s\S]*?)<\/script>/gi, (m, code) => { - scripts.push(code.trim()); return ''; - }); - - content.innerHTML = cleanHtml; - - // Execute inline scripts via dynamic