Files
SenSu/static/web_panel/pages/settings.html
T
qinglong 66ad51002c 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>
2026-06-13 14:28:59 +08:00

151 lines
7.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// ── 创建对话框 ──
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>