hookehuyr

feat(瀑布流): 在详情页添加图片瀑布流展示及预览功能

添加瀑布流布局组件用于展示图片列表,支持动态分配图片到两列
实现图片点击预览功能,使用vant组件支持索引和关闭按钮
初始化图片数据时兼容不同格式的图片地址
<!--
* @Date: 2025-10-30 20:00:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-04 18:15:15
* @LastEditTime: 2025-11-12 11:30:48
* @FilePath: /stdj_h5/src/views/MastersDetail.vue
* @Description: 文件描述
-->
......@@ -17,13 +17,37 @@
</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 } from 'vue'
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'
......@@ -34,6 +58,8 @@ const route = useRoute()
const router = useRouter()
const article_item = ref({})
const columns = reactive([[], []])
const all_images = ref([])
/**
* 为name字段的第一个文字添加上标效果
......@@ -63,21 +89,104 @@ const loadArticleDetail = async () => {
role: data.post_excerpt,
name: data.post_title,
src: data?.file_list?.photo?.value,
desc: data.post_content
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,
src: src,
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) {
// 预留:如需根据图片实际比例调整高度,可在此实现
}
/**
* 图片加载失败回调
* 说明:记录或上报错误,必要时移除该项
* @returns {void}
*/
const onImageError = function () {
// 预留:可移除该图片项或替换为占位图
}
/**
* 图片点击预览
* 说明:使用 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 })
showImagePreview({
images: images,
startPosition: currentIndex >= 0 ? currentIndex : 0,
showIndex: true,
closeable: true
})
}
onMounted(() => {
loadArticleDetail()
})
</script>
<style scoped>
<style lang="less" scoped>
.masters-detail-container {
padding: 1.5rem;
background: #F2EBDB;
......@@ -85,6 +194,36 @@ onMounted(() => {
/* 背景至少覆盖整个视口高度 */
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 {
......