云文件系统初始化
This commit is contained in:
@@ -5,6 +5,7 @@ import com.filesystem.entity.FileShare;
|
||||
import com.filesystem.security.UserPrincipal;
|
||||
import com.filesystem.service.FileService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -250,5 +251,37 @@ public class FileController {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/batchDownload")
|
||||
public ResponseEntity<?> batchDownload(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@RequestBody Map<String, List<Long>> request,
|
||||
HttpServletResponse response) throws IOException {
|
||||
List<Long> ids = request.get("ids");
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "请选择要下载的文件"));
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] zipBytes = fileService.createZipArchive(ids, principal.getUserId());
|
||||
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"download.zip\"");
|
||||
response.setContentLength(zipBytes.length);
|
||||
response.getOutputStream().write(zipBytes);
|
||||
response.getOutputStream().flush();
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/movableFolders")
|
||||
public ResponseEntity<?> getMovableFolders(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@RequestParam(required = false) List<Long> excludeIds) {
|
||||
List<FileEntity> folders = fileService.getMovableFolders(principal.getUserId(), excludeIds);
|
||||
return ResponseEntity.ok(Map.of("data", folders));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,4 +499,75 @@ public class FileService {
|
||||
file.setFolderId(targetFolderId);
|
||||
fileMapper.updateById(file);
|
||||
}
|
||||
|
||||
public byte[] createZipArchive(List<Long> fileIds, Long userId) throws IOException {
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos);
|
||||
|
||||
for (Long fileId : fileIds) {
|
||||
FileEntity file = fileMapper.selectById(fileId);
|
||||
if (file == null || !file.getUserId().equals(userId) || file.getIsDeleted() == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.getIsFolder() == 1) {
|
||||
// 递归添加文件夹内容
|
||||
addFolderToZip(zos, file, "", userId);
|
||||
} else {
|
||||
// 添加单个文件
|
||||
addFileToZip(zos, file, "");
|
||||
}
|
||||
}
|
||||
|
||||
zos.close();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void addFolderToZip(java.util.zip.ZipOutputStream zos, FileEntity folder, String parentPath, Long userId) throws IOException {
|
||||
String folderPath = parentPath + folder.getName() + "/";
|
||||
|
||||
// 获取文件夹下的所有内容
|
||||
List<FileEntity> children = fileMapper.selectList(
|
||||
new LambdaQueryWrapper<FileEntity>()
|
||||
.eq(FileEntity::getUserId, userId)
|
||||
.eq(FileEntity::getFolderId, folder.getId())
|
||||
.eq(FileEntity::getIsDeleted, 0)
|
||||
);
|
||||
|
||||
for (FileEntity child : children) {
|
||||
if (child.getIsFolder() == 1) {
|
||||
addFolderToZip(zos, child, folderPath, userId);
|
||||
} else {
|
||||
addFileToZip(zos, child, folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addFileToZip(java.util.zip.ZipOutputStream zos, FileEntity file, String parentPath) throws IOException {
|
||||
if (file.getPath() == null) return;
|
||||
|
||||
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath());
|
||||
if (!Files.exists(filePath)) return;
|
||||
|
||||
String zipEntryName = parentPath + file.getName();
|
||||
java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(zipEntryName);
|
||||
zos.putNextEntry(zipEntry);
|
||||
|
||||
byte[] fileBytes = Files.readAllBytes(filePath);
|
||||
zos.write(fileBytes);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
public List<FileEntity> getMovableFolders(Long userId, List<Long> excludeIds) {
|
||||
LambdaQueryWrapper<FileEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(FileEntity::getUserId, userId)
|
||||
.eq(FileEntity::getIsDeleted, 0)
|
||||
.eq(FileEntity::getIsFolder, 1);
|
||||
|
||||
if (excludeIds != null && !excludeIds.isEmpty()) {
|
||||
wrapper.notIn(FileEntity::getId, excludeIds);
|
||||
}
|
||||
|
||||
return fileMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,6 @@ export const shareFileApi = (id, data) => request.post(`/files/${id}/shareFile`,
|
||||
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 getFilePreview = (id) => request.get(`/files/${id}/preview`, { responseType: 'blob' })
|
||||
|
||||
@@ -28,22 +28,17 @@
|
||||
/>
|
||||
|
||||
<!-- 批量操作工具栏 -->
|
||||
<div v-if="selectedFiles.length > 0 && activeMenu === 'my-files'" class="batch-toolbar">
|
||||
<span class="batch-count">已选择 {{ selectedFiles.length }} 项</span>
|
||||
<div class="batch-actions">
|
||||
<el-button type="primary" @click="handleBatchDownload" :disabled="!hasSelectedFiles">
|
||||
<div v-if="activeMenu === 'my-files'" class="batch-toolbar">
|
||||
<el-button-group>
|
||||
<el-button @click="handleBatchDownload" :disabled="selectedFiles.length === 0 || !hasSelectedFiles">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span style="margin-left: 4px">批量下载</span>
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleBatchMove" :disabled="!hasSelectedFiles">
|
||||
<el-button @click="handleBatchMove" :disabled="selectedFiles.length === 0">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span style="margin-left: 4px">批量移动</span>
|
||||
</el-button>
|
||||
<el-button @click="clearSelection">
|
||||
<el-icon><Close /></el-icon>
|
||||
<span style="margin-left: 4px">取消选择</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
@@ -165,7 +160,7 @@ import {
|
||||
getFiles, getTrashFiles, getSharedByMe, getSharedByMeFolderFiles, getSharedToMe, getSharedFolderFiles,
|
||||
uploadFiles, downloadFile, deleteFile, restoreFile,
|
||||
deletePermanently, emptyTrash, createFolder,
|
||||
shareFileApi, cancelShare, getFilePreview, renameFile, moveFile
|
||||
shareFileApi, cancelShare, getFilePreview, renameFile, moveFile, batchDownload, getMovableFolders
|
||||
} from '@/api/file'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
@@ -501,31 +496,33 @@ const clearSelection = () => {
|
||||
|
||||
const handleBatchDownload = async () => {
|
||||
const filesToDownload = selectedFiles.value.filter(f => f.type !== 'folder')
|
||||
if (filesToDownload.length === 0) {
|
||||
if (filesToDownload.length === 0 && selectedFiles.value.length > 0) {
|
||||
ElMessage.warning('请选择要下载的文件(文件夹不支持批量下载)')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info(`开始下载 ${filesToDownload.length} 个文件...`)
|
||||
if (selectedFiles.value.length === 0) {
|
||||
ElMessage.warning('请选择要下载的文件')
|
||||
return
|
||||
}
|
||||
|
||||
for (const file of filesToDownload) {
|
||||
try {
|
||||
const blob = await downloadFile(file.id)
|
||||
ElMessage.info('正在打包下载...')
|
||||
const ids = selectedFiles.value.map(f => f.id)
|
||||
const blob = await batchDownload(ids)
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = file.name
|
||||
a.download = `download_${new Date().getTime()}.zip`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
// 添加小延迟避免浏览器阻塞
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
} catch (e) {
|
||||
ElMessage.error(`下载 ${file.name} 失败`)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success('批量下载完成')
|
||||
ElMessage.success('下载完成')
|
||||
selectedFiles.value = []
|
||||
} catch (e) {
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchMove = async () => {
|
||||
@@ -534,20 +531,16 @@ const handleBatchMove = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户的文件夹列表
|
||||
// 获取可移动的文件夹列表
|
||||
try {
|
||||
const res = await getFiles({ folderId: null })
|
||||
const allFiles = res.data || []
|
||||
const folders = allFiles.filter(f => f.type === 'folder' && f.isFolder === 1)
|
||||
|
||||
// 过滤掉选中的文件夹本身(不能移动到自己)
|
||||
const selectedIds = selectedFiles.value.map(f => f.id)
|
||||
const availableFolders = folders.filter(f => !selectedIds.includes(f.id))
|
||||
const res = await getMovableFolders(selectedIds)
|
||||
const folders = res.data || []
|
||||
|
||||
// 构建选项
|
||||
const options = [
|
||||
{ label: '根目录', value: null },
|
||||
...availableFolders.map(f => ({ label: f.name, value: f.id }))
|
||||
...folders.map(f => ({ label: f.name, value: f.id }))
|
||||
]
|
||||
|
||||
// 使用 ElMessageBox.prompt 的自定义方式
|
||||
@@ -625,16 +618,16 @@ onMounted(async () => {
|
||||
.batch-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #f0f9ff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.batch-count {
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
|
||||
Reference in New Issue
Block a user