uview瀑布流

组件

1<template> 2 <view class="u-waterfall"> 3 <view id="u-left-column" class="u-column"><slot name="left" :leftList="leftList"></slot></view> 4 <view id="u-right-column" class="u-column"><slot name="right" :rightList="rightList"></slot></view> 5 </view> 6</template> 7 8<script> 9/** 10 * waterfall 瀑布流 11 * @description 这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实现了真正的 组件化,搭配LazyLoad 懒加载和loadMore 加载更多组件,让您开箱即用,眼前一亮。 12 * @tutorial https://www.uviewui.com/components/waterfall.html 13 * @property {Array} flow-list 用于渲染的数据 14 * @property {String Number} add-time 单条数据添加到队列的时间间隔,单位ms,见上方注意事项说明(默认200) 15 * @example <u-waterfall :flowList="flowList"></u-waterfall> 16 */ 17export default { 18 name: "u-waterfall", 19 props: { 20 value: { 21 // 瀑布流数据 22 type: Array, 23 required: true, 24 default: function() { 25 return []; 26 } 27 }, 28 // 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好 29 // 单位ms 30 addTime: { 31 type: [Number, String], 32 default: 200 33 }, 34 // id值,用于清除某一条数据时,根据此idKey名称找到并移除,如数据为{idx: 22, name: 'lisa'} 35 // 那么该把idKey设置为idx 36 idKey: { 37 type: String, 38 default: 'id' 39 } 40 }, 41 data() { 42 return { 43 leftList: [], 44 rightList: [], 45 tempList: [], 46 children: [] 47 } 48 }, 49 watch: { 50 copyFlowList(nVal, oVal) { 51 // 取差值,即这一次数组变化新增的部分 52 let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0; 53 // 拼接上原有数据 54 this.tempList = this.tempList.concat(this.cloneData(nVal.slice(startIndex))); 55 this.splitData(); 56 } 57 }, 58 mounted() { 59 this.tempList = this.cloneData(this.copyFlowList); 60 this.splitData(); 61 }, 62 computed: { 63 // 破坏flowList变量的引用,否则watch的结果新旧值是一样的 64 copyFlowList() { 65 return this.cloneData(this.value); 66 } 67 }, 68 methods: { 69 async splitData() { 70 if (!this.tempList.length) return; 71 let leftRect = await this.$uGetRect('#u-left-column'); 72 let rightRect = await this.$uGetRect('#u-right-column'); 73 // 如果左边小于或等于右边,就添加到左边,否则添加到右边 74 let item = this.tempList[0]; 75 // 解决多次快速上拉后,可能数据会乱的问题,因为经过上面的两个await节点查询阻塞一定时间,加上后面的定时器干扰 76 // 数组可能变成[],导致此item值可能为undefined 77 if(!item) return ; 78 if (leftRect.height < rightRect.height) { 79 this.leftList.push(item); 80 } else if (leftRect.height > rightRect.height) { 81 this.rightList.push(item); 82 } else { 83 // 这里是为了保证第一和第二张添加时,左右都能有内容 84 // 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边 85 if (this.leftList.length <= this.rightList.length) { 86 this.leftList.push(item); 87 } else { 88 this.rightList.push(item); 89 } 90 } 91 // 移除临时列表的第一项 92 this.tempList.splice(0, 1); 93 // 如果临时数组还有数据,继续循环 94 if (this.tempList.length) { 95 setTimeout(() => { 96 this.splitData(); 97 }, this.addTime) 98 } 99 }, 100 // 复制而不是引用对象和数组 101 cloneData(data) { 102 return JSON.parse(JSON.stringify(data)); 103 }, 104 // 清空数据列表 105 clear() { 106 this.leftList = []; 107 this.rightList = []; 108 // 同时清除父组件列表中的数据 109 this.$emit('input', []); 110 this.tempList = []; 111 }, 112 // 清除某一条指定的数据,根据id实现 113 remove(id) { 114 // 如果findIndex找不到合适的条件,就会返回-1 115 let index = -1; 116 index = this.leftList.findIndex(val => val[this.idKey] == id); 117 if(index != -1) { 118 // 如果index不等于-1,说明已经找到了要找的id,根据index索引删除这一条数据 119 this.leftList.splice(index, 1); 120 } else { 121 // 同理于上方面的方法 122 index = this.rightList.findIndex(val => val[this.idKey] == id); 123 if(index != -1) this.rightList.splice(index, 1); 124 } 125 // 同时清除父组件的数据中的对应id的条目 126 index = this.value.findIndex(val => val[this.idKey] == id); 127 if(index != -1) this.$emit('input', this.value.splice(index, 1)); 128 }, 129 // 修改某条数据的某个属性 130 modify(id, key, value) { 131 // 如果findIndex找不到合适的条件,就会返回-1 132 let index = -1; 133 index = this.leftList.findIndex(val => val[this.idKey] == id); 134 if(index != -1) { 135 // 如果index不等于-1,说明已经找到了要找的id,修改对应key的值 136 this.leftList[index][key] = value; 137 } else { 138 // 同理于上方面的方法 139 index = this.rightList.findIndex(val => val[this.idKey] == id); 140 if(index != -1) this.rightList[index][key] = value; 141 } 142 // 修改父组件的数据中的对应id的条目 143 index = this.value.findIndex(val => val[this.idKey] == id); 144 if(index != -1) { 145 // 首先复制一份value的数据 146 let data = this.cloneData(this.value); 147 // 修改对应索引的key属性的值为value 148 data[index][key] = value; 149 // 修改父组件通过v-model绑定的变量的值 150 this.$emit('input', data); 151 } 152 } 153 } 154} 155</script> 156 157<style lang="scss" scoped> 158@import "./style.components.scss"; 159 160.u-waterfall { 161 @include vue-flex; 162 flex-direction: row; 163 align-items: flex-start; 164} 165 166.u-column { 167 @include vue-flex; 168 // flex: 1; 169 flex-direction: column; 170 height: auto; 171 width: 50%; 172} 173 174.u-image { 175 width: 100%; 176} 177</style> 178

style.components.scss

1// 定义混入指令,用于在非nvue环境下的flex定义,因为nvue没有display属性,会报错 2@mixin vue-flex($direction: row) { 3 /* #ifndef APP-NVUE */ 4 display: flex; 5 flex-direction: $direction; 6 /* #endif */ 7}

使用

1<template> 2 <view v-if="newslist.length > 0" class="insurance-lesson"> 3 <view class="flex justify-between u-section"> 4 <view class="u-section_title">保险小课堂</view> 5 </view> 6 <view class="wrap"> 7 <waterfall v-model="newslist" ref="uWaterfall"> 8 <template v-slot:left="{ leftList }"> 9 <view 10 class="demo-warter" 11 v-for="(item, index) in leftList" 12 :key="index" 13 @click="onRowClick(item, index)" 14 > 15 <view class="demo-imgBox"> 16 <image 17 class="articleImg" 18 mode="widthFix" 19 v-if="item.imageList && item.imageList.length > 0" 20 :src="item.imageList[0].fileUrl" 21 /> 22 <image 23 mode="widthFix" 24 class="playImg" 25 v-if="item.type === '1004'" 26 src="https://ecustomercdn.itaiping.com/static/TPTSQ_FILES/20231027/info_20231027104126.png" 27 /> 28 </view> 29 <view class="demo-title"> 30 {{ item.title }} 31 </view> 32 <view class="flex justify-between"> 33 <view class="flex items-center"> 34 <u-avatar 35 shape="circle" 36 size="34" 37 src="https://ecustomercdn.itaiping.com/static/TPTSQ_FILES/20230124/info_20230124103610.png" 38 ></u-avatar> 39 <text class="insurance-lesson-title-serve">吉象保</text> 40 </view> 41 <text class="insurance-lesson-title-time"> 42 {{ item.officialAcct.desc }} 43 </text> 44 </view> 45 </view> 46 </template> 47 <template v-slot:right="{ rightList }"> 48 <view 49 class="demo-warter" 50 v-for="(item, index) in rightList" 51 :key="index" 52 @click="onRowClick(item, index)" 53 > 54 <view class="demo-imgBox"> 55 <image 56 class="articleImg" 57 v-if="item.imageList && item.imageList.length > 0" 58 :src="item.imageList[0].fileUrl" 59 mode="widthFix" 60 /> 61 <image 62 mode="widthFix" 63 class="playImg" 64 v-if="item.type === '1004'" 65 src="https://ecustomercdn.itaiping.com/static/TPTSQ_FILES/20231027/info_20231027104126.png" 66 /> 67 </view> 68 <view class="demo-title"> 69 {{ item.title }} 70 </view> 71 <view class="flex justify-between"> 72 <view class="flex items-center"> 73 <u-avatar 74 shape="circle" 75 size="34" 76 src="https://ecustomercdn.itaiping.com/static/TPTSQ_FILES/20230124/info_20230124103610.png" 77 ></u-avatar> 78 <text class="insurance-lesson-title-serve">吉象保</text> 79 </view> 80 <text class="insurance-lesson-title-time"> 81 {{ item.officialAcct.desc }} 82 </text> 83 </view> 84 </view> 85 </template> 86 </waterfall> 87 </view> 88 </view> 89</template> 90 91<script> 92import { getContentByPluginId } from '@/api/common' 93import waterfall from '@/components/waterfall/waterfall.vue' 94const base64 = require('../../util/base64') 95const JXB_ICON = `https://ecustomercdn.itaiping.com/static/TPTSQ_FILES/20230124/info_20230124103610.png` 96 97export default { 98 options: { styleIsolation: 'shared' }, 99 name: 'new-list', 100 props: { 101 tab: { 102 type: String, 103 }, 104 shouldShowTop: Boolean, 105 }, 106 components: { 107 waterfall, 108 }, 109 data() { 110 return { 111 list: [], 112 JXB_ICON, 113 } 114 }, 115 computed: { 116 newslist() { 117 return this.list.map((item) => { 118 return { 119 type: item.type, 120 ...item.cell[0][0], 121 } 122 }) 123 }, 124 }, 125 mounted() { 126 this.$nextTick(() => { 127 this.getNewsListUrl() 128 }) 129 }, 130 methods: { 131 onRowClick(item, index) { 132 if (item.router) { 133 if (item.router.includes('//h5?')) { 134 const strs = item.router.split('&') 135 const Request = {} 136 if (strs && strs.length > 0) { 137 strs.map((i, key) => { 138 console.log(i) 139 Request['url'] = strs[key].split('=')[1] 140 return Request 141 }) 142 } 143 if (Request.url !== undefined) { 144 let url = base64.decode(Request.url) 145 let route = { 146 name: 'articleDetail.index', 147 params: { 148 articleUrl: encodeURIComponent(url), 149 }, 150 } 151 this.$Router.push(route) 152 } 153 } 154 } 155 }, 156 async getNewsListUrl(pageNo) { 157 const plugInId = { 158 SIT: 'da8ac97cf7c34a798dd0a4a9942cbd8b', 159 PRO: '26fa600f0256464186b376d8e6a9c1a2', 160 } 161 let data = { 162 plugInId: plugInId[process.env.VUE_APP_ENV], 163 type: 'GENERAL_PLUGIN', 164 pageNo: 1, 165 city: '1', 166 trackDesc: '保险小课堂', 167 pageSize: 10, 168 } 169 let res = await getContentByPluginId(data) 170 if (res) { 171 this.list = res 172 } 173 }, 174 }, 175} 176</script> 177 178<style lang="scss" scoped> 179h6 { 180 font-family: 'PingFang SC'; 181 font-style: normal; 182 font-weight: 500; 183 font-size: 36rpx; 184 line-height: 44rpx; 185 color: #323233; 186} 187$gap: 10rpx; 188.pad-bottom-10 { 189 padding-bottom: 10rpx; 190} 191 192.insurance-lesson { 193 width: 100%; 194 display: flex; 195 flex-direction: column; 196 overflow-x: hidden; 197 &-row { 198 // margin: 0 21rpx; 199 padding: 20rpx; 200 border-bottom: 1rpx solid rgba(211, 221, 226, 0.3); 201 background: #ffffff; 202 border-radius: 12rpx; 203 margin-bottom: 12rpx; 204 &-ltrp { 205 display: flex; 206 justify-content: space-between; 207 .l-title { 208 text-overflow: ellipsis; 209 display: -webkit-box; 210 overflow: hidden; 211 -webkit-line-clamp: 2; 212 font-family: 'PingFang SC'; 213 font-style: normal; 214 font-weight: 500; 215 font-size: 32rpx; 216 line-height: 45rpx; 217 color: #323233; 218 margin-bottom: 14rpx; 219 height: 90rpx; 220 } 221 .r-img { 222 width: 200rpx; 223 height: 132rpx; 224 margin-left: 24rpx; 225 } 226 } 227 &-3p { 228 &-images { 229 display: flex; 230 justify-content: space-between; 231 align-items: center; 232 margin-top: $gap; 233 margin-bottom: $gap; 234 &-img { 235 width: 200rpx; 236 height: 132rpx; 237 object-fit: cover; 238 &:last-child { 239 margin-right: 0; 240 } 241 } 242 } 243 &-title { 244 display: flex; 245 flex-direction: column; 246 justify-content: space-between; 247 margin-bottom: 14rpx; 248 font-family: 'PingFang SC'; 249 font-style: normal; 250 font-weight: 500; 251 font-size: 32rpx; 252 line-height: 45rpx; 253 color: #323233; 254 } 255 } 256 } 257 &-title { 258 font-family: 'PingFang SC'; 259 font-style: normal; 260 font-weight: 500; 261 font-size: 32rpx; 262 line-height: 45rpx; 263 color: #323233; 264 flex: 1; 265 &-serve { 266 font-family: 'PingFang SC'; 267 font-style: normal; 268 font-weight: 400; 269 font-size: 26rpx; 270 line-height: 22rpx; 271 color: #323233; 272 margin-left: 6rpx; 273 } 274 &-time { 275 font-family: 'PingFang SC'; 276 font-style: normal; 277 font-weight: 400; 278 font-size: 25rpx; 279 line-height: 35rpx; 280 color: #afafaf; 281 } 282 } 283 &-img-100 { 284 width: 100%; 285 min-height: 100%; 286 margin-top: 16rpx; 287 object-fit: cover; 288 margin-bottom: $gap; 289 } 290} 291.demo-warter { 292 border-radius: 8px; 293 margin: 5px; 294 background-color: #ffffff; 295 padding: 8px; 296 position: relative; 297 .articleImg { 298 display: block; 299 width: 100%; 300 border-radius: 4px; 301 } 302} 303 304.u-close { 305 position: absolute; 306 top: 32rpx; 307 right: 32rpx; 308} 309 310.demo-image { 311 width: 100%; 312 border-radius: 4px; 313} 314 315.demo-title { 316 font-family: 'PingFang SC'; 317 font-style: normal; 318 font-weight: 500; 319 font-size: 32rpx; 320 line-height: 45rpx; 321 color: #323233; 322 margin-bottom: 14rpx; 323 margin-top: 10rpx; 324} 325.demo-imgBox { 326 position: relative; 327 .playImg { 328 position: absolute; 329 top: 50%; 330 left: 50%; 331 transform: translate(-50%, -50%); 332 display: block; 333 width: 50rpx; 334 height: 50rpx; 335 z-index: 1; 336 } 337} 338 339.demo-tag { 340 display: flex; 341 margin-top: 5px; 342} 343 344.demo-tag-owner { 345 background-color: red; 346 color: #ffffff; 347 display: flex; 348 align-items: center; 349 padding: 4rpx 14rpx; 350 border-radius: 50rpx; 351 font-size: 20rpx; 352 line-height: 1; 353} 354 355.demo-tag-text { 356 border: 1px solid #eee; 357 color: #000; 358 margin-left: 10px; 359 border-radius: 50rpx; 360 line-height: 1; 361 padding: 4rpx 14rpx; 362 display: flex; 363 align-items: center; 364 border-radius: 50rpx; 365 font-size: 20rpx; 366} 367 368.demo-price { 369 font-size: 30rpx; 370 color: red; 371 margin-top: 5px; 372} 373 374.demo-shop { 375 font-size: 22rpx; 376 color: $u-tips-color; 377 margin-top: 5px; 378} 379</style> 380
点赞
收藏

评论区

加载中...

相关推荐

vue 路由懒加载

原文链接: vue路由懒加载(https://my.oschina.net/ahaoboy/blog/1796979)路由懒加载当打包构建应用时,Javascript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。结合Vue的异步组件

RecyclerView更全解析之 - 基本使用和分割线解析

1.概述昨天跟自己群里的人唠嗑的时候发现还有人在用Eclipse,我相信可能还是有很多人在用ListView,这里介绍一个已经出来的n年了的控件RecyclerView,实现ListView,GridView,瀑布流的效果。还可以轻松的实现一些复杂的功能,如QQ的拖动排序,侧滑删除等等。相关文章:              

React的单向数据流与组件间的沟通

今天来给大家总结下React的单向数据流与组件间的沟通。首先,我认为使用React的最大好处在于:功能组件化,遵守前端可维护的原则。先介绍单向数据流吧。React单向数据流:React是单向数据流,数据主要从父节点传递到子节点(通过props)。如果顶层(父级)的某个props改变了,React会重渲染所有的子节点。刚才我们提到了

JS通过ajax + 多列布局 + 自动加载来实现瀑布流效果

Ajax说明:本文效果是无限加载的,意思就是你一直滚动就会一直加载图片出现,通过鼠标滚动距离来判断的,所以不是说的那种加载一次就停了的那种,那种demo下次我会再做一次css部分用的是html5css3的新属性,图片会自动添加到每行的最顶端上去,而不是用js去判断。去除了一些js计算的麻烦。css部分:

ACMer博客瀑布流分析

ACMer博客瀑布流是一个专门收集ACMer博客并展示的站点。地址http://blog.acmicpc.info/(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fblog.acmicpc.info%2F)打开网页之后直接查看源代码发现functionentry2htm

小程序实现瀑布流

框架:uniappuView瀑布流组件/waterfall瀑布流@description这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实