diff --git a/internal/api/web/index.html b/internal/api/web/index.html
index 6f65c51..359049c 100644
--- a/internal/api/web/index.html
+++ b/internal/api/web/index.html
@@ -18,6 +18,29 @@ table{width:100%;border-collapse:separate;border-spacing:0}th,td{text-align:left
:root{--bg:#f5f7fb;--surface:#fff;--surface2:#f8fafc;--text:#172033;--muted:#7b8798;--border:#e5e9f2;--primary:#2563eb;--danger:#dc2626;--shadow:0 8px 30px rgba(20,30,50,.08)}
html.dark{--bg:#0f141c;--surface:#171e28;--surface2:#202936;--text:#e7edf7;--muted:#9aa7b8;--border:#2c3746;--primary:#60a5fa;--danger:#f87171;--shadow:0 12px 35px rgba(0,0,0,.3)}
+/* Button system: primary / secondary / ghost / danger */
+button{display:inline-flex;align-items:center;justify-content:center;gap:6px;font:inherit;font-size:13.5px;font-weight:500;line-height:1;padding:8px 14px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.04);cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease,transform .08s ease;user-select:none}
+button:hover{background:var(--surface2);border-color:color-mix(in srgb,var(--border) 70%,var(--text));box-shadow:0 2px 6px rgba(20,30,50,.07)}
+button:active{transform:translateY(1px);box-shadow:none}
+button:focus-visible{outline:2px solid color-mix(in srgb,var(--primary) 65%,transparent);outline-offset:2px}
+button:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none}
+button.primary,button.primary-action{background:var(--primary);border-color:var(--primary);color:#fff;box-shadow:0 2px 5px color-mix(in srgb,var(--primary) 28%,transparent)}
+button.primary:hover,button.primary-action:hover{background:color-mix(in srgb,var(--primary) 88%,#000);border-color:color-mix(in srgb,var(--primary) 88%,#000);box-shadow:0 4px 10px color-mix(in srgb,var(--primary) 30%,transparent)}
+button.ghost{background:transparent;border-color:transparent;color:var(--text);box-shadow:none}
+button.ghost:hover{background:var(--surface2);border-color:transparent;box-shadow:none}
+button.danger{background:color-mix(in srgb,var(--danger) 10%,var(--surface));border-color:color-mix(in srgb,var(--danger) 30%,transparent);color:var(--danger);box-shadow:none}
+button.danger:hover{background:var(--danger);border-color:var(--danger);color:#fff;box-shadow:0 3px 8px color-mix(in srgb,var(--danger) 25%,transparent)}
+button.secondary{background:var(--surface);border-color:var(--border);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.04)}
+button.iconbtn{width:38px;height:38px;padding:0;flex:0 0 auto}
+.view-switch{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:9px;overflow:hidden;background:var(--surface);box-shadow:0 1px 2px rgba(0,0,0,.04)}
+.view-switch .viewbtn{width:36px;height:34px;border:0;border-radius:0;box-shadow:none;background:transparent;color:var(--muted)}
+.view-switch .viewbtn:hover{background:var(--surface2);color:var(--text);box-shadow:none}
+.view-switch .viewbtn.active{background:var(--primary);color:#fff}
+.view-switch .viewbtn+ .viewbtn{border-left:1px solid var(--border)}
+.context{padding:5px;border-radius:10px}
+.context button{display:flex;align-items:center;min-height:36px;padding:8px 10px;border:0;background:transparent;box-shadow:none}
+.context button:hover{background:var(--surface2);border:0;box-shadow:none}
+
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)}
@@ -28,19 +51,19 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
将文件或文件夹拖到这里上传
__CHFS_NOTICE__
@@ -90,7 +113,7 @@ function clearSelection(){selected.clear();updateSelection();if(searchMode){rend
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'}
+function closeContext(){$("context").style.display="none"}
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="";
@@ -182,9 +205,9 @@ function fail(message){toast(message,true)}
function keyboardNavigation(e){const tag=(e.target?.tagName||"").toLowerCase();if(tag==="input"||tag==="textarea"||tag==="select"||tag==="button"||tag==="a"||e.target?.isContentEditable)return;if($("modal").classList.contains("show")&&$("galleryStage")&&galleryFiles.length){if(e.key==="ArrowLeft"){e.preventDefault();galleryMove(-1);return}if(e.key==="ArrowRight"){e.preventDefault();galleryMove(1);return}if(e.key==="Home"){e.preventDefault();galleryIndex=0;renderGallery();return}if(e.key==="End"){e.preventDefault();galleryIndex=galleryFiles.length-1;renderGallery();return}}if(e.key==="Escape"){if($("modal").classList.contains("show"))closeModal();else closeContext();return}if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="a"){e.preventDefault();toggleAll(true);return}if(e.key==="Enter"){e.preventDefault();openSelected();return}if(e.key==="Delete"&&selected.size){e.preventDefault();bulkDelete();return}if(e.key==="Backspace"&¤t&&!searchMode){e.preventDefault();goUp();return}if((e.key==="ArrowDown"||e.key==="ArrowUp")){const source=searchMode?searchItems:lastItems;if(!source.length)return;e.preventDefault();const paths=searchMode?source.map(searchPath):selectablePaths(),anchor=selectionAnchor&&paths.includes(selectionAnchor)?selectionAnchor:[...selected][0],idx=Math.max(0,paths.indexOf(anchor)),next=Math.max(0,Math.min(paths.length-1,idx+(e.key==="ArrowDown"?1:-1)));if(searchMode)selectSearchItem(paths[next],true,e);else selectItem(paths[next],true,e);return}if(e.key==="Home"||e.key==="End"){const source=searchMode?searchItems:lastItems;if(!source.length)return;e.preventDefault();const paths=searchMode?source.map(searchPath):selectablePaths(),p=e.key==="Home"?paths[0]:paths.at(-1);if(searchMode)selectSearchItem(p,true,e);else selectItem(p,true,e)}}
addEventListener("keydown",keyboardNavigation)
function show(html,preview=false){$("dialog").innerHTML=html;$("dialog").classList.toggle("preview-dialog",preview);$("modal").classList.add("show")}async function closeModal(){if(window._editorDirty&&$("editText")){const ok=await confirmBox("放弃未保存修改?","当前文本还有未保存的修改,关闭后这些修改将丢失。");if(!ok)return false}window._editorDirty=false;$("modal").classList.remove("show");$("dialog").classList.remove("preview-dialog");return true}
-function ask(title,label,value="",type="text"){return new Promise(resolve=>{show(''+esc(title)+'
');window._askResolve=v=>{closeModal();resolve(v)}})}
-function confirmBox(title,message){return new Promise(resolve=>{show(''+esc(title)+'
'+esc(message)+'
');window._confirmResolve=v=>resolve(v)})}
-$("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()})
+function ask(title,label,value="",type="text"){return new Promise(resolve=>{let settled=false;const finish=async v=>{if(settled)return;const closed=await closeModal();if(!closed)return;settled=true;window._askResolve=null;resolve(v)};window._askResolve=finish;show(''+esc(title)+'
')})}
+function confirmBox(title,message){return new Promise(resolve=>{let settled=false;const finish=async v=>{if(settled)return;const closed=await closeModal();if(!closed)return;settled=true;window._confirmResolve=null;resolve(v)};window._confirmResolve=finish;show(''+esc(title)+'
'+esc(message)+'
')})}
+document.addEventListener("pointerdown",e=>{const c=$("context");if(c?.style.display==="block"&&!c.contains(e.target))closeContext()});$("modal").addEventListener("click",e=>{if(e.target.id==="modal")closeModal()})
async function showUsers(){try{const d=await api("/chfs/users");let html='用户管理
';for(const u of d.users)html+='
'+esc(u.username)+' · '+(u.enabled?"启用":"已禁用")+' · '+esc(u.home||"")+(u.managed&&!u.admin?' ':"")+"
";html+='
';show(html)}catch(e){fail(e.message)}}
async function createUser(){try{await api("/chfs/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:$("nu").value,password:$("np").value})});showUsers()}catch(e){fail(e.message)}}
async function resetUser(u){const p=await ask("重置密码","为 "+u+" 设置新密码","","password");if(!p)return;try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});toast("密码已更新")}catch(e){fail(e.message)}}