Files
ops-assistant/templates/cpa_settings.html
2026-03-19 21:23:28 +08:00

208 lines
10 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CPA 配置 - Ops-Assistant</title>
<style>
:root{
--bg:#f5f7fb;--text:#222;--card:#fff;--border:#e5e7eb;--muted:#6b7280;
--accent:#ee5a24;--accent-hover:#d63031;--header-bg:linear-gradient(135deg,#ff6b6b,#ee5a24);
}
[data-theme="dark"]{
--bg:#0f172a;--text:#e5e7eb;--card:#111827;--border:#1f2937;--muted:#9ca3af;
--accent:#f97316;--accent-hover:#ea580c;--header-bg:linear-gradient(135deg,#7c2d12,#ea580c);
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.header{background:var(--header-bg);color:#fff;padding:18px 20px;position:sticky;top:0;z-index:100;box-shadow:0 2px 10px rgba(0,0,0,.15);display:flex;justify-content:space-between;align-items:center;gap:10px}
.header .right{display:flex;align-items:center;gap:8px}
.header a,.header button{color:#fff;text-decoration:none;background:rgba(255,255,255,.2);padding:0 10px;border-radius:8px;font-size:13px;border:none;cursor:pointer;line-height:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;}
.header button{width:28px;padding:0;transition:transform .2s ease}
.header button.spin{transform:rotate(180deg)}
.header button svg{width:16px;height:16px;stroke:#fff;fill:none;stroke-width:2}
.wrap{max-width:980px;margin:0 auto;padding:14px}
.card{background:var(--card);border-radius:6px;padding:14px;border:1px solid var(--border);box-shadow:none;margin-bottom:10px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:10px 0}
.row label{width:180px;color:var(--muted);font-size:13px}
.row input{flex:1;min-width:240px;padding:8px;border:1px solid var(--border);border-radius:6px;font-size:13px;background:var(--card);color:var(--text)}
.btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
button{border:none;border-radius:6px;padding:8px 12px;cursor:pointer;color:#fff;background:var(--accent);font-size:13px}
button.secondary{background:#6b7280}
button.danger{background:#9b1c1c}
.table{width:100%;border-collapse:collapse;font-size:13px}
.table th,.table td{border-bottom:1px solid var(--border);padding:8px;text-align:left}
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:12px}
.on{background:#dcfce7;color:#166534}.off{background:#e5e7eb;color:#374151}
small{color:var(--muted)}
.theme-hidden{display:none !important;}
</style>
</head>
<body>
<div class="header">
<div>🔧 CPA 配置 · {{.version}}</div>
<div class="right">
<button id="themeToggle" class="theme-hidden" title="切换主题" aria-label="切换主题">
<svg id="themeIcon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="2" x2="12" y2="4"></line>
<line x1="12" y1="20" x2="12" y2="22"></line>
<line x1="2" y1="12" x2="4" y2="12"></line>
<line x1="20" y1="12" x2="22" y2="12"></line>
<line x1="4.2" y1="4.2" x2="5.8" y2="5.8"></line>
<line x1="18.2" y1="18.2" x2="19.8" y2="19.8"></line>
<line x1="18.2" y1="5.8" x2="19.8" y2="4.2"></line>
<line x1="4.2" y1="19.8" x2="5.8" y2="18.2"></line>
</svg>
</button>
<a href="/">返回首页</a>
<a href="/logout">退出</a>
</div>
</div>
<div class="wrap">
<div class="card">
<h3>CPA 管理接口</h3>
<div class="row"><label>CPA Management Base</label><input id="base" placeholder="https://cpa.pao.xx.kg/v0/management"></div>
<div class="row"><label>CPA Management Token</label><input id="token" placeholder="请输入 Token"></div>
<small>用于访问 CPA 管理接口Bearer Token</small>
<div class="btns">
<button onclick="save()">保存</button>
<button class="secondary" onclick="load()">刷新</button>
</div>
<small id="msg"></small>
</div>
<div class="card">
<h3>目标主机Ops Targets</h3>
<div class="row"><label>Name</label><input id="name" placeholder="如 hwsg"></div>
<div class="row"><label>Host</label><input id="host" placeholder="如 124.243.132.158"></div>
<div class="row"><label>Port</label><input id="port" placeholder="22"></div>
<div class="row"><label>User</label><input id="user" placeholder="root"></div>
<div class="row"><label>Enabled</label><input id="enabled" type="checkbox" checked></div>
<div class="btns"><button onclick="createTarget()">新增</button><button class="secondary" onclick="loadTargets()">刷新列表</button></div>
<small id="tmsg"></small>
<div style="margin-top:10px;">
<table class="table" style="width:100%;border-collapse:collapse;font-size:13px;">
<thead><tr><th>Name</th><th>Host</th><th>Port</th><th>User</th><th>Enabled</th><th>操作</th></tr></thead>
<tbody id="tbody"></tbody>
</table>
</div>
</div>
</div>
<script>
async function api(url,opt={}){
const r=await fetch(url,opt);
const out=await r.json().catch(()=>({}));
if(!r.ok) throw new Error(out.message||('HTTP '+r.status));
return out?.data||{};
}
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
async function load(){
try{
const d=await api('/api/v1/admin/cpa/settings');
document.getElementById('token').value = d.settings?.cpa_management_token || '';
document.getElementById('base').value = d.settings?.cpa_management_base || '';
document.getElementById('msg').textContent='已加载';
}catch(e){document.getElementById('msg').textContent='加载失败:'+esc(e.message||e);}
}
async function save(){
const token=document.getElementById('token').value.trim();
const base=document.getElementById('base').value.trim();
try{
await api('/api/v1/admin/cpa/settings',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({management_token:token,management_base:base})});
document.getElementById('msg').textContent='已保存';
}catch(e){document.getElementById('msg').textContent='保存失败:'+esc(e.message||e);}
}
async function loadTargets(){
try{
const d=await api('/api/v1/admin/ops/targets');
const list=Array.isArray(d.targets)?d.targets:[];
const tbody=document.getElementById('tbody');
if(!list.length){tbody.innerHTML='<tr><td colspan="6"><small>暂无目标</small></td></tr>';return;}
tbody.innerHTML=list.map(t=>`<tr>
<td>${esc(t.name)}</td>
<td><input data-id="${t.id}" data-field="host" value="${esc(t.host)}"></td>
<td><input data-id="${t.id}" data-field="port" value="${esc(t.port)}"></td>
<td><input data-id="${t.id}" data-field="user" value="${esc(t.user)}"></td>
<td>${t.enabled?'<span class="badge on">ON</span>':'<span class="badge off">OFF</span>'}</td>
<td>
<button class="secondary" onclick="saveTarget(${t.id})">保存</button>
<button class="danger" onclick="toggleTarget(${t.id},${t.enabled?0:1})">${t.enabled?'禁用':'启用'}</button>
</td>
</tr>`).join('');
}catch(e){document.getElementById('tmsg').textContent='加载失败:'+esc(e.message||e);}
}
async function createTarget(){
const payload={
name:document.getElementById('name').value.trim(),
host:document.getElementById('host').value.trim(),
port:parseInt(document.getElementById('port').value||'22',10),
user:document.getElementById('user').value.trim(),
enabled:document.getElementById('enabled').checked
};
try{
await api('/api/v1/admin/ops/targets',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
document.getElementById('tmsg').textContent='已新增';
await loadTargets();
}catch(e){document.getElementById('tmsg').textContent='失败:'+esc(e.message||e);}
}
async function saveTarget(id){
const inputs=[...document.querySelectorAll(`input[data-id="${id}"]`)];
const payload={enabled:true};
inputs.forEach(i=>{payload[i.dataset.field]=i.value.trim();});
payload.port=parseInt(payload.port||'22',10);
try{
await api('/api/v1/admin/ops/targets/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
await loadTargets();
}catch(e){alert('保存失败:'+(e.message||e));}
}
async function toggleTarget(id, enable){
try{
await api('/api/v1/admin/ops/targets/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:!!enable})});
await loadTargets();
}catch(e){alert('失败:'+(e.message||e));}
}
function setThemeIcon(theme){
const icon=document.getElementById('themeIcon');
if(!icon) return;
if(theme==='dark'){
// 月亮:同线条风格
icon.innerHTML = '<path d="M21 12.8A8.5 8.5 0 1 1 11.2 3.1 7 7 0 0 0 21 12.8z"></path>';
}else{
// 太阳:保留原样式
icon.innerHTML = '<circle cx="12" cy="12" r="5"></circle>'+
'<line x1="12" y1="2" x2="12" y2="4"></line>'+
'<line x1="12" y1="20" x2="12" y2="22"></line>'+
'<line x1="2" y1="12" x2="4" y2="12"></line>'+
'<line x1="20" y1="12" x2="22" y2="12"></line>'+
'<line x1="4.2" y1="4.2" x2="5.8" y2="5.8"></line>'+
'<line x1="18.2" y1="18.2" x2="19.8" y2="19.8"></line>'+
'<line x1="18.2" y1="5.8" x2="19.8" y2="4.2"></line>'+
'<line x1="4.2" y1="19.8" x2="5.8" y2="18.2"></line>';
}
}
function applyTheme(theme){
if(theme==='dark'){document.documentElement.setAttribute('data-theme','dark');}
else{document.documentElement.removeAttribute('data-theme');}
localStorage.setItem('theme',theme);
setThemeIcon(theme);
}
function toggleTheme(){
const cur=localStorage.getItem('theme')||'dark';
const next = cur==='light'?'dark':'light';
const btn=document.getElementById('themeToggle');
if(btn){btn.classList.add('spin'); setTimeout(()=>btn.classList.remove('spin'),200);}
applyTheme(next);
}
(function(){
const saved=localStorage.getItem('theme')||'dark';
applyTheme(saved);
const btn=document.getElementById('themeToggle');
if(btn){btn.addEventListener('click',toggleTheme)}
load();
loadTargets();
})();
</script>
</body>
</html>