mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-25 10:05:49 +08:00
添加定时任务的日志
This commit is contained in:
@@ -65,9 +65,10 @@ public partial class App : Application
|
||||
services.AddSingleton<IRichTextBox>(richTextBox);
|
||||
|
||||
var loggerConfiguration = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext() // Enable LogContext
|
||||
.WriteTo.File(logFile,
|
||||
outputTemplate:
|
||||
"[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}",
|
||||
"[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{CorrelationId}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileCountLimit: 31,
|
||||
retainedFileTimeLimit: TimeSpan.FromDays(21))
|
||||
@@ -162,6 +163,8 @@ public partial class App : Application
|
||||
services.AddSingleton<OcrFactory>();
|
||||
services.AddSingleton<GearTaskStorageService>();
|
||||
services.AddSingleton<GearTriggerStorageService>();
|
||||
services.AddSingleton<ITriggerHistoryService, TriggerHistoryService>();
|
||||
services.AddSingleton<LogReaderService>();
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BetterGenshinImpact.Service;
|
||||
using BetterGenshinImpact.Service.GearTask;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace BetterGenshinImpact.Model.Gear.Triggers.QuartzJob;
|
||||
|
||||
@@ -19,34 +21,77 @@ public class QuartzGearTaskJob : IJob
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
var jobDataMap = context.MergedJobDataMap;
|
||||
var triggerName = jobDataMap.GetString("TriggerName") ?? "Unknown";
|
||||
var triggerId = jobDataMap.GetString("TriggerId") ?? Guid.NewGuid().ToString();
|
||||
var taskDefinitionName = jobDataMap.GetString("TaskDefinitionName");
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
using (LogContext.PushProperty("CorrelationId", correlationId))
|
||||
{
|
||||
var jobDataMap = context.MergedJobDataMap;
|
||||
var triggerName = jobDataMap.GetString("TriggerName") ?? "Unknown";
|
||||
var triggerId = jobDataMap.GetString("TriggerId") ?? Guid.NewGuid().ToString();
|
||||
var taskDefinitionName = jobDataMap.GetString("TaskDefinitionName");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(taskDefinitionName))
|
||||
var historyService = App.GetService<ITriggerHistoryService>();
|
||||
var record = new TriggerExecutionRecord
|
||||
{
|
||||
_logger.LogWarning("触发器 {TriggerName} 未配置任务定义名称", triggerName);
|
||||
return;
|
||||
TriggerName = triggerName,
|
||||
TriggerId = triggerId,
|
||||
TaskName = taskDefinitionName ?? "Unknown",
|
||||
StartTime = DateTime.Now,
|
||||
Status = TriggerExecutionStatus.Running,
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
|
||||
if (historyService != null)
|
||||
{
|
||||
await historyService.AddRecordAsync(record);
|
||||
}
|
||||
|
||||
var shouldInterrupt = jobDataMap.GetBooleanValue("ShouldInterruptOthers");
|
||||
if (shouldInterrupt)
|
||||
try
|
||||
{
|
||||
await InterruptOtherJobs(context, triggerId);
|
||||
if (string.IsNullOrWhiteSpace(taskDefinitionName))
|
||||
{
|
||||
_logger.LogWarning("触发器 {TriggerName} 未配置任务定义名称", triggerName);
|
||||
if (historyService != null)
|
||||
{
|
||||
record.EndTime = DateTime.Now;
|
||||
record.Status = TriggerExecutionStatus.Failed;
|
||||
record.Message = "未配置任务定义名称";
|
||||
await historyService.UpdateRecordAsync(record);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldInterrupt = jobDataMap.GetBooleanValue("ShouldInterruptOthers");
|
||||
if (shouldInterrupt)
|
||||
{
|
||||
await InterruptOtherJobs(context, triggerId);
|
||||
}
|
||||
|
||||
var executor = App.GetRequiredService<GearTaskExecutor>();
|
||||
await executor.ExecuteTaskDefinitionAsync(taskDefinitionName, context.CancellationToken);
|
||||
|
||||
_logger.LogInformation("触发器 {TriggerName} 的任务定义执行完成", triggerName);
|
||||
|
||||
if (historyService != null)
|
||||
{
|
||||
record.EndTime = DateTime.Now;
|
||||
record.Status = TriggerExecutionStatus.Success;
|
||||
record.Message = "执行成功";
|
||||
await historyService.UpdateRecordAsync(record);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "执行定时任务时发生错误");
|
||||
if (historyService != null)
|
||||
{
|
||||
record.EndTime = DateTime.Now;
|
||||
record.Status = TriggerExecutionStatus.Failed;
|
||||
record.Message = ex.Message;
|
||||
record.LogDetails = ex.ToString();
|
||||
await historyService.UpdateRecordAsync(record);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
var executor = App.GetRequiredService<GearTaskExecutor>();
|
||||
await executor.ExecuteTaskDefinitionAsync(taskDefinitionName, context.CancellationToken);
|
||||
|
||||
_logger.LogInformation("触发器 {TriggerName} 的任务定义执行完成", triggerName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "执行定时任务时发生错误");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BetterGenshinImpact.Model.Gear.Triggers;
|
||||
|
||||
public enum TriggerExecutionStatus
|
||||
{
|
||||
Running,
|
||||
Success,
|
||||
Failed,
|
||||
Skipped
|
||||
}
|
||||
|
||||
public class TriggerExecutionRecord
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[JsonProperty("trigger_name")]
|
||||
public string TriggerName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("trigger_id")]
|
||||
public string TriggerId { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("task_name")]
|
||||
public string TaskName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("start_time")]
|
||||
public DateTime StartTime { get; set; } = DateTime.Now;
|
||||
|
||||
[JsonProperty("end_time")]
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public TriggerExecutionStatus Status { get; set; } = TriggerExecutionStatus.Running;
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("log_details")]
|
||||
public string LogDetails { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("correlation_id")]
|
||||
public string CorrelationId { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public TimeSpan Duration => EndTime.HasValue ? EndTime.Value - StartTime : DateTime.Now - StartTime;
|
||||
}
|
||||
61
BetterGenshinImpact/Service/GearTask/LogReaderService.cs
Normal file
61
BetterGenshinImpact/Service/GearTask/LogReaderService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BetterGenshinImpact.Service.GearTask;
|
||||
|
||||
public class LogReaderService
|
||||
{
|
||||
private readonly string _logDirectory;
|
||||
|
||||
public LogReaderService()
|
||||
{
|
||||
_logDirectory = Path.Combine(AppContext.BaseDirectory, "log");
|
||||
}
|
||||
|
||||
public async Task<string> GetLogsForCorrelationIdAsync(string correlationId, DateTime date)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(correlationId))
|
||||
{
|
||||
return "No correlation ID provided.";
|
||||
}
|
||||
|
||||
var logFileName = $"better-genshin-impact{date:yyyyMMdd}.log";
|
||||
var logFilePath = Path.Combine(_logDirectory, logFileName);
|
||||
|
||||
if (!File.Exists(logFilePath))
|
||||
{
|
||||
return $"Log file not found: {logFilePath}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件内容
|
||||
// 由于日志文件可能很大,这里简单起见读取全部,生产环境可能需要更高效的方式(如流式读取)
|
||||
// 考虑到文件可能被占用,使用 FileShare.ReadWrite
|
||||
using var fileStream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
var content = await streamReader.ReadToEndAsync();
|
||||
|
||||
var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
|
||||
// 筛选包含 CorrelationId 的行
|
||||
// 日志格式: [Timestamp] [Level] [CorrelationId] SourceContext ...
|
||||
var relatedLines = lines.Where(line => line.Contains(correlationId)).ToList();
|
||||
|
||||
if (relatedLines.Count == 0)
|
||||
{
|
||||
return "No logs found for this execution.";
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, relatedLines);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error reading log file: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using BetterGenshinImpact.Core.Config;
|
||||
|
||||
namespace BetterGenshinImpact.Service.GearTask.Model;
|
||||
@@ -14,4 +14,5 @@ public class GearTaskPaths
|
||||
|
||||
public static readonly string TaskTriggerPath = Path.Combine(TaskV2Path, "trigger");
|
||||
|
||||
public static readonly string TaskHistoryPath = Path.Combine(TaskV2Path, "history");
|
||||
}
|
||||
147
BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs
Normal file
147
BetterGenshinImpact/Service/GearTask/TriggerHistoryService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BetterGenshinImpact.Model.Gear.Triggers;
|
||||
using BetterGenshinImpact.Service.GearTask.Model;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BetterGenshinImpact.Service.GearTask;
|
||||
|
||||
public interface ITriggerHistoryService
|
||||
{
|
||||
event EventHandler HistoryChanged;
|
||||
Task AddRecordAsync(TriggerExecutionRecord record);
|
||||
Task UpdateRecordAsync(TriggerExecutionRecord record);
|
||||
Task<List<TriggerExecutionRecord>> GetHistoryAsync();
|
||||
Task ClearHistoryAsync();
|
||||
}
|
||||
|
||||
public class TriggerHistoryService : ITriggerHistoryService
|
||||
{
|
||||
private readonly string _historyFilePath;
|
||||
private readonly ILogger<TriggerHistoryService> _logger;
|
||||
private readonly JsonSerializerSettings _jsonSettings;
|
||||
private List<TriggerExecutionRecord> _cachedHistory;
|
||||
private const int MaxHistoryCount = 200;
|
||||
|
||||
public event EventHandler? HistoryChanged;
|
||||
|
||||
public TriggerHistoryService(ILogger<TriggerHistoryService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
// 使用 GearTaskPaths 定义的路径
|
||||
var historyDir = GearTaskPaths.TaskHistoryPath;
|
||||
_historyFilePath = Path.Combine(historyDir, "trigger_history.json");
|
||||
|
||||
// 确保目录存在
|
||||
if (!Directory.Exists(historyDir))
|
||||
{
|
||||
Directory.CreateDirectory(historyDir);
|
||||
}
|
||||
|
||||
_jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
_cachedHistory = new List<TriggerExecutionRecord>();
|
||||
// 初始化时加载
|
||||
_ = LoadHistoryFromFileAsync();
|
||||
}
|
||||
|
||||
private async Task LoadHistoryFromFileAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_historyFilePath))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(_historyFilePath);
|
||||
var history = JsonConvert.DeserializeObject<List<TriggerExecutionRecord>>(json, _jsonSettings);
|
||||
if (history != null)
|
||||
{
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
_cachedHistory = history;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载触发器历史记录失败");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
string json;
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
// 保持最大记录数
|
||||
if (_cachedHistory.Count > MaxHistoryCount)
|
||||
{
|
||||
_cachedHistory = _cachedHistory.OrderByDescending(x => x.StartTime).Take(MaxHistoryCount).ToList();
|
||||
}
|
||||
json = JsonConvert.SerializeObject(_cachedHistory, _jsonSettings);
|
||||
}
|
||||
await File.WriteAllTextAsync(_historyFilePath, json);
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "保存触发器历史记录失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddRecordAsync(TriggerExecutionRecord record)
|
||||
{
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
_cachedHistory.Insert(0, record);
|
||||
}
|
||||
await SaveHistoryAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateRecordAsync(TriggerExecutionRecord record)
|
||||
{
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
var index = _cachedHistory.FindIndex(r => r.Id == record.Id);
|
||||
if (index != -1)
|
||||
{
|
||||
_cachedHistory[index] = record;
|
||||
}
|
||||
}
|
||||
await SaveHistoryAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TriggerExecutionRecord>> GetHistoryAsync()
|
||||
{
|
||||
// 如果缓存为空但文件存在,尝试重新加载
|
||||
if (_cachedHistory.Count == 0 && File.Exists(_historyFilePath))
|
||||
{
|
||||
await LoadHistoryFromFileAsync();
|
||||
}
|
||||
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
return _cachedHistory.OrderByDescending(x => x.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearHistoryAsync()
|
||||
{
|
||||
lock (_cachedHistory)
|
||||
{
|
||||
_cachedHistory.Clear();
|
||||
}
|
||||
await SaveHistoryAsync();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
|
||||
xmlns:component="clr-namespace:BetterGenshinImpact.ViewModel.Pages.Component"
|
||||
xmlns:triggers="clr-namespace:BetterGenshinImpact.Model.Gear.Triggers"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
d:DataContext="{d:DesignInstance Type=pages:GearTriggerPageViewModel}"
|
||||
d:DesignHeight="850"
|
||||
@@ -277,104 +278,210 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- 历史记录表格头部模板 -->
|
||||
<DataTemplate x:Key="HistoryTableHeaderTemplate">
|
||||
<Border Padding="10,12" Background="Transparent">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" /> <!-- 时间 -->
|
||||
<ColumnDefinition Width="80" /> <!-- 任务 -->
|
||||
<ColumnDefinition Width="50" /> <!-- 状态 -->
|
||||
<ColumnDefinition Width="*" /> <!-- 信息 -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Column="0" Text="时间" FontWeight="SemiBold" HorizontalAlignment="Center" />
|
||||
<ui:TextBlock Grid.Column="1" Text="任务" FontWeight="SemiBold" HorizontalAlignment="Center" />
|
||||
<ui:TextBlock Grid.Column="2" Text="状态" FontWeight="SemiBold" HorizontalAlignment="Center" />
|
||||
<ui:TextBlock Grid.Column="3" Text="信息" FontWeight="SemiBold" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- 历史记录数据项模板 -->
|
||||
<DataTemplate x:Key="HistoryItemTemplate" DataType="{x:Type triggers:TriggerExecutionRecord}">
|
||||
<Border BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="10,12"
|
||||
Background="Transparent">
|
||||
<Border.InputBindings>
|
||||
<MouseBinding MouseAction="LeftDoubleClick"
|
||||
Command="{Binding DataContext.ViewHistoryDetailsCommand, RelativeSource={RelativeSource AncestorType=ui:ListView}}"
|
||||
CommandParameter="{Binding}" />
|
||||
</Border.InputBindings>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ui:TextBlock Grid.Column="0" Text="{Binding StartTime, StringFormat='HH:mm:ss'}" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
<ui:TextBlock Grid.Column="1" Text="{Binding TaskName}" HorizontalAlignment="Center" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
<ui:TextBlock Grid.Column="2" Text="{Binding Status}" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
<ui:TextBlock Grid.Column="3" Text="{Binding Message}" HorizontalAlignment="Left" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" ToolTip="{Binding Message}" Padding="5,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- 简化的双Tab布局 -->
|
||||
<ui:Border Background="{DynamicResource CardBackground}"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Margin="10">
|
||||
<TabControl Style="{StaticResource ConsistentTabControlStyle}">
|
||||
<TabItem Header="定时触发" Style="{StaticResource ConsistentTabItemStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:Button Content="新增定时触发器"
|
||||
Command="{Binding AddTimedTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Add24}"
|
||||
Appearance="Primary"
|
||||
Margin="0,0,8,0" />
|
||||
<ui:Button Content="删除选中项"
|
||||
Command="{Binding DeleteTriggerCommand}"
|
||||
<!-- 左右分栏布局 -->
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="3*" /> <!-- 左侧触发器设置 -->
|
||||
<ColumnDefinition Width="10" /> <!-- 分隔 -->
|
||||
<ColumnDefinition Width="2*" /> <!-- 右侧执行历史 -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧:触发器设置 -->
|
||||
<ui:Border Grid.Column="0"
|
||||
Background="{DynamicResource CardBackground}"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<TabControl Style="{StaticResource ConsistentTabControlStyle}">
|
||||
<TabItem Header="定时触发" Style="{StaticResource ConsistentTabItemStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:Button Content="新增定时触发器"
|
||||
Command="{Binding AddTimedTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Add24}"
|
||||
Appearance="Primary"
|
||||
Margin="0,0,8,0" />
|
||||
<ui:Button Content="删除选中项"
|
||||
Command="{Binding DeleteTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Delete24}"
|
||||
Appearance="Secondary" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 固定表格头部 -->
|
||||
<ContentPresenter Grid.Row="1"
|
||||
ContentTemplate="{StaticResource TimedTableHeaderTemplate}" />
|
||||
|
||||
<!-- 可滚动的列表内容 -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ui:ListView ItemsSource="{Binding TimedTriggers}"
|
||||
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource TimedTriggerItemTemplate}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Margin="0" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="快捷键触发" Style="{StaticResource ConsistentTabItemStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:Button Content="新增快捷键触发器"
|
||||
Command="{Binding AddHotkeyTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Add24}"
|
||||
Appearance="Primary"
|
||||
Margin="0,0,8,0" />
|
||||
<ui:Button Content="删除选中项"
|
||||
Command="{Binding DeleteTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Delete24}"
|
||||
Appearance="Secondary" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 固定表格头部 -->
|
||||
<ContentPresenter Grid.Row="1"
|
||||
ContentTemplate="{StaticResource TableHeaderTemplate}" />
|
||||
|
||||
<!-- 可滚动的列表内容 -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ui:ListView ItemsSource="{Binding HotkeyTriggers}"
|
||||
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource TriggerItemTemplate}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Margin="0" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</ui:Border>
|
||||
|
||||
<!-- 右侧:执行历史 -->
|
||||
<ui:Border Grid.Column="2"
|
||||
Background="{DynamicResource CardBackground}"
|
||||
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<ui:TextBlock Text="执行历史"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<ui:Button Content="清空"
|
||||
Command="{Binding ClearHistoryCommand}"
|
||||
Icon="{ui:SymbolIcon Delete24}"
|
||||
Appearance="Secondary" />
|
||||
Appearance="Secondary"
|
||||
Padding="8,4" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 固定表格头部 -->
|
||||
<ContentPresenter Grid.Row="1"
|
||||
ContentTemplate="{StaticResource TimedTableHeaderTemplate}" />
|
||||
|
||||
<!-- 可滚动的列表内容 -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ui:ListView ItemsSource="{Binding TimedTriggers}"
|
||||
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource TimedTriggerItemTemplate}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Margin="0" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="快捷键触发" Style="{StaticResource ConsistentTabItemStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:Button Content="新增快捷键触发器"
|
||||
Command="{Binding AddHotkeyTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Add24}"
|
||||
Appearance="Primary"
|
||||
Margin="0,0,8,0" />
|
||||
<ui:Button Content="删除选中项"
|
||||
Command="{Binding DeleteTriggerCommand}"
|
||||
Icon="{ui:SymbolIcon Delete24}"
|
||||
Appearance="Secondary" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 固定表格头部 -->
|
||||
<ContentPresenter Grid.Row="1"
|
||||
ContentTemplate="{StaticResource TableHeaderTemplate}" />
|
||||
|
||||
<!-- 可滚动的列表内容 -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ui:ListView ItemsSource="{Binding HotkeyTriggers}"
|
||||
SelectedItem="{Binding SelectedTrigger, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource TriggerItemTemplate}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Margin="0" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</ui:Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 固定表格头部 -->
|
||||
<ContentPresenter Grid.Row="1"
|
||||
ContentTemplate="{StaticResource HistoryTableHeaderTemplate}" />
|
||||
|
||||
<!-- 可滚动的列表内容 -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ui:ListView ItemsSource="{Binding ExecutionHistory}"
|
||||
ItemTemplate="{StaticResource HistoryItemTemplate}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
BorderThickness="0"
|
||||
Margin="0" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ui:Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -6,6 +6,7 @@ using BetterGenshinImpact.Model.Gear.Triggers;
|
||||
using BetterGenshinImpact.Model;
|
||||
using BetterGenshinImpact.Helpers.Extensions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Quartz;
|
||||
|
||||
namespace BetterGenshinImpact.ViewModel.Pages.Component;
|
||||
|
||||
@@ -26,6 +27,11 @@ public partial class GearTriggerViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string? _cronExpression;
|
||||
|
||||
partial void OnCronExpressionChanged(string? value)
|
||||
{
|
||||
UpdateNextRunTime();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime _createdTime = DateTime.Now;
|
||||
|
||||
@@ -60,6 +66,36 @@ public partial class GearTriggerViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public string HotkeyTypeName => HotkeyType.ToChineseName();
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime? _nextRunTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime? _lastRunTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private TriggerExecutionStatus? _lastRunStatus;
|
||||
|
||||
public void UpdateNextRunTime()
|
||||
{
|
||||
if (TriggerType == TriggerType.Timed && !string.IsNullOrWhiteSpace(CronExpression))
|
||||
{
|
||||
try
|
||||
{
|
||||
var cron = new CronExpression(CronExpression);
|
||||
var next = cron.GetNextValidTimeAfter(DateTimeOffset.Now);
|
||||
NextRunTime = next?.LocalDateTime;
|
||||
}
|
||||
catch
|
||||
{
|
||||
NextRunTime = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NextRunTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GearTriggerViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -29,16 +29,70 @@ public partial class GearTriggerPageViewModel : ViewModel
|
||||
|
||||
[ObservableProperty]
|
||||
private GearTaskDefinitionViewModel? _selectedTaskDefinition;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<TriggerExecutionRecord> _executionHistory = new();
|
||||
|
||||
public GearTriggerPageViewModel(ILogger<GearTriggerPageViewModel> logger, GearTriggerStorageService storageService)
|
||||
private readonly ITriggerHistoryService _historyService;
|
||||
private readonly LogReaderService _logReaderService;
|
||||
|
||||
public GearTriggerPageViewModel(ILogger<GearTriggerPageViewModel> logger, GearTriggerStorageService storageService, ITriggerHistoryService historyService, LogReaderService logReaderService)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageService = storageService;
|
||||
_historyService = historyService;
|
||||
_logReaderService = logReaderService;
|
||||
|
||||
_historyService.HistoryChanged += OnHistoryChanged;
|
||||
}
|
||||
|
||||
private void OnHistoryChanged(object? sender, EventArgs e)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(async () =>
|
||||
{
|
||||
await LoadHistoryAsync();
|
||||
UpdateTriggerStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var history = await _historyService.GetHistoryAsync();
|
||||
ExecutionHistory.Clear();
|
||||
foreach (var record in history)
|
||||
{
|
||||
ExecutionHistory.Add(record);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载历史记录失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTriggerStatus()
|
||||
{
|
||||
var history = ExecutionHistory.ToList();
|
||||
|
||||
foreach (var trigger in TimedTriggers)
|
||||
{
|
||||
var lastRun = history.FirstOrDefault(x => x.TriggerName == trigger.Name);
|
||||
if (lastRun != null)
|
||||
{
|
||||
trigger.LastRunTime = lastRun.StartTime;
|
||||
trigger.LastRunStatus = lastRun.Status;
|
||||
}
|
||||
// 确保更新下次运行时间
|
||||
trigger.UpdateNextRunTime();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo()
|
||||
{
|
||||
_ = LoadTriggersAsync();
|
||||
_ = LoadHistoryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,6 +109,7 @@ public partial class GearTriggerPageViewModel : ViewModel
|
||||
|
||||
foreach (var trigger in timedTriggers)
|
||||
{
|
||||
trigger.UpdateNextRunTime();
|
||||
TimedTriggers.Add(trigger);
|
||||
}
|
||||
|
||||
@@ -63,6 +118,9 @@ public partial class GearTriggerPageViewModel : ViewModel
|
||||
HotkeyTriggers.Add(trigger);
|
||||
}
|
||||
|
||||
// 加载完触发器后更新状态
|
||||
UpdateTriggerStatus();
|
||||
|
||||
_logger.LogInformation("已加载 {TimedCount} 个定时触发器和 {HotkeyCount} 个快捷键触发器",
|
||||
TimedTriggers.Count, HotkeyTriggers.Count);
|
||||
}
|
||||
@@ -71,6 +129,55 @@ public partial class GearTriggerPageViewModel : ViewModel
|
||||
_logger.LogError(ex, "加载触发器数据时发生错误");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ClearHistory()
|
||||
{
|
||||
await _historyService.ClearHistoryAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ViewHistoryDetails(TriggerExecutionRecord? record)
|
||||
{
|
||||
if (record == null) return;
|
||||
|
||||
var logContent = "正在加载日志...";
|
||||
if (!string.IsNullOrEmpty(record.CorrelationId))
|
||||
{
|
||||
try
|
||||
{
|
||||
logContent = await _logReaderService.GetLogsForCorrelationIdAsync(record.CorrelationId, record.StartTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logContent = $"读取日志失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logContent = string.IsNullOrEmpty(record.LogDetails) ? "无关联的日志记录 (可能是旧版本数据)" : record.LogDetails;
|
||||
}
|
||||
|
||||
var message = $"""
|
||||
触发器: {record.TriggerName}
|
||||
任务: {record.TaskName}
|
||||
开始时间: {record.StartTime}
|
||||
结束时间: {record.EndTime}
|
||||
耗时: {record.Duration.TotalSeconds:F2} 秒
|
||||
状态: {record.Status}
|
||||
CorrelationId: {record.CorrelationId}
|
||||
|
||||
简述: {record.Message}
|
||||
|
||||
=== 详细日志 ===
|
||||
{logContent}
|
||||
""";
|
||||
|
||||
// 这里可以使用更高级的弹窗,目前先用 MessageBox
|
||||
// 为了更好的体验,建议后续改用专门的日志查看窗口
|
||||
System.Windows.MessageBox.Show(message, "执行详情");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 保存触发器数据
|
||||
|
||||
Reference in New Issue
Block a user