前言

2024 年 10 月 14 日,强大易用的开源建站工具 Halo 正式发布 2.20 LTS 版本。Halo 2.20 包含多项功能更新和问题修复,标志着首个 LTS(长期支持) 版本的诞生。此版本重构了登录相关的页面,登录入口从 /console/login 改为了 /login,此外,主题或者插件都可以提供 login.html 模版来自定义个性化的登录页面了,但不限于登录页面,包括注册页面、登出页面、重置密码页面都可以使用自定义模版。同时这个版本的更新也让开源版本用户享受到了专业版的自定义登录页面 logo 功能了😁,这是一个新特性,不是 bug

登录页面的重构,带来了很大的性能提升,之前的版本中,登录注册相关的逻辑完全由 Console 处理,Console 需要处理各种异常情况和重定向,这会导致 Console 的逻辑过重,且影响整个 Console 页面加载性能。因此之前版本的登录页面加载速度会非常慢。而重构后的登录页面提升了加载性能,降低至少 30 倍的资源体积,如下图所示:

perfomance-comporsable-tuya.webp

这篇文章我将记录一下如何个性化定义一个登录页面(自定义配置元素样式和模版样式),其他模版页面同理。

注意,该教程提供的自定义登录页面的 Halo版本不能低于 V2.20 ,修改前请确认你使用的 Halo 版本是否满足最低版本要求。

一、参照源码

如果不想了解源码以及重构后的登录认证等相关页面大致逻辑的可以跳过此部分,略过此部分内容,直接阅读修改教程。

这里我只列举一下一些页面组件的构成,怎么渲染这些组件,怎么修改样式。

1、组件的引入

源码预览

从 halo 的源码里我们能看到默认的登录页面模版是引入了如下的组件来构成如今的整个登录页面。所以,我们自定义的登录页面只需要引入 Halo 暴露给我的相关模版片段就可以实现自定义登录页面了,其中,gateway_fragments/common::haloLogo 就是 logo 展示组件,这个组件可以自定义自己的 logo,从而在使用开源版本的同时享受到专业版自定义登录页面 logo 功能了。其他的一些组件是关于登录表单,社交登录,用户注册,语言选择的组件,具体不在详细介绍,你可以在上图中的 resources/templates/gateway_fragments 文件夹下找到所有的模版片段,去研究他们。

<div th:replace="~{gateway_fragments/common::haloLogo}"></div>
<div class="halo-form-wrapper">
     <div th:replace="~{gateway_fragments/login::form}"></div>
     <div th:replace="~{gateway_fragments/login::formAuthProviders}"></div>
     <div th:replace="~{gateway_fragments/common::socialAuthProviders}"></div>
</div>

<div th:replace="~{gateway_fragments/common::signupNoticeContent}"></div>
<div th:replace="~{gateway_fragments/common::returnToSiteContent}"></div>
<div th:replace="~{gateway_fragments/common::languageSwitcher}"></div>

2、样式的修改

找到 halo 源码 resources/static/styles 位置下的 main.css 文件,就可以找到定义的 css 变量,你可以通过覆盖他们或者修改一些类名样式达到自定义样式的目的。该文件主要定义了如下 css 变量,覆盖的时候写法和其一样,具体 css 属性值自己定义即可。下面代码中的有些css变量代表的含义很明显,因此不多做解释。

.gateway-wrapper {
    /*主色调,默认浅绿色,就是聚焦输入框显示的那个颜色 */
    --color-primary: #4ccba0;
    /*次要色调,默认黑色,登录页面的登录按钮的那个颜色 */
    --color-secondary: #0e1731;
    --color-link: #1f75cb;
    /*文字颜色*/
    --color-text: #374151;
    /*边框颜色*/
    --color-border: #d1d5db;
    /*小尺寸圆角大小*/
    --rounded-sm: 0.125em;
    /*基本尺寸圆角大小*/
    --rounded-base: 0.25em;
    --rounded-lg: 0.5em;
    /* 元素间距大小 */
    --spacing-2xl: 1.5em;
    --spacing-xl: 1.25em;
    --spacing-lg: 1em;
    --spacing-md: 0.875em;
    --spacing-sm: 0.625em;
    --spacing-xs: 0.5em;
    --text-xl: 1.25em;
    --text-2xl: 1.5em;
    --text-lg: 1.125em;
    --text-base: 1em;
    --text-sm: 0.875em;
    --font-size-base: 16px;
    --font-size-md: 14px;
    padding: 5% var(--spacing-lg);
    font-size: var(--font-size-base);
    max-width: 28em;
    margin: 0 auto;
    font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
        Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
    line-height: 1.5;
}

