feat(app): 初始化 OmniScheduler WPF 调度器
基于 PRD 搭建 .NET 8 WPF 桌面应用,包含主控制台、任务编辑、全局设置、系统托盘和应用图标集成。 实现本地任务模型、触发器规则、JSON 状态持久化、OmniNotify HTTP 推送、执行日志记录、动态变量替换以及基础 Cron 预览能力。 补充 .gitignore,排除构建产物和本地 IDE 文件。 BREAKING CHANGE: 首次提交,建立项目初始结构
This commit is contained in:
246
OmniScheduler/TaskEditorWindow.xaml.cs
Normal file
246
OmniScheduler/TaskEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user