Files
omni-scheduler/OmniScheduler/TaskEditorWindow.xaml.cs
home-PC 2a669bdfe7 feat(app): 初始化 OmniScheduler WPF 调度器
基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。

实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。

补充 .gitignore,排除构建产物和本地 IDE 文件。

BREAKING CHANGE: 首次提交,建立项目初始结构
2026-05-20 00:12:17 +08:00

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