Browse Source

feat(openiddict): Add the implementation of SelectAccount

- 增加 `SelectAccount` 页面,实现select_account模式
pull/1416/head
colin 2 months ago
parent
commit
e5b57f63f0
  1. 1
      apps/vben5/packages/@abp/account/src/utils/auth.ts
  2. 23
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/AbpAccountWebOpenIddictModule.cs
  3. 4
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/LINGYUN.Abp.Account.Web.OpenIddict.csproj
  4. 15
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Localization/Resources/en.json
  5. 15
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Localization/Resources/zh-Hans.json
  6. 133
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml
  7. 468
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml.cs
  8. 43
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.js
  9. 29
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/wwwroot/css/select-account.css

1
apps/vben5/packages/@abp/account/src/utils/auth.ts

@ -157,6 +157,7 @@ const oidcSettings: UserManagerSettings = {
silent_redirect_uri: `${window.location.origin}/silent-renew.html`, silent_redirect_uri: `${window.location.origin}/silent-renew.html`,
automaticSilentRenew: true, automaticSilentRenew: true,
loadUserInfo: true, loadUserInfo: true,
prompt: 'select_account',
userStore: new WebStorageStateStore({ userStore: new WebStorageStateStore({
store: import.meta.env.DEV store: import.meta.env.DEV
? localStorage ? localStorage

23
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/AbpAccountWebOpenIddictModule.cs

@ -1,7 +1,8 @@
using LINGYUN.Abp.Account.Web.OpenIddict.ViewModels.Authorize; using LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.Localization; using Volo.Abp.Localization;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
@ -18,6 +19,11 @@ public class AbpAccountWebOpenIddictModule : AbpModule
{ {
public override void PreConfigureServices(ServiceConfigurationContext context) public override void PreConfigureServices(ServiceConfigurationContext context)
{ {
context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
{
options.AddAssemblyResource(typeof(AbpOpenIddictResource), typeof(AbpAccountWebOpenIddictModule).Assembly);
});
PreConfigure<IMvcBuilder>(mvcBuilder => PreConfigure<IMvcBuilder>(mvcBuilder =>
{ {
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountWebOpenIddictModule).Assembly); mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountWebOpenIddictModule).Assembly);
@ -39,6 +45,7 @@ public class AbpAccountWebOpenIddictModule : AbpModule
Configure<RazorPagesOptions>(options => Configure<RazorPagesOptions>(options =>
{ {
options.Conventions.AuthorizePage("/Authorize"); options.Conventions.AuthorizePage("/Authorize");
options.Conventions.AuthorizePage("/Account/SelectAccount");
}); });
Configure<AbpLocalizationOptions>(options => Configure<AbpLocalizationOptions>(options =>
@ -47,5 +54,19 @@ public class AbpAccountWebOpenIddictModule : AbpModule
.Get<AbpOpenIddictResource>() .Get<AbpOpenIddictResource>()
.AddVirtualJson("/Localization/Resources"); .AddVirtualJson("/Localization/Resources");
}); });
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles
.Add(typeof(SelectAccountModel).FullName, bundle =>
{
bundle.AddFiles("/Pages/Account/SelectAccount.js");
});
options.StyleBundles
.Add(typeof(SelectAccountModel).FullName, bundle =>
{
bundle.AddFiles("/css/select-account.css");
});
});
} }
} }

4
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/LINGYUN.Abp.Account.Web.OpenIddict.csproj

@ -18,9 +18,13 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Localization\**\*.json" /> <EmbeddedResource Include="Localization\**\*.json" />
<EmbeddedResource Include="Pages\**\*.js" />
<EmbeddedResource Include="Views\**\*.js" /> <EmbeddedResource Include="Views\**\*.js" />
<EmbeddedResource Include="wwwroot\**\*.css" />
<Content Remove="Localization\**\*.json" /> <Content Remove="Localization\**\*.json" />
<Content Remove="Pages\**\*.js" />
<Content Remove="Views\**\*.js" /> <Content Remove="Views\**\*.js" />
<Content Remove="wwwroot\**\*.css" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

