前言
在我写的之前的文章中,大多数都是针对一个技术点进行讲解,因为我是一个没有创意的人,所以很少写什么有很漂亮的效果的文章或者demo;
而这次是因为之前有一次逛掘金,看到了竞赛的模块,然后手一抖就点了报名,手都抖了那就只能硬着头皮上了,动画效果我就算了,本来就没啥创意,那就来一个简单的 ToDoList 吧;
但是我不允许我的 ToDoList 太 low,于是开始大量的参考别人设计的UI,然后加上自己的一些想法和创意,最后完成了这个简单的 ToDoList;
我要我的 ToDoList 不仅好看,还要能看很多技术点,同时这些个技术点不需要用的有多高明,让小白都能上手都能看懂一二,接下来就开始正式的炫技;
先看效果,感觉不错的话麻烦帮我在码上掘金中点个赞,非常感谢!!!
在 PC 上全屏查看效果最佳!
分解
我的 ToDoList 一共分为四个大部分,如上图所示,分别是:
- 顶部的标题栏
- 添加任务的输入框
- 显示任务的列表
- 浮动右侧的筛选
当然还需要一个好看的背景,这个就不算了,我直接一个线性渐变就搞定了;
css 部分
顶部的标题栏
顶部标题栏可以看到是一个艺术字的效果,有点立体的感觉,同时可以看大字体的颜色和背景色会有混合的效果;
其实这些样式都非常简单,css代码如下:
.todo-title {
font-family: 'Helvetica Neue', sans-serif;
font-size: 60px;
font-weight: bold;
font-style: italic;
text-align: center;
text-transform: uppercase;
color: #333;
text-shadow: 1px 1px 0 #999, 2px 2px 0 #888, 3px 3px 0 #777, 4px 4px 0 #666, 5px 5px 0 #555, 6px 6px 0 #444, 7px 7px 6px rgba(0, 0, 0, 0.4);
mix-blend-mode: color-burn;
}
首先随便设置一下字体样式,然后使用text-transform属性将字母转换为大写;
使用text-shadow属性设置文字的阴影,这里设置了七个阴影,使字体样式变的立体;
最后使用mix-blend-mode属性设置混合模式,这里使用的是color-burn,这个属性的作用是将文字的颜色和背景色进行混合;
可以看到这里并没有什么太多的高大上的技术,不熟悉的属性大概也就一两个,但是最后实现的效果很不错,不管你觉不觉得,反正我是这样觉得的;
友情提示:这次不讲技术点,只讲实现效果的代码,对技术点不熟悉的可以去
MDN查看相关属性的用法;
添加任务的输入框
这里的样式没啥好说的,就是一个输入框加一个按钮,但是可以看到这里的效果,输入框获取焦点的时候,整个包裹输入框的容器会有一个阴影回收聚焦的效果;
这里的效果也是非常简单,css代码如下:
.input-wrap:has(input:focus) {
box-shadow: 2px 2px 1px 2px rgba(0, 0, 0, 0.5);
}
这么炫酷的效果居然只有一行代码,这里使用了css的has伪类选择器,这个选择器的作用是选择包含指定选择器的元素;
可以理解为如果.input-wrap下面的input元素获取到了焦点,那么就会给.input-wrap添加一个阴影;
其他的css就是一些简单的样式,没有什么特别的属性,这里就不贴出来了;
显示任务的列表
显示任务的列表是整个 ToDoList 的核心,这里的样式也是最复杂的,但是也是最有意思的;
首先这里有三种状态的任务,分别是进行中、已完成、未完成;
他们的样式看起来是一样的,就是背景色不同,现在只是讲css部分,就先忽略它们之间的区别;
这里的样式也是非常简单,css代码如下:
.list-item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 4px;
background-image: linear-gradient(90deg, lighten(@active-color, 10%), darken(@active-color, 10%));
margin-bottom: 10px;
padding: 10px;
color: #fff;
cursor: pointer;
overflow: hidden;
transition: all 2s;
&::after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 0;
height: 100%;
z-index: -1;
transition: all 2s;
}
&.success::after {
background-image: linear-gradient(90deg, lighten(@success-color, 10%), darken(@success-color, 10%));
width: 100%;
}
&.fail::after {
background-image: linear-gradient(90deg, lighten(@fail-color, 10%), darken(@fail-color, 10%));
width: 100%;
}
svg {
margin-right: 10px;
path {
fill: transparent;
stroke: transparent;
stroke-width: 3;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
}
label {
font-size: 16px;
color: #fff;
flex-grow: 1;
cursor: pointer;
}
}
这里贴出的是全部的css代码,里面没有写注释,因为不需要全部都看懂,只需要看懂其中的一部分就可以了;
首先进行中的样式的颜色是使用linear-gradient属性设置的,而已完成和未完成的样式是使用::after伪元素设置的;
因为已完成和未完成最后会有一个动画效果,所以这里使用了::after伪元素来完成这个动画效果;
可以看到里面还有svg的样式,没想到还能掺杂svg的知识点吧,这里的svg是用来实现已完成和未完成的勾选图标的,同时里面也有动画效果,后面会讲到;
这里只要的技术点是less的lighten和darken函数,以及less的变量;
lighten函数的作用是将颜色变亮,darken函数的作用是将颜色变暗,这里的颜色是使用less的变量来设置的;
同时可以看到这个顶部有一个小提示的标题,这个标题使用了position: sticky属性,这个属性的作用是让元素在滚动到指定位置的时候固定在页面上;
浮动右侧的筛选
这里其实没有什么技术点,就是一个简单的绝对定位,没有动画效果,就不贴效果图和代码了;
js 部分
这里使用的技术是vue3,所以js方面也就是vue3的知识点;
添加任务
首先我这里没有删除任务的功能,也没有修改任务的功能,只有添加任务和修改任务状态的功能;
这一块只需要一个list就可以搞定了,然后新增的时候就直接往里面push,修改状态的时候就直接修改list里面的数据就可以了;
首先在输入框上面绑定一个text,然后在按钮上面绑定一个click事件,这里的click事件就是用来添加任务的;
<template>
<div class="input-wrap" @keyup.enter="handleAdd">
<input v-model="text" placeholder="新增一个待办事项吧"/>
<button @click="handleAdd">新增</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const text = ref('');
const list = ref([]);
const handleAdd = () => {
if (text.value === '') return;
list.value.push({
id: Math.random().toString(36).substr(2),
status: 'active',
label: text.value,
});
text.value = '';
}
</script>
可以看到我在最外层的div上面绑定了一个keyup.enter事件,这样我们就可以使用回车的方式来添加任务了;
这里的逻辑很简单,调用handleAdd就会往list里面push一个对象,这个对象里面有id、status、label三个属性;
id是一个随机的字符串,作为一个唯一的标识,用于后续的transition动画;
status是任务的状态,这里的状态有三种,分别是active、success、fail,没有使用TS就没有枚举,也懒得全局添加常量了;
label就是任务的内容,这里的label是从输入框里面获取的,所以这里需要一个text来绑定输入框的内容;
渲染任务
因为会有过滤状态,所以渲染任务并不是直接使用list来渲染的,而是使用computed来渲染的;
<template>
<div class="todo-list">
<div class="todo-tip">
今日事,今日毕,今天还有 {{ remainder }} 件事情要做
</div>
<div
v-for="item in filterList"
:key="item.id"
:class="['list-item', item.status]"
>
<!-- <svg>成功的svg省略</svg>-->
<!-- <svg>失败的svg省略</svg>-->
<label v-text="item.label"/>
</div>
<div class="filter-bar">
<div
:class="['filter-bar-item', filterStatus === '' && 'active']"
@click="filterStatus = ''"
>
全部
</div>
<div
:class="['filter-bar-item', filterStatus === 'active' && 'active']"
@click="filterStatus = 'active'"
>
待完成
</div>
<!-- <div>已完成、未完成省略</div>-->
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
// 进行中的数量
const remainder = computed(() => list.value.filter(item => item.status === 'active').length);
// 过滤状态标识
const filterStatus = ref('');
// 过滤后的任务列表
const filterList = computed(() => {
if (filterStatus.value === '') {
return list.value;
}
return list.value.filter(item => item.status === filterStatus.value);
});
</script>
上面为了方便阅读代码,删除了一些不相关的代码,省略了一些相同代码;
这里就是computed函数的运用了,使用在了两个地方,一个是remainder表示当前进行中的任务数量,还有一个是filterList表示过滤后的任务列表;
然后可以看到右侧的过滤列表的class绑定,这里并没使用三元表达式,而是使用了&&;
:class="['filter-bar-item', filterStatus === 'active' && 'active']"
这个表达式的意思是,如果前面一个条件成立,那么就会返回后面的值,如果前面一个条件不成立,那么就会返回false,这样就不会添加active这个class了;
一个小操作,可以比使用三元表达式更加简洁,还有||也是一样的,看各位的实际场景来使用;
修改任务状态
这里修改任务状态就是对列表中的status进行修改,然后filterList就会自动过滤掉;
<template>
<div class="todo-list">
<div
v-for="item in filterList"
:key="item.id"
:class="['list-item', item.status]"
>
<svg @click="handleDone(item, 'success')">
省略具体的svg
</svg>
<svg @click="handleDone(item, 'fail')">
省略具体的svg
</svg>
<label v-text="item.label"/>
</div>
</div>
</template>
<script setup>
const handleDone = (item, status) => {
item.status = status;
}
</script>
可以看到我这里用了两个svg来表示任务的完成状态按钮,也就是大家看到最前面截图的列表上前面两个框框;
这里的代码很简单,就是调用handleDone函数,然后传入当前的item和status,然后修改item的status就可以了;
剩下的交给computed和vue的响应式就可以了;
js想关的到这里就差不多了,剩下的就是动画部分了;
动画
动画最开始一个就是输入框的聚焦效果,没啥好说的,就是一个简单的transition属性就搞定了;
抛开这个,今天要介绍的动画效果有四个,分别是:
vue的transition动画svg的描边动画css的transition动画css的animation动画
vue的transition动画
这个动画是vue内置的组件,在我这里的应用就是任务列表的过渡动画;
<template>
<div class="todo-list">
<transition-group
name="fade"
mode="out-in"
:duration="500"
tag="div"
style="height: 500px; overflow: auto; margin: 0 -20px; padding: 0 20px;"
>
<div
v-for="item in filterList"
:key="item.id"
:class="['list-item', item.status]"
>
<!--省略-->
</div>
</transition-group>
</div>
</template>
<style lang="less">
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>
这是一个非常简单的渐入的过渡动画效果,可以看下面的截图,每次添加任务的时候会渐入的添加进来:
除了上面这个效果以外,还有就是列表切换的时候,也会有一个渐出的效果,这里大家可以自己在码上掘金中体验一下;
svg的描边动画
svg的描边动画在这个里面是用来表示任务的完成状态的,也就是前面提到的两个框框;
<template>
<div class="todo-list">
<div
v-for="item in filterList"
:key="item.id"
:class="['list-item', item.status]"
>
<svg
:class="['success-icon', item.status]"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
@mouseenter="handleHover($event, item, true)"
@mouseleave="handleHover($event, item, false)"
>
<rect x="1" y="1" rx="4" ry="4" width="30" height="30" stroke="#fff" fill="transparent" stroke-width="2"/>
<path d="M6 14l6 8 L24 10">
<animate
attributeName="stroke-dashoffset"
from="100"
to="0"
dur="1s"
begin="0s"
fill="freeze"
/>
</path>
</svg>
<svg
:class="['fail-icon', item.status]"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
@mouseenter="handleHover($event, item, true)"
@mouseleave="handleHover($event, item, false)"
>
<rect x="1" y="1" rx="4" ry="4" width="30" height="30" stroke="#fff" fill="transparent" stroke-width="2"/>
<path d="M8,8 L23,23 M23,8 L8,23" transform="rotate(-90, 15.5, 15.5)">
<animate
attributeName="stroke-dashoffset"
from="100"
to="0"
dur="1s"
begin="0s"
fill="freeze"
/>
</path>
</svg>
<label v-text="item.label"/>
</div>
</div>
</template>
<script setup>
const handleHover = (e, item, hasHover) => {
if (item.hover === hasHover) return;
item.hover = hasHover;
const path = e.target.getElementsByTagName('path')[0];
// 播放动画
if (hasHover) {
path.style.stroke = '#fff'
e.target.getElementsByTagName('animate')[0].beginElement();
} else {
path.style.stroke = 'transparent';
}
}
</script>
这里首先简简单单的画两个svg,里面的rect就是我们看到的框框,然后path就是我们看到的对勾和叉叉;
在path里面再加上一个animate标签,用来控制描边动画的效果;
但是这里有一个问题,就是svg的animate在一放到页面就会播放动画,这里就必须要用到js来控制了;
这里通过mouseenter和mouseleave来控制hover的状态,然后通过js来控制animate的播放;
为了在不是hover的时候,不显示对钩和叉叉,所以在mouseleave的时候,把path的stroke设置为transparent;
在mouseenter的时候,把path的stroke设置为#fff,这样就可以看到对钩和叉叉了;
然后执行beginElement方法,就可以播放动画了;
css的animation动画和transition动画
css的animation和transition放在一起讲,因为都是任务状态切换的动画效果;
css的animation动画应用在状态的切换,任务完成会播放一个庆祝的动画(放大缩小),任务失败会播放一个失败的动画(抖动);
css的transition动画就是背景色从0到100%填充的一个过程;
<template>
<div class="todo-list">
<div
v-for="item in filterList"
:key="item.id"
:class="['list-item', item.status]"
>
<svg
:class="['success-icon', item.status]"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
@click="handleDone(item, 'success')"
>
<rect x="1" y="1" rx="4" ry="4" width="30" height="30" stroke="#fff" fill="transparent" stroke-width="2"/>
<path
d="M6 14l6 8 L24 10"
>
<animate
attributeName="stroke-dashoffset"
from="100"
to="0"
dur="1s"
begin="0s"
fill="freeze"
/>
</path>
</svg>
<svg
:class="['fail-icon', item.status]"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
@click="handleDone(item, 'fail')"
>
<rect x="1" y="1" rx="4" ry="4" width="30" height="30" stroke="#fff" fill="transparent" stroke-width="2"/>
<path
d="M8,8 L23,23 M23,8 L8,23"
transform="rotate(-90, 15.5, 15.5)"
>
<animate
attributeName="stroke-dashoffset"
from="100"
to="0"
dur="1s"
begin="0s"
fill="freeze"
/>
</path>
</svg>
<label v-text="item.label"/>
</div>
</div>
</template>
<style lang="less">
/*失败动画,执行一次*/
.list-item.fail {
animation: shake 0.82s cubic-bezier(.36, .07, .19, .97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}
/*庆祝动画,执行一次*/
.list-item.success {
animation: celebrate 0.82s cubic-bezier(.36, .07, .19, .97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes celebrate {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
</style>
上面的代码就是css的animation动画,因为我们使用vue动态添加的class,所以每次状态切换都会播放动画;
animation动画默认只会播放一次,所以无需我们添加额外的属性来做控制;
animation动画的关键点就是keyframes,这里就不多说了;
.todo-list {
.list-item {
&::after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 0;
height: 100%;
z-index: -1;
transition: all 2s;
}
&.success::after {
background-image: linear-gradient(90deg, lighten(@success-color, 10%), darken(@success-color, 10%));
width: 100%;
}
&.fail::after {
background-image: linear-gradient(90deg, lighten(@fail-color, 10%), darken(@fail-color, 10%));
width: 100%;
}
}
}
上面省略了额外的一些样式,只保留状态切换背景色填充的样式;
上面的代码就是css的transition动画,原理就是使用::after伪元素,然后通过transition属性来控制背景色的填充;
最开始width为0,然后当添加了success或者fail的class时,width就会变为100%,然后就会有一个过渡的效果;
这样看起来的效果就是状态切换的时候,背景色会从0到100%填充,但是再次状态切换就没有这个效果了,因为::after这个时候的width已经是100%了,所以就没有过渡的效果了;
总结
到这里我这个 ToDoList 相关的技术点已经介绍了七七八八了,不用什么高大上的技术,但是也是一个不错的练手项目;
其中涉及到的技术点有:
vue的基本使用vue的computed计算属性vue的transition过渡动画css的animation动画css的transition动画css的has属性css的一些布局和其他基础知识less的使用less的颜色函数less的变量svg的animate动画
这些技术单独拿出来说可能都不是什么难点,但是这些技术点的组合使用,就能够做出一个不错的效果;
并且这里并不是特别深入的使用,非常适合新手练手;
最后这个在参加码上掘金编程挑战大赛,如果可以的话,麻烦在码上掘金中给我点个赞,谢谢;