Students.vue 7.86 KB
<!--
 * @Date: 2025-10-30 20:52:19
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-11-03 15:31:32
 * @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.src"
                  :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>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useTitle } from '@vueuse/core';
import { useRouter } from 'vue-router'
import { showImagePreview } from 'vant'

// 导入接口
import { getImgStreamAPI } from '@/api/index.js'

useTitle('同戒录')

const router = useRouter()

const i = ref(router.currentRoute.value.query.i)

// 响应式数据
const loading = ref(false)
const finished = ref(false)
const currentPage = ref(0) // API使用0作为起始页码
const pageSize = 10
const allImages = ref([])
const columns = reactive([[], []])

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

  try {
    // 调用正式接口
    const { code, data } = await getImgStreamAPI({
      i: i.value,
      page: currentPage.value,
      limit: pageSize
    })

    if (code) {
      const newData = data.list.map(item => ({
        id: item.id,
        src: item.value, // 修改为src,用于ImagePreview
        title: item.name,
        description: item.description,
        date: item.post_date,
        height: Math.floor(Math.random() * 200) + 200 // 随机高度用于瀑布流布局
      }))

      if (newData.length === 0) {
        finished.value = true
      } else {
        allImages.value.push(...newData)
        distributeImages(newData)
        currentPage.value++
      }
    } else {
      finished.value = true
    }
  } catch (error) {
    console.error('加载数据失败:', error)
    finished.value = true
  } 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)
    }
  })
}

// 图片点击事件 - 使用ImagePreview
const onImageClick = (item) => {
  console.log('点击图片:', item)

  // 获取当前点击图片在所有图片中的索引
  const currentIndex = allImages.value.findIndex(img => img.id === item.id)

  // 提取所有图片的src用于预览
  const images = allImages.value.map(img => img.src)

  // 显示图片预览
  showImagePreview({
    images,
    startPosition: currentIndex,
    closeable: true
  })
}

// 图片加载成功
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>