Vue实现一个画廊页面

本文最后更新于:2024年8月13日 下午

使用Vue实现一个简单的瀑布流画廊页面demo,图片简单地从assets目录下读取并在页面进行展示,在这过程中遇到了一些问题进行一下记录。

Vite读取assets目录下的文件

webpack中可以通过require.context方法来读取目录下的所有文件

其具体用法为

1
require.context(directory,useSubdirectories,regExp)

directory: 要搜索的目录

useSubdirectories:是否要搜索子目录

regExp:匹配文件的正则表达式

1
2
3
4
5
6
7
8
9
10
const files = require.context('public/icon',false,/\.svg$/);
const icons = Object.keys(files).map((path)=>{
let reg = /\/([^/]*)\.(jpg|png)/;
let name = path.match(reg)[1];
return {
key: path,
name: name.
value: files[path]
}
})

正则表达式解释

\/表示匹配一个/符号,用\做转义

([^/]*)表示一个子表达式,匹配除了/符号外的任意字符{0,}次

\.表示转义,匹配一个.符号

(jpg|png)表示匹配jpg后缀或png后缀的图片

最后通过[1]取得文件名,[0]是匹配到的整个/.....jpg文件名,[2]是匹配到的后缀名

vite不支持使用require.context方法,当在Vite中使用require时会报错,提示Uncaught (in promise) ReferenceError: require is not defined,这是因为require是Node.js的原生方法,以CommonJS的方式加载文件,Webpack默认支持,而Vite不支持,在Vite中使用特殊的import.meta.glob函数从文件系统中导入模块。

1
2
3
4
5
6
7
8
9
10
const files = import.meta.glob('/src/assets/images/*.jpg');
const images = Object.keys(files).map((path: any) => {
let reg = /\/([^/]*)\.(jpg|png)/;
let name = path.match(reg)[1];
return {
key: path,
name: name,
value: files[path],
}
})

函数中可以通过传入数组来匹配多种模式

1
const modules = import.meta.glob(['./dir/*.js','./another/*.js'])

也可用通过!来排除某些目录某些文件

1
const modules = import.meta.glob(['./dir/*.js','!**/bar.js'])

拓展链接-为什么要使用require来动态引入图片

经过这一步,我们读取到了assets/images目录下的所有图片文件,并保存到了变量images

显示图片

在读取了图片之后,我们便要将图片显示在页面中,通过v-for指令来遍历images数组,并将image的key值赋值给img标签的src属性来读取图片。

1
2
3
4
5
<div class='container'>
<div v-for="(image, index) in images">
<img :src="image.key" alt="">
</div>
</div>

这时候会发现图片按照原尺寸显示在画面中,铺满了整个页面,为了实现瀑布流的显示效果,我们需要再添加一些样式,首先给外层的div添加一个container类,用于控制列数。

1
2
3
4
5
.container {
width: 900px;
column-count: 3;
column-gap: 0;
}

设定容器的宽度,并通过column-count属性设置内容显示为3列,且设置间隙设为0,这时候图片就会以三列的形式进行呈现。

这时候图片已经分为了三列,但是还是按照原有的尺寸,并不能完整的显示,因此还需要再添加一些样式

1
2
3
4
img{
width: 100%;
object-fit: contain;
}

此时图片的宽度会填满整个容器(此时的容器宽度是container的1/3,因为container设置了分为三列),并且由于设置了object-fit:contain,图片会保持原有尺寸,这样就实现了简单的瀑布流的显示效果。

这时候我们会发现,在整个页面的纵向中,图片与图片之间存在着白色的间隙,而我们希望消除这一间隙,让每一张图片之间都严丝合缝。这时候有三种可行的解决方案。

  1. container添加属性line-height:0
  2. img添加属性display:block
  3. img添加属性vertical-align: bottom

出现这一情况的原因是:HTML中的img标签是内联元素,这意味着它们会被像文本一样对待,而在HTML中文本行有额外的空间,用于放置类似g,j等字母的下行部分,所以会出现空白间隙。 而通过给container设置line-height:0以及给img添加属性display:block都是在消除这些额外空间。具体内容查看图片下方出现空行的成因和解决方案

最后得到如图所示的显示效果:

显示遮罩+动画

光有了图片的展示肯定还不够,参考一般的图片网站,都会有鼠标移上去时的遮罩效果,并显示一些详细的信息,接下来我们便来实现这一效果。

