225 lines
8.2 KiB
Plaintext
225 lines
8.2 KiB
Plaintext
@page "/account/login"
|
||
@page "/{locale}/account/login"
|
||
@using System.Text.Json
|
||
@layout EmptyLayout
|
||
@inject ILogger<Login> Logger
|
||
@inject IJSRuntime JS
|
||
|
||
<PageTitle>登录</PageTitle>
|
||
|
||
@if (!dataLoaded)
|
||
{
|
||
<Spin Spinning="_isLoading" />
|
||
}
|
||
else
|
||
{
|
||
<Flex Style="height:100vh" Justify="FlexJustify.Center" Align="FlexAlign.Center" Direction="FlexDirection.Vertical">
|
||
<GridRow Justify="RowJustify.Center" Class="">
|
||
<GridCol>
|
||
<Card>
|
||
<Form @ref="form" Model="@login" OnFinish="LoginAsync">
|
||
<FluentValidationValidator />
|
||
<FormItem>
|
||
<AntDesign.Input Placeholder="登录账号" Size="InputSize.Large" @bind-Value="@login.Account">
|
||
<Prefix><Icon Type="user" /></Prefix>
|
||
</AntDesign.Input>
|
||
</FormItem>
|
||
<FormItem>
|
||
<AntDesign.Input Placeholder="登录密码" Size="InputSize.Large" @bind-Value="@login.Password" Type="InputType.Password">
|
||
<Prefix><Icon Type="lock" /></Prefix>
|
||
</AntDesign.Input>
|
||
</FormItem>
|
||
<FormItem>
|
||
<a style="float: left;">
|
||
忘记密码
|
||
</a>
|
||
<a style="float: right;">
|
||
<NavLink href="/register">马上注册</NavLink>
|
||
</a>
|
||
</FormItem>
|
||
<FormItem>
|
||
<Button Type="ButtonType.Primary" HtmlType="submit" Class="submit" Size="ButtonSize.Large" Block>登录</Button>
|
||
</FormItem>
|
||
<FormItem>
|
||
<a @onclick="setAccount">
|
||
设置开发帐号
|
||
</a>
|
||
</FormItem>
|
||
</Form>
|
||
</Card>
|
||
</GridCol>
|
||
</GridRow>
|
||
<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">runing as @handler</span>
|
||
</GridRow>
|
||
</Flex>
|
||
}
|
||
|
||
@code {
|
||
string handler = "Server";
|
||
|
||
[Parameter]
|
||
public string Locale { get; set; } = string.Empty;
|
||
|
||
[Parameter]
|
||
[SupplyParameterFromQuery(Name = "ReturnUrl")]
|
||
public string? ReturnUrl { get; set; }
|
||
|
||
[SupplyParameterFromForm]
|
||
public LoginModel login { get; set; } = new();
|
||
private Form<LoginModel> form = null!;
|
||
|
||
bool dataLoaded = false;
|
||
|
||
string message { get; set; } = string.Empty;
|
||
|
||
private bool _isLoading = false;
|
||
|
||
protected override void OnInitialized()
|
||
{
|
||
if (OperatingSystem.IsBrowser())
|
||
{
|
||
handler = "Wasm";
|
||
}
|
||
else
|
||
{
|
||
handler = "Server";
|
||
}
|
||
}
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
if (firstRender)
|
||
{
|
||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||
if (authState.User.Identity != null && authState.User.Identity.IsAuthenticated)
|
||
{
|
||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||
}
|
||
}
|
||
if (!dataLoaded)
|
||
{
|
||
dataLoaded = true;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private async Task LoginAsync()
|
||
{
|
||
if (!form.Validate()) return;
|
||
|
||
_isLoading = true;
|
||
StateHasChanged();
|
||
|
||
try
|
||
{
|
||
var api = "/api/sign/in";
|
||
|
||
if (!OperatingSystem.IsBrowser())
|
||
{
|
||
// Server 模式:使用浏览器发起的 fetch(通过 JS)并携带 credentials: 'include'
|
||
Logger.LogInformation("Server 模式,使用浏览器 fetch 登录");
|
||
var jsResult = await JS.InvokeAsync<JsonElement>("__atomx_post_json", api, login);
|
||
|
||
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 refresh = dprop.TryGetProperty("refreshToken", out var r) ? r.GetString() ?? string.Empty : string.Empty;
|
||
|
||
// WASM 的 localStorage 在 Server Circuit 中无意义,兼容auto模式写入 localStorage。
|
||
try
|
||
{
|
||
await localStorage.SetItemAsync(StorageKeys.AccessToken, token);
|
||
await localStorage.SetItemAsync(StorageKeys.RefreshToken, refresh);
|
||
|
||
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
|
||
{
|
||
provider.UpdateAuthenticationState(token);
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// 浏览器已通过 fetch 收到 Set-Cookie;强制重载使 Circuit 使用新 Cookie。
|
||
Logger.LogInformation($"登录成功,server 跳转: {ReturnUrl}");
|
||
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
|
||
}
|
||
else
|
||
{
|
||
var msg = jsResult.TryGetProperty("message", out var m) ? m.GetString() ?? "登录失败" : "登录失败";
|
||
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = msg });
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Wasm 模式:继续使用 HttpService(之前逻辑),保存 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)
|
||
{
|
||
Logger.LogError(ex, "登录失败");
|
||
ModalService.Error(new ConfirmOptions() { Title = "错误", Content = "登录异常,请稍后重试" });
|
||
}
|
||
finally
|
||
{
|
||
_isLoading = false;
|
||
StateHasChanged();
|
||
}
|
||
}
|
||
|
||
private async Task OnPasswordKeyDown(KeyboardEventArgs value)
|
||
{
|
||
if (value.Key == "Enter")
|
||
{
|
||
await LoginAsync();
|
||
}
|
||
}
|
||
|
||
void setAccount()
|
||
{
|
||
login.Account = "admin";
|
||
login.Password = "admin888";
|
||
}
|
||
}
|
||
|
||
@* 页面内 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> |