Files
SenSu/static/web_panel/pages/plugins.html
T
qinglong cc50ecb813 fix: Windows兼容 + Textual 8.x + MD3 SVG图标替代emoji
1. Windows: reuse_port 仅非win32平台启用
2. Textual 8.2.7: refresh() 加 **kwargs 兼容 repaint 参数
3. WebUI: 设置页/插件页 emoji → SVG (fill:currentColor 日夜间适配)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 20:42:22 +08:00

126 lines
8.3 KiB
HTML

<div class="plugin-page-root" style="padding:16px">
<h2 style="margin:0 0 12px 0"><svg width="22" height="22" viewBox="0 0 24 24" style="vertical-align:-4px;margin-right:6px"><path fill="currentColor" 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>
<!-- ── 统计卡片 ── -->
<div id="stats-card" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<div class="stat-chip"><span class="stat-num" id="st-total">-</span> 总计</div>
<div class="stat-chip loaded"><span class="stat-num" id="st-loaded">-</span> 已启用</div>
<div class="stat-chip disabled"><span class="stat-num" id="st-disabled">-</span> 已停用</div>
<div class="stat-chip failed"><span class="stat-num" id="st-failed">-</span> 启动失败</div>
</div>
<!-- ── 导入区域 ── -->
<div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;align-items:stretch">
<div class="import-card">
<div class="card-title"><svg width="18" height="18" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:4px"><path fill="currentColor" 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>导入 ZIP 包</div>
<label class="file-label" for="zip-file">选择 .zip 文件</label>
<input type="file" id="zip-file" accept=".zip" class="file-input">
<div class="card-actions">
<button class="btn btn-sm btn-filled" onclick="importZip()">上传安装</button>
<span id="zip-status" class="status-text"></span>
</div>
</div>
<div class="import-card">
<div class="card-title"><svg width="18" height="18" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:4px"><path fill="currentColor" d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>Git Clone 安装</div>
<input id="git-url" class="text-input" placeholder="https://github.com/user/repo.git">
<div class="card-actions">
<button class="btn btn-sm btn-filled" onclick="importGit()">Clone 安装</button>
<span id="git-status" class="status-text"></span>
</div>
</div>
</div>
<!-- ── 失败详情 ── -->
<div id="fail-detail" style="display:none;margin-bottom:12px;padding:10px;background:var(--md-sys-color-surface-container-lowest);border-radius:var(--shape-xs);font-size:.8rem;color:var(--error)"></div>
<!-- ── 插件列表 ── -->
<div id="plugin-list">加载中...</div>
<style>
.stat-chip { display:flex;align-items:center;gap:6px;padding:8px 14px;border-radius:var(--shape-sm);background:var(--md-sys-color-surface-container);min-width:100px }
.stat-chip.loaded { border-left:3px solid var(--success,#4caf50) }
.stat-chip.disabled { border-left:3px solid var(--text-dim) }
.stat-chip.failed { border-left:3px solid var(--error,#f44336) }
.stat-num { font-size:1.4rem;font-weight:700 }
.plugin-row { display:flex;align-items:center;justify-content:space-between;padding:10px 12px;margin-bottom:4px;background:var(--md-sys-color-surface-container);border-radius:var(--shape-sm);gap:8px }
.plugin-row .info { flex:1;min-width:0 }
.plugin-row .name { font-weight:600 } .plugin-row .ver { font-size:.75rem;color:var(--text-dim) }
.plugin-row .error-tag { font-size:.75rem;color:var(--error) }
.import-card { flex:1;min-width:280px;padding:14px;background:var(--md-sys-color-surface-container);border-radius:var(--shape-sm);display:flex;flex-direction:column;gap:8px;margin-bottom:4px }
.import-card .card-title { font-weight:600 }
.import-card .card-actions { display:flex;align-items:center;gap:8px;margin-top:auto }
.import-card .status-text { font-size:.8rem;white-space:nowrap }
.import-card .text-input { width:100%;padding:6px 8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs);box-sizing:border-box }
.import-card .text-input:focus { outline:1px solid var(--primary) }
.file-input { color:var(--text);font-size:.85rem }
.file-input::file-selector-button { padding:5px 12px;background:var(--md-sys-color-surface-container-lowest);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs);cursor:pointer;margin-right:8px }
.file-input::file-selector-button:hover { background:var(--md-sys-color-surface-container-highest) }
.file-label { font-size:.8rem;color:var(--text-dim) }
</style>
<script>
async function load() {
var list = document.getElementById("plugin-list");
try {
var r1 = await fetch("./api/plugins/stats"), stats = await r1.json();
document.getElementById("st-total").textContent = stats.total||0;
document.getElementById("st-loaded").textContent = stats.loaded||0;
document.getElementById("st-disabled").textContent = stats.disabled||0;
document.getElementById("st-failed").textContent = stats.failed||0;
var fd = document.getElementById("fail-detail");
if (stats.fail_details && Object.keys(stats.fail_details).length) {
fd.style.display = "block";
fd.innerHTML = "<strong>启动失败:</strong> " + Object.entries(stats.fail_details).map(function(e){return e[0]+": "+esc(e[1])}).join(" | ");
} else fd.style.display = "none";
var r2 = await fetch("./api/plugins"), data = await r2.json();
var plugins = data.plugins||[];
if (!plugins.length) { list.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-dim)">暂无插件</div>'; return; }
list.innerHTML = plugins.map(function(p){
return '<div class="plugin-row"><div class="info"><div class="name">'+esc(p.name)+'</div><div class="ver">v'+esc(p.version||'?')+(p.running?' ✅':' ⏸')+'</div>'+(p.error?'<div class="error-tag">'+esc(p.error)+'</div>':'')+'</div>'+
'<button class="btn btn-sm btn-outlined" onclick="togglePlugin(\''+esc(p.name)+'\',\''+(p.running?'unload':'enable')+'\')">'+(p.running?'停用':'启用')+'</button>'+
'<button class="btn btn-sm btn-outlined" onclick="togglePlugin(\''+esc(p.name)+'\',\'reload\')">重载</button></div>';
}).join("");
} catch(e) { list.innerHTML = '<div style="color:var(--error)">加载失败: '+esc(String(e))+'</div>'; }
}
async function togglePlugin(name, action) {
await fetch("./api/plugins/"+name+"/"+action, {method:"POST"});
load();
}
async function importZip() {
var f = document.getElementById("zip-file").files[0];
if (!f) { alert("请选择 .zip 文件"); return; }
var s = document.getElementById("zip-status");
s.textContent = "⏳ 上传中..."; s.style.color = "var(--text-dim)";
var fd = new FormData(); fd.append("file", f);
try {
var r = await fetch("./api/plugins/import", {method:"POST", body:fd});
var d = await r.json();
if (d.ok) { s.textContent = "✅ " + d.name + (d.loaded?" 已加载":" 待手动加载"); s.style.color = "var(--success)"; }
else { s.textContent = "❌ " + (d.error||"失败"); s.style.color = "var(--error)"; }
} catch(e) { s.textContent = "❌ " + e; s.style.color = "var(--error)"; }
load();
}
async function importGit() {
var url = document.getElementById("git-url").value.trim();
if (!url) { alert("请输入 Git 仓库地址"); return; }
var s = document.getElementById("git-status");
s.textContent = "⏳ Clone 中..."; s.style.color = "var(--text-dim)";
try {
var r = await fetch("./api/plugins/import", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({url:url})});
var d = await r.json();
if (d.ok) { s.textContent = "✅ " + d.name + (d.loaded?" 已加载":" 待手动加载"); s.style.color = "var(--success)"; }
else { s.textContent = "❌ " + (d.error||"失败"); s.style.color = "var(--error)"; }
} catch(e) { s.textContent = "❌ " + e; s.style.color = "var(--error)"; }
load();
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
load();
</script>
</div>