diff --git a/internal/api/web/index.html b/internal/api/web/index.html
index 1d8de45..8faa4d2 100644
--- a/internal/api/web/index.html
+++ b/internal/api/web/index.html
@@ -73,7 +73,7 @@ function fileDragEnd(e){e.currentTarget.classList.remove("dragging");document.qu
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 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){if(!me.username){guestItemActions(p);return}if(window.matchMedia&&window.matchMedia('(max-width:700px)').matches){activateItem(p);return}selectItem(p,true,e)}
+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 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)}
@@ -84,9 +84,9 @@ function toggleAll(v){const source=searchMode?searchItems:lastItems;source.forEa
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 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();if(!selected.has(p)){selected.clear();selected.add(p);updateSelection();renderList(lastItems);renderGrid(lastItems)}const f=lastItems.find(x=>(current?current+'/':'')+x.name===p);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+='下载 ZIP ';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 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 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+='下载 ZIP ';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'}
-addEventListener('click',e=>{if(!e.target.closest('#context')&&!e.target.closest('.tilemenu'))closeContext()});
+addEventListener('click',e=>{if(!e.target.closest('#context')&&!e.target.closest('.tile-action')&&!e.target.closest('.actions'))closeContext()});
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)}})}
@@ -122,7 +122,7 @@ async function newFolder(){const n=await ask("新建文件夹","文件夹名称"
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 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 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+' 个已取消':'全部完成'}
@@ -144,7 +144,7 @@ function renderSearchList(items){const body=$("files");body.innerHTML=items.leng
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();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+='下载 ZIP ';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='';const canRead=searchCan(f,"R"),canWrite=searchCan(f,"W"),canDelete=searchCan(f,"D");if(canRead)html+='下载 ';if(f.dir&&canRead)html+='下载 ZIP ';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()}