70 lines
11 KiB
HTML
70 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>__CHFS_TITLE__</title>
|
||
<style>
|
||
:root{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f5f7fb}
|
||
*{box-sizing:border-box}body{margin:0}.top{height:64px;background:#fff;border-bottom:1px solid #e5e9f2;display:flex;align-items:center;padding:0 22px;gap:18px;position:sticky;top:0;z-index:2}.brand{font-weight:750;font-size:20px}.path{color:#718096;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.spacer{flex:1}
|
||
button,input{font:inherit}button{border:0;border-radius:9px;padding:9px 13px;cursor:pointer;background:#2563eb;color:#fff}button.ghost{background:#eef2f7;color:#334155}button.danger{background:#dc2626}
|
||
main{max-width:1180px;margin:26px auto;padding:0 18px}.toolbar{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:18px}.toolbar input{min-width:220px;flex:1;padding:10px 12px;border:1px solid #d9dfeb;border-radius:9px;background:#fff}
|
||
.card{background:#fff;border:1px solid #e4e8f0;border-radius:14px;overflow:hidden;box-shadow:0 4px 18px rgba(20,30,50,.04)}
|
||
table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:13px 15px;border-bottom:1px solid #edf0f5}th{font-size:13px;color:#7b8798;background:#fafbfc}tr:last-child td{border-bottom:0}.name{display:flex;align-items:center;gap:10px}.name a{color:#172033;text-decoration:none;font-weight:550}.meta{color:#8792a5;font-size:13px}.actions{display:flex;gap:7px;justify-content:flex-end}.empty{padding:45px;text-align:center;color:#8a94a6}
|
||
.modal{position:fixed;inset:0;background:rgba(15,23,42,.4);display:none;align-items:center;justify-content:center;padding:18px;z-index:4}.modal.show{display:flex}.dialog{width:min(480px,100%);background:#fff;border-radius:15px;padding:22px}.dialog h2{margin-top:0}.field{display:grid;gap:7px;margin:13px 0}.field input{padding:10px 11px;border:1px solid #d7ddea;border-radius:9px}.row{display:flex;gap:9px;justify-content:flex-end;margin-top:18px}
|
||
.login{max-width:390px;margin:10vh auto}.notice{padding:12px 14px;background:#eff6ff;border-radius:10px;color:#315b9b;margin-bottom:14px}
|
||
@media(max-width:700px){th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){display:none}.top{padding:0 13px}.path{max-width:40vw}.actions button{padding:7px 9px}.toolbar input{min-width:160px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="top">
|
||
<div class="brand" id="brand">CHFS</div><div class="path" id="crumb">/</div><div class="spacer"></div>
|
||
<span id="who" class="meta"></span><button class="ghost" id="loginBtn" onclick="openLogin()">登录</button>
|
||
<button class="ghost" id="logoutBtn" onclick="logout()" hidden>退出</button>
|
||
</header>
|
||
<main>
|
||
<div id="serverNotice">__CHFS_NOTICE__</div><div id="notice"></div>
|
||
<div class="toolbar">
|
||
<button onclick="goUp()">上一级</button><button onclick="refresh()">刷新</button>
|
||
<button onclick="newFolder()">新建文件夹</button><button onclick="newText()">新建文本</button>
|
||
<label><button>上传</button><input id="upload" type="file" multiple hidden></label>
|
||
<input id="search" placeholder="搜索文件名,回车执行">
|
||
<button id="usersBtn" onclick="showUsers()" hidden>用户管理</button><button class="ghost" id="pwdBtn" onclick="changePassword()" hidden>修改密码</button>
|
||
</div>
|
||
<div class="card"><table><thead><tr><th>名称</th><th>大小</th><th>修改时间</th><th></th></tr></thead><tbody id="files"></tbody></table></div>
|
||
</main>
|
||
<div class="modal" id="modal"><div class="dialog" id="dialog"></div></div>
|
||
<script>
|
||
const $=id=>document.getElementById(id);let current="",me=null;
|
||
async function api(url,opt={}){const r=await fetch(url,opt);if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};throw Error(t)}return r.status===204?null:r.json()}
|
||
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}
|
||
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
|
||
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";await refresh()}
|
||
function updateUser(){$("who").textContent=me.username?me.username: "访客";$("loginBtn").hidden=!!me.username;$("logoutBtn").hidden=!me.username;$("usersBtn").hidden=!me.admin;$("pwdBtn").hidden=!me.username||!me.managed}
|
||
async function refresh(){try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);render(data.files||[]);$("crumb").textContent="/"+current;$("notice").innerHTML=""}catch(e){$("notice").innerHTML='<div class="notice">'+esc(e.message)+"</div>"}}
|
||
function render(items){const body=$("files");body.innerHTML="";if(!items.length){body.innerHTML='<tr><td colspan="4" class="empty">这里还没有文件</td></tr>';return}for(const f of items){const p=current?current+"/"+f.name:f.name;const row=document.createElement("tr");const icon=f.dir?"📁":"📄";row.innerHTML='<td><div class="name"><span>'+icon+'</span>'+ (f.dir?'<a href="#" onclick="openDir('+JSON.stringify(p)+');return false">'+esc(f.name)+'</a>':'<span>'+esc(f.name)+'</span>')+'</div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td><div class="actions">'+(f.dir?'<button class="ghost" onclick="downloadDir('+JSON.stringify(p)+')">ZIP</button>':'<button class="ghost" onclick="downloadFile('+JSON.stringify(p)+')">下载</button>')+'<button class="ghost" onclick="renameItem('+JSON.stringify(p)+')">改名</button><button class="danger" onclick="removeItem('+JSON.stringify(p)+')">删除</button></div></td>';body.appendChild(row)}}
|
||
function openDir(p){current=p;refresh()}function goUp(){if(!current)return;const root=me.home||"";if(current===root)return;let p=current.split("/");p.pop();let n=p.join("/");if(root&&!(n===root||n.startsWith(root+"/")))n=root;current=n;refresh()}
|
||
function downloadFile(p){location.href="/chfs/shared/"+encodeURIComponent(p).replace(/%2F/g,"/")}
|
||
function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)}
|
||
async function renameItem(p){const name=p.split("/").pop();const n=prompt("新名称",name);if(!n||n===name)return;const dest=(p.split("/").slice(0,-1).concat(n)).join("/");try{await api("/chfs/rename",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({old:p,new:dest})});refresh()}catch(e){alert(e.message)}}
|
||
async function removeItem(p){if(!confirm("确定删除 "+p+"?"))return;try{await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){alert(e.message)}}
|
||
async function newFolder(){const n=prompt("文件夹名称");if(!n)return;const p=current?current+"/"+n:n;try{await api("/chfs/newdir",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){alert(e.message)}}
|
||
async function newText(){const n=prompt("文件名","new.txt");if(!n)return;const text=prompt("文本内容","");try{await api("/chfs/newtext",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:current,title:n,text:text||""})});refresh()}catch(e){alert(e.message)}}
|
||
$("upload").addEventListener("change",async e=>{const fd=new FormData();fd.append("folder",current);for(const f of e.target.files)fd.append("file",f);try{await api("/chfs/upload",{method:"POST",body:fd});refresh()}catch(x){alert(x.message)}e.target.value=""})
|
||
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){alert(x.message)}})
|
||
function renderSearch(items){$("crumb").textContent="搜索结果";const body=$("files");body.innerHTML="";for(const f of items){const row=document.createElement("tr");row.innerHTML='<td><div class="name">🔎 <a href="#" onclick="openDir('+JSON.stringify(f.dir?f.path:f.path.split("/").slice(0,-1).join("/")).replace(/^"/,'\\"')+');return false">'+esc(f.path)+'</a></div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td></td>';body.appendChild(row)}}
|
||
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
|
||
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});alert("密码已更新");closeModal()}catch(e){alert(e.message)}}
|
||
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
|
||
async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){alert(e.message)}}
|
||
async function logout(){await fetch("/chfs/session",{method:"DELETE"});location.reload()}
|
||
function show(html){$("dialog").innerHTML=html;$("modal").classList.add("show")}function closeModal(){$("modal").classList.remove("show")}
|
||
$("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()})
|
||
async function showUsers(){try{const d=await api("/chfs/users");let html='<h2>用户管理</h2><div class="field"><input id="nu" placeholder="用户名"><input id="np" type="password" placeholder="初始密码"><button onclick="createUser()">创建用户</button></div><div>';for(const u of d.users)html+='<p><b>'+esc(u.username)+'</b> · '+esc(u.home||"")+(u.managed&&!u.admin?' <button class="ghost" onclick="resetUser('+JSON.stringify(u.username)+')">重置密码</button> <button class="danger" onclick="deleteUser('+JSON.stringify(u.username)+')">删除</button>':"")+"</p>";html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){alert(e.message)}}
|
||
async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$("nu").value,password:$("np").value})});showUsers()}catch(e){alert(e.message)}}
|
||
async function resetUser(u){const p=prompt("为 "+u+" 设置新密码");if(!p)return;try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});alert("密码已更新")}catch(e){alert(e.message)}}
|
||
async function deleteUser(u){if(!confirm("删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){alert(e.message)}}
|
||
init();
|
||
</script>
|
||
</body></html>
|