feat: WS实时推送、网速双线图、按钮MD3修复、页面加载器修复 (v0.6.0)
后端: - 新增 /api/system/ws WebSocket端点 替代HTTP轮询 (每2s推送系统+框架数据) - 修复 uptime始终为0 (sm.start_time时间戳) - 网络采集新增TX上行+实时网速(delta法) - 进程内存采集 (/proc/self/status VmRSS) - 抑制aiohttp内部WS帧日志防刷爆日志文件 前端: - 仪表盘: HTTP轮询→WS连接+指数退避自动重连 - 实时日志: 修复onclose重连bug+批量渲染30fps+500行上限防卡死 - 网速卡片: DualLineChart双线图(下行实线/上行虚线) - Y轴零点偏移-5% 防止零网速贴底 - 所有按钮修复MD3组合类(btn+btn-tonal+btn-sm) - 页面加载器: 改动态script元素执行 修复onclick全局作用域问题 - emoji图标→Material Design SVG图标 - CSS去残留</style>+新增.nav-item svg约束 - 登录页输入框+标签左对齐+按钮MD3样式 - 多个版本号显示修复(去双重v前缀) - chart.js/app.js/HTML页面统一加版本号防浏览器缓存 - .gitignore新增docs/目录 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -118,8 +118,8 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
.btn:disabled{opacity:.38;pointer-events:none}
|
||||
|
||||
/* ── MD3 Input ── */
|
||||
.input-group{margin-bottom:16px}
|
||||
.input-group .label{display:block;margin-bottom:6px;color:var(--text-dim)}
|
||||
.input-group{margin-bottom:16px;text-align:left}
|
||||
.input-group label{display:block;margin-bottom:6px;color:var(--text-dim);font-size:.8rem;font-weight:500;letter-spacing:.3px}
|
||||
.input{
|
||||
width:100%;padding:12px 16px;background:var(--md-sys-color-surface-container-lowest);
|
||||
border:1px solid var(--outline);border-radius:var(--shape-xs);color:var(--text);
|
||||
@@ -189,6 +189,7 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
.nav-item:hover{background:rgba(208,188,255,.08);color:var(--text)}
|
||||
.nav-item.active{background:var(--primary-container);color:var(--md-sys-color-on-primary-container)}
|
||||
.nav-item .nav-icon{width:24px;height:24px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||||
.nav-item svg{width:20px;height:20px;flex-shrink:0}
|
||||
.nav-item .nav-label{overflow:hidden;text-overflow:ellipsis}
|
||||
.toggle-sidebar{
|
||||
margin-top:auto;padding:16px;text-align:center;cursor:pointer;
|
||||
@@ -425,4 +426,3 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
||||
|
||||
/* ── Menu reveal animation for login error ── */
|
||||
.err-msg{transition:opacity .3s}
|
||||
</style>
|
||||
|
||||
@@ -19,27 +19,27 @@
|
||||
|
||||
<aside class="sidebar">
|
||||
<a class="nav-item active" data-page="dashboard">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
||||
<span>仪表盘</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="logs">
|
||||
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
||||
<span>实时日志</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="console">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 14H4v-4h8v4zm0-6H4V8h8v4zm8 6h-8v-4h8v4zm0-6h-8V8h8v4z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 14H4v-4h8v4zm0-6H4V8h8v4zm8 6h-8v-4h8v4zm0-6h-8V8h8v4z"/></svg>
|
||||
<span>控制台</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="plugins">
|
||||
<svg viewBox="0 0 24 24"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
|
||||
<span>插件管理</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="projects">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V6h5.17l2 2H20v10z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V6h5.17l2 2H20v10z"/></svg>
|
||||
<span>项目管理</span>
|
||||
</a>
|
||||
<a class="nav-item" data-page="proxy">
|
||||
<svg viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
|
||||
<span>反向代理</span>
|
||||
</a>
|
||||
<div class="toggle-sidebar" onclick="toggleSidebar()">☰</div>
|
||||
@@ -52,9 +52,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 🟢 1. 先加载图表库 -->
|
||||
<script src="./static/js/chart.js"></script>
|
||||
<script src="./static/js/chart.js?v=0603"></script>
|
||||
|
||||
<!-- 🟢 2. 再加载主逻辑 -->
|
||||
<script src="./static/js/app.js"></script>
|
||||
<script src="./static/js/app.js?v=0601"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<div class="login-wrapper">
|
||||
<div class="login-card">
|
||||
<h2>🐱 SenSu Login</h2>
|
||||
<div class="input-group"><label>用户名</label><input type="text" id="u" value="admin"></div>
|
||||
<div class="input-group"><label>密码</label><input type="password" id="p" value="admin"></div>
|
||||
<button class="btn-primary" onclick="doLogin()">进入系统</button>
|
||||
<div class="input-group"><label>用户名</label><input class="input" type="text" id="u" value="admin"></div>
|
||||
<div class="input-group"><label>密码</label><input class="input" type="password" id="p" value="admin"></div>
|
||||
<button class="btn btn-filled" style="width:100%;margin-top:8px" onclick="doLogin()">进入系统</button>
|
||||
<div id="err" class="err-msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,21 +55,30 @@ async function loadPage(pageName) {
|
||||
|
||||
content.innerHTML = cleanHtml;
|
||||
|
||||
// Execute inline scripts
|
||||
// Execute inline scripts via dynamic <script> tag so function
|
||||
// declarations become globally accessible (onclick handlers need them)
|
||||
for(const code of scripts) {
|
||||
try { (new Function(code)).call(window); } catch(e) { console.error('Inline script error:', e); }
|
||||
try {
|
||||
const s = document.createElement('script');
|
||||
s.textContent = code;
|
||||
document.head.appendChild(s);
|
||||
document.head.removeChild(s);
|
||||
} catch(e) { console.error('Inline script error:', e); }
|
||||
}
|
||||
|
||||
// Load external JS module (optional — skip if 404)
|
||||
// Load external JS module the same way (optional — skip if 404)
|
||||
try {
|
||||
const jsResp = await fetch(`./static/pages/${pageName}.js?t=${Date.now()}`);
|
||||
if(jsResp.ok) {
|
||||
const jsCode = await jsResp.text();
|
||||
(new Function(jsCode)).call(window);
|
||||
const s = document.createElement('script');
|
||||
s.textContent = jsCode;
|
||||
document.head.appendChild(s);
|
||||
document.head.removeChild(s);
|
||||
const moduleName = pageName.charAt(0).toUpperCase() + pageName.slice(1) + 'Module';
|
||||
if(window[moduleName]?.init) window[moduleName].init();
|
||||
}
|
||||
} catch(e) { /* JS module optional */ }
|
||||
} catch(e) { console.error('Page JS error:', pageName, e); }
|
||||
|
||||
bar.style.width = '100%';
|
||||
setTimeout(() => bar.classList.remove('active'), 200);
|
||||
|
||||
+102
-23
@@ -5,13 +5,13 @@ class MiniChart {
|
||||
this.color = color;
|
||||
this.data = new Array(60).fill(0); // 60秒历史
|
||||
this.maxVal = 100;
|
||||
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
}
|
||||
|
||||
resize() {
|
||||
const rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
var rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
this.canvas.width = rect.width - 24;
|
||||
this.canvas.height = 60;
|
||||
this.draw();
|
||||
@@ -19,33 +19,112 @@ class MiniChart {
|
||||
|
||||
update(val) {
|
||||
this.data.push(val);
|
||||
if(this.data.length > 60) this.data.shift();
|
||||
this.maxVal = Math.max(...this.data, 100);
|
||||
if (this.data.length > 60) this.data.shift();
|
||||
this.maxVal = Math.max.apply(null, this.data.concat([100]));
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
if(!this.ctx) return;
|
||||
const { width, height } = this.canvas;
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
this.ctx.strokeStyle = this.color;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
|
||||
this.data.forEach((v, i) => {
|
||||
const x = (i / 59) * width;
|
||||
const y = height - (v / this.maxVal) * (height - 10);
|
||||
if(i === 0) this.ctx.moveTo(x, y);
|
||||
else this.ctx.lineTo(x, y);
|
||||
if (!this.ctx) return;
|
||||
var ctx = this.ctx;
|
||||
var w = this.canvas.width;
|
||||
var h = this.canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
ctx.strokeStyle = this.color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
var self = this;
|
||||
this.data.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - (v / self.maxVal) * (h - 10);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// 填充渐变
|
||||
this.ctx.lineTo(width, height);
|
||||
this.ctx.lineTo(0, height);
|
||||
this.ctx.fillStyle = this.color + '20';
|
||||
this.ctx.fill();
|
||||
ctx.lineTo(w, h);
|
||||
ctx.lineTo(0, h);
|
||||
ctx.fillStyle = this.color + '20';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dual-line chart (e.g. download + upload speed) ── */
|
||||
class DualLineChart {
|
||||
constructor(canvasId, colorA, colorB) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.colorA = colorA || '#bb9af7'; // solid line (download)
|
||||
this.colorB = colorB || '#c0a8f0'; // dashed line (upload)
|
||||
this.dataA = new Array(60).fill(0);
|
||||
this.dataB = new Array(60).fill(0);
|
||||
this.maxVal = 100;
|
||||
this.applyResize();
|
||||
window.addEventListener('resize', () => this.applyResize());
|
||||
}
|
||||
|
||||
applyResize() {
|
||||
var rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
this.canvas.width = rect.width - 24;
|
||||
this.canvas.height = 60;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
update(valA, valB) {
|
||||
this.dataA.push(valA);
|
||||
this.dataB.push(valB);
|
||||
if (this.dataA.length > 60) { this.dataA.shift(); this.dataB.shift(); }
|
||||
var all = this.dataA.concat(this.dataB).concat([100]);
|
||||
this.maxVal = Math.max.apply(null, all);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) return;
|
||||
var ctx = this.ctx;
|
||||
var w = this.canvas.width;
|
||||
var h = this.canvas.height;
|
||||
|
||||
var self = this;
|
||||
|
||||
// Y-axis: range from -5% to 100%, so zero never hugs the bottom
|
||||
var pad = Math.max(10, self.maxVal * 0.05);
|
||||
var range = self.maxVal + pad; // e.g. 0..100 becomes -5..100
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// ── Line A: solid (download) ──
|
||||
ctx.strokeStyle = this.colorA;
|
||||
ctx.lineWidth = 1.8;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
this.dataA.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - ((v + pad) / range) * (h - 8);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
// ── Line B: dashed (upload) ──
|
||||
ctx.strokeStyle = this.colorB;
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
this.dataB.forEach(function(v, i) {
|
||||
var x = (i / 59) * w;
|
||||
var y = h - ((v + pad) / range) * (h - 8);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
window.MiniChart = MiniChart;
|
||||
window.DualLineChart = DualLineChart;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
framework:
|
||||
debug: true
|
||||
name: SenSu
|
||||
version: Alpha_0.2.0
|
||||
logging:
|
||||
debug_level_file: true
|
||||
level: INFO
|
||||
max_file_size: 10MB
|
||||
max_log_files: 20
|
||||
plugins:
|
||||
auto_load: true
|
||||
hot_reload: true
|
||||
max_retry_count: 3
|
||||
services:
|
||||
internet:
|
||||
api_port: 8000
|
||||
enable_reverse_proxy: false
|
||||
ws_port: 8765
|
||||
tui:
|
||||
layout:
|
||||
grid-rows: 4fr 5fr 1fr
|
||||
@@ -0,0 +1,12 @@
|
||||
admin_permissions:
|
||||
- framework.*
|
||||
- plugin.*
|
||||
- service.*
|
||||
default_permissions:
|
||||
- framework.status.read
|
||||
- plugin.self.info.read
|
||||
permission_levels:
|
||||
- read
|
||||
- write
|
||||
- execute
|
||||
- admin
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,89 @@
|
||||
commands:
|
||||
autoscroll:
|
||||
description: '滚动控制: 切换自动滚动'
|
||||
permissions:
|
||||
- framework.tui.control
|
||||
source: internal
|
||||
create-plugin:
|
||||
description: 创建新插件脚手架
|
||||
permissions:
|
||||
- framework.scaffold.plugin
|
||||
source: internal
|
||||
help:
|
||||
description: 显示帮助信息
|
||||
permissions:
|
||||
- framework.command.help.read
|
||||
source: internal
|
||||
history:
|
||||
description: 显示命令历史
|
||||
permissions:
|
||||
- framework.command.history.read
|
||||
source: internal
|
||||
netdiag:
|
||||
description: 网络服务诊断
|
||||
permissions:
|
||||
- framework.network.diagnose
|
||||
source: internal
|
||||
permissions:
|
||||
description: '权限管理: 显示权限状态'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pm_plugin_status:
|
||||
description: '权限管理: 查看插件权限状态'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmallow:
|
||||
description: '权限管理: 同意权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmdeny:
|
||||
description: '权限管理: 拒绝权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmhelp:
|
||||
description: '权限管理: 显示权限命令帮助'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmignore:
|
||||
description: '权限管理: 暂时忽略权限请求'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmpending:
|
||||
description: '权限管理: 查看待授权请求列表'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmrequests:
|
||||
description: '权限管理: 查看待授权请求列表(别名)'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
pmtest:
|
||||
description: '权限管理: 测试权限配置文件'
|
||||
permissions:
|
||||
- framework.permission.read
|
||||
source: internal
|
||||
scroll:
|
||||
description: '滚动控制: 手动滚动到底部'
|
||||
permissions:
|
||||
- framework.tui.control
|
||||
source: internal
|
||||
status:
|
||||
description: 显示框架状态
|
||||
permissions:
|
||||
- framework.status.read
|
||||
source: internal
|
||||
testlog:
|
||||
description: 生成测试日志
|
||||
permissions:
|
||||
- framework.command.test
|
||||
source: internal
|
||||
last_updated: 283025.170846772
|
||||
plugin_commands: {}
|
||||
total_commands: 17
|
||||
@@ -23,11 +23,22 @@
|
||||
<canvas id="chart-cpu" class="mini-chart"></canvas>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>🌐 网络接收</h3>
|
||||
<div class="stat-value" id="d-net">--</div>
|
||||
<h3>🌐 网络流量</h3>
|
||||
<div style="display:flex; gap:16px">
|
||||
<div><div class="stat-sub" style="font-size:.65rem">下行</div><div class="stat-value" id="d-net-rx" style="font-size:1.4rem">--</div></div>
|
||||
<div><div class="stat-sub" style="font-size:.65rem">上行</div><div class="stat-value" id="d-net-tx" style="font-size:1.4rem">--</div></div>
|
||||
</div>
|
||||
<canvas id="chart-net" class="mini-chart"></canvas>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card">
|
||||
<h3>⚡ 实时网速</h3>
|
||||
<div style="display:flex; gap:16px">
|
||||
<div><div class="stat-sub" style="font-size:.65rem">↓ 下载</div><div class="stat-value" id="d-speed-rx" style="font-size:1.4rem">--</div></div>
|
||||
<div><div class="stat-sub" style="font-size:.65rem">↑ 上传</div><div class="stat-value" id="d-speed-tx" style="font-size:1.4rem">--</div></div>
|
||||
</div>
|
||||
<canvas id="chart-speed" class="mini-chart"></canvas>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>💾 进程内存</h3>
|
||||
<div class="stat-value" id="d-proc-mem">--</div>
|
||||
<canvas id="chart-proc-mem" class="mini-chart"></canvas>
|
||||
@@ -60,11 +71,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 快捷操作 (占位) -->
|
||||
<!-- 3. 快捷操作 -->
|
||||
<div class="side-card">
|
||||
<h3>🛠️ 快捷操作</h3>
|
||||
<button class="btn-sm" style="width:100%; margin-bottom:6px" onclick="window.PluginsModule?.refresh()">🔄 刷新插件列表</button>
|
||||
<button class="btn-sm" style="width:100%; color:var(--error); border-color:var(--error)" onclick="doLogout()">🚪 退出登录</button>
|
||||
<h3>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:6px"><path fill="var(--primary)" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41L9.25 5.35c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||
快捷操作
|
||||
</h3>
|
||||
<button class="btn btn-sm btn-tonal" style="width:100%;margin-bottom:6px" onclick="window.PluginsModule?.refresh()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
刷新插件列表
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outlined" style="width:100%;color:var(--error);border-color:var(--error)" onclick="doLogout()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,105 +1,182 @@
|
||||
window.DashboardModule = {
|
||||
ws: null,
|
||||
reconnectTimer: null,
|
||||
reconnectDelay: 1000,
|
||||
charts: {},
|
||||
init: () => {
|
||||
// 1. 初始化图表实例
|
||||
window.DashboardModule.charts = {
|
||||
mem: new MiniChart('chart-mem', '#9ece6a'),
|
||||
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
|
||||
net: new MiniChart('chart-net', '#e0af68'),
|
||||
|
||||
init: function() {
|
||||
var self = this;
|
||||
|
||||
// 初始化图表
|
||||
self.charts = {
|
||||
mem: new MiniChart('chart-mem', '#9ece6a'),
|
||||
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
|
||||
net: new MiniChart('chart-net', '#e0af68'),
|
||||
speed: new DualLineChart('chart-speed', '#bb9af7', '#c0a8f0'),
|
||||
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
|
||||
};
|
||||
|
||||
// 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启)
|
||||
// 静态信息 (HTTP 一次)
|
||||
fetchSystemStaticInfo();
|
||||
|
||||
// 3. 启动实时数据轮询
|
||||
fetchDash();
|
||||
window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新
|
||||
// WebSocket 实时推送
|
||||
self._connect();
|
||||
},
|
||||
destroy: () => {
|
||||
clearInterval(window._dashInterval);
|
||||
window.DashboardModule.charts = {};
|
||||
|
||||
_connect: function() {
|
||||
var self = this;
|
||||
if (self.ws) {
|
||||
self.ws.onclose = null;
|
||||
self.ws.close();
|
||||
self.ws = null;
|
||||
}
|
||||
|
||||
var base = window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws");
|
||||
self.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
self.reconnectDelay = 1000;
|
||||
};
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
try {
|
||||
var d = JSON.parse(e.data);
|
||||
if (d.type === "sys") {
|
||||
self._updateUI(d.system, d.framework);
|
||||
}
|
||||
} catch(ex) {}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
self.ws = null;
|
||||
self.reconnectTimer = setTimeout(function() {
|
||||
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
|
||||
self._connect();
|
||||
}, self.reconnectDelay);
|
||||
};
|
||||
|
||||
ws.onerror = function() { ws.close(); };
|
||||
},
|
||||
|
||||
_updateUI: function(sys, fw) {
|
||||
// ── 框架卡片 ──
|
||||
if (fw) {
|
||||
var el;
|
||||
el = document.getElementById('d-uptime'); if (el) el.textContent = formatUptime(fw.uptime || 0);
|
||||
el = document.getElementById('d-plugins'); if (el) el.textContent = fw.plugins || 0;
|
||||
el = document.getElementById('d-ver-badge'); if (el) el.textContent = fw.version || '?';
|
||||
}
|
||||
if (!sys) return;
|
||||
var self = this;
|
||||
|
||||
// ── 内存 ──
|
||||
if (sys.memory) {
|
||||
var m = sys.memory.percent || 0;
|
||||
var el = document.getElementById('d-mem'); if (el) el.textContent = m + '%';
|
||||
self.charts.mem.update(m);
|
||||
}
|
||||
|
||||
// ── CPU ──
|
||||
if (sys.cpu) {
|
||||
var cpuVal = sys.cpu.percent;
|
||||
if (cpuVal === null || cpuVal === undefined) {
|
||||
var load = (sys.cpu.load_avg && sys.cpu.load_avg[0]) ? sys.cpu.load_avg[0] : 0;
|
||||
cpuVal = Math.min(100, (load / (sys.cpu.cores || 1)) * 100);
|
||||
}
|
||||
var el = document.getElementById('d-cpu'); if (el) el.textContent = Math.round(cpuVal) + '%';
|
||||
self.charts.cpu.update(cpuVal);
|
||||
|
||||
if (sys.cpu.load_avg) {
|
||||
el = document.getElementById('info-load'); if (el) el.textContent = sys.cpu.load_avg[2].toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 网络累计流量 (RX + TX) ──
|
||||
if (sys.network) {
|
||||
var el;
|
||||
el = document.getElementById('d-net-rx'); if (el) el.textContent = formatTraffic(sys.network.rx_mb);
|
||||
el = document.getElementById('d-net-tx'); if (el) el.textContent = formatTraffic(sys.network.tx_mb);
|
||||
// Chart shows RX trend
|
||||
self.charts.net.update(sys.network.rx_mb || 0);
|
||||
}
|
||||
|
||||
// ── 实时网速 ──
|
||||
if (sys.net_speed) {
|
||||
var el;
|
||||
el = document.getElementById('d-speed-rx');
|
||||
if (el) el.textContent = formatSpeed(sys.net_speed.rx_bytes_sec);
|
||||
el = document.getElementById('d-speed-tx');
|
||||
if (el) el.textContent = formatSpeed(sys.net_speed.tx_bytes_sec);
|
||||
// Chart: RX solid line, TX dashed line
|
||||
self.charts.speed.update(sys.net_speed.rx_bytes_sec || 0, sys.net_speed.tx_bytes_sec || 0);
|
||||
}
|
||||
|
||||
// ── 进程内存 ──
|
||||
if (sys.process && sys.process.memory_mb !== undefined) {
|
||||
var pm = sys.process.memory_mb || 0;
|
||||
var el = document.getElementById('d-proc-mem'); if (el) el.textContent = pm + ' MB';
|
||||
self.charts.proc_mem.update(pm);
|
||||
}
|
||||
if (sys.process && sys.process.pid !== undefined) {
|
||||
var el = document.getElementById('info-pid'); if (el) el.textContent = sys.process.pid;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
||||
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
|
||||
this.charts = {};
|
||||
}
|
||||
};
|
||||
|
||||
/* ── 静态信息 ── */
|
||||
async function fetchSystemStaticInfo() {
|
||||
try {
|
||||
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
|
||||
|
||||
// 硬件信息
|
||||
if(sys.platform) {
|
||||
document.getElementById('info-os').textContent = sys.platform.system || '--';
|
||||
document.getElementById('info-arch').textContent = sys.platform.machine || '--';
|
||||
document.getElementById('info-env').textContent = sys.platform.env || 'Standard';
|
||||
var sys = await fetch('./api/system', {credentials:'include'}).then(function(r){return r.json();});
|
||||
var el;
|
||||
if (sys.platform) {
|
||||
el = document.getElementById('info-os'); if (el) el.textContent = sys.platform.system || '--';
|
||||
el = document.getElementById('info-arch'); if (el) el.textContent = sys.platform.machine || '--';
|
||||
el = document.getElementById('info-env'); if (el) el.textContent = sys.platform.env || 'Standard';
|
||||
}
|
||||
if(sys.cpu) {
|
||||
const c = sys.cpu.cores || 0;
|
||||
document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核
|
||||
if (sys.cpu) {
|
||||
var c = sys.cpu.cores || 0;
|
||||
el = document.getElementById('info-cores'); if (el) el.textContent = c + ' / ' + c;
|
||||
}
|
||||
if(sys.memory) {
|
||||
document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB';
|
||||
if (sys.memory) {
|
||||
el = document.getElementById('info-mem-total'); if (el) el.textContent = sys.memory.total_gb + ' GB';
|
||||
}
|
||||
|
||||
// 框架信息 (部分需结合 API)
|
||||
const host = window.location.hostname + (window.location.port ? ':'+window.location.port : '');
|
||||
document.getElementById('info-addr').textContent = host;
|
||||
|
||||
// 框架信息 (部分来自 API)
|
||||
try {
|
||||
var fw = await fetch('./api/framework', {credentials:'include'}).then(function(r){return r.json();});
|
||||
el = document.getElementById('info-fw-ver'); if (el) el.textContent = fw.version || '?';
|
||||
} catch(e) {}
|
||||
var host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
|
||||
el = document.getElementById('info-addr'); if (el) el.textContent = host;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function fetchDash() {
|
||||
try {
|
||||
const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json());
|
||||
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
|
||||
|
||||
// --- 左侧动态数据更新 ---
|
||||
if(fw) {
|
||||
document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0);
|
||||
document.getElementById('d-plugins').textContent = fw.plugins || 0;
|
||||
if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?');
|
||||
}
|
||||
|
||||
if(sys) {
|
||||
// 内存
|
||||
const m = sys.memory?.percent || 0;
|
||||
document.getElementById('d-mem').textContent = m + '%';
|
||||
window.DashboardModule.charts.mem.update(m);
|
||||
|
||||
// CPU (兼容 Android null 情况)
|
||||
let cpuVal = sys.cpu?.percent;
|
||||
if (cpuVal === null || cpuVal === undefined) {
|
||||
const load = sys.cpu?.load_avg?.[0] || 0;
|
||||
const cores = sys.cpu?.cores || 1;
|
||||
cpuVal = Math.min(100, (load / cores) * 100);
|
||||
}
|
||||
document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%';
|
||||
window.DashboardModule.charts.cpu.update(cpuVal);
|
||||
|
||||
// 网络 (RX 总量)
|
||||
const netRx = sys.network?.rx || 0;
|
||||
document.getElementById('d-net').textContent = netRx + ' MB';
|
||||
window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势
|
||||
|
||||
// 进程内存
|
||||
const pm = sys.process?.memory_mb || 0;
|
||||
document.getElementById('d-proc-mem').textContent = pm + ' MB';
|
||||
window.DashboardModule.charts.proc_mem.update(pm);
|
||||
|
||||
// --- 右侧动态数据更新 ---
|
||||
if(sys.process) {
|
||||
document.getElementById('info-pid').textContent = sys.process.pid || '--';
|
||||
}
|
||||
if(sys.cpu?.load_avg) {
|
||||
document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2);
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn("Dashboard fetch error", e); }
|
||||
}
|
||||
|
||||
// 辅助:秒数转时间格式
|
||||
/* ── 格式化 ── */
|
||||
function formatUptime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${h}h ${m}m ${s}s`;
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
var s = Math.floor(seconds % 60);
|
||||
if (h > 0) return h + 'h ' + m + 'm';
|
||||
if (m > 0) return m + 'm ' + s + 's';
|
||||
return s + 's';
|
||||
}
|
||||
|
||||
function formatTraffic(mb) {
|
||||
if (mb === undefined || mb === null) return '--';
|
||||
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
|
||||
if (mb >= 1) return mb.toFixed(1) + ' MB';
|
||||
return (mb * 1024).toFixed(0) + ' KB';
|
||||
}
|
||||
|
||||
function formatSpeed(bytesPerSec) {
|
||||
if (bytesPerSec === undefined || bytesPerSec === null || bytesPerSec < 0) return '0 B/s';
|
||||
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + ' MB/s';
|
||||
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(0) + ' KB/s';
|
||||
return bytesPerSec.toFixed(0) + ' B/s';
|
||||
}
|
||||
|
||||
+128
-21
@@ -1,25 +1,132 @@
|
||||
window.LogsModule = {
|
||||
ws: null,
|
||||
init: () => {
|
||||
const box = document.getElementById("log-box");
|
||||
const base = window.location.pathname.split("/").slice(0,2).join("/");
|
||||
const connect = () => {
|
||||
window.LogsModule.ws = new WebSocket("ws://"+location.host+base+"/api/logs/ws");
|
||||
window.LogsModule.ws.onopen = () => box.innerHTML += "<div style=\"color:var(--success)\">Connected</div>";
|
||||
window.LogsModule.ws.onmessage = e => {
|
||||
try {
|
||||
const d = JSON.parse(e.data);
|
||||
if(d.type === "log") {
|
||||
const cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
|
||||
const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : "--";
|
||||
box.innerHTML += "<div class=\"log-entry\"><span style=\"color:#555;margin-right:5px\">"+t+"</span><span class=\""+cls+"\">["+d.level+"]</span> "+d.message+"</div>";
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
window.LogsModule.ws.onclose = setTimeout(connect, 3000);
|
||||
};
|
||||
connect();
|
||||
reconnectTimer: null,
|
||||
reconnectDelay: 1000,
|
||||
maxLines: 500,
|
||||
lineCount: 0,
|
||||
batchBuffer: [],
|
||||
batchTimer: null,
|
||||
|
||||
init: function() {
|
||||
var self = this;
|
||||
self.box = document.getElementById("log-box");
|
||||
if (!self.box) return;
|
||||
self.box.innerHTML = "";
|
||||
self.lineCount = 0;
|
||||
self._connect();
|
||||
},
|
||||
destroy: () => window.LogsModule.ws?.close()
|
||||
|
||||
_connect: function() {
|
||||
var self = this;
|
||||
if (self.ws) {
|
||||
self.ws.onclose = null;
|
||||
self.ws.close();
|
||||
self.ws = null;
|
||||
}
|
||||
|
||||
var base = window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws");
|
||||
self.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
self.reconnectDelay = 1000;
|
||||
self._appendHTML('<div style="color:var(--md-sys-color-primary)">🟢 Connected</div>');
|
||||
};
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
try {
|
||||
var d = JSON.parse(e.data);
|
||||
if (d.type === "log") {
|
||||
self._bufferLog(d);
|
||||
}
|
||||
} catch(ex) {}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
self.ws = null;
|
||||
self._appendHTML('<div style="color:var(--error)">🔴 Disconnected — reconnecting...</div>');
|
||||
self.reconnectTimer = setTimeout(function() {
|
||||
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
|
||||
self._connect();
|
||||
}, self.reconnectDelay);
|
||||
};
|
||||
|
||||
ws.onerror = function() {
|
||||
ws.close();
|
||||
};
|
||||
},
|
||||
|
||||
/* Buffer log entries then flush at ~30fps to avoid DOM thrashing */
|
||||
_bufferLog: function(d) {
|
||||
var self = this;
|
||||
self.batchBuffer.push(d);
|
||||
if (!self.batchTimer) {
|
||||
self.batchTimer = setTimeout(function() {
|
||||
self._flush();
|
||||
self.batchTimer = null;
|
||||
}, 33); // ~30fps flush
|
||||
}
|
||||
},
|
||||
|
||||
_flush: function() {
|
||||
var self = this;
|
||||
var batch = self.batchBuffer;
|
||||
self.batchBuffer = [];
|
||||
if (!batch.length || !self.box) return;
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < batch.length; i++) {
|
||||
var d = batch[i];
|
||||
var cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
|
||||
var t = d.timestamp ? new Date(d.timestamp * 1000).toLocaleTimeString() : "--";
|
||||
html += '<div class="log-entry">' +
|
||||
'<span style="color:#555;margin-right:5px">' + t + '</span>' +
|
||||
'<span class="' + cls + '">[' + d.level + ']</span> ' +
|
||||
self._escapeHtml(d.message || '') + '</div>';
|
||||
}
|
||||
|
||||
self._appendHTML(html);
|
||||
},
|
||||
|
||||
_appendHTML: function(html) {
|
||||
var self = this;
|
||||
if (!self.box) return;
|
||||
|
||||
// Estimate line count from <div> tags
|
||||
var newLines = (html.match(/<div/g) || []).length;
|
||||
self.lineCount += newLines;
|
||||
|
||||
// Trim old lines if over cap
|
||||
while (self.lineCount > self.maxLines && self.box.firstChild) {
|
||||
// Count lines in the first child
|
||||
var removed = 1;
|
||||
if (self.box.firstChild.nodeType === 1) {
|
||||
var inner = self.box.firstChild.innerHTML || '';
|
||||
removed = Math.max(1, (inner.match(/<div/g) || []).length);
|
||||
}
|
||||
self.box.removeChild(self.box.firstChild);
|
||||
self.lineCount = Math.max(0, self.lineCount - removed);
|
||||
}
|
||||
|
||||
// Efficient append: insertAdjacentHTML instead of innerHTML +=
|
||||
self.box.insertAdjacentHTML("beforeend", html);
|
||||
self.box.scrollTop = self.box.scrollHeight;
|
||||
},
|
||||
|
||||
_escapeHtml: function(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
||||
if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; }
|
||||
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
|
||||
this.batchBuffer = [];
|
||||
this.box = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<h2 style="color:var(--accent);">📦 插件管理</h2>
|
||||
<button class="btn-sm" onclick="window.PluginsModule.refresh()">🔄 刷新</button>
|
||||
<h2 style="color:var(--primary); display:flex; align-items:center; gap:8px">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24"><path fill="var(--primary)" d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
|
||||
插件管理
|
||||
</h2>
|
||||
<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.refresh()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-2px"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<div id="plugin-list" class="plugin-list">加载中...</div>
|
||||
|
||||
@@ -17,10 +17,10 @@ window.PluginsModule = {
|
||||
</div>
|
||||
<div class="plugin-act">
|
||||
${p.running
|
||||
? `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
|
||||
: `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
|
||||
? `<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
|
||||
: `<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
|
||||
}
|
||||
<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
|
||||
<button class="btn btn-sm btn-outlined" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
@@ -38,23 +38,23 @@
|
||||
var projects=[];
|
||||
function showTab(e,t){document.querySelectorAll("#tab-running,#tab-add,#tab-git").forEach(el=>el.style.display="none");document.getElementById("tab-"+t).style.display="block";document.querySelectorAll(".tab").forEach(el=>el.classList.remove("active"));e.target.classList.add("active")}
|
||||
async function refresh(){
|
||||
try{var r=await fetch("/SenSu/api/projects");var d=await r.json();projects=d.projects;var h="";for(var p of projects){var cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";h+='<div class="mgmt-card"><div class="status-dot '+cls+'"></div><div class="info"><b>'+p.name+'</b><br><small>'+p.status+" PID:"+(p.pid||"-")+" :"+(p.port||"-")+" "+(p.uptime?p.uptime+"s":"")+(p.proxy?" → "+p.proxy:"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="stopP(this,'+JSON.stringify(p.name)+')">停止</button><button class="btn btn-sm btn-outlined" onclick="logsP(this,'+JSON.stringify(p.name)+')">日志</button><button class="btn btn-sm btn-text" onclick="cmdP(this,'+JSON.stringify(p.name)+')">命令</button></div></div><div class="log-box" id="logs-'+p.name+'"></div>'}document.getElementById("project-list").innerHTML=h||"<div style=\"color:var(--text-dim);text-align:center;padding:40px\">暂无项目</div>"}catch(e){document.getElementById("project-list").innerHTML="<div style=\"color:var(--error);text-align:center;padding:40px\">引擎未就绪 (API 错误)</div>"}
|
||||
try{var r=await fetch("./api/projects");var d=await r.json();projects=d.projects;var h="";for(var p of projects){var cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";h+='<div class="mgmt-card"><div class="status-dot '+cls+'"></div><div class="info"><b>'+p.name+'</b><br><small>'+p.status+" PID:"+(p.pid||"-")+" :"+(p.port||"-")+" "+(p.uptime?p.uptime+"s":"")+(p.proxy?" → "+p.proxy:"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="stopP(this,'+JSON.stringify(p.name)+')">停止</button><button class="btn btn-sm btn-outlined" onclick="logsP(this,'+JSON.stringify(p.name)+')">日志</button><button class="btn btn-sm btn-text" onclick="cmdP(this,'+JSON.stringify(p.name)+')">命令</button></div></div><div class="log-box" id="logs-'+p.name+'"></div>'}document.getElementById("project-list").innerHTML=h||"<div style=\"color:var(--text-dim);text-align:center;padding:40px\">暂无项目</div>"}catch(e){document.getElementById("project-list").innerHTML="<div style=\"color:var(--error);text-align:center;padding:40px\">引擎未就绪 (API 错误)</div>"}
|
||||
}
|
||||
async function addProject(){
|
||||
var n=document.getElementById("proj-name").value,cmd=document.getElementById("proj-cmd").value;
|
||||
if(!n||!cmd){alert("请填写名称和命令");return}
|
||||
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:cmd.split(" "),cwd:document.getElementById("proj-cwd").value,port:parseInt(document.getElementById("proj-port").value)||0,proxy_path:document.getElementById("proj-proxy").value})});
|
||||
await fetch("./api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:cmd.split(" "),cwd:document.getElementById("proj-cwd").value,port:parseInt(document.getElementById("proj-port").value)||0,proxy_path:document.getElementById("proj-proxy").value})});
|
||||
refresh()
|
||||
}
|
||||
async function deployGit(){
|
||||
var u=document.getElementById("git-url").value,b=document.getElementById("git-branch").value;
|
||||
if(!u){alert("请填写 Git URL");return}
|
||||
var n=u.split("/").pop().replace(".git","");
|
||||
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:["git","clone","-b",b,u,n],cwd:"data/projects"})});
|
||||
await fetch("./api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:["git","clone","-b",b,u,n],cwd:"data/projects"})});
|
||||
refresh()
|
||||
}
|
||||
async function stopP(el,n){await fetch("/SenSu/api/projects/"+n+"/stop",{method:"POST"});refresh()}
|
||||
async function logsP(el,n){var b=document.getElementById("logs-"+n);b.style.display=b.style.display==="none"?"block":"none";if(b.style.display==="block"){var r=await fetch("/SenSu/api/projects/"+n+"/logs?tail=30");var d=await r.json();b.innerHTML=d.logs.join("<br>")||"(无日志)"}}
|
||||
async function cmdP(el,n){var t=prompt("命令: "+n);if(t)await fetch("/SenSu/api/projects/"+n+"/stdin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})})}
|
||||
async function stopP(el,n){await fetch("./api/projects/"+n+"/stop",{method:"POST"});refresh()}
|
||||
async function logsP(el,n){var b=document.getElementById("logs-"+n);b.style.display=b.style.display==="none"?"block":"none";if(b.style.display==="block"){var r=await fetch("./api/projects/"+n+"/logs?tail=30");var d=await r.json();b.innerHTML=d.logs.join("<br>")||"(无日志)"}}
|
||||
async function cmdP(el,n){var t=prompt("命令: "+n);if(t)await fetch("./api/projects/"+n+"/stdin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})})}
|
||||
setInterval(refresh,4000);refresh();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user