Code Monkey home page Code Monkey logo

v3-waterfall's Introduction

v3-waterfall 自适应瀑布流组件

一个 vue 3 的自适应瀑布流组件。

 

npm package downloads downloads

文档: 中文 | English

在线Demo

该 demo 即为本项目src/内容。

个人博客使用地址:这里

2.x 存在不兼容 1.x 更新,迁移方式参考文档最后说明。如需要查看 1.x 版本的文档,请查看docs/目录下的v1-README.md,原则上 1.x 版本不再进行更新。

 

1.支持功能

  • 一个针对 vue 3 的瀑布流组件
  • 支持无图模式及图片加载失败时默认图片
  • 图片预加载自动计算排版,不需要指定图片宽高(2.x 已支持多图模式、自定义提供元素高度
  • 响应式排版
  • 支持绑定滚动父元素
  • 支持虚拟列表
  • 支持头部插入元素(2.x 支持,满足类似下拉加载场景

 

2.使用方法

2.1 安装

pnpm add v3-waterfall

2.2 注册组件

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import V3waterfall from 'v3-waterfall'
import 'v3-waterfall/dist/style.css'

createApp(App).use(V3waterfall).mount('#app')

2.3 引入使用

<v3-waterfall
  :list="list"
  :colWidth="280"
  :virtual-time="400"
  :scrollBodySelector="isLimit ? '.limit-box' : ''"
  :isMounted="isMounted"
  :isLoading="loading"
  :isOver="over"
  class="waterfall"
  @scrollReachBottom="getNext"
>
  <template v-slot:default="slotProp">
    <div class="list-item">
      <a :href="'https://gkshi.com/blog/' + slotProp.item._id">
        <div class="cover-wrapper">
          <!-- 此处注意:data-key 是该图片的字段名称,目前只支持在一级的字段,不支持嵌套 -->
          <img v-if="slotProp.item.cover" :src="slotProp.item.cover" data-key="cover" class="cover" />
        </div>
        <div class="brief">
          <h3>{{ slotProp.item.title }}</h3>
          <p>{{ slotProp.item.outline }}</p>
        </div>
        <div class="cover-wrapper">
          <img :src="slotProp.item.notExistSrc" data-key="notExistSrc" class="cover" />
        </div>
      </a>
      <div class="outline-bottom">
        <p class="article-tags">
          <span>tags</span>
          <span v-for="tag of slotProp.item.tags" :key="tag" class="tag">{{
            tag
            }}</span>
        </p>
        <time>{{ slotProp.item.time }}</time>
      </div>
    </div>
  </template>
</v3-waterfall>

注意⚠️

1.尽管做了需要多次加载的校验,但首次加载的数据尽量达到可以出现滚动的条件

2.在对 list 进行增量时,请使用list.value = list.value.concat(addedList),因为组件内部会做增量加载,如果用push的方法添加,会造成多次触发,因此监听时取消了对push的响应

3.注意上面代码中的img标签使用,插槽中所有未在固定高度的容器中的图片(即会影响元素高度的图片),均需要加上data-key字段,要求见上面的注释。(如果你提供元素高度计算方法可忽略)

基本示例在src/App.vue中有具体代码,同时本项目pnpm dev运行起来的项目即能看到效果。

 

3.组件参数说明

参数 类型 默认值 是否必填 描述
list array [] 瀑布流列表数据
colWidth number | () => number 250 瀑布流卡片宽度,现在不同屏宽使用不同宽度需要自己写函数返回对应宽度
gap number | () => number 20 两列间的间距,单位:px
bottomGap number | () => number 10 上下卡片的间距,单位:px
isLoading boolean false 控制请求数据时显示加载状态提示
isOver boolean false 控制数据是否已经全部加载完成(即不需要再滚动加载)
active boolean true 类似van-tabs组件使用多个v3-waterfall时确认是否激活当前实例
swipeableDelay number 0 类似van-tabsswipeable属性开启后,可能在刚渲染时立即滑动到另一栏无法成功加载,需要设置此值,推荐为300,根据实际情况调整
dotsCount number 5 加载中显示点的数量
dotsColor string rgba(169, 169, 169, 0.8) 加载中显示点的颜色
overText string 呀,被看光了! 加载完的文字
overColor string #999999 加载完的文字的颜色
animation boolean true 是否开启内置动画
errorImgSrc string - 图片加载失败时展示的图片地址,有内置图片
distanceToScroll number 200 底部触发加载的距离,单位:px
scrollBodySelector string - 绑定滚动父元素选择器,默认为window对象,与isMounted参数配合使用
isMounted boolean false 父组件是否挂载完成,配合scrollBodySelector参数使用
virtualTime number 0 触发虚拟列表校验时间间隔,0 默认不开启虚拟列表
virtualLength number 500 默认移出视窗距离开启虚拟隐藏,单位: px
heightHook null | (slots, item, width, errorImgSrc) => Promise<number> - 自定义元素块高度函数钩子,支持promise,此为 props 字段
scrollReachBottom () => void - 触发加载更多时的函数
reRender () => void - - 通过 ref 可直接调用该组件方法进行重新渲染
insertBefore (insertList) => Promise<void> - - 通过 ref 可直接调用该组件方法往list首部插入元素列表

3.1 特殊字段说明

  • scrollBodySelectorisMounted

有时候我们的滚动不是相对于window对象,而是某个单独的父元素,这需要scrollBodySelectorisMounted字段配合。

<div class="father-box">
  <v3-waterfall scrollBodySelector=".father-box" :isMounted="isMounted"></v3-waterfall>
</div>

<script>
// 父组件
// ...
  setup () {
    const isMounted = ref(false)

    onMounted(() => {
      isMounted.value = true
    })

    return { isMounted }
  }
</script>

<style>
.father-box {
  height: 300px; /* 父元素一定要指定高度 */
  overflow-y: scroll; /* 一定要指定父元素超出滚动 */
}
</style>

由于子组件的mounted生命周期比父组件mounted先执行,所以需要通过父组件主动通知已挂载完成后,子组件才能往div.father-box元素上添加滚动监听等事件。  

  • virtualTimevirtualLength

提供数据量过大时的虚拟列表支持,如果数量不多,可以不开启。virtualTime是开启虚拟列表的关键,配置的是滚动事件发生后多久进行虚拟列表的计算,默认值为 0 ,此时不开启虚拟列表。如果需要使用,建议设置≥400的值。

virtualLength指的是当一个元素随着滚动消失在视窗外(可能消失在上方、下方)的距离需要被隐藏。

  • heightHook高度自定义钩子

组件内部支持自动计算元素块高度,但会对图片进行预加载后才进行计算,如果图片过大,会造成局部白屏时间太久。由于部分用户能够从接口获取每个元素中涉及的图片宽高数据,因此提供该钩子给用户自己计算高度,提升渲染性能。下面举个简单示例:

<div>
  <v3-waterfall ref="v3WaterfallRef" :list="list" :height-hook="heightHook"></v3-waterfall>
</div>

<script setup lang="ts" generic="T extends object">
import { render, ref, type Ref } from 'vue'

const list = ref([]) as Ref<T[]>
/*
此处场景设定为:每个卡片由一张图片+若干文字+其他标签框组成,且图片显示宽度为元素宽度,高度自适应

item 中的数据(从接口获取来的列表数据)包括:
{
  title: '标题',
  cover: 'http://xxx.xxx.com/xxx.png',
  cover_width: 800,
  cover_height: 500,
  tags: [‘tag1’, 'tag2']
}

*/

/**
 * 计算元素高度
 * @param {SlotsType} slots 内部 slots 组
 * @param {T} item 该元素块对应数据信息
 * @param {number} width 元素块宽度
 * @param {string} errorImgSrc 用户提供的错误图片
 * @returns {Promise<number>} 高度
 */
const heightHook = (slots, item, width, errorImgSrc) => {
  const div = document.createElement('div')
  div.style.position = 'absolute'
  div.style.left = '-1000px'
  div.style.width = width + 'px'
  div.style.visibility = 'hidden'

  // 使用 render 函数渲染出卡片 slot
  render(h(slots.default, { item }), div)

  // 将图片隐藏,图片的高度额外计算
  const img = div.querySelector('img')
  img.style.display = 'none'
  // 计算除图片外其他元素的高度
  const body = document.body || document.documentElement
  body.appendChild(div)
  const otherHeight = div.offsetHeight
  body.removeChild(div)

  // 单独计算图片实际展示高度
  const imgHeight = (width / item.cover_width) * item.cover_height
  // 返回该卡片的整体高度
  return imgHeight + otherHeight
}
</script>
  • reRenderinsertBefore

这两个方法暴露给组件ref直接调用,reRender方法适用于卡片内容发生变化需要重新渲染计算高度的场景,如卡片全部隐藏标题块;insertBefore方法适用需要在列表头部新增数据,例如下拉刷新加载,此方法不会重新计算已加载的卡片。示例如下:

<div>
  <v3-waterfall ref="v3WaterfallRef" :list="list"></v3-waterfall>
</div>

<script setup lang="ts" generic="T extends object">
import { ref, type Ref } from 'vue'
// 引入该类型支持方法调用类型提示
import type { V3WaterfallExpose } from 'v3-waterfall'

const list = ref([]) as Ref<T[]>
// 需要插入在最前面的元素组
const insertBeforeList = []

const v3WaterfallRef = ref<V3WaterfallExpose<T> | null>(null)

// 调用重渲染
v3WaterfallRef.value?.reRender()
// 调用头部插入,此方法会自动插入 list 当中,无须手动再次插入
const insertBefore = async () => {
  // 此处可以使用变量控制下拉 loading 状态的变化(请与组件的滚动底部 loading 区分)
  // pullLoading.value = true
  await v3WaterfallRef.value?.insertBefore(insertBeforeList)
  // pullLoading.value = false
}
insertBefore()
</script>

 

4.slot插槽

4.1 默认插槽(v-slot:default)

瀑布流卡片展示内容,自定义,展示什么,添加什么事件,可扩展。

4.2 加载插槽(v-slot:loading)

加载中在瀑布流底部显示的状态,默认为 5 个大小变化的点。

4.3 底部插槽(v-slot:footer)

数据全部加载完之后在底部显示的内容,默认为呀,被看光了!

 

5.迁移指南

从 1.x 迁移至 2.x 请参考docs/目录下的migration.md.

v3-waterfall's People

Contributors

gk-shi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

v3-waterfall's Issues

vue3 vite waterfall-item top 问题

"dependencies": {
"v3-waterfall": "^1.1.3",
"vue": "^3.2.45"
},

waterfall-item top属性很小 ,上下两行都叠加了
没有改动
<template> <V3waterfall class="waterfall" :list="state.list" srcKey="cover" :gap="12" :colWidth="280" :distanceToScroll="200" :isLoading="state.loading" :isOver="state.over" @scrollReachBottom="getNext"> <template v-slot:default="slotProp"> <div class="list-item"> <a :href="'https://gkshi.com/blog/' + slotProp.item.id"> <div class="cover-wrapper"> <img v-if="slotProp.item.url" :src="slotProp.item.url" class="cover"/> </div> <div class="brief"> <!-- slotProp.raw 可直接访问原始数据对象(响应式) --> <h3>{{ slotProp.item.name }}</h3> <p>{{ slotProp.item.name }}</p> </div> </a> </div> </template> </V3waterfall> </template>

组件如果使用v-if问题

如果waterfall的组件及其父组件被v-if标记,第一次从false到true时没有问题,但是当true到false时报错。
使用v-show没问题。
但是希望动态生成 而不是总是渲染 避免过大的开销

路由跳转的问题

路由跳转,waterfall就会抛错
TypeError: Cannot read properties of undefined (reading 'disconnect')

替换数据源或者删减数据源中的数据(`:list="data"`)后渲染不正确

Behavior:
image

Des:
如题,当以下code的watch中 filters更新后,会触发data重写this.data=[],data时:list"data"的数据源。然后有概率造成渲染错误。
已经尝试过在data更新后调用this.$refs.v3w.reRender(); 但是依然存在这个问题

Component:

<script setup>

</script>

<template>
<div class="container">
  <div class="waterfall-container">
    <v3-waterfall ref="v3w" class="waterfall" :list="data" srcKey="cover" :gap="10" :colWidth="itemWidth"
                  :distanceToScroll="200"
                  scrollBodySelector=".waterfall-container"
                  :isMounted="isMounted"
                  :isLoading="loading"
                  @scrollReachBottom="loadMore"
    >
      <template v-slot:default="slotProp">
        <div
            class="cell-item"
            @click="() => handleClick(slotProp.item.id)"
        >
          <img class="img" :src="slotProp.item.cover" alt="加载错误" />
        </div>
      </template>
    </v3-waterfall>
  </div>

</div>
</template>

<script>
export default {
  props: {
    filters: Object,
    isMounted: Boolean
  },
  data() {
    return {
      col: 3,
      data: [],
      offset: 0,
      size: 20,
      loading: false,
      isShow: true,
    };
  },
  methods: {
    loadMore() {
      this.offset += this.size;
      console.log("loadMore")
      this.loadImages();
    },
    handleClick(index) {
      console.log(index);
    },
    loadImages() {
      if (this.filters && !this.loading) {
        this.loading = true;
        let filters = [];
        for (let dim in this.filters) {
          filters.push({
            dimension: dim,
            minSimilarity: this.filters[dim][0],
            maxSimilarity: this.filters[dim][1]
          })
        }
        this.$http.post('inquiry_images_by_dimension', {
          filters: filters,
          offset: this.offset,
          size: this.size
        }).then(res => {
          res.data.forEach(item => {
            item.cover = this.getImageSrc(item);
          })
          this.data = this.data.concat(res.data);
          this.$refs.v3w.reRender();
          this.loading = false;
          this.isShow = true;
        })
      }
    },
    getImageSrc(item) {
      return this.$http.defaults.baseURL + `/image/${item.imagePath}/${item.imageName}`;
    }
  },
  computed: {
    itemWidth() {
      return (document.documentElement.clientWidth * 0.4 - 10 * (this.col - 1) - 10 - 10 * 2) / this.col;
    }
  },
  mounted() {
    if (this.loading) return;
    this.offset = 0;
    this.data = []
    console.log("mounted")
    this.loadImages();
  },
  watch: {
    filters: {
      handler() {
        if (this.loading) return;
        this.offset = 0;
        this.data = []
        console.log("watch")
        this.loadImages();
      },
      "deep": true
    }
  }
}
</script>

<style scoped>
.container {
  width: 40%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 20px;
  box-sizing: border-box;
  ::-webkit-scrollbar {
    width: 4px;
  }
}

.waterfall-container {
  width: 100%;
  height: 100%;
  padding: 10px;
  overflow-y: scroll; /* 一定要制定父元素超出滚动 */
}

.img {
  width: 100%;
  height: auto;
  display: block;
}

.cell-item {
  width: 100%;
  background: #ffffff;
  border: 2px solid #F0F0F0;
  border-radius: 12px 12px 12px 12px;
  overflow: hidden;
  box-sizing: border-box;
  margin-bottom: 10px;
}
</style>

在同一页面多次使用时样式错乱

需求:在同一页面使用tab标签展示不同类型数据,使用两个v3-waterfall组件展示不同数据时只有第一个组件渲染为正常使用,第二个tab组件渲染错乱如下图:

图片

第一个默认展示的tab效果正常,如图:

图片

vue3+ts 实现带搜索功能的瀑布流无法触发加载更多

vue3+ts 实现带搜索功能的瀑布流,初次进入不搜索可以正常滚动并加载更多,但是如果搜索加一个条件之后搜索出来不足一页的数据,然后清除条件加载出来的数据再滚动到底部无法触发加载更多,getNext方法不执行。代码就是参考的demo。

等高的图片底下出了一块空隙

复现地址:https://test-h5.aomiapp.com/uat/activity-v2/marketing/discover.html#/home
点击切换到“视频vlog”这个分类,可以复现。

image
据目前观察,有部分的浏览器上是正常的,部分能复现问题。
能复现问题的chrome版本:版本 124.0.6367.119(正式版本) (x86_64)

可以发现我的img src后面是加了一串?x-oss-process=style/HomePage-ProtocolView来让访问体积压缩,这个是阿里云oss的策略,有可能是被这个影响到了?

how to import in nuxt3?

I have wrote a v3-waterfall.client.ts like below:

import V3waterfall from 'v3-waterfall'
import 'v3-waterfall/dist/style.css'

export default defineNuxtPlugin((nuxtApp) => {
  // nuxtApp.vueApp.component('v3-waterfall', v3Waterfall)
  nuxtApp.vueApp.use(V3waterfall)
//   nuxtApp.vueApp.provide('v3-waterfall', V3waterfall)
})

but report error

[Vue warn]: Failed to resolve component: v3-waterfall
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

卡片就会添加到最左上角

  • 每次加载的数量不一样的话,卡片就会添加到最左上角。
  • 绑定滚动父元素上面时如果,进行数据切换(list数组全部替换了),给了key不会重新添加addEventListener('scroll', scrollFn)监听,不给key的话,滚动之后再切换list数据会添加两次数据。
我是给了key然后这样处理
watch(workList, (newV) => {
  if (newV.length === 0) {
    isMounted.value = false
    return
  }
  isMounted.value = true
})
  • 切换数据之后上滑刷新,卡片添加到了左上角不清楚是上面原因导致的
      state.workList.length = 0
      state.workList = []
      state.waterfallUpdate++
//切换之前清空了一遍数据
//workList 就是list的数据,我存在vuex里面

用的是vue3.2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.