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

1014 lines
39 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 "/accounts"
@inject AccountManagementService AccountManagement
@inject AccountCategoryManagementService CategoryManagement
@inject AccountTelegramToolsService AccountTelegramTools
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
@inject PanelTimeZoneService TimeZone
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
@using TelegramPanel.Web.Components.Dialogs
@implements IDisposable
<PageTitle>账号列表 - Telegram Panel</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4 tp-page-title">账号列表</MudText>
<MudPaper Class="pa-4 mb-4" Elevation="0" Outlined="true">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
<MudSelect @bind-Value="filterCategoryId" @bind-Value:after="OnFiltersChanged" Label="分类" Variant="Variant.Outlined" Dense="true"
Class="tp-input-full-xs"
Style="min-width: 180px; flex: 0 1 220px;">
<MudSelectItem Value="0">全部分类</MudSelectItem>
@foreach (var c in categories)
{
<MudSelectItem Value="@c.Id">@c.Name</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="searchString" 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" @bind-Value:after="OnSearchChanged" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
Disabled="@loading" OnClick="ReloadTable" Size="Size.Small" Class="tp-input-full-xs">
刷新列表
</MudButton>
</MudStack>
</MudPaper>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-2" Style="flex-wrap: wrap;">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@SelectionToggleIcon"
Disabled="@(loading || accounts.Count == 0)" OnClick="CycleSelectionToggle" Size="Size.Small">
@SelectionToggleText
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync"
Disabled="@loading" OnClick="BatchRefreshTelegramStatus" Size="Size.Small">
批量刷新状态
</MudButton>
<MudMenu Label="批量操作" Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.MoreHoriz"
Size="Size.Small" Disabled="@loading" Dense="true" EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchJoinSubscribe">批量加群/订阅(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchLeaveUnsubscribe">批量退群/退订(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchChangeTwoFactorPassword">修改二级密码(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="BatchKickAllOtherDevices">批量踢出所有其他设备(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchSetAccountCategory">批量修改分类(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchChangeNickname">批量改昵称(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchChangeUsername">批量改用户名(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="OpenBatchChangeBio">批量改Bio(已选)</MudMenuItem>
<MudMenuItem Disabled="@(selectedAccounts.Count == 0)" OnClick="DeleteSelectedAccounts">删除已选</MudMenuItem>
<MudMenuItem OnClick="ExportSelectedAccounts">导出已选</MudMenuItem>
<MudMenuItem OnClick="ExportCurrentPageAccounts">导出当前页</MudMenuItem>
</MudMenu>
@if (selectedAccounts.Count > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info" Class="ml-2">已选 @selectedAccounts.Count</MudChip>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="ml-2">共 @totalCount 个账号</MudText>
}
</MudStack>
<MudCard>
<MudCardContent>
<MudTable ServerData="LoadAccountsServerData" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading"
Class="tp-table"
MultiSelection="true" @bind-SelectedItems="selectedAccounts" @ref="_table">
<HeaderContent>
<MudTh>手机号</MudTh>
<MudTh>昵称</MudTh>
<MudTh>用户名</MudTh>
<MudTh>用户ID</MudTh>
<MudTh>分类</MudTh>
<MudTh>频道/群组</MudTh>
<MudTh>状态</MudTh>
<MudTh>Telegram 状态</MudTh>
<MudTh>最后数据同步</MudTh>
<MudTh Style="min-width: 180px;">操作</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="手机号">@context.DisplayPhone</MudTd>
<MudTd DataLabel="昵称">@(context.Nickname ?? "-")</MudTd>
<MudTd DataLabel="用户名">@(context.Username ?? "-")</MudTd>
<MudTd DataLabel="用户ID">@context.UserId</MudTd>
<MudTd DataLabel="分类">
@if (context.Category != null)
{
<MudChip T="string" Size="Size.Small" Style="@($"background-color: {context.Category.Color ?? "#9E9E9E"}")">
@context.Category.Name
</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">未分类</MudText>
}
</MudTd>
<MudTd DataLabel="频道/群组">
<MudText Typo="Typo.body2">@context.Channels.Count / @context.Groups.Count</MudText>
</MudTd>
<MudTd DataLabel="状态">
<MudChip T="string" Size="Size.Small" Color="@(context.IsActive ? Color.Success : Color.Default)">
@(context.IsActive ? "活跃" : "停用")
</MudChip>
</MudTd>
<MudTd DataLabel="Telegram 状态">
@if (telegramStatus.TryGetValue(context.Id, out var s))
{
<MudChip T="string" Size="Size.Small" Color="@(s.Ok ? Color.Success : Color.Error)" Title="@(BuildTelegramStatusTitle(s))">
@s.Summary
</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default">未检测</MudChip>
}
</MudTd>
<MudTd DataLabel="最后数据同步"><PanelTime Value="context.LastSyncAt" /></MudTd>
<MudTd DataLabel="操作">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
OnClick="@(() => ShowDetails(context))" Title="查看详情" />
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Info"
Disabled="@loading" OnClick="@(() => OpenEditUserProfile(context))" Title="编辑用户资料" />
<MudIconButton Icon="@Icons.Material.Filled.Sync" Size="Size.Small" Color="Color.Primary"
Disabled="@loading" OnClick="@(() => RefreshTelegramStatus(context))" Title="刷新 Telegram 状态" />
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small" Dense="true">
<MudMenuItem Icon="@Icons.Material.Filled.GroupAdd" Disabled="@loading" OnClick="@(() => OpenJoinSubscribe(context))">加群/订阅</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" Disabled="@loading" OnClick="@(() => OpenLeaveUnsubscribe(context))">退群/退订</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.MarkEmailUnread" Disabled="@loading" OnClick="@(() => OpenSystemInbox(context))">系统通知(验证码)</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Devices" Disabled="@loading" OnClick="@(() => OpenDevices(context))">在线设备</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Password" Disabled="@loading" OnClick="@(() => OpenChangeTwoFactorPassword(context))">修改二级密码</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.AlternateEmail" Disabled="@loading" OnClick="@(() => OpenChangeTwoFactorRecoveryEmail(context))">绑定/换绑找回邮箱</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@(context.IsActive ? Icons.Material.Filled.Stop : Icons.Material.Filled.PlayArrow)"
Disabled="@loading"
OnClick="@(() => ToggleActive(context))">
@(context.IsActive ? "停用" : "启用")
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" Disabled="@loading" OnClick="@(() => DeleteAccount(context))">删除账号</MudMenuItem>
</MudMenu>
</MudStack>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无账号数据,请先<MudLink Href="/accounts/import">导入账号</MudLink></MudText>
</NoRecordsContent>
<LoadingContent>
<MudProgressCircular Indeterminate="true" />
</LoadingContent>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 20, 50, 100, 200 }" />
</PagerContent>
</MudTable>
</MudCardContent>
</MudCard>
@code {
private MudTable<Account>? _table;
private List<Account> accounts = new();
private List<AccountCategory> categories = new();
private int totalCount;
private bool loading = true;
private string searchString = "";
private int filterCategoryId = 0;
private readonly Dictionary<int, TelegramAccountStatusResult> telegramStatus = new();
private HashSet<Account> selectedAccounts = new();
private readonly CancellationTokenSource _pageCts = new();
private CancellationTokenSource? _searchDebounceCts;
protected override async Task OnInitializedAsync()
{
await LoadCategories();
}
private async Task LoadCategories()
{
try
{
var list = await CategoryManagement.GetAllCategoriesAsync();
categories = list.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载分类失败:{ex.Message}", Severity.Error);
}
}
private async Task<TableData<Account>> LoadAccountsServerData(TableState state, CancellationToken cancellationToken)
{
loading = true;
try
{
var categoryId = filterCategoryId <= 0 ? (int?)null : filterCategoryId;
var (items, total) = await AccountManagement.QueryAccountsPagedAsync(
categoryId: categoryId,
search: searchString,
pageIndex: state.Page,
pageSize: state.PageSize,
cancellationToken: cancellationToken);
accounts = items.ToList();
totalCount = total;
// 从数据库字段恢复“Telegram 状态”缓存(仅需要当前页)
foreach (var a in accounts)
{
if (!a.TelegramStatusCheckedAtUtc.HasValue)
continue;
if (string.IsNullOrWhiteSpace(a.TelegramStatusSummary))
continue;
telegramStatus[a.Id] = new TelegramAccountStatusResult(
Ok: a.TelegramStatusOk ?? false,
Summary: a.TelegramStatusSummary,
Details: a.TelegramStatusDetails,
CheckedAtUtc: a.TelegramStatusCheckedAtUtc.Value);
}
// 翻页/筛选时清空选择(与 WordPress 列表行为一致,避免跨页误操作)
selectedAccounts.Clear();
return new TableData<Account> { Items = accounts, TotalItems = totalCount };
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
return new TableData<Account> { Items = Array.Empty<Account>(), TotalItems = 0 };
}
finally
{
loading = false;
}
}
private async Task ReloadTable()
{
selectedAccounts.Clear();
if (_table != null)
await _table.ReloadServerData();
}
private async Task OnFiltersChanged()
{
// 筛选改变时回到第一页更符合直觉,但 MudTable API 不同版本可能不同;这里仅触发刷新
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 ShowDetails(Account account)
{
var parameters = new DialogParameters
{
["AccountId"] = account.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.AccountDetailsDialog>("账号详情", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await ReloadTable();
}
private async Task OpenChangeTwoFactorPassword(Account account)
{
if (loading)
return;
var parameters = new DialogParameters
{
["AccountIds"] = new List<int> { account.Id }
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.ChangeTwoFactorPasswordDialog>("修改二级密码", parameters, options);
await Task.CompletedTask;
}
private async Task OpenChangeTwoFactorRecoveryEmail(Account account)
{
if (loading)
return;
var parameters = new DialogParameters
{
["AccountId"] = account.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.ChangeTwoFactorRecoveryEmailDialog>("绑定/换绑找回邮箱", parameters, options);
await Task.CompletedTask;
}
private async Task OpenEditUserProfile(Account account)
{
if (loading)
return;
var parameters = new DialogParameters
{
["AccountId"] = account.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.EditUserProfileDialog>("编辑用户资料", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await ReloadTable();
}
private async Task OpenBatchChangeTwoFactorPassword()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.ChangeTwoFactorPasswordDialog>("批量修改二级密码", parameters, options);
await Task.CompletedTask;
}
private async Task OpenBatchSetAccountCategory()
{
if (loading)
return;
var targets = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (targets.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
if (categories.Count == 0)
await LoadCategories();
var parameters = new DialogParameters
{
["Categories"] = categories,
["SelectedCount"] = targets.Count
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchSetAccountCategoryDialog>("批量修改分类", parameters, options)!;
var result = await dialog.Result;
if (result is not { Canceled: false })
return;
var categoryIdRaw = result.Data is int x ? x : 0;
int? categoryId = categoryIdRaw <= 0 ? null : categoryIdRaw;
loading = true;
try
{
foreach (var id in targets)
{
await AccountManagement.UpdateAccountCategoryAsync(id, categoryId);
}
selectedAccounts.Clear();
await ReloadTable();
Snackbar.Add($"分类已更新:{targets.Count} 个账号", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"修改分类失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private async Task OpenBatchChangeNickname()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChangeNicknameDialog>("批量修改昵称", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await ReloadTable();
}
private async Task OpenBatchChangeUsername()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChangeUsernameDialog>("批量修改用户名", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await ReloadTable();
}
private async Task OpenBatchChangeBio()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChangeBioDialog>("批量修改 Bio", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await ReloadTable();
}
private async Task OpenBatchJoinSubscribe()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids,
["Operation"] = "join"
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChatMembershipDialog>("批量加群/订阅", parameters, options);
await Task.CompletedTask;
}
private async Task OpenBatchLeaveUnsubscribe()
{
if (loading)
return;
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
var parameters = new DialogParameters
{
["AccountIds"] = ids,
["Operation"] = "leave"
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChatMembershipDialog>("批量退群/退订", parameters, options);
await Task.CompletedTask;
}
private async Task OpenJoinSubscribe(Account account)
{
if (loading)
return;
var parameters = new DialogParameters
{
["AccountIds"] = new List<int> { account.Id },
["Operation"] = "join"
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChatMembershipDialog>("加群/订阅", parameters, options);
await Task.CompletedTask;
}
private async Task OpenLeaveUnsubscribe(Account account)
{
if (loading)
return;
var parameters = new DialogParameters
{
["AccountIds"] = new List<int> { account.Id },
["Operation"] = "leave"
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChatMembershipDialog>("退群/退订", parameters, options);
await Task.CompletedTask;
}
private string BuildTelegramStatusTitle(TelegramAccountStatusResult s)
{
var details = string.IsNullOrWhiteSpace(s.Details) ? s.Summary : s.Details;
var checkedAt = TimeZone.Format(s.CheckedAtUtc, "yyyy-MM-dd HH:mm");
return $"{details}(检测时间:{checkedAt})";
}
private async Task RefreshTelegramStatus(Account account)
{
try
{
loading = true;
bool? probeResult = await DialogService.ShowMessageBox(
"刷新 Telegram 状态",
"是否进行深度探测(将创建并删除一个测试频道,用于判断【创建频道接口是否被冻结】)?",
yesText: "深度探测",
cancelText: "普通刷新");
var probe = probeResult == true;
// 风控检查:深度探测是敏感操作
if (probe)
{
var riskCheck = RiskService.CheckLoginDuration(account);
if (riskCheck.IsRisky)
{
var parameters = new DialogParameters
{
["Message"] = riskCheck.Message,
["DetailedMessage"] = riskCheck.DetailedMessage,
["ShowRecommendations"] = true
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium };
var dialog = DialogService.Show<RiskWarningDialog>("风控警告", parameters, options);
var dialogResult = await dialog.Result;
if (dialogResult.Canceled)
{
loading = false;
return; // 用户取消操作
}
}
}
var result = await AccountTelegramTools.RefreshAccountStatusAsync(account.Id, probeCreateChannel: probe, cancellationToken: _pageCts.Token);
telegramStatus[account.Id] = result;
// 资料刷新后重新加载(昵称/用户名/用户ID 可能更新)
await ReloadTable();
Snackbar.Add($"账号 {account.DisplayPhone} 状态:{result.Summary}", result.Ok ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
{
Snackbar.Add($"刷新失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private async Task BatchRefreshTelegramStatus()
{
if (accounts.Count == 0)
{
Snackbar.Add("暂无账号可刷新", Severity.Info);
return;
}
loading = true;
try
{
bool? probeResult = await DialogService.ShowMessageBox(
"批量刷新 Telegram 状态",
"是否进行深度探测(将对每个账号创建并删除一个测试频道,用于判断【创建频道接口是否被冻结】)?",
yesText: "深度探测",
cancelText: "普通刷新");
var probe = probeResult == true;
var categoryId = filterCategoryId <= 0 ? (int?)null : filterCategoryId;
var targets = (await AccountManagement.QueryAccountsAsync(categoryId, searchString, _pageCts.Token)).ToList();
// 批量风控检查:深度探测是敏感操作
if (probe && targets.Count > 0)
{
var batchCheck = RiskService.CheckBatchAccounts(targets);
if (batchCheck.HasRiskyAccounts)
{
var parameters = new DialogParameters
{
["Message"] = $"检测到 {batchCheck.RiskyCount} 个风险账号",
["DetailedMessage"] = batchCheck.GetRiskySummary(),
["ShowRecommendations"] = true,
["ShowExcludeOption"] = true,
["RiskyAccounts"] = batchCheck.RiskyAccounts.ToList()
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large };
var dialog = DialogService.Show<RiskWarningDialog>("批量操作风控警告", parameters, options);
var dialogResult = await dialog.Result;
if (dialogResult.Canceled)
{
loading = false;
return; // 用户取消操作
}
if (dialogResult.Data is TelegramPanel.Core.Services.RiskWarningAction action
&& action == TelegramPanel.Core.Services.RiskWarningAction.ExcludeRisky)
{
// 排除风险账号后继续
targets = batchCheck.SafeAccounts.ToList();
if (targets.Count == 0)
{
Snackbar.Add("排除风险账号后无剩余账号", Severity.Info);
loading = false;
return;
}
}
// else: 用户选择继续所有账号
}
}
foreach (var a in targets)
{
try
{
var result = await AccountTelegramTools.RefreshAccountStatusAsync(a.Id, probeCreateChannel: probe, cancellationToken: _pageCts.Token);
telegramStatus[a.Id] = result;
}
catch (Exception ex)
{
telegramStatus[a.Id] = new TelegramAccountStatusResult(
Ok: false,
Summary: "刷新失败",
Details: ex.Message,
CheckedAtUtc: DateTime.UtcNow);
}
StateHasChanged();
}
await ReloadTable();
Snackbar.Add($"批量刷新完成:{targets.Count} 个账号", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"批量刷新失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
public void Dispose()
{
try
{
_pageCts.Cancel();
}
catch
{
// ignore
}
finally
{
_pageCts.Dispose();
}
}
private Task SelectAllFiltered()
{
selectedAccounts = accounts.ToHashSet();
return Task.CompletedTask;
}
private Task InvertSelection()
{
var targets = accounts.ToList();
foreach (var a in targets)
{
if (selectedAccounts.Contains(a))
selectedAccounts.Remove(a);
else
selectedAccounts.Add(a);
}
return Task.CompletedTask;
}
private Task ClearSelection()
{
selectedAccounts.Clear();
return Task.CompletedTask;
}
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
};
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
}
private async Task DeleteSelectedAccounts()
{
var targets = selectedAccounts.ToList();
if (targets.Count == 0)
{
Snackbar.Add("未选择任何账号", Severity.Info);
return;
}
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除已选账号({targets.Count} 个)吗?将同时清理 sessions 文件,且不可恢复。若部分账号删除失败,将继续删除剩余账号并汇总失败原因。",
yesText: "删除", cancelText: "取消");
if (result != true)
return;
loading = true;
try
{
var success = 0;
var failures = new List<string>();
foreach (var a in targets)
{
try
{
await AccountManagement.DeleteAccountAsync(a.Id);
success++;
}
catch (Exception ex)
{
failures.Add($"{a.Phone}:{ex.Message}");
}
}
selectedAccounts.Clear();
await ReloadTable();
if (failures.Count == 0)
{
Snackbar.Add($"删除完成:成功 {success}/{targets.Count}", Severity.Success);
return;
}
Snackbar.Add($"删除完成:成功 {success}/{targets.Count},失败 {failures.Count}", Severity.Warning);
// 避免 Snackbar 过长,失败原因集中弹窗展示
var details = string.Join(Environment.NewLine, failures.Take(50));
if (failures.Count > 50)
details += Environment.NewLine + $"... 仅展示前 50 条(共 {failures.Count} 条)";
await DialogService.ShowMessageBox(
"删除失败明细",
details,
yesText: "知道了");
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private void ExportSelectedAccounts()
{
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("请先勾选要导出的账号", Severity.Info);
return;
}
var qs = string.Join(",", ids);
Navigation.NavigateTo($"/downloads/accounts.zip?ids={qs}", forceLoad: true);
}
private void ExportCurrentPageAccounts()
{
var ids = accounts.Select(a => a.Id).Distinct().ToList();
if (ids.Count == 0)
{
Snackbar.Add("当前页没有账号可导出", Severity.Info);
return;
}
var qs = string.Join(",", ids);
Navigation.NavigateTo($"/downloads/accounts.zip?ids={qs}", forceLoad: true);
}
private async Task OpenSystemInbox(Account account)
{
var parameters = new DialogParameters
{
["AccountId"] = account.Id,
["Phone"] = account.DisplayPhone
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.SystemInboxDialog>("系统通知(验证码)", parameters, options);
await Task.CompletedTask;
}
private async Task OpenDevices(Account account)
{
var parameters = new DialogParameters
{
["AccountId"] = account.Id,
["Phone"] = account.DisplayPhone
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
DialogService.Show<TelegramPanel.Web.Components.Dialogs.OnlineDevicesDialog>("在线设备", parameters, options);
await Task.CompletedTask;
}
private async Task ToggleActive(Account account)
{
try
{
var newIsActive = !account.IsActive;
await AccountManagement.SetAccountActiveStatusAsync(account.Id, newIsActive);
// 注意:Account 实体可能被同一个 DbContext 跟踪,SetAccountActiveStatusAsync 内部已更新该实例。
// 这里用 newIsActive 赋值/提示,避免出现“又反转一次”的提示错误。
account.IsActive = newIsActive;
Snackbar.Add($"账号已{(newIsActive ? "启用" : "停用")}", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"操作失败:{ex.Message}", Severity.Error);
}
}
private async Task DeleteAccount(Account account)
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除账号 {account.DisplayPhone} 吗?此操作不可撤销!",
yesText: "删除", cancelText: "取消");
if (result == true)
{
try
{
await AccountManagement.DeleteAccountAsync(account.Id);
await ReloadTable();
Snackbar.Add("账号已删除", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}
}
private async Task BatchKickAllOtherDevices()
{
if (loading)
return;
var targets = selectedAccounts.ToList();
if (targets.Count == 0)
{
Snackbar.Add("请先选择账号", Severity.Info);
return;
}
bool? result = await DialogService.ShowMessageBox(
"确认踢出",
$"将对 {targets.Count} 个账号执行【踢出所有其他设备】(会保留面板当前会话)。是否继续?",
yesText: "继续", cancelText: "取消");
if (result != true)
return;
loading = true;
try
{
var success = 0;
var failures = new List<string>();
foreach (var a in targets)
{
try
{
var ok = await AccountTelegramTools.KickAllOtherAuthorizationsAsync(a.Id);
if (ok) success++;
else failures.Add($"{a.DisplayPhone}:返回 false");
}
catch (Exception ex)
{
failures.Add($"{a.DisplayPhone}:{ex.Message}");
}
}
selectedAccounts.Clear();
if (failures.Count == 0)
{
Snackbar.Add($"踢出完成:成功 {success}/{targets.Count}", Severity.Success);
return;
}
Snackbar.Add($"踢出完成:成功 {success}/{targets.Count},失败 {failures.Count}", Severity.Warning);
var details = string.Join(Environment.NewLine, failures.Take(80));
if (failures.Count > 80)
details += Environment.NewLine + $"... 仅展示前 80 条(共 {failures.Count} 条)";
await DialogService.ShowMessageBox("失败明细(前 80 条)", details, "关闭");
}
finally
{
loading = false;
}
}
}