From 5863403b5ad989e76c2f1e14d2c3cfb9d40f4622 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 22 Sep 2026 02:56:56 +0800 Subject: [PATCH] feat: add desktop-like file interactions --- chfs-ng/internal/api/web/index.html | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index 2145eb7..14f1011 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -10,7 +10,7 @@ button,input{font:inherit}button{border:0;border-radius:9px;padding:9px 13px;cursor:pointer;background:#2563eb;color:#fff}button.ghost{background:#eef2f7;color:#334155}button.danger{background:#dc2626} main{max-width:1180px;margin:26px auto;padding:0 18px}.toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px}.toolgroup{display:flex;align-items:center;gap:6px}.toolbar .searchbox{min-width:220px;flex:1;display:flex;align-items:center;gap:8px;padding:0 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface);transition:border-color .15s,box-shadow .15s}.toolbar .searchbox:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 12%,transparent)}.toolbar .searchbox .ui-icon{width:16px;height:16px;color:var(--muted)}.toolbar .searchbox input{min-width:0;flex:1;padding:10px 0;border:0;outline:0;background:transparent;color:var(--text)}.toolbar .divider{width:1px;height:26px;background:var(--border);margin:0 2px}.toolbar .primary-action{box-shadow:0 2px 6px rgba(37,99,235,.18)}.toolbar .iconbtn{flex:0 0 auto} .card{background:#fff;border:1px solid #e4e8f0;border-radius:14px;overflow:hidden;box-shadow:0 4px 18px rgba(20,30,50,.04)} -table{width:100%;border-collapse:separate;border-spacing:0}th,td{text-align:left;padding:12px 15px;border-bottom:1px solid #edf0f5}th{font-size:12px;color:#7b8798;background:#fafbfc;font-weight:650}tr:last-child td{border-bottom:0}.name{display:flex;align-items:center;gap:10px;min-width:0}.name a{color:#172033;text-decoration:none;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sorthead{display:inline-flex;align-items:center;gap:5px;padding:3px 5px;margin:-3px -5px;background:transparent;color:inherit;font-size:inherit;font-weight:inherit}.sorthead:hover{background:var(--surface);color:var(--text)}.sorthead span{font-size:10px;color:var(--primary);min-width:8px}.meta{color:#8792a5;font-size:13px}.actions{display:flex;gap:7px;justify-content:flex-end;opacity:.55;transition:opacity .15s}.file-row:hover .actions,.file-row.selected .actions{opacity:1}.file-row{transition:background .12s}.file-row:hover{background:var(--surface2)}.file-row.selected{background:color-mix(in srgb,var(--primary) 7%,var(--surface))}.file-row:focus-within{box-shadow:inset 2px 0 0 var(--primary)}.tile:focus-visible,.file-row:focus-within{outline:2px solid color-mix(in srgb,var(--primary) 65%,transparent);outline-offset:-2px}.file-row .file-kind{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;background:var(--surface2);color:var(--muted);flex:0 0 auto}.file-row .file-kind .ui-icon{width:18px;height:18px}.file-row .size-cell,.file-row .time-cell{white-space:nowrap;color:var(--muted);font-size:12px}.file-row .perm-cell{white-space:nowrap}.perm-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--border);margin-right:5px}.perm-dot.read{background:var(--muted)}.perm-dot.write{background:var(--primary)}.perm-dot.delete{background:var(--danger)}.empty{padding:45px;text-align:center;color:#8a94a6} +table{width:100%;border-collapse:separate;border-spacing:0}th,td{text-align:left;padding:12px 15px;border-bottom:1px solid #edf0f5}th{font-size:12px;color:#7b8798;background:#fafbfc;font-weight:650}tr:last-child td{border-bottom:0}.name{display:flex;align-items:center;gap:10px;min-width:0}.name a{color:#172033;text-decoration:none;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sorthead{display:inline-flex;align-items:center;gap:5px;padding:3px 5px;margin:-3px -5px;background:transparent;color:inherit;font-size:inherit;font-weight:inherit}.sorthead:hover{background:var(--surface);color:var(--text)}.sorthead span{font-size:10px;color:var(--primary);min-width:8px}.meta{color:#8792a5;font-size:13px}.actions{display:flex;gap:7px;justify-content:flex-end;opacity:.55;transition:opacity .15s}.file-row:hover .actions,.file-row.selected .actions{opacity:1}.file-row{transition:background .12s}.file-row:hover{background:var(--surface2)}.file-row.selected{background:color-mix(in srgb,var(--primary) 7%,var(--surface))}.file-row.drop-target{background:color-mix(in srgb,var(--primary) 12%,var(--surface));box-shadow:inset 0 0 0 2px var(--primary)}.tile.drop-target{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 16%,transparent)}.side-item.drop-target{background:color-mix(in srgb,var(--primary) 12%,var(--surface2));color:var(--primary)}.dragging{opacity:.45}.gallery-nav{position:absolute;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,.12);color:#fff;border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center}.gallery-nav .ui-icon{width:22px;height:22px}.gallery-prev{left:16px}.gallery-next{right:16px}.gallery-count{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);padding:5px 9px;border-radius:99px;background:rgba(0,0,0,.48);color:#fff;font-size:12px}.preview-stage.media-stage{position:relative}.preview-stage video:focus{outline:2px solid var(--primary);outline-offset:2px}.media-hint{position:absolute;left:18px;top:14px;color:#aab5c5;font-size:12px;pointer-events:none}.file-row:focus-within{box-shadow:inset 2px 0 0 var(--primary)}.tile:focus-visible,.file-row:focus-within{outline:2px solid color-mix(in srgb,var(--primary) 65%,transparent);outline-offset:-2px}.file-row .file-kind{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;background:var(--surface2);color:var(--muted);flex:0 0 auto}.file-row .file-kind .ui-icon{width:18px;height:18px}.file-row .size-cell,.file-row .time-cell{white-space:nowrap;color:var(--muted);font-size:12px}.file-row .perm-cell{white-space:nowrap}.perm-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--border);margin-right:5px}.perm-dot.read{background:var(--muted)}.perm-dot.write{background:var(--primary)}.perm-dot.delete{background:var(--danger)}.empty{padding:45px;text-align:center;color:#8a94a6} .modal{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;padding:18px;z-index:4;backdrop-filter:blur(4px)}.modal.show{display:flex}.dialog{width:min(480px,100%);max-height:calc(100vh - 36px);overflow:auto;background:var(--surface);border:1px solid var(--border);border-radius:18px;padding:22px;box-shadow:0 24px 80px rgba(0,0,0,.18)}.dialog.preview-dialog{width:min(1000px,100%);padding:0;overflow:hidden}.dialog h2{margin-top:0}.field{display:grid;gap:7px;margin:13px 0}.field input{padding:10px 11px;border:1px solid #d7ddea;border-radius:9px}.row{display:flex;gap:9px;justify-content:flex-end;margin-top:18px}.preview-head{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border)}.preview-title{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:auto}.preview-stage{min-height:180px;max-height:calc(100vh - 150px);overflow:auto;background:#0b0f14;padding:24px;display:flex;align-items:center;justify-content:center}.preview-stage img{max-width:100%;max-height:70vh;object-fit:contain}.preview-stage video{width:min(920px,100%);max-height:70vh;border-radius:10px}.preview-stage audio{width:min(720px,100%)}.preview-stage iframe{width:100%;height:70vh;border:0;background:#fff}.preview-text{width:min(920px,100%);max-height:70vh;overflow:auto;text-align:left;white-space:pre-wrap;background:#111821;color:#dce5f2;padding:18px;border-radius:10px;font:13px/1.6 ui-monospace,SFMono-Regular,monospace}.preview-empty{color:#aab5c5;text-align:center}.preview-foot{padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px} .login{max-width:390px;margin:10vh auto}.notice{padding:12px 14px;background:#eff6ff;border-radius:10px;color:#315b9b;margin-bottom:14px} @media(max-width:700px){.size-col,.time-col,.perm-col,.size-cell,.time-cell,.perm-cell{display:none}.top{padding:0 13px}.path{max-width:40vw}.actions button{padding:7px 9px}.toolbar{gap:6px;margin-bottom:10px}.toolbar .toolgroup{flex-wrap:wrap}.toolbar .divider{display:none}.toolbar .searchbox{order:3;flex-basis:100%;min-width:0}.toolbar .toolgroup:nth-of-type(2){flex:1}.toolbar .toolgroup:last-child{margin-left:auto}.toolbar button{padding:8px 10px}.file-row td{padding:10px 9px}.bulk{position:sticky;bottom:10px;z-index:3;padding:8px 10px;border:1px solid var(--border);border-radius:11px;background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:var(--shadow);backdrop-filter:blur(10px);flex-wrap:wrap}.bulk .count{flex-basis:100%;margin-right:0}} @@ -63,8 +63,13 @@ function sortBy(key){if(sortKey===key)sortDir*=-1;else{sortKey=key;sortDir=1}ren 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);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);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')} +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,'');toast('正在移动 '+paths.length+' 项…');let done=0,failed=[];for(const source of paths){const name=source.split('/').pop(),destination=base?base+'/'+name:name;try{await api('/chfs/move',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({source,destination})});done++}catch(err){failed.push(name+': '+err.message)}}selected.clear();await refresh();if(failed.length)fail('移动完成 '+done+' 项,失败 '+failed.length+' 项');else toast('已移动 '+done+' 项')} 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 toggleTile(p){selectItem(p,!selected.has(p))} function openSelected(){if(selected.size!==1)return;const p=[...selected][0],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)} @@ -83,8 +88,12 @@ async function bulkCopy(){await bulkTransfer(true)} async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=await chooseDestination(copyOnly?"复制选中项目到":"移动选中项目到");if(dstDir===null)return;const sources=[...selected],base=String(dstDir).replace(/^\/+|\/+$/g,""),action=copyOnly?"复制":"移动";toast(action+" "+sources.length+" 项…");const failed=[];let done=0;for(const source of sources){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})});done++}catch(e){failed.push(name+": "+e.message)}}selected.clear();await refresh();if(failed.length)show('

部分操作失败

'+esc(action+"完成 "+done+" 项,失败 "+failed.length+" 项。\n\n"+failed.join("\n"))+'
');else toast(action+"完成,共 "+done+" 项")} async function bulkDelete(){if(!selected.size)return;if(!await confirmBox("批量删除","确定删除已选择的 "+selected.size+" 项?"))return;const targets=[...selected];toast("正在删除 "+targets.length+" 项…");const failed=[];let done=0;for(const p of targets){try{await api('/chfs/rmfiles',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath:p})});done++}catch(e){failed.push(p+": "+e.message)}}selected.clear();await refresh();if(failed.length)show('

部分删除失败

'+esc("已删除 "+done+" 项,失败 "+failed.length+" 项。\n\n"+failed.join("\n"))+'
');else toast("删除完成,共 "+done+" 项")} -async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'').join('')||'
没有可进入的位置
'}catch{}} +async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'').join('')||'
没有可进入的位置
'}catch{}} async function showRoots(){try{const d=await api("/chfs/roots");const roots=d.roots||[];let html='

选择位置

';for(const r of roots)html+='';if(!roots.length)html+='
没有可进入的共享位置
';html+='
';show(html)}catch(e){fail(e.message)}} +function sideDragOver(e,target){if(!e.dataTransfer?.types.includes('text/chfs-paths'))return;e.preventDefault();e.dataTransfer.dropEffect='move';e.currentTarget.classList.add('drop-target')} +function sideDragLeave(e){e.currentTarget.classList.remove('drop-target')} +async function sideDrop(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{};if(!paths.length)return;toast('正在移动 '+paths.length+' 项…');let done=0;for(const source of paths){const name=source.split('/').pop(),destination=target?target+'/'+name:name;try{await api('/chfs/move',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({source,destination})});done++}catch{}}selected.clear();await refresh();toast('已移动 '+done+' 项')} + function jumpRoot(p){current=p;navigationRoot=p;closeModal();refresh()} function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];const rootName=navigationRoot?(navigationRoot===me?.home?'我的空间':'当前位置'): '全部文件';let html='';let acc='';for(let i=0;i'+esc(parts[i])+''}el.innerHTML=html} function navigateTo(p){if(navigationRoot){if(p!==navigationRoot&&!p.startsWith(navigationRoot+'/'))return}else if(p!==''&&!me?.admin)return;current=p;refresh()} @@ -116,7 +125,19 @@ function selectSearchItem(p){document.querySelectorAll("#files .file-row").forEa function toggleSearchItem(p,checked){if(checked){selected.clear();selected.add(p)}else selected.delete(p);selectSearchItem(checked?p:"")} 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'} function openSearchResult(f){if(f.dir){current=f.path;navigationRoot=me.home||"";searchMode=false;$("search").value="";refresh()}else previewFile(f.path)} -async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().split('.').pop(),url='/chfs/preview?file='+encodeURIComponent(p),media=['mp4','webm','mov','mkv'].includes(ext),audio=['mp3','wav','flac','m4a','ogg'].includes(ext),image=['png','jpg','jpeg','gif','webp','bmp','svg'].includes(ext),pdf=ext==='pdf',text=['txt','md','markdown','json','yaml','yml','xml','csv','log','js','ts','jsx','tsx','css','scss','html','htm','go','rs','py','java','c','cpp','h','hpp','sh','sql','toml','ini','conf'].includes(ext);let body='';if(image)body=''+esc(n)+'';else if(media)body='';else if(audio)body='';else if(pdf)body='';else if(text){try{const r=await fetch(url);if(!r.ok)throw Error();body='
'+esc(await r.text())+'
'}catch{body='
无法读取文本内容
'}}else body='
暂不支持该文件类型的在线预览
';show('
'+esc(n)+'
'+body+'
',true)} +let galleryFiles=[],galleryIndex=0,activeMedia=null; +function previewFilesFor(p){const dir=p.includes('/')?p.split('/').slice(0,-1).join('/'):'',items=dir===current?lastItems:searchItems;return items.filter(f=>!f.dir&&fileType(f)==='image'&&(f.mask||'').includes('R'))} +function galleryOpen(p){galleryFiles=previewFilesFor(p);galleryIndex=Math.max(0,galleryFiles.findIndex(f=>(dirOf(f.path||f.name)===current?f.name:f.path)===p));renderGallery()} +function dirOf(p){return String(p||'').split('/').slice(0,-1).join('/')} +function galleryMove(delta){if(!galleryFiles.length)return;galleryIndex=(galleryIndex+delta+galleryFiles.length)%galleryFiles.length;const f=galleryFiles[galleryIndex],path=f.path||(current?current+'/'+f.name:f.name);renderGallery(path)} +function renderGallery(path){const f=galleryFiles[galleryIndex];if(!f)return;const p=path||((f.path)||((current?current+'/':'')+f.name)),n=f.name,src='/chfs/preview?file='+encodeURIComponent(p);show('
'+esc(n)+''+(galleryIndex+1)+' / '+galleryFiles.length+'
'+esc(n)+'
',true)} +function mediaKeydown(e){const mediaActive=!!activeMedia&&($('modal').classList.contains('show')||$('miniPlayer'));if(mediaActive){if(e.key===' '){e.preventDefault();activeMedia.paused?activeMedia.play():activeMedia.pause()}else if(e.key==='ArrowLeft'||e.key==='ArrowRight'){e.preventDefault();activeMedia.currentTime=Math.max(0,activeMedia.currentTime+(e.key==='ArrowLeft'?-5:5))}}if($('modal').classList.contains('show')&&galleryFiles.length&&(e.key==='ArrowLeft'||e.key==='ArrowRight')){e.preventDefault();galleryMove(e.key==='ArrowLeft'?-1:1)}} +addEventListener('keydown',mediaKeydown); +function editText(p,content){show('
编辑 · '+esc(p.split('/').pop())+'
Ctrl/Cmd + S 保存
',true);$('editText').focus();$('editText').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){e.preventDefault();saveTextEdit(p)}})} +async function saveTextEdit(p){const text=$('editText')?.value;if(text===undefined)return;const parts=p.split('/'),filename=parts.pop()||'';const filepath=parts.join('/');try{await api('/chfs/updatetext',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath,filename,text})});toast('已保存');closeModal();refresh()}catch(e){fail(e.message)}} +async function togglePiP(){if(!activeMedia||!('pictureInPictureEnabled' in document)||!document.pictureInPictureEnabled)return fail('当前浏览器不支持画中画');try{if(document.pictureInPictureElement)await document.exitPictureInPicture();else if(activeMedia.requestPictureInPicture)await activeMedia.requestPictureInPicture()}catch(e){fail('无法进入画中画')} } +function startMiniPlayer(url,name){let host=$('miniPlayer');if(!host){host=document.createElement('div');host.id='miniPlayer';host.style='position:fixed;right:18px;bottom:18px;z-index:30;width:min(380px,calc(100vw - 36px));padding:12px 14px;border:1px solid var(--border);border-radius:14px;background:var(--surface);box-shadow:var(--shadow);display:grid;gap:8px';document.body.appendChild(host)}host.innerHTML='
'+esc(name)+'
';activeMedia=host.querySelector('audio');closeModal();activeMedia?.focus()} +async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().split('.').pop(),url='/chfs/preview?file='+encodeURIComponent(p),media=['mp4','webm','mov','mkv'].includes(ext),audio=['mp3','wav','flac','m4a','ogg'].includes(ext),image=['png','jpg','jpeg','gif','webp','bmp','svg'].includes(ext),pdf=ext==='pdf',text=['txt','md','markdown','json','yaml','yml','xml','csv','log','js','ts','jsx','tsx','css','scss','html','htm','go','rs','py','java','c','cpp','h','hpp','sh','sql','toml','ini','conf'].includes(ext);if(image){galleryFiles=previewFilesFor(p);galleryIndex=Math.max(0,galleryFiles.findIndex(f=>(f.path||((current?current+'/':'')+f.name))===p));return renderGallery(p)}let content='';if(media){show('
'+esc(n)+'
空格暂停 · ← / → 快进 5 秒
',true);activeMedia=$('.preview-stage video');activeMedia?.focus();return}if(audio){startMiniPlayer(url,n);return}if(pdf){show('
'+esc(n)+'
',true);return}if(text){try{const r=await fetch(url);if(!r.ok)throw Error();content=await r.text();const canEdit=currentMask.includes('W');show('
'+esc(n)+''+(canEdit?'':'')+'
'+esc(content)+'
',true);window._previewText=content}catch{show('
无法读取文本内容
')}}else show('
暂不支持该文件类型的在线预览
')} function changePassword(){show('

修改密码

')} async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}