首页 > Vue >

vue3+ts实现瀑布流布局组件

时间: 作者:admin 浏览:

项目要实现瀑布流布局,本想用css实现的,无奈css根本无法满足需求,无论是flex布局还是grid布局,还有最终的column-count,都满足不了,瀑布流的特点是同宽不同高,项目要求最新的数据在前面,每列颜色相同,列数可变化,还有分页加载数据,折腾了一番css后,决定还是选择js来实现,先来看看效果图:

写成了vue3的composable组合式函数,代码如下:

小编已经将组件发布到npmjs库,也可以直接安装使用:

npm i usewaterfalllayout --save-dev
/**
 * @method useWaterFallLayout - 瀑布流布局组件
 * @param {Ref<HTMLElement | null>} containerRef -容器元素的 Ref
 * @param { string } itemSelector -要处理的子元素 选择器
 * @returns {Object} -初始化属性与方法
 * {
    init,               function - 初始化瀑布流布局
    column,             number - 瀑布流列数
    rowSpacing,         number - 瀑布流行间距
    columnSpacing,      number - 瀑布流列间距
    oddColumnBgColor,   string - 奇数列背景色
    evenColumnBgColor,  string - 偶数列背景色
    getItems            function - 获取瀑布流布局子元素
 * }
 *
 * @example:
 <template>
    <div
        ref="containerRef"
        class="w-100%"
    >
        <div
            class="item-selector"
            v-for="(item, index) in data"
            :key="item.id"
        >{{index}}</div>
    </div>
 </template>
 <script lang="ts" setup>
    const containerRef = ref<HTMLElement | null>(null)
    const { init, column, rowSpacing } = useWaterFallLayout(containerRef, '.item-selector')

    //指定列数
    column.value = 3

    //指定行距
    rowSpacing.value = 2

    //数据加载
    const data = ref([])
    const loading = ref(false)

    const getList = ()=>{
        loading.value = true
        getData().then((res)=>{
            const list = res?.list || []
            data.value.push(...list)
            init()
            loading.value = false
        }).catch((err)=>{
            loading.value = false
        })
    }

 </script>
 */
import type { Ref } from 'vue'

export function useWaterFallLayout(containerRef: Ref<HTMLElement | null>, itemSelector: string) {
    //初始化默认值
    const column = ref<number>(3)
    const rowSpacing = ref<number>(2)
    const columnSpacing = ref<number>(2)
    const oddColumnBgColor = ref<string>('#F5F8F8')
    const evenColumnBgColor = ref<string>('#FFFFFF')

    const transition = ref<string>('top 300ms ease-in-out, left 300ms ease-in-out')
    const containerWidth = ref<number>(0)
    const containerHeight = ref<number>(0)
    const columnWidth = ref<number>(0)

    const getItems = (selector?: string) =>
        document.querySelectorAll<HTMLElement>(selector || itemSelector)

    const setContainerPosition = () => {
        containerRef.value!.style.position = 'relative'
    }

    const useContainerWidth = () => {
        setContainerPosition()
        //这里暂时不考虑容器的边距边框等因素
        containerWidth.value = containerRef.value!.clientWidth
    }

    const useColumnWidth = () => {
        columnWidth.value =
            (containerWidth.value - columnSpacing.value * (column.value - 1)) / column.value
    }

    const getItemLeft = (index: number) => {
        const remain = index % column.value
        return remain === 0
            ? 0
            : remain * (columnWidth.value + columnSpacing.value) 
    }

    //获取同列顶头的位置高度
    const getNextTop = (item: HTMLElement) => {
        const { style, offsetHeight } = item
        return parseFloat(style.top) + offsetHeight + rowSpacing.value
    }

    const getItemTop = (index: number, items: NodeListOf<HTMLElement>) => {
        if (index >= column.value) {
            return getNextTop(items[index - column.value])
        }
        return 0
    }

    const increaseContainerHeight = (item: HTMLElement) => {
        const newTop = getNextTop(item)
        if (newTop > containerHeight.value) {
            containerHeight.value = newTop
            containerRef.value!.style.height = newTop + 'px'
        }
    }

    const isDealed = ref('is-dealed')

    const useItemAttrs = (isRearrange: boolean = false) => {
        const items = getItems()
        const itemsDealed = getItems('.' + isDealed.value)

        //大数据量时,避免重复处理
        let startIndex: number = 0
        if (itemsDealed.length > 0) {
            startIndex = itemsDealed.length
        }

        if (isRearrange) {
            startIndex = 0
            containerHeight.value = 0
        }

        for (let i = startIndex; i < items.length; i++) {
            items[i].style.position = 'absolute'
            items[i].style.transition = transition.value
            items[i].style.width = columnWidth.value + 'px'
            items[i].style.backgroundColor = getItemBgColor(i)

            items[i].style.left = getItemLeft(i) + 'px'
            items[i].style.top = getItemTop(i, items) + 'px'

            items[i].classList.add(isDealed.value)

            increaseContainerHeight(items[i])
        }
    }

    const getItemBgColor = (i: number) => {
        const index = i + 1
        if (column.value % 2 === 0) {
            if (index % 2 === 0) {
                return evenColumnBgColor.value
            } else {
                return oddColumnBgColor.value
            }
        } else {
            if (index % column.value != 0 && (index % column.value) % 2 === 0) {
                return evenColumnBgColor.value
            } else {
                return oddColumnBgColor.value
            }
        }
    }

    const init = (isRearrange: boolean = false) => {
        useContainerWidth()
        useColumnWidth()
        nextTick(() => {
            useItemAttrs(isRearrange)
        })
    }

    watch(
        () => column.value,
        (val) => {
            nextTick(() => {
                init(true)
            })
        }
    )

    return {
        init,
        column,
        rowSpacing,
        columnSpacing,
        oddColumnBgColor,
        evenColumnBgColor,
        getItems
    }
}
微信公众号
微信公众号:
  • 前端全栈之路(微信群)
前端QQ交流群
前端QQ交流群:
  • 794324979
  • 734802480(已满)

更多文章

栏目文章


Copyright © 2014-2023 seozhijia.net 版权所有-粤ICP备13087626号-4