完成重设密码界面

main
落雨楓 4 days ago
parent 9b38d27da4
commit 886b5d3d1a

@ -0,0 +1,39 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header">
${msg("emailForgotTitle")}
<#elseif section = "form">
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${(auth.attemptedUsername!'')}" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>" dir="ltr"/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"/>
</div>
</div>
</form>
<#elseif section = "info" >
<#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")}
<#else>
${msg("emailInstruction")}
</#if>
</#if>
</@layout.registrationLayout>

@ -20,7 +20,8 @@
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" class="${properties.kcFormClass!}" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post" novalidate="novalidate">
<@tabs.tabsBar panelId="login-panel" filled=true>
<input type="hidden" id="login-type" name="loginType" value="${auth.loginType!'username'}"/>
<@tabs.tabsBar panelId="login-panel" filled=true pill=true>
<@tabs.tabButton tabId="password" panelId="login-panel" label="loginByUsername" active=true />
<@tabs.tabButton tabId="phone" panelId="login-panel" label="loginByPhone" />
</@tabs.tabsBar>
@ -106,7 +107,7 @@
"areaNotSupported": "${msg("errorPhoneAreaNotSupported")}",
"captchaNotCompleted": "${msg("errorCaptchaRequired")}",
"cannotGetConfig": "${msg("errorCannotGetConfig")}",
"userNotExists": "${msg("errorUserNotFound")}",
"userNotExists": "${msg("errorPhoneUserNotFound")}",
"sendVerificationError": "${msg("errorSendVerificationError")}"
}
</script>

@ -0,0 +1,107 @@
<#import "template.ftl" as layout>
<#import "field.ftl" as field>
<#import "tabs.ftl" as tabs>
<#import "buttons.ftl" as buttons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header">
${msg("emailForgotTitle")}
<#elseif section = "form">
<script type="text/javascript">
// Add styles
loadCSS('${url.resourcesPath}/css/lib/choices.css');
loadCSS('${url.resourcesPath}/css/component/smsSenderForm.css');
</script>
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input type="hidden" id="validation-type" name="validationType" value="${form.validationType!'email'}"/>
<@tabs.tabsBar panelId="reset-password-panel" filled=true pill=true activedId=form.validationType!'email'>
<@tabs.tabButton tabId="email" panelId="reset-password-panel" label="resetPasswordByEmail" />
<@tabs.tabButton tabId="phone" panelId="reset-password-panel" label="resetPasswordByPhone" />
</@tabs.tabsBar>
<@tabs.tabPanel panelId="reset-password-panel">
<@tabs.tabContent tabId="email" active=true>
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label value=auth.attemptedUsername!form.username!'' autofocus=true />
<span class="${properties.kcLoginMainFooterHelperText!}">
<#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")}
<#else>
${msg("emailInstruction")}
</#if>
</span>
</@tabs.tabContent>
<@tabs.tabContent tabId="phone">
<@field.group name="phoneNumber" label=msg("phoneNumber") error=kcSanitize(messagesPerField.get('phoneNumber'))?no_esc required=false>
<div class="${properties.kcInputGroup!}">
<div class="${properties.kcInputGroupItemClass!}">
<div class="${properties.kcInputClass!} areaCodeInputContainer">
<select tabindex="1" class="areaCodeInput" name="areaCode" data-value="${(form.areaCode!'')}">
</select>
</div>
</div>
<div class="${properties.kcInputGroupItemClass!} ${properties.kcFill}">
<div class="${properties.kcInputClass!} <#if messagesPerField.get('phoneNumber')?has_content>${properties.kcError}</#if>">
<input tabindex="2" class="phoneNumberInput" name="phoneNumber" value="${(form.phoneNumber!'')}" type="tel" />
</div>
</div>
</div>
</@field.group>
<@field.group name="smsCode" label=msg("smsVerificationCode") error=kcSanitize(messagesPerField.get('smsCode'))?no_esc required=false>
<div class="${properties.kcInputGroup!}">
<span class="${properties.kcInputGroupItemClass!} ${properties.kcFill}">
<div class="${properties.kcInputClass!}">
<input tabindex="2" type="text" maxlength="6" class="smsCodeInput" name="smsCode" value="${(form.smsCode!'')}" nospin"/>
</div>
</span>
<span class="${properties.kcInputGroupItemClass!}">
<button class="${properties.kcButtonPrimaryClass!} btnSendSmsCode" type="button">
${msg("sendSmsBtn")}
</button>
</span>
</div>
</@field.group>
</@tabs.tabContent>
</@tabs.tabPanel>
<@buttons.actionGroup>
<@buttons.button id="kc-form-buttons" label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/>
<@buttons.buttonLink href=url.loginUrl label="backToLogin" class=["kcButtonSecondaryClass", "kcButtonBlockClass"]/>
</@buttons.actionGroup>
</form>
<script type="application/json" data-configs>
{
"realm": "${realm.name!''}",
"realmUrl": "/realms/${realm.name!''}"
}
</script>
<script type="application/json" data-messages>
{
"geetestCaptchaLangCode": "${msg("geetestCaptchaLangCode")}",
"error": "${msg("sendSmsError")}",
"captchaLoadError": "${msg("captchaLoadError")}",
"captchaCodeApiError": "${msg("captchaCodeApiError")}",
"countryNameLangCode": "${msg("phoneAreaCountryNameLangCode")}",
"sending": "${msg("smsSending")}",
"sendVerificationCode": "${msg("sendSmsBtn")}",
"resendVerificationCode": "${msg("resendSmsBtn")}",
"second": "${msg("second")}",
"phoneNumberIsEmpty": "${msg("errorPhoneNumberIsEmpty")}",
"sendSmsCodeError": "${msg("errorSendSmsCodeInternalError")}",
"areaNotSupported": "${msg("errorPhoneAreaNotSupported")}",
"captchaNotCompleted": "${msg("errorCaptchaRequired")}",
"cannotGetConfig": "${msg("errorCannotGetConfig")}",
"userNotExists": "${msg("errorPhoneUserNotFound")}",
"sendVerificationError": "${msg("errorSendVerificationError")}"
}
</script>
<script type="module" src="${url.resourcesPath}/js/resetPasswordPhoneOrEmail.js"></script>
</#if>
</@layout.registrationLayout>

