小程序实现瀑布流

LinMeng
• 阅读 277

框架:uni-app+uView

瀑布流组件

<template>
    <view class="u-waterfall">
        <view id="u-left-column" class="u-column"><slot name="left" :leftList="leftList"></slot></view>
        <view id="u-right-column" class="u-column"><slot name="right" :rightList="rightList"></slot></view>
    </view>
</template>

<script>
/**
 * waterfall 瀑布流
 * @description 这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实现了真正的 组件化,搭配LazyLoad 懒加载和loadMore 加载更多组件,让您开箱即用,眼前一亮。
 * @tutorial https://www.uviewui.com/components/waterfall.html
 * @property {Array} flow-list 用于渲染的数据
 * @property {String Number} add-time 单条数据添加到队列的时间间隔,单位ms,见上方注意事项说明(默认200)
 * @example <u-waterfall :flowList="flowList"></u-waterfall>
 */
export default {
    name: "u-waterfall",
    props: {
        value: {
            // 瀑布流数据
            type: Array,
            required: true,
            default: function() {
                return [];
            }
        },
        // 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好
        // 单位ms
        addTime: {
            type: [Number, String],
            default: 200
        },
        // id值,用于清除某一条数据时,根据此idKey名称找到并移除,如数据为{idx: 22, name: 'lisa'}
        // 那么该把idKey设置为idx
        idKey: {
            type: String,
            default: 'id'
        }
    },
    data() {
        return {
            leftList: [],
            rightList: [],
            tempList: [],
            children: []
        }
    },
    watch: {
        copyFlowList(nVal, oVal) {
            // 取差值,即这一次数组变化新增的部分
            let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
            // 拼接上原有数据
            this.tempList = this.tempList.concat(this.cloneData(nVal.slice(startIndex)));
            this.splitData();
        }
    },
    mounted() {
        this.tempList = this.cloneData(this.copyFlowList);
        this.splitData();
    },
    computed: {
        // 破坏flowList变量的引用,否则watch的结果新旧值是一样的
        copyFlowList() {
            return this.cloneData(this.value);
        }
    },
    methods: {
        async splitData() {
            if (!this.tempList.length) return;
            let leftRect = await this.$uGetRect('#u-left-column');
            let rightRect = await this.$uGetRect('#u-right-column');
            // 如果左边小于或等于右边,就添加到左边,否则添加到右边
            let item = this.tempList[0];
            // 解决多次快速上拉后,可能数据会乱的问题,因为经过上面的两个await节点查询阻塞一定时间,加上后面的定时器干扰
            // 数组可能变成[],导致此item值可能为undefined
            if(!item) return ;
            if (leftRect.height < rightRect.height) {
                this.leftList.push(item);
            } else if (leftRect.height > rightRect.height) {
                this.rightList.push(item);
            } else {
                // 这里是为了保证第一和第二张添加时,左右都能有内容
                // 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边
                if (this.leftList.length <= this.rightList.length) {
                    this.leftList.push(item);
                } else {
                    this.rightList.push(item);
                }
            }
            // 移除临时列表的第一项
            this.tempList.splice(0, 1);
            // 如果临时数组还有数据,继续循环
            if (this.tempList.length) {
                setTimeout(() => {
                    this.splitData();
                }, this.addTime)
            }
        },
        // 复制而不是引用对象和数组
        cloneData(data) {
            return JSON.parse(JSON.stringify(data));
        },
        // 清空数据列表
        clear() {
            this.leftList = [];
            this.rightList = [];
            // 同时清除父组件列表中的数据
            this.$emit('input', []);
            this.tempList = [];
        },
        // 清除某一条指定的数据,根据id实现
        remove(id) {
            // 如果findIndex找不到合适的条件,就会返回-1
            let index = -1;
            index = this.leftList.findIndex(val => val[this.idKey] == id);
            if(index != -1) {
                // 如果index不等于-1,说明已经找到了要找的id,根据index索引删除这一条数据
                this.leftList.splice(index, 1);
            } else {
                // 同理于上方面的方法
                index = this.rightList.findIndex(val => val[this.idKey] == id);
                if(index != -1) this.rightList.splice(index, 1);
            }
            // 同时清除父组件的数据中的对应id的条目
            index = this.value.findIndex(val => val[this.idKey] == id);
            if(index != -1) this.$emit('input', this.value.splice(index, 1));
        },
        // 修改某条数据的某个属性
        modify(id, key, value) {
            // 如果findIndex找不到合适的条件,就会返回-1
            let index = -1;
            index = this.leftList.findIndex(val => val[this.idKey] == id);
            if(index != -1) {
                // 如果index不等于-1,说明已经找到了要找的id,修改对应key的值
                this.leftList[index][key] = value;
            } else {
                // 同理于上方面的方法
                index = this.rightList.findIndex(val => val[this.idKey] == id);
                if(index != -1) this.rightList[index][key] = value;
            }
            // 修改父组件的数据中的对应id的条目
            index = this.value.findIndex(val => val[this.idKey] == id);
            if(index != -1) {
                // 首先复制一份value的数据
                let data = this.cloneData(this.value);
                // 修改对应索引的key属性的值为value
                data[index][key] = value;
                // 修改父组件通过v-model绑定的变量的值
                this.$emit('input', data);
            }
        }
    }
}
</script>

