feat: 初始化FTP服务器项目 - 支持Web管理界面

This commit is contained in:
Your Name
2026-05-06 17:32:38 +08:00
commit 1d36000b80
11 changed files with 2714 additions and 0 deletions
+498
View File
@@ -0,0 +1,498 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f0f2f5;
color: #333;
}
/* 登录页面 */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: #fff;
border-radius: 12px;
padding: 40px;
width: 400px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
}
.login-card h1 {
font-size: 28px;
margin-bottom: 8px;
color: #1a1a2e;
}
.login-card p {
color: #888;
margin-bottom: 30px;
}
/* 主布局 */
.main-app {
display: flex;
min-height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 220px;
background: #1a1a2e;
color: #fff;
display: flex;
flex-direction: column;
position: fixed;
height: 100vh;
z-index: 100;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h2 {
font-size: 20px;
font-weight: 600;
}
.sidebar-menu {
list-style: none;
flex: 1;
padding: 10px 0;
}
.sidebar-menu li {
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
color: rgba(255, 255, 255, 0.7);
}
.sidebar-menu li:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.sidebar-menu li.active {
background: rgba(255, 255, 255, 0.15);
color: #fff;
border-left: 3px solid #667eea;
}
.sidebar-menu .icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.sidebar-footer {
padding: 15px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 内容区 */
.content {
margin-left: 220px;
flex: 1;
padding: 30px;
}
.page {
display: none;
}
.page.active {
display: block;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
}
h2 {
font-size: 22px;
margin-bottom: 20px;
color: #1a1a2e;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.stat-card.wide {
grid-column: span 3;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #888;
}
/* 表格 */
.data-table {
width: 100%;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-collapse: collapse;
}
.data-table th {
background: #f8f9fa;
padding: 12px 16px;
text-align: left;
font-weight: 600;
font-size: 13px;
color: #666;
border-bottom: 2px solid #eee;
}
.data-table td {
padding: 10px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.data-table tr:hover td {
background: #f8f9fa;
}
.data-table .status-enabled {
color: #52c41a;
font-weight: 600;
}
.data-table .status-disabled {
color: #ff4d4f;
font-weight: 600;
}
/* 按钮 */
.btn {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn:hover {
border-color: #667eea;
color: #667eea;
}
.btn-primary {
background: #667eea;
color: #fff;
border-color: #667eea;
}
.btn-primary:hover {
background: #5a6fd6;
color: #fff;
}
.btn-danger {
background: #ff4d4f;
color: #fff;
border-color: #ff4d4f;
}
.btn-danger:hover {
background: #e04346;
color: #fff;
}
.btn-sm {
padding: 4px 12px;
font-size: 13px;
}
.btn-block {
width: 100%;
padding: 12px;
font-size: 16px;
}
/* 表单 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 14px;
color: #333;
}
.form-group small {
display: block;
margin-top: 4px;
color: #999;
font-size: 12px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.input-sm {
padding: 6px 10px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 13px;
}
.input-sm:focus {
outline: none;
border-color: #667eea;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
/* 对话框 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #fff;
border-radius: 12px;
width: 520px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.modal-close {
font-size: 24px;
cursor: pointer;
color: #999;
line-height: 1;
}
.modal-close:hover {
color: #333;
}
.modal-content form {
padding: 24px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* 面包屑 */
.breadcrumb {
background: #fff;
padding: 10px 16px;
border-radius: 6px;
margin-bottom: 12px;
font-size: 14px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.breadcrumb span {
cursor: pointer;
color: #667eea;
}
.breadcrumb span:hover {
text-decoration: underline;
}
/* 过滤栏 */
.filter-bar {
display: flex;
gap: 10px;
align-items: center;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
.pagination button {
padding: 6px 14px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.pagination button.active {
background: #667eea;
color: #fff;
border-color: #667eea;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 文件操作 */
.file-actions {
display: flex;
gap: 10px;
}
/* 设置 */
.settings-section {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.settings-section h3 {
margin-bottom: 16px;
font-size: 16px;
color: #1a1a2e;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
/* Toast提示 */
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 8px;
color: #fff;
font-size: 14px;
z-index: 2000;
display: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.toast.success {
background: #52c41a;
display: block;
}
.toast.error {
background: #ff4d4f;
display: block;
}
/* 操作按钮组 */
.action-btns {
display: flex;
gap: 4px;
}
.action-btns .btn {
padding: 4px 8px;
font-size: 12px;
}
/* 文件图标 */
.file-icon {
margin-right: 6px;
}
.dir-link {
color: #667eea;
cursor: pointer;
}
.dir-link:hover {
text-decoration: underline;
}
+341
View File
@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FTP Server 管理</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<!-- 登录页面 -->
<div id="login-page" class="login-page">
<div class="login-card">
<h1>FTP Server</h1>
<p>Web 管理控制台</p>
<form id="login-form">
<div class="form-group">
<label>用户名</label>
<input type="text" id="login-username" value="admin" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="login-password" value="admin123" required>
</div>
<button type="submit" class="btn btn-primary btn-block">登 录</button>
</form>
</div>
</div>
<!-- 主界面 -->
<div id="main-app" class="main-app" style="display:none">
<!-- 侧边栏 -->
<nav class="sidebar">
<div class="sidebar-header">
<h2>FTP Server</h2>
</div>
<ul class="sidebar-menu">
<li class="active" data-page="dashboard">
<span class="icon">&#9632;</span> 仪表盘
</li>
<li data-page="users">
<span class="icon">&#9775;</span> 用户管理
</li>
<li data-page="files">
<span class="icon">&#128193;</span> 文件管理
</li>
<li data-page="logs">
<span class="icon">&#128196;</span> 操作日志
</li>
<li data-page="online">
<span class="icon">&#128279;</span> 在线用户
</li>
<li data-page="settings">
<span class="icon">&#9881;</span> 系统设置
</li>
</ul>
<div class="sidebar-footer">
<button id="logout-btn" class="btn btn-sm">退出登录</button>
</div>
</nav>
<!-- 内容区 -->
<main class="content">
<!-- 仪表盘 -->
<div id="page-dashboard" class="page active">
<h2>仪表盘</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="stat-users">-</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-enabled">-</div>
<div class="stat-label">启用用户</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-online">-</div>
<div class="stat-label">在线用户</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-today-logins">-</div>
<div class="stat-label">今日登录</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-today-uploads">-</div>
<div class="stat-label">今日上传</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-today-downloads">-</div>
<div class="stat-label">今日下载</div>
</div>
</div>
<div class="stats-grid" style="margin-top:20px">
<div class="stat-card wide">
<div class="stat-value" id="stat-upload-bytes">-</div>
<div class="stat-label">总上传量</div>
</div>
<div class="stat-card wide">
<div class="stat-value" id="stat-download-bytes">-</div>
<div class="stat-label">总下载量</div>
</div>
</div>
</div>
<!-- 用户管理 -->
<div id="page-users" class="page">
<div class="page-header">
<h2>用户管理</h2>
<button class="btn btn-primary" onclick="showAddUser()">添加用户</button>
</div>
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>主目录</th>
<th>权限</th>
<th>配额</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
<!-- 文件管理 -->
<div id="page-files" class="page">
<div class="page-header">
<h2>文件管理</h2>
<div class="file-actions">
<button class="btn btn-primary" onclick="uploadFile()">上传文件</button>
<button class="btn" onclick="createFolder()">新建文件夹</button>
</div>
</div>
<div class="breadcrumb" id="file-breadcrumb">
<span>/</span>
</div>
<table class="data-table">
<thead>
<tr>
<th>名称</th>
<th>大小</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="files-tbody"></tbody>
</table>
</div>
<!-- 操作日志 -->
<div id="page-logs" class="page">
<div class="page-header">
<h2>操作日志</h2>
<div class="filter-bar">
<input type="text" id="log-username" placeholder="用户名筛选" class="input-sm">
<select id="log-action" class="input-sm">
<option value="">全部操作</option>
<option value="login">登录</option>
<option value="login_failed">登录失败</option>
<option value="upload">上传</option>
<option value="download">下载</option>
<option value="delete">删除</option>
</select>
<button class="btn btn-sm" onclick="loadLogs()">查询</button>
</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>IP</th>
<th>操作</th>
<th>文件路径</th>
<th>大小</th>
<th>状态</th>
</tr>
</thead>
<tbody id="logs-tbody"></tbody>
</table>
<div class="pagination" id="logs-pagination"></div>
</div>
<!-- 在线用户 -->
<div id="page-online" class="page">
<h2>在线用户</h2>
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>IP地址</th>
<th>登录时间</th>
<th>最后活动</th>
<th>当前目录</th>
</tr>
</thead>
<tbody id="online-tbody"></tbody>
</table>
</div>
<!-- 系统设置 -->
<div id="page-settings" class="page">
<h2>系统设置</h2>
<div class="settings-section">
<h3>FTP设置</h3>
<div class="form-grid">
<div class="form-group">
<label>FTP端口</label>
<input type="number" id="cfg-ftp-port" class="input-sm">
</div>
<div class="form-group">
<label>被动端口范围(起始)</label>
<input type="number" id="cfg-ftp-passive-min" class="input-sm">
</div>
<div class="form-group">
<label>被动端口范围(结束)</label>
<input type="number" id="cfg-ftp-passive-max" class="input-sm">
</div>
<div class="form-group">
<label>最大连接数</label>
<input type="number" id="cfg-ftp-max-conn" class="input-sm">
</div>
<div class="form-group">
<label>空闲超时(秒)</label>
<input type="number" id="cfg-ftp-idle-timeout" class="input-sm">
</div>
<div class="form-group">
<label>启用匿名访问</label>
<select id="cfg-ftp-anonymous" class="input-sm">
<option value="true"></option>
<option value="false"></option>
</select>
</div>
</div>
</div>
<div class="settings-section">
<h3>管理员设置</h3>
<div class="form-grid">
<div class="form-group">
<label>管理员用户名</label>
<input type="text" id="cfg-admin-username" class="input-sm">
</div>
<div class="form-group">
<label>新密码(留空不修改)</label>
<input type="password" id="cfg-admin-password" class="input-sm" placeholder="留空不修改">
</div>
</div>
</div>
<button class="btn btn-primary" onclick="saveConfig()" style="margin-top:20px">保存设置</button>
</div>
</main>
</div>
<!-- 添加/编辑用户对话框 -->
<div id="user-modal" class="modal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h3 id="user-modal-title">添加用户</h3>
<span class="modal-close" onclick="closeUserModal()">&times;</span>
</div>
<form id="user-form">
<input type="hidden" id="user-edit-mode" value="add">
<div class="form-group">
<label>用户名</label>
<input type="text" id="user-username" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="user-password" required>
</div>
<div class="form-group">
<label>主目录</label>
<input type="text" id="user-homedir" placeholder="留空自动设置为 /ftp_root/用户名">
<small>目录不存在时将自动创建</small>
</div>
<div class="form-group">
<label>权限</label>
<select id="user-permissions">
<option value="read">只读</option>
<option value="write">只写</option>
<option value="read,write" selected>读写</option>
</select>
</div>
<div class="form-grid">
<div class="form-group">
<label>空间配额(MB, 0=无限制)</label>
<input type="number" id="user-quota-size" value="0">
</div>
<div class="form-group">
<label>文件数配额(0=无限制)</label>
<input type="number" id="user-quota-files" value="0">
</div>
<div class="form-group">
<label>上传限速(KB/s, 0=无限制)</label>
<input type="number" id="user-upload-rate" value="0">
</div>
<div class="form-group">
<label>下载限速(KB/s, 0=无限制)</label>
<input type="number" id="user-download-rate" value="0">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="user-enabled" checked> 启用账户
</label>
</div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeUserModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 文件上传对话框 -->
<div id="upload-modal" class="modal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h3>上传文件</h3>
<span class="modal-close" onclick="closeUploadModal()">&times;</span>
</div>
<form id="upload-form">
<div class="form-group">
<input type="file" id="upload-file" required>
</div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeUploadModal()">取消</button>
<button type="submit" class="btn btn-primary">上传</button>
</div>
</form>
</div>
</div>
<!-- 提示消息 -->
<div id="toast" class="toast"></div>
<script src="/js/app.js"></script>
</body>
</html>
+460
View File
@@ -0,0 +1,460 @@
// 全局变量
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 '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">&#128193;</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">&#128196;</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();
}