From 886b5d3d1ad6ed49b19a9f1056be140eef616e89 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Fri, 12 Dec 2025 00:29:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=87=8D=E8=AE=BE=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login-reset-password-phone-or-email.ftl | 39 +++++++ .../login/login-phone-or-password.ftl | 5 +- .../login-reset-password-phone-or-email.ftl | 107 ++++++++++++++++++ keycloak.v2-dev/login/login.ftl | 3 +- .../messages/messages_zh_Hans.properties | 17 +-- .../resources/css/loginPhoneOrPassword.css | 14 --- .../login/resources/css/styles.css | 62 ++++++++++ .../login/resources/js/lib/common.js | 5 +- .../resources/js/lib/patternfly-simple.js | 56 +++++++-- .../resources/js/loginPhoneOrPassword.js | 36 ++---- .../resources/js/resetPasswordPhoneOrEmail.js | 20 ++++ keycloak.v2-dev/login/tabs.ftl | 6 +- 12 files changed, 304 insertions(+), 66 deletions(-) create mode 100644 base-dev/login/login-reset-password-phone-or-email.ftl create mode 100644 keycloak.v2-dev/login/login-reset-password-phone-or-email.ftl create mode 100644 keycloak.v2-dev/login/resources/js/resetPasswordPhoneOrEmail.js 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 messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
+ +
+ <#elseif section = "info" > + <#if realm.duplicateEmailsAllowed> + ${msg("emailInstructionUsername")} + <#else> + ${msg("emailInstruction")} + + + diff --git a/keycloak.v2-dev/login/login-phone-or-password.ftl b/keycloak.v2-dev/login/login-phone-or-password.ftl index 25b6744..cf3ca54 100755 --- a/keycloak.v2-dev/login/login-phone-or-password.ftl +++ b/keycloak.v2-dev/login/login-phone-or-password.ftl @@ -20,7 +20,8 @@
<#if realm.password>
- <@tabs.tabsBar panelId="login-panel" filled=true> + + <@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" /> @@ -106,7 +107,7 @@ "areaNotSupported": "${msg("errorPhoneAreaNotSupported")}", "captchaNotCompleted": "${msg("errorCaptchaRequired")}", "cannotGetConfig": "${msg("errorCannotGetConfig")}", - "userNotExists": "${msg("errorUserNotFound")}", + "userNotExists": "${msg("errorPhoneUserNotFound")}", "sendVerificationError": "${msg("errorSendVerificationError")}" } diff --git a/keycloak.v2-dev/login/login-reset-password-phone-or-email.ftl b/keycloak.v2-dev/login/login-reset-password-phone-or-email.ftl new file mode 100644 index 0000000..1c06712 --- /dev/null +++ b/keycloak.v2-dev/login/login-reset-password-phone-or-email.ftl @@ -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"> + + + + + <@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.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")} + + <@field.input name="username" label=label value=auth.attemptedUsername!form.username!'' autofocus=true /> + + + <#if realm.duplicateEmailsAllowed> + ${msg("emailInstructionUsername")} + <#else> + ${msg("emailInstruction")} + + + + <@tabs.tabContent tabId="phone"> + <@field.group name="phoneNumber" label=msg("phoneNumber") error=kcSanitize(messagesPerField.get('phoneNumber'))?no_esc required=false> +
+
+
+ +
+
+
+
+ +
+
+
+ + + <@field.group name="smsCode" label=msg("smsVerificationCode") error=kcSanitize(messagesPerField.get('smsCode'))?no_esc required=false> +
+ +
+ +
+
+ + + +
+ + + + + <@buttons.actionGroup> + <@buttons.button id="kc-form-buttons" label="doSubmit" class=["kcButtonPrimaryClass", "kcButtonBlockClass"]/> + <@buttons.buttonLink href=url.loginUrl label="backToLogin" class=["kcButtonSecondaryClass", "kcButtonBlockClass"]/> + + +
+ + + + + + \ 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")} - <@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}" + data-active-tab="${activedId!''}" >