借助 CSS 动画,撸一个“平平无奇”的登录页

2,363 阅读12分钟

前言

大家好!我是 嘟老板。登录页不用多说,但凡涉及用户的系统,必然少不了登录,Github 上也有很多优秀的后台管理项目,供大家借鉴。那为什么我还要为此特地写一篇文章呢,当然是想督促自己做点不一样的事情。随手拿来的东西,多半不会珍惜,自己设计,自己实现,才能更深刻。

阅读本文您将收获:

  1. 了解登录页的基础结构。
  2. 了解雷达波纹效果实现思路及代码。
  3. 了解 CSS 动画相关内容。

登录页实现

zimu-admin 是结合 Vue3、TS、ElementPlus、Sass 等技术栈搭建。因此登录页的代码涉及以上相关技术的应用。

基础结构

基础结构目前没有加入过多的元素,仅包括:

  • 用户名、密码输入框
  • 错误消息提示栏
  • 登录按钮
  • 注册
  • 忘记密码

登录页代码写在 login/index.vue 中。

template 代码如下:

<!--
  登录页面
-->
<template>
  <div class="login">
    <div class="login-content">
      <H2>登录</H2>
      <div class="login-info-wrapper">
        <el-input
          v-model="loginInfo.username"
          class="login-info-item"
          placeholder="用户名"
        />
        <el-input
          v-model="loginInfo.password"
          class="login-info-item"
          placeholder="密码"
        />
        <div class="login-error-message">{{ errorMessage }}</div>
      </div>
      <el-button class="login-button" type="primary" @click="handleLogin"
        >登录</el-button
      >
      <div class="login-extra-buttons">
        <el-button type="primary" link @click="handleRegister">注册</el-button>
        <el-button type="primary" link @click="handleForgetPassword"
          >忘记密码?</el-button
        >
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
// 登录信息
const loginInfo = reactive({
  username: '',
  password: ''
})

// 登录错误信息
const errorMessage = ref('')

/**
 * 登录
 */
const handleLogin = () => {
  if (!loginInfo.username || !loginInfo.password) {
    errorMessage.value = '用户名或密码不能为空'
  }
}

/**
 * 注册
 */
const handleRegister = () => {
  console.log('注册')
}

/**
 * 忘记密码
 */
const handleForgetPassword = () => {
  console.log('忘记密码')
}
</script>

