00:00:00
相册
PhotoGallery.vue
相册组件
vue
<template>
<div class="photo-gallery">
<!-- 标题 -->
<h2 class="gallery-title">我的相册</h2>
<!-- 分类筛选 -->
<div class="category-filter">
<button
v-for="category in categories"
:key="category"
:class="['filter-btn', activeCategory === category ? 'filter-btn--active' : '']"
@click="activeCategory = category"
>
{{ category }}
</button>
</div>
<!-- 图片网格 -->
<div class="photo-grid">
<div
v-for="(photo, index) in filteredPhotos"
:key="photo.id || index"
class="photo-item"
@click="openSwiper(index)"
>
<div class="photo-wrapper">
<img :src="photo.thumbnail || photo.src" :alt="photo.alt" class="photo-img" loading="lazy" />
<!-- 悬停信息 -->
<div class="photo-overlay">
<h3 class="overlay-title">{{ photo.title || "未命名" }}</h3>
<p class="overlay-desc">{{ photo.description || "" }}</p>
</div>
</div>
</div>
</div>
<!-- 使用 Swiper 组件 -->
<MySwiper v-if="showSwiper" :photos="filteredPhotos" :initial-index="currentIndex" @close="closeSwiper" />
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import MySwiper from "./MySwiper.vue"; // 导入 Swiper 组件
import { photos } from "./PhotoGalleryData";
// 响应式状态
const showSwiper = ref(false); // 控制 Swiper 显示
const currentIndex = ref(0); // 当前显示的图片索引
const activeCategory = ref("全部"); // 当前选中的分类
// 计算属性:筛选当前分类的图片
const filteredPhotos = computed(() => {
if (activeCategory.value === "全部") {
return photos.value;
}
return photos.value.filter(photo => photo.category === activeCategory.value);
});
// 计算属性:获取所有分类(含"全部")
const categories = computed(() => {
const categorySet = new Set(photos.value.map(photo => photo.category));
return ["全部", ...Array.from(categorySet)];
});
// 打开轮播的方法(点击图片时调用)
const openSwiper = index => {
showSwiper.value = true; // 显示轮播
currentIndex.value = index;
document.body.style.overflow = "hidden"; // 禁止页面滚动
};
// 方法:关闭 Swiper
const closeSwiper = () => {
showSwiper.value = false; // 隐藏轮播
document.body.style.overflow = ""; // 恢复页面滚动(之前为了禁止滚动设置了 overflow: hidden)
};
</script>
<style scoped>
/* 相册容器 */
.photo-gallery {
max-width: 90rem; /* 对应原 max-w-[90rem] */
margin: 0 auto; /* 对应原 mx-auto */
padding: 2rem 1rem; /* 对应原 px-4 py-8,移动端适配调整 */
}
/* 相册标题 */
.gallery-title {
/* 响应式字体大小:1.5rem ~ 2.5rem,对应原 clamp(1.5rem,3vw,2.5rem) */
font-size: 1.5rem;
font-weight: 700; /* 对应原 font-bold */
text-align: center; /* 对应原 text-center */
margin-bottom: 2rem; /* 对应原 mb-8 */
color: #1f2937; /* 对应原 text-gray-800 */
transition: color 0.3s ease;
}
html.dark .gallery-title {
color: #ffffff; /* 对应原 dark:text-white */
}
/* 分类筛选容器 */
.category-filter {
display: flex;
flex-wrap: wrap; /* 对应原 flex-wrap */
justify-content: center; /* 对应原 justify-center */
gap: 0.75rem; /* 对应原 gap-3 */
margin-bottom: 2rem; /* 对应原 mb-8 */
}
/* 筛选按钮基础样式 */
.filter-btn {
padding: 0.5rem 1rem; /* 对应原 px-4 py-2 */
border-radius: 9999px; /* 对应原 rounded-full */
font-size: 0.875rem; /* 对应原 text-sm */
transition: all 0.3s ease; /* 对应原 transition-all duration-300 */
border: none;
cursor: pointer;
background-color: #f3f4f6; /* 对应原 bg-gray-100 */
color: #374151; /* 对应原 text-gray-700 */
}
/* 筛选按钮 hover 状态 */
.filter-btn:hover:not(.filter-btn--active) {
background-color: #e5e7eb; /* 对应原 hover:bg-gray-200 */
}
/* 筛选按钮激活状态 */
.filter-btn--active {
background-color: #3b82f6; /* 对应原 bg-blue-500 */
color: #ffffff; /* 对应原 text-white */
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06); /* 对应原 shadow-md */
}
/* 深色模式:筛选按钮样式调整 */
html.dark .filter-btn:not(.filter-btn--active) {
background-color: #1f2937; /* 对应原 dark:bg-gray-800 */
color: #e5e7eb; /* 对应原 dark:text-gray-200 */
}
html.dark .filter-btn:hover:not(.filter-btn--active) {
background-color: #374151; /* 对应原 dark:hover:bg-gray-700 */
}
/* 图片网格容器 */
.photo-grid {
display: grid;
/* 响应式列数:1列 → 2列 → 3列 → 4列,对应原 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 */
grid-template-columns: repeat(1, 1fr);
gap: 1rem; /* 对应原 gap-4 */
}
/* 平板端(sm):2列 */
@media (min-width: 640px) {
.photo-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 桌面端(md):3列 + 更大间距 */
@media (min-width: 768px) {
.photo-grid {
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem; /* 对应原 md:gap-6 */
}
}
/* 大屏桌面端(lg):4列 */
@media (min-width: 1024px) {
.photo-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* 图片项容器 */
.photo-item {
cursor: pointer;
overflow: hidden; /* 对应原 overflow-hidden */
border-radius: 0.5rem; /* 对应原 rounded-lg */
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06); /* 对应原 shadow-md */
transition: all 0.3s ease; /* 对应原 transition-all duration-300 */
}
/* 图片项 hover 状态 */
.photo-item:hover {
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); /* 对应原 hover:shadow-xl */
}
/* 图片包裹层(控制宽高比) */
.photo-wrapper {
position: relative;
aspect-ratio: 5/3; /* 对应原 aspect-[5/3] */
overflow: hidden; /* 对应原 overflow-hidden */
}
/* 图片样式 */
.photo-img {
width: 100%;
height: 100%;
object-fit: cover; /* 对应原 object-cover */
transition: transform 0.7s ease; /* 对应原 transition-transform duration-700 */
}
/* 图片 hover 缩放效果 */
.photo-item:hover .photo-img {
transform: scale(1.1); /* 对应原 group-hover:scale-110 */
}
/* 图片悬停信息层 */
.photo-overlay {
position: absolute;
inset: 0; /* 对应原 inset-0 */
/* 渐变背景:从下到上透明,对应原 bg-gradient-to-t from-black/70 to-transparent */
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
opacity: 0; /* 初始透明 */
transition: opacity 0.3s ease; /* 对应原 transition-opacity duration-300 */
display: flex;
flex-direction: column;
justify-content: flex-end; /* 对应原 justify-end */
padding: 1rem; /* 对应原 p-4 */
}
/* 图片 hover 时显示信息层 */
.photo-item:hover .photo-overlay {
opacity: 1; /* 对应原 group-hover:opacity-100 */
}
/* 悬停标题 */
.overlay-title {
color: #ffffff; /* 对应原 text-white */
font-weight: 500; /* 对应原 font-medium */
font-size: 1.125rem; /* 对应原 text-lg */
}
/* 悬停描述 */
.overlay-desc {
color: rgba(255, 255, 255, 0.8); /* 对应原 text-white/80 */
font-size: 0.875rem; /* 对应原 text-sm */
margin-top: 0.25rem; /* 对应原 mt-1 */
}
/* 深色模式:确保覆盖层样式一致(无需额外调整,因已用 rgba 和白色) */
html.dark .photo-overlay {
/* 深色模式下渐变更明显,可选增强对比 */
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
}
</style>PhotoGalleryData.ts
相册数据
ts
import { ref } from "vue";
export const photos = ref([
{
src: "/home/bg01.webp",
thumbnail: "/home/bg01.webp",
w: 1200,
h: 800,
alt: "山间风景",
title: "山间云海",
description: "拍摄于黄山云海日出时分",
category: "风景"
}
]);MySwiper.vue
图片预览组件
vue
<template>
<div class="swiper-container">
<!-- 关闭按钮 -->
<button class="close-btn" @click="close">×</button>
<!-- Swiper 组件 -->
<swiper-container
init="false"
:initial-slide="initialIndex"
class="swiper-wrapper"
@swiperslidechange="onSlideChange"
>
<swiper-slide v-for="(photo, index) in photos" :key="photo.id || index" class="slide-item">
<div class="slide-content">
<img :src="photo.src" :alt="photo.alt" class="slide-image" />
</div>
</swiper-slide>
</swiper-container>
<!-- 图片信息 -->
<div class="photo-info">
<h3 class="photo-title">{{ currentPhoto.title || "照片" }}</h3>
<p class="photo-desc">{{ currentPhoto.description || "" }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { register } from "swiper/element/bundle";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
register();
const props = defineProps({
photos: {
type: Array,
required: true
},
initialIndex: {
type: Number,
default: 0
}
});
const emit = defineEmits(["close"]);
const currentSlideIndex = ref(props.initialIndex);
// 当前显示的照片(基于当前索引)
const currentPhoto = computed(() => {
// 处理循环时的索引问题
const index = currentSlideIndex.value % props.photos.length;
return props.photos[index >= 0 ? index : 0] || {};
});
// 关闭 Swiper
const close = () => {
if (swiperInstance) {
swiperInstance.destroy(); // 关闭 Swiper
swiperInstance = null;
}
emit("close");
};
// swiper实例对象
let swiperInstance = null;
// 监听幻灯片切换事件
const onSlideChange = e => {
console.log("onSlideChange", swiperInstance?.activeIndex);
currentSlideIndex.value = swiperInstance?.activeIndex;
};
onMounted(() => {
const swiperEl = document.querySelector("swiper-container");
const swiperParams = {
centeredSlides: true, // 使活动幻灯片居中
slidesPerView: 1, // 一次只显示一张幻灯片
spaceBetween: 10, // 幻灯片之间的间距为10px
loop: false, // 不启用循环播放
navigation: true, // 显示左右箭头导航
keyboard: {
enabled: true // 启用键盘方向键控制
},
//watchSlidesProgress: false, // 下方显示查看进度
/*autoplay: {
delay: 1000,
disableOnInteraction: false
},*/
effect: "cube", // 用立方体效果切换幻灯片, 'slide', 'fade', 'cube', 'coverflow', 'flip', 'creative' or 'cards'
fadeEffect: {
crossFade: true // 淡入淡出效果的交叉淡入
},
parallax: true, // 启用视差效果
lazy: {
enabled: true, // 启用图片懒加载
loadPrevNext: true // 加载当前幻灯片的前后一张
},
observer: true, // 监听Swiper元素变化
observeParents: true, // 监听父元素变化
virtualTranslate: true, // 虚拟位移提高性能
injectStyles: [
// 注入样式
`
@media (max-width: 700px) {
.swiper-button-prev {
display: none;
}
.swiper-button-next {
display: none;
}
}
`
],
/* breakpoints: {
640: {
slidesPerView: 1, // 屏幕宽度<=640px时显示1张幻灯片
spaceBetween: 10,
},
1024: {
slidesPerView: 3, // 屏幕宽度>=1024px时显示3张幻灯片
spaceBetween: 20,
},
},*/
on: {
init(el) {
swiperInstance = el;
console.log("Swiper 初始化完成", swiperInstance);
},
keyPress(swiper, keyCode) {
//console.log("Swiper keyPress", swiper, keyCode)
// 监听esc键
if (keyCode === 27) {
close();
}
}
}
};
// 合并配置
Object.assign(swiperEl, swiperParams);
// 初始化 Swiper
swiperEl.initialize();
});
</script>
<style scoped></style>
<style scoped>
/* 全屏容器 */
.swiper-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.95); /* 对应 bg-black/95 */
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
/* 关闭按钮 */
.close-btn {
position: absolute;
top: 1.5rem; /* 对应 top-6 */
right: 1.5rem; /* 对应 right-6 */
color: white;
font-size: 2.5rem; /* 对应 text-4xl */
background: none;
border: none;
cursor: pointer;
transition: color 0.3s ease; /* 对应 transition-colors */
z-index: 10; /* 对应 z-10 */
}
.close-btn:hover {
color: #d1d5db; /* 对应 text-gray-300 */
}
/* Swiper 容器 */
.swiper-wrapper {
width: 100%;
max-width: 87.5rem; /* 对应 max-w-7xl */
}
/* 轮播项 */
.slide-item {
max-height: 90vh;
max-width: 100%;
object-fit: contain;
}
/* 轮播内容容器 */
.slide-content {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* 轮播图片 */
.slide-image {
max-height: 90vh;
max-width: 100%;
object-fit: contain;
}
/* 图片信息区域 */
.photo-info {
width: fit-content;
margin: 0 auto;
position: absolute;
bottom: 2rem; /* 对应 bottom-8 */
left: 0;
right: 0;
text-align: center;
color: white;
z-index: 10; /* 对应 z-10 */
padding-left: 1rem; /* 对应 px-4 */
padding-right: 1rem;
transition: opacity 0.3s ease;
opacity: 0.5;
}
.photo-info:hover {
opacity: 1;
}
/* 图片标题 */
.photo-title {
font-size: 1.25rem; /* 对应 text-xl */
font-weight: 600; /* 对应 font-semibold */
margin: 0;
}
/* 图片描述 */
.photo-desc {
color: #d1d5db; /* 对应 text-gray-300 */
margin-top: 0.25rem; /* 对应 mt-1 */
margin-bottom: 0;
}
</style>配置
因为预览组件使用到了swiper,所以需要在docs/.vitepress/config.ts中配置
ts
export default defineConfig({
vue: {
template: {
compilerOptions: {
// 将 swiper- 开头的标签视为自定义元素(Web Components)
isCustomElement: tag => tag.startsWith("swiper-")
}
}
},依赖
package.json
json
"dependencies": {
"swiper": "^11.2.10",
}