diff --git a/FileShare-Services/Endpoints/AppEndpoints.cs b/FileShare-Services/Endpoints/AppEndpoints.cs index 782539b..95449a7 100644 --- a/FileShare-Services/Endpoints/AppEndpoints.cs +++ b/FileShare-Services/Endpoints/AppEndpoints.cs @@ -86,6 +86,10 @@ namespace FileShare_Services.Endpoints .WithOpenApi("FileLibrary", "永久删除文件(物理文件+数据库记录)。") .WithName("DeleteFile"); + endpoints.MapPost("api/files/compress", (service, request, _) => service.CompressFileAsync(request)) + .WithOpenApi("FileLibrary", "压缩文件为 ZIP(可选密码)。") + .WithName("CompressFile"); + endpoints.MapGet("api/files/detail", (service, request, _) => service.GetFileAsync(request)) .WithOpenApi("FileLibrary", "查询文件详情。") .WithName("GetFileDetail"); diff --git a/FileShare-Services/FileShare-Services.csproj b/FileShare-Services/FileShare-Services.csproj index 79d8126..4bd8e2c 100644 --- a/FileShare-Services/FileShare-Services.csproj +++ b/FileShare-Services/FileShare-Services.csproj @@ -9,6 +9,7 @@ + diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs index b3073af..0908f78 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs @@ -40,6 +40,12 @@ namespace FileShare_Services.Services.FileLibrary public sealed record DeleteFileRequest( [property: JsonPropertyName("id")] int Id); + /// + /// 压缩文件的请求。 + /// + public sealed record CompressFileRequest( + [property: JsonPropertyName("id")] int Id); + /// /// 查询服务器子目录的请求。 /// diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs index d3e6995..4035988 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs @@ -121,6 +121,14 @@ namespace FileShare_Services.Services.FileLibrary return ResponseHelper.Succeed("文件已永久删除。"); } + /// + public async Task CompressFileAsync(CompressFileRequest request) + { + ValidateFileId(request.Id); + await fileLibrary.CompressFileAsync(request.Id); + return ResponseHelper.Succeed("文件已压缩。"); + } + /// /// 验证文件 ID 是否有效,无效时抛出 。 /// diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs index cb43f03..b1df8fe 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs @@ -2,6 +2,7 @@ using FileShare_Common.Core; using FileShare_EFCore.Database; using FileShare_EFCore.Models; using FileShare_Services.Core; +using Ionic.Zip; using Microsoft.EntityFrameworkCore; using System.Text; @@ -593,6 +594,36 @@ namespace FileShare_Services.Services.FileLibrary await db.SaveChangesAsync(cancellationToken); } + /// + public async Task CompressFileAsync(int id, CancellationToken cancellationToken = default) + { + var record = await db.ManagedFileRecords + .FirstOrDefaultAsync(f => f.Id == id && f.Exists, cancellationToken) + ?? throw new InvalidOperationException("文件不存在。"); + + if (!File.Exists(record.AbsolutePath)) + throw new FileNotFoundException("源文件不存在于磁盘。"); + + var dir = Path.GetDirectoryName(record.AbsolutePath)!; + var nameWithoutExt = Path.GetFileNameWithoutExtension(record.FileName); + var zipPath = Path.Combine(dir, nameWithoutExt + ".zip"); + + // 删除已存在的同名 ZIP + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + // 密码为文件名去掉后缀 + var zipPassword = nameWithoutExt.ToLower(); + + // 创建 ZIP + using var zip = new Ionic.Zip.ZipFile(); + zip.Password = zipPassword; + zip.AddFile(record.AbsolutePath, ""); + zip.Save(zipPath); + } + /// /// 深度优先遍历目录树,枚举所有被 支持的媒体文件路径。 /// 遇到无权限的目录时跳过该分支继续遍历。 diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs index c96b327..e71fad5 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs @@ -112,5 +112,12 @@ namespace FileShare_Services.Services.FileLibrary /// 包含文件 ID 的请求。 /// API 响应。 Task DeleteFileAsync(DeleteFileRequest request); + + /// + /// 压缩指定文件为 ZIP(密码为文件名去掉后缀)。 + /// + /// 包含文件 ID 的请求。 + /// API 响应。 + Task CompressFileAsync(CompressFileRequest request); } } diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs index 691786c..bc9cad2 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs @@ -128,5 +128,12 @@ namespace FileShare_Services.Services.FileLibrary /// 文件记录 ID。 /// 取消令牌。 Task DeleteFileAsync(int id, CancellationToken cancellationToken = default); + + /// + /// 将指定文件压缩为 ZIP(密码为文件名去掉后缀),存储在源文件同级目录。 + /// + /// 文件记录 ID。 + /// 取消令牌。 + Task CompressFileAsync(int id, CancellationToken cancellationToken = default); } } diff --git a/FileShare-Web-VUE/src/api/index.ts b/FileShare-Web-VUE/src/api/index.ts index 57887f7..bea9676 100644 --- a/FileShare-Web-VUE/src/api/index.ts +++ b/FileShare-Web-VUE/src/api/index.ts @@ -116,5 +116,7 @@ export const api = { request('files/progress', { method: 'POST', body: { id, position } }), deleteFile: (id: number) => request('files/delete', { method: 'POST', body: { id } }), + compressFile: (id: number) => + request('files/compress', { method: 'POST', body: { id } }), qrCode: () => request<{ url: string; qrCodeBase64: string }>('qrcode'), } diff --git a/FileShare-Web-VUE/src/components/ClientPage.vue b/FileShare-Web-VUE/src/components/ClientPage.vue index 4d64568..64a7bba 100644 --- a/FileShare-Web-VUE/src/components/ClientPage.vue +++ b/FileShare-Web-VUE/src/components/ClientPage.vue @@ -12,6 +12,7 @@ import RootTabs from './client/RootTabs.vue' import SelectedMediaPlayerHost from './client/SelectedMediaPlayerHost.vue' import ViewToggle from './client/ViewToggle.vue' import QrCodeModal from './QrCodeModal.vue' +import ConfirmModal from './ConfirmModal.vue' type MediaPlayerHandle = { getVideoElement: () => HTMLVideoElement | null @@ -36,7 +37,9 @@ const recentLoading = ref(false) const viewMode = ref<'list' | 'grid'>('list') const qrModal = ref | null>(null) +const confirmModal = ref | null>(null) const mediaPlayer = ref(null) +const compressingIds = ref(new Set()) function setMediaPlayer(el: unknown) { mediaPlayer.value = (el as MediaPlayerHandle) ?? null @@ -273,9 +276,13 @@ function exitSearch() { } async function deleteFile(file: FileRecordDto) { - if (!window.confirm(`确定要永久删除 "${file.fileName}" 吗?\n\n删除后无法恢复!`)) { - return - } + const confirmed = await confirmModal.value?.open({ + title: '确认删除', + message: `确定要永久删除 "${file.fileName}" 吗?\n\n删除后无法恢复!`, + confirmText: '删除', + danger: true, + }) + if (!confirmed) return try { await api.deleteFile(file.id) await browseDirectory() @@ -290,6 +297,30 @@ async function deleteFile(file: FileRecordDto) { } } +async function compressFile(file: FileRecordDto) { + const nameWithoutExt = file.fileName.replace(/\.[^.]+$/, '') + const confirmed = await confirmModal.value?.open({ + title: '确认压缩', + message: `确定要将 "${file.fileName}" 压缩为 ZIP 吗?\n\n密码将自动设置为:${nameWithoutExt.toLocaleLowerCase()}`, + confirmText: '压缩', + }) + if (!confirmed) return + compressingIds.value.add(file.id) + try { + await api.compressFile(file.id) + await confirmModal.value?.open({ + title: '压缩完成', + message: `"${file.fileName}" 已压缩为 "${nameWithoutExt}.zip"。`, + showCancel: false, + confirmText: '确定', + }) + } catch (error) { + setError(error) + } finally { + compressingIds.value.delete(file.id) + } +} + function updatePlaybackPosition(id: number, position: number) { if (selectedFile.value?.id === id) { selectedFile.value.playbackPosition = position @@ -481,7 +512,7 @@ onBeforeUnmount(() => {

无匹配文件