提交用户部分代码

This commit is contained in:
chfei committed 2026-11-13 15:51:16 +08:00
1 parent 9b9e4cbd82
commit 127fc22de5
26 files changed
+1110 -206

No files matched your search

+8 -1
View File
@@ -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);
}
+2
View File
@@ -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();
}
}