Avalonia: вкладки-документы + нижняя строка состояния + монохромные иконки
- Вкладки-документы: разделы открываются вкладками (несколько сразу), закрываются, активная синхронизирована с верхней навигацией (ShellViewModel.Tabs/ActiveTab, CloseTabCommand) - Нижняя строка состояния: Пользователь | Аптека + кнопка «Настройки» - Монохромные иконки Segoe MDL2 (те же глифы, что в WinForms): Прайс E14C, Заказы E7BF, Накладные E8A5, Отчёты EB05, Загрузить E896, Отправить E725, Отказы E7BA, Справочник E8F1 - контент показывает активную вкладку Оболочка приведена к виду исходной программы. Проверено вживную. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cd2fb57bca
commit
0536d480cb
@ -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}"/>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user