JavaScript 已禁用

为了获得更好的体验,请启用JavaScript,或点击下方链接跳转:

跳转至百度
Skip to content

相册

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">&times;</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",
  }
最近更新