Compare commits

..

41 Commits

Author SHA1 Message Date
yxw
43820c6ec2 chore 2025-12-26 16:43:55 +08:00
13cc258cad 修复httpservice获取分页long丢失精度问题 2025-12-26 12:33:13 +08:00
030299fa53 完善国家管理和数据多语言 2025-12-26 00:21:11 +08:00
yxw
46794708ff 处理国家多语言数据 2025-12-25 18:43:30 +08:00
f7bb6bb2dc chore 2025-12-25 12:41:37 +08:00
7b6ec9c1d8 chore 2025-12-25 01:08:36 +08:00
53b6ceaa69 新增国家、州省地区的API 2025-12-25 00:59:03 +08:00
e396e66959 调整地区实体 2025-12-24 12:15:53 +08:00
yxw
a1516490d2 更新数据库 2025-12-23 18:39:39 +08:00
3c4144335f chore 2025-12-23 12:26:25 +08:00
yxw
1f0c84f75e chore 2025-12-22 17:54:57 +08:00
yxw
903d6d9304 添加页面 2025-12-17 17:16:11 +08:00
da8ac8a22b 增加app 版本管理 2025-12-17 00:37:57 +08:00
yxw
9d396fa96e chore 2025-12-16 19:23:39 +08:00
ed32b98867 调整数据库结构,实现消息模板管理 2025-12-16 11:26:55 +08:00
yxw
98e3f7ab73 调整UI布局,消息模版新增语言字段 2025-12-15 18:28:17 +08:00
9b8bf43eb6 chore 2025-12-15 13:04:44 +08:00
yxw
9edff983d8 完善语言文件的生成 2025-12-14 18:27:21 +08:00
54e9c7962d add jobs 2025-12-14 02:43:40 +08:00
0741368b44 chore 2025-12-13 13:11:03 +08:00
yxw
8a1ff0edf9 chore 2025-12-12 16:18:27 +08:00
cd43abc7eb chore 2025-12-12 12:56:22 +08:00
yxw
46e209081d update references 2025-12-11 19:15:02 +08:00
yxw
ed2e3ecd24 fix localization 2025-12-09 19:10:10 +08:00
yxw
2318dff192 fix localization 2025-12-09 17:39:21 +08:00
yxw
7334a9576f merge 2025-12-09 14:19:18 +08:00
yxw
e4550a9533 ? 2025-12-09 14:18:45 +08:00
24512412e5 fix locale 2025-12-09 04:09:33 +08:00
429fb39140 实现多语言切换 2025-12-09 03:31:07 +08:00
yxw
9631e00a12 chore 2025-12-07 18:31:58 +08:00
d91954e331 fix culture 2025-12-07 12:41:04 +08:00
8aca372fc1 chore fix 2025-12-06 13:30:17 +08:00
yxw
2972886576 chore 2025-12-05 15:06:15 +08:00
ec36b7c8b9 chore 2025-12-05 12:26:10 +08:00
cb881ae1e1 fix language edit 2025-12-05 12:22:51 +08:00
ac50b3fccd fix language 2025-12-05 02:13:05 +08:00
00dd4fa958 fix authorize 2025-12-05 00:27:43 +08:00
yxw
bd95848972 fix auth 2025-12-04 19:07:04 +08:00
yxw
6217a8ca55 chore 2025-12-04 17:14:46 +08:00
yxw
6ff39aa3d4 marge 2025-12-04 13:47:56 +08:00
yxw
4702e73b5a fix chore 2025-12-03 18:57:55 +08:00
228 changed files with 9860 additions and 4161 deletions

View File

@@ -7,4 +7,16 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Update="Microsoft.Testing.Extensions.TrxReport" Version="2.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="MSTest.TestAdapter" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
</Project> </Project>

View File

@@ -9,16 +9,24 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AntDesign" Version="1.5.0" /> <Compile Remove="wwwroot\localization\**" />
<Content Remove="wwwroot\localization\**" />
<EmbeddedResource Remove="wwwroot\localization\**" />
<None Remove="wwwroot\localization\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AntDesign" Version="1.5.1" />
<PackageReference Include="AntDesign.ProLayout" Version="1.4.0" /> <PackageReference Include="AntDesign.ProLayout" Version="1.4.0" />
<PackageReference Include="Blazilla" Version="2.0.1" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" /> <PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
<PackageReference Include="TinyMCE.Blazor" Version="2.2.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,90 @@
@inherits ComponentBase
<CascadingAuthenticationState>
<AuthorizeView Context="authContext">
<Authorized>
@if (_isAuthorized)
{
@ChildContent
}
else if (!string.IsNullOrEmpty(NotAuthorizedContent))
{
@NotAuthorizedContent
}
</Authorized>
<NotAuthorized>
@if (!string.IsNullOrEmpty(NotAuthenticatedContent))
{
@NotAuthenticatedContent
}
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthenticationStateTask { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public string? NotAuthorizedContent { get; set; }
[Parameter] public string? NotAuthenticatedContent { get; set; }
[Parameter] public string? Permission { get; set; } // 单个权限
[Parameter] public string[]? AnyPermissions { get; set; } // 多个权限
[Parameter] public string[]? Roles { get; set; } // 多个角色
[Parameter] public string? Policy { get; set; } // 策略名称
private bool _isAuthorized = false;
protected override async Task OnInitializedAsync()
{
// 如果 Claims 中没有权限信息,使用 PermissionService 异步检查
if (AuthenticationStateTask != null)
{
var authState = await AuthenticationStateTask;
var user = authState.User;
if (user.Identity?.IsAuthenticated ?? false)
{
var userPermissions = user.Claims.Where(c => c.Type == ClaimKeys.Permission).Select(c => c.Value).SingleOrDefault()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
if(userPermissions == null)
{
userPermissions = new List<string>();
}
// 检查单个权限
if (Roles?.Length > 0)
{
var hasRole = Roles.Any(role => user.IsInRole(role));
if (!hasRole)
{
_isAuthorized = true;
return;
}
}
if (!string.IsNullOrEmpty(Permission))
{
var hasAllPermissions = userPermissions.Contains(Permission);
if (hasAllPermissions)
{
_isAuthorized = true;
return;
}
}
if (AnyPermissions?.Length > 0)
{
var hasAnyPermission = AnyPermissions.Any(p => userPermissions.Contains(p));
if (!hasAnyPermission)
{
_isAuthorized = true;
return;
}
}
}
else
{
_isAuthorized = false;
}
}
}
}

View File

@@ -1,60 +0,0 @@
@using Microsoft.AspNetCore.Authorization
@inject IPermissionService PermissionService
@inject IAuthorizationService AuthorizationService
<CascadingAuthenticationState>
<AuthorizeView Context="authContext">
<Authorized>
@if (_hasPermission)
{
@ChildContent
}
else if (!string.IsNullOrEmpty(NotAuthorizedContent))
{
@NotAuthorizedContent
}
</Authorized>
<NotAuthorized>
@if (!string.IsNullOrEmpty(NotAuthenticatedContent))
{
@NotAuthenticatedContent
}
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public string? Permission { get; set; }
[Parameter] public string[]? Permissions { get; set; }
[Parameter] public bool RequireAll { get; set; }
[Parameter] public string? Policy { get; set; }
[Parameter] public string? NotAuthorizedContent { get; set; }
[Parameter] public string? NotAuthenticatedContent { get; set; }
private bool _hasPermission;
protected override async Task OnParametersSetAsync()
{
if (!string.IsNullOrEmpty(Policy))
{
var authState = await AuthorizationService.AuthorizeAsync(null, Policy);
_hasPermission = authState.Succeeded;
}
else if (!string.IsNullOrEmpty(Permission))
{
_hasPermission = await PermissionService.HasPermissionAsync(Permission);
}
else if (Permissions != null && Permissions.Length > 0)
{
if (RequireAll)
{
_hasPermission = await PermissionService.HasAllPermissionsAsync(Permissions);
}
else
{
_hasPermission = await PermissionService.HasAnyPermissionAsync(Permissions);
}
}
}
}

View File

@@ -1,29 +0,0 @@
@inject IJSRuntime JSRuntime
@inject ILocalizationService LocalizationService
@inject NavigationManager Navigation
@* <select @bind="_selectedCulture" @onchange="OnCultureChanged">
<option value="en-US">English</option>
<option value="zh-CN">中文</option>
<option value="ja-JP">日本語</option>
</select> *@
@code {
private string _selectedCulture = "en-US";
protected override async Task OnInitializedAsync()
{
_selectedCulture = await JSRuntime.InvokeAsync<string>("blazorCulture.get") ?? "en-US";
}
private async Task OnCultureChanged(ChangeEventArgs e)
{
var culture = e.Value?.ToString();
if (!string.IsNullOrEmpty(culture))
{
await JSRuntime.InvokeVoidAsync("blazorCulture.set", culture);
await LocalizationService.LoadResourcesAsync(culture);
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
}
}
}

View File

@@ -0,0 +1,82 @@
@inject ILocalizationProvider LocalizationProvider
@inject NavigationManager Navigation
@inject IJSRuntime JS
<select @onchange="OnChange" value="@selected">
@foreach (var item in options)
{
<option value="@item.Key">@item.Value</option>
}
</select>
@code {
private string selected = "zh";
private readonly Dictionary<string, string> options = new(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "<22><><EFBFBD><EFBFBD>" },
{ "en", "English" }
};
protected override async Task OnInitializedAsync()
{
var current = LocalizationProvider.CurrentCulture;
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
var found = mapping.FirstOrDefault(kv => string.Equals(kv.Value, current, StringComparison.OrdinalIgnoreCase)).Key;
if (!string.IsNullOrEmpty(found)) selected = found;
LocalizationProvider.LanguageChanged += (s, c) =>
{
var found2 = mapping.FirstOrDefault(kv => string.Equals(kv.Value, c, StringComparison.OrdinalIgnoreCase)).Key;
if (!string.IsNullOrEmpty(found2)) selected = found2;
StateHasChanged();
};
}
private async Task OnChange(ChangeEventArgs e)
{
if (e?.Value is string val)
{
selected = val;
await LocalizationProvider.SetCultureAsync(selected);
try
{
// Use NavigationManager to inspect and modify the current client-side path
var relative = Navigation.ToBaseRelativePath(Navigation.Uri).Trim('/');
var segments = string.IsNullOrEmpty(relative) ? Array.Empty<string>() : relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
string newUrl;
if (segments.Length > 0 && mapping.ContainsKey(segments[0]))
{
// replace existing locale prefix
var remaining = segments.Length > 1 ? "/" + string.Join('/', segments.Skip(1)) : "/";
newUrl = "/" + selected + remaining;
}
else
{
// keep current path, but trigger remount by navigating to same URI
newUrl = Navigation.Uri;
}
// trigger client-side navigation (no hard reload) so components remount and re-evaluate localizer
Navigation.NavigateTo(newUrl, forceLoad: false);
return;
}
catch
{
// fallback: just request UI refresh
StateHasChanged();
}
}
}
}

View File

@@ -1,26 +0,0 @@
@inject ILocalizationService LocalizationService
@Text
@code {
private string? _text;
[Parameter]
public string Key { get; set; } = string.Empty;
[Parameter]
public string? Culture { get; set; }
private string Text => _text ?? Key;
protected override async Task OnParametersSetAsync()
{
await LoadText();
}
private async Task LoadText()
{
_text = await LocalizationService.GetStringAsync(Key, Culture) ?? Key;
StateHasChanged();
}
}

View File

@@ -1,9 +1,41 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
<div style="min-height:100vh"> <div style="min-height:100vh">
@Body @Body
</div> </div>
@code { @code {
protected override void OnInitialized()
{
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged += OnLanguageChanged;
}
}
private void OnLanguageChanged(object? sender, string culture)
{
_ = InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && LocalizationProvider != null)
{
try
{
await LocalizationProvider.InitializeAsync();
}
catch { }
}
}
public void Dispose()
{
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
}
}
} }

View File

@@ -1,5 +1,7 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject ILogger<MainLayout> _logger @inject ILogger<MainLayout> _logger
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
@inject NavigationManager Navigation
<ErrorBoundary> <ErrorBoundary>
<ChildContent> <ChildContent>
@@ -19,6 +21,9 @@
DefaultValue="umi ui" DefaultValue="umi ui"
Options="DefaultOptions" /> Options="DefaultOptions" />
</SpaceItem> *@ </SpaceItem> *@
<SpaceItem>
<Text Type="TextElementType.Warning">@handler</Text>
</SpaceItem>
<SpaceItem> <SpaceItem>
<AntDesign.Tooltip Title="@("Help")" Placement="@Placement.Bottom"> <AntDesign.Tooltip Title="@("Help")" Placement="@Placement.Bottom">
<Unbound> <Unbound>
@@ -42,6 +47,9 @@
<ChildContent> <ChildContent>
@Body @Body
</ChildContent> </ChildContent>
<FooterRender>
<FooterView Copyright="2025 Atomlust.com"></FooterView>
</FooterRender>
</AntDesign.ProLayout.BasicLayout> </AntDesign.ProLayout.BasicLayout>
</ChildContent> </ChildContent>
<ErrorContent Context="ex"> <ErrorContent Context="ex">
@@ -62,6 +70,8 @@
@code { @code {
string handler = "Server";
private ErrorBoundary? _errorBoundary; private ErrorBoundary? _errorBoundary;
private void ResetError(Exception ex) private void ResetError(Exception ex)
@@ -81,10 +91,10 @@
private AvatarMenuItem[] AvatarMenuItems => private AvatarMenuItem[] AvatarMenuItems =>
[ [
new() { Key = "center", IconType = "user", Option = "通知消息"}, new() { Key = "center", IconType = "user", Option = "通知消息" },
new() { Key = "setting", IconType = "setting", Option ="修改资料" }, new() { Key = "setting", IconType = "setting", Option = "修改资料" },
new() { IsDivider = true }, new() { IsDivider = true },
new() { Key = "logout", IconType = "logout", Option = "退出登录"} new() { Key = "logout", IconType = "logout", Option = "退出登录" }
]; ];
public void HandleSelectUser(AntDesign.MenuItem item) public void HandleSelectUser(AntDesign.MenuItem item)
@@ -105,6 +115,14 @@
protected async override Task OnInitializedAsync() protected async override Task OnInitializedAsync()
{ {
if (OperatingSystem.IsBrowser())
{
handler = "Wasm";
}
else
{
handler = "Server";
}
var url = "/api/menu/tree"; var url = "/api/menu/tree";
var apiResult = await HttpService.Get<ApiResult<List<MenuDataItem>>>(url); var apiResult = await HttpService.Get<ApiResult<List<MenuDataItem>>>(url);
@@ -118,5 +136,46 @@
} }
} }
protected override void OnInitialized()
{
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged += OnLanguageChanged;
}
}
private void OnLanguageChanged(object? sender, string culture)
{
// ensure UI updates on the SyncContext; small delay to let cache update
_ = InvokeAsync(async () =>
{
await Task.Yield();
try
{
// Force route and page components to remount and re-render translations
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
}
catch { }
});
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && LocalizationProvider != null)
{
try
{
await LocalizationProvider.InitializeAsync();
}
catch { }
}
}
public void Dispose()
{
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
}
}
} }

View File

@@ -1,15 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema; namespace Atomx.Admin.Client.Models
using System.ComponentModel.DataAnnotations;
namespace Atomx.Admin.Client.Models
{ {
public class AppVersionModel public class AppVersionModel
{ {
/// <summary> /// <summary>
/// 数据ID /// 数据ID
/// </summary> /// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Key]
public long Id { get; set; } public long Id { get; set; }
/// <summary> /// <summary>
@@ -20,43 +15,36 @@ namespace Atomx.Admin.Client.Models
/// <summary> /// <summary>
/// 应用名称KEY /// 应用名称KEY
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public string AppName { get; set; } = string.Empty; public string AppName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 版本标题 /// 版本标题
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 版本 /// 版本
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public string Version { get; set; } = string.Empty; public string Version { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 主版本号(major)无法向下兼容时,需要递增 /// 主版本号(major)无法向下兼容时,需要递增
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public int VersionX { get; set; } public int VersionX { get; set; }
/// <summary> /// <summary>
/// 次版本号(minor)新增新的特性时,需要递增 /// 次版本号(minor)新增新的特性时,需要递增
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public int VersionY { get; set; } public int VersionY { get; set; }
/// <summary> /// <summary>
/// 修订版本号(patch)修复问题时,需要递增 /// 修订版本号(patch)修复问题时,需要递增
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public int VersionZ { get; set; } public int VersionZ { get; set; }
/// <summary> /// <summary>
/// 版本日期 /// 版本日期
/// </summary> /// </summary>
[Column(TypeName = "varchar(64)")]
public int VersionDate { get; set; } public int VersionDate { get; set; }
/// <summary> /// <summary>
@@ -67,7 +55,6 @@ namespace Atomx.Admin.Client.Models
/// <summary> /// <summary>
/// 更新内容说明 /// 更新内容说明
/// </summary> /// </summary>
[Column(TypeName = "text")]
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -10,7 +10,7 @@
/// <summary> /// <summary>
/// 状态 /// 状态
/// </summary> /// </summary>
public string Status { get; set; } = string.Empty; public string? Status { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 开始时间 /// 开始时间

View File

@@ -0,0 +1,8 @@
using Atomx.Common.Entities;
namespace Atomx.Admin.Client.Models
{
public class AreaModel:Area
{
}
}

View File

@@ -0,0 +1,9 @@
namespace Atomx.Admin.Client.Models
{
public class AreaSearch : BaseSearch
{
public long CountryId { get; set; }
public long StateProvinceId { get; set; }
public string Name { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,13 @@
using Atomx.Common.Entities;
namespace Atomx.Admin.Client.Models
{
public class CountryModel:Country
{
/// <summary>
/// 语言
/// </summary>
public string LanguageId { get; set; } = string.Empty;
public List<LocalizedProperty> Localized { get; set; } = new List<LocalizedProperty>();
}
}

View File

@@ -0,0 +1,7 @@
namespace Atomx.Admin.Client.Models
{
public class CountrySearch
{
public string Name { get; set; } = string.Empty;
}
}

View File

@@ -5,7 +5,7 @@
/// <summary> /// <summary>
/// 数据ID /// 数据ID
/// </summary> /// </summary>
public int Id { get; set; } public int? Id { get; set; }
/// <summary> /// <summary>
/// 语言 /// 语言
@@ -52,5 +52,10 @@
/// 是否编辑 /// 是否编辑
/// </summary> /// </summary>
public bool IsEdit { get; set; } public bool IsEdit { get; set; }
/// <summary>
/// 是否主货币
/// </summary>
public bool PrimaryCurrency { get; set; }
} }
} }

View File

@@ -0,0 +1,20 @@
namespace Atomx.Admin.Client.Models
{
public class CurrencySearchModel:BaseSearch
{
/// <summary>
/// 用户名
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 开始时间
/// </summary>
public DateTime?[] RangeTime { get; set; } = new DateTime?[] { null, null };
}
}

View File

@@ -1,15 +1,10 @@
using System.ComponentModel.DataAnnotations; namespace Atomx.Admin.Client.Models
using System.ComponentModel.DataAnnotations.Schema;
namespace Atomx.Admin.Client.Models
{ {
public class LanguageModel public class LanguageModel
{ {
/// <summary> /// <summary>
/// 数据ID /// 数据ID
/// </summary> /// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
@@ -23,7 +18,7 @@ namespace Atomx.Admin.Client.Models
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 语言名称 /// 语言文化
/// </summary> /// </summary>
public string Culture { get; set; } = string.Empty; public string Culture { get; set; } = string.Empty;
@@ -41,15 +36,5 @@ namespace Atomx.Admin.Client.Models
/// 是否可用,系统面 /// 是否可用,系统面
/// </summary> /// </summary>
public bool Enabled { get; set; } public bool Enabled { get; set; }
/// <summary>
/// 多语言资源的版本,可以是时间戳或哈希
/// </summary>
public string ResourceVersion { get; set; } = string.Empty;
/// <summary>
/// 是否编辑
/// </summary>
public bool IsEdit { get; set; }
} }
} }

View File

@@ -0,0 +1,17 @@
using Atomx.Common.Entities;
namespace Atomx.Admin.Client.Models
{
public class LocaleResourceItem:LocaleResource
{
/// <summary>
/// 语言标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 语言名称
/// </summary>
public string Culture { get; set; } = string.Empty;
}
}

View File

@@ -12,6 +12,11 @@
/// </summary> /// </summary>
public int LanguageId { get; set; } public int LanguageId { get; set; }
/// <summary>
/// 语言标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 语言名称 /// 语言名称
/// </summary> /// </summary>
@@ -26,10 +31,5 @@
/// 资源内容值 /// 资源内容值
/// </summary> /// </summary>
public string Value { get; set; } = string.Empty; public string Value { get; set; } = string.Empty;
/// <summary>
/// 是否编辑
/// </summary>
public bool IsEdit { get; set; }
} }
} }

View File

@@ -0,0 +1,11 @@
namespace Atomx.Admin.Client.Models
{
public class LocaleResourceSearch
{
public int LanguageId { get; set; }
public string? Name { get; set; }
public string? Value { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
namespace Atomx.Admin.Client.Models
{
public class MaterialSearch : BaseSearch
{
/// <summary>
/// 类型
/// </summary>
public int? Type { get; set; }
/// <summary>
/// 原料归属公司ID
/// </summary>
public long CorporationId { get; set; }
/// <summary>
/// 原料归属店铺网点ID
/// </summary>
public long StoreId { get; set; }
}
}

View File

@@ -7,6 +7,16 @@
/// </summary> /// </summary>
public long Id { get; set; } public long Id { get; set; }
/// <summary>
/// 站点ID
/// </summary>
public long SiteId { get; set; }
/// <summary>
/// 语言编码
/// </summary>
public int LanguageId { get; set; }
/// <summary> /// <summary>
/// 消息模板类型 /// 消息模板类型
/// </summary> /// </summary>
@@ -32,6 +42,11 @@
/// </summary> /// </summary>
public string Body { get; set; } = string.Empty; public string Body { get; set; } = string.Empty;
/// <summary>
/// 附件文件地址列表,多个附件以逗号分隔
/// </summary>
public string Attachments { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 是否可用 /// 是否可用
/// </summary> /// </summary>

View File

@@ -4,6 +4,8 @@
{ {
public int? Type { get; set; } public int? Type { get; set; }
public int? Language { get; set; }
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
} }
} }

View File

@@ -0,0 +1,8 @@
using Atomx.Common.Entities;
namespace Atomx.Admin.Client.Models
{
public class StateProvinceModel: StateProvince
{
}
}

View File

@@ -0,0 +1,11 @@
using System.Runtime.Serialization;
namespace Atomx.Admin.Client.Models
{
public class StateProvinceSearch
{
[IgnoreDataMember]
public long? CountryId { get; set; }
public string Name { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,24 @@
@page "/content/blog/create"
@page "/content/blog/edit/{id:long}"
@page "/{locale}/content/blog/create"
@page "/{locale}/content/blog/edit/{id:long}"
<PageContainer Title="@(Id > 0 ? "编辑博客文章" : "新增博客文章")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>博客文章</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long? Id { get; set; }
}

View File

@@ -0,0 +1,20 @@
@page "/content/blog/list"
@page "/{locale}/content/blog/list"
<PageContainer Title="博客文章">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>博客文章</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,5 +1,24 @@
<h3>PageEdit</h3> @page "/content/page/create"
@page "/content/page/edit/{id:long}"
@page "/{locale}/content/page/create"
@page "/{locale}/content/page/edit/{id:long}"
<PageContainer Title="@(Id > 0 ? "编辑主题页面" : "新增主题页面")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>主题页面</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
} public string Locale { get; set; } = string.Empty;
[Parameter]
public long? Id { get; set; }
}

View File

@@ -1,8 +1,20 @@
@page "/content/page/list" @page "/content/page/list"
@page "/{locale}/content/page/list"
<PageContainer Title="主题页面">
<h3>PageList</h3> <Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>主题页面</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
} public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,18 +1,69 @@
@page "/counter" @page "/counter"
@page "/{locale}/counter"
<PageTitle>Counter</PageTitle> @using Microsoft.Extensions.Localization
@inject IStringLocalizer<Counter> L
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
<h1>Counter</h1> <PageTitle>@L["site.name"]</PageTitle>
<p role="status">Current count: @currentCount</p> <h1>@L["site.name"]</h1>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <p role="status">@(L["current.count"] ?? "Current count"): @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">@(L["click.me"] ?? "Click me")</button>
<Atomx.Admin.Client.Components.LangSelector />
<div style="margin-top:16px;">
<strong>Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/weather">Weather</NavLink></span>
</div>
<div style="margin-top:16px;">
<strong>zh Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/zh/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/zh/weather">Weather</NavLink></span>
</div>
<div style="margin-top:16px;">
<strong>en Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/en/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/en/weather">Weather</NavLink></span>
</div>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
private int currentCount = 0; private int currentCount = 0;
protected override Task OnInitializedAsync()
{
// localization handled globally in Routes. No per-page initialization needed.
return Task.CompletedTask;
}
private void OnLanguageChanged(object? sender, string culture)
{
// no-op; global router remount will update page translations
}
public void Dispose()
{
// no per-page unsubscribe required
}
private void IncrementCount() private void IncrementCount()
{ {
currentCount++; currentCount++;
} }
private string GetShortCulture(string current)
{
if (string.IsNullOrEmpty(current)) return current;
if (current.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) return "zh";
if (current.StartsWith("en", StringComparison.OrdinalIgnoreCase)) return "en";
var prefix = current.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
return prefix ?? current;
}
} }

View File