总结一下,要实现自定义登录、注册等模板,只需要在主题的 templates 目录中新建与 Halo 源码中 application/src/main/resources/templates 同名的模板文件即可,下面是 Halo 源码中的目录结构:

├── challenges
│   └── two-factor
│       ├── totp.html                           两步验证页面
├── gateway_fragments
│   ├── common.html                             通用的模板片段
│   ├── input.html                              和输入框有关的模板片段
│   ├── layout.html                             通用的布局模板
│   ├── login.html                              登录相关的模板片段
│   ├── logout.html                             退出登录相关的模板片段
│   ├── password_reset_email_reset.html         重置密码相关的模板片段
│   ├── password_reset_email_send.html          发送重置密码邮件相关的模板片段
│   ├── signup.html                             注册相关的模板片段
│   ├── totp.html                               两步验证相关的模板片段
├── password-reset
│   └── email
│       ├── reset.html                          密码重置页面
│       ├── send.html                           发送重置密码邮件页面
├── login.html                                  登录页面
├── login_local.html                            本地登录方式的表单模板
├── logout.html                                 退出登录页面
├── setup.html                                  系统初始化页面
├── signup.html                                 注册页面

假设我们要在主题覆盖登录页面(login.html),那么只需要在主题的 templates 目录中新建一个 login.html 即可,意味着只要主题提供上方的目录以及对应的模板文件,那么就会优先使用主题的模板。

二、效果预览

可以访问本站的登录页面去预览效果, 预览地址

这三个模版的登录组件我都采用了毛玻璃风格的样式,如果不喜欢这种样式,可以在指定的类名中修改这些样式。

第一个模版是悬浮球效果的登录模版,可以自定义悬浮球的渐变颜色和。

login-circle-bar-tuya.webp


第二个是动态背景模版的登录页面,可以在规定的时间段内显示指定的背景图,制作灵感来源于mac 系统的 heic 格式的壁纸,该格式的背景壁纸可根据一天的时间变化来切换不同效果的壁纸,你可以在清晨和傍晚分别定义符合天气特征的壁纸,在这个网站上 可以看到这种壁纸的效果。比如下边的登录页面在早晨和傍晚的规定时间里会显示。

dynamic-morning-login-tuya.webp

dynamic-dark-login-tuya.webp


第三种是静态模版背景的登录页面,直接设置一张指定的背景图作为你的网站模版。

login-static-tuya.webp

三、修改步骤

为了方便后期的修改,所有模版我都加入了配置,可以自定义样式,方便不同的小伙伴来使用。如果有些样式不符合自己的预期,可以自行在加入样式修改。

1、加入配置

在你自己所使用的主题的根目录下找到配置文件,一般是 settings.yaml 文件,单独增加一项配置,不从属于其他设置项,注意加入时候的层级是如下代码所示的这个层级,具体位置可以自己调整。

apiVersion: v1alpha1
kind: Setting
metadata:
  name: theme-hao-setting
spec:
  forms:
    - group: loginTmplConfig
      label: 登录模版相关配置
      formSchema:

所有配置项如下,如果你在使用过程中修改了变量名称,那么在模版对应处你也要修改。

