feat(album): add cover picture functionality to albums and enhance album management API

This commit is contained in:
shiyu
2025-06-09 15:12:50 +08:00
parent 9d9393f9ce
commit e55f311c04
24 changed files with 1496 additions and 251 deletions

View File

@@ -55,7 +55,7 @@ public class AlbumController(IAlbumService albumService) : BaseApiController
if (userId == null)
return Error<AlbumResponse>("无法识别用户信息", 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<AlbumResponse>("无法识别用户信息", 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<bool>($"从相册移除图片失败: {ex.Message}", 500);
}
}
}

View File

@@ -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<ActionResult<PaginatedResult<AlbumResponse>>> 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<AlbumResponse>($"获取相册列表失败: {ex.Message}", 500);
}
}
[HttpGet("get_album/{id}")]
public async Task<ActionResult<BaseResult<AlbumResponse>>> GetAlbumById(int id)
{
try
{
var album = await albumManagementService.GetAlbumByIdAsync(id);
return Success(album, "相册获取成功");
}
catch (KeyNotFoundException knfex)
{
return Error<AlbumResponse>(knfex.Message, 404);
}
catch (Exception ex)
{
return Error<AlbumResponse>($"获取相册失败: {ex.Message}", 500);
}
}
[HttpPost("create_album")]
public async Task<ActionResult<BaseResult<AlbumResponse>>> CreateAlbum([FromBody] AlbumCreateRequest request)
{
try
{
var userId = GetCurrentUserId();
if (userId is null) return Error<AlbumResponse>("用户未登录或无法识别用户", 401);
var album = await albumManagementService.CreateAlbumAsync(request, (int)userId);
return Success(album, "相册创建成功");
}
catch (Exception ex)
{
return Error<AlbumResponse>($"创建相册失败: {ex.Message}", 500);
}
}
[HttpPost("update_album/{id}")]
public async Task<ActionResult<BaseResult<AlbumResponse>>> UpdateAlbum(int id,
[FromBody] AlbumUpdateRequest request)
{
try
{
var album = await albumManagementService.UpdateAlbumAsync(id, request);
return Success(album, "相册更新成功");
}
catch (KeyNotFoundException knfex)
{
return Error<AlbumResponse>(knfex.Message, 404);
}
catch (Exception ex)
{
return Error<AlbumResponse>($"更新相册失败: {ex.Message}", 500);
}
}
[HttpPost("delete_album")]
public async Task<ActionResult<BaseResult<bool>>> DeleteAlbum([FromBody] int id) // Or [FromQuery] int id
{
try
{
var result = await albumManagementService.DeleteAlbumAsync(id);
return Success(result, "相册删除成功");
}
catch (KeyNotFoundException knfex)
{
return Error<bool>(knfex.Message, 404);
}
catch (Exception ex)
{
return Error<bool>($"删除相册失败: {ex.Message}", 500);
}
}
[HttpPost("batch_delete_albums")]
public async Task<ActionResult<BaseResult<BatchDeleteResult>>> BatchDeleteAlbums([FromBody] List<int> ids)
{
try
{
if (ids == null || ids.Count == 0)
{
return Error<BatchDeleteResult>("未提供相册ID");
}
var result = await albumManagementService.BatchDeleteAlbumsAsync(ids);
return Success(result, $"成功删除 {result.SuccessCount} 个相册,失败 {result.FailedCount} 个");
}
catch (Exception ex)
{
return Error<BatchDeleteResult>($"批量删除相册失败: {ex.Message}", 500);
}
}
[HttpGet("get_albums_by_user/{userId}")]
public async Task<ActionResult<PaginatedResult<AlbumResponse>>> 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<AlbumResponse>($"获取用户相册列表失败: {ex.Message}", 500);
}
}
[HttpPost("{albumId}/picture/{pictureId}/add")]
public async Task<ActionResult<BaseResult<bool>>> AddPictureToAlbum(int albumId, int pictureId)
{
try
{
var result = await albumManagementService.AddPictureToAlbumAsync(albumId, pictureId);
return Success(result, "图片已成功添加到相册");
}
catch (KeyNotFoundException knfex)
{
return Error<bool>(knfex.Message, 404);
}
catch (Exception ex)
{
return Error<bool>($"添加图片到相册失败: {ex.Message}", 500);
}
}
[HttpPost("{albumId}/picture/{pictureId}/remove")]
public async Task<ActionResult<BaseResult<bool>>> RemovePictureFromAlbum(int albumId, int pictureId)
{
try
{
var result = await albumManagementService.RemovePictureFromAlbumAsync(albumId, pictureId);
return Success(result, "图片已成功从相册移除");
}
catch (KeyNotFoundException knfex)
{
return Error<bool>(knfex.Message, 404);
}
catch (Exception ex)
{
return Error<bool>($"从相册移除图片失败: {ex.Message}", 500);
}
}
[HttpGet("{albumId}/pictures")]
public async Task<ActionResult<PaginatedResult<PictureResponse>>> 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<PictureResponse>(knfex.Message, 404);
}
catch (Exception ex)
{
return PaginatedError<PictureResponse>($"获取相册内图片失败: {ex.Message}", 500);
}
}
[HttpPost("{albumId}/set_cover/{pictureId}")]
public async Task<ActionResult<BaseResult<bool>>> SetAlbumCover(int albumId, int pictureId)
{
try
{
var result = await albumManagementService.SetAlbumCoverAsync(albumId, pictureId);
return Success(result, "相册封面设置成功");
}
catch (KeyNotFoundException knfex)
{
return Error<bool>(knfex.Message, 404);
}
catch (InvalidOperationException ioex)
{
return Error<bool>(ioex.Message, 400);
}
catch (Exception ex)
{
return Error<bool>($"设置相册封面失败: {ex.Message}", 500);
}
}
}
}

