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

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();
}
}