feat: WebUI 框架设置页面 + API Key 管理 UI
新增: - static/web_panel/pages/settings.html — API Key CRUD 界面 - 侧边栏 '框架设置' 导航项 (齿轮图标) - BUILTIN_PAGES 增加 'settings' - UI 功能: 创建对话框(名称/权限/过期), 列表表格, 一键删除 权限模板: - readonly — framework.status.read + plugin.info.read - monitor — + framework.event.subscribe - full — admin (全部) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,6 @@ commands:
|
|||||||
permissions:
|
permissions:
|
||||||
- framework.command.test
|
- framework.command.test
|
||||||
source: internal
|
source: internal
|
||||||
last_updated: 13989.462275548
|
last_updated: 14102.680173838
|
||||||
plugin_commands: {}
|
plugin_commands: {}
|
||||||
total_commands: 18
|
total_commands: 18
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
http_port: 4200
|
http_port: 4200
|
||||||
last_updated: 13989.479860756
|
last_updated: 14102.696182483
|
||||||
plugin_routes: {}
|
plugin_routes: {}
|
||||||
websocket_port: 4240
|
websocket_port: 4240
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
<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-2zm-1 4H5V8h14v2zm0 2v6H5v-6h14z"/></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-2zm-1 4H5V8h14v2zm0 2v6H5v-6h14z"/></svg>
|
||||||
<span>文件管理</span>
|
<span>文件管理</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="nav-item" data-page="settings">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81a.484.484 0 00-.41-.3h-3.98c-.2 0-.38.13-.45.32l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.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.58a.49.49 0 00-.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.19.24.31.44.31h3.98c.2 0 .38-.13.45-.32l.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>
|
||||||
|
<span>框架设置</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- Plugin WebUI pages (collapsible) -->
|
<!-- Plugin WebUI pages (collapsible) -->
|
||||||
<div class="nav-expandable" id="nav-plugins">
|
<div class="nav-expandable" id="nav-plugins">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ window.toggleTheme = function(){
|
|||||||
// Built-in pages map to ./static/pages/{name}.html
|
// Built-in pages map to ./static/pages/{name}.html
|
||||||
// Plugin pages load from /plugin/{name}
|
// Plugin pages load from /plugin/{name}
|
||||||
|
|
||||||
var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files'];
|
var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files','settings'];
|
||||||
var _pluginPagesCache = null; // {plugin_name: {path, title, icon}}
|
var _pluginPagesCache = null; // {plugin_name: {path, title, icon}}
|
||||||
|
|
||||||
function currentRoute() {
|
function currentRoute() {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<div class="plugin-page-root">
|
||||||
|
<h2>⚙ 框架设置</h2>
|
||||||
|
|
||||||
|
<!-- ── API Key 管理 ── -->
|
||||||
|
<div class="card" style="margin-bottom:24px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
|
<h3 style="margin:0">🔑 API Key 管理</h3>
|
||||||
|
<button class="btn btn-filled" onclick="showCreateKey()">+ 创建 Key</button>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--text-dim);font-size:.85rem;margin-bottom:12px">
|
||||||
|
API Key 用于服务器间安全通信。支持权限范围控制和过期时间。
|
||||||
|
</p>
|
||||||
|
<div id="apikey-list" style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse">
|
||||||
|
<thead><tr style="text-align:left;border-bottom:1px solid var(--border)">
|
||||||
|
<th style="padding:8px">名称</th><th style="padding:8px">Key</th><th style="padding:8px">权限</th><th style="padding:8px">用量</th><th style="padding:8px">过期</th><th style="padding:8px">操作</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="apikey-tbody"><tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">加载中...</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 创建对话框 (hidden) ── -->
|
||||||
|
<div id="create-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1000;display:none;align-items:center;justify-content:center">
|
||||||
|
<div class="card" style="min-width:420px;max-width:500px">
|
||||||
|
<h3 style="margin:0 0 16px 0">创建 API Key</h3>
|
||||||
|
<div class="form-group" style="margin-bottom:12px">
|
||||||
|
<label style="display:block;margin-bottom:4px;font-size:.85rem">名称</label>
|
||||||
|
<input id="new-key-name" class="input" placeholder="例如: 监控插件" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:12px">
|
||||||
|
<label style="display:block;margin-bottom:4px;font-size:.85rem">权限范围</label>
|
||||||
|
<select id="new-key-preset" class="input" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||||||
|
<option value="readonly">只读 — 仅查询系统状态</option>
|
||||||
|
<option value="monitor" selected>监控 — 查询 + 事件订阅</option>
|
||||||
|
<option value="full">管理 — 全部权限</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:16px">
|
||||||
|
<label style="display:block;margin-bottom:4px;font-size:.85rem">过期时间 (秒, 0=永不过期)</label>
|
||||||
|
<input id="new-key-ttl" class="input" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs)">
|
||||||
|
<span style="font-size:.75rem;color:var(--text-dim)">常用: 86400(1天) 604800(7天) 2592000(30天)</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||||
|
<button class="btn btn-outlined" onclick="hideCreateKey()">取消</button>
|
||||||
|
<button class="btn btn-filled" onclick="doCreateKey()">创建</button>
|
||||||
|
</div>
|
||||||
|
<div id="create-result" style="margin-top:12px;display:none">
|
||||||
|
<div style="background:var(--md-sys-color-surface-container-lowest);padding:12px;border-radius:var(--shape-xs);word-break:break-all">
|
||||||
|
<strong style="color:var(--success)">✅ Key 已创建 (仅显示一次):</strong>
|
||||||
|
<code id="new-key-display" style="display:block;margin-top:4px;font-size:.9rem;color:var(--primary)"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.input:focus { outline:1px solid var(--primary) }
|
||||||
|
#apikey-tbody td { padding:8px; border-bottom:1px solid var(--md-sys-color-surface-container-lowest) }
|
||||||
|
#apikey-tbody tr:hover { background:var(--md-sys-color-surface-container-lowest) }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── 加载 API Keys ──
|
||||||
|
async function loadKeys() {
|
||||||
|
var tbody = document.getElementById("apikey-tbody");
|
||||||
|
try {
|
||||||
|
var resp = await fetch("./api/apikeys");
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!data.keys || !data.keys.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--text-dim)">暂无 API Key</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rows = [];
|
||||||
|
data.keys.forEach(function(k) {
|
||||||
|
var expStr = k.expires_at > 0 ? new Date(k.expires_at*1000).toLocaleDateString() : "永不过期";
|
||||||
|
if (k.expired) expStr = '<span style="color:var(--error)">已过期</span>';
|
||||||
|
var permLabel = k.permissions && k.permissions[0] === "admin" ? "管理" : (k.permissions && k.permissions.length <= 2 ? "只读" : "监控");
|
||||||
|
rows.push(
|
||||||
|
'<tr>' +
|
||||||
|
'<td>' + esc(k.name) + '</td>' +
|
||||||
|
'<td><code>' + esc(k.key) + '</code></td>' +
|
||||||
|
'<td><span class="badge">' + permLabel + '</span></td>' +
|
||||||
|
'<td>' + (k.request_count || 0) + '</td>' +
|
||||||
|
'<td>' + expStr + '</td>' +
|
||||||
|
'<td><button class="btn btn-sm btn-outlined" style="color:var(--error);border-color:var(--error)" onclick="deleteKey(\'' + esc(k.full_key) + '\',\'' + esc(k.name) + '\')">删除</button></td>' +
|
||||||
|
'</tr>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
tbody.innerHTML = rows.join("");
|
||||||
|
} catch(e) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="padding:16px;text-align:center;color:var(--error)">加载失败: ' + esc(String(e)) + '</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||||
|
|
||||||
|
// ── 创建对话框 ──
|
||||||
|
function showCreateKey() {
|
||||||
|
document.getElementById("create-dialog").style.display = "flex";
|
||||||
|
document.getElementById("create-result").style.display = "none";
|
||||||
|
document.getElementById("new-key-name").value = "";
|
||||||
|
document.getElementById("new-key-ttl").value = "0";
|
||||||
|
}
|
||||||
|
function hideCreateKey() {
|
||||||
|
document.getElementById("create-dialog").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCreateKey() {
|
||||||
|
var name = document.getElementById("new-key-name").value.trim();
|
||||||
|
if (!name) { alert("请输入名称"); return; }
|
||||||
|
var preset = document.getElementById("new-key-preset").value;
|
||||||
|
var ttl = parseInt(document.getElementById("new-key-ttl").value) || 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var resp = await fetch("./api/apikeys", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({name: name, preset: preset, ttl: ttl})
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (data.ok) {
|
||||||
|
document.getElementById("create-result").style.display = "block";
|
||||||
|
document.getElementById("new-key-display").textContent = data.key;
|
||||||
|
loadKeys();
|
||||||
|
} else {
|
||||||
|
alert("创建失败: " + (data.error || "未知错误"));
|
||||||
|
}
|
||||||
|
} catch(e) { alert("请求失败: " + e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 删除 ──
|
||||||
|
async function deleteKey(fullKey, name) {
|
||||||
|
if (!confirm("确认删除 Key '" + name + "'? 此操作不可撤销。")) return;
|
||||||
|
try {
|
||||||
|
var resp = await fetch("./api/apikeys", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({key: fullKey})
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (data.ok) loadKeys();
|
||||||
|
else alert("删除失败: " + (data.error || "未知错误"));
|
||||||
|
} catch(e) { alert("请求失败: " + e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 初始加载 ──
|
||||||
|
loadKeys();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user