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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 概览车站设施、车次等信息 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 管理车站候车室设施 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 管理车站检票口设施 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 管理车站站台设施 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 编辑此车站的车次 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 请再次检查各设施配置是否正确 + +