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