@@ -0,0 +1,123 @@
@page "/debug/localization"
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
@inject IHttpClientFactory HttpClientFactory
@inject IJSRuntime JS
@inject ILogger<DebugLocalization> Logger
@inject HttpClient Http
@inject NavigationManager Nav
<h3>Localization Debug</h3>
<p>CurrentCulture: <b>@LocalizationProvider.CurrentCulture</b></p>
<button @onclick="SetProviderZh">Set culture zh</button>
<button @onclick="SetProviderEn">Set culture en</button>
<button @onclick="FetchFiles">Fetch files via HttpClient</button>
<button @onclick="ReadCookie">Read cookie</button>
<h4>Provider Results</h4>
<ul>
@foreach (var kv in providerResults)
{
<li>@kv</li>
}
</ul>
<h4>HTTP Fetch Results</h4>
<ul>
@foreach (var kv in httpResults)
{
<li>@kv</li>
}
</ul>
<h4>JS Read</h4>
<p>@jsResult</p>
@code {
private List<string> providerResults = new();
private List<string> httpResults = new();
private string jsResult = string.Empty;
private async Task SetProviderZh()
{
providerResults.Clear();
try
{
// Use short code; provider will map to full culture and set active culture
await LocalizationProvider.SetCultureAsync("zh");
providerResults.Add($"Set culture to {LocalizationProvider.CurrentCulture}");
providerResults.Add($"site.name={LocalizationProvider.GetString("site.name")}");
providerResults.Add($"login.title={LocalizationProvider.GetString("login.title")}");
}
catch (Exception ex)
{
providerResults.Add("Set provider zh failed: " + ex.Message);
Logger.LogError(ex, "Set provider zh failed");
}
}
private async Task SetProviderEn()
{
providerResults.Clear();
try
{
await LocalizationProvider.SetCultureAsync("en");
providerResults.Add($"Set culture to {LocalizationProvider.CurrentCulture}");
providerResults.Add($"site.name={LocalizationProvider.GetString("site.name")}");
providerResults.Add($"login.title={LocalizationProvider.GetString("login.title")}");
}
catch (Exception ex)
{
providerResults.Add("Set provider en failed: " + ex.Message);
Logger.LogError(ex, "Set provider en failed");
}
}
private async Task FetchFiles()
{
httpResults.Clear();
try
{
// Prefer injected HttpClient that has BaseAddress set in Program.cs for WASM
var client = Http ?? HttpClientFactory.CreateClient();
var urlZ = new Uri(new Uri(Nav.BaseUri), $"localization/zh-Hans.json");
httpResults.Add($"GET {urlZ}");
var resZ = await client.GetAsync(urlZ);
httpResults.Add($"=> {resZ.StatusCode}");
if (resZ.IsSuccessStatusCode)
{
var txt = await resZ.Content.ReadAsStringAsync();
httpResults.Add("zh content len=" + txt.Length);
}
var urlE = new Uri(new Uri(Nav.BaseUri), $"localization/en-US.json");
httpResults.Add($"GET {urlE}");
var resE = await client.GetAsync(urlE);
httpResults.Add($"=> {resE.StatusCode}");
if (resE.IsSuccessStatusCode)
{
var txt = await resE.Content.ReadAsStringAsync();
httpResults.Add("en content len=" + txt.Length);
}
}
catch (Exception ex)
{
httpResults.Add("Fetch error: " + ex.Message);
Logger.LogError(ex, "FetchFiles error");
}
}
private async Task ReadCookie()
{
try
{
jsResult = await JS.InvokeAsync<string>("cookies.Read", "atomx.culture");
}
catch (Exception ex)
{
jsResult = "JS error: " + ex.Message;
}
}
}

View File

@@ -1,24 +1,38 @@
@page "/" @page "/"
@page "/{locale}/"
@attribute [Authorize] @attribute [Authorize]
<PageContainer Title="控制台首页">
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app. <h1>Hello, world!</h1>
<li> Welcome to your new app.
<a href="/category/list">产品分类</a>
</li> <li>
<li> <a href="/category/list">产品分类</a>
<a href="/product/category/edit">产品分类编辑</a> </li>
</li> <li>
<li> <a href="/product/category/edit">产品分类编辑</a>
<a href="/system/language/list">多语言设置</a> </li>
</li> <li>
<li> <a href="/system/language/list">多语言设置</a>
<a href="/system/locale/resource/list">多语言资源设置</a> </li>
</li> <li>
<li> <a href="/system/locale/resource/list">多语言资源设置</a>
<a href="/system/role/list">角色管理</a> </li>
</li> <li>
<a href="/system/role/list">角色管理</a>
</li>
<li>
<a href="/currency/list">货币设置</a>
</li>
<li>
<a href="/country/list">国家管理</a>
</li>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,8 +1,11 @@
@page "/account/login" @page "/account/login"
@page "/{locale}/account/login"
@layout EmptyLayout @layout EmptyLayout
@inject ILogger<Login> Logger @inject ILogger<Login> Logger
@inject IStringLocalizer<Login> L
<PageTitle>登录</PageTitle>
<PageTitle>@L["login.title"]</PageTitle>
@if (!dataLoaded) @if (!dataLoaded)
{ {
@@ -17,41 +20,70 @@ else
<Form @ref="form" Model="@login" OnFinish="LoginAsync"> <Form @ref="form" Model="@login" OnFinish="LoginAsync">
<FluentValidationValidator /> <FluentValidationValidator />
<FormItem> <FormItem>
<AntDesign.Input Placeholder="登录账号" Size="InputSize.Large" @bind-Value="@login.Account"> <AntDesign.Input Placeholder="@L["login.account.placeholder"]" Size="InputSize.Large" @bind-Value="@login.Account">
<Prefix><Icon Type="user" /></Prefix> <Prefix><Icon Type="user" /></Prefix>
</AntDesign.Input> </AntDesign.Input>
</FormItem> </FormItem>
<FormItem> <FormItem>
<AntDesign.Input Placeholder="登录密码" Size="InputSize.Large" @bind-Value="@login.Password" Type="InputType.Password"> <AntDesign.Input Placeholder="@L["login.password.placeholder"]" Size="InputSize.Large" @bind-Value="@login.Password" Type="InputType.Password">
<Prefix><Icon Type="lock" /></Prefix> <Prefix><Icon Type="lock" /></Prefix>
</AntDesign.Input> </AntDesign.Input>
</FormItem> </FormItem>
<FormItem> <FormItem>
<a style="float: left;"> <a style="float: left;">
忘记密码 @L["login.forgot"]
</a> </a>
<a style="float: right;"> <a style="float: right;">
<NavLink href="/register">马上注册</NavLink> <NavLink href="/register">@L["login.register"]</NavLink>
</a> </a>
</FormItem> </FormItem>
<FormItem> <FormItem>
<Button Type="ButtonType.Primary" HtmlType="submit" Class="submit" Size="ButtonSize.Large" Block>登录</Button> <Button Type="ButtonType.Primary" HtmlType="submit" Class="submit" Size="ButtonSize.Large" Block>@L["login.submit"]</Button>
</FormItem>
<FormItem>
<a @onclick="setAccount">
@L["login.setdev"]
</a>
</FormItem> </FormItem>
</Form> </Form>
</Card> </Card>
</GridCol> </GridCol>
</GridRow> </GridRow>
<GridRow Style="padding-top:40px"> <GridRow Style="padding-top:40px">
<span style="font-size:12px;padding-right:3px;">Copyright © 2025-@DateTime.Now.Year Atomlust.com All rights reserved.</span> <span style="font-size:12px;padding-right:3px;">@L["copyright"]</span>
<span style="font-size:12px">runing as @handler</span> <span style="font-size:12px">runing as @handler</span>
</GridRow> </GridRow>
<GridRow>
<Atomx.Admin.Client.Components.LangSelector />
</GridRow>
<GridRow Style="padding-top:10px">
<div>
<strong>Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/counter">Counter</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/weather">Weather</NavLink></span>
</div>
<div>
<strong>zh Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/zh/counter">Counter</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/zh/weather">Weather</NavLink></span>
</div>
<div>
<strong>en Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/en/counter">Counter</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/en/weather">Weather</NavLink></span>
</div>
</GridRow>
</Flex> </Flex>
} }
@code { @code {
string handler = "Server"; string handler = "Server";
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter] [Parameter]
[SupplyParameterFromQuery(Name = "ReturnUrl")] [SupplyParameterFromQuery(Name = "ReturnUrl")]
public string? ReturnUrl { get; set; } public string? ReturnUrl { get; set; }
@@ -104,30 +136,54 @@ else
try try
{ {
// 请求后端登录接口,后端返回 ApiResult<AuthResponse>
var api = "/api/sign/in"; var api = "/api/sign/in";
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
if (result.Success && result.Data != null) if (!OperatingSystem.IsBrowser())
{ {
var auth = result.Data; // Server 模式:使用浏览器发起的 fetch通过 JS并携带 credentials: 'include'
var jsResult = await JS.InvokeAsync<JsonElement>("ajax.Post", api, login);
// 保存 access + refresh 到 localStorageWASM 场景) var result = jsResult.ToJson().FromJson<ApiResult<AuthResponse>>();
await localStorage.SetItemAsync("accessToken", auth.Token); if (result != null && result.Success)
await localStorage.SetItemAsync("refreshToken", auth.RefreshToken);
// 更新客户端 AuthenticationState调用自定义 Provider 更新方法)
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
{ {
// provider 仅需要 access token 更新来触发 UI 更新 var auth = result.Data;
provider.UpdateAuthenticationState(auth.Token); await localStorage.SetItemAsync(StorageKeys.AccessToken, auth.Token);
} await localStorage.SetItemAsync(StorageKeys.RefreshToken, auth.RefreshToken);
Logger.LogInformation("登录成功,跳转: {ReturnUrl}", ReturnUrl); if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
Navigation.NavigateTo(ReturnUrl ?? "/"); {
provider.UpdateAuthenticationState(auth.Token);
}
Logger.LogInformation($"登录成功server 跳转: {ReturnUrl}");
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
}
else
{
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
}
} }
else else
{ {
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message }); // Wasm 模式:保存 localStorage 并更新 AuthStateProvider
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
if (result.Success && result.Data != null)
{
var auth = result.Data;
await localStorage.SetItemAsync(StorageKeys.AccessToken, auth.Token);
await localStorage.SetItemAsync(StorageKeys.RefreshToken, auth.RefreshToken);
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
{
provider.UpdateAuthenticationState(auth.Token);
}
Logger.LogInformation("登录成功wasm 跳转: {ReturnUrl}", ReturnUrl);
Navigation.NavigateTo(ReturnUrl ?? "/");
}
else
{
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -149,4 +205,10 @@ else
await LoginAsync(); await LoginAsync();
} }
} }
void setAccount()
{
login.Account = "admin";
login.Password = "admin888";
}
} }

View File

@@ -1,11 +1,84 @@
@page "/logout" @page "/logout"
@layout EmptyLayout @layout EmptyLayout
@inject IJSRuntime JS
@inject ILogger<Logout> Logger
@inject NavigationManager Navigation
@inject AuthenticationStateProvider AuthStateProvider
@inject HttpService HttpService
@using System.Text.Json
@code { @code {
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await ((PersistentAuthenticationStateProvider)AuthStateProvider).MarkUserAsLoggedOut(); if (!firstRender) return;
Navigation.NavigateTo("/account/login");
await base.OnAfterRenderAsync(firstRender); try
} {
} // 如果运行在浏览器 (WASM),直接调用后端 API 并清除 localStorage / provider
if (OperatingSystem.IsBrowser())
{
Logger.LogInformation("WASM logout: call API and clear local storage");
try
{
await HttpService.Post<ApiResult<string>>("/api/sign/out", null);
}
catch { /* 忽略网络错误,仍继续清理客户端状态 */ }
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
{
await provider.MarkUserAsLoggedOut();
}
Navigation.NavigateTo("/account/login");
}
else
{
// Server 模式:通过浏览器 fetch 发起带凭据的请求以便浏览器接收并删除 Cookie然后强制重载
Logger.LogInformation("Server logout: use browser fetch to call /api/sign/out");
var jsResult = await JS.InvokeAsync<JsonElement>("__atomx_post_json", "/api/sign/out", (object?)null);
// 尝试解析返回,忽略细节
var success = jsResult.ValueKind == JsonValueKind.Object && jsResult.TryGetProperty("success", out var sp) && sp.GetBoolean();
Logger.LogInformation("Server logout result: {Success}", success);
try
{
// 清理 localStorage如果有的话
await localStorage.RemoveItemAsync(StorageKeys.AccessToken);
await localStorage.RemoveItemAsync(StorageKeys.RefreshToken);
}
catch { }
// 尽管我们可能已经处理了服务器态,强制重新加载确保 Circuit 更新
Navigation.NavigateTo("/account/login", forceLoad: true);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Logout failed but proceeding to login page");
Navigation.NavigateTo("/account/login", forceLoad: true);
}
}
}
@* 页面内 JS 辅助:用于在 Server 模式下从浏览器发起 POST 并携带凭证,使浏览器接收 Set-Cookie/删除 Cookie *@
<script>
window.__atomx_post_json = async function (url, data) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: data ? JSON.stringify(data) : null
});
const text = await res.text();
try {
return JSON.parse(text);
} catch {
return { success: res.ok, message: text };
}
} catch (err) {
return { success: false, message: err?.toString() ?? 'network error' };
}
};
</script>

View File

@@ -0,0 +1,20 @@
@page "/deposit/list"
@page "/{locale}/deposit/list"
<PageContainer Title="储值订单">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>储值订单</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,22 @@
@page "/deposit/detail/{id:long}"
@page "/{locale}/deposit/detail/{id:long}"
<PageContainer Title="@($"订单{Id}详情")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>主题页面</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long? Id { get; set; }
}

View File

@@ -1,5 +1,20 @@
<h3>OrderList</h3> @page "/order/list"
@page "/{locale}/order/list"
<PageContainer Title="购物订单">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>购物订单</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
} public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,18 +1,25 @@
@page "/product/category/edit" @page "/product/category/edit"
@page "/product/category/edit/{Id:long?}" @page "/product/category/edit/{Id:long?}"
@page "/{locale}/product/category/edit"
@page "/{locale}/product/category/edit/{Id:long?}"
@inject ILogger<CategoryEdit> Logger @inject ILogger<CategoryEdit> Logger
@* @attribute [Authorize] *@ @attribute [Authorize]
<PageTitle>分类编辑</PageTitle> <PageContainer Title="编辑产品分类">
<Title Level="4">分类信息</Title> <Breadcrumb>
<Breadcrumb>
<Spin Spinning="pageLoading"> <BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<Card Title="" Class="hideborder" <BreadcrumbItem Href="/product/list">系统功能</BreadcrumbItem>
Style="margin-top: 24px;" <BreadcrumbItem>分类管理</BreadcrumbItem>
BodyStyle="padding: 0 32px 40px 32px"> </Breadcrumb>
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="19" OnFinish="OnFormFinishAsync"> </Breadcrumb>
@* @if (languages.Count > 1 && Id > 0) <ChildContent>
<Spin Spinning="pageLoading">
<Card Title="分类信息">
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="19" OnFinish="OnFormFinishAsync">
@* @if (languages.Count > 1 && Id > 0)
{ {
<Tabs ActiveKey="@context.Language" OnTabClick="OnLanguageTabChange"> <Tabs ActiveKey="@context.Language" OnTabClick="OnLanguageTabChange">
<TabPane Key="0"> <TabPane Key="0">
@@ -59,28 +66,33 @@
} }
</Button> <Button Size="@ButtonSize.Large" Type="@ButtonType.Text" Icon="@formModel.Image"></Button> </Button> <Button Size="@ButtonSize.Large" Type="@ButtonType.Text" Icon="@formModel.Image"></Button>
</FormItem> *@ </FormItem> *@
<FormItem Label="Meta描述"> <FormItem Label="Meta描述">
<Input @bind-Value="@context.MetaDescription" Placeholder="Meta描述" /> <Input @bind-Value="@context.MetaDescription" Placeholder="Meta描述" />
</FormItem> </FormItem>
<FormItem Label="Meta关键词"> <FormItem Label="Meta关键词">
<Input @bind-Value="@context.MetaKeywords" Placeholder="Meta关键词" /> <Input @bind-Value="@context.MetaKeywords" Placeholder="Meta关键词" />
</FormItem> </FormItem>
<FormItem Label="显示排序"> <FormItem Label="显示排序">
<Input @bind-Value="@context.DisplayOrder" Placeholder="显示排序" /> <Input @bind-Value="@context.DisplayOrder" Placeholder="显示排序" />
</FormItem> </FormItem>
<FormItem Label="状态"> <FormItem Label="状态">
<Checkbox @bind-Checked="@context.Enabled">启用</Checkbox> <Checkbox @bind-Checked="@context.Enabled">启用</Checkbox>
</FormItem> </FormItem>
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }"> <FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }">
<Button Type="@ButtonType.Primary" HtmlType="submit"> <Button Type="@ButtonType.Primary" HtmlType="submit">
提交保存 提交保存
</Button> </Button>
</FormItem> </FormItem>
</Form> </Form>
</Card> </Card>
</Spin> </Spin>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Lang { get; set; } int? Lang { get; set; }

View File

@@ -1,141 +1,153 @@
@page "/category/list" @page "/category/list"
@page "/{locale}/category/list"
@inject ILogger<CategoryList> Logger @inject ILogger<CategoryList> Logger
@attribute [Authorize] @attribute [Authorize]
<PageTitle>分类管理</PageTitle> <PageContainer Title="产品分类管理">
<Title Level="4">菜单管理</Title> <Breadcrumb>
<Card> <Breadcrumb>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish"> <BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<Row Justify="RowJustify.Start" Gutter="16"> <BreadcrumbItem Href="/product/list">系统功能</BreadcrumbItem>
<Col> <BreadcrumbItem>分类管理</BreadcrumbItem>
<FormItem Label="名称"> </Breadcrumb>
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear /> </Breadcrumb>
</FormItem> <ChildContent>
</Col> <Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col> <Col>
<div class="ant-form-item" style="width:200px;display:flex;"> <div class="ant-form-item" style="width:200px;display:flex;">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button> <Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button> <Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div> </div>
</Col> </Col>
</Row> </Row>
</Form> </Form>
</Card> </Card>
<Card Class="mt-3"> <br/>
<Table DataSource="categories" PageSize="100" HidePagination="true" Resizable> <Card Class="mt-3">
<TitleTemplate> <Table DataSource="categories" PageSize="100" HidePagination="true" Resizable>
<Flex Justify="FlexJustify.SpaceBetween"> <TitleTemplate>
菜单列表 <Flex Justify="FlexJustify.SpaceBetween">
<div> 菜单列表
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <div>
</div> <Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex> </Flex>
</TitleTemplate> </TitleTemplate>
<ColumnDefinitions> <ColumnDefinitions>
<PropertyColumn Property="c=>c.Name" Title="名称"> <PropertyColumn Property="c => c.Name" Title="名称">
<AntDesign.Text>@GetName(context)</AntDesign.Text> <AntDesign.Text>@GetName(context)</AntDesign.Text>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="c=>c.Slug" Title="缩略名" Width="80px" Align="ColumnAlign.Center"> <PropertyColumn Property="c => c.Slug" Title="缩略名" Width="80px" Align="ColumnAlign.Center">
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="c=>c.DisplayOrder" Title="排序" Width="100px" /> <PropertyColumn Property="c => c.DisplayOrder" Title="排序" Width="100px" />
<PropertyColumn Property="c=>c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center"> <PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled) @if (context.Enabled)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text> <AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
} }
else else
{ {
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /> <Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
} }
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="c=>c.CreateTime" Title="时间" Width="190px" /> <PropertyColumn Property="c => c.CreateTime" Title="时间" Width="190px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px"> <ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space> <Space>
@if (context.IsLast) @if (context.IsLast)
{ {
<SpaceItem> <SpaceItem>
<a Href="@($"/attribute/list?categoryid={context.Id}")">属性</a> <a Href="@($"/attribute/list?categoryid={context.Id}")">属性</a>
</SpaceItem> </SpaceItem>
<SpaceItem> <SpaceItem>
<a Href="@($"/specification/list?categoryid={context.Id}")">规格</a> <a Href="@($"/specification/list?categoryid={context.Id}")">规格</a>
</SpaceItem> </SpaceItem>
} }
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e)=>OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='("设置菜单")' OnClose="_=>CloseDrawer()"> <SpaceItem>
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish"> <Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<FluentValidationValidator /> <Overlay>
<FormItem Label="名称"> <Menu>
<Input @bind-Value="model.Name" For="(()=>model.Name)" />
</FormItem>
<FormItem Label="缩略名">
<Input @bind-Value="model.Slug" For="(()=>model.Slug)" />
</FormItem>
<FormItem Label="上级分类">
<Select @bind-Value="@model.ParentId" TItemValue="long" TItem="string" Placeholder="请选择上级分类">
<SelectOptions>
<SelectOption Value="0L" Label="无" />
@foreach (var item in categories)
{
<SelectOption Value="@item.Id" Label="@GetName(item)" />
}
</SelectOptions> <MenuItem>
</Select> <a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</FormItem> </MenuItem>
<FormItem Label="排序"> <MenuDivider />
<Input @bind-Value="model.DisplayOrder" For="(()=>model.DisplayOrder)" /> <MenuItem>
</FormItem> <Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
<FormItem Label="状态"> OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
<Checkbox @bind-Value="model.Enabled" For="(()=>model.Enabled)">启用</Checkbox> OkText="确定"
</FormItem> CancelText="取消">
<FormItem WrapperColOffset="4"> <a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button> </Popconfirm>
</FormItem> </MenuItem>
</Form> </Menu>
</Drawer> </Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='("设置菜单")' OnClose="_ => CloseDrawer()">
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="名称">
<Input @bind-Value="model.Name" For="(()=>model.Name)" />
</FormItem>
<FormItem Label="缩略名">
<Input @bind-Value="model.Slug" For="(()=>model.Slug)" />
</FormItem>
<FormItem Label="上级分类">
<Select @bind-Value="@model.ParentId" TItemValue="long" TItem="string" Placeholder="请选择上级分类">
<SelectOptions>
<SelectOption Value="0L" Label="无" />
@foreach (var item in categories)
{
<SelectOption Value="@item.Id" Label="@GetName(item)" />
}
</SelectOptions>
</Select>
</FormItem>
<FormItem Label="排序">
<Input @bind-Value="model.DisplayOrder" For="(()=>model.DisplayOrder)" />
</FormItem>
<FormItem Label="状态">
<Checkbox @bind-Value="model.Enabled" For="(()=>model.Enabled)">启用</Checkbox>
</FormItem>
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
</FormItem>
</Form>
</Drawer>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }
@@ -333,4 +345,4 @@
drawerVisible = false; drawerVisible = false;
editform.Reset(); editform.Reset();
} }
} }

View File

@@ -120,8 +120,6 @@
bool searchExpand { get; set; } = false; bool searchExpand { get; set; } = false;
private bool drawerVisible; private bool drawerVisible;
SpecificationAttributeOptionModelValidator validator = new();
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();

View File

@@ -0,0 +1,5 @@
<h3>AreaEdit</h3>
@code {
}

View File

@@ -0,0 +1,207 @@
@page "/area/list/{countryId:long}"
@page "/{locale}/area/list/{countryId:long}"
@inject ILogger<CountryList> Logger
@attribute [Authorize]
<PageContainer Title="国家管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem>Home</BreadcrumbItem>
<BreadcrumbItem>系统配置</BreadcrumbItem>
<BreadcrumbItem>国家管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
<div class="ant-form-item">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<Card Title="" Class="hideborder">
<Extra>
<div class="extraContent">
<Button Type="ButtonType.Primary" HtmlType="submit" OnClick="HandleAddNew">新增国家</Button>
</div>
</Extra>
<ChildContent>
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true">
<Selection CheckStrictly />
<PropertyColumn Property="c => c.Name" Title="名称" />
<PropertyColumn Property="c => c.Initial" Title="首字母" />
<PropertyColumn Property="c => c.NumericISOCode" Title="ISO代码" />
<PropertyColumn Property="c => c.Enabled" Title="状态">
@if (context.Enabled)
{
<Text>已激活</text>
}
else
{
<Text>未激活</text>
}
</PropertyColumn>
<PropertyColumn Property="c => c.DisplayOrder" Title="排序" />
<ActionColumn Title="操作" Align="ColumnAlign.Right">
<Space>
<SpaceItem>
<a @onclick="(e)=>HandleEdit(context)">编辑</a>
</SpaceItem>
@*<SpaceItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>*@
</Space>
</ActionColumn>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</ChildContent>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long CountryId { get; set; }
[SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
bool pageLoading = false;
Form<CountrySearch> searchForm = new();
CountrySearch search = new();
PagingList<Country> PagingList = new() { Size = 20 };
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
loadQueryString();
await LoadListAsync();
await base.OnParametersSetAsync();
}
void loadQueryString()
{
var uri = new Uri(Navigation.Uri);
var query = uri.Query;
search.Name = query.GetQueryString("Name");
}
async Task LoadListAsync()
{
try
{
pageLoading = true;
var url = "/api/country/search";
var apiResult = await HttpService.GetPagingList<Country>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
else if (apiResult.Code == 403)
{
ModalService.Error(new ConfirmOptions() { Title = "权限不足", Content = apiResult.Message });
}
StateHasChanged();
}
finally
{
pageLoading = false;
}
}
void OnSearchReset()
{
search = new CountrySearch();
searchForm?.Reset();
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/country/list?page={page}");
}
else
{
Navigation.NavigateTo($"/country/list");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/country/list?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/country/list?{queryString}");
}
}
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void HandleAddNew()
{
Navigation.NavigateTo($"/country/create");
}
void HandleEdit(Country model)
{
Navigation.NavigateTo($"/country/edit/{model.Id}");
}
}

View File

