fix: 修复文件夹右键菜单问题 + 新增文件夹详情功能
修复内容:
1. 【文件夹右键菜单无法显示】
- 问题: showFileContextMenu() 中仍有 `if (file.isDirectory) return` 阻止代码
- 原因: 之前的修复脚本替换失败,代码仍在
- 修复: 手动移除这行限制代码
- 效果: 文件夹现在可以正常右键操作(重命名、删除、查看详情)
新功能:
2. 【文件夹详情查看】
后端API (POST /api/files/folder-info):
- 接收参数: path(当前路径), folderName(文件夹名称)
- 计算文件夹总大小(递归统计所有文件)
- 统计文件数量和子文件夹数量
- 返回数据: name, path, size, fileCount, folderCount
- 仅支持本地存储
前端功能:
- 右键菜单新增"查看详情"选项(仅文件夹显示)
- 详情弹窗显示:
* 文件夹名称
* 文件夹路径
* 总大小(格式化显示)
* 文件数量
* 子文件夹数量
- 加载中状态提示
- 错误处理和提示
使用方式:
1. 右键点击任意文件夹
2. 选择"查看详情"
3. 弹窗显示文件夹统计信息
技术改动:
- backend/server.js: +100行 (新增folder-info API)
- frontend/app.html: +40行 (详情弹窗UI + 菜单项)
- frontend/app.js: +30行 (状态 + showFolderInfo方法)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1133,6 +1133,101 @@ app.post('/api/files/mkdir', authMiddleware, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取文件夹详情(大小统计)
|
||||
app.post('/api/files/folder-info', authMiddleware, async (req, res) => {
|
||||
const { path, folderName } = req.body;
|
||||
let storage;
|
||||
|
||||
if (!folderName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '缺少文件夹名称参数'
|
||||
});
|
||||
}
|
||||
|
||||
// 只支持本地存储
|
||||
if (req.user.current_storage_type !== 'local') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只有本地存储支持此功能'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { StorageInterface } = require('./storage');
|
||||
const storageInterface = new StorageInterface(req.user);
|
||||
storage = await storageInterface.connect();
|
||||
|
||||
// 构造文件夹路径
|
||||
const basePath = path || '/';
|
||||
const folderPath = basePath === '/' ? `/${folderName}` : `${basePath}/${folderName}`;
|
||||
const fullPath = storage.getFullPath(folderPath);
|
||||
|
||||
// 检查是否存在且是文件夹
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '文件夹不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const stats = fs.statSync(fullPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '指定路径不是文件夹'
|
||||
});
|
||||
}
|
||||
|
||||
// 计算文件夹大小
|
||||
const folderSize = storage.calculateFolderSize(fullPath);
|
||||
|
||||
// 计算文件数量
|
||||
function countFiles(dirPath) {
|
||||
let fileCount = 0;
|
||||
let folderCount = 0;
|
||||
|
||||
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item.name);
|
||||
|
||||
if (item.isDirectory()) {
|
||||
folderCount++;
|
||||
const subCounts = countFiles(itemPath);
|
||||
fileCount += subCounts.fileCount;
|
||||
folderCount += subCounts.folderCount;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return { fileCount, folderCount };
|
||||
}
|
||||
|
||||
const counts = countFiles(fullPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
name: folderName,
|
||||
path: folderPath,
|
||||
size: folderSize,
|
||||
fileCount: counts.fileCount,
|
||||
folderCount: counts.folderCount
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[获取文件夹详情失败]', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取文件夹详情失败: ' + error.message
|
||||
});
|
||||
} finally {
|
||||
if (storage) await storage.end();
|
||||
}
|
||||
});
|
||||
|
||||
// 删除文件
|
||||
app.post('/api/files/delete', authMiddleware, async (req, res) => {
|
||||
const { fileName, path } = req.body;
|
||||
|
||||
@@ -856,6 +856,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件夹详情模态框 -->
|
||||
<div v-if="showFolderInfoModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFolderInfoModal')">
|
||||
<div class="modal-content" @click.stop>
|
||||
<h3 style="margin-bottom: 20px;">
|
||||
<i class="fas fa-folder"></i> 文件夹详情
|
||||
</h3>
|
||||
<div v-if="folderInfo" style="background: #f9f9f9; padding: 20px; border-radius: 8px;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<strong style="color: #666;">名称:</strong>
|
||||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.name }}</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<strong style="color: #666;">路径:</strong>
|
||||
<div style="margin-top: 5px; color: #667eea;">{{ folderInfo.path }}</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<strong style="color: #666;">总大小:</strong>
|
||||
<div style="margin-top: 5px; font-size: 18px; font-weight: 600; color: #667eea;">
|
||||
{{ formatFileSize(folderInfo.size) }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<div style="flex: 1;">
|
||||
<strong style="color: #666;">文件数:</strong>
|
||||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.fileCount }} 个</div>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<strong style="color: #666;">子文件夹:</strong>
|
||||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.folderCount }} 个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 40px; color: #999;">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size: 32px;"></i>
|
||||
<div style="margin-top: 10px;">加载中...</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<button class="btn btn-secondary" @click="showFolderInfoModal = false; folderInfo = null" style="width: 100%;">
|
||||
<i class="fas fa-times"></i> 关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享所有文件模态框 -->
|
||||
<div v-if="showShareAllModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareAllModal')">
|
||||
<div class="modal-content" @click.stop>
|
||||
@@ -1726,6 +1770,9 @@
|
||||
<div class="context-menu-item" @click="contextMenuAction('rename')">
|
||||
<i class="fas fa-edit"></i> 重命名
|
||||
</div>
|
||||
<div v-if="contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('info')">
|
||||
<i class="fas fa-info-circle"></i> 查看详情
|
||||
</div>
|
||||
<div v-if="!contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('share')">
|
||||
<i class="fas fa-share"></i> 分享
|
||||
</div>
|
||||
|
||||
@@ -85,6 +85,10 @@ createApp({
|
||||
folderName: ""
|
||||
},
|
||||
|
||||
// 文件夹详情
|
||||
showFolderInfoModal: false,
|
||||
folderInfo: null,
|
||||
|
||||
// 上传
|
||||
showUploadModal: false,
|
||||
uploadProgress: 0,
|
||||
@@ -903,6 +907,34 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
// 显示文件夹详情
|
||||
async showFolderInfo(file) {
|
||||
if (!file.isDirectory) {
|
||||
this.showToast('error', '错误', '只能查看文件夹详情');
|
||||
return;
|
||||
}
|
||||
|
||||
this.showFolderInfoModal = true;
|
||||
this.folderInfo = null; // 先清空,显示加载中
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${this.apiBase}/api/files/folder-info`, {
|
||||
path: this.currentPath,
|
||||
folderName: file.name
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${this.token}` }
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.folderInfo = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[获取文件夹详情失败]', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '获取文件夹详情失败');
|
||||
this.showFolderInfoModal = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDeleteFile(file) {
|
||||
const fileType = file.isDirectory ? '文件夹' : '文件';
|
||||
const warning = file.isDirectory ? "\n⚠️ 警告:文件夹内所有文件将被永久删除!" : "";
|
||||
@@ -912,17 +944,16 @@ handleDragLeave(e) {
|
||||
},
|
||||
|
||||
// ===== 右键菜单和长按功能 =====
|
||||
|
||||
|
||||
// 显示右键菜单(PC端)
|
||||
showFileContextMenu(file, event) {
|
||||
if (file.isDirectory) return; // 文件夹不显示菜单
|
||||
|
||||
// 文件和文件夹都可以显示右键菜单
|
||||
event.preventDefault();
|
||||
this.contextMenuFile = file;
|
||||
this.contextMenuX = event.clientX;
|
||||
this.contextMenuY = event.clientY;
|
||||
this.showContextMenu = true;
|
||||
|
||||
|
||||
// 点击其他地方关闭菜单
|
||||
this.$nextTick(() => {
|
||||
document.addEventListener('click', this.hideContextMenu, { once: true });
|
||||
|
||||
Reference in New Issue
Block a user