diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index e99eb4c..9dfe45b 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -81,10 +81,13 @@ async function removeItem(p){if(!await confirmBox("确认删除","确定删除 " async function newFolder(){const n=await ask("新建文件夹","文件夹名称");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=await ask("新建文本","文件名","new.txt");if(!n)return;const text=await ask("新建文本","文本内容");if(text===null)return;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=>{await uploadFiles(e.target.files);e.target.value=""}) -async function uploadFiles(files){const list=[...files];if(!list.length)return;show('

上传文件

');const box=$("uploadList");for(const file of list){const row=document.createElement("div");row.className="upload-row";row.innerHTML=''+esc(file.name)+''+fmt(file.size)+' · 准备上传
';box.appendChild(row);await uploadOne(file,row)}refresh()} +async function uploadFiles(files){const list=[...files].map(file=>({file,relativePath:file.webkitRelativePath||file.name}));if(!list.length)return;await uploadEntries(list)} +async function uploadEntries(entries){show('

上传文件

正在上传 '+entries.length+' 个文件;目录结构会按原路径保留。
');const box=$("uploadList");for(const item of entries){const row=document.createElement("div");row.className="upload-row";row.innerHTML=''+esc(item.relativePath)+''+fmt(item.file.size)+' · 准备上传
';box.appendChild(row);await uploadOne(item.file,row,item.relativePath)}refresh()} +async function collectDroppedEntries(items){const out=[];async function walk(entry,prefix=""){if(entry.isFile){await new Promise(resolve=>entry.file(file=>{out.push({file,relativePath:prefix+file.name});resolve()},()=>resolve()));return}if(entry.isDirectory){const reader=entry.createReader();const all=[];while(true){const batch=await new Promise(resolve=>reader.readEntries(resolve,()=>resolve([])));if(!batch.length)break;all.push(...batch)}for(const child of all)await walk(child,prefix+entry.name+'/')}}for(const item of items){const entry=item.webkitGetAsEntry?.();if(entry)await walk(entry);else{const file=item.getAsFile?.();if(file)out.push({file,relativePath:file.name})}}return out} + function showMobileTransfer(){const u=new URL(location.href);u.hash='folder='+encodeURIComponent(current);show('

手机传输

用手机扫描二维码打开当前文件夹。登录后即可按你的权限上传、下载和管理文件。
QR Code
')} -function setupDropZone(){const z=$("dropZone"),main=$("fileCard");let depth=0;[document,main].forEach(el=>{el.addEventListener('dragenter',e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth++;z.style.display='block';z.classList.add('drop-active')});el.addEventListener('dragover',e=>{if(e.dataTransfer?.types.includes('Files'))e.preventDefault()});el.addEventListener('dragleave',e=>{if(!e.dataTransfer?.types.includes('Files'))return;depth--;if(depth<=0){depth=0;z.classList.remove('drop-active')}});el.addEventListener('drop',async e=>{if(!e.dataTransfer?.files?.length)return;e.preventDefault();depth=0;z.classList.remove('drop-active');z.style.display='none';await uploadFiles(e.dataTransfer.files)})})} -function uploadOne(file,row){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();fd.append("folder",current);fd.append("file",file);const meta=row.querySelector('.meta'),bar=row.querySelector('i');xhr.open('POST','/chfs/upload');xhr.upload.onprogress=e=>{if(e.lengthComputable){const pct=Math.round(e.loaded/e.total*100);bar.style.width=pct+'%';meta.textContent=fmt(file.size)+' · '+pct+'%'}};xhr.onload=()=>{if(xhr.status>=200&&xhr.status<300){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败';bar.style.width='0'}};xhr.onerror=()=>{meta.textContent='网络错误';bar.style.width='0'};xhr.onloadend=resolve;xhr.send(fd)})} +function setupDropZone(){const z=$("dropZone"),main=$("fileCard");let depth=0;[document,main].forEach(el=>{el.addEventListener('dragenter',e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth++;z.style.display='block';z.classList.add('drop-active')});el.addEventListener('dragover',e=>{if(e.dataTransfer?.types.includes('Files'))e.preventDefault()});el.addEventListener('dragleave',e=>{if(!e.dataTransfer?.types.includes('Files'))return;depth--;if(depth<=0){depth=0;z.classList.remove('drop-active')}});el.addEventListener('drop',async e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth=0;z.classList.remove('drop-active');z.style.display='none';const entries=await collectDroppedEntries(e.dataTransfer.items||[]);if(entries.length)await uploadEntries(entries);else if(e.dataTransfer.files?.length)await uploadFiles(e.dataTransfer.files)})})} +function uploadOne(file,row,relativePath=file.name){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();const clean=String(relativePath||file.name).replace(/\\/g,'/').replace(/^\/+|\/+$/g,'');const parts=clean.split('/');const name=parts.pop()||file.name;const folder=parts.length?[current,...parts].filter(Boolean).join('/') : current;fd.append("folder",folder);fd.append("file",file,name);const meta=row.querySelector('.meta'),bar=row.querySelector('i');xhr.open('POST','/chfs/upload');xhr.upload.onprogress=e=>{if(e.lengthComputable){const pct=Math.round(e.loaded/e.total*100);bar.style.width=pct+'%';meta.textContent=fmt(file.size)+' · '+pct+'%'}};xhr.onload=()=>{if(xhr.status>=200&&xhr.status<300){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败';bar.style.width='0'}};xhr.onerror=()=>{meta.textContent='网络错误';bar.style.width='0'};xhr.onloadend=resolve;xhr.send(fd)})} $("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="搜索结果";$("files").parentElement.hidden=false;$("grid").hidden=true;const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=f.path,canRead=true;return '
'+iconFor(f)+' '+esc(f.path)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+''}).join(""): '没有找到匹配文件'} function openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home||"";refresh()}else previewFile(f.path)} @@ -104,8 +107,8 @@ async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{ async function resetUser(u){const p=await ask("重置密码","为 "+u+" 设置新密码","","password");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 editUser(name){const d=await api("/chfs/users");const u=d.users.find(x=>x.username===name);if(!u)return;window._editUser=u;window._editRules=[...(u.rules||[])];show('

'+esc(u.username)+'

主目录:'+esc(u.home)+'
主目录始终保留 RWD;下面的“共享目录”用于给用户开放其他位置,也可以单独设置拒绝规则。

共享目录

高级规则
可对已授权目录增加更细的拒绝规则,例如禁止访问 public/private。
');renderEditRules()} function isHomeRule(r){return String(r.dir||"").replace(/^\/+|\/+$/g,"")===String(window._editUser.home||"").replace(/^\/+|\/+$/g,"")} -function setRulePreset(level){const mask=["","r","rw","rwd"][level];$("rd").checked=level===0;$("rm").value=mask;$("rp").focus()} -function addRule(){const dir=$("rp").value.trim().replace(/^\/+|\/+$/g,"");if(!dir)return;const rule={dir,mask:$("rm").value};if($("rd").checked)rule.deny=true;if($("rc").checked)rule.childrenOnly=true;window._editRules=window._editRules.filter(r=>!(String(r.dir||"").replace(/^\/+|\/+$/g,"")===dir&&!isHomeRule(r)));window._editRules.push(rule);$("rp").value="";$("rd").checked=false;$("rc").checked=false;renderEditRules()} +function setRulePreset(level){const mask=["","r","rw","rwd"][level];window._rulePreset={mask,deny:level===0};$("rp").focus();$("rp").dataset.mask=mask;$("rp").dataset.deny=level===0?"1":"0"} +function addRule(){const dir=$("rp").value.trim().replace(/^\/+|\/+$/g,"");if(!dir)return;const preset=$("rp").dataset.mask||"r";const rule={dir,mask:preset};if($("rp").dataset.deny==="1")rule.deny=true;if($("rc").checked)rule.childrenOnly=true;window._editRules=window._editRules.filter(r=>!(String(r.dir||"").replace(/^\/+|\/+$/g,"")===dir&&!isHomeRule(r)));window._editRules.push(rule);$("rp").value="";delete $("rp").dataset.mask;delete $("rp").dataset.deny;$("rc").checked=false;renderEditRules()} function removeRule(i){if(i<0||isHomeRule(window._editRules[i]))return;window._editRules.splice(i,1);renderEditRules()} function renderEditRules(){const shared=window._editRules.filter(r=>!isHomeRule(r));$("sharedRules").innerHTML=shared.length?shared.map(r=>{const i=window._editRules.indexOf(r);const flags=(r.deny?"禁止":"允许")+" · "+(r.mask||"无权限")+(r.childrenOnly?" · 仅子项":"");return '
'+esc(r.dir)+'
'+esc(flags)+'
'}).join(""):'
还没有共享目录权限
';$("rules").innerHTML=window._editRules.map((r,i)=>isHomeRule(r)?'':'
'+esc(r.dir)+' · '+esc((r.deny?"拒绝":"允许")+" · "+(r.mask||"禁止")+(r.childrenOnly?" · 仅子项":""))+'
').join("")} async function saveUser(u){try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,enabled:$("ue").checked,rules:window._editRules})});showUsers()}catch(e){alert(e.message)}}