diff --git a/CRSim.Core/Models/Simulator/TrainStatus.cs b/CRSim.Core/Models/Simulator/TrainStatus.cs new file mode 100644 index 0000000..995e298 --- /dev/null +++ b/CRSim.Core/Models/Simulator/TrainStatus.cs @@ -0,0 +1,14 @@ +namespace CRSim.Core.Models +{ + public static class TrainStatus + { + // 00:00:01 is reserved for "delay unknown"; normal input/export paths only use minute precision. + public static readonly TimeSpan DelayUnknown = TimeSpan.FromSeconds(1); + + public static bool IsDelayUnknown(TimeSpan? status) => + status.HasValue && status.Value == DelayUnknown; + + public static TimeSpan GetScheduleOffset(TimeSpan? status) => + IsDelayUnknown(status) ? TimeSpan.Zero : status ?? TimeSpan.Zero; + } +} diff --git a/CRSim.ScreenSimulator/Converters/TrainStateColorConverter.cs b/CRSim.ScreenSimulator/Converters/TrainStateColorConverter.cs index 5f71f37..2e64163 100644 --- a/CRSim.ScreenSimulator/Converters/TrainStateColorConverter.cs +++ b/CRSim.ScreenSimulator/Converters/TrainStateColorConverter.cs @@ -38,6 +38,7 @@ namespace CRSim.ScreenSimulator.Converters { if (values[0] is DateTime arriveTime){ if (values.Length > 1 && values[1] is TimeSpan state){ + if (TrainStatus.IsDelayUnknown(state)) return StopCheckInColor; if (state.TotalMinutes > 0) return ArrivingLateText; if (state.TotalMinutes < 0) return ArrivingEarlyText; return now >= arriveTime ? ArrivedText : ArrivingText; @@ -51,6 +52,10 @@ namespace CRSim.ScreenSimulator.Converters if (values[2] is null) return StopCheckInColor; + // 晚点未定 + if (values[2] is TimeSpan unknownState && TrainStatus.IsDelayUnknown(unknownState)) + return StopCheckInColor; + // 正点/晚点判断 if (values[0] != null && values[1] == null && values[2] is TimeSpan status) return GetStatusColor(status); diff --git a/CRSim.ScreenSimulator/Converters/TrainStateConverter.cs b/CRSim.ScreenSimulator/Converters/TrainStateConverter.cs index a9a7a4e..7e6cae8 100644 --- a/CRSim.ScreenSimulator/Converters/TrainStateConverter.cs +++ b/CRSim.ScreenSimulator/Converters/TrainStateConverter.cs @@ -21,6 +21,7 @@ namespace CRSim.ScreenSimulator.Converters public string CheckInText { get; set; } = "正在检票"; public string StopCheckInText { get; set; } = "停止检票"; public string SuspendText { get; set; } = "列车停运"; + public string DelayUnknownText { get; set; } = "晚点未定"; public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { _settings ??= StyleManager.ServiceProvider @@ -39,6 +40,8 @@ namespace CRSim.ScreenSimulator.Converters { if (values[2] is TimeSpan state) { + if (TrainStatus.IsDelayUnknown(state)) + return DelayUnknownText; // arriveTime += TimeSpan.FromMinutes(state.TotalMinutes); if (now >= arriveTime) { @@ -67,6 +70,10 @@ namespace CRSim.ScreenSimulator.Converters if (values[2] is null) return SuspendText; + // 晚点未定 + if (values[2] is TimeSpan unknownState && TrainStatus.IsDelayUnknown(unknownState)) + return DelayUnknownText; + // 有出发时间 if (values[1] is DateTime departureTime && departureTime != DateTime.MinValue) { @@ -114,4 +121,4 @@ namespace CRSim.ScreenSimulator.Converters return null; } } -} \ No newline at end of file +} diff --git a/CRSim.ScreenSimulator/Models/TrainInfo.cs b/CRSim.ScreenSimulator/Models/TrainInfo.cs index 1c21e16..7d518d9 100644 --- a/CRSim.ScreenSimulator/Models/TrainInfo.cs +++ b/CRSim.ScreenSimulator/Models/TrainInfo.cs @@ -1,4 +1,6 @@ -namespace CRSim.ScreenSimulator.Models +using CRSim.Core.Models; + +namespace CRSim.ScreenSimulator.Models { public class TrainInfo { @@ -9,11 +11,11 @@ private DateTime? departureTime { get; set; } public DateTime? ArrivalTime { - get + get { - return State is null ? arrivalTime : arrivalTime + State; - } - set + return State is null || TrainStatus.IsDelayUnknown(State) ? arrivalTime : arrivalTime + State; + } + set { arrivalTime = value; } @@ -22,7 +24,7 @@ { get { - return State is null || State < TimeSpan.Zero ? departureTime : departureTime + State; + return State is null || State < TimeSpan.Zero || TrainStatus.IsDelayUnknown(State) ? departureTime : departureTime + State; } set { diff --git a/CRSim.ScreenSimulator/ViewModels/BaseScreenViewModel.cs b/CRSim.ScreenSimulator/ViewModels/BaseScreenViewModel.cs index 8165f3a..172f5a4 100644 --- a/CRSim.ScreenSimulator/ViewModels/BaseScreenViewModel.cs +++ b/CRSim.ScreenSimulator/ViewModels/BaseScreenViewModel.cs @@ -196,22 +196,27 @@ namespace CRSim.ScreenSimulator.ViewModels { var now = TimeService.GetDateTimeNow(); var today = now.Date; + var scheduleOffset = TrainStatus.GetScheduleOffset(trainNumber.Status); - if (_settings.LoadTodayOnly && today.Add((trainNumber.DepartureTime ?? trainNumber.ArrivalTime)!.Value).Add(trainNumber.Status.Value) < now) + if (_settings.LoadTodayOnly && today.Add((trainNumber.DepartureTime ?? trainNumber.ArrivalTime)!.Value).Add(scheduleOffset) < now) { continue; } - DateTime? AdjustTime(TimeSpan? time, TimeSpan? status) => - time.HasValue ? (today.Add(time.Value).Add(status.Value) > now ? today.Add(time.Value) : today.Add(time.Value).AddDays(1)) : null; + DateTime? AdjustTime(TimeSpan? time, TimeSpan status) => + time.HasValue ? (today.Add(time.Value).Add(status) > now ? today.Add(time.Value) : today.Add(time.Value).AddDays(1)) : null; + + var departureOffset = trainNumber.Status is TimeSpan knownStatus && knownStatus > TimeSpan.Zero && !TrainStatus.IsDelayUnknown(knownStatus) + ? knownStatus + : TimeSpan.Zero; TrainInfo.Add(new TrainInfo { TrainNumber = trainNumber.Number, Terminal = trainNumber.Terminal, Origin = trainNumber.Origin, - ArrivalTime = AdjustTime(trainNumber.ArrivalTime, trainNumber.Status), - DepartureTime = trainNumber.Status > TimeSpan.Zero ? AdjustTime(trainNumber.DepartureTime, trainNumber.Status) : AdjustTime(trainNumber.DepartureTime, TimeSpan.Zero), + ArrivalTime = AdjustTime(trainNumber.ArrivalTime, scheduleOffset), + DepartureTime = AdjustTime(trainNumber.DepartureTime, departureOffset), TicketChecks = trainNumber.TicketCheckIds is null ? [] : [.. station.WaitingAreas .SelectMany(w => w.TicketChecks) .Where(tc => trainNumber.TicketCheckIds.Contains(tc.Id)) @@ -232,4 +237,4 @@ namespace CRSim.ScreenSimulator.ViewModels DataLoaded.SetResult(true); } } -} \ No newline at end of file +} diff --git a/CRSim.ScreenSimulator/ViewModels/MetroPlatformScreenViewModel.cs b/CRSim.ScreenSimulator/ViewModels/MetroPlatformScreenViewModel.cs index 8e1e822..d401f32 100644 --- a/CRSim.ScreenSimulator/ViewModels/MetroPlatformScreenViewModel.cs +++ b/CRSim.ScreenSimulator/ViewModels/MetroPlatformScreenViewModel.cs @@ -53,12 +53,13 @@ namespace CRSim.ScreenSimulator.ViewModels { var now = TimeService.GetDateTimeNow(); var today = now.Date; - if (_settings.LoadTodayOnly && today.Add((trainNumber.DepartureTime ?? trainNumber.ArrivalTime)!.Value).Add(trainNumber.Status.Value) < now) + var scheduleOffset = TrainStatus.GetScheduleOffset(trainNumber.Status); + if (_settings.LoadTodayOnly && today.Add((trainNumber.DepartureTime ?? trainNumber.ArrivalTime)!.Value).Add(scheduleOffset) < now) { continue; } DateTime? AdjustTime(TimeSpan? time) => - time.HasValue ? (today.Add(time.Value).Add(trainNumber.Status.Value) > now ? today.Add(time.Value) : today.Add(time.Value).AddDays(1)) : null; + time.HasValue ? (today.Add(time.Value).Add(scheduleOffset) > now ? today.Add(time.Value) : today.Add(time.Value).AddDays(1)) : null; TrainInfos.Add(new TrainInfo { diff --git a/CRSim/Converters/TimeSpanToStringConverter.cs b/CRSim/Converters/TimeSpanToStringConverter.cs index a7a3980..65f68d2 100644 --- a/CRSim/Converters/TimeSpanToStringConverter.cs +++ b/CRSim/Converters/TimeSpanToStringConverter.cs @@ -1,4 +1,5 @@ -using Microsoft.UI.Xaml.Data; +using CRSim.Core.Models; +using Microsoft.UI.Xaml.Data; using System.Globalization; namespace CRSim.Converters @@ -10,10 +11,15 @@ namespace CRSim.Converters public string Culture { get; set; } = "zh-CN"; public string NullString { get; set; } = string.Empty; public string ZeroString { get; set; } = string.Empty; + public string UnknownString { get; set; } = string.Empty; public object Convert(object value, Type targetType, object parameter, string language) { if (value is TimeSpan timeSpan) { + if (TrainStatus.IsDelayUnknown(timeSpan)) + { + return UnknownString; + } if (timeSpan == TimeSpan.Zero) { return ZeroString; diff --git a/CRSim/ViewModels/StationManagementPageViewModel.cs b/CRSim/ViewModels/StationManagementPageViewModel.cs index 816aa4b..4191f06 100644 --- a/CRSim/ViewModels/StationManagementPageViewModel.cs +++ b/CRSim/ViewModels/StationManagementPageViewModel.cs @@ -597,6 +597,7 @@ public partial class StationManagementPageViewModel : ObservableObject Status = worksheet.Cells[row, 10].Text switch { "停运" => null, + "晚点未定" => TrainStatus.DelayUnknown, "正点" => TimeSpan.Zero, var t when t.StartsWith('-') && TimeSpan.TryParseExact(t[1..], @"hh\:mm", null, out var ts) => -ts, @@ -645,6 +646,7 @@ public partial class StationManagementPageViewModel : ObservableObject worksheet.Cells[i + 2, 10].Value = TrainStops[i].Status switch { null => "停运", + var ts when TrainStatus.IsDelayUnknown(ts) => "晚点未定", var ts when ts == TimeSpan.Zero => "正点", var ts when ts < TimeSpan.Zero => $"-{ts.Value.Duration():hh\\:mm}", var ts => ts.Value.ToString(@"hh\:mm") @@ -855,4 +857,4 @@ public partial class StationManagementPageViewModel : ObservableObject { OnPropertyChanged(nameof(FilteredStationNames)); } -} \ No newline at end of file +} diff --git a/CRSim/Views/DialogContents/TrainStopDialog.xaml b/CRSim/Views/DialogContents/TrainStopDialog.xaml index cd08d30..49a0377 100644 --- a/CRSim/Views/DialogContents/TrainStopDialog.xaml +++ b/CRSim/Views/DialogContents/TrainStopDialog.xaml @@ -10,24 +10,29 @@ xmlns:selectors="using:CRSim.Selectors" mc:Ignorable="d"> - - + + - - + + + TicketCheckTemplate="{StaticResource TicketCheckTemplate}"/> - + - + @@ -38,47 +43,136 @@ - - - - - - - - + + + + + + + + 正点 + 早点 + 晚点 + 停运 + 晚点未定 + + - + 始发站 中间站 终到站 - - - - - - - + + + + + + + - - - - - - + + + + + + - + - - - - + + + + diff --git a/CRSim/Views/DialogContents/TrainStopDialog.xaml.cs b/CRSim/Views/DialogContents/TrainStopDialog.xaml.cs index b392449..63b1aff 100644 --- a/CRSim/Views/DialogContents/TrainStopDialog.xaml.cs +++ b/CRSim/Views/DialogContents/TrainStopDialog.xaml.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - namespace CRSim.Views.DialogContents; public sealed partial class TrainStopDialog : Page { @@ -24,6 +22,8 @@ public sealed partial class TrainStopDialog : Page } InitializeComponent(); _onValidityChanged = onValidityChanged; + StatusComboBox.SelectedIndex = 0; + UpdateStatusInputState(); Validate(this, EventArgs.Empty); } public TrainStopDialog(List waitingAreas, List platforms, TrainStop trainStop, Action onValidityChanged) @@ -63,14 +63,35 @@ public sealed partial class TrainStopDialog : Page EndMinute.IsEnabled = false; StationKindPanelRadioButtons.SelectedIndex = 2; } + if (trainStop.Status is null) { - SuspendToggleSwitch.IsOn = true; + StatusComboBox.SelectedItem = "停运"; + StatusMinutesTextBox.Text = "0"; + } + else if (TrainStatus.IsDelayUnknown(trainStop.Status)) + { + StatusComboBox.SelectedItem = "晚点未定"; + StatusMinutesTextBox.Text = "0"; + } + else if (trainStop.Status.Value > TimeSpan.Zero) + { + StatusComboBox.SelectedItem = "晚点"; + StatusMinutesTextBox.Text = Math.Abs((int)Math.Round(trainStop.Status.Value.TotalMinutes)).ToString(); + } + else if (trainStop.Status.Value < TimeSpan.Zero) + { + StatusComboBox.SelectedItem = "早点"; + StatusMinutesTextBox.Text = Math.Abs((int)Math.Round(trainStop.Status.Value.TotalMinutes)).ToString(); } else { - StatusTextBox.Text = trainStop.Status.Value.TotalMinutes.ToString(); + StatusComboBox.SelectedItem = "正点"; + StatusMinutesTextBox.Text = "0"; } + + UpdateStatusInputState(); + Validate(this, EventArgs.Empty); originalTrainStop = trainStop; } private static bool ValidateTime(string input, int maxValue) @@ -81,6 +102,13 @@ public sealed partial class TrainStopDialog : Page } return false; } + + private bool IsEarlyOrLateSelected() + { + if (StatusComboBox?.SelectedItem is not string status) return false; + return status == "早点" || status == "晚点"; + } + private void Validate(object sender, object e) { if (StartHour is null) return; @@ -89,6 +117,11 @@ public sealed partial class TrainStopDialog : Page (!StartMinute.IsEnabled || ValidateTime(StartMinute.Text, 60)) && (!EndHour.IsEnabled || ValidateTime(EndHour.Text, 24)) && (!EndMinute.IsEnabled || ValidateTime(EndMinute.Text, 60)); + + bool isStatusValid = + !IsEarlyOrLateSelected() || + (int.TryParse(StatusMinutesTextBox.Text, out int minutes) && minutes > 0); + bool isValid = (!WaitingAreasList.IsEnabled || WaitingAreasList.SelectedItems?.Count != 0) && @@ -98,7 +131,8 @@ public sealed partial class TrainStopDialog : Page !string.IsNullOrWhiteSpace(DepartureTextBox.Text) && int.TryParse(LengthTextBox.Text, out int i) && i > 0 && PlatformComboBox.SelectedItem != null && - int.TryParse(StatusTextBox.Text, out int m); + StatusComboBox.SelectedItem != null && + isStatusValid; _onValidityChanged?.Invoke(isValid); } @@ -119,8 +153,25 @@ public sealed partial class TrainStopDialog : Page Validate(this, EventArgs.Empty); if (EndHour.Text.Length == 2) EndMinute.Focus(FocusState.Programmatic); } + + private void StatusComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UpdateStatusInputState(); + Validate(this, EventArgs.Empty); + } + public void GenerateTrainStop() { + var selectedStatus = StatusComboBox.SelectedItem as string ?? "正点"; + TimeSpan? statusValue = selectedStatus switch + { + "停运" => null, + "晚点未定" => TrainStatus.DelayUnknown, + "晚点" => TimeSpan.FromMinutes(int.Parse(StatusMinutesTextBox.Text)), + "早点" => -TimeSpan.FromMinutes(int.Parse(StatusMinutesTextBox.Text)), + _ => TimeSpan.Zero + }; + GeneratedTrainStop = new TrainStop { Number = NumberTextBox.Text, @@ -134,10 +185,16 @@ public sealed partial class TrainStopDialog : Page Platform = (string)PlatformComboBox.SelectedItem, Length = int.Parse(LengthTextBox.Text), Landmark = (string)LandmarkComboBox.SelectedItem == "无" ? null : (string)LandmarkComboBox.SelectedItem, - Status = SuspendToggleSwitch.IsOn ? null : TimeSpan.FromMinutes(int.Parse(StatusTextBox.Text)) + Status = statusValue }; } + private void UpdateStatusInputState() + { + if (StatusMinutesTextBox is null || StatusComboBox is null) return; + StatusMinutesTextBox.IsEnabled = IsEarlyOrLateSelected(); + } + private void StationKindPanelRadioButtons_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (StartHour == null) return; @@ -199,4 +256,4 @@ public sealed partial class TrainStopDialog : Page } } -} \ No newline at end of file +} diff --git a/CRSim/Views/StationManagementPage.xaml b/CRSim/Views/StationManagementPage.xaml index e7e09f3..476682e 100644 --- a/CRSim/Views/StationManagementPage.xaml +++ b/CRSim/Views/StationManagementPage.xaml @@ -19,7 +19,7 @@ d:DataContext="{d:DesignInstance Type=viewmodels:StationManagementPageViewModel}"> - +