feat: add mobile transfer and drag upload
This commit is contained in:
@@ -21,7 +21,7 @@ body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}
|
||||
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)}
|
||||
.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)}.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}.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)}
|
||||
.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}.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>
|
||||
<body>
|
||||
@@ -31,11 +31,12 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
|
||||
<button class="ghost" id="logoutBtn" onclick="logout()" hidden>退出</button>
|
||||
</header>
|
||||
<main>
|
||||
<div id="dropZone" class="card" style="display:none;padding:24px;text-align:center;margin-bottom:14px;border:2px dashed var(--border)">将文件或文件夹拖到这里上传</div>
|
||||
<div id="serverNotice">__CHFS_NOTICE__</div><div id="notice"></div>
|
||||
<div class="toolbar">
|
||||
<button onclick="goUp()">上一级</button><button onclick="refresh()">刷新</button><button class="ghost" onclick="showRoots()">位置</button>
|
||||
<button id="newFolderBtn" onclick="newFolder()">新建文件夹</button><button id="newTextBtn" onclick="newText()">新建文本</button>
|
||||
<label><button id="uploadBtn">上传</button><input id="upload" type="file" multiple hidden></label>
|
||||
<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>
|
||||
</div>
|
||||
@@ -65,7 +66,7 @@ function setView(v){localStorage.setItem('chfs-view',v);$("files").parentElement
|
||||
function itemMenu(e,p){e.preventDefault();const f=lastItems.find(x=>(current?current+'/':'')+x.name===p);const canRead=(f?.mask||'').includes('R'),canWrite=(f?.mask||'').includes('W'),canDelete=(f?.mask||'').includes('D');let html='';if(canRead)html+='<button onclick="downloadFile('+JSON.stringify(p)+');closeContext()">下载</button>';if(f?.dir&&canRead)html+='<button onclick="downloadDir('+JSON.stringify(p)+');closeContext()">下载 ZIP</button>';if(canWrite)html+='<button onclick="renameItem('+JSON.stringify(p)+');closeContext()">重命名</button>';if(canDelete)html+='<button onclick="removeItem('+JSON.stringify(p)+');closeContext()">删除</button>';if(f?.guestMask?.includes('R'))html+='<button onclick="shareItem('+JSON.stringify(p)+');closeContext()">分享</button>';if(!html)html='<div class="meta" style="padding:8px">没有可用操作</div>';$("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'}
|
||||
addEventListener('click',e=>{if(!e.target.closest('#context')&&!e.target.closest('.tilemenu'))closeContext()});
|
||||
function bulkDownload(){if(!selected.size)return;const first=[...selected][0];if(selected.size===1){downloadFile(first);return}location.href='/chfs/downloaddir/'+encodeURI(first.split('/').slice(0,-1).join('/'))}
|
||||
function bulkDownload(){if(!selected.size)return;const q=[...selected].map(p=>'path='+encodeURIComponent(p)).join('&');location.href='/chfs/downloadzip?'+q}
|
||||
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){alert(p+': '+e.message)}}selected.clear();refresh()}
|
||||
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){alert(e.message)}}
|
||||
function jumpRoot(p){current=p;closeModal();refresh()}
|
||||
@@ -77,7 +78,10 @@ async function renameItem(p){const name=p.split("/").pop();const n=await ask("
|
||||
async function removeItem(p){if(!await confirmBox("确认删除","确定删除 "+p+"?"))return;try{await api("/chfs/rmfiles",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){alert(e.message)}}
|
||||
async function newFolder(){const n=await ask("新建文件夹","文件夹名称");if(!n)return;const p=current?current+"/"+n:n;try{await api("/chfs/newdir",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:p})});refresh()}catch(e){alert(e.message)}}
|
||||
async function newText(){const n=await ask("新建文本","文件名","new.txt");if(!n)return;const text=await ask("新建文本","文本内容");if(text===null)return;try{await api("/chfs/newtext",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({filepath:current,title:n,text:text||""})});refresh()}catch(e){alert(e.message)}}
|
||||
$("upload").addEventListener("change",async e=>{const files=[...e.target.files];if(!files.length)return;show('<h2>上传文件</h2><div id="uploadList"></div><div class="row"><button class="ghost" onclick="closeModal()">后台继续</button></div>');const box=$("uploadList");for(const file of files){const row=document.createElement("div");row.className="upload-row";row.innerHTML='<b>'+esc(file.name)+'</b><span class="meta">'+fmt(file.size)+' · 准备上传</span><div class="progress"><i></i></div>';box.appendChild(row);await uploadOne(file,row)}e.target.value="";refresh()})
|
||||
$("upload").addEventListener("change",async e=>{await uploadFiles(e.target.files);e.target.value=""})
|
||||
async function uploadFiles(files){const list=[...files];if(!list.length)return;show('<h2>上传文件</h2><div id="uploadList"></div><div class="row"><button class="ghost" onclick="closeModal()">后台继续</button></div>');const box=$("uploadList");for(const file of list){const row=document.createElement("div");row.className="upload-row";row.innerHTML='<b>'+esc(file.name)+'</b><span class="meta">'+fmt(file.size)+' · 准备上传</span><div class="progress"><i></i></div>';box.appendChild(row);await uploadOne(file,row)}refresh()}
|
||||
function showMobileTransfer(){const u=new URL(location.href);u.hash='folder='+encodeURIComponent(current);show('<h2>手机传输</h2><div class="notice">用手机扫描二维码打开当前文件夹。登录后即可按你的权限上传、下载和管理文件。</div><img class="qr" src="/chfs/qrcode?url='+encodeURIComponent(u.href)+'" alt="QR Code"><div class="field"><input value="'+esc(u.href)+'" readonly></div><div class="row"><button class="ghost" onclick="closeModal()">关闭</button></div>')}
|
||||
function setupDropZone(){const z=$("dropZone"),main=$("fileCard");let depth=0;[document,main].forEach(el=>{el.addEventListener('dragenter',e=>{if(!e.dataTransfer?.types.includes('Files'))return;e.preventDefault();depth++;z.style.display='block';z.classList.add('drop-active')});el.addEventListener('dragover',e=>{if(e.dataTransfer?.types.includes('Files'))e.preventDefault()});el.addEventListener('dragleave',e=>{if(!e.dataTransfer?.types.includes('Files'))return;depth--;if(depth<=0){depth=0;z.classList.remove('drop-active')}});el.addEventListener('drop',async e=>{if(!e.dataTransfer?.files?.length)return;e.preventDefault();depth=0;z.classList.remove('drop-active');z.style.display='none';await uploadFiles(e.dataTransfer.files)})})}
|
||||
function uploadOne(file,row){return new Promise(resolve=>{const xhr=new XMLHttpRequest(),fd=new FormData();fd.append("folder",current);fd.append("file",file);const meta=row.querySelector('.meta'),bar=row.querySelector('i');xhr.open('POST','/chfs/upload');xhr.upload.onprogress=e=>{if(e.lengthComputable){const pct=Math.round(e.loaded/e.total*100);bar.style.width=pct+'%';meta.textContent=fmt(file.size)+' · '+pct+'%'}};xhr.onload=()=>{if(xhr.status>=200&&xhr.status<300){bar.style.width='100%';meta.textContent=fmt(file.size)+' · 完成'}else{meta.textContent='上传失败';bar.style.width='0'}};xhr.onerror=()=>{meta.textContent='网络错误';bar.style.width='0'};xhr.onloadend=resolve;xhr.send(fd)})}
|
||||
$("search").addEventListener("keydown",async e=>{if(e.key!=="Enter")return;const s=e.target.value.trim();if(!s){refresh();return}try{const d=await api("/chfs/search?str="+encodeURIComponent(s));renderSearch(d.files||[])}catch(x){alert(x.message)}})
|
||||
function renderSearch(items){$("crumb").textContent="搜索结果";const body=$("files");body.innerHTML="";for(const f of items){const row=document.createElement("tr");row.innerHTML='<td><div class="name">🔎 <a href="#" onclick="openDir('+JSON.stringify(f.dir?f.path:f.path.split("/").slice(0,-1).join("/")).replace(/^"/,'\\"')+');return false">'+esc(f.path)+'</a></div></td><td class="meta">'+(f.dir?"—":fmt(f.size))+'</td><td class="meta">'+esc(f.modified||"")+'</td><td></td>';body.appendChild(row)}}
|
||||
|
||||
Reference in New Issue
Block a user