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

395 lines
15 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.
@namespace TelegramPanel.Web.Components.Dialogs
@inject BatchTaskManagementService TaskManagement
@inject TelegramPanel.Web.Modules.ModuleContributionRegistry Contributions
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@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">
@foreach (var c in AvailableCategories)
{
<MudSelectItem Value="@c">@GetCategoryDisplayName(c)</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 (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.CreateRoute))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
该任务建议使用配置窗口创建/编辑:<b>@CurrentOption.CreateRoute</b>
</MudAlert>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => OpenCreateDialog(CurrentOption.CreateRoute!))">
打开窗口
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="@(() => NavigateTo(CurrentOption.CreateRoute!))">
前往页面
</MudButton>
</MudStack>
}
else if (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.EditorComponentType) && editorComponentType != null)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
该任务由模块提供自定义创建器。
</MudAlert>
<DynamicComponent Type="editorComponentType" Parameters="editorParameters" />
@if (!string.IsNullOrWhiteSpace(editorDraft.ValidationError))
{
<MudText Typo="Typo.caption" Color="Color.Error">@editorDraft.ValidationError</MudText>
}
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
该任务类型暂无专用创建表单。你可以填写通用配置(JSON 字符串)提交任务;对应模块需要在后台实现该 TaskType 的执行器。
</MudAlert>
<MudTextField @bind-Value="genericConfigJson" Label="任务配置(JSON)" Variant="Variant.Outlined" Lines="8"
HelperText="模块可自行解析该 JSON。若无需配置可留空。" />
<MudNumericField @bind-Value="genericTotal" Label="总数(Total,可选)" Variant="Variant.Outlined" Min="0" Max="1000000"
HelperText="用于进度条展示;若不确定可填 0,由执行器在运行时持续更新进度。" />
}
</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 string _selectedCategory = "";
private string selectedCategory
{
get => _selectedCategory;
set
{
value = (value ?? "").Trim();
if (string.Equals(_selectedCategory, value, StringComparison.OrdinalIgnoreCase))
return;
_selectedCategory = value;
EnsureTaskTypeValid();
}
}
private string _selectedTaskType = "";
private string selectedTaskType
{
get => _selectedTaskType;
set
{
value = (value ?? "").Trim();
if (string.Equals(_selectedTaskType, value, StringComparison.OrdinalIgnoreCase))
return;
_selectedTaskType = value;
ResolveEditorComponent();
}
}
private bool creating;
// generic task config
private string genericConfigJson = "";
private int genericTotal = 0;
// module editor draft
private TelegramPanel.Modules.ModuleTaskDraft editorDraft = new(0, null, false, null);
private Type? editorComponentType;
private Dictionary<string, object?> editorParameters = new();
private IEnumerable<string> AvailableCategories =>
Contributions.Tasks
.Select(x => (x.Definition.Category ?? "").Trim())
.Where(x => x.Length > 0)
.Where(x => !IsSystemCategory(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase);
private IEnumerable<TelegramPanel.Modules.ModuleTaskDefinition> AvailableOptions =>
Contributions.Tasks
.Select(x => x.Definition)
.Where(o => !IsSystemCategory(o.Category))
.Where(o => string.Equals(o.Category, selectedCategory, StringComparison.OrdinalIgnoreCase))
.OrderBy(o => o.Order)
.ThenBy(o => o.DisplayName);
private TelegramPanel.Modules.ModuleTaskDefinition? CurrentOption =>
!string.IsNullOrWhiteSpace(selectedTaskType)
&& Contributions.TaskTypeToDefinition.TryGetValue(selectedTaskType, out var reg)
? reg.Definition
: null;
protected override async Task OnInitializedAsync()
{
if (string.IsNullOrWhiteSpace(selectedCategory))
selectedCategory = AvailableCategories.FirstOrDefault() ?? "user";
EnsureTaskTypeValid();
await Task.CompletedTask;
}
private void EnsureTaskTypeValid()
{
if (AvailableOptions.All(o => !string.Equals(o.TaskType, selectedTaskType, StringComparison.OrdinalIgnoreCase)))
{
selectedTaskType = AvailableOptions.FirstOrDefault()?.TaskType
?? Contributions.Tasks.FirstOrDefault()?.Definition.TaskType
?? "";
}
}
private static string GetCategoryDisplayName(string category) => category switch
{
"user" => "用户任务",
"bot" => "Bot 任务",
"system" => "系统任务",
_ => string.IsNullOrWhiteSpace(category) ? "-" : category
};
private static bool IsSystemCategory(string? category) =>
string.Equals((category ?? "").Trim(), "system", StringComparison.OrdinalIgnoreCase);
private void ResolveEditorComponent()
{
editorComponentType = null;
editorParameters = new Dictionary<string, object?>();
editorDraft = new TelegramPanel.Modules.ModuleTaskDraft(0, null, false, null);
var opt = CurrentOption;
if (opt == null || string.IsNullOrWhiteSpace(opt.EditorComponentType))
return;
if (!Contributions.TaskTypeToDefinition.TryGetValue(opt.TaskType, out var reg))
return;
editorComponentType = ModuleTaskEditorComponentResolver.ResolveCreateEditor(reg);
if (editorComponentType == null)
return;
editorParameters = new Dictionary<string, object?>
{
["Draft"] = editorDraft,
["DraftChanged"] = EventCallback.Factory.Create<TelegramPanel.Modules.ModuleTaskDraft>(this, OnEditorDraftChanged)
};
}
private void OnEditorDraftChanged(TelegramPanel.Modules.ModuleTaskDraft draft)
{
editorDraft = draft;
}
private async Task Create()
{
if (creating)
return;
if (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.CreateRoute))
{
Snackbar.Add("该任务建议使用独立页面创建,请点击“前往创建”。", Severity.Info);
return;
}
if (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.EditorComponentType) && editorComponentType != null)
{
if (!editorDraft.CanSubmit)
{
Snackbar.Add(string.IsNullOrWhiteSpace(editorDraft.ValidationError) ? "请先完善任务配置" : editorDraft.ValidationError!, Severity.Warning);
return;
}
var config = string.IsNullOrWhiteSpace(editorDraft.Config) ? null : editorDraft.Config.Trim();
if (!string.IsNullOrWhiteSpace(config))
{
try
{
JsonDocument.Parse(config);
}
catch (Exception ex)
{
Snackbar.Add($"配置 JSON 无效:{ex.Message}", Severity.Warning);
return;
}
}
var total = editorDraft.Total < 0 ? 0 : editorDraft.Total;
bool? confirm = await DialogService.ShowMessageBox(
"确认提交任务",
$"将提交任务类型:{selectedTaskType},总数:{total}。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
creating = true;
try
{
var task = new BatchTask
{
TaskType = selectedTaskType,
Total = total,
Completed = 0,
Failed = 0,
Config = config
};
var created = await TaskManagement.CreateTaskAsync(task);
Snackbar.Add($"已提交后台任务 #{created.Id}({selectedTaskType})。请到【任务中心】查看进度。", Severity.Success);
MudDialog.Close(DialogResult.Ok(created.Id));
}
catch (Exception ex)
{
Snackbar.Add($"提交失败:{ex.Message}", Severity.Error);
}
finally
{
creating = false;
}
return;
}
// Generic task create
if (!string.IsNullOrWhiteSpace(genericConfigJson))
{
try
{
JsonDocument.Parse(genericConfigJson);
}
catch (Exception ex)
{
Snackbar.Add($"配置 JSON 无效:{ex.Message}", Severity.Warning);
return;
}
}
bool? genericConfirm = await DialogService.ShowMessageBox(
"确认提交任务",
$"将提交任务类型:{selectedTaskType}。是否继续?",
yesText: "继续", cancelText: "取消");
if (genericConfirm != true)
return;
creating = true;
try
{
var task = new BatchTask
{
TaskType = selectedTaskType,
Total = genericTotal,
Completed = 0,
Failed = 0,
Config = string.IsNullOrWhiteSpace(genericConfigJson) ? null : genericConfigJson.Trim()
};
var created = await TaskManagement.CreateTaskAsync(task);
Snackbar.Add($"已提交后台任务 #{created.Id}({selectedTaskType})。请到【任务中心】查看进度。", 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 async Task OpenCreateDialog(string route)
{
route = (route ?? "").Trim();
if (route.Length == 0)
return;
// 仅对模块扩展页面提供“窗口打开”:/ext/{ModuleId}/{PageKey}
if (!TryParseExtRoute(route, out var moduleId, out var pageKey))
{
Snackbar.Add("该任务未提供可窗口打开的模块页面,请使用“前往页面”。", Severity.Info);
return;
}
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.ExtraLarge, FullWidth = true };
var parameters = new DialogParameters
{
["ModuleId"] = moduleId,
["PageKey"] = pageKey
};
DialogService.Show<TelegramPanel.Web.Components.Dialogs.ModulePageDialog>("任务配置", parameters, options);
await Task.CompletedTask;
}
private static bool TryParseExtRoute(string route, out string moduleId, out string pageKey)
{
moduleId = "";
pageKey = "";
route = (route ?? "").Trim();
if (!route.StartsWith("/ext/", StringComparison.OrdinalIgnoreCase))
return false;
// /ext/{moduleId}/{pageKey}
var parts = route.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length < 3)
return false;
if (!string.Equals(parts[0], "ext", StringComparison.OrdinalIgnoreCase))
return false;
moduleId = parts[1];
pageKey = parts[2];
return moduleId.Length > 0 && pageKey.Length > 0;
}
private void Cancel() => MudDialog.Close();
}