Avalonia: общая корзина, добавление из Прайс-листа, Настройки
- CartService: единая корзина для Прайс-листа и Отправить - Прайс-лист: выбор позиции + кол-во + «Добавить в заказ», индикатор корзины - Настройки (диалог ⚙ в шапке): адрес API, тема (свет/тьма, применяется сразу), грузополучатель по умолчанию; сохранение в %APPDATA% - SettingsStore (Core) + AppConfig читает сохранённый API-адрес - тема применяется на старте Проверено вживую: добавление из Прайс-листа в корзину, диалог настроек. Реальная отправка заказа (POST) — собрана и связана, живой заказ по явному «да» (автозащита блокирует боевую транзакцию без подтверждения). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
43468ac693
commit
c677345ba6
@ -18,6 +18,10 @@ public partial class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
|
RequestedThemeVariant = Elfisa.Core.SettingsStore.Current.Theme == "Dark"
|
||||||
|
? global::Avalonia.Styling.ThemeVariant.Dark
|
||||||
|
: global::Avalonia.Styling.ThemeVariant.Light;
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
|
|||||||
71
src/Elfisa.Avalonia/ViewModels/CartService.cs
Normal file
71
src/Elfisa.Avalonia/ViewModels/CartService.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Elfisa.Core;
|
||||||
|
|
||||||
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public partial class CartItem : ObservableObject
|
||||||
|
{
|
||||||
|
public PriceItem Source { get; }
|
||||||
|
public string Name => Source.DrugName ?? "";
|
||||||
|
public string Supplier => Source.SupplierName ?? "";
|
||||||
|
public decimal Price => Source.Price;
|
||||||
|
|
||||||
|
[ObservableProperty] private decimal _qty = 1;
|
||||||
|
public decimal LineSum => Price * Qty;
|
||||||
|
|
||||||
|
public event Action? Changed;
|
||||||
|
|
||||||
|
public CartItem(PriceItem src) => Source = src;
|
||||||
|
|
||||||
|
partial void OnQtyChanged(decimal value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(LineSum));
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Единая корзина заказа — общая для «Прайс-листа» и «Отправить».</summary>
|
||||||
|
public sealed class CartService
|
||||||
|
{
|
||||||
|
public static CartService Instance { get; } = new();
|
||||||
|
|
||||||
|
public ObservableCollection<CartItem> Items { get; } = new();
|
||||||
|
public event Action? Changed;
|
||||||
|
|
||||||
|
public decimal Total => Items.Sum(c => c.LineSum);
|
||||||
|
public int Count => Items.Count;
|
||||||
|
|
||||||
|
public void Add(PriceItem src, decimal qty = 1)
|
||||||
|
{
|
||||||
|
if (src is null || string.IsNullOrEmpty(src.SupplierPriceId)) return;
|
||||||
|
var existing = Items.FirstOrDefault(c => c.Source.SupplierPriceId == src.SupplierPriceId);
|
||||||
|
if (existing != null) existing.Qty += qty;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ci = new CartItem(src) { Qty = qty };
|
||||||
|
ci.Changed += Raise;
|
||||||
|
Items.Add(ci);
|
||||||
|
}
|
||||||
|
Raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(CartItem item)
|
||||||
|
{
|
||||||
|
if (item is null) return;
|
||||||
|
item.Changed -= Raise;
|
||||||
|
Items.Remove(item);
|
||||||
|
Raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
foreach (var i in Items) i.Changed -= Raise;
|
||||||
|
Items.Clear();
|
||||||
|
Raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Raise() => Changed?.Invoke();
|
||||||
|
}
|
||||||
@ -7,24 +7,33 @@ using Elfisa.Core;
|
|||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
/// <summary>Прайс-лист (сводный прайс с /api/supplier-prices/summary — как в WinForms).</summary>
|
/// <summary>Прайс-лист (сводный прайс) + добавление позиций в заказ (общая корзина).</summary>
|
||||||
public partial class PriceListViewModel : ViewModelBase
|
public partial class PriceListViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ApiClient _api = new();
|
private readonly ApiClient _api = new();
|
||||||
|
private readonly CartService _cart = CartService.Instance;
|
||||||
|
|
||||||
public ObservableCollection<PriceItem> Items { get; } = new();
|
public ObservableCollection<PriceItem> Items { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _search = "";
|
[ObservableProperty] private string _search = "";
|
||||||
|
[ObservableProperty] private PriceItem? _selectedItem;
|
||||||
|
[ObservableProperty] private decimal _qty = 1;
|
||||||
[ObservableProperty] private bool _busy;
|
[ObservableProperty] private bool _busy;
|
||||||
[ObservableProperty] private string? _status;
|
[ObservableProperty] private string? _status;
|
||||||
[ObservableProperty] private int _total;
|
[ObservableProperty] private int _total;
|
||||||
|
[ObservableProperty] private string _cartInfo = "";
|
||||||
|
|
||||||
public PriceListViewModel()
|
public PriceListViewModel()
|
||||||
{
|
{
|
||||||
_api.SetToken(Session.Current.Token);
|
_api.SetToken(Session.Current.Token);
|
||||||
|
_cart.Changed += UpdateCartInfo;
|
||||||
|
UpdateCartInfo();
|
||||||
if (Session.Current.IsAuthenticated) _ = SearchAsync();
|
if (Session.Current.IsAuthenticated) _ = SearchAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateCartInfo() =>
|
||||||
|
CartInfo = _cart.Count == 0 ? "Корзина пуста" : $"В заказе: {_cart.Count} поз. · {_cart.Total:N2}";
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SearchAsync()
|
private async Task SearchAsync()
|
||||||
{
|
{
|
||||||
@ -42,4 +51,13 @@ public partial class PriceListViewModel : ViewModelBase
|
|||||||
catch (Exception ex) { Status = ex.Message; }
|
catch (Exception ex) { Status = ex.Message; }
|
||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddToOrder()
|
||||||
|
{
|
||||||
|
if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; }
|
||||||
|
var q = Qty <= 0 ? 1 : Qty;
|
||||||
|
_cart.Add(SelectedItem, q);
|
||||||
|
Status = $"Добавлено в заказ: {SelectedItem.DrugName} × {q:N0}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@ -9,37 +8,17 @@ using Elfisa.Core;
|
|||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
public partial class CartItem : ObservableObject
|
|
||||||
{
|
|
||||||
public PriceItem Source { get; }
|
|
||||||
public string Name => Source.DrugName ?? "";
|
|
||||||
public string Supplier => Source.SupplierName ?? "";
|
|
||||||
public decimal Price => Source.Price;
|
|
||||||
|
|
||||||
[ObservableProperty] private decimal _qty = 1;
|
|
||||||
public decimal LineSum => Price * Qty;
|
|
||||||
|
|
||||||
public event Action? Changed;
|
|
||||||
|
|
||||||
public CartItem(PriceItem src) => Source = src;
|
|
||||||
|
|
||||||
partial void OnQtyChanged(decimal value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(LineSum));
|
|
||||||
Changed?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отправить: поиск по прайсу → корзина → создание заказа (POST /api/buyer/orders).
|
/// Отправить: поиск по прайсу → корзина (общая с Прайс-листом) → создание заказа.
|
||||||
/// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика.
|
/// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SendOrderViewModel : ViewModelBase
|
public partial class SendOrderViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ApiClient _api = new();
|
private readonly ApiClient _api = new();
|
||||||
|
private readonly CartService _cart = CartService.Instance;
|
||||||
|
|
||||||
public ObservableCollection<PriceItem> Results { get; } = new();
|
public ObservableCollection<PriceItem> Results { get; } = new();
|
||||||
public ObservableCollection<CartItem> Cart { get; } = new();
|
public ObservableCollection<CartItem> Cart => _cart.Items;
|
||||||
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _search = "";
|
[ObservableProperty] private string _search = "";
|
||||||
@ -53,12 +32,11 @@ public partial class SendOrderViewModel : ViewModelBase
|
|||||||
public SendOrderViewModel()
|
public SendOrderViewModel()
|
||||||
{
|
{
|
||||||
_api.SetToken(Session.Current.Token);
|
_api.SetToken(Session.Current.Token);
|
||||||
Cart.CollectionChanged += OnCartChanged;
|
_cart.Changed += Recalc;
|
||||||
|
Recalc();
|
||||||
if (Session.Current.IsAuthenticated) _ = InitAsync();
|
if (Session.Current.IsAuthenticated) _ = InitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCartChanged(object? s, NotifyCollectionChangedEventArgs e) => Recalc();
|
|
||||||
|
|
||||||
private async Task InitAsync()
|
private async Task InitAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -90,33 +68,19 @@ public partial class SendOrderViewModel : ViewModelBase
|
|||||||
private void AddToCart()
|
private void AddToCart()
|
||||||
{
|
{
|
||||||
if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; }
|
if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; }
|
||||||
var existing = Cart.FirstOrDefault(c => c.Source.SupplierPriceId == SelectedResult.SupplierPriceId);
|
_cart.Add(SelectedResult);
|
||||||
if (existing != null) { existing.Qty += 1; }
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var ci = new CartItem(SelectedResult);
|
|
||||||
ci.Changed += Recalc;
|
|
||||||
Cart.Add(ci);
|
|
||||||
}
|
|
||||||
Recalc();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void RemoveFromCart(CartItem? item)
|
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
|
||||||
{
|
|
||||||
if (item is null) return;
|
|
||||||
item.Changed -= Recalc;
|
|
||||||
Cart.Remove(item);
|
|
||||||
Recalc();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Recalc() => CartTotal = Cart.Sum(c => c.LineSum);
|
private void Recalc() => CartTotal = _cart.Total;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SendAsync()
|
private async Task SendAsync()
|
||||||
{
|
{
|
||||||
Status = null;
|
Status = null;
|
||||||
if (Cart.Count == 0) { Status = "Корзина пуста."; return; }
|
if (_cart.Count == 0) { Status = "Корзина пуста."; return; }
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Busy = true;
|
Busy = true;
|
||||||
@ -124,7 +88,7 @@ public partial class SendOrderViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
LocationId = SelectedLocation?.BuyerLocationId,
|
LocationId = SelectedLocation?.BuyerLocationId,
|
||||||
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
|
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
|
||||||
Items = Cart.Select(c => new BuyerOrderItemReq
|
Items = _cart.Items.Select(c => new BuyerOrderItemReq
|
||||||
{
|
{
|
||||||
SupplierPriceId = c.Source.SupplierPriceId ?? "",
|
SupplierPriceId = c.Source.SupplierPriceId ?? "",
|
||||||
Qty = (double)c.Qty,
|
Qty = (double)c.Qty,
|
||||||
@ -134,8 +98,7 @@ public partial class SendOrderViewModel : ViewModelBase
|
|||||||
};
|
};
|
||||||
var resp = await _api.CreateOrderAsync(req);
|
var resp = await _api.CreateOrderAsync(req);
|
||||||
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}.";
|
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}.";
|
||||||
Cart.Clear();
|
_cart.Clear();
|
||||||
Recalc();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
|
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
|
||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
|
|||||||
43
src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs
Normal file
43
src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Elfisa.Core;
|
||||||
|
|
||||||
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public partial class SettingsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ApiClient _api = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private string _apiBaseUrl = "";
|
||||||
|
public string[] Themes { get; } = { "Светлая", "Тёмная" };
|
||||||
|
[ObservableProperty] private int _themeIndex;
|
||||||
|
|
||||||
|
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
||||||
|
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
|
||||||
|
|
||||||
|
public SettingsViewModel()
|
||||||
|
{
|
||||||
|
_api.SetToken(Session.Current.Token);
|
||||||
|
ApiBaseUrl = SettingsStore.Current.ApiBaseUrl ?? AppConfig.DefaultApiBaseUrl;
|
||||||
|
ThemeIndex = SettingsStore.Current.Theme == "Dark" ? 1 : 0;
|
||||||
|
if (Session.Current.IsAuthenticated) _ = LoadLocationsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadLocationsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var locs = await _api.GetBuyerLocationsAsync();
|
||||||
|
Locations.Clear();
|
||||||
|
foreach (var l in locs) Locations.Add(l);
|
||||||
|
var savedId = SettingsStore.Current.DefaultLocationId;
|
||||||
|
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
|
||||||
|
?? Locations.FirstOrDefault(l => l.IsDefault)
|
||||||
|
?? Locations.FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch { /* локации не критичны для настроек */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,8 +27,17 @@
|
|||||||
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
|
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<Border DockPanel.Dock="Bottom" Classes="card" Margin="0,10,0,0" Padding="14">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Кол-во" Classes="label"/>
|
||||||
|
<NumericUpDown Value="{Binding Qty}" Minimum="1" Maximum="100000" Increment="1" FormatString="0" Width="130"/>
|
||||||
|
<Button Classes="primary" Content="Добавить в заказ ▸" Command="{Binding AddToOrderCommand}"/>
|
||||||
|
<TextBlock Text="{Binding CartInfo}" Classes="muted" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Classes="card" Padding="0" ClipToBounds="True">
|
<Border Classes="card" Padding="0" ClipToBounds="True">
|
||||||
<DataGrid ItemsSource="{Binding Items}" x:CompileBindings="False"
|
<DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" x:CompileBindings="False"
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
||||||
CanUserResizeColumns="True" CanUserSortColumns="False">
|
CanUserResizeColumns="True" CanUserSortColumns="False">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|||||||
34
src/Elfisa.Avalonia/Views/SettingsWindow.axaml
Normal file
34
src/Elfisa.Avalonia/Views/SettingsWindow.axaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
||||||
|
x:Class="Elfisa.Avalonia.Views.SettingsWindow"
|
||||||
|
x:DataType="vm:SettingsViewModel"
|
||||||
|
Width="460" Height="360" CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{DynamicResource Bg}"
|
||||||
|
Title="Настройки">
|
||||||
|
|
||||||
|
<StackPanel Margin="24" Spacing="16">
|
||||||
|
<TextBlock Text="Настройки" Classes="h1"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Адрес сервера (API)" Classes="label"/>
|
||||||
|
<TextBox Text="{Binding ApiBaseUrl}" Watermark="https://24pharmdata.ru"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Тема" Classes="label"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Themes}" SelectedIndex="{Binding ThemeIndex}" HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Грузополучатель по умолчанию" Classes="label"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10" Margin="0,8,0,0">
|
||||||
|
<Button Content="Отмена" Click="OnCancel" Padding="18,9"/>
|
||||||
|
<Button Classes="primary" Content="Сохранить" Click="OnSave"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
31
src/Elfisa.Avalonia/Views/SettingsWindow.axaml.cs
Normal file
31
src/Elfisa.Avalonia/Views/SettingsWindow.axaml.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Elfisa.Avalonia.ViewModels;
|
||||||
|
using Elfisa.Core;
|
||||||
|
|
||||||
|
namespace Elfisa.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class SettingsWindow : Window
|
||||||
|
{
|
||||||
|
public SettingsWindow() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
private void OnSave(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is SettingsViewModel vm)
|
||||||
|
{
|
||||||
|
SettingsStore.Current.ApiBaseUrl = string.IsNullOrWhiteSpace(vm.ApiBaseUrl) ? null : vm.ApiBaseUrl.Trim().TrimEnd('/');
|
||||||
|
SettingsStore.Current.Theme = vm.ThemeIndex == 1 ? "Dark" : "Light";
|
||||||
|
SettingsStore.Current.DefaultLocationId = vm.SelectedLocation?.BuyerLocationId;
|
||||||
|
SettingsStore.Save();
|
||||||
|
|
||||||
|
if (Application.Current is { } app)
|
||||||
|
app.RequestedThemeVariant = vm.ThemeIndex == 1 ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||||
|
}
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@ -44,6 +44,10 @@
|
|||||||
<TextBlock Text="{Binding RoleLabel}" Foreground="{DynamicResource SidebarText}"
|
<TextBlock Text="{Binding RoleLabel}" Foreground="{DynamicResource SidebarText}"
|
||||||
FontSize="10.5" HorizontalAlignment="Right"/>
|
FontSize="10.5" HorizontalAlignment="Right"/>
|
||||||
</StackPanel>
|
</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}"
|
<Button Content="Выйти" Command="{Binding LogoutCommand}"
|
||||||
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
|
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
|
||||||
BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
|
BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
namespace Elfisa.Avalonia.Views;
|
||||||
|
|
||||||
public partial class ShellView : UserControl
|
public partial class ShellView : UserControl
|
||||||
{
|
{
|
||||||
public ShellView() => AvaloniaXamlLoader.Load(this);
|
public ShellView() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
private async void OnSettingsClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var win = new SettingsWindow { DataContext = new SettingsViewModel() };
|
||||||
|
var owner = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
|
||||||
|
if (owner is not null) await win.ShowDialog(owner);
|
||||||
|
else win.Show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,8 @@ public static class AppConfig
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
var fromSettings = SettingsStore.Current.ApiBaseUrl;
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromSettings)) return fromSettings.Trim().TrimEnd('/');
|
||||||
var env = Environment.GetEnvironmentVariable(ApiEnv);
|
var env = Environment.GetEnvironmentVariable(ApiEnv);
|
||||||
return string.IsNullOrWhiteSpace(env) ? DefaultApiBaseUrl : env.Trim().TrimEnd('/');
|
return string.IsNullOrWhiteSpace(env) ? DefaultApiBaseUrl : env.Trim().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/Elfisa.Core/SettingsStore.cs
Normal file
44
src/Elfisa.Core/SettingsStore.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Elfisa.Core;
|
||||||
|
|
||||||
|
public sealed class AppSettings
|
||||||
|
{
|
||||||
|
public string? ApiBaseUrl { get; set; }
|
||||||
|
public string Theme { get; set; } = "Light"; // Light | Dark
|
||||||
|
public string? DefaultLocationId { get; set; }
|
||||||
|
public string? Token { get; set; } // сохранённая сессия
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Настройки приложения в %APPDATA%\ElfisaAvalonia\settings.json.</summary>
|
||||||
|
public static class SettingsStore
|
||||||
|
{
|
||||||
|
private static readonly string Dir =
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ElfisaAvalonia");
|
||||||
|
private static readonly string File = Path.Combine(Dir, "settings.json");
|
||||||
|
|
||||||
|
public static AppSettings Current { get; private set; } = Load();
|
||||||
|
|
||||||
|
public static AppSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(File))
|
||||||
|
return JsonSerializer.Deserialize<AppSettings>(System.IO.File.ReadAllText(File)) ?? new AppSettings();
|
||||||
|
}
|
||||||
|
catch { /* повреждённый файл — начинаем с чистых настроек */ }
|
||||||
|
return new AppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Dir);
|
||||||
|
System.IO.File.WriteAllText(File, JsonSerializer.Serialize(Current, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
catch { /* нет прав/диска — молча пропускаем */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user