- group: loginPage
      label: 登录相关模版
      formSchema:
        - $formkit: select
          name: loginTmpl
          id: loginTmpl
          key: loginTmpl
          label: 登录模版
          value: circle_ball
          options:
            - value: circle_ball
              label: 悬浮球
            - value: dynamic_bgImg_style
              label: 动态背景图模式
            - value: static_bgImg_style
              label: 静态背景图模式
        - $formkit: group
          name: circle_ball_style
          label: 悬浮球模版样式
          help: 仅在悬浮球模版下显示
          value:
            circleBigColor1: "#ffb566"
            circleBigColor2: "#f67"
            circleSmallColor1: "#de82ca"
            circleSmallColor2: "#259fac"
            bgColor1: "#a7cee0"
            bgColor2: "#d0dea7"
          children:
            - $formkit: color
              name: circleBigColor1
              label: 大号悬浮球第一种渐变色
              value: "#ffb566"
            - $formkit: color
              name: circleBigColor2
              label: 大号悬浮球第二种渐变色
              value: "#f67"
            - $formkit: color
              name: circleSmallColor1
              label: 小号悬浮球第一种渐变色
              value: "#de82ca"
            - $formkit: color
              name: circleSmallColor2
              label: 小号悬浮球第二种渐变色
              value: "#259fac"
            - $formkit: color
              name: bgColor1
              label: 渐变背景色1
              value: "#a7cee0"
            - $formkit: color
              name: bgColor2
              label: 渐变背景色2
              value: "#d0dea7"
        - $formkit: group
          name: bgSettings
          label: 背景图模版相关设置
          help: 仅在背景图模式下显示
          value:
            bgStaticImg:
            dynamicBgImg: []
          children:
            - $formkit: attachment
              name: bgStaticImg
              label: 登录页面背景图
              help: 仅在静态背景图模式下显示
              value: 
            - $formkit: repeater
              name: dynamicBgImg
              key: dynamicBgImg
              id: dynamicBgImg
              label: 动态背景图
              help: 可以自定义时间显示制定的背景图,请保证每个时间短都有相应的图片,否则将会默认第一张图片
              value: []
              max: 12
              min: 1
              children:
                - $formkit: text
                  name: start_time
                  validation: required
                  label: 开始时间
                  help: 请严格按照24小时时间制填写时和分,例如开始时间为15:00或者7:00,否则不会生效
                - $formkit: text
                  validation: required
                  name: end_time
                  label: 结束时间
                - $formkit: attachment
                  name: specialBgImg
                  label: 显示的背景图
                  validation: required
                  help: 请按照24小时时间制填写时和分,例如结束时间为17:00或者9:00,否则不会生效
                  value:

上边的配置表单最终的效果如下:

login-config-tuya.webp

在动态背景图的设置中,由于表单提供的 Date 组件时间格式是 YYYY-MM-DD 的,我无法找到一个只支持选择时分的组件,所以替换成了文本输入组件,如果有小伙伴知道 yaml 表单定义中有这样的组件,麻烦在评论区留言。

注意开始时间和结束时间的格式是 24 小时制,中间的冒号是半角状态下输入的,必须严格按照这种格式输入,否则会导致页面无法生效。例如 9:00 ,10:20,13:10

2、在 template 目录下新增 login.html 模版

<!doctype html>
<html
    xmlns:th="https://www.thymeleaf.org"
    th:replace="~{gateway_fragments/layout :: layout(title = |#{title} - ${site.title}|, head = null, title = ${site.title + ' | 登录'}, body = ~{::body})}"
