fix localization
This commit is contained in:
@@ -8,6 +8,13 @@
|
|||||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="wwwroot\localization\**" />
|
||||||
|
<Content Remove="wwwroot\localization\**" />
|
||||||
|
<EmbeddedResource Remove="wwwroot\localization\**" />
|
||||||
|
<None Remove="wwwroot\localization\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AntDesign" Version="1.5.0" />
|
<PackageReference Include="AntDesign" Version="1.5.0" />
|
||||||
<PackageReference Include="AntDesign.ProLayout" Version="1.4.0" />
|
<PackageReference Include="AntDesign.ProLayout" Version="1.4.0" />
|
||||||
@@ -26,8 +33,4 @@
|
|||||||
<ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" />
|
<ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\localization\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
@page "/account/login"
|
@page "/account/login"
|
||||||
@page "/{locale}/account/login"
|
@page "/{locale}/account/login"
|
||||||
@using System.Text.Json
|
|
||||||
@layout EmptyLayout
|
@layout EmptyLayout
|
||||||
@inject ILogger<Login> Logger
|
@inject ILogger<Login> Logger
|
||||||
@inject IJSRuntime JS
|
|
||||||
@using Microsoft.Extensions.Localization
|
|
||||||
@inject IStringLocalizer<Login> L
|
@inject IStringLocalizer<Login> L
|
||||||
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
|
||||||
|
|
||||||
<PageTitle>@L["login.title"]</PageTitle>
|
<PageTitle>@L["login.title"]</PageTitle>
|
||||||
|
|
||||||
@@ -144,41 +141,30 @@ else
|
|||||||
if (!OperatingSystem.IsBrowser())
|
if (!OperatingSystem.IsBrowser())
|
||||||
{
|
{
|
||||||
// Server 模式:使用浏览器发起的 fetch(通过 JS)并携带 credentials: 'include'
|
// Server 模式:使用浏览器发起的 fetch(通过 JS)并携带 credentials: 'include'
|
||||||
Logger.LogInformation("Server 模式,使用浏览器 fetch 登录");
|
var jsResult = await JS.InvokeAsync<JsonElement>("ajax.Post", api, login);
|
||||||
var jsResult = await JS.InvokeAsync<JsonElement>("__atomx_post_json", api, login);
|
var result = jsResult.ToJson().FromJson<ApiResult<AuthResponse>>();
|
||||||
|
if (result != null && result.Success)
|
||||||
var success = jsResult.TryGetProperty("success", out var sprop) && sprop.GetBoolean();
|
|
||||||
if (success && jsResult.TryGetProperty("data", out var dprop) && dprop.ValueKind == JsonValueKind.Object)
|
|
||||||
{
|
{
|
||||||
var token = dprop.TryGetProperty("token", out var t) ? t.GetString() ?? string.Empty : string.Empty;
|
var auth = result.Data;
|
||||||
var refresh = dprop.TryGetProperty("refreshToken", out var r) ? r.GetString() ?? string.Empty : string.Empty;
|
await localStorage.SetItemAsync(StorageKeys.AccessToken, auth.Token);
|
||||||
|
await localStorage.SetItemAsync(StorageKeys.RefreshToken, auth.RefreshToken);
|
||||||
// WASM 的 localStorage 在 Server Circuit 中无意义,兼容auto模式写入 localStorage。
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await localStorage.SetItemAsync(StorageKeys.AccessToken, token);
|
|
||||||
await localStorage.SetItemAsync(StorageKeys.RefreshToken, refresh);
|
|
||||||
|
|
||||||
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
|
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
|
||||||
{
|
{
|
||||||
provider.UpdateAuthenticationState(token);
|
provider.UpdateAuthenticationState(auth.Token);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// 浏览器已通过 fetch 收到 Set-Cookie;强制重载使 Circuit 使用新 Cookie。
|
|
||||||
Logger.LogInformation($"登录成功,server 跳转: {ReturnUrl}");
|
Logger.LogInformation($"登录成功,server 跳转: {ReturnUrl}");
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
|
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var msg = jsResult.TryGetProperty("message", out var m) ? m.GetString() ?? "登录失败" : "登录失败";
|
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
|
||||||
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = msg });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Wasm 模式:继续使用 HttpService(之前逻辑),保存 localStorage 并更新 AuthStateProvider
|
// Wasm 模式:保存 localStorage 并更新 AuthStateProvider
|
||||||
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
|
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
|
||||||
if (result.Success && result.Data != null)
|
if (result.Success && result.Data != null)
|
||||||
{
|
{
|
||||||
@@ -225,35 +211,4 @@ else
|
|||||||
login.Account = "admin";
|
login.Account = "admin";
|
||||||
login.Password = "admin888";
|
login.Password = "admin888";
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@* 页面内 JS 辅助:用于在 Server 模式下从浏览器发起 POST 并携带凭证,使浏览器接收 Set-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: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
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>
|
|
||||||
@@ -1,14 +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.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using System.Net.Http;
|
|
||||||
using FluentValidation;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ builder.Services.AddScoped<HttpService>(sp =>
|
|||||||
return new HttpService(httpClient, httpContextAccessor);
|
return new HttpService(httpClient, httpContextAccessor);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly);
|
builder.Services.AddValidatorsFromAssembly(typeof(LoginModelValidator).Assembly);
|
||||||
|
|
||||||
builder.Services.AddAntDesign();
|
builder.Services.AddAntDesign();
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ namespace Atomx.Admin.Client.Services
|
|||||||
|
|
||||||
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
|
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
|
||||||
{
|
{
|
||||||
_logger?.LogDebug("SetCultureInternalAsync start: {Culture}, persist={Persist}", cultureFull, persistCookie);
|
//_logger?.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2>Ļ<EFBFBD><C4BB>첽<EFBFBD><ECB2BD>ʼ: {Culture}, <EFBFBD>־û<EFBFBD>={Persist}", cultureFull, persistCookie);
|
||||||
await EnsureCultureLoadedAsync(cultureFull);
|
await EnsureCultureLoadedAsync(cultureFull);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
|
@using System.Text.Json
|
||||||
@using System.Security.Claims
|
@using System.Security.Claims
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@@ -32,8 +33,9 @@
|
|||||||
@using Blazored.FluentValidation
|
@using Blazored.FluentValidation
|
||||||
|
|
||||||
|
|
||||||
|
@inject IJSRuntime JS
|
||||||
@inject ILocalStorageService localStorage
|
@inject ILocalStorageService localStorage
|
||||||
|
@inject ILocalizationProvider LocalizationProvider
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject HttpService HttpService
|
@inject HttpService HttpService
|
||||||
|
|||||||
@@ -16,6 +16,30 @@
|
|||||||
}
|
}
|
||||||
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
|
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
|
},
|
||||||
|
Delete: function (cookie_name) {
|
||||||
|
document.cookie = cookie_name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ajax = {
|
||||||
|
Post: async function (url, data) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
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' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Resources\**" />
|
<Compile Remove="Resources\**" />
|
||||||
|
<Compile Remove="wwwroot\js\**" />
|
||||||
<Content Remove="Resources\**" />
|
<Content Remove="Resources\**" />
|
||||||
|
<Content Remove="wwwroot\js\**" />
|
||||||
<EmbeddedResource Remove="Resources\**" />
|
<EmbeddedResource Remove="Resources\**" />
|
||||||
|
<EmbeddedResource Remove="wwwroot\js\**" />
|
||||||
<None Remove="Resources\**" />
|
<None Remove="Resources\**" />
|
||||||
|
<None Remove="wwwroot\js\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -43,8 +47,4 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\js\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -155,38 +155,6 @@ builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Preload localization files on startup to ensure server-side rendering finds translations
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var scope = app.Services.CreateScope();
|
|
||||||
var logger = scope.ServiceProvider.GetService<ILoggerFactory>()?.CreateLogger("LocalizationStartup");
|
|
||||||
var provider = scope.ServiceProvider.GetService<ILocalizationProvider>();
|
|
||||||
if (provider != null)
|
|
||||||
{
|
|
||||||
logger?.LogInformation("Preloading localization files for server startup");
|
|
||||||
// preload supported cultures
|
|
||||||
var supported = new[] { "zh-Hans", "en-US" };
|
|
||||||
foreach (var c in supported)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
provider.LoadCultureAsync(c).GetAwaiter().GetResult();
|
|
||||||
logger?.LogInformation("Preloaded localization for {Culture}", c);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger?.LogWarning(ex, "Failed to preload localization for {Culture}", c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// don't block startup
|
|
||||||
var l = app.Services.GetService<ILoggerFactory>()?.CreateLogger("LocalizationStartup");
|
|
||||||
l?.LogWarning(ex, "Failed to run localization preload");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.AddDataMigrate();
|
app.AddDataMigrate();
|
||||||
|
|
||||||
// Forwarded headers<72><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// Forwarded headers<72><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"Seq": {
|
"Seq": {
|
||||||
"ServerUrl": "http://log.firestones.cn/",
|
"ServerUrl": "http://log.atomlust.com/",
|
||||||
"ApiKey": "bBWmvSE2LJh4KsMeidvF",
|
"ApiKey": "bBWmvSE2LJh4KsMeidvF",
|
||||||
"MinimumLevel": "Warning",
|
"MinimumLevel": "Warning",
|
||||||
"LevelOverride": {
|
"LevelOverride": {
|
||||||
|
|||||||
Reference in New Issue
Block a user