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

333 lines
13 KiB
Plaintext

@page "/channels"
@inject ChannelManagementService ChannelManagement
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Web.Services.DataSyncService DataSync
@inject IChannelService ChannelService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@using TelegramPanel.Web.Components.Dialogs
@using TelegramPanel.Data.Entities
<PageTitle>频道列表 - Telegram Panel</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">频道列表</MudText>
<MudCard>
<MudCardContent>
<MudTable Items="@channels" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading" Filter="FilterFunc">
<ToolBarContent>
<MudText Typo="Typo.h6">频道 (@channels.Count)</MudText>
<MudSpacer />
<MudSelect @bind-Value="filterAccount" @bind-Value:after="LoadChannels" Label="筛选账号" Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 220px;">
<MudSelectItem Value="0">全部账号</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@FormatAccountLabel(account)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="filterType" Label="频道类型" Variant="Variant.Outlined" Dense="true" Class="mr-2" Style="min-width: 120px;">
<MudSelectItem Value='@("all")'>全部</MudSelectItem>
<MudSelectItem Value='@("public")'>公开</MudSelectItem>
<MudSelectItem Value='@("private")'>私密</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync" Class="mr-2"
Disabled="@(loading || filterAccount <= 0)" OnClick="SyncCurrentAccountChannels">
同步当前账号
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync" Class="mr-2"
Disabled="@loading" OnClick="SyncAllAccountsChannels">
同步全部账号
</MudButton>
<MudTextField @bind-Value="searchString" Placeholder="搜索频道..." Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0" />
</ToolBarContent>
<HeaderContent>
<MudTh>频道名称</MudTh>
<MudTh>用户名</MudTh>
<MudTh>类型</MudTh>
<MudTh>成员数</MudTh>
<MudTh>创建账号</MudTh>
<MudTh>最后同步</MudTh>
<MudTh>操作</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="频道名称">
<div class="d-flex align-center">
<MudAvatar Color="Color.Primary" Size="Size.Small" Class="mr-2">@(string.IsNullOrWhiteSpace(context.Title) ? "?" : context.Title[0])</MudAvatar>
<div>
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
@if (!string.IsNullOrEmpty(context.About))
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@(context.About.Length > 50 ? context.About.Substring(0, 50) + "..." : context.About)</MudText>
}
</div>
</div>
</MudTd>
<MudTd DataLabel="用户名">
@if (!string.IsNullOrEmpty(context.Username))
{
<MudChip T="string" Size="Size.Small" Icon="@Icons.Material.Filled.AlternateEmail">@context.Username</MudChip>
}
else
{
<span>-</span>
}
</MudTd>
<MudTd DataLabel="类型">
<MudChip T="string" Size="Size.Small" Color="@(string.IsNullOrEmpty(context.Username) ? Color.Warning : Color.Success)">
@(string.IsNullOrEmpty(context.Username) ? "私密" : "公开")
</MudChip>
</MudTd>
<MudTd DataLabel="成员数">@context.MemberCount</MudTd>
<MudTd DataLabel="创建账号">
@if (context.CreatorAccount != null)
{
<span>@context.CreatorAccount.Phone</span>
}
else if (context.CreatorAccountId != null)
{
<span>账号 @context.CreatorAccountId</span>
}
else
{
<span>-</span>
}
</MudTd>
<MudTd DataLabel="最后同步">@context.SyncedAt.ToString("yyyy-MM-dd HH:mm")</MudTd>
<MudTd DataLabel="操作">
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(async () => await ShowDetails(context))">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await EditChannel(context))">编辑频道</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonRemove" OnClick="@(async () => await KickUser(context))">踢人</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd" OnClick="@(() => InviteUsers(context))">邀请用户</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(async () => await CopyLinkAsync(context))">复制链接</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(() => DeleteChannel(context.Id))">删除频道</MudMenuItem>
</MudMenu>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无频道数据</MudText>
</NoRecordsContent>
<LoadingContent>
<MudProgressCircular Indeterminate="true" />
</LoadingContent>
</MudTable>
</MudCardContent>
</MudCard>
@code {
private List<Channel> channels = new();
private List<Account> accounts = new();
private bool loading = true;
private string searchString = "";
private int filterAccount = 0;
private string filterType = "all";
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadChannels();
}
private async Task LoadAccounts()
{
try
{
var accountList = await AccountManagement.GetAllAccountsAsync();
accounts = accountList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
loading = true;
try
{
var channelList = await ChannelManagement.GetChannelsForViewAsync(filterAccount, includeNonCreator: false);
channels = channelList.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private bool FilterFunc(Channel channel)
{
if (filterType == "public" && string.IsNullOrEmpty(channel.Username))
return false;
if (filterType == "private" && !string.IsNullOrEmpty(channel.Username))
return false;
if (string.IsNullOrWhiteSpace(searchString))
return true;
if (channel.Title.Contains(searchString, StringComparison.OrdinalIgnoreCase))
return true;
if (channel.Username?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true)
return true;
return false;
}
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;
}
private async Task SyncCurrentAccountChannels()
{
if (loading)
return;
if (filterAccount <= 0)
{
Snackbar.Add("请先选择一个账号再同步", Severity.Info);
return;
}
loading = true;
try
{
var summary = await DataSync.SyncAccountAsync(filterAccount, CancellationToken.None);
foreach (var failure in summary.AccountFailures)
Snackbar.Add($"账号 {failure.Phone} 同步失败:{failure.Error}", Severity.Warning);
Snackbar.Add($"同步完成:{summary.TotalChannelsSynced} 个频道,{summary.TotalGroupsSynced} 个群组", summary.AccountFailures.Count == 0 ? Severity.Success : Severity.Warning);
await LoadChannels();
}
catch (Exception ex)
{
Snackbar.Add($"同步失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private async Task SyncAllAccountsChannels()
{
if (loading)
return;
loading = true;
try
{
var summary = await DataSync.SyncAllActiveAccountsAsync(CancellationToken.None);
foreach (var failure in summary.AccountFailures)
Snackbar.Add($"账号 {failure.Phone} 同步失败:{failure.Error}", Severity.Warning);
Snackbar.Add($"同步完成:{summary.TotalChannelsSynced} 个频道,{summary.TotalGroupsSynced} 个群组", summary.AccountFailures.Count == 0 ? Severity.Success : Severity.Warning);
await LoadChannels();
}
catch (Exception ex)
{
Snackbar.Add($"同步失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private async Task ShowDetails(Channel channel)
{
var parameters = new DialogParameters { ["ChannelDbId"] = channel.Id };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
DialogService.Show<ChannelDetailsDialog>("频道详情", parameters, options);
await Task.CompletedTask;
}
private async Task EditChannel(Channel channel)
{
var parameters = new DialogParameters { ["ChannelDbId"] = channel.Id };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<ChannelEditDialog>("编辑频道", parameters, options);
var result = await dialog.Result;
if (!result.Canceled)
await LoadChannels();
}
private async Task KickUser(Channel channel)
{
// 复用详情弹窗里的“踢人”能力,减少重复 UI
await ShowDetails(channel);
}
private void InviteUsers(Channel channel)
{
Navigation.NavigateTo($"/batch/invite?channelId={channel.Id}");
}
private async Task CopyLinkAsync(Channel channel)
{
try
{
var executeAccountId = channel.CreatorAccountId ?? filterAccount;
if (executeAccountId <= 0)
{
Snackbar.Add("无法导出链接:请先选择账号或确保频道有创建账号", Severity.Warning);
return;
}
var link = await ChannelService.ExportJoinLinkAsync(executeAccountId, channel.TelegramId);
await JS.InvokeVoidAsync("telegramPanel.copyText", link);
Snackbar.Add("已复制链接", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"复制失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteChannel(int id)
{
var channel = channels.FirstOrDefault(c => c.Id == id);
if (channel == null) return;
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除频道 {channel.Title} 吗?此操作不可恢复。",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await ChannelManagement.DeleteChannelAsync(id);
channels.Remove(channel);
Snackbar.Add("删除成功", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
}