>
    <body>
        <div id="circle-org"></div>
        <div id="circle-blue"></div>
        <th:block th:fragment="body">
            <div class="gateway-wrapper">
                <!-- 可以自定义自己的logo 无需专业版支持 -->
                <div th:replace="~{gateway_fragments/common::haloLogo}"></div>
                <div class="halo-form-wrapper">
                    <div th:replace="~{gateway_fragments/login::form}"></div>
                    <div th:replace="~{gateway_fragments/login::formAuthProviders}"></div>
                    <div th:replace="~{gateway_fragments/common::socialAuthProviders}"></div>
                </div>
                <div th:replace="~{gateway_fragments/common::signupNoticeContent}"></div>
                <div th:replace="~{gateway_fragments/common::returnToSiteContent}"></div>
                <div th:replace="~{gateway_fragments/common::languageSwitcher}"></div>
            </div>
        </th:block>
        <th:block th:if="${#strings.equals(theme.config.loginPage.loginTmpl, 'circle_ball')}">
            <style>
                .gateway-wrapper{
                    --color-primary: #4ccba0;
                    --color-secondary: #0e1731;
                    --color-link: #1f75cb;
                    --color-text: #374151;
                    --color-border: #dfe6ec;
                    --font-size-base: 15px;
                    width: 100%;
                    margin-top: 70px;
                    position: sticky;
                }
                .gateway-wrapper .halo-form-wrapper{
                    background-color: rgba(255, 255, 255, 0.1);
                    backdrop-filter: blur(6px);
                    -webkit-backdrop-filter: blur(6px);
                    border: 1px solid rgba(255, 255, 255, 0.18);
                    box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    -webkit-box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    border-radius: 10px;
                    -webkit-border-radius: 10px;
                    color: rgba(255, 255, 255, 0.3);
                }
                .gateway-wrapper .form-item .form-input{
                    background-color: rgba(255, 255, 255, 0.3);
                    border-radius: 10px;
                }
                #remember-me{
                    background-color: rgba(255, 255, 255, 0.3);
                }
                .halo-form button[type="submit"] {
                    background-color: rgba(255, 255, 255, 0.1);
                    backdrop-filter: blur(2.5px);
                    -webkit-backdrop-filter: blur(2.5px);
                    border: 1px solid rgba(255, 255, 255, 0.18);
                    box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    -webkit-box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    border-radius: 9px;
                    -webkit-border-radius: 9px;
                    color: rgba(255, 255, 255, 0.3);
                    color: var(--color-text);
                }
                body{
                    position: relative;
                    padding: 0;
                    margin: 0;
                    background-image: linear-gradient(135deg, [[${theme.config.loginPage.circle_ball_style.bgColor1}]] 50%, [[${theme.config.loginPage.circle_ball_style.bgColor2}]]);
                }

                /*  动态圆球特效 */
                #circle-org {
                    position: absolute;
                    width: 300px;
                    height: 300px;
                    border-radius: 50%;
                    top: 2%;
                    left: 27.5%;
                    background: linear-gradient(135deg, [[${theme.config.loginPage.circle_ball_style.circleBigColor1}]], [[${theme.config.loginPage.circle_ball_style.circleBigColor2}]]);
                    animation: bounce-down 5s linear infinite;
                }
                #circle-blue {
                    position: absolute;
                    width: 180px;
                    height: 180px;
                    border-radius: 50%;
                    top: 39%;
                    left: 56%;
                    background: linear-gradient(135deg, [[${theme.config.loginPage.circle_ball_style.circleSmallColor1}]], [[${theme.config.loginPage.circle_ball_style.circleSmallColor2}]]);
                    animation: bounce-down 8s linear infinite;
                    z-index: -2;
                }
                @keyframes bounce-down{
                    25% {
                        -webkit-transform: translateY(-20px);
                    }
                    100%, 50% {
                        -webkit-transform: translateY(0);
                    }
                    75% {
                        -webkit-transform: translateY(20px);
                    }
                }
                @media screen and (max-width: 500px) {
                    .gateway-wrapper{
                        margin: 0 auto;
                        position: inherit;
                        top: 10%;
                        right: 0;
                    }
                    #circle-org {
                        position: absolute;
                        width: 200px;
                        height: 200px;
                        top: 15%;
                        left: 2%;
                    }
                    #circle-blue {
                        position: absolute;
                        width: 150px;
                        height: 150px;
                        border-radius: 50%;
                        top: 44%;
                        right: 2%;
                        background: linear-gradient(135deg, #de82ca, #259fac);
                        animation: bounce-down 8s linear infinite;
                        z-index: -2;
                    }
                }
            </style>
        </th:block>
        <!-- 背景图模式 -->
        <th:block th:if="${#strings.equals(theme.config.loginPage.loginTmpl, 'static_bgImg_style') || #strings.equals(theme.config.loginPage.loginTmpl, 'dynamic_bgImg_style')}">
            <style>
                body{
                    position: relative;
                    padding: 0;
                    margin: 0;
                    background-attachment: fixed;
                }
                .gateway-wrapper{
                    --color-primary: #4ccba0;
                    --color-secondary: #0e1731;
                    --color-link: #1f75cb;
                    --color-text: #374151;
                    --color-border: #d1d5db;
                    --font-size-base: 15px;
                    width: 100%;
                    position: absolute;
                    top: 12%;
                    right: 7%;
                    /*  整个登录组件的毛玻璃  */
                   /* background-color: rgba(255, 255, 255, 0.25);
                    backdrop-filter: blur(6px);
                    -webkit-backdrop-filter: blur(6px);
                    border: 1px solid rgba(255, 255, 255, 0.18);
                    box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    -webkit-box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    border-radius: 12px;
                    -webkit-border-radius: 12px;
                    color: rgba(255, 255, 255, 0.75);*/
                }
                .gateway-wrapper .halo-form-wrapper{
                    background-color: rgba(255, 255, 255, 0.1);
                    backdrop-filter: blur(6px);
                    -webkit-backdrop-filter: blur(6px);
                    border: 1px solid rgba(255, 255, 255, 0.18);
                    box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    -webkit-box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    border-radius: 10px;
                    -webkit-border-radius: 10px;
                    color: rgba(255, 255, 255, 0.3);
                }
                .gateway-wrapper .form-item .form-input{
                    background-color: rgba(255, 255, 255, 0.3);
                    border-radius: 10px;
                }
                #remember-me{
                    background-color: rgba(255, 255, 255, 0.3);
                }
                .halo-form button[type="submit"] {
                    background-color: rgba(255, 255, 255, 0.1);
                    backdrop-filter: blur(2.5px);
                    -webkit-backdrop-filter: blur(2.5px);
                    border: 1px solid rgba(255, 255, 255, 0.18);
                    box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    -webkit-box-shadow: rgba(142, 142, 142, 0.19) 0px 6px 15px 0px;
                    border-radius: 9px;
                    -webkit-border-radius: 9px;
                    color: rgba(255, 255, 255, 0.3);
                    color: var(--color-text);
                }

                @media screen and (max-width: 500px) {
                    .gateway-wrapper{
                        margin: 0 auto;
                        position: inherit;
                        top: 10%;
                        right: 0;
                    }
                }
            </style>
            <script type="text/javascript" th:inline="javascript">
                let bgMode = [[${theme.config.loginPage.loginTmpl}]]
                function renderBgImg() {
                    if(bgMode == "static_bgImg_style"){
                        let imgUrl = [[${theme.config.loginPage.bgSettings.bgStaticImg}]]
                        document.body.style.backgroundImage = "url(" + imgUrl + ")";
                    }else if(bgMode == "dynamic_bgImg_style"){
                        //动态背景图
                        const dynamicBgImgList = [[${theme.config.loginPage.dynamicBgImg}]];
                        const nowDate = new Date();
                        const nowHours = nowDate.getHours();
                        const nowMinutes = nowDate.getMinutes();
                        const pathImg = patchDynamicBgImg(nowHours, nowMinutes);
                        document.body.style.backgroundImage = "url(" + pathImg + ")";
                    }
                    document.body.style.backgroundSize = 'cover';
                    document.body.style.backgroundPosition = 'center';
                 }
                 // 匹配动态图片
                 function patchDynamicBgImg(curHours, curMinutes) {
                     const dynamicBgImgList = [[${theme.config.loginPage.bgSettings.dynamicBgImg}]];
                     if(dynamicBgImgList && !dynamicBgImgList.length) return;
                     const patchRes = dynamicBgImgList.filter(item => {

                          let startTime = item.realNode.start_time.split(":");
                          let endTime = item.realNode.end_time.split(":");

                          const startMilliSecond = getMilliSecond(startTime[0], startTime[1]);
                          const endMilliSecond = getMilliSecond(endTime[0], endTime[1]);
                          const currentMilliSecond = Date.now();

                          if(currentMilliSecond >= startMilliSecond && currentMilliSecond <= endMilliSecond){
                              return item;
                          }
                      });
                     return patchRes.length ? patchRes[0].realNode.specialBgImg : dynamicBgImgList[0].realNode.specialBgImg;
                 }
                 // 获取时间间隔
                 function getMilliSecond(hour, minute){
                    if(!hour || !minute){
                        return;
                    }
                    const referTime = new Date();
                    const targetTime = new Date(referTime.getFullYear(), referTime.getMonth(), referTime.getDate(), hour, minute);
                    return targetTime.getTime();
                 }

                 document.addEventListener('DOMContentLoaded', ()=>{
                    renderBgImg();
                 });
            </script>
        </th:block>
    </body>
