[GH-ISSUE #706] 增加个局域网粘贴板功能 #5947

Open
opened 2026-05-29 23:48:58 +03:00 by zhus · 1 comment
Owner

Originally created by @kamkdd on GitHub (May 17, 2026).
Original GitHub issue: https://github.com/sigoden/dufs/issues/706

加个局域网粘贴板同步功能,用ai写的,但总感觉差了点

Image
Originally created by @kamkdd on GitHub (May 17, 2026). Original GitHub issue: https://github.com/sigoden/dufs/issues/706 加个局域网粘贴板同步功能,用ai写的,但总感觉差了点 <img width="1122" height="914" alt="Image" src="https://github.com/user-attachments/assets/11f84b95-229c-4afd-8de1-a652a65601cf" />
Author
Owner

@kamkdd commented on GitHub (May 17, 2026):

<html lang="zh-CN"> <head> <style> /* ── 默认明亮主题 (Light Mode) ── */ :root { --bg: #f8f9fa; --surface: #ffffff; --surface2: #f1f3f5; --border: rgba(0,0,0,0.06); --border-hi: rgba(0,0,0,0.11); --text: #212529; --text-dim: #495057; --text-muted: #adb5bd; --accent: #0d6efd; --accent-g: rgba(13,110,253,0.08); --accent2: #198754; --warn: #b58900; --danger: #dc3545; --ok: #198754; --r-sm: 8px; --r: 12px; --r-lg: 16px; --mono: 'DM Mono', 'SF Mono', 'Consolas', monospace; --sans: 'DM Sans', system-ui, sans-serif; --hover: rgba(0,0,0,0.02); --hover-active: rgba(0,0,0,0.05); --shadow: rgba(0,0,0,0.06); }

/* ── 自动跟随系统暗黑主题 (Dark Mode) ── */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2: #1c2128;
--border: rgba(255,255,255,0.08);
--border-hi: rgba(255,255,255,0.14);
--text: #cdd9e5;
--text-dim: #768390;
--text-muted: #444c56;
--accent: #4493f8;
--accent-g: rgba(68,147,248,0.15);
--accent2: #3fb950;
--warn: #d29922;
--danger: #f85149;
--ok: #3fb950;
--hover: rgba(255,255,255,0.03);
--hover-active: rgba(255,255,255,0.055);
--shadow: rgba(0,0,0,0.45);
}
}

,::before,*::after{box-sizing:border-box;margin:0;padding:0;}

html,body{
min-height:100dvh;
background:var(--bg);
color:var(--text);
font-family:var(--sans);
font-size:14px;
line-height:1.5;
-webkit-font-smoothing:antialiased;
transition:background .3s, color .3s;
}

/* 全局最外层精致滚动条 */
::-webkit-scrollbar{width:6px;height:6px;}
::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:var(--border-hi);border-radius:3px;}
::-webkit-scrollbar-thumb:hover{background:var(--text-muted);}

.shell{
max-width:840px;
margin:0 auto;
padding:20px 16px 60px;
display:flex;
flex-direction:column;
gap:0;
}

.hdr{display:flex;align-items:center;gap:10px;margin-bottom:24px;}
.logo{
width:30px;height:30px;border-radius:8px;flex-shrink:0;
background:linear-gradient(135deg,var(--accent) 0%,var(--accent2) 100%);
display:flex;align-items:center;justify-content:center;
font-family:var(--mono);font-size:13px;color:#fff;
box-shadow:0 0 18px rgba(68,147,248,0.3);
}
.hdr-title{
font-family:var(--mono);font-size:11px;letter-spacing:.1em;
text-transform:uppercase;color:var(--text-dim);
}
.hdr-spacer{flex:1;}
.dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted);transition:all .4s;}
.dot.ok {background:var(--ok);box-shadow:0 0 6px var(--ok);}
.dot.err{background:var(--danger);}

.slabel{
font-family:var(--mono);font-size:10px;font-weight:500;
color:var(--text-muted);letter-spacing:.12em;text-transform:uppercase;
padding:0 2px;margin-bottom:7px;
}

.clip{
background:var(--surface);border:1px solid var(--border);
border-radius:var(--r-lg);padding:4px;margin-bottom:12px;
box-shadow:0 4px 20px var(--shadow);transition:border-color .2s;
}
.clip:focus-within{border-color:rgba(13,110,253,.4);}
@media (prefers-color-scheme: dark) {
.clip:focus-within{border-color:rgba(68,147,248,.4);}
}
.clip.dirty{border-color:rgba(210,153,34,.45);}

textarea{
width:100%;height:140px;background:transparent;border:none;outline:none;resize:none;
color:var(--text);font-family:var(--mono);font-size:13.5px;line-height:1.7;padding:12px 14px;
}
textarea::placeholder{color:var(--text-muted);}
.clip-bar{display:flex;align-items:center;gap:6px;padding:4px 10px 8px;}
#indicator{
font-family:var(--mono);font-size:11px;color:var(--text-dim);
flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}

.btn{
display:inline-flex;align-items:center;gap:4px;padding:6px 13px;
border:1px solid var(--border-hi);border-radius:20px;font-size:12px;font-weight:500;
cursor:pointer;user-select:none;background:var(--surface2);color:var(--text-dim);
transition:all .12s;-webkit-tap-highlight-color:transparent;white-space:nowrap;
}
.btn:active{transform:scale(.92);}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none;}
.btn-red{color:var(--danger);border-color:rgba(248,81,73,.25);}
.btn-red:hover:not(:disabled){background:rgba(248,81,73,.1);}
.btn-blue{background:var(--accent);color:#fff;border-color:transparent;box-shadow:0 2px 14px rgba(68,147,248,.3);}
.btn-blue:hover:not(:disabled){opacity:0.9;color:#fff;}

/* ── 历史记录面板 ── */
.hist-panel{
background:var(--surface);border:1px solid var(--border);border-radius:var(--r);
margin-bottom:24px;padding:6px;box-shadow:0 2px 12px var(--shadow);
max-height:190px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;
}
.hist-item{
display:flex;align-items:center;gap:12px;
padding:7px 12px;border-radius:var(--r-sm);cursor:pointer;transition:all .15s;
}
.hist-item:hover{background:var(--surface2);}
.hist-text{
flex:1;min-width:0;font-family:var(--mono);font-size:12px;
color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.hist-item:hover .hist-text{color:var(--text);}
.hist-time{
font-family:var(--mono);font-size:11px;color:var(--text-muted);
white-space:nowrap;user-select:none;
}
.hist-del{
width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:10px;color:var(--text-muted);cursor:pointer;transition:all .15s;background:transparent;border:none;flex-shrink:0;
}
.hist-del:hover{background:rgba(248,81,73,.1);color:var(--danger);}

#toast{
position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(10px);
background:var(--surface2);color:var(--text);border:1px solid var(--border-hi);
padding:9px 20px;border-radius:25px;font-family:var(--mono);font-size:12px;
pointer-events:none;z-index:500;opacity:0;transition:all .22s;box-shadow:0 8px 30px var(--shadow);
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}

/* ── 文件管理面板 ── */
.fmc{
background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg);
overflow:hidden;box-shadow:0 4px 20px var(--shadow);
}
.fmc.dragover{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-g);}
.ftop{display:flex;align-items:center;padding:10px 12px;background:var(--surface2);border-bottom:1px solid var(--border);min-height:46px;flex-wrap:wrap;gap:8px;}

