mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-08-28 11:36:39 +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)
|
if (userId == null)
|
||||||
return Error<AlbumResponse>("无法识别用户信息", 401);
|
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, "相册创建成功");
|
return Success(album, "相册创建成功");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -74,8 +74,7 @@ public class AlbumController(IAlbumService albumService) : BaseApiController
|
|||||||
if (currentUserId == null)
|
if (currentUserId == null)
|
||||||
return Error<AlbumResponse>("无法识别用户信息", 401);
|
return Error<AlbumResponse>("无法识别用户信息", 401);
|
||||||
|
|
||||||
var album = await albumService.UpdateAlbumAsync(request.Id, request.Name, request.Description,
|
var album = await albumService.UpdateAlbumAsync(request.Id, request.Name, request.Description, currentUserId, request.CoverPictureId);
|
||||||
currentUserId);
|
|
||||||
return Success(album, "相册更新成功");
|
return Success(album, "相册更新成功");
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException)
|
catch (UnauthorizedAccessException)
|
||||||
@@ -192,4 +191,5 @@ public class AlbumController(IAlbumService albumService) : BaseApiController
|
|||||||
return Error<bool>($"从相册移除图片失败: {ex.Message}", 500);
|
return Error<bool>($"从相册移除图片失败: {ex.Message}", 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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.Storage.Providers;
|
||||||
using Foxel.Services.VectorDB;
|
using Foxel.Services.VectorDB;
|
||||||
using Foxel.Services.Background.Processors;
|
using Foxel.Services.Background.Processors;
|
||||||
|
using Foxel.Services.Mapping;
|
||||||
|
|
||||||
namespace Foxel.Extensions;
|
namespace Foxel.Extensions;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<IAlbumService, AlbumService>();
|
services.AddSingleton<IAlbumService, AlbumService>();
|
||||||
services.AddSingleton<IUserManagementService, UserManagementService>();
|
services.AddSingleton<IUserManagementService, UserManagementService>();
|
||||||
services.AddSingleton<IPictureManagementService, PictureManagementService>();
|
services.AddSingleton<IPictureManagementService, PictureManagementService>();
|
||||||
|
services.AddSingleton<IAlbumManagementService, AlbumManagementService>();
|
||||||
services.AddSingleton<ILogManagementService, LogManagementService>();
|
services.AddSingleton<ILogManagementService, LogManagementService>();
|
||||||
services.AddSingleton<IStorageManagementService, StorageManagementService>();
|
services.AddSingleton<IStorageManagementService, StorageManagementService>();
|
||||||
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||||
@@ -38,6 +40,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<PictureTaskProcessor>();
|
services.AddSingleton<PictureTaskProcessor>();
|
||||||
services.AddSingleton<VisualRecognitionTaskProcessor>();
|
services.AddSingleton<VisualRecognitionTaskProcessor>();
|
||||||
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
services.AddSingleton<IDatabaseInitializer, DatabaseInitializer>();
|
||||||
|
services.AddSingleton<IMappingService, MappingService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration)
|
public static void AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Foxel.Models.DataBase;
|
namespace Foxel.Models.DataBase;
|
||||||
|
|
||||||
@@ -14,5 +15,9 @@ public class Album : BaseModel
|
|||||||
[Required]
|
[Required]
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
|
public int? CoverPictureId { get; set; }
|
||||||
|
[ForeignKey("CoverPictureId")]
|
||||||
|
public Picture? CoverPicture { get; set; }
|
||||||
|
|
||||||
public ICollection<Picture>? Pictures { get; set; }
|
public ICollection<Picture>? Pictures { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)]
|
[StringLength(500)]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public int? CoverPictureId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ public record AlbumResponse
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
public int PictureCount { get; set; } = 0;
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { 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<Log> Logs { get; set; } = null!;
|
||||||
public DbSet<BackgroundTask> BackgroundTasks { get; set; } = null!;
|
public DbSet<BackgroundTask> BackgroundTasks { get; set; } = null!;
|
||||||
public DbSet<StorageMode> StorageModes { 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
-125
@@ -3,60 +3,66 @@ using Foxel.Models;
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using Foxel.Models.Response.Album;
|
using Foxel.Models.Response.Album;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Foxel.Services.Mapping; // 添加 using
|
||||||
|
|
||||||
namespace Foxel.Services.Media;
|
namespace Foxel.Services.Media;
|
||||||
|
|
||||||
public class AlbumService(
|
public class AlbumService(
|
||||||
IDbContextFactory<MyDbContext> contextFactory,
|
IDbContextFactory<MyDbContext> contextFactory,
|
||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IMappingService mappingService) // 注入新的映射服务
|
||||||
: IAlbumService
|
: IAlbumService
|
||||||
{
|
{
|
||||||
public async Task<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null)
|
public async Task<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null)
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
if (page < 1) page = 1;
|
||||||
if (pageSize < 1) pageSize = 10;
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
// 构建查询
|
// 构建查询
|
||||||
IQueryable<Album> query = dbContext.Albums
|
IQueryable<Album> query = dbContext.Albums
|
||||||
.Include(a => a.User)
|
.Include(a => a.User)
|
||||||
|
.Include(a => a.CoverPicture) // 确保包含封面图片
|
||||||
|
.Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount
|
||||||
.OrderByDescending(a => a.CreatedAt);
|
.OrderByDescending(a => a.CreatedAt);
|
||||||
|
|
||||||
// 如果指定了用户ID,则只获取该用户的相册
|
// 如果指定了用户ID,则只获取该用户的相册
|
||||||
if (userId.HasValue)
|
if (userId.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(a => a.UserId == userId.Value);
|
query = query.Where(a => a.UserId == userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取总数和分页数据
|
// 获取总数和分页数据
|
||||||
var totalCount = await query.CountAsync();
|
var totalCount = await query.CountAsync();
|
||||||
var albums = await query
|
var albums = await query
|
||||||
|
// .Include(a => a.CoverPicture) // 已在上面包含
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// 获取每个相册中的图片数量
|
// 获取每个相册中的图片数量 - 不再需要,MapAlbumToResponse 会处理
|
||||||
var albumIds = albums.Select(a => a.Id).ToList();
|
// var albumIds = albums.Select(a => a.Id).ToList();
|
||||||
var albumPictureCounts = await dbContext.Pictures
|
// var albumPictureCounts = await dbContext.Pictures
|
||||||
.Where(p => p.AlbumId != null && albumIds.Contains(p.AlbumId.Value))
|
// .Where(p => p.AlbumId != null && albumIds.Contains(p.AlbumId.Value))
|
||||||
.GroupBy(p => p.AlbumId)
|
// .GroupBy(p => p.AlbumId)
|
||||||
.Select(g => new { AlbumId = g.Key, Count = g.Count() })
|
// .Select(g => new { AlbumId = g.Key, Count = g.Count() })
|
||||||
.ToDictionaryAsync(x => x.AlbumId!.Value, x => x.Count);
|
// .ToDictionaryAsync(x => x.AlbumId!.Value, x => x.Count);
|
||||||
|
|
||||||
// 转换为响应模型
|
// 转换为响应模型
|
||||||
var albumResponses = albums.Select(a => new AlbumResponse
|
var albumResponses = albums.Select(mappingService.MapAlbumToResponse).ToList();
|
||||||
{
|
// var albumResponses = albums.Select(a => new AlbumResponse
|
||||||
Id = a.Id,
|
// {
|
||||||
Name = a.Name,
|
// Id = a.Id,
|
||||||
Description = a.Description,
|
// Name = a.Name,
|
||||||
PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0),
|
// Description = a.Description,
|
||||||
UserId = a.UserId,
|
// PictureCount = albumPictureCounts.GetValueOrDefault(a.Id, 0), // 使用 MapAlbumToResponse 后不再需要
|
||||||
Username = a.User.UserName,
|
// UserId = a.UserId,
|
||||||
CreatedAt = a.CreatedAt,
|
// Username = a.User.UserName,
|
||||||
UpdatedAt = a.UpdatedAt
|
// CreatedAt = a.CreatedAt,
|
||||||
}).ToList();
|
// UpdatedAt = a.UpdatedAt
|
||||||
|
// }).ToList();
|
||||||
|
|
||||||
return new PaginatedResult<AlbumResponse>
|
return new PaginatedResult<AlbumResponse>
|
||||||
{
|
{
|
||||||
Data = albumResponses,
|
Data = albumResponses,
|
||||||
@@ -65,206 +71,222 @@ public class AlbumService(
|
|||||||
TotalCount = totalCount
|
TotalCount = totalCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AlbumResponse> GetAlbumByIdAsync(int id)
|
public async Task<AlbumResponse> GetAlbumByIdAsync(int id)
|
||||||
{
|
{
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var album = await dbContext.Albums
|
var album = await dbContext.Albums
|
||||||
.Include(a => a.User)
|
.Include(a => a.User)
|
||||||
|
.Include(a => a.CoverPicture) // 确保包含封面图片
|
||||||
|
.Include(a => a.Pictures) // 确保包含图片集合以计算 PictureCount
|
||||||
.FirstOrDefaultAsync(a => a.Id == id);
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
if (album == null)
|
if (album == null)
|
||||||
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
||||||
|
|
||||||
// 获取相册中图片的数量
|
// 获取相册中图片的数量 - 不再需要
|
||||||
var pictureCount = await dbContext.Pictures
|
// var pictureCount = await dbContext.Pictures
|
||||||
.Where(p => p.AlbumId == id)
|
// .Where(p => p.AlbumId == id)
|
||||||
.CountAsync();
|
// .CountAsync();
|
||||||
|
|
||||||
// 转换为响应模型
|
// 转换为响应模型
|
||||||
var response = new AlbumResponse
|
return mappingService.MapAlbumToResponse(album);
|
||||||
{
|
// var response = new AlbumResponse
|
||||||
Id = album.Id,
|
// {
|
||||||
Name = album.Name,
|
// Id = album.Id,
|
||||||
Description = album.Description,
|
// Name = album.Name,
|
||||||
PictureCount = pictureCount,
|
// Description = album.Description,
|
||||||
UserId = album.UserId,
|
// PictureCount = pictureCount, // 使用 MapAlbumToResponse 后不再需要
|
||||||
Username = album.User.UserName,
|
// UserId = album.UserId,
|
||||||
CreatedAt = album.CreatedAt,
|
// Username = album.User.UserName,
|
||||||
UpdatedAt = album.UpdatedAt
|
// CreatedAt = album.CreatedAt,
|
||||||
};
|
// UpdatedAt = album.UpdatedAt
|
||||||
|
// };
|
||||||
return response;
|
//
|
||||||
|
// 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))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
throw new ArgumentException("相册名称不能为空", nameof(name));
|
throw new ArgumentException("相册名称不能为空", nameof(name));
|
||||||
|
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
// 检查用户是否存在
|
// 检查用户是否存在
|
||||||
var user = await dbContext.Users.FindAsync(userId);
|
var user = await dbContext.Users.FindAsync(userId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new KeyNotFoundException($"找不到ID为{userId}的用户");
|
throw new KeyNotFoundException($"找不到ID为{userId}的用户");
|
||||||
|
|
||||||
// 创建新相册
|
// 创建新相册
|
||||||
var album = new Album
|
var album = new Album
|
||||||
{
|
{
|
||||||
Name = name.Trim(),
|
Name = name.Trim(),
|
||||||
Description = description?.Trim() ?? string.Empty,
|
Description = description?.Trim() ?? string.Empty,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
User = user,
|
// User = user, // EF Core 会自动关联
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
CoverPictureId = coverPictureId
|
||||||
};
|
};
|
||||||
|
|
||||||
dbContext.Albums.Add(album);
|
dbContext.Albums.Add(album);
|
||||||
await dbContext.SaveChangesAsync();
|
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
|
return mappingService.MapAlbumToResponse(createdAlbum);
|
||||||
{
|
// return new AlbumResponse
|
||||||
Id = album.Id,
|
// {
|
||||||
Name = album.Name,
|
// Id = album.Id,
|
||||||
Description = album.Description,
|
// Name = album.Name,
|
||||||
PictureCount = 0,
|
// Description = album.Description,
|
||||||
UserId = album.UserId,
|
// PictureCount = 0, // MapAlbumToResponse 会处理
|
||||||
Username = user.UserName,
|
// UserId = album.UserId,
|
||||||
CreatedAt = album.CreatedAt,
|
// Username = user.UserName, // MapAlbumToResponse 会处理
|
||||||
UpdatedAt = album.UpdatedAt
|
// 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))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
throw new ArgumentException("相册名称不能为空", nameof(name));
|
throw new ArgumentException("相册名称不能为空", nameof(name));
|
||||||
|
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
// 获取相册
|
// 获取相册
|
||||||
var album = await dbContext.Albums
|
var album = await dbContext.Albums
|
||||||
.Include(a => a.User)
|
// .Include(a => a.User) // 将在更新后重新加载
|
||||||
.Include(a => a.Pictures)
|
// .Include(a => a.Pictures) // 将在更新后重新加载
|
||||||
.FirstOrDefaultAsync(a => a.Id == id);
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
if (album == null)
|
if (album == null)
|
||||||
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
throw new KeyNotFoundException($"找不到ID为{id}的相册");
|
||||||
|
if (!userId.HasValue) // userId 仍然需要用于权限检查
|
||||||
// 权限检查 - 只有相册的创建者或系统管理员可以更新
|
throw new ArgumentException("无效的用户ID", nameof(userId));
|
||||||
if (userId.HasValue && album.UserId != userId.Value)
|
if (album.UserId != userId.Value)
|
||||||
{
|
{
|
||||||
// 检查用户是否是管理员
|
throw new UnauthorizedAccessException("您没有权限更新此相册");
|
||||||
var user = await dbContext.Users.FindAsync(userId.Value);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException("您没有权限更新此相册");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新相册信息
|
// 更新相册信息
|
||||||
album.Name = name.Trim();
|
album.Name = name.Trim();
|
||||||
album.Description = description?.Trim() ?? album.Description;
|
album.Description = description?.Trim() ?? album.Description;
|
||||||
album.UpdatedAt = DateTime.UtcNow;
|
album.UpdatedAt = DateTime.UtcNow;
|
||||||
|
album.CoverPictureId = coverPictureId;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
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
|
return mappingService.MapAlbumToResponse(updatedAlbum);
|
||||||
{
|
// return new AlbumResponse
|
||||||
Id = album.Id,
|
// {
|
||||||
Name = album.Name,
|
// Id = album.Id,
|
||||||
Description = album.Description,
|
// Name = album.Name,
|
||||||
PictureCount = album.Pictures?.Count ?? 0,
|
// Description = album.Description,
|
||||||
UserId = album.UserId,
|
// PictureCount = album.Pictures?.Count ?? 0, // MapAlbumToResponse 会处理
|
||||||
Username = album.User.UserName,
|
// UserId = album.UserId,
|
||||||
CreatedAt = album.CreatedAt,
|
// Username = album.User.UserName, // MapAlbumToResponse 会处理
|
||||||
UpdatedAt = album.UpdatedAt
|
// CreatedAt = album.CreatedAt,
|
||||||
};
|
// UpdatedAt = album.UpdatedAt
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteAlbumAsync(int id)
|
public async Task<bool> DeleteAlbumAsync(int id)
|
||||||
{
|
{
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var album = await dbContext.Albums.FindAsync(id);
|
var album = await dbContext.Albums.FindAsync(id);
|
||||||
if (album == null)
|
if (album == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// 先找出所有属于这个相册的图片
|
// 先找出所有属于这个相册的图片
|
||||||
var pictures = await dbContext.Pictures
|
var pictures = await dbContext.Pictures
|
||||||
.Where(p => p.AlbumId == id)
|
.Where(p => p.AlbumId == id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// 将这些图片的AlbumId设置为null
|
// 将这些图片的AlbumId设置为null
|
||||||
foreach (var picture in pictures)
|
foreach (var picture in pictures)
|
||||||
{
|
{
|
||||||
picture.AlbumId = null;
|
picture.AlbumId = null;
|
||||||
picture.Album = null;
|
picture.Album = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存图片更改
|
// 保存图片更改
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
// 然后删除相册
|
// 然后删除相册
|
||||||
dbContext.Albums.Remove(album);
|
dbContext.Albums.Remove(album);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId)
|
public async Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId)
|
||||||
{
|
{
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
// 获取相册和图片
|
// 获取相册和图片
|
||||||
var album = await dbContext.Albums.FindAsync(albumId);
|
var album = await dbContext.Albums.FindAsync(albumId);
|
||||||
if (album == null)
|
if (album == null)
|
||||||
throw new KeyNotFoundException($"找不到ID为{albumId}的相册");
|
throw new KeyNotFoundException($"找不到ID为{albumId}的相册");
|
||||||
|
|
||||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||||
if (picture == null)
|
if (picture == null)
|
||||||
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
throw new KeyNotFoundException($"找不到ID为{pictureId}的图片");
|
||||||
|
|
||||||
// 将图片添加到相册
|
// 将图片添加到相册
|
||||||
picture.AlbumId = albumId;
|
picture.AlbumId = albumId;
|
||||||
picture.Album = album;
|
picture.Album = album;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId)
|
public async Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId)
|
||||||
{
|
{
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
// 获取图片
|
// 获取图片
|
||||||
var picture = await dbContext.Pictures
|
var picture = await dbContext.Pictures
|
||||||
.FirstOrDefaultAsync(p => p.Id == pictureId && p.AlbumId == albumId);
|
.FirstOrDefaultAsync(p => p.Id == pictureId && p.AlbumId == albumId);
|
||||||
|
|
||||||
if (picture == null)
|
if (picture == null)
|
||||||
throw new KeyNotFoundException($"在相册中找不到ID为{pictureId}的图片");
|
throw new KeyNotFoundException($"在相册中找不到ID为{pictureId}的图片");
|
||||||
|
|
||||||
// 从相册中移除图片
|
// 从相册中移除图片
|
||||||
picture.AlbumId = null;
|
picture.AlbumId = null;
|
||||||
picture.Album = null;
|
picture.Album = null;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds)
|
public async Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds)
|
||||||
{
|
{
|
||||||
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var album = await dbContext.Albums.FindAsync(albumId)
|
var album = await dbContext.Albums.FindAsync(albumId)
|
||||||
?? throw new KeyNotFoundException("相册不存在");
|
?? throw new KeyNotFoundException("相册不存在");
|
||||||
|
|
||||||
// 检查是否有权限修改此相册
|
// 检查是否有权限修改此相册
|
||||||
var currentUser = httpContextAccessor.HttpContext?.User;
|
var currentUser = httpContextAccessor.HttpContext?.User;
|
||||||
if (currentUser != null)
|
if (currentUser != null)
|
||||||
@@ -275,14 +297,14 @@ public class AlbumService(
|
|||||||
throw new UnauthorizedAccessException("您没有权限修改此相册");
|
throw new UnauthorizedAccessException("您没有权限修改此相册");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var successCount = 0;
|
var successCount = 0;
|
||||||
|
|
||||||
foreach (var pictureId in pictureIds)
|
foreach (var pictureId in pictureIds)
|
||||||
{
|
{
|
||||||
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
var picture = await dbContext.Pictures.FindAsync(pictureId);
|
||||||
if (picture == null) continue; // 跳过不存在的图片
|
if (picture == null) continue; // 跳过不存在的图片
|
||||||
|
|
||||||
// 直接更新 Picture 的 AlbumId
|
// 直接更新 Picture 的 AlbumId
|
||||||
if (picture.AlbumId != albumId)
|
if (picture.AlbumId != albumId)
|
||||||
{
|
{
|
||||||
@@ -290,13 +312,40 @@ public class AlbumService(
|
|||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (successCount > 0)
|
if (successCount > 0)
|
||||||
{
|
{
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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<PaginatedResult<AlbumResponse>> GetAlbumsAsync(int page = 1, int pageSize = 10, int? userId = null);
|
||||||
Task<AlbumResponse> GetAlbumByIdAsync(int id);
|
Task<AlbumResponse> GetAlbumByIdAsync(int id);
|
||||||
Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId);
|
Task<AlbumResponse> CreateAlbumAsync(string name, string? description, int userId,int? coverPictureId = null);
|
||||||
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null);
|
Task<AlbumResponse> UpdateAlbumAsync(int id, string name, string? description, int? userId = null, int? coverPictureId = null);
|
||||||
Task<bool> DeleteAlbumAsync(int id);
|
Task<bool> DeleteAlbumAsync(int id);
|
||||||
Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId);
|
Task<bool> AddPictureToAlbumAsync(int albumId, int pictureId);
|
||||||
Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds);
|
Task<bool> AddPicturesToAlbumAsync(int albumId, List<int> pictureIds);
|
||||||
Task<bool> RemovePictureFromAlbumAsync(int albumId, int pictureId);
|
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.Configuration;
|
||||||
using Foxel.Services.Storage;
|
using Foxel.Services.Storage;
|
||||||
using Foxel.Services.VectorDB;
|
using Foxel.Services.VectorDB;
|
||||||
|
using Foxel.Services.Mapping;
|
||||||
using Foxel.Utils;
|
using Foxel.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -20,50 +21,10 @@ public class PictureService(
|
|||||||
IBackgroundTaskQueue backgroundTaskQueue,
|
IBackgroundTaskQueue backgroundTaskQueue,
|
||||||
IVectorDbService vectorDbService,
|
IVectorDbService vectorDbService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
|
IMappingService mappingService, // 添加 IMappingService
|
||||||
ILogger<PictureService> logger)
|
ILogger<PictureService> logger)
|
||||||
: IPictureService
|
: 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(
|
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
|
||||||
int page = 1,
|
int page = 1,
|
||||||
@@ -143,20 +104,19 @@ public class PictureService(
|
|||||||
var picturesData = await dbContext.Pictures
|
var picturesData = await dbContext.Pictures
|
||||||
.Include(p => p.Tags)
|
.Include(p => p.Tags)
|
||||||
.Include(p => p.User)
|
.Include(p => p.User)
|
||||||
.Include(p=>p.StorageMode)
|
.Include(p => p.StorageMode)
|
||||||
.Where(p => ids.Contains((ulong)p.Id))
|
.Where(p => ids.Contains((ulong)p.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var picturesOrdered = ids
|
var picturesOrdered = ids
|
||||||
.Select(id => picturesData.FirstOrDefault(p => p.Id == (int)id))
|
.Select(id => picturesData.FirstOrDefault(p => p.Id == (int)id))
|
||||||
.Where(p => p != null)
|
.Where(p => p != null)
|
||||||
.ToList();
|
.ToList();
|
||||||
var paginatedResultsTasks = picturesOrdered
|
var paginatedResults = picturesOrdered
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.Select(async p => await MapPictureToResponseAsync(p!))
|
.Select(p => mappingService.MapPictureToResponse(p!))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var paginatedResults = (await Task.WhenAll(paginatedResultsTasks)).ToList();
|
|
||||||
|
|
||||||
var totalCount = picturesOrdered.Count;
|
var totalCount = picturesOrdered.Count;
|
||||||
|
|
||||||
@@ -220,16 +180,15 @@ public class PictureService(
|
|||||||
|
|
||||||
// 获取分页数据
|
// 获取分页数据
|
||||||
var picturesData = await query
|
var picturesData = await query
|
||||||
.Include(x=>x.StorageMode)
|
.Include(x => x.StorageMode)
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// 转换为响应格式
|
// 转换为响应格式
|
||||||
var picturesTasks = picturesData
|
var pictures = picturesData
|
||||||
.Select(async p => await MapPictureToResponseAsync(p))
|
.Select(p => mappingService.MapPictureToResponse(p))
|
||||||
.ToList();
|
.ToList();
|
||||||
var pictures = (await Task.WhenAll(picturesTasks)).ToList();
|
|
||||||
|
|
||||||
// 处理收藏信息
|
// 处理收藏信息
|
||||||
await PopulateFavoriteInfo(dbContext, pictures, userId);
|
await PopulateFavoriteInfo(dbContext, pictures, userId);
|
||||||
@@ -663,7 +622,7 @@ public class PictureService(
|
|||||||
await backgroundTaskQueue.QueueVisualRecognitionTaskAsync(visualRecognitionPayload);
|
await backgroundTaskQueue.QueueVisualRecognitionTaskAsync(visualRecognitionPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pictureResponse = await MapPictureToResponseAsync(picture);
|
var pictureResponse = mappingService.MapPictureToResponse(picture);
|
||||||
return (pictureResponse, picture.Id);
|
return (pictureResponse, picture.Id);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -906,7 +865,7 @@ public class PictureService(
|
|||||||
picture.UpdatedAt = DateTime.UtcNow;
|
picture.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
var pictureResponse = await MapPictureToResponseAsync(picture);
|
var pictureResponse = mappingService.MapPictureToResponse(picture);
|
||||||
return (pictureResponse, userId);
|
return (pictureResponse, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +945,7 @@ public class PictureService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pictureResponse = await MapPictureToResponseAsync(picture);
|
var pictureResponse = mappingService.MapPictureToResponse(picture);
|
||||||
|
|
||||||
return picture;
|
return picture;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ export interface AlbumResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
coverImageUrl?: string;
|
|
||||||
pictureCount: number;
|
pictureCount: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
coverPictureId?: number | null;
|
||||||
|
coverPicturePath?: string;
|
||||||
|
coverPictureThumbnailPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建相册请求
|
// 创建相册请求
|
||||||
export interface CreateAlbumRequest {
|
export interface CreateAlbumRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
coverPictureId?: number | null; // 新增:封面图片ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新相册请求
|
// 更新相册请求
|
||||||
@@ -24,6 +27,7 @@ export interface UpdateAlbumRequest {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
coverPictureId?: number | null; // 新增:封面图片ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相册图片操作请求
|
// 相册图片操作请求
|
||||||
|
|||||||
@@ -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 './authApi';
|
||||||
export * from './albumApi';
|
export * from './albumApi';
|
||||||
|
export * from './albumManagementApi';
|
||||||
export * from './backgroundTaskApi';
|
export * from './backgroundTaskApi';
|
||||||
export * from './configApi';
|
export * from './configApi';
|
||||||
export * from './fetchClient';
|
export * from './fetchClient';
|
||||||
|
|||||||
@@ -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 type { ColumnsType } from 'antd/es/table';
|
||||||
import { useOutletContext } from 'react-router';
|
import { useOutletContext } from 'react-router';
|
||||||
import { useNavigate } 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';
|
import type { UserResponse, PictureResponse } from '../../../api';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@@ -21,8 +21,6 @@ interface DashboardStats {
|
|||||||
totalAlbums: number;
|
totalAlbums: number;
|
||||||
totalPhotos: number;
|
totalPhotos: number;
|
||||||
storageUsagePercentage: number;
|
storageUsagePercentage: number;
|
||||||
newUsersToday: number;
|
|
||||||
newPhotosToday: number;
|
|
||||||
softwareVersion: string;
|
softwareVersion: string;
|
||||||
systemVersion: string;
|
systemVersion: string;
|
||||||
cpuArchitecture: string;
|
cpuArchitecture: string;
|
||||||
@@ -37,8 +35,6 @@ const AdminDashboard: React.FC = () => {
|
|||||||
totalAlbums: 0,
|
totalAlbums: 0,
|
||||||
totalPhotos: 0,
|
totalPhotos: 0,
|
||||||
storageUsagePercentage: 0,
|
storageUsagePercentage: 0,
|
||||||
newUsersToday: 0,
|
|
||||||
newPhotosToday: 0,
|
|
||||||
softwareVersion: 'N/A',
|
softwareVersion: 'N/A',
|
||||||
systemVersion: 'N/A',
|
systemVersion: 'N/A',
|
||||||
cpuArchitecture: 'N/A'
|
cpuArchitecture: 'N/A'
|
||||||
@@ -85,40 +81,32 @@ const AdminDashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算今日新增数据
|
// 获取相册总数
|
||||||
const calculateTodayStats = (users: UserResponse[], photos: PictureResponse[]) => {
|
const fetchTotalAlbums = async () => {
|
||||||
const today = new Date();
|
try {
|
||||||
today.setHours(0, 0, 0, 0);
|
// 我们只需要总数,所以 pageSize 可以设为 1
|
||||||
|
const response = await getManagementAlbums(1, 1);
|
||||||
const newUsersToday = users.filter(user => {
|
if (response.success) {
|
||||||
const userDate = new Date(user.createdAt);
|
setStats(prev => ({
|
||||||
userDate.setHours(0, 0, 0, 0);
|
...prev,
|
||||||
return userDate.getTime() === today.getTime();
|
totalAlbums: response.totalCount || 0
|
||||||
}).length;
|
}));
|
||||||
|
}
|
||||||
const newPhotosToday = photos.filter(photo => {
|
} catch (error) {
|
||||||
const photoDate = new Date(photo.createdAt);
|
console.error('Error fetching total albums:', error);
|
||||||
photoDate.setHours(0, 0, 0, 0);
|
message.error('获取相册总数失败');
|
||||||
return photoDate.getTime() === today.getTime();
|
}
|
||||||
}).length;
|
|
||||||
|
|
||||||
setStats(prev => ({
|
|
||||||
...prev,
|
|
||||||
newUsersToday,
|
|
||||||
newPhotosToday
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await Promise.all([fetchRecentUsers(), fetchRecentPhotos()]);
|
await Promise.all([fetchRecentUsers(), fetchRecentPhotos(), fetchTotalAlbums()]);
|
||||||
|
|
||||||
// 设置其他静态统计数据
|
// 设置其他静态统计数据
|
||||||
setStats(prev => ({
|
setStats(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
totalAlbums: 348, // 相册功能暂未实现,使用模拟数据
|
|
||||||
storageUsagePercentage: 68,
|
storageUsagePercentage: 68,
|
||||||
softwareVersion: 'Foxel Dev 尝鲜版',
|
softwareVersion: 'Foxel Dev 尝鲜版',
|
||||||
systemVersion: 'Fedora 42',
|
systemVersion: 'Fedora 42',
|
||||||
@@ -134,13 +122,6 @@ const AdminDashboard: React.FC = () => {
|
|||||||
loadData();
|
loadData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 当用户和图片数据都加载完成后计算今日统计
|
|
||||||
useEffect(() => {
|
|
||||||
if (recentUsers.length > 0 && recentPhotos.length > 0) {
|
|
||||||
calculateTodayStats(recentUsers, recentPhotos);
|
|
||||||
}
|
|
||||||
}, [recentUsers, recentPhotos]);
|
|
||||||
|
|
||||||
const userColumns = useMemo<ColumnsType<UserResponse>>(() => [
|
const userColumns = useMemo<ColumnsType<UserResponse>>(() => [
|
||||||
{
|
{
|
||||||
title: '用户名',
|
title: '用户名',
|
||||||
@@ -237,11 +218,6 @@ const AdminDashboard: React.FC = () => {
|
|||||||
title="用户总数"
|
title="用户总数"
|
||||||
value={stats.totalUsers}
|
value={stats.totalUsers}
|
||||||
prefix={<UserOutlined />}
|
prefix={<UserOutlined />}
|
||||||
suffix={
|
|
||||||
<Tag color="green" style={{ marginLeft: 8 }}>
|
|
||||||
<ArrowUpOutlined /> {stats.newUsersToday} 今日
|
|
||||||
</Tag>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -260,11 +236,6 @@ const AdminDashboard: React.FC = () => {
|
|||||||
title="照片总数"
|
title="照片总数"
|
||||||
value={stats.totalPhotos}
|
value={stats.totalPhotos}
|
||||||
prefix={<PictureOutlined />}
|
prefix={<PictureOutlined />}
|
||||||
suffix={
|
|
||||||
<Tag color="green" style={{ marginLeft: 8 }}>
|
|
||||||
<ArrowUpOutlined /> {stats.newPhotosToday} 今日
|
|
||||||
</Tag>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
|||||||
import { useParams, useNavigate, useOutletContext } from 'react-router';
|
import { useParams, useNavigate, useOutletContext } from 'react-router';
|
||||||
import {
|
import {
|
||||||
Typography, Button, Spin, Empty, message,
|
Typography, Button, Spin, Empty, message,
|
||||||
Popconfirm, Modal, Form, Input} from 'antd';
|
Popconfirm, Modal, Form, Input, InputNumber, Select // Added InputNumber, Select
|
||||||
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
EditOutlined, DeleteOutlined, PlusOutlined} from '@ant-design/icons';
|
EditOutlined, DeleteOutlined, PlusOutlined} from '@ant-design/icons';
|
||||||
import { getAlbumById, deleteAlbum, favoritePicture, unfavoritePicture, addPicturesToAlbum, updateAlbum } from '../../api';
|
import { getAlbumById, deleteAlbum, favoritePicture, unfavoritePicture, addPicturesToAlbum, updateAlbum } from '../../api';
|
||||||
@@ -28,7 +29,8 @@ function AlbumDetail() {
|
|||||||
const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
|
const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
|
||||||
const [editForm] = Form.useForm();
|
const [editForm] = Form.useForm();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0); // 添加刷新触发器
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
const [albumPicturesForSelect, setAlbumPicturesForSelect] = useState<PictureResponse[]>([]); // 用于编辑时选择封面
|
||||||
|
|
||||||
const loadAlbum = async () => {
|
const loadAlbum = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -37,10 +39,11 @@ function AlbumDetail() {
|
|||||||
const result = await getAlbumById(parseInt(id));
|
const result = await getAlbumById(parseInt(id));
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setAlbum(result.data);
|
setAlbum(result.data);
|
||||||
// 添加检查确保 updateBreadcrumbTitle 是一个函数
|
|
||||||
if (typeof updateBreadcrumbTitle === 'function') {
|
if (typeof updateBreadcrumbTitle === 'function') {
|
||||||
updateBreadcrumbTitle(result.data.name);
|
updateBreadcrumbTitle(result.data.name);
|
||||||
}
|
}
|
||||||
|
// 假设相册详情API返回了图片列表,或者需要另外获取
|
||||||
|
// setAlbumPicturesForSelect(result.data.pictures || []);
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message || '获取相册失败');
|
message.error(result.message || '获取相册失败');
|
||||||
}
|
}
|
||||||
@@ -54,7 +57,22 @@ function AlbumDetail() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAlbum();
|
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 () => {
|
const handleDeleteAlbum = async () => {
|
||||||
if (!album) return;
|
if (!album) return;
|
||||||
@@ -152,8 +170,12 @@ function AlbumDetail() {
|
|||||||
if (album) {
|
if (album) {
|
||||||
editForm.setFieldsValue({
|
editForm.setFieldsValue({
|
||||||
name: album.name,
|
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);
|
setIsEditModalVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -164,12 +186,13 @@ function AlbumDetail() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const values = await editForm.validateFields();
|
const values = await editForm.validateFields(); // values 包含 name, description, coverPictureId
|
||||||
|
|
||||||
const result = await updateAlbum({
|
const result = await updateAlbum({
|
||||||
id: album.id,
|
id: album.id,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
description: values.description
|
description: values.description,
|
||||||
|
coverPictureId: values.coverPictureId // 传递封面ID
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -350,7 +373,8 @@ function AlbumDetail() {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
name: album.name,
|
name: album.name,
|
||||||
description: album.description || ''
|
description: album.description || '',
|
||||||
|
coverPictureId: album.coverPictureId // 初始化表单的封面ID
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -371,6 +395,36 @@ function AlbumDetail() {
|
|||||||
showCount
|
showCount
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 { PlusOutlined, EditOutlined, DeleteOutlined, PictureOutlined } from '@ant-design/icons';
|
||||||
import { getAlbums, createAlbum, updateAlbum, deleteAlbum } from '../../api';
|
import { getAlbums, createAlbum, updateAlbum, deleteAlbum } from '../../api';
|
||||||
import type { AlbumResponse, CreateAlbumRequest, UpdateAlbumRequest } from '../../api';
|
import type { AlbumResponse, CreateAlbumRequest, UpdateAlbumRequest } from '../../api';
|
||||||
@@ -43,7 +43,7 @@ function Albums() {
|
|||||||
|
|
||||||
const handleCreateAlbum = async (values: CreateAlbumRequest) => {
|
const handleCreateAlbum = async (values: CreateAlbumRequest) => {
|
||||||
try {
|
try {
|
||||||
const result = await createAlbum(values);
|
const result = await createAlbum(values); // values 现在可以包含 coverPictureId
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
message.success('相册创建成功');
|
message.success('相册创建成功');
|
||||||
setIsCreateModalVisible(false);
|
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;
|
if (!currentAlbum) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await updateAlbum({
|
const requestData: UpdateAlbumRequest = {
|
||||||
...values,
|
...values,
|
||||||
id: currentAlbum.id
|
id: currentAlbum.id,
|
||||||
});
|
// coverPictureId 来自表单 values.coverPictureId
|
||||||
|
};
|
||||||
|
const result = await updateAlbum(requestData);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
message.success('相册更新成功');
|
message.success('相册更新成功');
|
||||||
@@ -101,7 +103,8 @@ function Albums() {
|
|||||||
setCurrentAlbum(album);
|
setCurrentAlbum(album);
|
||||||
editForm.setFieldsValue({
|
editForm.setFieldsValue({
|
||||||
name: album.name,
|
name: album.name,
|
||||||
description: album.description
|
description: album.description,
|
||||||
|
coverPictureId: album.coverPictureId // 设置当前封面ID
|
||||||
});
|
});
|
||||||
setIsEditModalVisible(true);
|
setIsEditModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -188,12 +191,16 @@ function Albums() {
|
|||||||
bodyStyle={{ padding: '20px' }}
|
bodyStyle={{ padding: '20px' }}
|
||||||
cover={
|
cover={
|
||||||
<Link to={`/albums/${album.id}`}>
|
<Link to={`/albums/${album.id}`}>
|
||||||
{album.coverImageUrl ? (
|
{album.coverPictureThumbnailPath || album.coverPicturePath ? (
|
||||||
<img alt={album.name} src={album.coverImageUrl} style={{
|
<img
|
||||||
height: 180,
|
alt={album.name}
|
||||||
width: '100%',
|
src={album.coverPictureThumbnailPath || album.coverPicturePath}
|
||||||
objectFit: 'cover'
|
style={{
|
||||||
}} />
|
height: 180,
|
||||||
|
width: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 180,
|
height: 180,
|
||||||
@@ -271,6 +278,32 @@ function Albums() {
|
|||||||
>
|
>
|
||||||
<TextArea placeholder="描述一下这个相册" rows={4} />
|
<TextArea placeholder="描述一下这个相册" rows={4} />
|
||||||
</Form.Item>
|
</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' }}>
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||||
<Button style={{ marginRight: 8 }} onClick={() => {
|
<Button style={{ marginRight: 8 }} onClick={() => {
|
||||||
setIsCreateModalVisible(false);
|
setIsCreateModalVisible(false);
|
||||||
@@ -314,6 +347,18 @@ function Albums() {
|
|||||||
>
|
>
|
||||||
<TextArea placeholder="相册描述" rows={4} />
|
<TextArea placeholder="相册描述" rows={4} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="coverPictureId"
|
||||||
|
label="封面图片 (可选)"
|
||||||
|
tooltip="输入图片ID。实际应用中应为图片选择器,图片源为当前相册内图片或用户所有图片。"
|
||||||
|
>
|
||||||
|
{/*
|
||||||
|
实际应用中替换为图片选择器。
|
||||||
|
如果从当前相册选择,需要获取相册内图片列表。
|
||||||
|
如果像管理员一样从所有图片选择,则使用类似创建时的逻辑。
|
||||||
|
*/}
|
||||||
|
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||||
<Button style={{ marginRight: 8 }} onClick={() => {
|
<Button style={{ marginRight: 8 }} onClick={() => {
|
||||||
setIsEditModalVisible(false);
|
setIsEditModalVisible(false);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import PictureManagement from '../pages/admin/pictures/Index';
|
|||||||
import UserDetail from '../pages/admin/users/UserDetail';
|
import UserDetail from '../pages/admin/users/UserDetail';
|
||||||
import AdminLogManagement from '../pages/admin/log/Index';
|
import AdminLogManagement from '../pages/admin/log/Index';
|
||||||
import StorageManagementPage from '../pages/admin/storage/StorageManagement';
|
import StorageManagementPage from '../pages/admin/storage/StorageManagement';
|
||||||
|
import AlbumManagement from '../pages/admin/album/Index';
|
||||||
|
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -175,6 +176,18 @@ const routes: RouteConfig[] = [
|
|||||||
title: '图片管理'
|
title: '图片管理'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'albums-admin',
|
||||||
|
key: 'admin-album',
|
||||||
|
icon: <FolderOutlined />,
|
||||||
|
label: '相册管理',
|
||||||
|
element: <AlbumManagement />,
|
||||||
|
area: 'admin',
|
||||||
|
groupLabel: '内容管理',
|
||||||
|
breadcrumb: {
|
||||||
|
title: '相册管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'log',
|
path: 'log',
|
||||||
key: 'admin-log',
|
key: 'admin-log',
|
||||||
|
|||||||
Reference in New Issue
Block a user