前言
大家好!我是 嘟老板。登录页不用多说,但凡涉及用户的系统,必然少不了登录,Github 上也有很多优秀的后台管理项目,供大家借鉴。那为什么我还要为此特地写一篇文章呢,当然是想督促自己做点不一样的事情。随手拿来的东西,多半不会珍惜,自己设计,自己实现,才能更深刻。
阅读本文您将收获:
- 了解登录页的基础结构。
- 了解雷达波纹效果实现思路及代码。
- 了解 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 属性。
渲染效果如下:
雷达效果
为了突出 登录部分,我添加了以此为中心,向外发射波纹的雷达效果。
实现思路
我们来拆解下雷达效果的实现思路:
- 明确 中心点,波纹从中心点开始,向外扩散,本页面中就是 登录部分。
- 明确波纹的圈数,比如要实现 6 圈波纹,无限重复发射。
- 明确波纹发射的动画效果,比如波纹的透明度逐层递减,越是外层,越趋近于透明。
- 明确波纹发射的时间间隔,从中心点开始,到最外层依次扩散。
实现代码
实现波纹向外扩散的效果,核心就是 CSS 动画。
DOM 结构
首先,确定波纹 DOM 结构,我们要实现 6 层波纹,就需要 6 个 div 元素。
<!-- 雷达效果 -->
<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-5 的 div 就是雷达波纹,从 0 到 5 依次触发动画。
编写样式
- 雷达波纹 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;
}
}
通过设置 position 及 top、left 将雷达 div 定位到页面中心位置,与登录部分重合,看起来仿佛从登录部分发出来一般。
背景色设置为 ElementPlus 内置的颜色变量,--el-color-primary-light-8,可根据背景色选择合适的同色系。
通过 animation 设置动画属性,指定动画序列为 rr(使用 @keyframes 创建),间隔 1.8s,匀速运动(linear)且无限重复(infinite)。
- 单个雷达波纹的特定样式
由于每个波纹开始动画的时间不同,需要为每个波纹设置动画延迟时间。
.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;
}
- 创建动画序列
在第一步的通用样式中,通过 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%,表示动画达到对应百分比时,渲染对应的样式。
每个关键帧设置了 transform 和 opacity 两个属性,使用 scale 函数放大波纹元素,并通过 opacity 增加元素透明度,实现波纹随着扩散范围的增大,逐渐变淡直至消失的效果。
效果展示
CSS 动画
于开发来说,实现不是终点,掌握背后的技术才是。
现在我们来看看背后的知识 - CSS 动画。
介绍
CSS 动画 可以将页面元素从一个样式切换到另一个样式。主要使用 CSS animation 相关属性实现。
实现动画分为以下两部分:
- 动画目标元素的样式;
- 设置动画运行过程中各时间节点的样式规则的 关键帧。
CSS 动画属性
我们可以通过 animation 属性及相关的子属性,设置动画的时间、时长、重复次数及其他动画细节。
animation 子属性:
-
animation-delay
:设置动画延时。指从元素加载完成到动画开始执行之间的等待时间。可用的属性值如下:
-
自定义时长:
自定义等待时长,默认值为 0s。由 数值 和 单位 两部分组成,如 2s。
数值 大于等于 0 时,在指定时间后开始执行动画;数值 小于 0 时,动画会立即开始,但是会从动画序列的 某个时间点(负数的绝对值) 开始。比如设置为 -1s,则会从动画运行到 1s 的位置开始。
单位必须,可选 秒(s) 或 毫秒(ms)。
-
-
animation-direction
:指定动画运行方向。可用的属性值如下:
-
默认值,正向运行,动画每次重复运行时,都会重置到初始状态,重新开始。
-
反向运行,动画每次重复运行时,都从结束时的状态开始,反向运行,且 animation-timing-function 的值也会反转,比如 ease-in 会变为 ease-out。
-
正反交替运行,动画重复运行时,从 1 开始计数,奇数次正向运行,偶数次反向运行。
-
正反交替运行,动画重复运行时,从 1 开始计数,奇数次反向运行,偶数次正向运行。
-
-
animation-duration
:用于设置动画周期时长。可用的属性值如下:
-
自定义时长:
自定义完成一次动画所用时长,由 数值 和 单位 两部分组成,如 2s。数值必须大于等于 0;单位必须,可选 秒(s) 或 毫秒(ms)。
-
-
animation-iteration-count
:用于设置动画的重复次数。可用的属性值如下:
-
无限循环播放动画。
-
自定义次数:
自定义动画重复的次数,数字类型,默认为 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)。
-
动画匀速运动。等同于 cubic-bezier(0.0, 0.0, 1.0, 1.0)。
-
动画开始速度慢,过程中逐渐加速,直至结束。等同于 cubic-bezier(0.42, 0, 1.0, 1.0)。
-
动画开始速度快,过程中逐渐减速,直至结束。等同于 cubic-bezier(0, 0, 0.58, 1.0)。
-
动画开始速度慢,过程中逐渐加速,最后在减速。等同于 cubic-bezier(0.42, 0, 0.58, 1.0)。
-
自定义
若以上预定义值不满足需求,可以使用
cubic-bezier(p1, p2, p3, p4)
和steps(n, <jumpterm>)
自定义。
-
-
animation-fill-mode
:指定在动画执行前和执行后如何将样式应用到目标元素。可用的属性值如下:
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%。若只有开始和结束两个关键帧,也可以用 from 和 to 别名来定义。
@keyframes rr {
from {
transform: scale(1);
opacity: 0.85;
}
to {
transform: scale(5);
opacity: 0.05;
}
}
动画事件监听
css 动画仅通过 css 便可实现,那 js 能否监听动画的执行呢?答案是肯定的。
监听动画的事件如下:
- animationstart:CSS 动画开始时触发,若设置了 animation-delay,则会在延迟时长后立即触发。
- animationend:CSS 动画结束时触发,若在动画完成时间结束动画,比如移除动画目标元素或移除动画,不会触发 animationend 时间。
- animationiteration:动画运行到每个周期最后一帧时触发,动画结束时不会触发,仅触发 animationend 事件。
我们改造一下登录页看下效果:
template 在 class="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 栏,查看打印日志:
可见 animationend 事件仅在动画完全结束时触发;animationiteration 事件在动画完成一个周期时触发,而在动画完全结束时不会触发。
总结
CSS 动画 通过 animation 相关属性就可实现神奇的动画效果,相较于传统的脚本实现方式,无论是上手难易程度还是代码量,都有极大的优势;并且基于浏览器渲染引擎的优化,CSS 动画 渲染性能更优,对于性能较低的设备,也有比较友好的体验。
结语
本文重点介绍了登录页的实现过程,雷达效果实现及 CSS 动画相关知识,旨在通过实际案例,帮助同学们加深对于 CSS 动画 的应用理解。相关代码已上传至 GitHub,欢迎 star。
如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。