diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9491a2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,363 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
\ No newline at end of file
diff --git a/CRSim.Core/CRSim.Core.csproj b/CRSim.Core/CRSim.Core.csproj
new file mode 100644
index 0000000..42cb3d9
--- /dev/null
+++ b/CRSim.Core/CRSim.Core.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net9.0
+ enable
+ enable
+ 13.0
+
+
+
diff --git a/CRSim.Core/Converters/TimeOnlyJsonConverter.cs b/CRSim.Core/Converters/TimeOnlyJsonConverter.cs
new file mode 100644
index 0000000..67085d6
--- /dev/null
+++ b/CRSim.Core/Converters/TimeOnlyJsonConverter.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Converters
+{
+ public class TimeOnlyJsonConverter : JsonConverter
+ {
+ private const string TimeFormat = "HH:mm:ss";
+
+ public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ string timeString = reader.GetString();
+ if (DateTime.TryParseExact(timeString, TimeFormat, null, System.Globalization.DateTimeStyles.None, out DateTime result))
+ {
+ return result;
+ }
+ }
+ return null; // 处理 null 值
+ }
+
+ public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
+ {
+ if (value.HasValue)
+ {
+ writer.WriteStringValue(value.Value.ToString(TimeFormat));
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+ }
+
+}
diff --git a/CRSim.Core/Converters/TimeSpanConverter.cs b/CRSim.Core/Converters/TimeSpanConverter.cs
new file mode 100644
index 0000000..2f38646
--- /dev/null
+++ b/CRSim.Core/Converters/TimeSpanConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Converters
+{
+ public class TimeSpanConverter : JsonConverter
+ {
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ {
+ writer.WriteNumberValue(value.TotalMinutes); // 以分钟为单位进行序列化
+ }
+
+ // 反序列化:将数值转换回 TimeSpan 类型
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // 这里假设 JSON 存储的是分钟的数值
+ double minutes = reader.GetDouble();
+ return TimeSpan.FromMinutes(minutes);
+ }
+ }
+}
diff --git a/CRSim.Core/Models/Json.cs b/CRSim.Core/Models/Json.cs
new file mode 100644
index 0000000..6e0ab43
--- /dev/null
+++ b/CRSim.Core/Models/Json.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Models
+{
+ public class Json
+ {
+ public List Stations { get; set; }
+ public List TrainNumbers { get; set; }
+ }
+}
diff --git a/CRSim.Core/Models/Settings.cs b/CRSim.Core/Models/Settings.cs
new file mode 100644
index 0000000..2bfe991
--- /dev/null
+++ b/CRSim.Core/Models/Settings.cs
@@ -0,0 +1,29 @@
+using CRSim.Core.Converters;
+using System.Text.Json.Serialization;
+
+namespace CRSim.Core.Models
+{
+ public class Settings
+ {
+ [JsonConverter(typeof(TimeSpanConverter))]
+ public TimeSpan TimeOffset { get; set; } = TimeSpan.Zero;
+
+ [JsonConverter(typeof(TimeSpanConverter))]
+ public TimeSpan DepartureCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(20);
+
+ [JsonConverter(typeof(TimeSpanConverter))]
+ public TimeSpan PassingCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(10);
+
+ ///
+ /// 直到发车前多久停止显示
+ ///
+ [JsonConverter(typeof(TimeSpanConverter))]
+ public TimeSpan StopDisplayUntilDepartureDuration { get; set; } = TimeSpan.FromMinutes(1);
+
+ [JsonConverter(typeof(TimeSpanConverter))]
+ public TimeSpan StopCheckInAdvanceDuration { get; set; } = TimeSpan.FromMinutes(2);
+
+ public int MaxPages { get; set; } = 3;
+ public int SwitchPageSeconds { get; set; } = 20;
+ }
+}
diff --git a/CRSim.Core/Models/Station.cs b/CRSim.Core/Models/Station.cs
new file mode 100644
index 0000000..417ef8a
--- /dev/null
+++ b/CRSim.Core/Models/Station.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Models
+{
+ public class Station
+ {
+ public string Name { get; set; }
+
+ public List WaitingAreas { get; set; } = [];
+
+ [JsonIgnore]
+ public List TicketChecks
+ {
+ get
+ {
+ List ticketChecks = [];
+ ticketChecks.AddRange([.. WaitingAreas.SelectMany(wa => wa.TicketChecks)]);
+ return ticketChecks;
+ }
+ }
+
+ public List StationStops { get; set; } = [];
+
+ public List Platforms { get; set; } = [];
+ }
+}
diff --git a/CRSim.Core/Models/StationStop.cs b/CRSim.Core/Models/StationStop.cs
new file mode 100644
index 0000000..1ec520c
--- /dev/null
+++ b/CRSim.Core/Models/StationStop.cs
@@ -0,0 +1,31 @@
+using CRSim.Core.Converters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Models
+{
+ public class StationStop
+ {
+ public string Number { get; set; }
+
+ [JsonConverter(typeof(TimeOnlyJsonConverter))]
+ public DateTime? ArrivalTime { get; set; }
+
+ [JsonConverter(typeof(TimeOnlyJsonConverter))]
+ public DateTime? DepartureTime { get; set; }
+
+ public List TicketChecks { get; set; }
+
+ public string Origin { get; set; }
+
+ public string Terminal { get; set; }
+
+ public string Platform { get; set; }
+
+ public string? Landmark { get; set; }
+ }
+}
diff --git a/CRSim.Core/Models/TrainNumber.cs b/CRSim.Core/Models/TrainNumber.cs
new file mode 100644
index 0000000..d244743
--- /dev/null
+++ b/CRSim.Core/Models/TrainNumber.cs
@@ -0,0 +1,9 @@
+
+namespace CRSim.Core.Models
+{
+ public class TrainNumber
+ {
+ public string Number { get; set; }
+ public List TimeTable { get; set; } = [];
+ }
+}
diff --git a/CRSim.Core/Models/TrainStop.cs b/CRSim.Core/Models/TrainStop.cs
new file mode 100644
index 0000000..902bb85
--- /dev/null
+++ b/CRSim.Core/Models/TrainStop.cs
@@ -0,0 +1,21 @@
+using CRSim.Core.Converters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Models
+{
+ public class TrainStop
+ {
+ public string Station { get; set; }
+
+ [JsonConverter(typeof(TimeOnlyJsonConverter))]
+ public DateTime? ArrivalTime { get; set; }
+
+ [JsonConverter(typeof(TimeOnlyJsonConverter))]
+ public DateTime? DepartureTime { get; set; }
+ }
+}
diff --git a/CRSim.Core/Models/WaitingArea.cs b/CRSim.Core/Models/WaitingArea.cs
new file mode 100644
index 0000000..440166f
--- /dev/null
+++ b/CRSim.Core/Models/WaitingArea.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Models
+{
+ public class WaitingArea
+ {
+ public string Name { get; set; }
+ public List TicketChecks { get; set; } = [];
+ }
+}
diff --git a/CRSim.Core/Services/DatabaseService.cs b/CRSim.Core/Services/DatabaseService.cs
new file mode 100644
index 0000000..8d3ba9a
--- /dev/null
+++ b/CRSim.Core/Services/DatabaseService.cs
@@ -0,0 +1,201 @@
+using CRSim.Core.Models;
+using System.Text.Json;
+
+namespace CRSim.Core.Services
+{
+ public class DatabaseService : IDatabaseService
+ {
+ private readonly string _jsonFilePath;
+ private List _stations;
+ private List _trainNumbers;
+
+ private readonly JsonSerializerOptions options = new() { WriteIndented = true };
+
+ public DatabaseService(string jsonFilePath)
+ {
+ ImportData(jsonFilePath);
+ _jsonFilePath = jsonFilePath;
+ }
+
+ public List GetAllStations()
+ {
+ return _stations;
+ }
+
+ public void ImportData(string jsonFilePath)
+ {
+ try
+ {
+ var json = File.ReadAllText(jsonFilePath);
+ var data = JsonSerializer.Deserialize(json);
+ _stations = data.Stations;
+ _trainNumbers = data.TrainNumbers;
+ }
+ catch
+ {
+ _stations = [];
+ _trainNumbers = [];
+ }
+ }
+ public Station GetStationByName(string name)
+ {
+ return _stations.FirstOrDefault(x => x.Name == name);
+ }
+ public TrainNumber GetTrainNumberByNumber(string number)
+ {
+ return _trainNumbers.FirstOrDefault(x => x.Number == number);
+ }
+ public void AddStationByName(string name)
+ {
+ _stations.Add(new Station()
+ {
+ Name = name,
+ });
+ }
+
+ public void DeleteStation(Station station)
+ {
+ _stations.Remove(station);
+ }
+
+ public void DeleteTrainNumber(TrainNumber trainNumber)
+ {
+ _trainNumbers.Remove(trainNumber);
+ }
+
+ public void UpdateStation(string stationName,Station station)
+ {
+ var targetStation = _stations.FirstOrDefault(x => x.Name == stationName);
+ if (targetStation != null)
+ {
+ // 直接更新 targetStation 中的内容
+ targetStation.Name = station.Name;
+ targetStation.WaitingAreas = station.WaitingAreas;
+ targetStation.StationStops = station.StationStops;
+ targetStation.Platforms = station.Platforms;
+ }
+ }
+
+ public void UpdateTimeTable(TrainNumber trainNumber, List trainStopsList)
+ {
+ var targetTrainNumber = _trainNumbers.FirstOrDefault(x => x.Number == trainNumber.Number);
+ targetTrainNumber.TimeTable = trainStopsList;
+ }
+
+ public async Task SaveData()
+ {
+ string json = JsonSerializer.Serialize(new Json()
+ {
+ Stations = _stations,
+ TrainNumbers = _trainNumbers
+ }, options);
+ await File.WriteAllTextAsync(_jsonFilePath, json);
+ }
+
+ public async Task ExportData(string p)
+ {
+ string json = JsonSerializer.Serialize(new Json()
+ {
+ Stations = _stations,
+ TrainNumbers = _trainNumbers
+ }, options);
+ await File.WriteAllTextAsync(p, json);
+ }
+
+ public List GetAllTrainNumbers()
+ {
+ return _trainNumbers;
+ }
+
+ public void AddTrainNumber(string number)
+ {
+ _trainNumbers.Add(new TrainNumber()
+ {
+ Number = number
+ });
+ }
+
+ public async Task ImportStationFrom7D(string path)
+ {
+ Station station = new() { Platforms = [], WaitingAreas = [], StationStops = [] };
+ var stationName = (await File.ReadAllTextAsync(path.Replace("车站广播系统.exe", "info.ini"))).Split("|")[0];
+ if (_stations.Any(x => x.Name == stationName))
+ {
+ _stations.Remove(GetStationByName(stationName));
+ }
+ var lines = await File.ReadAllLinesAsync(path.Replace("车站广播系统.exe", "data.csv"));
+ station.Name = stationName;
+ foreach (var line in lines)
+ {
+ var data = line.Split(',');
+ var waitingAreaName = string.IsNullOrWhiteSpace(data.Last()) ? "未知" : data.Last();
+ var platform = string.IsNullOrWhiteSpace(data[5]) ? "未知" : data[5];
+ var ticketCheck = string.IsNullOrWhiteSpace(data[4]) ? string.Empty : data[4];
+
+ if (!station.Platforms.Contains(platform))
+ {
+ station.Platforms.Add(platform);
+ }
+
+ var waitingArea = station.WaitingAreas.FirstOrDefault(x => x.Name == waitingAreaName);
+ if (waitingArea == null)
+ {
+ waitingArea = new WaitingArea() { Name = waitingAreaName };
+ station.WaitingAreas.Add(waitingArea);
+ }
+
+ foreach (var ticketCheckName in ticketCheck.Split('|'))
+ {
+ if (!string.IsNullOrWhiteSpace(ticketCheckName) && !waitingArea.TicketChecks.Contains(ticketCheckName))
+ {
+ waitingArea.TicketChecks.Add(ticketCheckName);
+ }
+ }
+
+ if (!string.IsNullOrEmpty(data[0]) && !station.StationStops.Any(x => x.Number == data[0]))
+ {
+ DateTime? arrivalTime = null;
+ DateTime? departureTime = null;
+
+ if (data[2] == stationName)
+ {
+ departureTime = DateTime.Parse(lines.FirstOrDefault(x => x.Split(",")[0] == data[0] && x.Split(",")[1].Contains("送车"))?.Split(",")[7] ?? string.Empty);
+ }
+ else if (data[3] == stationName)
+ {
+ arrivalTime = DateTime.Parse(data[7]);
+ }
+ else
+ {
+ arrivalTime = DateTime.Parse(data[7]);
+ departureTime = DateTime.Parse(lines.FirstOrDefault(x => x.Split(",")[0] == data[0] && x.Split(",")[1].Contains("送车"))?.Split(",")[7] ?? string.Empty);
+ }
+
+ station.StationStops.Add(new StationStop
+ {
+ Number = data[0],
+ Terminal = data[3],
+ Origin = data[2],
+ TicketChecks = [.. ticketCheck.Split('|')],
+ ArrivalTime = arrivalTime,
+ DepartureTime = departureTime,
+ Platform = platform,
+ Landmark = data[8] + "色" ?? null,
+ });
+ }
+ }
+ AddStation(station);
+ }
+
+ public void AddStation(Station station)
+ {
+ _stations.Add(station);
+ }
+
+ public void ClearData()
+ {
+ _stations.Clear();
+ _trainNumbers.Clear();
+ }
+ }
+}
diff --git a/CRSim.Core/Services/IDatabaseService.cs b/CRSim.Core/Services/IDatabaseService.cs
new file mode 100644
index 0000000..14030c4
--- /dev/null
+++ b/CRSim.Core/Services/IDatabaseService.cs
@@ -0,0 +1,24 @@
+using CRSim.Core.Models;
+
+namespace CRSim.Core.Services
+{
+ public interface IDatabaseService
+ {
+ List GetAllStations();
+ List GetAllTrainNumbers();
+ Station GetStationByName(string name);
+ TrainNumber GetTrainNumberByNumber(string number);
+ void AddStationByName(string name);
+ void AddStation(Station station);
+ Task ImportStationFrom7D(string path);
+ void AddTrainNumber(string number);
+ void DeleteStation(Station station);
+ void DeleteTrainNumber(TrainNumber trainNumber);
+ void UpdateStation(string stationName,Station station);
+ void UpdateTimeTable(TrainNumber trainNumber, List trainStopsList);
+ Task SaveData();
+ void ImportData(string jsonFilePath);
+ Task ExportData(string path);
+ void ClearData();
+ }
+}
diff --git a/CRSim.Core/Services/INetworkService.cs b/CRSim.Core/Services/INetworkService.cs
new file mode 100644
index 0000000..d857dc8
--- /dev/null
+++ b/CRSim.Core/Services/INetworkService.cs
@@ -0,0 +1,15 @@
+using CRSim.Core.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Services
+{
+ public interface INetworkService
+ {
+ Task?> GetTrainStopsAsync(string number);
+ Task> GetStationStopsAsync(string name);
+ }
+}
diff --git a/CRSim.Core/Services/ISettingsService.cs b/CRSim.Core/Services/ISettingsService.cs
new file mode 100644
index 0000000..da71bde
--- /dev/null
+++ b/CRSim.Core/Services/ISettingsService.cs
@@ -0,0 +1,15 @@
+using CRSim.Core.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Services
+{
+ public interface ISettingsService
+ {
+ void SaveSettings();
+ Settings GetSettings();
+ }
+}
diff --git a/CRSim.Core/Services/ITimeService.cs b/CRSim.Core/Services/ITimeService.cs
new file mode 100644
index 0000000..158a549
--- /dev/null
+++ b/CRSim.Core/Services/ITimeService.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Timers;
+
+namespace CRSim.Core.Services
+{
+ public interface ITimeService
+ {
+ event EventHandler OneSecondElapsed; // 1秒触发的事件
+ event EventHandler RefreshSecondsElapsed; // 20秒触发的事件
+
+ void Start();
+ void Stop();
+
+ DateTime GetDateTimeNow();
+ }
+}
\ No newline at end of file
diff --git a/CRSim.Core/Services/NetworkService.cs b/CRSim.Core/Services/NetworkService.cs
new file mode 100644
index 0000000..4af864e
--- /dev/null
+++ b/CRSim.Core/Services/NetworkService.cs
@@ -0,0 +1,147 @@
+using CRSim.Core.Models;
+using System.Net;
+using System.Text.Json;
+using System.Xml;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace CRSim.Core.Services
+{
+ public class NetworkService(IDatabaseService databaseService) : INetworkService
+ {
+ private readonly IDatabaseService _databaseService = databaseService;
+
+ public async Task?> GetTrainStopsAsync(string number)
+ {
+ try
+ {
+ HttpClient httpClient = new();
+ httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0");
+ HttpContent content = new FormUrlEncodedContent(new Dictionary()
+ {
+ {"trainCode",number },
+ {"startDay",DateTime.Now.ToString("yyyyMMdd")},
+ {"startTime",""},
+ {"endDay",""},
+ {"endTime",""}
+ });
+ var response = await httpClient.PostAsync($"https://mobile.12306.cn/wxxcx/wechat/main/travelServiceQrcodeTrainInfo",content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
+ var timeTable = new List();
+ if (doc.RootElement.GetProperty("data").GetProperty("trainDetail").TryGetProperty("stopTime", out JsonElement jsonElement))
+ {
+ foreach (var item in jsonElement.EnumerateArray())
+ {
+ var stationName = item.GetProperty("stationName").GetString();
+ var arriveTimeStr = item.GetProperty("arriveTime").GetString();
+ var startTimeStr = item.GetProperty("startTime").GetString();
+ //var arriveDayStr = item.GetProperty("arriveDayStr").GetString();
+ DateTime? arriveTime = ParseTime(arriveTimeStr);
+ DateTime? startTime = ParseTime(startTimeStr);
+ if (arriveTime.HasValue && startTime.HasValue && startTime.Value < arriveTime.Value)
+ {
+ startTime = startTime.Value.AddDays(1);
+ }
+ if (arriveTimeStr == startTimeStr)
+ {
+ if (timeTable.Count == 0)
+ {
+ arriveTime = null;
+ }
+ else
+ {
+ startTime = null;
+ }
+ }
+ var station = _databaseService.GetAllStations().Where(s => s.Name == stationName).FirstOrDefault();
+ List ticketChecks = station == null ? [] : station.TicketChecks;
+ timeTable.Add(new TrainStop
+ {
+ Station = stationName,
+ ArrivalTime = arriveTime,
+ DepartureTime = startTime
+ });
+ }
+ }
+ return timeTable;
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ return null;
+ }
+ private static DateTime? ParseTime(string timeStr)
+ {
+ timeStr = string.Concat(timeStr.AsSpan(0, 2), ":", timeStr.AsSpan(2, 2));
+ if (string.IsNullOrWhiteSpace(timeStr) || timeStr == "----")
+ return null;
+ if (DateTime.TryParseExact(timeStr, "HH:mm", null, System.Globalization.DateTimeStyles.None, out var time))
+ {
+ return time;
+ }
+ return null;
+ }
+
+ public async Task> GetStationStopsAsync(string name)
+ {
+ var client = new HttpClient();
+ var stations = (await client.GetStringAsync("https://kyfw.12306.cn/otn/resources/js/framework/station_name.js")).Split("|||");
+ string tel = "";
+ foreach (string station in stations)
+ {
+ if (station == "';") continue;
+ if (station.Split("|")[1] == name)
+ {
+ tel = station.Split("|")[2];
+ }
+ }
+ if (tel == "")
+ {
+ return [];
+ }
+ var baseAddress = new Uri("https://mobile.12306.cn/wxxcx/wechat/bigScreen/queryTrainByStation");
+ var cookieContainer = new CookieContainer();
+ cookieContainer.Add(baseAddress, new Cookie("BIGipServerweixin_xiaochengxu", "2028077578.5670.0000"));
+ var handler = new HttpClientHandler() { CookieContainer = cookieContainer, UseCookies = true };
+
+ HttpContent content = new FormUrlEncodedContent(new Dictionary()
+ {
+ {"train_station_code",tel },
+ {"train_start_date",DateTime.Now.ToString("yyyyMMdd")}
+ });
+ client = new HttpClient(handler) { BaseAddress = baseAddress };
+ client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11581");
+ var response = await client.PostAsync(baseAddress, content);
+ var json = await response.Content.ReadAsStringAsync();
+ if (json.Contains("操作失败,请稍后重试")) return [];
+ var list = JsonDocument.Parse(json).RootElement.GetProperty("data").EnumerateArray();
+ if (!list.Any()) return [];
+ List stationStops = [];
+ foreach (var item in list)
+ {
+ var arriveTimeStr = item.GetProperty("arrive_time").GetString();
+ var startTimeStr = item.GetProperty("start_time").GetString();
+ DateTime? arriveTime = null;
+ DateTime? startTime = null;
+ if (arriveTimeStr != "----")
+ {
+ arriveTime = DateTime.Parse(arriveTimeStr);
+ }
+ if (startTimeStr != arriveTimeStr)
+ {
+ startTime = DateTime.Parse(startTimeStr);
+ }
+ if (arriveTime.HasValue && startTime.HasValue && startTime.Value < arriveTime.Value)
+ {
+ startTime = startTime.Value.AddDays(1);
+ }
+ stationStops.Add(new StationStop() { Number = item.GetProperty("station_train_code").ToString(), Terminal = item.GetProperty("end_station_name").ToString(), Origin = item.GetProperty("start_station_name").ToString(), ArrivalTime = arriveTime, DepartureTime = startTime });
+ }
+ return stationStops;
+ }
+ }
+}
diff --git a/CRSim.Core/Services/SettingsService.cs b/CRSim.Core/Services/SettingsService.cs
new file mode 100644
index 0000000..d34e43a
--- /dev/null
+++ b/CRSim.Core/Services/SettingsService.cs
@@ -0,0 +1,44 @@
+using CRSim.Core.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace CRSim.Core.Services
+{
+ public class SettingsService : ISettingsService
+ {
+ private readonly string _settingsFilePath;
+ private Settings _settings;
+ private readonly JsonSerializerOptions options = new() { WriteIndented = true };
+ public SettingsService(string settingsFilePath)
+ {
+ _settingsFilePath = settingsFilePath;
+ LoadSettings();
+ }
+ public void SaveSettings()
+ {
+ string jsonString = JsonSerializer.Serialize(_settings, options);
+ File.WriteAllText(_settingsFilePath, jsonString);
+ }
+ private void LoadSettings()
+ {
+ if (File.Exists(_settingsFilePath))
+ {
+ string jsonString = File.ReadAllText(_settingsFilePath);
+ _settings = JsonSerializer.Deserialize(jsonString);
+ }
+ else
+ {
+ _settings = new Settings();
+ }
+ }
+
+ public Settings GetSettings()
+ {
+ return _settings;
+ }
+ }
+}
diff --git a/CRSim.Core/Services/TimeService.cs b/CRSim.Core/Services/TimeService.cs
new file mode 100644
index 0000000..0ea2b7d
--- /dev/null
+++ b/CRSim.Core/Services/TimeService.cs
@@ -0,0 +1,54 @@
+
+using CRSim.Core.Models;
+using System.Timers;
+
+namespace CRSim.Core.Services
+{
+ public class TimeService : ITimeService
+ {
+ private readonly Settings _settings;
+ private readonly System.Timers.Timer _oneSecondTimer;
+ private readonly System.Timers.Timer _twentySecondsTimer;
+
+ public event EventHandler OneSecondElapsed;
+ public event EventHandler RefreshSecondsElapsed;
+
+ public TimeService(ISettingsService settingsService)
+ {
+ _settings = settingsService.GetSettings();
+
+ _oneSecondTimer = new(1000);
+ _oneSecondTimer.Elapsed += OnOneSecondElapsed;
+
+ _twentySecondsTimer = new(_settings.SwitchPageSeconds*1000);
+ _twentySecondsTimer.Elapsed += OnTwentySecondsElapsed;
+ }
+
+ public void Start()
+ {
+ _oneSecondTimer.Start();
+ _twentySecondsTimer.Start();
+ }
+
+ public void Stop()
+ {
+ _oneSecondTimer.Stop();
+ _twentySecondsTimer.Stop();
+ }
+
+ private void OnOneSecondElapsed(object sender, ElapsedEventArgs e)
+ {
+ OneSecondElapsed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void OnTwentySecondsElapsed(object sender, ElapsedEventArgs e)
+ {
+ RefreshSecondsElapsed?.Invoke(this, EventArgs.Empty);
+ }
+
+ public DateTime GetDateTimeNow()
+ {
+ return DateTime.Now+ _settings.TimeOffset;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CRSim.sln b/CRSim.sln
new file mode 100644
index 0000000..d10d9cb
--- /dev/null
+++ b/CRSim.sln
@@ -0,0 +1,28 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35424.110 d17.12
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSim", "CRSim\CRSim.csproj", "{E16F847C-9278-42F5-ACDD-9FC96B45FFCF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSim.Core", "CRSim.Core\CRSim.Core.csproj", "{C72B16CC-AA3E-4E0F-A424-07D3987A9FD9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E16F847C-9278-42F5-ACDD-9FC96B45FFCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E16F847C-9278-42F5-ACDD-9FC96B45FFCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E16F847C-9278-42F5-ACDD-9FC96B45FFCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E16F847C-9278-42F5-ACDD-9FC96B45FFCF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C72B16CC-AA3E-4E0F-A424-07D3987A9FD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C72B16CC-AA3E-4E0F-A424-07D3987A9FD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C72B16CC-AA3E-4E0F-A424-07D3987A9FD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C72B16CC-AA3E-4E0F-A424-07D3987A9FD9}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/CRSim/App.xaml b/CRSim/App.xaml
new file mode 100644
index 0000000..e2192e9
--- /dev/null
+++ b/CRSim/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/App.xaml.cs b/CRSim/App.xaml.cs
new file mode 100644
index 0000000..5ad7ec5
--- /dev/null
+++ b/CRSim/App.xaml.cs
@@ -0,0 +1,74 @@
+namespace CRSim
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ private static readonly IHost _host = Host.CreateDefaultBuilder()
+ .ConfigureServices((context, services) =>
+ {
+ services.AddSingleton(sp => new SettingsService("app.config"));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp => new DatabaseService("data.json"));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ }).Build();
+
+ [STAThread]
+ public static void Main()
+ {
+ _host.Start();
+
+ App app = new();
+ app.InitializeComponent();
+ Current.Resources["ServiceProvider"] = _host.Services;
+ app.MainWindow = _host.Services.GetRequiredService();
+
+ _host.Services.GetRequiredService().Start();
+ _host.Services.GetRequiredService().SetOwner(app.MainWindow);
+
+ app.MainWindow.Visibility = Visibility.Visible;
+ app.Run();
+ }
+ }
+
+}
diff --git a/CRSim/AssemblyInfo.cs b/CRSim/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/CRSim/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/CRSim/Assets/Advertisement.mp4 b/CRSim/Assets/Advertisement.mp4
new file mode 100644
index 0000000..f2aa196
Binary files /dev/null and b/CRSim/Assets/Advertisement.mp4 differ
diff --git a/CRSim/Assets/CRSimIcon.ico b/CRSim/Assets/CRSimIcon.ico
new file mode 100644
index 0000000..55fe937
Binary files /dev/null and b/CRSim/Assets/CRSimIcon.ico differ
diff --git a/CRSim/Assets/CRSimIcon.png b/CRSim/Assets/CRSimIcon.png
new file mode 100644
index 0000000..d12d2c6
Binary files /dev/null and b/CRSim/Assets/CRSimIcon.png differ
diff --git a/CRSim/Assets/ControlImages/Data Management.png b/CRSim/Assets/ControlImages/Data Management.png
new file mode 100644
index 0000000..3b105a3
Binary files /dev/null and b/CRSim/Assets/ControlImages/Data Management.png differ
diff --git a/CRSim/Assets/ControlImages/Gate Simulation.png b/CRSim/Assets/ControlImages/Gate Simulation.png
new file mode 100644
index 0000000..61e0602
Binary files /dev/null and b/CRSim/Assets/ControlImages/Gate Simulation.png differ
diff --git a/CRSim/Assets/ControlImages/Screen Simulation.png b/CRSim/Assets/ControlImages/Screen Simulation.png
new file mode 100644
index 0000000..18dc1cd
Binary files /dev/null and b/CRSim/Assets/ControlImages/Screen Simulation.png differ
diff --git a/CRSim/Assets/ControlImages/Start Simulation.png b/CRSim/Assets/ControlImages/Start Simulation.png
new file mode 100644
index 0000000..d70a55c
Binary files /dev/null and b/CRSim/Assets/ControlImages/Start Simulation.png differ
diff --git a/CRSim/Assets/ControlImages/Station Management.png b/CRSim/Assets/ControlImages/Station Management.png
new file mode 100644
index 0000000..2aca4c3
Binary files /dev/null and b/CRSim/Assets/ControlImages/Station Management.png differ
diff --git a/CRSim/Assets/ControlImages/TVM Simulation.png b/CRSim/Assets/ControlImages/TVM Simulation.png
new file mode 100644
index 0000000..acd846c
Binary files /dev/null and b/CRSim/Assets/ControlImages/TVM Simulation.png differ
diff --git a/CRSim/Assets/ControlImages/TrainNumber Management.png b/CRSim/Assets/ControlImages/TrainNumber Management.png
new file mode 100644
index 0000000..7562c4d
Binary files /dev/null and b/CRSim/Assets/ControlImages/TrainNumber Management.png differ
diff --git a/CRSim/Assets/HomeHeaderTiles/CRSim-Icon.ico b/CRSim/Assets/HomeHeaderTiles/CRSim-Icon.ico
new file mode 100644
index 0000000..55fe937
Binary files /dev/null and b/CRSim/Assets/HomeHeaderTiles/CRSim-Icon.ico differ
diff --git a/CRSim/Assets/HomeHeaderTiles/Header-QQ.png b/CRSim/Assets/HomeHeaderTiles/Header-QQ.png
new file mode 100644
index 0000000..17b4515
Binary files /dev/null and b/CRSim/Assets/HomeHeaderTiles/Header-QQ.png differ
diff --git a/CRSim/Assets/HomeHeaderTiles/Header-Store.dark.png b/CRSim/Assets/HomeHeaderTiles/Header-Store.dark.png
new file mode 100644
index 0000000..d5fbc1e
Binary files /dev/null and b/CRSim/Assets/HomeHeaderTiles/Header-Store.dark.png differ
diff --git a/CRSim/Assets/HomeHeaderTiles/Header-bilibili.png b/CRSim/Assets/HomeHeaderTiles/Header-bilibili.png
new file mode 100644
index 0000000..d0d2c10
Binary files /dev/null and b/CRSim/Assets/HomeHeaderTiles/Header-bilibili.png differ
diff --git a/CRSim/Assets/StyleImages/BeijingXi/PrimaryScreen.png b/CRSim/Assets/StyleImages/BeijingXi/PrimaryScreen.png
new file mode 100644
index 0000000..0ba64bd
Binary files /dev/null and b/CRSim/Assets/StyleImages/BeijingXi/PrimaryScreen.png differ
diff --git a/CRSim/Assets/StyleImages/ChengduDong/PrimaryTicketCheckScreen.png b/CRSim/Assets/StyleImages/ChengduDong/PrimaryTicketCheckScreen.png
new file mode 100644
index 0000000..2212bfa
Binary files /dev/null and b/CRSim/Assets/StyleImages/ChengduDong/PrimaryTicketCheckScreen.png differ
diff --git a/CRSim/Assets/StyleImages/ChengduDong/SecondaryScreen.png b/CRSim/Assets/StyleImages/ChengduDong/SecondaryScreen.png
new file mode 100644
index 0000000..1b8e729
Binary files /dev/null and b/CRSim/Assets/StyleImages/ChengduDong/SecondaryScreen.png differ
diff --git a/CRSim/Assets/StyleImages/ChengduMetro/ChengduMetro-Icon.png b/CRSim/Assets/StyleImages/ChengduMetro/ChengduMetro-Icon.png
new file mode 100644
index 0000000..af0da64
Binary files /dev/null and b/CRSim/Assets/StyleImages/ChengduMetro/ChengduMetro-Icon.png differ
diff --git a/CRSim/Assets/StyleImages/ChengduMetro/PlatformScreen.png b/CRSim/Assets/StyleImages/ChengduMetro/PlatformScreen.png
new file mode 100644
index 0000000..630c7fd
Binary files /dev/null and b/CRSim/Assets/StyleImages/ChengduMetro/PlatformScreen.png differ
diff --git a/CRSim/Assets/StyleImages/Guangyuan/PrimaryScreen.png b/CRSim/Assets/StyleImages/Guangyuan/PrimaryScreen.png
new file mode 100644
index 0000000..eecd34e
Binary files /dev/null and b/CRSim/Assets/StyleImages/Guangyuan/PrimaryScreen.png differ
diff --git a/CRSim/Assets/StyleImages/Hanzhong/Image.png b/CRSim/Assets/StyleImages/Hanzhong/Image.png
new file mode 100644
index 0000000..aa3dc70
Binary files /dev/null and b/CRSim/Assets/StyleImages/Hanzhong/Image.png differ
diff --git a/CRSim/Assets/StyleImages/Hanzhong/PlatformScreen.png b/CRSim/Assets/StyleImages/Hanzhong/PlatformScreen.png
new file mode 100644
index 0000000..f9ae53d
Binary files /dev/null and b/CRSim/Assets/StyleImages/Hanzhong/PlatformScreen.png differ
diff --git a/CRSim/Assets/StyleImages/Harbin/PrimaryScreen.png b/CRSim/Assets/StyleImages/Harbin/PrimaryScreen.png
new file mode 100644
index 0000000..c9cfcb4
Binary files /dev/null and b/CRSim/Assets/StyleImages/Harbin/PrimaryScreen.png differ
diff --git a/CRSim/Assets/StyleImages/Shanghai/OutsideScreen.png b/CRSim/Assets/StyleImages/Shanghai/OutsideScreen.png
new file mode 100644
index 0000000..5f91de6
Binary files /dev/null and b/CRSim/Assets/StyleImages/Shanghai/OutsideScreen.png differ
diff --git a/CRSim/Assets/win11-dashboard.dark.png b/CRSim/Assets/win11-dashboard.dark.png
new file mode 100644
index 0000000..eb649e0
Binary files /dev/null and b/CRSim/Assets/win11-dashboard.dark.png differ
diff --git a/CRSim/Assets/win11-dashboard.light.png b/CRSim/Assets/win11-dashboard.light.png
new file mode 100644
index 0000000..7d965f5
Binary files /dev/null and b/CRSim/Assets/win11-dashboard.light.png differ
diff --git a/CRSim/CRSim.csproj b/CRSim/CRSim.csproj
new file mode 100644
index 0000000..f1f3d31
--- /dev/null
+++ b/CRSim/CRSim.csproj
@@ -0,0 +1,95 @@
+
+
+
+ WinExe
+ net9.0-windows
+ enable
+ enable
+ true
+ 13.0
+ CRSim.App
+ Assets\CRSimIcon.ico
+ 1.2.3.0
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Code
+
+
+ Code
+
+
+ Code
+
+
+ Code
+
+
+ Code
+
+
+ Code
+
+
+ Code
+
+
+
+
diff --git a/CRSim/Controls/HeaderTile.xaml b/CRSim/Controls/HeaderTile.xaml
new file mode 100644
index 0000000..eaa4240
--- /dev/null
+++ b/CRSim/Controls/HeaderTile.xaml
@@ -0,0 +1,70 @@
+
+
+
+
diff --git a/CRSim/Controls/HeaderTile.xaml.cs b/CRSim/Controls/HeaderTile.xaml.cs
new file mode 100644
index 0000000..467ab69
--- /dev/null
+++ b/CRSim/Controls/HeaderTile.xaml.cs
@@ -0,0 +1,52 @@
+namespace CRSim.Controls
+{
+ ///
+ /// Interaction logic for HeaderTile.xaml
+ ///
+ public partial class HeaderTile : UserControl
+ {
+ public HeaderTile()
+ {
+ InitializeComponent();
+ }
+
+ public string Title
+ {
+ get { return (string)GetValue(TitleProperty); }
+ set { SetValue(TitleProperty, value); }
+ }
+ public static readonly DependencyProperty TitleProperty =
+ DependencyProperty.Register("Title", typeof(string), typeof(HeaderTile), new PropertyMetadata(""));
+
+ public string Description
+ {
+ get { return (string)GetValue(DescriptionProperty); }
+ set { SetValue(DescriptionProperty, value); }
+ }
+ public static readonly DependencyProperty DescriptionProperty =
+ DependencyProperty.Register("ColorExplanation", typeof(string), typeof(HeaderTile), new PropertyMetadata(""));
+
+ public string Link
+ {
+ get { return (string)GetValue(LinkProperty); }
+ set { SetValue(LinkProperty, value); }
+ }
+
+ public static readonly DependencyProperty LinkProperty =
+ DependencyProperty.Register("Link", typeof(string), typeof(HeaderTile), new PropertyMetadata(null));
+
+ public object Source
+ {
+ get { return (object)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public static readonly DependencyProperty SourceProperty =
+ DependencyProperty.Register("Source", typeof(object), typeof(HeaderTile), new PropertyMetadata(null));
+
+ private void RootButton_Click(object sender, RoutedEventArgs e)
+ {
+ Process.Start(new ProcessStartInfo(Link) { UseShellExecute = true });
+ }
+ }
+}
diff --git a/CRSim/Controls/PageHeader.xaml b/CRSim/Controls/PageHeader.xaml
new file mode 100644
index 0000000..3fcf70e
--- /dev/null
+++ b/CRSim/Controls/PageHeader.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRSim/Controls/PageHeader.xaml.cs b/CRSim/Controls/PageHeader.xaml.cs
new file mode 100644
index 0000000..da907be
--- /dev/null
+++ b/CRSim/Controls/PageHeader.xaml.cs
@@ -0,0 +1,43 @@
+namespace CRSim.Controls;
+
+public class PageHeader : Control
+{
+ public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
+ nameof(Title),
+ typeof(string),
+ typeof(PageHeader),
+ new PropertyMetadata(null)
+ );
+
+ public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
+ nameof(Description),
+ typeof(string),
+ typeof(PageHeader),
+ new PropertyMetadata(null)
+ );
+
+ public static readonly DependencyProperty ShowDescriptionProperty = DependencyProperty.Register(
+ nameof(ShowDescription),
+ typeof(bool),
+ typeof(PageHeader),
+ new PropertyMetadata(true)
+ );
+
+ public string? Title
+ {
+ get => (string?)GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value);
+ }
+
+ public string? Description
+ {
+ get => (string?)GetValue(DescriptionProperty);
+ set => SetValue(DescriptionProperty, value);
+ }
+
+ public bool ShowDescription
+ {
+ get => (bool)GetValue(ShowDescriptionProperty);
+ set => SetValue(ShowDescriptionProperty, value);
+ }
+}
\ No newline at end of file
diff --git a/CRSim/Controls/TileGallery.xaml b/CRSim/Controls/TileGallery.xaml
new file mode 100644
index 0000000..79835da
--- /dev/null
+++ b/CRSim/Controls/TileGallery.xaml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Controls/TileGallery.xaml.cs b/CRSim/Controls/TileGallery.xaml.cs
new file mode 100644
index 0000000..9b20fbd
--- /dev/null
+++ b/CRSim/Controls/TileGallery.xaml.cs
@@ -0,0 +1,61 @@
+namespace CRSim.Controls
+{
+ ///
+ /// Interaction logic for TileGallery.xaml
+ ///
+ public partial class TileGallery : UserControl
+ {
+ public TileGallery()
+ {
+ InitializeComponent();
+ }
+
+ private void ScrollBackButton_Click(object sender, RoutedEventArgs e)
+ {
+ double newOffSet = RootScrollViewer.HorizontalOffset - 210;
+ RootScrollViewer.ScrollToHorizontalOffset(newOffSet);
+ UpdateScrollButtonsVisibility(newOffSet);
+ }
+
+ private void ScrollForwardButton_Click(object sender, RoutedEventArgs e)
+ {
+ double newOffSet = RootScrollViewer.HorizontalOffset + 210;
+ RootScrollViewer.ScrollToHorizontalOffset(newOffSet);
+ UpdateScrollButtonsVisibility(newOffSet);
+ }
+
+ private void UpdateScrollButtonsVisibility()
+ {
+ double offset = RootScrollViewer.HorizontalOffset;
+ UpdateScrollButtonsVisibility(offset);
+ }
+
+ private void UpdateScrollButtonsVisibility(double newOffset)
+ {
+ ScrollBackButton.Visibility = Visibility.Visible;
+ ScrollForwardButton.Visibility = Visibility.Visible;
+
+ if (RootScrollViewer.ActualWidth < TilesPanel.ActualWidth)
+ {
+ if(newOffset == 0)
+ {
+ ScrollBackButton.Visibility = Visibility.Collapsed;
+ }
+ else if(newOffset >= RootScrollViewer.ScrollableWidth)
+ {
+ ScrollForwardButton.Visibility = Visibility.Collapsed;
+ }
+ }
+ else
+ {
+ ScrollBackButton.Visibility = Visibility.Collapsed;
+ ScrollForwardButton.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void RootScrollViewer_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ UpdateScrollButtonsVisibility();
+ }
+ }
+}
diff --git a/CRSim/Converters/DateTimeToStringConverter.cs b/CRSim/Converters/DateTimeToStringConverter.cs
new file mode 100644
index 0000000..7b85bd2
--- /dev/null
+++ b/CRSim/Converters/DateTimeToStringConverter.cs
@@ -0,0 +1,23 @@
+namespace CRSim.Converters
+{
+ public class DateTimeToStringConverter : IValueConverter
+ {
+
+ public string Format { get; set; } = "yyyy-MM-dd HH:mm:ss"; // 默认格式是 "yyyy-MM-dd HH:mm:ss"
+ public string Culture { get; set; } = "zh-CN";
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is DateTime dateTime && dateTime != new DateTime())
+ {
+ var CultureInfo = new CultureInfo(Culture);
+ return dateTime.ToString(Format,CultureInfo);
+ }
+ return string.Empty;
+ }
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // 通常不需要实现 ConvertBack
+ return null;
+ }
+ }
+}
diff --git a/CRSim/Converters/EmptyToVisibilityConverter.cs b/CRSim/Converters/EmptyToVisibilityConverter.cs
new file mode 100644
index 0000000..37f6140
--- /dev/null
+++ b/CRSim/Converters/EmptyToVisibilityConverter.cs
@@ -0,0 +1,23 @@
+namespace CRSim.Converters
+{
+ public class EmptyToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is string str)
+ {
+ return string.IsNullOrWhiteSpace(str) ? Visibility.Collapsed : Visibility.Visible;
+ }
+ if(value is int i)
+ {
+ return i == 0 ? Visibility.Collapsed : Visibility.Visible;
+ }
+ return value is null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/LandmarkToStringConverter.cs b/CRSim/Converters/LandmarkToStringConverter.cs
new file mode 100644
index 0000000..a2ec5c0
--- /dev/null
+++ b/CRSim/Converters/LandmarkToStringConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Converters
+{
+ public class LandmarkToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if(value is null)
+ {
+ return "/";
+ }
+ return value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/MetroDateTimeToStringConverter.cs b/CRSim/Converters/MetroDateTimeToStringConverter.cs
new file mode 100644
index 0000000..6b834e3
--- /dev/null
+++ b/CRSim/Converters/MetroDateTimeToStringConverter.cs
@@ -0,0 +1,47 @@
+namespace CRSim.Converters
+{
+ public class MetroDateTimeToStringConverter : IValueConverter
+ {
+ private ITimeService _timeService;
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
+ _timeService = serviceProvider.GetRequiredService();
+ if (value is TrainInfo trainInfo && parameter is string para)
+ {
+ if(trainInfo.TrainNumber == null)
+ {
+ return "请耐心等待";
+ }
+ var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - _timeService.GetDateTimeNow();
+ bool isArrivalTimeNull = trainInfo.ArrivalTime == null;
+ var checkInAdvanceDuration = TimeSpan.FromMinutes(1);
+ if (timeDifference < checkInAdvanceDuration)
+ {
+ if (para == "TextBlock")
+ {
+ if (!isArrivalTimeNull)
+ {
+ if(trainInfo.ArrivalTime - _timeService.GetDateTimeNow() > TimeSpan.Zero)
+ {
+ return "即将进站";
+ }
+ }
+ return "列车进站";
+ }
+ }
+ else
+ {
+ if (para == "StackPanel")
+ return Math.Round((trainInfo.ArrivalTime - _timeService.GetDateTimeNow()).Value.TotalMinutes).ToString();
+ }
+ }
+ return string.Empty;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/MetroDateTimeToVisibilityConverter.cs b/CRSim/Converters/MetroDateTimeToVisibilityConverter.cs
new file mode 100644
index 0000000..7cf71d1
--- /dev/null
+++ b/CRSim/Converters/MetroDateTimeToVisibilityConverter.cs
@@ -0,0 +1,37 @@
+namespace CRSim.Converters
+{
+ public class MetroDateTimeToVisibilityConverter : IValueConverter
+ {
+ private ITimeService _timeService;
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
+ _timeService = serviceProvider.GetRequiredService();
+ if (value is TrainInfo trainInfo && parameter is string para)
+ {
+ var timeDifference = (trainInfo.ArrivalTime ?? DateTime.MinValue) - _timeService.GetDateTimeNow();
+ var checkInAdvanceDuration = TimeSpan.FromMinutes(1);
+ if (timeDifference < checkInAdvanceDuration)
+ {
+ if (para == "TextBlock")
+ return Visibility.Visible;
+ else if (para == "StackPanel")
+ return Visibility.Collapsed;
+ }
+ else
+ {
+ if (para == "TextBlock")
+ return Visibility.Collapsed;
+ else if (para == "StackPanel")
+ return Visibility.Visible;
+ }
+ }
+ return Visibility.Collapsed;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/NullToVisibilityConverter.cs b/CRSim/Converters/NullToVisibilityConverter.cs
new file mode 100644
index 0000000..36336cf
--- /dev/null
+++ b/CRSim/Converters/NullToVisibilityConverter.cs
@@ -0,0 +1,17 @@
+namespace CRSim.Converters;
+
+///
+/// Converts a null value to Visibility.Collapsed
+///
+internal sealed class NullToVisibilityConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value is null ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/CRSim/Converters/NumberToStringConverter.cs b/CRSim/Converters/NumberToStringConverter.cs
new file mode 100644
index 0000000..0a8224f
--- /dev/null
+++ b/CRSim/Converters/NumberToStringConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Converters
+{
+ public class NumberToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if(value is string number)
+ {
+ if(number.StartsWith('G')|| number.StartsWith('D')|| number.StartsWith('C'))
+ {
+ return Math.Abs(number.GetHashCode()) % 3 == 0 ? "1-8" : "9-16";
+ }
+ else
+ {
+ return Math.Abs(number.GetHashCode()) % 2 == 0 ? "1-9" : "10-18";
+ }
+ }
+ return value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/TextSpacingConverter.cs b/CRSim/Converters/TextSpacingConverter.cs
new file mode 100644
index 0000000..3a64355
--- /dev/null
+++ b/CRSim/Converters/TextSpacingConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CRSim.Converters
+{
+ public class TextSpacingConverter : IValueConverter
+ {
+ public int TextLength { get; set; } = 2;
+ public string Spacing { get; set; } = " ";
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string s && s.Length == TextLength)
+ {
+ return string.Join(Spacing, s.ToCharArray());
+ }
+ return value;
+ }
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CRSim/Converters/TicketCheckConverter.cs b/CRSim/Converters/TicketCheckConverter.cs
new file mode 100644
index 0000000..5d71a22
--- /dev/null
+++ b/CRSim/Converters/TicketCheckConverter.cs
@@ -0,0 +1,21 @@
+namespace CRSim.Converters
+{
+ public class TicketCheckConverter : IValueConverter
+ {
+ public string Separator { get; set; } = ""; // 默认分隔符是双引号间隔
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is List ticketChecks)
+ {
+ // 使用自定义的 Separator 将字符串连接
+ return string.Join(Separator, ticketChecks);
+ }
+ return string.Empty;
+ }
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // 通常不需要实现 ConvertBack
+ return null;
+ }
+ }
+}
diff --git a/CRSim/Converters/TrainStateColorConverter.cs b/CRSim/Converters/TrainStateColorConverter.cs
new file mode 100644
index 0000000..d991d9e
--- /dev/null
+++ b/CRSim/Converters/TrainStateColorConverter.cs
@@ -0,0 +1,50 @@
+namespace CRSim.Converters
+{
+ public class TrainStateColorConverter : IMultiValueConverter
+ {
+ private ITimeService _timeService;
+ private Settings _settings;
+ public SolidColorBrush WaitingColor { get; set; } = new(Colors.White);
+ object IMultiValueConverter.Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
+ _timeService = serviceProvider.GetRequiredService();
+ _settings = serviceProvider.GetRequiredService().GetSettings();
+ if (values[1] is DateTime departureTime && departureTime !=new DateTime())
+ {
+ if (values[0] is DateTime)
+ {
+ //过路站
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.PassingCheckInAdvanceDuration) && _timeService.GetDateTimeNow() < departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return new SolidColorBrush(Colors.LightGreen);
+ }
+ }
+ else
+ {
+ //始发站
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.DepartureCheckInAdvanceDuration) && _timeService.GetDateTimeNow() < departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return new SolidColorBrush(Colors.LightGreen);
+ }
+ }
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return new SolidColorBrush(Colors.Red);
+ }
+ if (values[2] is TimeSpan state && state.TotalMinutes > 0)
+ {
+ return new SolidColorBrush(Colors.Red);
+ }
+ return WaitingColor;
+ }
+ return new SolidColorBrush(Colors.Transparent);
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ // 通常不需要实现 ConvertBack
+ return null;
+ }
+ }
+}
diff --git a/CRSim/Converters/TrainStateConverter.cs b/CRSim/Converters/TrainStateConverter.cs
new file mode 100644
index 0000000..ced0883
--- /dev/null
+++ b/CRSim/Converters/TrainStateConverter.cs
@@ -0,0 +1,52 @@
+namespace CRSim.Converters
+{
+ public class TrainStateConverter : IMultiValueConverter
+ {
+ private ITimeService _timeService;
+ private Settings _settings;
+ public string WaitingText { get; set; } = "候车";
+ public string CheckInText { get; set; } = "正在检票";
+ public string StopCheckInText { get; set; } = "停止检票";
+ object IMultiValueConverter.Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ var serviceProvider = (IServiceProvider)Application.Current.Resources["ServiceProvider"];
+ _timeService = serviceProvider.GetRequiredService();
+ _settings = serviceProvider.GetRequiredService().GetSettings();
+ if (values[1] is DateTime departureTime && departureTime != new DateTime())
+ {
+ if (values[0] is DateTime)
+ {
+ //过路站
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.PassingCheckInAdvanceDuration) && _timeService.GetDateTimeNow() < departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return CheckInText;
+ }
+ }
+ else
+ {
+ //始发站
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.DepartureCheckInAdvanceDuration) && _timeService.GetDateTimeNow() < departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return CheckInText;
+ }
+ }
+ if (_timeService.GetDateTimeNow() > departureTime.Subtract(_settings.StopCheckInAdvanceDuration))
+ {
+ return StopCheckInText;
+ }
+ if (values[2] is TimeSpan state && state.TotalMinutes > 0)
+ {
+ return $"预计开点{departureTime.Add(state):HH:mm}";//晚点列车
+ }
+ return WaitingText;
+ }
+ return string.Empty;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ // 通常不需要实现 ConvertBack
+ return null;
+ }
+ }
+}
diff --git a/CRSim/MainWindow.xaml b/CRSim/MainWindow.xaml
new file mode 100644
index 0000000..cd25fae
--- /dev/null
+++ b/CRSim/MainWindow.xaml
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRSim/MainWindow.xaml.cs b/CRSim/MainWindow.xaml.cs
new file mode 100644
index 0000000..0c38f67
--- /dev/null
+++ b/CRSim/MainWindow.xaml.cs
@@ -0,0 +1,204 @@
+using System.Windows.Navigation;
+
+namespace CRSim
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly INavigationService _navigationService;
+ public MainWindowViewModel ViewModel { get; }
+ public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
+ {
+ _serviceProvider = serviceProvider;
+ DataContext = this;
+ ViewModel = viewModel;
+ InitializeComponent();
+
+ UpdateWindowBackground();
+ UpdateTitleBarButtonsVisibility();
+
+ _navigationService = navigationService;
+ _navigationService.Navigating += OnNavigating;
+ _navigationService.SetFrame(this.RootContentFrame);
+ _navigationService.Navigate(typeof(DashboardPage));
+
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+
+ SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
+ this.StateChanged += MainWindow_StateChanged;
+ }
+
+ private void MainWindow_StateChanged(object? sender, EventArgs e)
+ {
+ if (this.WindowState == WindowState.Maximized)
+ {
+ MainGrid.Margin = new Thickness(8);
+ }
+ else
+ {
+ MainGrid.Margin = default;
+ }
+ }
+ private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ UpdateTitleBarButtonsVisibility();
+ });
+ }
+
+ private void OnNavigating(object? sender, NavigatingEventArgs e)
+ {
+ List list = ViewModel.GetNavigationItemHierarchyFromPageType(e.PageType);
+
+ if (list.Count > 0)
+ {
+ TreeViewItem selectedTreeViewItem = null;
+ ItemsControl itemsControl = ControlsList;
+ foreach (ControlInfoDataItem item in list)
+ {
+ var tvi = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
+ if (tvi != null)
+ {
+ tvi.IsExpanded = true;
+ tvi.UpdateLayout();
+ itemsControl = tvi;
+ selectedTreeViewItem = tvi;
+ }
+ }
+
+ if (selectedTreeViewItem != null)
+ {
+ selectedTreeViewItem.IsSelected = true;
+ ControlsList_SelectedItemChanged();
+ }
+ }
+ }
+
+ private void UpdateWindowBackground()
+ {
+ if ((!Utility.IsBackdropDisabled() &&
+ !Utility.IsBackdropSupported()))
+ {
+ this.SetResourceReference(BackgroundProperty, "WindowBackground");
+ }
+ }
+ private void ControlsList_SelectedItemChanged()
+ {
+ if (ControlsList.SelectedItem is ControlInfoDataItem navItem)
+ {
+ _navigationService.Navigate(navItem.PageType);
+ var tvi = ControlsList.ItemContainerGenerator.ContainerFromItem(navItem) as TreeViewItem;
+ if (tvi != null)
+ {
+ tvi.BringIntoView();
+ }
+ }
+ }
+ private void UpdateTitleBarButtonsVisibility()
+ {
+ if (Utility.IsBackdropDisabled() || !Utility.IsBackdropSupported() ||
+ SystemParameters.HighContrast == true)
+ {
+ MinimizeButton.Visibility = Visibility.Visible;
+ MaximizeButton.Visibility = Visibility.Visible;
+ CloseButton.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ MinimizeButton.Visibility = Visibility.Collapsed;
+ MaximizeButton.Visibility = Visibility.Collapsed;
+ CloseButton.Visibility = Visibility.Collapsed;
+ }
+ }
+ private void MinimizeWindow(object sender, RoutedEventArgs e)
+ {
+ this.WindowState = WindowState.Minimized;
+ }
+
+ private void MaximizeWindow(object sender, RoutedEventArgs e)
+ {
+ if (this.WindowState == WindowState.Maximized)
+ {
+ this.WindowState = WindowState.Normal;
+ MaximizeIcon.Text = "\uE922";
+ }
+ else
+ {
+ this.WindowState = WindowState.Maximized;
+ MaximizeIcon.Text = "\uE923";
+ }
+ }
+
+ private void CloseWindow(object sender, RoutedEventArgs e)
+ {
+ Application.Current.Shutdown();
+ }
+
+ private void SettingsButton_Click(object sender, RoutedEventArgs e)
+ {
+ AutomationPeer peer = UIElementAutomationPeer.CreatePeerForElement((Button)sender);
+ peer.RaiseNotificationEvent(
+ AutomationNotificationKind.Other,
+ AutomationNotificationProcessing.ImportantMostRecent,
+ "Settings Page Opened",
+ "ButtonClickedActivity"
+ );
+ }
+
+ private void ControlsList_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (ControlsList.Items.Count > 0)
+ {
+ TreeViewItem firstItem = (TreeViewItem)ControlsList.ItemContainerGenerator.ContainerFromItem(ControlsList.Items[0]);
+ if (firstItem != null)
+ {
+ firstItem.IsSelected = true;
+ }
+ }
+ }
+
+ private void RootContentFrame_Navigated(object sender, NavigationEventArgs e)
+ {
+ ViewModel.UpdateCanNavigateBack();
+ }
+
+ private void ControlsList_PreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ SelectedItemChanged(ControlsList.ItemContainerGenerator.ContainerFromItem((sender as TreeView).SelectedItem) as TreeViewItem);
+ }
+ }
+
+ private void ControlsList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (e.OriginalSource is ToggleButton)
+ {
+ return;
+ }
+ SelectedItemChanged(ControlsList.ItemContainerGenerator.ContainerFromItem((sender as TreeView).SelectedItem) as TreeViewItem);
+ }
+ private void SelectedItemChanged(TreeViewItem? tvi)
+ {
+ ControlsList_SelectedItemChanged();
+ if (tvi != null)
+ {
+ tvi.IsExpanded = !tvi.IsExpanded;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CRSim/Models/ControlsInfoData.json b/CRSim/Models/ControlsInfoData.json
new file mode 100644
index 0000000..208455a
--- /dev/null
+++ b/CRSim/Models/ControlsInfoData.json
@@ -0,0 +1,72 @@
+[
+ {
+ "UniqueId": "Home",
+ "Title": "主页",
+ "PageName": "DashboardPage",
+ "IconGlyph": "\uE80F"
+ },
+ {
+ "UniqueId": "What's New",
+ "Title": "更新信息",
+ "PageName": "WhatsNewPage",
+ "IconGlyph": "\uEB51"
+ },
+ {
+ "UniqueId": "Data Management",
+ "Title": "数据管理",
+ "PageName": "DataManagementPage",
+ "IconGlyph": "\uE81E",
+ "Description": "车站、车次、交路等信息管理。",
+ "ImagePath": "Assets/ControlImages/Data Management.png",
+ "IsGroup": true,
+ "Items": [
+ {
+ "UniqueId": "Station",
+ "Title": "车站管理",
+ "PageName": "StationManagementPage",
+ "ImagePath": "Assets/ControlImages/Station Management.png",
+ "Description": "管理车站数据库。"
+ },
+ {
+ "UniqueId": "Train Number",
+ "Title": "车次管理",
+ "PageName": "TrainNumberManagementPage",
+ "ImagePath": "Assets/ControlImages/TrainNumber Management.png",
+ "Description": "管理车次数据库。"
+ }
+ ]
+ },
+ {
+ "UniqueId": "Start Simulation",
+ "Title": "开始模拟",
+ "PageName": "StartSimulationPage",
+ "IconGlyph": "\uE768",
+ "Description": "开始模拟信息显示。",
+ "ImagePath": "Assets/ControlImages/Start Simulation.png",
+ "IsGroup": true,
+ "Items": [
+ {
+ "UniqueId": "Screen",
+ "Title": "引导屏模拟",
+ "PageName": "ScreenSimulationPage",
+ "ImagePath": "Assets/ControlImages/Screen Simulation.png",
+ "Description": "旅客引导显示屏模拟。"
+ },
+ {
+ "UniqueId": "Gate",
+ "Title": "闸机模拟",
+ "PageName": "GateSimulationPage",
+ "ImagePath": "Assets/ControlImages/Gate Simulation.png",
+ "Description": "闸机模拟。(开发中)"
+ },
+ {
+ "UniqueId": "TVM",
+ "Title": "TVM模拟",
+ "PageName": "TVMSimulationPage",
+ "ImagePath": "Assets/ControlImages/TVM Simulation.png",
+ "Description": "自助服务机模拟。(开发中)"
+ }
+ ]
+ }
+
+]
\ No newline at end of file
diff --git a/CRSim/Models/ControlsInfoDataSource.cs b/CRSim/Models/ControlsInfoDataSource.cs
new file mode 100644
index 0000000..f181603
--- /dev/null
+++ b/CRSim/Models/ControlsInfoDataSource.cs
@@ -0,0 +1,107 @@
+namespace CRSim.Models
+{
+ public class ControlInfoDataItem
+ {
+ public string UniqueId { get; set; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public string IconGlyph { get; set; }
+ public string ImagePath { get; set; }
+ public string PageName { get; set; }
+ public bool IsGroup { get; set; } = false;
+
+ public Type PageType
+ {
+ get
+ {
+ return _assembly.GetType($"CRSim.Views.{PageName}");
+ }
+ }
+
+ public Uri ImageSource
+ {
+ get
+ {
+ return new Uri($"pack://application:,,,/{ImagePath}");
+ }
+ }
+
+ public ObservableCollection Items { get; set; } = [];
+
+ public override string ToString()
+ {
+ return Title;
+ }
+
+ private static readonly Assembly _assembly = typeof(ControlsInfoDataSource).Assembly;
+ }
+
+ public sealed class ControlsInfoDataSource
+ {
+
+ #region Singleton
+
+ private static readonly ControlsInfoDataSource _instance;
+
+ public static ControlsInfoDataSource Instance
+ {
+ get
+ {
+ return _instance;
+ }
+ }
+
+ static ControlsInfoDataSource()
+ {
+ _instance = new ControlsInfoDataSource();
+ }
+
+ private ControlsInfoDataSource()
+ {
+ var jsonText = ReadControlsData();
+ ControlsInfo = JsonSerializer.Deserialize>(jsonText);
+ }
+
+ #endregion
+
+ public ICollection ControlsInfo { get; }
+
+ private static string ReadControlsData()
+ {
+ var assembly = typeof(ControlsInfoDataSource).Assembly;
+ var resourceName = "CRSim.Models.ControlsInfoData.json";
+
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ using var reader = new System.IO.StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ public ICollection GetControlsInfo(string groupName)
+ {
+ return ControlsInfo.Where(x => x.UniqueId == groupName).FirstOrDefault()?.Items;
+ }
+
+ public ICollection GetAllControlsInfo()
+ {
+ ObservableCollection allControls = [];
+ foreach (ControlInfoDataItem ci in ControlsInfo)
+ {
+ if(ci.UniqueId != "Samples")
+ {
+ var items = ci.Items;
+ foreach (ControlInfoDataItem item in items)
+ {
+ allControls.Add(item);
+ }
+ }
+ }
+
+ return allControls;
+ }
+
+ public ICollection GetGroupedControlsInfo()
+ {
+ return ControlsInfo.Where(x => x.IsGroup == true).ToList();
+ }
+ }
+}
diff --git a/CRSim/Models/NavigatedEventArgs.cs b/CRSim/Models/NavigatedEventArgs.cs
new file mode 100644
index 0000000..1ae8ada
--- /dev/null
+++ b/CRSim/Models/NavigatedEventArgs.cs
@@ -0,0 +1,14 @@
+namespace CRSim.Models
+{
+ public class NavigatingEventArgs
+ {
+ public Type? PageType { get; set; } = null;
+
+ public NavigatingEventArgs() { }
+
+ public NavigatingEventArgs(Type pageType)
+ {
+ PageType = pageType;
+ }
+ }
+}
diff --git a/CRSim/Models/StylesInfoData.json b/CRSim/Models/StylesInfoData.json
new file mode 100644
index 0000000..91023ac
--- /dev/null
+++ b/CRSim/Models/StylesInfoData.json
@@ -0,0 +1,89 @@
+[
+ {
+ "UniqueId": "ChengduDong SecondaryScreen",
+ "Title": "成都东站次要看板",
+ "Station": "ChengduDong",
+ "StyleName": "SecondaryScreenView",
+ "ImagePath": "Assets/StyleImages/ChengduDong/SecondaryScreen.png",
+ "Parameters": [
+ "Station",
+ "Text"
+ ]
+ },
+ {
+ "UniqueId": "ChengduDong PrimaryTicketCheckScreen",
+ "Title": "成都东站检票口主要看板",
+ "Station": "ChengduDong",
+ "StyleName": "PrimaryTicketCheckScreenView",
+ "ImagePath": "Assets/StyleImages/ChengduDong/PrimaryTicketCheckScreen.png",
+ "Parameters": [
+ "Station",
+ "TicketCheck",
+ "Text"
+ ]
+ },
+ {
+ "UniqueId": "BeijingXi PrimaryScreen",
+ "Title": "北京西站主要看板",
+ "Station": "BeijingXi",
+ "StyleName": "PrimaryScreenView",
+ "ImagePath": "Assets/StyleImages/BeijingXi/PrimaryScreen.png",
+ "Parameters": [
+ "Station"
+ ]
+ },
+ {
+ "UniqueId": "Harbin PrimaryScreen",
+ "Title": "哈尔滨站主要看板",
+ "Station": "Harbin",
+ "StyleName": "PrimaryScreenView",
+ "ImagePath": "Assets/StyleImages/Harbin/PrimaryScreen.png",
+ "Parameters": [
+ "Station",
+ "Text"
+ ]
+ },
+ {
+ "UniqueId": "ChengduMetro PlatformScreen",
+ "Title": "成都地铁站台看板",
+ "Station": "ChengduMetro",
+ "StyleName": "PlatformScreenView",
+ "ImagePath": "Assets/StyleImages/ChengduMetro/PlatformScreen.png",
+ "Parameters": [
+ "Station",
+ "Text"
+ ]
+ },
+ {
+ "UniqueId": "Guangyuan PrimaryScreen",
+ "Title": "广元站主要看板",
+ "Station": "Guangyuan",
+ "StyleName": "PrimaryScreenView",
+ "ImagePath": "Assets/StyleImages/Guangyuan/PrimaryScreen.png",
+ "Parameters": [
+ "Station",
+ "Text"
+ ]
+ },
+ {
+ "UniqueId": "Shanghai OutsideScreen",
+ "Title": "上海站站外看板",
+ "Station": "Shanghai",
+ "StyleName": "OutsideScreenView",
+ "ImagePath": "Assets/StyleImages/Shanghai/OutsideScreen.png",
+ "Parameters": [
+ "Station"
+ ]
+ },
+ {
+ "UniqueId": "Hanzhong PlatformScreen",
+ "Title": "汉中站站台看板",
+ "Station": "Hanzhong",
+ "StyleName": "PlatformScreenView",
+ "ImagePath": "Assets/StyleImages/Hanzhong/PlatformScreen.png",
+ "Parameters": [
+ "Station",
+ "Platform"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/CRSim/Models/StylesInfoDataSource.cs b/CRSim/Models/StylesInfoDataSource.cs
new file mode 100644
index 0000000..7134177
--- /dev/null
+++ b/CRSim/Models/StylesInfoDataSource.cs
@@ -0,0 +1,87 @@
+namespace CRSim.Models
+{
+ public class StylesInfoDataItem
+ {
+ public string UniqueId { get; set; }
+ public string Title { get; set; }
+ public string Station { get; set; }
+ public string StyleName { get; set; }
+ public string ImagePath { get; set; }
+ public Type ViewType
+ {
+ get
+ {
+ string type = $"CRSim.Views.{Station}.{StyleName}";
+ return _assembly.GetType(type);
+ }
+ }
+
+ public Uri ImageSource
+ {
+ get
+ {
+ return new Uri($"pack://application:,,,/{ImagePath}");
+ }
+ }
+
+ public List Parameters { get; set; } = [];
+
+ public override string ToString()
+ {
+ return Title;
+ }
+
+ private static readonly Assembly _assembly = typeof(StylesInfoDataItem).Assembly;
+ }
+
+ public sealed class StylesInfoDataSource
+ {
+
+ #region Singleton
+
+ private static readonly StylesInfoDataSource _instance;
+
+ public static StylesInfoDataSource Instance
+ {
+ get
+ {
+ return _instance;
+ }
+ }
+
+ static StylesInfoDataSource()
+ {
+ _instance = new StylesInfoDataSource();
+ }
+
+ private StylesInfoDataSource()
+ {
+ var jsonText = ReadStylesData();
+ StylesInfo = JsonSerializer.Deserialize>(jsonText);
+ }
+
+ #endregion
+
+ public ICollection StylesInfo { get; }
+
+ private static string ReadStylesData()
+ {
+ var assembly = typeof(StylesInfoDataSource).Assembly;
+ var resourceName = "CRSim.Models.StylesInfoData.json";
+
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ using var reader = new System.IO.StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ public ICollection GetParametersInfo(Type type)
+ {
+ return StylesInfo.Where(x => x.ViewType == type).FirstOrDefault()?.Parameters;
+ }
+
+ public string GetStyleName(Type type)
+ {
+ return StylesInfo.Where(x => x.ViewType == type).FirstOrDefault()?.Title;
+ }
+ }
+}
diff --git a/CRSim/Models/TrainInfo.cs b/CRSim/Models/TrainInfo.cs
new file mode 100644
index 0000000..5660fa0
--- /dev/null
+++ b/CRSim/Models/TrainInfo.cs
@@ -0,0 +1,16 @@
+namespace CRSim.Models
+{
+ public class TrainInfo
+ {
+ public string TrainNumber { get; set; }
+ public string Origin { get; set; }
+ public string Terminal { get; set; }
+ public DateTime? ArrivalTime { get; set; }
+ public DateTime DepartureTime { get; set; }
+ public List TicketChecks { get; set; }
+ public string WaitingArea { get; set; }
+ public string Platform { get; set; }
+ public TimeSpan State { get; set; }
+ public string? Landmark { get; set; }
+ }
+}
diff --git a/CRSim/Properties/GlobalUsings.cs b/CRSim/Properties/GlobalUsings.cs
new file mode 100644
index 0000000..6b10b1b
--- /dev/null
+++ b/CRSim/Properties/GlobalUsings.cs
@@ -0,0 +1,31 @@
+global using System.Diagnostics;
+global using System.Globalization;
+global using System.ComponentModel;
+global using System.Collections.ObjectModel;
+global using System.Reflection;
+global using System.Text.Json;
+global using System.Windows;
+global using System.Windows.Data;
+global using System.Windows.Media;
+global using System.Windows.Threading;
+global using System.Windows.Input;
+global using System.Windows.Shell;
+global using System.Windows.Controls;
+global using System.Windows.Controls.Primitives;
+global using System.Windows.Automation;
+global using System.Windows.Automation.Peers;
+
+global using Microsoft.Win32;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+
+global using CommunityToolkit.Mvvm.ComponentModel;
+global using CommunityToolkit.Mvvm.Input;
+
+global using CRSim.Core.Services;
+global using CRSim.Core.Models;
+
+global using CRSim.Services;
+global using CRSim.Models;
+global using CRSim.Views;
+global using CRSim.ViewModels;
\ No newline at end of file
diff --git a/CRSim/Resources/PageStyles.xaml b/CRSim/Resources/PageStyles.xaml
new file mode 100644
index 0000000..6c82ee7
--- /dev/null
+++ b/CRSim/Resources/PageStyles.xaml
@@ -0,0 +1,93 @@
+
+
+ 12
+ 14
+ 20
+ 28
+ 40
+ 68
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRSim/Resources/Templates.xaml b/CRSim/Resources/Templates.xaml
new file mode 100644
index 0000000..796ffb4
--- /dev/null
+++ b/CRSim/Resources/Templates.xaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CRSim/Services/DialogService.cs b/CRSim/Services/DialogService.cs
new file mode 100644
index 0000000..e4f7b8f
--- /dev/null
+++ b/CRSim/Services/DialogService.cs
@@ -0,0 +1,149 @@
+
+namespace CRSim.Services
+{
+ public class DialogService : IDialogService
+ {
+ private Window? _owner;
+ public void SetOwner(Window owner)
+ {
+ _owner = owner;
+ }
+ public string? GetInput(string title)
+ {
+ var dialog = new InputDialog(title)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ string input = dialog.UserInput;
+ return input;
+ }
+ return null;
+ }
+ public bool GetConfirm(string title)
+ {
+ var dialog = new ConfirmDialog(title)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return true;
+ }
+ return false;
+ }
+ public void ShowMessage(string title, string message)
+ {
+ var dialog = new MessageDialog(title, message)
+ {
+ Owner = _owner
+ };
+ dialog.ShowDialog();
+ }
+
+ public TrainStop? GetInputTrainStop()
+ {
+ var dialog = new TrainStopDialog()
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return dialog.GeneratedTrainStop;
+ }
+ return null;
+ }
+
+ public TrainStop? GetInputTrainStop(TrainStop trainStop)
+ {
+ var dialog = new TrainStopDialog(trainStop)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return dialog.GeneratedTrainStop;
+ }
+ return null;
+ }
+
+ public (string, List) GetInputTicketCheck(List waitingAreaNames)
+ {
+ var dialog = new TicketCheckDialog(waitingAreaNames)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return (dialog.WaitingAreaName, dialog.TicketCheck);
+ }
+ return (null,null);
+ }
+
+ public string? GetFile(string filter)
+ {
+ OpenFileDialog openFileDialog = new()
+ {
+ Filter = filter
+ };
+ if (openFileDialog.ShowDialog() == true)
+ {
+ string filePath = openFileDialog.FileName;
+ return filePath;
+ }
+ return null;
+ }
+
+ public StationStop? GetInputStationStop(List ticketChecks, List platforms)
+ {
+ var dialog = new StationStopDialog(ticketChecks,platforms)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return dialog.GeneratedStationStop;
+ }
+ return null;
+
+ }
+
+ public StationStop? GetInputStationStop(List ticketChecks, List platforms, StationStop stationStop)
+ {
+ var dialog = new StationStopDialog(ticketChecks,platforms, stationStop)
+ {
+ Owner = _owner
+ };
+ bool? result = dialog.ShowDialog();
+ if (result == true)
+ {
+ return dialog.GeneratedStationStop;
+ }
+ return null;
+
+ }
+
+ public string? SaveFile(string filter, string f)
+ {
+ SaveFileDialog saveFileDialog = new()
+ {
+ FileName = f,
+ CheckPathExists = true,
+ Filter = filter
+ };
+ if (saveFileDialog.ShowDialog() == true)
+ {
+ string filePath = saveFileDialog.FileName;
+ return filePath;
+ }
+ return null;
+ }
+ }
+}
diff --git a/CRSim/Services/IDialogService.cs b/CRSim/Services/IDialogService.cs
new file mode 100644
index 0000000..91ebfe6
--- /dev/null
+++ b/CRSim/Services/IDialogService.cs
@@ -0,0 +1,17 @@
+namespace CRSim.Services
+{
+ public interface IDialogService
+ {
+ string? GetFile(string filter);
+ string? SaveFile(string filter,string fileName);
+ string? GetInput(string title);
+ bool GetConfirm(string title);
+ void ShowMessage(string title, string message);
+ TrainStop? GetInputTrainStop();
+ TrainStop? GetInputTrainStop(TrainStop trainStop);
+ StationStop? GetInputStationStop(List ticketChecks, List platforms);
+ StationStop? GetInputStationStop(List ticketChecks, List platforms, StationStop stationStop);
+ (string, List) GetInputTicketCheck(List waitingAreaNames);
+ void SetOwner(Window owner);
+ }
+}
diff --git a/CRSim/Services/INavigationService.cs b/CRSim/Services/INavigationService.cs
new file mode 100644
index 0000000..f016eb6
--- /dev/null
+++ b/CRSim/Services/INavigationService.cs
@@ -0,0 +1,19 @@
+namespace CRSim.Services
+{
+ public interface INavigationService
+ {
+ void Navigate(Type type);
+
+ void NavigateTo(Type type);
+
+ void SetFrame(Frame frame);
+
+ void NavigateBack();
+
+ void NavigateForward();
+
+ bool IsBackHistoryNonEmpty();
+
+ event EventHandler Navigating;
+ }
+}
diff --git a/CRSim/Services/NavigationService.cs b/CRSim/Services/NavigationService.cs
new file mode 100644
index 0000000..cae170b
--- /dev/null
+++ b/CRSim/Services/NavigationService.cs
@@ -0,0 +1,86 @@
+namespace CRSim.Services
+{
+ public class NavigationService(IServiceProvider serviceProvider) : INavigationService
+ {
+ private Frame _frame;
+
+ private Type _currentPageType = null;
+
+ private readonly Stack _history = new();
+ private readonly Stack _future = new();
+
+ private readonly IServiceProvider _serviceProvider = serviceProvider;
+
+ public event EventHandler Navigating;
+
+ public void SetFrame(Frame frame)
+ {
+ _frame = frame;
+ }
+
+ public void NavigateTo(Type type)
+ {
+ if (type != null)
+ {
+ _future.Clear();
+ RaiseNavigatingEvent(type);
+ }
+ }
+
+ public void Navigate(Type type)
+ {
+ if (type != null)
+ {
+ _history.Push(_currentPageType);
+ _currentPageType = type;
+ var page = _serviceProvider.GetRequiredService(type);
+ _frame.Navigate(page);
+ }
+ }
+
+ public void NavigateBack()
+ {
+ if (_history.Count > 0)
+ {
+ Type type = _history.Pop();
+ if (type != null)
+ {
+ _future.Push(type);
+ RaiseNavigatingEvent(type);
+ _history.Pop();
+ }
+ }
+ }
+
+ public void NavigateForward()
+ {
+ if (_future.Count > 0)
+ {
+ Type type = _future.Pop();
+ if (type != null)
+ {
+ _history.Push(type);
+ RaiseNavigatingEvent(type);
+ }
+ }
+ }
+
+ public void RaiseNavigatingEvent(Type type)
+ {
+ Navigating?.Invoke(this, new NavigatingEventArgs(type));
+ }
+
+ public bool IsBackHistoryNonEmpty()
+ {
+ var item = _history.Peek();
+ if (item == null)
+ {
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/CRSim/Utility.cs b/CRSim/Utility.cs
new file mode 100644
index 0000000..3c92015
--- /dev/null
+++ b/CRSim/Utility.cs
@@ -0,0 +1,27 @@
+namespace CRSim
+{
+ internal static class Utility
+ {
+ public static bool IsBackdropSupported()
+ {
+ var os = Environment.OSVersion;
+ var version = os.Version;
+
+ return version.Major >= 10 && version.Build >= 22621;
+ }
+
+ public static bool IsBackdropDisabled()
+ {
+ var appContextBackdropData = AppContext.GetData("Switch.System.Windows.Appearance.DisableFluentThemeWindowBackdrop");
+ bool disableFluentThemeWindowBackdrop = false;
+
+ if (appContextBackdropData != null)
+ {
+ disableFluentThemeWindowBackdrop = bool.Parse(Convert.ToString(appContextBackdropData));
+ }
+
+ return disableFluentThemeWindowBackdrop;
+ }
+ }
+
+}
diff --git a/CRSim/ViewModels/Pages/DashboardPageViewModel.cs b/CRSim/ViewModels/Pages/DashboardPageViewModel.cs
new file mode 100644
index 0000000..b336658
--- /dev/null
+++ b/CRSim/ViewModels/Pages/DashboardPageViewModel.cs
@@ -0,0 +1,20 @@
+namespace CRSim.ViewModels
+{
+ public partial class DashboardPageViewModel(INavigationService navigationService) : ObservableObject
+ {
+ [ObservableProperty]
+ private ICollection _navigationCards = ControlsInfoDataSource.Instance.GetGroupedControlsInfo();
+
+ private readonly INavigationService _navigationService = navigationService;
+
+ [RelayCommand]
+ public void Navigate(object pageType){
+ if (pageType is Type page)
+ {
+ _navigationService.NavigateTo(page);
+ }
+ }
+
+
+ }
+}
diff --git a/CRSim/ViewModels/Pages/DataManagement/StationManagementPageViewModel.cs b/CRSim/ViewModels/Pages/DataManagement/StationManagementPageViewModel.cs
new file mode 100644
index 0000000..45bbc84
--- /dev/null
+++ b/CRSim/ViewModels/Pages/DataManagement/StationManagementPageViewModel.cs
@@ -0,0 +1,445 @@
+using CRSim.Core.Models;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace CRSim.ViewModels;
+
+public partial class StationManagementPageViewModel : ObservableObject
+{
+ public class TicketCheck
+ {
+ public string WaitingAreaName { get; set; }
+ public string Name { get; set; }
+ }
+ [ObservableProperty]
+ private string _pageTitle = "վ";
+
+ [ObservableProperty]
+ private string _pageDescription = "Ǻܰѣǵñ! Owo";
+
+ [ObservableProperty]
+ private bool _isSelected = false;
+
+ [ObservableProperty]
+ private List _stationNames = [];
+
+ [ObservableProperty]
+ private Station _selectedStation = new();
+
+ public ObservableCollection WaitingAreaNames { get; private set; } = [];
+ public ObservableCollection Platforms { get; private set; } = [];
+
+ public ObservableCollection TicketChecks { get; private set; } = [];
+
+ public ObservableCollection StationStops { get; private set; } = [];
+
+ private readonly IDatabaseService _databaseService;
+
+ private readonly IDialogService _dialogService;
+
+ private readonly INetworkService _networkService;
+ public StationManagementPageViewModel(IDatabaseService databaseService,IDialogService dialogService,INetworkService networkService)
+ {
+ _databaseService = databaseService;
+ _dialogService = dialogService;
+ _networkService = networkService;
+ RefreshStationList();
+ }
+
+ public void RefreshStationList()
+ {
+ var stationsList = _databaseService.GetAllStations();
+ StationNames = [.. stationsList.Select(s => s.Name)];
+ }
+
+ [RelayCommand]
+ public void StationSelected(object selectedStation)
+ {
+ WaitingAreaNames.Clear();
+ Platforms.Clear();
+ TicketChecks.Clear();
+ StationStops.Clear();
+ if(selectedStation is string selectedStationName)
+ {
+ SelectedStation = _databaseService.GetStationByName(selectedStationName);
+ foreach(WaitingArea waitingArea in SelectedStation.WaitingAreas)
+ {
+ WaitingAreaNames.Add(waitingArea.Name);
+ foreach (string ticketCheck in waitingArea.TicketChecks)
+ {
+ TicketChecks.Add(new TicketCheck { WaitingAreaName = waitingArea.Name,Name=ticketCheck });
+ }
+ }
+ foreach (StationStop stationStop in SelectedStation.StationStops)
+ {
+ StationStops.Add(stationStop);
+ }
+ foreach(string platform in SelectedStation.Platforms)
+ {
+ Platforms.Add(platform);
+ }
+ IsSelected = true;
+ }
+ if(selectedStation == null)
+ {
+ IsSelected = false;
+ }
+ }
+
+ [RelayCommand]
+ public void AddTicketCheck()
+ {
+ (string waitingAreaName,List ticketChecks) = _dialogService.GetInputTicketCheck([.. WaitingAreaNames]);
+ if (ticketChecks != null && waitingAreaName != null)
+ {
+ foreach(string ticketCheck in ticketChecks)
+ {
+ if (TicketChecks.Select(x => x.Name).Contains(ticketCheck))
+ {
+ _dialogService.ShowMessage("ʧ", $" {ticketCheck} ƱѴڡ");
+ return;
+ }
+ TicketChecks.Add(new TicketCheck { WaitingAreaName = waitingAreaName, Name = ticketCheck });
+ }
+ }
+ }
+
+ [RelayCommand]
+ public void DeleteTicketCheck(object selectedTicketCheck)
+ {
+ if(selectedTicketCheck is TicketCheck ticketCheck)
+ {
+ TicketChecks.Remove(ticketCheck);
+ }
+ }
+ [RelayCommand]
+ public void DeleteAllTicketCheck()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫƱڣǷ")) return;
+ TicketChecks.Clear();
+ }
+ [RelayCommand]
+ public void AddWaitingArea()
+ {
+ string? waitingAreaName = _dialogService.GetInput("");
+ if (waitingAreaName != null)
+ {
+ if (WaitingAreaNames.Contains(waitingAreaName))
+ {
+ _dialogService.ShowMessage("ʧ",$" {waitingAreaName} Ѵڡ");
+ return;
+ }
+ WaitingAreaNames.Add(waitingAreaName);
+ }
+ }
+
+ [RelayCommand]
+ public void DeleteWaitingArea(object selectedWaitingArea)
+ {
+ if(selectedWaitingArea is string n)
+ {
+ WaitingAreaNames.Remove(n);
+ }
+ }
+ [RelayCommand]
+ public void DeleteAllWaitingArea()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫңǷ")) return;
+ WaitingAreaNames.Clear();
+ }
+ [RelayCommand]
+ public void AddPlatform()
+ {
+ string? platform = _dialogService.GetInput("վ̨");
+ if (platform != null)
+ {
+ string[] platforms = platform.Split('-');
+ if (platforms.Length == 2 && int.TryParse(platforms[0],out int startindex) && int.TryParse(platforms[1], out int endindex))
+ {
+ var startIndex = Math.Min(startindex, endindex);
+ var endIndex = Math.Max(startindex, endindex);
+ for(int i = startIndex; i < endIndex + 1; i++)
+ {
+ if (Platforms.Contains(i.ToString()))
+ {
+ _dialogService.ShowMessage("ʧ", $" {i} վ̨Ѵڡ");
+ return;
+ }
+ Platforms.Add(i.ToString());
+ }
+ }
+ else
+ {
+ if (Platforms.Contains(platform))
+ {
+ _dialogService.ShowMessage("ʧ", $" {platform} վ̨Ѵڡ");
+ return;
+ }
+ Platforms.Add(platform);
+ }
+ }
+ }
+
+ [RelayCommand]
+ public void DeletePlatform(object selectedPlatform)
+ {
+ if (selectedPlatform is string n)
+ {
+ Platforms.Remove(n);
+ }
+ }
+ [RelayCommand]
+ public void DeleteAllPlatform()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫվ̨Ƿ")) return;
+ Platforms.Clear();
+ }
+
+ [RelayCommand]
+ public async Task AddStation()
+ {
+ string? station = _dialogService.GetInput("복վ");
+ if (station != null)
+ {
+ if (StationNames.Contains(station))
+ {
+ _dialogService.ShowMessage("ʧ", $"վ {station} Ѵڡ");
+ return;
+ }
+ _databaseService.AddStationByName(station);
+ await _databaseService.SaveData();
+ RefreshStationList();
+ }
+ }
+
+ [RelayCommand]
+ public async Task DeleteStation(object selectedStation)
+ {
+ if (selectedStation is string s && !string.IsNullOrEmpty(s))
+ {
+ _databaseService.DeleteStation(_databaseService.GetStationByName(s));
+ await _databaseService.SaveData();
+ RefreshStationList();
+ }
+ }
+ [RelayCommand]
+ public async Task DeleteAllStations()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫվǷ")) return;
+ foreach(string stationName in StationNames)
+ {
+ _databaseService.DeleteStation(_databaseService.GetStationByName(stationName));
+ }
+ await _databaseService.SaveData();
+ RefreshStationList();
+ }
+ [RelayCommand]
+ public async Task SaveChanges()
+ {
+ _databaseService.UpdateStation(SelectedStation.Name, GenerateStation(SelectedStation.Name,WaitingAreaNames,TicketChecks,StationStops,Platforms));
+ await _databaseService.SaveData();
+ }
+ public static Station GenerateStation(string stationName, ObservableCollection waitingAreaNames, ObservableCollection ticketChecks,ObservableCollection stationStops,ObservableCollection platforms)
+ {
+ var station = new Station
+ {
+ Name = stationName,
+ WaitingAreas = [.. waitingAreaNames.Select(name => new WaitingArea { Name = name })],
+ StationStops = [.. stationStops],
+ Platforms = [..platforms]
+ };
+
+ // TicketCheck ӳ䵽Ӧ WaitingArea
+ foreach (var ticketCheck in ticketChecks)
+ {
+ var waitingArea = station.WaitingAreas.FirstOrDefault(wa => wa.Name == ticketCheck.WaitingAreaName);
+ waitingArea?.TicketChecks.Add(ticketCheck.Name);
+ }
+
+ return station;
+ }
+ [RelayCommand]
+ public async Task ImportFromTimeTable()
+ {
+ List stationNamesToAdd = [];
+ foreach(TrainNumber trainNumber in _databaseService.GetAllTrainNumbers())
+ {
+ var stationNames = trainNumber.TimeTable.Select(x => x.Station).ToList();
+ foreach(string stationName in stationNames)
+ {
+ if(!stationNamesToAdd.Contains(stationName) && !StationNames.Contains(stationName))
+ {
+ stationNamesToAdd.Add(stationName);
+ }
+ }
+ }
+ foreach(string stationName in stationNamesToAdd)
+ {
+ _databaseService.AddStationByName(stationName);
+ }
+ await SaveChanges();
+ RefreshStationList();
+ }
+ [RelayCommand]
+ public async Task ImportFromLulutong()
+ {
+ if (TicketChecks.Count == 0)
+ {
+ _dialogService.ShowMessage("ʧ", "ӼƱڡ");
+ return;
+ }
+ if (Platforms.Count == 0)
+ {
+ _dialogService.ShowMessage("ʧ", "վ̨");
+ return;
+ }
+ var path = _dialogService.GetFile("CSV ļ (*.csv)|*.csv|ļ (*.*)|*.*");
+ if (path == null) return;
+ try
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ var lines = new List();
+ var reader = new StreamReader(path, Encoding.GetEncoding("GBK"));
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync();
+ lines.Add(line);
+ }
+ foreach (var line in lines.Skip(13))
+ {
+ var data = line.Split(",");
+ var firstColumn = data[0].Trim();
+ if (!string.IsNullOrEmpty(firstColumn) && char.IsLetterOrDigit(firstColumn[0]))
+ {
+ int spaceIndex = firstColumn.IndexOf(' ');
+ if (spaceIndex != -1)
+ {
+ firstColumn = firstColumn[..spaceIndex];
+ }
+ if (StationStops.Any(x=> x.Number==firstColumn)) continue;
+ DateTime? departureTime = DateTime.Parse(data[4]);
+ DateTime? arrivalTime = departureTime.Value.Subtract(TimeSpan.FromMinutes(2));
+ if (data[1].Split("-")[0] == SelectedStation.Name)
+ {
+ arrivalTime = null;
+ }
+ else if (data[1].Split("-")[1] == SelectedStation.Name)
+ {
+ departureTime = null;
+ }
+ StationStops.Add(new StationStop()
+ {
+ Number = firstColumn,
+ Origin = data[1].Split("-")[0],
+ Terminal = data[1].Split("-")[1],
+ ArrivalTime = arrivalTime,
+ DepartureTime = departureTime,
+ TicketChecks = [TicketChecks[new Random().Next(TicketChecks.Count)].Name],
+ Platform = Platforms[new Random().Next(Platforms.Count)],
+ Landmark = new[] { "ɫ", "ɫ", "ɫ", "ɫ", "ɫ", "ɫ", "ɫ",null}[new Random().Next(8)]
+ });
+ }
+ }
+ }
+ catch
+ {
+ _dialogService.ShowMessage("ʧ", "ļʽռá");
+ }
+ }
+ [RelayCommand]
+ public async Task ImportFrom7D()
+ {
+ var path = _dialogService.GetFile("ִгļ (*.exe)|*.exe|ļ (*.*)|*.*");
+ if (path == null) return;
+ try
+ {
+ await _databaseService.ImportStationFrom7D(path);
+ await _databaseService.SaveData();
+ RefreshStationList();
+ }
+ catch
+ {
+ _dialogService.ShowMessage("ʧ", "ļʽռá");
+ }
+ }
+ [RelayCommand]
+ public void AddStationStop()
+ {
+ var newStationStop = _dialogService.GetInputStationStop([.. TicketChecks.Select(x => x.Name)], [.. Platforms]);
+ if (newStationStop != null)
+ {
+ if (StationStops.Any(x => x.Number == newStationStop.Number))
+ {
+ _dialogService.ShowMessage("ʧ", $" {newStationStop.Number} Ѵڡ");
+ return;
+ }
+ StationStops.Add(newStationStop);
+ }
+ }
+ [RelayCommand]
+ public void DeleteStationStop(object selectedStationStop)
+ {
+ if (selectedStationStop is StationStop s)
+ {
+ StationStops.Remove(s);
+ }
+ }
+ [RelayCommand]
+ public void DeleteAllStationStop()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫΣǷ")) return;
+ StationStops.Clear();
+ }
+ [RelayCommand]
+ public void EditStationStop(object _selectedStationStop)
+ {
+ if (_selectedStationStop is StationStop selectedStationStop)
+ {
+ var newStationStop = _dialogService.GetInputStationStop([.. TicketChecks.Select(x => x.Name)], [.. Platforms], selectedStationStop);
+ if (newStationStop != null)
+ {
+ StationStops[StationStops.IndexOf(selectedStationStop)] = newStationStop;
+ }
+ }
+ }
+ [RelayCommand]
+ public async Task ImportFromInternet()
+ {
+ if (TicketChecks.Count == 0)
+ {
+ _dialogService.ShowMessage("ʧ", "ӼƱڡ");
+ return;
+ }
+ if (Platforms.Count == 0)
+ {
+ _dialogService.ShowMessage("ʧ", "վ̨");
+ return;
+ }
+ if (StationStops.Count != 0)
+ {
+ if (!_dialogService.GetConfirm("ǰʱ̱Ƿ"))
+ {
+ return;
+ }
+ }
+
+ var stops = await _networkService.GetStationStopsAsync(SelectedStation.Name);
+ if (stops.Count !=0)
+ {
+ StationStops.Clear();
+ foreach (StationStop t in stops)
+ {
+ t.TicketChecks = [TicketChecks[new Random().Next(TicketChecks.Count)].Name];
+ t.Platform = Platforms[new Random().Next(Platforms.Count)];
+ t.Landmark = new[] { "ɫ", "ɫ", "ɫ", "ɫ", "ɫ", "ɫ", "ɫ", null }[new Random().Next(8)];
+ StationStops.Add(t);
+ }
+ }
+ else
+ {
+ _dialogService.ShowMessage("ȡʧ", "æվڡ");
+ }
+ }
+
+}
diff --git a/CRSim/ViewModels/Pages/DataManagement/TrainNumberManagementPageViewModel.cs b/CRSim/ViewModels/Pages/DataManagement/TrainNumberManagementPageViewModel.cs
new file mode 100644
index 0000000..c8f4bb6
--- /dev/null
+++ b/CRSim/ViewModels/Pages/DataManagement/TrainNumberManagementPageViewModel.cs
@@ -0,0 +1,290 @@
+using System.Collections.Specialized;
+using System.IO;
+
+namespace CRSim.ViewModels;
+
+public partial class TrainNumberManagementPageViewModel : ObservableObject
+{
+ [ObservableProperty]
+ private string _pageTitle = "ι";
+
+ [ObservableProperty]
+ private string _pageDescription = "Ǻܰѣǵñ! Owo";
+
+ [ObservableProperty]
+ private bool _isSelected = false;
+
+ [ObservableProperty]
+ private List _numbersList = [];
+
+ [ObservableProperty]
+ private TrainNumber _selectedTrainNumber = new();
+
+ [ObservableProperty]
+ private string _runningTime;
+
+ [ObservableProperty]
+ private int _progressValue = 0;
+
+ public ObservableCollection TrainStopsList { get; private set; }= [];
+
+ private readonly IDatabaseService _databaseService;
+
+ private readonly IDialogService _dialogService;
+
+ private readonly INetworkService _networkService;
+
+ public TrainNumberManagementPageViewModel(IDatabaseService databaseService,IDialogService dialogService,INetworkService networkService)
+ {
+ _databaseService = databaseService;
+ _dialogService = dialogService;
+ _networkService = networkService;
+ RefreshNumbersList();
+ TrainStopsList.CollectionChanged += RefreshTrainNumberInfo;
+ }
+ private void RefreshTrainNumberInfo(object? s,NotifyCollectionChangedEventArgs args)
+ {
+ if (SelectedTrainNumber != null && TrainStopsList.Count >= 2)
+ {
+ var timeSpan = (TrainStopsList.Last().ArrivalTime - TrainStopsList.First().DepartureTime).Value;
+ RunningTime = $"{timeSpan.Days} {timeSpan.Hours}Сʱ {timeSpan.Minutes}";
+ }
+ }
+
+ public void RefreshNumbersList()
+ {
+ var trainNumbersList = _databaseService.GetAllTrainNumbers();
+ NumbersList = new List(trainNumbersList.Select(s => s.Number));
+ }
+
+ [RelayCommand]
+ public void NumberSelected(object selectedNumber)
+ {
+ if(selectedNumber is string v)
+ {
+ SelectedTrainNumber = _databaseService.GetTrainNumberByNumber(v);
+
+ TrainStopsList.Clear();
+ foreach (TrainStop t in SelectedTrainNumber.TimeTable)
+ {
+ TrainStopsList.Add(t);
+ }
+ IsSelected = true;
+ }
+ if(selectedNumber == null)
+ {
+ IsSelected = false;
+ }
+ }
+ [RelayCommand]
+ public async void GetTrainStopsOnline()
+ {
+ if (TrainStopsList.Count != 0)
+ {
+ if (!_dialogService.GetConfirm("ǰʱ̱Ƿ"))
+ {
+ return;
+ }
+ }
+
+ var stops = await _networkService.GetTrainStopsAsync(SelectedTrainNumber.Number);
+ if (stops != null)
+ {
+ TrainStopsList.Clear();
+ foreach (TrainStop t in stops)
+ {
+ TrainStopsList.Add(t);
+ }
+ }
+ else
+ {
+ _dialogService.ShowMessage("ȡʧ", "δҵóΡ");
+ }
+ }
+ [RelayCommand]
+ public void AddTrainStop(object selectedTrainStop)
+ {
+ var newTrainStop = _dialogService.GetInputTrainStop();
+ if (newTrainStop != null)
+ {
+ if (TrainStopsList.Any(x => x.Station == newTrainStop.Station))
+ {
+ _dialogService.ShowMessage("ʧ", $"ͣվ {newTrainStop.Station} Ѵڡ");
+ return;
+ }
+ if (TrainStopsList.Any(x => x.ArrivalTime == null) && newTrainStop.ArrivalTime == null)
+ {
+ _dialogService.ShowMessage("ʧ", $"ʼվѴڡ");
+ return;
+ }
+ if (TrainStopsList.Any(x => x.DepartureTime == null) && newTrainStop.DepartureTime == null)
+ {
+ _dialogService.ShowMessage("ʧ", $"յվѴڡ");
+ return;
+ }
+ if (selectedTrainStop != null && newTrainStop.DepartureTime == null && TrainStopsList.IndexOf((TrainStop)selectedTrainStop) != TrainStopsList.Count-1)
+ {
+ _dialogService.ShowMessage("ʧ", $"յվֻܼʱ̱ĩλ");
+ return;
+ }
+ if(TrainStopsList.Count==0 && newTrainStop.ArrivalTime != null)
+ {
+ _dialogService.ShowMessage("ʧ", $"ʱ̱λֻʼվ");
+ return;
+ }
+ if (selectedTrainStop != null)
+ {
+ TrainStopsList.Insert(TrainStopsList.IndexOf((TrainStop)selectedTrainStop)+1, newTrainStop);
+ }
+ else
+ {
+ TrainStopsList.Add(newTrainStop);
+ }
+ }
+ }
+ [RelayCommand]
+ public void DeleteTrainStop(object selectedTrainStop)
+ {
+ if(selectedTrainStop is TrainStop s)
+ {
+ TrainStopsList.Remove(s);
+ }
+ }
+ [RelayCommand]
+ public void EditTrainStop(object _selectedTrainStop)
+ {
+ if (_selectedTrainStop is TrainStop selectedTrainStop)
+ {
+ var newTrainStop = _dialogService.GetInputTrainStop(selectedTrainStop);
+ if (newTrainStop != null)
+ {
+ TrainStopsList[TrainStopsList.IndexOf(selectedTrainStop)] = newTrainStop;
+ }
+ }
+ }
+ [RelayCommand]
+ public async Task AddTrainNumber()
+ {
+ string? number = _dialogService.GetInput("복κ").ToUpper();
+ if (number != null)
+ {
+ if (NumbersList.Contains(number))
+ {
+ _dialogService.ShowMessage("ʧ", $"κ {number} Ѵڡ");
+ return;
+ }
+ _databaseService.AddTrainNumber(number);
+ await _databaseService.SaveData();
+ RefreshNumbersList();
+ }
+ }
+ [RelayCommand]
+ public async Task DeleteTrainNumber(object selectedTrainNumber)
+ {
+ if (selectedTrainNumber is string s && !String.IsNullOrEmpty(s))
+ {
+ _databaseService.DeleteTrainNumber(_databaseService.GetTrainNumberByNumber(s));
+ await _databaseService.SaveData();
+ RefreshNumbersList();
+ }
+ }
+ [RelayCommand]
+ public async Task DeleteAllTrainNumbers()
+ {
+ if (!_dialogService.GetConfirm("ǰɾȫΣǷ")) return;
+ var trainNumbers = _databaseService.GetAllTrainNumbers().ToList();
+ foreach (TrainNumber trainNumber in trainNumbers)
+ {
+ _databaseService.DeleteTrainNumber(trainNumber);
+ }
+ await _databaseService.SaveData();
+ RefreshNumbersList();
+ }
+ [RelayCommand]
+ public async Task SaveChanges()
+ {
+ if (TrainStopsList.First().ArrivalTime != null)
+ {
+ _dialogService.ShowMessage("ʧ", "ʱ̱λʼվ");
+ return;
+ }
+ if (TrainStopsList.Last().DepartureTime != null)
+ {
+ _dialogService.ShowMessage("ʧ", "ʱ̱ĩλյվ");
+ return;
+ }
+ _databaseService.UpdateTimeTable(SelectedTrainNumber, [.. TrainStopsList]);
+ await _databaseService.SaveData();
+ }
+ [RelayCommand]
+ public async void ImportFromLulutong()
+ {
+ var path = _dialogService.GetFile("CSV ļ (*.csv)|*.csv|ļ (*.*)|*.*");
+ if (path == null) return;
+ try
+ {
+ var numbers = ReadCsvFirstColumn(path);
+ var intersection = numbers.Intersect(NumbersList).ToList();
+ if (intersection.Count != 0)
+ {
+ if (!_dialogService.GetConfirm("ǰܸгãǷ")) return;
+ }
+ foreach (string s in intersection)
+ {
+ _databaseService.DeleteTrainNumber(_databaseService.GetTrainNumberByNumber(s));
+ }
+ RefreshNumbersList();
+ int total = numbers.Count;
+ int current = 0;
+ foreach (string number in numbers)
+ {
+ current++;
+ ProgressValue = (int)((double)current / total * 100);
+ List? timeTable = await _networkService.GetTrainStopsAsync(number);
+ await Task.Delay(1000);
+ timeTable ??= [];
+ _databaseService.AddTrainNumber(number);
+ _databaseService.UpdateTimeTable(_databaseService.GetTrainNumberByNumber(number), timeTable);
+ }
+ ProgressValue = 0;
+ await _databaseService.SaveData();
+ RefreshNumbersList();
+ }
+ catch
+ {
+ _dialogService.ShowMessage("ʧ", "ļʽռá");
+ }
+ }
+ private static List ReadCsvFirstColumn(string filePath)
+ {
+ var firstColumnData = new List();
+
+ // ʹ StreamReader ȡļ
+ using (var reader = new StreamReader(filePath))
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine(); // ȡÿһ
+ var columns = line.Split(','); // նŷָ
+
+ if (columns.Length > 0)
+ {
+ var firstColumn = columns[0].Trim(); // ȡȥβո
+
+ if (!string.IsNullOrEmpty(firstColumn) && (char.IsLetterOrDigit(firstColumn[0])))
+ {
+ int spaceIndex = firstColumn.IndexOf(' ');
+ if (spaceIndex != -1)
+ {
+ firstColumn = firstColumn[..spaceIndex]; // ȡոǰIJ
+ }
+
+ if (firstColumnData.Contains(firstColumn)) continue;
+ firstColumnData.Add(firstColumn); // ӵб
+ }
+ }
+ }
+ }
+ return firstColumnData;
+ }
+}
diff --git a/CRSim/ViewModels/Pages/DataManagementPageViewModel.cs b/CRSim/ViewModels/Pages/DataManagementPageViewModel.cs
new file mode 100644
index 0000000..5d695d4
--- /dev/null
+++ b/CRSim/ViewModels/Pages/DataManagementPageViewModel.cs
@@ -0,0 +1,26 @@
+namespace CRSim.ViewModels
+{
+ public partial class DataManagementPageViewModel(INavigationService navigationService) : ObservableObject
+ {
+ [ObservableProperty]
+ private string _pageTitle = "ݹ";
+
+ [ObservableProperty]
+ private string _pageDescription = "վΡ·Ϣ";
+
+ [ObservableProperty]
+ private ICollection _navigationCards = ControlsInfoDataSource.Instance.GetControlsInfo("Data Management");
+
+ private readonly INavigationService _navigationService = navigationService;
+
+ [RelayCommand]
+ public void Navigate(object pageType){
+ if (pageType is Type page)
+ {
+ _navigationService.NavigateTo(page);
+ }
+ }
+
+
+ }
+}
diff --git a/CRSim/ViewModels/Pages/SettingsPageViewModel.cs b/CRSim/ViewModels/Pages/SettingsPageViewModel.cs
new file mode 100644
index 0000000..0da9e21
--- /dev/null
+++ b/CRSim/ViewModels/Pages/SettingsPageViewModel.cs
@@ -0,0 +1,148 @@
+namespace CRSim.ViewModels
+{
+ public partial class SettingsPageViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private string _pageTitle = "设置";
+ [ObservableProperty]
+ private string _appVersion = "";
+ private readonly Settings _settings;
+ private readonly ISettingsService _settingsService;
+ private readonly IDatabaseService _databaseService;
+ private readonly IDialogService _dialogService;
+
+ public SettingsPageViewModel(ISettingsService settingsService, IDatabaseService databaseService, IDialogService dialogService)
+ {
+ _settings = settingsService.GetSettings();
+ _settingsService = settingsService;
+ _databaseService = databaseService;
+ _dialogService = dialogService;
+ var version = Assembly.GetExecutingAssembly().GetName().Version;
+ AppVersion = $"CRSim v{version}";
+ _dialogService = dialogService;
+ }
+ #region 偏好设置
+ public string TimeOffset
+ {
+ get => _settings.TimeOffset.TotalMinutes.ToString();
+ set
+ {
+ if (double.TryParse(value, out double minutes))
+ {
+ _settings.TimeOffset = TimeSpan.FromMinutes(minutes);
+ OnPropertyChanged(nameof(TimeOffset));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+ public string DepartureCheckInAdvanceDuration
+ {
+ get => _settings.DepartureCheckInAdvanceDuration.TotalMinutes.ToString();
+ set
+ {
+ if (int.TryParse(value, out int minutes))
+ {
+ _settings.DepartureCheckInAdvanceDuration = TimeSpan.FromMinutes(minutes);
+ OnPropertyChanged(nameof(DepartureCheckInAdvanceDuration));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+
+ public string PassingCheckInAdvanceDuration
+ {
+ get => _settings.PassingCheckInAdvanceDuration.TotalMinutes.ToString();
+ set
+ {
+ if (int.TryParse(value, out int minutes))
+ {
+ _settings.PassingCheckInAdvanceDuration = TimeSpan.FromMinutes(minutes);
+ OnPropertyChanged(nameof(PassingCheckInAdvanceDuration));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+
+ public string StopDisplayUntilDepartureDuration
+ {
+ get => _settings.StopDisplayUntilDepartureDuration.TotalMinutes.ToString();
+ set
+ {
+ if (int.TryParse(value, out int minutes))
+ {
+ _settings.StopDisplayUntilDepartureDuration = TimeSpan.FromMinutes(minutes);
+ OnPropertyChanged(nameof(StopDisplayUntilDepartureDuration));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+
+ public string StopCheckInAdvanceDuration
+ {
+ get => _settings.StopCheckInAdvanceDuration.TotalMinutes.ToString();
+ set
+ {
+ if (int.TryParse(value, out int minutes))
+ {
+ _settings.StopCheckInAdvanceDuration = TimeSpan.FromMinutes(minutes);
+ OnPropertyChanged(nameof(StopCheckInAdvanceDuration));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+
+ public string MaxPages
+ {
+ get => _settings.MaxPages.ToString();
+ set
+ {
+ if (int.TryParse(value, out int maxPages))
+ {
+ _settings.MaxPages = maxPages;
+ OnPropertyChanged(nameof(MaxPages));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+
+ public string SwitchPageSeconds
+ {
+ get => _settings.SwitchPageSeconds.ToString();
+ set
+ {
+ if (int.TryParse(value, out int switchPageSeconds))
+ {
+ _settings.SwitchPageSeconds = switchPageSeconds;
+ OnPropertyChanged(nameof(SwitchPageSeconds));
+ _settingsService.SaveSettings();
+ }
+ }
+ }
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected void OnPropertyChanged(string propertyName) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ #endregion
+ [RelayCommand]
+ public async Task ClearData()
+ {
+ if (!_dialogService.GetConfirm("该操作将会清空所有数据,请否继续?")) return;
+ _databaseService.ClearData();
+ await _databaseService.SaveData();
+ }
+ [RelayCommand]
+ public async Task ExportData()
+ {
+ string? path = _dialogService.SaveFile("JSON 文件 (*.json)|*.json|所有文件 (*.*)|*.*","data");
+ if (string.IsNullOrWhiteSpace(path)) return;
+ await _databaseService.ExportData(path);
+ }
+ [RelayCommand]
+ public async Task ImportData()
+ {
+ string? path = _dialogService.GetFile("JSON 文件 (*.json)|*.json|所有文件 (*.*)|*.*");
+ if (string.IsNullOrWhiteSpace(path)) return;
+ _databaseService.ImportData(path);
+ await _databaseService.SaveData();
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Pages/Simulation/ScreenSimulationPageViewModel.cs b/CRSim/ViewModels/Pages/Simulation/ScreenSimulationPageViewModel.cs
new file mode 100644
index 0000000..8f16155
--- /dev/null
+++ b/CRSim/ViewModels/Pages/Simulation/ScreenSimulationPageViewModel.cs
@@ -0,0 +1,138 @@
+namespace CRSim.ViewModels;
+
+public partial class ScreenSimulationPageViewModel : ObservableObject
+{
+ [ObservableProperty]
+ private string _pageTitle = "ģ";
+
+ [ObservableProperty]
+ private string _pageDescription = "";
+
+ [ObservableProperty]
+ private ICollection _styleCards = StylesInfoDataSource.Instance.StylesInfo;
+
+ private Type selectedStyle;
+
+ [ObservableProperty]
+ private string _selectedStyleName = "";
+
+ [ObservableProperty]
+ private bool _stationNeeded;
+
+ [ObservableProperty]
+ private bool _ticketCheckNeeded;
+
+ [ObservableProperty]
+ private bool _platformNeeded;
+
+ [ObservableProperty]
+ private bool _textNeeded;
+
+ [ObservableProperty]
+ private string _text = "";
+
+ public string SelectedStationName = "";
+
+ public string SelectedTicketCheck = "";
+
+ public string SelectedPlatform = "";
+
+ public ObservableCollection StationNames { get; private set; } = [];
+ public ObservableCollection TicketChecks { get; private set; } = [];
+ public ObservableCollection Platforms { get; private set; } = [];
+
+ [ObservableProperty]
+ private bool _isStartSimulationAvailable = false;
+
+ private readonly IDatabaseService _databaseService;
+
+ private readonly IServiceProvider _serviceProvider;
+
+ private readonly IDialogService _dialogService;
+ public ScreenSimulationPageViewModel(IDatabaseService databaseService, IServiceProvider serviceProvider, IDialogService dialogService)
+ {
+ _databaseService = databaseService;
+ _serviceProvider = serviceProvider;
+ _dialogService = dialogService;
+ foreach (Station station in _databaseService.GetAllStations())
+ {
+ StationNames.Add(station.Name);
+ }
+ }
+
+ private void CheckCanStart()
+ {
+ IsStartSimulationAvailable = selectedStyle!=null &&
+ (!StationNeeded || !string.IsNullOrWhiteSpace(SelectedStationName))&&
+ (!TicketCheckNeeded || !string.IsNullOrWhiteSpace(SelectedTicketCheck)) &&
+ (!PlatformNeeded || !string.IsNullOrWhiteSpace(SelectedPlatform));
+ }
+
+ [RelayCommand]
+ public void SelectStyle(object s)
+ {
+ if (s is Type style)
+ {
+ selectedStyle = style;
+ SelectedStyleName = StylesInfoDataSource.Instance.GetStyleName(style);
+ var neededParameters = StylesInfoDataSource.Instance.GetParametersInfo(style);
+ StationNeeded = neededParameters.Contains("Station");
+ TicketCheckNeeded = neededParameters.Contains("TicketCheck");
+ PlatformNeeded = neededParameters.Contains("Platform");
+ TextNeeded = neededParameters.Contains("Text");
+ }
+ CheckCanStart();
+ }
+ [RelayCommand]
+ public void SelectStation(object s)
+ {
+ if(s is string station)
+ {
+ SelectedStationName = station;
+ TicketChecks.Clear();
+ foreach(string ticketCheck in _databaseService.GetStationByName(SelectedStationName).TicketChecks)
+ {
+ TicketChecks.Add(ticketCheck);
+ }
+ Platforms.Clear();
+ foreach (string platform in _databaseService.GetStationByName(SelectedStationName).Platforms)
+ {
+ Platforms.Add(platform);
+ }
+ }
+ CheckCanStart();
+ }
+ [RelayCommand]
+ public void SelectTicketCheck(object s)
+ {
+ if (s is string ticketCheck)
+ {
+ SelectedTicketCheck = ticketCheck;
+ }
+ CheckCanStart();
+ }
+ [RelayCommand]
+ public void SelectPlatform(object s)
+ {
+ if (s is string platform)
+ {
+ SelectedPlatform = platform;
+ }
+ CheckCanStart();
+ }
+ [RelayCommand]
+ public void StartSimulation()
+ {
+ var Window = _serviceProvider.GetRequiredService(selectedStyle) as dynamic;
+
+ if (TextNeeded && Text != string.Empty)
+ {
+ Window.ViewModel.Text = Text;
+ }
+
+ Window.ViewModel.LoadData(_databaseService.GetStationByName(SelectedStationName),
+ TicketCheckNeeded ? SelectedTicketCheck : string.Empty,
+ PlatformNeeded ? SelectedPlatform : string.Empty);
+ Window.Show();
+ }
+}
diff --git a/CRSim/ViewModels/Pages/StartSimulationPageViewModel.cs b/CRSim/ViewModels/Pages/StartSimulationPageViewModel.cs
new file mode 100644
index 0000000..33344c0
--- /dev/null
+++ b/CRSim/ViewModels/Pages/StartSimulationPageViewModel.cs
@@ -0,0 +1,26 @@
+namespace CRSim.ViewModels
+{
+ public partial class StartSimulationPageViewModel(INavigationService navigationService) : ObservableObject
+ {
+ [ObservableProperty]
+ private string _pageTitle = "ʼģ";
+
+ [ObservableProperty]
+ private string _pageDescription = "ѡģĿ";
+
+ [ObservableProperty]
+ private ICollection _navigationCards = ControlsInfoDataSource.Instance.GetControlsInfo("Start Simulation");
+
+ private readonly INavigationService _navigationService = navigationService;
+
+ [RelayCommand]
+ public void Navigate(object pageType){
+ if (pageType is Type page)
+ {
+ _navigationService.NavigateTo(page);
+ }
+ }
+
+
+ }
+}
diff --git a/CRSim/ViewModels/Windows/MainWindowViewModel.cs b/CRSim/ViewModels/Windows/MainWindowViewModel.cs
new file mode 100644
index 0000000..36d8545
--- /dev/null
+++ b/CRSim/ViewModels/Windows/MainWindowViewModel.cs
@@ -0,0 +1,151 @@
+namespace CRSim.ViewModels
+{
+ public partial class MainWindowViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private string _applicationTitle = "CRSim";
+
+ private readonly DispatcherTimer _timer;
+
+ private string _searchText = string.Empty;
+
+ [ObservableProperty]
+ private ICollection _controls;
+ [ObservableProperty]
+ private ControlInfoDataItem? _selectedControl;
+ private readonly INavigationService _navigationService;
+ [ObservableProperty]
+ private bool _canNavigateback;
+
+ [RelayCommand]
+ public void Settings()
+ {
+ _navigationService.Navigate(typeof(SettingsPage));
+ }
+
+ [RelayCommand]
+ public void Back()
+ {
+ _navigationService.NavigateBack();
+ }
+
+ [RelayCommand]
+ public void Forward()
+ {
+ _navigationService.NavigateForward();
+ }
+
+ public MainWindowViewModel(INavigationService navigationService)
+ {
+ _controls = ControlsInfoDataSource.Instance.ControlsInfo;
+ _navigationService = navigationService;
+
+ _timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(400)
+ };
+ _timer.Tick += PerformSearchNavigation;
+ }
+
+ public void UpdateSearchText(string searchText)
+ {
+ _searchText = searchText;
+ _timer.Stop();
+ _timer.Start();
+ }
+
+ private void PerformSearchNavigation(object? sender, EventArgs e)
+ {
+ _timer.Stop();
+ if (string.IsNullOrWhiteSpace(_searchText))
+ {
+ return;
+ }
+
+ _navigationService.NavigateTo(GetNavigationPageTypeFromName(_searchText, _controls));
+ }
+
+ private static Type? GetNavigationPageTypeFromName(string name, ICollection pages)
+ {
+ if (pages == null)
+ {
+ return null;
+ }
+
+ foreach (var item in pages)
+ {
+ if (item.Title.Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ return item.PageType!;
+ }
+
+ Type? type = GetNavigationPageTypeFromName(name, item.Items);
+ if (type != null)
+ {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ internal List GetNavigationItemHierarchyFromPageType(Type? pageType)
+ {
+ List list = [];
+ Stack _stack = [];
+ Stack _revStack = [];
+
+ if (pageType == null)
+ {
+ return list;
+ }
+
+ foreach (var item in Controls)
+ {
+ _stack.Push(item);
+ bool found = FindNavigationItemsHierarchyFromPageType(pageType, ref _stack);
+ if (found)
+ {
+ break;
+ }
+ _stack.Pop();
+ }
+
+ while (_stack.Count > 0)
+ {
+ _revStack.Push(_stack.Pop());
+ }
+
+ foreach (var item in _revStack)
+ {
+ list.Add(item);
+ }
+
+ return list;
+ }
+
+ private static bool FindNavigationItemsHierarchyFromPageType(Type pageType, ref Stack stack)
+ {
+ var item = stack.Peek();
+ if (pageType == item.PageType)
+ {
+ return true;
+ }
+
+ foreach (var child in item.Items)
+ {
+ stack.Push(child);
+ bool found = FindNavigationItemsHierarchyFromPageType(pageType, ref stack);
+ if (found) { return true; }
+ stack.Pop();
+ }
+
+ return false;
+ }
+
+ internal void UpdateCanNavigateBack()
+ {
+ CanNavigateback = _navigationService.IsBackHistoryNonEmpty();
+ }
+
+ }
+}
diff --git a/CRSim/ViewModels/Windows/ScreenViewModel.cs b/CRSim/ViewModels/Windows/ScreenViewModel.cs
new file mode 100644
index 0000000..a6521b9
--- /dev/null
+++ b/CRSim/ViewModels/Windows/ScreenViewModel.cs
@@ -0,0 +1,98 @@
+using System.Linq;
+
+namespace CRSim.ViewModels
+{
+ public partial class ScreenViewModel : ObservableObject
+ {
+ private readonly ITimeService _timeService;
+ public readonly Settings _settings;
+ private readonly TaskCompletionSource _dataLoaded = new();
+ [ObservableProperty]
+ private string _currentTime;
+ [ObservableProperty]
+ private string _currentDate;
+ [ObservableProperty]
+ private string _currentSecond;
+ [ObservableProperty]
+ private string _text = "";
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public List TrainInfo { get; set; } = [];
+
+ protected ScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ {
+
+ _timeService = timeService;
+ _settings = settingsService.GetSettings();
+
+ _timeService.OneSecondElapsed += OnTimeElapsed;
+ _timeService.RefreshSecondsElapsed += RefreshData;
+ }
+ public int CurrentPageIndex = 0;
+ public int ItemsPerPage;
+ public int PageCount;
+
+ public virtual void RefreshData(object? sender, EventArgs e)
+ {
+ if (CurrentPageIndex==0)
+ {
+ List itemsToRemove = [.. TrainInfo.Where(info => info.DepartureTime.Subtract(_settings.StopDisplayUntilDepartureDuration) < _timeService.GetDateTimeNow())];
+
+ foreach (var item in itemsToRemove)
+ {
+ TrainInfo.Remove(item);
+ }
+ }
+ }
+
+ private void OnTimeElapsed(object? sender, EventArgs e)
+ {
+ CurrentTime = _timeService.GetDateTimeNow().ToString("yyyy-MM-dd HH:mm:ss");
+ CurrentDate = _timeService.GetDateTimeNow().ToString("yyyy-MM-dd");
+ CurrentSecond = _timeService.GetDateTimeNow().ToString("HH:mm:ss");
+ }
+
+ public Task WaitForDataLoadAsync() => _dataLoaded.Task;
+
+ public void LoadData(Station station,string ticketCheck,string platform)
+ {
+ var trains = station.StationStops;
+ foreach (var trainNumber in trains)
+ {
+ if (trainNumber != null && trainNumber.DepartureTime != null && (ticketCheck==string.Empty || trainNumber.TicketChecks.Contains(ticketCheck)) && (platform == string.Empty || trainNumber.Platform==platform))
+ {
+ TrainInfo.Add(new TrainInfo
+ {
+ TrainNumber = trainNumber.Number,
+ Terminal = trainNumber.Terminal,
+ Origin = trainNumber.Origin,
+ ArrivalTime = trainNumber.ArrivalTime,
+ DepartureTime = trainNumber.DepartureTime ?? DateTime.MinValue,
+ TicketChecks = trainNumber.TicketChecks,
+ WaitingArea = station.WaitingAreas.Where(x => x.TicketChecks.Intersect(trainNumber.TicketChecks).ToList().Count!=0).FirstOrDefault().Name,
+ Platform = trainNumber.Platform,
+ Landmark = trainNumber.Landmark,
+ State = TimeSpan.Zero
+ });
+ TrainInfo.Add(new TrainInfo
+ {
+ TrainNumber = trainNumber.Number,
+ Terminal = trainNumber.Terminal,
+ Origin = trainNumber.Origin,
+ ArrivalTime = trainNumber.ArrivalTime?.AddDays(1),
+ DepartureTime = (trainNumber.DepartureTime ?? DateTime.MinValue).AddDays(1),
+ TicketChecks = trainNumber.TicketChecks,
+ WaitingArea = station.WaitingAreas.Where(x => x.TicketChecks.Intersect(trainNumber.TicketChecks).ToList().Count != 0).FirstOrDefault().Name,
+ Platform = trainNumber.Platform,
+ Landmark= trainNumber.Landmark,
+ State = TimeSpan.Zero
+ });
+ }
+ }
+ TrainInfo = [.. TrainInfo.OrderBy(x => x.DepartureTime)];
+ RefreshData(null, null);
+ _dataLoaded.SetResult(true);
+ }
+
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/BeijingXi/PrimaryScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/BeijingXi/PrimaryScreenViewModel.cs
new file mode 100644
index 0000000..d1fd989
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/BeijingXi/PrimaryScreenViewModel.cs
@@ -0,0 +1,53 @@
+namespace CRSim.ViewModels.BeijingXi
+{
+ public class PrimaryScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection LeftScreen { get; private set; } = [];
+ public ObservableCollection RightScreen { get; private set; } = [];
+ public PrimaryScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ ItemsPerPage = 13;
+ PageCount = 2;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ int pageCount = (int)Math.Ceiling((double)TrainInfo.Count / (ItemsPerPage * PageCount));
+ LeftScreen.Clear();
+ RightScreen.Clear();
+ int startIndex = CurrentPageIndex * ItemsPerPage * PageCount;
+ var leftItems = TrainInfo.Skip(startIndex).Take(ItemsPerPage).ToList();
+ while (leftItems.Count < ItemsPerPage) // Fill up with new object() if not enough items
+ {
+ leftItems.Add(new TrainInfo());
+ }
+ foreach (var item in leftItems)
+ {
+ LeftScreen.Add(item);
+ }
+
+ // Get the slice for the right screen (next 12)
+ var rightItems = TrainInfo.Skip(startIndex + ItemsPerPage).Take(ItemsPerPage).ToList();
+ while (rightItems.Count < ItemsPerPage) // Fill up with new object() if not enough items
+ {
+ rightItems.Add(new TrainInfo());
+ }
+ foreach (var item in rightItems)
+ {
+ RightScreen.Add(item);
+ }
+
+ CurrentPageIndex = CurrentPageIndex + 1 >= Math.Min(_settings.MaxPages, pageCount) ? 0 : CurrentPageIndex + 1;
+ });
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenViewModel.cs
new file mode 100644
index 0000000..bb01d71
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenViewModel.cs
@@ -0,0 +1,31 @@
+namespace CRSim.ViewModels.ChengduDong
+{
+ public class PlatformScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection Screen { get; set; } = [];
+ public PlatformScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ Text = "持户口簿、护照、临时身份证的旅客,请在非居民身份证通道排队。请主动礼让老、弱、病、残、孕的旅客。";
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ ItemsPerPage = 2;
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Screen.Clear();
+ for (int i = 0; i < ItemsPerPage; i++)
+ {
+ Screen.Add(TrainInfo.Count > i ? TrainInfo[i] : new());
+ }
+ });
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/ChengduDong/SecondaryScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/ChengduDong/SecondaryScreenViewModel.cs
new file mode 100644
index 0000000..ad3b9a0
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/ChengduDong/SecondaryScreenViewModel.cs
@@ -0,0 +1,51 @@
+namespace CRSim.ViewModels.ChengduDong
+{
+ public class SecondaryScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection LeftScreen { get; private set; } = [];
+ public ObservableCollection RightScreen { get; private set; } = [];
+ public SecondaryScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ Text = " 检票口信息请以车站显示屏信息为准。公共场所请妥善保管个人贵重物品。";
+ ItemsPerPage = 12;
+ PageCount = 2;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ int pageCount = (int)Math.Ceiling((double)TrainInfo.Count / (ItemsPerPage * PageCount));
+ LeftScreen.Clear();
+ RightScreen.Clear();
+ int startIndex = CurrentPageIndex * ItemsPerPage * PageCount;
+ var leftItems = TrainInfo.Skip(startIndex).Take(ItemsPerPage).ToList();
+ while (leftItems.Count < ItemsPerPage)
+ {
+ leftItems.Add(new TrainInfo());
+ }
+ foreach (var item in leftItems)
+ {
+ LeftScreen.Add(item);
+ }
+ var rightItems = TrainInfo.Skip(startIndex + ItemsPerPage).Take(ItemsPerPage).ToList();
+ while (rightItems.Count < ItemsPerPage)
+ {
+ rightItems.Add(new TrainInfo());
+ }
+ foreach (var item in rightItems)
+ {
+ RightScreen.Add(item);
+ }
+ CurrentPageIndex = CurrentPageIndex + 1 >= Math.Min(_settings.MaxPages, pageCount) ? 0 : CurrentPageIndex + 1;
+ });
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/ChengduMetro/PlatformScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/ChengduMetro/PlatformScreenViewModel.cs
new file mode 100644
index 0000000..d01cd5d
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/ChengduMetro/PlatformScreenViewModel.cs
@@ -0,0 +1,63 @@
+namespace CRSim.ViewModels.ChengduMetro
+{
+ public partial class PlatformScreenViewModel : ObservableObject
+ {
+ private readonly ITimeService _timeService;
+ [ObservableProperty]
+ private DateTime _currentTime = new();
+ [ObservableProperty]
+ private TrainInfo _firstTrain = new();
+ [ObservableProperty]
+ private TrainInfo _secondTrain = new();
+ [ObservableProperty]
+ private string _text = "不吃火锅,就吃烤匠。 建设成渝地区双城经济圈,共同唱好新时代西部“双城记”。 2018年2月8日,复兴号列车首次担当G89次列车,正式开进四川,标志着“复兴号”高速列车正式走上蜀道。然而,在行驶至西安北站时,发生了突发设备故障,车轴出现过热报警,导致车号为CR400BF-5033的复兴号列车被迫停运并进行检修。相关工作人员迅速展开故障排查,并确保乘客安全无虞。";
+ public List TrainInfos { get; set; } = [];
+ public PlatformScreenViewModel(ITimeService timeService)
+ {
+ _timeService = timeService;
+ timeService.OneSecondElapsed += RefreshDisplay;
+ }
+ public void LoadData(Station station,string _ticketCheck,string _platform)
+ {
+ var trains = station.StationStops;
+ foreach (var trainNumber in trains)
+ {
+ if (trainNumber != null && trainNumber.DepartureTime != null)
+ {
+ TrainInfos.Add(new TrainInfo
+ {
+ TrainNumber = trainNumber.Number,
+ Terminal = trainNumber.Terminal,
+ ArrivalTime = trainNumber.ArrivalTime,
+ DepartureTime = trainNumber.DepartureTime ?? DateTime.MinValue,
+ State = TimeSpan.Zero
+ });
+ TrainInfos.Add(new TrainInfo
+ {
+ TrainNumber = trainNumber.Number,
+ Terminal = trainNumber.Terminal,
+ ArrivalTime = trainNumber.ArrivalTime?.AddDays(1),
+ DepartureTime = (trainNumber.DepartureTime ?? DateTime.MinValue).AddDays(1),
+ State = TimeSpan.Zero
+ });
+ }
+ }
+ TrainInfos = [.. TrainInfos.OrderBy(x => x.ArrivalTime ?? x.DepartureTime)];
+ }
+
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ CurrentTime = _timeService.GetDateTimeNow();
+ List itemsToRemove = [.. TrainInfos.Where(info => info.DepartureTime < _timeService.GetDateTimeNow())];
+ foreach (var item in itemsToRemove)
+ {
+ TrainInfos.Remove(item);
+ }
+ FirstTrain = TrainInfos.Count > 0 ? TrainInfos[0] : new();
+ SecondTrain = TrainInfos.Count > 1 ? TrainInfos[1] : new();
+
+ OnPropertyChanged(nameof(FirstTrain));
+ OnPropertyChanged(nameof(SecondTrain));
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/Guangyuan/PrimaryScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/Guangyuan/PrimaryScreenViewModel.cs
new file mode 100644
index 0000000..fb0a882
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/Guangyuan/PrimaryScreenViewModel.cs
@@ -0,0 +1,40 @@
+namespace CRSim.ViewModels.Guangyuan
+{
+ public class OutsideScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection Screen { get; private set; } = [];
+ public OutsideScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ Text = "成都铁路客服电话:028-12306";
+ ItemsPerPage = 12;
+ PageCount = 1;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ int pageCount = (int)Math.Ceiling((double)TrainInfo.Count / (ItemsPerPage * PageCount));
+ Screen.Clear();
+ int startIndex = CurrentPageIndex * ItemsPerPage * PageCount;
+ var items = TrainInfo.Skip(startIndex).Take(ItemsPerPage).ToList();
+ while (items.Count < ItemsPerPage)
+ {
+ items.Add(new TrainInfo());
+ }
+ foreach (var item in items)
+ {
+ Screen.Add(item);
+ }
+ CurrentPageIndex = CurrentPageIndex + 1 >= Math.Min(_settings.MaxPages, pageCount) ? 0 : CurrentPageIndex + 1;
+ });
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/Hanzhong/PlatformScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/Hanzhong/PlatformScreenViewModel.cs
new file mode 100644
index 0000000..e5ba0e5
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/Hanzhong/PlatformScreenViewModel.cs
@@ -0,0 +1,40 @@
+namespace CRSim.ViewModels.Hanzhong
+{
+ public class PlatformScreenViewModel : ScreenViewModel
+ {
+ private ITimeService _timeService;
+ public ObservableCollection Screen { get; set; } = [];
+ public PlatformScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ _timeService = timeService;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Screen.Add(TrainInfo.Count > 0 ? TrainInfo[0] : new());
+ if (Screen.Count == 1) return;
+ Screen.RemoveAt(0);
+ });
+ }
+ public override void RefreshData(object? sender, EventArgs e)
+ {
+ if (CurrentPageIndex == 0)
+ {
+ List itemsToRemove = [.. TrainInfo.Where(info => info.DepartureTime < _timeService.GetDateTimeNow())];
+ foreach (var item in itemsToRemove)
+ {
+ TrainInfo.Remove(item);
+ }
+ }
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/Harbin/PrimaryScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/Harbin/PrimaryScreenViewModel.cs
new file mode 100644
index 0000000..e0e29f0
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/Harbin/PrimaryScreenViewModel.cs
@@ -0,0 +1,54 @@
+namespace CRSim.ViewModels.Harbin
+{
+ public class PrimaryScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection LeftScreen { get; private set; } = [];
+ public ObservableCollection RightScreen { get; private set; } = [];
+ public PrimaryScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ Text = $"开车前{settingsService.GetSettings().StopCheckInAdvanceDuration.TotalMinutes}分钟停止检票";
+ ItemsPerPage = 6;
+ PageCount = 2;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ int pageCount = (int)Math.Ceiling((double)TrainInfo.Count / (ItemsPerPage * PageCount));
+ LeftScreen.Clear();
+ RightScreen.Clear();
+ int startIndex = CurrentPageIndex * ItemsPerPage * PageCount;
+ var leftItems = TrainInfo.Skip(startIndex).Take(ItemsPerPage).ToList();
+ while (leftItems.Count < ItemsPerPage) // Fill up with new object() if not enough items
+ {
+ leftItems.Add(new TrainInfo());
+ }
+ foreach (var item in leftItems)
+ {
+ LeftScreen.Add(item);
+ }
+
+ // Get the slice for the right screen (next 12)
+ var rightItems = TrainInfo.Skip(startIndex + ItemsPerPage).Take(ItemsPerPage).ToList();
+ while (rightItems.Count < ItemsPerPage) // Fill up with new object() if not enough items
+ {
+ rightItems.Add(new TrainInfo());
+ }
+ foreach (var item in rightItems)
+ {
+ RightScreen.Add(item);
+ }
+
+ CurrentPageIndex = CurrentPageIndex + 1 >= Math.Min(_settings.MaxPages, pageCount) ? 0 : CurrentPageIndex + 1;
+ });
+ }
+ }
+}
diff --git a/CRSim/ViewModels/Windows/Stations/Shanghai/OutsideScreenViewModel.cs b/CRSim/ViewModels/Windows/Stations/Shanghai/OutsideScreenViewModel.cs
new file mode 100644
index 0000000..3deaf26
--- /dev/null
+++ b/CRSim/ViewModels/Windows/Stations/Shanghai/OutsideScreenViewModel.cs
@@ -0,0 +1,39 @@
+namespace CRSim.ViewModels.Shanghai
+{
+ public class OutsideScreenViewModel : ScreenViewModel
+ {
+ public ObservableCollection Screen { get; private set; } = [];
+ public OutsideScreenViewModel(ITimeService timeService, ISettingsService settingsService)
+ : base(timeService, settingsService)
+ {
+ ItemsPerPage = 9;
+ PageCount = 1;
+ timeService.RefreshSecondsElapsed += RefreshDisplay;
+ Initialize();
+ }
+ private async void Initialize()
+ {
+ await WaitForDataLoadAsync();
+ RefreshDisplay(null,null);
+ }
+ private void RefreshDisplay(object? sender, EventArgs e)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ int pageCount = (int)Math.Ceiling((double)TrainInfo.Count / (ItemsPerPage * PageCount));
+ Screen.Clear();
+ int startIndex = CurrentPageIndex * ItemsPerPage * PageCount;
+ var items = TrainInfo.Skip(startIndex).Take(ItemsPerPage).ToList();
+ while (items.Count < ItemsPerPage)
+ {
+ items.Add(new TrainInfo());
+ }
+ foreach (var item in items)
+ {
+ Screen.Add(item);
+ }
+ CurrentPageIndex = CurrentPageIndex + 1 >= Math.Min(_settings.MaxPages, pageCount) ? 0 : CurrentPageIndex + 1;
+ });
+ }
+ }
+}
diff --git a/CRSim/Views/Pages/DashboardPage.xaml b/CRSim/Views/Pages/DashboardPage.xaml
new file mode 100644
index 0000000..c945de8
--- /dev/null
+++ b/CRSim/Views/Pages/DashboardPage.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/DashboardPage.xaml.cs b/CRSim/Views/Pages/DashboardPage.xaml.cs
new file mode 100644
index 0000000..95166f5
--- /dev/null
+++ b/CRSim/Views/Pages/DashboardPage.xaml.cs
@@ -0,0 +1,17 @@
+namespace CRSim.Views
+{
+ ///
+ /// DashboardPage.xaml 的交互逻辑
+ ///
+ public partial class DashboardPage : Page
+ {
+ public DashboardPage(DashboardPageViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = this;
+ }
+
+ public DashboardPageViewModel ViewModel { get; }
+ }
+}
diff --git a/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml b/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml
new file mode 100644
index 0000000..34d2f3d
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml
@@ -0,0 +1,722 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 概览车站设施、车次等信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 管理车站候车室设施
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 管理车站检票口设施
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 管理车站站台设施
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑此车站的车次
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请再次检查各设施配置是否正确
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml.cs b/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml.cs
new file mode 100644
index 0000000..6a7e512
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagement/StationManagementPage.xaml.cs
@@ -0,0 +1,63 @@
+
+namespace CRSim.Views;
+public partial class StationManagementPage : Page
+{
+ public StationManagementPageViewModel ViewModel { get; }
+
+ public StationManagementPage(StationManagementPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ private void StationsList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ StationsList.RaiseEvent(eventArg);
+ }
+
+ private void WaitingAreasList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ WaitingAreasList.RaiseEvent(eventArg);
+ }
+
+ private void TicketChecksList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ TicketChecksList.RaiseEvent(eventArg);
+ }
+
+ private void StationStopsList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ StationStopsList.RaiseEvent(eventArg);
+ }
+
+ private void PlatformsList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ PlatformsList.RaiseEvent(eventArg);
+ }
+}
diff --git a/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml b/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml
new file mode 100644
index 0000000..8ea5abe
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml
@@ -0,0 +1,326 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 概览此车次基本信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑此车次的时刻表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请再次检查各项配置是否正确
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml.cs b/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml.cs
new file mode 100644
index 0000000..3952725
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagement/TrainNumberManagementPage.xaml.cs
@@ -0,0 +1,23 @@
+
+namespace CRSim.Views;
+public partial class TrainNumberManagementPage : Page
+{
+ public TrainNumberManagementPageViewModel ViewModel { get; }
+
+ public TrainNumberManagementPage(TrainNumberManagementPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ private void NumbersList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ NumbersList.RaiseEvent(eventArg);
+ }
+}
diff --git a/CRSim/Views/Pages/DataManagementPage.xaml b/CRSim/Views/Pages/DataManagementPage.xaml
new file mode 100644
index 0000000..fd42c01
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagementPage.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/DataManagementPage.xaml.cs b/CRSim/Views/Pages/DataManagementPage.xaml.cs
new file mode 100644
index 0000000..50949c8
--- /dev/null
+++ b/CRSim/Views/Pages/DataManagementPage.xaml.cs
@@ -0,0 +1,12 @@
+namespace CRSim.Views;
+
+public partial class DataManagementPage : Page
+{
+ public DataManagementPageViewModel ViewModel { get; }
+ public DataManagementPage(DataManagementPageViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = this;
+ }
+}
diff --git a/CRSim/Views/Pages/SettingsPage.xaml b/CRSim/Views/Pages/SettingsPage.xaml
new file mode 100644
index 0000000..12fc1d1
--- /dev/null
+++ b/CRSim/Views/Pages/SettingsPage.xaml
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置时间偏移
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正值为推后时间,负值为提前时间
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置最大页数、切换时长
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设置检票时长、停检时间等
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 从本地文件加载数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存数据到本地文件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 恢复到安装后状态
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 电排骨 2025
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/SettingsPage.xaml.cs b/CRSim/Views/Pages/SettingsPage.xaml.cs
new file mode 100644
index 0000000..561f912
--- /dev/null
+++ b/CRSim/Views/Pages/SettingsPage.xaml.cs
@@ -0,0 +1,33 @@
+using System.Text.RegularExpressions;
+using System.Windows.Documents;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using CRSim.ViewModels;
+
+namespace CRSim.Views
+{
+ ///
+ /// Interaction logic for SettingsPage.xaml
+ ///
+ public partial class SettingsPage : Page
+ {
+ public SettingsPageViewModel ViewModel { get; }
+
+ public SettingsPage(SettingsPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = this;
+ InitializeComponent();
+ }
+ private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
+ {
+ Regex regex = new("^[0-9]+$");
+ e.Handled = !regex.IsMatch(e.Text);
+ }
+ private void NumberValidationTextBox2(object sender, TextCompositionEventArgs e)
+ {
+ Regex regex = new("^-?[0-9]*$");
+ e.Handled = !regex.IsMatch(e.Text);
+ }
+ }
+}
diff --git a/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml b/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml
new file mode 100644
index 0000000..9176a54
--- /dev/null
+++ b/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 你想选哪个就选哪个!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 配置模拟参数
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 按下Esc以关闭窗口
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml.cs b/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml.cs
new file mode 100644
index 0000000..93e22c6
--- /dev/null
+++ b/CRSim/Views/Pages/Simulation/ScreenSimulationPage.xaml.cs
@@ -0,0 +1,13 @@
+namespace CRSim.Views;
+ public partial class ScreenSimulationPage : Page
+ {
+ public ScreenSimulationPageViewModel ViewModel { get; }
+
+ public ScreenSimulationPage(ScreenSimulationPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = this;
+
+ InitializeComponent();
+ }
+ }
diff --git a/CRSim/Views/Pages/StartSimulationPage.xaml b/CRSim/Views/Pages/StartSimulationPage.xaml
new file mode 100644
index 0000000..e78198b
--- /dev/null
+++ b/CRSim/Views/Pages/StartSimulationPage.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Pages/StartSimulationPage.xaml.cs b/CRSim/Views/Pages/StartSimulationPage.xaml.cs
new file mode 100644
index 0000000..61cf4e0
--- /dev/null
+++ b/CRSim/Views/Pages/StartSimulationPage.xaml.cs
@@ -0,0 +1,12 @@
+namespace CRSim.Views;
+
+public partial class StartSimulationPage : Page
+{
+ public StartSimulationPageViewModel ViewModel { get; }
+ public StartSimulationPage(StartSimulationPageViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = this;
+ }
+}
diff --git a/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml b/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml
new file mode 100644
index 0000000..56fd250
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml.cs
new file mode 100644
index 0000000..b939679
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/ConfirmDialog.xaml.cs
@@ -0,0 +1,37 @@
+namespace CRSim.Views
+{
+ ///
+ /// UserInputDialog.xaml 的交互逻辑
+ ///
+ public partial class ConfirmDialog : Window
+ {
+ public ConfirmDialog(string text)
+ {
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ Notice.Text = text;
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Dialogs/InputDialog.xaml b/CRSim/Views/Windows/Dialogs/InputDialog.xaml
new file mode 100644
index 0000000..49012e8
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/InputDialog.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/InputDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/InputDialog.xaml.cs
new file mode 100644
index 0000000..a444144
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/InputDialog.xaml.cs
@@ -0,0 +1,54 @@
+namespace CRSim.Views
+{
+ ///
+ /// UserInputDialog.xaml 的交互逻辑
+ ///
+ public partial class InputDialog : Window
+ {
+ public string UserInput { get; private set; }
+ public InputDialog(string text)
+ {
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ Notice.Text = text;
+ InputTextBox.Focus();
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ UserInput = InputTextBox.Text;
+ this.DialogResult = true;
+ this.Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void InputTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ AccentButton.IsEnabled = !string.IsNullOrWhiteSpace(InputTextBox.Text);
+ }
+
+ private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
+ {
+ if(e.Key == Key.Enter && !string.IsNullOrWhiteSpace(InputTextBox.Text))
+ {
+ OkButton_Click(null, null);
+ }
+ return;
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Dialogs/MessageDialog.xaml b/CRSim/Views/Windows/Dialogs/MessageDialog.xaml
new file mode 100644
index 0000000..c9bd096
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/MessageDialog.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/MessageDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/MessageDialog.xaml.cs
new file mode 100644
index 0000000..4cbe25c
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/MessageDialog.xaml.cs
@@ -0,0 +1,33 @@
+namespace CRSim.Views
+{
+ ///
+ /// UserInputDialog.xaml 的交互逻辑
+ ///
+ public partial class MessageDialog : Window
+ {
+ public string UserInput { get; private set; }
+ public MessageDialog(string text,string description)
+ {
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ Title.Text = text;
+ Description.Text = description;
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ this.DialogResult = true;
+ this.Close();
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml b/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml
new file mode 100644
index 0000000..303776f
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml.cs
new file mode 100644
index 0000000..e236c99
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/StationStopDialog.xaml.cs
@@ -0,0 +1,260 @@
+using CRSim.Core.Models;
+
+namespace CRSim.Views
+{
+ ///
+ /// UserInputDialog.xaml 的交互逻辑
+ ///
+ public partial class StationStopDialog : Window
+ {
+ public class ticketCheck
+ {
+ public string TicketCheck { get; set; }
+ public bool Checked { get; set; } = false;
+ }
+
+ public ObservableCollection Landmarks { get; set; } = ["无", "红色", "绿色", "褐色", "蓝色", "紫色", "黄色", "橙色"];
+
+ public StationStop GeneratedStationStop;
+
+ public ObservableCollection TicketChecksList { get; set; } = [];
+
+ public ObservableCollection Platforms { get; set; } = [];
+
+ public StationStopDialog(List ticketChecks,List platforms)
+ {
+ DataContext = this;
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ IntermediateStation.IsChecked = true;
+ TicketChecksList.Clear();
+ foreach (string s in ticketChecks)
+ {
+ TicketChecksList.Add(new ticketCheck()
+ {
+ TicketCheck = s,
+ Checked = false
+ });
+ }
+ foreach (string s in platforms)
+ {
+ Platforms.Add(s);
+ }
+ ValidateInput();
+ }
+
+ public StationStopDialog(List ticketChecks, List platforms, StationStop stationStop)
+ {
+ DataContext = this;
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ NumberTextBox.IsEnabled = false;
+ NumberTextBox.Text = stationStop.Number;
+ ArrivalTextBox.Text = stationStop.Terminal;
+ DepartureTextBox.Text = stationStop.Origin;
+ StationKindPanel.IsEnabled = false;
+ if (stationStop.ArrivalTime != null)
+ {
+ StartHour.Text = stationStop.ArrivalTime.Value.Hour.ToString();
+ StartMinute.Text = stationStop.ArrivalTime.Value.Minute.ToString();
+ }
+ else
+ {
+ StartingStation.IsChecked = true;
+ }
+ if (stationStop.DepartureTime != null)
+ {
+ EndHour.Text = stationStop.DepartureTime.Value.Hour.ToString();
+ EndMinute.Text = stationStop.DepartureTime.Value.Minute.ToString();
+ }
+ else
+ {
+ FinalStation.IsChecked = true;
+ }
+ if(!StartingStation.IsChecked.Value && !FinalStation.IsChecked.Value)
+ {
+ IntermediateStation.IsChecked = true;
+ }
+ TicketChecksList.Clear();
+ foreach (string s in ticketChecks)
+ {
+ TicketChecksList.Add(new ticketCheck()
+ {
+ TicketCheck = s,
+ Checked = stationStop.TicketChecks.Contains(s)
+ });
+ }
+ foreach (string s in platforms)
+ {
+ Platforms.Add(s);
+ }
+ ValidateInput();
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ GeneratedStationStop = new StationStop
+ {
+ Number = NumberTextBox.Text,
+ Terminal = ArrivalTextBox.Text,
+ Origin = DepartureTextBox.Text,
+ ArrivalTime = StartHour.IsEnabled ? DateTime.Parse($"{StartHour.Text}:{StartMinute.Text}") : null,
+ DepartureTime = EndHour.IsEnabled ? DateTime.Parse($"{EndHour.Text}:{EndMinute.Text}") : null,
+ TicketChecks = TicketChecksList.Where(x => x.Checked).Select(x => x.TicketCheck).ToList(),
+ Platform = (string)PlatformComboBox.SelectedItem,
+ Landmark = (string)LandmarkComboBox.SelectedItem == "无" ? null: (string)LandmarkComboBox.SelectedItem,
+ };
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void StartHour_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (StartHour.Text.Length == 2) StartMinute.Focus();
+ }
+
+ private void StartMinute_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (StartMinute.Text.Length == 2) EndHour.Focus();
+ }
+
+ private void EndHour_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (EndHour.Text.Length == 2) EndMinute.Focus();
+ }
+
+ private void EndMinute_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (EndMinute.Text.Length == 2) AccentButton.Focus();
+ }
+
+ private void CheckBox_Checked(object sender, RoutedEventArgs e)
+ {
+ ValidateInput();
+ }
+
+ private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
+ {
+ ValidateInput();
+ }
+
+ private static bool ValidateTime(string input, int maxValue)
+ {
+ if (!string.IsNullOrWhiteSpace(input) && int.TryParse(input, null, out int time))
+ {
+ return 0 <= time && time < maxValue;
+ }
+ return false;
+ }
+
+ private void ValidateInput()
+ {
+ // 检查四个文本框是否非空
+ bool areTextBoxesFilled =
+ (!StartHour.IsEnabled || ValidateTime(StartHour.Text, 24)) &&
+ (!StartMinute.IsEnabled || ValidateTime(StartMinute.Text, 60)) &&
+ (!EndHour.IsEnabled || ValidateTime(EndHour.Text, 24)) &&
+ (!EndMinute.IsEnabled || ValidateTime(EndMinute.Text, 60));
+
+ // 当所有条件满足时,启用 AccentButton
+ AccentButton.IsEnabled = areTextBoxesFilled && !string.IsNullOrWhiteSpace(NumberTextBox.Text) && !string.IsNullOrWhiteSpace(ArrivalTextBox.Text) && !string.IsNullOrWhiteSpace(DepartureTextBox.Text) && PlatformComboBox.SelectedItem!=null;
+ }
+
+ private T FindVisualChild(DependencyObject obj) where T : DependencyObject
+ {
+ if (obj == null)
+ return null;
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(obj, i);
+ if (child is T t)
+ {
+ return t;
+ }
+ else
+ {
+ T childOfChild = FindVisualChild(child);
+ if (childOfChild != null)
+ return childOfChild;
+ }
+ }
+ return null;
+ }
+
+ private void StartingStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = false;
+ StartMinute.IsEnabled = false;
+ EndHour.IsEnabled = true;
+ EndMinute.IsEnabled = true;
+ TicketChecksCheckList.IsEnabled = true;
+ }
+
+ private void IntermediateStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = true;
+ StartMinute.IsEnabled = true;
+ EndHour.IsEnabled = true;
+ EndMinute.IsEnabled = true;
+ TicketChecksCheckList.IsEnabled = true;
+ }
+
+ private void FinalStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = true;
+ StartMinute.IsEnabled = true;
+ EndHour.IsEnabled = false;
+ EndMinute.IsEnabled = false;
+ TicketChecksCheckList.IsEnabled = false;
+ }
+
+ private void TicketChecksCheckList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ TicketChecksCheckList.RaiseEvent(eventArg);
+ }
+
+ private void NumberTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ }
+ }
+}
\ No newline at end of file
diff --git a/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml b/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml
new file mode 100644
index 0000000..f92cab9
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml.cs
new file mode 100644
index 0000000..b322af7
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/TicketCheckDialog.xaml.cs
@@ -0,0 +1,120 @@
+using System.Text.RegularExpressions;
+
+namespace CRSim.Views
+{
+ public partial class TicketCheckDialog : Window
+ {
+ public string WaitingAreaName;
+ public List TicketCheck;
+ public TicketCheckDialog(List waitingAreaNames)
+ {
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ foreach (var n in waitingAreaNames)
+ {
+ WaitingAreasComboBox.Items.Add(n);
+ }
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ TicketCheck = Generate(TicketCheckInputBox.Text);
+ WaitingAreaName = WaitingAreasComboBox.SelectedItem.ToString();
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void InputTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ AccentButton.IsEnabled = !string.IsNullOrWhiteSpace(TicketCheckInputBox.Text) && WaitingAreasComboBox.SelectedItem!=null;
+ }
+
+ private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
+ {
+ if(e.Key == Key.Enter && !string.IsNullOrWhiteSpace(TicketCheckInputBox.Text))
+ {
+ OkButton_Click(null, null);
+ }
+ return;
+ }
+
+ public static List Generate(string input)
+ {
+ if (!input.Contains('-'))
+ {
+ return [input];
+ }
+
+ string[] parts = input.Split(['-'], 2);
+ if (parts.Length != 2)
+ {
+ return [input];
+ }
+
+ var startMatch = Regex.Match(parts[0], @"^(\d+)([AB]?)$");
+ var endMatch = Regex.Match(parts[1], @"^(\d+)([AB]?)$");
+
+ if (!startMatch.Success || !endMatch.Success)
+ {
+ return [input];
+ }
+
+ int startNum = int.Parse(startMatch.Groups[1].Value);
+ string sLetter = startMatch.Groups[2].Value;
+ int endNum = int.Parse(endMatch.Groups[1].Value);
+ string eLetter = endMatch.Groups[2].Value;
+
+ bool startHasLetter = !string.IsNullOrEmpty(sLetter);
+ bool endHasLetter = !string.IsNullOrEmpty(eLetter);
+
+ if (startHasLetter != endHasLetter)
+ {
+ return [input];
+ }
+
+ int min = Math.Min(startNum, endNum);
+ int max = Math.Max(startNum, endNum);
+ List numbers = Enumerable.Range(min, max - min + 1).ToList();
+
+ List result = [];
+
+ if (startHasLetter)
+ {
+ if (sLetter == eLetter)
+ {
+ numbers.ForEach(n => result.Add($"{n}{sLetter}"));
+ }
+ else
+ {
+ numbers.ForEach(n =>
+ {
+ result.Add($"{n}A");
+ result.Add($"{n}B");
+ });
+ }
+ }
+ else
+ {
+ numbers.ForEach(n => result.Add(n.ToString()));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml b/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml
new file mode 100644
index 0000000..f2358eb
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml.cs b/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml.cs
new file mode 100644
index 0000000..74f0335
--- /dev/null
+++ b/CRSim/Views/Windows/Dialogs/TrainStopDialog.xaml.cs
@@ -0,0 +1,198 @@
+using static CRSim.Views.TrainStopDialog;
+
+namespace CRSim.Views
+{
+ ///
+ /// UserInputDialog.xaml 的交互逻辑
+ ///
+ public partial class TrainStopDialog : Window
+ {
+ public TrainStop GeneratedTrainStop;
+
+ public TrainStopDialog()
+ {
+ DataContext = this;
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ IntermediateStation.IsChecked = true;
+ ValidateInput();
+ }
+
+ public TrainStopDialog(TrainStop trainStop)
+ {
+ DataContext = this;
+ WindowChrome.SetWindowChrome(
+ this,
+ new WindowChrome
+ {
+ CaptionHeight = 50,
+ CornerRadius = default,
+ GlassFrameThickness = new Thickness(-1),
+ ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
+ UseAeroCaptionButtons = true
+ }
+ );
+ InitializeComponent();
+ StationTextBox.IsEnabled = false;
+ StationTextBox.Text = trainStop.Station;
+ StationKindPanel.IsEnabled = false;
+ if (trainStop.ArrivalTime != null)
+ {
+ StartHour.Text = trainStop.ArrivalTime.Value.Hour.ToString();
+ StartMinute.Text = trainStop.ArrivalTime.Value.Minute.ToString();
+ }
+ else
+ {
+ StartingStation.IsChecked = true;
+ }
+ if (trainStop.DepartureTime != null)
+ {
+ EndHour.Text = trainStop.DepartureTime.Value.Hour.ToString();
+ EndMinute.Text = trainStop.DepartureTime.Value.Minute.ToString();
+ }
+ else
+ {
+ FinalStation.IsChecked = true;
+ }
+ ValidateInput();
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ GeneratedTrainStop = new TrainStop
+ {
+ Station = StationTextBox.Text,
+ ArrivalTime = StartHour.IsEnabled ? DateTime.Parse($"{StartHour.Text}:{StartMinute.Text}") : null,
+ DepartureTime = EndHour.IsEnabled ? DateTime.Parse($"{EndHour.Text}:{EndMinute.Text}") : null
+ };
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void StartHour_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (StartHour.Text.Length == 2) StartMinute.Focus();
+ }
+
+ private void StartMinute_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (StartMinute.Text.Length == 2) EndHour.Focus();
+ }
+
+ private void EndHour_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (EndHour.Text.Length == 2) EndMinute.Focus();
+ }
+
+ private void EndMinute_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ if (EndMinute.Text.Length == 2) AccentButton.Focus();
+ }
+
+ private void CheckBox_Checked(object sender, RoutedEventArgs e)
+ {
+ ValidateInput();
+ }
+
+ private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
+ {
+ ValidateInput();
+ }
+
+ private static bool ValidateTime(string input,int maxValue)
+ {
+ if(!string.IsNullOrWhiteSpace(input) && int.TryParse(input,null,out int time))
+ {
+ return 0 <= time && time < maxValue;
+ }
+ return false;
+ }
+
+ private void ValidateInput()
+ {
+ // 检查四个文本框是否非空
+ bool areTextBoxesFilled =
+ (!StartHour.IsEnabled || ValidateTime(StartHour.Text,23)) &&
+ (!StartMinute.IsEnabled || ValidateTime(StartMinute.Text, 59)) &&
+ (!EndHour.IsEnabled || ValidateTime(EndHour.Text, 23)) &&
+ (!EndMinute.IsEnabled || ValidateTime(EndMinute.Text, 59));
+
+ // 当所有条件满足时,启用 AccentButton
+ AccentButton.IsEnabled = areTextBoxesFilled && !string.IsNullOrWhiteSpace(StationTextBox.Text);
+ }
+
+ private T FindVisualChild(DependencyObject obj) where T : DependencyObject
+ {
+ if (obj == null)
+ return null;
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(obj, i);
+ if (child is T t)
+ {
+ return t;
+ }
+ else
+ {
+ T childOfChild = FindVisualChild(child);
+ if (childOfChild != null)
+ return childOfChild;
+ }
+ }
+ return null;
+ }
+
+ private void StartingStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = false;
+ StartMinute.IsEnabled = false;
+ EndHour.IsEnabled = true;
+ EndMinute.IsEnabled = true;
+ }
+
+ private void IntermediateStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = true;
+ StartMinute.IsEnabled = true;
+ EndHour.IsEnabled = true;
+ EndMinute.IsEnabled = true;
+ }
+
+ private void FinalStation_Checked(object sender, RoutedEventArgs e)
+ {
+ if (StartHour == null) return;
+ StartHour.IsEnabled = true;
+ StartMinute.IsEnabled = true;
+ EndHour.IsEnabled = false;
+ EndMinute.IsEnabled = false;
+ }
+
+ private void StationTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ValidateInput();
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml b/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml
new file mode 100644
index 0000000..96d9f07
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml.cs b/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml.cs
new file mode 100644
index 0000000..7765b08
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/BeijingXi/PrimaryScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.BeijingXi;
+
+namespace CRSim.Views.BeijingXi
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PrimaryScreenView : Window
+ {
+ public PrimaryScreenViewModel ViewModel { get; }
+ public PrimaryScreenView(PrimaryScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml b/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml
new file mode 100644
index 0000000..150abd7
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml.cs b/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml.cs
new file mode 100644
index 0000000..3441c4f
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduDong/PrimaryTicketCheckScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.ChengduDong;
+
+namespace CRSim.Views.ChengduDong
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PrimaryTicketCheckScreenView : Window
+ {
+ public PlatformScreenViewModel ViewModel { get; }
+ public PrimaryTicketCheckScreenView(PlatformScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml b/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml
new file mode 100644
index 0000000..7bf7c50
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml.cs b/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml.cs
new file mode 100644
index 0000000..57f9878
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduDong/SecondaryScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.ChengduDong;
+
+namespace CRSim.Views.ChengduDong
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class SecondaryScreenView : Window
+ {
+ public SecondaryScreenViewModel ViewModel { get; }
+ public SecondaryScreenView(SecondaryScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml b/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml
new file mode 100644
index 0000000..b949d3a
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml.cs b/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml.cs
new file mode 100644
index 0000000..6deeb45
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/ChengduMetro/PlatformScreenView.xaml.cs
@@ -0,0 +1,44 @@
+using CRSim.ViewModels.ChengduMetro;
+using System.Windows.Controls;
+using System.Windows.Media.Animation;
+
+namespace CRSim.Views.ChengduMetro
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PlatformScreenView : Window
+ {
+ public PlatformScreenViewModel ViewModel { get; }
+ public PlatformScreenView(PlatformScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = this;
+ player.Source = new Uri("Assets\\Advertisement.mp4", UriKind.Relative);
+ player.Play();
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+
+ private void MediaElement_MediaEnded(object sender, RoutedEventArgs e)
+ {
+ player.Position = TimeSpan.Zero;
+ player.Play();
+ }
+
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml b/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml
new file mode 100644
index 0000000..5b0fe28
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml.cs b/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml.cs
new file mode 100644
index 0000000..8fe4333
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Guangyuan/PrimaryScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.Guangyuan;
+
+namespace CRSim.Views.Guangyuan
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PrimaryScreenView : Window
+ {
+ public OutsideScreenViewModel ViewModel { get; }
+ public PrimaryScreenView(OutsideScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml b/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml
new file mode 100644
index 0000000..3b2e767
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml.cs b/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml.cs
new file mode 100644
index 0000000..0e80d31
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Hanzhong/PlatformScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.Hanzhong;
+
+namespace CRSim.Views.Hanzhong
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PlatformScreenView : Window
+ {
+ public PlatformScreenViewModel ViewModel { get; }
+ public PlatformScreenView(PlatformScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml b/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml
new file mode 100644
index 0000000..1ee41a9
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml
@@ -0,0 +1,324 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml.cs b/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml.cs
new file mode 100644
index 0000000..bc69799
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Harbin/PrimaryScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.Harbin;
+
+namespace CRSim.Views.Harbin
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class PrimaryScreenView : Window
+ {
+ public PrimaryScreenViewModel ViewModel { get; }
+ public PrimaryScreenView(PrimaryScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}
diff --git a/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml b/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml
new file mode 100644
index 0000000..b572619
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml.cs b/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml.cs
new file mode 100644
index 0000000..889f22e
--- /dev/null
+++ b/CRSim/Views/Windows/Stations/Shanghai/OutsideScreenView.xaml.cs
@@ -0,0 +1,33 @@
+using CRSim.ViewModels.Shanghai;
+
+namespace CRSim.Views.Shanghai
+{
+ ///
+ /// SecondaryScreen.xaml 的交互逻辑
+ ///
+ public partial class OutsideScreenView : Window
+ {
+ public OutsideScreenViewModel ViewModel { get; }
+ public OutsideScreenView(OutsideScreenViewModel viewModel)
+ {
+ InitializeComponent();
+ ViewModel = viewModel;
+ DataContext = viewModel;
+ }
+ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left)
+ {
+ DragMove();
+ }
+ }
+
+ private void Window_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close();
+ }
+ }
+ }
+}