@@ -0,0 +1,202 @@
@page "/country/create"
@page "/country/edit/{id:long}"
@page "/{locale}/country/create"
@page "/{locale}/country/edit/{id:long}"
@inject ILogger<CountryEdit> Logger
@attribute [Authorize]
<PageContainer Title="@(Id > 0 ? "编辑国家信息" : "新增国家信息")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem Href="/country/list">国家管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card Title="国家信息">
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="14" OnFinish="OnFormFinishAsync">
@if (Id > 0 && languageList.Count() > 0)
{
<Tabs ActiveKey="@model.LanguageId" OnTabClick="OnLanguageTabChange">
@* <TabPane Key="0">
<TabTemplate>
<span>标准</span>
</TabTemplate>
</TabPane> *@
@foreach (var item in languageList)
{
<TabPane Key="@item.Key.ToString()">
<TabTemplate>
<span>@item.Value</span>
</TabTemplate>
</TabPane>
}
</Tabs>
}
<FormItem Label="名称" Required>
<Input @bind-Value="@model.Name" Placeholder="国家" />
</FormItem>
<FormItem Label="首字母">
<Input @bind-Value="@model.Initial" Placeholder="首字母" />
</FormItem>
<FormItem Label="两个字母ISO代码">
<Input @bind-Value="@model.TwoLetterISOCode" Placeholder="两个字母ISO代码" />
</FormItem>
<FormItem Label="三字母ISO代码">
<Input @bind-Value="@model.ThreeLetterISOCode" Placeholder="三字母ISO代码" />
</FormItem>
<FormItem Label="数字ISO代码">
<Input @bind-Value="@model.NumericISOCode" Placeholder="数字ISO代码" />
</FormItem>
<FormItem Label="允许发货">
<Checkbox Checked="@model.AllowShipping">允许发货</Checkbox>
</FormItem>
<FormItem Label="显示排序">
<Input @bind-Value="@model.DisplayOrder" Placeholder="显示排序" />
</FormItem>
<FormItem Label="状态">
<Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
</FormItem>
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }">
<Button Type="@ButtonType.Primary" HtmlType="submit" Loading="saving">
提交保存
</Button>
</FormItem>
</Form>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long Id { get; set; }
[SupplyParameterFromForm]
CountryModel model { get; set; } = new();
Form<CountryModel> editform = null!;
CountryLocalizedModel country = new();
List<KeyValue> languageList = new();
bool pageLoading = false;
bool saving = false;
protected override void OnInitialized()
{
base.OnInitialized();
}
protected override void OnParametersSet()
{
_ = LoadLanguage();
if (Id > 0)
{
LoadData();
}
base.OnParametersSet();
}
async Task LoadLanguage()
{
var url = $"/api/language/enabled";
var apiResult = await HttpService.Get<ApiResult<List<KeyValue>>>(url);
if (apiResult.Success)
{
if(apiResult.Data == null)
{
languageList = new List<KeyValue>();
}
else
{
languageList = apiResult.Data;
languageList.Insert(0, new KeyValue() { Key = "0", Value = "标准" });
}
StateHasChanged();
}
}
async void LoadData()
{
pageLoading = true;
var url = $"/api/country/detail?id={Id}";
var apiResult = await HttpService.Get<ApiResult<CountryLocalizedModel>>(url);
if (apiResult.Success)
{
if (apiResult.Data == null)
{
Navigation.NavigateTo($"/country/create");
}
else
{
country = apiResult.Data;
model = apiResult.Data.Adapt<CountryModel>();
}
}
else
{
Navigation.NavigateTo($"/country/create");
}
pageLoading = false;
StateHasChanged();
}
async void OnFormFinishAsync()
{
if (editform.Validate())
{
Console.WriteLine(model.ToJson());
saving = true;
var url = $"api/country/save";
var result = new ApiResult<string>();
result = await HttpService.Post<ApiResult<string>>(url, model);
if (result.Success)
{
saving = false;
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
Navigation.NavigateTo($"/country/list");
}
else
{
saving = false;
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
void OnLanguageTabChange(string key)
{
if (key != "0")
{
model.LanguageId = key;
var data = country.Locales.Where(p => p.LanguageId == key.ToInt()).ToList();
if (data.Any())
{
var name = nameof(model.Name);
model.Name = data.SingleOrDefault(p => p.Key == name)?.Value ?? "";
}
else
{
model.Name = string.Empty;
}
}
else
{
model = country.Adapt<CountryModel>();
model.LanguageId = key;
}
}
}

View File

@@ -0,0 +1,224 @@
@page "/country/list"
@page "/{locale}/country/list"
@inject ILogger<CountryList> Logger
@attribute [Authorize]
<PageContainer Title="国家管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem>国家管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col>
<div class="ant-form-item">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Title="国家列表" Class="hideborder">
<Extra>
<div class="extraContent">
<Button Type="ButtonType.Primary" HtmlType="submit" OnClick="HandleAddNew">新增国家</Button>
</div>
</Extra>
<ChildContent>
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true">
<Selection CheckStrictly />
<PropertyColumn Property="c => c.Name" Title="名称" />
<PropertyColumn Property="c => c.Initial" Title="首字母" />
<PropertyColumn Property="c => c.NumericISOCode" Title="ISO代码" />
<PropertyColumn Property="c => c.Enabled" Title="状态">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.DisplayOrder" Title="排序" />
<ActionColumn Title="操作" Align="ColumnAlign.Right">
<Space>
<SpaceItem>
<a @onclick="(e) => GotoStateProvince(context)">州省管理</a>
</SpaceItem>
<SpaceItem>
<a @onclick="(e) => GotoArea(context)">城市管理</a>
</SpaceItem>
<SpaceItem>
<a @onclick="(e)=>HandleEdit(context)">编辑</a>
</SpaceItem>
@*<SpaceItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>*@
</Space>
</ActionColumn>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</ChildContent>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
bool pageLoading = false;
Form<CountrySearch> searchForm = new();
CountrySearch search = new();
PagingList<Country> PagingList = new() { Size = 20 };
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
loadQueryString();
await LoadListAsync();
await base.OnParametersSetAsync();
}
void loadQueryString()
{
var uri = new Uri(Navigation.Uri);
var query = uri.Query;
search.Name = query.GetQueryString("Name");
}
async Task LoadListAsync()
{
pageLoading = true;
try
{
var url = "/api/country/search";
var apiResult = await HttpService.GetPagingList<Country>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
else if (apiResult.Code == 403)
{
ModalService.Error(new ConfirmOptions() { Title = "权限不足", Content = apiResult.Message });
}
StateHasChanged();
}
finally
{
pageLoading = false;
}
}
void OnSearchReset()
{
search = new CountrySearch();
searchForm?.Reset();
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/country/list?page={page}");
}
else
{
Navigation.NavigateTo($"/country/list");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/country/list?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/country/list?{queryString}");
}
}
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void HandleAddNew()
{
Navigation.NavigateTo($"/country/create");
}
void HandleEdit(Country model)
{
Navigation.NavigateTo($"/country/edit/{model.Id}");
}
void GotoStateProvince(Country model)
{
Navigation.NavigateTo($"/stateprovince/list/{model.Id}");
}
void GotoArea(Country model)
{
Navigation.NavigateTo($"/area/list/{model.Id}");
}
}

View File

@@ -0,0 +1,137 @@
@page "/currency/create"
@page "/currency/edit/{id:long}"
@page "/{locale}/currency/create"
@page "/{locale}/currency/edit/{id:long}"
@inject ILogger<CurrencyEdit> Logger
@attribute [Authorize]
<PageContainer Title="@(Id > 0 ? "编辑货币信息" : "新增货币信息")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统配置</BreadcrumbItem>
<BreadcrumbItem Href="/currency/list">货币管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card Title="货币信息">
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="14" OnFinish="OnFormFinishAsync">
<FormItem Label="名称" Required>
<Input @bind-Value="@context.Name" Placeholder="货币名称" />
</FormItem>
<FormItem Label="货币代码">
<Input @bind-Value="@context.CurrencyCode" Placeholder="货币代码" />
</FormItem>
<FormItem Label="汇率">
<Input @bind-Value="@context.Rate" Placeholder="汇率" />
</FormItem>
<FormItem Label="展示本地">
<SimpleSelect @bind-Value="@context.DisplayLocale" Placeholder="语言文化">
<SelectOptions>
@foreach (var item in LanguageCultures)
{
<SimpleSelectOption Value="@item.Key" Label="@($"{item.Value} - {item.Key}")"></SimpleSelectOption>
}
</SelectOptions>
</SimpleSelect>
</FormItem>
<FormItem Label="自定义格式">
<Input @bind-Value="@context.CustomFormatting" Placeholder="自定义格式" />
</FormItem>
<FormItem Label="显示排序">
<Input @bind-Value="@context.DisplayOrder" Placeholder="显示排序" />
</FormItem>
<FormItem Label="状态">
<Checkbox Checked="@context.Enabled">启用</Checkbox>
</FormItem>
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }">
<Button Type="@ButtonType.Primary" HtmlType="submit" Loading="saving">
提交保存
</Button>
</FormItem>
</Form>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long Id { get; set; }
[SupplyParameterFromForm]
CurrencyModel model { get; set; } = new();
Form<CurrencyModel> editform = null!;
Dictionary<string, string> LanguageCultures = LanguageCulture.Descriptions.ToDictionary();
bool pageLoading = false;
bool saving = false;
protected override void OnInitialized()
{
base.OnInitialized();
}
protected override void OnParametersSet()
{
if (Id > 0)
{
LoadData();
}
base.OnParametersSet();
}
async void LoadData()
{
pageLoading = true;
var url = $"/api/currency/{Id}";
var apiResult = await HttpService.Get<ApiResult<Currency>>(url);
if (apiResult.Success)
{
if (apiResult.Data == null)
{
Navigation.NavigateTo($"/currency/create");
}
else
{
model = apiResult.Data.Adapt<CurrencyModel>();
}
}
else
{
Navigation.NavigateTo($"/currency/create");
}
pageLoading = false;
StateHasChanged();
}
async void OnFormFinishAsync()
{
if (editform.Validate())
{
saving = true;
var url = $"api/currency/save";
var result = new ApiResult<string>();
result = await HttpService.Post<ApiResult<string>>(url, model);
if (result.Code == (int)ResultCode.Success)
{
saving = false;
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
Navigation.NavigateTo($"/currency/list");
}
else
{
saving = false;
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
}

View File

@@ -0,0 +1,263 @@
@page "/currency/list"
@page "/{locale}/currency/list"
@inject ILogger<CurrencyList> Logger
@attribute [Authorize]
<PageContainer Title="货币管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem>货币管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="(16, 16)">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
@if (searchExpand)
{
<AntDesign.Col>
<FormItem Label="发布时间">
<RangePicker @bind-Value="search.RangeTime"></RangePicker>
</FormItem>
</AntDesign.Col>
<AntDesign.Col>
<FormItem Label="状态">
<SimpleSelect DefaultValue="" Style="width:120px;" @bind-Value="@search.Status">
<SelectOptions>
<SimpleSelectOption Value="" Label="全部"></SimpleSelectOption>
<SimpleSelectOption Value="1" Label="草稿"></SimpleSelectOption>
<SimpleSelectOption Value="2" Label="已发布"></SimpleSelectOption>
<SimpleSelectOption Value="3" Label="已删除"></SimpleSelectOption>
</SelectOptions>
</SimpleSelect>
</FormItem>
</AntDesign.Col>
}
<Col>
<div class="ant-form-item" style="display:flex">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
<a style="font-size:12px; display:flex; align-items:center;text-align:center;" @onclick="()=>{searchExpand=!searchExpand;}">
<Icon Type="@(searchExpand?"up":"down")"></Icon> @if (searchExpand)
{
<span>收起</span>
}
else
{
<span>展开</span>
}
</a>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Title="" Class="hideborder">
<Extra>
<div class="extraContent">
<Button Type="ButtonType.Primary" HtmlType="submit" OnClick="HandleAddNew">新增货币</Button>
</div>
</Extra>
<ChildContent>
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true">
<Selection CheckStrictly />
<PropertyColumn Property="c => c.Name" Title="名称" />
<PropertyColumn Property="c => c.CurrencyCode" Title="货币代码" />
<PropertyColumn Property="c => c.Rate" Title="汇率" />
<PropertyColumn Property="c => c.PrimaryCurrency" Title="默认货币">
@if (context.PrimaryCurrency)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme="IconThemeType.Outline" /></AntDesign.Text>
}
else
{
<Icon Type="minus" Theme="IconThemeType.Outline" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.Enabled" Title="状态">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.DisplayOrder" Title="排序" />
<ActionColumn Title="操作" Align="ColumnAlign.Right">
<Space>
<SpaceItem>
<a @onclick="(e)=>HandleEdit(context)">编辑</a>
</SpaceItem>
@*<SpaceItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>*@
</Space>
</ActionColumn>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</ChildContent>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
bool pageLoading = false;
bool searchExpand = false;
Form<CurrencySearchModel> searchForm = new();
CurrencySearchModel search = new();
PagingList<CurrencyModel> PagingList = new() { Size = 20 };
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
loadQueryString();
await LoadListAsync();
await base.OnParametersSetAsync();
}
void loadQueryString()
{
var uri = new Uri(Navigation.Uri);
var query = uri.Query;
search.Name = query.GetQueryString("Name");
search.Status = query.GetQueryString("Status");
var data = query.GetQueryString("RangeTime");
if (!string.IsNullOrEmpty(data))
{
var rangetime = data.Split("-");
if (rangetime != null && rangetime.Length > 0)
{
searchExpand = true;
search.RangeTime[0] = rangetime[0].NumberToDateTime();
search.RangeTime[1] = rangetime[1].NumberToDateTime();
StateHasChanged();
}
}
}
async Task LoadListAsync()
{
try
{
pageLoading = true;
var url = "/api/currency/search";
var apiResult = await HttpService.GetPagingList<CurrencyModel>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
else if (apiResult.Code == 403)
{
ModalService.Error(new ConfirmOptions() { Title = "权限不足", Content = apiResult.Message });
}
StateHasChanged();
}
finally
{
pageLoading = false;
}
}
void OnSearchReset()
{
search = new CurrencySearchModel();
searchForm?.Reset();
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/currency/list?page={page}");
}
else
{
Navigation.NavigateTo($"/currency/list");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/currency/list?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/currency/list?{queryString}");
}
}
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void HandleAddNew()
{
Navigation.NavigateTo($"/currency/create");
}
void HandleEdit(CurrencyModel model)
{
Navigation.NavigateTo($"/currency/edit/{model.Id}");
}
}

View File

