hookehuyr

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

添加瀑布流布局组件用于展示图片列表,支持动态分配图片到两列
实现图片点击预览功能,使用vant组件支持索引和关闭按钮
初始化图片数据时兼容不同格式的图片地址
1 <!-- 1 <!--
2 * @Date: 2025-10-30 20:00:25 2 * @Date: 2025-10-30 20:00:25
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-04 18:15:15 4 + * @LastEditTime: 2025-11-12 11:30:48
5 * @FilePath: /stdj_h5/src/views/MastersDetail.vue 5 * @FilePath: /stdj_h5/src/views/MastersDetail.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -17,13 +17,37 @@ ...@@ -17,13 +17,37 @@
17 </div> 17 </div>
18 </div> 18 </div>
19 </section> 19 </section>
20 + <!-- 瀑布流显示图片 -->
21 + <div class="waterfall-content" v-if="columns[0].length || columns[1].length">
22 + <div class="waterfall-container">
23 + <div class="waterfall-column" v-for="(column, cidx) in columns" :key="cidx">
24 + <div
25 + class="waterfall-item"
26 + v-for="item in column"
27 + :key="item.id"
28 + @click="onImageClick(item)"
29 + >
30 + <div class="image-wrapper">
31 + <img
32 + :src="item.src"
33 + :alt="item.title"
34 + :style="{ height: item.height + 'px' }"
35 + @load="onImageLoad"
36 + @error="onImageError"
37 + />
38 + </div>
39 + </div>
40 + </div>
41 + </div>
42 + </div>
20 </div> 43 </div>
21 </template> 44 </template>
22 45
23 <script setup> 46 <script setup>
24 -import { ref, onMounted } from 'vue' 47 +import { ref, onMounted, reactive } from 'vue'
25 import { useTitle } from '@vueuse/core'; 48 import { useTitle } from '@vueuse/core';
26 import { useRoute, useRouter } from 'vue-router' 49 import { useRoute, useRouter } from 'vue-router'
50 +import { showImagePreview } from 'vant'
27 51
28 // 导入接口 52 // 导入接口
29 import { getArticleDetailAPI } from '@/api/index.js' 53 import { getArticleDetailAPI } from '@/api/index.js'
...@@ -34,6 +58,8 @@ const route = useRoute() ...@@ -34,6 +58,8 @@ const route = useRoute()
34 const router = useRouter() 58 const router = useRouter()
35 59
36 const article_item = ref({}) 60 const article_item = ref({})
61 +const columns = reactive([[], []])
62 +const all_images = ref([])
37 63
38 /** 64 /**
39 * 为name字段的第一个文字添加上标效果 65 * 为name字段的第一个文字添加上标效果
...@@ -63,21 +89,104 @@ const loadArticleDetail = async () => { ...@@ -63,21 +89,104 @@ const loadArticleDetail = async () => {
63 role: data.post_excerpt, 89 role: data.post_excerpt,
64 name: data.post_title, 90 name: data.post_title,
65 src: data?.file_list?.photo?.value, 91 src: data?.file_list?.photo?.value,
66 - desc: data.post_content 92 + desc: data.post_content,
93 + imgs: data?.file_list?.img || []
67 } 94 }
95 +
96 + // 初始化瀑布流图片
97 + initWaterfallImages(article_item.value.imgs)
68 } 98 }
69 } catch (error) { 99 } catch (error) {
70 console.error('加载文章详情失败:', error) 100 console.error('加载文章详情失败:', error)
71 } 101 }
72 } 102 }
73 103
104 +/**
105 + * 将后端返回的 imgs 初始化为瀑布流数据
106 + * 说明:兼容字符串数组或对象数组(对象可能含 value/src 字段)
107 + * @param {Array} imgs 原始图片列表
108 + * @returns {void}
109 + */
110 +const initWaterfallImages = function (imgs) {
111 + const list = Array.isArray(imgs) ? imgs : []
112 + const mapped = list.map(function (item, idx) {
113 + /**
114 + * 提取图片地址
115 + * 说明:优先读取对象的 value/src 字段,否则视为字符串
116 + */
117 + const src = typeof item === 'string' ? item : (item?.value || item?.src || '')
118 + return {
119 + id: typeof item === 'object' && item?.id ? item.id : idx + 1,
120 + src: src,
121 + title: typeof item === 'object' && item?.name ? item.name : ('图片' + (idx + 1)),
122 + height: Math.floor(Math.random() * 200) + 200
123 + }
124 + }).filter(function (it) { return String(it.src || '').trim().length > 0 })
125 +
126 + all_images.value = mapped
127 + distributeImages(mapped)
128 +}
129 +
130 +/**
131 + * 分配图片到两列
132 + * 说明:根据当前列累计高度将图片放入较低的一列
133 + * @param {Array} images 新增图片列表
134 + * @returns {void}
135 + */
136 +const distributeImages = function (images) {
137 + images.forEach(function (image) {
138 + const leftHeight = columns[0].reduce(function (sum, item) { return sum + item.height + 20 }, 0)
139 + const rightHeight = columns[1].reduce(function (sum, item) { return sum + item.height + 20 }, 0)
140 + if (leftHeight <= rightHeight) {
141 + columns[0].push(image)
142 + } else {
143 + columns[1].push(image)
144 + }
145 + })
146 +}
147 +
148 +/**
149 + * 图片加载完成回调
150 + * 说明:可在此根据实际宽高调整高度,当前保持随机高度布局
151 + * @param {Event} e 图片加载事件
152 + * @returns {void}
153 + */
154 +const onImageLoad = function (e) {
155 + // 预留:如需根据图片实际比例调整高度,可在此实现
156 +}
157 +
158 +/**
159 + * 图片加载失败回调
160 + * 说明:记录或上报错误,必要时移除该项
161 + * @returns {void}
162 + */
163 +const onImageError = function () {
164 + // 预留:可移除该图片项或替换为占位图
165 +}
166 +
167 +/**
168 + * 图片点击预览
169 + * 说明:使用 Vant 的图片预览组件,支持索引与关闭按钮
170 + * @param {Object} item 当前图片项
171 + * @returns {void}
172 + */
173 +const onImageClick = function (item) {
174 + const currentIndex = all_images.value.findIndex(function (img) { return img.id === item.id })
175 + const images = all_images.value.map(function (img) { return img.src })
176 + showImagePreview({
177 + images: images,
178 + startPosition: currentIndex >= 0 ? currentIndex : 0,
179 + showIndex: true,
180 + closeable: true
181 + })
182 +}
183 +
74 onMounted(() => { 184 onMounted(() => {
75 loadArticleDetail() 185 loadArticleDetail()
76 }) 186 })
77 </script> 187 </script>
78 188
79 - 189 +<style lang="less" scoped>
80 -<style scoped>
81 .masters-detail-container { 190 .masters-detail-container {
82 padding: 1.5rem; 191 padding: 1.5rem;
83 background: #F2EBDB; 192 background: #F2EBDB;
...@@ -85,6 +194,36 @@ onMounted(() => { ...@@ -85,6 +194,36 @@ onMounted(() => {
85 /* 背景至少覆盖整个视口高度 */ 194 /* 背景至少覆盖整个视口高度 */
86 width: 100%; 195 width: 100%;
87 box-sizing: border-box; 196 box-sizing: border-box;
197 +
198 + // 瀑布流区域样式
199 + .waterfall-content {
200 + margin-top: 1rem;
201 + }
202 + .waterfall-container {
203 + display: flex;
204 + gap: 0.5rem;
205 + align-items: flex-start;
206 + }
207 + .waterfall-column {
208 + flex: 1;
209 + display: flex;
210 + flex-direction: column;
211 + gap: 0.5rem;
212 + }
213 + .waterfall-item {
214 + .image-wrapper {
215 + width: 100%;
216 + background: #FFFFFF;
217 + border-radius: 0.5rem;
218 + overflow: hidden;
219 + box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.08);
220 + img {
221 + width: 100%;
222 + display: block;
223 + object-fit: cover;
224 + }
225 + }
226 + }
88 } 227 }
89 228
90 .single-list { 229 .single-list {
......