<style lang="scss" scoped>
@import "./style.components.scss";

.u-waterfall {
    @include vue-flex;
    flex-direction: row;
    align-items: flex-start;
}

.u-column {
    @include vue-flex;
    // flex: 1;
    flex-direction: column;
    height: auto;
    width: 50%;
}

.u-image {
    width: 100%;
}
</style>

style.components.scss

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

使用

<template>
    <view v-if="newslist.length > 0" class="insurance-lesson">
        <view class="flex justify-between u-section">
            <u--image
                :showLoading="true"
                mode="widthFix"
                :src="TitleUrl"
                width="170rpx"
                height="44rpx"
            ></u--image>
        </view>
        <view class="wrap">
            <waterfall v-model="newslist" ref="uWaterfall">
                <template v-slot:left="{ leftList }">
                    <view
                        class="demo-warter"
                        v-for="(item, index) in leftList"
                        :key="index"
                        @click="onRowClick(item, index)"
                    >
                        <view class="demo-imgBox">
                            <image
                                class="articleImg"
                                mode="widthFix"
                                v-if="item.imageList && item.imageList.length > 0"
                                :src="item.imageList[0].fileUrl"
                            />
                            <image
                                mode="widthFix"
                                class="playImg"
                                v-if="item.type === '1004'"
                                src="https://。。。。。/20231027/info_20231027104126.png"
                            />
                        </view>
                        <view class="demo-title">
                            {{ item.title }}
                        </view>
                        <view class="flex justify-between">
                            <view class="flex items-center">
                                <u-avatar
                                    shape="circle"
                                    size="34"
                                    src="https://。。。。。。/20230124/info_20230124103610.png"
                                ></u-avatar>
                                <text class="insurance-lesson-title-serve">程序名</text>
                            </view>
                            <text class="insurance-lesson-title-time">
                                {{ item.officialAcct.desc }}
                                <!-- {{ item.officialAcct.desc | formateYear }} -->
                            </text>
                        </view>
                    </view>
                </template>
                <template v-slot:right="{ rightList }">
                    <view
                        class="demo-warter"
                        v-for="(item, index) in rightList"
                        :key="index"
                        @click="onRowClick(item, index)"
                    >
                        <view class="demo-imgBox">
                            <image
                                class="articleImg"
                                v-if="item.imageList && item.imageList.length > 0"
                                :src="item.imageList[0].fileUrl"
                                mode="widthFix"
                            />
                            <image
                                mode="widthFix"
                                class="playImg"
                                v-if="item.type === '1004'"
                                src="https://。。。。。。/info_20231027104126.png"
                            />
                        </view>
                        <view class="demo-title">
                            {{ item.title }}
                        </view>
                        <view class="flex justify-between">
                            <view class="flex items-center">
                                <u-avatar
                                    shape="circle"
                                    size="34"
                                    src="https:。。。。。/info_20230124103610.png"
                                ></u-avatar>
                                <text class="insurance-lesson-title-serve">小程序名</text>
                            </view>
                            <text class="insurance-lesson-title-time">
                                {{ item.officialAcct.desc }}
                                <!-- {{ item.officialAcct.desc | formateYear }} -->
                            </text>
                        </view>
                    </view>
                </template>
            </waterfall>
        </view>
    </view>
</template>

<script>
import { getContentByPluginId } from '@/api/common'
import waterfall from '@/components/waterfall/waterfall.vue'
const base64 = require('../../util/base64')
const JXB_ICON = `https://。。。。。24/info_20230124103610.png`
const TitleUrl =
    'https://e。。。。。/20231123/info_20231123160117.png'

