基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。 实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。 补充 .gitignore,排除构建产物和本地 IDE 文件。 BREAKING CHANGE: 首次提交,建立项目初始结构
247 lines
8.3 KiB
C#
247 lines
8.3 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
|
|
namespace OmniScheduler;
|
|
|
|
public partial class TaskEditorWindow : Window
|
|
{
|
|
private readonly ScheduledTask _task;
|
|
private readonly AppSettings _settings;
|
|
private readonly NotifyClient _client;
|
|
private bool _loading;
|
|
|
|
private TaskEditorWindow(Window owner, ScheduledTask task, AppSettings settings, NotifyClient client)
|
|
{
|
|
InitializeComponent();
|
|
Owner = owner;
|
|
_task = task;
|
|
_settings = settings;
|
|
_client = client;
|
|
|
|
KindBox.ItemsSource = Enum.GetValues<TriggerKind>();
|
|
IntervalUnitBox.ItemsSource = Enum.GetValues<IntervalUnit>();
|
|
MisfireBox.ItemsSource = Enum.GetValues<MisfireStrategy>();
|
|
ChannelBox.ItemsSource = owner is MainWindow main
|
|
? main.State.Tasks.Select(t => t.Channel).Where(c => !string.IsNullOrWhiteSpace(c)).Distinct().ToList()
|
|
: null;
|
|
|
|
LoadTask();
|
|
}
|
|
|
|
public static bool Edit(Window owner, ScheduledTask task, AppSettings settings, NotifyClient client)
|
|
{
|
|
var workingCopy = task.Clone();
|
|
workingCopy.Id = task.Id;
|
|
workingCopy.Name = task.Name;
|
|
var dialog = new TaskEditorWindow(owner, workingCopy, settings, client);
|
|
if (dialog.ShowDialog() == true)
|
|
{
|
|
task.CopyFrom(workingCopy);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void LoadTask()
|
|
{
|
|
NameBox.Text = _task.Name;
|
|
DescriptionBox.Text = _task.Description;
|
|
EnabledBox.IsChecked = _task.IsEnabled;
|
|
TargetUrlBox.Text = _settings.OmniNotifyUrl;
|
|
ChannelBox.Text = _task.Channel;
|
|
TitleBox.Text = _task.Title;
|
|
BodyBox.Text = _task.Body;
|
|
MisfireBox.SelectedItem = _task.MisfireStrategy;
|
|
TriggersList.ItemsSource = _task.Triggers;
|
|
TriggersList.SelectedIndex = _task.Triggers.Count > 0 ? 0 : -1;
|
|
}
|
|
|
|
private void LoadTrigger(TaskTrigger? trigger)
|
|
{
|
|
_loading = true;
|
|
try
|
|
{
|
|
if (trigger is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
KindBox.SelectedItem = trigger.Kind;
|
|
OneTimeBox.Text = trigger.OneTimeAt.ToString("yyyy-MM-dd HH:mm:ss");
|
|
IntervalValueBox.Text = trigger.IntervalValue.ToString();
|
|
IntervalUnitBox.SelectedItem = trigger.IntervalUnit;
|
|
StartsAtBox.Text = trigger.StartsAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
|
|
EndsAtBox.Text = trigger.EndsAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
|
|
TimeOfDayBox.Text = trigger.TimeOfDay.ToString(@"hh\:mm\:ss");
|
|
MonthDayBox.Text = trigger.MonthDay.ToString();
|
|
LastBusinessDayBox.IsChecked = trigger.LastBusinessDay;
|
|
CronBox.Text = trigger.CronExpression;
|
|
|
|
SunBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Sunday);
|
|
MonBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Monday);
|
|
TueBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Tuesday);
|
|
WedBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Wednesday);
|
|
ThuBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Thursday);
|
|
FriBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Friday);
|
|
SatBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Saturday);
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
RefreshPreview();
|
|
}
|
|
}
|
|
|
|
private bool SaveTriggerFields()
|
|
{
|
|
if (_loading || TriggersList.SelectedItem is not TaskTrigger trigger)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
trigger.Kind = KindBox.SelectedItem is TriggerKind kind ? kind : TriggerKind.Interval;
|
|
trigger.IntervalUnit = IntervalUnitBox.SelectedItem is IntervalUnit unit ? unit : IntervalUnit.Minutes;
|
|
trigger.LastBusinessDay = LastBusinessDayBox.IsChecked == true;
|
|
trigger.CronExpression = CronBox.Text.Trim();
|
|
|
|
if (int.TryParse(IntervalValueBox.Text, out var interval))
|
|
{
|
|
trigger.IntervalValue = interval;
|
|
}
|
|
|
|
if (int.TryParse(MonthDayBox.Text, out var monthDay))
|
|
{
|
|
trigger.MonthDay = Math.Clamp(monthDay, 1, 31);
|
|
}
|
|
|
|
if (DateTime.TryParse(OneTimeBox.Text, out var oneTime))
|
|
{
|
|
trigger.OneTimeAt = oneTime;
|
|
}
|
|
|
|
trigger.StartsAt = DateTime.TryParse(StartsAtBox.Text, out var startsAt) ? startsAt : null;
|
|
trigger.EndsAt = DateTime.TryParse(EndsAtBox.Text, out var endsAt) ? endsAt : null;
|
|
|
|
if (TimeSpan.TryParse(TimeOfDayBox.Text, out var time))
|
|
{
|
|
trigger.TimeOfDay = time;
|
|
}
|
|
|
|
var days = DayOfWeekFlags.None;
|
|
if (SunBox.IsChecked == true) days |= DayOfWeekFlags.Sunday;
|
|
if (MonBox.IsChecked == true) days |= DayOfWeekFlags.Monday;
|
|
if (TueBox.IsChecked == true) days |= DayOfWeekFlags.Tuesday;
|
|
if (WedBox.IsChecked == true) days |= DayOfWeekFlags.Wednesday;
|
|
if (ThuBox.IsChecked == true) days |= DayOfWeekFlags.Thursday;
|
|
if (FriBox.IsChecked == true) days |= DayOfWeekFlags.Friday;
|
|
if (SatBox.IsChecked == true) days |= DayOfWeekFlags.Saturday;
|
|
trigger.WeekDays = days;
|
|
trigger.Notify(nameof(TaskTrigger.Summary));
|
|
TriggersList.Items.Refresh();
|
|
RefreshPreview();
|
|
return true;
|
|
}
|
|
|
|
private bool SaveTaskFields()
|
|
{
|
|
ValidationText.Text = "";
|
|
SaveTriggerFields();
|
|
|
|
if (string.IsNullOrWhiteSpace(NameBox.Text))
|
|
{
|
|
ValidationText.Text = "任务名称不能为空";
|
|
return false;
|
|
}
|
|
|
|
foreach (var trigger in _task.Triggers)
|
|
{
|
|
if (trigger.Kind == TriggerKind.Interval && trigger.IntervalValue < 1)
|
|
{
|
|
ValidationText.Text = "固定间隔不能小于 1";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_task.Name = NameBox.Text.Trim();
|
|
_task.Description = DescriptionBox.Text.Trim();
|
|
_task.IsEnabled = EnabledBox.IsChecked == true;
|
|
_task.Channel = ChannelBox.Text.Trim();
|
|
_task.Title = TitleBox.Text.Trim();
|
|
_task.Body = BodyBox.Text;
|
|
_task.MisfireStrategy = MisfireBox.SelectedItem is MisfireStrategy strategy ? strategy : MisfireStrategy.DoNothing;
|
|
return true;
|
|
}
|
|
|
|
private void TriggersList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
=> LoadTrigger(TriggersList.SelectedItem as TaskTrigger);
|
|
|
|
private void AddTrigger_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var trigger = new TaskTrigger();
|
|
_task.Triggers.Add(trigger);
|
|
TriggersList.SelectedItem = trigger;
|
|
TriggersList.Items.Refresh();
|
|
}
|
|
|
|
private void RemoveTrigger_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (TriggersList.SelectedItem is TaskTrigger trigger)
|
|
{
|
|
_task.Triggers.Remove(trigger);
|
|
TriggersList.Items.Refresh();
|
|
}
|
|
}
|
|
|
|
private void Field_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!_loading)
|
|
{
|
|
SaveTriggerFields();
|
|
}
|
|
}
|
|
|
|
private async void TestSend_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!SaveTaskFields())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TestResultText.Text = "发送中...";
|
|
var log = await _client.SendAsync(_task, "Test", _settings.OmniNotifyUrl);
|
|
TestResultText.Text = log.Level == "INFO" ? "测试发送成功" : log.Message;
|
|
TestResultText.Foreground = log.Level == "INFO"
|
|
? System.Windows.Media.Brushes.ForestGreen
|
|
: System.Windows.Media.Brushes.Firebrick;
|
|
}
|
|
|
|
private void Save_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!SaveTaskFields())
|
|
{
|
|
return;
|
|
}
|
|
|
|
DialogResult = true;
|
|
}
|
|
|
|
private void Cancel_Click(object sender, RoutedEventArgs e)
|
|
=> DialogResult = false;
|
|
|
|
private void RefreshPreview()
|
|
{
|
|
if (TriggersList.SelectedItem is not TaskTrigger trigger)
|
|
{
|
|
PreviewBox.Text = "";
|
|
return;
|
|
}
|
|
|
|
var preview = NextRunCalculator.Preview(trigger, DateTime.Now);
|
|
PreviewBox.Text = preview.Count == 0
|
|
? "无法计算未来执行时间,请检查规则。"
|
|
: string.Join(Environment.NewLine, preview.Select(d => d.ToString("yyyy-MM-dd HH:mm:ss")));
|
|
}
|
|
}
|