Compare commits

...

2 commits

Author SHA1 Message Date
nns c8c0be56b4 docs(pos-ui): скриншоты после полировки + адаптив (density) + Windows 11
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
- Реальное окно Windows 11 заново: продажа (1280×800, компактный уровень —
  «Очистить чек» виден, ничего не перекрывается) и оплата.
- docs/pos-screenshots/density/: экран продажи в 1280×720/800, 1366×768, 1920×1080
  (подтверждает адаптив панели по высоте и шапки по ширине) + раздел в галерее.
- Перегенерированы «Стало»-кадры и compare-*.png — вёрстка 1:1 с макетом сохранена.
- docs/pos-ui-progress.md: раздел «Полировка» 7/7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:52:03 +05:00
nns 4b08782e82 fix(pos-ui): полировка по реальному окну Windows 11
Правки по замечаниям на windows11-sale.png (VM 1280×800 maximized — высоты
меньше, чем в headless):

1. Адаптив правой панели по высоте (SaleView.axaml.cs, подписка на Bounds):
   уровни normal/compact/tiny — сжимаются отступы, ИТОГО, кнопки; скрывается
   блок «Последний чек»; ClipToBounds, чтобы ничего не уходило под полосу клавиш.
2. Шапка — Grid вместо StackPanel: чипы в отдельной колонке и не обрезаются;
   адаптив по ширине (MainWindow.axaml.cs): <1400 короткие чипы, <1100 без «Магазин…».
   BadgesCompact во VM переключает тексты ShiftBadge/QueueBadge.
3. Иконка «Выплата» (одиночная горизонталь) схлопывалась при Stretch=Uniform →
   Stretch=None; «Внесение» выровнял так же.
4. Hover строки корзины — светлый #F7FBF8 вместо тёмного из темы.
5. Плитки избранного — Grid *,Auto: цена прибита к низу, у всех на одной линии.
6. Полоса горячих клавиш — выравнивание влево (как в макете).
7. --density: рендер экрана продажи в 1280×720/800, 1366×768, 1920×1080 для проверки.

--uitest OK, 82 unit зелёные, smoke OK, скорость не хуже.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:51:51 +05:00
31 changed files with 205 additions and 37 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -65,6 +65,27 @@
НЕ трогать: прод, shared-postgres, global.json, gateway nginx, sklad, другие VM. НЕ трогать: прод, shared-postgres, global.json, gateway nginx, sklad, другие VM.
## Полировка по реальному окну Windows 11 (Mac-Claude, 08.10)
Замечания по `windows/windows11-sale.png` (VM 1280×800, maximized → высоты меньше, чем в headless):
1. [x] Правая панель не влезала по высоте → адаптив по высоте в `SaleView.axaml.cs`
(подписка на Bounds): уровни normal/compact/tiny — меньше отступы (margin 24→18→12,
spacing 16→10→8), скрыт блок «Последний чек», ИТОГО 52→46→40, Оплатить 76→64→56,
кнопки 52→46→42; `ClipToBounds` на панели. Проверено 1280×720/800, 1366×768, 1920×1080
(docs/pos-screenshots/density/) + реальное окно VM 1280×800 — «Очистить чек» виден, ничего не
перекрывается. 125% = меньше логическое разрешение → уровень tiny (та же логика по высоте).
2. [x] Шапка — Grid вместо StackPanel; чипы в отдельной колонке (никогда не обрезаются).
Адаптив по ширине в `MainWindow.axaml.cs`: <1400 — короткие чипы («Смена открыта»,
«На связи» без очереди); <1100 — скрыт «Магазин …».
3. [x] Иконка «Выплата» (одиночная горизонталь) схлопывалась при Stretch=Uniform → Stretch=None
с координатами в 18-боксе; «Внесение» выровнял так же. Остальные Path — 2D, в порядке.
4. [x] Hover строки — светлый #F7FBF8 (PosSelRow) вместо тёмного из темы; тема-hover на
ContentPresenter погашен.
5. [x] Плитки избранного — Grid `*,Auto`: название по центру сверху, цена прибита к низу —
у всех плиток цена на одной линии; высота 84 едина.
6. [x] Полоса горячих клавиш — `HorizontalAlignment=Left` (как в макете).
7. [x] Реальное окно Windows 11 заново (продажа компактная 1280×800 + оплата) + compare + density;
просмотрел каждый кадр — обрезок нет.
## Журнал ## Журнал
- 08.10 — старт; сохранены «до»-скриншоты (22), замерен baseline; планы зафиксированы. - 08.10 — старт; сохранены «до»-скриншоты (22), замерен baseline; планы зафиксированы.
- 08.10 — светлая тема + избранное + правки выравнивания (коммиты 7a93c19, ac55da8, 29ef769, 85e486a). - 08.10 — светлая тема + избранное + правки выравнивания (коммиты 7a93c19, ac55da8, 29ef769, 85e486a).
@ -72,3 +93,7 @@
экрана, бейдж отложенных, исправлена подсветка выбранной строки; CompareRunner (`--compare`) → экрана, бейдж отложенных, исправлена подсветка выбранной строки; CompareRunner (`--compare`) →
compare-sale/compare-payment (расхождений > 4px нет); реальное окно Windows 11 (продажа + оплата). compare-sale/compare-payment (расхождений > 4px нет); реальное окно Windows 11 (продажа + оплата).
Проверено: --uitest OK, 82 unit зелёные, smoke OK, скорость не хуже (старт 74мс, скан 13.7мс). Проверено: --uitest OK, 82 unit зелёные, smoke OK, скорость не хуже (старт 74мс, скан 13.7мс).
- 08.10 — полировка по реальному окну Windows 11 (7 пунктов): адаптив правой панели по высоте
(normal/compact/tiny) и шапки по ширине, fix иконки «Выплата», светлый hover, выравнивание
плиток избранного, hotkey-полоса влево. `--density` рендерит продажу в 4 разрешениях.
Реальное окно VM 1280×800 — ничего не перекрывается. --uitest OK, 82 unit, smoke OK (старт 147мс).

View file

@ -16,6 +16,7 @@ public static int Main(string[] args)
if (args.Contains("--smoke")) return SmokeRunner.Run(args); if (args.Contains("--smoke")) return SmokeRunner.Run(args);
if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args); if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args);
if (args.Contains("--compare")) return CompareRunner.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); if (args.Contains("--uitest")) return UiTestRunner.Run(args);
// --demo : показать реальное окно с примерами данных (для скриншота на Windows). // --demo : показать реальное окно с примерами данных (для скриншота на Windows).

View file

@ -71,6 +71,40 @@ public static int Run(string[] args)
return 0; return 0;
} }
/// <summary>Проверка адаптива правой панели по высоте: экран продажи в разных
/// разрешениях → docs/pos-screenshots/density/. Запуск: dotnet run -- --density</summary>
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<App>()
.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 SaleViewModel EmptySale() => new();
private static Control ShellWith(SaleViewModel sale) 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("<header style=\"margin-top:8px\"><h1>Адаптив — одно разрешение на кадр</h1>" +
"<div style=\"color:#94a3b8\">Правая панель сжимается по высоте (меньше отступы, " +
"скрыт блок «Последний чек», кнопки 44px), шапка — по ширине (сокращение чипов). " +
"Ничего не перекрывается на 1280×720/800, 1366×768, 1920×1080.</div></header>");
sb.Append("<div class=\"grid\">");
foreach (var d in dens)
{
var rel = "density/" + Path.GetFileName(d);
sb.Append($"<figure><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(Path.GetFileNameWithoutExtension(d))}</figcaption></figure>");
}
sb.Append("</div>");
}
}
sb.Append("</body></html>"); sb.Append("</body></html>");
File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString()); File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString());
} }

View file

@ -32,14 +32,19 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private PaymentViewModel? _payment; [ObservableProperty] private PaymentViewModel? _payment;
[ObservableProperty] private bool _isPaymentOpen; [ObservableProperty] private bool _isPaymentOpen;
[ObservableProperty] private string? _updateMessage; [ObservableProperty] private string? _updateMessage;
/// <summary>Узкая шапка: сокращать тексты чипов (устанавливается из code-behind по ширине окна).</summary>
[ObservableProperty] private bool _badgesCompact;
public string NetBadge => IsOnline ? "На связи" : "Нет связи"; public string NetBadge => IsOnline ? "На связи" : "Нет связи";
public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта"; public string ShiftBadge => IsShiftOpen
public string QueueBadge => QueueCount > 0 ? $"· {QueueCount} в очереди" : "· очередь пуста"; ? (BadgesCompact ? "Смена открыта" : $"Смена {ShiftNumber}")
: "Смена закрыта";
public string QueueBadge => BadgesCompact ? "" : (QueueCount > 0 ? $"· {QueueCount} в очереди" : "· очередь пуста");
partial void OnIsOnlineChanged(bool value) { OnPropertyChanged(nameof(NetBadge)); } partial void OnIsOnlineChanged(bool value) { OnPropertyChanged(nameof(NetBadge)); }
partial void OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); } partial void OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); }
partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge)); partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge));
partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge)); partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge));
partial void OnBadgesCompactChanged(bool value) { OnPropertyChanged(nameof(ShiftBadge)); OnPropertyChanged(nameof(QueueBadge)); }
/// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary> /// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary>
public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Демо-магазин"; PointName = "Касса 1"; } public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Демо-магазин"; PointName = "Касса 1"; }

