Files
Telegram-Panel/src/TelegramPanel.Web/Components/Dialogs/CreateBatchTaskDialog.razor
T

325 lines
14 KiB
Plaintext

@namespace TelegramPanel.Web.Components.Dialogs
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using System.Text.Json
@using TelegramPanel.Core.BatchTasks
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
任务会提交到后台执行,可在【任务中心】查看进度与结果。
</MudAlert>
<MudGrid>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="selectedCategory" Label="任务分类" Variant="Variant.Outlined" Dense="true">
<MudSelectItem Value='@("user")'>用户任务</MudSelectItem>
<MudSelectItem Value='@("bot")'>Bot 任务</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="selectedTaskType" Label="任务类型" Variant="Variant.Outlined" Dense="true">
@foreach (var opt in AvailableOptions)
{
<MudSelectItem Value="@opt.TaskType">@opt.DisplayName</MudSelectItem>
}
</MudSelect>
</MudItem>
</MudGrid>
@if (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.Description))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">@CurrentOption.Description</MudText>
}
@if (selectedTaskType == BatchTaskTypes.UserJoinSubscribe)
{
<MudDivider Class="my-2" />
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
批量订阅频道/加群(用户任务):支持输入多个链接/用户名,换行分隔。支持:<b>https://t.me/xxx</b>、<b>t.me/+hash</b>、<b>@username</b>、<b>username</b>、<b>tg://join?invite=hash</b>。
</MudAlert>
<MudTextField @bind-Value="linksText" Label="频道/群链接列表(换行分隔)" Variant="Variant.Outlined" Lines="8"
HelperText="每行一个链接或用户名;将对所选账号逐个执行订阅/加入。" />
<MudNumericField @bind-Value="delayMs" Label="操作间隔(毫秒)" Variant="Variant.Outlined" Min="0" Max="10000"
HelperText="建议设置 1500-4000ms,避免触发风控(会额外加少量随机抖动)。" />
<MudStack direction="Row" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.subtitle2">选择执行账号:@selectedAccounts.Count / @accounts.Count</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="SelectAll" Disabled="@(accounts.Count == 0)">全选</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary" OnClick="ClearSelection" Disabled="@(selectedAccounts.Count == 0)">清空</MudButton>
</MudStack>
<MudTextField @bind-Value="accountSearch" Placeholder="搜索账号(手机号/昵称/用户名)" Variant="Variant.Outlined"
Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" Immediate="true" />
<MudTable Items="@FilteredAccounts" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm"
MultiSelection="true" @bind-SelectedItems="selectedAccounts" Loading="@loadingAccounts">
<HeaderContent>
<MudTh>手机号</MudTh>
<MudTh>昵称</MudTh>
<MudTh>用户名</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="手机号">@context.Phone</MudTd>
<MudTd DataLabel="昵称">@(string.IsNullOrWhiteSpace(context.Nickname) ? "-" : context.Nickname)</MudTd>
<MudTd DataLabel="用户名">@(string.IsNullOrWhiteSpace(context.Username) ? "-" : "@" + context.Username)</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无账号</MudText>
</NoRecordsContent>
</MudTable>
}
else if (selectedTaskType == BatchTaskTypes.Invite)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
当前版本暂未在任务中心内置“批量邀请”的创建表单,请使用“批量邀请”页面创建任务。
</MudAlert>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => NavigateTo("/batch/invite"))">
前往批量邀请
</MudButton>
}
else if (selectedTaskType == BatchTaskTypes.SetAdmin)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
当前版本暂未在任务中心内置“设置管理员”的创建表单,请使用“批量设置管理员”页面创建任务。
</MudAlert>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => NavigateTo("/batch/admins"))">
前往批量设置管理员
</MudButton>
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel" Disabled="@creating">关闭</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Create" Disabled="@creating">
@if (creating)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>提交中...</span>
}
else
{
<span>提交任务</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
private sealed record TaskOption(string Category, string TaskType, string DisplayName, string? Description);
private static readonly List<TaskOption> Options = new()
{
new("bot", BatchTaskTypes.Invite, "批量邀请", "对所选频道批量邀请用户名列表(Bot/执行账号需具备权限)。"),
new("bot", BatchTaskTypes.SetAdmin, "设置管理员", "对所选频道批量设置管理员(Bot/执行账号需具备权限)。"),
new("user", BatchTaskTypes.UserJoinSubscribe, "批量订阅/加群", "用户账号批量加入群组/订阅频道(Bot 无法主动加入)。")
};
private string _selectedCategory = "user";
private string selectedCategory
{
get => _selectedCategory;
set
{
if (string.Equals(_selectedCategory, value, StringComparison.Ordinal))
return;
_selectedCategory = value;
EnsureTaskTypeValid();
}
}
private string selectedTaskType = BatchTaskTypes.UserJoinSubscribe;
private bool creating;
// join/subscribe config
private string linksText = "";
private int delayMs = 2000;
private bool loadingAccounts;
private List<Account> accounts = new();
private string accountSearch = "";
private HashSet<Account> selectedAccounts = new();
private IEnumerable<TaskOption> AvailableOptions => Options.Where(o => o.Category == selectedCategory);
private TaskOption? CurrentOption => Options.FirstOrDefault(o => o.TaskType == selectedTaskType);
private IEnumerable<Account> FilteredAccounts =>
accounts.Where(a =>
{
if (string.IsNullOrWhiteSpace(accountSearch))
return true;
var q = accountSearch.Trim();
return a.Phone.Contains(q, StringComparison.OrdinalIgnoreCase)
|| (a.Nickname?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false)
|| (a.Username?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false);
});
protected override async Task OnInitializedAsync()
{
EnsureTaskTypeValid();
await LoadAccounts();
}
private async Task LoadAccounts()
{
loadingAccounts = true;
try
{
var list = await AccountManagement.GetActiveAccountsAsync();
accounts = list.OrderBy(a => a.Id).ToList();
selectedAccounts = selectedAccounts.Where(a => accounts.Any(x => x.Id == a.Id)).ToHashSet();
}
catch (Exception ex)
{
Snackbar.Add($"加载账号失败:{ex.Message}", Severity.Error);
}
finally
{
loadingAccounts = false;
}
}
private void EnsureTaskTypeValid()
{
if (AvailableOptions.All(o => o.TaskType != selectedTaskType))
{
selectedTaskType = AvailableOptions.FirstOrDefault()?.TaskType
?? Options.First().TaskType;
}
}
private void SelectAll() => selectedAccounts = accounts.ToHashSet();
private void ClearSelection() => selectedAccounts = new HashSet<Account>();
private async Task Create()
{
if (creating)
return;
EnsureTaskTypeValid();
if (selectedTaskType != BatchTaskTypes.UserJoinSubscribe)
{
Snackbar.Add("当前任务类型暂不支持从任务中心直接创建", Severity.Info);
return;
}
var links = ParseLines(linksText);
if (links.Count == 0)
{
Snackbar.Add("请至少输入一个频道/群链接", Severity.Warning);
return;
}
var selected = selectedAccounts.ToList();
if (selected.Count == 0)
{
Snackbar.Add("请至少选择一个执行账号", Severity.Warning);
return;
}
// 批量风控检查:加入群/订阅频道是敏感操作
var batchCheck = RiskService.CheckBatchAccounts(selected);
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<TelegramPanel.Web.Components.Dialogs.RiskWarningDialog>("批量操作风控警告", parameters, options);
var result = await dialog.Result;
if (result.Canceled)
return;
if (result.Data is TelegramPanel.Core.Services.RiskWarningAction action && action == TelegramPanel.Core.Services.RiskWarningAction.ExcludeRisky)
{
selected = batchCheck.SafeAccounts.ToList();
if (selected.Count == 0)
{
Snackbar.Add("排除风险账号后无剩余账号", Severity.Info);
return;
}
}
}
bool? confirm = await DialogService.ShowMessageBox(
"确认提交任务",
$"将对 {selected.Count} 个账号执行 {links.Count} 个链接的订阅/加群操作,总计 {selected.Count * links.Count} 次。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
creating = true;
try
{
var ids = selected.Select(a => a.Id).Distinct().ToList();
var task = new BatchTask
{
TaskType = BatchTaskTypes.UserJoinSubscribe,
Total = ids.Count * links.Count,
Completed = 0,
Failed = 0,
Config = JsonSerializer.Serialize(new
{
AccountIds = ids,
Links = links,
DelayMs = delayMs
})
};
var created = await TaskManagement.CreateTaskAsync(task);
Snackbar.Add($"已提交后台任务 #{created.Id}(批量订阅/加群)。请到【任务中心】查看进度。", Severity.Success);
MudDialog.Close(DialogResult.Ok(created.Id));
}
catch (Exception ex)
{
Snackbar.Add($"提交失败:{ex.Message}", Severity.Error);
}
finally
{
creating = false;
}
}
private void NavigateTo(string url)
{
MudDialog.Close();
Navigation.NavigateTo(url);
}
private static List<string> ParseLines(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return new List<string>();
return text
.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => (x ?? string.Empty).Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void Cancel() => MudDialog.Close();
}