.bc{display:flex;align-items:center;flex-wrap:wrap;font-family:var(--mono);font-size:12px;color:var(--text-dim);flex:1;min-width:120px;gap:1px;}
.bc-item{padding:2px 5px;border-radius:5px;cursor:pointer;white-space:nowrap;transition:.15s;}
.bc-item:hover{color:var(--accent);background:var(--accent-g);}
.bc-item.cur{color:var(--text);cursor:default;background:transparent;}
.bc-sep{color:var(--border-hi);}

.sort-grp{display:inline-flex;background:var(--surface);border:1px solid var(--border-hi);border-radius:20px;padding:2px;}
.sort-btn{font-size:11px;padding:3px 10px;border-radius:16px;cursor:pointer;color:var(--text-dim);user-select:none;transition:.15s;font-weight:500;}
.sort-btn.active{background:var(--surface2);color:var(--accent);}

.fsw{position:relative;flex-shrink:0;}
.fs{
background:var(--surface);border:1px solid var(--border-hi);border-radius:20px;outline:none;
color:var(--text);font-family:var(--mono);font-size:12px;padding:5px 11px 5px 28px;width:120px;transition:.2s;
}
.fs:focus{width:160px;border-color:rgba(68,147,248,.4);}
.fs-ico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:13px;}

.up-lbl{
display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border:1px solid var(--border-hi);
border-radius:20px;font-size:12px;font-weight:500;cursor:pointer;background:var(--surface);color:var(--text-dim);transition:.15s;
}
.up-lbl:hover{color:var(--accent2);border-color:rgba(63,185,80,.35);}
#file-input{display:none;}

#ubar-w{height:2px;background:transparent;overflow:hidden;opacity:0;}
#ubar-w.on{opacity:1;}
#ubar{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .3s;}

/* 去掉文件列表内部硬编码高度,完美交由最外层包裹滚动 */
.fl{list-style:none;min-height:100px;}
.fstate{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:44px 20px;font-family:var(--mono);font-size:12px;color:var(--text-muted);}
.fstate .ico{font-size:26px;opacity:.35;}

.frow{
display:flex;align-items:center;border-bottom:1px solid var(--border);
transition:background .1s;animation:rowIn .16s ease both;
}
.frow:last-child{border-bottom:none;}
.frow:hover{background:var(--hover);}

.fclick{
display:flex;align-items:center;gap:10px;flex:1;min-width:0;
padding:10px 14px;color:inherit;text-decoration:none;-webkit-tap-highlight-color:transparent;
}
.ficon{width:32px;height:32px;border-radius:var(--r-sm);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--surface2);}
.ficon.d{background:var(--accent-g);}
.finfo{flex:1;min-width:0;}
.fname{font-size:13.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.fmeta{font-family:var(--mono);font-size:11px;color:var(--text-muted);margin-top:1px;}

.factions{display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .15s;flex-shrink:0;padding-right:14px;}
.frow:hover .factions{opacity:1;}

.ibtn{
width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--surface);
color:var(--text-dim);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;
}
.ibtn:hover{background:var(--accent-g);color:var(--accent);border-color:var(--accent);transform:translateY(-1px);}
.ibtn.del:hover{background:rgba(248,81,73,.08);color:var(--danger);border-color:rgba(248,81,73,.3);}

@media(max-width:580px){
.factions{opacity:1;}
.fs{width:90px;}.fs:focus{width:110px;}
.hdr-title{display:none;}.ftop{gap:6px;}
}

#cov{position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(5px);display:flex;align-items:center;justify-content:center;z-index:400;opacity:0;pointer-events:none;transition:opacity .2s;}
#cov.show{opacity:1;pointer-events:auto;}
.cbox{background:var(--surface);border:1px solid var(--border-hi);border-radius:var(--r-lg);padding:22px 26px;max-width:320px;width:90%;box-shadow:0 20px 60px var(--shadow);transform:translateY(10px);transition:transform .2s;}
#cov.show .cbox{transform:none;}
.ctitle{font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px;}
.cmsg{font-size:12px;font-family:var(--mono);color:var(--text-dim);word-break:break-all;margin-bottom:18px;line-height:1.5;}
.cbtns{display:flex;gap:8px;justify-content:flex-end;}
</style>

</head>
确认删除
取消 删除
剪贴板 · 局域网剪贴板
// 剪贴板同步
连接中… ✕ 清空 ⎘ 复制 ↑ 同步
// 历史记录 (安全云同步)
暂无历史记录
// 文件管理
时间 名称 大小
↑ 上传
  • 正在加载…
