修改代码

This commit is contained in:
chfei committed 2026-11-12 18:22:14 +08:00
1 parent f03e2b08f4
commit 3dcb6a1c34
24 files changed
+882 -480

No files matched your search

@@ -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));
}
}
+20 -17
View File
@@ -7,23 +7,26 @@ namespace Store.Management.Api;
/// <summary>一个服务地址与一个用户会话对应一个实例;在应用容器中注册为单例。</summary>
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; }
/// <param name="httpClient">可选的外部 HTTP 客户端,由调用方管理其生命周期。</param>
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);
}
/// <param name="httpClient">可选的外部 HTTP 客户端,由调用方管理其生命周期。</param>
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();
}
}
@@ -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}");
@@ -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; }
}
@@ -1,17 +1,22 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Store.Management.Framework.Controls"
xmlns:ext="clr-namespace:Store.Management.Framework.Controls"
xmlns:f="clr-namespace:Store.Management.Framework.Controls.Attach;assembly=Store.Management.Framework"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
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">
<sys:Double x:Key="DataGridExFontSize">12</sys:Double>
<FontFamily x:Key="DataGridExFontFamily">Microsoft YaHei UI, Segoe UI</FontFamily>
<BooleanToVisibilityConverter x:Key="DataGridExVisibility"/>
<Style x:Key="DataGridExCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}"/>
<Style x:Key="DataGridExDefaultCellHeaderStyle" TargetType="DataGridColumnHeader">
<FontFamily x:Key="DataGridExFontFamily">Microsoft YaHei UI, Segoe UI</FontFamily>
<wpf:ThicknessSplitConverter x:Key="DataGridExBorderThicknessSplit" />
<BooleanToVisibilityConverter x:Key="DataGridExVisibility" />
<Style
x:Key="DataGridExCheckBoxStyle"
BasedOn="{StaticResource {x:Type CheckBox}}"
TargetType="CheckBox" />
<Style x:Key="DataGridExDefaultCellHeaderStyle" TargetType="DataGridColumnHeader">
<Setter Property="UseLayoutRounding" Value="True" />
<!--<Setter Property="f:DataGridHelper.ShowBorder" Value="True" />-->
@@ -240,7 +245,8 @@
Grid.Column="1"
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
Visibility="{TemplateBinding DetailsVisibility}" />
<DataGridRowHeader x:Name="PART_RowHeader"
<DataGridRowHeader
x:Name="PART_RowHeader"
Grid.RowSpan="2"
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
@@ -355,84 +361,143 @@
</Style>
<Style x:Key="DataGridExDefaultDataGridStyle" TargetType="local:DataGridEx">
<Style.Resources>
<Style x:Key="DataGridExColumnMenu" TargetType="ContextMenu">
<Setter Property="Width" Value="240"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#343A40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Background="White" BorderBrush="#DDE0E5" BorderThickness="1" CornerRadius="6" Padding="0">
<StackPanel KeyboardNavigation.DirectionalNavigation="Cycle">
<StackPanel Margin="14,12,14,8">
<TextBlock Text="显示的列" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="勾选后立即生效" Foreground="#858A94" Margin="0,4,0,0"/>
</StackPanel>
<ScrollViewer MaxHeight="340" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="6,0,6,8">
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
</ScrollViewer>
<Border BorderBrush="#ECEEF1" BorderThickness="0,1,0,0" Padding="12,9" Background="#FAFAFB">
<Button x:Name="PART_DoneButton" Content="完成" HorizontalAlignment="Right" Width="64" Height="28"
Command="{x:Static ApplicationCommands.Close}"
Foreground="White" Background="#98638F" BorderThickness="0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="ButtonBorder" Background="{TemplateBinding Background}" CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="ButtonBorder" Property="Opacity" Value="0.85"/></Trigger>
<Trigger Property="IsKeyboardFocused" Value="True"><Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#343A40"/><Setter TargetName="ButtonBorder" Property="BorderThickness" Value="1"/></Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="Height" Value="30"/>
<Setter Property="Padding" Value="8,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="ItemBorder" Background="Transparent" CornerRadius="4" Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="24"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Border x:Name="CheckBorder" Width="14" Height="14" HorizontalAlignment="Left" VerticalAlignment="Center"
BorderBrush="#C8CDD4" BorderThickness="1" CornerRadius="3" Background="White">
<Path x:Name="CheckMark" Data="M 2,6 L 5,9 L 11,3" Stroke="White" StrokeThickness="1.5" Visibility="Collapsed"/>
</Border>
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True"><Setter TargetName="ItemBorder" Property="Background" Value="#F3F4F6"/></Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBorder" Property="Background" Value="#98638F"/>
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#98638F"/>
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#8A8F98"/>
<Setter TargetName="CheckBorder" Property="Background" Value="#D5D8DE"/>
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#D5D8DE"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Style.Resources>
<Style x:Key="DataGridExColumnMenu" TargetType="ContextMenu">
<Setter Property="Width" Value="240" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="#343A40" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border
Padding="0"
Background="White"
BorderBrush="#DDE0E5"
BorderThickness="1"
CornerRadius="6">
<StackPanel KeyboardNavigation.DirectionalNavigation="Cycle">
<StackPanel Margin="14,12,14,8">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Text="显示的列" />
<TextBlock
Margin="0,4,0,0"
Foreground="#858A94"
Text="勾选后立即生效" />
</StackPanel>
<ScrollViewer
MaxHeight="340"
Margin="6,0,6,8"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle" />
</ScrollViewer>
<Border
Padding="12,9"
Background="#FAFAFB"
BorderBrush="#ECEEF1"
BorderThickness="0,1,0,0">
<Button
x:Name="PART_DoneButton"
Width="64"
Height="28"
HorizontalAlignment="Right"
Background="#98638F"
BorderThickness="0"
Command="{x:Static ApplicationCommands.Close}"
Content="完成"
Foreground="White">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border
x:Name="ButtonBorder"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.85" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="#343A40" />
<Setter TargetName="ButtonBorder" Property="BorderThickness" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="Height" Value="30" />
<Setter Property="Padding" Value="8,0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border
x:Name="ItemBorder"
Padding="{TemplateBinding Padding}"
Background="Transparent"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
x:Name="CheckBorder"
Width="14"
Height="14"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="White"
BorderBrush="#C8CDD4"
BorderThickness="1"
CornerRadius="3">
<Path
x:Name="CheckMark"
Data="M 2,6 L 5,9 L 11,3"
Stroke="White"
StrokeThickness="1.5"
Visibility="Collapsed" />
</Border>
<ContentPresenter
Grid.Column="1"
VerticalAlignment="Center"
ContentSource="Header" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="#F3F4F6" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBorder" Property="Background" Value="#98638F" />
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#98638F" />
<Setter TargetName="CheckMark" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#8A8F98" />
<Setter TargetName="CheckBorder" Property="Background" Value="#D5D8DE" />
<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#D5D8DE" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Style.Resources>
<Setter Property="UseLayoutRounding" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Background" Value="White" />
@@ -449,7 +514,7 @@
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Foreground" Value="#343A40" />
<Setter Property="BorderBrush" Value="#E1E4E8" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="VerticalGridLinesBrush" Value="#E1E4E8" />
<Setter Property="HorizontalGridLinesBrush" Value="#E1E4E8" />
<Setter Property="RowDetailsVisibilityMode" Value="VisibleWhenSelected" />
@@ -466,194 +531,263 @@
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DataGridEx}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<!-- 调用方可以提供批量操作内容,默认展示选择提示和列设置。 -->
<Border x:Name="PART_ToolbarArea" Grid.Row="0" Background="#FAFAFA" BorderBrush="#E1E4E8" BorderThickness="1,1,1,0" Padding="8,5"
Visibility="{Binding ShowToolbar, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
<DockPanel LastChildFill="True">
<Button x:Name="PART_ColumnsButton" DockPanel.Dock="Right" Content="列设置" Padding="10,3" MinHeight="26"
Visibility="{Binding ShowColumnSettings, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}"/>
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center" Margin="0,0,12,0"
Text="{Binding SelectionSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"/>
<ContentPresenter Content="{TemplateBinding ToolbarContent}" VerticalAlignment="Center"/>
</DockPanel>
</Border>
<!-- 原生 DataGrid 滚动与虚拟化宿主。 -->
<Border x:Name="PART_TableArea" Grid.Row="1"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True"
UseLayoutRounding="True">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Grid.Column="0"
Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
Background="#eeeeee"
BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
BorderThickness="0,0,1,1">
<CheckBox
Width="16"
HorizontalAlignment="Center"
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}">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource DataGridExCheckBoxStyle}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
<Condition Binding="{Binding ShowSelectAll, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Border>
<DataGridColumnHeadersPresenter
x:Name="PART_ColumnHeadersPresenter"
Grid.Column="1"
Grid.ColumnSpan="2"
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
<Grid
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2">
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" />
<Grid
x:Name="NoDataTipsGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Path
Width="100"
Height="60"
Data="M156.662278 699.758173h21.097186A10.444152 10.444152 0 0 1 187.994733 710.202325c0 5.765172-4.490985 10.444152-10.235269 10.444152H156.662278v21.097186A10.444152 10.444152 0 0 1 146.218126 751.978932a10.277045 10.277045 0 0 1-10.444152-10.235269V720.646477H114.676787A10.444152 10.444152 0 0 1 104.441518 710.202325c0-5.765172 4.490985-10.444152 10.235269-10.444152H135.773974v-21.097187A10.444152 10.444152 0 0 1 146.218126 668.425717c5.765172 0 10.444152 4.490985 10.444152 10.235269v21.097187z m1378.628042-83.553215v-21.097186A10.277045 10.277045 0 0 0 1524.846168 584.872503a10.444152 10.444152 0 0 0-10.444152 10.235269v21.097186h-21.097186a10.277045 10.277045 0 0 0-10.235269 10.444152c0 5.598065 4.595427 10.444152 10.235269 10.444152h21.097186v21.097187c0 5.744284 4.67898 10.235269 10.444152 10.235268a10.444152 10.444152 0 0 0 10.444152-10.235268V637.093262h21.097187c5.744284 0 10.235269-4.67898 10.235268-10.444152a10.444152 10.444152 0 0 0-10.235268-10.444152H1535.29032zM776.460024 960.861969H250.596979A20.80475 20.80475 0 0 1 229.77134 939.973665c0-11.530344 9.462402-20.888304 20.825639-20.888303h94.728457A83.010119 83.010119 0 0 1 334.212859 877.413196v-605.96969A83.49055 83.49055 0 0 1 417.849627 187.994733H480.430984V167.001988A83.49055 83.49055 0 0 1 564.067752 83.553215h501.152182A83.448773 83.448773 0 0 1 1148.856702 167.001988v605.969689c0 15.185797-4.052331 29.410732-11.133466 41.672166h115.554096c11.551232 0 20.909192 9.274407 20.909192 20.888304 0 11.530344-9.295295 20.888304-20.888304 20.888304H1002.638576v20.992745c0 15.185797-4.052331 29.410732-11.133466 41.672166h11.196131c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304h-109.893365c9.545955 16.000441 7.478013 36.972297-6.41271 50.863019a41.672166 41.672166 0 0 1-59.072122 0L776.460024 960.861969z m76.367638-41.776607h66.424806c22.977134 0 41.609501-18.59059 41.609501-41.881049V270.461756c0-22.559368-18.047494-40.690416-40.314426-40.690416H416.303892c-22.266932 0-40.314426 18.214601-40.314426 40.690416v606.742557c0 23.123352 18.799473 41.881049 41.588613 41.881049h317.084449l-10.736588-10.757477a41.693054 41.693054 0 0 1-10.861918-40.377091l-19.718558-19.739447A146.259902 146.259902 0 0 1 502.363703 627.693525a146.218126 146.218126 0 0 1 220.517822 190.981761l19.739447 19.739447a41.630389 41.630389 0 0 1 40.377091 10.841029L852.827662 919.085362zM1002.638576 814.643843h62.852906A41.797496 41.797496 0 0 0 1107.080095 772.867236V167.106429c0-23.14424-18.632367-41.776607-41.588613-41.776607H563.775316A41.797496 41.797496 0 0 0 522.207592 167.106429v20.888304h396.794216A83.448773 83.448773 0 0 1 1002.638576 271.443506V814.643843zM266.325872 46.998683h31.123572c8.773088 0 15.875111 6.955805 15.875111 15.666228 0 8.647758-7.102023 15.666228-15.875111 15.666228h-31.123572v31.123572c0 8.773088-6.955805 15.875111-15.666228 15.875111a15.770669 15.770669 0 0 1-15.666228-15.875111V78.331139H203.869844A15.728893 15.728893 0 0 1 187.994733 62.664911c0-8.647758 7.102023-15.666228 15.875111-15.666228h31.123572V15.875111c0-8.773088 6.955805-15.875111 15.666228-15.875111 8.647758 0 15.666228 7.102023 15.666228 15.875111v31.123572zM20.888304 939.973665c0-11.530344 9.462402-20.888304 20.825638-20.888303h125.455152c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304H41.713942A20.80475 20.80475 0 0 1 20.888304 939.973665z m658.733544-135.021995a104.441518 104.441518 0 1 0-147.722083-147.722083 104.441518 104.441518 0 0 0 147.722083 147.722083zM459.542681 313.324555a20.888304 20.888304 0 0 1 20.867415-20.888304H710.202325a20.888304 20.888304 0 1 1 0 41.776608H480.430984A20.825639 20.825639 0 0 1 459.542681 313.324555z m0 104.441518c0-11.530344 9.295295-20.888304 20.742085-20.888303h334.505295c11.44679 0 20.742086 9.274407 20.742086 20.888303 0 11.530344-9.295295 20.888304-20.742086 20.888304H480.284766A20.762974 20.762974 0 0 1 459.542681 417.766073z m0 104.441519c0-11.530344 9.316183-20.888304 20.846527-20.888304h146.301679c11.509455 0 20.846527 9.274407 20.846527 20.888304 0 11.530344-9.316183 20.888304-20.846527 20.888303h-146.301679A20.80475 20.80475 0 0 1 459.542681 522.207592zM62.664911 396.87777a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911zM1357.739739 271.547948a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911z"
Fill="LightGray"
Stretch="Fill" />
<TextBlock
Grid.Row="1"
Margin="10"
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 调用方可以提供批量操作内容,默认展示选择提示和列设置。 -->
<Border
x:Name="PART_ToolbarArea"
Grid.Row="0"
Padding="8,5"
Background="#FAFAFA"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DataGridExBorderThicknessSplit}, ConverterParameter='1,1,1,0'}"
Visibility="{Binding ShowToolbar, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
<DockPanel LastChildFill="True">
<Button
x:Name="PART_ColumnsButton"
MinHeight="26"
Padding="10,3"
Content="列设置"
DockPanel.Dock="Right"
Visibility="{Binding ShowColumnSettings, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}" />
<TextBlock
Margin="0,0,12,0"
VerticalAlignment="Center"
DockPanel.Dock="Left"
Text="{Binding SelectionSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
<ContentPresenter VerticalAlignment="Center" Content="{TemplateBinding ToolbarContent}" />
</DockPanel>
</Border>
<!-- 原生 DataGrid 滚动与虚拟化宿主。 -->
<Border
x:Name="PART_TableArea"
Grid.Row="1"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True"
UseLayoutRounding="True">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Grid.Column="0"
Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
Background="#eeeeee"
BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
BorderThickness="0,0,1,1">
<CheckBox
Width="16"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontSize="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=FontSize, Mode=OneWay}"
Foreground="LightGray"
Text="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips), Mode=OneWay}" />
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}">
<CheckBox.Style>
<Style BasedOn="{StaticResource DataGridExCheckBoxStyle}" TargetType="CheckBox">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ShowCheckBox, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
<Condition Binding="{Binding ShowSelectAll, RelativeSource={RelativeSource AncestorType={x:Type ext:DataGridEx}}}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Border>
<DataGridColumnHeadersPresenter
x:Name="PART_ColumnHeadersPresenter"
Grid.Column="1"
Grid.ColumnSpan="2"
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
<Grid
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2">
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" />
<Grid
x:Name="NoDataTipsGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Path
Width="100"
Height="60"
Data="M156.662278 699.758173h21.097186A10.444152 10.444152 0 0 1 187.994733 710.202325c0 5.765172-4.490985 10.444152-10.235269 10.444152H156.662278v21.097186A10.444152 10.444152 0 0 1 146.218126 751.978932a10.277045 10.277045 0 0 1-10.444152-10.235269V720.646477H114.676787A10.444152 10.444152 0 0 1 104.441518 710.202325c0-5.765172 4.490985-10.444152 10.235269-10.444152H135.773974v-21.097187A10.444152 10.444152 0 0 1 146.218126 668.425717c5.765172 0 10.444152 4.490985 10.444152 10.235269v21.097187z m1378.628042-83.553215v-21.097186A10.277045 10.277045 0 0 0 1524.846168 584.872503a10.444152 10.444152 0 0 0-10.444152 10.235269v21.097186h-21.097186a10.277045 10.277045 0 0 0-10.235269 10.444152c0 5.598065 4.595427 10.444152 10.235269 10.444152h21.097186v21.097187c0 5.744284 4.67898 10.235269 10.444152 10.235268a10.444152 10.444152 0 0 0 10.444152-10.235268V637.093262h21.097187c5.744284 0 10.235269-4.67898 10.235268-10.444152a10.444152 10.444152 0 0 0-10.235268-10.444152H1535.29032zM776.460024 960.861969H250.596979A20.80475 20.80475 0 0 1 229.77134 939.973665c0-11.530344 9.462402-20.888304 20.825639-20.888303h94.728457A83.010119 83.010119 0 0 1 334.212859 877.413196v-605.96969A83.49055 83.49055 0 0 1 417.849627 187.994733H480.430984V167.001988A83.49055 83.49055 0 0 1 564.067752 83.553215h501.152182A83.448773 83.448773 0 0 1 1148.856702 167.001988v605.969689c0 15.185797-4.052331 29.410732-11.133466 41.672166h115.554096c11.551232 0 20.909192 9.274407 20.909192 20.888304 0 11.530344-9.295295 20.888304-20.888304 20.888304H1002.638576v20.992745c0 15.185797-4.052331 29.410732-11.133466 41.672166h11.196131c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304h-109.893365c9.545955 16.000441 7.478013 36.972297-6.41271 50.863019a41.672166 41.672166 0 0 1-59.072122 0L776.460024 960.861969z m76.367638-41.776607h66.424806c22.977134 0 41.609501-18.59059 41.609501-41.881049V270.461756c0-22.559368-18.047494-40.690416-40.314426-40.690416H416.303892c-22.266932 0-40.314426 18.214601-40.314426 40.690416v606.742557c0 23.123352 18.799473 41.881049 41.588613 41.881049h317.084449l-10.736588-10.757477a41.693054 41.693054 0 0 1-10.861918-40.377091l-19.718558-19.739447A146.259902 146.259902 0 0 1 502.363703 627.693525a146.218126 146.218126 0 0 1 220.517822 190.981761l19.739447 19.739447a41.630389 41.630389 0 0 1 40.377091 10.841029L852.827662 919.085362zM1002.638576 814.643843h62.852906A41.797496 41.797496 0 0 0 1107.080095 772.867236V167.106429c0-23.14424-18.632367-41.776607-41.588613-41.776607H563.775316A41.797496 41.797496 0 0 0 522.207592 167.106429v20.888304h396.794216A83.448773 83.448773 0 0 1 1002.638576 271.443506V814.643843zM266.325872 46.998683h31.123572c8.773088 0 15.875111 6.955805 15.875111 15.666228 0 8.647758-7.102023 15.666228-15.875111 15.666228h-31.123572v31.123572c0 8.773088-6.955805 15.875111-15.666228 15.875111a15.770669 15.770669 0 0 1-15.666228-15.875111V78.331139H203.869844A15.728893 15.728893 0 0 1 187.994733 62.664911c0-8.647758 7.102023-15.666228 15.875111-15.666228h31.123572V15.875111c0-8.773088 6.955805-15.875111 15.666228-15.875111 8.647758 0 15.666228 7.102023 15.666228 15.875111v31.123572zM20.888304 939.973665c0-11.530344 9.462402-20.888304 20.825638-20.888303h125.455152c11.488567 0 20.825639 9.274407 20.825639 20.888303 0 11.530344-9.462402 20.888304-20.825639 20.888304H41.713942A20.80475 20.80475 0 0 1 20.888304 939.973665z m658.733544-135.021995a104.441518 104.441518 0 1 0-147.722083-147.722083 104.441518 104.441518 0 0 0 147.722083 147.722083zM459.542681 313.324555a20.888304 20.888304 0 0 1 20.867415-20.888304H710.202325a20.888304 20.888304 0 1 1 0 41.776608H480.430984A20.825639 20.825639 0 0 1 459.542681 313.324555z m0 104.441518c0-11.530344 9.295295-20.888304 20.742085-20.888303h334.505295c11.44679 0 20.742086 9.274407 20.742086 20.888303 0 11.530344-9.295295 20.888304-20.742086 20.888304H480.284766A20.762974 20.762974 0 0 1 459.542681 417.766073z m0 104.441519c0-11.530344 9.316183-20.888304 20.846527-20.888304h146.301679c11.509455 0 20.846527 9.274407 20.846527 20.888304 0 11.530344-9.316183 20.888304-20.846527 20.888303h-146.301679A20.80475 20.80475 0 0 1 459.542681 522.207592zM62.664911 396.87777a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911zM1357.739739 271.547948a62.664911 62.664911 0 1 1 0-125.329822 62.664911 62.664911 0 0 1 0 125.329822z m0-31.332456a31.332456 31.332456 0 1 0 0-62.664911 31.332456 31.332456 0 0 0 0 62.664911z"
Fill="LightGray"
Stretch="Fill" />
<TextBlock
Grid.Row="1"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontSize="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=FontSize, Mode=OneWay}"
Foreground="LightGray"
Text="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips), Mode=OneWay}" />
</Grid>
</Grid>
<ScrollBar
x:Name="PART_VerticalScrollBar"
Grid.Row="1"
Grid.Column="2"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
<Grid
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2">
<ScrollBar
x:Name="PART_HorizontalScrollBar"
Maximum="{TemplateBinding ScrollableWidth}"
Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
</Grid>
</Grid>
<ScrollBar
x:Name="PART_VerticalScrollBar"
Grid.Row="1"
Grid.Column="2"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<ScrollBar
x:Name="PART_HorizontalScrollBar"
Maximum="{TemplateBinding ScrollableWidth}"
Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />
</Grid>
</Grid>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.IsActive)}" Value="false" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=HasItems}" Value="false" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Visible" />
</MultiDataTrigger>
<!--<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips)}" Value="{x:Null}">
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.IsActive)}" Value="false" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=HasItems}" Value="false" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Visible" />
</MultiDataTrigger>
<!--<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips)}" Value="{x:Null}">
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=(f:DataGridHelper.NoDataTips)}" Value="">
<Setter TargetName="NoDataTipsGrid" Property="Visibility" Value="Collapsed" />
</DataTrigger>-->
</ControlTemplate.Triggers>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
<!-- 分页仅请求数据,不截断调用方的 ItemsSource。 -->
<Border x:Name="PART_PaginationArea" Grid.Row="2" Background="#FAFAFA" BorderBrush="#E1E4E8" BorderThickness="1,0,1,1" Padding="8,4"
Visibility="{Binding ShowPagination, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
<Grid>
<ContentPresenter x:Name="PART_PaginationContent" Content="{TemplateBinding PaginationContent}"/>
<DockPanel x:Name="PART_DefaultPagination" Visibility="Collapsed">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<TextBlock Text="每页" VerticalAlignment="Center" Margin="0,0,6,0"/>
<ComboBox Width="70" MinHeight="26" IsEditable="False" Margin="0,0,8,0"
ItemsSource="{Binding PageSizes, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
SelectedItem="{Binding PageSize, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="首页" Command="{x:Static local:DataGridEx.FirstPageCommand}" Padding="8,2" Margin="0,0,4,0"/>
<Button Content="上一页" Command="{x:Static local:DataGridEx.PreviousPageCommand}" Padding="8,2"/>
<TextBlock Text="{Binding PageSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" VerticalAlignment="Center" Margin="10,0"/>
<Button Content="下一页" Command="{x:Static local:DataGridEx.NextPageCommand}" Padding="8,2"/>
<Button Content="末页" Command="{x:Static local:DataGridEx.LastPageCommand}" Padding="8,2" Margin="4,0,0,0"/>
</StackPanel>
<TextBlock Text="{Binding TotalSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" VerticalAlignment="Center"/>
</DockPanel>
</Grid>
</Border>
<Border x:Name="PART_LoadingOverlay" Grid.RowSpan="3" Background="#DDFFFFFF" Visibility="Collapsed" Cursor="Wait">
<StackPanel Width="180" HorizontalAlignment="Center" VerticalAlignment="Center">
<ProgressBar x:Name="PART_LoadingProgress" Height="4" IsIndeterminate="False" Foreground="#98638F" Background="#EDE5EB" BorderThickness="0"/>
<TextBlock Text="{TemplateBinding LoadingText}" HorizontalAlignment="Center" Margin="0,12,0,0" Foreground="#626873"
AutomationProperties.LiveSetting="Polite"/>
</StackPanel>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsLoading" Value="True">
<Setter TargetName="PART_LoadingOverlay" Property="Visibility" Value="Visible"/>
<Setter TargetName="PART_LoadingProgress" Property="IsIndeterminate" Value="True"/>
<Setter TargetName="PART_ToolbarArea" Property="IsEnabled" Value="False"/>
<Setter TargetName="PART_TableArea" Property="IsEnabled" Value="False"/>
<Setter TargetName="PART_PaginationArea" Property="IsEnabled" Value="False"/>
</Trigger>
<Trigger Property="PaginationContent" Value="{x:Null}">
<Setter TargetName="PART_DefaultPagination" Property="Visibility" Value="Visible"/>
<Setter TargetName="PART_PaginationContent" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ControlTemplate.Triggers>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
<!-- 分页仅请求数据,不截断调用方的 ItemsSource。 -->
<Border
x:Name="PART_PaginationArea"
Grid.Row="2"
Padding="8,4"
Background="#FAFAFA"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DataGridExBorderThicknessSplit}, ConverterParameter='1,0,1,1'}"
Visibility="{Binding ShowPagination, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay, Converter={StaticResource DataGridExVisibility}}">
<Grid>
<ContentPresenter x:Name="PART_PaginationContent" Content="{TemplateBinding PaginationContent}" />
<DockPanel x:Name="PART_DefaultPagination" Visibility="Collapsed">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<TextBlock
Margin="0,0,6,0"
VerticalAlignment="Center"
Text="每页" />
<ComboBox
Width="70"
MinHeight="26"
Margin="0,0,8,0"
IsEditable="False"
ItemsSource="{Binding PageSizes, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
SelectedItem="{Binding PageSize, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
Margin="0,0,4,0"
Padding="8,2"
Command="{x:Static local:DataGridEx.FirstPageCommand}"
Content="首页" />
<Button
Padding="8,2"
Command="{x:Static local:DataGridEx.PreviousPageCommand}"
Content="上一页" />
<TextBlock
Margin="10,0"
VerticalAlignment="Center"
Text="{Binding PageSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
<Button
Padding="8,2"
Command="{x:Static local:DataGridEx.NextPageCommand}"
Content="下一页" />
<Button
Margin="4,0,0,0"
Padding="8,2"
Command="{x:Static local:DataGridEx.LastPageCommand}"
Content="末页" />
</StackPanel>
<TextBlock VerticalAlignment="Center" Text="{Binding TotalSummary, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
</DockPanel>
</Grid>
</Border>
<Border
x:Name="PART_LoadingOverlay"
Grid.RowSpan="3"
Background="#DDFFFFFF"
Cursor="Wait"
Visibility="Collapsed">
<StackPanel
Width="180"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ProgressBar
x:Name="PART_LoadingProgress"
Height="4"
Background="#EDE5EB"
BorderThickness="0"
Foreground="#98638F"
IsIndeterminate="False" />
<TextBlock
Margin="0,12,0,0"
HorizontalAlignment="Center"
AutomationProperties.LiveSetting="Polite"
Foreground="#626873"
Text="{TemplateBinding LoadingText}" />
</StackPanel>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsLoading" Value="True">
<Setter TargetName="PART_LoadingOverlay" Property="Visibility" Value="Visible" />
<Setter TargetName="PART_LoadingProgress" Property="IsIndeterminate" Value="True" />
<Setter TargetName="PART_ToolbarArea" Property="IsEnabled" Value="False" />
<Setter TargetName="PART_TableArea" Property="IsEnabled" Value="False" />
<Setter TargetName="PART_PaginationArea" Property="IsEnabled" Value="False" />
</Trigger>
<Trigger Property="PaginationContent" Value="{x:Null}">
<Setter TargetName="PART_DefaultPagination" Property="Visibility" Value="Visible" />
<Setter TargetName="PART_PaginationContent" Property="Visibility" Value="Collapsed" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
@@ -667,7 +801,7 @@
</Style.Triggers>
</Style>
<Style x:Key="DataGridExCellStyle" TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0" />
@@ -704,8 +838,8 @@
</Setter>
</Style>
<Style x:Key="DataGridExRowStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="ContextMenu" Value="{Binding RowContextMenu, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
<Setter Property="Tag" Value="{Binding DataContext, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
<Setter Property="ContextMenu" Value="{Binding RowContextMenu, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
<Setter Property="Tag" Value="{Binding DataContext, RelativeSource={RelativeSource AncestorType=local:DataGridEx}}" />
<Setter Property="BorderThickness" Value="0,0,1,0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="FontSize" Value="{StaticResource DataGridExFontSize}" />
@@ -739,7 +873,8 @@
Grid.Column="1"
SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding AreRowDetailsFrozen, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"
Visibility="{TemplateBinding DetailsVisibility}" />
<DataGridRowHeader x:Name="PART_RowHeader"
<DataGridRowHeader
x:Name="PART_RowHeader"
Grid.RowSpan="2"
SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical"
Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Row}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />
@@ -838,18 +973,28 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DataGridExRowHeaderStyle" BasedOn="{StaticResource DataGridExDefaultRowHeaderStyle}" TargetType="DataGridRowHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridRowHeader">
<Border Background="Transparent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="0,0,1,1">
<CheckBox Style="{StaticResource DataGridExCheckBoxStyle}" HorizontalAlignment="Center" VerticalAlignment="Center" AutomationProperties.Name="选择行"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="DataGridExRowHeaderStyle"
BasedOn="{StaticResource DataGridExDefaultRowHeaderStyle}"
TargetType="DataGridRowHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridRowHeader">
<Border
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0,0,1,1">
<CheckBox
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="选择行"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource DataGridExCheckBoxStyle}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="DataGridExNumberCellStyle"
@@ -859,11 +1004,11 @@
</Style>
<Style BasedOn="{StaticResource DataGridExDefaultDataGridStyle}" TargetType="local:DataGridEx">
<Setter Property="EnableRowVirtualization" Value="True" />
<Setter Property="EnableColumnVirtualization" Value="True" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="EnableRowVirtualization" Value="True" />
<Setter Property="EnableColumnVirtualization" Value="True" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="SelectionUnit" Value="FullRow" />
@@ -1,7 +1,8 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:f="clr-namespace:Store.Management.Framework.Controls">
xmlns:f="clr-namespace:Store.Management.Framework.Controls"
xmlns:wpf="MAX.WPF">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Fonts/FontGeometry.xaml" />
<ResourceDictionary Source="pack://application:,,,/MAX.Common.WPF;component/Resources/Generic.xaml" />
@@ -35,4 +36,14 @@
</Style>
<Style BasedOn="{StaticResource WindowBase}" TargetType="f:WindowBase" />
<Style BasedOn="{StaticResource DialogWindowStyle}" TargetType="f:DialogWindow" />
<Style
x:Key="VerticalAlignment"
BasedOn="{StaticResource TitleContentBaseStyle}"
TargetType="wpf:TitleContent">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="TitleVerticalAlignment" Value="Center" />
<Setter Property="Placement" Value="Left" />
</Style>
</ResourceDictionary>
@@ -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);
/// <summary>各页面共享主窗口底部的最新状态。</summary>
public interface IStatusBarService
{
StatusBarMessage Current { get; }
/// <summary>发布一条状态。进度范围为 0 到 100,省略时隐藏进度条。</summary>
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));
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ public partial class App : FrameworkApplication
containerRegistry.RegisterDialog<DataGridTestView, DataGridTestViewModel>();
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
ViewModelLocationProvider.Register<MainWindow, MainViewModel>();
containerRegistry.RegisterDialog<LoginWindow, LoginViewModel>();
-45
View File
@@ -1,45 +0,0 @@
<f:WindowBase
x:Class="Store.Management.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:f="Store.Management.Framework"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:prism="http://prismlibrary.com/"
Title="{Binding Title}"
Width="1140"
Height="720"
MinWidth="900"
MinHeight="560"
WindowStartupLocation="CenterScreen">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,0,1,0">
<local:LeftNavView />
</Border>
<!-- 右侧页面标题与内容 -->
<DockPanel Grid.Column="1">
<TextBlock
Margin="16,16,16,0"
DockPanel.Dock="Top"
Text="{Binding SelectedItem.Header, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
<ContentControl x:Name="PART_SelectedContentHost" prism:RegionManager.RegionName="{Binding Source={x:Static f:Navigation.MainContext}}" />
</DockPanel>
<Border
Grid.Row="1"
Grid.ColumnSpan="2"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,1,0,0" />
</Grid>
</Border>
</f:WindowBase>
@@ -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);
}
}
@@ -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()
{
}
}
}
@@ -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<NavItemViewModel> 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<NavItemViewModel>(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);
}
}
}
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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<UserInfo> 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<DataGridPageRequest> 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<UserInfo> 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);
}
}
@@ -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"
@@ -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">
<UserControl.Resources>
<wpf:CommandProxy x:Key="NavItemCommand" Command="{Binding NavItemCommand}" />
@@ -0,0 +1,13 @@
<UserControl
x:Class="Store.Management.Views.MainHeaderView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Height="50"
d:Background="White"
d:Width="800"
mc:Ignorable="d">
<Grid />
</UserControl>
@@ -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
{
/// <summary>
/// MainHeaderView.xaml 的交互逻辑
/// </summary>
public partial class MainHeaderView : UserControl
{
public MainHeaderView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,107 @@
<f:WindowBase
x:Class="Store.Management.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:f="Store.Management.Framework"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
Width="1140"
Height="720"
MinWidth="900"
MinHeight="560"
Background="#f4f5f7"
ShowIcon="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,0,1,0">
<local:LeftNavView />
</Border>
<!-- 右侧页面标题与内容 -->
<Border Grid.Column="1" Padding="0">
<ContentControl x:Name="PART_SelectedContentHost" prism:RegionManager.RegionName="{Binding Source={x:Static f:Navigation.MainContext}}" />
</Border>
<Border
Grid.Row="1"
Grid.ColumnSpan="2"
Padding="0,0"
Background="{DynamicResource RegionBrush}"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,1,0,0"
DataContext="{Binding StatusBar.Current}">
<Grid Width="auto" HorizontalAlignment="Stretch">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="TextElement.Foreground" Value="{DynamicResource PrimaryTextBrush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Level}" Value="Success">
<Setter Property="TextElement.Foreground" Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Level}" Value="Warning">
<Setter Property="TextElement.Foreground" Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Level}" Value="Error">
<Setter Property="TextElement.Foreground" Value="{DynamicResource DangerBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ProgressBar
x:Name="StatusProgress"
Grid.Column="1"
Width="80"
Height="6"
Margin="15,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
d:Visibility="Visible"
hc:BorderElement.CornerRadius="2"
hc:VisualElement.Text=""
AutomationProperties.Name="任务进度"
Foreground="{Binding Path=(TextElement.Foreground), RelativeSource={RelativeSource AncestorType=Grid}}"
Value="{Binding Progress, Mode=OneWay, TargetNullValue=0}">
<ProgressBar.Style>
<Style BasedOn="{StaticResource {x:Type ProgressBar}}" TargetType="ProgressBar">
<Style.Triggers>
<DataTrigger Binding="{Binding Progress}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
<TextBlock
x:Name="StatusText"
Grid.Column="0"
Margin="15,0,0,0"
VerticalAlignment="Center"
d:Text="就绪"
FontSize="12"
Foreground="{Binding Path=(TextElement.Foreground), RelativeSource={RelativeSource AncestorType=Grid}}"
Text="{Binding Text}"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Text}" />
</Grid>
</Border>
</Grid>
</Border>
</f:WindowBase>
@@ -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(), _ => { }));
//}
@@ -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">
<DockPanel Margin="16">
<DockPanel IsEnabled="{Binding CanInteract}">
<!-- 查询及用户操作 -->
<wpf:SpacingStackPanel
Margin="0,0,0,12"
DockPanel.Dock="Top"
HorizontalSpacing="8"
Orientation="Horizontal">
<wpf:SpacingStackPanel.Style>
<Style TargetType="wpf:SpacingStackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding Status.IsLoading, Mode=OneWay}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</wpf:SpacingStackPanel.Style>
<Button Command="{Binding LoginCommand}" Content="登录" />
<wpf:TitleContent
Title="关键词"
Placement="Left"
TitleVerticalAlignment="Center">
<TextBox
Width="175"
AutomationProperties.Name="用户查询关键词"
MaxLength="{x:Static vm:UserManagementPageViewModel.KeywordLimit}"
Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchCommand}" />
</TextBox.InputBindings>
</TextBox>
</wpf:TitleContent>
<Button Command="{Binding SearchCommand}" Content="查询" />
<Button Command="{Binding CreateCommand}" Content="创建用户" />
<Button Command="{Binding EditCommand}" Content="编辑" />
<Button Command="{Binding DeleteCommand}" Content="删除" />
</wpf:SpacingStackPanel>
<TextBlock
DockPanel.Dock="Bottom"
Foreground="{DynamicResource DangerBrush}"
Text="{Binding Status.ErrorMessage, Mode=OneWay}"
TextWrapping="Wrap" />
<Border
Padding="10,5"
VerticalAlignment="Center"
DockPanel.Dock="Top">
<wpf:SpacingStackPanel
Height="44"
HorizontalSpacing="8"
Orientation="Horizontal">
<wpf:TitleContent Title="关键词" Style="{StaticResource VerticalAlignment}">
<TextBox
Width="175"
AutomationProperties.Name="用户查询关键词"
MaxLength="{x:Static vm:UserManagementPageViewModel.KeywordLimit}"
Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchCommand}" />
</TextBox.InputBindings>
</TextBox>
</wpf:TitleContent>
<Button
Command="{Binding SearchCommand}"
Content="查询"
Style="{StaticResource ButtonPrimary}" />
<Button Command="{Binding CreateCommand}" Content="创建用户" />
<Button Command="{Binding EditCommand}" Content="编辑" />
<Button Command="{Binding DeleteCommand}" Content="删除" />
</wpf:SpacingStackPanel>
</Border>
<!-- 用户列表与服务端分页 -->
<f:DataGridEx
AutoGenerateColumns="False"
BorderThickness="1"
CanUserSortColumns="False"
IsLoading="{Binding Status.IsLoading, Mode=OneWay}"
IsReadOnly="True"
ItemsSource="{Binding Users, Mode=OneWay}"
LoadingText="{Binding Status.Message, Mode=OneWay}"
NoDataTips="没有符合条件的用户"
PageChangedCommand="{Binding PageCommand}"
PageSize="50"
@@ -95,4 +89,5 @@
</f:DataGridEx.Columns>
</f:DataGridEx>
</DockPanel>
</UserControl>
@@ -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<StoreApiClient>()), "自动注册 API 单例");
Check(productionContainer.Resolve<Store.Management.Framework.Services.ILoginService>() != 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<StoreApiClient>()), "保留测试客户端注册");
var service = container.Resolve<Store.Management.Framework.Services.ILoginService>();
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<IStatusBarService>();
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<IDialogService>(), 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<DialogWindow>().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<HttpResponseMessage> 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));
@@ -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);