546 lines
20 KiB
Plaintext
546 lines
20 KiB
Plaintext
@page "/bots/channels"
|
||
@inject BotManagementService BotManagement
|
||
@inject TelegramPanel.Core.Services.Telegram.BotTelegramService BotTelegram
|
||
@inject ISnackbar Snackbar
|
||
@inject IDialogService DialogService
|
||
@inject NavigationManager Navigation
|
||
@inject IJSRuntime JS
|
||
@using TelegramPanel.Data.Entities
|
||
@using TelegramPanel.Web.Components.Dialogs
|
||
|
||
<PageTitle>Bot 频道 - Telegram Panel</PageTitle>
|
||
|
||
<MudText Typo="Typo.h4" Class="mb-4">Bot 频道</MudText>
|
||
|
||
<MudCard>
|
||
<MudCardContent>
|
||
<MudTable Items="@filtered" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading"
|
||
MultiSelection="true" @bind-SelectedItems="selected">
|
||
<ToolBarContent>
|
||
<MudSelect T="int" Value="@selectedBotId" ValueChanged="@(async (int id) => await OnSelectedBotChanged(id))" Label="选择机器人"
|
||
Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 260px;">
|
||
<MudSelectItem Value="0">-- 请选择机器人 --</MudSelectItem>
|
||
@foreach (var b in bots)
|
||
{
|
||
<MudSelectItem Value="@b.Id">@b.Name</MudSelectItem>
|
||
}
|
||
</MudSelect>
|
||
|
||
<MudText Typo="Typo.h6">
|
||
频道 (@channels.Count)
|
||
</MudText>
|
||
|
||
<MudSpacer />
|
||
|
||
<MudSelect @bind-Value="filterCategoryId" Label="分类" Variant="Variant.Outlined" Dense="true" Style="min-width: 160px;"
|
||
Disabled="@(selectedBotId <= 0)">
|
||
<MudSelectItem Value="0">全部分类</MudSelectItem>
|
||
<MudSelectItem Value="-1">未分类</MudSelectItem>
|
||
@foreach (var c in categories)
|
||
{
|
||
<MudSelectItem Value="@c.Id">@c.Name</MudSelectItem>
|
||
}
|
||
</MudSelect>
|
||
|
||
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Primary" Size="Size.Small"
|
||
Title="新建分类" OnClick="OpenCreateCategoryDialog"
|
||
Disabled="@(selectedBotId <= 0)" Class="mr-2" />
|
||
|
||
<MudTextField @bind-Value="search" Placeholder="搜索频道..." Adornment="Adornment.Start"
|
||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"
|
||
Immediate="true" Disabled="@(selectedBotId <= 0)" />
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync" Class="ml-2"
|
||
Disabled="@(loading || selectedBotId <= 0)" OnClick="Sync">
|
||
同步
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Success" StartIcon="@Icons.Material.Filled.Link" Class="ml-2"
|
||
Disabled="@(loading || selectedBotId <= 0 || selected.Count == 0)" OnClick="ExportInvites">
|
||
导出邀请(已选)
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AdminPanelSettings" Class="ml-2"
|
||
Disabled="@(loading || selectedBotId <= 0 || selected.Count == 0)" OnClick="OpenSetAdmins">
|
||
设置管理员(已选)
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Category" Class="ml-2"
|
||
Disabled="@(loading || selectedBotId <= 0 || selected.Count == 0)" OnClick="OpenBatchSetCategory">
|
||
批量设置分类(@selected.Count)
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Outlined" Color="Color.Error" StartIcon="@Icons.Material.Filled.PersonRemove" Class="ml-2"
|
||
Disabled="@(loading || selectedBotId <= 0 || selected.Count == 0)" OnClick="OpenBanMember">
|
||
踢出用户(@selected.Count)
|
||
</MudButton>
|
||
|
||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
|
||
Disabled="@(loading || selectedBotId <= 0)" OnClick="LoadBotData" Class="ml-2">
|
||
刷新
|
||
</MudButton>
|
||
</ToolBarContent>
|
||
|
||
<HeaderContent>
|
||
<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="分类">
|
||
<MudSelect T="int?" Value="@context.CategoryId" ValueChanged="@(async v => await SetCategoryAsync(context, v))"
|
||
Dense="true" Variant="Variant.Outlined" Style="min-width: 160px;">
|
||
<MudSelectItem Value="@(default(int?))">未分类</MudSelectItem>
|
||
@foreach (var c in categories)
|
||
{
|
||
<MudSelectItem Value="@((int?)c.Id)">@c.Name</MudSelectItem>
|
||
}
|
||
</MudSelect>
|
||
</MudTd>
|
||
<MudTd DataLabel="最后同步"><PanelTime Value="context.SyncedAt" /></MudTd>
|
||
<MudTd DataLabel="操作">
|
||
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
|
||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(async () => await CopyLinkAsync(context))">复制链接</MudMenuItem>
|
||
<MudDivider />
|
||
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(async () => await ShowDetailsAsync(context))">查看详情</MudMenuItem>
|
||
<MudMenuItem Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await EditChannelAsync(context))">编辑频道</MudMenuItem>
|
||
</MudMenu>
|
||
</MudTd>
|
||
</RowTemplate>
|
||
<NoRecordsContent>
|
||
@if (selectedBotId <= 0)
|
||
{
|
||
<MudText>请先选择机器人</MudText>
|
||
}
|
||
else
|
||
{
|
||
<MudText>暂无频道</MudText>
|
||
}
|
||
</NoRecordsContent>
|
||
</MudTable>
|
||
</MudCardContent>
|
||
</MudCard>
|
||
|
||
@code {
|
||
[SupplyParameterFromQuery(Name = "botId")]
|
||
public int? BotIdQuery { get; set; }
|
||
|
||
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 HashSet<BotChannel> selected = new();
|
||
|
||
private int filterCategoryId = 0;
|
||
private string search = "";
|
||
|
||
private IEnumerable<BotChannel> filtered => selectedBotId <= 0
|
||
? Enumerable.Empty<BotChannel>()
|
||
: channels.Where(c =>
|
||
{
|
||
if (filterCategoryId == -1 && c.CategoryId != null)
|
||
return false;
|
||
if (filterCategoryId > 0 && c.CategoryId != filterCategoryId)
|
||
return false;
|
||
|
||
if (string.IsNullOrWhiteSpace(search))
|
||
return true;
|
||
|
||
return c.Title.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||
|| (c.Username?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false);
|
||
});
|
||
|
||
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();
|
||
}
|
||
else if (bots.Count == 1)
|
||
{
|
||
selectedBotId = bots[0].Id;
|
||
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;
|
||
search = "";
|
||
selected.Clear();
|
||
|
||
if (selectedBotId <= 0)
|
||
{
|
||
selectedBot = null;
|
||
categories.Clear();
|
||
channels.Clear();
|
||
Navigation.NavigateTo("/bots/channels", replace: true);
|
||
return;
|
||
}
|
||
|
||
Navigation.NavigateTo($"/bots/channels?botId={selectedBotId}", replace: true);
|
||
await LoadBotData();
|
||
}
|
||
|
||
private async Task LoadBotData()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
loading = true;
|
||
try
|
||
{
|
||
selectedBot = await BotManagement.GetBotAsync(selectedBotId);
|
||
if (selectedBot == null)
|
||
{
|
||
Snackbar.Add("机器人不存在", Severity.Error);
|
||
selectedBotId = 0;
|
||
return;
|
||
}
|
||
|
||
categories = (await BotManagement.GetCategoriesAsync(selectedBotId)).ToList();
|
||
channels = (await BotManagement.GetChannelsAsync(selectedBotId)).ToList();
|
||
|
||
// 清理已选
|
||
var ids = channels.Select(x => x.Id).ToHashSet();
|
||
selected = selected.Where(x => ids.Contains(x.Id)).ToHashSet();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
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 LoadBotData();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"同步失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
private async Task OpenCreateCategoryDialog()
|
||
{
|
||
if (selectedBotId <= 0)
|
||
return;
|
||
|
||
var parameters = new DialogParameters
|
||
{
|
||
["BotId"] = selectedBotId
|
||
};
|
||
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 SetCategoryAsync(BotChannel channel, int? categoryId)
|
||
{
|
||
try
|
||
{
|
||
await BotManagement.SetChannelCategoryAsync(channel.Id, categoryId);
|
||
channel.CategoryId = categoryId;
|
||
Snackbar.Add("已保存分类", Severity.Success);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
|
||
}
|
||
}
|
||
|
||
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 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
|
||
{
|
||
["SelectedCount"] = selected.Count
|
||
};
|
||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, 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
|
||
{
|
||
var channelTelegramIds = selected.Select(x => x.TelegramId).ToList();
|
||
var banResult = await BotTelegram.BanChatMemberAsync(
|
||
selectedBotId,
|
||
channelTelegramIds,
|
||
request.UserId,
|
||
request.PermanentBan,
|
||
CancellationToken.None);
|
||
|
||
var actionText = request.PermanentBan ? "封禁" : "踢出";
|
||
if (banResult.Failures.Count == 0)
|
||
{
|
||
Snackbar.Add($"已成功从 {banResult.SuccessCount} 个频道{actionText}用户 {request.UserId}", Severity.Success);
|
||
}
|
||
else
|
||
{
|
||
var msg = $"成功:{banResult.SuccessCount} 个,失败:{banResult.Failures.Count} 个";
|
||
if (banResult.Failures.Count > 0)
|
||
{
|
||
var firstError = banResult.Failures.First();
|
||
msg += $"(首个失败原因:{firstError.Value})";
|
||
}
|
||
Snackbar.Add(msg, Severity.Warning);
|
||
}
|
||
|
||
selected.Clear();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"操作失败:{ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
loading = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|