diff --git a/Api/AlbumController.cs b/Api/AlbumController.cs index 1f14451..f0cfade 100644 --- a/Api/AlbumController.cs +++ b/Api/AlbumController.cs @@ -55,7 +55,7 @@ public class AlbumController(IAlbumService albumService) : BaseApiController if (userId == null) return Error("无法识别用户信息", 401); - var album = await albumService.CreateAlbumAsync(request.Name, request.Description, userId.Value); + var album = await albumService.CreateAlbumAsync(request.Name, request.Description, userId.Value, request.CoverPictureId); return Success(album, "相册创建成功"); } catch (Exception ex) @@ -74,8 +74,7 @@ public class AlbumController(IAlbumService albumService) : BaseApiController if (currentUserId == null) return Error("无法识别用户信息", 401); - var album = await albumService.UpdateAlbumAsync(request.Id, request.Name, request.Description, - currentUserId); + var album = await albumService.UpdateAlbumAsync(request.Id, request.Name, request.Description, currentUserId, request.CoverPictureId); return Success(album, "相册更新成功"); } catch (UnauthorizedAccessException) @@ -192,4 +191,5 @@ public class AlbumController(IAlbumService albumService) : BaseApiController return Error($"从相册移除图片失败: {ex.Message}", 500); } } + } \ No newline at end of file diff --git a/Api/Management/AlbumManagementController.cs b/Api/Management/AlbumManagementController.cs new file mode 100644 index 0000000..f43880a --- /dev/null +++ b/Api/Management/AlbumManagementController.cs @@ -0,0 +1,216 @@ +using Foxel.Controllers; +using Foxel.Models; +using Foxel.Models.Request.Album; +using Foxel.Models.Response.Album; +using Foxel.Models.Response.Picture; +using Foxel.Services.Management; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Foxel.Api.Management +{ + [Authorize(Roles = "Administrator")] + [Route("api/management/album")] + public class AlbumManagementController(IAlbumManagementService albumManagementService) : BaseApiController + { + [HttpGet("get_albums")] + public async Task>> GetAlbums( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? searchQuery = null, + [FromQuery] int? userId = null) + { + try + { + var result = await albumManagementService.GetAlbumsAsync(page, pageSize, searchQuery, userId); + return PaginatedSuccess(result.Data, result.TotalCount, result.Page, result.PageSize); + } + catch (Exception ex) + { + return PaginatedError($"获取相册列表失败: {ex.Message}", 500); + } + } + + [HttpGet("get_album/{id}")] + public async Task>> GetAlbumById(int id) + { + try + { + var album = await albumManagementService.GetAlbumByIdAsync(id); + return Success(album, "相册获取成功"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (Exception ex) + { + return Error($"获取相册失败: {ex.Message}", 500); + } + } + + [HttpPost("create_album")] + public async Task>> CreateAlbum([FromBody] AlbumCreateRequest request) + { + try + { + var userId = GetCurrentUserId(); + if (userId is null) return Error("用户未登录或无法识别用户", 401); + var album = await albumManagementService.CreateAlbumAsync(request, (int)userId); + return Success(album, "相册创建成功"); + } + catch (Exception ex) + { + return Error($"创建相册失败: {ex.Message}", 500); + } + } + + [HttpPost("update_album/{id}")] + public async Task>> UpdateAlbum(int id, + [FromBody] AlbumUpdateRequest request) + { + try + { + var album = await albumManagementService.UpdateAlbumAsync(id, request); + return Success(album, "相册更新成功"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (Exception ex) + { + return Error($"更新相册失败: {ex.Message}", 500); + } + } + + [HttpPost("delete_album")] + public async Task>> DeleteAlbum([FromBody] int id) // Or [FromQuery] int id + { + try + { + var result = await albumManagementService.DeleteAlbumAsync(id); + return Success(result, "相册删除成功"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (Exception ex) + { + return Error($"删除相册失败: {ex.Message}", 500); + } + } + + [HttpPost("batch_delete_albums")] + public async Task>> BatchDeleteAlbums([FromBody] List ids) + { + try + { + if (ids == null || ids.Count == 0) + { + return Error("未提供相册ID"); + } + + var result = await albumManagementService.BatchDeleteAlbumsAsync(ids); + return Success(result, $"成功删除 {result.SuccessCount} 个相册,失败 {result.FailedCount} 个"); + } + catch (Exception ex) + { + return Error($"批量删除相册失败: {ex.Message}", 500); + } + } + + [HttpGet("get_albums_by_user/{userId}")] + public async Task>> GetAlbumsByUserId( + int userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + try + { + var result = await albumManagementService.GetAlbumsByUserIdAsync(userId, page, pageSize); + return PaginatedSuccess(result.Data, result.TotalCount, result.Page, result.PageSize); + } + catch (Exception ex) + { + return PaginatedError($"获取用户相册列表失败: {ex.Message}", 500); + } + } + + [HttpPost("{albumId}/picture/{pictureId}/add")] + public async Task>> AddPictureToAlbum(int albumId, int pictureId) + { + try + { + var result = await albumManagementService.AddPictureToAlbumAsync(albumId, pictureId); + return Success(result, "图片已成功添加到相册"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (Exception ex) + { + return Error($"添加图片到相册失败: {ex.Message}", 500); + } + } + + [HttpPost("{albumId}/picture/{pictureId}/remove")] + public async Task>> RemovePictureFromAlbum(int albumId, int pictureId) + { + try + { + var result = await albumManagementService.RemovePictureFromAlbumAsync(albumId, pictureId); + return Success(result, "图片已成功从相册移除"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (Exception ex) + { + return Error($"从相册移除图片失败: {ex.Message}", 500); + } + } + + [HttpGet("{albumId}/pictures")] + public async Task>> GetPicturesInAlbum( + int albumId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + try + { + var result = await albumManagementService.GetPicturesInAlbumAsync(albumId, page, pageSize); + return PaginatedSuccess(result.Data, result.TotalCount, result.Page, result.PageSize); + } + catch (KeyNotFoundException knfex) + { + return PaginatedError(knfex.Message, 404); + } + catch (Exception ex) + { + return PaginatedError($"获取相册内图片失败: {ex.Message}", 500); + } + } + + [HttpPost("{albumId}/set_cover/{pictureId}")] + public async Task>> SetAlbumCover(int albumId, int pictureId) + { + try + { + var result = await albumManagementService.SetAlbumCoverAsync(albumId, pictureId); + return Success(result, "相册封面设置成功"); + } + catch (KeyNotFoundException knfex) + { + return Error(knfex.Message, 404); + } + catch (InvalidOperationException ioex) + { + return Error(ioex.Message, 400); + } + catch (Exception ex) + { + return Error($"设置相册封面失败: {ex.Message}", 500); + } + } + } +} \ No newline at end of file diff --git a/Extensions/ServiceCollectionExtensions.cs b/Extensions/ServiceCollectionExtensions.cs index d30f177..1c0e5ec 100644 --- a/Extensions/ServiceCollectionExtensions.cs +++ b/Extensions/ServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ using Foxel.Services.Storage; using Foxel.Services.Storage.Providers; using Foxel.Services.VectorDB; using Foxel.Services.Background.Processors; +using Foxel.Services.Mapping; namespace Foxel.Extensions; @@ -30,6 +31,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -38,6 +40,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } public static void AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration) diff --git a/Models/DataBase/Album.cs b/Models/DataBase/Album.cs index 61e0b1a..85d9059 100644 --- a/Models/DataBase/Album.cs +++ b/Models/DataBase/Album.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Foxel.Models.DataBase; @@ -14,5 +15,9 @@ public class Album : BaseModel [Required] public User User { get; set; } + public int? CoverPictureId { get; set; } + [ForeignKey("CoverPictureId")] + public Picture? CoverPicture { get; set; } + public ICollection? Pictures { get; set; } } diff --git a/Models/Request/Album/AlbumCreateRequest.cs b/Models/Request/Album/AlbumCreateRequest.cs new file mode 100644 index 0000000..82206b7 --- /dev/null +++ b/Models/Request/Album/AlbumCreateRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Foxel.Models.Request.Album +{ + public class AlbumCreateRequest + { + [Required(ErrorMessage = "相册名称不能为空")] + [StringLength(100, ErrorMessage = "相册名称长度不能超过100个字符")] + public string Name { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "相册描述长度不能超过500个字符")] + public string? Description { get; set; } + + public int? CoverPictureId { get; set; } + } +} diff --git a/Models/Request/Album/AlbumUpdateRequest.cs b/Models/Request/Album/AlbumUpdateRequest.cs new file mode 100644 index 0000000..a72806a --- /dev/null +++ b/Models/Request/Album/AlbumUpdateRequest.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Foxel.Models.Request.Album +{ + public class AlbumUpdateRequest + { + [StringLength(100, ErrorMessage = "相册名称长度不能超过100个字符")] + public string? Name { get; set; } + + [StringLength(500, ErrorMessage = "相册描述长度不能超过500个字符")] + public string? Description { get; set; } + + public int? CoverPictureId { get; set; } + } +} diff --git a/Models/Request/Album/CreateAlbumRequest.cs b/Models/Request/Album/CreateAlbumRequest.cs index ef64863..c36294e 100644 --- a/Models/Request/Album/CreateAlbumRequest.cs +++ b/Models/Request/Album/CreateAlbumRequest.cs @@ -10,4 +10,7 @@ public record CreateAlbumRequest [StringLength(500)] public string? Description { get; set; } + + + public int? CoverPictureId { get; set; } } diff --git a/Models/Response/Album/AlbumResponse.cs b/Models/Response/Album/AlbumResponse.cs index 0f4f3ad..184819f 100644 --- a/Models/Response/Album/AlbumResponse.cs +++ b/Models/Response/Album/AlbumResponse.cs @@ -7,9 +7,11 @@ public record AlbumResponse public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; - public int PictureCount { get; set; } = 0; public int UserId { get; set; } public string? Username { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } + public string? CoverPicturePath { get; set; } + public string? CoverPictureThumbnailPath { get; set; } + public int PictureCount { get; set; } } diff --git a/MyDbContext.cs b/MyDbContext.cs index 8b5b9cc..8362999 100644 --- a/MyDbContext.cs +++ b/MyDbContext.cs @@ -15,4 +15,20 @@ public class MyDbContext(DbContextOptions options) : DbContext(opti public DbSet Logs { get; set; } = null!; public DbSet BackgroundTasks { get; set; } = null!; public DbSet StorageModes { get; set; } = null!; + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(a => a.CoverPicture) + .WithMany() + .HasForeignKey(a => a.CoverPictureId) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasMany(a => a.Pictures) + .WithOne(p => p.Album) + .HasForeignKey(p => p.AlbumId) + .OnDelete(DeleteBehavior.Cascade); + } } \ No newline at end of file diff --git a/Services/Management/AlbumManagementService.cs b/Services/Management/AlbumManagementService.cs new file mode 100644 index 0000000..bf2ff84 --- /dev/null +++ b/Services/Management/AlbumManagementService.cs @@ -0,0 +1,308 @@ +using Foxel.Models; +using Foxel.Models.DataBase; +using Foxel.Models.Request.Album; +using Foxel.Models.Response.Album; +using Foxel.Models.Response.Picture; +using Microsoft.EntityFrameworkCore; +using Foxel.Services.Mapping; + + +namespace Foxel.Services.Management +{ + public class AlbumManagementService( + IDbContextFactory contextFactory, + IMappingService mappingService, + ILogger logger) + : IAlbumManagementService + { + public async Task> GetAlbumsAsync(int page = 1, int pageSize = 10, + string? searchQuery = null, int? userId = null) + { + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var query = dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) // To get PictureCount + .AsQueryable(); + + if (!string.IsNullOrWhiteSpace(searchQuery)) + { + query = query.Where(a => + a.Name.Contains(searchQuery) || (a.Description != null && a.Description.Contains(searchQuery))); + } + + if (userId.HasValue) + { + query = query.Where(a => a.UserId == userId.Value); + } + + query = query.OrderByDescending(a => a.CreatedAt); + + var totalCount = await query.CountAsync(); + var albums = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + var albumResponses = albums.Select(mappingService.MapAlbumToResponse).ToList(); + + return new PaginatedResult + { + Data = albumResponses, + Page = page, + PageSize = pageSize, + TotalCount = totalCount + }; + } + + public async Task GetAlbumByIdAsync(int id) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var album = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) // Ensure Pictures is included for PictureCount + .FirstOrDefaultAsync(a => a.Id == id); + + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {id} 的相册"); + + return mappingService.MapAlbumToResponse(album); + } + + public async Task CreateAlbumAsync(AlbumCreateRequest request, int creatorUserId) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + + var album = new Album + { + Name = request.Name, + Description = request.Description ?? string.Empty, + UserId = creatorUserId, + CoverPictureId = request.CoverPictureId, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await dbContext.Albums.AddAsync(album); + await dbContext.SaveChangesAsync(); + + // Reload to include navigation properties if needed for response, or map manually + var createdAlbum = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) // Ensure Pictures is included for PictureCount + .FirstAsync(a => a.Id == album.Id); + + return mappingService.MapAlbumToResponse(createdAlbum); + } + + public async Task UpdateAlbumAsync(int id, AlbumUpdateRequest request) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var album = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) // Keep this for cover picture + .Include(a => a.Pictures) // Keep this for PictureCount + .FirstOrDefaultAsync(a => a.Id == id); + + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {id} 的相册"); + + if (request.Name != null) + album.Name = request.Name; + if (request.Description != null) + album.Description = request.Description; + if (request.CoverPictureId.HasValue) + album.CoverPictureId = request.CoverPictureId.Value == 0 ? null : request.CoverPictureId; + + + album.UpdatedAt = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + + // Reload CoverPicture if it was changed by ID + if (request.CoverPictureId.HasValue) + { + album = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) // Ensure Pictures is included for PictureCount + .FirstAsync(a => a.Id == id); + } + // If CoverPictureId was not updated, but other fields were, we still need the full album for mapping + else if (album.CoverPicture == null && album.CoverPictureId != null) // Case where CoverPicture was null but ID existed + { + album = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) + .FirstAsync(a => a.Id == id); + } + + + return mappingService.MapAlbumToResponse(album); + } + + public async Task DeleteAlbumAsync(int id) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var album = await dbContext.Albums.FirstOrDefaultAsync(a => a.Id == id); + + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {id} 的相册"); + + // Find all pictures belonging to this album + var picturesInAlbum = await dbContext.Pictures + .Where(p => p.AlbumId == id) + .ToListAsync(); + + // Disassociate pictures from the album + foreach (var picInAlbum in picturesInAlbum) + { + picInAlbum.AlbumId = null; + } + + dbContext.Albums.Remove(album); + await dbContext.SaveChangesAsync(); // This will save both picture updates and album deletion. + return true; + } + + public async Task BatchDeleteAlbumsAsync(List ids) + { + var result = new BatchDeleteResult(); + foreach (var id in ids) + { + try + { + var success = await DeleteAlbumAsync(id); + if (success) + result.SuccessCount++; + else + { + result.FailedCount++; + result.FailedIds.Add(id); + } + } + catch (Exception ex) + { + logger.LogError(ex, $"批量删除相册失败,ID: {id}"); + result.FailedCount++; + result.FailedIds.Add(id); + } + } + + return result; + } + + public async Task> GetAlbumsByUserIdAsync(int userId, int page = 1, + int pageSize = 10) + { + return await GetAlbumsAsync(page, pageSize, null, userId); + } + + public async Task AddPictureToAlbumAsync(int albumId, int pictureId) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var album = await dbContext.Albums.FindAsync(albumId); + var picture = await dbContext.Pictures.FindAsync(pictureId); + + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {albumId} 的相册"); + if (picture == null) + throw new KeyNotFoundException($"找不到ID为 {pictureId} 的图片"); + + if (picture.AlbumId == albumId) + { + // Picture is already in this album or no change needed + return true; + } + + picture.AlbumId = albumId; + // picture.Album = album; // EF Core will link this based on AlbumId if Album navigation property exists on Picture + + await dbContext.SaveChangesAsync(); + return true; + } + + public async Task RemovePictureFromAlbumAsync(int albumId, int pictureId) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var picture = await dbContext.Pictures + .FirstOrDefaultAsync(p => p.Id == pictureId && p.AlbumId == albumId); + + if (picture == null) + throw new KeyNotFoundException($"在相册 {albumId} 中找不到图片 {pictureId}"); + + picture.AlbumId = null; + // picture.Album = null; // EF Core will update this + await dbContext.SaveChangesAsync(); + return true; + } + + public async Task> GetPicturesInAlbumAsync(int albumId, int page = 1, + int pageSize = 10) + { + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + + await using var dbContext = await contextFactory.CreateDbContextAsync(); + + var albumExists = await dbContext.Albums.AnyAsync(a => a.Id == albumId); + if (!albumExists) + { + throw new KeyNotFoundException($"找不到ID为 {albumId} 的相册"); + } + + var query = dbContext.Pictures + .Where(p => p.AlbumId == albumId) + .Include(p => p.User) + .Include(p => p.Tags) + .Include(p => p.Favorites); + + query = + (Microsoft.EntityFrameworkCore.Query.IIncludableQueryable?>)query + .OrderByDescending(p => p.CreatedAt); + + var totalCount = await query.CountAsync(); + var pictures = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + var pictureResponses = pictures.Select(mappingService.MapPictureToResponse).ToList(); + + return new PaginatedResult + { + Data = pictureResponses, + Page = page, + PageSize = pageSize, + TotalCount = totalCount + }; + } + + public async Task SetAlbumCoverAsync(int albumId, int pictureId) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + var album = await dbContext.Albums.FindAsync(albumId); + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {albumId} 的相册"); + + var picture = await dbContext.Pictures.FindAsync(pictureId); + if (picture == null) + throw new KeyNotFoundException($"找不到ID为 {pictureId} 的图片"); + + // Ensure the picture is part of the album by checking its AlbumId + if (picture.AlbumId != albumId) + throw new InvalidOperationException($"图片 {pictureId} 不属于相册 {albumId}"); + + album.CoverPictureId = pictureId; + album.UpdatedAt = DateTime.UtcNow; + await dbContext.SaveChangesAsync(); + return true; + } + } +} \ No newline at end of file diff --git a/Services/Management/IAlbumManagementService.cs b/Services/Management/IAlbumManagementService.cs new file mode 100644 index 0000000..716479f --- /dev/null +++ b/Services/Management/IAlbumManagementService.cs @@ -0,0 +1,22 @@ +using Foxel.Models; +using Foxel.Models.Request.Album; +using Foxel.Models.Response.Album; +using Foxel.Models.Response.Picture; + +namespace Foxel.Services.Management +{ + public interface IAlbumManagementService + { + Task> GetAlbumsAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null); + Task GetAlbumByIdAsync(int id); + Task CreateAlbumAsync(AlbumCreateRequest request, int creatorUserId); + Task UpdateAlbumAsync(int id, AlbumUpdateRequest request); + Task DeleteAlbumAsync(int id); + Task BatchDeleteAlbumsAsync(List ids); + Task> GetAlbumsByUserIdAsync(int userId, int page = 1, int pageSize = 10); + Task AddPictureToAlbumAsync(int albumId, int pictureId); + Task RemovePictureFromAlbumAsync(int albumId, int pictureId); + Task> GetPicturesInAlbumAsync(int albumId, int page = 1, int pageSize = 10); + Task SetAlbumCoverAsync(int albumId, int pictureId); + } +} diff --git a/Services/Mapping/IMappingService.cs b/Services/Mapping/IMappingService.cs new file mode 100644 index 0000000..6d45a55 --- /dev/null +++ b/Services/Mapping/IMappingService.cs @@ -0,0 +1,12 @@ +using Foxel.Models.DataBase; +using Foxel.Models.Response.Album; +using Foxel.Models.Response.Picture; + +namespace Foxel.Services.Mapping +{ + public interface IMappingService + { + AlbumResponse MapAlbumToResponse(Album album); + PictureResponse MapPictureToResponse(Picture picture); + } +} diff --git a/Services/Mapping/MappingService.cs b/Services/Mapping/MappingService.cs new file mode 100644 index 0000000..68f7e41 --- /dev/null +++ b/Services/Mapping/MappingService.cs @@ -0,0 +1,70 @@ +using Foxel.Models.DataBase; +using Foxel.Models.Response.Album; +using Foxel.Models.Response.Picture; +using Foxel.Services.Storage; + +namespace Foxel.Services.Mapping +{ + public class MappingService(IStorageService storageService) + : IMappingService + { + public AlbumResponse MapAlbumToResponse(Album album) + { + string? coverPath = null; + string? coverThumbnailPath = null; + + if (album.CoverPicture != null) + { + coverPath = storageService.ExecuteAsync(album.CoverPicture.StorageModeId, + provider => Task.FromResult(provider.GetUrl(album.CoverPicture.Id, album.CoverPicture.Path))) + .Result; // Consider making this async if possible in the future + if (!string.IsNullOrEmpty(album.CoverPicture.ThumbnailPath)) + { + coverThumbnailPath = storageService.ExecuteAsync(album.CoverPicture.StorageModeId, + provider => Task.FromResult(provider.GetUrl(album.CoverPicture.Id, + album.CoverPicture.ThumbnailPath))).Result; // Consider async + } + } + + return new AlbumResponse + { + Id = album.Id, + Name = album.Name, + Description = album.Description, + UserId = album.UserId, + Username = album.User.UserName, + CreatedAt = album.CreatedAt, + UpdatedAt = album.UpdatedAt, + CoverPicturePath = coverPath, + CoverPictureThumbnailPath = coverThumbnailPath, + PictureCount = album.Pictures?.Count ?? 0 + }; + } + + public PictureResponse MapPictureToResponse(Picture picture) + { + return new PictureResponse + { + Id = picture.Id, + Name = picture.Name, + Path = storageService.ExecuteAsync(picture.StorageModeId, provider => + Task.FromResult(provider.GetUrl(picture.Id, picture.Path))).Result, + ThumbnailPath = storageService.ExecuteAsync(picture.StorageModeId, provider => + Task.FromResult(provider.GetUrl(picture.Id, picture.ThumbnailPath ?? string.Empty))) + .Result, + Description = picture.Description, + CreatedAt = picture.CreatedAt, + TakenAt = picture.TakenAt, + ExifInfo = picture.ExifInfo, + UserId = picture.UserId, + Username = picture.User?.UserName, + Tags = picture.Tags?.Select(t => t.Name).ToList(), + AlbumId = picture.AlbumId, + AlbumName = picture.Album?.Name, + Permission = picture.Permission, + FavoriteCount = picture.Favorites?.Count ?? 0, + StorageModeName = picture.StorageMode?.Name + }; + } + } +} \ No newline at end of file diff --git a/Services/Media/AlbumService.cs b/Services/Media/AlbumService.cs index a165e78..ad30e3a 100644 --- a/Services/Media/AlbumService.cs +++ b/Services/Media/AlbumService.cs @@ -3,60 +3,66 @@ using Foxel.Models; using Foxel.Models.DataBase; using Foxel.Models.Response.Album; using Microsoft.EntityFrameworkCore; +using Foxel.Services.Mapping; // 添加 using namespace Foxel.Services.Media; public class AlbumService( IDbContextFactory contextFactory, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IMappingService mappingService) // 注入新的映射服务 : IAlbumService { public async Task> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null) { if (page < 1) page = 1; if (pageSize < 1) pageSize = 10; - + await using var dbContext = await contextFactory.CreateDbContextAsync(); - + // 构建查询 IQueryable query = dbContext.Albums .Include(a => a.User) + .Include(a => a.CoverPicture) // 确保包含封面图片 + .Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount .OrderByDescending(a => a.CreatedAt); - + // 如果指定了用户ID,则只获取该用户的相册 if (userId.HasValue) { query = query.Where(a => a.UserId == userId.Value); } - + // 获取总数和分页数据 var totalCount = await query.CountAsync(); var albums = await query + // .Include(a => a.CoverPicture) // 已在上面包含 .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); - - // 获取每个相册中的图片数量 - var albumIds = albums.Select(a => a.Id).ToList(); - var albumPictureCounts = await dbContext.Pictures - .Where(p => p.AlbumId != null && albumIds.Contains(p.AlbumId.Value)) - .GroupBy(p => p.AlbumId) - .Select(g => new { AlbumId = g.Key, Count = g.Count() }) - .ToDictionaryAsync(x => x.AlbumId!.Value, x => x.Count); - + + // 获取每个相册中的图片数量 - 不再需要,MapAlbumToResponse 会处理 + // var albumIds = albums.Select(a => a.Id).ToList(); + // var albumPictureCounts = await dbContext.Pictures + // .Where(p => p.AlbumId != null && albumIds.Contains(p.AlbumId.Value)) + // .GroupBy(p => p.AlbumId) + // .Select(g => new { AlbumId = g.Key, Count = g.Count() }) + // .ToDictionaryAsync(x => x.AlbumId!.Value, x => x.Count); + // 转换为响应模型 - var albumResponses = albums.Select(a => new AlbumResponse - { - Id = a.Id, - Name = a.Name, - Description = a.Description, - PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0), - UserId = a.UserId, - Username = a.User.UserName, - CreatedAt = a.CreatedAt, - UpdatedAt = a.UpdatedAt - }).ToList(); - + var albumResponses = albums.Select(mappingService.MapAlbumToResponse).ToList(); + // var albumResponses = albums.Select(a => new AlbumResponse + // { + // Id = a.Id, + // Name = a.Name, + // Description = a.Description, + // PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0), // 使用 MapAlbumToResponse 后不再需要 + // UserId = a.UserId, + // Username = a.User.UserName, + // CreatedAt = a.CreatedAt, + // UpdatedAt = a.UpdatedAt + // }).ToList(); + return new PaginatedResult { Data = albumResponses, @@ -65,206 +71,222 @@ public class AlbumService( TotalCount = totalCount }; } - + public async Task GetAlbumByIdAsync(int id) { await using var dbContext = await contextFactory.CreateDbContextAsync(); - + var album = await dbContext.Albums .Include(a => a.User) + .Include(a => a.CoverPicture) // 确保包含封面图片 + .Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount .FirstOrDefaultAsync(a => a.Id == id); - + if (album == null) throw new KeyNotFoundException($"找不到ID为{id}的相册"); - - // 获取相册中图片的数量 - var pictureCount = await dbContext.Pictures - .Where(p => p.AlbumId == id) - .CountAsync(); - + + // 获取相册中图片的数量 - 不再需要 + // var pictureCount = await dbContext.Pictures + // .Where(p => p.AlbumId == id) + // .CountAsync(); + // 转换为响应模型 - var response = new AlbumResponse - { - Id = album.Id, - Name = album.Name, - Description = album.Description, - PictureCount = pictureCount, - UserId = album.UserId, - Username = album.User.UserName, - CreatedAt = album.CreatedAt, - UpdatedAt = album.UpdatedAt - }; - - return response; + return mappingService.MapAlbumToResponse(album); + // var response = new AlbumResponse + // { + // Id = album.Id, + // Name = album.Name, + // Description = album.Description, + // PictureCount = pictureCount, // 使用 MapAlbumToResponse 后不再需要 + // UserId = album.UserId, + // Username = album.User.UserName, + // CreatedAt = album.CreatedAt, + // UpdatedAt = album.UpdatedAt + // }; + // + // return response; } - - public async Task CreateAlbumAsync(string name, string? description, int userId) + + public async Task CreateAlbumAsync(string name, string? description, int userId, int? coverPictureId) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("相册名称不能为空", nameof(name)); - + await using var dbContext = await contextFactory.CreateDbContextAsync(); - + // 检查用户是否存在 var user = await dbContext.Users.FindAsync(userId); if (user == null) throw new KeyNotFoundException($"找不到ID为{userId}的用户"); - + // 创建新相册 var album = new Album { Name = name.Trim(), Description = description?.Trim() ?? string.Empty, UserId = userId, - User = user, + // User = user, // EF Core 会自动关联 CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + CoverPictureId = coverPictureId }; - + dbContext.Albums.Add(album); await dbContext.SaveChangesAsync(); - + + // 重新加载创建的相册以包含导航属性,用于 MapAlbumToResponse + var createdAlbum = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) + .FirstAsync(a => a.Id == album.Id); + // 转换为响应模型 - return new AlbumResponse - { - Id = album.Id, - Name = album.Name, - Description = album.Description, - PictureCount = 0, - UserId = album.UserId, - Username = user.UserName, - CreatedAt = album.CreatedAt, - UpdatedAt = album.UpdatedAt - }; + return mappingService.MapAlbumToResponse(createdAlbum); + // return new AlbumResponse + // { + // Id = album.Id, + // Name = album.Name, + // Description = album.Description, + // PictureCount = 0, // MapAlbumToResponse 会处理 + // UserId = album.UserId, + // Username = user.UserName, // MapAlbumToResponse 会处理 + // CreatedAt = album.CreatedAt, + // UpdatedAt = album.UpdatedAt + // }; } - - public async Task UpdateAlbumAsync(int id, string name, string? description, int? userId = null) + + public async Task UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("相册名称不能为空", nameof(name)); - + await using var dbContext = await contextFactory.CreateDbContextAsync(); - + // 获取相册 var album = await dbContext.Albums - .Include(a => a.User) - .Include(a => a.Pictures) + // .Include(a => a.User) // 将在更新后重新加载 + // .Include(a => a.Pictures) // 将在更新后重新加载 .FirstOrDefaultAsync(a => a.Id == id); - + if (album == null) throw new KeyNotFoundException($"找不到ID为{id}的相册"); - - // 权限检查 - 只有相册的创建者或系统管理员可以更新 - if (userId.HasValue && album.UserId != userId.Value) + if (!userId.HasValue) // userId 仍然需要用于权限检查 + throw new ArgumentException("无效的用户ID", nameof(userId)); + if (album.UserId != userId.Value) { - // 检查用户是否是管理员 - var user = await dbContext.Users.FindAsync(userId.Value); - if (user == null) - { - throw new UnauthorizedAccessException("您没有权限更新此相册"); - } + throw new UnauthorizedAccessException("您没有权限更新此相册"); } - + // 更新相册信息 album.Name = name.Trim(); album.Description = description?.Trim() ?? album.Description; album.UpdatedAt = DateTime.UtcNow; - + album.CoverPictureId = coverPictureId; + await dbContext.SaveChangesAsync(); - + + // 重新加载更新后的相册以包含导航属性 + var updatedAlbum = await dbContext.Albums + .Include(a => a.User) + .Include(a => a.CoverPicture) + .Include(a => a.Pictures) + .FirstAsync(a => a.Id == album.Id); + // 转换为响应模型 - return new AlbumResponse - { - Id = album.Id, - Name = album.Name, - Description = album.Description, - PictureCount = album.Pictures?.Count ?? 0, - UserId = album.UserId, - Username = album.User.UserName, - CreatedAt = album.CreatedAt, - UpdatedAt = album.UpdatedAt - }; + return mappingService.MapAlbumToResponse(updatedAlbum); + // return new AlbumResponse + // { + // Id = album.Id, + // Name = album.Name, + // Description = album.Description, + // PictureCount = album.Pictures?.Count ?? 0, // MapAlbumToResponse 会处理 + // UserId = album.UserId, + // Username = album.User.UserName, // MapAlbumToResponse 会处理 + // CreatedAt = album.CreatedAt, + // UpdatedAt = album.UpdatedAt + // }; } - + public async Task DeleteAlbumAsync(int id) { await using var dbContext = await contextFactory.CreateDbContextAsync(); - + var album = await dbContext.Albums.FindAsync(id); if (album == null) return false; - + // 先找出所有属于这个相册的图片 var pictures = await dbContext.Pictures .Where(p => p.AlbumId == id) .ToListAsync(); - + // 将这些图片的AlbumId设置为null foreach (var picture in pictures) { picture.AlbumId = null; picture.Album = null; } - + // 保存图片更改 await dbContext.SaveChangesAsync(); - + // 然后删除相册 dbContext.Albums.Remove(album); await dbContext.SaveChangesAsync(); - + return true; } - + public async Task AddPictureToAlbumAsync(int albumId, int pictureId) { await using var dbContext = await contextFactory.CreateDbContextAsync(); - + // 获取相册和图片 var album = await dbContext.Albums.FindAsync(albumId); if (album == null) throw new KeyNotFoundException($"找不到ID为{albumId}的相册"); - + var picture = await dbContext.Pictures.FindAsync(pictureId); if (picture == null) throw new KeyNotFoundException($"找不到ID为{pictureId}的图片"); - + // 将图片添加到相册 picture.AlbumId = albumId; picture.Album = album; - + await dbContext.SaveChangesAsync(); - + return true; } - + public async Task RemovePictureFromAlbumAsync(int albumId, int pictureId) { await using var dbContext = await contextFactory.CreateDbContextAsync(); - + // 获取图片 var picture = await dbContext.Pictures .FirstOrDefaultAsync(p => p.Id == pictureId && p.AlbumId == albumId); - + if (picture == null) throw new KeyNotFoundException($"在相册中找不到ID为{pictureId}的图片"); - + // 从相册中移除图片 picture.AlbumId = null; picture.Album = null; - + await dbContext.SaveChangesAsync(); - + return true; } public async Task AddPicturesToAlbumAsync(int albumId, List pictureIds) { await using var dbContext = await contextFactory.CreateDbContextAsync(); - - var album = await dbContext.Albums.FindAsync(albumId) + + var album = await dbContext.Albums.FindAsync(albumId) ?? throw new KeyNotFoundException("相册不存在"); - + // 检查是否有权限修改此相册 var currentUser = httpContextAccessor.HttpContext?.User; if (currentUser != null) @@ -275,14 +297,14 @@ public class AlbumService( throw new UnauthorizedAccessException("您没有权限修改此相册"); } } - + var successCount = 0; - + foreach (var pictureId in pictureIds) { var picture = await dbContext.Pictures.FindAsync(pictureId); if (picture == null) continue; // 跳过不存在的图片 - + // 直接更新 Picture 的 AlbumId if (picture.AlbumId != albumId) { @@ -290,13 +312,40 @@ public class AlbumService( successCount++; } } - + if (successCount > 0) { await dbContext.SaveChangesAsync(); return true; } - + return false; } + + public async Task SetAlbumCoverAsync(int albumId, int pictureId, int userId) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + + var album = await dbContext.Albums.FirstOrDefaultAsync(a => a.Id == albumId); + if (album == null) + throw new KeyNotFoundException($"找不到ID为 {albumId} 的相册"); + + // 权限检查:只有相册所有者可以设置封面 + if (album.UserId != userId) + throw new UnauthorizedAccessException("您没有权限修改此相册的封面"); + + var picture = await dbContext.Pictures.FirstOrDefaultAsync(p => p.Id == pictureId); + if (picture == null) + throw new KeyNotFoundException($"找不到ID为 {pictureId} 的图片"); + + // 确保图片属于该相册 + if (picture.AlbumId != albumId) + throw new InvalidOperationException($"图片 {pictureId} 不属于相册 {albumId}"); + + album.CoverPictureId = pictureId; + album.UpdatedAt = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + return true; + } } diff --git a/Services/Media/IAlbumService.cs b/Services/Media/IAlbumService.cs index e732bd1..fb748cb 100644 --- a/Services/Media/IAlbumService.cs +++ b/Services/Media/IAlbumService.cs @@ -7,10 +7,11 @@ public interface IAlbumService { Task> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null); Task GetAlbumByIdAsync(int id); - Task CreateAlbumAsync(string name, string? description, int userId); - Task UpdateAlbumAsync(int id, string name, string? description, int? userId = null); + Task CreateAlbumAsync(string name, string? description, int userId,int? coverPictureId = null); + Task UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = null); Task DeleteAlbumAsync(int id); Task AddPictureToAlbumAsync(int albumId, int pictureId); Task AddPicturesToAlbumAsync(int albumId, List pictureIds); Task RemovePictureFromAlbumAsync(int albumId, int pictureId); + Task SetAlbumCoverAsync(int albumId, int pictureId, int userId); } diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index 5e1d8ec..7d3df75 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -8,6 +8,7 @@ using Foxel.Services.Background; using Foxel.Services.Configuration; using Foxel.Services.Storage; using Foxel.Services.VectorDB; +using Foxel.Services.Mapping; using Foxel.Utils; using Microsoft.EntityFrameworkCore; @@ -20,50 +21,10 @@ public class PictureService( IBackgroundTaskQueue backgroundTaskQueue, IVectorDbService vectorDbService, IStorageService storageService, + IMappingService mappingService, // 添加 IMappingService ILogger logger) : IPictureService { - private async Task MapPictureToResponseAsync(Picture picture) - { - string pathUrl = string.Empty; - if (!string.IsNullOrEmpty(picture.Path)) - { - pathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider => - Task.FromResult(provider.GetUrl(picture.Id,picture.Path))); - } - - string originalPathUrl = string.Empty; - if (!string.IsNullOrEmpty(picture.OriginalPath)) - { - originalPathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider => - Task.FromResult(provider.GetUrl(picture.Id,picture.OriginalPath))); - } - - string? thumbnailPathUrl = null; - if (!string.IsNullOrEmpty(picture.ThumbnailPath)) - { - thumbnailPathUrl = await storageService.ExecuteAsync(picture.StorageModeId, provider => - Task.FromResult(provider.GetUrl(picture.Id,picture.ThumbnailPath))); - } - return new PictureResponse - { - Id = picture.Id, - Name = picture.Name, - Path = pathUrl, - OriginalPath = originalPathUrl, - ThumbnailPath = thumbnailPathUrl, - Description = picture.Description, - CreatedAt = picture.CreatedAt, - Tags = picture.Tags != null ? picture.Tags.Select(t => t.Name).ToList() : new List(), - TakenAt = picture.TakenAt, - ExifInfo = picture.ExifInfo ?? new ExifInfo(), - UserId = picture.UserId, - Username = picture.User?.UserName, - AlbumId = picture.AlbumId, - Permission = picture.Permission, - StorageModeName = picture.StorageMode?.Name - }; - } public async Task> GetPicturesAsync( int page = 1, @@ -143,20 +104,19 @@ public class PictureService( var picturesData = await dbContext.Pictures .Include(p => p.Tags) .Include(p => p.User) - .Include(p=>p.StorageMode) + .Include(p => p.StorageMode) .Where(p => ids.Contains((ulong)p.Id)) .ToListAsync(); var picturesOrdered = ids .Select(id => picturesData.FirstOrDefault(p => p.Id == (int)id)) .Where(p => p != null) .ToList(); - var paginatedResultsTasks = picturesOrdered + var paginatedResults = picturesOrdered .Skip((page - 1) * pageSize) .Take(pageSize) - .Select(async p => await MapPictureToResponseAsync(p!)) + .Select(p => mappingService.MapPictureToResponse(p!)) .ToList(); - var paginatedResults = (await Task.WhenAll(paginatedResultsTasks)).ToList(); var totalCount = picturesOrdered.Count; @@ -220,16 +180,15 @@ public class PictureService( // 获取分页数据 var picturesData = await query - .Include(x=>x.StorageMode) + .Include(x => x.StorageMode) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); // 转换为响应格式 - var picturesTasks = picturesData - .Select(async p => await MapPictureToResponseAsync(p)) + var pictures = picturesData + .Select(p => mappingService.MapPictureToResponse(p)) .ToList(); - var pictures = (await Task.WhenAll(picturesTasks)).ToList(); // 处理收藏信息 await PopulateFavoriteInfo(dbContext, pictures, userId); @@ -663,7 +622,7 @@ public class PictureService( await backgroundTaskQueue.QueueVisualRecognitionTaskAsync(visualRecognitionPayload); } - var pictureResponse = await MapPictureToResponseAsync(picture); + var pictureResponse = mappingService.MapPictureToResponse(picture); return (pictureResponse, picture.Id); } finally @@ -906,7 +865,7 @@ public class PictureService( picture.UpdatedAt = DateTime.UtcNow; await dbContext.SaveChangesAsync(); - var pictureResponse = await MapPictureToResponseAsync(picture); + var pictureResponse = mappingService.MapPictureToResponse(picture); return (pictureResponse, userId); } @@ -986,7 +945,7 @@ public class PictureService( return null; } - var pictureResponse = await MapPictureToResponseAsync(picture); + var pictureResponse = mappingService.MapPictureToResponse(picture); return picture; } diff --git a/Web/src/api/albumApi.ts b/Web/src/api/albumApi.ts index b65a911..737f783 100644 --- a/Web/src/api/albumApi.ts +++ b/Web/src/api/albumApi.ts @@ -5,18 +5,21 @@ export interface AlbumResponse { id: number; name: string; description: string; - coverImageUrl?: string; pictureCount: number; userId: number; username?: string; createdAt: Date; updatedAt: Date; + coverPictureId?: number | null; + coverPicturePath?: string; + coverPictureThumbnailPath?: string; } // 创建相册请求 export interface CreateAlbumRequest { name: string; description: string; + coverPictureId?: number | null; // 新增:封面图片ID } // 更新相册请求 @@ -24,6 +27,7 @@ export interface UpdateAlbumRequest { id: number; name: string; description: string; + coverPictureId?: number | null; // 新增:封面图片ID } // 相册图片操作请求 diff --git a/Web/src/api/albumManagementApi.ts b/Web/src/api/albumManagementApi.ts new file mode 100644 index 0000000..78092bd --- /dev/null +++ b/Web/src/api/albumManagementApi.ts @@ -0,0 +1,96 @@ +import type { AlbumResponse } from './albumApi'; +import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient'; +import type { PictureResponse } from './pictureApi'; // For pictures within an album + + +export interface AlbumCreateRequest { + name: string; + description?: string; + coverPictureId?: number | null; +} + +export interface AlbumUpdateRequest { + name?: string; + description?: string; + coverPictureId?: number | null; +} + +export const getManagementAlbums = async ( + page: number = 1, + pageSize: number = 10, + searchQuery?: string, + userId?: number +): Promise> => { + const params = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString() + }); + if (searchQuery) params.append('searchQuery', searchQuery); + if (userId) params.append('userId', userId.toString()); + + const response = await fetchApi(`/management/album/get_albums?${params.toString()}`); + return response as PaginatedResult; +}; + +// Get album by ID +export const getManagementAlbumById = async (id: number): Promise> => { + return fetchApi(`/management/album/get_album/${id}`); +}; + +// Create album +export const createManagementAlbum = async (request: AlbumCreateRequest): Promise> => { + return fetchApi( + '/management/album/create_album', + { + method: 'POST', + body: JSON.stringify(request) + } + ); +}; + +// Update album +export const updateManagementAlbum = async (id: number, request: AlbumUpdateRequest): Promise> => { + return fetchApi( + `/management/album/update_album/${id}`, + { + method: 'POST', + body: JSON.stringify(request) + } + ); +}; + +// Delete album +export const deleteManagementAlbum = async (id: number): Promise> => { + return fetchApi( + '/management/album/delete_album', + { + method: 'POST', + body: JSON.stringify(id) // Backend expects int id in body + } + ); +}; + +// Batch delete albums +export const batchDeleteManagementAlbums = async (ids: number[]): Promise> => { + return fetchApi( + '/management/album/batch_delete_albums', + { + method: 'POST', + body: JSON.stringify(ids) + } + ); +}; + +// Get pictures in album +export const getPicturesInAlbum = async ( + albumId: number, + page: number = 1, + pageSize: number = 10 +): Promise> => { + const params = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString() + }); + const response = await fetchApi(`/management/album/${albumId}/pictures?${params.toString()}`); + return response as PaginatedResult; +}; diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 158685a..c6ab60f 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -1,5 +1,6 @@ export * from './authApi'; export * from './albumApi'; +export * from './albumManagementApi'; export * from './backgroundTaskApi'; export * from './configApi'; export * from './fetchClient'; diff --git a/Web/src/pages/admin/album/Index.tsx b/Web/src/pages/admin/album/Index.tsx new file mode 100644 index 0000000..4842c72 --- /dev/null +++ b/Web/src/pages/admin/album/Index.tsx @@ -0,0 +1,364 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Table, Button, Card, Input, Space, Modal, + message, Typography, Popconfirm, Row, Col, Image, + AutoComplete, Form, Divider, Select, Tag +} from 'antd'; +import { + BookOutlined, DeleteOutlined, SearchOutlined, ExclamationCircleOutlined, + ReloadOutlined, FilterOutlined, ClearOutlined, PlusOutlined, EditOutlined, PictureOutlined +} from '@ant-design/icons'; +import { + getManagementAlbums, deleteManagementAlbum, batchDeleteManagementAlbums, + createManagementAlbum, updateManagementAlbum, + type AlbumCreateRequest, type AlbumUpdateRequest +} from '../../../api/albumManagementApi'; +import { getUsers, getManagementPictures, type AlbumResponse } from '../../../api'; // Renamed to avoid conflict +import { useOutletContext } from 'react-router'; +import type { Breakpoint } from 'antd'; + +const { Title, Text } = Typography; +const { confirm } = Modal; + +interface PictureOption { + value: number; + label: string; + thumbnailPath?: string; +} + +const AlbumManagement: React.FC = () => { + const { isMobile } = useOutletContext<{ isMobile: boolean }>(); + + const [albums, setAlbums] = useState([]); + const [userOptions, setUserOptions] = useState<{ value: number; label: string }[]>([]); + const [pictureOptions, setPictureOptions] = useState([]); + + const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const [searchQuery, setSearchQuery] = useState(''); + const [selectedUserId, setSelectedUserId] = useState(); + const [showFilters, setShowFilters] = useState(false); + const [filterForm] = Form.useForm(); + + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingAlbum, setEditingAlbum] = useState(null); + const [albumForm] = Form.useForm(); + + const searchUsers = useCallback(async (searchValue: string) => { + if (!searchValue.trim()) { + setUserOptions([]); + return; + } + try { + const response = await getUsers({ page: 1, pageSize: 20, searchQuery: searchValue }); + if (response.success && response.data) { + setUserOptions(response.data.map(user => ({ value: user.id, label: `${user.userName} (${user.email})` }))); + } + } catch (error) { console.error('Error searching users:', error); } + }, []); + + const searchPicturesForCover = useCallback(async (searchValue: string) => { + if (!searchValue.trim() && pictureOptions.length > 5) return; // Avoid frequent calls if not searching + try { + const response = await getManagementPictures(1, 20, searchValue); + if (response.success && response.data) { + setPictureOptions(response.data.map(pic => ({ + value: pic.id, + label: `${pic.name || '未命名图片'} (ID: ${pic.id})`, + thumbnailPath: pic.thumbnailPath || pic.path, + }))); + } + } catch (error) { + console.error('Error searching pictures for cover:', error); + message.error('搜索封面图片失败'); + } + }, [pictureOptions.length]); + + + const fetchAlbums = useCallback(async ( + page = currentPage, size = pageSize, query = searchQuery, userId = selectedUserId + ) => { + setLoading(true); + try { + const response = await getManagementAlbums(page, size, query, userId); + if (response.success && response.data) { + setAlbums(response.data || []); + setTotal(response.totalCount || 0); + } else { + message.error(response.message || '获取相册列表失败'); + } + } catch (error) { + message.error('获取相册列表失败,请检查网络连接'); + } finally { + setLoading(false); + } + }, [currentPage, pageSize, searchQuery, selectedUserId]); + + useEffect(() => { + fetchAlbums(); + searchPicturesForCover(''); // Initial load for picture options + }, [fetchAlbums, searchPicturesForCover]); + + const handlePageChange = (page: number, size?: number) => { + setCurrentPage(page); + if (size) setPageSize(size); + fetchAlbums(page, size || pageSize); + }; + + const handleQuickSearch = (value: string) => { + setSearchQuery(value); + setCurrentPage(1); + fetchAlbums(1, pageSize, value, selectedUserId); + }; + + const handleFilter = async () => { + const values = await filterForm.validateFields(); + setSearchQuery(values.searchQuery || ''); + setSelectedUserId(values.userId); + setCurrentPage(1); + fetchAlbums(1, pageSize, values.searchQuery, values.userId); + }; + + const handleClearFilters = () => { + filterForm.resetFields(); + setSearchQuery(''); + setSelectedUserId(undefined); + setUserOptions([]); + setCurrentPage(1); + fetchAlbums(1, pageSize, '', undefined); + }; + + const showCreateModal = () => { + setEditingAlbum(null); + albumForm.resetFields(); + albumForm.setFieldsValue({ coverPictureId: null }); // Ensure coverPictureId is null or undefined + setIsModalVisible(true); + }; + + const showEditModal = (album: AlbumResponse) => { + setEditingAlbum(album); + const formValues = { + name: album.name, + description: album.description, + coverPictureId: album.coverPictureId, // 使用正确的 coverPictureId + }; + + // 如果存在封面图片ID,并且该图片不在当前选项中,则尝试添加它以便Select可以正确显示 + if (album.coverPictureId && (album.coverPictureThumbnailPath || album.coverPicturePath)) { + const existingOption = pictureOptions.find(opt => opt.value === album.coverPictureId); + if (!existingOption) { + // 为了在Select中显示当前封面,需要一个标签。 + // 理想情况下,AlbumResponse会包含封面图片的名称。 + // 此处使用文件名或ID作为后备标签。 + const pictureLabel = album.name ? `${album.name} (封面)` : `图片ID: ${album.coverPictureId}`; + + const newPictureOption: PictureOption = { + value: album.coverPictureId, + label: pictureLabel, // 使用相册名或ID作为临时标签 + thumbnailPath: album.coverPictureThumbnailPath || album.coverPicturePath, + }; + // 将当前封面图片添加到选项列表的开头,以便在Select中显示 + setPictureOptions(prevOptions => [newPictureOption, ...prevOptions.filter(opt => opt.value !== album.coverPictureId)]); + } + } + + albumForm.setFieldsValue(formValues); + setIsModalVisible(true); + }; + + const handleModalOk = async () => { + try { + const values = await albumForm.validateFields() as AlbumCreateRequest | AlbumUpdateRequest; + setLoading(true); + let response; + if (editingAlbum) { + response = await updateManagementAlbum(editingAlbum.id, values); + } else { + response = await createManagementAlbum(values as AlbumCreateRequest); + } + + if (response.success) { + message.success(editingAlbum ? '相册更新成功' : '相册创建成功'); + setIsModalVisible(false); + fetchAlbums(); + } else { + message.error(response.message || (editingAlbum ? '更新失败' : '创建失败')); + } + } catch (errorInfo) { + console.log('Validate Failed:', errorInfo); + message.error('请检查表单输入'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: number) => { + try { + const response = await deleteManagementAlbum(id); + if (response.success) { + message.success('相册删除成功'); + fetchAlbums(); + } else { + message.error(response.message || '删除相册失败'); + } + } catch (error) { message.error('删除相册失败'); } + }; + + const handleBatchDelete = async () => { + if (selectedRowKeys.length === 0) { + message.warning('请选择要删除的相册'); + return; + } + confirm({ + title: `确定要删除 ${selectedRowKeys.length} 个相册吗?`, + icon: , + content: '此操作不可逆,相册将被永久删除 (相册内图片不会被删除)', + async onOk() { + try { + const response = await batchDeleteManagementAlbums(selectedRowKeys as number[]); + if (response.success && response.data) { + message.success(`成功删除 ${response.data.successCount} 个相册`); + if (response.data.failedCount > 0) { + message.warning(`${response.data.failedCount} 个相册删除失败`); + } + setSelectedRowKeys([]); + fetchAlbums(); + } else { + message.error(response.message || '批量删除失败'); + } + } catch (error) { message.error('批量删除失败'); } + } + }); + }; + + + + const columns = [ + { title: 'ID', dataIndex: 'id', key: 'id', responsive: ['md'] as Breakpoint[] }, + { + title: '封面', dataIndex: 'coverPictureThumbnailPath', key: 'cover', + render: (path?: string, record?: AlbumResponse) => ( + path ? + : record?.coverPicturePath ? + : + ), + }, + { title: '名称', dataIndex: 'name', key: 'name' }, + { title: '描述', dataIndex: 'description', key: 'description', responsive: ['md'] as Breakpoint[], render: (desc: string) => desc || '-'}, + { title: '图片数', dataIndex: 'pictureCount', key: 'pictureCount', render: (count: number) => {count} }, + { title: '用户', dataIndex: 'username', key: 'username', responsive: ['lg'] as Breakpoint[] }, + { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', responsive: ['lg'] as Breakpoint[], render: (date: Date) => new Date(date).toLocaleString() }, + { + title: '操作', key: 'action', + render: (_: any, record: AlbumResponse) => ( + + + handleDelete(record.id)}> + + + {/* Add 'Set Cover' and 'Manage Pictures' buttons here later */} + + ), + }, + ]; + + return ( +
+ + + + + 相册管理 + + + 管理系统中的所有相册,包括创建、编辑、删除和批量操作 + + + + + + + + + + + + + + + + } onSearch={handleQuickSearch} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> + + + + {showFilters && ( + <> + +
+ + + + + +
+
+ + + )} + + `共 ${total} 条` }} + rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys) }} + size={isMobile ? "small" : "middle"} + scroll={{ x: 'max-content' }} + /> + + + setIsModalVisible(false)} + confirmLoading={loading} + destroyOnClose + width={600} + > +
+ + + + + + + + + option?.label.toLowerCase().includes(input.toLowerCase()) + } + options={albumPicturesForSelect.map(p => ({ + value: p.id, + label: p.name || `图片 ${p.id}`, + thumbnail: p.thumbnailPath || p.path + }))} + // optionRender={(option) => ( + // + // {option.data.thumbnail && {option.label}} + // {option.label} + // + // )} + /> + */} + +
diff --git a/Web/src/pages/albums/Index.tsx b/Web/src/pages/albums/Index.tsx index a284a61..42f61f8 100644 --- a/Web/src/pages/albums/Index.tsx +++ b/Web/src/pages/albums/Index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Typography, Row, Col, Card, Button, Modal, Form, Input, Spin, Empty, message, Popconfirm } from 'antd'; +import { Typography, Row, Col, Card, Button, Modal, Form, Input, Spin, Empty, message, Popconfirm, InputNumber, Select } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, PictureOutlined } from '@ant-design/icons'; import { getAlbums, createAlbum, updateAlbum, deleteAlbum } from '../../api'; import type { AlbumResponse, CreateAlbumRequest, UpdateAlbumRequest } from '../../api'; @@ -43,7 +43,7 @@ function Albums() { const handleCreateAlbum = async (values: CreateAlbumRequest) => { try { - const result = await createAlbum(values); + const result = await createAlbum(values); // values 现在可以包含 coverPictureId if (result.success && result.data) { message.success('相册创建成功'); setIsCreateModalVisible(false); @@ -58,14 +58,16 @@ function Albums() { } }; - const handleEditAlbum = async (values: UpdateAlbumRequest) => { + const handleEditAlbum = async (values: Omit) => { // Form values don't include id if (!currentAlbum) return; try { - const result = await updateAlbum({ + const requestData: UpdateAlbumRequest = { ...values, - id: currentAlbum.id - }); + id: currentAlbum.id, + // coverPictureId 来自表单 values.coverPictureId + }; + const result = await updateAlbum(requestData); if (result.success && result.data) { message.success('相册更新成功'); @@ -101,7 +103,8 @@ function Albums() { setCurrentAlbum(album); editForm.setFieldsValue({ name: album.name, - description: album.description + description: album.description, + coverPictureId: album.coverPictureId // 设置当前封面ID }); setIsEditModalVisible(true); }; @@ -188,12 +191,16 @@ function Albums() { bodyStyle={{ padding: '20px' }} cover={ - {album.coverImageUrl ? ( - {album.name} + {album.coverPictureThumbnailPath || album.coverPicturePath ? ( + {album.name} ) : (