8dbf628dd6
- New: services/project_engine.py (async subprocess manager) - New: services/pyenv_manager.py (Python version + venv + git clone) - New: services/web_panel/routes/projects.py (5 REST endpoints) - New: static/web_panel/pages/projects.html (WebUI) - Tests: 28/28 passing, API verified (GET /api/projects) - Docs: Phase1_Progress.md (local)
119 lines
5.0 KiB
HTML
119 lines
5.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh">
|
|
<head><meta charset="UTF-8"><title>项目管理 - SenSu</title>
|
|
<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="tab active" onclick="showTab('running')">运行中</div>
|
|
<div class="tab" onclick="showTab('add')">+ 添加项目</div>
|
|
</div>
|
|
|
|
<div id="tab-running">
|
|
<div id="project-list">加载中...</div>
|
|
</div>
|
|
|
|
<div id="tab-add" style="display:none">
|
|
<div class="card">
|
|
<h3>添加新项目</h3>
|
|
<input id="proj-name" placeholder="项目名称" style="width:200px"><br>
|
|
<input id="proj-cmd" placeholder="启动命令 (如: python3 main.py)" style="width:400px"><br>
|
|
<input id="proj-cwd" placeholder="工作目录 (默认: .)" value="." style="width:200px"><br>
|
|
<input id="proj-port" placeholder="端口 (可选)" type="number" style="width:100px"><br>
|
|
<input id="proj-proxy" placeholder="代理路径 (可选, 如: /myapp)" style="width:200px"><br>
|
|
<button onclick="addProject()">启动项目</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>
|
|
|
|
<script>
|
|
let projects=[];
|
|
async function refresh(){
|
|
try{
|
|
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(){
|
|
const name=document.getElementById("proj-name").value;
|
|
const cmdStr=document.getElementById("proj-cmd").value;
|
|
if(!name||!cmdStr){alert("请填写名称和命令");return}
|
|
await fetch("/SenSu/api/projects/run",{
|
|
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(){
|
|
const url=document.getElementById("git-url").value;
|
|
const branch=document.getElementById("git-branch").value;
|
|
if(!url){alert("请填写 Git URL");return}
|
|
const name=url.split("/").pop().replace(".git","");
|
|
const resp=await fetch("/SenSu/api/projects/run",{
|
|
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){
|
|
await fetch(`/SenSu/api/projects/${name}/stop`,{method:"POST"});
|
|
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>
|
|
</body></html> |