Main页面

This commit is contained in:
chfei committed 2026-11-11 14:33:39 +08:00
1 parent a003d90b0e
commit f03e2b08f4
33 files changed
+54543 -34

No files matched your search

+10 -1
View File
@@ -18,9 +18,10 @@ globs: "**/*.xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:wpf="MAX.WPF"
xmlns:s="clr-namespace:xxxx.xxx;assembly=xxx.xxx"
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
Style="{DynamicResource UserControlStyle}"
DataContext="{core:DISource Type=local:XxxViewModel}"
d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"
d:Background="White"
d:DesignHeight="450"
d:DesignWidth="800">
```
@@ -42,6 +43,14 @@ DataContext="{core:DISource Type=local:DialogViewModel}">
- `d:DataContext="{d:DesignInstance Type=local:XxxViewModel}"` 提供设计时数据
- `UserControlStyle` 与 `ViewModelBaseEx` 强相关,必须搭配使用
### 控件必须有设计数据
例如
TextBlock > d:Text="TextBlock"
ComboBox > d:ItemsSource="{d:SampleData ItemCount=5}"
## BindingProxy + CommandProxy(DataTemplate/ContextMenu 绑定)
**在 `DataTemplate`、`ContextMenu`、`DataGridTemplateColumn.CellTemplate` 中禁止使用 `RelativeSource AncestorType`,必须用 `BindingProxy` + `CommandProxy`。**
@@ -23,6 +23,14 @@ public sealed class UserApi(ApiTransport transport)
return transport.GetRequiredAsync<UserInfo>("user/AddUser", Method.Post, request, cancellationToken: cancellationToken);
}
public Task DeleteAsync(int id, CancellationToken cancellationToken = default)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id);
return transport.SendAsync<object>("user/DelUser", Method.Post,
query: new Dictionary<string, string?> { ["id"] = id.ToString(System.Globalization.CultureInfo.InvariantCulture) },
cancellationToken: cancellationToken);
}
public Task<UserInfo> UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
@@ -0,0 +1,42 @@
using PipeReviewReport.Resources.Fonts;
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
{
public class IconText : TextBlock
{
static IconText()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(IconText), new FrameworkPropertyMetadata(typeof(IconText)));
}
public IconNameEnum Icon
{
get => (IconNameEnum)GetValue(IconNameProperty);
set => SetValue(IconNameProperty, value);
}
public static readonly DependencyProperty IconNameProperty = DependencyProperty.Register(
nameof(Icon),
typeof(IconNameEnum),
typeof(IconText),
new PropertyMetadata(IconNameEnum.Empty, OnIconChanged)
);
private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is IconText self)
{
self.Text = self.Icon.GetGlyphString();
}
}
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Store.Management.Framework
{
public class Navigation
{
public static string MainContext { get; } = nameof(MainContext);
}
}
@@ -0,0 +1,65 @@
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ output extension=".xaml" encoding="utf-8" #>
<#
StringBuilder FontString = new StringBuilder();
FontString.AppendLine("<ResourceDictionary");
FontString.AppendLine("\txmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"");
FontString.AppendLine("\txmlns:PresentationOptions=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation/options\"");
FontString.AppendLine("\txmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"");
FontString.AppendLine("\txmlns:sys=\"clr-namespace:System;assembly=mscorlib\"");
FontString.AppendLine("\t>");
FontString.AppendLine("");
FontString.AppendLine("");
string cssPath = this.Host.ResolvePath("iconfont.css");
string svgPath = this.Host.ResolvePath("iconfont.js");
if (File.Exists(cssPath))
{
string cssContent = File.ReadAllText(cssPath, Encoding.UTF8);
MatchCollection matchs = Regex.Matches(cssContent, "\\.icon-(?<name>.+?):before.+?\\{.+?content:.+?\"\\\\(?<value>e.+?)\";.+?\\}", RegexOptions.Singleline);
foreach (Match match in matchs)
{
string name = match.Groups["name"].Value;
//name = Regex.Replace(name,"(a-(\\d{4,}(.*)?_fluent_|SegoeFluentIconsRegular-))|(_regular_icon|$)","",RegexOptions.Singleline);
string value = match.Groups["value"].Value;
FontString.AppendLine(string.Format("\t<sys:String x:Key=\"txt-{0}\">&#x{1};</sys:String>", name, value));
}
}
if (File.Exists(svgPath))
{
FontString.AppendLine("");
FontString.AppendLine("");
string svgContent = File.ReadAllText(svgPath, Encoding.UTF8);
MatchCollection matchs = Regex.Matches(svgContent, "<symbol.+?id=\"(?<name>.+?)\".+?>(?<value>.+?)</symbol>");
foreach (Match match in matchs)
{
string name = match.Groups["name"].Value;
string symbol = match.Groups["value"].Value;
StringBuilder value =new StringBuilder();
MatchCollection pathMatchs = Regex.Matches(symbol,"<path.+?d=\"(?<value>.+?)\"");
foreach(Match pathMatch in pathMatchs)
{
value.AppendLine("\t\t"+pathMatch.Groups["value"].Value);
}
FontString.AppendLine(string.Format("\t<Geometry x:Key=\"{0}\" PresentationOptions:Freeze=\"True\">\n{1}\t</Geometry>", name, value));
}
}
FontString.AppendLine("");
FontString.AppendLine("");
FontString.AppendLine("</ResourceDictionary>");
string content = FontString.ToString();
string savePath =Path.Combine(this.Host.ResolvePath(""),@"..\Themes\FontGeometry.xaml");
//File.WriteAllText(savePath,content,Encoding.UTF8);
#>
<#=content#>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,284 @@
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ assembly name="$(ProjectDir)\Resources\DLLs\IconResourcesGenerator.dll" #>
<#@ import namespace="IconResourcesGenerator" #>
<#@ output extension=".cs" encoding="utf-8" #>
<#
ITextTemplatingEngineHost host = this.Host;
string jsPath = host.ResolvePath("iconfont.js");
string cssPath = host.ResolvePath("iconfont.css");
string namespaceName = "PipeReviewReport.Resources.Fonts";
Generator generator = new Generator(jsPath, cssPath);
Dictionary<string, string> glyphToKey = generator.ExecuteGlyphToKey();
Dictionary<string, string> keyToGlyph = generator.ExecuteKeyToGlyph();
(Dictionary<string, string> simpleNameToKey, Dictionary<string, string> keyToSimpleName) = generator.ExecuteSimpleNameToKey();
#>
using System;
// using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace <#= namespaceName #>;
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
internal class __________________Temp__________________
{
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "<Pending>")]
private void Test()
{
}
};
/// <summary>
/// 图标名称枚举
/// </summary>
// [TypeConverter(typeof(EnumConverter))]
public enum IconNameEnum
{
Empty,
<#
foreach (var entry in simpleNameToKey)
{
#>
/// <summary>
/// <#= keyToGlyph[entry.Value] #>
/// <br/>
/// icon-<#= entry.Value #>
/// </summary>
<#= entry.Key #>,
<#
}
#>
}
public static class IconNameEnumExtensions
{
/// <summary>
/// 根据图标名称枚举获取 Key
/// </summary>
/// <param name="e">图标名称枚举</param>
/// <returns>注册到资源中的 Key</returns>
/// <exception cref="NotImplementedException"></exception>
public static string GetKey(this IconNameEnum e) {
return e switch
{
IconNameEnum.Empty => null,
<#
foreach (var entry in simpleNameToKey)
{
#>
IconNameEnum.<#= entry.Key #> => "icon-<#= entry.Value #>",
<#
}
#>
_ => throw new NotImplementedException(),
};
}
/// <summary>
/// 根据图标名称获取 Glyph 枚举
/// </summary>
/// <param name="e">图标名称枚举</param>
/// <returns>图标 Glyph 枚举</returns>
/// <exception cref="NotImplementedException"></exception>
public static IconGlyphEnum GetGlyphEnum(this IconNameEnum e) {
return e switch
{
IconNameEnum.Empty => IconGlyphEnum.Empty,
<#
foreach (var entry in simpleNameToKey)
{
#>
IconNameEnum.<#= entry.Key #> => IconGlyphEnum.<#= keyToGlyph[entry.Value] #>,
<#
}
#>
_ => throw new NotImplementedException(),
};
}
/// <summary>
/// 根据图标名称获取 Glyph 字符串
/// </summary>
/// <param name="e">图标名称枚举</param>
/// <returns>图标 Glyph 字符串</returns>
/// <exception cref="NotImplementedException"></exception>
public static string GetGlyphString(this IconNameEnum e) {
return e switch
{
IconNameEnum.Empty => string.Empty,
<#
foreach (var entry in simpleNameToKey)
{
#>
IconNameEnum.<#= entry.Key #> => "\u<#= keyToGlyph[entry.Value] #>",
<#
}
#>
_ => throw new NotImplementedException(),
};
}
public static string GetImageSourceKey(this IconNameEnum e) {
return GetKey(e) + "_Image";
}
}
/// <summary>
/// 图标 Glyph 枚举
/// </summary>
// [TypeConverter(typeof(EnumConverter))]
public enum IconGlyphEnum
{
Empty,
<#
foreach (var entry in glyphToKey)
{
#>
/// <summary>
/// <#= keyToSimpleName[entry.Value] #>
/// <br/>
/// icon-<#= entry.Value #>
/// </summary>
<#= entry.Key #>,
<#
}
#>
}
public static class IconGlyphEnumExtensions
{
/// <summary>
/// 根据 Glyph 枚举获取 Key
/// </summary>
/// <param name="e">Glyph 枚举</param>
/// <returns>注册到资源中的 Key</returns>
/// <exception cref="NotImplementedException"></exception>
public static string GetKey(this IconGlyphEnum e)
{
return e switch
{
IconGlyphEnum.Empty => null,
<#
foreach (var entry in glyphToKey)
{
#>
IconGlyphEnum.<#= entry.Key #> => "icon-<#= entry.Value #>",
<#
}
#>
_ => throw new NotImplementedException(),
};
}
/// <summary>
/// 根据图标 Glyph 枚举获取图标名称枚举
/// </summary>
/// <param name="e">图标 Glyph 枚举</param>
/// <returns>图标名称枚举</returns>
/// <exception cref="NotImplementedException"></exception>
public static IconNameEnum GetNameEnum(this IconGlyphEnum e) {
return e switch
{
IconGlyphEnum.Empty => IconNameEnum.Empty,
<#
foreach (var entry in glyphToKey)
{
#>
IconGlyphEnum.<#= entry.Key #> => IconNameEnum.<#= keyToSimpleName[entry.Value] #>,
<#
}
#>
_ => throw new NotImplementedException(),
};
}
/// <summary>
/// 根据图标 Glyph 枚举获取 Glyph 字符串
/// </summary>
/// <param name="e">图标 Glyph 枚举</param>
/// <returns>图标 Glyph 字符串</returns>
/// <exception cref="NotImplementedException"></exception>
public static string GetGlyphString(this IconGlyphEnum e) {
return e switch
{
IconGlyphEnum.Empty => string.Empty,
<#
foreach (var entry in glyphToKey)
{
#>
IconGlyphEnum.<#= entry.Key #> => "\u<#= keyToGlyph[entry.Value] #>",
<#
}
#>
_ => throw new NotImplementedException(),
};
}
public static string GetImageSourceKey(this IconGlyphEnum e) {
return GetKey(e) + "_Image";
}
}
public class IconEnumNameToGlyphConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is IconNameEnum nameEnum) {
return nameEnum.GetGlyphEnum();
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is IconGlyphEnum glyphEnum) {
return glyphEnum.GetNameEnum();
}
return DependencyProperty.UnsetValue;
}
}
public class IconGlyphNameToNameConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is IconGlyphEnum glyphEnum) {
return glyphEnum.GetNameEnum();
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
if (value is IconNameEnum nameEnum) {
return nameEnum.GetGlyphEnum();
}
return DependencyProperty.UnsetValue;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
window._iconfont_svg_string_5223831='<svg><symbol id="icon-fapiao" viewBox="0 0 1024 1024"><path d="M170.666667 868.693333l98.346666-94.72a21.333333 21.333333 0 0 1 30.72 1.173334l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 31.829334 0l98.176 110.122666 98.176-110.122666a21.333333 21.333333 0 0 1 30.72-1.173334L853.333333 868.693333V128H170.666667v740.693333z m-42.666667 49.386667V106.709333C128 94.890667 137.706667 85.333333 149.333333 85.333333h725.333334c11.776 0 21.333333 9.642667 21.333333 21.184v811.541334a21.333333 21.333333 0 0 1-36.117333 16.149333l-118.506667-114.133333-99.349333 111.445333a21.333333 21.333333 0 0 1-31.850667 0L512 821.397333l-98.176 110.122667a21.333333 21.333333 0 0 1-31.850667 0l-99.349333-111.445333-118.485333 114.133333A21.333333 21.333333 0 0 1 128 918.08zM298.666667 320c0-11.776 9.493333-21.333333 21.184-21.333333h341.632c11.690667 0 21.184 9.472 21.184 21.333333 0 11.776-9.493333 21.333333-21.184 21.333333H319.850667A21.205333 21.205333 0 0 1 298.666667 320z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 448z m0 128c0-11.776 9.6-21.333333 21.482666-21.333333h255.701334c11.861333 0 21.482667 9.472 21.482666 21.333333 0 11.776-9.6 21.333333-21.482666 21.333333H320.149333A21.354667 21.354667 0 0 1 298.666667 576z" fill="#3D3D3D" ></path></symbol><symbol id="icon-calendar_work_week" viewBox="0 0 1024 1024"><path d="M330.666667 298.666667a32 32 0 0 0-32 32v170.666666c0 17.664 14.336 32 32 32h362.666666a32 32 0 0 0 32-32v-170.666666a32 32 0 0 0-32-32h-362.666666z m32 170.666666V362.666667h298.666666V469.333333h-298.666666z" ></path><path d="M757.333333 896A138.666667 138.666667 0 0 0 896 757.333333V266.666667A138.666667 138.666667 0 0 0 757.333333 128H266.666667A138.666667 138.666667 0 0 0 128 266.666667v490.666666A138.666667 138.666667 0 0 0 266.666667 896h490.666666zM832 266.666667v490.666666a74.666667 74.666667 0 0 1-74.666667 74.666667H266.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V266.666667c0-41.216 33.450667-74.666667 74.666667-74.666667h490.666666c41.216 0 74.666667 33.450667 74.666667 74.666667z" ></path></symbol><symbol id="icon-document_endnote" viewBox="0 0 1024 1024"><path d="M234.666667 181.333333a32 32 0 0 1 32-32H512V341.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h192v416a32 32 0 0 1-32 32H554.666667V938.666667h202.666666A96 96 0 0 0 853.333333 842.666667V412.330667c0-19.797333-7.850667-38.784-21.888-52.778667l-252.330666-252.330667A74.666667 74.666667 0 0 0 526.336 85.333333H266.666667A96 96 0 0 0 170.666667 181.333333V512h64V181.333333z m341.333333 160V194.56l168.106667 168.106667H597.333333a21.333333 21.333333 0 0 1-21.333333-21.333334zM192 586.666667a32 32 0 0 1-32 32H106.666667v298.666666h53.333333a32 32 0 0 1 0 64H106.666667A64 64 0 0 1 42.666667 917.333333v-298.666666A64 64 0 0 1 106.666667 554.666667h53.333333a32 32 0 0 1 32 32z" ></path><path d="M394.666667 618.666667a32 32 0 0 1 0-64h53.333333a64 64 0 0 1 64 64v298.666666a64 64 0 0 1-64 64H394.666667a32 32 0 0 1 0-64h53.333333v-298.666666H394.666667zM277.333333 714.666667a32 32 0 0 1 32 32V896a32 32 0 0 1-64 0v-149.333333a32 32 0 0 1 32-32zM277.333333 682.666667a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" ></path></symbol><symbol id="icon-leaf_three" viewBox="0 0 1024 1024"><path d="M810.666667 320c0 22.186667-3.072 43.648-8.832 64h69.12C908.373333 384 938.666667 414.293333 938.666667 451.669333V661.333333a277.333333 277.333333 0 0 1-449.493334 217.429334L438.613333 929.28a32 32 0 1 1-45.226666-45.226667l50.517333-50.56a277.077333 277.077333 0 0 1-40.704-70.613333A256 256 0 0 1 96 512V320.597333C96 284.928 124.928 256 160.597333 256h189.568A234.752 234.752 0 0 1 576 85.333333h173.226667c33.92 0 61.44 27.52 61.44 61.44V320zM414.805333 263.765333a256.554667 256.554667 0 0 1 165.504 132.266667A277.290667 277.290667 0 0 1 661.333333 384h72.96c7.978667-19.754667 12.373333-41.386667 12.373334-64v-170.666667h-170.666667a170.752 170.752 0 0 0-161.194667 114.432z m120.021334 569.344A213.333333 213.333333 0 0 0 874.666667 661.333333v-209.664a3.669333 3.669333 0 0 0-3.669334-3.669333H661.333333a213.333333 213.333333 0 0 0-171.776 339.84l181.162667-181.12a32 32 0 1 1 45.226667 45.226667l-181.12 181.162666zM384 661.333333c0-25.258667 3.413333-49.749333 9.728-73.002666L276.053333 470.613333a32 32 0 1 1 45.226667-45.226666l98.901333 98.858666a278.698667 278.698667 0 0 1 101.333334-102.485333A192 192 0 0 0 352 320H160.384l-0.213333 0.170667a0.938667 0.938667 0 0 0-0.128 0.213333l-0.042667 0.213333V512a192 192 0 0 0 226.816 188.842667A279.765333 279.765333 0 0 1 384 661.333333z" ></path></symbol><symbol id="icon-re_order_dots_horizontal" viewBox="0 0 1024 1024"><path d="M298.666667 661.333333a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m0-298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 0z m298.666666 298.666666a64 64 0 1 1-128 0 64 64 0 0 1 128 Line truncated
File diff suppressed because it is too large. Load diff
@@ -2,13 +2,22 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:f="clr-namespace:Store.Management.Framework.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Fonts/FontGeometry.xaml" />
<ResourceDictionary Source="pack://application:,,,/MAX.Common.WPF;component/Resources/Generic.xaml" />
<ResourceDictionary Source="/Store.Management.Framework;component/Controls/DataGridEx/DataGridEx.xaml" />
</ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="IconPack">Fonts/iconfont.ttf#iconfont</FontFamily>
<Style x:Key="TextBlockIcon" TargetType="{x:Type TextBlock}">
<Setter Property="FontFamily" Value="{StaticResource IconPack}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="18" />
</Style>
<Style BasedOn="{StaticResource TextBlockIcon}" TargetType="{x:Type f:IconText}" />
<Style
x:Key="WindowBase"
BasedOn="{StaticResource WindowWin10}"
@@ -2,8 +2,10 @@
using MAX.Common.WPF.Converters;
using MAX.Common.WPF.Utilities;
using MAX.Core;
using MAX.Core.Autofac;
using MAX.Framework.Interfaces;
using Store.Management.Api;
using Store.Management.Framework.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -28,6 +30,31 @@ namespace Store.Management.Framework
BaseUrl = new Uri("http://localhost:14312/api/")
});
builder.Register(_ => client).SingleInstance().PreserveExistingDefaults();
DISource.Resolver = Resolve;
}
protected virtual object? Resolve(Type? type, string? name)
{
if (type == null)
{
return null;
}
if (IocManager.Container is not null)
{
if (name != null)
{
if (IocManager.Container.IsRegisteredWithName(name, type))
{
return IocManager.Container.ResolveNamed(name, type);
}
return null;
}
return IocManager.Resolve(type);
}
return Activator.CreateInstance(type);
}
}
}
@@ -9,6 +9,10 @@
<Content Remove="C:\Users\chaih\.nuget\packages\handycontrol\3.6.2.1\contentFiles\any\net8.0-windows7.0\LangProvider.tt" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Fonts\iconfont.ttf" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HandyControl" Version="3.6.2.1" />
<PackageReference Include="MAX.Common.WPF" Version="1.4.23" />
@@ -22,4 +26,31 @@
<Folder Include="DependencyInjection\" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Fonts\iconfont.ttf" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\Fonts\FontGeometry.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>FontGeometry.xaml</LastGenOutput>
</None>
<None Update="Resources\Fonts\IconEnum.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>IconEnum.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Fonts\IconEnum.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>IconEnum.tt</DependentUpon>
</Compile>
</ItemGroup>
</Project>
+1 -3
View File
@@ -5,9 +5,7 @@
xmlns:mf="clr-namespace:Store.Management.Framework;assembly=Store.Management.Framework">
<Application.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="PrimaryBrush" Color="{DynamicResource PrimaryColor}" />
<SolidColorBrush x:Key="TextIconBrush" Color="{DynamicResource TextIconColor}" />
<SolidColorBrush x:Key="BackgroundBrush" Color="{DynamicResource BackgroundColor}" />
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Store.Management.Framework;component/Resources/HandyControlBasic.xaml" />
+36 -4
View File
@@ -7,11 +7,13 @@ using Store.Management.Notifications;
using Store.Management.Services;
using Store.Management.ViewModels;
using Store.Management.Views;
using System.Diagnostics;
using System.Windows;
namespace Store.Management;
public partial class App : FrameworkApplication
{
protected override Window CreateShell()
private AppSplashScreen _appSplashScreen;
protected override Window CreateShell()
{
if (Environment.GetCommandLineArgs().Contains("--grid-demo"))
{
@@ -25,10 +27,33 @@ public partial class App : FrameworkApplication
}
return Container.Resolve<MainWindow>();
}
protected override void OnStartup(StartupEventArgs e)
{
_appSplashScreen = new()
{
CloseCallBack = () =>
{
base.OnStartup(e);
},
};
_appSplashScreen.ShowDialog();
}
protected override async void OnInitialized()
{
try
{
//await LoadModules();
base.OnInitialized();
_appSplashScreen.Close();
_appSplashScreen = null;
}
catch (Exception ex)
{
Fatal(ex);
Debug.WriteLine(ex);
}
protected override void OnInitialized()
{
base.OnInitialized();
//base.OnInitialized();
//// 登录弹窗关闭与主窗口创建之间不触发 WPF 的最后窗口退出。
//ShutdownMode = ShutdownMode.OnExplicitShutdown;
@@ -51,11 +76,18 @@ public partial class App : FrameworkApplication
{
containerRegistry.RegisterDialogWindow<Framework.Controls.DialogWindow>();
containerRegistry.RegisterDialog<UserEditorDialog, UserEditorDialogViewModel>();
containerRegistry.RegisterDialog<DataGridTestView, DataGridTestViewModel>();
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
containerRegistry.RegisterDialog<LoginWindow, LoginViewModel>();
containerRegistry.RegisterForNavigation<UserManagementPage, UserManagementPageViewModel>(nameof(UserManagementPage));
containerRegistry.RegisterForNavigation<DataGridTestView, DataGridTestViewModel>(nameof(DataGridTestView));
// registry.RegisterSingleton<SingletonProbe>();
// registry.Register<TransientProbe>();
// registry.RegisterSingleton<NotificationExampleState>();
+37 -23
View File
@@ -1,31 +1,45 @@
<f:WindowBase
<f:WindowBase
x:Class="Store.Management.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:f="Store.Management.Framework"
xmlns:views="clr-namespace:Store.Management.Views"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:prism="http://prismlibrary.com/"
Title="{Binding Title}"
Width="960"
Width="1140"
Height="720"
MinWidth="720"
MinWidth="900"
MinHeight="560"
WindowStartupLocation="CenterScreen">
<DockPanel>
<!-- 控件测试入口 -->
<Button
Margin="16,12"
Padding="16,6"
HorizontalAlignment="Left"
Command="{Binding OpenDataGridTestCommand, Mode=OneWay}"
Content="打开 DataGrid 测试"
DockPanel.Dock="Top" />
<Button
Margin="16,12"
Padding="16,6"
HorizontalAlignment="Left"
Command="{Binding OpenLoginCommand, Mode=OneWay}"
Content="登录 测试"
DockPanel.Dock="Top" />
<views:FrameworkTestPage />
</DockPanel>
</f:WindowBase>
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,0,1,0">
<local:LeftNavView />
</Border>
<!-- 右侧页面标题与内容 -->
<DockPanel Grid.Column="1">
<TextBlock
Margin="16,16,16,0"
DockPanel.Dock="Top"
Text="{Binding SelectedItem.Header, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
<ContentControl x:Name="PART_SelectedContentHost" prism:RegionManager.RegionName="{Binding Source={x:Static f:Navigation.MainContext}}" />
</DockPanel>
<Border
Grid.Row="1"
Grid.ColumnSpan="2"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,1,0,0" />
</Grid>
</Border>
</f:WindowBase>
@@ -0,0 +1,68 @@
using MAX.Common.Helpers;
using PipeReviewReport.Resources.Fonts;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using Store.Management.Framework;
using Store.Management.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Store.Management.ViewModels
{
internal class LeftNavViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public ObservableCollection<NavItemViewModel> NavItems { get; } = [];
public LeftNavViewModel(IRegionManager regionManager)
{
NavItems.Add(new NavItemViewModel
{
Code = "1",
Title = "角色管理",
Icon = IconNameEnum.Person,
Url=nameof(DataGridTestView)
});
NavItems.Add(new NavItemViewModel
{
Code = "1",
Title = "商铺管理",
Icon = IconNameEnum.BuildingShop,
IsSelected = true,
Url=nameof(UserManagementPage)
});
_regionManager = regionManager;
}
public ICommand NavItemCommand => new AsyncDelegateCommand<NavItemViewModel>(NavItem);
private async Task NavItem(NavItemViewModel model)
{
NavItems.ForEach(a => a.IsSelected = false);
model.IsSelected = true;
_regionManager.RequestNavigate(Navigation.MainContext, model.Url);
}
}
public class NavItemViewModel : BindableBase
{
public string Code { get; set; }
public string Title { get; set; }
public IconNameEnum Icon { get; set; }
public string Url { get; set; }
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
}
}
@@ -52,9 +52,11 @@ public sealed class LoginViewModel(ILoginService loginService) : ConfirmDialogVi
// 服务端要求 UTF-8 密码的 32 位大写 MD5,保留首尾空格。
_passwordHash = string.IsNullOrEmpty(password)
? "" : Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(password)));
? "" : HashPassword(password);
}
internal static string HashPassword(string password) => Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(password)));
public override ICommand ConfirmCommand => LoginCommand;
public override void OnDialogOpened(IDialogParameters parameters)
@@ -0,0 +1,100 @@
using Prism.Commands;
using Prism.Services.Dialogs;
using Store.Management.Api;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
using Store.Management.Framework.Controls;
using System.ComponentModel.DataAnnotations;
using System.Windows.Input;
namespace Store.Management.ViewModels;
public sealed class UserEditorDialogViewModel(StoreApiClient client) : ConfirmDialogViewModel
{
public const int UserNameLimit = 20;
public const int PasswordLimit = 128;
public const int NickNameLimit = 50;
public const int MobileLimit = 30;
public const int CompanyLimit = 100;
public const int NotesLimit = 255;
private UserInfo? user;
private string passwordHash = "";
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();
public override void OnDialogOpened(IDialogParameters parameters)
{
user = parameters.GetValue<UserInfo?>("user");
UserName = user?.UserName ?? "";
Draft = user == null ? new UserCreateRequest() : new UserUpdateRequest
{
Id = user.Id, NickName = user.NickName, Mobile = user.Mobile,
CompanyName = user.CompanyName, Notes = user.Notes
};
Title = IsCreate ? "创建用户" : "编辑用户";
RaisePropertyChanged(nameof(IsCreate));
}
public void UpdatePassword(string password)
{
passwordHash = password.Length >= 6 && !string.IsNullOrWhiteSpace(password)
? LoginViewModel.HashPassword(password) : "";
}
public override bool CanCloseDialog() => !IsLoading;
public override ICommand ConfirmCommand => field ??= new AsyncDelegateCommand(SaveAsync);
private async Task SaveAsync()
{
if (Draft is UserCreateRequest create)
{
create.UserName = UserName.Trim();
if (create.UserName.Length == 0 || passwordHash.Length == 0)
{
ErrorMsg = "请输入用户名和至少 6 位的初始密码。";
return;
}
create.Password = passwordHash;
}
IsLoading = true;
ErrorMsg = "";
try
{
if (Draft is UserCreateRequest newUser)
{
await client.Users.CreateAsync(newUser);
}
else
{
await client.Users.UpdateAsync((UserUpdateRequest)Draft);
}
}
catch (ApiException ex)
{
ErrorMsg = ex.Message;
return;
}
catch (ValidationException ex)
{
ErrorMsg = ex.Message;
return;
}
finally
{
IsLoading = false;
}
OnRequestClose(new DialogResult(ButtonResult.OK));
}
public override void OnDialogClosed()
{
passwordHash = "";
if (Draft is UserCreateRequest create)
{
create.Password = "";
}
}
}
@@ -0,0 +1,122 @@
using Prism.Commands;
using Prism.Mvvm;
using Prism.Services.Dialogs;
using Store.Management.Api;
using Store.Management.Api.Models;
using Store.Management.Api.Transport;
using Store.Management.Api.Users;
using Store.Management.Framework.Controls;
using Store.Management.Framework.ViewModels;
using Store.Management.Views;
using System.Windows;
namespace Store.Management.ViewModels;
public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogService dialogs) : BindableBase
{
public const int KeywordLimit = 100;
private string appliedKeyword = "";
private int pageSize = 50;
public string Keyword { get; set; } = "";
public LoadingStatus Status { get; } = new();
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), _ => !Status.IsLoading);
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);
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 (Status.IsLoading)
{
return;
}
Status.Initialize("正在查询用户…");
try
{
UserSearchRequest request = new() { Keyword = keyword, Page = page, PageSize = size };
PageResult<UserInfo> result = await client.Users.SearchAsync(request);
// 删除末页最后一条记录后,按服务端总数回到有效页。
int lastPage = (int)Math.Clamp((result.Total - 1) / size + 1, 1, page);
if (page > lastPage)
{
request.Page = lastPage;
result = await client.Users.SearchAsync(request);
}
appliedKeyword = keyword;
pageSize = size;
SelectedUser = null;
Users = result.Items;
Total = (int)Math.Min(result.Total, int.MaxValue);
Page = request.Page;
}
catch (ApiException ex)
{
SelectedUser = null;
Users = [];
Total = 0;
Page = 1;
appliedKeyword = keyword;
pageSize = size;
Status.Error(ex.Message);
}
finally
{
Status.Done();
}
}
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);
}
}
private async Task DeleteAsync()
{
UserInfo user = SelectedUser!;
if (MessageBox.Show($"确定删除用户“{user.UserName}”?", "删除用户", MessageBoxButton.OKCancel,
MessageBoxImage.Warning, MessageBoxResult.Cancel) != MessageBoxResult.OK)
{
return;
}
Status.Initialize("正在删除用户…");
try
{
await client.Users.DeleteAsync(user.Id);
}
catch (ApiException ex)
{
Status.Error(ex.Message);
return;
}
Status.Done();
await LoadAsync(appliedKeyword, Page, pageSize);
}
}
@@ -0,0 +1,24 @@
<Window
x:Class="Store.Management.Views.AppSplashScreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="AppSplashScreen"
Width="990"
Height="570"
AllowsTransparency="True"
Background="Transparent"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
mc:Ignorable="d">
<Border>
<!--<Viewbox>
<TextBlock Foreground="#dedede" Text="启动" />
</Viewbox>-->
</Border>
</Window>
@@ -0,0 +1,136 @@
using Store.Management.Framework.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Store.Management.Views
{
/// <summary>
/// AppSplashScreen.xaml 的交互逻辑
/// </summary>
public partial class AppSplashScreen : Window
{
public Action CloseCallBack { get; set; }
public AppSplashScreen()
{
InitializeComponent();
Loaded += StartView_Loaded;
Unloaded += StartView_Unloaded;
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
Activate();
}
private void StartView_Loaded(object sender, RoutedEventArgs e)
{
Activate();
}
private void StartView_Unloaded(object sender, RoutedEventArgs e)
{
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (e.ChangedButton == MouseButton.Left)
{
DragMove();
e.Handled = true;
}
}
protected override async void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
try
{
await LoadApp();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
private async Task LoadApp()
{
SetProgress(0);
Exception exception = null;
await Task.Run(() =>
{
try
{
LoadDLLs();
SetProgress(15);
}
catch (Exception ex)
{
exception = ex;
}
});
if (exception != null)
{
Debug.WriteLine(exception);
SetError(exception);
}
SetProgress(75);
Application.Current.Dispatcher.Invoke(() =>
{
try
{
CloseCallBack?.Invoke();
}
catch (Exception ex)
{
SetError(ex);
Close();
}
finally
{
SetProgress(100);
}
});
}
private void LoadDLLs()
{
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
}
public void SetProgress(double progress)
{
Dispatcher.Invoke(() =>
{
//Progress.Value = progress;
});
}
private void SetError(Exception ex)
{
ApplicationLogService.WriteFatal(ex, "AppSplashScreen");
}
}
}
@@ -0,0 +1,83 @@
<UserControl
x:Class="Store.Management.Views.LeftNavView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:f="Store.Management.Framework"
xmlns:local="clr-namespace:Store.Management.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
d:Background="White"
d:DataContext="{d:DesignInstance Type=vm:LeftNavViewModel}"
d:DesignHeight="450"
d:Width="255"
Background="White"
DataContext="{f:DISource Type=vm:LeftNavViewModel}"
mc:Ignorable="d">
<UserControl.Resources>
<wpf:CommandProxy x:Key="NavItemCommand" Command="{Binding NavItemCommand}" />
<Style
x:Key="NavItemButton"
BasedOn="{StaticResource ButtonCustom}"
TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}" />
</Trigger>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="NavItemTemplate" DataType="{x:Type vm:NavItemViewModel}">
<Button
HorizontalContentAlignment="Stretch"
Command="{StaticResource NavItemCommand}"
CommandParameter="{Binding}"
Style="{StaticResource NavItemButton}">
<Button.Content>
<Border
Height="45"
Background="Transparent"
BorderBrush="{StaticResource SecondaryBorderBrush}"
BorderThickness="0,0,0,1"
Cursor="Hand">
<Grid>
<StackPanel Orientation="Horizontal">
<f:IconText
Margin="10,0,10,0"
FontSize="18"
Icon="{Binding Icon}" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="{Binding Title}" />
</StackPanel>
<Border
Margin="0,0,10,0"
HorizontalAlignment="Right"
Visibility="{Binding IsSelected, Converter={StaticResource Boolean2VisibilityConverter}}">
<f:IconText Icon="ChevronRight" />
</Border>
</Grid>
</Border>
</Button.Content>
</Button>
</DataTemplate>
</UserControl.Resources>
<ItemsControl
d:ItemsSource="{d:SampleData ItemCount=10}"
ItemTemplate="{StaticResource NavItemTemplate}"
ItemsSource="{Binding NavItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<wpf:SpacingStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Store.Management.Views
{
/// <summary>
/// LeftNavView.xaml 的交互逻辑
/// </summary>
public partial class LeftNavView : UserControl
{
public LeftNavView()
{
InitializeComponent();
}
}
}
@@ -10,6 +10,7 @@
Height="620"
MinWidth="440"
MinHeight="540"
d:Background="White"
d:DataContext="{d:DesignInstance Type=vm:LoginViewModel}"
Background="{DynamicResource BackgroundBrush}"
Foreground="{DynamicResource PrimaryTextBrush}"
@@ -21,6 +22,7 @@
<StackPanel MaxWidth="360">
<!-- 系统标识与登录说明 -->
<TextBlock
d:Text="TextBlock"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryBrush}"
@@ -0,0 +1,53 @@
<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>
@@ -0,0 +1,15 @@
using Store.Management.ViewModels;
using System.Windows;
using System.Windows.Controls;
namespace Store.Management.Views;
public partial class UserEditorDialog : UserControl
{
public UserEditorDialog() => InitializeComponent();
private void OnPasswordChanged(object sender, RoutedEventArgs e)
{
((UserEditorDialogViewModel)DataContext).UpdatePassword(((PasswordBox)sender).Password);
}
}
@@ -0,0 +1,98 @@
<UserControl
x:Class="Store.Management.Views.UserManagementPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:f="Store.Management.Framework"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Store.Management.ViewModels"
xmlns:wpf="MAX.WPF"
d:Background="White"
d:DesignHeight="760"
d:DesignWidth="1280"
Loaded="OnLoaded"
mc:Ignorable="d">
<DockPanel Margin="16">
<!-- 查询及用户操作 -->
<wpf:SpacingStackPanel
Margin="0,0,0,12"
DockPanel.Dock="Top"
HorizontalSpacing="8"
Orientation="Horizontal">
<wpf:SpacingStackPanel.Style>
<Style TargetType="wpf:SpacingStackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding Status.IsLoading, Mode=OneWay}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</wpf:SpacingStackPanel.Style>
<Button Command="{Binding LoginCommand}" Content="登录" />
<wpf:TitleContent
Title="关键词"
Placement="Left"
TitleVerticalAlignment="Center">
<TextBox
Width="175"
AutomationProperties.Name="用户查询关键词"
MaxLength="{x:Static vm:UserManagementPageViewModel.KeywordLimit}"
Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchCommand}" />
</TextBox.InputBindings>
</TextBox>
</wpf:TitleContent>
<Button Command="{Binding SearchCommand}" Content="查询" />
<Button Command="{Binding CreateCommand}" Content="创建用户" />
<Button Command="{Binding EditCommand}" Content="编辑" />
<Button Command="{Binding DeleteCommand}" Content="删除" />
</wpf:SpacingStackPanel>
<TextBlock
DockPanel.Dock="Bottom"
Foreground="{DynamicResource DangerBrush}"
Text="{Binding Status.ErrorMessage, Mode=OneWay}"
TextWrapping="Wrap" />
<!-- 用户列表与服务端分页 -->
<f:DataGridEx
AutoGenerateColumns="False"
CanUserSortColumns="False"
IsLoading="{Binding Status.IsLoading, Mode=OneWay}"
IsReadOnly="True"
ItemsSource="{Binding Users, Mode=OneWay}"
LoadingText="{Binding Status.Message, Mode=OneWay}"
NoDataTips="没有符合条件的用户"
PageChangedCommand="{Binding PageCommand}"
PageSize="50"
SelectedItem="{Binding SelectedUser, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Single"
ShowCheckBox="False"
ShowPagination="True"
ShowToolbar="False"
TotalItemCount="{Binding Total, Mode=OneWay}"
PageIndex="{Binding Page, Mode=OneWay}">
<f:DataGridEx.Columns>
<DataGridTextColumn
Width="*"
Binding="{Binding UserName}"
Header="用户名" />
<DataGridTextColumn
Width="*"
Binding="{Binding NickName}"
Header="昵称" />
<DataGridTextColumn
Width="125"
Binding="{Binding Mobile}"
Header="手机号" />
<DataGridTextColumn
Width="*"
Binding="{Binding CompanyName}"
Header="公司名称" />
<DataGridTextColumn
Width="*"
Binding="{Binding Notes}"
Header="备注" />
</f:DataGridEx.Columns>
</f:DataGridEx>
</DockPanel>
</UserControl>
@@ -0,0 +1,16 @@
using Store.Management.Framework.Controls;
using Store.Management.ViewModels;
using System.Windows;
using System.Windows.Controls;
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();
}
}
@@ -28,7 +28,13 @@ handler.Json = """{"code":200,"data":{"total":1,"items":[{"id":3,"userName":"ali
var page = await api.Users.SearchAsync(new UserSearchRequest { Page = 2, PageSize = 10 });
Check(page.Total == 1 && page.Items[0].UserName == "alice", "分页解析失败");
Check(handler.Authorization == "Bearer session-one" && handler.Body!.Contains("\"page\":2"), "Token 或分页请求体错误");
handler.Json = """{"code":200,"data":null}""";
await api.Users.DeleteAsync(3);
Check(handler.Path.EndsWith("/user/DelUser") && handler.Query == "?id=3" && handler.Method == HttpMethod.Post,
"删除用户必须 POST 并通过 query 指定 ID");
var count = handler.Count;
try { await api.Users.DeleteAsync(0); throw new Exception("未校验用户 ID"); }
catch (ArgumentOutOfRangeException) { Check(handler.Count == count, "无效 ID 不应发送"); }
try { await api.Users.SearchAsync(new UserSearchRequest { Page = 0 }); throw new Exception("未校验分页"); }
catch (ValidationException) { Check(handler.Count == count, "无效参数不应发送"); }
handler.Json = """{"code":2001,"msg":"业务失败","traceId":"trace-1","data":null}""";
@@ -153,6 +159,8 @@ sealed class StubHandler : HttpMessageHandler
public HttpStatusCode Status { get; set; } = HttpStatusCode.OK;
public int Count { get; private set; }
public string Path { get; private set; } = "";
public string? Query { get; private set; }
public HttpMethod? Method { get; private set; }
public string? Authorization { get; private set; }
public string? Body { get; private set; }
public Func<CancellationToken, Task>? BeforeResponse { get; set; }
@@ -160,6 +168,8 @@ sealed class StubHandler : HttpMessageHandler
{
Count++;
Path = request.RequestUri!.AbsolutePath;
Query = request.RequestUri.Query;
Method = request.Method;
Authorization = request.Headers.Authorization?.ToString();
Body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
HttpStatusCode status = Status;