View file

@ -11,24 +11,31 @@
<!-- Верхняя полоса --> <!-- Верхняя полоса -->
<Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="24,0"> <Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="24,0">
<StackPanel Orientation="Horizontal" Spacing="24" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,Auto,*,Auto" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center" FontSize="20"> <!-- Лого (всегда) -->
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="20">
<Run Text="FOOD MARKET" Foreground="{DynamicResource PosBrand}" FontWeight="ExtraBold"/><Run Text=" · Касса" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/> <Run Text="FOOD MARKET" Foreground="{DynamicResource PosBrand}" FontWeight="ExtraBold"/><Run Text=" · Касса" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
</TextBlock> </TextBlock>
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
<!-- Мета (сворачивается по ширине) -->
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center" Margin="24,0,0,0">
<StackPanel x:Name="StoreMeta" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}"> <TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
<Run Text="Магазин "/><Run Text="{Binding StoreName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/><Run Text=" · Точка "/><Run Text="{Binding PointName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/> <Run Text="Магазин "/><Run Text="{Binding StoreName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/><Run Text=" · Точка "/><Run Text="{Binding PointName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
</TextBlock> </TextBlock>
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/> <Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
</StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}"> <TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
<Run Text="Кассир "/><Run Text="{Binding CashierName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/> <Run Text="Кассир "/><Run Text="{Binding CashierName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
</TextBlock> </TextBlock>
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}" </StackPanel>
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock VerticalAlignment="Center" FontSize="13" Foreground="{DynamicResource PosWarn}" Text="{Binding UpdateMessage}"
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right"> <!-- Сообщение об обновлении (в растяжимой зоне) -->
<TextBlock Grid.Column="2" VerticalAlignment="Center" Margin="24,0" FontSize="13" Foreground="{DynamicResource PosWarn}" Text="{Binding UpdateMessage}"
TextTrimming="CharacterEllipsis" IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<!-- Чипы статуса (всегда, никогда не обрезаются) -->
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}"> <Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse Width="8" Height="8" VerticalAlignment="Center" Fill="{DynamicResource PosBrand}"/> <Ellipse Width="8" Height="8" VerticalAlignment="Center" Fill="{DynamicResource PosBrand}"/>
@ -43,7 +50,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </Grid>
</Border> </Border>
<!-- Текущая страница --> <!-- Текущая страница -->
@ -51,7 +58,7 @@
<!-- Полоса горячих клавиш --> <!-- Полоса горячих клавиш -->
<Border Grid.Row="2" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="24,0"> <Border Grid.Row="2" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="24,0">
<StackPanel Orientation="Horizontal" Spacing="28" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="28" VerticalAlignment="Center" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F1"/></Border><TextBlock Text="Помощь" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F1"/></Border><TextBlock Text="Помощь" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F2"/></Border><TextBlock Text="Поиск" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F2"/></Border><TextBlock Text="Поиск" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F4"/></Border><TextBlock Text="Скидка" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F4"/></Border><TextBlock Text="Скидка" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>

View file

@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using foodmarket.Pos.ViewModels; using foodmarket.Pos.ViewModels;
@ -10,6 +11,26 @@ public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(KeyDownEvent, OnKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); 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<StackPanel>("StoreMeta") is { } sm) sm.IsVisible = w >= 1100;
}
private sealed class BoundsObserver : System.IObserver<Rect>
{
private readonly System.Action<Rect> _on;
public BoundsObserver(System.Action<Rect> on) => _on = on;
public void OnCompleted() { }
public void OnError(System.Exception error) { }
public void OnNext(Rect value) => _on(value);
} }
/// <summary>Горячие клавиши кассы (работа без мыши). F-клавиши и Esc/Del /// <summary>Горячие клавиши кассы (работа без мыши). F-клавиши и Esc/Del

