// 全局变量
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');
}
}