������� �� Avalonia 2.0 (�����-���������, ����������, �������� ��������) #1

Merged
Exest merged 18 commits from feature/avalonia-ui into master 2026-08-11 09:41:09 +03:00
3 changed files with 159 additions and 26 deletions
Showing only changes of commit 0536d480cb - Show all commits

View File

@ -135,6 +135,30 @@
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<!-- ============ Вкладки-документы ============ -->
<Style Selector="ListBox.tabs">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem">
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="3,6"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Foreground" Value="{DynamicResource Muted}"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource RowAlt}"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentSoft}"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:selected">
<Setter Property="Foreground" Value="{DynamicResource AccentPressed}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- ============ Таблица ============ -->
<Style Selector="DataGrid">
<Setter Property="Background" Value="{DynamicResource Surface}"/>

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
@ -10,7 +11,7 @@ namespace Elfisa.Avalonia.ViewModels;
public class NavItem
{
public string Title { get; }
public string Icon { get; }
public string Icon { get; } // глиф Segoe MDL2 Assets
public Func<ViewModelBase> Factory { get; }
public NavItem(string title, string icon, Func<ViewModelBase> factory)
{
@ -18,14 +19,29 @@ public class NavItem
}
}
/// <summary>Открытая вкладка-документ.</summary>
public sealed class DocumentTab
{
public string Title { get; }
public string Key { get; }
public ViewModelBase Content { get; }
public DocumentTab(string title, string key, ViewModelBase content)
{
Title = title; Key = key; Content = content;
}
}
public partial class ShellViewModel : ViewModelBase
{
private readonly Action _logout;
private readonly ApiClient _api = new();
private bool _sync;
public ObservableCollection<NavItem> NavItems { get; }
public ObservableCollection<DocumentTab> Tabs { get; } = new();
[ObservableProperty] private NavItem? _selectedNav;
[ObservableProperty] private ViewModelBase? _currentPage;
[ObservableProperty] private DocumentTab? _activeTab;
public string UserName => Session.Current.Username ?? "";
public string RoleLabel => Session.Current.Role switch
@ -37,25 +53,84 @@ public partial class ShellViewModel : ViewModelBase
_ => Session.Current.Role ?? ""
};
[ObservableProperty] private string _pharmacyAddress = "";
// Segoe MDL2 Assets — те же глифы, что в WinForms-версии
private const string IcoPrice = "";
private const string IcoOrders = "";
private const string IcoInvoices = "";
private const string IcoReports = "";
private const string IcoUpload = "";
private const string IcoSend = "";
private const string IcoRefusals = "";
private const string IcoDirectory = "";
public ShellViewModel(Action logout)
{
_logout = logout;
_api.SetToken(Session.Current.Token);
NavItems = new ObservableCollection<NavItem>
{
new("Прайс-лист", "🧾", () => new PriceListViewModel()),
new("Заказы", "🛒", () => new OrdersViewModel()),
new("Накладные", "📄", () => new InvoicesViewModel()),
new("Отчёты", "📊", () => new ReportsViewModel()),
new("Загрузить", "📥", () => new LoadPriceViewModel()),
new("Отправить", "📤", () => new SendOrderViewModel()),
new("Отказы", "⛔", () => new PlaceholderPageViewModel("Отказы",
"Отказные позиции по заказам. Нужен серверный эндпоинт: данные (RefuseItemsCount) сейчас есть только в локальной БД WinForms-клиента.")),
new("Справочник", "📖", () => new DirectoryViewModel()),
new("Прайс-лист", IcoPrice, () => new PriceListViewModel()),
new("Заказы", IcoOrders, () => new OrdersViewModel()),
new("Накладные", IcoInvoices, () => new InvoicesViewModel()),
new("Отчёты", IcoReports, () => new ReportsViewModel()),
new("Загрузить", IcoUpload, () => new LoadPriceViewModel()),
new("Отправить", IcoSend, () => new SendOrderViewModel()),
new("Отказы", IcoRefusals, () => new PlaceholderPageViewModel("Отказы",
"Отказные позиции по заказам. Нужен серверный эндпоинт (RefuseItemsCount сейчас только в локальной БД WinForms).")),
new("Справочник", IcoDirectory, () => new DirectoryViewModel()),
};
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
if (Session.Current.IsAuthenticated) _ = LoadPharmacyAsync();
}
partial void OnSelectedNavChanged(NavItem? value) => CurrentPage = value?.Factory();
private async Task LoadPharmacyAsync()
{
try
{
var locs = await _api.GetBuyerLocationsAsync();
var def = locs.FirstOrDefault(l => l.IsDefault) ?? locs.FirstOrDefault();
PharmacyAddress = def?.Address ?? "";
}
catch { }
}
partial void OnSelectedNavChanged(NavItem? value)
{
if (_sync || value is null) return;
OpenOrActivate(value);
}
private void OpenOrActivate(NavItem nav)
{
var existing = Tabs.FirstOrDefault(t => t.Key == nav.Title);
if (existing is not null) { ActiveTab = existing; return; }
var tab = new DocumentTab(nav.Title, nav.Title, nav.Factory());
Tabs.Add(tab);
ActiveTab = tab;
}
partial void OnActiveTabChanged(DocumentTab? value)
{
if (value is null) return;
_sync = true;
SelectedNav = NavItems.FirstOrDefault(n => n.Title == value.Key);
_sync = false;
}
[RelayCommand]
private void CloseTab(DocumentTab? tab)
{
if (tab is null) return;
int idx = Tabs.IndexOf(tab);
bool wasActive = ReferenceEquals(ActiveTab, tab);
Tabs.Remove(tab);
if (wasActive)
ActiveTab = Tabs.Count == 0 ? null : Tabs[Math.Min(idx, Tabs.Count - 1)];
}
[RelayCommand] private void Logout() => _logout();
}

