feat(瀑布流): 在详情页添加图片瀑布流展示及预览功能
添加瀑布流布局组件用于展示图片列表,支持动态分配图片到两列 实现图片点击预览功能,使用vant组件支持索引和关闭按钮 初始化图片数据时兼容不同格式的图片地址
Showing
1 changed file
with
144 additions
and
5 deletions
| 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 { | ... | ... |
-
Please register or login to post a comment