Students.vue 7.91 KB
<!--
 * @Date: 2025-10-30 20:52:19
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-10-30 21:22:42
 * @FilePath: /stdj_h5/src/views/Students.vue
 * @Description: 戒子页面 - 图片瀑布流展示
-->

<template>
  <div class="students-container">
    <!-- 瀑布流内容 -->
    <div class="waterfall-content">
      <van-list
        v-model:loading="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onLoad"
      >
        <div class="waterfall-container">
          <div class="waterfall-column" v-for="(column, index) in columns" :key="index">
            <div
              class="waterfall-item"
              v-for="item in column"
              :key="item.id"
              @click="onImageClick(item)"
            >
              <div class="image-wrapper">
                <img
                  :src="item.url"
                  :alt="item.title"
                  :style="{ height: item.height + 'px' }"
                  @load="onImageLoad"
                  @error="onImageError"
                />
                <div class="image-overlay">
                  <span class="image-title">{{ item.title }}</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </van-list>
    </div>

    <!-- 遮罩层弹窗 -->
    <van-overlay :show="showOverlay" @click="closeOverlay">
      <div class="overlay-content" @click.stop>
        <!-- 关闭按钮 -->
        <div class="close-btn" @click="closeOverlay">
          <van-icon name="cross" size="1.5rem" color="#fff" />
        </div>

        <!-- 图片展示 -->
        <div class="overlay-image-wrapper">
          <img
            :src="selectedImage?.url"
            :alt="selectedImage?.title"
            class="overlay-image"
          />
        </div>

        <!-- 描述内容 -->
        <div class="overlay-description">
          <h3 class="overlay-title">{{ selectedImage?.title }}</h3>
          <p class="overlay-text">{{ selectedImage?.description }}</p>
        </div>
      </div>
    </van-overlay>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { generateWaterfallData } from '@/utils/mockData'
import { useTitle } from '@vueuse/core';

useTitle('同戒录')

// 响应式数据
const loading = ref(false)
const finished = ref(false)
const currentPage = ref(1)
const pageSize = 10
const allImages = ref([])
const columns = reactive([[], []])

// 遮罩层相关状态
const showOverlay = ref(false)
const selectedImage = ref(null)

// 加载数据
const onLoad = async () => {
  loading.value = true

  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 1000))

  try {
    const newData = generateWaterfallData(currentPage.value, pageSize)

    if (newData.length === 0) {
      finished.value = true
    } else {
      allImages.value.push(...newData)
      distributeImages(newData)
      currentPage.value++
    }
  } catch (error) {
    console.error('加载数据失败:', error)
  } finally {
    loading.value = false
  }
}

// 分配图片到两列
const distributeImages = (images) => {
  images.forEach(image => {
    // 计算两列的当前高度
    const leftHeight = columns[0].reduce((sum, item) => sum + item.height + 20, 0)
    const rightHeight = columns[1].reduce((sum, item) => sum + item.height + 20, 0)

    // 将图片添加到高度较小的列
    if (leftHeight <= rightHeight) {
      columns[0].push(image)
    } else {
      columns[1].push(image)
    }
  })
}

// 图片点击事件
const onImageClick = (item) => {
  console.log('点击图片:', item)
  selectedImage.value = item
  showOverlay.value = true
}

// 关闭遮罩层
const closeOverlay = () => {
  showOverlay.value = false
  selectedImage.value = null
}

// 图片加载成功
const onImageLoad = (event) => {
  console.log('图片加载成功:', event.target.src)
}

// 图片加载失败
const onImageError = (event) => {
  console.error('图片加载失败:', event.target.src)
  // 可以设置默认图片
  event.target.src = 'https://via.placeholder.com/300x400?text=加载失败'
}

// 组件挂载时初始化
onMounted(() => {
  // 初始加载第一页数据
  onLoad()
})
</script>

<style scoped>
.students-container {
  background-color: #F2EBDB;
  min-height: 100vh;
}

.header {
  position: sticky;
  top: 0;
  z-index: 100;
  background-color: #fff;
}

.waterfall-content {
  padding: 1rem;
}

.waterfall-container {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
}

.waterfall-column {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.waterfall-item {
  background-color: #fff;
  border-radius: 0.5rem;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  cursor: pointer;
}

.waterfall-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

.image-wrapper {
  position: relative;
  overflow: hidden;
}

.image-wrapper img {
  width: 100%;
  display: block;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.waterfall-item:hover .image-wrapper img {
  transform: scale(1.05);
}

.image-overlay {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
  padding: 1rem 0.75rem 0.75rem;
  transform: translateY(100%);
  transition: transform 0.3s ease;
}

.waterfall-item:hover .image-overlay {
  transform: translateY(0);
}

.image-title {
  color: #fff;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.4;
}

/* 加载状态样式 */
:deep(.van-list__loading) {
  padding: 1rem;
  text-align: center;
  color: #969799;
}

:deep(.van-list__finished-text) {
  padding: 1rem;
  text-align: center;
  color: #969799;
  font-size: 0.875rem;
}

/* 响应式设计 */
@media (max-width: 480px) {
  .waterfall-content {
    padding: 0.75rem;
  }

  .waterfall-container {
    gap: 0.5rem;
  }

  .waterfall-column {
    gap: 0.5rem;
  }

  .image-title {
    font-size: 0.8125rem;
  }
}

/* 骨架屏效果 */
.waterfall-item.loading {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

/* 遮罩层样式 */
:deep(.van-overlay) {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.overlay-content {
  background-color: #fff;
  border-radius: 1rem;
  max-width: 90vw;
  max-height: 80vh;
  overflow: hidden;
  position: relative;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}

.close-btn {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2.5rem;
  height: 2.5rem;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: background-color 0.2s ease;
}

.close-btn:hover {
  background-color: rgba(0, 0, 0, 0.7);
}

.overlay-image-wrapper {
  width: 100%;
  max-height: 60vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f5f5f5;
}

.overlay-image {
  width: 100%;
  height: auto;
  max-height: 60vh;
  object-fit: contain;
  display: block;
}

.overlay-description {
  padding: 1.5rem;
  background-color: #fff;
}

.overlay-title {
  font-size: 1.25rem;
  font-weight: 600;
  color: #333;
  margin: 0 0 1rem 0;
  line-height: 1.4;
}

.overlay-text {
  font-size: 1rem;
  color: #666;
  line-height: 1.6;
  margin: 0;
  white-space: pre-wrap;
}

/* 移动端适配 */
@media (max-width: 480px) {
  .overlay-content {
    max-width: 95vw;
    max-height: 85vh;
    border-radius: 0.75rem;
  }

  .close-btn {
    top: 0.75rem;
    right: 0.75rem;
    width: 2rem;
    height: 2rem;
  }

  .overlay-description {
    padding: 1rem;
  }

  .overlay-title {
    font-size: 1.125rem;
  }

  .overlay-text {
    font-size: 0.875rem;
  }
}
</style>