From 127fc22de525eab5f56778688c56ee80a975ee1c Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 13 Nov 2026 15:51:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=94=A8=E6=88=B7=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/rules/wpf-xaml.md | 40 +++- Source/Store.Management.Api/Users/UserApi.cs | 9 +- .../Store.Management.Api/Users/UserGroup.cs | 18 ++ .../Store.Management.Api/Users/UserModels.cs | 38 +++ .../Configs/FolderConfig.cs | 2 +- .../Controls/DataGridEx/DataGridEx.xaml | 2 - .../Controls/DialogWindow.xaml | 2 +- .../Controls/HorizontalIconText.cs | 127 ++++++++++ .../Controls/ViewLifecycle.cs | 2 +- .../Controls/ViewModelBase.cs | 14 ++ .../Resources/FrameworkTheme.xaml | 180 +++++++++++++- .../Services/UserService.cs | 8 + Source/Store.Management/App.xaml.cs | 2 + .../ViewModels/UserEditorDialogViewModel.cs | 108 ++++++++- .../ViewModels/UserManagementPageViewModel.cs | 224 +++++++++--------- .../Views/DataGridTestView.xaml | 2 - .../Views/Dialogs/UserBillingDialog.xaml | 42 ++++ .../Views/Dialogs/UserBillingDialog.xaml.cs | 127 ++++++++++ .../Store.Management/Views/LoginWindow.xaml | 2 - Source/Store.Management/Views/MainWindow.xaml | 1 - .../Views/UserEditorDialog.xaml | 150 ++++++++---- .../Views/UserEditorDialog.xaml.cs | 2 +- .../Views/UserManagementPage.xaml | 59 ++++- .../Views/UserManagementPage.xaml.cs | 5 - .../Program.cs | 146 +++++++++++- .../Program.cs | 4 +- 26 files changed, 1110 insertions(+), 206 deletions(-) create mode 100644 Source/Store.Management.Api/Users/UserGroup.cs create mode 100644 Source/Store.Management.Framework/Controls/HorizontalIconText.cs create mode 100644 Source/Store.Management/Views/Dialogs/UserBillingDialog.xaml create mode 100644 Source/Store.Management/Views/Dialogs/UserBillingDialog.xaml.cs diff --git a/.agents/rules/wpf-xaml.md b/.agents/rules/wpf-xaml.md index ae61713..d7a3c67 100644 --- a/.agents/rules/wpf-xaml.md +++ b/.agents/rules/wpf-xaml.md @@ -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 +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 + + + + + + + +``` + ## 文字与简单内容布局 **页面排版遇到“说明文字 + 简单内容”(如输入框、选择器、按钮等输入或点击控件)时,优先使用 `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)。 ## 布局注释 diff --git a/Source/Store.Management.Api/Users/UserApi.cs b/Source/Store.Management.Api/Users/UserApi.cs index a5f1dcb..487b8a4 100644 --- a/Source/Store.Management.Api/Users/UserApi.cs +++ b/Source/Store.Management.Api/Users/UserApi.cs @@ -20,9 +20,13 @@ public sealed class UserApi(ApiTransport transport) public Task CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return transport.GetRequiredAsync("user/AddUser", Method.Post, request, cancellationToken: cancellationToken); + string route = request.GroupId is null or UserGroup.MainAccount ? "user/AddUser" : "user/AddSubUser"; + return transport.GetRequiredAsync(route, Method.Post, request, cancellationToken: cancellationToken); } + public Task ChangePasswordAsync(ChangePasswordRequest request, CancellationToken cancellationToken = default) + => transport.SendAsync("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 UpdateBillingAsync(UserBillingUpdateRequest request, CancellationToken cancellationToken = default) + => transport.GetRequiredAsync("user/UpdateUser", Method.Post, request, cancellationToken: cancellationToken); + public Task UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); diff --git a/Source/Store.Management.Api/Users/UserGroup.cs b/Source/Store.Management.Api/Users/UserGroup.cs new file mode 100644 index 0000000..79b2ca5 --- /dev/null +++ b/Source/Store.Management.Api/Users/UserGroup.cs @@ -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 + { + /// 主号。 + [Description("主号")] MainAccount = 3, + /// 客服。 + [Description("客服")] CustomerService = 4, + /// 加盟(旧系统分组 5)。 + } +} diff --git a/Source/Store.Management.Api/Users/UserModels.cs b/Source/Store.Management.Api/Users/UserModels.cs index 27fc1cf..5ca61d8 100644 --- a/Source/Store.Management.Api/Users/UserModels.cs +++ b/Source/Store.Management.Api/Users/UserModels.cs @@ -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; } +} + /// 管理端当前使用的用户资料字段;不接收密码。 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; } } diff --git a/Source/Store.Management.Framework/Configs/FolderConfig.cs b/Source/Store.Management.Framework/Configs/FolderConfig.cs index c3726e0..691a52c 100644 --- a/Source/Store.Management.Framework/Configs/FolderConfig.cs +++ b/Source/Store.Management.Framework/Configs/FolderConfig.cs @@ -24,7 +24,7 @@ public sealed class FolderConfig : IFolderConfig /// 当前用户的数据根目录:%LOCALAPPDATA%\Store.Management。 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"); diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml index 19565a5..be5e320 100644 --- a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml +++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml @@ -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 @@ diff --git a/Source/Store.Management.Framework/Controls/DialogWindow.xaml b/Source/Store.Management.Framework/Controls/DialogWindow.xaml index 2a7ad9a..c759aff 100644 --- a/Source/Store.Management.Framework/Controls/DialogWindow.xaml +++ b/Source/Store.Management.Framework/Controls/DialogWindow.xaml @@ -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"> diff --git a/Source/Store.Management.Framework/Controls/HorizontalIconText.cs b/Source/Store.Management.Framework/Controls/HorizontalIconText.cs new file mode 100644 index 0000000..57759cc --- /dev/null +++ b/Source/Store.Management.Framework/Controls/HorizontalIconText.cs @@ -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(); + } + + } +} diff --git a/Source/Store.Management.Framework/Controls/ViewLifecycle.cs b/Source/Store.Management.Framework/Controls/ViewLifecycle.cs index 2187f70..1fb9a72 100644 --- a/Source/Store.Management.Framework/Controls/ViewLifecycle.cs +++ b/Source/Store.Management.Framework/Controls/ViewLifecycle.cs @@ -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) diff --git a/Source/Store.Management.Framework/Controls/ViewModelBase.cs b/Source/Store.Management.Framework/Controls/ViewModelBase.cs index 51faa20..ed0fdad 100644 --- a/Source/Store.Management.Framework/Controls/ViewModelBase.cs +++ b/Source/Store.Management.Framework/Controls/ViewModelBase.cs @@ -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(); + } + + /// 当前实例首次加载时调用,先于 OnLoaded。 + protected virtual void OnLoadedOnce() { } protected internal virtual void OnLoaded() { } protected internal virtual void OnUnloaded() { } protected internal virtual void OnActivated() { } diff --git a/Source/Store.Management.Framework/Resources/FrameworkTheme.xaml b/Source/Store.Management.Framework/Resources/FrameworkTheme.xaml index 84dc7a4..bf3a6dc 100644 --- a/Source/Store.Management.Framework/Resources/FrameworkTheme.xaml +++ b/Source/Store.Management.Framework/Resources/FrameworkTheme.xaml @@ -1,8 +1,12 @@  + xmlns:hc="https://handyorg.github.io/handycontrol" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:wpf="MAX.WPF" + mc:Ignorable="d"> @@ -37,6 +41,180 @@ --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - -