前言
在我写的之前的文章中,大多数都是针对一个技术点进行讲解,因为我是一个没有创意的人,所以很少写什么有很漂亮的效果的文章或者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
动画
这些技术单独拿出来说可能都不是什么难点,但是这些技术点的组合使用,就能够做出一个不错的效果;
并且这里并不是特别深入的使用,非常适合新手练手;
最后这个在参加码上掘金编程挑战大赛,如果可以的话,麻烦在码上掘金中给我点个赞,谢谢;