@ -17,8 +17,7 @@
<#assign label>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</#assign>
<@field.input name="username" label=label error=kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc
autofocus=true autocomplete="${(enableWebAuthnConditionalUI?has_content)?then('username webauthn', 'username')}" value=login.username!'' />
<@field.input name="username" label=label error=kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc autofocus=true autocomplete="${(enableWebAuthnConditionalUI?has_content)?then('username webauthn', 'username')}" value=login.username!'' />
<@field.password name="password" label=msg("password") error="" forgotPassword=realm.resetPasswordAllowed autofocus=usernameHidden?? autocomplete="current-password">
<#if realm.rememberMe && !usernameHidden??>
<@field.checkbox name="rememberMe" label=msg("rememberMe") value=login.rememberMe?? />

@ -19,13 +19,16 @@ captchaLoadError=验证码加载失败,请刷新页面重试
captchaCodeApiError=获取验证码参数失败,请刷新页面重试
smsSending=发送中...
second=
errorPhoneNumberIsEmpty=请输入手机号
errorSendSmsCodeInternalError=无法发送短信验证码,请联系网站管理员
errorPhoneAreaNotSupported=该地区号码暂不支持,请使用邮箱注册
errorCaptchaRequired=请通过人机验证
errorCannotGetConfig=无法获取验证码配置,请刷新页面重试
errorUserNotFound=没有找到该手机号对应的用户
errorSendVerificationError=发送短信验证码出错: {0}
invalidPhoneNumber=无效的手机号
missingPhoneNumber=请输入手机号
cannotSendSmsCodeInternalError=无法发送短信验证码,请联系网站管理员
cannotSendSmsCode=发送短信验证码出错: {0}
phoneAreaNotSupported=该地区号码暂不支持,请使用邮箱注册
phoneNumberAlreadyExists=该手机号已被注册
captchaRequired=请通过人机验证
cannotGetCaptchaConfig=无法获取验证码配置,请刷新页面重试
phoneUserNotFound=没有找到该手机号对应的用户
invalidSmsCode=无效的短信验证码
# IDP记住密码
loginSuccessTitle=登录成功

@ -11,18 +11,4 @@ html, body {
max-height: 200px;
max-height: 50vh;
overflow-y: auto;
}
.pf-v5-c-form {
--pf-v5-c-form--GridGap: 1rem;
}
.pf-v5-c-tab-content {
display: flex;
flex-direction: column;
gap: var(--pf-v5-c-form--GridGap);
}
.pf-v5-c-tab-content[hidden] {
display: none;
}

