Files
Telegram-Panel/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor
T

546 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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();
}
}