View File

@ -6,17 +6,15 @@
<DockPanel>
<!-- Верхняя панель навигации (как в исходной программе) -->
<!-- Верхняя панель навигации -->
<Border DockPanel.Dock="Top" Background="{DynamicResource Sidebar}" Height="82">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="18,0">
<!-- Логотип -->
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,14,0">
<TextBlock Text="ЭльФиСА" FontSize="18" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="Электронная Фармация" Foreground="{DynamicResource SidebarText}" FontSize="10"/>
</StackPanel>
<!-- Навигация -->
<ListBox Grid.Column="1" Classes="topnav" VerticalAlignment="Center" HorizontalAlignment="Left"
ItemsSource="{Binding NavItems}" SelectedItem="{Binding SelectedNav}">
<ListBox.ItemsPanel>
@ -26,17 +24,16 @@
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:NavItem">
<StackPanel Orientation="Vertical" Width="66" Spacing="2">
<TextBlock Text="{Binding Icon}" FontSize="16" HorizontalAlignment="Center"/>
<StackPanel Orientation="Vertical" Width="66" Spacing="3">
<TextBlock Text="{Binding Icon}" FontFamily="Segoe MDL2 Assets" FontSize="18"
Foreground="White" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Title}" Foreground="{DynamicResource SidebarText}"
FontSize="11" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
FontSize="11" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Пользователь + выход -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center" Margin="8,0,0,0">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding UserName}" Foreground="White" FontSize="12" FontWeight="SemiBold"
@ -44,10 +41,6 @@
<TextBlock Text="{Binding RoleLabel}" Foreground="{DynamicResource SidebarText}"
FontSize="10.5" HorizontalAlignment="Right"/>
</StackPanel>
<Button Content="⚙" Click="OnSettingsClick" ToolTip.Tip="Настройки"
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
BorderThickness="1" CornerRadius="8" Width="40" Height="38" Padding="0"
FontSize="16" Cursor="Hand"/>
<Button Content="Выйти" Command="{Binding LogoutCommand}"
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
@ -55,7 +48,48 @@
</Grid>
</Border>
<!-- Контент -->
<ContentControl Content="{Binding CurrentPage}"/>
<!-- Строка вкладок-документов -->
<Border DockPanel.Dock="Top" Background="{DynamicResource Surface}"
BorderBrush="{DynamicResource Border}" BorderThickness="0,0,0,1" MinHeight="44" Padding="10,0">
<ListBox Classes="tabs" VerticalAlignment="Center" x:CompileBindings="False"
ItemsSource="{Binding Tabs}" SelectedItem="{Binding ActiveTab}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:DocumentTab">
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" VerticalAlignment="Center" FontSize="13"/>
<Button Content="✕" FontSize="11" Width="20" Height="20" Padding="0"
Background="Transparent" BorderThickness="0" Cursor="Hand"
Foreground="{DynamicResource Muted}"
Command="{Binding $parent[ListBox].DataContext.CloseTabCommand}"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Нижняя строка состояния -->
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Surface}"
BorderBrush="{DynamicResource Border}" BorderThickness="0,1,0,0" Height="32" Padding="16,0">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Text="Пользователь:" Classes="muted" FontSize="12"/>
<TextBlock Text="{Binding UserName}" FontSize="12" Foreground="{DynamicResource Text}"/>
<TextBlock Text=" | Аптека:" Classes="muted" FontSize="12"/>
<TextBlock Text="{Binding PharmacyAddress}" FontSize="12" Foreground="{DynamicResource Text}" TextTrimming="CharacterEllipsis"/>
</StackPanel>
<Button Grid.Column="1" Content="Настройки" Click="OnSettingsClick"
Background="Transparent" Foreground="{DynamicResource Accent}" BorderThickness="0"
Padding="10,4" FontSize="12.5" Cursor="Hand"/>
</Grid>
</Border>
<!-- Контент активной вкладки -->
<ContentControl Content="{Binding ActiveTab.Content}"/>
</DockPanel>
</UserControl>