diff --git a/src/food-market.pos/Program.cs b/src/food-market.pos/Program.cs index ecfddc7..6833e99 100644 --- a/src/food-market.pos/Program.cs +++ b/src/food-market.pos/Program.cs @@ -16,6 +16,7 @@ public static int Main(string[] args) if (args.Contains("--smoke")) return SmokeRunner.Run(args); if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args); if (args.Contains("--compare")) return CompareRunner.Run(args); + if (args.Contains("--density")) return ScreenshotRunner.RunDensity(args); if (args.Contains("--uitest")) return UiTestRunner.Run(args); // --demo : показать реальное окно с примерами данных (для скриншота на Windows). diff --git a/src/food-market.pos/Tooling/ScreenshotRunner.cs b/src/food-market.pos/Tooling/ScreenshotRunner.cs index 9039410..3ba7693 100644 --- a/src/food-market.pos/Tooling/ScreenshotRunner.cs +++ b/src/food-market.pos/Tooling/ScreenshotRunner.cs @@ -71,6 +71,40 @@ public static int Run(string[] args) return 0; } + /// Проверка адаптива правой панели по высоте: экран продажи в разных + /// разрешениях → docs/pos-screenshots/density/. Запуск: dotnet run -- --density + public static int RunDensity(string[] args) + { + var outDir = args.SkipWhile(a => a != "--density").Skip(1).FirstOrDefault(a => !a.StartsWith("--")) + ?? Path.Combine(FindRepoRoot(), "docs", "pos-screenshots", "density"); + Directory.CreateDirectory(outDir); + + AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }) + .WithInterFont() + .SetupWithoutStarting(); + + var sizes = new (string name, int w, int h)[] + { + ("1280x720", 1280, 720), ("1280x800", 1280, 800), + ("1366x768", 1366, 768), ("1920x1080", 1920, 1080), + }; + foreach (var (name, w, h) in sizes) + { + var window = new MainWindow { DataContext = new MainWindowViewModel() }; + window.Width = w; window.Height = h; + window.Show(); + for (int i = 0; i < 6; i++) Dispatcher.UIThread.RunJobs(); + var frame = window.CaptureRenderedFrame(); + frame?.Save(Path.Combine(outDir, $"sale-{name}.png")); + window.Close(); + Console.WriteLine($"[density] sale-{name}.png"); + } + Console.WriteLine($"[done] density → {outDir}"); + return 0; + } + private static SaleViewModel EmptySale() => new(); private static Control ShellWith(SaleViewModel sale) { @@ -165,6 +199,27 @@ private static void WriteGallery(string dir, List<(string file, string caption)> } } + // Раздел «Адаптив» — один экран продажи в разных разрешениях (если есть). + var densDir = Path.Combine(dir, "density"); + if (Directory.Exists(densDir)) + { + var dens = Directory.GetFiles(densDir, "*.png").OrderBy(f => f).ToList(); + if (dens.Count > 0) + { + sb.Append("

Адаптив — одно разрешение на кадр

