From c29ba6eb406767307637faa6f3dea25f41f14245 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 22 Sep 2026 03:03:31 +0800 Subject: [PATCH] feat: add desktop-style multi-selection --- chfs-ng/internal/api/web/index.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index d6041bd..5b00ab2 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -56,15 +56,18 @@ function showState(title,text,type="error"){const icon=type==="lock"?'',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")} -function selectItem(p,checked){if(checked)selected.add(p);else selected.delete(p);updateSelection();renderList(lastItems)} +let selectionAnchor=null; +function itemPath(f){return current?current+"/"+f.name:f.name} +function selectablePaths(){return lastItems.map(itemPath)} +function selectItem(p,checked,e){if(checked){const paths=selectablePaths();if(e?.shiftKey&&selectionAnchor){const a=paths.indexOf(selectionAnchor),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()} let selected=new Set(),lastItems=[],sortKey="name",sortDir=1; function sortItems(items){return [...items].sort((a,b)=>{if(a.dir!==b.dir)return a.dir?-1:1;let av=sortKey==="size"?Number(a.size||0):sortKey==="modified"?String(a.modified||""):String(a.name||"").toLocaleLowerCase();let bv=sortKey==="size"?Number(b.size||0):sortKey==="modified"?String(b.modified||""):String(b.name||"").toLocaleLowerCase();return avbv?sortDir:0})} function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}render(lastItems)} 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 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 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 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;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')} @@ -120,7 +123,7 @@ function setupDropZone(){const z=$("dropZone"),main=$("fileCard");let depth=0;[d function uploadOne(file,row,relativePath=file.name,task=null){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();if(task)task.xhr=xhr;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=()=>{const ok=xhr.status>=200&&xhr.status<300;if(ok){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败'+(xhr.status?' · '+xhr.status:'');bar.style.width='0'};if(task)task.xhr=null;resolve(ok)};xhr.onerror=()=>{if(task&&task.state==='cancelled'){meta.textContent=fmt(file.size)+' · 已取消'}else{meta.textContent='网络错误';bar.style.width='0'};if(task)task.xhr=null;resolve(false)};xhr.onabort=()=>{if(task)task.xhr=null;resolve(false)};xhr.send(fd)})} let searchMode=false,searchItems=[]; $("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){searchMode=false;searchItems=[];refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){fail(x.message)}}) -function renderSearch(items){searchMode=true;searchItems=items;selected.clear();$("crumb").textContent="搜索结果";$("files").parentElement.hidden=false;$("grid").hidden=true;const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=f.path,name=f.name||p.split("/").pop(),parent=p.includes("/")?p.slice(0,p.lastIndexOf("/")):"当前位置";return '
'+iconFor(f)+''+esc(name)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'
'+esc(parent)+'
搜索结果
'}).join(""):''+emptyState("没有找到匹配文件","换一个关键词试试,或者清空搜索框返回当前位置。")+'';updateSelection()} +function renderSearch(items){searchMode=true;searchItems=items;selected.clear();$("crumb").textContent="搜索结果";$("files").parentElement.hidden=false;$("grid").hidden=true;const body=$("files");body.innerHTML=items.length?items.map(f=>{const p=f.path,name=f.name||p.split("/").pop(),parent=p.includes("/")?p.slice(0,p.lastIndexOf("/")):"当前位置";return '
'+iconFor(f)+''+esc(name)+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'
'+esc(parent)+'
搜索结果
'}).join(""):''+emptyState("没有找到匹配文件","换一个关键词试试,或者清空搜索框返回当前位置。")+'';updateSelection()} function selectSearchItem(p){selected.clear();if(p)selected.add(p);updateSelection()} function toggleSearchItem(p,checked){if(checked)selected.add(p);else selected.delete(p);updateSelection()} function searchMenu(e,f){e.preventDefault();selectSearchItem(f.path);let html='';html+='';if(f.dir)html+='';html+='';$("context").innerHTML=html;$("context").style.left=Math.min(e.clientX,innerWidth-190)+'px';$("context").style.top=Math.min(e.clientY,innerHeight-160)+'px';$("context").style.display='block'}