@@ -1,144 +1,165 @@
@page "/setting/messagetemplate/list" @page "/setting/messagetemplate/list"
@page "/{locale}/setting/messagetemplate/list"
@inject ILogger<MessageTemplateList> Logger @inject ILogger<MessageTemplateList> Logger
@* @attribute [Authorize] *@ @attribute [Authorize]
<PageTitle>消息模板</PageTitle> <PageContainer Title="消息模板">
<Title Level="4">消息模版管理</Title> <Breadcrumb>
<Card> <Breadcrumb>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish"> <BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<Row Justify="RowJustify.Start" Gutter="16"> <BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<Col> <BreadcrumbItem>系统设置</BreadcrumbItem>
<FormItem Label="名称"> </Breadcrumb>
<Input @bind-Value="search.Key" Placeholder="名称" AllowClear /> </Breadcrumb>
</FormItem> <ChildContent>
</Col> <Card>
<Col> <Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<div class="ant-form-item" style="width:200px;display:flex;"> <Row Justify="RowJustify.Start" Gutter="16">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button> <Col>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button> <FormItem Label="名称">
</div> <Input @bind-Value="search.Key" Placeholder="名称" AllowClear />
</Col> </FormItem>
</Row> </Col>
</Form> <Col>
</Card> <div class="ant-form-item" style="width:200px;display:flex;">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
菜单列表
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Type" Title="模版类型" Width="150px">
@if (context.Type == (int)MessageTemplateType.Message)
{
<AntDesign.Text>站内信</AntDesign.Text>
}
else if (context.Type == (int)MessageTemplateType.Email)
{
<AntDesign.Text>邮件</AntDesign.Text>
}
else if (context.Type == (int)MessageTemplateType.Sms)
{
<AntDesign.Text>短信</AntDesign.Text>
}
</PropertyColumn>
<PropertyColumn Property="c => c.Name" Title="模版名称" />
<PropertyColumn Property="c => c.LanguageId" Title="语言">
@GetLanuageName(context.LanguageId)
</PropertyColumn>
<PropertyColumn Property="c => c.Key" Title="模版Code" Width="100px" />
<PropertyColumn Property="c => c.Title" Title="模版标题" />
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
<Card Class="mt-3"> }
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable> else
<TitleTemplate> {
<Flex Justify="FlexJustify.SpaceBetween"> <Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
菜单列表 }
<div> </PropertyColumn>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <PropertyColumn Property="c => c.CreateTime" Title="时间" Width="190px" />
</div> <ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
</Flex> <Space>
</TitleTemplate> <SpaceItem>
<ColumnDefinitions> <Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<PropertyColumn Property="c => c.Type" Title="模版类型" Width="150px"> <Overlay>
@if (context.Type == (int)MessageTemplateType.Message) <Menu>
<MenuItem>
<a @onclick="(e) => HandleEdit(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{ {
<AntDesign.Text>站内信</AntDesign.Text> <Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
} }
else if (context.Type == (int)MessageTemplateType.Email) </Row>
{ </Card>
<AntDesign.Text>邮件</AntDesign.Text>
}
else if (context.Type == (int)MessageTemplateType.Sms)
{
<AntDesign.Text>短信</AntDesign.Text>
}
</PropertyColumn>
<PropertyColumn Property="c => c.Name" Title="模版名称" />
<PropertyColumn Property="c => c.Key" Title="模版Code" Width="100px" />
<PropertyColumn Property="c => c.Title" Title="模版标题" />
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.CreateTime" Title="时间" Width="190px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e) => HandleEdit(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<Row Justify="RowJustify.End">
<Pagination PageIndex="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
</Row>
</Card>
<Modal Title="@("消息模版设置")" Visible="@modalVisible" Width="700" MaskClosable="true" OkText="@("保存")" CancelText="@("取消")" OnOk="@HandleModalOk" OnCancel="@HandleCancel"> <Modal Title="@("消息模版设置")" Visible="@modalVisible" Width="700" MaskClosable="true" OkText="@("保存")" CancelText="@("取消")" OnOk="@HandleModalOk" OnCancel="@HandleCancel">
<Form Model="@template" @ref="@editForm" LabelCol="new ColLayoutParam { Span = 5 }" WrapperCol="new ColLayoutParam { Span = 15 }" Name="modalForm" OnFinish="OnFormFinish"> <Form Model="@model" @ref="@editForm" LabelCol="new ColLayoutParam { Span = 5 }" WrapperCol="new ColLayoutParam { Span = 15 }" Name="modalForm" OnFinish="OnFormFinish">
<FluentValidationValidator /> <FluentValidationValidator />
<FormItem Label="消息模版类型"> <FormItem Label="消息模版类型">
<SimpleSelect DefaultValue="" Style="width:120px;" @bind-Value="@context.Type"> <SimpleSelect DefaultValue="" Style="width:120px;" @bind-Value="@context.Type">
<SelectOptions> <SelectOptions>
<SimpleSelectOption Value="" Label="请选择消息模版类型"></SimpleSelectOption> <SimpleSelectOption Value="" Label="请选择消息模版类型"></SimpleSelectOption>
<SimpleSelectOption Value="@(((int)MessageTemplateType.Message).ToString())" Label="站内信"></SimpleSelectOption> <SimpleSelectOption Value="@(((int)MessageTemplateType.Message).ToString())" Label="站内信"></SimpleSelectOption>
<SimpleSelectOption Value="@(((int)MessageTemplateType.Email).ToString())" Label="邮件模版"></SimpleSelectOption> <SimpleSelectOption Value="@(((int)MessageTemplateType.Email).ToString())" Label="邮件模版"></SimpleSelectOption>
<SimpleSelectOption Value="@(((int)MessageTemplateType.Sms).ToString())" Label="短信模版"></SimpleSelectOption> <SimpleSelectOption Value="@(((int)MessageTemplateType.Sms).ToString())" Label="短信模版"></SimpleSelectOption>
</SelectOptions> </SelectOptions>
</SimpleSelect> </SimpleSelect>
</FormItem> </FormItem>
<FormItem Label="消息模版名称"> <FormItem Label="消息模版名称">
<Input Placeholder="消息模版名称" @bind-Value="@context.Name" /> <Input Placeholder="消息模版名称" @bind-Value="@context.Name" />
</FormItem> </FormItem>
<FormItem Label="语言">
<FormItem Label="模版Code"> <Select DataSource="@languages" @bind-Value="@context.LanguageId" ItemValue="p => p.Id" ItemLabel="p => p.Title" Disabled="@(context.Id > 0)">
<Input Placeholder="模版Code" @bind-Value="@context.Key" Disabled="@(context.Id > 0)" /> </Select>
</FormItem> </FormItem>
<FormItem Label="消息标题"> <FormItem Label="模版Code">
<Input Placeholder="消息标题" @bind-Value="@context.Title" /> <Input Placeholder="模版Code" @bind-Value="@context.Key" Disabled="@(context.Id > 0)" />
</FormItem> </FormItem>
<FormItem Label="消息内容"> <FormItem Label="消息标题">
<TextArea Placeholder="消息内容" @bind-Value="@context.Body" /> <Input Placeholder="消息标题" @bind-Value="@context.Title" />
</FormItem> </FormItem>
<FormItem Label="状态"> <FormItem Label="消息内容">
<Checkbox @bind-Value="@context.Enabled" Disabled=false> <TextArea Placeholder="消息内容" @bind-Value="@context.Body" />
启用 </FormItem>
</Checkbox> <FormItem Label="状态">
</FormItem> <Checkbox @bind-Value="@context.Enabled" Disabled=false>
</Form> 启用
</Modal> </Checkbox>
</FormItem>
</Form>
</Modal>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }
@@ -147,10 +168,10 @@
Form<MessageTemplateSearch> searchForm = null!; Form<MessageTemplateSearch> searchForm = null!;
[SupplyParameterFromForm] [SupplyParameterFromForm]
MessageTemplateModel template { get; set; } = new(); MessageTemplateModel model { get; set; } = new();
Form<MessageTemplateModel> editForm = null!; Form<MessageTemplateModel> editForm = null!;
List<Language> languages = new();
PagingList<MessageTemplate> PagingList = new(); PagingList<MessageTemplate> PagingList = new();
bool loading { get; set; } = true; bool loading { get; set; } = true;
bool searchExpand { get; set; } = false; bool searchExpand { get; set; } = false;
@@ -166,7 +187,7 @@
{ {
loadQueryString(); loadQueryString();
LoadList(); LoadList();
_ = LoadLanguages();
base.OnParametersSet(); base.OnParametersSet();
} }
@@ -177,6 +198,20 @@
search.Key = query.GetQueryString("Key"); search.Key = query.GetQueryString("Key");
} }
private async Task LoadLanguages()
{
var url = $"/api/language/enabled";
var apiResult = await HttpService.Get<ApiResult<List<Language>>>(url);
if (apiResult.Success)
{
if (apiResult.Data != null)
{
languages = apiResult.Data;
StateHasChanged();
}
}
}
private async void LoadList() private async void LoadList()
{ {
@@ -265,13 +300,13 @@
void OnCreateClick() void OnCreateClick()
{ {
template = new(); model = new();
modalVisible = true; modalVisible = true;
} }
void HandleEdit(MessageTemplate model) void HandleEdit(MessageTemplate data)
{ {
template = model.Adapt<MessageTemplateModel>(); model = data.Adapt<MessageTemplateModel>();
modalVisible = true; modalVisible = true;
} }
@@ -285,18 +320,8 @@
if (editForm.Validate()) if (editForm.Validate())
{ {
var result = new ApiResult<string>(); var result = new ApiResult<string>();
var data = template.Adapt<MessageTemplate>(); var url = $"api/messagetemplate/save";
result = await HttpService.Post<ApiResult<string>>(url, model);
if (template.Id > 0)
{
var url = $"api/messagetemplate/edit";
result = await HttpService.Post<ApiResult<string>>(url, data);
}
else
{
var url = $"api/messagetemplate/add";
result = await HttpService.Post<ApiResult<string>>(url, data);
}
if (result.Code == (int)ResultCode.Success) if (result.Code == (int)ResultCode.Success)
{ {
@@ -314,4 +339,14 @@
{ {
modalVisible = false; modalVisible = false;
} }
string GetLanuageName(int languageId)
{
var language = languages.FirstOrDefault(l => l.Id == languageId);
if (language != null)
{
return language.Title;
}
return "-";
}
} }

View File

@@ -1,7 +1,22 @@
@page "/settings" @page "/settings"
@* @attribute [Authorize] *@ @page "/{locale}/settings"
@attribute [Authorize]
@inject ILogger<Settings> Logger @inject ILogger<Settings> Logger
@code { <PageContainer Title="系统设置">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>系统设置</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
} @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,175 @@
@page "/stateprovince/{countryId:long}/create"
@page "/stateprovince/{countryId:long}/edit/{id:long}"
@page "/{locale}/stateprovince/{countryId:long}/create"
@page "/{locale}/stateprovince/{countryId:long}/edit/{id:long}"
@inject ILogger<StateProvinceEdit> Logger
@attribute [Authorize]
<PageContainer Title="@(Id > 0 ? "编辑州省信息" : "新增州省信息")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem Href="/country/list">国家管理</BreadcrumbItem>
<BreadcrumbItem Href="@($"/stateprovince/list/{CountryId}")">州省管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card Title="州省信息">
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="14" OnFinish="OnFormFinishAsync">
<FormItem Label="国家">
<Input @bind-Value="@country.Name" Placeholder="国家名称" Disabled />
</FormItem>
<FormItem Label="名称" Required>
<Input @bind-Value="@context.Name" Placeholder="名称" />
</FormItem>
<FormItem Label="首字母">
<Input @bind-Value="@context.Initial" Placeholder="首字母" />
</FormItem>
<FormItem Label="缩写">
<Input @bind-Value="@context.Abbreviation" Placeholder="缩写" />
</FormItem>
<FormItem Label="显示排序">
<Input @bind-Value="@context.DisplayOrder" Placeholder="显示排序" />
</FormItem>
<FormItem Label="状态">
<Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
</FormItem>
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }">
<Button Type="@ButtonType.Primary" HtmlType="submit" Loading="saving">
提交保存
</Button>
</FormItem>
</Form>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long CountryId { get; set; }
[Parameter]
public long Id { get; set; }
[SupplyParameterFromForm]
StateProvinceModel model { get; set; } = new();
Form<StateProvinceModel> editform = null!;
List<KeyValue> languageList = new();
Country country = new();
StateProvinceLocalizedModel stateProvince = new();
bool pageLoading = false;
bool saving = false;
protected override void OnInitialized()
{
base.OnInitialized();
}
protected override void OnParametersSet()
{
model.CountryId = CountryId;
_ = LoadLanguage();
_ = LoadCountry();
if (Id > 0)
{
LoadData();
}
base.OnParametersSet();
}
async Task LoadCountry()
{
var url = $"/api/country?id={CountryId}";
var apiResult = await HttpService.Get<ApiResult<Country>>(url);
if (apiResult.Success)
{
if (apiResult.Data != null)
{
country = apiResult.Data;
StateHasChanged();
}
else
{
Navigation.NavigateTo($"/country/list");
}
}
}
async Task LoadLanguage()
{
var url = $"/api/language/enabled";
var apiResult = await HttpService.Get<ApiResult<List<KeyValue>>>(url);
if (apiResult.Success)
{
if (apiResult.Data == null)
{
languageList = new List<KeyValue>();
}
else
{
languageList = apiResult.Data;
languageList.Insert(0, new KeyValue() { Key = "0", Value = "标准" });
}
StateHasChanged();
}
}
async void LoadData()
{
pageLoading = true;
var url = $"/api/stateprovince/detail?id={Id}";
var apiResult = await HttpService.Get<ApiResult<StateProvinceLocalizedModel>>(url);
if (apiResult.Success)
{
if (apiResult.Data == null)
{
Navigation.NavigateTo($"/country/list");
}
else
{
stateProvince = apiResult.Data;
model = apiResult.Data.Adapt<StateProvinceModel>();
}
}
else
{
Navigation.NavigateTo($"/country/list");
}
pageLoading = false;
StateHasChanged();
}
async void OnFormFinishAsync()
{
if (editform.Validate())
{
saving = true;
var url = $"api/stateprovince/save";
var result = new ApiResult<string>();
result = await HttpService.Post<ApiResult<string>>(url, model);
if (result.Code == (int)ResultCode.Success)
{
saving = false;
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
Navigation.NavigateTo($"/stateprovince/list/{CountryId}");
}
else
{
saving = false;
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
}

View File

@@ -0,0 +1,215 @@
@page "/stateprovince/list/{countryId:long}"
@page "/{locale}/stateprovince/list/{countryId:long}"
@inject ILogger<StateProvinceList> Logger
@attribute [Authorize]
<PageContainer Title="州省管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem Href="/country/list">国家管理</BreadcrumbItem>
<BreadcrumbItem>州省管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="(16, 16)">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col>
<div class="ant-form-item" style="display:flex">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Title="州、省列表" Class="hideborder">
<Extra>
<div class="extraContent">
<Button Type="ButtonType.Primary" HtmlType="submit" OnClick="HandleAddNew">新增州省</Button>
</div>
</Extra>
<ChildContent>
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true">
<Selection CheckStrictly />
<PropertyColumn Property="c => c.Name" Title="名称" />
<PropertyColumn Property="c => c.Initial" Title="首字母" />
<PropertyColumn Property="c => c.Abbreviation" Title="缩写" />
<PropertyColumn Property="c => c.Enabled" Title="状态">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.DisplayOrder" Title="排序" />
<ActionColumn Title="操作" Align="ColumnAlign.Right">
<Space>
<SpaceItem>
<a @onclick="(e) => HandleEdit(context)">编辑</a>
</SpaceItem>
@*<SpaceItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>*@
</Space>
</ActionColumn>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</ChildContent>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long CountryId { get; set; }
[SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
bool pageLoading = false;
bool searchExpand = false;
Form<StateProvinceSearch> searchForm = new();
StateProvinceSearch search = new();
PagingList<StateProvince> PagingList = new() { Size = 20 };
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
loadQueryString();
await LoadListAsync();
await base.OnParametersSetAsync();
}
void loadQueryString()
{
var uri = new Uri(Navigation.Uri);
var query = uri.Query;
search.Name = query.GetQueryString("Name");
search.CountryId = CountryId;
}
async Task LoadListAsync()
{
try
{
Console.WriteLine(search.ToJson());
pageLoading = true;
var url = "/api/stateprovince/search";
var apiResult = await HttpService.GetPagingList<StateProvince>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
else if (apiResult.Code == 403)
{
ModalService.Error(new ConfirmOptions() { Title = "权限不足", Content = apiResult.Message });
}
StateHasChanged();
}
finally
{
pageLoading = false;
}
}
void OnSearchReset()
{
search = new();
searchForm?.Reset();
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/stateprovince/list/{CountryId}?page={page}");
}
else
{
Navigation.NavigateTo($"/stateprovince/list/{CountryId}");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/stateprovince/list/{CountryId}?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/stateprovince/list/{CountryId}?{queryString}");
}
}
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void HandleAddNew()
{
Navigation.NavigateTo($"/stateprovince/{CountryId}/create");
}
void HandleEdit(StateProvince model)
{
Navigation.NavigateTo($"/stateprovince/{CountryId}/edit/{model.Id}");
}
}

View File

@@ -0,0 +1,152 @@
@page "/system/app/version/create"
@page "/system/app/version/edit/{id:long}"
@page "/{locale}/system/app/version/create"
@page "/{locale}/system/app/version/edit/{id:long}"
@inject ILogger<AppVersionEdit> Logger
@attribute [Authorize]
<PageContainer Title="@(Id > 0 ? "编辑版本信息" : "新增版本信息")">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>版本管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="pageLoading">
<Card Title="版本信息">
<Form @ref="editform" Model="@model" LabelColSpan="5" WrapperColSpan="14" OnFinish="OnFormFinishAsync">
<FormItem Label="应用名称" Required>
<Input @bind-Value="@context.AppName" Placeholder="应用名称" />
</FormItem>
<FormItem Label="版本标题" Required>
<Input @bind-Value="@context.Title" Placeholder="版本标题" />
</FormItem>
<FormItem Label="运行平台">
<Select TItemValue="int" TItem="int" Style="width:250px;" @bind-Value="@context.Platform">
<SelectOption Value="0" Label="请选择运行平台"></SelectOption>
<SelectOption Value="1" Label="Windows桌面"></SelectOption>
<SelectOption Value="2" Label="安卓系统"></SelectOption>
<SelectOption Value="3" Label="iOS系统"></SelectOption>
</Select>
</FormItem>
<FormItem Label="版本状态">
<Select TItemValue="int" TItem="int" Style="width:250px;" @bind-Value="@context.VersionState">
<SelectOption Value="0" Label="请选版本状态"></SelectOption>
<SelectOption Value="1" Label="开发版"></SelectOption>
<SelectOption Value="2" Label="Alphal内测版"></SelectOption>
<SelectOption Value="3" Label="Beta公测版"></SelectOption>
<SelectOption Value="4" Label="Rc版"></SelectOption>
<SelectOption Value="5" Label="正式版"></SelectOption>
</Select>
</FormItem>
<FormItem Label="版本信息" Required>
<InputGroup Compact>
<Input @bind-Value="@context.VersionX" Placeholder="主版本号" Style="width: 20%;" />
<Input @bind-Value="@context.VersionY" Placeholder="次版本号" Style="width: 20%;" />
<Input @bind-Value="@context.VersionZ" Placeholder="修订版本" Style="width: 20%;" />
</InputGroup>
</FormItem>
<FormItem Label="更新内容" Required>
<TextArea Rows="10" @bind-Value="@context.Content" Placeholder="更新内容" />
</FormItem>
<FormItem Label="状态">
<RadioGroup @bind-Value="@context.Status">
<Radio RadioButton Value=1>草稿</Radio>
<Radio RadioButton Value=2>发布</Radio>
</RadioGroup>
</FormItem>
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 5 }">
<Button Type="@ButtonType.Primary" HtmlType="submit">
提交保存
</Button>
</FormItem>
</Form>
</Card>
</Spin>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public long Id { get; set; }
[SupplyParameterFromForm]
AppVersionModel model { get; set; } = new();
Form<AppVersionModel> editform = null!;
bool pageLoading = false;
protected override void OnInitialized()
{
base.OnInitialized();
}
protected override void OnParametersSet()
{
if (Id > 0)
{
LoadData();
}
base.OnParametersSet();
}
async void LoadData()
{
pageLoading = true;
var url = $"/api/appversion/{Id}";
var apiResult = await HttpService.Get<ApiResult<AppVersion>>(url);
if (apiResult.Success)
{
if (apiResult.Data == null)
{
Navigation.NavigateTo($"/system/app/version/create");
}
else
{
model = apiResult.Data.Adapt<AppVersionModel>();
}
}
else
{
Navigation.NavigateTo($"/system/app/version/create");
}
pageLoading = false;
StateHasChanged();
}
async void OnFormFinishAsync()
{
if (editform.Validate())
{
var result = new ApiResult<string>();
if (model.Id > 0)
{
var url = $"api/appversion/edit";
result = await HttpService.Post<ApiResult<string>>(url, model);
}
else
{
var url = $"api/appversion/add";
result = await HttpService.Post<ApiResult<string>>(url, model);
}
if (result.Code == (int)ResultCode.Success)
{
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
Navigation.NavigateTo($"/system/app/version/list");
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
}

View File

@@ -1,10 +1,318 @@
@page "/system/app/version/list" @page "/system/app/version/list"
@page "/{locale}/system/app/version/list"
@inject ILogger<AppVersionList> Logger @inject ILogger<AppVersionList> Logger
@inject HttpService HttpService @attribute [Authorize]
@* @attribute [Authorize] *@
<PageContainer Title="App版本管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/settings">系统配置</BreadcrumbItem>
<BreadcrumbItem>版本管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Card Class="">
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col>
<FormItem Label="状态">
<SimpleSelect DefaultValue="" Style="width:120px;" @bind-Value="@context.Status">
<SelectOptions>
<SimpleSelectOption Value="" Label="全部"></SimpleSelectOption>
<SimpleSelectOption Value="1" Label="草稿"></SimpleSelectOption>
<SimpleSelectOption Value="2" Label="已发布"></SimpleSelectOption>
</SelectOptions>
</SimpleSelect>
</FormItem>
</Col>
<Col>
<div class="ant-form-item" style="width:200px;display:flex;">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
版本记录
<div>
<AuthorizeCheck Permission="@Permissions.Admin.Create">
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</AuthorizeCheck>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Title" Title="标题">
</PropertyColumn>
<PropertyColumn Property="c => c.AppName" Title="应用">
</PropertyColumn>
<PropertyColumn Property="c => c.Platform" Title="平台">
@if (context.Platform == 1)
{
<Tag>windows</Tag>
}
else if (context.Platform == 2)
{
<Tag>安卓</Tag>
}
else if (context.Platform == 3)
{
<Tag>iOS</Tag>
}
else
{
<Tag>未设置</Tag>
}
</PropertyColumn>
<PropertyColumn Property="c => c.VersionX" Title="版本号">
@context.VersionX.@context.VersionY.@context.VersionZ
</PropertyColumn>
<PropertyColumn Property="c => c.VersionState" Title="版本状态">
@if (context.VersionState == 1)
{
<Tag>开发版</Tag>
}
else if (context.VersionState == 2)
{
<Tag>Alphal内测版</Tag>
}
else if (context.VersionState == 3)
{
<Tag>Beta公测版</Tag>
}
else if (context.VersionState == 4)
{
<Tag>Rc版</Tag>
}
else if (context.VersionState == 5)
{
<Tag>正式版</Tag>
}
else
{
<Tag>未设置</Tag>
}
</PropertyColumn>
<PropertyColumn Property="c => c.Status" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Status == 1)
{
<Tag>草稿</Tag>
}
else if (context.Status == 2)
{
<Tag>发布</Tag>
}
else
{
<Tag>未设置</Tag>
}
</PropertyColumn>
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e) => OnEditClick(context.Id)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</Card>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
} [SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
[SupplyParameterFromForm]
AppVersionSearch search { get; set; } = new();
Form<AppVersionSearch> searchForm = null!;
PagingList<AppVersion> PagingList = new();
bool loading { get; set; } = true;
bool searchExpand { get; set; } = false;
bool drawerVisible;
protected override void OnInitialized()
{
base.OnInitialized();
}
protected override void OnParametersSet()
{
loadQueryString();
_ = LoadList();
base.OnParametersSet();
}
void loadQueryString()
{
var uri = new Uri(Navigation.Uri);
var query = uri.Query;
search.Name = query.GetQueryString("Name");
search.Status = query.GetQueryString("Status");
var data = query.GetQueryString("RangeTime");
if (!string.IsNullOrEmpty(data))
{
var rangetime = data.Split("-");
if (rangetime != null && rangetime.Length > 0)
{
searchExpand = true;
search.RangeTime[0] = rangetime[0].NumberToDateTime();
search.RangeTime[1] = rangetime[1].NumberToDateTime();
StateHasChanged();
}
}
}
private async Task LoadList()
{
loading = true;
var url = "/api/appversion/search";
var apiResult = await HttpService.GetPagingList<AppVersion>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
loading = false;
StateHasChanged();
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
private void OnReset()
{
search = new();
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/system/app/version/list?page={page}");
}
else
{
Navigation.NavigateTo($"/system/app/version/list");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/system/app/version/list?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/system/app/version/list?{queryString}");
}
}
}
async Task HandleDeleteConfirmAsync(MouseEventArgs e, long id)
{
var url = $"/api/appversion/delete/{id}";
var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success)
{
await LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "操作提示", Content = $"数据删除失败.{apiResult.Message}" });
}
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void OnCreateClick()
{
Navigation.NavigateTo($"/system/app/version/create");
}
void OnEditClick(long id)
{
Navigation.NavigateTo($"/system/app/version/edit/{id}");
}
}

View File

@@ -1,7 +1,22 @@
@page "/system/app/list" @page "/system/app/list"
@page "/{locale}/system/app/list"
@inject ILogger<SiteAppList> Logger @inject ILogger<SiteAppList> Logger
@* @attribute [Authorize] *@ @attribute [Authorize]
<PageContainer Title="系统工具">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>系统工具</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
} }

View File

@@ -1,101 +1,120 @@
@page "/system/file/list" @page "/system/file/list"
@page "/{locale}/system/file/list"
@inject ILogger<UploadList> Logger
@attribute [Authorize]
<PageTitle>上传文件</PageTitle> <PageContainer Title="上传文件">
<Title Level="4">上传文件</Title> <Breadcrumb>
<Card> <Breadcrumb>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish"> <BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<Row Justify="RowJustify.Start" Gutter="16"> <BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<Col> <BreadcrumbItem>上传文件</BreadcrumbItem>
<FormItem Label="文件名"> </Breadcrumb>
<Input @bind-Value="search.Name" Placeholder="文件名" AllowClear /> </Breadcrumb>
</FormItem> <ChildContent>
</Col> <Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="文件名">
<Input @bind-Value="search.Name" Placeholder="文件名" AllowClear />
</FormItem>
</Col>
<Col> <Col>
<div class="ant-form-item" style="width:200px;display:flex;"> <div class="ant-form-item" style="width:200px;display:flex;">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button> <Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button> <Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div> </div>
</Col> </Col>
</Row> </Row>
</Form> </Form>
</Card> </Card>
<Card Class="mt-3"> <Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable> <Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate> <TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween"> <Flex Justify="FlexJustify.SpaceBetween">
文件列表 文件列表
<div> <div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">上传新文件</Button> <Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">上传新文件</Button>
</div> </div>
</Flex> </Flex>
</TitleTemplate> </TitleTemplate>
<ColumnDefinitions> <ColumnDefinitions>
<PropertyColumn Property="c => c.Name" Title="文件"> <PropertyColumn Property="c => c.Name" Title="文件">
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="c => c.ContentType" Title="类型" Width="80px" Align="ColumnAlign.Center"> <PropertyColumn Property="c => c.ContentType" Title="类型" Width="80px" Align="ColumnAlign.Center">
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="c => c.Size" Title="文件大小" Width="100px" /> <PropertyColumn Property="c => c.Size" Title="文件大小" Width="100px" />
<PropertyColumn Property="c => c.Type" Title="状态" Width="80px" Align="ColumnAlign.Center"> <PropertyColumn Property="c => c.Type" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Type == 1) @if (context.Type == 1)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.CreateTime" Title="最后登录" Width="190px" />
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text> <Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
} }
else </Row>
{ </Card>
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.CreateTime" Title="最后登录" Width="190px" />
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem> <Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增帐号" : "编辑帐号")' OnClose="_ => CloseDrawer()">
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a> <Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
</MenuItem> <FluentValidationValidator />
<MenuDivider /> <FormItem Label="帐号名称">
<MenuItem> <Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="帐号名称" />
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")" </FormItem>
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增帐号" : "编辑帐号")' OnClose="_ => CloseDrawer()"> <FormItem WrapperColOffset="4">
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish"> <Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
<FluentValidationValidator /> </FormItem>
<FormItem Label="帐号名称"> </Form>
<Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="帐号名称" /> </Drawer>
</FormItem> </ChildContent>
</PageContainer>
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
</FormItem>
</Form>
</Drawer>
@code { @code {
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
@@ -278,9 +297,9 @@
} }
} }
private void OnPageChanged(int args) private void OnPageChanged(PaginationEventArgs args)
{ {
OnSearch(args); OnSearch(args.Page);
} }
void CloseDrawer() void CloseDrawer()

View File

@@ -1,158 +1,168 @@
@page "/admin/list" @page "/admin/list"
@using Atomx.Common.Constants @page "/{locale}/admin/list"
@inject ILogger<AdminList> Logger @inject ILogger<AdminList> Logger
@* @attribute [Authorize] *@ @attribute [Authorize]
<PageTitle>管理员账号管理</PageTitle> <PageContainer Title="管理员管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>管理员管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Card Class="">
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="帐号">
<Input @bind-Value="search.Username" Placeholder="帐号" AllowClear />
</FormItem>
</Col>
<Title Level="4">管理员帐号</Title> <Col>
<Card Class=""> <div class="ant-form-item" style="width:200px;display:flex;">
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish"> <Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Row Justify="RowJustify.Start" Gutter="16"> <Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
<Col> </div>
<FormItem Label="帐号"> </Col>
<Input @bind-Value="search.Username" Placeholder="帐号" AllowClear /> </Row>
</FormItem> </Form>
</Col> </Card>
<br />
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
帐号列表
<div>
<AuthorizeCheck Permission="@Permissions.Admin.Create">
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</AuthorizeCheck>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Username" Title="帐号">
</PropertyColumn>
<PropertyColumn Property="c => c.Email" Title="邮件">
<Col> </PropertyColumn>
<div class="ant-form-item" style="width:200px;display:flex;"> <PropertyColumn Property="c => c.Mobile" Title="手机号" />
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button> <PropertyColumn Property="c => c.Status" Title="状态" Width="80px" Align="ColumnAlign.Center">
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button> @if (context.Status == 1)
</div> {
</Col> <AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
</Row>
</Form>
</Card>
<br />
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
帐号列表
<div>
<AuthorizePermissionView Permission="@Permissions.User.Create">
<button class="btn btn-primary">创建用户</button>
</AuthorizePermissionView>
@* <AuthorizeView Policy="@Permissions.Admin.Edit">
<Authorized>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</Authorized>
<NotAuthorized>
没有权限
</NotAuthorized>
</AuthorizeView> *@
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Username" Title="帐号">
</PropertyColumn>
<PropertyColumn Property="c => c.Email" Title="邮件" Width="80px" Align="ColumnAlign.Center">
</PropertyColumn> }
<PropertyColumn Property="c => c.Mobile" Title="手机号" Width="100px" /> else
<PropertyColumn Property="c => c.Status" Title="状态" Width="80px" Align="ColumnAlign.Center"> {
@if (context.Status == 1) <Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.LastLogin" Title="最后登录" />
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text> <Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
} }
else </Row>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增帐号" : "编辑帐号")' OnClose="_ => CloseDrawer()">
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="帐号名称">
<Input @bind-Value="model.Username" For="(()=>model.Username)" Placeholder="帐号名称" />
</FormItem>
<FormItem Label="电子邮件">
<Input @bind-Value="model.Email" For="(()=>model.Email)" Placeholder="电子邮件" />
</FormItem>
<FormItem Label="手机号码">
<Input @bind-Value="model.Mobile" For="(()=>model.Mobile)" Placeholder="手机号码" />
</FormItem>
@if (context.Id > 0)
{ {
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /> <FormItem Label="密码设置">
<Checkbox @bind-Value="@context.SetPassword" Disabled=false>
重置密码
</Checkbox>
</FormItem>
} }
</PropertyColumn> @if (context.Id == 0)
<PropertyColumn Property="c => c.LastLogin" Title="最后登录" Width="190px" /> {
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" /> <FormItem Label="登录密码">
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px"> <Input @bind-Value="@context.Password" Placeholder="登录密码" />
<Space> </FormItem>
<SpaceItem> }
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })"> @if ((context.Id > 0 && context.SetPassword) || context.Id == 0)
<Overlay> {
<Menu>
<MenuItem> <FormItem Label="新密码">
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a> <Input @bind-Value="@context.NewPassword" Placeholder="新登录密码" />
</MenuItem> </FormItem>
<MenuDivider /> }
<MenuItem> @if ((context.Id > 0 && context.SetPassword) || context.Id == 0)
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")" {
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增帐号" : "编辑帐号")' OnClose="_ => CloseDrawer()"> <FormItem Label="确认密码">
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish"> <Input @bind-Value="@context.RePassword" Placeholder="确认密码" />
<FluentValidationValidator /> </FormItem>
<FormItem Label="帐号名称"> }
<Input @bind-Value="model.Username" For="(()=>model.Username)" Placeholder="帐号名称" /> <FormItem Label="可用状态">
</FormItem> <RadioGroup @bind-Value="@context.Status">
<FormItem Label="电子邮件"> <Radio RadioButton Value=0>禁用</Radio>
<Input @bind-Value="model.Email" For="(()=>model.Email)" Placeholder="电子邮件" /> <Radio RadioButton Value=1>启用</Radio>
</FormItem> </RadioGroup>
<FormItem Label="手机号码"> </FormItem>
<Input @bind-Value="model.Mobile" For="(()=>model.Mobile)" Placeholder="手机号码" /> <FormItem WrapperColOffset="4">
</FormItem> <Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
@if (context.Id > 0) </FormItem>
{ </Form>
<FormItem Label="密码设置"> </Drawer>
<Checkbox @bind-Value="@context.SetPassword" Disabled=false> </ChildContent>
重置密码 </PageContainer>
</Checkbox>
</FormItem>
}
@if (context.Id == 0)
{
<FormItem Label="登录密码">
<Input @bind-Value="@context.Password" Placeholder="登录密码" />
</FormItem>
}
@if ((context.Id > 0 && context.SetPassword) || context.Id == 0)
{
<FormItem Label="新密码">
<Input @bind-Value="@context.NewPassword" Placeholder="新登录密码" />
</FormItem>
}
@if ((context.Id > 0 && context.SetPassword) || context.Id == 0)
{
<FormItem Label="确认密码">
<Input @bind-Value="@context.RePassword" Placeholder="确认密码" />
</FormItem>
}
<FormItem Label="可用状态">
<RadioGroup @bind-Value="@context.Status">
<Radio RadioButton Value=0>禁用</Radio>
<Radio RadioButton Value=1>启用</Radio>
</RadioGroup>
</FormItem>
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
</FormItem>
</Form>
</Drawer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }
@@ -182,7 +192,7 @@
{ {
loadQueryString(); loadQueryString();
LoadList(); _ = LoadList();
base.OnParametersSet(); base.OnParametersSet();
} }
@@ -213,28 +223,34 @@
} }
private async void LoadList() private async Task LoadList()
{ {
loading = true; loading = true;
var url = "/api/admin/search"; var url = "/api/admin/search";
var apiResult = await HttpService.GetPagingList<Admin>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20)); try
if (apiResult.Success)
{ {
if (apiResult.Data != null) var apiResult = await HttpService.GetPagingList<Admin>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{ {
PagingList = apiResult.Data; if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
} }
} }
loading = false; finally
StateHasChanged(); {
loading = false;
StateHasChanged();
}
} }
private void OnReset() private void OnReset()
{ {
search = new(); search = new();
LoadList(); _ = LoadList();
} }
void OnSearchReset() void OnSearchReset()
@@ -279,6 +295,11 @@
} }
} }
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void OnCreateClick() void OnCreateClick()
{ {
model = new(); model = new();
@@ -298,7 +319,7 @@
var apiResult = await HttpService.Post<ApiResult<string>>(url, new()); var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success) if (apiResult.Success)
{ {
LoadList(); _ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" }); await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
} }
else else
@@ -328,7 +349,7 @@
{ {
CloseDrawer(); CloseDrawer();
LoadList(); _ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" }); await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
} }
else else

View File

@@ -1,7 +0,0 @@
@page "/system/currency/list"
<h3>CurrencyList</h3>
@code {
}

View File

@@ -0,0 +1,19 @@
@page "/system/info"
@page "/{locale}/system/info"
<PageContainer Title="系统信息">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>系统信息</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,97 +1,129 @@
@page "/system/language/list" @page "/system/language/list"
@page "/{locale}/system/language/list"
@inject ILogger<LanguageList> Logger @inject ILogger<LanguageList> Logger
@attribute [Authorize]
<PageTitle>语言管理</PageTitle> <PageContainer Title="语言管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>语言管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
多语言列表
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Title" Title="语言名称">
<Title Level="4">多语言</Title> </PropertyColumn>
<PropertyColumn Property="c => c.Name" Title="语言本地化">
</PropertyColumn>
<PropertyColumn Property="c => c.Culture" Title="语言文化" Width="100px" />
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
<Card Class="mt-3"> }
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable> else
<TitleTemplate> {
<Flex Justify="FlexJustify.SpaceBetween"> <Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
多语言列表 }
<div> </PropertyColumn>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <PropertyColumn Property="c => c.ResourceVersion" Title="资源版本" />
</div> <PropertyColumn Property="c => c.UpdateTime" Title="最后更新" />
</Flex> <ActionColumn Title="操作" Align="ColumnAlign.Right">
</TitleTemplate> <Space>
<ColumnDefinitions> <SpaceItem>
<PropertyColumn Property="c => c.Name" Title="帐号"> <a href="@($"/system/locale/resource/list/{context.Id}")"> <Icon Type="@IconType.Outline.Edit" /> 语言资源</a>
</PropertyColumn> </SpaceItem>
<PropertyColumn Property="c => c.Title" Title="邮件" Width="80px" Align="ColumnAlign.Center"> <SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
</PropertyColumn> <MenuItem>
<PropertyColumn Property="c => c.Culture" Title="手机号" Width="100px" /> <a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center"> </MenuItem>
@if (context.Enabled) <MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text> <Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
} }
else </Row>
{ </Card>
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<PropertyColumn Property="c => c.ResourceVersion" Title="最后登录" Width="190px" />
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem> <Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增语言" : "编辑语言")' OnClose="_ => CloseDrawer()">
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a> <Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
</MenuItem> <FluentValidationValidator />
<MenuDivider /> <FormItem Label="语言标题">
<MenuItem> <Input @bind-Value="model.Title" For="(()=>model.Title)" Placeholder="语言名称" />
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")" </FormItem>
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))" <FormItem Label="语言名称">
OkText="确定" <Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="语言本地化" />
CancelText="取消"> </FormItem>
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a> <FormItem Label="语言文化">
</Popconfirm> <SimpleSelect @bind-Value="model.Culture" For="(()=>model.Culture)" Placeholder="语言文化">
</MenuItem> <SelectOptions>
</Menu> @foreach (var item in LanguageCultures)
</Overlay> {
<ChildContent> <SimpleSelectOption Value="@item.Key" Label="@($"{item.Value} - {item.Key}")"></SimpleSelectOption>
<a class="ant-dropdown-link" @onclick:preventDefault> }
<Icon Type="@IconType.Outline.Menu" /> </SelectOptions>
</a> </SimpleSelect>
</ChildContent> </FormItem>
</Dropdown> <FormItem Label="显示排序">
</SpaceItem> <AntDesign.InputNumber @bind-Value="model.DisplayOrder" For="(()=>model.DisplayOrder)" Placeholder="显示排序" />
</Space> </FormItem>
</ActionColumn> <FormItem Label="可用状态">
</ColumnDefinitions> <Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
</Table> </FormItem>
</Card> <FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增帐号" : "编辑帐号")' OnClose="_ => CloseDrawer()"> </FormItem>
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish"> </Form>
<FluentValidationValidator /> </Drawer>
<FormItem Label="语言名称"> </ChildContent>
<Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="语言名称" /> </PageContainer>
</FormItem>
<FormItem Label="语言标题">
<Input @bind-Value="model.Culture" For="(()=>model.Culture)" Placeholder="语言标题" />
</FormItem>
<FormItem Label="手机号码">
<Input @bind-Value="model.FlagImage" For="(()=>model.FlagImage)" Placeholder="手机号码" />
</FormItem>
@* <FormItem Label="可用状态">
<Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
</FormItem> *@
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
</FormItem>
</Form>
</Drawer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }
@@ -99,12 +131,14 @@
int? PageSize { get; set; } int? PageSize { get; set; }
[SupplyParameterFromForm] [SupplyParameterFromForm]
LanguageSearch search { get; set; } = new(); LanguageSearch search { get; set; } = default!;
[SupplyParameterFromForm] [SupplyParameterFromForm]
LanguageModel model { get; set; } = new(); LanguageModel model { get; set; } = default!;
Form<LanguageModel> editform = null!; Form<LanguageModel> editform = null!;
Dictionary<string, string> LanguageCultures = LanguageCulture.Descriptions.ToDictionary();
PagingList<Language> PagingList = new(); PagingList<Language> PagingList = new();
bool loading { get; set; } = true; bool loading { get; set; } = true;
@@ -112,18 +146,20 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
search ??= new LanguageSearch();
model ??= new LanguageModel();
base.OnInitialized(); base.OnInitialized();
} }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
LoadList(); _ = LoadList();
base.OnParametersSet(); base.OnParametersSet();
} }
private async void LoadList() private async Task LoadList()
{ {
loading = true; loading = true;
@@ -169,7 +205,7 @@
void OnCreateClick() void OnCreateClick()
{ {
model = new(); model = new() { Culture = LanguageCulture.zhHans };
drawerVisible = true; drawerVisible = true;
} }
@@ -185,7 +221,7 @@
var apiResult = await HttpService.Post<ApiResult<string>>(url, new()); var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success) if (apiResult.Success)
{ {
LoadList(); _ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" }); await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
} }
else else
@@ -199,25 +235,23 @@
if (editform.Validate()) if (editform.Validate())
{ {
var result = new ApiResult<string>(); var url = $"api/language/save";
if (model.Id > 0) var result = await HttpService.Post<ApiResult<bool>>(url, model);
if (result.Success)
{ {
var url = $"api/language/edit"; if (result.Data)
result = await HttpService.Post<ApiResult<string>>(url, model); {
}
else CloseDrawer();
{ _ = LoadList();
var url = $"api/language/add"; await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
result = await HttpService.Post<ApiResult<string>>(url, model); }
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
} }
if (result.Code == (int)ResultCode.Success)
{
CloseDrawer();
LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
}
else else
{ {
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message }); await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
@@ -225,9 +259,9 @@
} }
} }
private void OnPageChanged(int args) private void OnPageChanged(PaginationEventArgs args)
{ {
OnSearch(args); OnSearch(args.Page);
} }
void CloseDrawer() void CloseDrawer()