" + + "
Правая панель сжимается по высоте (меньше отступы, " + + "скрыт блок «Последний чек», кнопки 44px), шапка — по ширине (сокращение чипов). " + + "Ничего не перекрывается на 1280×720/800, 1366×768, 1920×1080.
"); + sb.Append("
"); + foreach (var d in dens) + { + var rel = "density/" + Path.GetFileName(d); + sb.Append($"
{System.Net.WebUtility.HtmlEncode(Path.GetFileNameWithoutExtension(d))}
"); + } + sb.Append("
"); + } + } + sb.Append(""); File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString()); } diff --git a/src/food-market.pos/ViewModels/ViewModels.cs b/src/food-market.pos/ViewModels/ViewModels.cs index 8d0920a..d337056 100644 --- a/src/food-market.pos/ViewModels/ViewModels.cs +++ b/src/food-market.pos/ViewModels/ViewModels.cs @@ -32,14 +32,19 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private PaymentViewModel? _payment; [ObservableProperty] private bool _isPaymentOpen; [ObservableProperty] private string? _updateMessage; + /// Узкая шапка: сокращать тексты чипов (устанавливается из code-behind по ширине окна). + [ObservableProperty] private bool _badgesCompact; public string NetBadge => IsOnline ? "На связи" : "Нет связи"; - public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта"; - public string QueueBadge => QueueCount > 0 ? $"· {QueueCount} в очереди" : "· очередь пуста"; + public string ShiftBadge => IsShiftOpen + ? (BadgesCompact ? "Смена открыта" : $"Смена {ShiftNumber}") + : "Смена закрыта"; + public string QueueBadge => BadgesCompact ? "" : (QueueCount > 0 ? $"· {QueueCount} в очереди" : "· очередь пуста"); partial void OnIsOnlineChanged(bool value) { OnPropertyChanged(nameof(NetBadge)); } partial void OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); } partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge)); partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge)); + partial void OnBadgesCompactChanged(bool value) { OnPropertyChanged(nameof(ShiftBadge)); OnPropertyChanged(nameof(QueueBadge)); } /// Конструктор для дизайнера/скриншотов (демо-данные). public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Демо-магазин"; PointName = "Касса 1"; } diff --git a/src/food-market.pos/Views/MainWindow.axaml b/src/food-market.pos/Views/MainWindow.axaml index 802f96a..26582af 100644 --- a/src/food-market.pos/Views/MainWindow.axaml +++ b/src/food-market.pos/Views/MainWindow.axaml @@ -11,24 +11,31 @@ - - + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -43,7 +50,7 @@ - + @@ -51,7 +58,7 @@ - + diff --git a/src/food-market.pos/Views/MainWindow.axaml.cs b/src/food-market.pos/Views/MainWindow.axaml.cs index ce856c3..3b28ea1 100644 --- a/src/food-market.pos/Views/MainWindow.axaml.cs +++ b/src/food-market.pos/Views/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using foodmarket.Pos.ViewModels; @@ -10,6 +11,26 @@ public MainWindow() { InitializeComponent(); AddHandler(KeyDownEvent, OnKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); + // Адаптив шапки по ширине: при нехватке места сокращаем тексты чипов и + // прячем «Магазин …», но чипы статуса никогда не обрезаем. + this.GetObservable(BoundsProperty).Subscribe(new BoundsObserver(_ => ApplyTopBarDensity())); + } + + private void ApplyTopBarDensity() + { + var w = Bounds.Width; + if (w <= 0) return; + if (DataContext is MainWindowViewModel vm) vm.BadgesCompact = w < 1400; + if (this.FindControl("StoreMeta") is { } sm) sm.IsVisible = w >= 1100; + } + + private sealed class BoundsObserver : System.IObserver + { + private readonly System.Action _on; + public BoundsObserver(System.Action on) => _on = on; + public void OnCompleted() { } + public void OnError(System.Exception error) { } + public void OnNext(Rect value) => _on(value); } /// Горячие клавиши кассы (работа без мыши). F-клавиши и Esc/Del diff --git a/src/food-market.pos/Views/SaleView.axaml b/src/food-market.pos/Views/SaleView.axaml index 59d5253..b471ecb 100644 --- a/src/food-market.pos/Views/SaleView.axaml +++ b/src/food-market.pos/Views/SaleView.axaml @@ -38,13 +38,14 @@ - @@ -77,8 +78,11 @@ + + + @@ -115,19 +119,19 @@ - - - + + + - + - + - + @@ -136,13 +140,13 @@ - - + diff --git a/src/food-market.pos/Views/SaleView.axaml.cs b/src/food-market.pos/Views/SaleView.axaml.cs index 602845f..01e2a46 100644 --- a/src/food-market.pos/Views/SaleView.axaml.cs +++ b/src/food-market.pos/Views/SaleView.axaml.cs @@ -1,8 +1,57 @@ +using System.Linq; +using Avalonia; using Avalonia.Controls; namespace foodmarket.Pos.Views; public partial class SaleView : UserControl { - public SaleView() => InitializeComponent(); + public SaleView() + { + InitializeComponent(); + // Адаптив по высоте: в реальном окне (maximized минус заголовок/таскбар) + // высоты меньше, чем в headless. Чтобы правая панель не вылезала под полосу + // клавиш — сжимаем отступы/кнопки и прячем блок «Последний чек». + this.GetObservable(BoundsProperty).Subscribe(new AnonymousObserver(_ => ApplyDensity())); + } + + private int _tier = -1; // 0 — обычный, 1 — компактный, 2 — минимальный + + private void ApplyDensity() + { + var h = Bounds.Height; + if (h <= 0) return; + int tier = h < 560 ? 2 : h < 660 ? 1 : 0; + if (tier == _tier) return; + _tier = tier; + + // Параметры по уровням плотности. + double payH = tier == 0 ? 76 : tier == 1 ? 64 : 56; + double actH = tier == 0 ? 52 : tier == 1 ? 46 : 42; + double spacing = tier == 0 ? 16 : tier == 1 ? 10 : 8; + double margin = tier == 0 ? 24 : tier == 1 ? 18 : 12; + double itogo = tier == 0 ? 52 : tier == 1 ? 46 : 40; + var itogoPad = tier == 0 ? new Thickness(22, 20) : tier == 1 ? new Thickness(20, 14) : new Thickness(16, 10); + bool showLastCheck = tier == 0; + + if (this.FindControl("LastCheckBlock") is { } lc) lc.IsVisible = showLastCheck; + if (this.FindControl