</html>

这部分代码里边的样式可以根据自己的喜好进行修改,我主要说一下自定义样式的一些注意点。

1、自定义的 css 变量必须要在 .gateway-wrapper 这个类名下,例如:

.gateway-wrapper{
     --color-primary: #4ccba0;
     --color-secondary: #0e1731;
     --color-link: #1f75cb;
     --color-text: #374151;
     --color-border: #dfe6ec;
     --font-size-base: 15px;
     width: 100%;
     margin-top: 70px;
     position: sticky;
}

2、.halo-form-wrapper 这个类名是用来定义登录表单的盒子样式,不包括语言切换和注册那部分的盒子。

login-form-wrap-tuya.webp

3、登录按钮的选择器是 .halo-form button[type="submit"] ,由于我没有自定义注册页面模版以及登出模版,所以为了不影响这些模版的按钮样式,就使用这个选择器只修改登录页面下的登录按钮。

4、其他的元素样式自行在浏览器的开发者工具上自行调试修改。

5、css 样式根据自己的主题逻辑可以进行抽离,不一定需要写在 login.html 这个模版里边。

6、登录 logo 可以自己修改如下dom元素。由于我使用的是专业版,所以我在此教程上没有修改,使用了默认的logo 模版片段。

<div th:replace="~{gateway_fragments/common::haloLogo}"></div>

6、关于动态背景图的逻辑,可以自行修改,目前只是在指定的时间内加载指定的背景图片。

7、有些背景图可能和文字颜色发生冲突,这时候需要你自己手动修改一下,或者自己把文字颜色加入配置项中 ,每次在修改了背景图后指定一种文字颜色。由于个人时间有限,我后期在慢慢补充这个配置吧。