View File

@@ -0,0 +1,218 @@
@page "/system/locale/resource/detail/{Name}"
@page "/{locale}/system/locale/resource/detail/{Name}"
@inject ILogger<LocaleResourceList> Logger
@attribute [Authorize]
<PageContainer Title="本地化语言资源">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>本地化语言资源</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="loading">
<Card Class="mt-3">
<Table DataSource="ResourceItems" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
@(@Name)多语言资源列表,可用语言@(@languages.Count)种
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Name" Title="资源Key">
</PropertyColumn>
<PropertyColumn Property="c => c.Title" Title="语言">
</PropertyColumn>
<PropertyColumn Property="c => c.Value" Title="内容">
</PropertyColumn>
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="120px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="100px">
<Space>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
</Spin>
<Modal Title="@("语言资源设置")" Visible="@modalVisible" Width="700" MaskClosable="true" OkText="@("保存")" CancelText="@("取消")" OnOk="@HandleModalOk" OnCancel="@HandleCancel">
<Form Model="@model" @ref="@editform" LabelCol="new ColLayoutParam { Span = 5 }" WrapperCol="new ColLayoutParam { Span = 15 }" Name="modalForm" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="语言文字">
@if (context.Id == 0)
{
<Select DataSource="@languages" @bind-Value="@context.LanguageId" ItemValue="p => p.Id" ItemLabel="p => p.Title">
</Select>
}
else
{
<Input Placeholder="资源名称" @bind-Value="@context.Title" Disabled />
}
</FormItem>
<FormItem Label="资源名称">
<Input Placeholder="资源名称" @bind-Value="@context.Name" Disabled />
</FormItem>
<FormItem Label="资源内容">
<TextArea Placeholder="资源内容" @bind-Value="@context.Value" />
</FormItem>
</Form>
</Modal>
</ChildContent>
</PageContainer>
@code {
bool loading = false;
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public string Name { get; set; }
[SupplyParameterFromForm]
LocaleResourceModel model { get; set; } = default!;
Form<LocaleResourceModel> editform = null!;
List<LocaleResourceItem> ResourceItems = new();
List<Language> languages = new();
bool modalVisible = false;
protected override void OnInitialized()
{
model ??= new LocaleResourceModel() { };
base.OnInitialized();
}
protected override async Task OnParametersSetAsync()
{
_ = LoadLanguages();
_ = LoadList();
}
private async Task LoadLanguages()
{
var url = $"/api/language/enabled";
var apiResult = await HttpService.Get<ApiResult<List<Language>>>(url);
if (apiResult.Success)
{
if (apiResult.Data != null)
{
languages = apiResult.Data;
StateHasChanged();
}
}
}
private async Task LoadList()
{
loading = true;
var url = $"/api/localeresource/{Name}";
var apiResult = await HttpService.Get<ApiResult<List<LocaleResourceItem>>>(url);
if (apiResult.Success)
{
if (apiResult.Data != null)
{
ResourceItems = apiResult.Data;
}
}
loading = false;
StateHasChanged();
}
void OnCreateClick()
{
Console.WriteLine("OnCreateClick");
model = new() { Name = Name };
modalVisible = true;
}
void OnEditClick(LocaleResourceItem data)
{
this.model = data.Adapt<LocaleResourceModel>();
modalVisible = true;
}
async Task HandleDeleteConfirmAsync(MouseEventArgs e, long id)
{
var url = $"/api/localeresource/delete/{id}";
var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success)
{
_ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "操作提示", Content = $"数据删除失败.{apiResult.Message}" });
}
}
void HandleModalOk()
{
editform.Submit();
}
async Task OnFormFinish()
{
if (editform.Validate())
{
var result = new ApiResult<bool>();
var url = $"api/localeresource/save";
result = await HttpService.Post<ApiResult<bool>>(url, model);
if (result.Success)
{
modalVisible = false;
_ = LoadList();
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
void HandleCancel()
{
modalVisible = false;
}
}

View File

@@ -1,6 +1,294 @@
@page "/system/locale/resource/list/{culture}" @page "/system/locale/resource/list/{Id:int}"
@page "/{locale}/system/locale/resource/list/{Id:int}"
@inject ILogger<LocaleResourceList> Logger @inject ILogger<LocaleResourceList> Logger
@attribute [Authorize]
<PageContainer Title="多语言本地资源管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>多语言本地资源管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Spin Spinning="loading">
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col>
<FormItem Label="内容">
<Input @bind-Value="search.Value" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Col>
<div class="ant-form-item" style="width:200px;display:flex;">
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
</div>
</Col>
</Row>
</Form>
</Card>
<br />
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
@(@language.Name)语言资源列表
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Name" Title="资源Key">
</PropertyColumn>
<PropertyColumn Property="c => c.Value" Title="内容">
</PropertyColumn>
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="120px" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="120px">
<Space>
<SpaceItem>
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</SpaceItem>
<SpaceItem>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
<Overlay>
<Menu>
<MenuItem>
<a href="@($"/system/locale/resource/detail/{context.Name}")"> <Icon Type="@IconType.Outline.Edit" /> 其他语言比对</a>
</MenuItem>
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{
<Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
}
</Row>
</Card>
</Spin>
<Modal Title="@("语言资源设置")" Visible="@modalVisible" Width="700" MaskClosable="true" OkText="@("保存")" CancelText="@("取消")" OnOk="@HandleModalOk" OnCancel="@HandleCancel">
<Form Model="@model" @ref="@editform" LabelCol="new ColLayoutParam { Span = 5 }" WrapperCol="new ColLayoutParam { Span = 15 }" Name="modalForm" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="语言文字">
@language.Name
</FormItem>
<FormItem Label="资源名称">
<Input Placeholder="资源名称" @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="资源内容">
<TextArea Placeholder="资源内容" @bind-Value="@context.Value" />
</FormItem>
</Form>
</Modal>
</ChildContent>
</PageContainer>
@code { @code {
bool loading = false;
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter]
public int Id { get; set; }
[SupplyParameterFromQuery]
int? Page { get; set; }
[SupplyParameterFromQuery(Name = "size")]
int? PageSize { get; set; }
[SupplyParameterFromForm]
LocaleResourceSearch search { get; set; } = default!;
Form<LocaleResourceSearch> searchForm = null!;
[SupplyParameterFromForm]
LocaleResourceModel model { get; set; } = default!;
Form<LocaleResourceModel> editform = null!;
Language language = new();
PagingList<LocaleResource> PagingList = new();
bool modalVisible = false;
protected override void OnInitialized()
{
search ??= new LocaleResourceSearch() { LanguageId = Id };
model ??= new LocaleResourceModel() { LanguageId = Id };
base.OnInitialized();
}
protected override async Task OnParametersSetAsync()
{
await LoadLanguage();
await LoadList();
}
private async Task LoadLanguage()
{
var url = $"/api/language/{Id}";
var apiResult = await HttpService.Get<ApiResult<Language>>(url);
if (apiResult.Success)
{
if (apiResult.Data != null)
{
language = apiResult.Data;
}
}
}
private async Task LoadList()
{
loading = true;
var url = $"/api/localeresource/search";
var apiResult = await HttpService.GetPagingList<LocaleResource>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{
if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
}
loading = false;
StateHasChanged();
}
void OnSearchFinish()
{
Page = Page.GetValueOrDefault(1) - 1;
OnSearch(Page.Value);
}
void OnSearchReset()
{
search = new() { LanguageId = Id };
searchForm?.Reset();
OnSearchFinish();
}
void OnCreateClick()
{
Console.WriteLine("OnCreateClick");
model = new() { Culture = LanguageCulture.zhHans, LanguageId = Id };
modalVisible = true;
}
void OnEditClick(LocaleResource data)
{
this.model = data.Adapt<LocaleResourceModel>();
modalVisible = true;
}
async Task HandleDeleteConfirmAsync(MouseEventArgs e, long id)
{
var url = $"/api/localeresource/delete/{id}";
var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success)
{
_ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "操作提示", Content = $"数据删除失败.{apiResult.Message}" });
}
}
private void OnSearch(int page)
{
var queryString = search.BuildQueryString();
if (string.IsNullOrEmpty(queryString))
{
if (page > 1)
{
Navigation.NavigateTo($"/system/locale/resource/list/{Id}?page={page}");
}
else
{
Navigation.NavigateTo($"/system/locale/resource/list/{Id}");
}
}
else
{
if (page > 1)
{
Navigation.NavigateTo($"/system/locale/resource/list/{Id}?page={page}&{queryString}");
}
else
{
Navigation.NavigateTo($"/system/locale/resource/list/{Id}?{queryString}");
}
}
}
private void OnPageChanged(PaginationEventArgs args)
{
OnSearch(args.Page);
}
void HandleModalOk()
{
editform.Submit();
}
async Task OnFormFinish()
{
if (editform.Validate())
{
var result = new ApiResult<bool>();
var url = $"api/localeresource/save";
result = await HttpService.Post<ApiResult<bool>>(url, model);
if (result.Success)
{
modalVisible = false;
_ = LoadList();
}
else
{
await ModalService.ErrorAsync(new ConfirmOptions() { Title = "服务异常", Content = result.Message });
}
}
}
void HandleCancel()
{
modalVisible = false;
}
} }

View File

