首先感谢@Chengyunlai 的开源✔️
本文所有内容基建于开源项目✔️
欢迎大家去看项目的README,更加精简直接,配套学习更佳✔️
欢迎大家STAR!如果你也热爱前端基础的学习,一起来吧✔️
[项目介绍]
第一个项目是可伸缩类手风琴图形方块。效果如下图

\首先我们对于动效进行分解:
-
点击实现目标图片横向展开放大:一个点击事件
-
已放大图片收缩:消除上次点击图片的放大效果,转换为默认状态
-
图片的收缩和扩大以动画实现平滑过渡
/源JS代码
js
const panels = document.querySelectorAll('.panel')
panels.forEach(panel => {
panel.addEventListener('click', () => {
removeActiveClasses()
panel.classList.add('active')
})
})
function removeActiveClasses() {
panels.forEach(panel => {
panel.classList.remove('active')
})
}
\简单解读:
- 选中所有
.panel
元素(方便统一操作)
(panels
是一个 NodeList
,其数据结构类似于数组,这样我们就可以用.forEach给每个元素进行操作)
-
为每个
.panel
添加点击事件. -
每次点击时,先清除所有
.panel
的active
类,再给当前点击的那个添加active
类. (active
类来自于CSS中.) -
这样就能达到"点击一个卡片,它展开,其他都收缩"的效果.
/源CSS代码如下
css
@import url('https://fonts.googleapis.com/css?family=Muli&display=swap');
/* 从 Google Fonts 引入 "Muli" 字体,display=swap 表示字体加载失败时先显示系统字体 */
* {
box-sizing: border-box;
/* 所有元素使用 border-box 模型,可以控制元素宽高 */
}
body {
font-family: 'Muli', sans-serif;
/* 设置页面字体为 Muli,若加载失败使用 sans-serif */
display: flex;
/* 使用 Flexbox 布局 */
align-items: center;
/* 垂直方向居中对齐子元素 */
justify-content: center;
/* 水平方向居中对齐子元素 */
height: 100vh;
/* 设置 body 高度与浏览器窗口高度一致 */
overflow: hidden;
/* 当页面的内容超出了"body"元素的范围时,超出的部分会被隐藏 */
margin: 0;
/* 去掉默认的外边距 */
}
.container {
display: flex;
/* container 也使用 Flexbox,子元素 .panel 水平排列 */
width: 90vw;
/* 宽度为浏览器窗口宽度的 90%,响应式布局 */
}
.panel {
background-size: cover;
/* 背景图像等比缩放铺满容器 */
background-position: center;
/* 背景图像居中对齐 */
background-repeat: no-repeat;
/* 背景图像不重复 */
height: 80vh;
/* 高度为窗口高度的 80% */
border-radius: 50px;
/* 圆角半径为 50px,使边角圆润 */
color: #fff;
/* 文字颜色为白色 */
cursor: pointer;
/* 鼠标悬停时显示指针,表示可点击 */
flex: 0.5;
/* 初始占据容器宽度的 0.5 单位 */
margin: 10px;
/* 元素之间间距为 10px */
position: relative;
/* 相对定位,便于内部绝对定位子元素(如 h3) */
-webkit-transition: all 700ms ease-in;
/* 平滑过渡动画,持续 700ms,开始时较慢 */
}
.panel h3 {
font-size: 24px;
/* 设置标题字体大小为 24px */
position: absolute;
/* 绝对定位,相对于 .panel */
bottom: 20px;
/* 离底部 20px */
left: 20px;
/* 离左边 20px */
margin: 0;
/* 去除默认外边距 */
opacity: 0;
/* 默认透明,不显示标题 */
}
.panel.active {
flex: 5;
/* 激活状态时,占据 5 倍空间,实现放大效果 */
}
.panel.active h3 {
opacity: 1;
/* 激活时标题变为可见 */
transition: opacity 0.3s ease-in 0.4s;
/* 标题透明度动画:0.4 秒延迟后开始,0.3 秒缓慢进入 */
}
@media (max-width: 480px) {
/*进行移动端优化, 当屏幕宽度小于等于 480px(如手机屏幕)时 */
.container {
width: 100vw;
/* container 宽度占满整个视口宽度 */
}
.panel:nth-of-type(4),
.panel:nth-of-type(5) {
display: none;
/* 第 4 和第 5 个 panel 在小屏设备上隐藏,提高可视性和响应性能 */
}
}
为了实现放大效果,利用了flex,其中.panel.active{flex:5;}
,等价于
js
.panel.active {
flex-grow: 5; /* 扩展比例 */
flex-shrink: 1; /* 收缩比例(默认值)*/
flex-basis: 0%; /* 初始尺寸(默认值)*/
}
要点总结:flex控制元素大小变化,结合transition动画,实现平滑过渡。
flex能够弹性设置一维布局的动态比例,有更好的视觉效果,例如在本次项目中:
-
总共 5 个面板
-
其中一个是 active(flex: 5),其他四个是普通的(flex: 0.5)
-
那总 flex 值是:
5 + 0.5 * 4 = 7
-
所以占比:
- active:5 / 7 ≈ 71%
- 其他普通面板:0.5 / 7 ≈ 7.1%
✔️ 这就自然分配好了空间,且始终不会超出容器宽度。
[VUE实现]
直接参考项目的vue代码,git完代码,yarn安装完依赖后,可以在pages对应项目文件夹中看到并运行。
/将图片card分离模块化到data.ts文件
ts
interface Card {
name: string;
url: string;
}
const cards: Card[] = [
{
name: 'Explore The World',
url: 'https://images.unsplash.com/photo-1558979158-65a1eaa08691?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'
},
{
name: 'Wild Forest',
url: 'https://images.unsplash.com/photo-1572276596237-5db2c3e16c5d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'
},
{
name: 'Sunny Beach',
url: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1353&q=80'
},
{
name: 'City on Winter',
url: 'https://images.unsplash.com/photo-1551009175-8a68da93d5f9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1351&q=80'
},
{
name: 'Mountains - Clouds',
url: 'https://images.unsplash.com/photo-1549880338-65ddcdfd017b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'
}
];
export default cards;
/vue实现代码
js
<script setup lang="ts">
/* 'lang'声明TypeScript */
import cards from "~/pages/expanding-cards/data";
//导入图片数据
const activeIndex = ref<number | null>(0)
//定义activeIndex,null(0)表示开始触发效果的盒子
onMounted(() => {
// 组件加载完后,此时可以安全访问 document
document.title = '拓展盒子'
})
</script>
js
<template>
//创建一个横向排列、间距均匀的容器
// mx-10: 左右水平方向的外边距; `flex`: 启用 Flex 布局;`w-full`: 设置宽度为容器的 100%;`gap-5`: 设置子元素之间的间距
<div class="mx-10 flex w-full gap-5">
js
<div
v-for="(card, index) in cards"//从data里提出cards进行for循环遍历
:key="index"//标记每个唯一的盒子
:style="{ backgroundImage: 'url(' + card.url + ')' }"//为每个盒子设置图片
class="relative h-[80vh] flex-[0.5] rounded-lg bg-cover bg-center transition-all duration-700 ease-in hover:cursor-pointer"
//- `h-[80vh]`: 卡片高度占浏览器视口 80%;
//`flex-[0.5]`: 初始flex设置为0.5;
//`rounded-lg`: 圆角;
//`bg-cover`, `bg-center`: 背景图填满容器并居中;
//`transition-all duration-700 ease-in`: 动画过渡属性;
//`hover:cursor-pointer`: 鼠标悬停时显示手型;
:class="{ 'flex-[5]': index === activeIndex }"//如果当前盒子响应,flex设置为5,"==="严格等于保证未激活盒子保持默认flex-[0.5]
@click="activeIndex = index"//定义点击事件,点击激活盒子响应
>
js
<h3
//文字默认设置
class="absolute bottom-20 left-20 text-2xl font-bold text-white opacity-0"
:class="{ 'opacity-100 delay-[400ms] transition-opacity duration-300 ease-in': index === activeIndex }"
>//文字响应后的设置
js
</h3>
</div>
</div>
</template>
Vue 功能/组件 | 功能描述 | 用途说明 |
---|---|---|
<script setup> |
用更少的代码写组件逻辑 | 让你不必在vue里写 export default ,代码更短、更清晰,逻辑和模板一目了然 |
ref() |
创建一个"可自动更新"的变量 | 当这个变量的值变化时,页面上的内容会自动刷新,无需手动操作 |
onMounted() |
"组件准备好"时自动运行代码 | 组件加载完成后做一次性操作,比如给页面贴标题或发起数据请求 |
<template> |
写组件的 HTML 模板 | 把变量、指令和 HTML 结构放在一起,决定页面长什么样 |
v-for |
按数组数量生成多个元素 | 自动为数组中每一项创建一个 DOM 元素 |
:key |
给每个循环出来的元素一个标识 | 帮 Vue 快速找到对应的元素,避免重复渲染和奇怪的闪烁 |
:style |
给元素设置"实时更新"的样式 | 根据图片地址动态填背景图,不用写死 CSS |
:class |
给元素添加或移掉 CSS 类 | 根据条件给卡片放大、文字渐显等效果 |
@click |
为元素绑定点击动作 | 用户点一下就能触发函数,切换激活卡片或执行其他交互 |
[动画扩展:更Q弹]
对于transition动画加一个贝塞尔曲线的微调,实现先回拉后展开,像橡皮筋有个回弹效果
js
:style="{ backgroundImage: 'url(' + card.url + ')' }"
==>修改为
:style="{ backgroundImage: 'url(' + card.url + ')' , transitionTimingFunction: 'cubic-bezier(0.61,-0.19,0.7,-0.11)' }"
效果如下图

本文可能部分存在错误,或者排版问题,欢迎各位指教,我将及时更正,大家对相关知识感兴趣也可以一起讨论,一起学习进步😃