feat: 添加SFTP空间使用统计功能

- 新增 /api/user/sftp-usage API,递归统计SFTP服务器空间使用情况
- 返回总使用空间、文件数、文件夹数
- 在设置页面显示SFTP空间统计信息
- 支持手动刷新统计数据
- 适配"仅SFTP"和"用户可选"两种权限模式的UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 13:39:51 +08:00
parent 86ed1f4040
commit 4f9b281039
3 changed files with 203 additions and 29 deletions

View File

@@ -1571,6 +1571,88 @@ app.post('/api/user/update-ftp',
} }
); );
// 获取SFTP存储空间使用情况
app.get('/api/user/sftp-usage', authMiddleware, async (req, res) => {
let sftp = null;
try {
// 检查用户是否配置了SFTP
if (!req.user.has_ftp_config) {
return res.status(400).json({
success: false,
message: '未配置SFTP服务器'
});
}
// 连接SFTP
sftp = await connectToSFTP(req.user);
// 递归计算目录大小的函数
async function calculateDirSize(dirPath) {
let totalSize = 0;
let fileCount = 0;
let dirCount = 0;
try {
const list = await sftp.list(dirPath);
for (const item of list) {
// 跳过 . 和 .. 目录
if (item.name === '.' || item.name === '..') continue;
const itemPath = dirPath === '/' ? `/${item.name}` : `${dirPath}/${item.name}`;
if (item.type === 'd') {
// 是目录,递归计算
dirCount++;
const subResult = await calculateDirSize(itemPath);
totalSize += subResult.totalSize;
fileCount += subResult.fileCount;
dirCount += subResult.dirCount;
} else {
// 是文件,累加大小
fileCount++;
totalSize += item.size || 0;
}
}
} catch (err) {
// 跳过无法访问的目录
console.warn(`[SFTP统计] 无法访问目录 ${dirPath}: ${err.message}`);
}
return { totalSize, fileCount, dirCount };
}
// 从根目录开始计算
const result = await calculateDirSize('/');
res.json({
success: true,
usage: {
totalSize: result.totalSize,
totalSizeFormatted: formatFileSize(result.totalSize),
fileCount: result.fileCount,
dirCount: result.dirCount
}
});
} catch (error) {
console.error('[SFTP统计] 获取失败:', error);
res.status(500).json({
success: false,
message: '获取SFTP空间使用情况失败: ' + error.message
});
} finally {
if (sftp) {
try {
await sftp.end();
} catch (e) {
// 忽略关闭错误
}
}
}
});
// 修改管理员账号信息(仅管理员可修改用户名) // 修改管理员账号信息(仅管理员可修改用户名)
app.post('/api/admin/update-profile', app.post('/api/admin/update-profile',
authMiddleware, authMiddleware,

View File

@@ -1281,6 +1281,29 @@
<div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; padding: 10px; border-radius: 8px; margin-bottom: 10px;"> <div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; padding: 10px; border-radius: 8px; margin-bottom: 10px;">
<i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换 <i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换
</div> </div>
<!-- SFTP空间使用统计user_choice模式 -->
<div v-if="user?.has_ftp_config" style="margin-bottom: 10px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: #64748b;">空间统计</span>
<button
style="background: none; border: none; color: #4b5fc9; cursor: pointer; font-size: 12px; padding: 2px 6px;"
@click.stop="loadSftpUsage()"
:disabled="sftpUsageLoading">
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
</button>
</div>
<div v-if="sftpUsageLoading && !sftpUsage" style="text-align: center; color: #667eea; font-size: 12px;">
<i class="fas fa-spinner fa-spin"></i> 统计中...
</div>
<div v-else-if="sftpUsageError" style="font-size: 12px; color: #c53030;">
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
</div>
<div v-else-if="sftpUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
{{ sftpUsage.totalSizeFormatted }}
<span style="font-weight: 400; color: #64748b; font-size: 12px;">{{ sftpUsage.fileCount }} 文件)</span>
</div>
<div v-else style="font-size: 12px; color: #94a3b8;">点击刷新查看</div>
</div>
<div style="margin-top: auto;"> <div style="margin-top: auto;">
<button <button
class="btn" class="btn"
@@ -1342,34 +1365,7 @@
</div> </div>
</div> </div>
<!-- SFTP存储信息 - 仅SFTP权限 --> <!-- SFTP 概览 / 配置入口 - 仅SFTP权限 -->
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only' && user.has_ftp_config" style="margin-bottom: 40px;">
<h3 style="margin-bottom: 20px;">
<i class="fas fa-server"></i> SFTP存储
</h3>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: #333;">存储方式: </span>
<span style="color: #667eea; font-weight: 600;">SFTP存储</span>
<span style="margin-left: 10px; padding: 4px 12px; background: #17a2b8; color: white; border-radius: 12px; font-size: 12px;">
<i class="fas fa-lock"></i> 仅SFTP
</span>
</div>
<div style="margin-bottom: 15px;">
<span style="font-weight: 600; color: #333;">服务器: </span>
<span>{{ user.ftp_host }}:{{ user.ftp_port }}</span>
</div>
<div style="padding: 10px; background: #d1ecf1; border-left: 4px solid #0c5460; border-radius: 6px; font-size: 13px; color: #0c5460;">
<i class="fas fa-info-circle"></i>
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅SFTP存储"您的文件存储在远程SFTP服务器上。如需使用本地存储请联系管理员修改权限设置。
</div>
</div>
</div>
<!-- SFTP 概览 / 配置入口 -->
<div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;"> <div v-if="user && !user.is_admin && storagePermission === 'sftp_only'" style="margin-bottom: 40px;">
<h3 style="margin-bottom: 20px;"> <h3 style="margin-bottom: 20px;">
<i class="fas fa-server"></i> SFTP存储 <i class="fas fa-server"></i> SFTP存储
@@ -1389,6 +1385,68 @@
<i class="fas fa-tools"></i> 配置 / 修改 SFTP <i class="fas fa-tools"></i> 配置 / 修改 SFTP
</button> </button>
</div> </div>
<!-- 服务器信息 -->
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 10px; border: 1px solid #e2e8f0;">
<div style="font-weight: 600; color: #333; margin-bottom: 8px;">
<i class="fas fa-server" style="color: #667eea;"></i> 服务器信息
</div>
<div style="color: #475569; font-size: 14px;">
{{ user.ftp_host }}:{{ user.ftp_port }}
</div>
</div>
<!-- SFTP空间使用统计 -->
<div v-if="user.has_ftp_config" style="margin-bottom: 12px; padding: 12px; background: white; border-radius: 10px; border: 1px solid #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 600; color: #333;">
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
</div>
<button
class="btn btn-secondary"
style="padding: 4px 10px; font-size: 12px; border-radius: 6px;"
@click="loadSftpUsage()"
:disabled="sftpUsageLoading">
<i :class="sftpUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
{{ sftpUsageLoading ? '统计中...' : '刷新' }}
</button>
</div>
<!-- 加载中 -->
<div v-if="sftpUsageLoading && !sftpUsage" style="text-align: center; padding: 20px; color: #667eea;">
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
<div style="margin-top: 8px; font-size: 13px;">正在统计 SFTP 空间使用情况...</div>
<div style="margin-top: 4px; font-size: 12px; color: #999;">(文件较多时可能需要一些时间)</div>
</div>
<!-- 错误提示 -->
<div v-else-if="sftpUsageError" style="padding: 12px; background: #fff5f5; border-radius: 8px; color: #c53030; font-size: 13px;">
<i class="fas fa-exclamation-triangle"></i> {{ sftpUsageError }}
</div>
<!-- 统计结果 -->
<div v-else-if="sftpUsage" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
<div style="font-size: 20px; font-weight: 700;">{{ sftpUsage.totalSizeFormatted }}</div>
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">总使用空间</div>
</div>
<div style="text-align: center; padding: 12px; background: #f0f9ff; border-radius: 10px; border: 1px solid #e0f2fe;">
<div style="font-size: 20px; font-weight: 700; color: #0369a1;">{{ sftpUsage.fileCount }}</div>
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件数</div>
</div>
<div style="text-align: center; padding: 12px; background: #fefce8; border-radius: 10px; border: 1px solid #fef08a;">
<div style="font-size: 20px; font-weight: 700; color: #a16207;">{{ sftpUsage.dirCount }}</div>
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">文件夹数</div>
</div>
</div>
<!-- 未统计提示 -->
<div v-else style="text-align: center; padding: 16px; color: #64748b; font-size: 13px;">
<i class="fas fa-database" style="font-size: 24px; color: #cbd5e1; margin-bottom: 8px; display: block;"></i>
点击"刷新"按钮统计 SFTP 空间使用情况
</div>
</div>
<div style="padding: 10px; background: #eef2ff; border-radius: 10px; color: #374151; font-size: 13px;"> <div style="padding: 10px; background: #eef2ff; border-radius: 10px; color: #374151; font-size: 13px;">
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i> <i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。 数据存储在你的 SFTP 服务器上,如需切换回本地请联系管理员调整权限。

View File

@@ -220,7 +220,12 @@ createApp({
// SFTP配置引导弹窗 // SFTP配置引导弹窗
showSftpGuideModal: false, showSftpGuideModal: false,
showSftpConfigModal: false showSftpConfigModal: false,
// SFTP空间使用统计
sftpUsage: null, // { totalSize, totalSizeFormatted, fileCount, dirCount }
sftpUsageLoading: false,
sftpUsageError: null
}; };
}, },
@@ -1840,6 +1845,35 @@ handleDragLeave(e) {
console.error('加载用户资料失败:', error); console.error('加载用户资料失败:', error);
} }
}, },
// 加载SFTP空间使用统计
async loadSftpUsage() {
// 仅在用户已配置SFTP时才加载
if (!this.user?.has_ftp_config) {
this.sftpUsage = null;
return;
}
this.sftpUsageLoading = true;
this.sftpUsageError = null;
try {
const response = await axios.get(
`${this.apiBase}/api/user/sftp-usage`,
{ headers: { Authorization: `Bearer ${this.token}` } }
);
if (response.data.success) {
this.sftpUsage = response.data.usage;
}
} catch (error) {
console.error('获取SFTP空间使用情况失败:', error);
this.sftpUsageError = error.response?.data?.message || '获取失败';
} finally {
this.sftpUsageLoading = false;
}
},
// 启动定期检查用户配置 // 启动定期检查用户配置
startProfileSync() { startProfileSync() {
// 清除已有的定时器 // 清除已有的定时器