@ -140,4 +140,66 @@ hr {
input:focus, select:focus, textarea:focus {
outline: none;
}
/* Custom components */
.pf-v5-c-button {
transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
}
.pf-v5-c-button.pf-m-secondary::after {
transition: border-color 150ms ease-in-out, border-width 100ms ease-in-out;
}
.pf-c-tabs-pills {
--pf-v5-c-tabs--before--BorderBottomWidth: 0;
}
.pf-c-tabs-pills .pf-v5-c-tabs__list {
gap: 1rem;
}
.pf-c-tabs-pills .pf-v5-c-tabs__item {
--pf-v5-c-tabs__link--BackgroundColor: var(--pf-v5-global--BackgroundColor--200);
flex-grow: 1;
flex-shrink: 1;
width: 100%;
}
.pf-c-tabs-pills .pf-v5-c-tabs__item.pf-m-action,
.pf-c-tabs-pills .pf-v5-c-tabs__link {
border-radius: var(--pf-v5-global--BorderRadius--sm);
--pf-v5-c-tabs__link--after--BorderWidth: 0;
transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
}
.pf-c-tabs-pills .pf-v5-c-tabs__item.pf-m-current {
--pf-v5-c-tabs__link--BackgroundColor: var(--pf-v5-global--primary-color--100);
--pf-v5-c-tabs__link--Color: var(--pf-v5-global--Color--light-100);
--pf-v5-c-tabs__link--after--BorderWidth: 0;
}
.pf-c-tabs-pills .pf-v5-c-tabs__item.pf-m-action:hover,
.pf-c-tabs-pills .pf-v5-c-tabs__link:hover {
--pf-v5-c-tabs__link--BackgroundColor: var(--pf-v5-global--BackgroundColor--light-200);
--pf-v5-c-tabs__link--after--BorderWidth: 0;
}
.pf-c-tabs-pills .pf-v5-c-tabs__item.pf-m-current .pf-v5-c-tabs__link:hover {
--pf-v5-c-tabs__link--BackgroundColor: var(--pf-v5-global--primary-color--100);
--pf-v5-c-tabs__link--after--BorderWidth: 0;
}
.pf-v5-c-form {
--pf-v5-c-form--GridGap: 1rem;
}
.pf-v5-c-tab-content {
display: flex;
flex-direction: column;
gap: var(--pf-v5-c-form--GridGap);
}
.pf-v5-c-tab-content[hidden] {
display: none;
}

@ -45,9 +45,8 @@ class Message {
replaceVariables(template, params = []) {
let result = template;
let paramId;
result = result.replace(/(?<!\\)\$\{(\w+)\}/g, (...matches) => {
paramId = parseInt(matches[1]) - 1;
console.log(paramId);
result = result.replace(/\{(\w+)\}/g, (...matches) => {
paramId = parseInt(matches[1]);
if (params[paramId]) {
return params[paramId];
} else {

@ -109,6 +109,15 @@ const _pfui = {
this.$tabsBar.querySelectorAll(".pf-v5-c-tabs__link").forEach(($tab) => {
$tab.addEventListener("click", this._handleTabClickEvent);
});
if (this.$tabsBar.dataset.activeTab) {
const $initialTab = this.$tabsBar.querySelector(
`.pf-v5-c-tabs__link[data-tab-target="${this.$tabsBar.dataset.activeTab}"]`
);
if ($initialTab) {
this._activateTabElement($initialTab, false);
}
}
}
destroy() {
@ -120,10 +129,10 @@ const _pfui = {
_handleTabClick(e) {
e.preventDefault();
const $tab = e.currentTarget;
this._activateTab($tab);
this._activateTabElement($tab);
}
_activateTab($tab) {
_activateTabElement($tab, animate = true, event = true) {
const targetId = $tab.dataset.tabTarget;
if (!targetId) {
return;
@ -151,7 +160,11 @@ const _pfui = {
.querySelectorAll(".pf-v5-c-tab-content")
.forEach(($panel) => {
if (!$panel.hidden) {
promises.push(_pfui.fadeOutElement($panel, 200, true));
if (animate) {
promises.push(_pfui.fadeOutElement($panel, 200, true));
} else {
$panel.hidden = true;
}
}
});
@ -161,17 +174,38 @@ const _pfui = {
`.pf-v5-c-tab-content[data-tab-id="${targetId}"]`
);
if ($targetPanel) {
_pfui.fadeInElement($targetPanel, 200, true);
if (animate) {
_pfui.fadeInElement($targetPanel, 200, true);
} else {
$targetPanel.hidden = false;
}
this.$tabsBar.dataset.activeTab = targetId;
}
});
// Send pf.tab.activate event
const activateEvent = new CustomEvent("pf.tab.activate", {
detail: {
tabId: targetId,
},
});
this.$tabsBar.dispatchEvent(activateEvent);
if (event) {
// Send pf.tab.activate event
const activateEvent = new CustomEvent("pf.tab.activate", {
detail: {
tabId: targetId,
},
});
this.$tabsBar.dispatchEvent(activateEvent);
}
}
activeTab(tabId, animate = true, event = true) {
const $tab = this.$tabsBar.querySelector(
`.pf-v5-c-tabs__link[data-tab-target="${tabId}"]`
);
if ($tab) {
this._activateTabElement($tab, animate, event);
return true;
}
return false;
}
}

@ -3,29 +3,17 @@ import { SmsSenderForm } from './component/smsSenderForm.js';
document.addEventListener('DOMContentLoaded', function () {
// Initialize SMS sender form for login
let $form = document.querySelector('#kc-form-login');
window.SmsSenderForm = new SmsSenderForm("login", $form);
window._smsSenderForm = new SmsSenderForm("login", $form);
// Handle tab switching to set the login type
document.addEventListener('pf.tab.activate', function(event) {
const tabId = event.detail.tabId;
let loginTypeInput = document.querySelector('#loginType');
if (!loginTypeInput) {
loginTypeInput = document.createElement('input');
loginTypeInput.type = 'hidden';
loginTypeInput.name = 'loginType';
loginTypeInput.id = 'loginType';
document.querySelector('#kc-form-login').appendChild(loginTypeInput);
}
loginTypeInput.value = tabId; // 'password' or 'phone'
});
// Set initial login type to password (default active tab)
const initialLoginType = document.createElement('input');
initialLoginType.type = 'hidden';
initialLoginType.name = 'loginType';
initialLoginType.id = 'loginType';
initialLoginType.value = 'password';
document.querySelector('#kc-form-login').appendChild(initialLoginType);
// Handle tab switching to set the login typelet tabsBar = document.querySelector('.pf-c-tabsbar');
if (tabsBar) {
tabsBar.addEventListener('pf.tab.activate', function(event) {
const tabId = event.detail.tabId;
let loginTypeInput = document.querySelector('#login-type');
loginTypeInput.value = tabId; // 'password' or 'phone'
});
} else {
console.warn("Tabs bar not found for login form.");
}
});

@ -0,0 +1,20 @@
import { SmsSenderForm } from './component/smsSenderForm.js';
document.addEventListener('DOMContentLoaded', function () {
// Initialize SMS sender form for password reset
let $form = document.querySelector('#kc-reset-password-form');
window._smsSenderForm = new SmsSenderForm("reset-credential", $form);
// Handle tab switching to set the validation type
let tabsBar = document.querySelector('.pf-c-tabsbar');
if (tabsBar) {
tabsBar.addEventListener('pf.tab.activate', function(event) {
console.log(event);
const tabId = event.detail.tabId;
let validationTypeInput = document.querySelector('#validation-type');
validationTypeInput.value = tabId; // 'email' or 'phone'
});
} else {
console.warn("Tabs bar not found for reset password form.");
}
});

@ -1,9 +1,9 @@
<#macro tabsBar panelId filled=false>
<#macro tabsBar panelId activedId='' filled=false pill=false>
<div
class="pf-v5-c-tabs<#if filled> pf-m-fill</#if>"
class="pf-v5-c-tabs pf-c-tabsbar<#if filled> pf-m-fill</#if><#if pill> pf-c-tabs-pills</#if>"
role="region"
id="default-tabs"
<#if panelId??>data-tab-panel="${panelId}"</#if>
data-active-tab="${activedId!''}"
>
<button
class="pf-v5-c-tabs__scroll-button"

Loading…
Cancel
Save