15
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Localization/Resources/en.json

@ -1,6 +1,19 @@
{ {
"culture": "en", "culture": "en",
"texts": { "texts": {
"Required": "Required" "Required": "Required",
"InvalidSelectAccountRequest": "Invalid request for a selected account!",
"NoAvailableAccounts": "No Available Accounts!",
"InvalidSelectAccount": "Invalid Select Account!",
"SelectAccount": "Select Account",
"CurrentAccount": "Current",
"SignedInAs": "Select to login to the account of {0}",
"LastLoginTime": "Last Login",
"Continue": "Continue",
"SwitchToAnotherAccount": "Switch to another account",
"CreateNewAccount": "Create a new account",
"SelectedAccountId": "Selected a cccount",
"RememberSelection": "Remember selection",
"RememberMe": "Remember me"
} }
} }

15
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Localization/Resources/zh-Hans.json

@ -1,6 +1,19 @@
{ {
"culture": "zh-Hans", "culture": "zh-Hans",
"texts": { "texts": {
"Required": "必须" "Required": "必须",
"InvalidSelectAccountRequest": "无效的选择账户请求!",
"NoAvailableAccounts": "未找到可用账户!",
"InvalidSelectAccount": "选择的账户无效!",
"SelectAccount": "选择账户",
"CurrentAccount": "当前账户",
"SignedInAs": "选择登录到 {0} 的账户",
"LastLoginTime": "上次登录",
"Continue": "继续",
"SwitchToAnotherAccount": "使用其他账户",
"CreateNewAccount": "创建新账户",
"SelectedAccountId": "请选择一个账户",
"RememberSelection": "记住我的选择",
"RememberMe": "保持登录状态"
} }
} }

133
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml

@ -0,0 +1,133 @@
@page
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Layout
@using Volo.Abp.AspNetCore.Mvc.UI.Theming;
@using Volo.Abp.OpenIddict.Localization
@using LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account
@inject IThemeManager ThemeManager
@inject IPageLayout PageLayout
@inject IHtmlLocalizer<AbpOpenIddictResource> L
@model SelectAccountModel
@{
Layout = ThemeManager.CurrentTheme.GetAccountLayout();
PageLayout.Content.Title = L["SelectAccount"].Value;
}
@section styles
{
<abp-style-bundle name="@typeof(SelectAccountModel).FullName" />
}
@section scripts
{
<abp-script-bundle name="@typeof(SelectAccountModel).FullName" />
}
<abp-card>
<abp-card-body>
@if (Model.AvailableAccounts.Any())
{
<form method="post" id="selectAccountForm">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Input.ClientId" value="@Model.ClientName" />
<input type="hidden" asp-for="Input.RedirectUri" value="@Model.RedirectUri" />
<input type="hidden" asp-for="Input.RememberSelection" value="true" />
<input type="hidden" asp-for="Input.RememberMe" value="true" />
<div class="alert alert-info d-flex align-items-center mb-4">
<i class="fas fa-user-check me-2"></i>
<span>@string.Format(@L["SignedInAs"].Value, @Model.UserName)</span>
</div>
<div class="list-group list-group-flush rounded-3">
@for (int i = 0; i < Model.AvailableAccounts.Count; i++)
{
var account = Model.AvailableAccounts[i];
var accountId = $"{account.UserId}@{account.TenantId}";
var accountName = $"{account.UserName}";
if (!string.IsNullOrWhiteSpace(account.TenantName))
{
accountName += "/" + account.TenantName;
}
// TODO: Date format
var lastLoginTime = account.LastLoginTime.HasValue ? account.LastLoginTime.Value.ToString("yyyy-MM:dd HH:mm:ss") : "";
var isCurrent = account.IsCurrentAccount;
// TODO: 实现用户头像
var avatar = !string.IsNullOrWhiteSpace(account.UserName) ? account.UserName[0].ToString().ToUpper() : "U";
<label class="list-group-item list-group-item-action border-0 cursor-pointer @(isCurrent ? "active" : "") account-item"
for="account-@i">
<div class="d-flex align-items-center">
<!-- 头像 -->
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3"
style="width: 40px; height: 40px; font-size: 16px; font-weight: 500;">
@avatar
</div>
<!-- 账户信息 -->
<div class="flex-grow-1 me-3">
<div class="d-flex align-items-center">
<span class="fw-semibold">@accountName</span>
@if (isCurrent)
{
<span class="badge bg-success ms-2">@L["CurrentAccount"].Value</span>
}
</div>
<div class="text-muted small">
@account.Email
</div>
@if (!string.IsNullOrWhiteSpace(lastLoginTime))
{
<div class="text-muted small">
@L["LastLoginTime"]: @lastLoginTime
</div>
}
</div>
<!-- 选择账户 -->
<input type="radio"
asp-for="Input.SelectedAccountId"
value="@accountId"
id="account-@i"
class="form-check-input"
style="width: 1.2em; height: 1.2em;"
checked="@isCurrent" />
</div>
</label>
}
</div>
<!-- 操作按钮 -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary" id="continueButton">
@L["Continue"].Value
<i class="fas fa-arrow-right"></i>
</button>
</div>
</form>
}
else
{
<div class="alert alert-warning mb-4">
<i class="fas fa-info-circle me-2"></i>
@L["NoAccountsAvailable"].Value
</div>
}
<!-- 其他选项 -->
<div class="d-grid gap-2 mt-2">
<a href="@Url.Page("/Account/Login", new { ReturnUrl = Model.RedirectUri, prompt = "login" })"
class="btn btn-outline-primary">
<i class="fas fa-exchange-alt me-2"></i>
@L["SwitchToAnotherAccount"].Value
</a>
<a href="@Url.Page("/Account/Register", new { ReturnUrl = Model.RedirectUri })"
class="btn btn-outline-primary">
<i class="fas fa-user-plus me-2"></i>
@L["CreateNewAccount"].Value
</a>
</div>
</abp-card-body>
</abp-card>

468
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml.cs

