diff --git a/base-dev/login/login-reset-password-phone-or-email.ftl b/base-dev/login/login-reset-password-phone-or-email.ftl
new file mode 100644
index 0000000..8e96fb4
--- /dev/null
+++ b/base-dev/login/login-reset-password-phone-or-email.ftl
@@ -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">
+
<#if realm.password>
+
+
+
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak.v2-dev/login/login.ftl b/keycloak.v2-dev/login/login.ftl
index 4d56e33..76b0dab 100755
--- a/keycloak.v2-dev/login/login.ftl
+++ b/keycloak.v2-dev/login/login.ftl
@@ -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?? />
diff --git a/keycloak.v2-dev/login/messages/messages_zh_Hans.properties b/keycloak.v2-dev/login/messages/messages_zh_Hans.properties
index 6c70f7a..8dae1bf 100644
--- a/keycloak.v2-dev/login/messages/messages_zh_Hans.properties
+++ b/keycloak.v2-dev/login/messages/messages_zh_Hans.properties
@@ -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=登录成功
diff --git a/keycloak.v2-dev/login/resources/css/loginPhoneOrPassword.css b/keycloak.v2-dev/login/resources/css/loginPhoneOrPassword.css
index 1a2338b..9ffc1a8 100644
--- a/keycloak.v2-dev/login/resources/css/loginPhoneOrPassword.css
+++ b/keycloak.v2-dev/login/resources/css/loginPhoneOrPassword.css
@@ -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;
}
\ No newline at end of file
diff --git a/keycloak.v2-dev/login/resources/css/styles.css b/keycloak.v2-dev/login/resources/css/styles.css
index 0098c18..d849a65 100644
--- a/keycloak.v2-dev/login/resources/css/styles.css
+++ b/keycloak.v2-dev/login/resources/css/styles.css
@@ -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;
}
\ No newline at end of file
diff --git a/keycloak.v2-dev/login/resources/js/lib/common.js b/keycloak.v2-dev/login/resources/js/lib/common.js
index 8f95a6f..dd07522 100644
--- a/keycloak.v2-dev/login/resources/js/lib/common.js
+++ b/keycloak.v2-dev/login/resources/js/lib/common.js
@@ -45,9 +45,8 @@ class Message {
replaceVariables(template, params = []) {
let result = template;
let paramId;
- result = result.replace(/(? {
- 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 {
diff --git a/keycloak.v2-dev/login/resources/js/lib/patternfly-simple.js b/keycloak.v2-dev/login/resources/js/lib/patternfly-simple.js
index d7a125d..76b5e35 100644
--- a/keycloak.v2-dev/login/resources/js/lib/patternfly-simple.js
+++ b/keycloak.v2-dev/login/resources/js/lib/patternfly-simple.js
@@ -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;
}
}
diff --git a/keycloak.v2-dev/login/resources/js/loginPhoneOrPassword.js b/keycloak.v2-dev/login/resources/js/loginPhoneOrPassword.js
index 8f909ff..cb07709 100644
--- a/keycloak.v2-dev/login/resources/js/loginPhoneOrPassword.js
+++ b/keycloak.v2-dev/login/resources/js/loginPhoneOrPassword.js
@@ -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.");
+ }
});
\ No newline at end of file
diff --git a/keycloak.v2-dev/login/resources/js/resetPasswordPhoneOrEmail.js b/keycloak.v2-dev/login/resources/js/resetPasswordPhoneOrEmail.js
new file mode 100644
index 0000000..1b07fbb
--- /dev/null
+++ b/keycloak.v2-dev/login/resources/js/resetPasswordPhoneOrEmail.js
@@ -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.");
+ }
+});
\ No newline at end of file
diff --git a/keycloak.v2-dev/login/tabs.ftl b/keycloak.v2-dev/login/tabs.ftl
index fab4fcf..9a2849b 100644
--- a/keycloak.v2-dev/login/tabs.ftl
+++ b/keycloak.v2-dev/login/tabs.ftl
@@ -1,9 +1,9 @@
-<#macro tabsBar panelId filled=false>
+<#macro tabsBar panelId activedId='' filled=false pill=false>
data-tab-panel="${panelId}"#if>
+ data-active-tab="${activedId!''}"
>