<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Archive — 文件服务</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌿</text></svg>"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet"> <style> *, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
:root { --bg: #f2f7f5; --bg-alt: #e9f0ec; --surface: #ffffff; --surface-2: #f8fbfa; --border: #d8e4de; --border-light:#e4ede8; --accent: #059669; --accent-h: #047857; --accent-bg: #ecfdf5; --accent-bg-2: #d1fae5; --folder: #d97706; --folder-bg: #fffbeb; --text: #111827; --text-2: #4b5563; --text-3: #9ca3af; --white: #ffffff; --radius: 14px; --radius-sm: 10px; --radius-xs: 7px; --shadow: 0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.03); --shadow-md: 0 4px 16px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04); --shadow-lg: 0 8px 30px rgba(0,0,0,0.08); --sans: 'DM Sans', sans-serif; --serif: 'Outfit', sans-serif; --mono: 'JetBrains Mono', monospace; --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
html { font-size: 15px; }
body { font-family: var(--sans); background: var(--bg); background-image: radial-gradient(ellipse at 25% -10%, rgba(16,185,129,0.07) 0%, transparent 50%), radial-gradient(ellipse at 85% 110%, rgba(59,130,246,0.05) 0%, transparent 45%); color: var(--text); min-height: 100vh; -webkit-font-smoothing: antialiased; }
a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } ::selection { background: var(--accent-bg-2); color: var(--accent-h); } ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.app { max-width: 1100px; margin: 0 auto; padding: 0 28px; display: flex; flex-direction: column; min-height: 100vh; }
.header { display: flex; align-items: center; gap: 18px; padding: 24px 0 20px; }
.nav-group { display: flex; align-items: center; gap: 6px; }
.logo-back { display: none; width: 34px; height: 34px; align-items: center; justify-content: center; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-xs); color: var(--text-3); cursor: pointer; transition: all var(--transition); box-shadow: var(--shadow); } .logo-back svg { width: 16px; height: 16px; } .logo-back:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); } .nav-group.has-parent .logo-back { display: flex; }
.logo { display: flex; align-items: center; gap: 10px; font-family: var(--serif); font-size: 1.55rem; font-weight: 600; color: var(--text); white-space: nowrap; user-select: none; text-decoration: none; } .logo:hover { text-decoration: none; color: var(--text); }
.logo-icon { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; background: var(--accent-bg); border-radius: var(--radius-xs); color: var(--accent); transition: all var(--transition); } .logo-icon svg { width: 20px; height: 20px; } .logo:hover .logo-icon { background: var(--accent-bg-2); transform: scale(1.05); }
.search-wrap { flex: 1; position: relative; max-width: 380px; margin-left: auto; } .search-wrap svg { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-3); pointer-events: none; } .search { width: 100%; padding: 10px 14px 10px 40px; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font: 400 0.87rem var(--sans); outline: none; transition: all var(--transition); box-shadow: var(--shadow); } .search::placeholder { color: var(--text-3); } .search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(5,150,105,0.1), var(--shadow); }
.header-actions { display: flex; align-items: center; gap: 8px; }
.view-btns { display: flex; gap: 4px; } .view-btn { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-xs); cursor: pointer; color: var(--text-3); transition: all var(--transition); box-shadow: var(--shadow); } .view-btn:hover { color: var(--text-2); } .view-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); } .view-btn svg { width: 18px; height: 18px; }
.user-menu { position: relative; }
.user-btn { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-xs); cursor: pointer; color: var(--text-2); transition: all var(--transition); box-shadow: var(--shadow); }
.user-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
.user-btn svg { width: 18px; height: 18px; }
.user-dropdown { position: absolute; top: calc(100% + 8px); right: 0; background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-lg); min-width: 160px; opacity: 0; visibility: hidden; transform: translateY(-8px); transition: all 0.2s ease; z-index: 100; }
.user-dropdown.show { opacity: 1; visibility: visible; transform: translateY(0); }
.user-dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 0.85rem; color: var(--text-2); cursor: pointer; transition: all 0.15s ease; }
.user-dropdown-item:first-child { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.user-dropdown-item:last-child { border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
.user-dropdown-item:hover { background: var(--accent-bg); color: var(--accent); }
.user-dropdown-item svg { width: 16px; height: 16px; }
.user-dropdown-divider { height: 1px; background: var(--border-light); margin: 4px 0; }
.user-dropdown-item.danger { color: #dc2626; }
.user-dropdown-item.danger:hover { background: #fef2f2; color: #dc2626; }
.breadcrumb { display: flex; align-items: center; gap: 0; padding: 14px 0 12px; font-size: .82rem; flex-wrap: wrap; } .crumb { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 20px; color: var(--text-3); transition: all var(--transition); } .crumb:hover { background: var(--accent-bg); color: var(--accent); text-decoration: none; } .crumb.current { background: var(--accent-bg); color: var(--accent); font-weight: 500; } .crumb-sep { color: var(--text-3); font-size: .7rem; opacity: .4; margin: 0 2px; }
.file-card { background: var(--surface); border: 1.5px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md); overflow: hidden; flex: 1; display: flex; flex-direction: column; }
.list-header { display: grid; grid-template-columns: 1fr 110px 170px 44px; gap: 12px; padding: 10px 20px; font-size: .72rem; font-weight: 500; letter-spacing: .05em; text-transform: uppercase; color: var(--text-3); background: var(--surface-2); border-bottom: 1.5px solid var(--border-light); cursor: pointer; user-select: none; } .list-header span:hover { color: var(--text-2); } .list-header .sort-icon { font-size: .55rem; margin-left: 3px; opacity: .5; } .grid-header { display: none; }
.file-list { flex: 1; display: flex; flex-direction: column; }
.file-row { display: grid; grid-template-columns: 1fr 110px 170px 44px; gap: 12px; padding: 10px 20px; align-items: center; border-bottom: 1px solid var(--border-light); transition: background var(--transition); cursor: pointer; opacity: 0; animation: fadeUp .35s ease forwards; } .file-row:last-child { border-bottom: none; } .file-row:hover { background: var(--accent-bg); }
.file-name { display: flex; align-items: center; gap: 12px; min-width: 0; color: var(--text); } .file-name .name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-icon { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-xs); transition: transform var(--transition); } .file-row:hover .file-icon { transform: scale(1.08); } .file-icon svg { width: 18px; height: 18px; } .file-size { font: 300 .8rem var(--mono); color: var(--text-2); } .file-date { font: 300 .8rem var(--mono); color: var(--text-3); } .file-actions { display: flex; justify-content: center; }
.btn-copy { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: transparent; border: 1.5px solid transparent; cursor: pointer; color: var(--text-3); border-radius: var(--radius-xs); transition: all var(--transition); } .btn-copy:hover { color: var(--accent); background: var(--accent-bg); border-color: var(--accent-bg-2); } .btn-copy.copied { color: var(--accent); } .btn-copy svg { width: 15px; height: 15px; }
.file-icon.type-folder { background: var(--folder-bg); color: var(--folder); } .file-icon.type-image { background: #fef2f2; color: #ef4444; } .file-icon.type-archive { background: #f5f3ff; color: #8b5cf6; } .file-icon.type-code { background: #ecfdf5; color: #10b981; } .file-icon.type-doc { background: #eff6ff; color: #3b82f6; } .file-icon.type-audio { background: #fffbeb; color: #f59e0b; } .file-icon.type-video { background: #fdf2f8; color: #ec4899; } .file-icon.type-text { background: #f8fafc; color: #64748b; } .file-icon.type-file { background: #f8fafc; color: #94a3b8; }
.app.grid-mode .list-header { display: none; } .app.grid-mode .grid-header { display: block; padding: 10px 20px 14px; font-size: .78rem; color: var(--text-3); background: var(--surface-2); border-bottom: 1.5px solid var(--border-light); } .app.grid-mode .file-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(175px, 1fr)); gap: 14px; padding: 18px; } .app.grid-mode .file-row { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 22px 14px 16px; background: var(--surface-2); border: 1.5px solid var(--border-light); border-radius: var(--radius-sm); text-align: center; position: relative; } .app.grid-mode .file-row:hover { border-color: var(--accent); background: var(--accent-bg); box-shadow: var(--shadow-md); } .app.grid-mode .file-icon { width: 50px; height: 50px; border-radius: var(--radius-sm); } .app.grid-mode .file-icon svg { width: 24px; height: 24px; } .app.grid-mode .file-name { flex-direction: column; gap: 2px; } .app.grid-mode .file-name .name-text { max-width: 100%; font-size: .82rem; } .app.grid-mode .file-size { font-size: .72rem; } .app.grid-mode .file-date { display: none; } .app.grid-mode .file-actions { position: absolute; top: 8px; right: 8px; }
.footer { padding: 18px 0; margin-top: auto; font-size: .78rem; color: var(--text-3); display: flex; justify-content: space-between; align-items: center; } .footer-tag { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; background: var(--accent-bg); color: var(--accent); border-radius: 20px; font-size: .7rem; font-weight: 500; } .footer-tag svg { width: 12px; height: 12px; }
.state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 80px 20px; text-align: center; } .state-icon { width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; background: var(--accent-bg); border-radius: 50%; font-size: 1.8rem; } .state.error .state-icon { background: #fef2f2; } .state-title { font-family: var(--serif); font-size: 1.2rem; font-weight: 500; color: var(--text-2); } .state-text { font-size: .85rem; color: var(--text-3); max-width: 320px; line-height: 1.5; }
.loader { display: none; } .loader.show { display: flex; } .loader-bar { width: 100%; max-width: 180px; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; } .loader-bar::after { content:''; display:block; width:35%; height:100%; background: linear-gradient(90deg, var(--accent), #34d399); border-radius: 2px; animation: slide .9s ease-in-out infinite; } @keyframes slide { 0%{transform:translateX(-120%)} 100%{transform:translateX(400%)} }
.toast { position:fixed; bottom:30px; left:50%; transform:translateX(-50%) translateY(60px); padding:10px 22px; background:var(--surface); border:1.5px solid var(--border); border-radius:var(--radius-sm); font-size:.82rem; font-weight:500; color:var(--accent); box-shadow:var(--shadow-lg); opacity:0; transition:all .3s ease; z-index:1000; pointer-events:none; } .toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@keyframes fadeUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
@media (max-width:700px) { .app { padding: 0 16px; } .header { flex-wrap: wrap; gap: 10px; } .search-wrap { max-width: 100%; order: 3; flex-basis: 100%; } .list-header { display: none; } .file-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 12px 16px; } .file-date, .file-actions { display: none; } .app.grid-mode .file-list { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); padding: 12px; } } </style> </head> <body> <div class="app" id="app"> <header class="header"> <div class="nav-group" id="nav-group"> <button class="logo-back" id="logo-back" title="返回上级目录"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> </button> <a class="logo" id="logo-btn" href="#/" title="返回根目录"> <div class="logo-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg> </div> <span>Archive</span> </a> </div>
<div class="search-wrap"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> <input type="text" class="search" id="search" placeholder="搜索文件..." autocomplete="off"> </div>
<div class="header-actions"> <div class="view-btns"> <button class="view-btn active" id="btn-list" title="列表视图"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg> </button> <button class="view-btn" id="btn-grid" title="网格视图"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> </button> </div>
<div class="user-menu"> <button class="user-btn" id="user-btn" title="用户菜单"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> <circle cx="12" cy="7" r="4"/> </svg> </button> <div class="user-dropdown" id="user-dropdown"> <div class="user-dropdown-item" id="user-info"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> <circle cx="12" cy="7" r="4"/> </svg> <span id="username-display">用户</span> </div> <div class="user-dropdown-divider"></div> <div class="user-dropdown-item danger" id="logout-btn"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> <polyline points="16 17 21 12 16 7"/> <line x1="21" y1="12" x2="9" y2="12"/> </svg> <span>退出登录</span> </div> </div> </div> </div> </header>
<nav class="breadcrumb" id="breadcrumb"></nav>
<div class="file-card"> <div class="list-header" id="list-header"> <span data-sort="name">名称 <span class="sort-icon"></span></span> <span data-sort="size">大小 <span class="sort-icon"></span></span> <span data-sort="date">修改时间 <span class="sort-icon"></span></span> <span></span> </div> <div class="grid-header" id="grid-header"></div> <div class="file-list" id="file-list"></div> </div>
<div class="state loader" id="loader"> <div class="loader-bar"></div> </div>
<div class="state error" id="error" style="display:none"> <div class="state-icon">⚠</div> <div class="state-title" id="error-title">加载失败</div> <div class="state-text" id="error-text"></div> </div>
<div class="state" id="empty" style="display:none"> <div class="state-icon">📁</div> <div class="state-title">空文件夹</div> <div class="state-text">这个目录下还没有任何文件</div> </div>
<footer class="footer" id="footer"></footer> </div>
<div class="toast" id="toast"></div>
<script> (() => { 'use strict';
const API = '/api'; const DL = '/dl'; const TOKEN_KEY = 'archive_auth_token'; const USER_KEY = 'archive_username';
function checkAuth() { const token = localStorage.getItem(TOKEN_KEY); const username = localStorage.getItem(USER_KEY); if (!token || !username) { window.location.href = '/login'; return false; } return true; }
function logout() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); localStorage.removeItem('archive_remember'); window.location.href = '/login'; }
function getUsername() { return localStorage.getItem(USER_KEY) || '用户'; }
const ICONS = { folder: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`, file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>`, image: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`, archive: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 8v13H3V8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>`, code: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`, doc: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`, audio: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`, video: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`, text: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`, }; const COPY_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
const TYPE_MAP = { image: ['jpg','jpeg','png','gif','webp','svg','bmp','ico','tiff','avif'], archive: ['zip','tar','gz','bz2','xz','7z','rar','tgz','zst'], code: ['js','ts','py','java','c','cpp','h','go','rs','rb','php','html','css','json','xml','yaml','yml','toml','sh','bash','sql','vue','jsx','tsx','swift','kt'], doc: ['pdf','doc','docx','xls','xlsx','ppt','pptx','odt','ods','odp','rtf'], audio: ['mp3','wav','flac','aac','ogg','wma','m4a'], video: ['mp4','mkv','avi','mov','wmv','flv','webm','m4v'], text: ['txt','md','log','csv','ini','conf','cfg','env','gitignore','dockerignore'], };
function getFileType(name) { const ext = name.split('.').pop().toLowerCase(); for (const [type, exts] of Object.entries(TYPE_MAP)) { if (exts.includes(ext)) return type; } return 'file'; }
function getIcon(item) { if (item.type === 'directory') return { icon: ICONS.folder, cls: 'type-folder' }; const t = getFileType(item.name); return { icon: ICONS[t] || ICONS.file, cls: 'type-' + t }; }
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function fmtSize(bytes) { if (bytes == null) return '—'; if (bytes === 0) return '0 B'; const u = ['B','KB','MB','GB','TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + u[i]; }
function fmtDate(iso) { if (!iso) return '—'; const d = new Date(iso); const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; }
function toast(msg) { const el = document.getElementById('toast'); el.textContent = msg; el.classList.add('show'); clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove('show'), 2000); }
let state = { path: '/', files: [], sortKey: 'name', sortAsc: true, view: 'list', query: '', };
const $app = document.getElementById('app'); const $list = document.getElementById('file-list'); const $bread = document.getElementById('breadcrumb'); const $footer = document.getElementById('footer'); const $loader = document.getElementById('loader'); const $error = document.getElementById('error'); const $empty = document.getElementById('empty'); const $search = document.getElementById('search'); const $listHead = document.getElementById('list-header'); const $gridHead = document.getElementById('grid-header'); const $navGroup = document.getElementById('nav-group'); const $logoBack = document.getElementById('logo-back'); const $logoBtn = document.getElementById('logo-btn'); const $userBtn = document.getElementById('user-btn'); const $userDropdown = document.getElementById('user-dropdown'); const $usernameDisplay = document.getElementById('username-display'); const $logoutBtn = document.getElementById('logout-btn');
function getPath() { let h = location.hash.slice(1) || '/'; if (!h.startsWith('/')) h = '/' + h; if (!h.endsWith('/')) h += '/'; return h; }
function getParentPath(path) { const parts = path.split('/').filter(Boolean); if (parts.length === 0) return '/'; parts.pop(); return '/' + (parts.length ? parts.join('/') + '/' : ''); }
function isRoot() { return state.path === '/' || state.path === ''; }
function navigate(path) { if (state.path === path) { load(); return; } state.path = path; location.hash = '#' + path; load(); }
function updateNavState() { if (isRoot()) { $navGroup.classList.remove('has-parent'); $logoBtn.title = 'Archive'; } else { $navGroup.classList.add('has-parent'); $logoBtn.title = '返回根目录'; } }
async function load() { $list.innerHTML = ''; $error.style.display = 'none'; $empty.style.display = 'none'; $loader.classList.add('show'); updateNavState();
try { const resp = await fetch(API + state.path); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); let data = await resp.json(); if (!Array.isArray(data)) data = []; state.files = data; render(); } catch (e) { $error.style.display = 'flex'; document.getElementById('error-title').textContent = '无法加载目录'; document.getElementById('error-text').textContent = e.message; } finally { $loader.classList.remove('show'); } }
function render() { renderBread(); renderFiles(); renderFooter(); updateSortIndicators(); }
function renderBread() { const parts = state.path.split('/').filter(Boolean); let html = '';
if (parts.length === 0) { html = `<span class="crumb current"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg> Archive </span>`; } else { html = `<a class="crumb" href="#/"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg> Archive </a>`; }
let cumPath = '/'; for (let i = 0; i < parts.length; i++) { cumPath += parts[i] + '/'; html += `<span class="crumb-sep">›</span>`; if (i === parts.length - 1) { html += `<span class="crumb current">${esc(parts[i])}</span>`; } else { html += `<a class="crumb" href="#${cumPath}">${esc(parts[i])}</a>`; } } $bread.innerHTML = html; }
function getFiltered() { let files = [...state.files]; if (state.query) { const q = state.query.toLowerCase(); files = files.filter(f => f.name.toLowerCase().includes(q)); } files.sort((a, b) => { if (a.type === 'directory' && b.type !== 'directory') return -1; if (a.type !== 'directory' && b.type === 'directory') return 1; let va, vb; switch (state.sortKey) { case 'size': va = a.size || 0; vb = b.size || 0; break; case 'date': va = a.mtime || ''; vb = b.mtime || ''; break; default: va = a.name.toLowerCase(); vb = b.name.toLowerCase(); } if (va < vb) return state.sortAsc ? -1 : 1; if (va > vb) return state.sortAsc ? 1 : -1; return 0; }); return files; }
function renderFiles() { const filtered = getFiltered();
if (state.files.length === 0) { $empty.style.display = 'flex'; $empty.querySelector('.state-title').textContent = '空文件夹'; $empty.querySelector('.state-text').textContent = '这个目录下还没有任何文件'; $list.innerHTML = ''; return; } if (filtered.length === 0) { $list.innerHTML = ''; $empty.style.display = 'flex'; $empty.querySelector('.state-title').textContent = '无匹配结果'; $empty.querySelector('.state-text').textContent = `没有找到包含 "${state.query}" 的文件`; return; } $empty.style.display = 'none';
let html = ''; filtered.forEach((item, i) => { const { icon, cls } = getIcon(item); const isFolder = item.type === 'directory'; const href = isFolder ? `#${state.path}${item.name}/` : `${DL}${state.path}${encodeURIComponent(item.name)}`;
html += ` <div class="file-row" style="animation-delay:${i * 0.025}s" data-href="${esc(href)}" data-folder="${isFolder}"> <div class="file-name"> <div class="file-icon ${cls}">${icon}</div> <span class="name-text" title="${esc(item.name)}">${esc(item.name)}</span> </div> <div class="file-size">${isFolder ? '—' : fmtSize(item.size)}</div> <div class="file-date">${fmtDate(item.mtime)}</div> <div class="file-actions"> ${isFolder ? '' : `<button class="btn-copy" data-link="${esc(href)}" title="复制下载链接">${COPY_ICON}</button>`} </div> </div>`; }); $list.innerHTML = html; $gridHead.textContent = `${filtered.length} 个项目`; }
function renderFooter() { const total = state.files.reduce((s, f) => s + (f.size || 0), 0); const dirs = state.files.filter(f => f.type === 'directory').length; const files = state.files.length - dirs; let parts = []; if (dirs) parts.push(`${dirs} 个文件夹`); if (files) parts.push(`${files} 个文件`); if (total) parts.push(fmtSize(total)); $footer.innerHTML = ` <span>${parts.join(' · ') || '空目录'}</span> <span class="footer-tag"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> Cloudflare Tunnel </span>`; }
function updateSortIndicators() { document.querySelectorAll('.list-header span[data-sort]').forEach(el => { const key = el.dataset.sort; const icon = el.querySelector('.sort-icon'); icon.textContent = (key === state.sortKey) ? (state.sortAsc ? '↑' : '↓') : ''; }); }
$logoBtn.addEventListener('click', e => { e.preventDefault(); navigate('/'); });
$logoBack.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); navigate(getParentPath(state.path)); });
$list.addEventListener('click', e => { const btn = e.target.closest('.btn-copy'); if (btn) { e.stopPropagation(); const link = location.origin + btn.dataset.link; navigator.clipboard.writeText(link).then(() => { btn.classList.add('copied'); toast('链接已复制到剪贴板'); setTimeout(() => btn.classList.remove('copied'), 1500); }); return; } const row = e.target.closest('.file-row'); if (!row) return; if (row.dataset.folder === 'true') { navigate(row.dataset.href.slice(1)); } else { const a = document.createElement('a'); a.href = row.dataset.href; a.download = ''; a.click(); } });
$search.addEventListener('input', () => { state.query = $search.value.trim(); renderFiles(); });
$listHead.addEventListener('click', e => { const col = e.target.closest('[data-sort]'); if (!col) return; const key = col.dataset.sort; if (state.sortKey === key) { state.sortAsc = !state.sortAsc; } else { state.sortKey = key; state.sortAsc = true; } renderFiles(); updateSortIndicators(); });
document.getElementById('btn-list').addEventListener('click', () => { state.view = 'list'; $app.classList.remove('grid-mode'); document.getElementById('btn-list').classList.add('active'); document.getElementById('btn-grid').classList.remove('active'); }); document.getElementById('btn-grid').addEventListener('click', () => { state.view = 'grid'; $app.classList.add('grid-mode'); document.getElementById('btn-grid').classList.add('active'); document.getElementById('btn-list').classList.remove('active'); });
$userBtn.addEventListener('click', (e) => { e.stopPropagation(); $userDropdown.classList.toggle('show'); });
document.addEventListener('click', () => { $userDropdown.classList.remove('show'); });
$logoutBtn.addEventListener('click', () => { logout(); });
document.addEventListener('keydown', e => { if (e.key === '/' && document.activeElement !== $search) { e.preventDefault(); $search.focus(); } if (e.key === 'Escape') { $search.value = ''; state.query = ''; $search.blur(); renderFiles(); } if (e.key === 'Backspace' && document.activeElement !== $search) { e.preventDefault(); const parent = getParentPath(state.path); if (parent !== state.path) navigate(parent); } });
window.addEventListener('hashchange', () => { const newPath = getPath(); if (newPath !== state.path) { state.path = newPath; load(); } });
if (!checkAuth()) { return; }
$usernameDisplay.textContent = getUsername(); state.path = getPath(); load(); })(); </script> </body> </html>
|