/
kochkatech
/
Queue
Обзор
Документация
Войти
/
kochkatech
/
Queue
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/Queue.Admin/ViewModels/ScreensViewModel.cs
166 строк
6 KB
kochkareal
feat: вкладка "Экраны" в Admin — запуск/управление табло и АРМ оператора
27 июл 2026, 21:26
27 июл 2026, 21:26
62e41f9
Код
Авторство
О чём код?
using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Reflection; using System.Threading.Tasks; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Queue.Admin.Models; using Queue.Admin.Services; namespace Queue.Admin.ViewModels; // Локальный launcher для тестирования Queue.Display/Queue.OperatorApp на одной машине — // НЕ описывает продовую топологию (там каждый модуль запускается на своём ПК, см. // docs/architecture.md раздел 5.2). Появилась после того, как первый локальный запуск // табло сразу развернулся в режиме киоска и заблокировал экран без возможности выйти — // здесь администратор сначала видит табло оконным, сам перетаскивает на нужный монитор // и только потом включает киоск явной кнопкой. public partial class ScreensViewModel : ObservableObject { public ObservableCollection<LaunchedScreenItem> Items { get; } = new(); [RelayCommand] private void LaunchDisplay() { Launch(ScreenType.Display, "Queue.Display", "--windowed"); } [RelayCommand] private void LaunchOperator() { Launch(ScreenType.Operator, "Queue.OperatorApp"); } // Заменяет остановленную строку новым запущенным процессом того же типа — без этого // единственным способом снова открыть табло/АРМ было нажать "Добавить..." заново и // разбираться, какая из старых остановленных строк какая (список со временем засорялся). [RelayCommand] private void Restart(LaunchedScreenItem? item) { if (item is null || item.Status != "Остановлено") { return; } Items.Remove(item); if (item.Type == ScreenType.Display) { LaunchDisplay(); } else { LaunchOperator(); } } [RelayCommand] private void Remove(LaunchedScreenItem? item) { if (item is null || item.Status != "Остановлено") { return; } Items.Remove(item); } private void Launch(ScreenType type, string projectName, params string[] args) { var exePath = ResolveExePath(projectName); if (exePath is null) { MessageBox.Show( $"Не найден {projectName}.exe рядом с Queue.Admin.exe. Соберите проект {projectName} (dotnet build) и повторите.", "Экраны", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var process = Process.Start(new ProcessStartInfo(exePath, string.Join(' ', args)) { UseShellExecute = false }); if (process is null) { return; } var pipeHandle = type == ScreenType.Display ? new DisplayProcessHandle(process) : null; var statusListener = type == ScreenType.Display ? new ScreenStatusListener(process.Id) : null; var item = new LaunchedScreenItem(type, process, pipeHandle, statusListener); if (statusListener is not null) { statusListener.ModeChanged += isKiosk => Application.Current.Dispatcher.Invoke(() => item.IsKiosk = isKiosk); } process.EnableRaisingEvents = true; process.Exited += (_, _) => Application.Current.Dispatcher.Invoke(() => { item.Status = "Остановлено"; item.StatusListener?.Dispose(); }); Items.Add(item); } [RelayCommand] private async Task ToggleKioskAsync(LaunchedScreenItem? item) { if (item?.PipeHandle is null) { return; } try { await item.PipeHandle.ToggleKioskAsync(!item.IsKiosk); item.IsKiosk = !item.IsKiosk; } catch (Exception ex) when (ex is IOException or TimeoutException) { MessageBox.Show($"Не удалось связаться с окном табло: {ex.Message}", "Экраны", MessageBoxButton.OK, MessageBoxImage.Warning); } } [RelayCommand] private async Task StopAsync(LaunchedScreenItem? item) { if (item is null || item.Process.HasExited) { return; } item.Process.CloseMainWindow(); // Даём окну шанс закрыться штатно (сохранить настройки и т.п.) и только потом убиваем // принудительно — CloseMainWindow не гарантирует, что окно действительно закроется // (например, если приложение показывает диалог подтверждения). var closedGracefully = await Task.Run(() => item.Process.WaitForExit(2000)); if (!closedGracefully && !item.Process.HasExited) { item.Process.Kill(); } item.Status = "Остановлено"; } // Путь ищется относительно собственной сборки Admin — работает только при запуске из // исходников на одной машине (Debug/Release рядом в src/*/bin/...), поэтому это осознанно // локальное решение для тестирования, а не механизм для продовой развёртки. private static string? ResolveExePath(string projectName) { var adminBinDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var configDir = Path.GetFileName(Path.GetDirectoryName(adminBinDir)) ?? "Debug"; var targetFramework = Path.GetFileName(adminBinDir); var candidate = Path.GetFullPath(Path.Combine( adminBinDir, "..", "..", "..", "..", projectName, "bin", configDir, targetFramework, $"{projectName}.exe")); return File.Exists(candidate) ? candidate : null; } }