<script> "use strict"; const $=id=>document.getElementById(id); const editor=$('editor'),saveBtn=$('saveBtn'),indicator=$('indicator'); const toastEl=$('toast'),dot=$('dot'),clip=$('clip'),histPanel=$('histPanel'); const fmc=$('fmc'),fl=$('fl'),fs=$('fs'),fileInput=$('file-input'); let lastContent=null,isDirty=false,isSyncing=false,isFocused=false; let lastTyped=0,tTimer=null,historyList=[]; let curPath='/',allFiles=[],searchQ='',currentSort='time'; let savedScrollPos = 0; function toast(msg,ms=2300){ clearTimeout(tTimer);toastEl.textContent=msg;toastEl.classList.add('show'); tTimer=setTimeout(()=>toastEl.classList.remove('show'),ms); } function status(txt,col){indicator.textContent=txt;indicator.style.color=col||'var(--text-dim)';} function setConn(ok){dot.className='dot '+(ok?'ok':'err');} editor.addEventListener('input',()=>{ lastTyped=Date.now();if(lastContent===null)return; const c=editor.value!==lastContent; if(c!==isDirty){ isDirty=c;clip.classList.toggle('dirty',isDirty); status(isDirty?'● 待同步':'已就绪',isDirty?'var(--warn)':null); } }); editor.addEventListener('focus',()=>isFocused=true); editor.addEventListener('blur', ()=>isFocused=false); function clearText(){editor.value='';editor.dispatchEvent(new Event('input'));editor.focus();} async function copyText(){ if(!editor.value){toast('编辑框为空');return;} try{await navigator.clipboard.writeText(editor.value);} catch{editor.select();document.execCommand('copy');} toast('⎘ 已复制'); } function getTimestamp(){ const d=new Date(); const p=n=>String(n).padStart(2,'0'); return `${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } async function loadHistory(){ try{ const r=await fetch('history.json?t='+Date.now(),{cache:'no-store'}); if(r.ok){ const data=await r.json(); historyList = data.map(item => (typeof item === 'string') ? {txt: item, time: getTimestamp()} : item); } }catch{ historyList=[]; } renderHistory(); } function renderHistory(){ if(!historyList || !historyList.length){ histPanel.innerHTML=`
暂无历史记录
`; return; } histPanel.innerHTML=''; historyList.forEach((item,idx)=>{ const div=document.createElement('div');div.className='hist-item'; const txtSpan=document.createElement('span');txtSpan.className='hist-text'; txtSpan.textContent=item.txt.trim() || '[空文本]'; const timeSpan=document.createElement('span');timeSpan.className='hist-time'; timeSpan.textContent=item.time || ''; const delBtn=document.createElement('button');delBtn.className='hist-del'; delBtn.innerHTML='✕'; div.onclick=()=>{ editor.value=item.txt; isDirty=editor.value!==lastContent; clip.classList.toggle('dirty',isDirty); status(isDirty?'● 待同步':'已就绪',isDirty?'var(--warn)':null); toast('💡 已回填到文本框'); }; delBtn.onclick=(e)=>{e.stopPropagation();pushHistoryDelete(idx);}; div.appendChild(txtSpan);div.appendChild(timeSpan);div.appendChild(delBtn); histPanel.appendChild(div); }); } async function appendToHistory(newTxt){ if(!newTxt || !newTxt.trim()) return; historyList=historyList.filter(x=>x.txt.trim()!==newTxt.trim()); historyList.unshift({txt: newTxt, time: getTimestamp()}); if(historyList.length>40) historyList.pop(); await saveHistoryToServer(); } async function pushHistoryDelete(index){ historyList.splice(index,1); await saveHistoryToServer(); toast('✓ 历史已清除'); } async function saveHistoryToServer(){ try{ await fetch('history.json',{ method:'PUT', body:JSON.stringify(historyList,null,2), headers:{'Content-Type':'application/json;charset=utf-8'} }); renderHistory(); }catch{toast('✕ 历史记录云同步失败');} } async function saveText(){ if(isSyncing)return;isSyncing=true;const snap=editor.value; saveBtn.textContent='同步中…';saveBtn.disabled=true;status(' 同步中…','var(--accent)'); try{ const r=await fetch('clipboard.txt',{method:'PUT',body:snap,headers:{'Content-Type':'text/plain;charset=utf-8','Cache-Control':'no-cache'}}); if(!r.ok)throw new Error(); lastContent=snap;isDirty=editor.value!==lastContent;clip.classList.toggle('dirty',isDirty); setConn(true);status('✓ 已同步','var(--ok)');toast('✓ 同步成功'); await appendToHistory(snap); setTimeout(()=>{if(!isDirty)status('已就绪');},3000); loadFiles(curPath); }catch{setConn(false);status('⚠ 同步失败','var(--danger)');toast('✕ 同步失败',3200);} finally{isSyncing=false;saveBtn.textContent='↑ 同步';saveBtn.disabled=false;} } async function pollClip(){ if(isSyncing||isDirty||isFocused)return;if(Date.now()-lastTyped<2000)return; try { const r=await fetch('clipboard.txt?t='+Date.now(),{cache:'no-store'});if(!r.ok)throw new Error(); const txt=await r.text();setConn(true); if(lastContent===null){lastContent=txt;editor.value=txt;status('已就绪');loadHistory();return;} if(txt!==lastContent){ lastContent=txt;editor.value=txt;status('↓ 远程已更新','var(--accent)'); await loadHistory(); setTimeout(()=>status('已就绪'),2500); } }catch{setConn(false);if(lastContent===null)status('⚠ 无法连接','var(--danger)');} } document.addEventListener('keydown',e=>{(e.ctrlKey||e.metaKey)&&e.key==='s'&&(e.preventDefault(),saveText());}); /* ── 文件管理逻辑 ── */ const ICONS={ txt:'📄',md:'📝',pdf:'📕',doc:'📘',docx:'📘',xls:'📗',xlsx:'📗',ppt:'📙',pptx:'📙', jpg:'🖼',jpeg:'🖼',png:'🖼',gif:'🖼',webp:'🖼',svg:'🖼',ico:'🖼',mp4:'🎬',mov:'🎬', zip:'📦',rar:'📦','7z':'📦',js:'',ts:'',html:'🌐',css:'🎨',py:'🐍',json:'{}',exe:'⚙',apk:'📱' }; function icon(name,isDir){if(isDir)return'📁';return ICONS[name.split('.').pop().toLowerCase()]||'📄';} function fmtSz(b){ if(b==null)return '';if(b<1024)return b+'B'; if(b<1<<20)return(b/1024).toFixed(1)+'KB';if(b<1<<30)return(b/(1<<20)).toFixed(1)+'MB'; return(b/(1<<30)).toFixed(2)+'GB'; } function fmtT(iso){ if(!iso)return '';const d=new Date(iso),p=n=>String(n).padStart(2,'0'),now=new Date(); if(d.toDateString()===now.toDateString())return'今天 '+p(d.getHours())+':'+p(d.getMinutes()); return(d.getMonth()+1)+'/'+d.getDate()+' '+p(d.getHours())+':'+p(d.getMinutes()); } function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} function renderBc(path){ const bc=$('bc');bc.innerHTML='';const parts=path.replace(/^\/|\/$/g,'').split('/').filter(Boolean); const mkItem=(txt,cb,cur)=>{ const s=document.createElement('span');s.className='bc-item'+(cur?' cur':'');s.textContent=txt; if(!cur)s.onclick=cb;return s; }; bc.appendChild(mkItem('🏠 根目录',()=>navTo('/'),path==='/')); let cum=''; parts.forEach((p,i)=>{ cum+='/'+p;bc.appendChild(document.createRange().createContextualFragment(' / ')); const cp=cum;bc.appendChild(mkItem(decodeURIComponent(p),()=>navTo(cp),i===parts.length-1)); }); } function changeSort(type){ currentSort=type; document.querySelectorAll('.sort-btn').forEach(b=>b.classList.remove('active')); $('sb-'+type).classList.add('active'); savedScrollPos = window.scrollY; renderFiles(allFiles); if(savedScrollPos > 0) { requestAnimationFrame(()=>{window.scrollTo(0, savedScrollPos); savedScrollPos=0;}); } } async function loadFiles(path){ renderBc(path); fl.innerHTML=`
  • 加载中…
  • `; try{ const base = location.origin + (path.endsWith('/') ? path : path + '/'); const r=await fetch(base + '?json&t='+Date.now(),{cache:'no-store'}); if(!r.ok)throw new Error(); const d=await r.json(); allFiles=d.paths||[]; renderFiles(allFiles); if (savedScrollPos > 0) { requestAnimationFrame(() => { window.scrollTo(0, savedScrollPos); savedScrollPos = 0; }); } }catch{ fl.innerHTML=`
  • 加载失败,请重试
  • `; } } function renderFiles(files){ const q=searchQ.trim().toLowerCase(); const list=q?files.filter(f=>(f.name||'').toLowerCase().includes(q)):files; if(!list.length){fl.innerHTML=`
  • ${q?'无匹配':'空目录'}
  • `;return;} const sorted=[...list].sort((a,b)=>{ if(a.is_dir!==b.is_dir)return a.is_dir?-1:1; if(currentSort==='time')return new Date(b.mtime||0)-new Date(a.mtime||0); if(currentSort==='size')return(b.size||0)-(a.size||0); return(a.name||'').localeCompare(b.name||'','zh'); }); fl.innerHTML=''; sorted.forEach((item,i)=>{ const li=document.createElement('li');li.className='frow';li.style.animationDelay=Math.min(i*16,180)+'ms'; const rawName = item.name; if(!rawName || rawName === "null") return; const baseDir = curPath.endsWith('/') ? curPath : curPath + '/'; let href = location.origin + baseDir + encodeURIComponent(rawName); if(item.is_dir && !href.endsWith('/')) href += '/'; const name=decodeURIComponent(rawName),meta=[fmtSz(item.size),fmtT(item.mtime)].filter(Boolean).join(' · '); li.innerHTML=`
    ${icon(rawName,item.is_dir)}
    ${esc(name)}
    ${meta?`
    ${meta}
    `:''}
    `; const dlBtn = li.querySelector('.btn-dl'); const delBtn = li.querySelector('.btn-del'); dlBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); dlFile(href, name); }; delBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); askDel(name, href); }; if(item.is_dir){ li.querySelector('.fclick').addEventListener('click',e=>{ e.preventDefault();navTo(baseDir + rawName); }); } fl.appendChild(li); }); } function navTo(path){ let safePath = path; if(!safePath.startsWith('/')) safePath = '/' + safePath; curPath = safePath; searchQ = ''; fs.value = ''; loadFiles(safePath); } function dlFile(href,name){const a=document.createElement('a');a.href=href;a.download=name;a.click();} /* ── 删除模块 ── */ let pendUrl=null; function askDel(name,href){ if(!href || href.endsWith('/null') || href === "null") { toast('✕ 路径异常,取消操作'); return; } pendUrl=href;$('cmsg').textContent=name;$('cov').classList.add('show'); } function closeConfirm(){$('cov').classList.remove('show');pendUrl=null;} $('cok').onclick = doDel; async function doDel(){ if(!pendUrl || pendUrl.endsWith('/null') || pendUrl === "null") return closeConfirm(); const targetUrl = pendUrl; closeConfirm(); savedScrollPos = window.scrollY; try{ const r=await fetch(targetUrl,{method:'DELETE'}); if(!r.ok) throw new Error(); toast('✓ 已删除'); loadFiles(curPath); }catch{toast('✕ 删除失败:文件可能被占用',3000);} } document.addEventListener('keydown',e=>e.key==='Escape'&&closeConfirm()); $('cov').onclick=e=>e.target===$('cov')&&closeConfirm(); /* ── 上传模块 ── */ fileInput.addEventListener('change',()=>{if(fileInput.files.length)upload(Array.from(fileInput.files));fileInput.value='';}); fmc.addEventListener('dragover',e=>{e.preventDefault();fmc.classList.add('dragover');}); fmc.addEventListener('dragleave',()=>fmc.classList.remove('dragover')); fmc.addEventListener('drop',e=>{e.preventDefault();fmc.classList.remove('dragover');const files=Array.from(e.dataTransfer.files);if(files.length)upload(files);}); async function upload(files){ const bw=$('ubar-w'),bar=$('ubar');bw.classList.add('on');bar.style.width='0%';let done=0; const baseDir = curPath.endsWith('/') ? curPath : curPath + '/'; savedScrollPos = window.scrollY; for(const f of files){ try{await fetch(location.origin + baseDir + encodeURIComponent(f.name),{method:'PUT',body:f,headers:{'Content-Type':f.type||'application/octet-stream'}});}catch{} bar.style.width=(++done/files.length*100)+'%'; } toast(`✓ 上传完成 (${files.length} 个文件)`); setTimeout(()=>{bw.classList.remove('on');bar.style.width='0%';},700); loadFiles(curPath); } fs.addEventListener('input',()=>{searchQ=fs.value;renderFiles(allFiles);}); window.addEventListener('load',()=>{pollClip();setInterval(pollClip,2500);loadFiles('/');}); </script> </html>
    <!-- gh-comment-id:4470987184 --> @kamkdd commented on GitHub (May 17, 2026): <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>剪贴板</title> <!-- 核心:动态把 ⌘ 符号变成浏览器的 Favicon 网站图标,并支持跟随系统的明亮/暗黑模式 --> <!-- 升级版:使用 dominant-baseline 和 text-anchor 确保 ⌘ 符号在浏览器标签栏绝对上下左右居中 --> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><style>@media (prefers-color-scheme: dark) { text { fill: %234493f8; } }</style><text x='50' y='50' font-size='76' dominant-baseline='central' text-anchor='middle' fill='%230d6efd'>⌘</text></svg>"> <link rel="alternate icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text x='50' y='50' font-size='76' dominant-baseline='central' text-anchor='middle' fill='%230d6efd'>⌘</text></svg>"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"> <style> /* ── 默认明亮主题 (Light Mode) ── */ :root { --bg: #f8f9fa; --surface: #ffffff; --surface2: #f1f3f5; --border: rgba(0,0,0,0.06); --border-hi: rgba(0,0,0,0.11); --text: #212529; --text-dim: #495057; --text-muted: #adb5bd; --accent: #0d6efd; --accent-g: rgba(13,110,253,0.08); --accent2: #198754; --warn: #b58900; --danger: #dc3545; --ok: #198754; --r-sm: 8px; --r: 12px; --r-lg: 16px; --mono: 'DM Mono', 'SF Mono', 'Consolas', monospace; --sans: 'DM Sans', system-ui, sans-serif; --hover: rgba(0,0,0,0.02); --hover-active: rgba(0,0,0,0.05); --shadow: rgba(0,0,0,0.06); } /* ── 自动跟随系统暗黑主题 (Dark Mode) ── */ @media (prefers-color-scheme: dark) { :root { --bg: #0d1117; --surface: #161b22; --surface2: #1c2128; --border: rgba(255,255,255,0.08); --border-hi: rgba(255,255,255,0.14); --text: #cdd9e5; --text-dim: #768390; --text-muted: #444c56; --accent: #4493f8; --accent-g: rgba(68,147,248,0.15); --accent2: #3fb950; --warn: #d29922; --danger: #f85149; --ok: #3fb950; --hover: rgba(255,255,255,0.03); --hover-active: rgba(255,255,255,0.055); --shadow: rgba(0,0,0,0.45); } } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} html,body{ min-height:100dvh; background:var(--bg); color:var(--text); font-family:var(--sans); font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; transition:background .3s, color .3s; } /* 全局最外层精致滚动条 */ ::-webkit-scrollbar{width:6px;height:6px;} ::-webkit-scrollbar-track{background:transparent;} ::-webkit-scrollbar-thumb{background:var(--border-hi);border-radius:3px;} ::-webkit-scrollbar-thumb:hover{background:var(--text-muted);} .shell{ max-width:840px; margin:0 auto; padding:20px 16px 60px; display:flex; flex-direction:column; gap:0; } .hdr{display:flex;align-items:center;gap:10px;margin-bottom:24px;} .logo{ width:30px;height:30px;border-radius:8px;flex-shrink:0; background:linear-gradient(135deg,var(--accent) 0%,var(--accent2) 100%); display:flex;align-items:center;justify-content:center; font-family:var(--mono);font-size:13px;color:#fff; box-shadow:0 0 18px rgba(68,147,248,0.3); } .hdr-title{ font-family:var(--mono);font-size:11px;letter-spacing:.1em; text-transform:uppercase;color:var(--text-dim); } .hdr-spacer{flex:1;} .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted);transition:all .4s;} .dot.ok {background:var(--ok);box-shadow:0 0 6px var(--ok);} .dot.err{background:var(--danger);} .slabel{ font-family:var(--mono);font-size:10px;font-weight:500; color:var(--text-muted);letter-spacing:.12em;text-transform:uppercase; padding:0 2px;margin-bottom:7px; } .clip{ background:var(--surface);border:1px solid var(--border); border-radius:var(--r-lg);padding:4px;margin-bottom:12px; box-shadow:0 4px 20px var(--shadow);transition:border-color .2s; } .clip:focus-within{border-color:rgba(13,110,253,.4);} @media (prefers-color-scheme: dark) { .clip:focus-within{border-color:rgba(68,147,248,.4);} } .clip.dirty{border-color:rgba(210,153,34,.45);} textarea{ width:100%;height:140px;background:transparent;border:none;outline:none;resize:none; color:var(--text);font-family:var(--mono);font-size:13.5px;line-height:1.7;padding:12px 14px; } textarea::placeholder{color:var(--text-muted);} .clip-bar{display:flex;align-items:center;gap:6px;padding:4px 10px 8px;} #indicator{ font-family:var(--mono);font-size:11px;color:var(--text-dim); flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } .btn{ display:inline-flex;align-items:center;gap:4px;padding:6px 13px; border:1px solid var(--border-hi);border-radius:20px;font-size:12px;font-weight:500; cursor:pointer;user-select:none;background:var(--surface2);color:var(--text-dim); transition:all .12s;-webkit-tap-highlight-color:transparent;white-space:nowrap; } .btn:active{transform:scale(.92);} .btn:disabled{opacity:.4;cursor:not-allowed;transform:none;} .btn-red{color:var(--danger);border-color:rgba(248,81,73,.25);} .btn-red:hover:not(:disabled){background:rgba(248,81,73,.1);} .btn-blue{background:var(--accent);color:#fff;border-color:transparent;box-shadow:0 2px 14px rgba(68,147,248,.3);} .btn-blue:hover:not(:disabled){opacity:0.9;color:#fff;} /* ── 历史记录面板 ── */ .hist-panel{ background:var(--surface);border:1px solid var(--border);border-radius:var(--r); margin-bottom:24px;padding:6px;box-shadow:0 2px 12px var(--shadow); max-height:190px;overflow-y:auto;display:flex;flex-direction:column;gap:4px; } .hist-item{ display:flex;align-items:center;gap:12px; padding:7px 12px;border-radius:var(--r-sm);cursor:pointer;transition:all .15s; } .hist-item:hover{background:var(--surface2);} .hist-text{ flex:1;min-width:0;font-family:var(--mono);font-size:12px; color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } .hist-item:hover .hist-text{color:var(--text);} .hist-time{ font-family:var(--mono);font-size:11px;color:var(--text-muted); white-space:nowrap;user-select:none; } .hist-del{ width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center; font-size:10px;color:var(--text-muted);cursor:pointer;transition:all .15s;background:transparent;border:none;flex-shrink:0; } .hist-del:hover{background:rgba(248,81,73,.1);color:var(--danger);} #toast{ position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(10px); background:var(--surface2);color:var(--text);border:1px solid var(--border-hi); padding:9px 20px;border-radius:25px;font-family:var(--mono);font-size:12px; pointer-events:none;z-index:500;opacity:0;transition:all .22s;box-shadow:0 8px 30px var(--shadow); } #toast.show{opacity:1;transform:translateX(-50%) translateY(0);} /* ── 文件管理面板 ── */ .fmc{ background:var(--surface);border:1px solid var(--border);border-radius:var(--r-lg); overflow:hidden;box-shadow:0 4px 20px var(--shadow); } .fmc.dragover{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-g);} .ftop{display:flex;align-items:center;padding:10px 12px;background:var(--surface2);border-bottom:1px solid var(--border);min-height:46px;flex-wrap:wrap;gap:8px;} .bc{display:flex;align-items:center;flex-wrap:wrap;font-family:var(--mono);font-size:12px;color:var(--text-dim);flex:1;min-width:120px;gap:1px;} .bc-item{padding:2px 5px;border-radius:5px;cursor:pointer;white-space:nowrap;transition:.15s;} .bc-item:hover{color:var(--accent);background:var(--accent-g);} .bc-item.cur{color:var(--text);cursor:default;background:transparent;} .bc-sep{color:var(--border-hi);} .sort-grp{display:inline-flex;background:var(--surface);border:1px solid var(--border-hi);border-radius:20px;padding:2px;} .sort-btn{font-size:11px;padding:3px 10px;border-radius:16px;cursor:pointer;color:var(--text-dim);user-select:none;transition:.15s;font-weight:500;} .sort-btn.active{background:var(--surface2);color:var(--accent);} .fsw{position:relative;flex-shrink:0;} .fs{ background:var(--surface);border:1px solid var(--border-hi);border-radius:20px;outline:none; color:var(--text);font-family:var(--mono);font-size:12px;padding:5px 11px 5px 28px;width:120px;transition:.2s; } .fs:focus{width:160px;border-color:rgba(68,147,248,.4);} .fs-ico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:13px;} .up-lbl{ display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border:1px solid var(--border-hi); border-radius:20px;font-size:12px;font-weight:500;cursor:pointer;background:var(--surface);color:var(--text-dim);transition:.15s; } .up-lbl:hover{color:var(--accent2);border-color:rgba(63,185,80,.35);} #file-input{display:none;} #ubar-w{height:2px;background:transparent;overflow:hidden;opacity:0;} #ubar-w.on{opacity:1;} #ubar{height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .3s;} /* 去掉文件列表内部硬编码高度,完美交由最外层包裹滚动 */ .fl{list-style:none;min-height:100px;} .fstate{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:44px 20px;font-family:var(--mono);font-size:12px;color:var(--text-muted);} .fstate .ico{font-size:26px;opacity:.35;} .frow{ display:flex;align-items:center;border-bottom:1px solid var(--border); transition:background .1s;animation:rowIn .16s ease both; } .frow:last-child{border-bottom:none;} .frow:hover{background:var(--hover);} .fclick{ display:flex;align-items:center;gap:10px;flex:1;min-width:0; padding:10px 14px;color:inherit;text-decoration:none;-webkit-tap-highlight-color:transparent; } .ficon{width:32px;height:32px;border-radius:var(--r-sm);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--surface2);} .ficon.d{background:var(--accent-g);} .finfo{flex:1;min-width:0;} .fname{font-size:13.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .fmeta{font-family:var(--mono);font-size:11px;color:var(--text-muted);margin-top:1px;} .factions{display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .15s;flex-shrink:0;padding-right:14px;} .frow:hover .factions{opacity:1;} .ibtn{ width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--surface); color:var(--text-dim);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s; } .ibtn:hover{background:var(--accent-g);color:var(--accent);border-color:var(--accent);transform:translateY(-1px);} .ibtn.del:hover{background:rgba(248,81,73,.08);color:var(--danger);border-color:rgba(248,81,73,.3);} @media(max-width:580px){ .factions{opacity:1;} .fs{width:90px;}.fs:focus{width:110px;} .hdr-title{display:none;}.ftop{gap:6px;} } #cov{position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(5px);display:flex;align-items:center;justify-content:center;z-index:400;opacity:0;pointer-events:none;transition:opacity .2s;} #cov.show{opacity:1;pointer-events:auto;} .cbox{background:var(--surface);border:1px solid var(--border-hi);border-radius:var(--r-lg);padding:22px 26px;max-width:320px;width:90%;box-shadow:0 20px 60px var(--shadow);transform:translateY(10px);transition:transform .2s;} #cov.show .cbox{transform:none;} .ctitle{font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px;} .cmsg{font-size:12px;font-family:var(--mono);color:var(--text-dim);word-break:break-all;margin-bottom:18px;line-height:1.5;} .cbtns{display:flex;gap:8px;justify-content:flex-end;} </style> </head> <body> <div id="toast"></div> <div id="cov"> <div class="cbox"> <div class="ctitle">确认删除</div> <div class="cmsg" id="cmsg"></div> <div class="cbtns"> <button class="btn" onclick="closeConfirm()">取消</button> <button class="btn btn-red" id="cok">删除</button> </div> </div> </div> <div class="shell"> <header class="hdr"> <div class="logo">⌘</div> <span class="hdr-title">剪贴板 · 局域网剪贴板</span> <div class="hdr-spacer"></div> <div class="dot" id="dot"></div> </header> <div class="slabel">// 剪贴板同步</div> <div class="clip" id="clip"> <textarea id="editor" placeholder="在此输入内容,自动同步到局域网内所有设备…" spellcheck="false"></textarea> <div class="clip-bar"> <span id="indicator">连接中…</span> <button class="btn btn-red" onclick="clearText()">✕ 清空</button> <button class="btn" onclick="copyText()">⎘ 复制</button> <button class="btn btn-blue" id="saveBtn" onclick="saveText()">↑ 同步</button> </div> </div> <div class="slabel">// 历史记录 (安全云同步)</div> <div class="hist-panel" id="histPanel"> <div style="padding:16px;text-align:center;color:var(--text-muted);font-size:12px;font-family:var(--mono);">暂无历史记录</div> </div> <div class="slabel">// 文件管理</div> <div class="fmc" id="fmc"> <div id="ubar-w"><div id="ubar"></div></div> <div class="ftop"> <div class="bc" id="bc"></div> <div class="sort-grp"> <span class="sort-btn active" id="sb-time" onclick="changeSort('time')">时间</span> <span class="sort-btn" id="sb-name" onclick="changeSort('name')">名称</span> <span class="sort-btn" id="sb-size" onclick="changeSort('size')">大小</span> </div> <div class="fsw"> <span class="fs-ico">⌕</span> <input class="fs" id="fs" type="search" placeholder="搜索…" autocomplete="off" spellcheck="false"> </div> <label class="up-lbl">↑ 上传<input id="file-input" type="file" multiple></label> </div> <ul class="fl" id="fl"> <li class="fstate"><div class="ico">◌</div><span>正在加载…</span></li> </ul> </div> </div> <script> "use strict"; const $=id=>document.getElementById(id); const editor=$('editor'),saveBtn=$('saveBtn'),indicator=$('indicator'); const toastEl=$('toast'),dot=$('dot'),clip=$('clip'),histPanel=$('histPanel'); const fmc=$('fmc'),fl=$('fl'),fs=$('fs'),fileInput=$('file-input'); let lastContent=null,isDirty=false,isSyncing=false,isFocused=false; let lastTyped=0,tTimer=null,historyList=[]; let curPath='/',allFiles=[],searchQ='',currentSort='time'; let savedScrollPos = 0; function toast(msg,ms=2300){ clearTimeout(tTimer);toastEl.textContent=msg;toastEl.classList.add('show'); tTimer=setTimeout(()=>toastEl.classList.remove('show'),ms); } function status(txt,col){indicator.textContent=txt;indicator.style.color=col||'var(--text-dim)';} function setConn(ok){dot.className='dot '+(ok?'ok':'err');} editor.addEventListener('input',()=>{ lastTyped=Date.now();if(lastContent===null)return; const c=editor.value!==lastContent; if(c!==isDirty){ isDirty=c;clip.classList.toggle('dirty',isDirty); status(isDirty?'● 待同步':'已就绪',isDirty?'var(--warn)':null); } }); editor.addEventListener('focus',()=>isFocused=true); editor.addEventListener('blur', ()=>isFocused=false); function clearText(){editor.value='';editor.dispatchEvent(new Event('input'));editor.focus();} async function copyText(){ if(!editor.value){toast('编辑框为空');return;} try{await navigator.clipboard.writeText(editor.value);} catch{editor.select();document.execCommand('copy');} toast('⎘ 已复制'); } function getTimestamp(){ const d=new Date(); const p=n=>String(n).padStart(2,'0'); return `${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } async function loadHistory(){ try{ const r=await fetch('history.json?t='+Date.now(),{cache:'no-store'}); if(r.ok){ const data=await r.json(); historyList = data.map(item => (typeof item === 'string') ? {txt: item, time: getTimestamp()} : item); } }catch{ historyList=[]; } renderHistory(); } function renderHistory(){ if(!historyList || !historyList.length){ histPanel.innerHTML=`<div style="padding:16px;text-align:center;color:var(--text-muted);font-size:12px;font-family:var(--mono);">暂无历史记录</div>`; return; } histPanel.innerHTML=''; historyList.forEach((item,idx)=>{ const div=document.createElement('div');div.className='hist-item'; const txtSpan=document.createElement('span');txtSpan.className='hist-text'; txtSpan.textContent=item.txt.trim() || '[空文本]'; const timeSpan=document.createElement('span');timeSpan.className='hist-time'; timeSpan.textContent=item.time || ''; const delBtn=document.createElement('button');delBtn.className='hist-del'; delBtn.innerHTML='✕'; div.onclick=()=>{ editor.value=item.txt; isDirty=editor.value!==lastContent; clip.classList.toggle('dirty',isDirty); status(isDirty?'● 待同步':'已就绪',isDirty?'var(--warn)':null); toast('💡 已回填到文本框'); }; delBtn.onclick=(e)=>{e.stopPropagation();pushHistoryDelete(idx);}; div.appendChild(txtSpan);div.appendChild(timeSpan);div.appendChild(delBtn); histPanel.appendChild(div); }); } async function appendToHistory(newTxt){ if(!newTxt || !newTxt.trim()) return; historyList=historyList.filter(x=>x.txt.trim()!==newTxt.trim()); historyList.unshift({txt: newTxt, time: getTimestamp()}); if(historyList.length>40) historyList.pop(); await saveHistoryToServer(); } async function pushHistoryDelete(index){ historyList.splice(index,1); await saveHistoryToServer(); toast('✓ 历史已清除'); } async function saveHistoryToServer(){ try{ await fetch('history.json',{ method:'PUT', body:JSON.stringify(historyList,null,2), headers:{'Content-Type':'application/json;charset=utf-8'} }); renderHistory(); }catch{toast('✕ 历史记录云同步失败');} } async function saveText(){ if(isSyncing)return;isSyncing=true;const snap=editor.value; saveBtn.textContent='同步中…';saveBtn.disabled=true;status('⏳ 同步中…','var(--accent)'); try{ const r=await fetch('clipboard.txt',{method:'PUT',body:snap,headers:{'Content-Type':'text/plain;charset=utf-8','Cache-Control':'no-cache'}}); if(!r.ok)throw new Error(); lastContent=snap;isDirty=editor.value!==lastContent;clip.classList.toggle('dirty',isDirty); setConn(true);status('✓ 已同步','var(--ok)');toast('✓ 同步成功'); await appendToHistory(snap); setTimeout(()=>{if(!isDirty)status('已就绪');},3000); loadFiles(curPath); }catch{setConn(false);status('⚠ 同步失败','var(--danger)');toast('✕ 同步失败',3200);} finally{isSyncing=false;saveBtn.textContent='↑ 同步';saveBtn.disabled=false;} } async function pollClip(){ if(isSyncing||isDirty||isFocused)return;if(Date.now()-lastTyped<2000)return; try { const r=await fetch('clipboard.txt?t='+Date.now(),{cache:'no-store'});if(!r.ok)throw new Error(); const txt=await r.text();setConn(true); if(lastContent===null){lastContent=txt;editor.value=txt;status('已就绪');loadHistory();return;} if(txt!==lastContent){ lastContent=txt;editor.value=txt;status('↓ 远程已更新','var(--accent)'); await loadHistory(); setTimeout(()=>status('已就绪'),2500); } }catch{setConn(false);if(lastContent===null)status('⚠ 无法连接','var(--danger)');} } document.addEventListener('keydown',e=>{(e.ctrlKey||e.metaKey)&&e.key==='s'&&(e.preventDefault(),saveText());}); /* ── 文件管理逻辑 ── */ const ICONS={ txt:'📄',md:'📝',pdf:'📕',doc:'📘',docx:'📘',xls:'📗',xlsx:'📗',ppt:'📙',pptx:'📙', jpg:'🖼',jpeg:'🖼',png:'🖼',gif:'🖼',webp:'🖼',svg:'🖼',ico:'🖼',mp4:'🎬',mov:'🎬', zip:'📦',rar:'📦','7z':'📦',js:'⚡',ts:'⚡',html:'🌐',css:'🎨',py:'🐍',json:'{}',exe:'⚙',apk:'📱' }; function icon(name,isDir){if(isDir)return'📁';return ICONS[name.split('.').pop().toLowerCase()]||'📄';} function fmtSz(b){ if(b==null)return '';if(b<1024)return b+'B'; if(b<1<<20)return(b/1024).toFixed(1)+'KB';if(b<1<<30)return(b/(1<<20)).toFixed(1)+'MB'; return(b/(1<<30)).toFixed(2)+'GB'; } function fmtT(iso){ if(!iso)return '';const d=new Date(iso),p=n=>String(n).padStart(2,'0'),now=new Date(); if(d.toDateString()===now.toDateString())return'今天 '+p(d.getHours())+':'+p(d.getMinutes()); return(d.getMonth()+1)+'/'+d.getDate()+' '+p(d.getHours())+':'+p(d.getMinutes()); } function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');} function renderBc(path){ const bc=$('bc');bc.innerHTML='';const parts=path.replace(/^\/|\/$/g,'').split('/').filter(Boolean); const mkItem=(txt,cb,cur)=>{ const s=document.createElement('span');s.className='bc-item'+(cur?' cur':'');s.textContent=txt; if(!cur)s.onclick=cb;return s; }; bc.appendChild(mkItem('🏠 根目录',()=>navTo('/'),path==='/')); let cum=''; parts.forEach((p,i)=>{ cum+='/'+p;bc.appendChild(document.createRange().createContextualFragment('<span class="bc-sep"> / </span>')); const cp=cum;bc.appendChild(mkItem(decodeURIComponent(p),()=>navTo(cp),i===parts.length-1)); }); } function changeSort(type){ currentSort=type; document.querySelectorAll('.sort-btn').forEach(b=>b.classList.remove('active')); $('sb-'+type).classList.add('active'); savedScrollPos = window.scrollY; renderFiles(allFiles); if(savedScrollPos > 0) { requestAnimationFrame(()=>{window.scrollTo(0, savedScrollPos); savedScrollPos=0;}); } } async function loadFiles(path){ renderBc(path); fl.innerHTML=`<li class="fstate"><div class="ico">◌</div><span>加载中…</span></li>`; try{ const base = location.origin + (path.endsWith('/') ? path : path + '/'); const r=await fetch(base + '?json&t='+Date.now(),{cache:'no-store'}); if(!r.ok)throw new Error(); const d=await r.json(); allFiles=d.paths||[]; renderFiles(allFiles); if (savedScrollPos > 0) { requestAnimationFrame(() => { window.scrollTo(0, savedScrollPos); savedScrollPos = 0; }); } }catch{ fl.innerHTML=`<li class="fstate"><div class="ico">⚠</div><span>加载失败,请重试</span></li>`; } } function renderFiles(files){ const q=searchQ.trim().toLowerCase(); const list=q?files.filter(f=>(f.name||'').toLowerCase().includes(q)):files; if(!list.length){fl.innerHTML=`<li class="fstate"><div class="ico">∅</div><span>${q?'无匹配':'空目录'}</span></li>`;return;} const sorted=[...list].sort((a,b)=>{ if(a.is_dir!==b.is_dir)return a.is_dir?-1:1; if(currentSort==='time')return new Date(b.mtime||0)-new Date(a.mtime||0); if(currentSort==='size')return(b.size||0)-(a.size||0); return(a.name||'').localeCompare(b.name||'','zh'); }); fl.innerHTML=''; sorted.forEach((item,i)=>{ const li=document.createElement('li');li.className='frow';li.style.animationDelay=Math.min(i*16,180)+'ms'; const rawName = item.name; if(!rawName || rawName === "null") return; const baseDir = curPath.endsWith('/') ? curPath : curPath + '/'; let href = location.origin + baseDir + encodeURIComponent(rawName); if(item.is_dir && !href.endsWith('/')) href += '/'; const name=decodeURIComponent(rawName),meta=[fmtSz(item.size),fmtT(item.mtime)].filter(Boolean).join(' · '); li.innerHTML=` <a class="fclick" href="${item.is_dir?'javascript:void(0);':href}" ${item.is_dir?'':'target="_blank"'}> <div class="ficon${item.is_dir?' d':''}">${icon(rawName,item.is_dir)}</div> <div class="finfo"> <div class="fname" title="${esc(name)}">${esc(name)}</div> ${meta?`<div class="fmeta">${meta}</div>`:''} </div> </a> <div class="factions"> <button class="ibtn btn-dl" title="下载"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> </button> <button class="ibtn del btn-del" title="删除"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg> </button> </div>`; const dlBtn = li.querySelector('.btn-dl'); const delBtn = li.querySelector('.btn-del'); dlBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); dlFile(href, name); }; delBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); askDel(name, href); }; if(item.is_dir){ li.querySelector('.fclick').addEventListener('click',e=>{ e.preventDefault();navTo(baseDir + rawName); }); } fl.appendChild(li); }); } function navTo(path){ let safePath = path; if(!safePath.startsWith('/')) safePath = '/' + safePath; curPath = safePath; searchQ = ''; fs.value = ''; loadFiles(safePath); } function dlFile(href,name){const a=document.createElement('a');a.href=href;a.download=name;a.click();} /* ── 删除模块 ── */ let pendUrl=null; function askDel(name,href){ if(!href || href.endsWith('/null') || href === "null") { toast('✕ 路径异常,取消操作'); return; } pendUrl=href;$('cmsg').textContent=name;$('cov').classList.add('show'); } function closeConfirm(){$('cov').classList.remove('show');pendUrl=null;} $('cok').onclick = doDel; async function doDel(){ if(!pendUrl || pendUrl.endsWith('/null') || pendUrl === "null") return closeConfirm(); const targetUrl = pendUrl; closeConfirm(); savedScrollPos = window.scrollY; try{ const r=await fetch(targetUrl,{method:'DELETE'}); if(!r.ok) throw new Error(); toast('✓ 已删除'); loadFiles(curPath); }catch{toast('✕ 删除失败:文件可能被占用',3000);} } document.addEventListener('keydown',e=>e.key==='Escape'&&closeConfirm()); $('cov').onclick=e=>e.target===$('cov')&&closeConfirm(); /* ── 上传模块 ── */ fileInput.addEventListener('change',()=>{if(fileInput.files.length)upload(Array.from(fileInput.files));fileInput.value='';}); fmc.addEventListener('dragover',e=>{e.preventDefault();fmc.classList.add('dragover');}); fmc.addEventListener('dragleave',()=>fmc.classList.remove('dragover')); fmc.addEventListener('drop',e=>{e.preventDefault();fmc.classList.remove('dragover');const files=Array.from(e.dataTransfer.files);if(files.length)upload(files);}); async function upload(files){ const bw=$('ubar-w'),bar=$('ubar');bw.classList.add('on');bar.style.width='0%';let done=0; const baseDir = curPath.endsWith('/') ? curPath : curPath + '/'; savedScrollPos = window.scrollY; for(const f of files){ try{await fetch(location.origin + baseDir + encodeURIComponent(f.name),{method:'PUT',body:f,headers:{'Content-Type':f.type||'application/octet-stream'}});}catch{} bar.style.width=(++done/files.length*100)+'%'; } toast(`✓ 上传完成 (${files.length} 个文件)`); setTimeout(()=>{bw.classList.remove('on');bar.style.width='0%';},700); loadFiles(curPath); } fs.addEventListener('input',()=>{searchQ=fs.value;renderFiles(allFiles);}); window.addEventListener('load',()=>{pollClip();setInterval(pollClip,2500);loadFiles('/');}); </script> </body> </html>
    Sign in to join this conversation.
    1 Participants
    Notifications
    Due Date
    No due date set.
    Dependencies

    No dependencies set.

    Reference: sigoden/dufs#5947