From 3107b11bc42836cce28b5614014f3f10a922e9e0 Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Wed, 1 Apr 2026 23:50:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=91=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filesystem/controller/FileController.java | 37 ++++-- .../com/filesystem/service/FileService.java | 16 ++- web-vue/src/api/file.js | 2 +- web-vue/src/components/BatchMoveDialog.vue | 67 +++++++++++ web-vue/src/views/files/index.vue | 106 +++++++++--------- 5 files changed, 163 insertions(+), 65 deletions(-) create mode 100644 web-vue/src/components/BatchMoveDialog.vue diff --git a/src/main/java/com/filesystem/controller/FileController.java b/src/main/java/com/filesystem/controller/FileController.java index 8e1d3ab..0600417 100644 --- a/src/main/java/com/filesystem/controller/FileController.java +++ b/src/main/java/com/filesystem/controller/FileController.java @@ -19,6 +19,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.nio.file.Paths; import java.util.List; import java.util.Map; @@ -242,8 +245,12 @@ public class FileController { public ResponseEntity moveFile( @AuthenticationPrincipal UserPrincipal principal, @PathVariable Long id, - @RequestBody Map request) { - Long folderId = request.get("folderId"); + @RequestBody Map request) { + Object folderIdObj = request.get("folderId"); + Long folderId = null; + if (folderIdObj != null && !folderIdObj.toString().isEmpty() && !"null".equals(folderIdObj.toString()) && !"undefined".equals(folderIdObj.toString())) { + folderId = Long.parseLong(folderIdObj.toString()); + } try { fileService.moveFile(id, principal.getUserId(), folderId); return ResponseEntity.ok(Map.of("message", "移动成功")); @@ -255,10 +262,25 @@ public class FileController { @PostMapping("/batchDownload") public ResponseEntity batchDownload( @AuthenticationPrincipal UserPrincipal principal, - @RequestBody Map> request, + @RequestBody Map request, HttpServletResponse response) throws IOException { - List ids = request.get("ids"); - if (ids == null || ids.isEmpty()) { + Object idsObj = request.get("ids"); + if (idsObj == null) { + return ResponseEntity.badRequest().body(Map.of("message", "请选择要下载的文件")); + } + + List ids = new ArrayList<>(); + if (idsObj instanceof List) { + for (Object id : (List) idsObj) { + if (id instanceof Number) { + ids.add(((Number) id).longValue()); + } else if (id instanceof String) { + ids.add(Long.parseLong((String) id)); + } + } + } + + if (ids.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("message", "请选择要下载的文件")); } @@ -280,8 +302,9 @@ public class FileController { @GetMapping("/movableFolders") public ResponseEntity getMovableFolders( @AuthenticationPrincipal UserPrincipal principal, - @RequestParam(required = false) List excludeIds) { - List folders = fileService.getMovableFolders(principal.getUserId(), excludeIds); + @RequestParam(required = false) List excludeIds, + @RequestParam(required = false) Long currentFolderId) { + List folders = fileService.getMovableFolders(principal.getUserId(), excludeIds, currentFolderId); return ResponseEntity.ok(Map.of("data", folders)); } } diff --git a/src/main/java/com/filesystem/service/FileService.java b/src/main/java/com/filesystem/service/FileService.java index 13798c5..3c20107 100644 --- a/src/main/java/com/filesystem/service/FileService.java +++ b/src/main/java/com/filesystem/service/FileService.java @@ -497,7 +497,12 @@ public class FileService { } file.setFolderId(targetFolderId); - fileMapper.updateById(file); + // 使用直接更新确保 null 值也能被设置 + fileMapper.update(null, + new LambdaUpdateWrapper() + .eq(FileEntity::getId, fileId) + .set(FileEntity::getFolderId, targetFolderId) + ); } public byte[] createZipArchive(List fileIds, Long userId) throws IOException { @@ -558,12 +563,19 @@ public class FileService { zos.closeEntry(); } - public List getMovableFolders(Long userId, List excludeIds) { + public List getMovableFolders(Long userId, List excludeIds, Long currentFolderId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(FileEntity::getUserId, userId) .eq(FileEntity::getIsDeleted, 0) .eq(FileEntity::getIsFolder, 1); + // 只查询当前目录下的文件夹(同级) + if (currentFolderId == null) { + wrapper.isNull(FileEntity::getFolderId); + } else { + wrapper.eq(FileEntity::getFolderId, currentFolderId); + } + if (excludeIds != null && !excludeIds.isEmpty()) { wrapper.notIn(FileEntity::getId, excludeIds); } diff --git a/web-vue/src/api/file.js b/web-vue/src/api/file.js index 24db905..ad167de 100644 --- a/web-vue/src/api/file.js +++ b/web-vue/src/api/file.js @@ -26,5 +26,5 @@ export const cancelShare = (id) => request.delete(`/files/${id}/cancelShare`) export const renameFile = (id, name) => request.put(`/files/${id}/rename`, { name }) export const moveFile = (id, folderId) => request.put(`/files/${id}/move`, { folderId }) export const batchDownload = (ids) => request.post('/files/batchDownload', { ids }, { responseType: 'blob' }) -export const getMovableFolders = (excludeIds) => request.get('/files/movableFolders', { params: { excludeIds } }) +export const getMovableFolders = (excludeIds, currentFolderId) => request.get('/files/movableFolders', { params: { excludeIds, currentFolderId } }) export const getFilePreview = (id) => request.get(`/files/${id}/preview`, { responseType: 'blob' }) diff --git a/web-vue/src/components/BatchMoveDialog.vue b/web-vue/src/components/BatchMoveDialog.vue new file mode 100644 index 0000000..108b3f1 --- /dev/null +++ b/web-vue/src/components/BatchMoveDialog.vue @@ -0,0 +1,67 @@ + + + diff --git a/web-vue/src/views/files/index.vue b/web-vue/src/views/files/index.vue index 9ffb876..a21d305 100644 --- a/web-vue/src/views/files/index.vue +++ b/web-vue/src/views/files/index.vue @@ -30,7 +30,7 @@
- + 批量下载 @@ -141,6 +141,13 @@ :file="currentRenameFile" @rename="handleConfirmRename" /> +
@@ -156,6 +163,7 @@ import FolderDialog from '@/components/FolderDialog.vue' import ShareDialog from '@/components/ShareDialog.vue' import PreviewDialog from '@/components/PreviewDialog.vue' import RenameDialog from '@/components/RenameDialog.vue' +import BatchMoveDialog from '@/components/BatchMoveDialog.vue' import { getFiles, getTrashFiles, getSharedByMe, getSharedByMeFolderFiles, getSharedToMe, getSharedFolderFiles, uploadFiles, downloadFile, deleteFile, restoreFile, @@ -191,7 +199,8 @@ const previewContent = ref('') const renameVisible = ref(false) const currentRenameFile = ref(null) const selectedFiles = ref([]) -const hasSelectedFiles = computed(() => selectedFiles.value.length > 0) +const batchMoveVisible = ref(false) +const movableFolders = ref([]) // 存储 —— 真实数据 const storagePercent = computed(() => { @@ -337,7 +346,7 @@ const handleUpload = async (fileList) => { if (totalSize > remaining) { const needGb = (totalSize / (1024 * 1024 * 1024)).toFixed(2) const remainMb = (remaining / (1024 * 1024)).toFixed(2) - ElMessage.warning(`存储空间不足!需要 ${needGb} GB,剩余仅 ${remainMb} MB`) + ElMessage.warning(`存储空间不足,需要 ${needGb} GB,剩余仅 ${remainMb} MB`) return } @@ -413,7 +422,7 @@ const handleDownload = async (file) => { } const handleDelete = async (file) => { - await ElMessageBox.confirm("确定删除此文件吗?", '提示', { type: 'warning' }) + await ElMessageBox.confirm("确定删除此文件或目录吗?", '提示', { type: 'warning' }) try { await deleteFile(file.id) ElMessage.success('已移至回收站') @@ -434,7 +443,7 @@ const handleRestore = async (file) => { } const handleDeletePermanently = async (file) => { - await ElMessageBox.confirm("确定彻底删除此文件吗?", '警告', { type: 'error' }) + await ElMessageBox.confirm("确定彻底删除此文件或目录吗?", '警告', { type: 'error' }) try { await deletePermanently(file.id) ElMessage.success('已彻底删除') @@ -490,14 +499,10 @@ const handleSelectionChange = (selection) => { selectedFiles.value = selection } -const clearSelection = () => { - selectedFiles.value = [] -} - const handleBatchDownload = async () => { const filesToDownload = selectedFiles.value.filter(f => f.type !== 'folder') if (filesToDownload.length === 0 && selectedFiles.value.length > 0) { - ElMessage.warning('请选择要下载的文件(文件夹不支持批量下载)') + ElMessage.warning('请选择要下载的文件') return } @@ -517,8 +522,6 @@ const handleBatchDownload = async () => { a.download = `download_${new Date().getTime()}.zip` a.click() URL.revokeObjectURL(url) - - ElMessage.success('下载完成') selectedFiles.value = [] } catch (e) { ElMessage.error('下载失败') @@ -531,54 +534,47 @@ const handleBatchMove = async () => { return } - // 获取可移动的文件夹列表 try { const selectedIds = selectedFiles.value.map(f => f.id) - const res = await getMovableFolders(selectedIds) - const folders = res.data || [] - - // 构建选项 - const options = [ - { label: '根目录', value: null }, - ...folders.map(f => ({ label: f.name, value: f.id })) - ] - - // 使用 ElMessageBox.prompt 的自定义方式 - const { value: targetFolderId } = await ElMessageBox.confirm( - '请选择目标文件夹', - '批量移动', - { - confirmButtonText: '移动', - cancelButtonText: '取消', - type: 'info', - distinguishCancelAndClose: true, - message: h => h('div', [ - h('p', '请选择目标文件夹:'), - h('select', { - style: 'width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;', - onChange: (e) => { window.selectedTargetFolder = e.target.value === 'null' ? null : Number(e.target.value) } - }, options.map(opt => h('option', { value: opt.value === null ? 'null' : opt.value }, opt.label))) - ]) - } - ).then(() => window.selectedTargetFolder) - - // 执行移动 - for (const file of selectedFiles.value) { - try { - await moveFile(file.id, targetFolderId) - } catch (e) { - ElMessage.error(`移动 ${file.name} 失败`) - } - } - - ElMessage.success('批量移动完成') - selectedFiles.value = [] - loadFiles() + const res = await getMovableFolders(selectedIds, currentFolderId.value) + movableFolders.value = res.data || [] + batchMoveVisible.value = true } catch (e) { - if (e !== 'cancel') { - ElMessage.error('移动失败') + ElMessage.error('获取文件夹列表失败') + } +} + +const handleConfirmBatchMove = async (targetFolderId) => { + // 处理各种选项 + let finalFolderId = null + if (targetFolderId === 'parent') { + if (folderStack.value.length > 0) { + const parentFolder = folderStack.value[folderStack.value.length - 1] + finalFolderId = parentFolder.id + } else { + finalFolderId = null + } + } else if (targetFolderId === 'root' || targetFolderId === '' || targetFolderId === null || targetFolderId === undefined) { + // 根目录 + finalFolderId = null + } else { + // 具体文件夹 + finalFolderId = targetFolderId + } + + console.log('targetFolderId:', targetFolderId, 'finalFolderId:', finalFolderId) + + for (const file of selectedFiles.value) { + try { + await moveFile(file.id, finalFolderId) + } catch (e) { + ElMessage.error("移动文件或目录失败") } } + + ElMessage.success('批量移动完成') + selectedFiles.value = [] + loadFiles() } const handleRowDblClick = (row) => {