View File

@@ -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<IAlbumService, AlbumService>();
services.AddSingleton<IUserManagementService, UserManagementService>();
services.AddSingleton<IPictureManagementService, PictureManagementService>();
services.AddSingleton<IAlbumManagementService, AlbumManagementService>();
services.AddSingleton<ILogManagementService, LogManagementService>();
services.AddSingleton<IStorageManagementService, StorageManagementService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
@@ -38,6 +40,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<PictureTaskProcessor>();
services.AddSingleton<VisualRecognitionTaskProcessor>();
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
services.AddSingleton<IMappingService, MappingService>();
}
public static void AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration)

View File

@@ -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<Picture>? Pictures { get; set; }
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -10,4 +10,7 @@ public record CreateAlbumRequest
[StringLength(500)]
public string? Description { get; set; }
public int? CoverPictureId { get; set; }
}

View File

@@ -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; }
}

View File

@@ -15,4 +15,20 @@ public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(opti
public DbSet<Log> Logs { get; set; } = null!;
public DbSet<BackgroundTask> BackgroundTasks { get; set; } = null!;
public DbSet<StorageMode> StorageModes { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Album>()
.HasOne(a => a.CoverPicture)
.WithMany()
.HasForeignKey(a => a.CoverPictureId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Album>()
.HasMany(a => a.Pictures)
.WithOne(p => p.Album)
.HasForeignKey(p => p.AlbumId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@@ -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<MyDbContext> contextFactory,
IMappingService mappingService,
ILogger<AlbumManagementService> logger)
: IAlbumManagementService
{
public async Task<PaginatedResult<AlbumResponse>> 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<AlbumResponse>
{
Data = albumResponses,
Page = page,
PageSize = pageSize,
TotalCount = totalCount
};
}
public async Task<AlbumResponse> 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<AlbumResponse> 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<AlbumResponse> 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<bool> 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<BatchDeleteResult> BatchDeleteAlbumsAsync(List<int> 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<PaginatedResult<AlbumResponse>> GetAlbumsByUserIdAsync(int userId, int page = 1,
int pageSize = 10)
{
return await GetAlbumsAsync(page, pageSize, null, userId);
}
public async Task<bool> 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<bool> 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<PaginatedResult<PictureResponse>> 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<Picture, ICollection<Favorite>?>)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<PictureResponse>
{
Data = pictureResponses,
Page = page,
PageSize = pageSize,
TotalCount = totalCount
};
}
public async Task<bool> 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;
}
}
}

