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('
').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)}}