208 lines
10 KiB
HTML
208 lines
10 KiB
HTML
<!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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|