View file

@ -38,13 +38,14 @@
<ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate></ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Button Height="84" Margin="6" Padding="4" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="12" <Button Height="84" Margin="6" Padding="8,10" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="12"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Center" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).AddFavoriteCommand}" CommandParameter="{Binding}"> Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).AddFavoriteCommand}" CommandParameter="{Binding}">
<StackPanel Spacing="6" HorizontalAlignment="Stretch"> <Grid RowDefinitions="*,Auto">
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold" TextAlignment="Center" TextWrapping="Wrap" Foreground="{DynamicResource PosText}"/> <TextBlock Grid.Row="0" Text="{Binding Name}" FontSize="15" FontWeight="SemiBold" TextAlignment="Center" TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
<TextBlock Text="{Binding PriceText}" FontSize="15" FontWeight="Bold" TextAlignment="Center" Foreground="{DynamicResource PosBrandDark}"/> HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource PosText}"/>
</StackPanel> <TextBlock Grid.Row="1" Text="{Binding PriceText}" FontSize="15" FontWeight="Bold" TextAlignment="Center" HorizontalAlignment="Center" Margin="0,4,0,0" Foreground="{DynamicResource PosBrandDark}"/>
</Grid>
</Button> </Button>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
@ -77,8 +78,11 @@
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
</Style> </Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter"><Setter Property="Background" Value="Transparent"/></Style> <Style Selector="ListBoxItem:selected /template/ ContentPresenter"><Setter Property="Background" Value="Transparent"/></Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter"><Setter Property="Background" Value="Transparent"/></Style>
<Style Selector="Border.row"><Setter Property="Background" Value="Transparent"/></Style> <Style Selector="Border.row"><Setter Property="Background" Value="Transparent"/></Style>
<Style Selector="Rectangle.accent"><Setter Property="IsVisible" Value="False"/></Style> <Style Selector="Rectangle.accent"><Setter Property="IsVisible" Value="False"/></Style>
<!-- Светлая подсветка строки (как в макете #F7FBF8), а не тёмная из темы -->
<Style Selector="ListBoxItem:pointerover Border.row"><Setter Property="Background" Value="{DynamicResource PosSelRow}"/></Style>
<Style Selector="ListBoxItem:selected Border.row"><Setter Property="Background" Value="{DynamicResource PosSelRow}"/></Style> <Style Selector="ListBoxItem:selected Border.row"><Setter Property="Background" Value="{DynamicResource PosSelRow}"/></Style>
<Style Selector="ListBoxItem:selected Rectangle.accent"><Setter Property="IsVisible" Value="True"/></Style> <Style Selector="ListBoxItem:selected Rectangle.accent"><Setter Property="IsVisible" Value="True"/></Style>
</ListBox.Styles> </ListBox.Styles>
@ -115,19 +119,19 @@
</Grid> </Grid>
<!-- ПРАВО --> <!-- ПРАВО -->
<Border Grid.Column="1" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="14" Margin="8,16,24,16"> <Border Grid.Column="1" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="14" Margin="8,16,24,16" ClipToBounds="True">
<Grid RowDefinitions="Auto,*,Auto" Margin="24"> <Grid x:Name="RightInner" RowDefinitions="Auto,*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="16"> <StackPanel x:Name="SummaryStack" Grid.Row="0" Spacing="16">
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Подытог" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Subtotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Подытог" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Subtotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding HasAnyDiscount}"><TextBlock Text="Скидка" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="17" FontWeight="SemiBold"/></Grid> <Grid ColumnDefinitions="*,Auto" IsVisible="{Binding HasAnyDiscount}"><TextBlock Text="Скидка" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="17" FontWeight="SemiBold"/></Grid>
<Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="22,20"> <Border x:Name="ItogoBorder" Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="22,20">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="ИТОГО" Foreground="{DynamicResource PosBrandDark}" FontSize="18" FontWeight="Bold" VerticalAlignment="Bottom"/> <TextBlock Text="ИТОГО" Foreground="{DynamicResource PosBrandDark}" FontSize="18" FontWeight="Bold" VerticalAlignment="Bottom"/>
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="52" FontWeight="ExtraBold"/> <TextBlock x:Name="ItogoValue" Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="52" FontWeight="ExtraBold"/>
</Grid> </Grid>
</Border> </Border>
<Border BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="0,16,0,0"> <Border x:Name="LastCheckBlock" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="0,16,0,0">
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<Grid ColumnDefinitions="*,Auto"><TextBlock Foreground="{DynamicResource PosTextMuted}" FontSize="14"><Run Text="Последний чек "/><Run Text="{Binding LastReceiptNumber}"/></TextBlock><TextBlock Grid.Column="1" Text="{Binding LastReceiptInfo}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Foreground="{DynamicResource PosTextMuted}" FontSize="14"><Run Text="Последний чек "/><Run Text="{Binding LastReceiptNumber}"/></TextBlock><TextBlock Grid.Column="1" Text="{Binding LastReceiptInfo}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Чеков за смену" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/><TextBlock Grid.Column="1" Text="{Binding ReceiptsInShift}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Чеков за смену" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/><TextBlock Grid.Column="1" Text="{Binding ReceiptsInShift}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid>
@ -136,13 +140,13 @@
</StackPanel> </StackPanel>
<StackPanel Grid.Row="2" Spacing="12"> <StackPanel Grid.Row="2" Spacing="12">
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="76" Command="{Binding PayCommand}"> <Button x:Name="PayButton" Classes="pos primary" HorizontalAlignment="Stretch" Height="76" Command="{Binding PayCommand}">
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Center">
<TextBlock Text="Оплатить" FontSize="24" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/> <TextBlock Text="Оплатить" FontSize="24" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<Border Classes="kbd onbrand"><TextBlock Text="F12"/></Border> <Border Classes="kbd onbrand"><TextBlock Text="F12"/></Border>
</StackPanel> </StackPanel>
</Button> </Button>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto"> <Grid x:Name="ActionsGrid" ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto">
<Button Grid.Row="0" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10"> <Button Grid.Row="0" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M19 5L5 19M7 7h.01M17 17h.01"/> <Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M19 5L5 19M7 7h.01M17 17h.01"/>
@ -172,13 +176,14 @@
</Button> </Button>
<Button Grid.Row="2" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10" Command="{Binding GoShiftCommand}"> <Button Grid.Row="2" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10" Command="{Binding GoShiftCommand}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M12 5v14M5 12h14"/> <Path Width="18" Height="18" Stretch="None" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M9 3v12M3 9h12"/>
<TextBlock Text="Внесение" VerticalAlignment="Center"/> <TextBlock Text="Внесение" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button Grid.Row="2" Grid.Column="1" Classes="pos ghost" Height="52" Margin="5,0,0,10" Command="{Binding GoShiftCommand}"> <Button Grid.Row="2" Grid.Column="1" Classes="pos ghost" Height="52" Margin="5,0,0,10" Command="{Binding GoShiftCommand}">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M5 12h14"/> <!-- Минус: Stretch=None — одиночная горизонталь при Uniform схлопывается в точку -->
<Path Width="18" Height="18" Stretch="None" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M3 9h12"/>
<TextBlock Text="Выплата" VerticalAlignment="Center"/> <TextBlock Text="Выплата" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
</Button> </Button>

