341 lines
12 KiB
Plaintext
341 lines
12 KiB
Plaintext
@page "/tasks"
|
|
@inject BatchTaskManagementService TaskManagement
|
|
@inject TelegramPanel.Web.Modules.ModuleContributionRegistry Contributions
|
|
@inject PanelTimeZoneService TimeZone
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
@using TelegramPanel.Data.Entities
|
|
@using TelegramPanel.Core.BatchTasks
|
|
@implements IDisposable
|
|
|
|
<PageTitle>任务中心 - Telegram Panel</PageTitle>
|
|
|
|
<MudText Typo="Typo.h4" Class="mb-4">任务中心</MudText>
|
|
|
|
<MudCard>
|
|
<MudCardContent>
|
|
<MudTable Items="@filteredTasks" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@loading">
|
|
<ToolBarContent>
|
|
<MudText Typo="Typo.h6">任务列表 (@tasks.Count)</MudText>
|
|
<MudSpacer />
|
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Color="Color.Default" Size="Size.Small"
|
|
Disabled="@loading" OnClick="@(async (MouseEventArgs _) => await LoadTasks())" />
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AddTask"
|
|
Disabled="@loading" OnClick="OpenCreateTaskDialog" Class="ml-2">
|
|
新建任务
|
|
</MudButton>
|
|
<MudSelect @bind-Value="filterCategory" Label="任务分类" Variant="Variant.Outlined" Dense="true" Class="ml-2" Style="min-width: 140px;">
|
|
<MudSelectItem Value='@("all")'>全部</MudSelectItem>
|
|
@foreach (var c in AvailableCategories)
|
|
{
|
|
<MudSelectItem Value="@c">@GetCategoryDisplayName(c)</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
<MudSelect @bind-Value="filterStatus" Label="状态筛选" Variant="Variant.Outlined" Dense="true" Style="min-width: 120px;">
|
|
<MudSelectItem Value='@("all")'>全部</MudSelectItem>
|
|
<MudSelectItem Value='@("pending")'>待执行</MudSelectItem>
|
|
<MudSelectItem Value='@("running")'>执行中</MudSelectItem>
|
|
<MudSelectItem Value='@("completed")'>已完成</MudSelectItem>
|
|
<MudSelectItem Value='@("failed")'>失败</MudSelectItem>
|
|
</MudSelect>
|
|
</ToolBarContent>
|
|
<HeaderContent>
|
|
<MudTh>任务ID</MudTh>
|
|
<MudTh>归属</MudTh>
|
|
<MudTh>任务类型</MudTh>
|
|
<MudTh>状态</MudTh>
|
|
<MudTh>进度</MudTh>
|
|
<MudTh>创建时间</MudTh>
|
|
<MudTh>完成时间</MudTh>
|
|
<MudTh>操作</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="任务ID">@context.Id</MudTd>
|
|
<MudTd DataLabel="归属">
|
|
<MudChip T="string" Size="Size.Small" Color="@GetCategoryColor(context.TaskType)">@GetCategoryName(context.TaskType)</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="任务类型">
|
|
<MudChip T="string" Size="Size.Small" Icon="@GetTaskIcon(context.TaskType)">@GetTaskTypeName(context.TaskType)</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="状态">
|
|
<MudChip T="string" Size="Size.Small" Color="@GetStatusColor(context.Status)">@GetStatusName(context.Status)</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="进度">
|
|
<MudProgressLinear Value="@GetProgress(context)" Color="@GetProgressColor(context.Status)" />
|
|
<MudText Typo="Typo.caption">@context.Completed / @context.Total (@context.Failed 失败)</MudText>
|
|
</MudTd>
|
|
<MudTd DataLabel="创建时间"><PanelTime Value="context.CreatedAt" /></MudTd>
|
|
<MudTd DataLabel="完成时间">
|
|
@if (context.CompletedAt.HasValue)
|
|
{
|
|
<PanelTime Value="context.CompletedAt.Value" />
|
|
}
|
|
else
|
|
{
|
|
<span>-</span>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="操作">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
|
|
OnClick="@(() => ShowTaskDetails(context))" />
|
|
@if (context.Status == "pending" || context.Status == "running")
|
|
{
|
|
<MudIconButton Icon="@Icons.Material.Filled.Cancel" Size="Size.Small" Color="Color.Warning"
|
|
OnClick="@(() => CancelTask(context.Id))" />
|
|
}
|
|
@if (context.Status == "completed" || context.Status == "failed")
|
|
{
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
|
OnClick="@(() => DeleteTask(context.Id))" />
|
|
}
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<NoRecordsContent>
|
|
<MudText>暂无任务</MudText>
|
|
</NoRecordsContent>
|
|
</MudTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
@code {
|
|
private List<BatchTask> tasks = new();
|
|
private bool loading = true;
|
|
private string filterStatus = "all";
|
|
private string filterCategory = "all";
|
|
|
|
private PeriodicTimer? refreshTimer;
|
|
private CancellationTokenSource? refreshCts;
|
|
|
|
private IEnumerable<BatchTask> filteredTasks =>
|
|
tasks.Where(t =>
|
|
(filterStatus == "all" || t.Status == filterStatus) &&
|
|
(filterCategory == "all" || GetCategory(t.TaskType) == filterCategory));
|
|
|
|
private IEnumerable<string> AvailableCategories =>
|
|
Contributions.Tasks
|
|
.Select(x => (x.Definition.Category ?? "").Trim())
|
|
.Where(x => x.Length > 0)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase);
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadTasks();
|
|
|
|
refreshCts = new CancellationTokenSource();
|
|
refreshTimer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (await refreshTimer.WaitForNextTickAsync(refreshCts.Token))
|
|
{
|
|
if (tasks.Any(t => t.Status is "pending" or "running"))
|
|
await InvokeAsync(LoadTasks);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// ignore
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task LoadTasks()
|
|
{
|
|
loading = true;
|
|
try
|
|
{
|
|
var taskList = await TaskManagement.GetRecentTasksAsync(50);
|
|
tasks = taskList.OrderByDescending(t => t.CreatedAt).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
refreshCts?.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
finally
|
|
{
|
|
refreshTimer?.Dispose();
|
|
refreshCts?.Dispose();
|
|
}
|
|
}
|
|
|
|
private double GetProgress(BatchTask task)
|
|
{
|
|
if (task.Total == 0) return 0;
|
|
return (double)task.Completed / task.Total * 100;
|
|
}
|
|
|
|
private string GetCategory(string taskType)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(taskType))
|
|
return "other";
|
|
|
|
if (Contributions.TaskTypeToDefinition.TryGetValue(taskType, out var reg))
|
|
{
|
|
var c = (reg.Definition.Category ?? "").Trim();
|
|
return string.IsNullOrWhiteSpace(c) ? "other" : c;
|
|
}
|
|
|
|
// fallback:兼容历史任务
|
|
return "other";
|
|
}
|
|
|
|
private string GetCategoryName(string taskType) => GetCategoryDisplayName(GetCategory(taskType));
|
|
|
|
private Color GetCategoryColor(string taskType) => GetCategoryColorByCategory(GetCategory(taskType));
|
|
|
|
private static string GetCategoryDisplayName(string category) => category switch
|
|
{
|
|
"user" => "用户任务",
|
|
"bot" => "Bot 任务",
|
|
"system" => "系统任务",
|
|
_ => string.IsNullOrWhiteSpace(category) ? "-" : category
|
|
};
|
|
|
|
private static Color GetCategoryColorByCategory(string category) => category switch
|
|
{
|
|
"user" => Color.Info,
|
|
"bot" => Color.Secondary,
|
|
"system" => Color.Default,
|
|
_ => Color.Default
|
|
};
|
|
|
|
private string GetTaskIcon(string type)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(type)
|
|
&& Contributions.TaskTypeToDefinition.TryGetValue(type, out var reg)
|
|
&& !string.IsNullOrWhiteSpace(reg.Definition.Icon))
|
|
return reg.Definition.Icon;
|
|
|
|
return Icons.Material.Filled.Task;
|
|
}
|
|
|
|
private string GetTaskTypeName(string type)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(type)
|
|
&& Contributions.TaskTypeToDefinition.TryGetValue(type, out var reg)
|
|
&& !string.IsNullOrWhiteSpace(reg.Definition.DisplayName))
|
|
return reg.Definition.DisplayName;
|
|
|
|
return type;
|
|
}
|
|
|
|
private string GetStatusName(string status) => status switch
|
|
{
|
|
"pending" => "待执行",
|
|
"running" => "执行中",
|
|
"completed" => "已完成",
|
|
"failed" => "失败",
|
|
_ => status
|
|
};
|
|
|
|
private Color GetStatusColor(string status) => status switch
|
|
{
|
|
"completed" => Color.Success,
|
|
"running" => Color.Info,
|
|
"pending" => Color.Default,
|
|
"failed" => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
|
|
private Color GetProgressColor(string status) => status switch
|
|
{
|
|
"completed" => Color.Success,
|
|
"running" => Color.Primary,
|
|
"failed" => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
|
|
private void ShowTaskDetails(BatchTask task)
|
|
{
|
|
var createdAt = TimeZone.Format(task.CreatedAt, "yyyy-MM-dd HH:mm");
|
|
var details = $"任务类型: {GetTaskTypeName(task.TaskType)}\n" +
|
|
$"状态: {GetStatusName(task.Status)}\n" +
|
|
$"总数: {task.Total}\n" +
|
|
$"已完成: {task.Completed}\n" +
|
|
$"失败: {task.Failed}\n" +
|
|
$"创建时间: {createdAt}\n";
|
|
|
|
if (task.StartedAt.HasValue)
|
|
details += $"开始时间: {TimeZone.Format(task.StartedAt.Value, "yyyy-MM-dd HH:mm")}\n";
|
|
|
|
if (task.CompletedAt.HasValue)
|
|
details += $"完成时间: {TimeZone.Format(task.CompletedAt.Value, "yyyy-MM-dd HH:mm")}\n";
|
|
|
|
if (!string.IsNullOrEmpty(task.Config))
|
|
details += $"\n配置信息:\n{task.Config}";
|
|
|
|
DialogService.ShowMessageBox("任务详情", details);
|
|
}
|
|
|
|
private async Task CancelTask(int id)
|
|
{
|
|
bool? result = await DialogService.ShowMessageBox(
|
|
"确认取消",
|
|
"确定要取消这个任务吗?",
|
|
yesText: "取消任务", cancelText: "返回");
|
|
|
|
if (result == true)
|
|
{
|
|
try
|
|
{
|
|
await TaskManagement.CompleteTaskAsync(id, false);
|
|
await LoadTasks();
|
|
Snackbar.Add("任务已取消", Severity.Warning);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"取消失败:{ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DeleteTask(int id)
|
|
{
|
|
bool? result = await DialogService.ShowMessageBox(
|
|
"确认删除",
|
|
"确定要删除这个任务记录吗?此操作不可恢复。",
|
|
yesText: "删除", cancelText: "取消");
|
|
|
|
if (result == true)
|
|
{
|
|
try
|
|
{
|
|
await TaskManagement.DeleteTaskAsync(id);
|
|
tasks.RemoveAll(t => t.Id == id);
|
|
Snackbar.Add("任务已删除", Severity.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task OpenCreateTaskDialog()
|
|
{
|
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
|
|
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.CreateBatchTaskDialog>("新建任务", new DialogParameters(), options)!;
|
|
var result = await dialog.Result;
|
|
if (result is { Canceled: false })
|
|
await LoadTasks();
|
|
}
|
|
}
|