diff --git a/Source/Store.Management.Api/ApiClientOptions.cs b/Source/Store.Management.Api/ApiClientOptions.cs
index 35e35be..0622cfb 100644
--- a/Source/Store.Management.Api/ApiClientOptions.cs
+++ b/Source/Store.Management.Api/ApiClientOptions.cs
@@ -3,7 +3,6 @@ namespace Store.Management.Api;
public sealed class ApiClientOptions
{
public required Uri BaseUrl { get; init; }
- public string Version { get; init; } = "v1";
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
internal void Validate()
@@ -12,8 +11,6 @@ public sealed class ApiClientOptions
(BaseUrl.Scheme != "https" && BaseUrl.Scheme != "http") ||
!string.IsNullOrEmpty(BaseUrl.UserInfo) || !string.IsNullOrEmpty(BaseUrl.Query) || !string.IsNullOrEmpty(BaseUrl.Fragment))
throw new ArgumentException("BaseUrl 必须是无凭据、查询参数和片段的 HTTP(S) 绝对地址。");
- if (!System.Text.RegularExpressions.Regex.IsMatch(Version, @"^v\d+(\.\d+)?$"))
- throw new ArgumentException("Version 格式应为 v1 或 v1.0。");
if (Timeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(Timeout));
}
}
diff --git a/Source/Store.Management.Api/StoreApiClient.cs b/Source/Store.Management.Api/StoreApiClient.cs
index 13629a1..0b493b0 100644
--- a/Source/Store.Management.Api/StoreApiClient.cs
+++ b/Source/Store.Management.Api/StoreApiClient.cs
@@ -7,23 +7,26 @@ namespace Store.Management.Api;
/// 一个服务地址与一个用户会话对应一个实例;在应用容器中注册为单例。
public sealed class StoreApiClient : IDisposable
{
- public TokenSession Session { get; } = new();
- public ApiTransport Transport { get; }
- public AuthApi Auth { get; }
- public UserApi Users { get; }
+ public TokenSession Session { get; } = new();
+ public ApiTransport Transport { get; }
+ public AuthApi Auth { get; }
+ public UserApi Users { get; }
- /// 可选的外部 HTTP 客户端,由调用方管理其生命周期。
- public StoreApiClient(ApiClientOptions options, HttpClient? httpClient = null)
- {
- ArgumentNullException.ThrowIfNull(options);
- Transport = new ApiTransport(options, Session, httpClient);
- Auth = new AuthApi(Transport, Session);
- Users = new UserApi(Transport);
- }
+ /// 可选的外部 HTTP 客户端,由调用方管理其生命周期。
+ public StoreApiClient(ApiClientOptions options, HttpClient? httpClient = null)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+#if DEBUG
+ Session.Set("store-local-dev-7f2c9e4a6b814d38a5910e62c4f837bd");
+#endif
+ Transport = new ApiTransport(options, Session, httpClient);
+ Auth = new AuthApi(Transport, Session);
+ Users = new UserApi(Transport);
+ }
- public void Dispose()
- {
- Session.Clear();
- Transport.Dispose();
- }
+ public void Dispose()
+ {
+ Session.Clear();
+ Transport.Dispose();
+ }
}
diff --git a/Source/Store.Management.Api/Transport/ApiTransport.cs b/Source/Store.Management.Api/Transport/ApiTransport.cs
index c43367a..1d13667 100644
--- a/Source/Store.Management.Api/Transport/ApiTransport.cs
+++ b/Source/Store.Management.Api/Transport/ApiTransport.cs
@@ -12,14 +12,12 @@ public sealed class ApiTransport : IDisposable
{
private readonly RestClient _client;
private readonly TokenSession _session;
- private readonly string _version;
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
internal ApiTransport(ApiClientOptions options, TokenSession session, HttpClient? httpClient)
{
options.Validate();
_session = session;
- _version = options.Version;
RestClientOptions settings = new RestClientOptions(new Uri(options.BaseUrl.AbsoluteUri.TrimEnd('/') + "/"))
{
Timeout = options.Timeout,
@@ -47,7 +45,7 @@ public sealed class ApiTransport : IDisposable
{
throw new ApiException(ApiErrorKind.Unauthorized, "请先登录。", code: 401);
}
- RestRequest request = new RestRequest($"{_version}/{route}", method);
+ RestRequest request = new RestRequest($"/{route}", method);
if (token is not null)
{
request.AddHeader("Authorization", $"Bearer {token}");
diff --git a/Source/Store.Management.Api/Users/UserModels.cs b/Source/Store.Management.Api/Users/UserModels.cs
index d23bca7..27fc1cf 100644
--- a/Source/Store.Management.Api/Users/UserModels.cs
+++ b/Source/Store.Management.Api/Users/UserModels.cs
@@ -6,7 +6,7 @@ namespace Store.Management.Api.Users;
public sealed class UserSearchRequest : PageRequest
{
[StringLength(100)] public string? Keyword { get; set; }
- public int? GroupId { get; set; }
+ public int? GroupId { get; set; }
[StringLength(20)] public string? ParentUserName { get; set; }
}
diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml
index 73885b3..bbfa67d 100644
--- a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml
+++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml
@@ -1,17 +1,22 @@
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:ext="clr-namespace:Store.Management.Framework.Controls"
+ xmlns:f="clr-namespace:Store.Management.Framework.Controls.Attach;assembly=Store.Management.Framework"
+ xmlns:local="clr-namespace:Store.Management.Framework.Controls"
+ xmlns:wpf="MAX.WPF"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib">
12
- Microsoft YaHei UI, Segoe UI
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -449,7 +514,7 @@
-
+
@@ -466,194 +531,263 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VerticalAlignment="Center"
+ HorizontalContentAlignment="Center"
+ AutomationProperties.Name="全选行"
+ Command="{x:Static ext:DataGridEx.ToggleSelectAllCommand}"
+ CommandTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}"
+ IsChecked="{Binding AreAllRowsSelected, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}, Mode=OneWay}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -667,7 +801,7 @@
-
+
-
+
+
+
\ No newline at end of file
diff --git a/Source/Store.Management.Framework/Services/StatusBarService.cs b/Source/Store.Management.Framework/Services/StatusBarService.cs
new file mode 100644
index 0000000..75b10c3
--- /dev/null
+++ b/Source/Store.Management.Framework/Services/StatusBarService.cs
@@ -0,0 +1,44 @@
+using MAX.Common.WPF.Helpers;
+using MAX.Core.DependencyInjection;
+using Prism.Mvvm;
+
+namespace Store.Management.Framework.Services;
+
+public enum StatusLevel
+{
+ Info,
+ Success,
+ Warning,
+ Error
+}
+
+public sealed record StatusBarMessage(string Text, StatusLevel Level, int? Progress);
+
+/// 各页面共享主窗口底部的最新状态。
+public interface IStatusBarService
+{
+ StatusBarMessage Current { get; }
+
+ /// 发布一条状态。进度范围为 0 到 100,省略时隐藏进度条。
+ void Report(string text, StatusLevel level = StatusLevel.Info, int? progress = null);
+}
+
+internal sealed class StatusBarService : BindableBase, IStatusBarService, ISingletonDependency
+{
+ private StatusBarMessage _current = new("就绪", StatusLevel.Info, null);
+ public StatusBarMessage Current
+ {
+ get => _current;
+ private set => SetProperty(ref _current, value);
+ }
+
+ public void Report(string text, StatusLevel level = StatusLevel.Info, int? progress = null)
+ {
+ ArgumentNullException.ThrowIfNull(text);
+ if (progress is < 0 or > 100)
+ {
+ throw new ArgumentOutOfRangeException(nameof(progress), "进度必须在 0 到 100 之间。");
+ }
+ DispatcherHelper.RunInDispatcher(() => Current = new StatusBarMessage(text, level, progress));
+ }
+}
diff --git a/Source/Store.Management/App.xaml.cs b/Source/Store.Management/App.xaml.cs
index 6dc4102..f268c1f 100644
--- a/Source/Store.Management/App.xaml.cs
+++ b/Source/Store.Management/App.xaml.cs
@@ -80,7 +80,7 @@ public partial class App : FrameworkApplication
containerRegistry.RegisterDialog();
- ViewModelLocationProvider.Register();
+ ViewModelLocationProvider.Register();
containerRegistry.RegisterDialog();
diff --git a/Source/Store.Management/MainWindow.xaml b/Source/Store.Management/MainWindow.xaml
deleted file mode 100644
index 6be2190..0000000
--- a/Source/Store.Management/MainWindow.xaml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/Store.Management/MainWindow.xaml.cs b/Source/Store.Management/MainWindow.xaml.cs
deleted file mode 100644
index 12c448c..0000000
--- a/Source/Store.Management/MainWindow.xaml.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Prism.Mvvm;
-using Prism.Commands;
-using Prism.Services.Dialogs;
-using Store.Management.Views;
-using Store.Management.Framework.Controls;
-using System.Windows;
-namespace Store.Management;
-
-public partial class MainWindow : WindowBase
-{
- public MainWindow() => InitializeComponent();
-}
-
-public class MainWindowViewModel(IDialogService dialogService) : BindableBase
-{
- public DelegateCommand OpenDataGridTestCommand => field ??= new(() =>
- dialogService.ShowDialog(nameof(DataGridTestView), new DialogParameters(), _ => { }));
-
-
- public DelegateCommand OpenLoginCommand => field ??= new(() =>
- dialogService.ShowDialog(nameof(LoginWindow), new DialogParameters(), _ => { }));
-
- private string _title="TTTT";
- public string Title
- {
- get => _title;
- set => SetProperty(ref _title, value);
- }
-}
\ No newline at end of file
diff --git a/Source/Store.Management/ViewModels/MainViewModel.Header.cs b/Source/Store.Management/ViewModels/MainViewModel.Header.cs
new file mode 100644
index 0000000..7b5d333
--- /dev/null
+++ b/Source/Store.Management/ViewModels/MainViewModel.Header.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Store.Management.ViewModels
+{
+ partial class MainViewModel
+ {
+ partial void InitHeader()
+ {
+
+ }
+ }
+}
diff --git a/Source/Store.Management/ViewModels/LeftNavViewModel.cs b/Source/Store.Management/ViewModels/MainViewModel.LeftNav.cs
similarity index 61%
rename from Source/Store.Management/ViewModels/LeftNavViewModel.cs
rename to Source/Store.Management/ViewModels/MainViewModel.LeftNav.cs
index 255e71f..a9d0b70 100644
--- a/Source/Store.Management/ViewModels/LeftNavViewModel.cs
+++ b/Source/Store.Management/ViewModels/MainViewModel.LeftNav.cs
@@ -1,8 +1,6 @@
-
-using MAX.Common.Helpers;
+using MAX.Common.Helpers;
using PipeReviewReport.Resources.Fonts;
using Prism.Commands;
-using Prism.Mvvm;
using Prism.Regions;
using Store.Management.Framework;
using Store.Management.Views;
@@ -16,19 +14,17 @@ using System.Windows.Input;
namespace Store.Management.ViewModels
{
- internal class LeftNavViewModel : BindableBase
+ partial class MainViewModel
{
- private readonly IRegionManager _regionManager;
-
public ObservableCollection NavItems { get; } = [];
- public LeftNavViewModel(IRegionManager regionManager)
+ partial void InitNav()
{
NavItems.Add(new NavItemViewModel
{
Code = "1",
Title = "角色管理",
Icon = IconNameEnum.Person,
- Url=nameof(DataGridTestView)
+ Url = nameof(DataGridTestView)
});
NavItems.Add(new NavItemViewModel
{
@@ -36,9 +32,8 @@ namespace Store.Management.ViewModels
Title = "商铺管理",
Icon = IconNameEnum.BuildingShop,
IsSelected = true,
- Url=nameof(UserManagementPage)
+ Url = nameof(UserManagementPage)
});
- _regionManager = regionManager;
}
public ICommand NavItemCommand => new AsyncDelegateCommand(NavItem);
@@ -51,18 +46,4 @@ namespace Store.Management.ViewModels
_regionManager.RequestNavigate(Navigation.MainContext, model.Url);
}
}
-
- public class NavItemViewModel : BindableBase
- {
- public string Code { get; set; }
- public string Title { get; set; }
- public IconNameEnum Icon { get; set; }
- public string Url { get; set; }
- private bool _isSelected;
- public bool IsSelected
- {
- get => _isSelected;
- set => SetProperty(ref _isSelected, value);
- }
- }
}
diff --git a/Source/Store.Management/ViewModels/MainViewModel.cs b/Source/Store.Management/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..b33ed3f
--- /dev/null
+++ b/Source/Store.Management/ViewModels/MainViewModel.cs
@@ -0,0 +1,26 @@
+using Store.Management.Framework.Services;
+using Prism.Mvvm;
+using Prism.Regions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Store.Management.ViewModels
+{
+ partial class MainViewModel : BindableBase
+ {
+ private readonly IRegionManager _regionManager;
+ public IStatusBarService StatusBar { get; }
+ public MainViewModel(IRegionManager regionManager, IStatusBarService statusBar)
+ {
+ _regionManager = regionManager;
+ StatusBar = statusBar;
+ InitNav();
+ InitHeader();
+ }
+ partial void InitNav();
+ partial void InitHeader();
+ }
+}
diff --git a/Source/Store.Management/ViewModels/NavItemViewModel.cs b/Source/Store.Management/ViewModels/NavItemViewModel.cs
new file mode 100644
index 0000000..70080b2
--- /dev/null
+++ b/Source/Store.Management/ViewModels/NavItemViewModel.cs
@@ -0,0 +1,19 @@
+using PipeReviewReport.Resources.Fonts;
+using Prism.Mvvm;
+
+namespace Store.Management.ViewModels
+{
+ public class NavItemViewModel : BindableBase
+ {
+ public string Code { get; set; }
+ public string Title { get; set; }
+ public IconNameEnum Icon { get; set; }
+ public string Url { get; set; }
+ private bool _isSelected;
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set => SetProperty(ref _isSelected, value);
+ }
+ }
+}
diff --git a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
index 73c1aaf..abe95fb 100644
--- a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
+++ b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
@@ -6,20 +6,22 @@ using Store.Management.Api.Models;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
using Store.Management.Framework.Controls;
-using Store.Management.Framework.ViewModels;
+using Store.Management.Framework.Services;
using Store.Management.Views;
using System.Windows;
+using System.Windows.Controls.Primitives;
namespace Store.Management.ViewModels;
-public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogService dialogs) : BindableBase
+public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogService dialogs, IStatusBarService statusBar) : BindableBase
{
public const int KeywordLimit = 100;
private string appliedKeyword = "";
private int pageSize = 50;
public string Keyword { get; set; } = "";
- public LoadingStatus Status { get; } = new();
+ private bool _canInteract = true;
+ public bool CanInteract { get => _canInteract; private set => SetProperty(ref _canInteract, value); }
public int Page { get; private set => SetProperty(ref field, value); } = 1;
public int Total { get; private set => SetProperty(ref field, value); }
public IReadOnlyList Users { get; private set => SetProperty(ref field, value); } = [];
@@ -27,7 +29,7 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe
public AsyncDelegateCommand SearchCommand => field ??= new(() => LoadAsync(Keyword.Trim(), 1, pageSize));
public AsyncDelegateCommand PageCommand => field ??= new(
- request => LoadAsync(appliedKeyword, request.PageIndex, request.PageSize), _ => !Status.IsLoading);
+ request => LoadAsync(appliedKeyword, request.PageIndex, request.PageSize), _ => CanInteract);
public AsyncDelegateCommand CreateCommand => field ??= new(() => OpenEditorAsync(null));
public AsyncDelegateCommand EditCommand => field ??= new AsyncDelegateCommand(() => OpenEditorAsync(SelectedUser),
() => SelectedUser != null).ObservesProperty(() => SelectedUser);
@@ -48,13 +50,14 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe
private async Task LoadAsync(string keyword, int page, int size)
{
- if (Status.IsLoading)
+ if (!CanInteract)
{
return;
}
- Status.Initialize("正在查询用户…");
+ CanInteract = false;
try
{
+ statusBar.Report("正在查询用户…");
UserSearchRequest request = new() { Keyword = keyword, Page = page, PageSize = size };
PageResult result = await client.Users.SearchAsync(request);
// 删除末页最后一条记录后,按服务端总数回到有效页。
@@ -70,6 +73,8 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe
Users = result.Items;
Total = (int)Math.Min(result.Total, int.MaxValue);
Page = request.Page;
+
+ statusBar.Report($"用户查询完成,共 {Total} 条。", StatusLevel.Success,100);
}
catch (ApiException ex)
{
@@ -79,11 +84,11 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe
Page = 1;
appliedKeyword = keyword;
pageSize = size;
- Status.Error(ex.Message);
+ statusBar.Report(ex.Message, StatusLevel.Error);
}
finally
{
- Status.Done();
+ CanInteract = true;
}
}
@@ -106,17 +111,21 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe
{
return;
}
- Status.Initialize("正在删除用户…");
+ CanInteract = false;
try
{
+ statusBar.Report("正在删除用户…");
await client.Users.DeleteAsync(user.Id);
}
catch (ApiException ex)
{
- Status.Error(ex.Message);
+ statusBar.Report(ex.Message, StatusLevel.Error);
return;
}
- Status.Done();
+ finally
+ {
+ CanInteract = true;
+ }
await LoadAsync(appliedKeyword, Page, pageSize);
}
}
diff --git a/Source/Store.Management/Views/DataGridTestView.xaml b/Source/Store.Management/Views/DataGridTestView.xaml
index c9ccabc..1c93a2e 100644
--- a/Source/Store.Management/Views/DataGridTestView.xaml
+++ b/Source/Store.Management/Views/DataGridTestView.xaml
@@ -6,6 +6,7 @@
xmlns:f="Store.Management.Framework"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
+ d:Background="White"
d:DataContext="{d:DesignInstance Type=vm:DataGridTestViewModel}"
d:DesignHeight="760"
d:DesignWidth="1280"
@@ -109,7 +110,8 @@
x:Name="StoreGrid"
Grid.Row="1"
Margin="10"
- d:IsLoading="True"
+ d:IsLoading="false"
+ BorderThickness="1"
CanUserSortColumns="True"
IsLoading="{Binding LoadingStatus.IsLoading, Mode=OneWay}"
IsReadOnly="True"
diff --git a/Source/Store.Management/Views/LeftNavView.xaml b/Source/Store.Management/Views/LeftNavView.xaml
index ceb50bd..db9003e 100644
--- a/Source/Store.Management/Views/LeftNavView.xaml
+++ b/Source/Store.Management/Views/LeftNavView.xaml
@@ -9,11 +9,10 @@
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
d:Background="White"
- d:DataContext="{d:DesignInstance Type=vm:LeftNavViewModel}"
+ d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
d:DesignHeight="450"
d:Width="255"
Background="White"
- DataContext="{f:DISource Type=vm:LeftNavViewModel}"
mc:Ignorable="d">
diff --git a/Source/Store.Management/Views/MainHeaderView.xaml b/Source/Store.Management/Views/MainHeaderView.xaml
new file mode 100644
index 0000000..d01af26
--- /dev/null
+++ b/Source/Store.Management/Views/MainHeaderView.xaml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/Source/Store.Management/Views/MainHeaderView.xaml.cs b/Source/Store.Management/Views/MainHeaderView.xaml.cs
new file mode 100644
index 0000000..b323799
--- /dev/null
+++ b/Source/Store.Management/Views/MainHeaderView.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace Store.Management.Views
+{
+ ///
+ /// MainHeaderView.xaml 的交互逻辑
+ ///
+ public partial class MainHeaderView : UserControl
+ {
+ public MainHeaderView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Source/Store.Management/Views/MainWindow.xaml b/Source/Store.Management/Views/MainWindow.xaml
new file mode 100644
index 0000000..fe1c449
--- /dev/null
+++ b/Source/Store.Management/Views/MainWindow.xaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Store.Management/Views/MainWindow.xaml.cs b/Source/Store.Management/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000..b6e6149
--- /dev/null
+++ b/Source/Store.Management/Views/MainWindow.xaml.cs
@@ -0,0 +1,29 @@
+using Prism.Mvvm;
+using Prism.Commands;
+using Prism.Services.Dialogs;
+using Store.Management.Views;
+using Store.Management.Framework.Controls;
+using System.Windows;
+namespace Store.Management.Views;
+
+public partial class MainWindow : WindowBase
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ NonClientAreaHeight = 50;
+ NonClientAreaContent = new MainHeaderView();
+ }
+}
+
+//public class MainWindowViewModel(IDialogService dialogService) : BindableBase
+//{
+// public DelegateCommand OpenDataGridTestCommand => field ??= new(() =>
+// dialogService.ShowDialog(nameof(DataGridTestView), new DialogParameters(), _ => { }));
+
+
+// public DelegateCommand OpenLoginCommand => field ??= new(() =>
+// dialogService.ShowDialog(nameof(LoginWindow), new DialogParameters(), _ => { }));
+
+
+//}
\ No newline at end of file
diff --git a/Source/Store.Management/Views/UserManagementPage.xaml b/Source/Store.Management/Views/UserManagementPage.xaml
index f4a2fd9..fa8bdc9 100644
--- a/Source/Store.Management/Views/UserManagementPage.xaml
+++ b/Source/Store.Management/Views/UserManagementPage.xaml
@@ -7,60 +7,54 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
+ Margin="5"
d:Background="White"
+ d:DataContext="{d:DesignInstance Type=vm:UserManagementPageViewModel}"
d:DesignHeight="760"
d:DesignWidth="1280"
+ Background="White"
Loaded="OnLoaded"
mc:Ignorable="d">
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Store.Management.App.SmokeTests/Program.cs b/Tests/Store.Management.App.SmokeTests/Program.cs
index 81fb88b..f5d353e 100644
--- a/Tests/Store.Management.App.SmokeTests/Program.cs
+++ b/Tests/Store.Management.App.SmokeTests/Program.cs
@@ -1,4 +1,6 @@
using Autofac;
+using Store.Management.Framework.Services;
+using System.Windows.Controls;
using System.Windows;
using System.Windows.Threading;
using Store.Management;
@@ -30,13 +32,10 @@ class Program
Check(ReferenceEquals(productionClient, productionContainer.Resolve()), "自动注册 API 单例");
Check(productionContainer.Resolve() != null, "自动注册登录服务");
}
- var builder = new ContainerBuilder();
- builder.RegisterInstance(client);
- new Store.Management.Framework.DependencyInjection.ApiClientRegistration().Register(builder);
- builder.RegisterAssemblyTypes(typeof(Store.Management.Framework.Services.ILoginService).Assembly)
- .Where(type => typeof(MAX.Core.DependencyInjection.ISingletonDependency).IsAssignableFrom(type))
- .AsImplementedInterfaces().SingleInstance();
- using var container = builder.Build();
+ var testExtension = new Prism.Autofac.AutofacContainerExtension();
+ testExtension.RegisterInstance(typeof(StoreApiClient), client);
+ testExtension.FinalizeExtension();
+ using var container = testExtension.Instance;
Check(ReferenceEquals(client, container.Resolve()), "保留测试客户端注册");
var service = container.Resolve();
using var vm = new LoginViewModel(service);
@@ -59,13 +58,51 @@ class Program
Console.WriteLine("PASS: LoginViewModel -> ILoginService -> AuthApi, retry, password contract and cancellation.");
return 0;
}
- var app = new TestApp(client);
+ App app = args.Contains("--status-bar") ? new App() : new TestApp(client);
app.InitializeComponent();
int exitCode = 0;
app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () =>
{
try
{
+ if (args.Contains("--status-bar"))
+ {
+ for (int attempt = 0; attempt < 200 && app.MainWindow is not MainWindow; attempt++)
+ {
+ await Task.Delay(50);
+ }
+ MainWindow shell = (MainWindow)app.MainWindow;
+ IStatusBarService status = app.Container.Resolve();
+ TextBlock text = (TextBlock)shell.FindName("StatusText");
+ ProgressBar progress = (ProgressBar)shell.FindName("StatusProgress");
+ Check(text.Text == "就绪" && progress.Visibility == Visibility.Collapsed, "初始状态");
+ foreach ((StatusLevel level, string brush, int value) in new[] {
+ (StatusLevel.Info, "PrimaryTextBrush", 0), (StatusLevel.Warning, "WarningBrush", 50),
+ (StatusLevel.Error, "DangerBrush", 75), (StatusLevel.Success, "SuccessBrush", 100) })
+ {
+ await Task.Run(() => status.Report(level.ToString(), level, value));
+ await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
+ Check(text.Text == level.ToString() && text.Foreground == shell.FindResource(brush), "状态文字和颜色");
+ Check(progress.Visibility == Visibility.Visible && progress.Value == value, "任务进度");
+ }
+ status.Report("完成");
+ await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
+ Check(text.Text == "完成" && progress.Visibility == Visibility.Collapsed, "纯文本状态隐藏进度");
+ using var usersClient = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("http://localhost:14312/api/v1/") }, httpClient);
+ usersClient.Session.Clear();
+ var usersVm = new UserManagementPageViewModel(usersClient, app.Container.Resolve(), status);
+ await usersVm.SearchCommand.Execute();
+ Check(status.Current.Level == StatusLevel.Error && usersVm.CanInteract, "查询失败后显示错误并允许重试");
+ await usersClient.Auth.LoginAsync(new() { UserName = "admin", Password = "320BB1D43EC042096A83DBAC6DC9294A" });
+ Task search = usersVm.SearchCommand.Execute();
+ Check(!usersVm.CanInteract && status.Current.Level == StatusLevel.Info, "查询期间禁用操作");
+ await search;
+ Check(usersVm.CanInteract && usersVm.Total == 51 && status.Current.Level == StatusLevel.Success, "重试查询成功");
+ await usersVm.PageCommand.Execute(new DataGridPageRequest(2, 50));
+ Check(usersVm.Page == 2 && usersVm.CanInteract && status.Current.Level == StatusLevel.Success, "分页查询成功");
+ Console.WriteLine("PASS: status bar bindings, colors and background progress reports.");
+ return;
+ }
for (int attempt = 0; attempt < 100 && !app.Windows.OfType().Any(window => window.Content is LoginWindow && window.IsLoaded); attempt++)
{
await Task.Delay(20);
@@ -185,6 +222,14 @@ sealed class LoginHandler : HttpMessageHandler
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
RequestCount++;
+ if (request.RequestUri?.AbsolutePath == "/api/v1/user/GetAllUser")
+ {
+ await Task.Delay(30, cancellationToken);
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("""{"code":200,"data":{"total":51,"items":[{"id":1,"userName":"admin"}]}}""", Encoding.UTF8, "application/json")
+ };
+ }
if (request.RequestUri?.AbsoluteUri != "http://localhost:14312/api/v1/Auth/Login")
throw new Exception("登录路由错误");
var body = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken));
diff --git a/Tests/Store.Management.DataGrid.SmokeTests/Program.cs b/Tests/Store.Management.DataGrid.SmokeTests/Program.cs
index bf0e4ab..7b9efe4 100644
--- a/Tests/Store.Management.DataGrid.SmokeTests/Program.cs
+++ b/Tests/Store.Management.DataGrid.SmokeTests/Program.cs
@@ -54,7 +54,16 @@ internal static class Program
window = new Window { Content = view, Width = 1280, Height = 760, Left = -10000, Top = -10000, ShowInTaskbar = false };
window.Show();
Pump();
- Check(grid.BorderThickness.Left == 1 && grid.BorderThickness.Right == 1, "continuous table side borders");
+ Border toolbar = (Border)grid.Template.FindName("PART_ToolbarArea", grid);
+ Border pagination = (Border)grid.Template.FindName("PART_PaginationArea", grid);
+ Check(grid.BorderThickness == new Thickness(0) && toolbar.BorderThickness == grid.BorderThickness && pagination.BorderThickness == grid.BorderThickness, "borderless demo includes toolbar and pagination");
+ grid.BorderThickness = new Thickness(2, 3, 4, 5);
+ grid.BorderBrush = Brushes.Transparent;
+ Pump();
+ Check(toolbar.BorderThickness == new Thickness(2, 3, 4, 0) && pagination.BorderThickness == new Thickness(2, 0, 4, 5), "toolbar and pagination follow outer border widths");
+ Check(toolbar.BorderBrush == grid.BorderBrush && pagination.BorderBrush == grid.BorderBrush, "toolbar and pagination follow grid border brush");
+ grid.BorderThickness = new Thickness(0);
+ grid.ClearValue(Control.BorderBrushProperty);
Check(vm.TotalCount == 237 && grid.Items.Count == 50 && grid.PageCount == 5, "Bogus data and page count");
Check(grid.Columns.Count == 10 && Equals(grid.Columns[0].Header, "序号"), "separate serial column");
DataGridRow row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(0);