325 lines
14 KiB
Plaintext
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();
|
|
}
|