diff --git a/src/TelegramPanel.Core/Services/BotManagementService.cs b/src/TelegramPanel.Core/Services/BotManagementService.cs index 7458880..4ae6576 100644 --- a/src/TelegramPanel.Core/Services/BotManagementService.cs +++ b/src/TelegramPanel.Core/Services/BotManagementService.cs @@ -351,15 +351,55 @@ public class BotManagementService } /// - /// 按 TelegramId 全局删除频道:会移除该频道本体及所有 Bot 绑定关系。 + /// 获取指定频道绑定的 Bot 列表(按名称排序)。 /// - public async Task DeleteChannelGloballyByTelegramIdAsync(long telegramId) + public async Task> GetChannelBoundBotsAsync(long telegramId) { var ch = await _botChannelRepository.GetGlobalByTelegramIdAsync(telegramId); if (ch == null) - return; + return Array.Empty(); - await _botChannelRepository.DeleteAsync(ch); + var members = await _memberRepository.FindAsync(x => x.BotChannelId == ch.Id); + var botIdSet = members + .Select(x => x.BotId) + .Where(x => x > 0) + .Distinct() + .ToHashSet(); + + if (botIdSet.Count == 0) + return Array.Empty(); + + var bots = (await _botRepository.GetAllAsync()) + .Where(x => botIdSet.Contains(x.Id)) + .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + return bots; + } + + /// + /// 删除指定频道上选中 Bot 的绑定关系;若无剩余绑定,会自动清理频道本体。 + /// 返回实际删除的绑定条数。 + /// + public async Task DeleteChannelBindingsByBotIdsAsync(long telegramId, IReadOnlyCollection botIds) + { + var selected = botIds + .Where(x => x > 0) + .Distinct() + .ToList(); + if (selected.Count == 0) + return 0; + + var ch = await _botChannelRepository.GetGlobalByTelegramIdAsync(telegramId); + if (ch == null) + return 0; + + var removed = await _memberRepository.DeleteByChannelAndBotsAsync(ch.Id, selected); + var remains = await _memberRepository.CountForChannelAsync(ch.Id); + if (remains == 0) + await _botChannelRepository.DeleteAsync(ch); + + return removed; } public async Task UpdateChannelStatusAsync(int botId, long telegramId, bool ok, string? error, DateTime checkedAtUtc) diff --git a/src/TelegramPanel.Data/Repositories/BotChannelMemberRepository.cs b/src/TelegramPanel.Data/Repositories/BotChannelMemberRepository.cs index 33911ec..186090b 100644 --- a/src/TelegramPanel.Data/Repositories/BotChannelMemberRepository.cs +++ b/src/TelegramPanel.Data/Repositories/BotChannelMemberRepository.cs @@ -44,9 +44,29 @@ public class BotChannelMemberRepository : Repository, IBotChan await _context.SaveChangesAsync(); } + public async Task DeleteByChannelAndBotsAsync(int botChannelId, IReadOnlyCollection botIds) + { + if (botChannelId <= 0 || botIds.Count == 0) + return 0; + + var targets = botIds.Where(x => x > 0).Distinct().ToList(); + if (targets.Count == 0) + return 0; + + var rows = await _dbSet + .Where(x => x.BotChannelId == botChannelId && targets.Contains(x.BotId)) + .ToListAsync(); + + if (rows.Count == 0) + return 0; + + _dbSet.RemoveRange(rows); + await _context.SaveChangesAsync(); + return rows.Count; + } + public async Task CountForChannelAsync(int botChannelId) { return await _dbSet.CountAsync(x => x.BotChannelId == botChannelId); } } - diff --git a/src/TelegramPanel.Data/Repositories/IBotChannelMemberRepository.cs b/src/TelegramPanel.Data/Repositories/IBotChannelMemberRepository.cs index e6ace96..d5af219 100644 --- a/src/TelegramPanel.Data/Repositories/IBotChannelMemberRepository.cs +++ b/src/TelegramPanel.Data/Repositories/IBotChannelMemberRepository.cs @@ -7,6 +7,6 @@ public interface IBotChannelMemberRepository : IRepository Task GetAsync(int botId, int botChannelId); Task UpsertAsync(int botId, int botChannelId, DateTime syncedAt); Task DeleteAsync(int botId, int botChannelId); + Task DeleteByChannelAndBotsAsync(int botChannelId, IReadOnlyCollection botIds); Task CountForChannelAsync(int botChannelId); } - diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotChannelDeleteBindingsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotChannelDeleteBindingsDialog.razor new file mode 100644 index 0000000..b0bd50a --- /dev/null +++ b/src/TelegramPanel.Web/Components/Dialogs/BotChannelDeleteBindingsDialog.razor @@ -0,0 +1,147 @@ +@namespace TelegramPanel.Web.Components.Dialogs +@using TelegramPanel.Data.Entities + + + + + @Prompt + + @if (options.Count == 0) + { + + 当前没有可删除的 Bot 绑定。 + + } + else + { + + + 仅当前 Bot + + + 全选 + + + 清空 + + + + 已选 @selectedBotIds.Count / @options.Count + + + + + + @foreach (var bot in options) + { + + } + + + } + + + + 取消 + + 删除选中绑定 + + + + +@code { + [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] public string Prompt { get; set; } = ""; + [Parameter] public List Bots { get; set; } = new(); + [Parameter] public int CurrentBotId { get; set; } + + private List options = new(); + private HashSet selectedBotIds = new(); + + protected override void OnParametersSet() + { + options = (Bots ?? new List()) + .Where(x => x.Id > 0) + .GroupBy(x => x.Id) + .Select(x => x.First()) + .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + selectedBotIds = new HashSet(); + if (options.Count == 0) + return; + + if (CurrentBotId > 0 && options.Any(x => x.Id == CurrentBotId)) + { + selectedBotIds.Add(CurrentBotId); + return; + } + + foreach (var bot in options) + selectedBotIds.Add(bot.Id); + } + + private bool IsChecked(int botId) => selectedBotIds.Contains(botId); + + private void SetChecked(int botId, bool selected) + { + if (selected) + selectedBotIds.Add(botId); + else + selectedBotIds.Remove(botId); + } + + private void SelectCurrentBot() + { + selectedBotIds.Clear(); + if (CurrentBotId > 0) + selectedBotIds.Add(CurrentBotId); + } + + private void SelectAll() + { + selectedBotIds.Clear(); + foreach (var bot in options) + selectedBotIds.Add(bot.Id); + } + + private void ClearAll() + { + selectedBotIds.Clear(); + } + + private static string BuildBotLabel(Bot bot) + { + var name = string.IsNullOrWhiteSpace(bot.Name) ? $"Bot {bot.Id}" : bot.Name.Trim(); + var username = string.IsNullOrWhiteSpace(bot.Username) ? null : "@" + bot.Username.Trim().TrimStart('@'); + return string.IsNullOrWhiteSpace(username) ? name : $"{name}({username})"; + } + + private void Cancel() + { + MudDialog.Cancel(); + } + + private void Confirm() + { + var result = selectedBotIds + .Where(x => x > 0) + .OrderBy(x => x) + .ToList(); + + if (result.Count == 0) + { + MudDialog.Cancel(); + return; + } + + MudDialog.Close(DialogResult.Ok(result)); + } +} diff --git a/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor b/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor index d24eca5..602a6af 100644 --- a/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor +++ b/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor @@ -88,7 +88,7 @@ 批量设置管理员(机器人/ID) 批量设置分类(已选) 踢出用户(已选) - 全局删除频道(已选) + 删除频道(已选) @if (selected.Count > 0) @@ -157,7 +157,7 @@ + OnClick="@(async () => await DeleteSingleChannelAsync(context))" title="删除频道" /> @@ -484,19 +484,18 @@ return; var channelName = string.IsNullOrWhiteSpace(channel.Title) ? channel.TelegramId.ToString() : channel.Title; - bool? ok = await DialogService.ShowMessageBox( - "确认删除", - $"确定全局删除频道“{channelName}”吗?该频道会从所有 Bot 的频道列表中移除。", - yesText: "删除", cancelText: "取消"); - - if (ok != true) + var selectedBotIds = await SelectBotsForDeleteAsync(new[] { channel.TelegramId }, channelName); + if (selectedBotIds == null || selectedBotIds.Count == 0) return; loading = true; try { - await BotManagement.DeleteChannelGloballyByTelegramIdAsync(channel.TelegramId); - Snackbar.Add($"已全局删除频道:{channelName}", Severity.Success); + var removed = await BotManagement.DeleteChannelBindingsByBotIdsAsync(channel.TelegramId, selectedBotIds); + if (removed > 0) + Snackbar.Add($"已删除频道绑定:{channelName}(删除 {removed} 条绑定)", Severity.Success); + else + Snackbar.Add($"未删除任何绑定:{channelName}(所选 Bot 可能未绑定该频道)", Severity.Info); await ReloadTable(); } catch (Exception ex) @@ -520,26 +519,24 @@ return; } - bool? ok = await DialogService.ShowMessageBox( - "确认批量删除", - $"确定全局删除已选 {selected.Count} 个频道吗?这些频道会从所有 Bot 的频道列表中移除。", - yesText: "删除", cancelText: "取消"); - - if (ok != true) + var ids = selected.Select(x => x.TelegramId).Distinct().ToList(); + var selectedBotIds = await SelectBotsForDeleteAsync(ids, null); + if (selectedBotIds == null || selectedBotIds.Count == 0) return; loading = true; try { - var ids = selected.Select(x => x.TelegramId).Distinct().ToList(); var success = 0; var failed = 0; + var removedTotal = 0; foreach (var chatId in ids) { try { - await BotManagement.DeleteChannelGloballyByTelegramIdAsync(chatId); + var removed = await BotManagement.DeleteChannelBindingsByBotIdsAsync(chatId, selectedBotIds); + removedTotal += removed; success++; } catch @@ -550,11 +547,11 @@ if (failed == 0) { - Snackbar.Add($"批量删除完成:成功 {success} 个频道", Severity.Success); + Snackbar.Add($"批量删除完成:处理 {success} 个频道,删除绑定 {removedTotal} 条", Severity.Success); } else { - Snackbar.Add($"批量删除完成:成功 {success} 个,失败 {failed} 个", Severity.Warning); + Snackbar.Add($"批量删除完成:处理成功 {success} 个,失败 {failed} 个,删除绑定 {removedTotal} 条", Severity.Warning); } await ReloadTable(); @@ -569,6 +566,53 @@ } } + private async Task?> SelectBotsForDeleteAsync(IReadOnlyCollection channelTelegramIds, string? singleChannelName) + { + var ids = channelTelegramIds.Where(x => x != 0).Distinct().ToList(); + if (ids.Count == 0) + return null; + + var botMap = new Dictionary(); + foreach (var telegramId in ids) + { + var boundBots = await BotManagement.GetChannelBoundBotsAsync(telegramId); + foreach (var bot in boundBots) + { + if (bot.Id > 0 && !botMap.ContainsKey(bot.Id)) + botMap[bot.Id] = bot; + } + } + + if (botMap.Count == 0) + { + Snackbar.Add("未找到可删除的 Bot 绑定", Severity.Info); + return null; + } + + var prompt = ids.Count == 1 + ? $"频道“{singleChannelName ?? ids[0].ToString()}”当前绑定了以下 Bot,请勾选要删除的绑定:" + : $"已选 {ids.Count} 个频道。请勾选要批量删除的 Bot 绑定(未绑定的频道会自动跳过)。"; + + var parameters = new DialogParameters + { + ["Prompt"] = prompt, + ["Bots"] = botMap.Values.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToList(), + ["CurrentBotId"] = selectedBotId + }; + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + var dialog = DialogService.Show("选择要删除的 Bot 绑定", parameters, options); + var result = await dialog.Result; + if (result.Canceled) + return null; + + if (result.Data is List selectedIds) + return selectedIds.Where(x => x > 0).Distinct().ToList(); + if (result.Data is IReadOnlyList selectedReadOnlyIds) + return selectedReadOnlyIds.Where(x => x > 0).Distinct().ToList(); + + return null; + } + private async Task OpenCreateCategoryDialog() { if (selectedBotId <= 0)