37 lines
25 KiB
HTML
37 lines
25 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>SOCKS5 Island Monitor</title>
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='18' fill='%2319c8b9'/%3E%3Ccircle cx='19' cy='23' r='8' fill='%23f7cd67'/%3E%3Ccircle cx='43' cy='22' r='9' fill='%2386d67a'/%3E%3Cpath d='M10 47c8-15 20-19 31-12 6 4 10 8 13 12H10z' fill='%23f8f8f0'/%3E%3Cpath d='M24 44c3-8 9-13 17-16' stroke='%23725d42' stroke-width='4' stroke-linecap='round' fill='none'/%3E%3Ctext x='32' y='55' text-anchor='middle' font-size='14' font-family='Arial' fill='%23725d42'%3ES5%3C/text%3E%3C/svg%3E">
|
|
<link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='18' fill='%2319c8b9'/%3E%3Ccircle cx='19' cy='23' r='8' fill='%23f7cd67'/%3E%3Ccircle cx='43' cy='22' r='9' fill='%2386d67a'/%3E%3Cpath d='M10 47c8-15 20-19 31-12 6 4 10 8 13 12H10z' fill='%23f8f8f0'/%3E%3Cpath d='M24 44c3-8 9-13 17-16' stroke='%23725d42' stroke-width='4' stroke-linecap='round' fill='none'/%3E%3Ctext x='32' y='55' text-anchor='middle' font-size='14' font-family='Arial' fill='%23725d42'%3ES5%3C/text%3E%3C/svg%3E">
|
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&family=Zen+Maru+Gothic:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{--bg:#f8f8f0;--card:rgb(247,243,223);--text:#725d42;--head:#794f27;--muted:#9f927d;--soft:#c4b89e;--line:#9f927d;--mint:#19c8b9;--mint2:#e6f9f6;--green:#6fba2c;--red:#e05a5a;--yellow:#f5c31c;--blue:#889df0;--pink:#f8a6b2;--purple:#b77dee;--orange:#e59266;--teal:#82d5bb;--shadow:#bdaea0}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 12% 18%,rgba(130,213,187,.35),transparent 22%),radial-gradient(circle at 88% 8%,rgba(247,205,103,.36),transparent 21%),linear-gradient(180deg,#a7dec0 0,#f8f8f0 42%);color:var(--text);font-family:Nunito,'Noto Sans SC','Zen Maru Gothic',-apple-system,'PingFang SC',sans-serif;font-weight:500;letter-spacing:.01em}.wrap{max-width:1420px;margin:0 auto;padding:26px}.island{background:rgba(248,248,240,.76);border:3px solid rgba(159,146,125,.45);border-radius:42px 36px 48px 34px/36px 44px 34px 46px;padding:22px;box-shadow:0 12px 0 rgba(114,93,66,.12)}.nav{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:22px}.brand{display:flex;align-items:center;gap:14px}.logo{width:58px;height:58px;border-radius:22px;background:linear-gradient(145deg,var(--mint),#86d67a);border:3px solid var(--line);box-shadow:0 5px 0 var(--shadow);display:grid;place-items:center;font-size:30px}.brand h1{margin:0;color:var(--head);font-size:34px;font-weight:900;letter-spacing:.02em}.muted{color:var(--muted)}.tabs{display:flex;gap:10px;flex-wrap:wrap}.tab,.btn{border:2.5px solid var(--line);background:var(--card);color:var(--text);border-radius:50px;height:44px;padding:0 18px;font-weight:800;cursor:pointer;box-shadow:0 5px 0 var(--shadow);transition:all .22s cubic-bezier(.4,0,.2,1)}.tab:hover,.btn:hover{transform:translateY(-1px);box-shadow:0 6px 0 var(--shadow)}.tab:active,.btn:active{transform:translateY(2px);box-shadow:0 1px 0 var(--shadow)}.tab.active,.btn.primary{background:var(--mint);color:white;border-color:#11a89b;box-shadow:0 5px 0 #0f8f85}.btn.danger{background:#fff1ef;color:var(--red);border-color:var(--red);box-shadow:0 5px 0 #c94444}.btn.small{height:34px;padding:0 13px;font-size:12px;border-radius:18px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:18px}.hero{display:grid;grid-template-columns:1.2fr .8fr;gap:18px;margin-bottom:18px}.card{background:var(--card);border:3px solid rgba(159,146,125,.88);border-radius:28px 24px 32px 23px/25px 32px 24px 30px;padding:18px;box-shadow:0 4px 10px rgba(107,92,67,.42);transition:all .22s}.card:hover{transform:translateY(-2px)}.card.blue{background:#eef2ff}.card.green{background:#eef9e8}.card.yellow{background:#fff5cf}.card.pink{background:#fff0f3}.row{display:flex;align-items:center;justify-content:space-between;gap:12px}.metric{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:16px}.metric .box{background:linear-gradient(180deg,#fff,var(--bg));border:2.5px solid #d4cfc3;border-radius:20px;padding:14px;box-shadow:0 3px 0 #d4c9b4}.num{font-size:34px;font-weight:900;color:#8b7355}.section{display:none}.section.active{display:block}h2,h3{color:var(--head)}.pill{display:inline-flex;align-items:center;gap:7px;border:2px solid var(--line);border-radius:999px;padding:5px 10px;background:#fff;font-size:12px;font-weight:900}.dot{width:9px;height:9px;border-radius:50%;background:var(--yellow)}.up{color:var(--green)}.up .dot{background:var(--green)}.down{color:var(--red)}.down .dot{background:var(--red)}.unknown{color:#dba90e}.unknown .dot{background:var(--yellow)}.mono{font-family:'Zen Maru Gothic',ui-monospace,monospace}.kv{display:grid;grid-template-columns:110px 1fr;gap:7px;margin:14px 0;font-size:13px}.toolbar{display:flex;gap:9px;flex-wrap:wrap}table{width:100%;border-collapse:separate;border-spacing:0 7px;font-size:13px}td,th{text-align:left;padding:8px 10px}th{color:var(--muted);font-weight:900}tbody tr{background:rgba(255,255,255,.55)}tbody td:first-child{border-radius:14px 0 0 14px}tbody td:last-child{border-radius:0 14px 14px 0}.err{max-width:420px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.drawer{position:fixed;right:18px;top:18px;bottom:18px;width:min(620px,calc(100vw - 36px));background:var(--card);border:3px solid var(--line);border-radius:40px 35px 45px 38px/38px 45px 35px 40px;padding:24px;box-shadow:0 18px 0 rgba(114,93,66,.22),0 30px 80px rgba(107,92,67,.38);transform:translateX(120%);transition:.25s;z-index:10;overflow:auto}.drawer.open{transform:none}.overlay{position:fixed;inset:0;background:rgba(80,60,40,.38);display:none;z-index:9}.overlay.open{display:block}.form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.form .full{grid-column:1/-1}label{font-size:12px;color:var(--muted);font-weight:900;display:block;margin:8px 0 5px}input,select,textarea{width:100%;height:42px;background:#fff;color:var(--text);border:2.5px solid #c4b89e;border-radius:50px;padding:0 16px;box-shadow:0 3px 0 #d4c9b4;outline:none;font-weight:700}textarea{height:94px;border-radius:20px;padding:12px 16px}input:focus,select:focus,textarea:focus{border-color:#ffcc00;box-shadow:0 3px 0 #e0b800,0 0 0 3px rgba(255,204,0,.15)}.type-fields{background:rgba(255,255,255,.48);border:2px dashed #c4b89e;border-radius:22px;padding:12px}.channel{display:grid;grid-template-columns:1fr auto;align-items:center;gap:12px}.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:var(--mint);color:#fff;border:3px solid #11a89b;border-radius:50px;padding:12px 18px;box-shadow:0 5px 0 #0f8f85;font-weight:900;display:none;z-index:20}.toast.show{display:block}.sea{height:60px;margin-top:18px;border-radius:0 0 34px 34px;background:repeating-linear-gradient(135deg,#82d5bb 0 18px,#19c8b9 18px 36px);opacity:.45}@media(max-width:900px){.hero{grid-template-columns:1fr}.metric{grid-template-columns:repeat(2,1fr)}.form{grid-template-columns:1fr}.nav{align-items:flex-start;flex-direction:column}.wrap{padding:12px}}
|
|
</style></head><body><div class="wrap"><div class="island"><div class="nav"><div class="brand"><div class="logo">🏝️</div><div><h1>SOCKS5 Island</h1><div class="muted">代理健康监控面板</div></div></div><div class="tabs"><button class="tab active" data-tab="dashboard">仪表盘</button><button class="tab" data-tab="proxies">节点管理</button><button class="tab" data-tab="notify">通知渠道</button><button class="tab" data-tab="settings">设置</button><button class="tab" data-tab="logs">日志</button></div></div><div class="hero"><div class="card yellow"><div class="row"><div><h2 style="margin:0 0 8px">运行概览</h2><div class="muted" id="now">加载中...</div></div><button class="btn" onclick="refresh()">刷新</button></div><div class="metric"><div class="box"><div class="muted">节点</div><div class="num" id="mTotal">0</div></div><div class="box"><div class="muted">正常</div><div class="num up" id="mUp">0</div></div><div class="box"><div class="muted">故障</div><div class="num down" id="mDown">0</div></div><div class="box"><div class="muted">通知</div><div class="num" id="mChan">0</div></div><div class="box"><div class="muted">单节点记录</div><div class="num" id="mKeep">-</div></div></div></div><div class="card green"><h2 style="margin-top:0">快速操作</h2><div class="toolbar"><button class="btn primary" onclick="openProxy()">新增节点</button><button class="btn" onclick="openChannel()">新增通知</button><button class="btn" onclick="openSettings()">检测设置</button><button class="btn danger" onclick="clearLogs()">清除日志</button><button class="btn" onclick="testAllNotify()">测试全部通知</button></div><p class="muted">管理节点和通知渠道。</p></div></div><section id="dashboard" class="section active"><div id="proxyCards" class="grid"></div></section><section id="proxies" class="section"><div class="card"><div class="row"><h2>SOCKS5 节点</h2><button class="btn primary" onclick="openProxy()">新增节点</button></div><div id="proxyList"></div></div></section><section id="notify" class="section"><div class="card"><div class="row"><h2>通知渠道</h2><button class="btn primary" onclick="openChannel()">新增渠道</button></div><div id="channelList"></div></div></section><section id="settings" class="section"><div class="card"><div class="row"><div><h2>检测设置</h2><p class="muted">配置全局检测目标、间隔、超时和状态阈值。</p></div><button class="btn primary" onclick="openSettings()">编辑全局设置</button></div><div class="metric"><div class="box"><div class="muted">默认检测目标</div><div class="mono" id="sTarget">-</div></div><div class="box"><div class="muted">默认间隔</div><div class="num" id="sInterval">-</div></div><div class="box"><div class="muted">默认超时</div><div class="num" id="sTimeout">-</div></div><div class="box"><div class="muted">阈值</div><div class="mono" id="sThreshold">-</div></div><div class="box"><div class="muted">单节点记录上限</div><div class="num" id="sKeep">-</div></div></div></div></section><section id="logs" class="section"><div class="grid"><div class="card"><h2>事件</h2><table><thead><tr><th>时间</th><th>节点</th><th>状态</th><th>消息</th></tr></thead><tbody id="events"></tbody></table></div><div class="card"><h2>通知记录</h2><table><thead><tr><th>时间</th><th>渠道</th><th>结果</th><th>标题</th></tr></thead><tbody id="notifLogs"></tbody></table></div></div></section><div class="sea"></div></div></div><div id="overlay" class="overlay" onclick="closeDrawer()"></div><div id="drawer" class="drawer"></div><div id="toast" class="toast"></div>
|
|
<script>
|
|
let DATA=null, editingProxy=null, editingChannel=null; const $=id=>document.getElementById(id); function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} function ts(t){return t?new Date(t*1000).toLocaleString():'-'} function toast(s){$('toast').textContent=s;$('toast').classList.add('show');setTimeout(()=>$('toast').classList.remove('show'),2400)} async function api(url,opt={}){let r=await fetch(url,{headers:{'Content-Type':'application/json'},...opt});let j=await r.json(); if(!r.ok||j.ok===false) throw new Error(j.error||'请求失败'); return j} function cfg(c){return c.config||{}}
|
|
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{closeDrawer();document.querySelectorAll('.tab,.section').forEach(x=>x.classList.remove('active'));b.classList.add('active');$(b.dataset.tab).classList.add('active')})
|
|
function statusPill(s){let c=s||'unknown';return `<span class="pill ${c}"><span class="dot"></span>${esc(c)}</span>`}
|
|
async function refresh(){DATA=await api('/api/status'); $('now').textContent='当前时间:'+DATA.now_text; let ps=DATA.proxies, up=ps.filter(x=>x.state.status==='up').length, down=ps.filter(x=>x.state.status==='down').length; $('mTotal').textContent=ps.length;$('mUp').textContent=up;$('mDown').textContent=down;$('mChan').textContent=(DATA.notifications.channels||[]).length;if($('mKeep'))$('mKeep').textContent=DATA.settings.max_checks_per_proxy||'-'; renderSettings(); renderProxies(); renderChannels(); renderLogs()}
|
|
function renderProxies(){let cards='', list=''; for(const it of DATA.proxies){let p=it.proxy,s=it.state; let recent=(it.recent||[]).slice(0,5).map(r=>`<tr><td>${ts(r.ts)}</td><td class="${r.ok?'up':'down'}">${r.ok?'OK':'FAIL'}</td><td>${r.latency_ms??'-'}ms</td><td class="mono">${esc(r.exit_ip||'-')}</td><td class="err">${esc(r.error||'')}</td></tr>`).join(''); let color=s.status==='up'?'green':s.status==='down'?'pink':'blue'; let block=`<div class="card ${color}"><div class="row"><div><h2 style="margin:.1em 0">${esc(p.name||p.id)}</h2><div class="muted mono">${esc(p.host)}:${esc(p.port)} → ${esc(p.target_host)}:${esc(p.target_port)} · ${esc(p.interval_seconds||DATA.settings.interval_seconds)}s</div></div>${statusPill(s.status)}</div><div class="kv"><div class="muted">最后检测</div><div>${ts(s.last_ts)}</div><div class="muted">延迟</div><div>${s.latency_ms??'-'} ms</div><div class="muted">代理出口 IP</div><div class="mono">${esc(s.exit_ip||'-')}</div><div class="muted">失败/成功</div><div>${s.fail_count} / ${s.ok_count}</div><div class="muted">错误</div><div class="err">${esc(s.error||'-')}</div></div><div class="toolbar"><button class="btn small" onclick="checkProxy('${esc(p.id)}')">立即检测</button><button class="btn small" onclick="openProxy('${esc(p.id)}')">编辑</button><button class="btn small danger" onclick="delProxy('${esc(p.id)}')">删除</button></div><table style="margin-top:12px"><thead><tr><th>时间</th><th>结果</th><th>延迟</th><th>出口 IP</th><th>错误</th></tr></thead><tbody>${recent}</tbody></table></div>`; cards+=block; list+=block } $('proxyCards').innerHTML=cards||'<div class="card muted">还没有节点,点击新增节点。</div>'; $('proxyList').innerHTML=list||'<p class="muted">暂无节点</p>'}
|
|
function channelSummary(c){let x=cfg(c); if(c.type==='bark')return x.key?'Key 已配置':'Key 未配置'; if(c.type==='pushplus')return x.token?'Token 已配置':'Token 未配置'; if(c.type==='autman')return x.access_token?'Token 已配置':'Token 未配置'; return x.url||''}
|
|
function renderSettings(){let s=DATA.settings||{}; if($('sTarget')){$('sTarget').textContent=(s.target_host||'-')+':'+(s.target_port||'-');$('sInterval').textContent=s.interval_seconds||'-';$('sTimeout').textContent=s.timeout_seconds||'-';$('sThreshold').textContent='失败 '+(s.fail_threshold||'-')+' / 恢复 '+(s.recover_threshold||'-');if($('sKeep'))$('sKeep').textContent=s.max_checks_per_proxy||'-'}}
|
|
function renderChannels(){let html=''; for(const c of DATA.notifications.channels||[]){html+=`<div class="card channel"><div><div class="row" style="justify-content:flex-start"><h3 style="margin:0">${esc(c.name)}</h3><span class="pill">${esc(c.type)}</span><span class="pill ${c.enabled?'up':'unknown'}"><span class="dot"></span>${c.enabled?'启用':'停用'}</span></div><div class="muted mono">${esc(channelSummary(c))}</div></div><div class="toolbar"><button class="btn small" onclick="testChannel('${esc(c.id)}')">测试</button><button class="btn small" onclick="openChannel('${esc(c.id)}')">编辑</button><button class="btn small danger" onclick="delChannel('${esc(c.id)}')">删除</button></div></div>`} $('channelList').innerHTML=html||'<p class="muted">暂无通知渠道</p>'}
|
|
function renderLogs(){ $('events').innerHTML=(DATA.events||[]).map(e=>`<tr><td>${ts(e.ts)}</td><td>${esc(e.proxy_id)}</td><td>${esc(e.old_status)} → ${esc(e.new_status)}</td><td>${esc(e.message)}</td></tr>`).join(''); $('notifLogs').innerHTML=(DATA.notification_logs||[]).map(n=>`<tr><td>${ts(n.ts)}</td><td>${esc(n.channel_name)}</td><td class="${n.ok?'up':'down'}">${n.ok?'OK':'FAIL'}</td><td>${esc(n.title)}</td></tr>`).join('')}
|
|
function closeDrawer(){ $('drawer').classList.remove('open');$('overlay').classList.remove('open')} function draw(html){$('drawer').innerHTML=html;$('drawer').classList.add('open');$('overlay').classList.add('open')}
|
|
function openProxy(id){let d=(DATA&&DATA.settings)||{};let p=id?DATA.proxies.find(x=>x.proxy.id===id).proxy:{enabled:true,port:1080,target_host:d.target_host||'www.baidu.com',target_port:d.target_port||443,timeout_seconds:d.timeout_seconds||8,interval_seconds:d.interval_seconds||300,fail_threshold:d.fail_threshold||3,recover_threshold:d.recover_threshold||1}; editingProxy=id||null; draw(`<h2>${id?'编辑':'新增'} SOCKS5 节点</h2><form id="pf" class="form"><div><label>名称</label><input name="name" value="${esc(p.name||'')}"></div><div><label>Host</label><input name="host" value="${esc(p.host||'')}"></div><div><label>Port</label><input name="port" type="number" value="${esc(p.port||1080)}"></div><div><label>用户名</label><input name="username" value="${esc(p.username||'')}"></div><div><label>密码(留空保持原值)</label><input name="password" type="password" value=""></div><details class="full"><summary>高级检测设置</summary><div class="form" style="margin-top:12px"><div><label>测试目标 Host</label><input name="target_host" value="${esc(p.target_host||((DATA.settings||{}).target_host)||'www.baidu.com')}"></div><div><label>测试目标 Port</label><input name="target_port" type="number" value="${esc(p.target_port||443)}"></div><div><label>超时秒</label><input name="timeout_seconds" type="number" value="${esc(p.timeout_seconds||8)}"></div><div><label>检测间隔秒</label><input name="interval_seconds" type="number" min="5" value="${esc(p.interval_seconds||((DATA.settings||{}).interval_seconds)||300)}"></div><div><label>失败阈值</label><input name="fail_threshold" type="number" value="${esc(p.fail_threshold||3)}"></div><div><label>恢复阈值</label><input name="recover_threshold" type="number" value="${esc(p.recover_threshold||1)}"></div></div></details><div class="full"><label><input style="width:auto;height:auto" name="enabled" type="checkbox" ${p.enabled!==false?'checked':''}> 启用监控</label></div><div class="full toolbar"><button class="btn primary" type="submit">保存</button><button class="btn" type="button" onclick="closeDrawer()">取消</button></div></form>`); $('pf').onsubmit=saveProxy}
|
|
async function saveProxy(e){e.preventDefault();let fd=new FormData(e.target), o=Object.fromEntries(fd.entries()); ['port','target_port','timeout_seconds','interval_seconds','fail_threshold','recover_threshold'].forEach(k=>o[k]=Number(o[k])); o.enabled=fd.get('enabled')==='on'; if(!o.password) delete o.password; await api(editingProxy?'/api/proxies/'+encodeURIComponent(editingProxy):'/api/proxies',{method:editingProxy?'PUT':'POST',body:JSON.stringify(o)}); closeDrawer(); toast('节点已保存'); await refresh()}
|
|
|
|
function openSettings(){let s=(DATA&&DATA.settings)||{}; draw(`<h2>全局检测设置</h2><form id="sf" class="form"><div><label>默认检测目标 Host</label><input name="target_host" value="${esc(s.target_host||'www.baidu.com')}"><p class="muted">可填公网、内网或业务目标。</p></div><div><label>默认检测目标 Port</label><input name="target_port" type="number" min="1" max="65535" value="${esc(s.target_port||443)}"></div><div><label>默认检测间隔秒</label><input name="interval_seconds" type="number" min="5" value="${esc(s.interval_seconds||300)}"><p class="muted">新增节点默认使用此值。</p></div><div><label>默认超时秒</label><input name="timeout_seconds" type="number" min="1" value="${esc(s.timeout_seconds||8)}"></div><div><label>默认失败阈值</label><input name="fail_threshold" type="number" min="1" value="${esc(s.fail_threshold||3)}"></div><div><label>默认恢复阈值</label><input name="recover_threshold" type="number" min="1" value="${esc(s.recover_threshold||1)}"></div><div><label>单节点最多保留记录</label><input name="max_checks_per_proxy" type="number" min="1" value="${esc(s.max_checks_per_proxy||500)}"><p class="muted">每个节点只保留最近 N 条监测记录,避免日志无限增长。</p></div><div class="full toolbar"><button class="btn primary" type="submit">保存设置</button><button class="btn" type="button" onclick="closeDrawer()">取消</button></div></form>`); $('sf').onsubmit=saveSettings}
|
|
async function saveSettings(e){e.preventDefault();let fd=new FormData(e.target), o=Object.fromEntries(fd.entries()); ['target_port','interval_seconds','timeout_seconds','fail_threshold','recover_threshold','max_checks_per_proxy'].forEach(k=>o[k]=Number(o[k])); await api('/api/settings',{method:'PUT',body:JSON.stringify(o)}); closeDrawer(); toast('检测设置已保存'); await refresh()}
|
|
async function clearLogs(){if(confirm('确定清空所有监测记录、状态事件和通知日志?')){let r=await api('/api/logs',{method:'DELETE'});toast('日志已清空:'+JSON.stringify(r.removed));await refresh()}}
|
|
async function delProxy(id){if(confirm('删除节点 '+id+' ?')){await api('/api/proxies/'+encodeURIComponent(id),{method:'DELETE'});toast('已删除');refresh()}} async function checkProxy(id){await api('/api/check/'+encodeURIComponent(id),{method:'POST'});toast('检测完成');refresh()}
|
|
function channelFields(type,c={}){let x=cfg(c); if(type==='bark')return `<div class="type-fields full"><h3>Bark 配置</h3><label>Server</label><input name="server" value="${esc(x.server||'https://api.day.app')}"><label>Bark Key</label><input name="key" value="${esc(x.key||'')}"><label>Group</label><input name="group" value="${esc(x.group||'SOCKS5监控')}"></div>`; if(type==='pushplus')return `<div class="type-fields full"><h3>PushPlus 配置</h3><label>Token</label><input name="token" value="${esc(x.token||'')}"></div>`; if(type==='autman')return `<div class="type-fields full"><h3>Autman 配置</h3><p class="muted">发送到 Autman。</p><label>API URL</label><input name="url" placeholder="http://ip:port/m/push1" value="${esc(x.url||'')}"><label>Access Token</label><input name="access_token" value="${esc(x.access_token||'')}"></div>`; return `<div class="type-fields full"><h3>Webhook 配置</h3><label>Webhook URL</label><input name="url" value="${esc(x.url||'')}"><details><summary>高级 Webhook 设置</summary><label>Headers JSON</label><textarea name="headers">${esc(JSON.stringify(x.headers||{},null,2))}</textarea><label>Payload JSON</label><textarea name="payload">${esc(JSON.stringify(x.payload||{},null,2))}</textarea></details></div>`}
|
|
function openChannel(id){let c=id?(DATA.notifications.channels||[]).find(x=>x.id===id):{type:'bark',enabled:true,config:{server:'https://api.day.app',group:'SOCKS5监控'}}; editingChannel=id||null; draw(`<h2>${id?'编辑':'新增'}通知渠道</h2><form id="cf" class="form"><div><label>名称</label><input name="name" value="${esc(c.name||'')}"></div><div><label>类型</label><select id="channelType" name="type"><option value="bark">Bark</option><option value="pushplus">PushPlus</option><option value="autman">Autman</option><option value="webhook">Webhook</option></select></div><div><label>状态</label><select name="enabled"><option value="true">启用</option><option value="false">停用</option></select></div><div id="channelFields" class="full"></div><div class="full toolbar"><button class="btn primary" type="submit">保存</button><button class="btn" type="button" onclick="closeDrawer()">取消</button></div></form>`); $('channelType').value=c.type||'bark'; document.querySelector('[name=enabled]').value=String(c.enabled!==false); $('channelFields').outerHTML=channelFields($('channelType').value,c); $('channelType').onchange=()=>{document.querySelector('.type-fields').outerHTML=channelFields($('channelType').value,{type:$('channelType').value,config:{}})}; $('cf').onsubmit=saveChannel}
|
|
async function saveChannel(e){e.preventDefault();let fd=new FormData(e.target), type=fd.get('type'); let o={name:fd.get('name')||type,type,enabled:fd.get('enabled')==='true',config:{}}; if(type==='bark') o.config={server:fd.get('server')||'https://api.day.app',key:fd.get('key')||'',group:fd.get('group')||'SOCKS5监控'}; else if(type==='pushplus') o.config={token:fd.get('token')||'',template:'txt',topic:''}; else if(type==='autman') o.config={url:fd.get('url')||'',access_token:fd.get('access_token')||''}; else {try{o.config={url:fd.get('url')||'',headers:JSON.parse(fd.get('headers')||'{}'),payload:JSON.parse(fd.get('payload')||'{}')}}catch(e){toast('JSON 格式错误');return}} await api(editingChannel?'/api/channels/'+encodeURIComponent(editingChannel):'/api/channels',{method:editingChannel?'PUT':'POST',body:JSON.stringify(o)}); closeDrawer(); toast('渠道已保存'); refresh()}
|
|
async function delChannel(id){if(confirm('删除通知渠道 '+id+' ?')){await api('/api/channels/'+encodeURIComponent(id),{method:'DELETE'});toast('已删除');refresh()}} async function testChannel(id){let r=await api('/api/channels/'+encodeURIComponent(id)+'/test',{method:'POST'});toast('测试结果:'+(r.result.ok?'OK':'FAIL'))} async function testAllNotify(){await api('/api/notify/test',{method:'POST'});toast('已触发全部通知测试')}
|
|
refresh(); setInterval(refresh,10000)
|
|
</script></body></html>
|