MastersDetail.vue 7.48 KB
<!--
 * @Date: 2025-10-30 20:00:25
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-11-13 14:20:03
 * @FilePath: /stdj_h5/src/views/MastersDetail.vue
 * @Description: 文件描述
-->
<template>
  <div class="masters-detail-container">
    <section class="single-list">
      <div class="item-card">
        <img :src="article_item.src" :alt="article_item.title" class="item-image" />
        <div class="item-content">
          <div class="item-role">{{ article_item.role }}</div>
          <div class="item-name" v-html="formatNameWithSuperscript(article_item.name)"></div>
          <div class="item-desc" v-html="article_item.desc"></div>
        </div>
      </div>
    </section>
    <!-- 瀑布流显示图片 -->
    <div class="waterfall-content" v-if="columns[0].length || columns[1].length">
        <div class="waterfall-container">
            <div class="waterfall-column" v-for="(column, cidx) in columns" :key="cidx">
                <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>
                </div>
            </div>
        </div>
    </div>
  </div>
</template>

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

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

useTitle('三師七證 - 詳情')

const route = useRoute()
const router = useRouter()

const article_item = ref({})
const columns = reactive([[], []])
const all_images = ref([])

/**
 * 为name字段的第一个文字添加上标效果
 * @param {string} name - 原始姓名
 * @returns {string} - 带上标的HTML字符串
 */
const formatNameWithSuperscript = (name) => {
  if (!name || name.length === 0) return name

  const firstChar = name.charAt(0)
  const restChars = name.slice(1)

  return `<sup style="font-size: 0.6rem;">上</sup>${firstChar}<sup style="font-size: 0.6rem;">下</sup>${restChars}`
}

/**
 * 加载文章详情
 */
const loadArticleDetail = async () => {
  try {
    const articleId = route.params.id
    const { code, data } = await getArticleDetailAPI({ i: articleId })
    if (code) {
      // 遍历data对象,将每个元素转换为新的对象格式
      article_item.value = {
        id: data.id,
        role: data.post_excerpt,
        name: data.post_title,
        src: data?.file_list?.photo?.value + '?imageMogr2/thumbnail/400x/strip/quality/70',
        desc: data.post_content,
        imgs: data?.file_list?.img || []
      }

      // 初始化瀑布流图片
      initWaterfallImages(article_item.value.imgs)
    }
  } catch (error) {
    console.error('加载文章详情失败:', error)
  }
}

/**
 * 将后端返回的 imgs 初始化为瀑布流数据
 * 说明:兼容字符串数组或对象数组(对象可能含 value/src 字段)
 * @param {Array} imgs 原始图片列表
 * @returns {void}
 */
const initWaterfallImages = function (imgs) {
    const list = Array.isArray(imgs) ? imgs : []
    const mapped = list.map(function (item, idx) {
        /**
         * 提取图片地址
         * 说明:优先读取对象的 value/src 字段,否则视为字符串
         */
        const src = typeof item === 'string' ? item : (item?.value || item?.src || '')
        return {
            id: typeof item === 'object' && item?.id ? item.id : idx + 1,
            // 如果item.size 大于 20MB 不加后缀
            src: src + (item.size > 20 * 1024 * 1024 ? '' : '?imageMogr2/thumbnail/400x/strip/quality/70'),
            title: typeof item === 'object' && item?.name ? item.name : ('图片' + (idx + 1)),
            height: Math.floor(Math.random() * 200) + 200
        }
    }).filter(function (it) { return String(it.src || '').trim().length > 0 })

    all_images.value = mapped
    distributeImages(mapped)
}

/**
 * 分配图片到两列
 * 说明:根据当前列累计高度将图片放入较低的一列
 * @param {Array} images 新增图片列表
 * @returns {void}
 */
const distributeImages = function (images) {
    images.forEach(function (image) {
        const leftHeight = columns[0].reduce(function (sum, item) { return sum + item.height + 20 }, 0)
        const rightHeight = columns[1].reduce(function (sum, item) { return sum + item.height + 20 }, 0)
        if (leftHeight <= rightHeight) {
            columns[0].push(image)
        } else {
            columns[1].push(image)
        }
    })
}

/**
 * 图片加载完成回调
 * 说明:可在此根据实际宽高调整高度,当前保持随机高度布局
 * @param {Event} e 图片加载事件
 * @returns {void}
 */
const onImageLoad = function (e) {
    // 预留:如需根据图片实际比例调整高度,可在此实现
}

/**
 * 图片加载失败回调
 * 说明:记录或上报错误,必要时移除该项
 * @param {Event} evt 图片加载事件
 * @returns {void}
 */
const onImageError = function (evt) {
    // 预留:可移除该图片项或替换为占位图
    console.warn(`图片加载失败:${evt.target.src}`)
}

/**
 * 图片点击预览
 * 说明:使用 Vant 的图片预览组件,支持索引与关闭按钮
 * @param {Object} item 当前图片项
 * @returns {void}
 */
const onImageClick = function (item) {
    const currentIndex = all_images.value.findIndex(function (img) { return img.id === item.id })
    const images = all_images.value.map(function (img) { return img.src.replace('?imageMogr2/thumbnail/400x/strip/quality/70', '') })
    showImagePreview({
        images: images,
        startPosition: currentIndex >= 0 ? currentIndex : 0,
        showIndex: true,
        closeable: true
    })
}

onMounted(() => {
  loadArticleDetail()
})
</script>

<style lang="less" scoped>
.masters-detail-container {
  padding: 1.5rem;
  background: #F2EBDB;
  min-height: 100vh;
  /* 背景至少覆盖整个视口高度 */
  width: 100%;
  box-sizing: border-box;

  // 瀑布流区域样式
  .waterfall-content {
    margin-top: 1rem;
  }
  .waterfall-container {
    display: flex;
    gap: 0.5rem;
    align-items: flex-start;
  }
  .waterfall-column {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
  .waterfall-item {
    .image-wrapper {
      width: 100%;
      background: #FFFFFF;
      border-radius: 0.5rem;
      overflow: hidden;
      box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.08);
      img {
        width: 100%;
        display: block;
        object-fit: cover;
      }
    }
  }
}

.single-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 1rem;
}

.item-card {
  position: relative;
  padding: 0.5rem;
  background: #F2EBDB;
  border: 2px solid #6B4102;
  overflow: hidden;
  transition: transform 0.2s ease;
}

.item-image {
  width: 100%;
  height: auto;
  display: block;
}

.item-content {
  padding: 0.85rem;
  text-align: center;
  color: #6B4102;
  background: #FCF8F1;
}

.item-role {
  font-size: 0.75rem;
  opacity: 0.95;
}

.item-name {
  font-size: 1.25rem;
  font-weight: 600;
  margin-top: 0.25rem;
}

.item-desc {
  margin-top: 0.5rem;
}
</style>