<style scoped lang="scss">
.login {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  position: relative;
  background: url('./imgs/background.png') center/100% no-repeat;

  &-content {
    width: 250px;
    height: fit-content;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: auto;
    z-index: 1;
    padding: 20px 10px;
    border-radius: 10px;
    background-color: var(--el-color-white);

    h2 {
      margin: auto;
      color: var(--el-text-color-regular);
      text-align: center;
    }
  }

  &-info-item {
    padding-top: 10px;
  }

  &-button {
    width: 100%;
  }

  &-error-message {
    height: 20px;
    font-size: 12px;
    color: var(--el-color-danger);
  }

  &-extra-buttons {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
}
</style>

ps: 以上代码只是实现的页面的基本布局,还没有添加实际的登录、注册等功能。

使用 el-input 组件实现用户名、密码的输入,使用 el-button 组件实现操作按钮,其中注册和忘记密码使用文字按钮,即 link 属性。

渲染效果如下:

image.png

雷达效果

为了突出 登录部分,我添加了以此为中心,向外发射波纹的雷达效果。

实现思路

我们来拆解下雷达效果的实现思路:

  1. 明确 中心点,波纹从中心点开始,向外扩散,本页面中就是 登录部分
  2. 明确波纹的圈数,比如要实现 6 圈波纹,无限重复发射。
  3. 明确波纹发射的动画效果,比如波纹的透明度逐层递减,越是外层,越趋近于透明。
  4. 明确波纹发射的时间间隔,从中心点开始,到最外层依次扩散。

实现代码

实现波纹向外扩散的效果,核心就是 CSS 动画

DOM 结构

首先,确定波纹 DOM 结构,我们要实现 6 层波纹,就需要 6div 元素。

<!-- 雷达效果 -->
<div class="radar-ripples">
  <div class="rr0"></div>
  <div class="rr1"></div>
  <div class="rr2"></div>
  <div class="rr3"></div>
  <div class="rr4"></div>
  <div class="rr5"></div>
</div>

其中 class 0-5div 就是雷达波纹,从 05 依次触发动画。

编写样式

  1. 雷达波纹 div 通用样式
.radar-ripples {
    div {
      width: 150px;
      height: 150px;
      position: absolute;
      top: calc((100% - 150px) / 2);
      left: calc((100% - 150px) / 2);
      border-radius: 50%;
      background: var(--el-color-primary-light-8);
      animation: rr 1.8s linear infinite;
    }
}

通过设置 positiontopleft 将雷达 div 定位到页面中心位置,与登录部分重合,看起来仿佛从登录部分发出来一般。

背景色设置为 ElementPlus 内置的颜色变量,--el-color-primary-light-8,可根据背景色选择合适的同色系。

通过 animation 设置动画属性,指定动画序列为 rr(使用 @keyframes 创建),间隔 1.8s,匀速运动(linear)且无限重复(infinite)。

  1. 单个雷达波纹的特定样式

由于每个波纹开始动画的时间不同,需要为每个波纹设置动画延迟时间

.rr0 {
  animation-delay: 0.3s;
}
.rr1 {
  animation-delay: 0.6s;
}
.rr2 {
  animation-delay: 0.9s;
}
.rr3 {
  animation-delay: 1.2s;
}
.rr4 {
  animation-delay: 1.5s;
}
.rr5 {
  animation-delay: 1.8s;
}
  1. 创建动画序列

在第一步的通用样式中,通过 animation 设置了动画序列 rr,我们来创建一下。

@keyframes rr {
    0% {
      transform: scale(1);
      opacity: 0.85;
    }
    25% {
      transform: scale(2);
      opacity: 0.65;
    }
    50% {
      transform: scale(3);
      opacity: 0.45;
    }
    75% {
      transform: scale(4);
      opacity: 0.25;
    }
    100% {
      transform: scale(5);
      opacity: 0.05;
    }
  }

使用 @keyframes 创建了名为 rr 的动画序列,包括 5 个关键帧,分别为 0%,25%,50%,75%100%,表示动画达到对应百分比时,渲染对应的样式。

每个关键帧设置了 transformopacity 两个属性,使用 scale 函数放大波纹元素,并通过 opacity 增加元素透明度,实现波纹随着扩散范围的增大,逐渐变淡直至消失的效果。

效果展示

Jul-04-2024 14-44-14.gif

CSS 动画

于开发来说,实现不是终点,掌握背后的技术才是。

现在我们来看看背后的知识 - CSS 动画

介绍

CSS 动画 可以将页面元素从一个样式切换到另一个样式。主要使用 CSS animation 相关属性实现。

实现动画分为以下两部分:

  • 动画目标元素的样式;
  • 设置动画运行过程中各时间节点的样式规则的 关键帧

CSS 动画属性

我们可以通过 animation 属性及相关的子属性,设置动画的时间时长重复次数及其他动画细节。

animation 子属性:

  • animation-delay:设置动画延时。指从元素加载完成到动画开始执行之间的等待时间。

    可用的属性值如下:

    • 自定义时长:

      自定义等待时长,默认值为 0s。由 数值单位 两部分组成,如 2s。

      数值 大于等于 0 时,在指定时间后开始执行动画;数值 小于 0 时,动画会立即开始,但是会从动画序列的 某个时间点(负数的绝对值) 开始。比如设置为 -1s,则会从动画运行到 1s 的位置开始。

      单位必须,可选 秒(s)毫秒(ms)

  • animation-direction:指定动画运行方向。

    可用的属性值如下:

    • normal:

      默认值,正向运行,动画每次重复运行时,都会重置到初始状态,重新开始。

    • reverse:

      反向运行,动画每次重复运行时,都从结束时的状态开始,反向运行,且 animation-timing-function 的值也会反转,比如 ease-in 会变为 ease-out

    • alternate:

      正反交替运行,动画重复运行时,从 1 开始计数,奇数次正向运行,偶数次反向运行。

    • alternate-reverse:

      正反交替运行,动画重复运行时,从 1 开始计数,奇数次反向运行,偶数次正向运行。

  • animation-duration:用于设置动画周期时长。

    可用的属性值如下:

    • 自定义时长:

      自定义完成一次动画所用时长,由 数值单位 两部分组成,如 2s。数值必须大于等于 0;单位必须,可选 秒(s)毫秒(ms)

  • animation-iteration-count:用于设置动画的重复次数。

    可用的属性值如下:

    • infinite:

      无限循环播放动画。

    • 自定义次数:

      自定义动画重复的次数,数字类型,默认为 1。可指定小数,表示指定到动画的某一部分,比如 0.5 表示执行一半动画。不能设置为负数。

  • animation-name:指定 @keyframes 创建的动画序列名称。

    可用的属性值如下:

    • none:

      表示没有关键帧,可用于禁用动画。

    • 自定义动画序列名:

      使用 @keyframes 创建的动画序列的名称。

  • animation-play-state:设置动画暂停或运行。

    可用的属性值如下:

  • animation-timing-function:设置动画运行速度。通过指定加速曲线,设置动画在各关键帧之间的变化速度。

    可用的属性值如下:

    • ease:

      默认值,动画在中间时加速,结尾时减速。等同于 cubic-bezier(0.25, 0.1, 0.25, 1.0)

    • linear:

      动画匀速运动。等同于 cubic-bezier(0.0, 0.0, 1.0, 1.0)

    • ease-in:

      动画开始速度慢,过程中逐渐加速,直至结束。等同于 cubic-bezier(0.42, 0, 1.0, 1.0)

    • ease-out:

      动画开始速度快,过程中逐渐减速,直至结束。等同于 cubic-bezier(0, 0, 0.58, 1.0)

    • ease-in-out:

      动画开始速度慢,过程中逐渐加速,最后在减速。等同于 cubic-bezier(0.42, 0, 0.58, 1.0)

    • 自定义

      若以上预定义值不满足需求,可以使用 cubic-bezier(p1, p2, p3, p4)steps(n, <jumpterm>) 自定义。

  • animation-fill-mode:指定在动画执行前执行后如何将样式应用到目标元素。

    可用的属性值如下:

    • none:

      默认值,动画未开始时,不会将关键帧中的任何样式应用到目标元素,仅应用元素已有的 CSS 样式渲染元素。

    • forwards:

      目标元素将保留动画最后一个关键帧的样式。

    • backwards:

      目标元素应用动画第一个关键帧的样式,并在 animation-delay 设置的时长内始终保留。

    • both:

      同时应用 forwardsbackwards 的规则。

animation 属性

animation 属性就是以上子属性的简写形式,类似 background,通过将相应子属性的值以 空格 分隔,设置到 animation 属性,可以达到和单独设置子属性一致的效果。

/* animation-duration | animation-timing-function | animation-delay | animation-iteration-count | animation-direction | animation-fill-mode | animation-play-state | animation-name */
animation: 3s ease-in 1s 2 reverse both paused slidein;

子属性匹配规则

到这有的同学可能会有疑问,animation 配置那么一大串,是如何匹配对应子属性的呢?

animation 语法格式如下:

animation: <animation-duration> || <animation-timing-function> || <animation-delay> || <animation-iteration-count> || <animation-direction> || <animation-fill-mode> || <animation-play-state> || [none | <animation-name>] 

其中:

  • <animation-timing-function><animation-iteration-count><animation-direction><animation-fill-mode><animation-play-state> 属性的值可以设置 0 次1 次,若未设置,则使用默认值。

  • 时间类属性的值可设置 0次1次2次,优先设置 <animation-duration> 属性。比如设置 2 次,第一个值赋值给 <animation-duration> 属性,第二个值会赋值给 <animation-delay> 属性。

  • 除了 animation-name 之外的其他非时间类属性值,必须可以被前面未匹配到值的属性接受。也就是说,解析 animation 属性值时,会按照语法中的子属性顺序,从前往后依次匹配,若未匹配到值,则使用默认值。

配置多组动画

animation 属性还可以用来指定多组动画,每组动画之前使用 逗号(,) 分隔。

animation:
  3s linear slidein,
  3s ease-out 5s slideout;

动画序列

动画序列用来设置动画的实际表现,使用 @keyframes 创建。

每个动画序列包含 2 个2 个以上 关键帧,关键帧指定了在特定时间节点的元素样式。

关键帧可通过以下两种方式定义:

  • 百分比

0% 表示动画开始的时刻;100% 表示动画结束的时刻;也可以添加 0%100% 之间的百分比,表示动画运行期间的状态,如 25%、50%、75% 等。

@keyframes rr {
    0% {
      transform: scale(1);
      opacity: 0.85;
    }
    25% {
      transform: scale(2);
      opacity: 0.65;
    }
    50% {
      transform: scale(3);
      opacity: 0.45;
    }
    75% {
      transform: scale(4);
      opacity: 0.25;
    }
    100% {
      transform: scale(5);
      opacity: 0.05;
    }
}
  • from、to

from 对应 0%to 对应 100%。若只有开始和结束两个关键帧,也可以用 fromto 别名来定义。

@keyframes rr {
    from {
      transform: scale(1);
      opacity: 0.85;
    }
    to {
      transform: scale(5);
      opacity: 0.05;
    }
}

动画事件监听

css 动画仅通过 css 便可实现,那 js 能否监听动画的执行呢?答案是肯定的。

监听动画的事件如下:

  • animationstartCSS 动画开始时触发,若设置了 animation-delay,则会在延迟时长后立即触发。
  • animationendCSS 动画结束时触发,若在动画完成时间结束动画,比如移除动画目标元素或移除动画,不会触发 animationend 时间。
  • animationiteration:动画运行到每个周期最后一帧时触发,动画结束时不会触发,仅触发 animationend 事件。

我们改造一下登录页看下效果:

templateclass="rr0" 的波纹 div 上绑定事件:

<div
    class="rr0"
    @animationstart="handleAnimationStart"
    @animationend="handleAnimationEnd"
    @animationiteration="handleAnimationIteration"
></div>

script 创建监听函数,打印事件触发时经过的时长:

const handleAnimationStart = (e: AnimationEvent) => {
  console.log(`AnimationStart: 时长  ${e.elapsedTime}`)
}

const handleAnimationEnd = (e: AnimationEvent) => {
  console.log(`AnimationEnd: 时长  ${e.elapsedTime}`)
}

const handleAnimationIteration = (e: AnimationEvent) => {
  console.log(`AnimationIteration: 时长  ${e.elapsedTime}`)
}

style 将动画重复次数改为 2 次,即运行 2 个周期后结束动画。

.radar-ripples {
    div {
      width: 150px;
      height: 150px;
      position: absolute;
      top: calc((100% - 150px) / 2);
      left: calc((100% - 150px) / 2);
      border-radius: 50%;
      background: var(--el-color-primary-light-8);
      animation: rr 1.8s linear 2;
    }
}

浏览器运行页面,动画在运行 2 个周期后结束。

打开控制台 Console 栏,查看打印日志:

image.png

可见 animationend 事件仅在动画完全结束时触发;animationiteration 事件在动画完成一个周期时触发,而在动画完全结束时不会触发。

总结

CSS 动画 通过 animation 相关属性就可实现神奇的动画效果,相较于传统的脚本实现方式,无论是上手难易程度还是代码量,都有极大的优势;并且基于浏览器渲染引擎的优化,CSS 动画 渲染性能更优,对于性能较低的设备,也有比较友好的体验。

结语

本文重点介绍了登录页的实现过程,雷达效果实现及 CSS 动画相关知识,旨在通过实际案例,帮助同学们加深对于 CSS 动画 的应用理解。相关代码已上传至 GitHub,欢迎 star

如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