66ad51002c
新增: - 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>
151 lines
7.6 KiB
HTML
151 lines
7.6 KiB
HTML
<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>
|