WebUI: MD3 projects/proxy pages, sidebar links, CSS animations
- Add projects + proxy nav items to sidebar - Rewrite projects.html with MD3 cards + tabs + mgmt-cards - Rewrite proxy.html with MD3 form + mgmt-cards - CSS: button ripple, card elevation, page fade, tab slide, input glow, nav dot, status pulse, log entry fade, shimmer, scrollbar, badge bounce, card hover mouse-glow (428 lines)
This commit is contained in:
@@ -331,3 +331,98 @@ h3{font-size:1rem;font-weight:500;letter-spacing:.15px}
|
|||||||
font-size:20px;transition:.2s;
|
font-size:20px;transition:.2s;
|
||||||
}
|
}
|
||||||
.theme-toggle:hover{background:rgba(208,188,255,.08)}
|
.theme-toggle:hover{background:rgba(208,188,255,.08)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MD3 Animations & Micro-interactions
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Button Ripple ── */
|
||||||
|
.btn{position:relative;overflow:hidden;transform:translateZ(0)}
|
||||||
|
.btn::after{
|
||||||
|
content:"";position:absolute;inset:0;background:radial-gradient(circle at center,currentColor 10%,transparent 10%);
|
||||||
|
background-size:0 0;background-repeat:no-repeat;opacity:0;transition:background-size .4s,opacity .3s;
|
||||||
|
}
|
||||||
|
.btn:active::after{background-size:300% 300%;opacity:.12;transition:0s}
|
||||||
|
|
||||||
|
/* ── Card Elevation Transition ── */
|
||||||
|
.card,.stat-card,.mgmt-card,.side-card{
|
||||||
|
transition:box-shadow .3s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1),background .3s;
|
||||||
|
}
|
||||||
|
.card:hover,.stat-card:hover,.mgmt-card:hover{transform:translateY(-1px)}
|
||||||
|
.card:active{transform:translateY(0)}
|
||||||
|
|
||||||
|
/* ── Page Transition Fade ── */
|
||||||
|
.page-container{animation:pageIn .25s cubic-bezier(.4,0,.2,1)}
|
||||||
|
@keyframes pageIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||||
|
|
||||||
|
/* ── Tab Indicator Slide ── */
|
||||||
|
.tab{position:relative;transition:color .2s,background .25s}
|
||||||
|
.tab.active::after{
|
||||||
|
content:"";position:absolute;bottom:0;left:25%;right:25%;height:2px;
|
||||||
|
background:var(--primary);border-radius:2px 2px 0 0;
|
||||||
|
animation:tabSlide .3s cubic-bezier(.4,0,.2,1)
|
||||||
|
}
|
||||||
|
@keyframes tabSlide{from{transform:scaleX(0)}to{transform:scaleX(1)}}
|
||||||
|
|
||||||
|
/* ── Input Focus Glow ── */
|
||||||
|
.input{transition:border-color .25s,box-shadow .25s,background .25s}
|
||||||
|
.input:focus{animation:inputGlow .6s ease-out}
|
||||||
|
@keyframes inputGlow{0%{box-shadow:0 0 0 0 rgba(208,188,255,.4)}100%{box-shadow:0 0 0 4px rgba(208,188,255,0)}}
|
||||||
|
|
||||||
|
/* ── Sidebar Collapse ── */
|
||||||
|
.sidebar{transition:width .3s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.nav-item .nav-label{transition:opacity .25s,width .3s;opacity:1}
|
||||||
|
.app-frame.collapsed .nav-label{opacity:0;width:0}
|
||||||
|
|
||||||
|
/* ── Nav Item Active Dot ── */
|
||||||
|
.nav-item.active::before{
|
||||||
|
content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
|
||||||
|
width:3px;height:20px;background:var(--primary);border-radius:0 3px 3px 0;
|
||||||
|
animation:navDotIn .3s cubic-bezier(.4,0,.2,1)
|
||||||
|
}
|
||||||
|
@keyframes navDotIn{from{height:0;opacity:0}to{height:20px;opacity:1}}
|
||||||
|
|
||||||
|
/* ── Status Dot Pulse ── */
|
||||||
|
.status-running{animation:statusPulse 2s ease-in-out infinite}
|
||||||
|
@keyframes statusPulse{0%,100%{box-shadow:0 0 4px rgba(158,206,106,.4)}50%{box-shadow:0 0 12px rgba(158,206,106,.7)}}
|
||||||
|
|
||||||
|
/* ── Loading Skeleton Shimmer ── */
|
||||||
|
.skeleton{background:linear-gradient(90deg,var(--bg-card) 25%,var(--bg-elevated) 50%,var(--bg-card) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--shape-xs);height:1rem}
|
||||||
|
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||||
|
|
||||||
|
/* ── Progress Bar ── */
|
||||||
|
.progress-bar{transition:width .3s cubic-bezier(.4,0,.2,1),opacity .2s}
|
||||||
|
|
||||||
|
/* ── Badge Bounce ── */
|
||||||
|
@keyframes badgeIn{0%{transform:scale(0);opacity:0}60%{transform:scale(1.2)}100%{transform:scale(1);opacity:1}}
|
||||||
|
.badge{animation:badgeIn .3s cubic-bezier(.4,0,.2,1)}
|
||||||
|
|
||||||
|
/* ── FAB Float ── */
|
||||||
|
.fab{transition:box-shadow .3s,transform .2s cubic-bezier(.4,0,.2,1),filter .2s}
|
||||||
|
.fab:hover{transform:scale(1.05)}
|
||||||
|
|
||||||
|
/* ── Log Entry Fade ── */
|
||||||
|
.log-entry{animation:logIn .2s ease-out}
|
||||||
|
@keyframes logIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
|
||||||
|
|
||||||
|
/* ── Toggle Theme Button Spin ── */
|
||||||
|
.theme-toggle{transition:transform .3s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.theme-toggle:hover{transform:rotate(30deg)}
|
||||||
|
|
||||||
|
/* ── Scrollbar MD3 Style ── */
|
||||||
|
::-webkit-scrollbar{width:6px}
|
||||||
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--outline);border-radius:3px}
|
||||||
|
::-webkit-scrollbar-thumb:hover{background:var(--text-dim)}
|
||||||
|
|
||||||
|
/* ── Ripple on Management Cards ── */
|
||||||
|
.mgmt-card,.plugin-card{position:relative;overflow:hidden}
|
||||||
|
.mgmt-card::before,.plugin-card::before{
|
||||||
|
content:"";position:absolute;inset:0;background:radial-gradient(circle at var(--mouse-x,50%) var(--mouse-y,50%),rgba(208,188,255,.1) 0%,transparent 60%);
|
||||||
|
opacity:0;transition:opacity .3s
|
||||||
|
}
|
||||||
|
.mgmt-card:hover::before,.plugin-card:hover::before{opacity:1}
|
||||||
|
|
||||||
|
/* ── Menu reveal animation for login error ── */
|
||||||
|
.err-msg{transition:opacity .3s}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
<svg viewBox="0 0 24 24"><path 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>
|
<svg viewBox="0 0 24 24"><path 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>
|
||||||
<span>插件管理</span>
|
<span>插件管理</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="nav-item" data-page="projects">
|
||||||
|
<svg viewBox="0 0 24 24"><path 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>
|
||||||
|
<span>项目管理</span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" data-page="proxy">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
|
||||||
|
<span>反向代理</span>
|
||||||
|
</a>
|
||||||
<div class="toggle-sidebar" onclick="toggleSidebar()">☰</div>
|
<div class="toggle-sidebar" onclick="toggleSidebar()">☰</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +1,60 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<div>
|
||||||
<head><meta charset="UTF-8"><title>项目管理 - SenSu</title>
|
<h2 style="margin-bottom:16px">📦 项目管理</h2>
|
||||||
<style>
|
|
||||||
body{font-family:monospace;background:#0e1416;color:#e0e3e4;margin:0;padding:16px}
|
|
||||||
h2{color:#00bcd4;border-bottom:1px solid #333;padding-bottom:8px}
|
|
||||||
.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0}
|
|
||||||
input,button,select{padding:6px 10px;margin:4px;border-radius:4px;border:1px solid #444;background:#222;color:#e0e3e4}
|
|
||||||
button{background:#00bcd4;color:#000;cursor:pointer;font-weight:bold}
|
|
||||||
button:hover{opacity:0.8}
|
|
||||||
button.danger{background:#ff5252}
|
|
||||||
.status-running{color:#4caf50}.status-stopped{color:#ff5252}.status-error{color:#ff9800}
|
|
||||||
.logs{background:#000;color:#0f0;padding:8px;border-radius:4px;max-height:300px;overflow-y:auto;font-size:12px}
|
|
||||||
.tabs{display:flex;gap:4px;margin-bottom:12px}
|
|
||||||
.tab{padding:6px 12px;background:#333;border-radius:4px 4px 0 0;cursor:pointer}
|
|
||||||
.tab.active{background:#00bcd4;color:#000}
|
|
||||||
</style></head>
|
|
||||||
<body>
|
|
||||||
<h2>📦 项目管理</h2>
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="showTab('running')">运行中</div>
|
<div class="tab active" onclick="showTab(event,'running')">运行中</div>
|
||||||
<div class="tab" onclick="showTab('add')">+ 添加项目</div>
|
<div class="tab" onclick="showTab(event,'add')">+ 添加项目</div>
|
||||||
|
<div class="tab" onclick="showTab(event,'git')">Git 部署</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-running">
|
<div id="tab-running">
|
||||||
<div id="project-list">加载中...</div>
|
<div id="project-list" style="display:flex;flex-direction:column;gap:12px">加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-add" style="display:none">
|
<div id="tab-add" style="display:none">
|
||||||
<div class="card">
|
<div class="card outlined" style="max-width:500px">
|
||||||
<h3>添加新项目</h3>
|
<h3 style="margin-bottom:12px">添加新项目</h3>
|
||||||
<input id="proj-name" placeholder="项目名称" style="width:200px"><br>
|
<div class="input-group"><span class="label">项目名称</span><input class="input" id="proj-name" placeholder="如: my-app"></div>
|
||||||
<input id="proj-cmd" placeholder="启动命令 (如: python3 main.py)" style="width:400px"><br>
|
<div class="input-group"><span class="label">启动命令</span><input class="input" id="proj-cmd" placeholder="如: python3 main.py"></div>
|
||||||
<input id="proj-cwd" placeholder="工作目录 (默认: .)" value="." style="width:200px"><br>
|
<div class="input-group"><span class="label">工作目录</span><input class="input" id="proj-cwd" value="."></div>
|
||||||
<input id="proj-port" placeholder="端口 (可选)" type="number" style="width:100px"><br>
|
<div class="input-group"><span class="label">端口 (可选)</span><input class="input" id="proj-port" type="number" placeholder="5000" style="width:120px"></div>
|
||||||
<input id="proj-proxy" placeholder="代理路径 (可选, 如: /myapp)" style="width:200px"><br>
|
<div class="input-group"><span class="label">代理路径 (可选)</span><input class="input" id="proj-proxy" placeholder="如: /myapp" style="width:200px"></div>
|
||||||
<button onclick="addProject()">启动项目</button>
|
<button class="btn btn-filled" onclick="addProject()" style="margin-top:8px">启动项目</button>
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>从 Git 部署</h3>
|
|
||||||
<input id="git-url" placeholder="Git URL" style="width:400px"><br>
|
|
||||||
<input id="git-branch" placeholder="分支 (默认: main)" value="main" style="width:200px"><br>
|
|
||||||
<button onclick="deployGit()">克隆并部署</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-git" style="display:none">
|
||||||
|
<div class="card outlined" style="max-width:500px">
|
||||||
|
<h3 style="margin-bottom:12px">从 Git 部署</h3>
|
||||||
|
<div class="input-group"><span class="label">Git URL</span><input class="input" id="git-url" placeholder="https://git.yeij.top/user/repo.git"></div>
|
||||||
|
<div class="input-group"><span class="label">分支</span><input class="input" id="git-branch" value="main" style="width:160px"></div>
|
||||||
|
<button class="btn btn-filled" onclick="deployGit()" style="margin-top:8px">克隆并部署</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let projects=[];
|
var projects=[];
|
||||||
|
function showTab(e,t){document.querySelectorAll("#tab-running,#tab-add,#tab-git").forEach(el=>el.style.display="none");document.getElementById("tab-"+t).style.display="block";document.querySelectorAll(".tab").forEach(el=>el.classList.remove("active"));e.target.classList.add("active")}
|
||||||
async function refresh(){
|
async function refresh(){
|
||||||
try{
|
try{var r=await fetch("/SenSu/api/projects");var d=await r.json();projects=d.projects;var h="";for(var p of projects){var cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";h+='<div class="mgmt-card"><div class="status-dot '+cls+'"></div><div class="info"><b>'+p.name+'</b><br><small>'+p.status+" PID:"+(p.pid||"-")+" :"+(p.port||"-")+" "+(p.uptime?p.uptime+"s":"")+(p.proxy?" → "+p.proxy:"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="stopP(this,'+JSON.stringify(p.name)+')">停止</button><button class="btn btn-sm btn-outlined" onclick="logsP(this,'+JSON.stringify(p.name)+')">日志</button><button class="btn btn-sm btn-text" onclick="cmdP(this,'+JSON.stringify(p.name)+')">命令</button></div></div><div class="log-box" id="logs-'+p.name+'"></div>'}document.getElementById("project-list").innerHTML=h||"暂无项目"}catch(e){console.error(e)}
|
||||||
const r=await fetch("/SenSu/api/projects");
|
|
||||||
const d=await r.json();
|
|
||||||
projects=d.projects;
|
|
||||||
let h="";
|
|
||||||
for(const p of projects){
|
|
||||||
const cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";
|
|
||||||
h+=`<div class="card">
|
|
||||||
<b>${p.name}</b> <span class="${cls}">● ${p.status}</span>
|
|
||||||
${p.pid?` PID:${p.pid}`:""} ${p.port?` :${p.port}`:""} ${p.uptime?` ${p.uptime}s`:""}
|
|
||||||
${p.proxy?` → ${p.proxy}`:""}
|
|
||||||
<button class="danger" onclick="stopProject('${p.name}')">停止</button>
|
|
||||||
<button onclick="viewLogs('${p.name}')">日志</button>
|
|
||||||
<button onclick="sendCmd('${p.name}')">发送命令</button>
|
|
||||||
<div id="logs-${p.name}" class="logs" style="display:none"></div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
document.getElementById("project-list").innerHTML=h||"暂无项目";
|
|
||||||
}catch(e){console.error(e)}
|
|
||||||
}
|
}
|
||||||
async function addProject(){
|
async function addProject(){
|
||||||
const name=document.getElementById("proj-name").value;
|
var n=document.getElementById("proj-name").value,cmd=document.getElementById("proj-cmd").value;
|
||||||
const cmdStr=document.getElementById("proj-cmd").value;
|
if(!n||!cmd){alert("请填写名称和命令");return}
|
||||||
if(!name||!cmdStr){alert("请填写名称和命令");return}
|
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:cmd.split(" "),cwd:document.getElementById("proj-cwd").value,port:parseInt(document.getElementById("proj-port").value)||0,proxy_path:document.getElementById("proj-proxy").value})});
|
||||||
await fetch("/SenSu/api/projects/run",{
|
refresh()
|
||||||
method:"POST",headers:{"Content-Type":"application/json"},
|
|
||||||
body:JSON.stringify({
|
|
||||||
name,cwd:document.getElementById("proj-cwd").value,
|
|
||||||
cmd:cmdStr.split(" "),
|
|
||||||
port:parseInt(document.getElementById("proj-port").value)||0,
|
|
||||||
proxy_path:document.getElementById("proj-proxy").value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
refresh();
|
|
||||||
}
|
}
|
||||||
async function deployGit(){
|
async function deployGit(){
|
||||||
const url=document.getElementById("git-url").value;
|
var u=document.getElementById("git-url").value,b=document.getElementById("git-branch").value;
|
||||||
const branch=document.getElementById("git-branch").value;
|
if(!u){alert("请填写 Git URL");return}
|
||||||
if(!url){alert("请填写 Git URL");return}
|
var n=u.split("/").pop().replace(".git","");
|
||||||
const name=url.split("/").pop().replace(".git","");
|
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:["git","clone","-b",b,u,n],cwd:"data/projects"})});
|
||||||
const resp=await fetch("/SenSu/api/projects/run",{
|
refresh()
|
||||||
method:"POST",headers:{"Content-Type":"application/json"},
|
|
||||||
body:JSON.stringify({name,cmd:["git","clone","-b",branch,url,name],cwd:"data/projects"})
|
|
||||||
});
|
|
||||||
refresh();
|
|
||||||
}
|
}
|
||||||
async function stopProject(name){
|
async function stopP(el,n){await fetch("/SenSu/api/projects/"+n+"/stop",{method:"POST"});refresh()}
|
||||||
await fetch(`/SenSu/api/projects/${name}/stop`,{method:"POST"});
|
async function logsP(el,n){var b=document.getElementById("logs-"+n);b.style.display=b.style.display==="none"?"block":"none";if(b.style.display==="block"){var r=await fetch("/SenSu/api/projects/"+n+"/logs?tail=30");var d=await r.json();b.innerHTML=d.logs.join("<br>")||"(无日志)"}}
|
||||||
refresh();
|
async function cmdP(el,n){var t=prompt("命令: "+n);if(t)await fetch("/SenSu/api/projects/"+n+"/stdin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})})}
|
||||||
}
|
setInterval(refresh,4000);refresh();
|
||||||
async function viewLogs(name){
|
|
||||||
const el=document.getElementById("logs-"+name);
|
|
||||||
el.style.display=el.style.display==="none"?"block":"none";
|
|
||||||
if(el.style.display==="block"){
|
|
||||||
const r=await fetch(`/SenSu/api/projects/${name}/logs?tail=30`);
|
|
||||||
const d=await r.json();
|
|
||||||
el.innerHTML=d.logs.join("\n")||"(无日志)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function sendCmd(name){
|
|
||||||
const text=prompt("发送命令到 "+name+":");
|
|
||||||
if(text) await fetch(`/SenSu/api/projects/${name}/stdin`,{
|
|
||||||
method:"POST",headers:{"Content-Type":"application/json"},
|
|
||||||
body:JSON.stringify({text})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function showTab(t){document.querySelectorAll("#tab-running,#tab-add").forEach(e=>e.style.display="none");document.getElementById("tab-"+t).style.display="block"}
|
|
||||||
setInterval(refresh,3000);refresh();
|
|
||||||
</script>
|
</script>
|
||||||
</body></html>
|
|
||||||
@@ -1,19 +1,32 @@
|
|||||||
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>反向代理 - SenSu</title>
|
<div>
|
||||||
<style>body{font-family:monospace;background:#0e1416;color:#e0e3e4;margin:0;padding:16px}
|
<h2 style="margin-bottom:16px">🔀 反向代理</h2>
|
||||||
h2{color:#00bcd4;border-bottom:1px solid #333;padding-bottom:8px}
|
|
||||||
.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0}
|
<div class="card outlined" style="max-width:520px;margin-bottom:16px">
|
||||||
input,button{padding:6px 10px;margin:4px;border-radius:4px;border:1px solid #444;background:#222;color:#e0e3e4}
|
<h3 style="margin-bottom:12px">添加代理</h3>
|
||||||
button{background:#00bcd4;color:#000;cursor:pointer;font-weight:bold}
|
<div class="input-group"><span class="label">框架路径</span><input class="input" id="px-path" placeholder="/app"></div>
|
||||||
button.danger{background:#ff5252}.status-active{color:#4caf50}
|
<div class="input-group"><span class="label">目标 URL</span><input class="input" id="px-target" placeholder="http://192.168.1.5:8080"></div>
|
||||||
</style></head><body>
|
<div class="input-group"><span class="label">描述 (可选)</span><input class="input" id="px-desc" placeholder="我的应用"></div>
|
||||||
<h2>🔀 反向代理</h2>
|
<button class="btn btn-filled" onclick="addP()">添加代理</button>
|
||||||
<div class="card"><h3>添加代理</h3>
|
</div>
|
||||||
<input id="px-path" placeholder="路径 (如: /app)" style="width:200px"><br>
|
|
||||||
<input id="px-target" placeholder="目标 (如: http://192.168.1.5:8080)" style="width:400px"><br>
|
<div id="proxy-list" style="display:flex;flex-direction:column;gap:12px">加载中...</div>
|
||||||
<input id="px-desc" placeholder="描述" style="width:300px"><br>
|
</div>
|
||||||
<button onclick="add()">添加</button></div>
|
|
||||||
<div id="list">加载中...</div>
|
|
||||||
<script>
|
<script>
|
||||||
async function r(){const d=await(await fetch("/SenSu/api/proxy")).json();let h="";for(const p of d.proxies||[]){h+=`<div class="card"><b>${p.path}</b> <span class="status-active">● ${p.status}</span><br>→ ${p.target} ${p.description||""}<button class="danger" onclick="del('${p.path}')">删除</button></div>`}document.getElementById("list").innerHTML=h||"暂无代理"}
|
function refresh(){
|
||||||
async function add(){await fetch("/SenSu/api/proxy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:document.getElementById("px-path").value,target:document.getElementById("px-target").value,description:document.getElementById("px-desc").value})});r()}
|
fetch("/SenSu/api/proxy").then(r=>r.json()).then(d=>{
|
||||||
async function del(p){await fetch(`/SenSu/api/proxy/${encodeURIComponent(p)}`,{method:"DELETE"});r()}r()</script></body></html>
|
var h="";
|
||||||
|
for(var p of d.proxies||[]){
|
||||||
|
h+='<div class="mgmt-card"><div class="status-dot status-running"></div><div class="info"><b>'+p.path+'</b><br><small>→ '+p.target+(p.description?" ("+p.description+")":"")+(p.external?" 🌐":"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="delP(\"'+p.path+'\")">删除</button></div></div>';
|
||||||
|
}
|
||||||
|
document.getElementById("proxy-list").innerHTML=h||"暂无代理"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function addP(){
|
||||||
|
var p=document.getElementById("px-path").value,t=document.getElementById("px-target").value;
|
||||||
|
if(!p||!t){alert("请填写");return}
|
||||||
|
fetch("/SenSu/api/proxy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:p,target:t,description:document.getElementById("px-desc").value})}).then(r=>r.json()).then(d=>{if(d.ok)refresh()})
|
||||||
|
}
|
||||||
|
function delP(path){fetch("/SenSu/api/proxy/"+encodeURIComponent(path),{method:"DELETE"}).then(()=>refresh())}
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user