1233 lines
43 KiB
Plaintext
1233 lines
43 KiB
Plaintext
@page "/bots/channels"
|
||
@inject BotManagementService BotManagement
|
||
@inject AccountManagementService AccountManagement
|
||
@inject IChannelService ChannelService
|
||
@inject TelegramPanel.Core.Services.Telegram.BotTelegramService BotTelegram
|
||
@inject ISnackbar Snackbar
|
||
@inject IDialogService DialogService
|
||
@inject NavigationManager Navigation
|
||
@inject IJSRuntime JS
|
||
@using TelegramPanel.Core.Interfaces
|
||
@using TelegramPanel.Data.Entities
|
||
@using TelegramPanel.Web.Components.Dialogs
|
||
|
||
<PageTitle>Bot 频道 - Telegram Panel</PageTitle>
|
||
|
||
<MudText Typo="Typo.h4" Class="mb-4 tp-page-title">Bot 频道</MudText>
|
||
|
||
<MudPaper Class="pa-4 mb-4" Elevation="0" Outlined="true">
|
||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
|
||
<MudSelect T="int" Value="@selectedBotId" ValueChanged="@(async (int id) => await OnSelectedBotChanged(id))" Label="选择机器人"
|
||
Variant="Variant.Outlined" Dense="true" Class="tp-input-full-xs"
|
||
Style="min-width: 200px; flex: 1; max-width: 400px;">
|
||
<MudSelectItem Value="0">全部机器人</MudSelectItem>
|
||
@foreach (var b in bots)
|
||
{
|
||
<MudSelectItem Value="@b.Id">@b.Name</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 200px;" Disabled="@(bots.Count == 0)">
|
||
<MudSelectItem Value="0">全部分类</MudSelectItem>
|
||
<MudSelectItem Value="-1">未分类</MudSelectItem>
|
||
@foreach (var c in categories)
|
||
{
|
||
<MudSelectItem Value="@c.Id">@c.Name</MudSelectItem>
|
||
}
|
||
</MudSelect>
|
||
|
||
<MudSelect @bind-Value="filterStatus" @bind-Value:after="OnFiltersChanged" Label="频道状态" Variant="Variant.Outlined" Dense="true"
|
||
Class="tp-input-full-xs"
|
||
Style="min-width: 160px; flex: 0 1 200px;" Disabled="@(bots.Count == 0)">
|
||
<MudSelectItem Value="0">全部状态</MudSelectItem>
|
||
<MudSelectItem Value="1">正常</MudSelectItem>
|
||
<MudSelectItem Value="2">异常</MudSelectItem>
|
||
<MudSelectItem Value="3">未检测</MudSelectItem>
|
||
</MudSelect>
|
||
|
||
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Primary" Size="Size.Small"
|
||
Title="新建分类" OnClick="OpenCreateCategoryDialog"
|
||
Disabled="@(bots.Count == 0)" />
|
||
|
||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" Size="Size.Small"
|
||
Title="编辑当前分类" OnClick="OpenEditCategoryDialog"
|
||
Disabled="@(bots.Count == 0 || filterCategoryId <= 0)" />
|
||
|
||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
|
||
Title="删除当前分类" OnClick="DeleteSelectedCategoryAsync"
|
||
Disabled="@(bots.Count == 0 || filterCategoryId <= 0)" />
|
||
|
||
<MudTextField @bind-Value="search" 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;"
|
||
Immediate="true" Disabled="@(bots.Count == 0)" @bind-Value:after="OnSearchChanged" />
|
||
</MudStack>
|
||
</MudPaper>
|
||
|
||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-2" Style="flex-wrap: wrap;">
|
||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
|
||
Disabled="@(loading || bots.Count == 0)" OnClick="ReloadTable" Size="Size.Small">
|
||
刷新
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@SelectionToggleIcon"
|
||
Disabled="@(loading || channels.Count == 0)" OnClick="CycleSelectionToggle" Size="Size.Small">
|
||
@SelectionToggleText
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync"
|
||
Disabled="@(loading || selectedBotId <= 0)" OnClick="Sync" Size="Size.Small">
|
||
同步
|
||
</MudButton>
|
||
|
||
<MudMenu Label="批量操作" Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.MoreHoriz"
|
||
Size="Size.Small" Disabled="@(loading || bots.Count == 0)" Dense="true" EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="CheckSelectedChannelsStatus">检测状态(已选)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="ExportInvites">导出邀请(已选)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="OpenBatchInviteMembers">批量邀请成员(已选)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="OpenBatchSetAdminsByAccount">批量设置管理员(账号执行)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="OpenSetAdmins">批量设置管理员(机器人/ID)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="OpenBatchSetCategory">批量设置分类(已选)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0 || selectedBotId <= 0)" OnClick="OpenBanMember">踢出用户(已选)</MudMenuItem>
|
||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="DeleteSelectedChannelsAsync">删除频道(已选)</MudMenuItem>
|
||
</MudMenu>
|
||
|
||
@if (selected.Count > 0)
|
||
{
|
||
<MudChip T="string" Size="Size.Small" Color="Color.Info" Class="ml-2">已选 @selected.Count</MudChip>
|
||
}
|
||
else
|
||
{
|
||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="ml-2">频道 (@totalCount)</MudText>
|
||
}
|
||
</MudStack>
|
||
|
||
<MudCard>
|
||
<MudCardContent>
|
||
<MudTable ServerData="LoadChannelsServerData" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading"
|
||
Class="tp-table"
|
||
MultiSelection="true" @bind-SelectedItems="selected" @ref="_table">
|
||
<HeaderContent>
|
||
<MudTh>频道名称</MudTh>
|
||
<MudTh>用户名</MudTh>
|
||
<MudTh>分类</MudTh>
|
||
<MudTh>频道状态</MudTh>
|
||
<MudTh>最后同步</MudTh>
|
||
<MudTh>操作</MudTh>
|
||
</HeaderContent>
|
||
<RowTemplate>
|
||
<MudTd DataLabel="频道名称">
|
||
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
|
||
</MudTd>
|
||
<MudTd DataLabel="用户名">
|
||
@if (!string.IsNullOrWhiteSpace(context.Username))
|
||
{
|
||
<MudChip T="string" Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
|
||
}
|
||
else
|
||
{
|
||
<span>-</span>
|
||
}
|
||
</MudTd>
|
||
<MudTd DataLabel="分类">
|
||
@if (context.Category != null)
|
||
{
|
||
<MudChip T="string" Size="Size.Small">@context.Category.Name</MudChip>
|
||
}
|
||
else
|
||
{
|
||
<MudText Typo="Typo.body2" Color="Color.Default">未分类</MudText>
|
||
}
|
||
</MudTd>
|
||
<MudTd DataLabel="频道状态">
|
||
<MudTooltip Text="@GetChannelStatusTooltip(context)">
|
||
<MudChip T="string" Size="Size.Small" Color="@GetChannelStatusColor(context)">
|
||
@GetChannelStatusText(context)
|
||
</MudChip>
|
||
</MudTooltip>
|
||
</MudTd>
|
||
<MudTd DataLabel="最后同步"><PanelTime Value="context.SyncedAt" /></MudTd>
|
||
<MudTd DataLabel="操作">
|
||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
|
||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" Color="Color.Success"
|
||
Disabled="@(selectedBotId <= 0)"
|
||
OnClick="@(async () => await CheckSingleChannelStatus(context))" title="检测状态" />
|
||
<MudIconButton Icon="@Icons.Material.Filled.Link" Size="Size.Small" Color="Color.Info"
|
||
Disabled="@(selectedBotId <= 0)"
|
||
OnClick="@(async () => await CopyLinkAsync(context))" Title="复制链接" />
|
||
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Default"
|
||
Disabled="@(selectedBotId <= 0)"
|
||
OnClick="@(async () => await ShowDetailsAsync(context))" Title="查看详情" />
|
||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
|
||
Disabled="@(selectedBotId <= 0)"
|
||
OnClick="@(async () => await EditChannelAsync(context))" Title="编辑频道" />
|
||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||
OnClick="@(async () => await DeleteSingleChannelAsync(context))" title="删除频道" />
|
||
</MudStack>
|
||
</MudTd>
|
||
</RowTemplate>
|
||
<NoRecordsContent>
|
||
@if (bots.Count == 0)
|
||
{
|
||
<MudText>暂无机器人</MudText>
|
||
}
|
||
else
|
||
{
|
||
<MudText>暂无频道</MudText>
|
||
}
|
||
</NoRecordsContent>
|
||
<PagerContent>
|
||
<MudTablePager PageSizeOptions="new int[] { 20, 50, 100, 200 }" />
|
||
</PagerContent>
|
||
</MudTable>
|
||
</MudCardContent>
|
||
</MudCard>
|
||
|
||
@code {
|
||
[SupplyParameterFromQuery(Name = "botId")]
|
||
public int? BotIdQuery { get; set; }
|
||
|
||
private MudTable<BotChannel>? _table;
|
||
private List<Bot> bots = new();
|
||
private Bot? selectedBot;
|
||
private int selectedBotId = 0;
|
||
|
||
private bool loading = true;
|
||
private List<BotChannelCategory> categories = new();
|
||
private List<BotChannel> channels = new();
|
||
private int totalCount;
|
||
|
||
private HashSet<BotChannel> selected = new();
|
||
|
||
private int filterCategoryId = 0;
|
||
private int filterStatus = 0;
|
||
private string search = "";
|
||
private bool _actionsExpanded = true;
|
||
private CancellationTokenSource? _searchDebounceCts;
|
||
|
||
private SelectionToggleMode _selectionToggleMode = SelectionToggleMode.SelectAll;
|
||
private string SelectionToggleText => _selectionToggleMode switch
|
||
{
|
||
SelectionToggleMode.SelectAll => "全选本页",
|
||
SelectionToggleMode.Invert => "反选本页",
|
||
SelectionToggleMode.Clear => "清空选择",
|
||
_ => "全选本页"
|
||
};
|
||
|
||
private string SelectionToggleIcon => _selectionToggleMode switch
|
||
{
|
||
SelectionToggleMode.SelectAll => Icons.Material.Filled.SelectAll,
|
||
SelectionToggleMode.Invert => Icons.Material.Filled.Flip,
|
||
SelectionToggleMode.Clear => Icons.Material.Filled.Clear,
|
||
_ => Icons.Material.Filled.SelectAll
|
||
};
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
try
|
||
{
|
||
bots = (await BotManagement.GetAllBotsAsync()).ToList();
|
||
|
||
if (BotIdQuery is > 0 && bots.Any(x => x.Id == BotIdQuery.Value))
|
||
{
|
||
selectedBotId = BotIdQuery.Value;
|
||
}
|
||
|
||
await LoadBotData();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"加载机器人失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task OnSelectedBotChanged(int botId)
|
||
{
|
||
if (selectedBotId == botId)
|
||
return;
|
||
|
||
selectedBotId = botId;
|
||
filterCategoryId = 0;
|
||
filterStatus = 0;
|
||
search = "";
|
||
selected.Clear();
|
||
|
||
if (selectedBotId <= 0)
|
||
{
|
||
selectedBot = null;
|
||
Navigation.NavigateTo("/bots/channels", replace: true);
|
||
await LoadBotData();
|
||
return;
|
||
}
|
||
|
||
Navigation.NavigateTo($"/bots/channels?botId={selectedBotId}", replace: true);
|
||
await LoadBotData();
|
||
}
|
||
|
||
private async Task LoadBotData()
|
||
{
|
||
loading = true;
|
||
try
|
||
{
|
||
if (selectedBotId > 0)
|
||
{
|
||
selectedBot = await BotManagement.GetBotAsync(selectedBotId);
|
||
if (selectedBot == null)
|
||
{
|
||
Snackbar.Add("机器人不存在,已切回全部机器人视图", Severity.Warning);
|
||
selectedBotId = 0;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
selectedBot = null;
|
||
}
|
||
|
||
categories = (await BotManagement.GetCategoriesAsync()).ToList();
|
||
await ReloadTable();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task<TableData<BotChannel>> LoadChannelsServerData(TableState state, CancellationToken cancellationToken)
|
||
{
|
||
loading = true;
|
||
try
|
||
{
|
||
var (items, total) = await BotManagement.QueryChannelsPagedAsync(
|
||
botId: selectedBotId,
|
||
categoryId: filterCategoryId == -1 ? -1 : (filterCategoryId > 0 ? filterCategoryId : null),
|
||
search: search,
|
||
statusFilter: filterStatus,
|
||
pageIndex: state.Page,
|
||
pageSize: state.PageSize,
|
||
cancellationToken: cancellationToken);
|
||
|
||
channels = items.ToList();
|
||
totalCount = total;
|
||
selected.Clear();
|
||
|
||
return new TableData<BotChannel> { Items = channels, TotalItems = totalCount };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
|
||
return new TableData<BotChannel> { Items = Array.Empty<BotChannel>(), TotalItems = 0 };
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private void ToggleActions()
|
||
{
|
||
_actionsExpanded = !_actionsExpanded;
|
||
}
|
||
|
||
private async Task ReloadTable()
|
||
{
|
||
selected.Clear();
|
||
if (_table != null)
|
||
await _table.ReloadServerData();
|
||
}
|
||
|
||
private async Task OnFiltersChanged()
|
||
{
|
||
await ReloadTable();
|
||
}
|
||
|
||
private async Task OnSearchChanged()
|
||
{
|
||
_searchDebounceCts?.Cancel();
|
||
_searchDebounceCts?.Dispose();
|
||
_searchDebounceCts = new CancellationTokenSource();
|
||
|
||
var token = _searchDebounceCts.Token;
|
||
try
|
||
{
|
||
await Task.Delay(300, token);
|
||
await ReloadTable();
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
private async Task Sync()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
var result = await BotTelegram.SyncBotChannelsAsync(selectedBotId, CancellationToken.None);
|
||
if (result.AppliedUpdates > 0 || result.RemovedStale > 0)
|
||
Snackbar.Add($"同步完成:应用更新 {result.AppliedUpdates} 条,清理失效 {result.RemovedStale} 个频道", Severity.Success);
|
||
else
|
||
Snackbar.Add("同步完成:没有需要处理的更新或失效频道。提示:请先把 Bot 设为频道管理员,再点同步。", Severity.Info);
|
||
|
||
await ReloadTable();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"同步失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task CheckSingleChannelStatus(BotChannel channel)
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
var result = await BotTelegram.CheckChannelsStatusAsync(
|
||
selectedBotId,
|
||
new List<long> { channel.TelegramId },
|
||
CancellationToken.None);
|
||
|
||
if (result.FailedCount == 0)
|
||
{
|
||
Snackbar.Add($"频道“{channel.Title}”检测正常", Severity.Success);
|
||
}
|
||
else
|
||
{
|
||
var msg = result.Failures.TryGetValue(channel.TelegramId, out var err) ? err : "检测失败";
|
||
Snackbar.Add($"频道“{channel.Title}”检测异常:{msg}", Severity.Warning);
|
||
}
|
||
|
||
await ReloadTable();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"检测失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task CheckSelectedChannelsStatus()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
bool? ok = await DialogService.ShowMessageBox(
|
||
"确认检测",
|
||
$"将检测 {selected.Count} 个频道的状态(发送一条探测消息并立即删除)。是否继续?",
|
||
yesText: "继续", cancelText: "取消");
|
||
|
||
if (ok != true)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
var ids = selected.Select(x => x.TelegramId).Distinct().ToList();
|
||
var result = await BotTelegram.CheckChannelsStatusAsync(selectedBotId, ids, CancellationToken.None);
|
||
|
||
if (result.FailedCount == 0)
|
||
{
|
||
Snackbar.Add($"检测完成:{result.SuccessCount} 个频道均正常", Severity.Success);
|
||
}
|
||
else
|
||
{
|
||
var first = result.Failures.FirstOrDefault();
|
||
var msg = $"检测完成:正常 {result.SuccessCount} 个,异常 {result.FailedCount} 个";
|
||
if (first.Key != 0)
|
||
msg += $"(首个失败:{first.Value})";
|
||
Snackbar.Add(msg, Severity.Warning);
|
||
}
|
||
|
||
await ReloadTable();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"检测失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task DeleteSingleChannelAsync(BotChannel channel)
|
||
{
|
||
var channelName = string.IsNullOrWhiteSpace(channel.Title) ? channel.TelegramId.ToString() : channel.Title;
|
||
var selectedBotIds = await SelectBotsForDeleteAsync(new[] { channel.TelegramId }, channelName);
|
||
if (selectedBotIds == null || selectedBotIds.Count == 0)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
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)
|
||
{
|
||
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task DeleteSelectedChannelsAsync()
|
||
{
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
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 success = 0;
|
||
var failed = 0;
|
||
var removedTotal = 0;
|
||
|
||
foreach (var chatId in ids)
|
||
{
|
||
try
|
||
{
|
||
var removed = await BotManagement.DeleteChannelBindingsByBotIdsAsync(chatId, selectedBotIds);
|
||
removedTotal += removed;
|
||
success++;
|
||
}
|
||
catch
|
||
{
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
if (failed == 0)
|
||
{
|
||
Snackbar.Add($"批量删除完成:处理 {success} 个频道,删除绑定 {removedTotal} 条", Severity.Success);
|
||
}
|
||
else
|
||
{
|
||
Snackbar.Add($"批量删除完成:处理成功 {success} 个,失败 {failed} 个,删除绑定 {removedTotal} 条", Severity.Warning);
|
||
}
|
||
|
||
await ReloadTable();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"批量删除失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task<List<int>?> SelectBotsForDeleteAsync(IReadOnlyCollection<long> channelTelegramIds, string? singleChannelName)
|
||
{
|
||
var ids = channelTelegramIds.Where(x => x != 0).Distinct().ToList();
|
||
if (ids.Count == 0)
|
||
return null;
|
||
|
||
var botMap = new Dictionary<int, Bot>();
|
||
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<BotChannelDeleteBindingsDialog>("选择要删除的 Bot 绑定", parameters, options);
|
||
var result = await dialog.Result;
|
||
if (result.Canceled)
|
||
return null;
|
||
|
||
if (result.Data is List<int> selectedIds)
|
||
return selectedIds.Where(x => x > 0).Distinct().ToList();
|
||
if (result.Data is IReadOnlyList<int> selectedReadOnlyIds)
|
||
return selectedReadOnlyIds.Where(x => x > 0).Distinct().ToList();
|
||
|
||
return null;
|
||
}
|
||
|
||
private async Task OpenCreateCategoryDialog()
|
||
{
|
||
if (bots.Count == 0)
|
||
return;
|
||
|
||
var parameters = new DialogParameters();
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||
var dialog = DialogService.Show<BotChannelCategoryDialog>("新建分类", parameters, options);
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled)
|
||
{
|
||
// 重新加载分类列表
|
||
await LoadBotData();
|
||
}
|
||
}
|
||
|
||
private BotChannelCategory? GetSelectedCategory()
|
||
{
|
||
if (filterCategoryId <= 0)
|
||
return null;
|
||
|
||
return categories.FirstOrDefault(x => x.Id == filterCategoryId);
|
||
}
|
||
|
||
private async Task OpenEditCategoryDialog()
|
||
{
|
||
if (bots.Count == 0)
|
||
return;
|
||
|
||
var cat = GetSelectedCategory();
|
||
if (cat == null)
|
||
{
|
||
Snackbar.Add("请先在“分类”里选择一个具体分类再编辑", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["Category"] = cat
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||
var dialog = DialogService.Show<BotChannelCategoryDialog>("编辑分类", parameters, options);
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled)
|
||
{
|
||
await LoadBotData();
|
||
}
|
||
}
|
||
|
||
private async Task DeleteSelectedCategoryAsync()
|
||
{
|
||
if (bots.Count == 0)
|
||
return;
|
||
|
||
var cat = GetSelectedCategory();
|
||
if (cat == null)
|
||
{
|
||
Snackbar.Add("请先在“分类”里选择一个具体分类再删除", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
bool? ok = await DialogService.ShowMessageBox(
|
||
"确认删除",
|
||
$"确定删除分类“{cat.Name}”吗?删除后,该分类下的频道会变为“未分类”。",
|
||
yesText: "删除", cancelText: "取消");
|
||
|
||
if (ok != true)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
await BotManagement.DeleteCategoryAsync(cat.Id);
|
||
Snackbar.Add("分类已删除", Severity.Success);
|
||
|
||
filterCategoryId = 0;
|
||
await LoadBotData();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task CopyLinkAsync(BotChannel channel)
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
var link = await BotTelegram.ExportInviteLinkAsync(selectedBotId, channel.TelegramId, CancellationToken.None);
|
||
await JS.InvokeVoidAsync("telegramPanel.copyText", link);
|
||
Snackbar.Add("已复制链接", Severity.Success);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"复制失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task ExportInvites()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
bool? result = await DialogService.ShowMessageBox(
|
||
"确认导出",
|
||
$"将为 {selected.Count} 个频道生成邀请链接并导出(需要机器人具备管理权限)。是否继续?",
|
||
yesText: "继续", cancelText: "取消");
|
||
|
||
if (result != true)
|
||
return;
|
||
|
||
var ids = string.Join(",", selected.Select(x => x.TelegramId));
|
||
var url = $"/downloads/bots/{selectedBotId}/invites.txt?ids={Uri.EscapeDataString(ids)}";
|
||
Navigation.NavigateTo(url, forceLoad: true);
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
private async Task OpenSetAdmins()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["ChannelTelegramIds"] = selected.Select(x => x.TelegramId).Distinct().ToList()
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||
var dialog = DialogService.Show<BotSetAdminsDialog>("批量设置管理员", parameters, options);
|
||
var result = await dialog.Result;
|
||
if (!result.Canceled)
|
||
{
|
||
// 管理员设置后不影响本地数据,但用户可能期望刷新一下
|
||
await LoadBotData();
|
||
}
|
||
}
|
||
|
||
private async Task OpenBatchInviteMembers()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["Channels"] = selected.ToList()
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||
var dialog = DialogService.Show<BotChannelBatchInviteDialog>("批量邀请成员", parameters, options);
|
||
var result = await dialog.Result;
|
||
if (!result.Canceled)
|
||
{
|
||
// 邀请不影响本地数据,但用户可能期望刷新一下
|
||
await LoadBotData();
|
||
}
|
||
}
|
||
|
||
private async Task OpenBatchSetAdminsByAccount()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["Channels"] = selected.ToList()
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||
var dialog = DialogService.Show<BotChannelBatchAdminsDialog>("批量设置管理员(账号执行)", parameters, options);
|
||
var result = await dialog.Result;
|
||
if (!result.Canceled)
|
||
{
|
||
// 管理员设置后不影响本地数据,但用户可能期望刷新一下
|
||
await LoadBotData();
|
||
}
|
||
}
|
||
|
||
private async Task OpenBatchSetCategory()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["Categories"] = categories,
|
||
["SelectedCount"] = selected.Count
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||
var dialog = DialogService.Show<BotBatchSetCategoryDialog>("批量设置分类", parameters, options);
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled && result.Data is int categoryId)
|
||
{
|
||
loading = true;
|
||
try
|
||
{
|
||
var channelIds = selected.Select(x => x.Id).ToList();
|
||
int successCount = 0;
|
||
int failCount = 0;
|
||
|
||
foreach (var channelId in channelIds)
|
||
{
|
||
try
|
||
{
|
||
await BotManagement.SetChannelCategoryAsync(channelId, categoryId == 0 ? null : categoryId);
|
||
successCount++;
|
||
}
|
||
catch
|
||
{
|
||
failCount++;
|
||
}
|
||
}
|
||
|
||
if (failCount == 0)
|
||
{
|
||
Snackbar.Add($"已成功设置 {successCount} 个频道的分类", Severity.Success);
|
||
}
|
||
else
|
||
{
|
||
Snackbar.Add($"成功:{successCount} 个,失败:{failCount} 个", Severity.Warning);
|
||
}
|
||
|
||
// 重新加载数据
|
||
await LoadBotData();
|
||
selected.Clear();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"批量设置失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task OpenBanMember()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
if (selected.Count == 0)
|
||
{
|
||
Snackbar.Add("请先选择频道", Severity.Info);
|
||
return;
|
||
}
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["Channels"] = selected.ToList(),
|
||
["SelectedCount"] = selected.Count
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||
var dialog = DialogService.Show<BotBanMemberDialog>("移除用户", parameters, options);
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled && result.Data is BotBanMemberDialog.BanUserRequest request)
|
||
{
|
||
loading = true;
|
||
try
|
||
{
|
||
if (request.UseAccountExecution)
|
||
await ExecuteBanByAccountAsync(request);
|
||
else
|
||
await ExecuteBanByBotAsync(request);
|
||
|
||
selected.Clear();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"操作失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteBanByBotAsync(BotBanMemberDialog.BanUserRequest request)
|
||
{
|
||
if (request.UserId is null or <= 0)
|
||
{
|
||
Snackbar.Add("机器人执行仅支持用户 ID(纯数字)", Severity.Warning);
|
||
return;
|
||
}
|
||
|
||
var channelTelegramIds = selected.Select(x => x.TelegramId).Distinct().ToList();
|
||
var banResult = await BotTelegram.BanChatMemberAsync(
|
||
selectedBotId,
|
||
channelTelegramIds,
|
||
request.UserId.Value,
|
||
request.PermanentBan,
|
||
CancellationToken.None);
|
||
|
||
var actionText = request.PermanentBan ? "封禁" : "踢出";
|
||
if (banResult.Failures.Count == 0)
|
||
{
|
||
Snackbar.Add($"已成功从 {banResult.SuccessCount} 个频道{actionText}用户 {request.UserId}", Severity.Success);
|
||
return;
|
||
}
|
||
|
||
var message = $"成功:{banResult.SuccessCount} 个,失败:{banResult.Failures.Count} 个";
|
||
if (banResult.Failures.Count > 0)
|
||
{
|
||
var firstError = banResult.Failures.First();
|
||
message += $"(首个失败原因:{firstError.Value})";
|
||
}
|
||
Snackbar.Add(message, Severity.Warning);
|
||
}
|
||
|
||
private async Task ExecuteBanByAccountAsync(BotBanMemberDialog.BanUserRequest request)
|
||
{
|
||
var targetChannels = selected
|
||
.Where(x => x != null)
|
||
.GroupBy(x => x.TelegramId)
|
||
.Select(x => x.First())
|
||
.ToList();
|
||
if (targetChannels.Count == 0)
|
||
return;
|
||
|
||
if (string.IsNullOrWhiteSpace(request.Username) && (request.UserId is null or <= 0))
|
||
{
|
||
Snackbar.Add("账号执行需要填写 @username 或用户 ID", Severity.Warning);
|
||
return;
|
||
}
|
||
|
||
var allAccounts = (await AccountManagement.GetAllAccountsAsync()).ToList();
|
||
var availableAccounts = allAccounts
|
||
.Where(x => x.IsActive && x.Category?.ExcludeFromOperations != true && x.UserId > 0)
|
||
.ToList();
|
||
var accountsById = availableAccounts.ToDictionary(x => x.Id);
|
||
var accountsByUserId = availableAccounts
|
||
.GroupBy(x => x.UserId)
|
||
.ToDictionary(x => x.Key, x => x.First());
|
||
|
||
var channelAdmins = new Dictionary<long, List<BotTelegramService.BotChatAdminInfo>>();
|
||
foreach (var channel in targetChannels)
|
||
{
|
||
try
|
||
{
|
||
channelAdmins[channel.TelegramId] = await BotTelegram.GetChatAdminsAsync(selectedBotId, channel.TelegramId, CancellationToken.None);
|
||
}
|
||
catch
|
||
{
|
||
channelAdmins[channel.TelegramId] = new List<BotTelegramService.BotChatAdminInfo>();
|
||
}
|
||
}
|
||
|
||
var failures = new Dictionary<long, string>();
|
||
var success = 0;
|
||
foreach (var channel in targetChannels)
|
||
{
|
||
var (executorId, reason) = ResolveBanExecutorAccountId(
|
||
channel,
|
||
request.SelectedAccountId,
|
||
channelAdmins,
|
||
accountsById,
|
||
accountsByUserId);
|
||
|
||
if (executorId == null)
|
||
{
|
||
failures[channel.TelegramId] = NormalizeReason(reason ?? "无可用执行账号");
|
||
continue;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(request.Username))
|
||
{
|
||
await ChannelService.KickUserAsync(executorId.Value, channel.TelegramId, request.Username!, request.PermanentBan);
|
||
}
|
||
else if (request.UserId > 0)
|
||
{
|
||
await ChannelService.KickUserByUserIdAsync(executorId.Value, channel.TelegramId, request.UserId.Value, request.PermanentBan);
|
||
}
|
||
else
|
||
{
|
||
throw new InvalidOperationException("缺少可用的目标用户标识");
|
||
}
|
||
|
||
success++;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
failures[channel.TelegramId] = NormalizeReason(ex.Message);
|
||
}
|
||
}
|
||
|
||
var actionText = request.PermanentBan ? "封禁" : "踢出";
|
||
var targetText = !string.IsNullOrWhiteSpace(request.Username)
|
||
? $"@{request.Username}"
|
||
: request.UserId?.ToString() ?? "-";
|
||
|
||
if (failures.Count == 0)
|
||
{
|
||
Snackbar.Add($"已成功从 {success} 个频道{actionText}用户 {targetText}", Severity.Success);
|
||
return;
|
||
}
|
||
|
||
var summary = $"成功:{success} 个,失败:{failures.Count} 个";
|
||
if (failures.Count > 0)
|
||
{
|
||
var first = failures.First();
|
||
var channelTitle = targetChannels.FirstOrDefault(x => x.TelegramId == first.Key)?.Title ?? first.Key.ToString();
|
||
summary += $"(首个失败:{channelTitle}:{first.Value})";
|
||
}
|
||
Snackbar.Add(summary, Severity.Warning);
|
||
}
|
||
|
||
private static (int? ExecutorId, string? Reason) ResolveBanExecutorAccountId(
|
||
BotChannel channel,
|
||
int selectedAccountId,
|
||
IReadOnlyDictionary<long, List<BotTelegramService.BotChatAdminInfo>> channelAdmins,
|
||
IReadOnlyDictionary<int, Account> accountsById,
|
||
IReadOnlyDictionary<long, Account> accountsByUserId)
|
||
{
|
||
if (!channelAdmins.TryGetValue(channel.TelegramId, out var admins) || admins.Count == 0)
|
||
return (null, "无法获取频道管理员列表(请确认 Bot 已加入且为管理员)");
|
||
|
||
if (selectedAccountId > 0)
|
||
{
|
||
if (!accountsById.TryGetValue(selectedAccountId, out var selected) || selected.UserId <= 0)
|
||
return (null, "所选执行账号无效");
|
||
|
||
var admin = admins.FirstOrDefault(x => x.UserId == selected.UserId);
|
||
if (admin == null)
|
||
return (null, "所选执行账号不是该频道管理员");
|
||
|
||
if (!admin.IsCreator && !admin.CanRestrictMembers)
|
||
return (null, "所选执行账号缺少“封禁用户”权限");
|
||
|
||
return (selected.Id, null);
|
||
}
|
||
|
||
var creator = admins.FirstOrDefault(x => x.IsCreator);
|
||
if (creator != null && accountsByUserId.TryGetValue(creator.UserId, out var creatorAcc))
|
||
return (creatorAcc.Id, null);
|
||
|
||
foreach (var admin in admins)
|
||
{
|
||
if (!admin.IsCreator && !admin.CanRestrictMembers)
|
||
continue;
|
||
|
||
if (accountsByUserId.TryGetValue(admin.UserId, out var account))
|
||
return (account.Id, null);
|
||
}
|
||
|
||
return (null, "无可用执行账号(需要该频道管理员且拥有“封禁用户”权限,并且在系统中存在)");
|
||
}
|
||
|
||
private static string NormalizeReason(string? reason)
|
||
{
|
||
var text = (reason ?? string.Empty).Trim();
|
||
return string.IsNullOrWhiteSpace(text) ? "失败" : text;
|
||
}
|
||
|
||
private async Task ShowDetailsAsync(BotChannel channel)
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["ChannelTelegramId"] = channel.TelegramId
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
|
||
DialogService.Show<BotChannelDetailsDialog>("频道详情", parameters, options);
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
private async Task EditChannelAsync(BotChannel channel)
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId,
|
||
["ChannelTelegramId"] = channel.TelegramId
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||
var dialog = DialogService.Show<BotChannelEditDialog>("编辑频道", parameters, options);
|
||
var result = await dialog.Result;
|
||
if (!result.Canceled)
|
||
await LoadBotData();
|
||
}
|
||
|
||
private static string GetChannelStatusText(BotChannel channel)
|
||
{
|
||
return channel.ChannelStatusOk switch
|
||
{
|
||
true => "正常",
|
||
false => "异常",
|
||
null => "未检测"
|
||
};
|
||
}
|
||
|
||
private static Color GetChannelStatusColor(BotChannel channel)
|
||
{
|
||
return channel.ChannelStatusOk switch
|
||
{
|
||
true => Color.Success,
|
||
false => Color.Error,
|
||
null => Color.Default
|
||
};
|
||
}
|
||
|
||
private static string GetChannelStatusTooltip(BotChannel channel)
|
||
{
|
||
if (channel.ChannelStatusOk == null)
|
||
return "尚未检测";
|
||
|
||
var checkedAt = channel.ChannelStatusCheckedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||
if (channel.ChannelStatusOk == true)
|
||
return $"最近检测:{checkedAt}";
|
||
|
||
var err = string.IsNullOrWhiteSpace(channel.ChannelStatusError) ? "未知错误" : channel.ChannelStatusError;
|
||
return $"最近检测:{checkedAt};失败原因:{err}";
|
||
}
|
||
|
||
private Task SelectAllFiltered()
|
||
{
|
||
selected = channels.ToHashSet();
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private Task InvertSelection()
|
||
{
|
||
var targets = channels.ToList();
|
||
foreach (var c in targets)
|
||
{
|
||
if (selected.Contains(c))
|
||
selected.Remove(c);
|
||
else
|
||
selected.Add(c);
|
||
}
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private Task ClearSelection()
|
||
{
|
||
selected.Clear();
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private async Task CycleSelectionToggle()
|
||
{
|
||
switch (_selectionToggleMode)
|
||
{
|
||
case SelectionToggleMode.SelectAll:
|
||
await SelectAllFiltered();
|
||
_selectionToggleMode = SelectionToggleMode.Invert;
|
||
break;
|
||
case SelectionToggleMode.Invert:
|
||
await InvertSelection();
|
||
_selectionToggleMode = SelectionToggleMode.Clear;
|
||
break;
|
||
case SelectionToggleMode.Clear:
|
||
await ClearSelection();
|
||
_selectionToggleMode = SelectionToggleMode.SelectAll;
|
||
break;
|
||
default:
|
||
_selectionToggleMode = SelectionToggleMode.SelectAll;
|
||
break;
|
||
}
|
||
}
|
||
|
||
private enum SelectionToggleMode
|
||
{
|
||
SelectAll = 0,
|
||
Invert = 1,
|
||
Clear = 2
|
||
}
|
||
}
|