feat: refine minimal line icon file UI

This commit is contained in:
OpenClaw Agent
2026-09-22 02:29:31 +08:00
parent ba41474f0f
commit e3c7d8ecab
2 changed files with 15 additions and 9 deletions
+7 -1
View File
@@ -359,7 +359,7 @@ func (s *Server) transfer(w http.ResponseWriter, r *http.Request, copyOnly bool)
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
return
}
if !a.MaskFor(src).HasRead() || !a.MaskFor(dst).HasWrite() || !a.MaskFor(path.Dir(dst)).HasWrite() {
if !a.MaskFor(src).HasRead() || (!copyOnly && !a.MaskFor(src).HasDelete()) || !a.MaskFor(dst).HasWrite() || !a.MaskFor(path.Dir(dst)).HasWrite() {
writeJSONError(w, http.StatusForbidden, "has no privilege")
return
}
@@ -384,6 +384,12 @@ func (s *Server) transfer(w http.ResponseWriter, r *http.Request, copyOnly bool)
writeJSONError(w, http.StatusForbidden, err.Error())
return
}
if !copyOnly {
if err = checkDeleteTree(s.Store, a, src); err != nil {
writeJSONError(w, http.StatusForbidden, err.Error())
return
}
}
}
if copyOnly {
err = s.Store.Copy(src, dst)
+8 -8
View File
@@ -20,7 +20,7 @@ html.dark{--bg:#0f141c;--surface:#171e28;--surface2:#202936;--text:#e7edf7;--mut
body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}.top,.card,.dialog{background:var(--surface);border-color:var(--border)}
button.ghost{background:var(--surface2);color:var(--text)}button.danger{background:var(--danger)}.meta,.path{color:var(--muted)}
.card{box-shadow:var(--shadow)}th{background:var(--surface2);color:var(--muted)}th,td{border-color:var(--border)}.field input,.toolbar input,.field select{background:var(--surface);color:var(--text);border-color:var(--border)}
.iconbtn{width:38px;height:38px;padding:0;background:var(--surface2);color:var(--text)}.viewbtn.active{background:var(--primary);color:#fff}.selectbox{width:18px;height:18px;accent-color:var(--primary)}
.iconbtn{width:38px;height:38px;padding:0;background:var(--surface2);color:var(--text);display:inline-flex;align-items:center;justify-content:center}.ui-icon{width:18px;height:18px;display:block;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.ui-icon.fill{fill:currentColor;stroke:none}.viewbtn.active{background:var(--primary);color:#fff}.selectbox{width:18px;height:18px;accent-color:var(--primary)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;padding:14px}.tile{position:relative;border:1px solid var(--border);border-radius:12px;padding:14px;background:var(--surface);min-height:145px;cursor:pointer}.tile:hover{border-color:var(--primary)}.tile.selected{outline:2px solid var(--primary)}.tile .thumb{height:82px;display:flex;align-items:center;justify-content:center;font-size:42px;border-radius:9px;background:var(--surface2);overflow:hidden;margin-bottom:9px}.tile .thumb img{width:100%;height:100%;object-fit:cover}.progress{height:8px;background:var(--surface2);border-radius:99px;overflow:hidden}.progress i{display:block;height:100%;width:0;background:var(--primary);transition:width .15s}.upload-row{display:grid;gap:7px;padding:10px 0;border-bottom:1px solid var(--border)}.drop-active{border-color:var(--primary)!important;background:var(--surface2)!important}.qr{width:min(320px,80vw);aspect-ratio:1;margin:auto;display:block;border-radius:12px;background:#fff}.drop-hint{color:var(--muted);font-size:13px;margin-top:7px}.tile .tilecheck{position:absolute;top:9px;left:9px}.tile .tilemenu{position:absolute;right:7px;top:7px}.file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bulk{display:none;align-items:center;gap:8px;margin-bottom:12px}.bulk.show{display:flex}.bulk .count{font-weight:650;margin-right:auto}.layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:16px}.sidebar{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px;height:max-content;position:sticky;top:82px}.side-title{font-size:12px;font-weight:700;color:var(--muted);padding:6px 9px}.side-item{width:100%;text-align:left;background:transparent;color:var(--text);margin:2px 0}.side-item.active{background:var(--surface2);color:var(--primary)}@media(max-width:900px){.layout{display:block}.sidebar{position:static;margin-bottom:14px;display:flex;gap:6px;overflow:auto}.side-title{display:none}.side-item{white-space:nowrap;width:auto}}.toast-host{position:fixed;right:18px;bottom:18px;z-index:20;display:grid;gap:8px}.toast{min-width:240px;max-width:min(420px,calc(100vw - 36px));padding:11px 14px;border:1px solid var(--border);border-radius:11px;background:var(--surface);box-shadow:var(--shadow);color:var(--text);animation:toastIn .18s ease}@keyframes toastIn{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}.toast.error{border-color:var(--danger)}.context{position:fixed;z-index:10;display:none;min-width:170px;background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow);border-radius:10px;padding:5px}.context button{display:block;width:100%;text-align:left;background:transparent;color:var(--text);border-radius:7px}.context button:hover{background:var(--surface2)}
</style>
</head>
@@ -39,7 +39,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
<button id="newFolderBtn" onclick="newFolder()">新建文件夹</button><button id="newTextBtn" onclick="newText()">新建文本</button>
<label><button id="uploadBtn">上传</button><button class="ghost" onclick="showMobileTransfer()">手机传输</button><input id="upload" type="file" multiple hidden></label>
<input id="search" placeholder="搜索文件名,回车执行">
<button id="usersBtn" onclick="showUsers()" hidden>用户管理</button><button class="ghost" id="pwdBtn" onclick="changePassword()" hidden>修改密码</button><button class="iconbtn" id="themeBtn" onclick="toggleTheme()" title="主题">◐</button><button class="iconbtn viewbtn active" id="listBtn" onclick="setView("list")" title="列表">☷</button><button class="iconbtn viewbtn" id="gridBtn" onclick="setView("grid")" title="网格">▦</button>
<button id="usersBtn" onclick="showUsers()" hidden>用户管理</button><button class="ghost" id="pwdBtn" onclick="changePassword()" hidden>修改密码</button><button class="iconbtn" id="themeBtn" onclick="toggleTheme()" title="主题"><span id="themeIcon"></span></button><button class="iconbtn viewbtn active" id="listBtn" onclick="setView("list")" title="列表"><span id="listIcon"></span></button><button class="iconbtn viewbtn" id="gridBtn" onclick="setView("grid")" title="网格"><span id="gridIcon"></span></button>
</div>
<div id="bulk" class="bulk"><span id="selectedCount" class="count"></span><button class="ghost" onclick="bulkMove()">移动</button><button class="ghost" onclick="bulkCopy()">复制</button><button class="ghost" onclick="bulkDownload()">下载 ZIP</button><button class="danger" onclick="bulkDelete()">删除</button><button class="ghost" onclick="clearSelection()">取消选择</button></div><div class="card" id="fileCard"><table><thead><tr><th><input class="selectbox" type="checkbox" onchange="toggleAll(this.checked)"></th><th>名称</th><th>大小</th><th>修改时间</th><th></th></tr></thead><tbody id="files"></tbody></table><div id="grid" class="grid" hidden></div></div></section></div>
</main><div id="context" class="context"></div><div id="toastHost" class="toast-host"></div>
@@ -53,7 +53,7 @@ async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:""
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}
async function refresh(){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){$("notice").innerHTML='<div class="notice">'+esc(e.message)+"</div>"}}
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 iconFor(f){return {folder:"📁",image:"🖼️",video:"🎬",audio:"🎵",pdf:"📕",file:"📄"}[fileType(f)]}
function svgIcon(type){const p={folder:'<path d="M3 6.5h6l2 2H21v9.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',image:'<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8" cy="9" r="1.4"/><path d="m4 17 5-5 4 4 2-2 5 5"/>',video:'<rect x="3" y="5" width="13" height="14" rx="2"/><path d="m16 10 5-3v10l-5-3z"/>',audio:'<path d="M9 18V6l9-2v12"/><circle cx="6" cy="18" r="3"/><circle cx="15" cy="16" r="3"/>',pdf:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5M8 16h8M8 12h6"/>',archive:'<path d="M7 3h10v18H7z"/><path d="M10 3v3h4V3M10 8h4v3h-4v3h4v3h-4"/>',document:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5M9 12h6M9 16h6"/>',code:'<path d="m9 7-5 5 5 5M15 7l5 5-5 5M13 4l-2 16"/>',spreadsheet:'<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M4 8h16M10 8v13M15 8v13M4 13h16M4 17h16"/>',presentation:'<rect x="3" y="4" width="18" height="14" rx="2"/><path d="M12 18v3M8 21h8M7 14l3-4 3 2 4-5"/>',file:'<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v5h5"/>'};return '<svg class="ui-icon" viewBox="0 0 24 24" aria-hidden="true">'+(p[type]||p.file)+'</svg>'}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()}
let selected=new Set(),lastItems=[];
function render(items){lastItems=items;selected=new Set([...selected].filter(p=>items.some(f=>(current?current+"/":"")+f.name===p)));renderList(items);renderGrid(items);updateSelection()}
@@ -75,11 +75,11 @@ async function bulkCopy(){await bulkTransfer(true)}
async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=await chooseDestination(copyOnly?"复制选中项目到":"移动选中项目到");if(dstDir===null)return;const base=String(dstDir).replace(/^\/+|\/+$/g,"");const failed=[];for(const source of [...selected]){const name=source.split("/").pop(),destination=base?(base+"/"+name):name;try{await api(copyOnly?"/chfs/copy":"/chfs/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({source,destination})})}catch(e){failed.push(name+": "+e.message)}}selected.clear();await refresh();if(failed.length)show('<h2>部分操作失败</h2><div class="notice">'+esc(failed.join("\n"))+'</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}
async function bulkDelete(){if(!selected.size)return;if(!await confirmBox("批量删除","确定删除已选择的 "+selected.size+" 项?"))return;for(const p of [...selected]){try{await api('/chfs/rmfiles',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath:p})})}catch(e){fail(p+': '+e.message)}}selected.clear();refresh()}
async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'<button class="side-item '+(r.path===current?'active':'')+'" onclick="jumpRoot('+JSON.stringify(r.path)+')">'+(r.home?'👤':'📂')+' '+esc(r.name)+'</button>').join('')||'<div class="meta" style="padding:8px">没有可进入的位置</div>'}catch{}}
async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'<button class="side-item '+(r.path===current?'active':'')+'" onclick="jumpRoot('+JSON.stringify(r.path)+')">'+svgIcon(r.home?'folder':'folder')+' '+esc(r.name)+'</button>').join('')||'<div class="meta" style="padding:8px">没有可进入的位置</div>'}catch{}}
async function showRoots(){try{const d=await api("/chfs/roots");const roots=d.roots||[];let html='<h2>选择位置</h2><div>';for(const r of roots)html+='<button class="ghost" style="display:block;width:100%;text-align:left;margin:8px 0" onclick="jumpRoot('+JSON.stringify(r.path)+')"><b>'+esc(r.name)+'</b><span class="meta"> · '+esc(r.path||"/")+' · '+esc(r.mask)+'</span></button>';if(!roots.length)html+='<div class="empty">没有可进入的共享位置</div>';html+='</div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>';show(html)}catch(e){fail(e.message)}}
function jumpRoot(p){current=p;navigationRoot=p;closeModal();refresh()}
function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];let html='<button class="crumb-seg '+(!current?'current':'')+'" onclick="navigateTo("")">全部文件</button>';let acc='';for(let i=0;i<parts.length;i++){acc=acc?acc+'/'+parts[i]:parts[i];html+='<span class="crumb-sep">/</span><button class="crumb-seg '+(i===parts.length-1?'current':'')+'" onclick="navigateTo('+JSON.stringify(acc)+')">'+esc(parts[i])+'</button>'}el.innerHTML=html}
function navigateTo(p){current=p;if(!navigationRoot||p===navigationRoot||p.startsWith(navigationRoot+'/')||p===''){refresh()}}
function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];const rootName=navigationRoot?(navigationRoot===me?.home?'我的空间':'当前位置'): '全部文件';let html='<button class="crumb-seg '+(!current||current===navigationRoot?'current':'')+'" onclick="navigateTo('+JSON.stringify(navigationRoot||'')+')">'+esc(rootName)+'</button>';let acc='';for(let i=0;i<parts.length;i++){acc=acc?acc+'/'+parts[i]:parts[i];html+='<span class="crumb-sep">/</span><button class="crumb-seg '+(i===parts.length-1?'current':'')+'" onclick="navigateTo('+JSON.stringify(acc)+')">'+esc(parts[i])+'</button>'}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="/chfs/shared/"+encodeURIComponent(p).replace(/%2F/g,"/")}
function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)}
@@ -123,8 +123,8 @@ function removeRule(i){if(i<0||isHomeRule(window._editRules[i]))return;window._e
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 '<div class="row" style="justify-content:space-between;border-top:1px solid #edf0f5;padding:9px 0;margin:0"><span><b>'+esc(r.dir)+'</b><br><span class="meta">'+esc(flags)+'</span></span><button class="danger" onclick="removeRule('+i+')">移除</button></div>'}).join(""):'<div class="empty" style="padding:18px">还没有共享目录权限</div>';$("rules").innerHTML=window._editRules.map((r,i)=>isHomeRule(r)?'':'<div class="row" style="justify-content:space-between;border-top:1px solid #edf0f5;padding:8px 0"><span><b>'+esc(r.dir)+'</b> · '+esc((r.deny?"拒绝":"允许")+" · "+(r.mask||"禁止")+(r.childrenOnly?" · 仅子项":""))+'</span><button class="danger" onclick="removeRule('+i+')">移除</button></div>').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){fail(e.message)}}
async function deleteUser(u){if(!await confirmBox("删除用户","删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){fail(e.message)}}
function toggleTheme(){const dark=document.documentElement.classList.toggle("dark");localStorage.setItem("chfs-theme",dark?"dark":"light")}
function toggleTheme(){const dark=document.documentElement.classList.toggle("dark");localStorage.setItem("chfs-theme",dark?"dark":"light");renderChromeIcons()}function renderChromeIcons(){const dark=document.documentElement.classList.contains("dark");$("themeIcon").innerHTML='<svg class="ui-icon" viewBox="0 0 24 24">'+(dark?'<path d="M20 15.5A8.5 8.5 0 0 1 8.5 4 8.5 8.5 0 1 0 20 15.5z"/>':'<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>')+'</svg>';$('listIcon').innerHTML='<svg class="ui-icon" viewBox="0 0 24 24"><path d="M4 6h2M9 6h11M4 12h2M9 12h11M4 18h2M9 18h11"/></svg>';$('gridIcon').innerHTML='<svg class="ui-icon" viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><rect x="14" y="14" width="6" height="6" rx="1"/></svg>'}
if(localStorage.getItem("chfs-theme")==="dark"||(localStorage.getItem("chfs-theme")===null&&matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.classList.add("dark");
setView(localStorage.getItem("chfs-view")||"list");setupDropZone();init();
setView(localStorage.getItem("chfs-view")||"list");renderChromeIcons();setupDropZone();init();
</script>
</body></html>