@@ -1,142 +1,152 @@
@page "/system/menu/list" @page "/system/menu/list"
@page "/{locale}/system/menu/list"
@inject ILogger<MenuList> Logger @inject ILogger<MenuList> Logger
@using MenuItem = Atomx.Admin.Client.Models.MenuItem @using MenuItem = Atomx.Admin.Client.Models.MenuItem
@using Menu = Atomx.Common.Entities.Menu @using Menu = Atomx.Common.Entities.Menu
@* @attribute [Authorize] *@ @attribute [Authorize]
<PageTitle>菜单管理</PageTitle> <PageContainer Title="菜单管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>菜单管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Card>
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish">
<Row Justify="RowJustify.Start" Gutter="16">
<Col>
<FormItem Label="名称">
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear />
</FormItem>
</Col>
<Title Level="4">菜单管理</Title> <Col>
<Card> <div class="ant-form-item" style="width:200px;display:flex;">
<Form @ref="searchForm" Model="search" Layout="FormLayout.Inline" Class="search-form" OnFinish="OnSearchFinish"> <Button Type="ButtonType.Primary" HtmlType="submit">查询</Button>
<Row Justify="RowJustify.Start" Gutter="16"> <Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button>
<Col> </div>
<FormItem Label="名称"> </Col>
<Input @bind-Value="search.Name" Placeholder="名称" AllowClear /> </Row>
</FormItem> </Form>
</Col> </Card>
<Card Class="mt-3">
<Table DataSource="Menus" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
菜单列表
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
<Col> </Flex>
<div class="ant-form-item" style="width:200px;display:flex;"> </TitleTemplate>
<Button Type="ButtonType.Primary" HtmlType="submit">查询</Button> <ColumnDefinitions>
<Button Style="margin: 0 8px;" OnClick="OnSearchReset">重置</Button> <PropertyColumn Property="c => c.Name" Title="名称">
</div> <AntDesign.Text>@GetName(context)</AntDesign.Text>
</Col> </PropertyColumn>
</Row> <PropertyColumn Property="c => c.Icon" Title="图标" Width="60px" Align="ColumnAlign.Center">
</Form> @if (!string.IsNullOrEmpty(context.Icon))
</Card> {
<Card Class="mt-3"> <Icon Type="@context.Icon" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
<Table DataSource="Menus" PageSize="100" HidePagination="true" Resizable> }
<TitleTemplate> </PropertyColumn>
<Flex Justify="FlexJustify.SpaceBetween"> <PropertyColumn Property="c => c.Key" Title="分组标识" Ellipsis />
菜单列表 <PropertyColumn Property="c => c.Url" Title="链接" />
<div> <PropertyColumn Property="c => c.DisplayOrder" Title="排序" Width="80px" Align="ColumnAlign.Center" />
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
</div> @if (context.Enabled)
{
</Flex> <AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
</TitleTemplate>
<ColumnDefinitions> }
<PropertyColumn Property="c=>c.Name" Title="名称"> else
<AntDesign.Text>@GetName(context)</AntDesign.Text> {
</PropertyColumn> <Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
<PropertyColumn Property="c=>c.Icon" Title="图标" Width="60px" Align="ColumnAlign.Center"> }
@if (!string.IsNullOrEmpty(context.Icon)) { </PropertyColumn>
<Icon Type="@context.Icon" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /> <PropertyColumn Property="c => c.CreateTime" Title="时间" Width="150px" />
} <ActionColumn Title="操作" Align="ColumnAlign.Right" Width="120px">
</PropertyColumn> <Space>
<PropertyColumn Property="c=>c.Key" Title="分组标识" Ellipsis/> <SpaceItem>
<PropertyColumn Property="c=>c.Url" Title="链接" /> <a @onclick="(e) => OnEditClick(context)">编辑</a>
<PropertyColumn Property="c=>c.DisplayOrder" Title="排序" Width="80px" Align="ColumnAlign.Center" /> </SpaceItem>
<PropertyColumn Property="c=>c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center"> <SpaceItem>
@if (context.Enabled) <Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='("设置菜单")' OnClose="_ => CloseDrawer()">
<Form LabelColSpan="4" @ref="@editform" Model="@menu" Class="px-5" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="名称">
<Input @bind-Value="menu.Name" For="(()=>menu.Name)" />
</FormItem>
<FormItem Label="分组标识">
<Input @bind-Value="menu.Key" For="(()=>menu.Key)" />
</FormItem>
<FormItem Label="链接">
<Input @bind-Value="menu.Url" For="(()=>menu.Url)" />
</FormItem>
<FormItem Label="上级分类">
<Select @bind-Value="@menu.ParentId" TItemValue="long" TItem="string" Placeholder="请选择上级分类">
<SelectOptions>
<SelectOption Value="0L" Label="无" />
@foreach (var item in Menus)
{
<SelectOption Value="@item.Id" Label="@GetName(item)" />
}
</SelectOptions>
</Select>
</FormItem>
@if (menu.ParentId == 0)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
<FormItem Label="图标">
<Select ItemValue="c => c"
ItemLabel="c => c"
DataSource="IconsExtension.IconList()"
@bind-Value="@menu.Icon">
<ItemTemplate Context="Icon">
<p class="mt-2"><Icon Type="@Icon" /> @Icon</p>
</ItemTemplate>
<LabelTemplate Context="Icon">
<p class="mt-1"><Icon Type="@Icon" /> @Icon</p>
</LabelTemplate>
</Select>
</FormItem>
} }
else <FormItem Label="显示排序">
{ <Input @bind-Value="menu.DisplayOrder" For="(()=>menu.DisplayOrder)" />
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /> </FormItem>
} <FormItem Label="状态">
</PropertyColumn> <Checkbox T="bool" Label="启用" @bind-value="menu.Enabled" Size="InputSize.Small" Class="ps-0" />
<PropertyColumn Property="c=>c.CreateTime" Title="时间" Width="150px" /> </FormItem>
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="120px"> <FormItem WrapperColOffset="4">
<Space> <Button Type="ButtonType.Primary" HtmlType="submit" Class="mt-5" Style="width: 100%;">保存</Button>
<SpaceItem> </FormItem>
<a @onclick="(e)=>OnEditClick(context)">编辑</a> </Form>
</SpaceItem> </Drawer>
<SpaceItem> </ChildContent>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")" </PageContainer>
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a>删除</a>
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='("设置菜单")' OnClose="_=>CloseDrawer()">
<Form LabelColSpan="4" @ref="@editform" Model="@menu" Class="px-5" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="名称">
<Input @bind-Value="menu.Name" For="(()=>menu.Name)" />
</FormItem>
<FormItem Label="分组标识">
<Input @bind-Value="menu.Key" For="(()=>menu.Key)" />
</FormItem>
<FormItem Label="链接">
<Input @bind-Value="menu.Url" For="(()=>menu.Url)" />
</FormItem>
<FormItem Label="上级分类">
<Select @bind-Value="@menu.ParentId" TItemValue="long" TItem="string" Placeholder="请选择上级分类">
<SelectOptions>
<SelectOption Value="0L" Label="无" />
@foreach(var item in Menus)
{
<SelectOption Value="@item.Id" Label="@GetName(item)" />
}
</SelectOptions>
</Select>
</FormItem>
@if (menu.ParentId == 0)
{
<FormItem Label="图标">
<Select ItemValue="c=>c"
ItemLabel="c=>c"
DataSource="IconsExtension.IconList()"
@bind-Value="@menu.Icon">
<ItemTemplate Context="Icon">
<p class="mt-2"><Icon Type="@Icon" /> @Icon</p>
</ItemTemplate>
<LabelTemplate Context="Icon">
<p class="mt-1"><Icon Type="@Icon" /> @Icon</p>
</LabelTemplate>
</Select>
</FormItem>
}
<FormItem Label="显示排序">
<Input @bind-Value="menu.DisplayOrder" For="(()=>menu.DisplayOrder)" />
</FormItem>
<FormItem Label="状态">
<Checkbox T="bool" Label="启用" @bind-value="menu.Enabled" Size="InputSize.Small" Class="ps-0" />
</FormItem>
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Class="mt-5" Style="width: 100%;">保存</Button>
</FormItem>
</Form >
</Drawer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }
@@ -285,7 +295,7 @@
CloseDrawer(); CloseDrawer();
LoadList(); LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title="提示", Content="数据提交成功!" }); await ModalService.InfoAsync(new ConfirmOptions() { Title = "提示", Content = "数据提交成功!" });
} }
else else
{ {
@@ -294,7 +304,7 @@
} }
} }
private string GetPath(string path) private string GetPath(string path)
@@ -335,7 +345,7 @@
return name; return name;
} }
private void OpenDrawer() private void OpenDrawer()
{ {

View File

@@ -1,89 +1,103 @@
@page "/system/role/permission/{RoleId:long}" @page "/system/role/permission/{RoleId:long}"
@page "/{locale}/system/role/permission/{RoleId:long}"
@attribute [Authorize]
@inject ILogger<RoleList> Logger @inject ILogger<RoleList> Logger
<PageTitle>权限设置</PageTitle> <PageContainer Title="权限角色设置">
<Spin Spinning="loading"> <Breadcrumb>
<Title Level="4">编辑角色权限</Title> <Breadcrumb>
<Card> <BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<Form @ref="editForm" Model="model" LabelColSpan="2" WrapperColSpan="22" Class="search-form" OnFinish="OnFormFinishAsync"> <BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<FormItem Label="角色"> <BreadcrumbItem>权限编辑</BreadcrumbItem>
为角色 <Text>@role?.Name</Text> 设置权限 </Breadcrumb>
</FormItem> </Breadcrumb>
<FormItem Label="权限"> <ChildContent>
<div class="ant-form-item-control-input-content"> <Spin Spinning="loading">
@if (!PermissionGroups.Any()) <Card>
{ <Form @ref="editForm" Model="model" LabelColSpan="2" WrapperColSpan="22" Class="search-form" OnFinish="OnFormFinishAsync">
<div> <FormItem Label="角色">
<p>暂无权限可设置</p> 为角色 <Text>@role?.Name</Text> 设置权限
</div> </FormItem>
} <FormItem Label="权限">
else <div class="ant-form-item-control-input-content">
{ @if (!PermissionGroups.Any())
@foreach (var group in PermissionGroups) {
{ <div>
<GridRow Style="padding-top:10px;"> <p>暂无权限可设置</p>
<GridCol Span="24"> </div>
<label class="ant-checkbox-wrapper"> }
<span class="ant-checkbox @((@group.PermissionItems.Count(p => p.IsSelected) > 0 && @group.PermissionItems.Count(p => p.IsSelected) < @group.PermissionItems.Count) ? "ant-checkbox-indeterminate" : "")"> else
<input class="ant-checkbox-input" type="checkbox" {
@onchange="(e) => ToggleAllPermissions(group, (bool)e.Value!)" @foreach (var group in PermissionGroups)
checked="@group.PermissionItems.All(p => p.IsSelected)" />
<span class="ant-checkbox-inner"></span>
</span>
<span>
<span class="form-check-label fw-bold">
@group.CategoryName
</span>
<small class="text-muted ms-2">
(@group.PermissionItems.Count(p => p.IsSelected)/@group.PermissionItems.Count)
</small>
</span>
</label>
</GridCol>
</GridRow>
<GridRow Style="margin-left:20px;margin-right:20px;">
@foreach (var permission in group.PermissionItems)
{ {
<GridCol Span="6"> <GridRow Style="padding-top:10px;">
<label class="ant-checkbox-wrapper"> <GridCol Span="24">
<span class="ant-checkbox"> <label class="ant-checkbox-wrapper">
<input class="ant-checkbox-input" type="checkbox" <span class="ant-checkbox @((@group.PermissionItems.Count(p => p.IsSelected) > 0 && @group.PermissionItems.Count(p => p.IsSelected) < @group.PermissionItems.Count) ? "ant-checkbox-indeterminate" : "")">
@bind="permission.IsSelected" <input class="ant-checkbox-input" type="checkbox"
id="perm_@permission.Name" /> @onchange="(e) => ToggleAllPermissions(group, (bool)e.Value!)"
<span class="ant-checkbox-inner"></span> checked="@group.PermissionItems.All(p => p.IsSelected)" />
<span class="ant-checkbox-inner"></span>
</span> </span>
<span class="form-check-label" for="perm_@permission.Name"> <span>
@permission.Description <span class="form-check-label fw-bold">
<small class="text-muted d-block">@permission.Name</small> @group.CategoryName
</span> </span>
</label> <small class="text-muted ms-2">
</GridCol> (@group.PermissionItems.Count(p => p.IsSelected)/@group.PermissionItems.Count)
</small>
</span>
</label>
</GridCol>
</GridRow>
<GridRow Style="margin-left:20px;margin-right:20px;">
@foreach (var permission in group.PermissionItems)
{
<GridCol Span="6">
<label class="ant-checkbox-wrapper">
<span class="ant-checkbox">
<input class="ant-checkbox-input" type="checkbox"
@bind="permission.IsSelected"
id="perm_@permission.Name" />
<span class="ant-checkbox-inner"></span>
</span>
<span class="form-check-label" for="perm_@permission.Name">
@permission.Description
<small class="text-muted d-block">@permission.Name</small>
</span>
</label>
</GridCol>
}
</GridRow>
} }
</GridRow> }
} </div>
} </FormItem>
</div> <FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 2 }">
</FormItem> <Button Type="@ButtonType.Primary" HtmlType="submit" Loading="@isSaving">
<FormItem WrapperCol="new ColLayoutParam { Span = 24, Offset = 2 }"> @if (isSaving)
<Button Type="@ButtonType.Primary" HtmlType="submit" Loading="@isSaving"> {
@if (isSaving) <span>保存中...</span>
{ }
<span>保存中...</span> else
} {
else <span>保存权限设置</span>
{ }
<span>保存权限设置</span>
}
</Button> </Button>
</FormItem> </FormItem>
</Form> </Form>
</Card> </Card>
</Spin> </Spin>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[Parameter] [Parameter]
public long RoleId { get; set; } public long RoleId { get; set; }

View File

@@ -1,111 +1,126 @@
@page "/system/role/list" @page "/system/role/list"
@page "/{locale}/system/role/list"
@attribute [Authorize]
@inject ILogger<RoleList> Logger @inject ILogger<RoleList> Logger
<PageTitle>角色管理</PageTitle> <PageContainer Title="角色管理">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>角色管理</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<Card Class="mt-3">
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable>
<TitleTemplate>
<Flex Justify="FlexJustify.SpaceBetween">
角色列表
<div>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</div>
</Flex>
</TitleTemplate>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Name" Title="角色">
</PropertyColumn>
<PropertyColumn Property="c => c.Description" Title="说明" />
<PropertyColumn Property="c => c.IsSystemRole" Title="系统" Width="80px" Align="ColumnAlign.Center">
@if (context.IsSystemRole)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
else
{
<AntDesign.Text Type="TextElementType.Secondary"><Icon Type="minus" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
</PropertyColumn>
<PropertyColumn Property="c => c.Count" Title="用户数" Width="100px" />
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
<Title Level="4">角色管理</Title> }
else
{
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" />
}
</PropertyColumn>
<Card Class="mt-3"> <PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" />
<Table DataSource="PagingList.Items" PageSize="100" HidePagination="true" Resizable> <ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px">
<TitleTemplate> <Space>
<Flex Justify="FlexJustify.SpaceBetween"> <SpaceItem>
角色列表 <Button Type="ButtonType.Link" OnClick="() => OnEditPermissionClick(context)">权限管理</Button>
<div> </SpaceItem>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <SpaceItem>
</div> <Dropdown Trigger="@(new Trigger[] { Trigger.Click })">
</Flex> <Overlay>
</TitleTemplate> <Menu>
<ColumnDefinitions>
<PropertyColumn Property="c => c.Name" Title="角色"> <MenuItem>
</PropertyColumn> <a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
<PropertyColumn Property="c => c.Description" Title="说明" /> </MenuItem>
<PropertyColumn Property="c => c.IsSystemRole" Title="系统" Width="80px" Align="ColumnAlign.Center"> @if (!context.IsSystemRole)
@if (context.IsSystemRole) {
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
@if (PagingList.Count > 0)
{ {
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text> <Pagination Current="PagingList.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
} }
else </Row>
{ </Card>
<AntDesign.Text Type="TextElementType.Secondary"><Icon Type="minus" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
}
</PropertyColumn>
<PropertyColumn Property="c => c.Count" Title="用户数" Width="100px" />
<PropertyColumn Property="c => c.Enabled" Title="状态" Width="80px" Align="ColumnAlign.Center">
@if (context.Enabled)
{
<AntDesign.Text Type="TextElementType.Success"><Icon Type="check" Theme=" IconThemeType.Outline" Width="1.3em" Height="1.3em" /></AntDesign.Text>
} <Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增角色" : "编辑角色")' OnClose="_ => CloseDrawer()">
else <Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
{ <FluentValidationValidator />
<Icon Type="close" Theme="IconThemeType.Outline" Width="1.3em" Height="1.3em" /> <FormItem Label="角色名称">
} <Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="角色名称" />
</PropertyColumn> </FormItem>
<FormItem Label="角色说明">
<PropertyColumn Property="c => c.UpdateTime" Title="最后更新" Width="190px" /> <Input @bind-Value="model.Description" For="(()=>model.Culture)" Placeholder="角色说明" />
<ActionColumn Title="操作" Align="ColumnAlign.Right" Width="160px"> </FormItem>
<Space> <FormItem Label="可用状态">
<SpaceItem> <Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
<Button Type="ButtonType.Link" OnClick="() => OnEditPermissionClick(context)">权限管理</Button> </FormItem>
</SpaceItem> <FormItem WrapperColOffset="4">
<SpaceItem> <Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
<Dropdown Trigger="@(new Trigger[] { Trigger.Click })"> </FormItem>
<Overlay> </Form>
<Menu> </Drawer>
</ChildContent>
<MenuItem> </PageContainer>
<a @onclick="(e) => OnEditClick(context)"> <Icon Type="@IconType.Outline.Edit" /> 编辑</a>
</MenuItem>
@if (!context.IsSystemRole)
{
<MenuDivider />
<MenuItem>
<Popconfirm Placement="@Placement.Left" Title="@("删除这条数据无法恢复,您确定要删除吗?")"
OnConfirm="@(e=>HandleDeleteConfirmAsync(e,context.Id))"
OkText="确定"
CancelText="取消">
<a> <Icon Type="@IconType.Outline.Delete" /> 删除</a>
</Popconfirm>
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<a class="ant-dropdown-link" @onclick:preventDefault>
<Icon Type="@IconType.Outline.Menu" />
</a>
</ChildContent>
</Dropdown>
</SpaceItem>
</Space>
</ActionColumn>
</ColumnDefinitions>
</Table>
<br />
<Row Justify="RowJustify.End">
<Pagination PageIndex="pager.Index" Total="PagingList.Count" PageSize="PagingList.Size" ShowSizeChanger="false" OnChange="OnPageChanged"></Pagination>
</Row>
</Card>
<Drawer Closable="true" Width="520" Visible="drawerVisible" Title='(model.Id == 0 ? "新增角色" : "编辑角色")' OnClose="_ => CloseDrawer()">
<Form LabelColSpan="5" @ref="@editform" Model="@model" OnFinish="OnFormFinish">
<FluentValidationValidator />
<FormItem Label="角色名称">
<Input @bind-Value="model.Name" For="(()=>model.Name)" Placeholder="角色名称" />
</FormItem>
<FormItem Label="角色说明">
<Input @bind-Value="model.Description" For="(()=>model.Culture)" Placeholder="角色说明" />
</FormItem>
<FormItem Label="可用状态">
<Checkbox T="bool" Label="启用" @bind-value="model.Enabled" Size="InputSize.Small" Class="ps-0" />
</FormItem>
<FormItem WrapperColOffset="4">
<Button Type="ButtonType.Primary" HtmlType="submit" Style="width: 100%;">保存</Button>
</FormItem>
</Form>
</Drawer>
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
int? Page { get; set; } int? Page { get; set; }

View File

@@ -0,0 +1,20 @@
@page "/system/tools"
@page "/{locale}/system/tools"
<PageContainer Title="系统工具">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/admin/list">系统功能</BreadcrumbItem>
<BreadcrumbItem>系统工具</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code {
[Parameter]
public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,5 +1,20 @@
<h3>UserList</h3> @page "/user/list"
@page "/{locale}/user/list"
<PageContainer Title="用户列表">
<Breadcrumb>
<Breadcrumb>
<BreadcrumbItem Href="/">管理后台</BreadcrumbItem>
<BreadcrumbItem Href="/content/page/list">内容管理</BreadcrumbItem>
<BreadcrumbItem>用户列表</BreadcrumbItem>
</Breadcrumb>
</Breadcrumb>
<ChildContent>
<h3>Tools</h3>
</ChildContent>
</PageContainer>
@code { @code {
[Parameter]
} public string Locale { get; set; } = string.Empty;
}

View File

@@ -1,10 +1,33 @@
@page "/weather" @page "/weather"
@page "/{locale}/weather"
<PageTitle>Weather</PageTitle> @using Microsoft.Extensions.Localization
@inject IStringLocalizer<Weather> L
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
<h1>Weather</h1> <PageTitle>@L["weather.title"]</PageTitle>
<p>This component demonstrates showing data.</p> <h1>@L["weather.title"]</h1>
<p>@L["weather.summary"]</p>
<div style="margin-top:12px; margin-bottom:12px;">
<strong>Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/counter">Counter</NavLink></span>
</div>
<div style="margin-top:12px; margin-bottom:12px;">
<strong>zh Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/zh/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/zh/counter">Counter</NavLink></span>
</div>
<div style="margin-top:12px; margin-bottom:12px;">
<strong>en Quick links:</strong>
<span style="padding-left:10px;"><NavLink href="/en/account/login">Login</NavLink></span>
<span style="padding-left:10px;"><NavLink href="/en/counter">Counter</NavLink></span>
</div>
@if (forecasts == null) @if (forecasts == null)
{ {
@@ -16,7 +39,7 @@ else
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th> <th aria-label="Temperature in Celsius">@L["weather.temperature"]</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th> <th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th> <th>Summary</th>
</tr> </tr>
@@ -36,23 +59,18 @@ else
} }
@code { @code {
[Parameter]
public string Locale { get; set; } = string.Empty;
private WeatherForecast[]? forecasts; private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
// Simulate asynchronous loading to demonstrate a loading indicator // localization handled globally in Routes. No per-page initialization needed.
await Task.Delay(500); return Task.CompletedTask;
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
} }
public void Dispose() { }
private class WeatherForecast private class WeatherForecast
{ {
public DateOnly Date { get; set; } public DateOnly Date { get; set; }
@@ -60,4 +78,13 @@ else
public string? Summary { get; set; } public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
} }
private string GetShortCulture(string current)
{
if (string.IsNullOrEmpty(current)) return current;
if (current.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) return "zh";
if (current.StartsWith("en", StringComparison.OrdinalIgnoreCase)) return "en";
var prefix = current.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
return prefix ?? current;
}
} }

View File

@@ -1,8 +1,12 @@
using Atomx.Admin.Client.Services; using Atomx.Admin.Client.Services;
using Atomx.Admin.Client.Utils; using Atomx.Admin.Client.Utils;
using Atomx.Admin.Client.Validators;
using Blazored.LocalStorage; using Blazored.LocalStorage;
using FluentValidation;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -17,10 +21,23 @@ builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticat
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ʵ<EFBFBD>֣<EFBFBD> // Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ʵ<EFBFBD>֣<EFBFBD>
builder.Services.AddScoped<IPermissionService, ClientPermissionService>(); builder.Services.AddScoped<IPermissionService, ClientPermissionService>();
builder.Services.AddScoped<IconsExtension>(); builder.Services.AddScoped<IconsExtension>();
builder.Services.AddScoped<ILocalizationService, LocalizationClientService>();
// Token provider<65><72>WASM<53><4D>: <20><> localStorage <20><>ȡ access token // Ϊ<EFBFBD><EFBFBD>̬<EFBFBD><EFBFBD>Դ<EFBFBD><EFBFBD>wwwroot<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD>ר<EFBFBD><EFBFBD> HttpClient<6E><74>BaseAddress ָ<><D6B8>Ӧ<EFBFBD>ø<EFBFBD><C3B8><EFBFBD>
builder.Services.AddScoped<ITokenProvider, ClientTokenProvider>(); // <20><> HttpClient <20><><EFBFBD>ڼ<EFBFBD><DABC><EFBFBD> wwwroot/Localization/{culture}.json <20>Ⱦ<EFBFBD>̬<EFBFBD><CCAC><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// ע<><D7A2> LocalizationProvider (<28><><EFBFBD><EFBFBD> WASM)
// Use Scoped lifetime because LocalizationProvider depends on IJSRuntime (scoped) and IHttpClient etc.
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
// ע<><D7A2> ILocalizationService <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> Culture <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E4B4AB>
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
// ע<><D7A2> IStringLocalizer<T> ʵ<><CAB5>
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddLocalization();
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
builder.Services.AddScoped<AuthHeaderHandler>(); builder.Services.AddScoped<AuthHeaderHandler>();
@@ -40,13 +57,22 @@ builder.Services.AddHttpClient("RefreshClient", client =>
client.BaseAddress = new Uri(apiBase); client.BaseAddress = new Uri(apiBase);
}); });
// Ĭ<><C4AC> HttpClient<6E><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2> HttpClient<6E><74> // Ĭ<><C4AC> HttpClient<6E><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2> HttpClient<6E><74><EFBFBD>˴<EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ApiClient<6E><74>
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient")); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));
// <20><> WASM DI <EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD> HttpService<63><65>ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient ʵ<EFBFBD><EFBFBD> // <20><> WASM <EFBFBD><EFBFBD> Program.cs<63><73><EFBFBD>ͻ<EFBFBD><CDBB>ˣ<EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD> HttpService ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܴ<EFBFBD><EFBFBD>ڵ<EFBFBD> IHttpContextAccessor<EFBFBD><EFBFBD>Server <20><EFBFBD><E1B9A9>WASM Ϊ null<EFBFBD><EFBFBD>
builder.Services.AddScoped<HttpService>(sp => new HttpService(sp.GetRequiredService<HttpClient>())); builder.Services.AddScoped<HttpService>(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
return new HttpService(httpClient, httpContextAccessor);
});
builder.Services.AddValidatorsFromAssembly(typeof(LoginModelValidator).Assembly);
builder.Services.AddAntDesign(); builder.Services.AddAntDesign();
await builder.Build().RunAsync(); var host = builder.Build();
await host.RunAsync();

View File

@@ -3,6 +3,7 @@
@code { @code {
protected override void OnInitialized() protected override void OnInitialized()
{ {
Console.WriteLine("blazor跳转登录页");
Navigation.NavigateTo($"/account/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); Navigation.NavigateTo($"/account/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
} }
} }

View File

@@ -1,13 +1,129 @@
<Router AppAssembly="typeof(Program).Assembly"> @using Microsoft.AspNetCore.Components.Routing
<Found Context="routeData"> @inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
<CascadingValue Value="routeData"> @inject NavigationManager Navigation
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized> <div @key="LocalizationProvider.CurrentCulture">
<RedirectToLogin /> <Router AppAssembly="typeof(Program).Assembly">
</NotAuthorized> <Found Context="routeData">
</AuthorizeRouteView> <CascadingValue Value="routeData">
</CascadingValue> <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<FocusOnNavigate RouteData="routeData" Selector="h1" /> <NotAuthorized>
</Found> <RedirectToLogin />
</Router> </NotAuthorized>
<AntContainer /> </AuthorizeRouteView>
</CascadingValue>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
</div>
<AntContainer />
@code {
private bool _initialized;
protected override async Task OnInitializedAsync()
{
// Subscribe to language changes to remount router when needed
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged += OnLanguageChanged;
}
// Subscribe to navigation events so client-side nav to /zh/... or /en/... triggers culture change
Navigation.LocationChanged += OnLocationChanged;
// Try detect first URL segment as short culture (e.g. /zh/ or /en/) and set culture before first render
try
{
var relative = Navigation.ToBaseRelativePath(Navigation.Uri).Trim('/');
var segments = string.IsNullOrEmpty(relative) ? Array.Empty<string>() : relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
var first = segments.FirstOrDefault();
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
if (!string.IsNullOrEmpty(first) && mapping.TryGetValue(first, out var mapped))
{
try
{
if (OperatingSystem.IsBrowser())
{
await LocalizationProvider.SetCultureAsync(mapped);
}
else
{
if (LocalizationProvider is Atomx.Admin.Client.Services.LocalizationProvider concrete)
{
concrete.SetCultureForServer(mapped);
}
}
}
catch { }
}
}
catch { }
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs args)
{
try
{
var relative = Navigation.ToBaseRelativePath(args.Location).Trim('/');
var segments = string.IsNullOrEmpty(relative) ? Array.Empty<string>() : relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
var first = segments.FirstOrDefault();
if (string.IsNullOrEmpty(first)) return;
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
if (mapping.TryGetValue(first, out var mapped))
{
// if culture already set to same, skip
if (string.Equals(LocalizationProvider.CurrentCulture, mapped, StringComparison.OrdinalIgnoreCase)) return;
// Call async set; ignore errors
await LocalizationProvider.SetCultureAsync(mapped);
// Trigger router remount via StateHasChanged
_ = InvokeAsync(StateHasChanged);
}
}
catch { }
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_initialized)
{
_initialized = true;
try
{
if (LocalizationProvider != null)
{
await LocalizationProvider.InitializeAsync();
}
}
catch { }
}
}
private void OnLanguageChanged(object? s, string culture)
{
// Remount router via @key by triggering StateHasChanged
_ = InvokeAsync(StateHasChanged);
}
public void Dispose()
{
if (LocalizationProvider != null)
{
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
}
Navigation.LocationChanged -= OnLocationChanged;
}
}

View File

@@ -1,15 +1,32 @@
using Atomx.Common.Models; using Atomx.Common.Models;
using Atomx.Utils.Json; using Atomx.Utils.Json;
using Microsoft.AspNetCore.Http;
using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Json;
namespace Atomx.Admin.Client.Services namespace Atomx.Admin.Client.Services
{ {
public class HttpService(HttpClient httpClient) public class HttpService
{ {
private readonly HttpClient _httpClient;
private readonly IHttpContextAccessor? _httpContextAccessor;
private readonly ILogger<HttpService> _logger;
public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger<HttpService>? logger = null)
{
_httpClient = httpClient;
_httpContextAccessor = httpContextAccessor;
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<HttpService>.Instance;
}
public async Task<T> Get<T>(string url) public async Task<T> Get<T>(string url)
{ {
var response = await httpClient.GetAsync(url); using var request = new HttpRequestMessage(HttpMethod.Get, url);
AttachCookieIfServer(request);
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
@@ -17,6 +34,8 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
await LogNonSuccessAsync(url, response);
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}"); throw new Exception($"Error: {response.StatusCode}");
} }
} }
@@ -24,8 +43,13 @@ namespace Atomx.Admin.Client.Services
public async Task<T> Post<T>(string url, object data) public async Task<T> Post<T>(string url, object data)
{ {
var json = data.ToJson(); var json = data.ToJson();
var content = new StringContent(json, Encoding.UTF8, "application/json"); using var request = new HttpRequestMessage(HttpMethod.Post, url)
var response = await httpClient.PostAsync(url, content); {
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
AttachCookieIfServer(request);
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
@@ -33,6 +57,8 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
await LogNonSuccessAsync(url, response);
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}"); throw new Exception($"Error: {response.StatusCode}");
} }
} }
@@ -46,7 +72,15 @@ namespace Atomx.Admin.Client.Services
page = 1; page = 1;
} }
url = $"{url}?page={page}&size={size}"; url = $"{url}?page={page}&size={size}";
var response = await httpClient.PostAsJsonAsync(url, data); var json = data.ToJson();
using var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
//Content = JsonContent.Create(data)
};
AttachCookieIfServer(request);
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
@@ -54,14 +88,104 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
throw new Exception($"Error: {response.StatusCode}"); await LogNonSuccessAsync(url, response);
// 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}");
} }
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
Console.WriteLine(ex.ToString()); _logger.LogError(ex, "HttpRequestException while calling {Url}", url);
Console.Error.WriteLine($"[{DateTime.UtcNow:o}] HttpRequestException Url:{url} Error:{ex.Message}");
throw new Exception($"api {url} service failure"); throw new Exception($"api {url} service failure");
} }
} }
/// <summary>
/// 如果在 Server 环境并且 IHttpContextAccessor 可用,则把浏览器请求的 Cookie 转发到后端请求中
/// </summary>
private void AttachCookieIfServer(HttpRequestMessage request)
{
try
{
if (!OperatingSystem.IsBrowser())
{
var ctx = _httpContextAccessor?.HttpContext;
if (ctx != null && ctx.Request.Headers.TryGetValue("Cookie", out var cookie) && !string.IsNullOrEmpty(cookie))
{
// 覆盖或添加 Cookie header
if (request.Headers.Contains("Cookie"))
{
request.Headers.Remove("Cookie");
}
request.Headers.Add("Cookie", (string)cookie);
}
}
}
catch
{
// 忽略任何转发异常,保持健壮性
}
}
private async Task LogNonSuccessAsync(string url, HttpResponseMessage response)
{
try
{
var status = response.StatusCode;
var reason = response.ReasonPhrase;
string userId = "unknown";
string ip = "unknown";
var ctx = _httpContextAccessor?.HttpContext;
if (ctx != null)
{
userId = ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? ctx.User?.FindFirst("sub")?.Value
?? "unknown";
if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff))
{
ip = xff.ToString().Split(',')[0].Trim();
}
else
{
ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
// 结构化日志记录
_logger.LogWarning("UserId:{UserId} Url:{Url} Ip:{Ip} HttpStatus:{StatusCode} Reason:{ReasonPhrase}", userId, url, ip, (int)status, reason);
// 控制台输出一份,便于本地/容器查看
var consoleMsg = new
{
Timestamp = DateTime.UtcNow.ToString("o"),
Level = "Warning",
UserId = userId,
Url = url,
Ip = ip,
Status = (int)status,
Reason = reason
};
Console.WriteLine(JsonSerializer.Serialize(consoleMsg));
}
catch (Exception ex)
{
// 日志失败不能影响主流程
_logger.LogError(ex, "Failed to log non-success response for {Url}", url);
}
}
private void ThrowForStatus(HttpStatusCode statusCode, string url)
{
if (statusCode == HttpStatusCode.Unauthorized || statusCode == HttpStatusCode.Forbidden)
{
// 抛出明确的授权异常,便于上层按需处理(例如提示登陆、重定向或显示权限不足)
throw new UnauthorizedAccessException($"Error: {statusCode} when calling {url}");
}
}
} }
} }

View File