View File

@@ -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<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null);
Task<AlbumResponse> GetAlbumByIdAsync(int id);
Task<AlbumResponse> CreateAlbumAsync(AlbumCreateRequest request, int creatorUserId);
Task<AlbumResponse> UpdateAlbumAsync(int id, AlbumUpdateRequest request);
Task<bool> DeleteAlbumAsync(int id);
Task<BatchDeleteResult> BatchDeleteAlbumsAsync(List<int> ids);
Task<PaginatedResult<AlbumResponse>> GetAlbumsByUserIdAsync(int userId, int page = 1, int pageSize = 10);
Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId);
Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId);
Task<PaginatedResult<PictureResponse>> GetPicturesInAlbumAsync(int albumId, int page = 1, int pageSize = 10);
Task<bool> SetAlbumCoverAsync(int albumId, int pictureId);
}
}

View File

@@ -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);
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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<MyDbContext> contextFactory,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
IMappingService mappingService) // 注入新的映射服务
: IAlbumService
{
public async Task<PaginatedResult<AlbumResponse>> 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<Album> 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<AlbumResponse>
{
Data = albumResponses,
@@ -65,206 +71,222 @@ public class AlbumService(
TotalCount = totalCount
};
}
public async Task<AlbumResponse> 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<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId)
public async Task<AlbumResponse> 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<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null)
public async Task<AlbumResponse> 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<bool> 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<bool> 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<bool> 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<bool> AddPicturesToAlbumAsync(int albumId, List<int> 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<bool> 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;
}
}

View File

@@ -7,10 +7,11 @@ public interface IAlbumService
{
Task<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null);
Task<AlbumResponse> GetAlbumByIdAsync(int id);
Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId);
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null);
Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId,int? coverPictureId = null);
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = null);
Task<bool> DeleteAlbumAsync(int id);
Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId);
Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds);
Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId);
Task<bool> SetAlbumCoverAsync(int albumId, int pictureId, int userId);
}

View File

@@ -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<PictureService> logger)
: IPictureService
{
private async Task<PictureResponse> 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<string>(),
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<PaginatedResult<PictureResponse>> 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;
}

View File

@@ -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
}
// 相册图片操作请求

View File

@@ -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<PaginatedResult<AlbumResponse>> => {
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<AlbumResponse>;
};
// Get album by ID
export const getManagementAlbumById = async (id: number): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(`/management/album/get_album/${id}`);
};
// Create album
export const createManagementAlbum = async (request: AlbumCreateRequest): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(
'/management/album/create_album',
{
method: 'POST',
body: JSON.stringify(request)
}
);
};
// Update album
export const updateManagementAlbum = async (id: number, request: AlbumUpdateRequest): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(
`/management/album/update_album/${id}`,
{
method: 'POST',
body: JSON.stringify(request)
}
);
};
// Delete album
export const deleteManagementAlbum = async (id: number): Promise<BaseResult<boolean>> => {
return fetchApi<boolean>(
'/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<BaseResult<BatchDeleteResult>> => {
return fetchApi<BatchDeleteResult>(
'/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<PaginatedResult<PictureResponse>> => {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString()
});
const response = await fetchApi(`/management/album/${albumId}/pictures?${params.toString()}`);
return response as PaginatedResult<PictureResponse>;
};

View File

@@ -1,5 +1,6 @@
export * from './authApi';
export * from './albumApi';
export * from './albumManagementApi';
export * from './backgroundTaskApi';
export * from './configApi';
export * from './fetchClient';

