mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-27 02:01:34 +08:00
feat(album): add cover picture functionality to albums and enhance album management API
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
216
Api/Management/AlbumManagementController.cs
Normal file
216
Api/Management/AlbumManagementController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
16
Models/Request/Album/AlbumCreateRequest.cs
Normal file
16
Models/Request/Album/AlbumCreateRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
Models/Request/Album/AlbumUpdateRequest.cs
Normal file
15
Models/Request/Album/AlbumUpdateRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,7 @@ public record CreateAlbumRequest
|
||||
|
||||
[StringLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
|
||||
public int? CoverPictureId { get; set; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
308
Services/Management/AlbumManagementService.cs
Normal file
308
Services/Management/AlbumManagementService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Services/Management/IAlbumManagementService.cs
Normal file
22
Services/Management/IAlbumManagementService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
12
Services/Mapping/IMappingService.cs
Normal file
12
Services/Mapping/IMappingService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
70
Services/Mapping/MappingService.cs
Normal file
70
Services/Mapping/MappingService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 相册图片操作请求
|
||||
|
||||
96
Web/src/api/albumManagementApi.ts
Normal file
96
Web/src/api/albumManagementApi.ts
Normal 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>;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './authApi';
|
||||
export * from './albumApi';
|
||||
export * from './albumManagementApi';
|
||||
export * from './backgroundTaskApi';
|
||||
export * from './configApi';
|
||||
export * from './fetchClient';
|
||||
|
||||
364
Web/src/pages/admin/album/Index.tsx
Normal file
364
Web/src/pages/admin/album/Index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user