| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- // 全局变量
- 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 => `
- <tr>
- <td>${u.id}</td>
- <td>${u.username}</td>
- <td title="${u.home_dir}">${u.home_dir}</td>
- <td>${u.permissions}</td>
- <td>${u.quota_size > 0 ? formatBytes(u.quota_size) : '无限制'}</td>
- <td><span class="${u.enabled ? 'status-enabled' : 'status-disabled'}">${u.enabled ? '启用' : '禁用'}</span></td>
- <td>${formatTime(u.created_at)}</td>
- <td class="action-btns">
- <button class="btn btn-sm" onclick="editUser('${u.username}')">编辑</button>
- <button class="btn btn-sm" onclick="resetPassword('${u.username}')">改密</button>
- <button class="btn btn-sm btn-danger" onclick="deleteUser('${u.username}')">删除</button>
- </td>
- </tr>
- `).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 += `<tr>
- <td colspan="4"><span class="dir-link" onclick="loadFiles('')">[根目录]</span></td>
- </tr>`;
- }
- data.files.forEach(f => {
- if (f.is_dir) {
- html += `<tr>
- <td><span class="file-icon">📁</span><span class="dir-link" onclick="loadFiles('${f.path.replace(/\\/g, '\\\\')}')">${f.name}</span></td>
- <td>-</td>
- <td>${formatTime(f.mod_time)}</td>
- <td class="action-btns">
- <button class="btn btn-sm btn-danger" onclick="deleteFile('${f.path.replace(/\\/g, '\\\\')}', true)">删除</button>
- </td>
- </tr>`;
- } else {
- html += `<tr>
- <td><span class="file-icon">📄</span>${f.name}</td>
- <td>${formatBytes(f.size)}</td>
- <td>${formatTime(f.mod_time)}</td>
- <td class="action-btns">
- <button class="btn btn-sm btn-danger" onclick="deleteFile('${f.path.replace(/\\/g, '\\\\')}', false)">删除</button>
- </td>
- </tr>`;
- }
- });
- if (!data.files.length && !currentPath) {
- html = '<tr><td colspan="4" style="text-align:center;color:#999;padding:40px">目录为空</td></tr>';
- }
- tbody.innerHTML = html;
- // 面包屑
- document.getElementById('file-breadcrumb').innerHTML = '<span onclick="loadFiles(\'\')">/</span> ' +
- currentPath.replace(/\\/g, '/').split('/').filter(Boolean).map((p, i, arr) => {
- const subPath = arr.slice(0, i + 1).join('/');
- return '<span onclick="loadFiles(\'' + subPath + '\')">' + p + '</span>';
- }).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 = '<tr><td colspan="7" style="text-align:center;color:#999;padding:40px">暂无日志</td></tr>';
- } else {
- tbody.innerHTML = data.logs.map(l => {
- let statusClass = l.status === 'success' ? 'status-enabled' : 'status-disabled';
- return `<tr>
- <td>${formatTime(l.created_at)}</td>
- <td>${l.username || '-'}</td>
- <td>${l.ip || '-'}</td>
- <td>${l.action}</td>
- <td title="${l.file_path}">${l.file_path || '-'}</td>
- <td>${l.file_size > 0 ? formatBytes(l.file_size) : '-'}</td>
- <td><span class="${statusClass}">${l.status}</span></td>
- </tr>`;
- }).join('');
- }
- // 分页
- const totalPages = Math.ceil(data.total / 20);
- let pagHtml = `<button ${logPage <= 1 ? 'disabled' : ''} onclick="logPage=${logPage - 1};loadLogs()">上一页</button>`;
- pagHtml += `<span style="padding:6px 12px">第 ${logPage} / ${totalPages || 1} 页 (共 ${data.total} 条)</span>`;
- pagHtml += `<button ${logPage >= totalPages ? 'disabled' : ''} onclick="logPage=${logPage + 1};loadLogs()">下一页</button>`;
- 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 = '<tr><td colspan="5" style="text-align:center;color:#999;padding:40px">暂无在线用户</td></tr>';
- } else {
- tbody.innerHTML = users.map(u => `
- <tr>
- <td>${u.username || '-'}</td>
- <td>${u.ip}</td>
- <td>${formatTime(u.login_time)}</td>
- <td>${formatTime(u.last_activity)}</td>
- <td>${u.current_dir || '-'}</td>
- </tr>
- `).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 rules = await api('GET', '/api/ip-rules');
- const tbody = document.getElementById('ip-rules-tbody');
- if (!rules || !rules.length) {
- tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:40px">暂无IP规则,所有IP默认允许连接</td></tr>';
- return;
- }
- tbody.innerHTML = rules.map(r => {
- const typeLabel = r.type === 'whitelist'
- ? '<span style="color:#667eea;font-weight:600">白名单</span>'
- : '<span style="color:#ff4d4f;font-weight:600">黑名单</span>';
- const statusLabel = r.enabled
- ? '<span class="status-enabled">启用</span>'
- : '<span class="status-disabled">禁用</span>';
- return `<tr>
- <td>${r.id}</td>
- <td><code style="background:#f5f5f5;padding:2px 6px;border-radius:3px">${r.ip}</code></td>
- <td>${typeLabel}</td>
- <td>${r.note || '-'}</td>
- <td>${statusLabel}</td>
- <td>${formatTime(r.created_at)}</td>
- <td class="action-btns">
- <button class="btn btn-sm" onclick="editIPRule(${r.id}, '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">编辑</button>
- <button class="btn btn-sm" onclick="toggleIPRule(${r.id}, '${r.ip}', '${r.type}', '${(r.note||'').replace(/'/g, "\\'")}', ${r.enabled})">${r.enabled ? '禁用' : '启用'}</button>
- <button class="btn btn-sm btn-danger" onclick="deleteIPRule(${r.id})">删除</button>
- </td>
- </tr>`;
- }).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, ip, type, note, enabled) {
- document.getElementById('ip-rule-modal-title').textContent = '编辑IP规则';
- document.getElementById('ip-rule-edit-id').value = id;
- 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 = {
- 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, ip, type, note, enabled) {
- try {
- await api('PUT', '/api/ip-rules/' + id, {
- 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');
- }
- }
|