遮罩设计

首先先设计一个简单的遮罩样式,首先遮罩应该要完全覆盖图片大小,并且通过一个深色半透明背景来暗化图片,在遮罩的中间显示一个“眼睛”图标,在遮罩的下方显示图片的名称,如下图所示。

定位和布局
将背景设置position:absolute,并设置lefttop为0,widthheight为100%,使得遮罩能够铺满整个图片,此处因为在html中该div位于图片下方,所以没有设置z-index

图标
从网上找到一个眼睛图样的svg图标,导入到项目后通过之前实现的[[通过Vite-plugin-svg-icons实现自定义的Icon组件 | Icon组件]]来使用,布局到页面的正中间。

图片名称
在之前获取的images数组中,通过正则表达式截取了每个图片文件的名称,在此处同样通过position:absolute定位到容器的下方,并设置文本溢出时显示省略号。

实现代码

1
2
3
4
<div class="mask">
<VIcon name="eye" size="24" />
<div>{{ image.name }}</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.mask {
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
background-color: rgba(0, 0, 0, 0.4);

svg {
stroke: none;
color: rgba(255, 255, 255, 0.9);
}

div {
position: absolute;
bottom: 30px;
width: 90%;
height: 15px;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #FFFFFFc2;
}
}

触发显隐事件

一开始对于遮罩的显隐通过一个布尔变量来控制,并监听鼠标事件,当鼠标进入该元素时,将布尔变量设置为true,鼠标离开时设置为false,但这时候发现当鼠标进入到一个元素后,所有的图片上都会显示遮罩,这是因为所有的图片遮罩都由一个变量进行控制,所以会同步显隐。因此需要对每个图片进行单独控制。采用index比对的方式来控制遮罩的显隐。

定义一个变量hoverIndex用于表示当前所hover的元素是哪个元素,同样监听鼠标进入和离开事件,当鼠标进入时将hoverIndex赋值为当前元素的index,离开时将hoverIndex设置为-1,在遮罩的显示逻辑中替换成hoverIndex===index,当元素下标等于激活元素下标时显示遮罩。

1
2
3
4
5
6
7
<div v-for="(image, index) in images" class="item" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
<img :src="image.key" alt="">
<div v-show="hoverIndex === index" class="mask">
<VIcon name="eye" size="24" />
<div>{{ image.name }}</div>
</div>
</div>

此时实现了当鼠标移动到一个图片时就可以显示对应的遮罩的效果,但是该遮罩的效果是直接出现,也是直接消失,显得非常的生硬,便希望通过添加一些过渡效果,使得遮罩的出现和消失更加自然,而由于使用v-show来进行遮罩的显隐,没有办法直接使用transition来给opacity添加过渡效果。

过渡动画

因此采用Vue内带的<transition>标签来实现过渡动画,在元素外层包裹上<transition>标签即可,<transition>存在6个css类用于整个过渡过程。

v-enter-fromv-leave-to,这两个类分别控制的是进入动画开始前和离开动画结束后的样式,相当于基础样式,一般相同。
v-enter-tov-leave-from,这两个类分别控制的是进入动画结束后和离开动画开始前的样式,
相当于激活样式,一般相同。
v-enter-activev-leave-active,这两个类分别控制在进入和离开过程中的动画效果,对上述四个类中的样式进行过渡。
在没有给<transition>命名时,默认都使用v-的类名,如果给<transition>添加了name属性,例如fade,则类名都变成fade-

在本次的画廊案例中,我们需要添加的过渡效果有

  • 进入时遮罩由隐到显,图标从略下方向上浮出
  • 离开时遮罩由显到隐,图标向下方隐去

实现代码:

1
2
3
4
5
6
7
8
9
<div v-for="(image, index) in images" class="item" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1" >
<img :src="image.key" alt="">
<transition name="fade">
<div v-show="hoverIndex === index" class="mask">
<VIcon name="eye" size="24" />
<div>{{ image.name }}</div>
</div>
</transition>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.fade-enter-from,
.fade-leave-to {
opacity: 0;

svg {
transform: translateY(30px);
}
}

.fade-enter-to,
.fade-leave-from {
opacity: 1;

svg {
transform: translateY(0px);
}
}

.fade-enter-active,
.fade-leave-active {
transition: opacity .5s ease;

svg {
transition: transform .5s ease;
}
}

