feat: 新增永久删除文件接口和前端删除按钮

后端新增 POST api/files/delete 接口,同时删除物理文件、缩略图和数据库记录。
前端在 FileCard 和 FileListItem 中增加 hover 可见的删除按钮,点击后弹出确认提示。
This commit is contained in:
2026-07-09 17:42:27 +08:00
parent 8c92d2fbac
commit 60878f9b6e
11 changed files with 587 additions and 3 deletions
@@ -34,6 +34,12 @@ namespace FileShare_Services.Services.FileLibrary
public sealed record DeleteLibraryRootRequest(
[property: JsonPropertyName("id")] int Id);
/// <summary>
/// 永久删除文件的请求。
/// </summary>
public sealed record DeleteFileRequest(
[property: JsonPropertyName("id")] int Id);
/// <summary>
/// 查询服务器子目录的请求。
/// </summary>
@@ -113,6 +113,14 @@ namespace FileShare_Services.Services.FileLibrary
return ResponseHelper.Succeed();
}
/// <inheritdoc />
public async Task<IApiResponse> DeleteFileAsync(DeleteFileRequest request)
{
ValidateFileId(request.Id);
await fileLibrary.DeleteFileAsync(request.Id);
return ResponseHelper.Succeed("文件已永久删除。");
}
/// <summary>
/// 验证文件 ID 是否有效,无效时抛出 <see cref="ArgumentException"/>。
/// </summary>
@@ -545,6 +545,54 @@ namespace FileShare_Services.Services.FileLibrary
}
}
/// <inheritdoc />
public async Task DeleteFileAsync(int id, CancellationToken cancellationToken = default)
{
var record = await db.ManagedFileRecords
.Include(f => f.Thumbnail)
.FirstOrDefaultAsync(f => f.Id == id, cancellationToken);
if (record is null)
{
return;
}
// 删除物理文件
try
{
if (File.Exists(record.AbsolutePath))
{
File.Delete(record.AbsolutePath);
}
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "删除物理文件失败 FilePath={FilePath}", record.AbsolutePath);
}
// 删除缩略图文件
if (record.Thumbnail is not null)
{
try
{
var thumbnailPath = thumbnailService.GetAbsolutePath(record.Thumbnail.RelativePath);
if (File.Exists(thumbnailPath))
{
File.Delete(thumbnailPath);
}
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "删除缩略图文件失败 ThumbnailId={ThumbnailId}", record.ThumbnailId);
}
db.ManagedThumbnailMaps.Remove(record.Thumbnail);
}
db.ManagedFileRecords.Remove(record);
await db.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// 深度优先遍历目录树,枚举所有被 <see cref="MediaFileTypes"/> 支持的媒体文件路径。
/// 遇到无权限的目录时跳过该分支继续遍历。
@@ -105,5 +105,12 @@ namespace FileShare_Services.Services.FileLibrary
/// <param name="request">包含文件 ID 和播放位置的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> SaveFileProgressAsync(SaveFileProgressRequest request);
/// <summary>
/// 永久删除指定文件。
/// </summary>
/// <param name="request">包含文件 ID 的请求。</param>
/// <returns>API 响应。</returns>
Task<IApiResponse> DeleteFileAsync(DeleteFileRequest request);
}
}
@@ -121,5 +121,12 @@ namespace FileShare_Services.Services.FileLibrary
/// <param name="position">播放位置(秒)。</param>
/// <param name="cancellationToken">取消令牌。</param>
Task SaveFileProgressAsync(int id, double position, CancellationToken cancellationToken = default);
/// <summary>
/// 永久删除指定文件,同时删除物理文件和缩略图。
/// </summary>
/// <param name="id">文件记录 ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
Task DeleteFileAsync(int id, CancellationToken cancellationToken = default);
}
}