// 全局变量 let token = localStorage.getItem('ftp_token') || ''; let currentPath = ''; let logPage = 1; // --- API 封装 --- async function api(method, url, data) { const opts = { method, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } }; if (data && method !== 'GET') { opts.body = JSON.stringify(data); } const resp = await fetch(url, opts); const json = await resp.json(); if (resp.status === 401) { token = ''; localStorage.removeItem('ftp_token'); showLogin(); throw new Error('登录已过期'); } if (json.error) { throw new Error(json.error); } return json.data; } // --- 工具函数 --- function showToast(msg, type = 'success') { const toast = document.getElementById('toast'); toast.textContent = msg; toast.className = 'toast ' + type; setTimeout(() => { toast.className = 'toast'; }, 3000); } function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; } function formatTime(t) { if (!t) return '-'; return t.replace('T', ' ').substring(0, 19); } function showLogin() { document.getElementById('login-page').style.display = 'flex'; document.getElementById('main-app').style.display = 'none'; } function showMain() { document.getElementById('login-page').style.display = 'none'; document.getElementById('main-app').style.display = 'flex'; } // --- 登录 --- document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; try { const data = await api('POST', '/api/login', { username, password }); token = data.token; localStorage.setItem('ftp_token', token); showMain(); loadDashboard(); } catch (err) { showToast(err.message, 'error'); } }); document.getElementById('logout-btn').addEventListener('click', () => { token = ''; localStorage.removeItem('ftp_token'); showLogin(); }); // --- 导航切换 --- document.querySelectorAll('.sidebar-menu li').forEach(li => { li.addEventListener('click', () => { document.querySelectorAll('.sidebar-menu li').forEach(el => el.classList.remove('active')); li.classList.add('active'); const page = li.dataset.page; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById('page-' + page).classList.add('active'); loadPage(page); }); }); function loadPage(page) { switch (page) { case 'dashboard': loadDashboard(); break; case 'users': loadUsers(); break; case 'files': loadFiles(currentPath); break; case 'logs': loadLogs(); break; case 'online': loadOnline(); break; case 'ip-rules': loadIPRules(); break; case 'settings': loadConfig(); break; } } // --- 仪表盘 --- async function loadDashboard() { try { const stats = await api('GET', '/api/dashboard'); document.getElementById('stat-users').textContent = stats.total_users || 0; document.getElementById('stat-enabled').textContent = stats.enabled_users || 0; document.getElementById('stat-online').textContent = stats.online_users || 0; document.getElementById('stat-today-logins').textContent = stats.today_logins || 0; document.getElementById('stat-today-uploads').textContent = stats.today_uploads || 0; document.getElementById('stat-today-downloads').textContent = stats.today_downloads || 0; document.getElementById('stat-upload-bytes').textContent = formatBytes(stats.total_upload_bytes); document.getElementById('stat-download-bytes').textContent = formatBytes(stats.total_download_bytes); } catch (err) { showToast(err.message, 'error'); } } // --- 用户管理 --- async function loadUsers() { try { const users = await api('GET', '/api/users'); const tbody = document.getElementById('users-tbody'); tbody.innerHTML = users.map(u => ` ${u.id} ${u.username} ${u.home_dir} ${u.permissions} ${u.quota_size > 0 ? formatBytes(u.quota_size) : '无限制'} ${u.enabled ? '启用' : '禁用'} ${formatTime(u.created_at)} `).join(''); } catch (err) { showToast(err.message, 'error'); } } function showAddUser() { document.getElementById('user-modal-title').textContent = '添加用户'; document.getElementById('user-edit-mode').value = 'add'; document.getElementById('user-form').reset(); document.getElementById('user-username').disabled = false; document.getElementById('user-password').required = true; document.getElementById('user-enabled').checked = true; document.getElementById('user-modal').style.display = 'flex'; } async function editUser(username) { try { const user = await api('GET', '/api/users/' + username); document.getElementById('user-modal-title').textContent = '编辑用户'; document.getElementById('user-edit-mode').value = 'edit'; document.getElementById('user-username').value = user.username; document.getElementById('user-username').disabled = true; document.getElementById('user-password').value = ''; document.getElementById('user-password').required = false; document.getElementById('user-homedir').value = user.home_dir; document.getElementById('user-permissions').value = user.permissions; document.getElementById('user-quota-size').value = Math.round(user.quota_size / 1024 / 1024); document.getElementById('user-quota-files').value = user.quota_files; document.getElementById('user-upload-rate').value = user.upload_rate; document.getElementById('user-download-rate').value = user.download_rate; document.getElementById('user-enabled').checked = user.enabled; document.getElementById('user-modal').style.display = 'flex'; } catch (err) { showToast(err.message, 'error'); } } document.getElementById('user-form').addEventListener('submit', async (e) => { e.preventDefault(); const mode = document.getElementById('user-edit-mode').value; const username = document.getElementById('user-username').value; const password = document.getElementById('user-password').value; const homeDir = document.getElementById('user-homedir').value; const quotaMB = parseInt(document.getElementById('user-quota-size').value) || 0; const data = { username, password, home_dir: homeDir, permissions: document.getElementById('user-permissions').value, quota_size: quotaMB * 1024 * 1024, quota_files: parseInt(document.getElementById('user-quota-files').value) || 0, upload_rate: parseInt(document.getElementById('user-upload-rate').value) || 0, download_rate: parseInt(document.getElementById('user-download-rate').value) || 0, enabled: document.getElementById('user-enabled').checked }; try { if (mode === 'add') { await api('POST', '/api/users', data); showToast('用户添加成功'); } else { await api('PUT', '/api/users/' + username, data); if (password) { await api('PUT', '/api/users/' + username + '/password', { password }); } showToast('用户更新成功'); } closeUserModal(); loadUsers(); } catch (err) { showToast(err.message, 'error'); } }); function closeUserModal() { document.getElementById('user-modal').style.display = 'none'; } async function deleteUser(username) { if (!confirm('确定删除用户 "' + username + '" 吗?')) return; try { await api('DELETE', '/api/users/' + username); showToast('用户已删除'); loadUsers(); } catch (err) { showToast(err.message, 'error'); } } async function resetPassword(username) { const password = prompt('请输入新密码:'); if (!password) return; try { await api('PUT', '/api/users/' + username + '/password', { password }); showToast('密码已更新'); } catch (err) { showToast(err.message, 'error'); } } // --- 文件管理 --- async function loadFiles(path) { try { const url = '/api/files?path=' + encodeURIComponent(path || ''); const data = await api('GET', url); currentPath = data.path; const tbody = document.getElementById('files-tbody'); let html = ''; // 返回上级 if (currentPath) { html += ` [根目录] `; } data.files.forEach(f => { if (f.is_dir) { html += ` 📁${f.name} - ${formatTime(f.mod_time)} `; } else { html += ` 📄${f.name} ${formatBytes(f.size)} ${formatTime(f.mod_time)} `; } }); if (!data.files.length && !currentPath) { html = '目录为空'; } tbody.innerHTML = html; // 面包屑 document.getElementById('file-breadcrumb').innerHTML = '/ ' + currentPath.replace(/\\/g, '/').split('/').filter(Boolean).map((p, i, arr) => { const subPath = arr.slice(0, i + 1).join('/'); return '' + p + ''; }).join(' / '); } catch (err) { showToast(err.message, 'error'); } } async function deleteFile(path, isDir) { const name = path.split(/[\\/]/).pop(); if (!confirm('确定删除 "' + name + '" 吗?' + (isDir ? '将删除文件夹内所有内容!' : ''))) return; try { await api('DELETE', '/api/files?path=' + encodeURIComponent(path)); showToast('删除成功'); loadFiles(currentPath); } catch (err) { showToast(err.message, 'error'); } } function uploadFile() { document.getElementById('upload-form').reset(); document.getElementById('upload-modal').style.display = 'flex'; } function closeUploadModal() { document.getElementById('upload-modal').style.display = 'none'; } document.getElementById('upload-form').addEventListener('submit', async (e) => { e.preventDefault(); const fileInput = document.getElementById('upload-file'); if (!fileInput.files.length) return; const formData = new FormData(); formData.append('file', fileInput.files[0]); try { const resp = await fetch('/api/upload?path=' + encodeURIComponent(currentPath), { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); const json = await resp.json(); if (json.error) throw new Error(json.error); showToast('上传成功'); closeUploadModal(); loadFiles(currentPath); } catch (err) { showToast(err.message, 'error'); } }); async function createFolder() { const name = prompt('请输入文件夹名称:'); if (!name) return; try { await api('POST', '/api/files', { path: currentPath, name, type: 'dir' }); showToast('文件夹已创建'); loadFiles(currentPath); } catch (err) { showToast(err.message, 'error'); } } // --- 日志 --- async function loadLogs() { const username = document.getElementById('log-username').value; const action = document.getElementById('log-action').value; try { const data = await api('GET', `/api/logs?username=${encodeURIComponent(username)}&action=${action}&page=${logPage}&page_size=20`); const tbody = document.getElementById('logs-tbody'); if (!data.logs || !data.logs.length) { tbody.innerHTML = '暂无日志'; } else { tbody.innerHTML = data.logs.map(l => { let statusClass = l.status === 'success' ? 'status-enabled' : 'status-disabled'; return ` ${formatTime(l.created_at)} ${l.username || '-'} ${l.ip || '-'} ${l.action} ${l.file_path || '-'} ${l.file_size > 0 ? formatBytes(l.file_size) : '-'} ${l.status} `; }).join(''); } // 分页 const totalPages = Math.ceil(data.total / 20); let pagHtml = ``; pagHtml += `第 ${logPage} / ${totalPages || 1} 页 (共 ${data.total} 条)`; pagHtml += ``; document.getElementById('logs-pagination').innerHTML = pagHtml; } catch (err) { showToast(err.message, 'error'); } } // --- 在线用户 --- async function loadOnline() { try { const users = await api('GET', '/api/online'); const tbody = document.getElementById('online-tbody'); if (!users || !users.length) { tbody.innerHTML = '暂无在线用户'; } else { tbody.innerHTML = users.map(u => ` ${u.username || '-'} ${u.ip} ${formatTime(u.login_time)} ${formatTime(u.last_activity)} ${u.current_dir || '-'} `).join(''); } } catch (err) { showToast(err.message, 'error'); } } // --- 系统设置 --- async function loadConfig() { try { const cfg = await api('GET', '/api/config'); document.getElementById('cfg-ftp-port').value = cfg.ftp.port; document.getElementById('cfg-ftp-passive-min').value = cfg.ftp.passive_port_min; document.getElementById('cfg-ftp-passive-max').value = cfg.ftp.passive_port_max; document.getElementById('cfg-ftp-max-conn').value = cfg.ftp.max_connections; document.getElementById('cfg-ftp-idle-timeout').value = cfg.ftp.idle_timeout; document.getElementById('cfg-ftp-anonymous').value = String(cfg.ftp.enable_anonymous); } catch (err) { showToast(err.message, 'error'); } } async function saveConfig() { const data = { ftp: { port: parseInt(document.getElementById('cfg-ftp-port').value), passive_port_min: parseInt(document.getElementById('cfg-ftp-passive-min').value), passive_port_max: parseInt(document.getElementById('cfg-ftp-passive-max').value), max_connections: parseInt(document.getElementById('cfg-ftp-max-conn').value), idle_timeout: parseInt(document.getElementById('cfg-ftp-idle-timeout').value), enable_anonymous: document.getElementById('cfg-ftp-anonymous').value === 'true' }, admin: { username: document.getElementById('cfg-admin-username').value, password: document.getElementById('cfg-admin-password').value } }; try { await api('PUT', '/api/config', data); showToast('配置已保存,部分设置需要重启生效'); } catch (err) { showToast(err.message, 'error'); } } // --- 初始化 --- if (token) { showMain(); loadDashboard(); } else { showLogin(); } // --- IP规则管理 --- async function loadIPRules() { try { const filter = document.getElementById('ip-rule-filter').value; let url = '/api/ip-rules'; if (filter === 'global') url += '?username=__empty__'; else if (filter === 'user') url += '?username=__has__'; const rules = await api('GET', url); const tbody = document.getElementById('ip-rules-tbody'); if (!rules || !rules.length) { tbody.innerHTML = '暂无IP规则,所有IP默认允许连接'; return; } tbody.innerHTML = rules.map(r => { const scopeLabel = r.username ? `用户: ${r.username}` : '全局'; const typeLabel = r.type === 'whitelist' ? '白名单' : '黑名单'; const statusLabel = r.enabled ? '启用' : '禁用'; return ` ${r.id} ${scopeLabel} ${r.ip} ${typeLabel} ${r.note || '-'} ${statusLabel} ${formatTime(r.created_at)} `; }).join(''); } catch (err) { showToast(err.message, 'error'); } } function showAddIPRule() { document.getElementById('ip-rule-modal-title').textContent = '添加IP规则'; document.getElementById('ip-rule-edit-id').value = ''; document.getElementById('ip-rule-form').reset(); document.getElementById('ip-rule-enabled').checked = true; document.getElementById('ip-rule-modal').style.display = 'flex'; } function editIPRule(id, username, ip, type, note, enabled) { document.getElementById('ip-rule-modal-title').textContent = '编辑IP规则'; document.getElementById('ip-rule-edit-id').value = id; document.getElementById('ip-rule-username').value = username; document.getElementById('ip-rule-ip').value = ip; document.getElementById('ip-rule-type').value = type; document.getElementById('ip-rule-note').value = note; document.getElementById('ip-rule-enabled').checked = enabled; document.getElementById('ip-rule-modal').style.display = 'flex'; } function closeIPRuleModal() { document.getElementById('ip-rule-modal').style.display = 'none'; } document.getElementById('ip-rule-form').addEventListener('submit', async (e) => { e.preventDefault(); const editId = document.getElementById('ip-rule-edit-id').value; const data = { username: document.getElementById('ip-rule-username').value, ip: document.getElementById('ip-rule-ip').value, type: document.getElementById('ip-rule-type').value, note: document.getElementById('ip-rule-note').value, enabled: document.getElementById('ip-rule-enabled').checked }; try { if (editId) { await api('PUT', '/api/ip-rules/' + editId, data); showToast('规则已更新'); } else { await api('POST', '/api/ip-rules', data); showToast('规则添加成功'); } closeIPRuleModal(); loadIPRules(); } catch (err) { showToast(err.message, 'error'); } }); async function toggleIPRule(id, username, ip, type, note, enabled) { try { await api('PUT', '/api/ip-rules/' + id, { username, ip, type, note, enabled: !enabled }); showToast(!enabled ? '规则已启用' : '规则已禁用'); loadIPRules(); } catch (err) { showToast(err.message, 'error'); } } async function deleteIPRule(id) { if (!confirm('确定删除此IP规则吗?')) return; try { await api('DELETE', '/api/ip-rules/' + id); showToast('规则已删除'); loadIPRules(); } catch (err) { showToast(err.message, 'error'); } }