diff --git a/internal/api/web/index.html b/internal/api/web/index.html index 012497b..420b397 100644 --- a/internal/api/web/index.html +++ b/internal/api/web/index.html @@ -48,12 +48,16 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot=""; async function api(url,opt={}){const r=await fetch(url,{credentials:"same-origin",cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()} function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}function attrJSON(v){return esc(JSON.stringify(v))} +function effectiveMask(f){if(!f)return me?.username?currentMask:(currentMask||"");return String(me?.username?f.mask:(f.guestMask??f.mask)||"")} +function can(action,f){const m=effectiveMask(f);if(action==="read"||action==="download"||action==="share")return m.includes("R");if(action==="write"||action==="rename"||action==="move"||action==="copy")return m.includes("W");if(action==="delete")return m.includes("D");return false} +function canTargetWrite(f){return !!f?.dir&&can("write",f)} +function canSelection(action){if(!me?.username||!selected.size)return false;const source=searchMode?searchItems:lastItems;return [...selected].every(p=>{const f=source.find(x=>(searchMode?searchPath(x):(current?current+"/":"")+x.name)===p);return can(action,f)})} 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||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;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} function showLoading(){const row='
'.repeat(6);$("files").innerHTML='
'+row+'
';$("grid").innerHTML='
'+row+'
'} function showState(title,text,type="error"){const icon=type==="lock"?'':type==="offline"?'':'';$("files").innerHTML='
'+icon+'

'+esc(title)+'

'+esc(text)+'

';$("grid").innerHTML='
'+icon+'

'+esc(title)+'

'+esc(text)+'

'} -async function refresh(){showLoading();try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);currentMask=data.currentMask||"";const writable=currentMask.includes("W");$("newFolderBtn").disabled=!writable;$("newTextBtn").disabled=!writable;$("uploadBtn").disabled=!writable;$("search").disabled=!data.currentMask?.includes("R");render(data.files||[]);loadSideRoots();renderBreadcrumb();$("notice").innerHTML=""}catch(e){$("newFolderBtn").disabled=true;$("newTextBtn").disabled=true;$("uploadBtn").disabled=true;$("search").disabled=true;const status=e.status||0;if(status===401){showState("需要登录","当前会话已失效,请重新登录后继续。","lock")}else if(status===403){showState("没有访问权限","当前账号无权访问这个位置,请返回其他可访问位置。","lock")}else if(status===404){showState("位置不存在","这个文件夹可能已经被移动、删除,或当前链接已经失效。")}else if(status===0){showState("连接失败","暂时无法连接到 CHFS,请检查网络后重试。","offline")}else{showState("无法加载","服务器暂时无法读取这个位置。","error")}$("notice").innerHTML=""}} +async function refresh(){showLoading();try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);currentMask=data.currentMask||"";const writable=can("write");$("newFolderBtn").disabled=!writable;$("newTextBtn").disabled=!writable;$("uploadBtn").disabled=!writable;$("search").disabled=!data.currentMask?.includes("R");render(data.files||[]);loadSideRoots();renderBreadcrumb();$("notice").innerHTML=""}catch(e){$("newFolderBtn").disabled=true;$("newTextBtn").disabled=true;$("uploadBtn").disabled=true;$("search").disabled=true;const status=e.status||0;if(status===401){showState("需要登录","当前会话已失效,请重新登录后继续。","lock")}else if(status===403){showState("没有访问权限","当前账号无权访问这个位置,请返回其他可访问位置。","lock")}else if(status===404){showState("位置不存在","这个文件夹可能已经被移动、删除,或当前链接已经失效。")}else if(status===0){showState("连接失败","暂时无法连接到 CHFS,请检查网络后重试。","offline")}else{showState("无法加载","服务器暂时无法读取这个位置。","error")}$("notice").innerHTML=""}} function fileType(f){const n=f.name.toLowerCase();if(f.dir)return "folder";if(/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(n))return "image";if(/\.(mp4|webm|mov|mkv)$/.test(n))return "video";if(/\.(mp3|wav|flac|m4a|ogg)$/.test(n))return "audio";if(/\.(pdf)$/.test(n))return "pdf";return "file"} function svgIcon(type){const p={folder:'',image:'',video:'',audio:'',pdf:'',archive:'',document:'',code:'',spreadsheet:'',presentation:'',file:''};return ''}function iconFor(f){const n=f.name.toLowerCase();const t=fileType(f);if(t!=="file")return svgIcon(t);if(/\.(zip|7z|rar|tar|gz|bz2|xz)$/.test(n))return svgIcon("archive");if(/\.(xls|xlsx|ods)$/.test(n))return svgIcon("spreadsheet");if(/\.(ppt|pptx|odp)$/.test(n))return svgIcon("presentation");if(/\.(doc|docx|odt|rtf)$/.test(n))return svgIcon("document");if(/\.(js|ts|jsx|tsx|css|scss|html|htm|go|rs|py|java|c|cpp|h|hpp|sh|sql|json|yaml|yml|xml)$/.test(n))return svgIcon("code");return svgIcon("file")} let selectionAnchor=null; @@ -66,23 +70,23 @@ function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}if( function renderSortState(){["name","size","modified"].forEach(k=>{const el=$("sort-"+k);if(el)el.textContent=sortKey===k?(sortDir===1?"↑":"↓"):""})} function emptyState(title,text,action=""){const writable=currentMask.includes("W");let buttons="";if(action==="folder"&&writable)buttons='
';if(action==="upload"&&writable)buttons='
';return '
'+svgIcon("folder")+'

'+esc(title)+'

'+esc(text)+'

'+buttons+'
'} function render(items){lastItems=sortItems(items);selected=new Set([...selected].filter(p=>lastItems.some(f=>(current?current+"/":"")+f.name===p)));renderList(lastItems);renderGrid(lastItems);updateSelection();renderSortState()} -function renderList(items){const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),canWrite=(f.mask||"").includes("W"),canDelete=(f.mask||"").includes("D"),selectedNow=selected.has(p),drag=' draggable="true"';const perms='';return '
'+iconFor(f)+''+esc(f.name)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+''+perms+'
'}).join(""):''+emptyState("这个文件夹还是空的","可以上传文件,或者创建一个新的文件夹。","upload")+''} +function renderList(items){const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=can("read",f),canWrite=can("write",f),canDelete=can("delete",f),selectedNow=selected.has(p),drag=' draggable="true"';const perms='';return '
'+iconFor(f)+''+esc(f.name)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+''+perms+'
'}).join(""):''+emptyState("这个文件夹还是空的","可以上传文件,或者创建一个新的文件夹。","upload")+''} function renderGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=current?current+"/"+f.name:f.name,canRead=(f.mask||"").includes("R"),type=fileType(f),selectedNow=selected.has(p),drag=' draggable="true"';let thumb=iconFor(f);if(type==="image"&&canRead)thumb='';const tileClass=(f.dir?'folder-tile ':'')+(['video','audio'].includes(type)?'media-tile':'');const meta=f.dir?'文件夹':fmt(f.size);return '
'+thumb+'
'+iconFor(f)+'
'+esc(f.name)+'
'+meta+''+(f.modified?''+esc(f.modified)+'':'')+'
'}).join(''):emptyState("这个文件夹还是空的","上传文件或创建内容后,它们会显示在这里。","upload")} function fileDragStart(e,p){const paths=selected.has(p)?[...selected]:[p];e.dataTransfer.effectAllowed="move";e.dataTransfer.setData("text/chfs-paths",JSON.stringify(paths));e.currentTarget.classList.add("dragging")} function fileDragEnd(e){e.currentTarget.classList.remove("dragging");document.querySelectorAll('.drop-target').forEach(el=>el.classList.remove('drop-target'))} -function folderDragOver(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;const f=lastItems.find(x=>(current?current+'/':'')+x.name===target);if(!f?.dir||!(f.mask||'').includes('W'))return;e.preventDefault();if(target===current||target===""||[...selected].some(p=>p===target||target.startsWith(p+'/')))return;e.dataTransfer.dropEffect="move";e.currentTarget.classList.add('drop-target')} +function folderDragOver(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;const f=lastItems.find(x=>(current?current+'/':'')+x.name===target);if(!canTargetWrite(f))return;e.preventDefault();if(target===current||target===""||[...selected].some(p=>p===target||target.startsWith(p+'/')))return;e.dataTransfer.dropEffect="move";e.currentTarget.classList.add('drop-target')} function folderDragLeave(e){e.currentTarget.classList.remove('drop-target')} async function folderDrop(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;e.preventDefault();e.stopPropagation();e.currentTarget.classList.remove('drop-target');let paths=[];try{paths=JSON.parse(e.dataTransfer.getData('text/chfs-paths')||'[]')}catch{};const f=lastItems.find(x=>(current?current+'/':'')+x.name===target);if(!f?.dir||!paths.length)return;if(paths.some(p=>p===target||target.startsWith(p+'/')))return fail('不能移动到自身或子目录');const base=target.replace(/^\/+|\/+$/g,'');await runOperation('移动 '+paths.length+' 项',async()=>{let done=0,failed=[],renamed=[];for(const source of paths){const name=source.split('/').pop(),destination=base?base+'/'+name:name;try{const actual=await transferWithConflict(source,destination,false);if(actual!==destination)renamed.push(name+' → '+actual.split('/').pop());done++}catch(err){failed.push(name+': '+err.message)}}selected.clear();await refresh();if(failed.length||renamed.length)show('

移动完成

'+esc('成功 '+done+' 项,失败 '+failed.length+' 项'+(renamed.length?'。\n\n自动重命名:\n'+renamed.join('\n'):'')+(failed.length?'。\n\n失败:\n'+failed.join('\n'):''))+'
');else toast('已移动 '+done+' 项')})} function handleItemClick(e,p){e.preventDefault();if(!me.username){guestItemActions(p);return}activateItem(p)} -function activateItem(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&(f.mask||"").includes("R"))openDir(p);else if((f.mask||"").includes("R"))previewFile(p)} +function activateItem(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&can("read",f))openDir(p);else if(can("read",f))previewFile(p)} function sharedURL(p){return new URL("/chfs/shared/"+p.split("/").map(encodeURIComponent).join("/"),location.href).href} -function guestItemActions(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f||!(f.mask||"").includes("R")){toast("当前项目没有访问权限",true);return}if(f.dir){openDir(p);return}const url=sharedURL(p);let html='

'+esc(f.name)+'

访客可以直接下载或复制分享链接。
';show(html)} +function guestItemActions(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f||!can("read",f)){toast("当前项目没有访问权限",true);return}if(f.dir){openDir(p);return}const url=sharedURL(p);let html='

'+esc(f.name)+'

访客可以直接下载或复制分享链接。
';show(html)} async function guestCopyShare(url){try{await navigator.clipboard.writeText(url);toast("分享链接已复制");closeModal()}catch{show('

分享链接

')}} function toggleTile(p){selectItem(p,!selected.has(p))} -function openSelected(){if(selected.size!==1)return;const p=[...selected][0];if(searchMode){const f=searchItems.find(x=>x.path===p);if(f)openSearchResult(f);return}const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&(f.mask||"").includes("R"))openDir(p);else if((f.mask||"").includes("R"))previewFile(p)} +function openSelected(){if(selected.size!==1)return;const p=[...selected][0];if(searchMode){const f=searchItems.find(x=>x.path===p);if(f)openSearchResult(f);return}const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!f)return;if(f.dir&&can("read",f))openDir(p);else if(can("read",f))previewFile(p)} function toggleAll(v){const source=searchMode?searchItems:lastItems;source.forEach(f=>{const p=searchMode?f.path:(current?current+"/":"")+f.name;if(v)selected.add(p);else selected.delete(p)});searchMode?updateSelection():render(lastItems)} function clearSelection(){selected.clear();updateSelection();if(searchMode){renderSearch(searchItems);return}renderGrid(lastItems);renderList(lastItems)} -function updateSelection(){const n=selected.size;$("bulk").classList.toggle("show",n>0);$("selectedCount").textContent=n?("已选择 "+n+" 项"):"";const source=searchMode?searchItems:lastItems;const header=$("files").closest("table")?.querySelector("thead .selectbox");if(header){const total=source.length;header.checked=total>0&&n===total;header.indeterminate=n>0&&n{const p=row.dataset.searchPath||row.dataset.filePath;row.classList.toggle("selected",!!p&&selected.has(p));const cb=row.querySelector(".selectbox");if(cb)cb.checked=!!p&&selected.has(p)});$("grid").querySelectorAll(".tile").forEach((el,i)=>{const f=source[i];if(f){const p=searchMode?f.path:(current?current+"/":"")+f.name,yes=selected.has(p);el.classList.toggle("selected",yes);const cb=el.querySelector(".selectbox");if(cb)cb.checked=yes}})} +function updateSelection(){const n=selected.size;$("bulk").classList.toggle("show",n>0&&!!me?.username);$("selectedCount").textContent=n?("已选择 "+n+" 项"):"";const bulkBtns=$("bulk")?.querySelectorAll("button");if(bulkBtns?.length){bulkBtns[0].hidden=!canSelection("move");bulkBtns[1].hidden=!canSelection("copy");bulkBtns[2].hidden=!canSelection("download");bulkBtns[3].hidden=!canSelection("delete");}const source=searchMode?searchItems:lastItems;const header=$("files").closest("table")?.querySelector("thead .selectbox");if(header){const total=source.length;header.checked=total>0&&n===total;header.indeterminate=n>0&&n{const p=row.dataset.searchPath||row.dataset.filePath;row.classList.toggle("selected",!!p&&selected.has(p));const cb=row.querySelector(".selectbox");if(cb)cb.checked=!!p&&selected.has(p)});$("grid").querySelectorAll(".tile").forEach((el,i)=>{const f=source[i];if(f){const p=searchMode?f.path:(current?current+"/":"")+f.name,yes=selected.has(p);el.classList.toggle("selected",yes);const cb=el.querySelector(".selectbox");if(cb)cb.checked=yes}})} function setView(v){localStorage.setItem('chfs-view',v);$("files").parentElement.hidden=v==='grid';$("grid").hidden=v!=='grid';document.querySelectorAll('.viewbtn').forEach(b=>b.classList.remove('active'));$(v+'Btn').classList.add('active')} function itemMenu(e,p){e.preventDefault();e.stopPropagation();if(!selected.has(p)){selected.clear();selected.add(p);updateSelection()}const f=lastItems.find(x=>(current?current+'/':'')+x.name===p);const guest=!me.username;if(guest){const canRead=(f?.guestMask||f?.mask||'').includes('R');let html='';if(canRead)html+='';if(f?.dir&&canRead)html+='';if(canRead)html+='';if(!html)html='
没有可用操作
';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+'px';$("context").style.top=Math.min(e.clientY,innerHeight-220)+'px';$("context").style.display='block';return}const canRead=(f?.mask||'').includes('R'),canWrite=(f?.mask||'').includes('W'),canDelete=(f?.mask||'').includes('D');let html='';if(canRead)html+='';if(f?.dir&&canRead)html+='';if(canWrite)html+='';if(canDelete)html+='';if(f?.guestMask?.includes('R'))html+='';if(!html)html='
没有可用操作
';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+'px';$("context").style.top=Math.min(e.clientY,innerHeight-220)+'px';$("context").style.display='block'} function closeContext(){$("context").style.display='none'} @@ -90,7 +94,7 @@ addEventListener('click',e=>{if(!e.target.closest('#context')&&!e.target.closest function bulkDownload(){if(!selected.size)return;const q=[...selected].map(p=>'path='+encodeURIComponent(p)).join('&');location.href='/chfs/downloadzip?'+q} let destinationResolve=null,destinationPath=""; async function chooseDestination(title){destinationPath=current;return await new Promise(async resolve=>{destinationResolve=resolve;try{const d=await api("/chfs/roots"),roots=d.roots||[];const writableCurrent=currentMask.includes("W");if(!writableCurrent){const first=roots.find(r=>String(r.mask||"").includes("W"));if(first)destinationPath=first.path||""}await renderDestinationPicker(title)}catch(e){destinationResolve=null;fail("无法打开目标目录:"+e.message);resolve(null)}})} -async function renderDestinationPicker(title){const data=await api("/chfs/files?filepath="+encodeURIComponent(destinationPath));const dirs=(data.files||[]).filter(f=>f.dir&&(f.mask||"").includes("W"));const parent=destinationPath?destinationPath.split("/").slice(0,-1).join("/"):null;let html='

'+esc(title)+'

当前目标:'+esc(destinationPath||"全部文件")+'
';if(parent!==null)html+='';for(const d of dirs){const p=destinationPath?(destinationPath+"/"+d.name):d.name;html+=''}if(!dirs.length)html+='
这里没有可写入的子目录。
';html+='
';show(html)} +async function renderDestinationPicker(title){const data=await api("/chfs/files?filepath="+encodeURIComponent(destinationPath));const dirs=(data.files||[]).filter(f=>canTargetWrite(f));const parent=destinationPath?destinationPath.split("/").slice(0,-1).join("/"):null;let html='

'+esc(title)+'

当前目标:'+esc(destinationPath||"全部文件")+'
';if(parent!==null)html+='';for(const d of dirs){const p=destinationPath?(destinationPath+"/"+d.name):d.name;html+=''}if(!dirs.length)html+='
这里没有可写入的子目录。
';html+='
';show(html)} function resolveDestination(path){const resolve=destinationResolve;destinationResolve=null;closeModal();if(resolve)resolve(path)} let operationBusy=false; async function runOperation(label,fn){if(operationBusy)return null;operationBusy=true;toast(label+"…");try{return await fn()}finally{operationBusy=false}} @@ -103,7 +107,7 @@ async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=awai async function bulkDelete(){if(!selected.size)return;const paths=[...selected];if(!await confirmBox("确认删除",paths.length===1?"确定删除 "+paths[0]+"?":"确定删除这 "+paths.length+" 项?删除文件夹时会同时删除其中内容。"))return;await runOperation("删除中",async()=>{try{const body=new URLSearchParams();paths.forEach(p=>body.append("filepath",p));await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body});selected.clear();await refresh();toast(paths.length===1?"已删除":"已删除 "+paths.length+" 项")}catch(e){fail("删除失败:"+e.message)}})} -async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>{const writable=String(r.mask||"").includes("W");return ''}).join('')||'
当前账号没有其他位置
'}catch(e){$("sideRoots").innerHTML='
位置加载失败
'}} +async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>{const writable=!!me?.username&&String(r.mask||"").includes("W");return ''}).join('')||'
当前账号没有其他位置
'}catch(e){$("sideRoots").innerHTML='
位置加载失败
'}} async function showRoots(){try{const d=await api("/chfs/roots");const roots=d.roots||[];let html='

选择位置

';for(const r of roots)html+='';if(!roots.length)html+='
没有可进入的共享位置
';html+='
';show(html)}catch(e){fail(e.message)}} function sideDragOver(e,target,writable){if(!e.dataTransfer?.types.includes('text/chfs-paths')||!writable)return;e.preventDefault();e.dataTransfer.dropEffect='move';e.currentTarget.classList.add('drop-target')} function sideDragLeave(e){e.currentTarget.classList.remove('drop-target')} @@ -113,16 +117,16 @@ function jumpRoot(p){current=p;navigationRoot=p;closeModal();refresh()} function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];const rootName=navigationRoot?(navigationRoot===me?.home?'我的空间':'当前位置'): '全部文件';let html='';let acc='';for(let i=0;i'+esc(parts[i])+''}el.innerHTML=html} function navigateTo(p){if(navigationRoot){if(p!==navigationRoot&&!p.startsWith(navigationRoot+'/'))return}else if(p!==''&&!me?.admin)return;current=p;refresh()} function openDir(p){current=p;refresh()}function goUp(){if(!current||current===navigationRoot)return;let p=current.split("/");p.pop();let n=p.join("/");if(navigationRoot&&!(n===navigationRoot||n.startsWith(navigationRoot+"/")))n=navigationRoot;current=n;refresh()} -function downloadFile(p){location.href=sharedURL(p)} -function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)} +function downloadFile(p){const f=searchMode?searchItems.find(x=>searchPath(x)===p):lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!can("download",f))return toast("当前项目没有下载权限",true);location.href=sharedURL(p)} +function downloadDir(p){const f=lastItems.find(x=>(current?current+"/":"")+x.name===p);if(!can("download",f))return toast("当前项目没有下载权限",true);location.href="/chfs/downloaddir/"+encodeURI(p)} async function shareItem(p){const url=sharedURL(p);try{await navigator.clipboard.writeText(url);toast("分享链接已复制")}catch{show("

分享链接

")}} async function renameItem(p){const name=p.split("/").pop(),n=await ask("重命名","新名称",name);if(!n||n===name)return;const dest=(p.split("/").slice(0,-1).concat(n)).join("/");await runOperation("正在重命名",async()=>{try{await api("/chfs/rename",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({old:p,new:dest})});selected.clear();await refresh();toast("已重命名")}catch(e){if(e.status===400){const retry=await ask("目标已存在","请输入新的名称",n);if(retry&&retry!==n){try{const retryDest=(p.split("/").slice(0,-1).concat(retry)).join("/");await api("/chfs/rename",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({old:p,new:retryDest})});selected.clear();await refresh();toast("已重命名")}catch(err){fail("重命名失败:"+err.message)}}}else fail("重命名失败:"+e.message)}})} async function removeItem(p){if(!await confirmBox("确认删除","确定删除 "+p+"?"))return;await runOperation("正在删除",async()=>{try{await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});selected.delete(p);await refresh();toast("已删除")}catch(e){fail("删除失败:"+e.message)}})} async function newFolder(){const n=await ask("新建文件夹","文件夹名称");if(!n)return;const p=current?current+"/"+n:n;toast("正在创建文件夹…");try{await api("/chfs/newdir",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});await refresh();toast("文件夹已创建")}catch(e){fail(e.message)}} async function newText(){const n=await ask("新建文本","文件名","new.txt");if(!n)return;const text=await ask("新建文本","文本内容");if(text===null)return;toast("正在创建文本…");try{await api("/chfs/newtext",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:current,title:n,text:text||""})});await refresh();toast("文本已创建")}catch(e){fail(e.message)}} $("uploadBtn").addEventListener("click",e=>{e.preventDefault();if($("uploadBtn").disabled){toast("当前位置没有上传权限",true);return}$("upload").click()});$("upload").addEventListener("change",async e=>{await uploadFiles(e.target.files);e.target.value=""}) -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){const tasks=entries.map((item,index)=>({id:"upload-"+Date.now()+"-"+index,file:item.file,relativePath:item.relativePath,state:"queued",xhr:null,row:null}));window._uploadTasks=tasks;show('
上传文件
共 '+tasks.length+' 个文件,最多同时上传 3 个。关闭窗口不会中断上传。
准备上传
');const box=$("uploadList");tasks.forEach(task=>{const row=document.createElement("div");row.className="upload-row";row.innerHTML='
'+esc(task.relativePath)+'
'+fmt(task.file.size)+' · 排队中
';row.querySelector('[data-upload-action="cancel"]').onclick=()=>cancelUploadTask(task);row.querySelector('[data-upload-action="retry"]').onclick=()=>retryUploadTask(task);task.row=row;box.appendChild(row)});await runUploadQueue(tasks,3);await refresh();const failed=tasks.filter(t=>t.state==="failed").length,cancelled=tasks.filter(t=>t.state==="cancelled").length,done=tasks.filter(t=>t.state==="done").length;if(failed||cancelled)toast("上传完成:成功 "+done+",失败 "+failed+",取消 "+cancelled,failed>0);else toast("上传完成,共 "+done+" 个文件")} +async function uploadFiles(files){if(!can("write")){toast("当前位置没有上传权限",true);return} const list=[...files].map(file=>({file,relativePath:file.webkitRelativePath||file.name}));if(!list.length)return;await uploadEntries(list)} +async function uploadEntries(entries){if(!can("write")){toast("当前位置没有上传权限",true);return} const tasks=entries.map((item,index)=>({id:"upload-"+Date.now()+"-"+index,file:item.file,relativePath:item.relativePath,state:"queued",xhr:null,row:null}));window._uploadTasks=tasks;show('
上传文件
共 '+tasks.length+' 个文件,最多同时上传 3 个。关闭窗口不会中断上传。
准备上传
');const box=$("uploadList");tasks.forEach(task=>{const row=document.createElement("div");row.className="upload-row";row.innerHTML='
'+esc(task.relativePath)+'
'+fmt(task.file.size)+' · 排队中
';row.querySelector('[data-upload-action="cancel"]').onclick=()=>cancelUploadTask(task);row.querySelector('[data-upload-action="retry"]').onclick=()=>retryUploadTask(task);task.row=row;box.appendChild(row)});await runUploadQueue(tasks,3);await refresh();const failed=tasks.filter(t=>t.state==="failed").length,cancelled=tasks.filter(t=>t.state==="cancelled").length,done=tasks.filter(t=>t.state==="done").length;if(failed||cancelled)toast("上传完成:成功 "+done+",失败 "+failed+",取消 "+cancelled,failed>0);else toast("上传完成,共 "+done+" 个文件")} async function runUploadQueue(tasks,limit){let cursor=0;async function worker(){while(cursorworker()));updateUploadSummary(tasks)} function setUploadTaskState(task,state){if(!task.row)return;const meta=task.row.querySelector('.meta'),cancel=task.row.querySelector('[data-upload-action="cancel"]'),retry=task.row.querySelector('[data-upload-action="retry"]');if(state==="uploading"){meta.textContent=fmt(task.file.size)+' · 上传中';cancel.hidden=false;retry.hidden=true}else if(state==="done"){meta.textContent=fmt(task.file.size)+' · 完成';cancel.hidden=true;retry.hidden=true}else if(state==="failed"){retry.hidden=false;cancel.hidden=true}else if(state==="cancelled"){meta.textContent=fmt(task.file.size)+' · 已取消';cancel.hidden=true;retry.hidden=false}else{meta.textContent=fmt(task.file.size)+' · 排队中';cancel.hidden=false;retry.hidden=true}} function updateUploadSummary(tasks){const el=$("uploadSummary");if(!el)return;const done=tasks.filter(t=>t.state==="done").length,failed=tasks.filter(t=>t.state==="failed").length,cancelled=tasks.filter(t=>t.state==="cancelled").length,active=tasks.filter(t=>t.state==="uploading").length,queued=tasks.filter(t=>t.state==="queued").length;el.querySelector('.count').textContent=done+failed+cancelled+' / '+tasks.length+' 个完成';el.querySelector('.meta').textContent=active?'正在上传 '+active+' 个'+(queued?',排队 '+queued+' 个':''):failed?'有 '+failed+' 个失败':cancelled?'有 '+cancelled+' 个已取消':'全部完成'} @@ -138,13 +142,13 @@ $("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const function searchPath(f){return f.path||f.name||""} function searchName(f){const p=searchPath(f);return f.name||p.split("/").pop()} function searchParent(f){const p=searchPath(f);return p.includes("/")?p.slice(0,p.lastIndexOf("/")):"全部文件"} -function searchCan(f,mask){return String(f.mask||"").includes(mask)} +function searchCan(f,mask){if(mask==="R")return can("read",f);if(mask==="W")return can("write",f);if(mask==="D")return can("delete",f);return false} function renderSearch(items){searchMode=true;searchItems=sortItems(items);selected=new Set([...selected].filter(p=>searchItems.some(f=>searchPath(f)===p)));$("crumb").textContent="搜索结果";$("files").parentElement.hidden=localStorage.getItem("chfs-view")==="grid";$("grid").hidden=localStorage.getItem("chfs-view")!=="grid";renderSearchList(searchItems);renderSearchGrid(searchItems);updateSelection();renderSortState()} function renderSearchList(items){const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D"),selectedNow=selected.has(p),perms='';return '
'+iconFor(f)+''+esc(name)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'
'+esc(searchParent(f))+'
'}).join(""):''+emptyState("没有找到匹配文件","换一个关键词试试,或者清空搜索框返回当前位置。")+''} function renderSearchGrid(items){$("grid").innerHTML=items.length?items.map(f=>{const p=searchPath(f),name=searchName(f),canRead=searchCan(f,"R"),type=fileType(f),selectedNow=selected.has(p);let thumb=iconFor(f);if(type==="image"&&canRead)thumb='';const tileClass=(f.dir?'folder-tile ':'')+(['video','audio'].includes(type)?'media-tile':'');return '
'+thumb+'
'+iconFor(f)+'
'+esc(name)+'
'+esc(searchParent(f)||"全部文件")+''+(f.modified?''+esc(f.modified)+'':'')+'
'}).join(""):'
'+svgIcon("file")+'

没有找到匹配文件

换一个关键词试试,或者清空搜索框返回当前位置。

'} function selectSearchItem(p,checked,e){if(checked){if(e?.shiftKey){const paths=searchItems.map(searchPath),anchor=selectionAnchor&&paths.includes(selectionAnchor)?selectionAnchor:[...selected][0],a=paths.indexOf(anchor),b=paths.indexOf(p);if(a>=0&&b>=0){const lo=Math.min(a,b),hi=Math.max(a,b);if(!(e.ctrlKey||e.metaKey))selected.clear();for(let i=lo;i<=hi;i++)selected.add(paths[i])}else selected.add(p)}else if(e?.ctrlKey||e?.metaKey)selected.add(p);else{selected.clear();selected.add(p)}selectionAnchor=p}else{selected.delete(p);selectionAnchor=p}updateSelection();renderSearch(searchItems)} function toggleSearchItem(p,checked,e){selectSearchItem(p,checked,e)} -function searchMenu(e,f){e.preventDefault();e.stopPropagation();const p=searchPath(f);if(!selected.has(p)){selected.clear();selected.add(p)}updateSelection();let html='';const canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D");if(canRead)html+='';if(f.dir&&canRead)html+='';if(canWrite)html+='';if(canDelete)html+='';if(f.guestMask?.includes("R"))html+='';if(!html)html='
没有可用操作
';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+"px";$("context").style.top=Math.min(e.clientY,innerHeight-220)+"px";$("context").style.display="block"} +function searchMenu(e,f){e.preventDefault();e.stopPropagation();const p=searchPath(f);if(!selected.has(p)){selected.clear();selected.add(p)}updateSelection();let html='';if(!me?.username){if(can("read",f))html+=''}else{const canRead=can("read",f),canWrite=can("write",f),canDelete=can("delete",f);if(canRead)html+='';if(f.dir&&canRead)html+='';if(canWrite)html+='';if(canDelete)html+='';if(f.guestMask?.includes("R"))html+=''}if(!html)html='
没有可用操作
';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+"px";$("context").style.top=Math.min(e.clientY,innerHeight-220)+"px";$("context").style.display="block"} function openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home&&f.path.startsWith(me.home)?me.home:f.path.split("/")[0]||"";searchMode=false;$("search").value="";selected.clear();refresh()}else previewFile(f.path)} let galleryFiles=[],galleryIndex=0,activeMedia=null; async function openGallery(p){const dir=dirOf(p);let items=dir===current?lastItems:null;if(!items){try{const d=await api("/chfs/files?filepath="+encodeURIComponent(dir));items=d.files||[]}catch(e){return fail("无法加载图片目录:"+e.message)}}galleryFiles=items.filter(f=>!f.dir&&fileType(f)==="image"&&(f.mask||"").includes("R")).map(f=>({...f,path:f.path||(dir?(dir+"/"+f.name):f.name)}));galleryIndex=Math.max(0,galleryFiles.findIndex(f=>f.path===p));renderGallery()} @@ -164,7 +168,7 @@ function editText(p,content){window._editorDirty=false;show('
';activeMedia=host.querySelector('audio');closeModal();activeMedia?.focus()} -async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().split('.').pop(),url='/chfs/preview?file='+encodeURIComponent(p),media=['mp4','webm','mov','mkv'].includes(ext),audio=['mp3','wav','flac','m4a','ogg'].includes(ext),image=['png','jpg','jpeg','gif','webp','bmp','svg'].includes(ext),pdf=ext==='pdf',text=['txt','md','markdown','json','yaml','yml','xml','csv','log','js','ts','jsx','tsx','css','scss','html','htm','go','rs','py','java','c','cpp','h','hpp','sh','sql','toml','ini','conf'].includes(ext);if(image){return openGallery(p)}let content='';if(media){show('
'+esc(n)+'
空格暂停 · ← / → 快进 5 秒
',true);activeMedia=$('.preview-stage video');activeMedia?.focus();return}if(audio){startMiniPlayer(url,n);return}if(pdf){show('
'+esc(n)+'
',true);return}if(text){try{const r=await fetch(url);if(!r.ok)throw Error();content=await r.text();const targetFile=searchMode?searchItems.find(f=>searchPath(f)===p):lastItems.find(f=>(current?current+'/':'')+f.name===p),canEdit=String(targetFile?.mask||currentMask).includes('W');show('
'+esc(n)+''+(canEdit?'':'')+'
'+esc(content)+'
',true);window._previewText=content}catch{show('
无法读取文本内容
')}}else show('
暂不支持该文件类型的在线预览
')} +async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().split('.').pop(),url='/chfs/preview?file='+encodeURIComponent(p),media=['mp4','webm','mov','mkv'].includes(ext),audio=['mp3','wav','flac','m4a','ogg'].includes(ext),image=['png','jpg','jpeg','gif','webp','bmp','svg'].includes(ext),pdf=ext==='pdf',text=['txt','md','markdown','json','yaml','yml','xml','csv','log','js','ts','jsx','tsx','css','scss','html','htm','go','rs','py','java','c','cpp','h','hpp','sh','sql','toml','ini','conf'].includes(ext);if(image){return openGallery(p)}let content='';if(media){show('
'+esc(n)+'
空格暂停 · ← / → 快进 5 秒
',true);activeMedia=$('.preview-stage video');activeMedia?.focus();return}if(audio){startMiniPlayer(url,n);return}if(pdf){show('
'+esc(n)+'
',true);return}if(text){try{const r=await fetch(url);if(!r.ok)throw Error();content=await r.text();const targetFile=searchMode?searchItems.find(f=>searchPath(f)===p):lastItems.find(f=>(current?current+'/':'')+f.name===p),canEdit=can("write",targetFile);show('
'+esc(n)+''+(canEdit?'':'')+'
'+esc(content)+'
',true);window._previewText=content}catch{show('
无法读取文本内容
')}}else show('
暂不支持该文件类型的在线预览
')} function changePassword(){show('

修改密码

')} async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}