mirror of
https://github.com/moeacgx/Telegram-Panel.git
synced 2026-05-17 12:39:20 +08:00
feat: add category filters and sync action
This commit is contained in:
@@ -35,6 +35,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<ChannelManagementService>();
|
||||
services.AddScoped<ChannelGroupManagementService>();
|
||||
services.AddScoped<GroupManagementService>();
|
||||
services.AddScoped<GroupCategoryManagementService>();
|
||||
services.AddScoped<BatchTaskManagementService>();
|
||||
services.AddScoped<BotManagementService>();
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ChannelManagementService
|
||||
/// </summary>
|
||||
public async Task<(IReadOnlyList<Channel> Items, int TotalCount)> QueryChannelsForViewPagedAsync(
|
||||
int accountId,
|
||||
int? groupId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
@@ -44,7 +45,7 @@ public class ChannelManagementService
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _channelRepository.QueryForViewPagedAsync(accountId, filterType, membershipRole, search, pageIndex, pageSize, cancellationToken);
|
||||
return await _channelRepository.QueryForViewPagedAsync(accountId, groupId, filterType, membershipRole, search, pageIndex, pageSize, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Channel>> GetChannelsByCreatorAsync(int accountId)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using TelegramPanel.Data.Entities;
|
||||
using TelegramPanel.Data.Repositories;
|
||||
|
||||
namespace TelegramPanel.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 群组分类管理服务
|
||||
/// </summary>
|
||||
public class GroupCategoryManagementService
|
||||
{
|
||||
private readonly IGroupCategoryRepository _categoryRepository;
|
||||
|
||||
public GroupCategoryManagementService(IGroupCategoryRepository categoryRepository)
|
||||
{
|
||||
_categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<GroupCategory>> GetAllCategoriesAsync()
|
||||
{
|
||||
return await _categoryRepository.GetAllAsync();
|
||||
}
|
||||
|
||||
public async Task<GroupCategory?> GetCategoryAsync(int id)
|
||||
{
|
||||
return await _categoryRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task<GroupCategory?> GetCategoryByNameAsync(string name)
|
||||
{
|
||||
return await _categoryRepository.GetByNameAsync(name);
|
||||
}
|
||||
|
||||
public async Task<GroupCategory> CreateCategoryAsync(GroupCategory category)
|
||||
{
|
||||
return await _categoryRepository.AddAsync(category);
|
||||
}
|
||||
|
||||
public async Task UpdateCategoryAsync(GroupCategory category)
|
||||
{
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
}
|
||||
|
||||
public async Task DeleteCategoryAsync(int id)
|
||||
{
|
||||
var category = await _categoryRepository.GetByIdAsync(id);
|
||||
if (category != null)
|
||||
await _categoryRepository.DeleteAsync(category);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public class GroupManagementService
|
||||
|
||||
public async Task<(IReadOnlyList<Group> Items, int TotalCount)> QueryGroupsForViewPagedAsync(
|
||||
int accountId,
|
||||
int? categoryId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
@@ -41,7 +42,7 @@ public class GroupManagementService
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _groupRepository.QueryForViewPagedAsync(accountId, filterType, membershipRole, search, pageIndex, pageSize, cancellationToken);
|
||||
return await _groupRepository.QueryForViewPagedAsync(accountId, categoryId, filterType, membershipRole, search, pageIndex, pageSize, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Group>> GetGroupsByCreatorAsync(int accountId)
|
||||
@@ -62,6 +63,8 @@ public class GroupManagementService
|
||||
existing.MemberCount = group.MemberCount;
|
||||
existing.About = group.About;
|
||||
existing.AccessHash = group.AccessHash;
|
||||
if (group.CategoryId.HasValue)
|
||||
existing.CategoryId = group.CategoryId;
|
||||
if (existing.CreatorAccountId == null && group.CreatorAccountId != null)
|
||||
existing.CreatorAccountId = group.CreatorAccountId;
|
||||
if (group.CreatedAt.HasValue)
|
||||
@@ -89,6 +92,17 @@ public class GroupManagementService
|
||||
await _groupRepository.DeleteAsync(group);
|
||||
}
|
||||
|
||||
public async Task UpdateGroupCategoryAsync(int groupId, int? categoryId)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(groupId);
|
||||
if (group != null)
|
||||
{
|
||||
group.CategoryId = categoryId;
|
||||
group.SyncedAt = DateTime.UtcNow;
|
||||
await _groupRepository.UpdateAsync(group);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalGroupCountAsync()
|
||||
{
|
||||
return await _groupRepository.CountAsync();
|
||||
|
||||
@@ -20,6 +20,7 @@ public class AppDbContext : DbContext
|
||||
public DbSet<Channel> Channels => Set<Channel>();
|
||||
public DbSet<ChannelGroup> ChannelGroups => Set<ChannelGroup>();
|
||||
public DbSet<Group> Groups => Set<Group>();
|
||||
public DbSet<GroupCategory> GroupCategories => Set<GroupCategory>();
|
||||
public DbSet<BatchTask> BatchTasks => Set<BatchTask>();
|
||||
public DbSet<Bot> Bots => Set<Bot>();
|
||||
public DbSet<BotChannel> BotChannels => Set<BotChannel>();
|
||||
@@ -44,11 +45,8 @@ public class AppDbContext : DbContext
|
||||
entity.Property(e => e.TelegramStatusDetails).HasMaxLength(2000);
|
||||
|
||||
entity.HasIndex(e => e.Phone).IsUnique();
|
||||
// 注意:UserId 在未登录/未验证的 session 导入场景可能为 0,SQLite 的 UNIQUE 约束会导致多账号导入失败。
|
||||
// 这里保留索引但不做唯一约束,真正的去重以 Phone 为准。
|
||||
entity.HasIndex(e => e.UserId);
|
||||
|
||||
// 与AccountCategory的关系
|
||||
entity.HasOne(e => e.Category)
|
||||
.WithMany(c => c.Accounts)
|
||||
.HasForeignKey(e => e.CategoryId)
|
||||
@@ -78,13 +76,11 @@ public class AppDbContext : DbContext
|
||||
entity.HasIndex(e => e.TelegramId).IsUnique();
|
||||
entity.HasIndex(e => e.Username);
|
||||
|
||||
// 与Account的关系(创建者)
|
||||
entity.HasOne(e => e.CreatorAccount)
|
||||
.WithMany(a => a.Channels)
|
||||
.HasForeignKey(e => e.CreatorAccountId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// 与ChannelGroup的关系
|
||||
entity.HasOne(e => e.Group)
|
||||
.WithMany(g => g.Channels)
|
||||
.HasForeignKey(e => e.GroupId)
|
||||
@@ -95,7 +91,6 @@ public class AppDbContext : DbContext
|
||||
modelBuilder.Entity<AccountChannel>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.HasIndex(e => new { e.AccountId, e.ChannelId }).IsUnique();
|
||||
entity.HasIndex(e => e.ChannelId);
|
||||
|
||||
@@ -130,19 +125,23 @@ public class AppDbContext : DbContext
|
||||
|
||||
entity.HasIndex(e => e.TelegramId).IsUnique();
|
||||
entity.HasIndex(e => e.Username);
|
||||
entity.HasIndex(e => e.CategoryId);
|
||||
|
||||
// 与Account的关系(创建者)
|
||||
entity.HasOne(e => e.CreatorAccount)
|
||||
.WithMany(a => a.Groups)
|
||||
.HasForeignKey(e => e.CreatorAccountId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(e => e.Category)
|
||||
.WithMany(c => c.Groups)
|
||||
.HasForeignKey(e => e.CategoryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// AccountGroup配置
|
||||
modelBuilder.Entity<AccountGroup>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.HasIndex(e => new { e.AccountId, e.GroupId }).IsUnique();
|
||||
entity.HasIndex(e => e.GroupId);
|
||||
|
||||
@@ -157,6 +156,16 @@ public class AppDbContext : DbContext
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// GroupCategory配置
|
||||
modelBuilder.Entity<GroupCategory>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
|
||||
entity.HasIndex(e => e.Name).IsUnique();
|
||||
});
|
||||
|
||||
// BatchTask配置
|
||||
modelBuilder.Entity<BatchTask>(entity =>
|
||||
{
|
||||
|
||||
@@ -13,10 +13,12 @@ public class Group
|
||||
public int MemberCount { get; set; }
|
||||
public string? About { get; set; }
|
||||
public int? CreatorAccountId { get; set; }
|
||||
public int? CategoryId { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// 导航属性
|
||||
public Account? CreatorAccount { get; set; }
|
||||
public GroupCategory? Category { get; set; }
|
||||
public ICollection<AccountGroup> AccountGroups { get; set; } = new List<AccountGroup>();
|
||||
}
|
||||
|
||||
15
src/TelegramPanel.Data/Entities/GroupCategory.cs
Normal file
15
src/TelegramPanel.Data/Entities/GroupCategory.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace TelegramPanel.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 群组分类实体
|
||||
/// </summary>
|
||||
public class GroupCategory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// 导航属性
|
||||
public ICollection<Group> Groups { get; set; } = new List<Group>();
|
||||
}
|
||||
731
src/TelegramPanel.Data/Migrations/20260308172014_AddGroupCategories.Designer.cs
generated
Normal file
731
src/TelegramPanel.Data/Migrations/20260308172014_AddGroupCategories.Designer.cs
generated
Normal file
@@ -0,0 +1,731 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TelegramPanel.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260308172014_AddGroupCategories")]
|
||||
partial class AddGroupCategories
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.24");
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApiHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ApiId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastSyncAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("TelegramStatusCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TelegramStatusDetails")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("TelegramStatusOk")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TelegramStatusSummary")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TwoFactorPassword")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ExcludeFromOperations")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountChannel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCreator")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("AccountId", "ChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCreator")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("AccountId", "GroupId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BatchTask", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Completed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Failed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TaskType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Total")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("BatchTasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Bot", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastSyncAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("LastUpdateId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Bots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ChannelStatusCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChannelStatusError")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("ChannelStatusOk")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsBroadcast")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("ChannelStatusOk");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("BotChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BotChannelCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelMember", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BotChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BotId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BotChannelId");
|
||||
|
||||
b.HasIndex("BotId");
|
||||
|
||||
b.HasIndex("BotId", "BotChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BotChannelMembers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("CreatorAccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("GroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsBroadcast")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorAccountId");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.ChannelGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChannelGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("CreatorAccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("CreatorAccountId");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("GroupCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.AccountCategory", "Category")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountChannel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "Account")
|
||||
.WithMany("AccountChannels")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Channel", "Channel")
|
||||
.WithMany("AccountChannels")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Channel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountGroup", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "Account")
|
||||
.WithMany("AccountGroups")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Group", "Group")
|
||||
.WithMany("AccountGroups")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.BotChannelCategory", "Category")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelMember", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.BotChannel", "BotChannel")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("BotChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Bot", "Bot")
|
||||
.WithMany("ChannelMembers")
|
||||
.HasForeignKey("BotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bot");
|
||||
|
||||
b.Navigation("BotChannel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "CreatorAccount")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.ChannelGroup", "Group")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.GroupCategory", "Category")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "CreatorAccount")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.Navigation("AccountChannels");
|
||||
|
||||
b.Navigation("AccountGroups");
|
||||
|
||||
b.Navigation("Channels");
|
||||
|
||||
b.Navigation("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountCategory", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Bot", b =>
|
||||
{
|
||||
b.Navigation("ChannelMembers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelCategory", b =>
|
||||
{
|
||||
b.Navigation("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.Navigation("AccountChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.ChannelGroup", b =>
|
||||
{
|
||||
b.Navigation("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.Navigation("AccountGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Navigation("Groups");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGroupCategories : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CategoryId",
|
||||
table: "Groups",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GroupCategories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GroupCategories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Groups_CategoryId",
|
||||
table: "Groups",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GroupCategories_Name",
|
||||
table: "GroupCategories",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Groups_GroupCategories_CategoryId",
|
||||
table: "Groups",
|
||||
column: "CategoryId",
|
||||
principalTable: "GroupCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Groups_GroupCategories_CategoryId",
|
||||
table: "Groups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GroupCategories");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Groups_CategoryId",
|
||||
table: "Groups");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CategoryId",
|
||||
table: "Groups");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,6 +494,9 @@ namespace TelegramPanel.Data.Migrations
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -520,6 +523,8 @@ namespace TelegramPanel.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("CreatorAccountId");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
@@ -530,6 +535,32 @@ namespace TelegramPanel.Data.Migrations
|
||||
b.ToTable("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("GroupCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.AccountCategory", "Category")
|
||||
@@ -626,11 +657,18 @@ namespace TelegramPanel.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.GroupCategory", "Category")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "CreatorAccount")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
});
|
||||
|
||||
@@ -679,6 +717,11 @@ namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
b.Navigation("AccountGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Navigation("Groups");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
{
|
||||
}
|
||||
|
||||
private IQueryable<Channel> BuildForViewQuery(int accountId, string? filterType, string? membershipRole, string? search)
|
||||
private IQueryable<Channel> BuildForViewQuery(int accountId, int? groupId, string? filterType, string? membershipRole, string? search)
|
||||
{
|
||||
var query = _dbSet
|
||||
.AsNoTracking()
|
||||
@@ -22,28 +22,37 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
.AsSplitQuery()
|
||||
.AsQueryable();
|
||||
|
||||
query = query.Where(c => c.CreatorAccountId != null || c.AccountChannels.Any());
|
||||
|
||||
membershipRole = (membershipRole ?? "all").Trim().ToLowerInvariant();
|
||||
if (accountId > 0)
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.AccountId == accountId));
|
||||
|
||||
membershipRole = (membershipRole ?? "all").Trim().ToLowerInvariant();
|
||||
if (membershipRole == "creator")
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.AccountId == accountId && x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "admin")
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.AccountId == accountId && x.IsAdmin && !x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "member")
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.AccountId == accountId && !x.IsAdmin));
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (membershipRole == "creator")
|
||||
{
|
||||
query = query.Where(c => c.CreatorAccountId != null || c.AccountChannels.Any());
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "admin")
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => x.IsAdmin && !x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "member")
|
||||
{
|
||||
query = query.Where(c => c.AccountChannels.Any(x => !x.IsAdmin));
|
||||
}
|
||||
|
||||
if (groupId.HasValue && groupId.Value > 0)
|
||||
query = query.Where(c => c.GroupId == groupId.Value);
|
||||
else if (groupId.HasValue && groupId.Value == 0)
|
||||
query = query.Where(c => c.GroupId == null);
|
||||
|
||||
filterType = (filterType ?? "all").Trim().ToLowerInvariant();
|
||||
if (filterType == "public")
|
||||
@@ -57,7 +66,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
var like = $"%{search}%";
|
||||
query = query.Where(c =>
|
||||
EF.Functions.Like(c.Title, like)
|
||||
|| (c.Username != null && EF.Functions.Like(c.Username, like)));
|
||||
|| (c.Username != null && EF.Functions.Like(c.Username, like))
|
||||
|| (c.Group != null && EF.Functions.Like(c.Group.Name, like)));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(c => c.SyncedAt);
|
||||
@@ -68,6 +78,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
}
|
||||
|
||||
@@ -76,6 +88,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -85,6 +99,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.TelegramId == telegramId);
|
||||
}
|
||||
|
||||
@@ -93,6 +109,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.CreatorAccountId != null)
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
@@ -103,6 +121,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.CreatorAccountId == accountId)
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
@@ -116,6 +136,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.Where(c => links.Any(x => x.ChannelId == c.Id))
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
@@ -126,6 +148,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.GroupId == groupId)
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
@@ -136,6 +160,8 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
return await _dbSet
|
||||
.Include(c => c.CreatorAccount)
|
||||
.Include(c => c.Group)
|
||||
.Include(c => c.AccountChannels)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.IsBroadcast)
|
||||
.OrderByDescending(c => c.SyncedAt)
|
||||
.ToListAsync();
|
||||
@@ -143,6 +169,7 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
|
||||
public async Task<(IReadOnlyList<Channel> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? groupId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
@@ -154,7 +181,7 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
if (pageSize <= 0) pageSize = 20;
|
||||
if (pageSize > 500) pageSize = 500;
|
||||
|
||||
var query = BuildForViewQuery(accountId, filterType, membershipRole, search);
|
||||
var query = BuildForViewQuery(accountId, groupId, filterType, membershipRole, search);
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.Skip(pageIndex * pageSize)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TelegramPanel.Data.Entities;
|
||||
|
||||
namespace TelegramPanel.Data.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 群组分类仓储实现
|
||||
/// </summary>
|
||||
public class GroupCategoryRepository : Repository<GroupCategory>, IGroupCategoryRepository
|
||||
{
|
||||
public GroupCategoryRepository(AppDbContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<GroupCategory>> GetAllAsync()
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.Groups)
|
||||
.OrderBy(g => g.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<GroupCategory?> GetByNameAsync(string name)
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.Groups)
|
||||
.FirstOrDefaultAsync(g => g.Name == name);
|
||||
}
|
||||
}
|
||||
@@ -12,37 +12,47 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
{
|
||||
}
|
||||
|
||||
private IQueryable<Group> BuildForViewQuery(int accountId, string? filterType, string? membershipRole, string? search)
|
||||
private IQueryable<Group> BuildForViewQuery(int accountId, int? categoryId, string? filterType, string? membershipRole, string? search)
|
||||
{
|
||||
var query = _dbSet
|
||||
.AsNoTracking()
|
||||
.Include(g => g.CreatorAccount)
|
||||
.Include(g => g.Category)
|
||||
.Include(g => g.AccountGroups)
|
||||
.AsSplitQuery()
|
||||
.AsQueryable();
|
||||
|
||||
query = query.Where(g => g.CreatorAccountId != null || g.AccountGroups.Any());
|
||||
|
||||
membershipRole = (membershipRole ?? "all").Trim().ToLowerInvariant();
|
||||
if (accountId > 0)
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.AccountId == accountId));
|
||||
|
||||
membershipRole = (membershipRole ?? "all").Trim().ToLowerInvariant();
|
||||
if (membershipRole == "creator")
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.AccountId == accountId && x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "admin")
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.AccountId == accountId && x.IsAdmin && !x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "member")
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.AccountId == accountId && !x.IsAdmin));
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (membershipRole == "creator")
|
||||
{
|
||||
query = query.Where(g => g.CreatorAccountId != null || g.AccountGroups.Any());
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "admin")
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => x.IsAdmin && !x.IsCreator));
|
||||
}
|
||||
else if (membershipRole == "member")
|
||||
{
|
||||
query = query.Where(g => g.AccountGroups.Any(x => !x.IsAdmin));
|
||||
}
|
||||
|
||||
if (categoryId.HasValue && categoryId.Value > 0)
|
||||
query = query.Where(g => g.CategoryId == categoryId.Value);
|
||||
else if (categoryId.HasValue && categoryId.Value == 0)
|
||||
query = query.Where(g => g.CategoryId == null);
|
||||
|
||||
filterType = (filterType ?? "all").Trim().ToLowerInvariant();
|
||||
if (filterType == "public")
|
||||
@@ -56,7 +66,8 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
var like = $"%{search}%";
|
||||
query = query.Where(g =>
|
||||
EF.Functions.Like(g.Title, like)
|
||||
|| (g.Username != null && EF.Functions.Like(g.Username, like)));
|
||||
|| (g.Username != null && EF.Functions.Like(g.Username, like))
|
||||
|| (g.Category != null && EF.Functions.Like(g.Category.Name, like)));
|
||||
}
|
||||
|
||||
return query.OrderByDescending(g => g.SyncedAt);
|
||||
@@ -66,6 +77,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.CreatorAccount)
|
||||
.Include(g => g.Category)
|
||||
.Include(g => g.AccountGroups)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(g => g.Id == id);
|
||||
@@ -75,6 +87,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.CreatorAccount)
|
||||
.Include(g => g.Category)
|
||||
.Include(g => g.AccountGroups)
|
||||
.AsSplitQuery()
|
||||
.Where(g => g.CreatorAccountId != null || g.AccountGroups.Any())
|
||||
@@ -86,6 +99,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.CreatorAccount)
|
||||
.Include(g => g.Category)
|
||||
.Include(g => g.AccountGroups)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(g => g.TelegramId == telegramId);
|
||||
@@ -95,6 +109,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
{
|
||||
return await _dbSet
|
||||
.Include(g => g.CreatorAccount)
|
||||
.Include(g => g.Category)
|
||||
.Include(g => g.AccountGroups)
|
||||
.AsSplitQuery()
|
||||
.Where(g => g.CreatorAccountId == accountId)
|
||||
@@ -104,6 +119,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
|
||||
public async Task<(IReadOnlyList<Group> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? categoryId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
@@ -115,7 +131,7 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
if (pageSize <= 0) pageSize = 20;
|
||||
if (pageSize > 500) pageSize = 500;
|
||||
|
||||
var query = BuildForViewQuery(accountId, filterType, membershipRole, search);
|
||||
var query = BuildForViewQuery(accountId, categoryId, filterType, membershipRole, search);
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.Skip(pageIndex * pageSize)
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface IChannelRepository : IRepository<Channel>
|
||||
|
||||
Task<(IReadOnlyList<Channel> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? groupId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using TelegramPanel.Data.Entities;
|
||||
|
||||
namespace TelegramPanel.Data.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 群组分类仓储接口
|
||||
/// </summary>
|
||||
public interface IGroupCategoryRepository : IRepository<GroupCategory>
|
||||
{
|
||||
Task<GroupCategory?> GetByNameAsync(string name);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public interface IGroupRepository : IRepository<Group>
|
||||
Task<IEnumerable<Group>> GetByCreatorAccountAsync(int accountId);
|
||||
Task<(IReadOnlyList<Group> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? categoryId,
|
||||
string? filterType,
|
||||
string? membershipRole,
|
||||
string? search,
|
||||
|
||||
@@ -27,6 +27,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IChannelRepository, ChannelRepository>();
|
||||
services.AddScoped<IChannelGroupRepository, ChannelGroupRepository>();
|
||||
services.AddScoped<IGroupRepository, GroupRepository>();
|
||||
services.AddScoped<IGroupCategoryRepository, GroupCategoryRepository>();
|
||||
services.AddScoped<IBatchTaskRepository, BatchTaskRepository>();
|
||||
services.AddScoped<IBotRepository, BotRepository>();
|
||||
services.AddScoped<IBotChannelRepository, BotChannelRepository>();
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
将为 <strong>@SelectedCount</strong> 个频道设置分组
|
||||
将为 <strong>@SelectedCount</strong> 个频道设置分类
|
||||
</MudAlert>
|
||||
|
||||
<MudSelect @bind-Value="selectedGroupId" Label="选择目标分组" Variant="Variant.Outlined" Required="true">
|
||||
<MudSelectItem Value="0">未分组</MudSelectItem>
|
||||
<MudSelect @bind-Value="selectedGroupId" Label="选择目标分类" Variant="Variant.Outlined" Required="true">
|
||||
<MudSelectItem Value="0">未分类</MudSelectItem>
|
||||
@foreach (var g in Groups)
|
||||
{
|
||||
<MudSelectItem Value="@g.Id">@g.Name</MudSelectItem>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@namespace TelegramPanel.Web.Components.Dialogs
|
||||
@using TelegramPanel.Data.Entities
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
将为 <strong>@SelectedCount</strong> 个群组设置分类
|
||||
</MudAlert>
|
||||
|
||||
<MudSelect @bind-Value="selectedCategoryId" Label="选择目标分类" Variant="Variant.Outlined" Required="true">
|
||||
<MudSelectItem Value="0">未分类</MudSelectItem>
|
||||
@foreach (var category in Categories)
|
||||
{
|
||||
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">取消</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Confirm">确定</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Parameter] public List<GroupCategory> Categories { get; set; } = new();
|
||||
[Parameter] public int SelectedCount { get; set; }
|
||||
|
||||
private int selectedCategoryId;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
MudDialog.Close(DialogResult.Ok(selectedCategoryId));
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,12 @@
|
||||
<MudNavGroup Title="频道管理" Icon="@Icons.Material.Filled.Campaign" Expanded="true">
|
||||
<MudNavLink Href="/channels" Icon="@Icons.Material.Filled.List">频道列表</MudNavLink>
|
||||
<MudNavLink Href="/channels/create" Icon="@Icons.Material.Filled.Add">创建频道</MudNavLink>
|
||||
<MudNavLink Href="/channels/groups" Icon="@Icons.Material.Filled.Folder">频道分组</MudNavLink>
|
||||
<MudNavLink Href="/channels/groups" Icon="@Icons.Material.Filled.Folder">频道分类</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavGroup Title="群组管理" Icon="@Icons.Material.Filled.Group">
|
||||
<MudNavLink Href="/groups" Icon="@Icons.Material.Filled.List">群组列表</MudNavLink>
|
||||
<MudNavLink Href="/groups/categories" Icon="@Icons.Material.Filled.Folder">群组分类</MudNavLink>
|
||||
</MudNavGroup>
|
||||
|
||||
<MudNavGroup Title="机器人管理" Icon="@Icons.Material.Filled.SmartToy">
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
@page "/channels/groups"
|
||||
@inject ChannelGroupManagementService GroupManagement
|
||||
@inject ChannelManagementService ChannelManagement
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@using TelegramPanel.Data.Entities
|
||||
|
||||
<PageTitle>频道分组 - Telegram Panel</PageTitle>
|
||||
<PageTitle>频道分类 - Telegram Panel</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">频道分组管理</MudText>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">频道分类管理</MudText>
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">添加分组</MudText>
|
||||
<MudText Typo="Typo.h6">@(editingGroupId > 0 ? "编辑分类" : "添加分类")</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudTextField @bind-Value="newGroupName" Label="分组名称" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="newGroupName" Label="分类名称" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="newGroupDesc" Label="描述" Variant="Variant.Outlined" Lines="3" Class="mt-3" />
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
Disabled="@string.IsNullOrEmpty(newGroupName)" OnClick="AddGroup">
|
||||
添加分组
|
||||
@if (editingGroupId > 0)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelEdit">取消</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="@(editingGroupId <= 0)"
|
||||
Disabled="@string.IsNullOrWhiteSpace(newGroupName)" OnClick="SaveGroup">
|
||||
@(editingGroupId > 0 ? "保存修改" : "添加分类")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
@@ -31,27 +36,31 @@
|
||||
<MudItem xs="12" md="8">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">分组列表</MudText>
|
||||
<MudText Typo="Typo.h6">分类列表</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudTable Items="@groups" Hover="true" Loading="@loading">
|
||||
<HeaderContent>
|
||||
<MudTh>分组名称</MudTh>
|
||||
<MudTh>分类名称</MudTh>
|
||||
<MudTh>描述</MudTh>
|
||||
<MudTh>频道数量</MudTh>
|
||||
<MudTh>操作</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="分组名称">@context.Name</MudTd>
|
||||
<MudTd DataLabel="分类名称">@context.Name</MudTd>
|
||||
<MudTd DataLabel="描述">@(context.Description ?? "-")</MudTd>
|
||||
<MudTd DataLabel="频道数量">@context.Channels.Count</MudTd>
|
||||
<MudTd DataLabel="操作">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(() => DeleteGroup(context))" />
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
|
||||
OnClick="@(() => BeginEdit(context))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(() => DeleteGroup(context))" />
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>暂无分组,请添加分组</MudText>
|
||||
<MudText>暂无分类,请添加分类</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
</MudCardContent>
|
||||
@@ -59,32 +68,43 @@
|
||||
|
||||
<MudCard Class="mt-4">
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">分组绑定频道</MudText>
|
||||
<MudText Typo="Typo.h6">分类绑定频道</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudSelect @bind-Value="assignGroupId" Label="选择分组" Variant="Variant.Outlined" Dense="true"
|
||||
@bind-Value:after="OnAssignGroupChanged">
|
||||
<MudSelectItem Value="0">-- 请选择分组 --</MudSelectItem>
|
||||
@foreach (var g in groups)
|
||||
{
|
||||
<MudSelectItem Value="@g.Id">@g.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudStack Row="true" Spacing="2" Style="flex-wrap: wrap;">
|
||||
<MudSelect @bind-Value="assignGroupId" Label="选择分类" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 220px;" @bind-Value:after="OnAssignGroupChanged">
|
||||
<MudSelectItem Value="0">-- 请选择分类 --</MudSelectItem>
|
||||
@foreach (var g in groups)
|
||||
{
|
||||
<MudSelectItem Value="@g.Id">@g.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTable Items="@channels" Hover="true" Dense="true" Class="mt-3"
|
||||
<MudSelect @bind-Value="filterAccountId" Label="筛选账号" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 220px;" @bind-Value:after="OnAssignGroupChanged">
|
||||
<MudSelectItem Value="0">全部账号</MudSelectItem>
|
||||
@foreach (var account in accounts)
|
||||
{
|
||||
<MudSelectItem Value="@account.Id">@FormatAccountLabel(account)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
|
||||
<MudTable Items="@VisibleChannels" Hover="true" Dense="true" Class="mt-3"
|
||||
MultiSelection="true" @bind-SelectedItems="selectedChannels"
|
||||
Loading="@channelsLoading">
|
||||
<HeaderContent>
|
||||
<MudTh>频道名称</MudTh>
|
||||
<MudTh>用户名</MudTh>
|
||||
<MudTh>创建账号</MudTh>
|
||||
<MudTh>当前分组</MudTh>
|
||||
<MudTh>当前分类</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="频道名称">@context.Title</MudTd>
|
||||
<MudTd DataLabel="用户名">@(string.IsNullOrWhiteSpace(context.Username) ? "-" : context.Username)</MudTd>
|
||||
<MudTd DataLabel="创建账号">@(context.CreatorAccount?.Phone ?? (context.CreatorAccountId?.ToString() ?? "(非系统创建)"))</MudTd>
|
||||
<MudTd DataLabel="当前分组">@(context.Group?.Name ?? "未分组")</MudTd>
|
||||
<MudTd DataLabel="当前分类">@(context.Group?.Name ?? "未分类")</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>暂无频道数据</MudText>
|
||||
@@ -95,7 +115,7 @@
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
Disabled="@(assignGroupId == 0 || channelsLoading)"
|
||||
OnClick="@(async (MouseEventArgs _) => await SaveGroupAssignments())">
|
||||
保存勾选到分组
|
||||
保存勾选到分类
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
@@ -105,20 +125,41 @@
|
||||
@code {
|
||||
private string newGroupName = "";
|
||||
private string newGroupDesc = "";
|
||||
private int editingGroupId = 0;
|
||||
private List<ChannelGroup> groups = new();
|
||||
private bool loading = true;
|
||||
|
||||
private List<Channel> channels = new();
|
||||
private List<Account> accounts = new();
|
||||
private bool channelsLoading = true;
|
||||
private int assignGroupId = 0;
|
||||
private int filterAccountId = 0;
|
||||
private HashSet<Channel> selectedChannels = new();
|
||||
|
||||
private IEnumerable<Channel> VisibleChannels => channels.Where(MatchAccountFilter);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAccounts();
|
||||
await LoadGroups();
|
||||
await LoadChannels();
|
||||
}
|
||||
|
||||
private async Task LoadAccounts()
|
||||
{
|
||||
try
|
||||
{
|
||||
var accountList = await AccountManagement.GetAllAccountsAsync();
|
||||
accounts = accountList
|
||||
.Where(a => a.Category?.ExcludeFromOperations != true)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载账号失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadGroups()
|
||||
{
|
||||
loading = true;
|
||||
@@ -144,6 +185,7 @@
|
||||
{
|
||||
var list = await ChannelManagement.GetAllChannelsAsync();
|
||||
channels = list.ToList();
|
||||
await OnAssignGroupChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -155,11 +197,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchAccountFilter(Channel channel)
|
||||
{
|
||||
if (filterAccountId <= 0)
|
||||
return true;
|
||||
|
||||
return channel.AccountChannels.Any(x => x.AccountId == filterAccountId);
|
||||
}
|
||||
|
||||
private Task OnAssignGroupChanged()
|
||||
{
|
||||
selectedChannels = assignGroupId <= 0
|
||||
? new HashSet<Channel>()
|
||||
: channels.Where(c => c.GroupId == assignGroupId).ToHashSet();
|
||||
: VisibleChannels.Where(c => c.GroupId == assignGroupId).ToHashSet();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -167,30 +217,30 @@
|
||||
{
|
||||
if (assignGroupId <= 0)
|
||||
{
|
||||
Snackbar.Add("请选择分组", Severity.Error);
|
||||
Snackbar.Add("请选择分类", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var currentlyInGroup = channels.Where(c => c.GroupId == assignGroupId).ToList();
|
||||
var scope = VisibleChannels.ToList();
|
||||
var currentlyInGroup = scope.Where(c => c.GroupId == assignGroupId).ToList();
|
||||
var selected = selectedChannels.ToHashSet();
|
||||
|
||||
// 取消勾选的 -> 移出分组
|
||||
foreach (var channel in currentlyInGroup.Where(c => !selected.Contains(c)))
|
||||
{
|
||||
await ChannelManagement.UpdateChannelGroupAsync(channel.Id, null);
|
||||
}
|
||||
|
||||
// 新勾选的 -> 加入分组
|
||||
foreach (var channel in selected.Where(c => c.GroupId != assignGroupId))
|
||||
{
|
||||
await ChannelManagement.UpdateChannelGroupAsync(channel.Id, assignGroupId);
|
||||
}
|
||||
|
||||
await LoadGroups();
|
||||
await LoadChannels();
|
||||
selectedChannels = channels.Where(c => c.GroupId == assignGroupId).ToHashSet();
|
||||
Snackbar.Add("分组绑定已保存", Severity.Success);
|
||||
selectedChannels = VisibleChannels.Where(c => c.GroupId == assignGroupId).ToHashSet();
|
||||
Snackbar.Add("分类绑定已保存", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -198,35 +248,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddGroup()
|
||||
private async Task SaveGroup()
|
||||
{
|
||||
try
|
||||
{
|
||||
var group = new ChannelGroup
|
||||
if (editingGroupId > 0)
|
||||
{
|
||||
Name = newGroupName,
|
||||
Description = newGroupDesc,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
var existing = await GroupManagement.GetGroupAsync(editingGroupId);
|
||||
if (existing == null)
|
||||
{
|
||||
Snackbar.Add("分类不存在", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
await GroupManagement.CreateGroupAsync(group);
|
||||
existing.Name = newGroupName.Trim();
|
||||
existing.Description = string.IsNullOrWhiteSpace(newGroupDesc) ? null : newGroupDesc.Trim();
|
||||
await GroupManagement.UpdateGroupAsync(existing);
|
||||
Snackbar.Add($"分类 \"{newGroupName}\" 修改成功", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
var group = new ChannelGroup
|
||||
{
|
||||
Name = newGroupName.Trim(),
|
||||
Description = string.IsNullOrWhiteSpace(newGroupDesc) ? null : newGroupDesc.Trim(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await GroupManagement.CreateGroupAsync(group);
|
||||
Snackbar.Add($"分类 \"{newGroupName}\" 添加成功", Severity.Success);
|
||||
}
|
||||
|
||||
CancelEdit();
|
||||
await LoadGroups();
|
||||
Snackbar.Add($"分组 \"{newGroupName}\" 添加成功", Severity.Success);
|
||||
|
||||
newGroupName = "";
|
||||
newGroupDesc = "";
|
||||
await LoadChannels();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"添加失败:{ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginEdit(ChannelGroup group)
|
||||
{
|
||||
editingGroupId = group.Id;
|
||||
newGroupName = group.Name;
|
||||
newGroupDesc = group.Description ?? string.Empty;
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
editingGroupId = 0;
|
||||
newGroupName = string.Empty;
|
||||
newGroupDesc = string.Empty;
|
||||
}
|
||||
|
||||
private async Task DeleteGroup(ChannelGroup group)
|
||||
{
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"确认删除",
|
||||
$"确定要删除分组 {group.Name} 吗?关联的频道将变为未分组。",
|
||||
$"确定要删除分类 {group.Name} 吗?关联的频道将变为未分类。",
|
||||
yesText: "删除", cancelText: "取消");
|
||||
|
||||
if (result == true)
|
||||
@@ -234,7 +315,10 @@
|
||||
try
|
||||
{
|
||||
await GroupManagement.DeleteGroupAsync(group.Id);
|
||||
groups.Remove(group);
|
||||
if (editingGroupId == group.Id)
|
||||
CancelEdit();
|
||||
await LoadGroups();
|
||||
await LoadChannels();
|
||||
Snackbar.Add("删除成功", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -243,4 +327,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatAccountLabel(Account account)
|
||||
{
|
||||
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
|
||||
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
|
||||
|
||||
var namePart = nickname ?? account.Phone;
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
return $"{namePart} (@{username})";
|
||||
return namePart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,21 @@
|
||||
<MudSelectItem Value='@("private")'>私密</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="filterMembershipRole" @bind-Value:after="OnFiltersChanged" Label="账号角色" Variant="Variant.Outlined" Dense="true"
|
||||
Disabled="@(filterAccount <= 0)"
|
||||
Class="tp-input-full-xs" Style="min-width: 140px; flex: 0 1 180px;">
|
||||
<MudSelectItem Value='@("all")'>全部角色</MudSelectItem>
|
||||
<MudSelectItem Value='@("creator")'>我创建</MudSelectItem>
|
||||
<MudSelectItem Value='@("admin")'>我管理</MudSelectItem>
|
||||
<MudSelectItem Value='@("creator")'>创建者</MudSelectItem>
|
||||
<MudSelectItem Value='@("admin")'>管理员</MudSelectItem>
|
||||
<MudSelectItem Value='@("member")'>非管理员</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="filterGroupId" @bind-Value:after="OnFiltersChanged" Label="频道分类" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 160px; flex: 0 1 220px;">
|
||||
<MudSelectItem Value='@(-1)'>全部分类</MudSelectItem>
|
||||
<MudSelectItem Value='@(0)'>未分类</MudSelectItem>
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<MudSelectItem Value="@group.Id">@group.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="searchString" Placeholder="搜索频道..." Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0 tp-input-full-xs"
|
||||
Style="min-width: 200px; flex: 1; max-width: 400px;"
|
||||
@@ -67,7 +75,7 @@
|
||||
<MudMenuItem Disabled="@(selectedChannels.Count == 0)" OnClick="BatchCopyInvitesSelected">批量复制邀请链接(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedChannels.Count == 0)" OnClick="BatchExportInvitesSelected">批量导出邀请链接(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedChannels.Count == 0)" OnClick="OpenBatchSetAdminsSelected">批量设置管理员(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedChannels.Count == 0)" OnClick="OpenBatchSetGroupSelected">批量修改分组(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedChannels.Count == 0)" OnClick="OpenBatchSetGroupSelected">批量修改分类(已选)</MudMenuItem>
|
||||
</MudMenu>
|
||||
|
||||
@if (selectedChannels.Count > 0)
|
||||
@@ -89,6 +97,7 @@
|
||||
<MudTh>频道名称</MudTh>
|
||||
<MudTh>用户名</MudTh>
|
||||
<MudTh>类型</MudTh>
|
||||
<MudTh>频道分类</MudTh>
|
||||
<MudTh>成员数</MudTh>
|
||||
@if (filterAccount > 0)
|
||||
{
|
||||
@@ -126,6 +135,11 @@
|
||||
@(string.IsNullOrEmpty(context.Username) ? "私密" : "公开")
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="频道分类">
|
||||
<MudChip T="string" Size="Size.Small" Color="@(context.GroupId.HasValue ? Color.Info : Color.Default)">
|
||||
@(context.Group?.Name ?? "未分类")
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="成员数">@context.MemberCount</MudTd>
|
||||
@if (filterAccount > 0)
|
||||
{
|
||||
@@ -189,6 +203,7 @@
|
||||
private bool loading = true;
|
||||
private string searchString = "";
|
||||
private int filterAccount = 0;
|
||||
private int filterGroupId = -1;
|
||||
private string filterType = "all";
|
||||
private string filterMembershipRole = "all";
|
||||
private HashSet<Channel> selectedChannels = new();
|
||||
@@ -241,7 +256,7 @@
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载分组失败:{ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"加载分类失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,8 +267,9 @@
|
||||
{
|
||||
var (items, total) = await ChannelManagement.QueryChannelsForViewPagedAsync(
|
||||
accountId: filterAccount,
|
||||
groupId: filterGroupId,
|
||||
filterType: filterType,
|
||||
membershipRole: filterAccount > 0 ? filterMembershipRole : "all",
|
||||
membershipRole: filterMembershipRole,
|
||||
search: searchString,
|
||||
pageIndex: state.Page,
|
||||
pageSize: state.PageSize,
|
||||
@@ -310,9 +326,6 @@
|
||||
|
||||
private async Task OnFiltersChanged()
|
||||
{
|
||||
if (filterAccount <= 0)
|
||||
filterMembershipRole = "all";
|
||||
|
||||
await ReloadTable();
|
||||
}
|
||||
|
||||
@@ -684,7 +697,7 @@
|
||||
["SelectedCount"] = targets.Count
|
||||
};
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = DialogService.Show<BatchSetChannelGroupDialog>("批量修改分组", parameters, options);
|
||||
var dialog = DialogService.Show<BatchSetChannelGroupDialog>("批量修改分类", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
if (result is not { Canceled: false })
|
||||
return;
|
||||
@@ -700,11 +713,11 @@
|
||||
|
||||
selectedChannels.Clear();
|
||||
await ReloadTable();
|
||||
Snackbar.Add($"分组已更新:{targets.Count} 个频道", Severity.Success);
|
||||
Snackbar.Add($"分类已更新:{targets.Count} 个频道", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"修改分组失败:{ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"修改分类失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
341
src/TelegramPanel.Web/Components/Pages/GroupCategories.razor
Normal file
341
src/TelegramPanel.Web/Components/Pages/GroupCategories.razor
Normal file
@@ -0,0 +1,341 @@
|
||||
@page "/groups/categories"
|
||||
@inject GroupCategoryManagementService CategoryManagement
|
||||
@inject GroupManagementService GroupManagement
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@using TelegramPanel.Data.Entities
|
||||
|
||||
<PageTitle>群组分类 - Telegram Panel</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">群组分类管理</MudText>
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">@(editingCategoryId > 0 ? "编辑分类" : "添加分类")</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudTextField @bind-Value="newCategoryName" Label="分类名称" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="newCategoryDesc" Label="描述" Variant="Variant.Outlined" Lines="3" Class="mt-3" />
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
@if (editingCategoryId > 0)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelEdit">取消</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="@(editingCategoryId <= 0)"
|
||||
Disabled="@string.IsNullOrWhiteSpace(newCategoryName)" OnClick="SaveCategory">
|
||||
@(editingCategoryId > 0 ? "保存修改" : "添加分类")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="8">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">分类列表</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudTable Items="@categories" Hover="true" Loading="@categoriesLoading">
|
||||
<HeaderContent>
|
||||
<MudTh>分类名称</MudTh>
|
||||
<MudTh>描述</MudTh>
|
||||
<MudTh>群组数量</MudTh>
|
||||
<MudTh>操作</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="分类名称">@context.Name</MudTd>
|
||||
<MudTd DataLabel="描述">@(context.Description ?? "-")</MudTd>
|
||||
<MudTd DataLabel="群组数量">@context.Groups.Count</MudTd>
|
||||
<MudTd DataLabel="操作">
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
|
||||
OnClick="@(() => BeginEdit(context))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(() => DeleteCategory(context))" />
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>暂无分类,请添加分类</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<MudCard Class="mt-4">
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">分类绑定群组</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudStack Row="true" Spacing="2" Style="flex-wrap: wrap;">
|
||||
<MudSelect @bind-Value="assignCategoryId" Label="选择分类" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 220px;" @bind-Value:after="OnAssignCategoryChanged">
|
||||
<MudSelectItem Value="0">-- 请选择分类 --</MudSelectItem>
|
||||
@foreach (var category in categories)
|
||||
{
|
||||
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect @bind-Value="filterAccountId" Label="筛选账号" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 220px;" @bind-Value:after="OnAssignCategoryChanged">
|
||||
<MudSelectItem Value="0">全部账号</MudSelectItem>
|
||||
@foreach (var account in accounts)
|
||||
{
|
||||
<MudSelectItem Value="@account.Id">@FormatAccountLabel(account)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
|
||||
<MudTable Items="@VisibleGroups" Hover="true" Dense="true" Class="mt-3"
|
||||
MultiSelection="true" @bind-SelectedItems="selectedGroups"
|
||||
Loading="@groupsLoading">
|
||||
<HeaderContent>
|
||||
<MudTh>群组名称</MudTh>
|
||||
<MudTh>用户名</MudTh>
|
||||
<MudTh>创建账号</MudTh>
|
||||
<MudTh>当前分类</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="群组名称">@context.Title</MudTd>
|
||||
<MudTd DataLabel="用户名">@(string.IsNullOrWhiteSpace(context.Username) ? "-" : context.Username)</MudTd>
|
||||
<MudTd DataLabel="创建账号">@(context.CreatorAccount?.Phone ?? (context.CreatorAccountId?.ToString() ?? "(非系统创建)"))</MudTd>
|
||||
<MudTd DataLabel="当前分类">@(context.Category?.Name ?? "未分类")</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText>暂无群组数据</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
Disabled="@(assignCategoryId == 0 || groupsLoading)"
|
||||
OnClick="@(async (MouseEventArgs _) => await SaveCategoryAssignments())">
|
||||
保存勾选到分类
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@code {
|
||||
private string newCategoryName = string.Empty;
|
||||
private string newCategoryDesc = string.Empty;
|
||||
private int editingCategoryId;
|
||||
private List<GroupCategory> categories = new();
|
||||
private bool categoriesLoading = true;
|
||||
|
||||
private List<Group> groups = new();
|
||||
private List<Account> accounts = new();
|
||||
private bool groupsLoading = true;
|
||||
private int assignCategoryId;
|
||||
private int filterAccountId;
|
||||
private HashSet<Group> selectedGroups = new();
|
||||
|
||||
private IEnumerable<Group> VisibleGroups => groups.Where(MatchAccountFilter);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAccounts();
|
||||
await LoadCategories();
|
||||
await LoadGroups();
|
||||
}
|
||||
|
||||
private async Task LoadAccounts()
|
||||
{
|
||||
try
|
||||
{
|
||||
var accountList = await AccountManagement.GetAllAccountsAsync();
|
||||
accounts = accountList
|
||||
.Where(a => a.Category?.ExcludeFromOperations != true)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载账号失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
categoriesLoading = true;
|
||||
try
|
||||
{
|
||||
var result = await CategoryManagement.GetAllCategoriesAsync();
|
||||
categories = result.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载分类失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
categoriesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadGroups()
|
||||
{
|
||||
groupsLoading = true;
|
||||
try
|
||||
{
|
||||
var list = await GroupManagement.GetAllGroupsAsync();
|
||||
groups = list.ToList();
|
||||
await OnAssignCategoryChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载群组失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
groupsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchAccountFilter(Group group)
|
||||
{
|
||||
if (filterAccountId <= 0)
|
||||
return true;
|
||||
|
||||
return group.AccountGroups.Any(x => x.AccountId == filterAccountId);
|
||||
}
|
||||
|
||||
private Task OnAssignCategoryChanged()
|
||||
{
|
||||
selectedGroups = assignCategoryId <= 0
|
||||
? new HashSet<Group>()
|
||||
: VisibleGroups.Where(g => g.CategoryId == assignCategoryId).ToHashSet();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task SaveCategoryAssignments()
|
||||
{
|
||||
if (assignCategoryId <= 0)
|
||||
{
|
||||
Snackbar.Add("请选择分类", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var scope = VisibleGroups.ToList();
|
||||
var currentlyInCategory = scope.Where(g => g.CategoryId == assignCategoryId).ToList();
|
||||
var selected = selectedGroups.ToHashSet();
|
||||
|
||||
foreach (var group in currentlyInCategory.Where(g => !selected.Contains(g)))
|
||||
{
|
||||
await GroupManagement.UpdateGroupCategoryAsync(group.Id, null);
|
||||
}
|
||||
|
||||
foreach (var group in selected.Where(g => g.CategoryId != assignCategoryId))
|
||||
{
|
||||
await GroupManagement.UpdateGroupCategoryAsync(group.Id, assignCategoryId);
|
||||
}
|
||||
|
||||
await LoadCategories();
|
||||
await LoadGroups();
|
||||
selectedGroups = VisibleGroups.Where(g => g.CategoryId == assignCategoryId).ToHashSet();
|
||||
Snackbar.Add("分类绑定已保存", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveCategory()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (editingCategoryId > 0)
|
||||
{
|
||||
var existing = await CategoryManagement.GetCategoryAsync(editingCategoryId);
|
||||
if (existing == null)
|
||||
{
|
||||
Snackbar.Add("分类不存在", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
existing.Name = newCategoryName.Trim();
|
||||
existing.Description = string.IsNullOrWhiteSpace(newCategoryDesc) ? null : newCategoryDesc.Trim();
|
||||
await CategoryManagement.UpdateCategoryAsync(existing);
|
||||
Snackbar.Add($"分类 \"{newCategoryName}\" 修改成功", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
var category = new GroupCategory
|
||||
{
|
||||
Name = newCategoryName.Trim(),
|
||||
Description = string.IsNullOrWhiteSpace(newCategoryDesc) ? null : newCategoryDesc.Trim(),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await CategoryManagement.CreateCategoryAsync(category);
|
||||
Snackbar.Add($"分类 \"{newCategoryName}\" 添加成功", Severity.Success);
|
||||
}
|
||||
|
||||
CancelEdit();
|
||||
await LoadCategories();
|
||||
await LoadGroups();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginEdit(GroupCategory category)
|
||||
{
|
||||
editingCategoryId = category.Id;
|
||||
newCategoryName = category.Name;
|
||||
newCategoryDesc = category.Description ?? string.Empty;
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
editingCategoryId = 0;
|
||||
newCategoryName = string.Empty;
|
||||
newCategoryDesc = string.Empty;
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(GroupCategory category)
|
||||
{
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"确认删除",
|
||||
$"确定要删除分类 {category.Name} 吗?关联的群组将变为未分类。",
|
||||
yesText: "删除", cancelText: "取消");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CategoryManagement.DeleteCategoryAsync(category.Id);
|
||||
if (editingCategoryId == category.Id)
|
||||
CancelEdit();
|
||||
await LoadCategories();
|
||||
await LoadGroups();
|
||||
Snackbar.Add("删除成功", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatAccountLabel(Account account)
|
||||
{
|
||||
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
|
||||
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
|
||||
|
||||
var namePart = nickname ?? account.Phone;
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
return $"{namePart} (@{username})";
|
||||
return namePart;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/groups"
|
||||
@inject GroupManagementService GroupManagement
|
||||
@inject GroupCategoryManagementService GroupCategoryManagement
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject TelegramPanel.Web.Services.DataSyncService DataSync
|
||||
@inject IGroupService GroupService
|
||||
@@ -31,13 +32,21 @@
|
||||
<MudSelectItem Value='@("private")'>私密</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="filterMembershipRole" @bind-Value:after="OnFiltersChanged" Label="账号角色" Variant="Variant.Outlined" Dense="true"
|
||||
Disabled="@(filterAccount <= 0)"
|
||||
Class="tp-input-full-xs" Style="min-width: 140px; flex: 0 1 180px;">
|
||||
<MudSelectItem Value='@("all")'>全部角色</MudSelectItem>
|
||||
<MudSelectItem Value='@("creator")'>我创建</MudSelectItem>
|
||||
<MudSelectItem Value='@("admin")'>我管理</MudSelectItem>
|
||||
<MudSelectItem Value='@("member")'>非管理员</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="filterCategoryId" @bind-Value:after="OnFiltersChanged" Label="群组分类" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs" Style="min-width: 160px; flex: 0 1 220px;">
|
||||
<MudSelectItem Value='@(-1)'>全部分类</MudSelectItem>
|
||||
<MudSelectItem Value='@(0)'>未分类</MudSelectItem>
|
||||
@foreach (var category in categories)
|
||||
{
|
||||
<MudSelectItem Value="@category.Id">@category.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="searchString" Placeholder="搜索群组..." Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0 tp-input-full-xs"
|
||||
Style="min-width: 200px; flex: 1; max-width: 400px;"
|
||||
@@ -64,6 +73,7 @@
|
||||
Size="Size.Small" Disabled="@loading" Dense="true" EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
|
||||
<MudMenuItem Disabled="@(selectedGroups.Count == 0)" OnClick="BatchCopyLinksSelected">批量复制加入链接(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedGroups.Count == 0)" OnClick="BatchExportLinksSelected">批量导出加入链接(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selectedGroups.Count == 0)" OnClick="OpenBatchSetCategorySelected">批量修改分类(已选)</MudMenuItem>
|
||||
<MudDivider />
|
||||
<MudMenuItem Disabled="@(selectedGroups.Count == 0)" Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="DeleteSelectedGroups">批量删除(已选)</MudMenuItem>
|
||||
</MudMenu>
|
||||
@@ -87,6 +97,7 @@
|
||||
<MudTh>群组名称</MudTh>
|
||||
<MudTh>用户名</MudTh>
|
||||
<MudTh>类型</MudTh>
|
||||
<MudTh>群组分类</MudTh>
|
||||
<MudTh>成员数</MudTh>
|
||||
@if (filterAccount > 0)
|
||||
{
|
||||
@@ -124,6 +135,11 @@
|
||||
@(string.IsNullOrWhiteSpace(context.Username) ? "私密" : "公开")
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="群组分类">
|
||||
<MudChip T="string" Size="Size.Small" Color="@(context.CategoryId.HasValue ? Color.Info : Color.Default)">
|
||||
@(context.Category?.Name ?? "未分类")
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="成员数">@context.MemberCount</MudTd>
|
||||
@if (filterAccount > 0)
|
||||
{
|
||||
@@ -177,10 +193,12 @@
|
||||
private MudTable<Group>? _table;
|
||||
private List<Group> groups = new();
|
||||
private List<Account> accounts = new();
|
||||
private List<GroupCategory> categories = new();
|
||||
private int totalCount;
|
||||
private bool loading = true;
|
||||
private string searchString = "";
|
||||
private int filterAccount = 0;
|
||||
private int filterCategoryId = -1;
|
||||
private string filterType = "all";
|
||||
private string filterMembershipRole = "all";
|
||||
private HashSet<Group> selectedGroups = new();
|
||||
@@ -206,6 +224,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAccounts();
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadAccounts()
|
||||
@@ -223,6 +242,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await GroupCategoryManagement.GetAllCategoriesAsync();
|
||||
categories = list.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"加载分类失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TableData<Group>> LoadGroupsServerData(TableState state, CancellationToken cancellationToken)
|
||||
{
|
||||
loading = true;
|
||||
@@ -230,8 +262,9 @@
|
||||
{
|
||||
var (items, total) = await GroupManagement.QueryGroupsForViewPagedAsync(
|
||||
accountId: filterAccount,
|
||||
categoryId: filterCategoryId,
|
||||
filterType: filterType,
|
||||
membershipRole: filterAccount > 0 ? filterMembershipRole : "all",
|
||||
membershipRole: filterMembershipRole,
|
||||
search: searchString,
|
||||
pageIndex: state.Page,
|
||||
pageSize: state.PageSize,
|
||||
@@ -289,9 +322,6 @@
|
||||
|
||||
private async Task OnFiltersChanged()
|
||||
{
|
||||
if (filterAccount <= 0)
|
||||
filterMembershipRole = "all";
|
||||
|
||||
await ReloadTable();
|
||||
}
|
||||
|
||||
@@ -491,6 +521,52 @@
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OpenBatchSetCategorySelected()
|
||||
{
|
||||
var targets = selectedGroups.ToList();
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
Snackbar.Add("未选择任何群组", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categories.Count == 0)
|
||||
await LoadCategories();
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Categories"] = categories,
|
||||
["SelectedCount"] = targets.Count
|
||||
};
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = DialogService.Show<BatchSetGroupCategoryDialog>("批量修改分类", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
if (result is not { Canceled: false })
|
||||
return;
|
||||
|
||||
var categoryIdRaw = result.Data is int x ? x : 0;
|
||||
int? categoryId = categoryIdRaw <= 0 ? null : categoryIdRaw;
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
foreach (var group in targets)
|
||||
await GroupManagement.UpdateGroupCategoryAsync(group.Id, categoryId);
|
||||
|
||||
selectedGroups.Clear();
|
||||
await ReloadTable();
|
||||
Snackbar.Add($"分类已更新:{targets.Count} 个群组", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"修改分类失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncCurrentAccountGroups()
|
||||
{
|
||||
if (loading || filterAccount <= 0)
|
||||
|
||||
@@ -158,10 +158,20 @@
|
||||
<MudNumericField @bind-Value="syncInterval" Label="同步间隔 (小时)" Variant="Variant.Outlined" Class="mt-3"
|
||||
Min="1" Max="24" />
|
||||
}
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true" Class="mt-3"
|
||||
OnClick="@(async (MouseEventArgs _) => await SaveSyncConfig())">
|
||||
保存同步设置
|
||||
</MudButton>
|
||||
<MudStack Row="true" Spacing="2" Class="mt-3" Style="flex-wrap: wrap;">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="tp-input-full-xs"
|
||||
Style="flex: 1; min-width: 220px;"
|
||||
OnClick="@(async (MouseEventArgs _) => await SaveSyncConfig())">
|
||||
保存同步设置
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" Class="tp-input-full-xs"
|
||||
Style="flex: 1; min-width: 220px;"
|
||||
Disabled="@syncing"
|
||||
OnClick="@(async (MouseEventArgs _) => await SyncNow())">
|
||||
@(syncing ? "同步中..." : "立即同步频道/群组")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
@@ -180,10 +190,6 @@
|
||||
保存 Bot 自动同步设置
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" FullWidth="true" Class="mt-4"
|
||||
OnClick="@(async (MouseEventArgs _) => await SyncNow())">
|
||||
立即同步
|
||||
</MudButton>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user