View file

@ -1,8 +1,57 @@
using System.Linq;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
namespace foodmarket.Pos.Views; namespace foodmarket.Pos.Views;
public partial class SaleView : UserControl 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<Border>("LastCheckBlock") is { } lc) lc.IsVisible = showLastCheck;
if (this.FindControl<Button>("PayButton") is { } pay) pay.Height = payH;
if (this.FindControl<StackPanel>("SummaryStack") is { } ss) ss.Spacing = spacing;
if (this.FindControl<Grid>("RightInner") is { } ri) ri.Margin = new Thickness(margin);
if (this.FindControl<TextBlock>("ItogoValue") is { } iv) iv.FontSize = itogo;
if (this.FindControl<Border>("ItogoBorder") is { } ib) ib.Padding = itogoPad;
if (this.FindControl<Grid>("ActionsGrid") is { } ag)
foreach (var b in ag.Children.OfType<Button>())
b.Height = actH;
}
/// <summary>Лёгкий IObserver для подписки на Bounds без Rx-пакета.</summary>
private sealed class AnonymousObserver : System.IObserver<Rect>
{
private readonly System.Action<Rect> _on;
public AnonymousObserver(System.Action<Rect> on) => _on = on;
public void OnCompleted() { }
public void OnError(System.Exception error) { }
public void OnNext(Rect value) => _on(value);
}
} }