提交用户部分代码
This commit is contained in:
1 parent
9b9e4cbd82
commit
127fc22de5
26 files changed
+1110
-206
No files matched your search
@@ -6,6 +6,8 @@ globs: "**/*.xaml"
|
||||
|
||||
# XAML 视图编写约定
|
||||
|
||||
项目 XAML 默认不添加 `AutomationProperties.Name`。
|
||||
|
||||
## View XAML 头部模板
|
||||
|
||||
```XML
|
||||
@@ -13,6 +15,8 @@ globs: "**/*.xaml"
|
||||
x:Class="xxxxx.(xxxx)?.Views.XxxView"
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:xxxx.(xxxx)?.Views"
|
||||
xmlns:core="MAX.Core"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
@@ -23,20 +27,26 @@ globs: "**/*.xaml"
|
||||
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
|
||||
d:Background="White"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800">
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
>
|
||||
```
|
||||
|
||||
## Dialog View XAML
|
||||
|
||||
```XML
|
||||
<UserControl
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:core="MAX.Core"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
d:Background="White"
|
||||
d:DataContext="{d:DesignInstance Type=local:DialogViewModel}"
|
||||
Style="{StaticResource DialogStyle.Base}"
|
||||
DataContext="{core:DISource Type=local:DialogViewModel}">
|
||||
DataContext="{core:DISource Type=local:DialogViewModel}"
|
||||
mc:Ignorable="d"
|
||||
>
|
||||
```
|
||||
|
||||
- `{core:DISource Type=local:XxxViewModel}` 绑定 ViewModel
|
||||
@@ -47,9 +57,12 @@ DataContext="{core:DISource Type=local:DialogViewModel}">
|
||||
|
||||
### 控件必须有设计数据
|
||||
|
||||
例如
|
||||
TextBlock > d:Text="TextBlock"
|
||||
ComboBox > d:ItemsSource="{d:SampleData ItemCount=5}"
|
||||
页面、弹窗和 `DataTemplate` 应能在 XAML 设计器中直接看出字段内容、排列方向、对齐和间距。设计值使用 `d:` 命名空间,根元素声明 `xmlns:d`、`xmlns:mc` 和 `mc:Ignorable="d"`,保留运行时绑定。
|
||||
|
||||
- `TextBox`、`TextBlock` 的文本来自绑定或默认为空时,必须设置有业务含义的非空示例,如 `d:Text="示例商贸有限公司"`。静态文案已可见时无需重复填写。
|
||||
- 绑定集合的 `ItemsControl`、`ListBox`、`ComboBox`、`DataGridEx` 等必须提供设计时数据,默认使用 `d:ItemsSource="{d:SampleData ItemCount=5}"`。行数可按实际布局调整。下拉框要预览选中项时可加 `d:SelectedIndex="0"`。
|
||||
- 其他控件按实际显示属性提供设计值,例如绑定标题的 `wpf:TitleContent` 用 `d:Title="中转仓快递费(元/单)"`,金额输入框用 `d:Value="2.20"`,绑定按钮文案用 `d:Content="保存"`。运行时按状态显示的区域,可用 `d:Visibility="Visible"` 预览。
|
||||
- 集合模板既要能看到多行,也要能看清每行内容。若样本数据未填充自定义控件的显示属性,在模板内补相应的 `d:` 值。
|
||||
|
||||
## BindingProxy + CommandProxy(DataTemplate/ContextMenu 绑定)
|
||||
|
||||
@@ -284,12 +297,27 @@ C# 侧:`"#FF5733".toColor()`、`"#80000000".toColor()`。
|
||||
- 需要行列划分、跨行跨列、重叠或虚拟化时,使用满足这些要求的布局容器,不强行替换为 `SpacingStackPanel`。
|
||||
- 属性及用法见 [max-common-wpf SpacingStackPanel 参考](../skills/max-common-wpf/references/api.md#controlsspacingstackpanelcs)。
|
||||
|
||||
### ItemsControl 的顺序排版
|
||||
|
||||
少量集合项按行或列顺序排列时,必须在 `ItemsControl.ItemsPanel` 的 `ItemsPanelTemplate` 中使用 `wpf:SpacingStackPanel`。项与项之间的间距由这个面板控制;`ItemTemplate` 负责单项内部布局。优先复用已有间距样式,例如计费弹窗的 `VerticalSpacingPanel4`。需要虚拟化的长列表保留虚拟化面板。
|
||||
|
||||
```xml
|
||||
<ItemsControl ItemsSource="{Binding Fields, Mode=OneWay}"
|
||||
d:ItemsSource="{d:SampleData ItemCount=5}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<wpf:SpacingStackPanel Style="{StaticResource VerticalSpacingPanel4}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
```
|
||||
|
||||
## 文字与简单内容布局
|
||||
|
||||
**页面排版遇到“说明文字 + 简单内容”(如输入框、选择器、按钮等输入或点击控件)时,优先使用 `wpf:TitleContent`。** 将说明文字放在 `Title`,交互控件作为内容,避免为同一组合另用 `TextBlock` 配合 `Grid` / `StackPanel` 手工拼接。
|
||||
|
||||
- 按布局设置 `Placement`;左侧标题表单优先使用 `Placement="Left"`,并按需设置 `TitleWidth`、`TitleVerticalAlignment="Center"` 和 `LeftTitlePadding`,保持同组字段对齐。
|
||||
- 内容控件仍自行配置绑定、命令、输入长度和可访问名称;`TitleContent` 只负责标题与内容的布局。
|
||||
- 内容控件仍自行配置绑定、命令和输入长度;`TitleContent` 只负责标题与内容的布局。
|
||||
- 属性及用法见 [max-common-wpf TitleContent 参考](../skills/max-common-wpf/references/api.md#controlstitlecontentcs)。
|
||||
|
||||
## 布局注释
|
||||
|
||||
@@ -20,9 +20,13 @@ public sealed class UserApi(ApiTransport transport)
|
||||
public Task<UserInfo> CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return transport.GetRequiredAsync<UserInfo>("user/AddUser", Method.Post, request, cancellationToken: cancellationToken);
|
||||
string route = request.GroupId is null or UserGroup.MainAccount ? "user/AddUser" : "user/AddSubUser";
|
||||
return transport.GetRequiredAsync<UserInfo>(route, Method.Post, request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task ChangePasswordAsync(ChangePasswordRequest request, CancellationToken cancellationToken = default)
|
||||
=> transport.SendAsync<object>("user/ChangePassword", Method.Post, request, cancellationToken: cancellationToken);
|
||||
|
||||
public Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id);
|
||||
@@ -31,6 +35,9 @@ public sealed class UserApi(ApiTransport transport)
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<UserInfo> UpdateBillingAsync(UserBillingUpdateRequest request, CancellationToken cancellationToken = default)
|
||||
=> transport.GetRequiredAsync<UserInfo>("user/UpdateUser", Method.Post, request, cancellationToken: cancellationToken);
|
||||
|
||||
public Task<UserInfo> UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Store.Management.Api.Users
|
||||
{
|
||||
public enum UserGroup : byte
|
||||
{
|
||||
/// <summary>主号。</summary>
|
||||
[Description("主号")] MainAccount = 3,
|
||||
/// <summary>客服。</summary>
|
||||
[Description("客服")] CustomerService = 4,
|
||||
/// <summary>加盟(旧系统分组 5)。</summary>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Store.Management.Api.Models;
|
||||
|
||||
namespace Store.Management.Api.Users;
|
||||
@@ -12,6 +13,10 @@ public sealed class UserSearchRequest : PageRequest
|
||||
|
||||
public class UserEditRequest
|
||||
{
|
||||
public UserGroup? GroupId { get; set; }
|
||||
public UserAdminLevel? SaLevel { get; set; }
|
||||
[StringLength(50)] public string? OemId { get; set; }
|
||||
[StringLength(1000)] public string? Authority { get; set; }
|
||||
[StringLength(50)] public string? NickName { get; set; }
|
||||
[StringLength(30)] public string? Mobile { get; set; }
|
||||
[StringLength(100)] public string? CompanyName { get; set; }
|
||||
@@ -25,11 +30,34 @@ public sealed class UserCreateRequest : UserEditRequest
|
||||
[StringLength(20)] public string? ParentUserName { get; set; }
|
||||
}
|
||||
|
||||
public sealed record ChangePasswordRequest(string UserName, string NewPassword, string? OldPassword = null);
|
||||
|
||||
public enum UserAdminLevel : short
|
||||
{
|
||||
[System.ComponentModel.Description("普通用户")] Normal = 0,
|
||||
[System.ComponentModel.Description("超级管理员")] SuperAdmin = 1
|
||||
}
|
||||
|
||||
public sealed class UserUpdateRequest : UserEditRequest
|
||||
{
|
||||
[Range(1, int.MaxValue)] public int Id { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserBillingUpdateRequest
|
||||
{
|
||||
[Range(1, int.MaxValue)] public int Id { get; init; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? ShopPointsPerMonth { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public decimal? PostagePerOrder { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public decimal? RefundPostagePerOrder { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public decimal? GiftOemAmountPerOrder { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public decimal? SyncLogisticsPerOrder { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>管理端当前使用的用户资料字段;不接收密码。</summary>
|
||||
public sealed class UserInfo
|
||||
{
|
||||
@@ -41,6 +69,16 @@ public sealed class UserInfo
|
||||
public string? Notes { get; init; }
|
||||
public string? ParentUserName { get; init; }
|
||||
public int GroupId { get; init; }
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public UserGroup? Group => GroupId is >= byte.MinValue and <= byte.MaxValue ? (UserGroup)GroupId : null;
|
||||
public UserAdminLevel? SaLevel { get; init; }
|
||||
public string? OemId { get; init; }
|
||||
public string? Authority { get; init; }
|
||||
public int ShopPointsPerMonth { get; init; }
|
||||
public decimal? PostagePerOrder { get; init; }
|
||||
public decimal? RefundPostagePerOrder { get; init; }
|
||||
public decimal? GiftOemAmountPerOrder { get; init; }
|
||||
public decimal? SyncLogisticsPerOrder { get; init; }
|
||||
public decimal Points { get; init; }
|
||||
public decimal? Balance { get; init; }
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public sealed class FolderConfig : IFolderConfig
|
||||
/// <summary>当前用户的数据根目录:%LOCALAPPDATA%\Store.Management。</summary>
|
||||
public static string AppFolder => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppName);
|
||||
|
||||
//, MAX.Core.Cryptography.MD5Helper.Md5_32(ExeFolder)
|
||||
public static string ConfigFolder => Path.Combine(AppFolder, "Config");
|
||||
public static string DataFolder => Path.Combine(AppFolder, "Data");
|
||||
public static string LoggingFolder => Path.Combine(AppFolder, "Logs");
|
||||
|
||||
@@ -602,7 +602,6 @@
|
||||
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}">
|
||||
@@ -992,7 +991,6 @@
|
||||
<CheckBox
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="选择行"
|
||||
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource DataGridExCheckBoxStyle}" />
|
||||
</Border>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Store.Management.Framework.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="DialogWindow"
|
||||
Title="{Binding Title}"
|
||||
Width="800"
|
||||
Height="450"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using MAX.Common.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Store.Management.Framework.Controls
|
||||
{
|
||||
internal class HorizontalIconText : Control
|
||||
{
|
||||
|
||||
public string Icon
|
||||
{
|
||||
get => (string)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
|
||||
nameof(Icon),
|
||||
typeof(string),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(null, OnOnIconChanged)
|
||||
);
|
||||
|
||||
public bool ShowIcon
|
||||
{
|
||||
get => (bool)GetValue(ShowIconProperty);
|
||||
private set => SetValue(ShowIconPropertyKey, value);
|
||||
}
|
||||
|
||||
private static readonly DependencyPropertyKey ShowIconPropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(ShowIcon),
|
||||
typeof(bool),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(false)
|
||||
);
|
||||
|
||||
public static readonly DependencyProperty ShowIconProperty = ShowIconPropertyKey.DependencyProperty;
|
||||
|
||||
|
||||
|
||||
|
||||
public bool ShowSpace
|
||||
{
|
||||
get => (bool)GetValue(ShowSpaceProperty);
|
||||
private set => SetValue(ShowSpacePropertyKey, value);
|
||||
}
|
||||
|
||||
private static readonly DependencyPropertyKey ShowSpacePropertyKey = DependencyProperty.RegisterReadOnly(
|
||||
nameof(ShowSpace),
|
||||
typeof(bool),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(true)
|
||||
);
|
||||
|
||||
public static readonly DependencyProperty ShowSpaceProperty = ShowSpacePropertyKey.DependencyProperty;
|
||||
|
||||
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
|
||||
nameof(Text),
|
||||
typeof(string),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(string.Empty)
|
||||
);
|
||||
|
||||
|
||||
|
||||
public double IconSize
|
||||
{
|
||||
get => (double)GetValue(IconSizeProperty);
|
||||
set => SetValue(IconSizeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconSizeProperty = DependencyProperty.Register(
|
||||
nameof(IconSize),
|
||||
typeof(double),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(14d)
|
||||
);
|
||||
|
||||
|
||||
|
||||
public double Spacing
|
||||
{
|
||||
get => (double)GetValue(SpacingProperty);
|
||||
set => SetValue(SpacingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register(
|
||||
nameof(Spacing),
|
||||
typeof(double),
|
||||
typeof(HorizontalIconText),
|
||||
new PropertyMetadata(4d)
|
||||
);
|
||||
|
||||
|
||||
private static void OnOnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is HorizontalIconText view)
|
||||
{
|
||||
view.ShowIcon = view.Icon.IsNotBlank();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
if (e.Property == ShowSpaceProperty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowSpace = ShowIcon && Text.IsNotBlank();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ internal static class ViewLifecycle
|
||||
if (ReferenceEquals(previous, next)) return;
|
||||
view.SetValue(LoadedViewModelProperty, next);
|
||||
previous?.OnUnloaded();
|
||||
next?.OnLoaded();
|
||||
next?.NotifyLoaded();
|
||||
}
|
||||
|
||||
private static void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -5,6 +5,20 @@ namespace Store.Management.Framework.Controls;
|
||||
|
||||
public abstract class ViewModelBase : BindableBase
|
||||
{
|
||||
private bool hasLoaded;
|
||||
|
||||
internal void NotifyLoaded()
|
||||
{
|
||||
if (!hasLoaded)
|
||||
{
|
||||
hasLoaded = true;
|
||||
OnLoadedOnce();
|
||||
}
|
||||
OnLoaded();
|
||||
}
|
||||
|
||||
/// <summary>当前实例首次加载时调用,先于 OnLoaded。</summary>
|
||||
protected virtual void OnLoadedOnce() { }
|
||||
protected internal virtual void OnLoaded() { }
|
||||
protected internal virtual void OnUnloaded() { }
|
||||
protected internal virtual void OnActivated() { }
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<ResourceDictionary
|
||||
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="clr-namespace:Store.Management.Framework.Controls"
|
||||
xmlns:wpf="MAX.WPF">
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
mc:Ignorable="d">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Fonts/FontGeometry.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MAX.Common.WPF;component/Resources/Generic.xaml" />
|
||||
@@ -37,6 +41,180 @@
|
||||
<Style BasedOn="{StaticResource WindowBase}" TargetType="f:WindowBase" />
|
||||
<Style BasedOn="{StaticResource DialogWindowStyle}" TargetType="f:DialogWindow" />
|
||||
|
||||
|
||||
<!--<Style x:Key="HorizontalSpacingPanel" TargetType="hc:UniformSpacingPanel">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="HorizontalSpacing" Value="2" />
|
||||
<Setter Property="ItemVerticalAlignment" Value="Center" />
|
||||
</Style>-->
|
||||
|
||||
<!--<Style x:Key="HorizontalSpacingPanel2" TargetType="hc:UniformSpacingPanel">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="HorizontalSpacing" Value="4" />
|
||||
<Setter Property="ItemVerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="VerticalSpacingPanel" TargetType="hc:UniformSpacingPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="VerticalSpacing" Value="2" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="VerticalSpacingPanel2" TargetType="hc:UniformSpacingPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="VerticalSpacing" Value="4" />
|
||||
</Style>-->
|
||||
|
||||
|
||||
<Style x:Key="HorizontalSpacingPanel" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="HorizontalSpacing" Value="2" />
|
||||
<Setter Property="ItemVerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HorizontalSpacingPanel4" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="HorizontalSpacing" Value="4" />
|
||||
<Setter Property="ItemVerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HorizontalSpacingPanel8" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="HorizontalSpacing" Value="8" />
|
||||
<Setter Property="ItemVerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
|
||||
<Style x:Key="VerticalSpacingPanel" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="VerticalSpacing" Value="2" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="VerticalSpacingPanel4" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="VerticalSpacing" Value="4" />
|
||||
</Style>
|
||||
<Style x:Key="VerticalSpacingPanel8" TargetType="wpf:SpacingStackPanel">
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
<Setter Property="VerticalSpacing" Value="8" />
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="HorizontalIconText"
|
||||
BasedOn="{StaticResource BaseStyle}"
|
||||
TargetType="f:HorizontalIconText">
|
||||
<Setter Property="Spacing" Value="4" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="IconSize" Value="16" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="f:HorizontalIconText">
|
||||
<Border>
|
||||
<DockPanel>
|
||||
<TextBlock
|
||||
FontSize="{TemplateBinding IconSize}"
|
||||
Style="{StaticResource TextBlockIcon}"
|
||||
Text="{TemplateBinding Icon}"
|
||||
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=ShowIcon, Converter={StaticResource Boolean2VisibilityConverter}}" />
|
||||
<Border Width="{TemplateBinding Spacing}" Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=ShowSpace, Converter={StaticResource Boolean2VisibilityConverter}}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{TemplateBinding Text}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource HorizontalIconText}" TargetType="f:HorizontalIconText" />
|
||||
|
||||
<Style x:Key="Dialog" TargetType="UserControl">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Focusable" Value="True" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="DialogButtonStyle"
|
||||
BasedOn="{StaticResource ButtonDefault}"
|
||||
TargetType="Button">
|
||||
<Setter Property="MinWidth" Value="70" />
|
||||
<Setter Property="Height" Value="26" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="DialogPrimaryButtonStyle"
|
||||
BasedOn="{StaticResource ButtonPrimary}"
|
||||
TargetType="Button">
|
||||
<Setter Property="MinWidth" Value="70" />
|
||||
<Setter Property="Height" Value="26" />
|
||||
</Style>
|
||||
|
||||
|
||||
|
||||
|
||||
<Style
|
||||
x:Key="ConfirmDialog"
|
||||
BasedOn="{StaticResource Dialog}"
|
||||
TargetType="UserControl">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="UserControl">
|
||||
<Border
|
||||
d:DataContext="{d:DesignInstance Type=f:ConfirmDialogViewModel}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="5,8"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid>
|
||||
<Border Padding="2,0">
|
||||
<f:HorizontalIconText
|
||||
d:Text="错误信息"
|
||||
Foreground="{StaticResource DangerBrush}"
|
||||
Icon="{StaticResource txt-warning}"
|
||||
Text="{Binding ErrorMsg}"
|
||||
Visibility="{Binding ErrorMsg, Converter={StaticResource String2VisibilityConverter}}" />
|
||||
</Border>
|
||||
<wpf:SpacingStackPanel HorizontalAlignment="Right" Style="{StaticResource HorizontalSpacingPanel4}">
|
||||
<Button Command="{Binding ConfirmCommand}" Style="{StaticResource DialogPrimaryButtonStyle}">
|
||||
<TextBlock Text="确认" />
|
||||
</Button>
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CancelCommand}"
|
||||
Style="{StaticResource DialogButtonStyle}">
|
||||
<TextBlock Text="取消" />
|
||||
</Button>
|
||||
</wpf:SpacingStackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
|
||||
|
||||
|
||||
<Style
|
||||
x:Key="VerticalAlignment"
|
||||
BasedOn="{StaticResource TitleContentBaseStyle}"
|
||||
|
||||
@@ -10,6 +10,8 @@ public interface IUserService
|
||||
Task<PageResult<UserInfo>> SearchAsync(UserSearchRequest request, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default);
|
||||
Task<UserInfo> UpdateBillingAsync(UserBillingUpdateRequest request, CancellationToken cancellationToken = default);
|
||||
Task ChangePasswordAsync(ChangePasswordRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -24,6 +26,12 @@ internal sealed class UserService(StoreApiClient client) : IUserService, ISingle
|
||||
public Task<UserInfo> UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default)
|
||||
=> client.Users.UpdateAsync(request, cancellationToken);
|
||||
|
||||
public Task<UserInfo> UpdateBillingAsync(UserBillingUpdateRequest request, CancellationToken cancellationToken = default)
|
||||
=> client.Users.UpdateBillingAsync(request, cancellationToken);
|
||||
|
||||
public Task ChangePasswordAsync(ChangePasswordRequest request, CancellationToken cancellationToken = default)
|
||||
=> client.Users.ChangePasswordAsync(request, cancellationToken);
|
||||
|
||||
public Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> client.Users.DeleteAsync(id, cancellationToken);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Store.Management.Notifications;
|
||||
using Store.Management.Services;
|
||||
using Store.Management.ViewModels;
|
||||
using Store.Management.Views;
|
||||
using Store.Management.Views.Dialogs;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
namespace Store.Management;
|
||||
@@ -76,6 +77,7 @@ public partial class App : FrameworkApplication
|
||||
{
|
||||
|
||||
containerRegistry.RegisterDialogWindow<Framework.Controls.DialogWindow>();
|
||||
containerRegistry.RegisterDialog<UserBillingDialog, UserBillingDialogViewModel>();
|
||||
containerRegistry.RegisterDialog<UserEditorDialog, UserEditorDialogViewModel>();
|
||||
|
||||
containerRegistry.RegisterDialog<DataGridTestView, DataGridTestViewModel>();
|
||||
|
||||
@@ -18,8 +18,40 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
|
||||
public const int CompanyLimit = 100;
|
||||
public const int NotesLimit = 255;
|
||||
|
||||
public const int AgentCodeLimit = 50;
|
||||
public sealed class PermissionOption(string name)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
|
||||
public PermissionOption[] Permissions { get; } =
|
||||
[
|
||||
new("微信回采"), new("货源选择"), new("评分考核"), new("爆款数据"),
|
||||
new("在线寄件"), new("佣金上货"), new("充值用户"), new("商品采集"),
|
||||
new("裂变调试"), new("学员数据"), new("定向品牌"), new("活动库存")
|
||||
];
|
||||
|
||||
private UserInfo? user;
|
||||
private string passwordHash = "";
|
||||
private string? passwordHash;
|
||||
private string? confirmationHash;
|
||||
private string? oldPasswordHash;
|
||||
public bool ShowOldPassword => !IsCreate && passwordHash != null;
|
||||
public string? ParentUserName { get; set; }
|
||||
public bool IsCreatingSubUser => IsCreate && Group != UserGroup.MainAccount;
|
||||
public UserGroup? Group
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref field, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsCreatingSubUser));
|
||||
}
|
||||
}
|
||||
}
|
||||
public UserAdminLevel Level { get; set => SetProperty(ref field, value); }
|
||||
public string AgentCode { get; set => SetProperty(ref field, value); } = "";
|
||||
public bool IsCreate => user == null;
|
||||
public string UserName { get; set => SetProperty(ref field, value); } = "";
|
||||
public UserEditRequest Draft { get; private set => SetProperty(ref field, value); } = new();
|
||||
@@ -33,14 +65,25 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
|
||||
Id = user.Id, NickName = user.NickName, Mobile = user.Mobile,
|
||||
CompanyName = user.CompanyName, Notes = user.Notes
|
||||
};
|
||||
Group = user == null ? UserGroup.MainAccount : (UserGroup)user.GroupId;
|
||||
Level = user?.SaLevel ?? UserAdminLevel.Normal;
|
||||
AgentCode = user?.OemId ?? "";
|
||||
var granted = (user?.Authority ?? "").Split(',', StringSplitOptions.TrimEntries);
|
||||
foreach (var option in Permissions)
|
||||
{
|
||||
option.IsSelected = granted.Contains(option.Name);
|
||||
}
|
||||
Title = IsCreate ? "创建用户" : "编辑用户";
|
||||
RaisePropertyChanged(nameof(IsCreate));
|
||||
}
|
||||
|
||||
public void UpdatePassword(string password)
|
||||
public void UpdatePasswords(string password, string confirmation, string oldPassword)
|
||||
{
|
||||
passwordHash = password.Length >= 6 && !string.IsNullOrWhiteSpace(password)
|
||||
passwordHash = password.Length == 0 ? null : password.Length >= 6 && !string.IsNullOrWhiteSpace(password)
|
||||
? LoginViewModel.HashPassword(password) : "";
|
||||
oldPasswordHash = oldPassword.Length == 0 ? null : LoginViewModel.HashPassword(oldPassword);
|
||||
RaisePropertyChanged(nameof(ShowOldPassword));
|
||||
confirmationHash = confirmation.Length == 0 ? null : LoginViewModel.HashPassword(confirmation);
|
||||
}
|
||||
|
||||
public override bool CanCloseDialog() => !IsLoading;
|
||||
@@ -48,19 +91,53 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Draft.NickName) || string.IsNullOrWhiteSpace(Draft.Mobile))
|
||||
{
|
||||
ErrorMsg = "请输入昵称和手机号。";
|
||||
return;
|
||||
}
|
||||
if (Draft.Mobile.Trim().Length != 11 || !Draft.Mobile.Trim().All(char.IsAsciiDigit))
|
||||
{
|
||||
ErrorMsg = "手机号应为 11 位数字。";
|
||||
return;
|
||||
}
|
||||
if (passwordHash == "" || (IsCreate && passwordHash == null))
|
||||
{
|
||||
ErrorMsg = "请输入至少 6 位密码。";
|
||||
return;
|
||||
}
|
||||
if (passwordHash != confirmationHash)
|
||||
{
|
||||
ErrorMsg = "两次输入的密码不一致。";
|
||||
return;
|
||||
}
|
||||
if (Group == null || (user != null && Group != (UserGroup)user.GroupId &&
|
||||
(Group == UserGroup.MainAccount || user.GroupId == (int)UserGroup.MainAccount)))
|
||||
{
|
||||
ErrorMsg = "请选择有效分组,已有账户不能切换主号和子账号。";
|
||||
return;
|
||||
}
|
||||
Draft.GroupId = user?.GroupId == (int?)Group ? null : Group;
|
||||
Draft.SaLevel = Level == (user?.SaLevel ?? UserAdminLevel.Normal) ? null : Level;
|
||||
Draft.OemId = AgentCode.Trim() == (user?.OemId ?? "") ? null : AgentCode.Trim();
|
||||
var granted = (user?.Authority ?? "").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToHashSet();
|
||||
bool changed = false;
|
||||
foreach (var option in Permissions)
|
||||
{
|
||||
changed |= option.IsSelected ? granted.Add(option.Name) : granted.Remove(option.Name);
|
||||
}
|
||||
Draft.Authority = changed ? string.Join(",", granted) : null;
|
||||
|
||||
if (Draft is UserCreateRequest create)
|
||||
{
|
||||
create.UserName = UserName.Trim();
|
||||
if (create.UserName.Length == 0 || passwordHash.Length == 0)
|
||||
{
|
||||
ErrorMsg = "请输入用户名和至少 6 位的初始密码。";
|
||||
return;
|
||||
}
|
||||
create.Password = passwordHash;
|
||||
create.ParentUserName = IsCreatingSubUser ? ParentUserName : null;
|
||||
create.UserName = Draft.Mobile.Trim();
|
||||
create.Password = passwordHash!;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
ErrorMsg = "";
|
||||
bool profileSaved = false;
|
||||
try
|
||||
{
|
||||
if (Draft is UserCreateRequest newUser)
|
||||
@@ -69,12 +146,17 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
|
||||
}
|
||||
else
|
||||
{
|
||||
await userService.UpdateAsync((UserUpdateRequest)Draft);
|
||||
user = await userService.UpdateAsync((UserUpdateRequest)Draft);
|
||||
profileSaved = true;
|
||||
if (passwordHash != null)
|
||||
{
|
||||
await userService.ChangePasswordAsync(new ChangePasswordRequest(UserName, passwordHash, oldPasswordHash));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
ErrorMsg = ex.Message;
|
||||
ErrorMsg = profileSaved ? $"资料已保存,密码修改失败:{ex.Message}" : ex.Message;
|
||||
return;
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
@@ -91,7 +173,7 @@ public sealed class UserEditorDialogViewModel(IUserService userService) : Confir
|
||||
|
||||
public override void OnDialogClosed()
|
||||
{
|
||||
passwordHash = "";
|
||||
passwordHash = confirmationHash = oldPasswordHash = null;
|
||||
if (Draft is UserCreateRequest create)
|
||||
{
|
||||
create.Password = "";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Regions;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Api.Models;
|
||||
using Store.Management.Api.Transport;
|
||||
@@ -7,6 +8,7 @@ using Store.Management.Api.Users;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.Framework.Services;
|
||||
using Store.Management.Views;
|
||||
using Store.Management.Views.Dialogs;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
@@ -14,122 +16,124 @@ namespace Store.Management.ViewModels;
|
||||
|
||||
public sealed class UserManagementPageViewModel(IUserService userService, IDialogService dialogs, IStatusBarService statusBar) : ViewModelBase
|
||||
{
|
||||
public const int KeywordLimit = 100;
|
||||
private string appliedKeyword = "";
|
||||
private int pageSize = 50;
|
||||
public const int KeywordLimit = 100;
|
||||
private string appliedKeyword = "";
|
||||
private UserGroup? appliedGroup;
|
||||
private int pageSize = 50;
|
||||
|
||||
public string Keyword { get; set; } = "";
|
||||
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); } = [];
|
||||
public UserInfo? SelectedUser { get; set => SetProperty(ref field, value); }
|
||||
public string Keyword { get; set; } = "";
|
||||
public UserGroup? SelectedGroup { get; set; }
|
||||
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); } = [];
|
||||
public UserInfo? SelectedUser { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public AsyncDelegateCommand SearchCommand => field ??= new(() => LoadAsync(Keyword.Trim(), 1, pageSize));
|
||||
public AsyncDelegateCommand<DataGridPageRequest> PageCommand => field ??= new(
|
||||
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);
|
||||
public AsyncDelegateCommand DeleteCommand => field ??= new AsyncDelegateCommand(DeleteAsync,
|
||||
() => SelectedUser != null).ObservesProperty(() => SelectedUser);
|
||||
|
||||
public AsyncDelegateCommand LoginCommand => field ??= new(LoginAsync);
|
||||
|
||||
protected override void OnLoaded()
|
||||
public AsyncDelegateCommand SearchCommand => field ??= new(() => LoadAsync(Keyword.Trim(), SelectedGroup, 1, pageSize));
|
||||
public AsyncDelegateCommand<DataGridPageRequest> PageCommand => field ??= new(
|
||||
request => LoadAsync(appliedKeyword, appliedGroup, request.PageIndex, request.PageSize), _ => CanInteract);
|
||||
public AsyncDelegateCommand CreateCommand => field ??= new(() => OpenEditorAsync(null));
|
||||
public AsyncDelegateCommand EditCommand => field ??= new AsyncDelegateCommand(() => OpenEditorAsync(SelectedUser),
|
||||
() => SelectedUser != null).ObservesProperty(() => SelectedUser);
|
||||
public AsyncDelegateCommand BillingCommand => field ??= new AsyncDelegateCommand(async () =>
|
||||
{
|
||||
base.OnLoaded();
|
||||
ButtonResult result = ButtonResult.None;
|
||||
dialogs.ShowDialog(nameof(UserBillingDialog), SelectedUser!, response => result = response.Result);
|
||||
if (result == ButtonResult.OK)
|
||||
{
|
||||
await LoadAsync(appliedKeyword, appliedGroup, Page, pageSize);
|
||||
}
|
||||
}, () => SelectedUser != null).ObservesProperty(() => SelectedUser);
|
||||
public AsyncDelegateCommand DeleteCommand => field ??= new AsyncDelegateCommand(DeleteAsync,
|
||||
() => SelectedUser != null).ObservesProperty(() => SelectedUser);
|
||||
|
||||
protected override void OnLoadedOnce()
|
||||
{
|
||||
_ = LoadAsync(Keyword.Trim(), SelectedGroup, 1, pageSize);
|
||||
}
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
ButtonResult result = ButtonResult.None;
|
||||
dialogs.ShowDialog(nameof(LoginWindow), new DialogParameters(), response => result = response.Result);
|
||||
if (result == ButtonResult.OK)
|
||||
{
|
||||
await LoadAsync(Keyword.Trim(), 1, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync(string keyword, int page, int size)
|
||||
{
|
||||
if (!CanInteract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CanInteract = false;
|
||||
try
|
||||
{
|
||||
statusBar.Report("正在查询用户…");
|
||||
UserSearchRequest request = new() { Keyword = keyword, Page = page, PageSize = size };
|
||||
PageResult<UserInfo> result = await userService.SearchAsync(request);
|
||||
// 删除末页最后一条记录后,按服务端总数回到有效页。
|
||||
int lastPage = (int)Math.Clamp((result.Total - 1) / size + 1, 1, page);
|
||||
if (page > lastPage)
|
||||
{
|
||||
request.Page = lastPage;
|
||||
result = await userService.SearchAsync(request);
|
||||
}
|
||||
appliedKeyword = keyword;
|
||||
pageSize = size;
|
||||
SelectedUser = null;
|
||||
Users = result.Items;
|
||||
Total = (int)Math.Min(result.Total, int.MaxValue);
|
||||
Page = request.Page;
|
||||
private async Task LoadAsync(string keyword, UserGroup? group, int page, int size)
|
||||
{
|
||||
if (!CanInteract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CanInteract = false;
|
||||
try
|
||||
{
|
||||
statusBar.Report("正在查询用户…");
|
||||
UserSearchRequest request = new() { Keyword = keyword, GroupId = (int?)group, Page = page, PageSize = size };
|
||||
PageResult<UserInfo> result = await userService.SearchAsync(request);
|
||||
// 删除末页最后一条记录后,按服务端总数回到有效页。
|
||||
int lastPage = (int)Math.Clamp((result.Total - 1) / size + 1, 1, page);
|
||||
if (page > lastPage)
|
||||
{
|
||||
request.Page = lastPage;
|
||||
result = await userService.SearchAsync(request);
|
||||
}
|
||||
appliedKeyword = keyword;
|
||||
appliedGroup = group;
|
||||
pageSize = size;
|
||||
SelectedUser = null;
|
||||
Users = result.Items;
|
||||
Total = (int)Math.Min(result.Total, int.MaxValue);
|
||||
Page = request.Page;
|
||||
|
||||
statusBar.Report($"用户查询完成,共 {Total} 条。", StatusLevel.Success,100);
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
SelectedUser = null;
|
||||
Users = [];
|
||||
Total = 0;
|
||||
Page = 1;
|
||||
appliedKeyword = keyword;
|
||||
pageSize = size;
|
||||
statusBar.Report(ex.Message, StatusLevel.Error);
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
CanInteract = true;
|
||||
}
|
||||
}
|
||||
statusBar.Report($"用户查询完成,共 {Total} 条。", StatusLevel.Success, 100);
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
SelectedUser = null;
|
||||
Users = [];
|
||||
Total = 0;
|
||||
Page = 1;
|
||||
appliedKeyword = keyword;
|
||||
appliedGroup = group;
|
||||
pageSize = size;
|
||||
statusBar.Report(ex.Message, StatusLevel.Error);
|
||||
}
|
||||
|
||||
private async Task OpenEditorAsync(UserInfo? user)
|
||||
{
|
||||
ButtonResult result = ButtonResult.None;
|
||||
dialogs.ShowDialog(nameof(UserEditorDialog), new DialogParameters { { "user", user } },
|
||||
response => result = response.Result);
|
||||
if (result == ButtonResult.OK)
|
||||
{
|
||||
await LoadAsync(appliedKeyword, user == null ? 1 : Page, pageSize);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CanInteract = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
UserInfo user = SelectedUser!;
|
||||
if (MessageBox.Show($"确定删除用户“{user.UserName}”?", "删除用户", MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CanInteract = false;
|
||||
try
|
||||
{
|
||||
statusBar.Report("正在删除用户…");
|
||||
await userService.DeleteAsync(user.Id);
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
statusBar.Report(ex.Message, StatusLevel.Error);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CanInteract = true;
|
||||
}
|
||||
await LoadAsync(appliedKeyword, Page, pageSize);
|
||||
}
|
||||
private async Task OpenEditorAsync(UserInfo? user)
|
||||
{
|
||||
ButtonResult result = ButtonResult.None;
|
||||
dialogs.ShowDialog(nameof(UserEditorDialog), new DialogParameters { { "user", user } },
|
||||
response => result = response.Result);
|
||||
if (result == ButtonResult.OK)
|
||||
{
|
||||
await LoadAsync(appliedKeyword, appliedGroup, user == null ? 1 : Page, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
UserInfo user = SelectedUser!;
|
||||
if (MessageBox.Show($"确定删除用户“{user.UserName}”?", "删除用户", MessageBoxButton.OKCancel,
|
||||
MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CanInteract = false;
|
||||
try
|
||||
{
|
||||
statusBar.Report("正在删除用户…");
|
||||
await userService.DeleteAsync(user.Id);
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
statusBar.Report(ex.Message, StatusLevel.Error);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
CanInteract = true;
|
||||
}
|
||||
await LoadAsync(appliedKeyword, appliedGroup, Page, pageSize);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@
|
||||
Text="当前页实时搜索" />
|
||||
<TextBox
|
||||
Width="220"
|
||||
AutomationProperties.Name="当前页实时搜索"
|
||||
MaxLength="{x:Static vm:DataGridTestViewModel.SearchTextLimit}"
|
||||
Text="{Binding LocalKeyword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="输入即过滤当前页;清空恢复本页数据,不改变分页总数" />
|
||||
@@ -93,7 +92,6 @@
|
||||
Text="后台搜索(模拟)" />
|
||||
<TextBox
|
||||
Width="220"
|
||||
AutomationProperties.Name="后台搜索关键字"
|
||||
MaxLength="{x:Static vm:DataGridTestViewModel.SearchTextLimit}"
|
||||
Text="{Binding BackendKeyword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="按名称、ID、分组、子账号、手机号、状态或备注搜索全部样例" />
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<UserControl
|
||||
x:Class="Store.Management.Views.Dialogs.UserBillingDialog"
|
||||
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:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
Width="460"
|
||||
Height="310"
|
||||
d:Background="White"
|
||||
Style="{StaticResource ConfirmDialog}"
|
||||
mc:Ignorable="d">
|
||||
<ItemsControl
|
||||
d:ItemsSource="{d:SampleData ItemCount=5}"
|
||||
IsEnabled="{Binding EnableConfirm, Mode=OneWay}"
|
||||
ItemsSource="{Binding Fields, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<wpf:SpacingStackPanel Style="{StaticResource VerticalSpacingPanel4}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<wpf:TitleContent
|
||||
Title="{Binding Label, Mode=OneWay}"
|
||||
Placement="Left"
|
||||
TitleVerticalAlignment="Center"
|
||||
TitleWidth="200">
|
||||
<hc:NumericUpDown
|
||||
DecimalPlaces="{Binding DecimalPlaces, Mode=OneWay}"
|
||||
Increment="{Binding Increment, Mode=OneWay}"
|
||||
Maximum="{Binding Maximum, Mode=OneWay}"
|
||||
Minimum="{Binding Minimum, Mode=OneWay}"
|
||||
TextBoxBase.TextChanged="OnAmountTextChanged"
|
||||
ValueChanged="OnAmountValueChanged"
|
||||
Value="{Binding Value, Mode=OneWay}" />
|
||||
</wpf:TitleContent>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,127 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Api.Transport;
|
||||
using Store.Management.Api.Users;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.Framework.Services;
|
||||
using NumericUpDown = HandyControl.Controls.NumericUpDown;
|
||||
using System.Globalization;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Store.Management.Views.Dialogs;
|
||||
|
||||
public partial class UserBillingDialog : UserControl
|
||||
{
|
||||
public UserBillingDialog() => InitializeComponent();
|
||||
|
||||
private void OnAmountValueChanged(object sender, HandyControl.Data.FunctionEventArgs<double> e)
|
||||
{
|
||||
if (sender is NumericUpDown { IsKeyboardFocusWithin: true, DataContext: UserBillingDialogViewModel.BillingField field })
|
||||
{
|
||||
field.Value = (decimal)e.Info;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAmountTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
// 显式输入与控件初始值相同的金额时,Value 不会触发变化。
|
||||
if (sender is NumericUpDown { IsKeyboardFocusWithin: true, DataContext: UserBillingDialogViewModel.BillingField field }
|
||||
&& e.OriginalSource is TextBox input
|
||||
&& decimal.TryParse(input.Text, NumberStyles.Number, CultureInfo.CurrentCulture, out decimal amount)
|
||||
&& amount == field.Value)
|
||||
{
|
||||
field.Value = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UserBillingDialogViewModel(IUserService userService) : ConfirmDialogViewModel<UserInfo>
|
||||
{
|
||||
public sealed class BillingField(string label, decimal? original, decimal minimum,
|
||||
Action<UserBillingUpdateRequest, decimal> apply, bool integer = false)
|
||||
{
|
||||
private readonly decimal? originalValue = original;
|
||||
public string Label { get; } = label;
|
||||
public decimal? EnteredValue { get; private set; } = original;
|
||||
public decimal Value
|
||||
{
|
||||
get => EnteredValue ?? minimum;
|
||||
set => EnteredValue = value;
|
||||
}
|
||||
public decimal Minimum => Math.Min(originalValue ?? minimum, minimum);
|
||||
public decimal Maximum => Math.Max(originalValue ?? minimum, integer ? int.MaxValue : 999999m);
|
||||
public int DecimalPlaces => integer ? 0 : 2;
|
||||
public decimal Increment => integer ? 1m : 0.01m;
|
||||
|
||||
internal bool ApplyChange(UserBillingUpdateRequest request)
|
||||
{
|
||||
if (EnteredValue is not decimal value || value == originalValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
decimal maximum = integer ? int.MaxValue : 999999m;
|
||||
if (value < minimum || value > maximum || decimal.Round(value, DecimalPlaces) != value)
|
||||
{
|
||||
throw new FormatException($"{Label}范围为 {minimum} 至 {maximum}" + (integer ? ",请输入整数。" : ",最多两位小数。"));
|
||||
}
|
||||
apply(request, value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public BillingField[] Fields { get; private set => SetProperty(ref field, value); } = [];
|
||||
|
||||
public override void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
base.OnDialogOpened(parameters);
|
||||
Title = $"计费配置 · {Parameter.UserName}";
|
||||
Fields =
|
||||
[
|
||||
new("店铺每月续费积分(积分/月)", Parameter.ShopPointsPerMonth, 30m, (request, value) => request.ShopPointsPerMonth = (int)value, integer: true),
|
||||
new("中转仓快递费(元/单)", Parameter.PostagePerOrder, 2.20m, (request, value) => request.PostagePerOrder = value),
|
||||
new("中转仓退货快递费(元/单)", Parameter.RefundPostagePerOrder, 2.50m, (request, value) => request.RefundPostagePerOrder = value),
|
||||
new("礼物单附加费(元/单)", Parameter.GiftOemAmountPerOrder, 0m, (request, value) => request.GiftOemAmountPerOrder = value),
|
||||
new("物流同步费(元/单)", Parameter.SyncLogisticsPerOrder, 0m, (request, value) => request.SyncLogisticsPerOrder = value)
|
||||
];
|
||||
}
|
||||
|
||||
public override bool CanCloseDialog() => !IsLoading;
|
||||
public override ICommand ConfirmCommand => field ??= new AsyncDelegateCommand(SaveAsync);
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
var request = new UserBillingUpdateRequest { Id = Parameter.Id };
|
||||
try
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (BillingField field in Fields)
|
||||
{
|
||||
changed |= field.ApplyChange(request);
|
||||
}
|
||||
if (!changed)
|
||||
{
|
||||
OnRequestClose(new DialogResult(ButtonResult.Cancel));
|
||||
return;
|
||||
}
|
||||
ErrorMsg = "";
|
||||
IsLoading = true;
|
||||
await userService.UpdateBillingAsync(request);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
ErrorMsg = ex.Message;
|
||||
return;
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
ErrorMsg = ex.Message;
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
OnRequestClose(new DialogResult(ButtonResult.OK));
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@
|
||||
TitleWidth="50">
|
||||
<TextBox
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="用户名"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
Text="{Binding UserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
@@ -72,7 +71,6 @@
|
||||
<PasswordBox
|
||||
x:Name="PasswordBox"
|
||||
VerticalContentAlignment="Center"
|
||||
AutomationProperties.Name="密码"
|
||||
IsEnabled="{Binding CanEdit, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:LoginViewModel.InputMaxLength}"
|
||||
PasswordChanged="OnPasswordChanged" />
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
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>
|
||||
|
||||
@@ -1,53 +1,99 @@
|
||||
<UserControl x:Class="Store.Management.Views.UserEditorDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpf="MAX.WPF"
|
||||
xmlns:vm="clr-namespace:Store.Management.ViewModels" Width="480" Height="460">
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="ShowWhenTrue"/>
|
||||
<Style TargetType="wpf:TitleContent" BasedOn="{StaticResource {x:Type wpf:TitleContent}}">
|
||||
<Setter Property="Placement" Value="Left"/>
|
||||
<Setter Property="TitleWidth" Value="80"/>
|
||||
<Setter Property="TitleVerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<DockPanel Margin="24" IsEnabled="{Binding EnableConfirm, Mode=OneWay}">
|
||||
<!-- 保存结果与操作 -->
|
||||
<wpf:SpacingStackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" HorizontalSpacing="8">
|
||||
<Button Content="取消" Command="{Binding CancelCommand}"/>
|
||||
<Button Content="保存" Command="{Binding ConfirmCommand}"/>
|
||||
</wpf:SpacingStackPanel>
|
||||
<!-- 用户资料 -->
|
||||
<wpf:SpacingStackPanel VerticalSpacing="10" Orientation="Vertical">
|
||||
<wpf:TitleContent Title="用户名">
|
||||
<TextBox AutomationProperties.Name="用户名" IsEnabled="{Binding IsCreate, Mode=OneWay}"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.UserNameLimit}"
|
||||
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="初始密码" Visibility="{Binding IsCreate, Mode=OneWay, Converter={StaticResource ShowWhenTrue}}">
|
||||
<PasswordBox AutomationProperties.Name="初始密码" MaxLength="{x:Static vm:UserEditorDialogViewModel.PasswordLimit}"
|
||||
PasswordChanged="OnPasswordChanged"/>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="昵称">
|
||||
<TextBox AutomationProperties.Name="昵称"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.NickNameLimit}"
|
||||
Text="{Binding Draft.NickName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="手机号">
|
||||
<TextBox AutomationProperties.Name="手机号"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.MobileLimit}"
|
||||
Text="{Binding Draft.Mobile, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="公司名称">
|
||||
<TextBox AutomationProperties.Name="公司名称"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.CompanyLimit}"
|
||||
Text="{Binding Draft.CompanyName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="备注">
|
||||
<TextBox AutomationProperties.Name="备注"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.NotesLimit}"
|
||||
Text="{Binding Draft.Notes, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</wpf:TitleContent>
|
||||
<TextBlock Text="{Binding ErrorMsg, Mode=OneWay}" Foreground="{DynamicResource DangerBrush}" TextWrapping="Wrap"/>
|
||||
</wpf:SpacingStackPanel>
|
||||
</DockPanel>
|
||||
<UserControl
|
||||
x:Class="Store.Management.Views.UserEditorDialog"
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:users="clr-namespace:Store.Management.Api.Users;assembly=Store.Management.Api"
|
||||
xmlns:vm="clr-namespace:Store.Management.ViewModels"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
Width="500"
|
||||
Height="620"
|
||||
Style="{StaticResource ConfirmDialog}"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="ShowWhenTrue" />
|
||||
<Style BasedOn="{StaticResource {x:Type wpf:TitleContent}}" TargetType="wpf:TitleContent">
|
||||
<Setter Property="Placement" Value="Left" />
|
||||
<Setter Property="TitleWidth" Value="80" />
|
||||
<Setter Property="TitleVerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer IsEnabled="{Binding EnableConfirm, Mode=OneWay}" VerticalScrollBarVisibility="Auto">
|
||||
<wpf:SpacingStackPanel Orientation="Vertical" VerticalSpacing="8">
|
||||
<wpf:TitleContent Title="昵称">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.NickNameLimit}" Text="{Binding Draft.NickName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="手机号">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.MobileLimit}" Text="{Binding Draft.Mobile, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="密码">
|
||||
<PasswordBox
|
||||
x:Name="NewPassword"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.PasswordLimit}"
|
||||
PasswordChanged="OnPasswordChanged" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="重复密码">
|
||||
<PasswordBox
|
||||
x:Name="Confirmation"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.PasswordLimit}"
|
||||
PasswordChanged="OnPasswordChanged" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="原密码" Visibility="{Binding ShowOldPassword, Mode=OneWay, Converter={StaticResource ShowWhenTrue}}">
|
||||
<PasswordBox
|
||||
x:Name="OldPassword"
|
||||
MaxLength="{x:Static vm:UserEditorDialogViewModel.PasswordLimit}"
|
||||
PasswordChanged="OnPasswordChanged"
|
||||
ToolTip="普通用户修改自己的密码时填写" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="分组">
|
||||
<ComboBox
|
||||
d:ItemsSource="{d:SampleData ItemCount=2}"
|
||||
DisplayMemberPath="Display"
|
||||
ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type users:UserGroup},
|
||||
Converter={StaticResource EnumDisplayConverter}}"
|
||||
SelectedValue="{Binding Group, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Value" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="父账号" Visibility="{Binding IsCreatingSubUser, Mode=OneWay, Converter={StaticResource ShowWhenTrue}}">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.UserNameLimit}" Text="{Binding ParentUserName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="公司名称">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.CompanyLimit}" Text="{Binding Draft.CompanyName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="代理编码">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.AgentCodeLimit}" Text="{Binding AgentCode, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="超管等级">
|
||||
<ComboBox
|
||||
d:ItemsSource="{d:SampleData ItemCount=2}"
|
||||
DisplayMemberPath="Display"
|
||||
ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type users:UserAdminLevel},
|
||||
Converter={StaticResource EnumDisplayConverter}}"
|
||||
SelectedValue="{Binding Level, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Value" />
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="权限">
|
||||
<ItemsControl ItemsSource="{Binding Permissions, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Margin="0,2"
|
||||
Content="{Binding Name, Mode=OneWay}"
|
||||
IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="备注">
|
||||
<TextBox MaxLength="{x:Static vm:UserEditorDialogViewModel.NotesLimit}" Text="{Binding Draft.Notes, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</wpf:TitleContent>
|
||||
<TextBlock Text="新增时使用手机号登录。编辑时密码留空即保留原密码。" TextWrapping="Wrap" />
|
||||
</wpf:SpacingStackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -10,6 +10,6 @@ public partial class UserEditorDialog : UserControl
|
||||
|
||||
private void OnPasswordChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((UserEditorDialogViewModel)DataContext).UpdatePassword(((PasswordBox)sender).Password);
|
||||
((UserEditorDialogViewModel)DataContext).UpdatePasswords(NewPassword.Password, Confirmation.Password, OldPassword.Password);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:users="clr-namespace:Store.Management.Api.Users;assembly=Store.Management.Api"
|
||||
xmlns:vm="clr-namespace:Store.Management.ViewModels"
|
||||
xmlns:wpf="MAX.WPF"
|
||||
Margin="5"
|
||||
@@ -13,9 +15,19 @@
|
||||
d:DesignHeight="760"
|
||||
d:DesignWidth="1280"
|
||||
Background="White"
|
||||
Loaded="OnLoaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<wpf:BindingProxy x:Key="PageViewModel" Data="{Binding Mode=OneWay}" />
|
||||
<ContextMenu x:Key="UserRowMenu" DataContext="{Binding Data, Source={StaticResource PageViewModel}, Mode=OneWay}">
|
||||
<MenuItem Command="{Binding EditCommand}" Header="编辑" />
|
||||
<MenuItem Command="{Binding BillingCommand}" Header="计费配置" />
|
||||
<MenuItem Command="{Binding DeleteCommand}" Header="删除" />
|
||||
<Separator />
|
||||
<MenuItem Command="{Binding CreateCommand}" Header="创建用户" />
|
||||
</ContextMenu>
|
||||
</UserControl.Resources>
|
||||
|
||||
<DockPanel IsEnabled="{Binding CanInteract}">
|
||||
|
||||
<!-- 查询及用户操作 -->
|
||||
@@ -30,14 +42,28 @@
|
||||
<wpf:TitleContent Title="关键词" Style="{StaticResource VerticalAlignment}">
|
||||
<TextBox
|
||||
Width="175"
|
||||
AutomationProperties.Name="用户查询关键词"
|
||||
hc:InfoElement.Placeholder="用户名,昵称,公司"
|
||||
MaxLength="{x:Static vm:UserManagementPageViewModel.KeywordLimit}"
|
||||
Style="{StaticResource TextBoxExtend}"
|
||||
Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SearchCommand}" />
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
</wpf:TitleContent>
|
||||
<wpf:TitleContent Title="用户组" Style="{StaticResource VerticalAlignment}">
|
||||
<ComboBox
|
||||
x:Name="GroupFilter"
|
||||
Width="120"
|
||||
d:ItemsSource="{d:SampleData ItemCount=8}"
|
||||
DisplayMemberPath="Display"
|
||||
ItemsSource="{wpf:EnumBindingMarkup EnumType={x:Type users:UserGroup},
|
||||
AllowNull=True,
|
||||
NullDisplay=全部,
|
||||
Converter={StaticResource EnumDisplayConverter}}"
|
||||
SelectedValue="{Binding SelectedGroup, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Value" />
|
||||
</wpf:TitleContent>
|
||||
<Button
|
||||
Command="{Binding SearchCommand}"
|
||||
Content="查询"
|
||||
@@ -50,14 +76,18 @@
|
||||
|
||||
<!-- 用户列表与服务端分页 -->
|
||||
<f:DataGridEx
|
||||
d:ItemsSource="{d:SampleData ItemCount=8}"
|
||||
AutoGenerateColumns="False"
|
||||
BorderThickness="1"
|
||||
CanUserSortColumns="False"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding Users, Mode=OneWay}"
|
||||
NoDataTips="没有符合条件的用户"
|
||||
PageChangedCommand="{Binding PageCommand}"
|
||||
PageSize="50"
|
||||
RowContextMenu="{StaticResource UserRowMenu}"
|
||||
SelectRowOnRightClick="True"
|
||||
SelectedItem="{Binding SelectedUser, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectionMode="Single"
|
||||
ShowCheckBox="False"
|
||||
@@ -68,22 +98,47 @@
|
||||
<f:DataGridEx.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
MinWidth="140"
|
||||
Binding="{Binding UserName}"
|
||||
Header="用户名" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
MinWidth="120"
|
||||
Binding="{Binding NickName}"
|
||||
Header="昵称" />
|
||||
<DataGridTextColumn
|
||||
Width="125"
|
||||
MinWidth="125"
|
||||
Binding="{Binding Mobile}"
|
||||
Header="手机号" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
MinWidth="180"
|
||||
Binding="{Binding CompanyName}"
|
||||
Header="公司名称" />
|
||||
<DataGridTextColumn
|
||||
Width="90"
|
||||
MinWidth="90"
|
||||
Binding="{Binding Group, Mode=OneWay, Converter={StaticResource EnumDisplayConverter}}"
|
||||
Header="分组" />
|
||||
<DataGridTextColumn
|
||||
Width="110"
|
||||
MinWidth="110"
|
||||
Binding="{Binding OemId, Mode=OneWay}"
|
||||
Header="代理编码" />
|
||||
<DataGridTextColumn
|
||||
Width="100"
|
||||
MinWidth="100"
|
||||
Binding="{Binding SaLevel, Mode=OneWay, Converter={StaticResource EnumDisplayConverter}}"
|
||||
Header="超管等级" />
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
MinWidth="220"
|
||||
Binding="{Binding Authority, Mode=OneWay}"
|
||||
Header="权限" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
MinWidth="160"
|
||||
Binding="{Binding Notes}"
|
||||
Header="备注" />
|
||||
</f:DataGridEx.Columns>
|
||||
|
||||
@@ -8,9 +8,4 @@ namespace Store.Management.Views;
|
||||
public partial class UserManagementPage : UserControl
|
||||
{
|
||||
public UserManagementPage() => InitializeComponent();
|
||||
|
||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//await ((UserManagementPageViewModel)DataContext).SearchCommand.Execute();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Store.Management.Views;
|
||||
using Store.Management.ViewModels;
|
||||
using Store.Management.Framework.Controls;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Api.Users;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
@@ -75,10 +76,10 @@ class Program
|
||||
Check(pageVm.Unloads == 1, "移出页面触发卸载");
|
||||
window.Content = page;
|
||||
app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
|
||||
Check(pageVm.Loads == 2, "重新加入页面再次加载");
|
||||
Check(pageVm.Loads == 2 && pageVm.FirstLoads == 1, "重新加入页面再次加载");
|
||||
var replacement = new LifecycleProbe();
|
||||
page.DataContext = replacement;
|
||||
Check(pageVm.Unloads == 2 && replacement.Loads == 1, "已加载的页面更换 ViewModel 后转移通知");
|
||||
Check(pageVm.Unloads == 2 && replacement.Loads == 1 && replacement.FirstLoads == 1, "已加载的页面更换 ViewModel 后转移通知");
|
||||
windowVm.CancelClose = true;
|
||||
window.Close();
|
||||
Check(window.IsVisible && windowVm.Closes == 0, "ViewModel 可以取消关闭");
|
||||
@@ -142,12 +143,120 @@ class Program
|
||||
await usersVm.SearchCommand.Execute();
|
||||
Check(status.Current.Level == StatusLevel.Error && usersVm.CanInteract, "查询失败后显示错误并允许重试");
|
||||
await usersClient.Auth.LoginAsync(new() { UserName = "admin", Password = "320BB1D43EC042096A83DBAC6DC9294A" });
|
||||
var usersPage = new UserManagementPage { DataContext = usersVm };
|
||||
var grid = ((DockPanel)usersPage.Content).Children.OfType<DataGridEx>().Single();
|
||||
var sample = new UserInfo { GroupId = 4, SaLevel = UserAdminLevel.SuperAdmin, OemId = "A01", Authority = "在线寄件,商品采集" };
|
||||
foreach (var (header, expected) in new[] { ("分组", "客服"), ("代理编码", "A01"), ("超管等级", "超级管理员"), ("权限", "在线寄件,商品采集") })
|
||||
{
|
||||
var column = grid.Columns.OfType<DataGridTextColumn>().Single(c => Equals(c.Header, header));
|
||||
var cell = new TextBlock { DataContext = sample };
|
||||
cell.SetBinding(TextBlock.TextProperty, column.Binding);
|
||||
Check(cell.Text == expected, $"用户列表{header}显示错误:{cell.Text}");
|
||||
cell.DataContext = new UserInfo { GroupId = 5 };
|
||||
Check(cell.Text == (header == "分组" ? "5" : ""), $"用户列表{header}历史数据或空值显示错误");
|
||||
}
|
||||
var groupFilter = LogicalTreeHelper.FindLogicalNode(usersPage, "GroupFilter") as ComboBox;
|
||||
Check(groupFilter?.Items.Count == Enum.GetValues<UserGroup>().Length + 1, "用户组包含全部和当前枚举选项");
|
||||
await app.Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle);
|
||||
var menu = grid.RowContextMenu;
|
||||
Check(grid.SelectRowOnRightClick && ReferenceEquals(menu, usersPage.Resources["UserRowMenu"]), "使用页面行菜单并选中右键行");
|
||||
var actions = menu.Items.OfType<MenuItem>().Select(item => item.Command).ToArray();
|
||||
Check(actions.SequenceEqual(new System.Windows.Input.ICommand[] { usersVm.EditCommand, usersVm.BillingCommand, usersVm.DeleteCommand, usersVm.CreateCommand }),
|
||||
"行菜单绑定编辑、删除和创建用户命令");
|
||||
Check(groupFilter!.Text == "全部", "默认显示全部用户组");
|
||||
groupFilter.SelectedIndex = 1;
|
||||
Check(usersVm.SelectedGroup == Store.Management.Api.Users.UserGroup.MainAccount, "选择主号更新搜索条件");
|
||||
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, "重试查询成功");
|
||||
var narrowWindow = new Window { Content = usersPage, Width = 900, Height = 560, ShowInTaskbar = false };
|
||||
narrowWindow.Show();
|
||||
narrowWindow.UpdateLayout();
|
||||
var listScroll = (ScrollViewer)grid.Template.FindName("DG_ScrollViewer", grid);
|
||||
Check(listScroll.ComputedHorizontalScrollBarVisibility == Visibility.Visible && listScroll.ScrollableWidth > 0,
|
||||
"窄窗口显示用户列表横向滚动条");
|
||||
listScroll.ScrollToRightEnd();
|
||||
narrowWindow.UpdateLayout();
|
||||
Check(listScroll.HorizontalOffset > 0 && Math.Abs(listScroll.HorizontalOffset - listScroll.ScrollableWidth) < 1,
|
||||
"用户列表可滚动到最右列");
|
||||
narrowWindow.Close();
|
||||
Check(handler.LastGroupId == 3, "查询按主号分组");
|
||||
groupFilter.SelectedIndex = 0;
|
||||
await usersVm.PageCommand.Execute(new DataGridPageRequest(2, 50));
|
||||
Check(usersVm.Page == 2 && usersVm.CanInteract && status.Current.Level == StatusLevel.Success, "分页查询成功");
|
||||
Check(handler.LastGroupId == 3, "翻页保留已查询的用户组");
|
||||
await usersVm.SearchCommand.Execute();
|
||||
Check(handler.LastGroupId == null && usersVm.Page == 1, "全部清除分组并返回首页");
|
||||
var billing = new Store.Management.Views.Dialogs.UserBillingDialogViewModel(userService);
|
||||
billing.OnDialogOpened(new DialogParameters { { "parameter", new UserInfo
|
||||
{
|
||||
Id = 7, UserName = "billing", ShopPointsPerMonth = 45, PostagePerOrder = 3.5m,
|
||||
RefundPostagePerOrder = 4m, GiftOemAmountPerOrder = null, SyncLogisticsPerOrder = 0m
|
||||
} } });
|
||||
var billingView = new Store.Management.Views.Dialogs.UserBillingDialog { DataContext = billing };
|
||||
var billingWindow = new Window { Content = billingView, ShowInTaskbar = false };
|
||||
billingWindow.Show();
|
||||
billingWindow.UpdateLayout();
|
||||
Check(billing.Fields.Length == 5 && billing.Fields[0].Value == 45 && billing.Fields[3].EnteredValue == null &&
|
||||
billing.Fields[4].EnteredValue == 0, "计费配置回填原值并区分空值和零");
|
||||
var saveBilling = (Prism.Commands.AsyncDelegateCommand)billing.ConfirmCommand;
|
||||
int beforeBilling = handler.RequestCount;
|
||||
await saveBilling.Execute();
|
||||
Check(handler.RequestCount == beforeBilling, "未改动计费配置不发送请求");
|
||||
billing.Fields[0].Value = 29;
|
||||
await saveBilling.Execute();
|
||||
Check(handler.RequestCount == beforeBilling && billing.ErrorMsg.Length > 0, "拦截无效积分");
|
||||
billing.Fields[0].Value = 50;
|
||||
var fields = (ItemsControl)billingView.Content;
|
||||
var presenter = (ContentPresenter)fields.ItemContainerGenerator.ContainerFromIndex(3);
|
||||
var title = (MAX.Common.WPF.Controls.TitleContent)System.Windows.Media.VisualTreeHelper.GetChild(presenter, 0);
|
||||
var amount = (HandyControl.Controls.NumericUpDown)title.Content;
|
||||
Check(amount.IsEnabled, "空金额可以直接输入");
|
||||
var amountInput = (TextBox)amount.Template.FindName("PART_TextBox", amount);
|
||||
amountInput.Focus();
|
||||
amountInput.SelectAll();
|
||||
amountInput.SelectedText = "0";
|
||||
await saveBilling.Execute();
|
||||
using (var json = System.Text.Json.JsonDocument.Parse(handler.LastUpdateBody!))
|
||||
{
|
||||
var body = json.RootElement;
|
||||
Check(body.EnumerateObject().Count() == 3 && body.GetProperty("id").GetInt32() == 7 &&
|
||||
body.GetProperty("shopPointsPerMonth").GetInt32() == 50 && body.GetProperty("giftOemAmountPerOrder").GetDecimal() == 0,
|
||||
"计费更新请求仅含用户 ID 和两项改动");
|
||||
}
|
||||
billingWindow.Close();
|
||||
var editor = new UserEditorDialogViewModel(userService);
|
||||
editor.OnDialogOpened(new DialogParameters { { "user", null } });
|
||||
var editorView = new UserEditorDialog { DataContext = editor };
|
||||
editor.Draft.Mobile = "13800000000";
|
||||
editor.Draft.NickName = "测试";
|
||||
editor.Group = UserGroup.CustomerService;
|
||||
editor.ParentUserName = "parent";
|
||||
editor.AgentCode = "agent";
|
||||
editor.Level = UserAdminLevel.SuperAdmin;
|
||||
editor.Permissions[0].IsSelected = true;
|
||||
var editorPassword = (PasswordBox)editorView.FindName("NewPassword");
|
||||
var confirmation = (PasswordBox)editorView.FindName("Confirmation");
|
||||
editorPassword.Password = "secret";
|
||||
await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute();
|
||||
Check(editor.ErrorMsg.Contains("不一致"), "密码不一致应阻止保存");
|
||||
confirmation.Password = "secret";
|
||||
await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute();
|
||||
Check(handler.SavedCreate is { UserName: "13800000000", GroupId: UserGroup.CustomerService,
|
||||
ParentUserName: "parent", Authority: "微信回采", OemId: "agent", SaLevel: UserAdminLevel.SuperAdmin },
|
||||
"新增子账号提交手机号、分组、代理和权限");
|
||||
editor.OnDialogClosed();
|
||||
editor = new UserEditorDialogViewModel(userService);
|
||||
editor.OnDialogOpened(new DialogParameters { { "user", new UserInfo { Id = 1, UserName = "old-login",
|
||||
GroupId = 3, Authority = "微信回采,历史权限", Mobile = "13800000000", NickName = "测试" } } });
|
||||
Check(editor.Permissions[0].IsSelected, "编辑回填权限");
|
||||
editor.Permissions[0].IsSelected = false;
|
||||
await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute();
|
||||
Check(handler.SavedUpdate?.Authority == "历史权限" && handler.PasswordTarget == null, "保留未知权限,留空不改密");
|
||||
editor.UpdatePasswords("new-password", "new-password", "");
|
||||
await ((Prism.Commands.AsyncDelegateCommand)editor.ConfirmCommand).Execute();
|
||||
Check(handler.PasswordTarget == "old-login", "按原登录名修改密码");
|
||||
await userService.DeleteAsync(1);
|
||||
Console.WriteLine("PASS: status bar bindings, colors and background progress reports.");
|
||||
return;
|
||||
@@ -267,10 +376,16 @@ class Program
|
||||
|
||||
sealed class LifecycleProbe : ViewModelBase
|
||||
{
|
||||
public int FirstLoads { get; private set; }
|
||||
public int Loads { get; private set; }
|
||||
public int Unloads { get; private set; }
|
||||
public int Closes { get; private set; }
|
||||
public bool CancelClose { get; set; }
|
||||
protected override void OnLoadedOnce()
|
||||
{
|
||||
if (Loads != 0) throw new InvalidOperationException("首次加载应先于 OnLoaded");
|
||||
FirstLoads++;
|
||||
}
|
||||
protected override void OnLoaded() => Loads++;
|
||||
protected override void OnUnloaded() => Unloads++;
|
||||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) => e.Cancel = CancelClose;
|
||||
@@ -279,18 +394,45 @@ sealed class LifecycleProbe : ViewModelBase
|
||||
|
||||
sealed class LoginHandler : HttpMessageHandler
|
||||
{
|
||||
public UserCreateRequest? SavedCreate { get; private set; }
|
||||
public string? LastUpdateBody { get; private set; }
|
||||
public UserUpdateRequest? SavedUpdate { get; private set; }
|
||||
public string? PasswordTarget { get; private set; }
|
||||
public int? LastGroupId { get; private set; }
|
||||
public int RequestCount { get; private set; }
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
RequestCount++;
|
||||
if (request.RequestUri?.AbsolutePath == "/api/v1/user/GetAllUser")
|
||||
{
|
||||
using var query = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken));
|
||||
LastGroupId = query.RootElement.TryGetProperty("groupId", out var group) && group.ValueKind == System.Text.Json.JsonValueKind.Number
|
||||
? group.GetInt32() : null;
|
||||
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")
|
||||
};
|
||||
}
|
||||
string? route = request.RequestUri?.AbsolutePath;
|
||||
if (route is "/api/v1/user/AddSubUser" or "/api/v1/user/UpdateUser" or "/api/v1/user/ChangePassword")
|
||||
{
|
||||
string bodyText = await request.Content!.ReadAsStringAsync(cancellationToken);
|
||||
var options = new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web);
|
||||
if (route.EndsWith("/AddSubUser"))
|
||||
SavedCreate = System.Text.Json.JsonSerializer.Deserialize<UserCreateRequest>(bodyText, options);
|
||||
else if (route.EndsWith("/UpdateUser"))
|
||||
{
|
||||
LastUpdateBody = bodyText;
|
||||
SavedUpdate = System.Text.Json.JsonSerializer.Deserialize<UserUpdateRequest>(bodyText, options);
|
||||
}
|
||||
else
|
||||
PasswordTarget = System.Text.Json.JsonSerializer.Deserialize<ChangePasswordRequest>(bodyText, options)?.UserName;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("""{"code":200,"data":{"id":1,"userName":"old-login","groupId":3,"authority":"历史权限"}}""", Encoding.UTF8, "application/json")
|
||||
};
|
||||
}
|
||||
if (request.Method == HttpMethod.Post && request.RequestUri?.PathAndQuery == "/api/v1/user/DelUser?id=1")
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
|
||||
@@ -54,6 +54,7 @@ internal static class Program
|
||||
window = new Window { Content = view, Width = 1280, Height = 760, Left = -10000, Top = -10000, ShowInTaskbar = false };
|
||||
window.Show();
|
||||
Pump();
|
||||
CheckBox selectAll = Descendants<CheckBox>(grid).Single(box => box.Command == DataGridEx.ToggleSelectAllCommand);
|
||||
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");
|
||||
@@ -70,9 +71,6 @@ internal static class Program
|
||||
Check(DataGridEx.GetRowNumber(row) == 1, "first row number");
|
||||
Check(Descendants<TextBlock>(row).Any(text => text.Text == "1"), "row number binding inherits into cell");
|
||||
Check(Descendants<CheckBox>(row).Any(), "row checkbox rendered independently");
|
||||
Check(Descendants<CheckBox>(grid).Any(box => System.Windows.Automation.AutomationProperties.GetName(box) == "全选行"), "header checkbox rendered");
|
||||
|
||||
CheckBox selectAll = Descendants<CheckBox>(grid).Single(box => System.Windows.Automation.AutomationProperties.GetName(box) == "全选行");
|
||||
CheckBox selectRow = Descendants<CheckBox>(row).Single();
|
||||
Check(ReferenceEquals(selectAll.Template, selectRow.Template), "header and row checkbox share the theme template");
|
||||
double headerCenter = selectAll.TranslatePoint(new Point(selectAll.ActualWidth / 2, 0), grid).X;
|
||||
|
||||
Reference in new issue
Block a user