@@ -1,24 +0,0 @@
using System.Threading.Tasks;
namespace Atomx.Admin.Client.Services
{
/// <summary>
/// 统一的 Token 提供器接口(放在共享项目)
/// 目标:
/// - Server 与 WASM 使用相同的接口类型以避免 DI 注入类型不一致
/// - 仅负责“提供”当前可用的 access token不承担刷新策略
/// </summary>
public interface ITokenProvider
{
/// <summary>
/// 返回当前可用的 access token如果没有则返回 null
/// </summary>
Task<string?> GetTokenAsync();
/// <summary>
/// 快速判断当前 token 是否存在且(如果可以解析为 JWT未过期。
/// 注意:此方法为快速检查,不能替代服务端的完整验证。
/// </summary>
Task<bool> IsTokenValidAsync();
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.Extensions.Localization;
using System.Globalization;
namespace Atomx.Admin.Client.Services
{
/// <summary>
/// 基于 ILocalizationProvider 的 IStringLocalizer 实现:
/// 使用 JSON 文件中的键值,未找到返回 key 本身。
/// 名称改为 JsonStringLocalizer 避免与框架的 StringLocalizer 冲突。
/// </summary>
public class JsonStringLocalizer<T> : IStringLocalizer<T>
{
private readonly ILocalizationProvider _provider;
public JsonStringLocalizer(ILocalizationProvider provider)
{
_provider = provider;
}
public LocalizedString this[string name]
{
get
{
var value = _provider.GetString(name);
if (value == null)
{
// 避免在服务端 prerender 阶段进行同步阻塞。以后台方式启动加载并返回 key。
try
{
_ = _provider.LoadCultureAsync(_provider.CurrentCulture);
}
catch { }
}
var result = value ?? name;
return new LocalizedString(name, result, resourceNotFound: result == name);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var fmt = _provider.GetString(name);
if (fmt == null)
{
try
{
_ = _provider.LoadCultureAsync(_provider.CurrentCulture);
}
catch { }
}
var format = fmt ?? name;
var value = string.Format(format, arguments);
return new LocalizedString(name, value, resourceNotFound: format == name);
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
var list = new List<LocalizedString>();
var providerType = _provider.GetType();
var currentProp = providerType.GetProperty("CurrentCulture");
var culture = currentProp?.GetValue(_provider) as string ?? string.Empty;
var cacheField = providerType.GetField("_cache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (!string.IsNullOrEmpty(culture) && cacheField?.GetValue(_provider) is Dictionary<string, Dictionary<string, string>> cache && cache.TryGetValue(culture, out var dict))
{
foreach (var kv in dict)
{
list.Add(new LocalizedString(kv.Key, kv.Value, resourceNotFound: false));
}
}
return list;
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
return this;
}
}
}

View File

@@ -1,322 +0,0 @@
using Atomx.Common.Models;
using Microsoft.JSInterop;
using System.Text.Json;
namespace Atomx.Admin.Client.Services
{
public interface ILocalizationService
{
/// <summary>
/// 根据name获取制定文化语言的译文
/// </summary>
/// <param name="name"></param>
/// <param name="culture"></param>
/// <returns></returns>
Task<string?> GetStringAsync(string name, string? culture = null);
/// <summary>
/// 把本地化文化语言加载到内存中
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
Task<bool> LoadResourcesAsync(string culture);
event EventHandler<string>? ResourcesUpdated;
}
public class LocalizationClientService : ILocalizationService, IAsyncDisposable
{
private readonly HttpService _httpService;
private readonly IJSRuntime _jsRuntime;
private readonly ILogger<LocalizationClientService> _logger;
private readonly Dictionary<string, Dictionary<string, string>> _resources = new();
private readonly Dictionary<string, string> _versions = new();
private readonly SemaphoreSlim _semaphore = new(1, 1);
public event EventHandler<string>? ResourcesUpdated;
public LocalizationClientService(
HttpService httpService,
IJSRuntime jsRuntime,
ILogger<LocalizationClientService> logger)
{
_httpService = httpService;
_jsRuntime = jsRuntime;
_logger = logger;
}
/// <summary>
/// 根据name获取制定文化语言的译文
/// </summary>
/// <param name="name"></param>
/// <param name="culture"></param>
/// <returns></returns>
public async Task<string?> GetStringAsync(string name, string? culture = null)
{
culture ??= await GetCurrentCultureAsync();
if (_resources.TryGetValue(culture, out var cultureResources))
{
if (cultureResources.TryGetValue(name, out var value))
{
return value;
}
// 键不存在,触发资源更新检查
_ = Task.Run(async () => await CheckAndUpdateResourcesAsync(culture));
}
else
{
// 资源未加载,立即加载
await LoadResourcesAsync(culture);
// 重试获取
if (_resources.TryGetValue(culture, out cultureResources) &&
cultureResources.TryGetValue(name, out var value))
{
return value;
}
}
_logger.LogWarning("Localization key not found: {Key} for culture: {Culture}", name, culture);
return null;
}
/// <summary>
/// 把本地化文化语言加载到内存中
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
public async Task<bool> LoadResourcesAsync(string culture)
{
await _semaphore.WaitAsync();
try
{
// 先尝试从localStorage加载
if (await TryLoadFromLocalStorage(culture))
{
// 检查服务器版本,如果需要更新则从服务器加载
if (await CheckAndUpdateFromServer(culture))
{
return true;
}
return true; // 本地版本仍然有效
}
// 从服务器加载
return await LoadFromServer(culture);
}
finally
{
_semaphore.Release();
}
}
/// <summary>
/// 从单例对象中获取版本记录
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<string?> GetResourceVersionAsync(string culture)
{
if (_versions.TryGetValue(culture, out var version))
{
return version;
}
var storedVersion = await GetStoredVersion(culture);
return storedVersion;
}
/// <summary>
/// 检查更新资源
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<bool> CheckAndUpdateResourcesAsync(string culture)
{
return await CheckAndUpdateFromServer(culture);
}
/// <summary>
/// 从本地加载文化语言数据
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<bool> TryLoadFromLocalStorage(string culture)
{
try
{
var resourcesJson = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_{culture}");
var version = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_version_{culture}");
if (string.IsNullOrEmpty(resourcesJson) || string.IsNullOrEmpty(version))
{
return false;
}
var resources = JsonSerializer.Deserialize<Dictionary<string, string>>(resourcesJson);
if (resources != null && version != null)
{
_resources[culture] = resources;
_versions[culture] = version;
return true;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load resources from localStorage for culture: {Culture}", culture);
}
return false;
}
/// <summary>
/// 从服务器更新获取文化语言数据
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<bool> LoadFromServer(string culture)
{
try
{
var response = await _httpService.Get<ApiResult<LocalizationFile>>($"/api/localeresource/resources/{culture}");
if (response.Success)
{
_resources[culture] = response.Data.Translations;
_versions[culture] = response.Data.ResourceVersion;
// 保存到localStorage
await SaveToLocalStorage(culture, response.Data.Translations, response.Data.ResourceVersion);
ResourcesUpdated?.Invoke(this, culture);
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load resources from server for culture: {Culture}", culture);
}
return false;
}
/// <summary>
/// 根据版本信息上从服务器数据对比版本更新数据
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<bool> CheckAndUpdateFromServer(string culture)
{
try
{
var currentVersion = await GetResourceVersionAsync(culture);
var serverVersion = await GetServerVersion(culture);
if (serverVersion != null && (currentVersion == null || serverVersion != currentVersion))
{
_logger.LogInformation("Updating resources for culture: {Culture}", culture);
return await LoadFromServer(culture);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check and update resources for culture: {Culture}", culture);
}
return false;
}
/// <summary>
/// 从服务器获取当前多语言文化数据版本信息
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<string?> GetServerVersion(string culture)
{
try
{
var api = $"/api/localeresource/version/{culture}";
var result = await _httpService.Get<ApiResult<string>>(api);
if (result.Success)
{
return result.Data;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get server version for culture: {Culture}", culture);
}
return null;
}
/// <summary>
/// 获取存储在本地的文化语言版本信息
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
private async Task<string?> GetStoredVersion(string culture)
{
try
{
var version = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_version_{culture}");
if (!string.IsNullOrEmpty(version))
{
return version;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get stored version for culture: {Culture}", culture);
}
return null;
}
/// <summary>
/// 存储版本和文化语言信息
/// </summary>
/// <param name="culture"></param>
/// <param name="resources"></param>
/// <param name="version"></param>
/// <returns></returns>
private async Task SaveToLocalStorage(string culture, Dictionary<string, string> resources, string version)
{
try
{
var resourcesJson = JsonSerializer.Serialize(resources);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", $"locales_{culture}", resourcesJson);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", $"locales_version_{culture}", version);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to save resources to localStorage for culture: {Culture}", culture);
}
}
/// <summary>
/// 获取浏览器上的文化语言信息
/// </summary>
/// <returns></returns>
private async Task<string> GetCurrentCultureAsync()
{
try
{
return await _jsRuntime.InvokeAsync<string>("blazorCulture.get");
}
catch
{
return "en-US"; // 默认文化
}
}
public async ValueTask DisposeAsync()
{
_semaphore?.Dispose();
}
}
}

View File

@@ -0,0 +1,544 @@
using Microsoft.JSInterop;
using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
namespace Atomx.Admin.Client.Services
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E1B9A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON <20>ļ<EFBFBD><C4BC>ļ<EFBFBD><C4BC>ء<EFBFBD><D8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>л<EFBFBD><D0BB><EFBFBD><EFBFBD>ܡ<EFBFBD>
/// <20><>Ҫְ<D2AA><D6B0><EFBFBD><EFBFBD>
/// - <20><> Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender <20><> Blazor Server<65><72>ʱ<EFBFBD><CAB1><EFBFBD>Դ<EFBFBD> webroot ͬ<><CDAC><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD> JSON <20>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// <20>Ա<EFBFBD><D4B1>ڷ<EFBFBD><DAB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ⱦ<EFBFBD>׶<EFBFBD><D7B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>á<EFBFBD>
/// - <20><> WASM <20><><EFBFBD><EFBFBD>ʱʹ<CAB1><CAB9>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient <20><> /localization/{culture}.json <20><><EFBFBD>ز<EFBFBD><D8B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// - <20><><EFBFBD>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD>ʱд<CAB1><D0B4><EFBFBD><EFBFBD>Ϊ `atomx.culture` <20><> cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˶<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3><EFBFBD><EFBFBD> HTML lang <20><><EFBFBD>ԡ<EFBFBD>
/// - <20><EFBFBD>¼<EFBFBD>֪ͨ LanguageChanged<65><64><EFBFBD><EFBFBD> UI <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6><EFBFBD>Ա<EFBFBD><D4B1><EFBFBD><EFBFBD><EFBFBD>
///
/// ˵<><CBB5><EFBFBD><EFBFBD>Ϊ<EFBFBD>˼<EFBFBD><CBBC><EFBFBD> Server <20><> WASM<53><4D><EFBFBD><EFBFBD> Provider <20><EFBFBD><E1BEA1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD>ط<EFBFBD>ʽ<EFBFBD><CABD>
/// </summary>
public interface ILocalizationProvider
{
string CurrentCulture { get; }
string? GetString(string key);
Task SetCultureAsync(string cultureShortOrFull);
Task InitializeAsync();
/// <summary>
/// <20><>ָ֤<D6A4><D6B8><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD>أ<EFBFBD><D8A3><EFBFBD><ECB2BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ⲿ<EFBFBD><E2B2BF><EFBFBD><EFBFBD>Ҫʱ<D2AA><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ء<EFBFBD>
/// </summary>
Task LoadCultureAsync(string culture);
event EventHandler<string>? LanguageChanged;
}
/// <summary>
/// LocalizationProvider <20><>ʵ<EFBFBD>֣<EFBFBD>
/// - ά<><CEAC>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>̬<EFBFBD><CCAC><EFBFBD><EFBFBD><E6A3AC><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD><EFBFBD>/<2F><>ȡ<EFBFBD><C8A1>Դ<EFBFBD><D4B4>
/// - ֧<>ֶ<EFBFBD><D6B6><EFBFBD><EBA3A8> zh / en<65><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD> zh-Hans / en-US<55><53>֮<EFBFBD><D6AE><EFBFBD><EFBFBD>ӳ<EFBFBD>
/// - <20><> Server <20><>֧<EFBFBD><D6A7>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public class LocalizationProvider : ILocalizationProvider
{
private readonly IServiceProvider _sp;
private readonly IHttpClientFactory? _httpClientFactory;
private readonly IJSRuntime? _jsRuntime;
private readonly ILogger<LocalizationProvider> _logger;
private readonly ILocalizationService _localizationService;
// <20><><EFBFBD>棺culture -> translations<6E><73>ʹ<EFBFBD><CAB9> ConcurrentDictionary <20><><EFBFBD>̰߳<DFB3>ȫ<EFBFBD>ع<EFBFBD><D8B9><EFBFBD><EFBFBD><EFBFBD>
// ʹ<>þ<EFBFBD>̬<EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD>Ϊ<EFBFBD><CEAA>ʹ<EFBFBD>м<EFBFBD><D0BC><EFBFBD>/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬһ<CDAC><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܹ<EFBFBD><DCB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѽ<EFBFBD><D1BC>صķ<D8B5><C4B7><EFBFBD><EBA3AC><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD> I/O<><4F>
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
// ֧<>ֵĶ<D6B5><C4B6><EFBFBD>ӳ<EFBFBD><EFBFBD><E4A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>չ<EFBFBD><D5B9><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD>ӡ<EFBFBD>
private static readonly Dictionary<string, string> ShortToCulture = new(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
// Ĭ<><C4AC><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ܴ<EFBFBD><DCB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/URL/Cookie <20>н<EFBFBD><D0BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD>ʱʹ<CAB1>ã<EFBFBD>
private string _currentCulture = "zh-Hans";
private const string CookieName = "atomx.culture";
/// <summary>
/// <20><><EFBFBD><EFBFBD><ECBAAF><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// ע<><EFBFBD><EFBFBD><ECBAAF><EFBFBD>в<EFBFBD>Ӧִ<D3A6>к<EFBFBD>ʱ<EFBFBD><CAB1> JS <20><><EFBFBD>ص<EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public LocalizationProvider(IServiceProvider sp, ILogger<LocalizationProvider> logger, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILocalizationService localizationService)
{
_sp = sp;
_httpClientFactory = httpClientFactory;
_jsRuntime = jsRuntime;
_logger = logger;
_localizationService = localizationService;
// <20><><EFBFBD>Ը<EFBFBD><D4B8>ݵ<EFBFBD>ǰ<EFBFBD>߳<EFBFBD> culture <20><>ʼ<EFBFBD><CABC> _currentCulture<72><65><EFBFBD><EFBFBD><EFBFBD>׳<EFBFBD><D7B3><EFBFBD><ECB3A3>
try
{
var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture;
if (!string.IsNullOrEmpty(threadUi?.Name))
{
_currentCulture = MapToFullCulture(threadUi!.Name);
_logger.LogDebug("LocalizationProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> UI <20>Ļ<EFBFBD>: {Culture}", _currentCulture);
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "LocalizationProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>߳<EFBFBD><DFB3>Ļ<EFBFBD>ʧ<EFBFBD><CAA7>");
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><CEB4>ʹ<EFBFBD><CAB9> JSRuntime<6D><65><EFBFBD><EFBFBD>ζ<EFBFBD>ŷ<EFBFBD><C5B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD>֧<EFBFBD><D6A7> prerender
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
?? Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment");
if (envType != null)
{
var env = _sp.GetService(envType);
if (env != null && _jsRuntime == null)
{
var webRootProp = envType.GetProperty("WebRootPath");
var contentRootProp = envType.GetProperty("ContentRootPath");
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
var path = Path.Combine(webRoot ?? ".", "localization", _currentCulture + ".json");
if (File.Exists(path))
{
try
{
var json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[_currentCulture] = dict;
_logger.LogInformation("(Server ͬ<><CDAC>) <20><>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD> {Path}<7D><>Culture:{Culture}<7D><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>:{Count}", path, _currentCulture, dict.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "(Server ͬ<><CDAC>) <20><>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Path}", path);
}
}
else
{
_logger.LogDebug("<22><><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>δ<EFBFBD><CEB4>·<EFBFBD><C2B7><EFBFBD>ҵ<EFBFBD>: {Path}", path);
}
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "LocalizationProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>س<EFBFBD><D8B3><EFBFBD>ʧ<EFBFBD><CAA7>");
}
}
public string CurrentCulture => _currentCulture;
public event EventHandler<string>? LanguageChanged;
/// <summary>
/// <20>ӻ<EFBFBD><D3BB><EFBFBD><EFBFBD>ж<EFBFBD>ȡָ<C8A1><D6B8><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1>ػ<EFBFBD><D8BB>ַ<EFBFBD><D6B7><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD><EFBFBD> null<6C><6C>
/// </summary>
public string? GetString(string key)
{
if (string.IsNullOrEmpty(key)) return null;
if (_cache.TryGetValue(_currentCulture, out var dict) && dict.TryGetValue(key, out var val))
{
return val;
}
return null;
}
/// <summary>
/// <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD>̣<EFBFBD><CCA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˣ<EFBFBD>
/// <20><><EFBFBD>ȼ<EFBFBD><C8BC><EFBFBD>URL <20><><EFBFBD><EFBFBD>ǰ׺ -> Cookie(atomx.culture) -> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> -> Ĭ<><C4AC>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBA3A8> zh/en<65><6E><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD> zh-Hans/en-US<55><53><EFBFBD><EFBFBD>
/// </summary>
public async Task InitializeAsync()
{
_logger.LogDebug("LocalizationProvider.InitializeAsync <20><>ʼ. CurrentCulture={Culture}", _currentCulture);
string? urlFirstSegment = null;
try
{
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var path = await _jsRuntime.InvokeAsync<string>("eval", "location.pathname");
_logger.LogDebug("JS location.pathname='{Path}'", path);
if (!string.IsNullOrEmpty(path))
{
var trimmed = path.Trim('/');
if (!string.IsNullOrEmpty(trimmed))
{
var seg = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
urlFirstSegment = seg;
_logger.LogDebug("<22><><EFBFBD>⵽ URL <20>׶<EFBFBD>: {Segment}", urlFirstSegment);
}
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "<22><>ȡ location.pathname ʧ<><CAA7>");
}
if (!string.IsNullOrEmpty(urlFirstSegment) && ShortToCulture.TryGetValue(urlFirstSegment, out var mapped))
{
_logger.LogDebug("URL <20><><EFBFBD><EFBFBD> '{Seg}' ӳ<><D3B3>Ϊ<EFBFBD>Ļ<EFBFBD> '{Culture}'", urlFirstSegment, mapped);
await SetCultureInternalAsync(mapped, persistCookie: false);
return;
}
try
{
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var cookieVal = await _jsRuntime.InvokeAsync<string>("cookies.Read", CookieName);
_logger.LogDebug("<22><>ȡ Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
if (!string.IsNullOrEmpty(cookieVal))
{
if (ShortToCulture.TryGetValue(cookieVal, out var mappedFromCookie))
{
_logger.LogDebug("Cookie <20><><EFBFBD><EFBFBD> '{Cookie}' ӳ<><D3B3>Ϊ<EFBFBD>Ļ<EFBFBD> {Culture}", cookieVal, mappedFromCookie);
await SetCultureInternalAsync(mappedFromCookie, persistCookie: false);
return;
}
else
{
// cookie <20>п<EFBFBD><D0BF><EFBFBD><EFBFBD>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƽ<EFBFBD><C6BD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>
await SetCultureInternalAsync(MapToFullCulture(cookieVal), persistCookie: false);
return;
}
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>");
}
try
{
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var browserLang = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
_logger.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {BrowserLang}", browserLang);
if (!string.IsNullOrEmpty(browserLang))
{
var mappedFromBrowser = MapToFullCulture(browserLang);
_logger.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3>Ϊ {Culture}", mappedFromBrowser);
await SetCultureInternalAsync(mappedFromBrowser, persistCookie: false);
return;
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "<22><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>");
}
// <20><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD>ǰĬ<C7B0><C4AC><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ
_logger.LogDebug("InitializeAsync <20><><EFBFBD><EFBFBD>ʹ<EFBFBD>õ<EFBFBD>ǰ<EFBFBD>Ļ<EFBFBD> {Culture}", _currentCulture);
await EnsureCultureLoadedAsync(_currentCulture);
await SetCultureInternalAsync(_currentCulture, persistCookie: false);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><ECB2BD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD>ɴ<EFBFBD><C9B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD>Ƿ<EFBFBD><C7B7>־û<D6BE><C3BB><EFBFBD> Cookie
/// </summary>
public async Task SetCultureAsync(string cultureShortOrFull)
{
if (string.IsNullOrEmpty(cultureShortOrFull)) return;
if (ShortToCulture.TryGetValue(cultureShortOrFull, out var mapped))
{
await SetCultureInternalAsync(mapped, persistCookie: true);
}
else
{
await SetCultureInternalAsync(MapToFullCulture(cultureShortOrFull), persistCookie: true);
}
}
public Task LoadCultureAsync(string culture) => EnsureCultureLoadedAsync(MapToFullCulture(culture));
/// <summary>
/// Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender ʱ<><CAB1>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> Culture <20><><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD> webroot ͬ<><CDAC><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD>
/// <20>÷<EFBFBD><C3B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E1B4A5> JS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʺ<EFBFBD><CABA><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>á<EFBFBD>
/// </summary>
public void SetCultureForServer(string cultureShortOrFull)
{
try
{
var cultureFull = MapToFullCulture(cultureShortOrFull);
if (string.IsNullOrEmpty(cultureFull)) return;
// <20><><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> culture<72><65>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˴<EFBFBD><CBB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IStringLocalizer<65><72>
try
{
var ci = new CultureInfo(cultureFull);
CultureInfo.DefaultThreadCurrentCulture = ci;
CultureInfo.DefaultThreadCurrentUICulture = ci;
_currentCulture = cultureFull;
}
catch { }
// ͬ<><CDAC><EFBFBD><EFBFBD> webroot <20><><EFBFBD><EFBFBD> JSON <20><><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD>
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
?? Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment");
if (envType != null)
{
var env = _sp.GetService(envType);
if (env != null)
{
var webRootProp = envType.GetProperty("WebRootPath");
var contentRootProp = envType.GetProperty("ContentRootPath");
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
var path = Path.Combine(webRoot ?? ".", "localization", cultureFull + ".json");
if (File.Exists(path))
{
try
{
var json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger.LogInformation("(Server ͬ<><CDAC>) <20><>Ϊ {Culture} <20><>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "(Server ͬ<><CDAC>) <20><>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Path}", path);
}
}
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "SetCultureForServer <20><><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Culture}", cultureFull);
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "SetCultureForServer ִ<>й<EFBFBD><D0B9><EFBFBD><EFBFBD>з<EFBFBD><D0B7><EFBFBD><EFBFBD>쳣");
}
}
/// <summary>
/// <20>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫʱ<D2AA>־û<D6BE> Cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD> localization service <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD>
/// </summary>
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
{
//_logger.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2>Ļ<EFBFBD><C4BB><EFBFBD><ECB2BD>ʼ: {Culture}, <20>־û<D6BE>={Persist}", cultureFull, persistCookie);
await EnsureCultureLoadedAsync(cultureFull);
try
{
var ci = new CultureInfo(cultureFull);
CultureInfo.DefaultThreadCurrentCulture = ci;
CultureInfo.DefaultThreadCurrentUICulture = ci;
_currentCulture = cultureFull;
_localizationService.SetLanguage(ci);
_logger.LogDebug("<22>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ {Culture}", cultureFull);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "<22><><EFBFBD><EFBFBD> Culture ʧ<><CAA7>: {Culture}", cultureFull);
}
if (persistCookie && _jsRuntime != null)
{
try
{
var shortKey = ShortToCulture.FirstOrDefault(kv => string.Equals(kv.Value, cultureFull, StringComparison.OrdinalIgnoreCase)).Key ?? cultureFull;
// <20><> shortKey д<><D0B4> cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server ģʽ<C4A3>µ<EFBFBD><C2B5>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>
await _jsRuntime.InvokeVoidAsync("cookies.Write", CookieName, shortKey, DateTime.UtcNow.AddYears(1).ToString("o"));
}
catch (Exception ex)
{
_logger.LogDebug(ex, <><D0B4> Cookie ʧ<><CAA7>");
}
}
try
{
if (_jsRuntime != null)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><> lang <20><><EFBFBD>ԣ<EFBFBD><D4A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD>/SEO
await _jsRuntime.InvokeVoidAsync("setHtmlLang", cultureFull);
}
}
catch { }
// ֪ͨ<CDA8><D6AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
LanguageChanged?.Invoke(this, cultureFull);
}
/// <summary>
/// ȷ<><C8B7>ָ<EFBFBD><D6B8><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD> JSON <20>ļ<EFBFBD><C4BC>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD><EFBFBD><E6A1A3><EFBFBD><EFBFBD>˳<EFBFBD><CBB3><EFBFBD><EFBFBD>WASM HttpClient -> <20>ļ<EFBFBD>ϵͳ -> <20><><EFBFBD>ֵ<EFBFBD>ռλ<D5BC><CEBB>
/// </summary>
private async Task EnsureCultureLoadedAsync(string cultureFull)
{
// <20><>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBA3A8><EFBFBD><EFBFBD> zh -> zh-Hans<6E><73>
cultureFull = MapToFullCulture(cultureFull);
if (string.IsNullOrEmpty(cultureFull)) return;
if (_cache.ContainsKey(cultureFull))
{
_logger.LogDebug("EnsureCultureLoadedAsync: <20>Ļ<EFBFBD> {Culture} <20>ѻ<EFBFBD><D1BB><EFBFBD>", cultureFull);
return;
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>WASM<53><4D><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD> JSON
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
_logger.LogInformation("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> HttpClient <20><><EFBFBD><EFBFBD> {Culture}", cultureFull);
try
{
var http = _sp.GetService(typeof(HttpClient)) as HttpClient;
if (http == null && _httpClientFactory != null)
{
_logger.LogDebug(<><CEB4> ServiceProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient<6E><74>ʹ<EFBFBD><CAB9> IHttpClientFactory <20><><EFBFBD><EFBFBD>");
http = _httpClientFactory.CreateClient();
}
else
{
_logger.LogDebug("<22><> ServiceProvider <20><><EFBFBD><EFBFBD> HttpClient: {HasClient}", http != null);
}
if (http != null)
{
var url = $"/localization/{cultureFull}.json";
Uri? requestUri = null;
// <20><> HttpClient <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> BaseAddress ʱʹ<CAB1>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> JS <20><>ȡ location.origin <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URI
if (http.BaseAddress != null)
{
requestUri = new Uri(http.BaseAddress, url);
}
else if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
try
{
var origin = await _jsRuntime.InvokeAsync<string>("eval", "location.origin");
if (!string.IsNullOrEmpty(origin))
{
requestUri = new Uri(new Uri(origin), url);
}
}
catch (Exception jsEx)
{
_logger.LogDebug(jsEx, "<22><> JS <20><>ȡ location.origin ʧ<><CAA7>");
}
}
if (requestUri != null)
{
_logger.LogInformation("<22><> {Url} <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB><EFBFBD>Դ", requestUri);
var txt = await http.GetStringAsync(requestUri);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(txt) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger.LogInformation(<><CDA8> HttpClient Ϊ {Culture} <20><><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD>ݣ<EFBFBD><DDA3><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
return;
}
else
{
_logger.LogWarning("HttpClient <20>޷<EFBFBD><DEB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URL<52><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> HttpClient <20><><EFBFBD><EFBFBD> {Culture}", cultureFull);
}
}
else
{
_logger.LogWarning(<>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD>õ<EFBFBD> HttpClient <20>Լ<EFBFBD><D4BC><EFBFBD> {Culture}", cultureFull);
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, <><CDA8> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
}
}
_logger.LogDebug("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD><EFBFBD> {Culture}", cultureFull);
// <20><><EFBFBD>ˣ<EFBFBD><CBA3><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> IWebHostEnvironment <20><><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ<EFBFBD><C8A1>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD>
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
?? Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment");
if (envType != null)
{
var env = _sp.GetService(envType);
if (env != null)
{
var webRootProp = envType.GetProperty("WebRootPath");
var contentRootProp = envType.GetProperty("ContentRootPath");
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
var path = Path.Combine(webRoot ?? ".", "localization", cultureFull + ".json");
_logger.LogDebug("<22><><EFBFBD>ұ<EFBFBD><D2B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>·<EFBFBD><C2B7>: {Path}", path);
if (File.Exists(path))
{
var json = await File.ReadAllTextAsync(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger.LogInformation("<22><><EFBFBD>ļ<EFBFBD>Ϊ {Culture} <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
return;
}
else
{
_logger.LogDebug(<><CEB4>·<EFBFBD><C2B7><EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>: {Path}", path);
// <20><><EFBFBD><EFBFBD>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>Ŀ¼<C4BF><C2BC> wwwroot
try
{
var alt = Path.Combine(AppContext.BaseDirectory ?? ".", "wwwroot", "localization", cultureFull + ".json");
_logger.LogDebug("<22><><EFBFBD>ұ<EFBFBD><D2B1><EFBFBD>·<EFBFBD><C2B7>: {AltPath}", alt);
if (File.Exists(alt))
{
var json2 = await File.ReadAllTextAsync(alt);
var dict2 = JsonSerializer.Deserialize<Dictionary<string, string>>(json2) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict2;
_logger.LogInformation("<22>ӱ<EFBFBD><D3B1><EFBFBD>·<EFBFBD><C2B7>Ϊ {Culture} <20><><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict2.Count);
return;
}
else
{
_logger.LogDebug("<22><><EFBFBD><EFBFBD>·<EFBFBD><C2B7>δ<EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>: {AltPath}", alt);
}
}
catch (Exception exAlt)
{
_logger.LogDebug(exAlt, "<22><><EFBFBD><EFBFBD>ñ<EFBFBD><C3B1>ػ<EFBFBD>·<EFBFBD><C2B7>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>");
}
}
}
else
{
_logger.LogDebug("<22>޷<EFBFBD><DEB7><EFBFBD> ServiceProvider <20><>ȡ IWebHostEnvironment ʵ<><CAB5>");
}
}
else
{
_logger.LogDebug(<><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><CEB4><EFBFBD>ҵ<EFBFBD> IWebHostEnvironment <20><><EFBFBD><EFBFBD>");
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, <><CDA8><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
}
_logger.LogDebug("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD>Ϊ {Culture} <20>Ŀ<EFBFBD><C4BF>ֵ<EFBFBD>ռλ", cultureFull);
_cache[cultureFull] = new Dictionary<string, string>();
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><D6B7><EFBFBD>ӳ<EFBFBD><D3B3>Ϊ<EFBFBD>ڲ<EFBFBD>ʹ<EFBFBD>õ<EFBFBD><C3B5><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD> zh -> zh-Hans<6E><73>
/// </summary>
private string MapToFullCulture(string culture)
{
if (string.IsNullOrEmpty(culture)) return culture;
// ֱ<><D6B1>ӳ<EFBFBD><D3B3>
if (ShortToCulture.TryGetValue(culture, out var mapped)) return mapped;
// <20><><EFBFBD><EFBFBD>ǰ׺<C7B0><D7BA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> zh-CN -> zh
var prefix = culture.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2;
return culture;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Globalization;
namespace Atomx.Admin.Client.Services
{
public interface ILocalizationService
{
/// <summary>
/// 获取当前文化环境
/// </summary>d
CultureInfo CurrentCulture { get; }
/// <summary>
/// 当语言发生改变时触发的事件。
/// </summary>
event EventHandler<CultureInfo> LanguageChanged;
/// <summary>
/// 当语言发生改变时触发的事件。调用 <see cref="InteractiveStringLocalizer"/> 来更改语言环境。
/// </summary>
/// <param name="culture"></param>
void SetLanguage(CultureInfo culture);
}
public class LocalizationService : ILocalizationService
{
private CultureInfo? _currentCulture;
/// <summary>
/// 获取当前文化环境
/// </summary>
public CultureInfo CurrentCulture => _currentCulture ?? CultureInfo.CurrentCulture;
public event EventHandler<CultureInfo> LanguageChanged = default!;
public void SetLanguage(CultureInfo culture)
{
if (!culture.Equals(CultureInfo.CurrentCulture))
{
CultureInfo.CurrentCulture = culture;
}
if (_currentCulture == null || !_currentCulture.Equals(culture))
{
_currentCulture = culture;
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
LanguageChanged?.Invoke(this, culture);
}
}
}
}

View File

@@ -0,0 +1,11 @@
namespace Atomx.Admin.Client.Services
{
public interface INavigationService
{
}
public class NavigationService
{
}
}

View File

@@ -18,7 +18,6 @@ namespace Atomx.Admin.Client.Utils
/// </summary> /// </summary>
public class AuthHeaderHandler : DelegatingHandler public class AuthHeaderHandler : DelegatingHandler
{ {
private readonly ITokenProvider _tokenProvider;
private readonly NavigationManager _navigationManager; private readonly NavigationManager _navigationManager;
private readonly ILogger<AuthHeaderHandler> _logger; private readonly ILogger<AuthHeaderHandler> _logger;
private readonly ILocalStorageService _localStorage; private readonly ILocalStorageService _localStorage;
@@ -26,13 +25,11 @@ namespace Atomx.Admin.Client.Utils
private static readonly SemaphoreSlim _refreshLock = new(1, 1); private static readonly SemaphoreSlim _refreshLock = new(1, 1);
public AuthHeaderHandler( public AuthHeaderHandler(
ITokenProvider tokenProvider,
NavigationManager navigationManager, NavigationManager navigationManager,
ILogger<AuthHeaderHandler> logger, ILogger<AuthHeaderHandler> logger,
ILocalStorageService localStorage, ILocalStorageService localStorage,
IHttpClientFactory httpClientFactory) IHttpClientFactory httpClientFactory)
{ {
_tokenProvider = tokenProvider;
_navigationManager = navigationManager; _navigationManager = navigationManager;
_logger = logger; _logger = logger;
_localStorage = localStorage; _localStorage = localStorage;
@@ -45,7 +42,12 @@ namespace Atomx.Admin.Client.Utils
try try
{ {
// 从 ITokenProvider 获取当前 access tokenWASM: ClientTokenProvider 从 localStorage 读取) // 从 ITokenProvider 获取当前 access tokenWASM: ClientTokenProvider 从 localStorage 读取)
var token = await _tokenProvider.GetTokenAsync(); var token = string.Empty;
try
{
token = await _localStorage.GetItemAsync<string>(StorageKeys.AccessToken);
}
catch { }
if (!string.IsNullOrEmpty(token)) if (!string.IsNullOrEmpty(token))
{ {
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
@@ -206,5 +208,20 @@ namespace Atomx.Admin.Client.Utils
return clone; return clone;
} }
private async Task HandleUnauthorizedAsync()
{
// 在WASM模式下重定向到登录页
if (OperatingSystem.IsBrowser())
{
_navigationManager.NavigateTo("/account/login", true);
}
// 在Server模式下可以执行其他操作
else
{
// Server端的处理逻辑
_logger.LogWarning("Unauthorized access detected in server mode");
}
}
} }
} }

View File

@@ -1,38 +0,0 @@
using Atomx.Admin.Client.Services;
using Microsoft.JSInterop;
namespace Atomx.Admin.Client.Utils
{
/// <summary>
/// WASM 客户端下的 Token 提供器(实现共享的 ITokenProvider
/// - 直接从浏览器 storagelocalStorage/sessionStorage读取 access token
/// - 设计为轻量,仅负责读取 token刷新逻辑放在 AuthHeaderHandler / 后端 Refresh 接口
/// </summary>
public class ClientTokenProvider : ITokenProvider
{
private readonly IJSRuntime _jsRuntime;
public ClientTokenProvider(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<string?> GetTokenAsync()
{
try
{
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "accessToken");
}
catch
{
return null;
}
}
public async Task<bool> IsTokenValidAsync()
{
var token = await GetTokenAsync();
return !string.IsNullOrEmpty(token);
}
}
}

View File

@@ -1,11 +1,10 @@
using Blazored.LocalStorage; using Atomx.Common.Constants;
using Atomx.Common.Configuration;
using Atomx.Utils.Extension; using Atomx.Utils.Extension;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using Atomx.Common.Constants;
namespace Atomx.Admin.Client.Utils namespace Atomx.Admin.Client.Utils
{ {
@@ -13,59 +12,62 @@ namespace Atomx.Admin.Client.Utils
{ {
readonly ClaimsPrincipal anonymous = new(new ClaimsIdentity()); readonly ClaimsPrincipal anonymous = new(new ClaimsIdentity());
static readonly Task<AuthenticationState> defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); // 如果运行在 Server 且在 prerender 时有 Persisted UserInfo则存储预设的 AuthenticationState
readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask; private Task<AuthenticationState>? _preRenderedAuthState;
readonly ILocalStorageService _localStorage; readonly ILocalStorageService _localStorage;
public PersistentAuthenticationStateProvider(PersistentComponentState state, ILocalStorageService localStorageService) public PersistentAuthenticationStateProvider(IServiceProvider serviceProvider, ILocalStorageService localStorageService)
{ {
_localStorage = localStorageService; _localStorage = localStorageService;
if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null) // 尝试有条件解析 PersistedComponentState仅在 Server 交互渲染时可用)
var state = serviceProvider.GetService<PersistentComponentState>();
if (state != null)
{ {
return; if (state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) && userInfo is not null)
}
var claims = new List<Claim>
{ {
new(ClaimKeys.Id, userInfo.Id.ToString()), var claims = new List<Claim>
new(ClaimKeys.Name, userInfo.Name), {
new(ClaimKeys.Email, userInfo.Email), new(ClaimKeys.UId, userInfo.Id.ToString()),
new(ClaimKeys.Mobile, userInfo.MobilePhone), new(ClaimKeys.Name, userInfo.Name),
new(ClaimKeys.Role, userInfo.Role), new(ClaimKeys.Email, userInfo.Email),
}; new(ClaimKeys.Mobile, userInfo.MobilePhone),
foreach (var role in userInfo.Permissions) new(ClaimKeys.Role, userInfo.Role),
{ };
claims.Add(new Claim(ClaimKeys.Permission, role)); foreach (var role in userInfo.Permissions ?? Array.Empty<string>())
} {
claims.Add(new Claim(ClaimKeys.Permission, role));
}
authenticationStateTask = Task.FromResult( var cp = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider)));
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider))))); _preRenderedAuthState = Task.FromResult(new AuthenticationState(cp));
}
}
} }
public override async Task<AuthenticationState> GetAuthenticationStateAsync() public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{ {
// 如果在 prerender 阶段已从 PersistentComponentState 恢复用户优先返回该状态Server prerender
if (_preRenderedAuthState != null)
return await _preRenderedAuthState;
try try
{ {
var jwtToken = await _localStorage.GetItemAsStringAsync(StorageKeys.AccessToken); var jwtToken = await _localStorage.GetItemAsStringAsync(StorageKeys.AccessToken);
if (string.IsNullOrEmpty(jwtToken)) if (string.IsNullOrEmpty(jwtToken))
return await Task.FromResult(new AuthenticationState(anonymous)); return new AuthenticationState(anonymous);
else
{ var getUserClaims = DecryptToken(jwtToken);
var getUserClaims = DecryptToken(jwtToken); if (getUserClaims == null || string.IsNullOrEmpty(getUserClaims.Name))
if (getUserClaims == null) return new AuthenticationState(anonymous);
return await Task.FromResult(new AuthenticationState(anonymous));
else var claimsPrincipal = SetClaimPrincipal(getUserClaims);
{ return new AuthenticationState(claimsPrincipal);
var claimsPrincipal = SetClaimPrincipal(getUserClaims);
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
}
} }
catch catch
{ {
return await Task.FromResult(new AuthenticationState(anonymous)); return new AuthenticationState(anonymous);
} }
} }
@@ -77,19 +79,18 @@ namespace Atomx.Admin.Client.Utils
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimKeys.Id, customUserClaims.Id.ToString()), new(ClaimKeys.UId, customUserClaims.Id.ToString()),
new(ClaimKeys.Name, customUserClaims.Name), new(ClaimKeys.Name, customUserClaims.Name),
new(ClaimKeys.Email, customUserClaims.Email), new(ClaimKeys.Email, customUserClaims.Email),
new(ClaimKeys.Mobile, customUserClaims.MobilePhone), new(ClaimKeys.Mobile, customUserClaims.MobilePhone),
new(ClaimKeys.Role, customUserClaims.Role.ToString()), new(ClaimKeys.Role, customUserClaims.Role.ToString()),
}; };
foreach (var role in customUserClaims.Permissions) foreach (var role in customUserClaims.Permissions ?? Array.Empty<string>())
{ {
claims.Add(new Claim(ClaimKeys.Permission, role)); claims.Add(new Claim(ClaimKeys.Permission, role));
} }
return new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider))); return new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider)));
} }
} }
public void UpdateAuthenticationState(string jwtToken = "") public void UpdateAuthenticationState(string jwtToken = "")
@@ -113,9 +114,7 @@ namespace Atomx.Admin.Client.Utils
var handler = new JwtSecurityTokenHandler(); var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwtToken); var token = handler.ReadJwtToken(jwtToken);
var id = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.UId)?.Value ?? string.Empty;
var id = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Id)?.Value ?? string.Empty;
var name = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Name)?.Value ?? string.Empty; var name = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Name)?.Value ?? string.Empty;
var email = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Email)?.Value ?? string.Empty; var email = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Email)?.Value ?? string.Empty;
var phone = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty; var phone = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty;
@@ -132,7 +131,6 @@ namespace Atomx.Admin.Client.Utils
await _localStorage.RemoveItemAsync(StorageKeys.RefreshToken); await _localStorage.RemoveItemAsync(StorageKeys.RefreshToken);
var authState = Task.FromResult(new AuthenticationState(anonymous)); var authState = Task.FromResult(new AuthenticationState(anonymous));
NotifyAuthenticationStateChanged(authState); NotifyAuthenticationStateChanged(authState);
} }
} }

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class AddressModelValidator : AbstractValidator<AddressModel> public class AddressModelValidator : AbstractValidator<AddressModel>
{ {
public AddressModelValidator() public AddressModelValidator(IStringLocalizer<AddressModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("请填写收件人信息"); RuleFor(p => p.Name).NotEmpty().WithMessage("请填写收件人信息");
RuleFor(p => p.Email).EmailAddress().When(p => !string.IsNullOrEmpty(p.Email)).WithMessage("请填写你常用的邮箱地址"); RuleFor(p => p.Email).EmailAddress().When(p => !string.IsNullOrEmpty(p.Email)).WithMessage("请填写你常用的邮箱地址");

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class AdminModelValidator : AbstractValidator<AdminModel> public class AdminModelValidator : AbstractValidator<AdminModel>
{ {
public AdminModelValidator() public AdminModelValidator(IStringLocalizer<AdminModelValidator> localizer)
{ {
RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空"); RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空");
RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间"); RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间");

View File

@@ -1,12 +1,13 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using Atomx.Common.Enums; using Atomx.Common.Enums;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class AppVersionModelValidator : AbstractValidator<AppVersionModel> public class AppVersionModelValidator : AbstractValidator<AppVersionModel>
{ {
public AppVersionModelValidator() public AppVersionModelValidator(IStringLocalizer<LoginModelValidator> localizer)
{ {
RuleFor(p => p.AppName).NotEmpty().WithMessage("应用名不能为空"); RuleFor(p => p.AppName).NotEmpty().WithMessage("应用名不能为空");
RuleFor(p => p.Title).NotEmpty().WithMessage("版本标题不能为空"); RuleFor(p => p.Title).NotEmpty().WithMessage("版本标题不能为空");

View File

@@ -0,0 +1,14 @@
using Atomx.Admin.Client.Models;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators
{
public class AreaModelValidator : AbstractValidator<AreaModel>
{
public AreaModelValidator(IStringLocalizer<AreaModelValidator> localizer)
{
RuleFor(p => p.Name).NotEmpty().WithMessage("请填名称信息");
}
}
}

View File

@@ -1,12 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using Atomx.Common.Enums;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class CategoryModelValidator : AbstractValidator<CategoryModel> public class CategoryModelValidator : AbstractValidator<CategoryModel>
{ {
public CategoryModelValidator() public CategoryModelValidator(IStringLocalizer<CategoryModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空");
RuleFor(p => p.ParentId).Must((p,id)=> ValidateParent(id,p)).WithMessage("不能选择自己做上级分类"); RuleFor(p => p.ParentId).Must((p,id)=> ValidateParent(id,p)).WithMessage("不能选择自己做上级分类");

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class CorporationModelValidator : AbstractValidator<CorporationModel> public class CorporationModelValidator : AbstractValidator<CorporationModel>
{ {
public CorporationModelValidator() public CorporationModelValidator(IStringLocalizer<CorporationModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("公司名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("公司名称不能为空");
} }

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class CorporationStaffModelValidator : AbstractValidator<CorporationStaffModel> public class CorporationStaffModelValidator : AbstractValidator<CorporationStaffModel>
{ {
public CorporationStaffModelValidator() public CorporationStaffModelValidator(IStringLocalizer<CorporationStaffModelValidator> localizer)
{ {
RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空"); RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空");
RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间"); RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间");

View File

@@ -0,0 +1,14 @@
using Atomx.Admin.Client.Models;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators
{
public class CountryModelValidator : AbstractValidator<CountryModel>
{
public CountryModelValidator(IStringLocalizer<CountryModelValidator> localizer)
{
RuleFor(p => p.Name).NotEmpty().WithMessage("请填名称信息");
}
}
}

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class CurrencyModelValidator : AbstractValidator<CurrencyModel> public class CurrencyModelValidator : AbstractValidator<CurrencyModel>
{ {
public CurrencyModelValidator() public CurrencyModelValidator(IStringLocalizer<CurrencyModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("请填写货币名称"); RuleFor(p => p.Name).NotEmpty().WithMessage("请填写货币名称");
} }

View File

@@ -1,11 +1,12 @@
using Atomx.Common.Configuration; using Atomx.Common.Configuration;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class GeneralConfigValidator : AbstractValidator<GeneralConfig> public class GeneralConfigValidator : AbstractValidator<GeneralConfig>
{ {
public GeneralConfigValidator() public GeneralConfigValidator(IStringLocalizer<GeneralConfigValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("网站名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("网站名称不能为空");
} }

View File

@@ -1,13 +1,16 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class LanguageModelValidator : AbstractValidator<LanguageModel> public class LanguageModelValidator : AbstractValidator<LanguageModel>
{ {
public LanguageModelValidator() public LanguageModelValidator(IStringLocalizer<LanguageModelValidator> localizer)
{ {
RuleFor(p => p.Title).NotEmpty().WithMessage("请填写语言标题");
RuleFor(p => p.Name).NotEmpty().WithMessage("请填写语言名称"); RuleFor(p => p.Name).NotEmpty().WithMessage("请填写语言名称");
RuleFor(p => p.Culture).NotEmpty().WithMessage("请选择语言文化");
} }
} }
} }

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class LocaleResourceModelValidator : AbstractValidator<LocaleResourceModel> public class LocaleResourceModelValidator : AbstractValidator<LocaleResourceModel>
{ {
public LocaleResourceModelValidator() public LocaleResourceModelValidator(IStringLocalizer<LocaleResourceModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("本地化多语言信息不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("本地化多语言信息不能为空");
} }

View File

@@ -1,25 +1,36 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class LoginModelValidator : AbstractValidator<LoginModel> public class LoginModelValidator : AbstractValidator<LoginModel>
{ {
public LoginModelValidator() public LoginModelValidator(IStringLocalizer<LoginModelValidator> localizer)
{ {
RuleFor(p => p.Account).NotEmpty().WithMessage("登录账号不能为空"); // helper funcs to get localized text or fallback
RuleFor(p => p.Account).Length(2, 100).When(p => !string.IsNullOrEmpty(p.Account)).WithMessage("用户名长度必须再2-100个字符之间"); string AccountEmpty() => localizer?["Login.Account.Empty"].Value ?? "登录账号不能为空";
//RuleFor(p => p.Account).EmailAddress().When(p => !p.Account.Contains("@") && !string.IsNullOrEmpty(p.Account)).WithMessage("电子邮件地址不正确"); string AccountLength() => localizer?["Login.Account.Length"].Value ?? "用户名长度必须再2-100个字符之间";
string PasswordEmpty() => localizer?["Login.Password.Empty"].Value ?? "请输入登录密码";
string PasswordLength() => localizer?["Login.Password.Length"].Value ?? "登录密码必须在6-32位长度之间";
//RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空"); RuleFor(p => p.Account)
//RuleFor(p => p.Username).Length(2, 50).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-50个字符之间"); .NotEmpty()
//RuleFor(p => p.Account).NotEmpty().WithMessage("电子邮件地址不能为空"); .WithMessage(_ => AccountEmpty());
//RuleFor(p => p.Email).EmailAddress().When(p => !string.IsNullOrEmpty(p.Email)).WithMessage(p => localizer["Form.Email.Invalid"]);
//RuleFor(p => p.Email).MaximumLength(128).When(p => !string.IsNullOrEmpty(p.Email)).WithMessage(p => localizer["Form.Email.LengthInvalid"]); RuleFor(p => p.Account)
RuleFor(p => p.Password).NotEmpty().WithMessage("请输入登录密码"); .Length(2, 100)
RuleFor(p => p.Password).Length(6, 32).When(p => !string.IsNullOrEmpty(p.Password)).WithMessage("登录密码必须在6-32位长度之间"); .When(p => !string.IsNullOrEmpty(p.Account))
//RuleFor(p => p.ConfirmPassword).NotEmpty().WithMessage(p => localizer["Form.ConfirmPassword.Empty"]); .WithMessage(_ => AccountLength());
//RuleFor(p => p.ConfirmPassword).Equal(p => p.Password).When(p => !string.IsNullOrEmpty(p.Password) && !string.IsNullOrEmpty(p.ConfirmPassword)).WithMessage(p => localizer["Form.ConfirmPassword.Different"]);
RuleFor(p => p.Password)
.NotEmpty()
.WithMessage(_ => PasswordEmpty());
RuleFor(p => p.Password)
.Length(6, 32)
.When(p => !string.IsNullOrEmpty(p.Password))
.WithMessage(_ => PasswordLength());
} }
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) => public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class ManufacturerModelValidator : AbstractValidator<ManufacturerModel> public class ManufacturerModelValidator : AbstractValidator<ManufacturerModel>
{ {
public ManufacturerModelValidator() public ManufacturerModelValidator(IStringLocalizer<ManufacturerModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空");
} }

View File

@@ -1,13 +0,0 @@
using Atomx.Common.Entities;
using FluentValidation;
namespace Atomx.Admin.Client.Validators
{
public class MaterialBatchValidator : AbstractValidator<MaterialBatch>
{
public MaterialBatchValidator()
{
RuleFor(p => p.Price).NotEmpty().WithMessage("价格不能为空");
}
}
}

View File

@@ -1,13 +0,0 @@
using Atomx.Common.Entities;
using FluentValidation;
namespace Atomx.Admin.Client.Validators
{
public class MaterialRecordValidator : AbstractValidator<MaterialRecord>
{
public MaterialRecordValidator()
{
RuleFor(p => p.UserId).NotEmpty().WithMessage("名称不能为空");
}
}
}

View File

@@ -1,13 +0,0 @@
using Atomx.Common.Entities;
using FluentValidation;
namespace Atomx.Admin.Client.Validators
{
public class MaterialValidator : AbstractValidator<Material>
{
public MaterialValidator()
{
//RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空");
}
}
}

View File

@@ -1,12 +1,13 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using Atomx.Common.Models; using Atomx.Common.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class MenuModelValidator : AbstractValidator<MenuModel> public class MenuModelValidator : AbstractValidator<MenuModel>
{ {
public MenuModelValidator() public MenuModelValidator(IStringLocalizer<MenuModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("名称不能为空");
} }

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class MessageTemplateModelValidator : AbstractValidator<MessageTemplateModel> public class MessageTemplateModelValidator : AbstractValidator<MessageTemplateModel>
{ {
public MessageTemplateModelValidator() public MessageTemplateModelValidator(IStringLocalizer<MessageTemplateModelValidator> localizer)
{ {
RuleFor(p => p.Name).NotEmpty().WithMessage("消息模板名称不能为空"); RuleFor(p => p.Name).NotEmpty().WithMessage("消息模板名称不能为空");
RuleFor(p => p.Title).NotEmpty().WithMessage("消息模板标题不能为空"); RuleFor(p => p.Title).NotEmpty().WithMessage("消息模板标题不能为空");

View File

@@ -1,11 +1,12 @@
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Validators namespace Atomx.Admin.Client.Validators
{ {
public class ProductAddStockModelValidator : AbstractValidator<ProductAddStockModel> public class ProductAddStockModelValidator : AbstractValidator<ProductAddStockModel>
{ {
public ProductAddStockModelValidator() public ProductAddStockModelValidator(IStringLocalizer<ProductAddStockModelValidator> localizer)
{ {
//RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空"); //RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空");
//RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间"); //RuleFor(p => p.Username).Length(2, 64).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-64个字符之间");

Some files were not shown because too many files have changed in this diff Show More