export default {
    options: { styleIsolation: 'shared' },
    name: 'new-list',
    props: {
        tab: {
            type: String,
        },
        shouldShowTop: Boolean,
    },
    components: {
        waterfall,
    },
    filters: {
        formateYear(val) {
            return val.replace('年', '月')
        },
    },
    data() {
        return {
            list: [],
            TitleUrl,
            XCX_ICON,
        }
    },
    computed: {
        newslist() {
            return this.list.map((item) => {
                return {
                    type: item.type,
                    ...item.cell[0][0],
                }
            })
        },
    },
    mounted() {
        this.$nextTick(() => {
            this.getNewsListUrl()
        })
    },
    methods: {
        onRowClick(item, index) {
            if (item.router) {
                if (item.router.includes('//h5?')) {
                    const strs = item.router.split('&')
                    const Request = {}
                    if (strs && strs.length > 0) {
                        strs.map((i, key) => {
                            console.log(i)
                            Request['url'] = strs[key].split('=')[1]
                            return Request
                        })
                    }
                    if (Request.url !== undefined) {
                        let url = base64.decode(Request.url)
                        let route = {
                            name: 'articleDetail.index',
                            params: {
                                articleUrl: encodeURIComponent(url),
                            },
                        }
                        this.$Router.push(route)
                    }
                }
            }
        },
        async getNewsListUrl(pageNo) {
            const plugInId = {
                SIT: '11111111111111',
                PRO: '222222',
            }
            let data = {
                plugInId: plugInId[process.env.VUE_APP_ENV],
                type: 'GENERAL_PLUGIN',
                pageNo: 1,
                city: '1',
                trackDesc: '保险小课堂',
                pageSize: 10,
            }
            let res = await getContentByPluginId(data)
            if (res) {
                this.list = res
            }
        },
    },
}
</script>

<style lang="scss" scoped>
h6 {
    font-family: 'PingFang SC';
    font-style: normal;
    font-weight: 500;
    font-size: 36rpx;
    line-height: 44rpx;
    color: #323233;
}
$gap: 10rpx;
.pad-bottom-10 {
    padding-bottom: 10rpx;
}

.insurance-lesson {
    width: 100%;
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    &-row {
        // margin: 0 21rpx;
        padding: 20rpx;
        border-bottom: 1rpx solid rgba(211, 221, 226, 0.3);
        background: #ffffff;
        border-radius: 12rpx;
        margin-bottom: 12rpx;
        &-ltrp {
            display: flex;
            justify-content: space-between;
            .l-title {
                text-overflow: ellipsis;
                display: -webkit-box;
                overflow: hidden;
                -webkit-line-clamp: 2;
                font-family: 'PingFang SC';
                font-style: normal;
                font-weight: 500;
                font-size: 32rpx;
                line-height: 45rpx;
                color: #323233;
                margin-bottom: 14rpx;
                height: 90rpx;
            }
            .r-img {
                width: 200rpx;
                height: 132rpx;
                margin-left: 24rpx;
            }
        }
        &-3p {
            &-images {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-top: $gap;
                margin-bottom: $gap;
                &-img {
                    width: 200rpx;
                    height: 132rpx;
                    object-fit: cover;
                    &:last-child {
                        margin-right: 0;
                    }
                }
            }
            &-title {
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                margin-bottom: 14rpx;
                font-family: 'PingFang SC';
                font-style: normal;
                font-weight: 500;
                font-size: 32rpx;
                line-height: 45rpx;
                color: #323233;
            }
        }
    }
    &-title {
        font-family: 'PingFang SC';
        font-style: normal;
        font-weight: 500;
        font-size: 32rpx;
        line-height: 45rpx;
        color: #323233;
        flex: 1;
        &-serve {
            font-family: 'PingFang SC';
            font-style: normal;
            font-weight: 400;
            font-size: 26rpx;
            line-height: 22rpx;
            color: #323233;
            margin-left: 6rpx;
        }
        &-time {
            font-family: 'PingFang SC';
            font-style: normal;
            font-weight: 400;
            font-size: 25rpx;
            line-height: 35rpx;
            color: #afafaf;
        }
    }
    &-img-100 {
        width: 100%;
        min-height: 100%;
        margin-top: 16rpx;
        object-fit: cover;
        margin-bottom: $gap;
    }
}
.demo-warter {
    border-radius: 8px;
    margin: 5px;
    background-color: #ffffff;
    padding: 8px;
    position: relative;
    .articleImg {
        display: block;
        width: 100%;
        border-radius: 4px;
    }
}

.u-close {
    position: absolute;
    top: 32rpx;
    right: 32rpx;
}

.demo-image {
    width: 100%;
    border-radius: 4px;
}

.demo-title {
    font-family: 'PingFang SC';
    font-style: normal;
    font-weight: 500;
    font-size: 32rpx;
    line-height: 45rpx;
    color: #323233;
    margin-bottom: 14rpx;
    margin-top: 10rpx;
}
.demo-imgBox {
    position: relative;
    .playImg {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        display: block;
        width: 50rpx;
        height: 50rpx;
        z-index: 1;
    }
}

.demo-tag {
    display: flex;
    margin-top: 5px;
}

.demo-tag-owner {
    background-color: red;
    color: #ffffff;
    display: flex;
    align-items: center;
    padding: 4rpx 14rpx;
    border-radius: 50rpx;
    font-size: 20rpx;
    line-height: 1;
}

.demo-tag-text {
    border: 1px solid #eee;
    color: #000;
    margin-left: 10px;
    border-radius: 50rpx;
    line-height: 1;
    padding: 4rpx 14rpx;
    display: flex;
    align-items: center;
    border-radius: 50rpx;
    font-size: 20rpx;
}

.demo-price {
    font-size: 30rpx;
    color: red;
    margin-top: 5px;
}

.demo-shop {
    font-size: 22rpx;
    color: $u-tips-color;
    margin-top: 5px;
}
</style>
点赞
收藏
评论区
推荐文章
【敏捷研发系列】前端DevOps流水线实践
软件开发从传统的瀑布流方式到敏捷开发,将软件交付过程中开发和测试形成快速的迭代交付,但在软件交付客户之前或者使用过程中,还包括集成、部署、运维等环节需要进一步优化交付效率。因此Devops的产生将敏捷的相关理念扩展到运维侧,从而将产品、设计、开发、测试、运维团队更紧密的结合在一起。而从交付给客户产品视角看,前端研发通常又是在整个产品设计开发链条的最终节点,意味着前端团队受到上游变更的影响是最大的,并且从经营理念效率出发,提升前端交付效率是至关重要的。
红橙Darren 红橙Darren
3年前
RecyclerView更全解析之 - 基本使用和分割线解析
1.概述昨天跟自己群里的人唠嗑的时候发现还有人在用Eclipse,我相信可能还是有很多人在用ListView,这里介绍一个已经出来的n年了的控件RecyclerView,实现ListView,GridView,瀑布流的效果。还可以轻松的实现一些复杂的功能,如QQ的拖动排序,侧滑删除等等。相关文章:              
Wesley13 Wesley13
3年前
IO(输入输出)
IO流有很多种,按照操作数据的不同,可以分为字节流和字符流,按照数组传输方向的不同又可分为输入流和输出流。字节流的输入输出流分别用java.io.InputStream和java.io.OutputStream表示,字符流的输入输出流分别用java.io.Reader和java.io.Writer表示。!(https://oscimg
Stella981 Stella981
3年前
React的单向数据流与组件间的沟通
今天来给大家总结下React的单向数据流与组件间的沟通。首先,我认为使用React的最大好处在于:功能组件化,遵守前端可维护的原则。先介绍单向数据流吧。React单向数据流:React是单向数据流,数据主要从父节点传递到子节点(通过props)。如果顶层(父级)的某个props改变了,React会重渲染所有的子节点。刚才我们提到了
Wesley13 Wesley13
3年前
CSS瀑布流布局
瀑布流布局是什么瀑布流布局是一种常见的网页布局方式,视觉上给人一种参差不齐的多栏的效果,常用于图片为主的版块,如下图。!(https://timgsa.baidu.com/timg?image&quality80&sizeb9999_10000&sec1589721778046&dib34a014e7481f1a5685
Stella981 Stella981
3年前
JS通过ajax + 多列布局 + 自动加载来实现瀑布流效果
Ajax说明:本文效果是无限加载的,意思就是你一直滚动就会一直加载图片出现,通过鼠标滚动距离来判断的,所以不是说的那种加载一次就停了的那种,那种demo下次我会再做一次css部分用的是html5css3的新属性,图片会自动添加到每行的最顶端上去,而不是用js去判断。去除了一些js计算的麻烦。css部分:
Stella981 Stella981
3年前
ACMer博客瀑布流分析
ACMer博客瀑布流是一个专门收集ACMer博客并展示的站点。地址http://blog.acmicpc.info/(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fblog.acmicpc.info%2F)打开网页之后直接查看源代码发现functionentry2htm
Easter79 Easter79
3年前
Tableau必知必会之如何将甘特图做成瀑布图
我在之前的文章中写过甘特图的制作过程,但是,如果你希望图表既能反映数据的多少,又能直观的反映出数据的增减变化。那么,你就需要在此基础上,通过巧妙的设置,使图表中数据点的排列形状看似瀑布。这种排列似瀑布的甘特图,人们形象的称之为“瀑布图”。通常,瀑布图被常用于元数据有分类的情况下,来反应各部分之间的差异。!(https://im
LinMeng LinMeng
1年前
uview瀑布流
组件/waterfall瀑布流@description这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实现了真正的组件化,搭配LazyLoa
京东云开发者 京东云开发者
1个月前
鸿蒙跨端实践-长列表解决方案和性能优化
作者:京东科技徐超这是我参加创作者计划的第一篇文章。前言长列表是前端和客户端应用中最常见的业务场景,比如商品瀑布流等,有成千上万条数据,因此长列表的渲染性能在iOS,Android,Harmony,Web等各大平台都非常重要。HarmonyOS和iOS类似
LinMeng
LinMeng
Lv1
争取早日实现“代码自由” wa !!!
文章
50
粉丝
7
获赞
33