View File

@@ -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<AlbumResponse[]>([]);
const [userOptions, setUserOptions] = useState<{ value: number; label: string }[]>([]);
const [pictureOptions, setPictureOptions] = useState<PictureOption[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [selectedUserId, setSelectedUserId] = useState<number | undefined>();
const [showFilters, setShowFilters] = useState(false);
const [filterForm] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingAlbum, setEditingAlbum] = useState<AlbumResponse | null>(null);
const [albumForm] = Form.useForm<AlbumCreateRequest | AlbumUpdateRequest>();
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: <ExclamationCircleOutlined />,
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 ? <Image width={50} height={50} src={path} style={{ objectFit: 'cover', borderRadius: 4 }} />
: record?.coverPicturePath ? <Image width={50} height={50} src={record.coverPicturePath} style={{ objectFit: 'cover', borderRadius: 4 }} />
: <PictureOutlined style={{ fontSize: 30, color: '#ccc' }}/>
),
},
{ 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) => <Tag>{count}</Tag> },
{ 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) => (
<Space size="small" wrap>
<Button type="link" icon={<EditOutlined />} onClick={() => showEditModal(record)}></Button>
<Popconfirm title="确定删除此相册?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
{/* Add 'Set Cover' and 'Manage Pictures' buttons here later */}
</Space>
),
},
];
return (
<div className="album-management">
<Row gutter={[16, 16]} align="middle" justify="space-between">
<Col>
<Space align="center">
<BookOutlined style={{ fontSize: 24 }} />
<Title level={2} style={{ margin: 0 }}></Title>
</Space>
<Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
</Text>
</Col>
</Row>
<Card style={{ marginTop: 16 }}>
<Row gutter={[16, 16]} justify="space-between" style={{ marginBottom: 16 }}>
<Col xs={24} sm={12} md={16}>
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={showCreateModal}></Button>
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0}></Button>
<Button icon={<ReloadOutlined />} onClick={() => fetchAlbums()}></Button>
<Button icon={<FilterOutlined />} onClick={() => setShowFilters(!showFilters)} type={showFilters ? 'primary' : 'default'}></Button>
</Space>
</Col>
<Col xs={24} sm={12} md={8}>
<Input.Search placeholder="搜索相册名称或描述" allowClear enterButton={<SearchOutlined />} onSearch={handleQuickSearch} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
</Col>
</Row>
{showFilters && (
<>
<Card size="small" style={{ marginBottom: 16, backgroundColor: '#fafafa' }}>
<Form form={filterForm} layout="inline" onFinish={handleFilter} initialValues={{ searchQuery, userId: selectedUserId }}>
<Form.Item name="searchQuery" label="关键词"><Input placeholder="名称或描述" style={{ width: 200 }} /></Form.Item>
<Form.Item name="userId" label="所属用户">
<AutoComplete style={{ width: 250 }} options={userOptions} onSearch={searchUsers} placeholder="输入用户名或邮箱" allowClear filterOption={false} />
</Form.Item>
<Form.Item><Space><Button type="primary" htmlType="submit" icon={<SearchOutlined />}></Button><Button icon={<ClearOutlined />} onClick={handleClearFilters}></Button></Space></Form.Item>
</Form>
</Card>
<Divider style={{ margin: '16px 0' }} />
</>
)}
<Table
rowKey="id"
columns={columns}
dataSource={albums}
loading={loading}
pagination={{ current: currentPage, pageSize, total, showSizeChanger: true, showQuickJumper: true, onChange: handlePageChange, showTotal: (total) => `${total}` }}
rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys) }}
size={isMobile ? "small" : "middle"}
scroll={{ x: 'max-content' }}
/>
</Card>
<Modal
title={editingAlbum ? '编辑相册' : '创建新相册'}
open={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
confirmLoading={loading}
destroyOnClose
width={600}
>
<Form form={albumForm} layout="vertical" name="albumForm">
<Form.Item name="name" label="相册名称" rules={[{ required: true, message: '请输入相册名称' }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="相册描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="coverPictureId" label="封面图片 (可选)">
<Select
showSearch
allowClear
placeholder="搜索并选择封面图片"
onSearch={searchPicturesForCover}
filterOption={false} // Server-side search
notFoundContent={loading ? "搜索中..." : "无匹配图片"}
options={pictureOptions}
optionRender={(option) => (
<Space>
{option.data?.thumbnailPath && <Image src={option.data.thumbnailPath} width={30} height={30} preview={false} style={{objectFit: 'cover'}}/>}
<span>{option.label}</span>
</Space>
)}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AlbumManagement;

View File

@@ -11,7 +11,7 @@ import {
import type { ColumnsType } from 'antd/es/table';
import { useOutletContext } from 'react-router';
import { useNavigate } from 'react-router';
import { getUsers, getManagementPictures } from '../../../api';
import { getUsers, getManagementPictures, getManagementAlbums } from '../../../api';
import type { UserResponse, PictureResponse } from '../../../api';
const { Title, Text } = Typography;
@@ -21,8 +21,6 @@ interface DashboardStats {
totalAlbums: number;
totalPhotos: number;
storageUsagePercentage: number;
newUsersToday: number;
newPhotosToday: number;
softwareVersion: string;
systemVersion: string;
cpuArchitecture: string;
@@ -37,8 +35,6 @@ const AdminDashboard: React.FC = () => {
totalAlbums: 0,
totalPhotos: 0,
storageUsagePercentage: 0,
newUsersToday: 0,
newPhotosToday: 0,
softwareVersion: 'N/A',
systemVersion: 'N/A',
cpuArchitecture: 'N/A'
@@ -85,40 +81,32 @@ const AdminDashboard: React.FC = () => {
}
};
// 计算今日新增数据
const calculateTodayStats = (users: UserResponse[], photos: PictureResponse[]) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const newUsersToday = users.filter(user => {
const userDate = new Date(user.createdAt);
userDate.setHours(0, 0, 0, 0);
return userDate.getTime() === today.getTime();
}).length;
const newPhotosToday = photos.filter(photo => {
const photoDate = new Date(photo.createdAt);
photoDate.setHours(0, 0, 0, 0);
return photoDate.getTime() === today.getTime();
}).length;
setStats(prev => ({
...prev,
newUsersToday,
newPhotosToday
}));
// 获取相册总数
const fetchTotalAlbums = async () => {
try {
// 我们只需要总数,所以 pageSize 可以设为 1
const response = await getManagementAlbums(1, 1);
if (response.success) {
setStats(prev => ({
...prev,
totalAlbums: response.totalCount || 0
}));
}
} catch (error) {
console.error('Error fetching total albums:', error);
message.error('获取相册总数失败');
}
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
await Promise.all([fetchRecentUsers(), fetchRecentPhotos()]);
await Promise.all([fetchRecentUsers(), fetchRecentPhotos(), fetchTotalAlbums()]);
// 设置其他静态统计数据
setStats(prev => ({
...prev,
totalAlbums: 348, // 相册功能暂未实现,使用模拟数据
storageUsagePercentage: 68,
softwareVersion: 'Foxel Dev 尝鲜版',
systemVersion: 'Fedora 42',
@@ -134,13 +122,6 @@ const AdminDashboard: React.FC = () => {
loadData();
}, []);
// 当用户和图片数据都加载完成后计算今日统计
useEffect(() => {
if (recentUsers.length > 0 && recentPhotos.length > 0) {
calculateTodayStats(recentUsers, recentPhotos);
}
}, [recentUsers, recentPhotos]);
const userColumns = useMemo<ColumnsType<UserResponse>>(() => [
{
title: '用户名',
@@ -237,11 +218,6 @@ const AdminDashboard: React.FC = () => {
title="用户总数"
value={stats.totalUsers}
prefix={<UserOutlined />}
suffix={
<Tag color="green" style={{ marginLeft: 8 }}>
<ArrowUpOutlined /> {stats.newUsersToday}
</Tag>
}
/>
</Card>
</Col>
@@ -260,11 +236,6 @@ const AdminDashboard: React.FC = () => {
title="照片总数"
value={stats.totalPhotos}
prefix={<PictureOutlined />}
suffix={
<Tag color="green" style={{ marginLeft: 8 }}>
<ArrowUpOutlined /> {stats.newPhotosToday}
</Tag>
}
/>
</Card>
</Col>

View File

@@ -2,7 +2,8 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useParams, useNavigate, useOutletContext } from 'react-router';
import {
Typography, Button, Spin, Empty, message,
Popconfirm, Modal, Form, Input} from 'antd';
Popconfirm, Modal, Form, Input, InputNumber, Select // Added InputNumber, Select
} from 'antd';
import {
EditOutlined, DeleteOutlined, PlusOutlined} from '@ant-design/icons';
import { getAlbumById, deleteAlbum, favoritePicture, unfavoritePicture, addPicturesToAlbum, updateAlbum } from '../../api';
@@ -28,7 +29,8 @@ function AlbumDetail() {
const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
const [editForm] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); // 添加刷新触发器
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [albumPicturesForSelect, setAlbumPicturesForSelect] = useState<PictureResponse[]>([]); // 用于编辑时选择封面
const loadAlbum = async () => {
if (!id) return;
@@ -37,10 +39,11 @@ function AlbumDetail() {
const result = await getAlbumById(parseInt(id));
if (result.success && result.data) {
setAlbum(result.data);
// 添加检查确保 updateBreadcrumbTitle 是一个函数
if (typeof updateBreadcrumbTitle === 'function') {
updateBreadcrumbTitle(result.data.name);
}
// 假设相册详情API返回了图片列表或者需要另外获取
// setAlbumPicturesForSelect(result.data.pictures || []);
} else {
message.error(result.message || '获取相册失败');
}
@@ -54,7 +57,22 @@ function AlbumDetail() {
useEffect(() => {
loadAlbum();
}, [id]);
// 如果需要单独获取相册图片用于选择器:
// if (id) fetchPicturesForAlbumSelect(parseInt(id));
}, [id, refreshTrigger]); // refreshTrigger 确保编辑后重新加载图片列表(如果需要)
// 示例:获取相册内图片用于选择封面的函数
// const fetchPicturesForAlbumSelect = async (albumId: number) => {
// try {
// // 替换为实际获取相册内图片的API调用
// // const picturesResult = await getPicturesInAlbum(albumId, 1, 200); // 获取相册内所有图片
// // if (picturesResult.success && picturesResult.data) {
// // setAlbumPicturesForSelect(picturesResult.data);
// // }
// } catch (error) {
// message.error('获取相册内图片列表失败');
// }
// };
const handleDeleteAlbum = async () => {
if (!album) return;
@@ -152,8 +170,12 @@ function AlbumDetail() {
if (album) {
editForm.setFieldsValue({
name: album.name,
description: album.description || ''
description: album.description || '',
coverPictureId: album.coverPictureId // 设置当前封面ID
});
// 如果 album.pictures 存在,可以用它来填充选择器
// 或者在打开模态框时调用 fetchPicturesForAlbumSelect(album.id)
// setAlbumPicturesForSelect(album.pictures || []); // 假设 album 对象包含 pictures 数组
setIsEditModalVisible(true);
}
};
@@ -164,12 +186,13 @@ function AlbumDetail() {
try {
setSubmitting(true);
const values = await editForm.validateFields();
const values = await editForm.validateFields(); // values 包含 name, description, coverPictureId
const result = await updateAlbum({
id: album.id,
name: values.name,
description: values.description
description: values.description,
coverPictureId: values.coverPictureId // 传递封面ID
});
if (result.success) {
@@ -350,7 +373,8 @@ function AlbumDetail() {
layout="vertical"
initialValues={{
name: album.name,
description: album.description || ''
description: album.description || '',
coverPictureId: album.coverPictureId // 初始化表单的封面ID
}}
>
<Form.Item
@@ -371,6 +395,36 @@ function AlbumDetail() {
showCount
/>
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="从当前相册中选择一张图片作为封面。实际应用中应为图片选择器。"
>
{/*
实际应用中替换为图片选择器,例如:
<Select
showSearch
allowClear
placeholder="选择封面图片"
// loading={loadingAlbumPicturesForSelect}
filterOption={(input, option) =>
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) => (
// <Space>
// {option.data.thumbnail && <img src={option.data.thumbnail} alt={option.label} style={{width: 24, height: 24, objectFit: 'cover'}}/>}
// <span>{option.label}</span>
// </Space>
// )}
/>
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>

View File

@@ -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<UpdateAlbumRequest, 'id'>) => { // 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={
<Link to={`/albums/${album.id}`}>
{album.coverImageUrl ? (
<img alt={album.name} src={album.coverImageUrl} style={{
height: 180,
width: '100%',
objectFit: 'cover'
}} />
{album.coverPictureThumbnailPath || album.coverPicturePath ? (
<img
alt={album.name}
src={album.coverPictureThumbnailPath || album.coverPicturePath}
style={{
height: 180,
width: '100%',
objectFit: 'cover'
}}
/>
) : (
<div style={{
height: 180,
@@ -271,6 +278,32 @@ function Albums() {
>
<TextArea placeholder="描述一下这个相册" rows={4} />
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="输入图片ID。实际应用中应为图片选择器。"
>
{/*
实际应用中替换为图片选择器,例如:
<Select
showSearch
allowClear
placeholder="选择封面图片"
loading={loadingUserPictures}
filterOption={(input, option) =>
option?.label.toLowerCase().includes(input.toLowerCase())
}
options={userPictures.map(p => ({ value: p.id, label: p.name || `图片 ${p.id}`, thumbnail: p.thumbnailPath }))}
// optionRender={(option) => (
// <Space>
// {option.data.thumbnail && <img src={option.data.thumbnail} alt={option.label} style={{width: 24, height: 24, objectFit: 'cover'}}/>}
// <span>{option.label}</span>
// </Space>
// )}
/>
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Button style={{ marginRight: 8 }} onClick={() => {
setIsCreateModalVisible(false);
@@ -314,6 +347,18 @@ function Albums() {
>
<TextArea placeholder="相册描述" rows={4} />
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="输入图片ID。实际应用中应为图片选择器图片源为当前相册内图片或用户所有图片。"
>
{/*
实际应用中替换为图片选择器。
如果从当前相册选择,需要获取相册内图片列表。
如果像管理员一样从所有图片选择,则使用类似创建时的逻辑。
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Button style={{ marginRight: 8 }} onClick={() => {
setIsEditModalVisible(false);

View File

@@ -26,6 +26,7 @@ import PictureManagement from '../pages/admin/pictures/Index';
import UserDetail from '../pages/admin/users/UserDetail';
import AdminLogManagement from '../pages/admin/log/Index';
import StorageManagementPage from '../pages/admin/storage/StorageManagement';
import AlbumManagement from '../pages/admin/album/Index';
export interface RouteConfig {
path: string;
@@ -175,6 +176,18 @@ const routes: RouteConfig[] = [
title: '图片管理'
}
},
{
path: 'albums-admin',
key: 'admin-album',
icon: <FolderOutlined />,
label: '相册管理',
element: <AlbumManagement />,
area: 'admin',
groupLabel: '内容管理',
breadcrumb: {
title: '相册管理'
}
},
{
path: 'log',
key: 'admin-log',