@ -0,0 +1,468 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Data;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict.Localization;
using Volo.Abp.Users;
using Volo.Abp.Validation;
using static LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account.SelectAccountModel;
namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account;
[Authorize]
public class SelectAccountModel : AccountPageModel
{
private const string LastLoginTimeFieldName = "LastLoginTime";
[BindProperty(SupportsGet = true)]
public string RedirectUri { get; set; }
public string ClientName { get; set; }
public string UserName { get; set; }
[BindProperty]
public SelectAccountInput Input { get; set; }
public List<UserAccountInfo> AvailableAccounts { get; set; } = new();
protected IOpenIddictApplicationManager ApplicationManager => LazyServiceProvider.LazyGetRequiredService<IOpenIddictApplicationManager>();
protected ITenantStore TenantStore => LazyServiceProvider.LazyGetRequiredService<ITenantStore>();
public SelectAccountModel()
{
LocalizationResourceType = typeof(AbpOpenIddictResource);
}
public async virtual Task<IActionResult> OnGetAsync()
{
// 检查用户是否已登录
if (!User.Identity.IsAuthenticated)
{
// 未登录,重定向到登录页面
return RedirectToPage("/Account/Login", new
{
ReturnUrl = Url.Page("/Account/SelectAccount", new { RedirectUri }),
Prompt = "select_account"
});
}
var requestInfo = await ParseOriginalRequestFromRedirectUriAsync();
if (requestInfo == null)
{
Alerts.Warning(L["InvalidSelectAccountRequest"]);
return Page();
}
var application = await ApplicationManager.FindByClientIdAsync(requestInfo.ClientId);
ClientName = await ApplicationManager.GetLocalizedDisplayNameAsync(application) ?? requestInfo.ClientId;
var currentUser = await UserManager.GetUserAsync(User);
if (currentUser == null)
{
await SignInManager.SignOutAsync();
return RedirectToPage("/Account/Login", new
{
ReturnUrl = requestInfo.RedirectUri
});
}
UserName = currentUser.UserName;
AvailableAccounts = await DiscoverUserAccountsAsync(currentUser, requestInfo.ClientId);
if (AvailableAccounts.Count == 0)
{
Alerts.Warning(L["NoAvailableAccounts"]);
}
// 仅关联一个账户时直接登录
if (AvailableAccounts.Count == 1)
{
return await HandleLoginAsync(currentUser, Input.RememberMe, Input.RememberSelection);
}
return Page();
}
public async virtual Task<IActionResult> OnPostAsync()
{
try
{
ValidateModel();
await IdentityOptions.SetAsync();
var tenantUser = ParseSelectedAccountId(Input.SelectedAccountId);
if (tenantUser == null)
{
Alerts.Warning(L["InvalidSelectAccount"]);
return Page();
}
var user = await ValidateSelectedAccountAsync(tenantUser.UserId, tenantUser.TenantId);
if (user == null)
{
Alerts.Warning(L["InvalidSelectAccount"]);
return Page();
}
using (CurrentTenant.Change(tenantUser.TenantId))
{
return await HandleLoginAsync(user, Input.RememberMe, Input.RememberSelection);
}
}
catch (AbpIdentityResultException e)
{
if (!string.IsNullOrWhiteSpace(e.Message))
{
Alerts.Warning(GetLocalizeExceptionMessage(e));
return Page();
}
throw;
}
catch (AbpValidationException)
{
return Page();
}
}
protected async virtual Task<IActionResult> HandleLoginAsync(IdentityUser user, bool rememberMe = false, bool rememberSelection = false)
{
// 检查是否已经是当前用户
var currentUser = await UserManager.GetUserAsync(User);
if (currentUser == null || currentUser.Id == user.Id)
{
// 使用选择的账户登录
// TODO: 实现 RememberMe
await SignInManager.SignInAsync(user, rememberMe);
// TODO: date format
user.SetProperty(LastLoginTimeFieldName, Clock.Now.ToString("yyyy-MM-dd HH:mm:ss"));
// TODO: 实现 RememberSelection
if (rememberSelection)
{
await SaveAccountSelectionAsync(
Input.ClientId,
user.Id,
user.TenantId);
}
}
// 重定向回原始授权请求
return await RedirectSafelyAsync(Input.RedirectUri);
}
protected virtual Task<OriginalRequestInfo> ParseOriginalRequestFromRedirectUriAsync()
{
if (string.IsNullOrWhiteSpace(RedirectUri))
{
return Task.FromResult<OriginalRequestInfo>(null);
}
try
{
var info = new OriginalRequestInfo();
string queryString = null;
if (RedirectUri.StartsWith("/"))
{
int queryIndex = RedirectUri.IndexOf('?');
if (queryIndex >= 0)
{
queryString = RedirectUri.Substring(queryIndex + 1);
}
else
{
// 没有查询参数,尝试直接解析整个字符串
queryString = RedirectUri;
}
}
else if (RedirectUri.Contains("://"))
{
try
{
var uri = new Uri(RedirectUri);
queryString = uri.Query;
if (queryString.StartsWith("?"))
{
queryString = queryString.Substring(1);
}
}
catch (UriFormatException)
{
queryString = RedirectUri;
}
}
else
{
queryString = RedirectUri;
}
// 解析查询参数
if (!string.IsNullOrWhiteSpace(queryString))
{
var query = QueryHelpers.ParseQuery(queryString);
info.ClientId = GetQueryValue(query, "client_id");
info.RedirectUri = GetQueryValue(query, "redirect_uri");
info.ResponseType = GetQueryValue(query, "response_type");
info.Scope = GetQueryValue(query, "scope");
info.State = GetQueryValue(query, "state");
info.Nonce = GetQueryValue(query, "nonce");
info.CodeChallenge = GetQueryValue(query, "code_challenge");
info.CodeChallengeMethod = GetQueryValue(query, "code_challenge_method");
info.Prompt = GetQueryValue(query, "prompt");
}
return Task.FromResult(info);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "解析RedirectUri参数错误: {message}", ex.Message);
return Task.FromResult<OriginalRequestInfo>(null);
}
}
protected async virtual Task<List<UserAccountInfo>> DiscoverUserAccountsAsync(
IdentityUser currentUser,
string clientId)
{
var accounts = new List<UserAccountInfo>
{
new UserAccountInfo
{
IsCurrentAccount = true,
UserId = currentUser.Id.ToString(),
TenantId = CurrentTenant.Id,
TenantName = CurrentTenant.Name,
UserName = currentUser.UserName,
Email = currentUser.Email,
LastLoginTime = currentUser.GetProperty<DateTime?>(LastLoginTimeFieldName),
}
};
// 获取客户端允许的租户
var allowedTenants = await GetAllowedTenantsForClientAsync(clientId);
foreach (var tenant in allowedTenants)
{
var tenantUserAccount = await GetTenantUserAccountInfoAsync(currentUser.UserName, tenant);
if (tenantUserAccount != null)
{
accounts.Add(tenantUserAccount);
}
}
// 按最后登录时间排序,当前账户排第一
return accounts
.OrderByDescending(a => a.IsCurrentAccount)
.ThenByDescending(a => a.LastLoginTime)
.ToList();
}
protected async virtual Task<UserAccountInfo> GetTenantUserAccountInfoAsync(string userName, TenantInfo tenant)
{
using (CurrentTenant.Change(tenant.Id, tenant.Name))
{
var user = await UserManager.FindByNameAsync(userName);
if (user != null)
{
// 检查用户是否有效
if (user.IsActive && !await UserManager.IsLockedOutAsync(user))
{
var lastLoginTime = user.GetProperty<DateTime?>(LastLoginTimeFieldName);
return new UserAccountInfo
{
IsCurrentAccount = false,
UserId = user.Id.ToString(),
TenantId = tenant.Id,
TenantName = tenant.Name,
UserName = user.UserName,
Email = user.Email,
LastLoginTime = lastLoginTime,
};
}
}
return null;
}
}
protected async virtual Task<List<TenantInfo>> GetAllowedTenantsForClientAsync(string clientId)
{
var tenants = new List<TenantInfo>();
if (string.IsNullOrWhiteSpace(clientId))
{
return tenants;
}
var application = await ApplicationManager.FindByClientIdAsync(clientId);
if (application == null)
{
return tenants;
}
var properties = await ApplicationManager.GetPropertiesAsync(application);
if (properties.TryGetValue("AllowedTenants", out var allowedTenantsValue))
{
var tenantIds = allowedTenantsValue.ToString().Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var tenantIdString in tenantIds)
{
if (Guid.TryParse(tenantIdString.Trim(), out var tenantId))
{
var tenant = await TenantStore.FindAsync(tenantId);
if (tenant != null && tenant.IsActive)
{
tenants.Add(new TenantInfo
{
Id = tenant.Id,
Name = tenant.Name
});
}
}
}
}
return tenants;
}
protected virtual TenantUser ParseSelectedAccountId(string selectedAccountId)
{
// 解析账户格式为: UserId@TenantId
if (string.IsNullOrWhiteSpace(selectedAccountId))
{
return new TenantUser();
}
var parts = selectedAccountId.Split('@', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 1)
{
return new TenantUser();
}
if (Guid.TryParse(parts[0], out var userId))
{
if (parts.Length > 1 &&
Guid.TryParse(parts[1], out var tenantId))
{
return new TenantUser(tenantId, userId);
}
return new TenantUser(null, userId);
}
return new TenantUser();
}
protected virtual string GetQueryValue(Dictionary<string, StringValues> query, string key)
{
return query.TryGetValue(key, out var value) ? value.ToString() : null;
}
protected async virtual Task<IdentityUser> ValidateSelectedAccountAsync(Guid userId, Guid? tenantId)
{
using (CurrentTenant.Change(tenantId))
{
var user = await UserManager.FindByIdAsync(userId.ToString());
if (user == null)
{
return null;
}
if (!user.IsActive)
{
return null;
}
if (await UserManager.IsLockedOutAsync(user))
{
return null;
}
return user;
}
}
protected virtual Task SaveAccountSelectionAsync(string clientId, Guid userId, Guid? tenantId)
{
// TODO: 保存用户当前选择账户, 下次选择账户时默认选择此账户
return Task.CompletedTask;
}
public class OriginalRequestInfo
{
public string ClientId { get; set; }
public string RedirectUri { get; set; }
public string Scope { get; set; }
public string State { get; set; }
public string Nonce { get; set; }
public string ResponseType { get; set; }
public string CodeChallenge { get; set; }
public string CodeChallengeMethod { get; set; }
public string Prompt { get; set; }
}
public class SelectAccountInput
{
[Required]
public string SelectedAccountId { get; set; }
[Required]
public string ClientId { get; set; }
[Required]
public string RedirectUri { get; set; }
public bool RememberSelection { get; set; } = true;
public bool RememberMe { get; set; } = true;
}
public class UserAccountInfo
{
public string UserId { get; set; }
public Guid? TenantId { get; set; }
public string TenantName { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public DateTime? LastLoginTime { get; set; }
public bool IsCurrentAccount { get; set; }
}
public class TenantInfo
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class TenantUser
{
public Guid? TenantId { get; set; }
public Guid UserId { get; set; }
public TenantUser()
{
}
public TenantUser(Guid? tenantId, Guid userId)
{
TenantId = tenantId;
UserId = userId;
}
}
}