全屏显示

到了这时候显然还缺少了一个部分,就是全屏显示的部分,都已经显示眼睛图标了,如果不能点击打开新窗口,那这图标有什么用呢。

定位和布局

全屏显示的布局也同样非常简洁,全屏的深色半透明背景,中间显示图片,点击空余部分则关闭窗口回到原来的界面。
这一窗口的设计和遮罩的设计非常类似,只不过这一窗口的大小是整个屏幕。因为部分图片的原尺寸太大,会造成整个屏幕都显示不下的情况,且如此会导致没有空余空间可以点击来退出全屏窗口,因此对图片的尺寸进行限制,最多占用屏幕宽高的90%

点击事件

首先新增两个变量

  • isShowOverlay:布尔变量,用于控制全屏窗口的显隐
  • showPic:图片地址,用于控制全屏窗口显示的图片
    当点击元素时,将isShowOverlay设置为true,显示窗口,将showPic设置为图片的地址,用于在全屏窗口中进行显示。给overlay元素添加点击事件,当鼠标点击时将isShowOverlay设为false,关闭窗口,同时将showPic设为空。这时候点击图片的时候也会触发父元素overlay的点击事件,使得全屏窗口关闭,因此需要对点击事件进行拦截,此时就用到了Vue的事件修饰符,通过给图片也添加点击事件,并添加事件修饰符.stop来阻止事件的冒泡传递,这样在子元素中触发的点击事件就不会再触发父元素的点击事件。

图片大小与点击事件冲突

在一开始,限制图片的大小时,将图片的widthheight都设置为了90%,并且在点击事件处理中,将图片本身的点击事件进行了拦截,不做任何处理。这也导致了一个问题,即在距离图片较近的背景部分点击时,也不会关闭窗口。
在调试时发现是因为设置了widthheight为90%,导致整个图片占据了整个画面宽高90%的大小,而不是图片本身的大小,也就是说即使图片是个长方形,它占据的面积仍然是一个正方形,从而使得整个正方形内的点击事件都失效。
因此需要对样式进行调整,将widthheight都调整为fit-content,并将max-widthmax-height设置为90%,由此实现了全屏窗口的展示。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="container">
<div v-for="(image, index) in images" class="item" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1" @click="showPic = image.key; isShowOverlay = true">
<img :src="image.key" alt="">
<transition name="fade">
<div v-show="hoverIndex === index" class="mask">
<VIcon name="eye" size="24" />
<div>{{ image.name }}</div>
</div>
</transition>
</div>
</div>
<transition name="fade">
<div class="overlay" v-if="isShowOverlay" @click="isShowOverlay = false; showPic = ''">
<img :src="showPic" alt="" @click.stop="">
</div>
</transition>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import VIcon from '@/components/VIcon.vue'
import { ref } from 'vue'

const files = import.meta.glob('/src/assets/images/*.jpg');
const images = Object.keys(files).map((path: any) => {
let reg = /\/([^\/]*)\.(jpg|png)/;
let name = path.match(reg)[1];
return {
key: path,
name: name,
value: files[path],
}
})
const hoverIndex = ref(-1)
const isShowOverlay = ref(false)
const showPic = ref('')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<style scoped>
.container {
width: 900px;
column-count: 3;
column-gap: 0;
line-height: 0;
}

.item {
position: relative;
}

.mask {
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
background-color: rgba(0, 0, 0, 0.4);

svg {
stroke: none;
color: rgba(255, 255, 255, 0.9);
}

div {
position: absolute;
bottom: 30px;
width: 90%;
height: 15px;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #FFFFFFc2;
}
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;

svg {
transform: translateY(30px);
}
}

.fade-enter-to,
.fade-leave-from {
opacity: 1;

svg {
transform: translateY(0px);
}
}

.fade-enter-active,
.fade-leave-active {
transition: opacity .5s ease;

svg {
transition: transform .5s ease;
}
}

img {
width: 100%;
object-fit: contain;
box-sizing: border-box;
break-inside: avoid;
}

.overlay {
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;

img {
width: fit-content;
height: fit-content;
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
}
</style>


Vue实现一个画廊页面
http://starnight.top/2024/08/10/Vue实现一个画廊页面/
作者
Cardy Xie
发布于
2024年8月10日
许可协议