43
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.js

@ -0,0 +1,43 @@
$(function () {
var l = abp.localization.getResource('AbpOpenIddict');
// 账户选择交互 - 使用 Bootstrap 的 list-group-item-action
$('.account-item').on('click', function (e) {
// 如果点击的是链接或按钮,不处理
if ($(e.target).is('a, button, .btn') ||
$(e.target).closest('a, button, .btn').length) {
return;
}
// 移除所有 active 状态
$('.account-item').removeClass('active');
// 添加当前选中状态
$(this).addClass('active');
// 选中对应的 radio button
var radio = $(this).find('input[type="radio"]');
radio.prop('checked', true);
});
// 表单提交验证
$('#selectAccountForm').on('submit', function (e) {
var selectedAccount = $('input[name="Input.SelectedAccountId"]:checked').val();
if (!selectedAccount) {
e.preventDefault();
abp.message.warn(l('SelectedAccountId'));
return false;
}
return true;
});
// 初始选中第一个账户(如果不是当前账户)
if (!$('.account-item.active').length) {
$('.account-item:first').trigger('click');
}
// 自动聚焦到选中的账户
$('.account-item.active').find('input[type="radio"]').focus();
});

29
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/wwwroot/css/select-account.css

@ -0,0 +1,29 @@
/* 鼠标手型 */
.cursor-pointer {
cursor: pointer;
}
/* 账户列表项悬停效果增强 */
.account-item:hover {
background-color: var(--bs-list-group-action-hover-bg);
transform: translateY(-1px);
transition: all 0.2s ease;
}
/* 当前账户特殊样式 */
.account-item.active {
background-color: var(--bs-primary-bg-subtle);
border-left: 3px solid var(--bs-primary);
}
/* 响应式调整 */
@media (max-width: 576px) {
.min-vh-100 {
min-height: auto;
padding: 1rem 0;
}
.card-body {
padding: 1rem !important;
}
}
Loading…
Cancel
Save