Browse Source

feat: Waterfall

kailong321200875 1 year ago
parent
commit
d543e56efb

+ 106 - 39
src/components/Waterfall/src/Waterfall.vue

@@ -1,89 +1,156 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
-import { ref, nextTick, unref, onMounted } from 'vue'
-import { isString } from '@/utils/is'
+import { ref, nextTick, unref, onMounted, watch } from 'vue'
+import { useEventListener, useIntersectionObserver } from '@vueuse/core'
+import { debounce } from 'lodash-es'
 
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('waterfall')
 
+const emit = defineEmits(['loadMore'])
+
 const prop = defineProps({
   data: propTypes.arrayOf(propTypes.any),
-  reset: propTypes.bool.def(false),
+  reset: propTypes.bool.def(true),
   width: propTypes.number.def(200),
-  height: propTypes.number.def(0),
   gap: propTypes.number.def(20),
-  getContainer: propTypes.func.def(() => document.body),
   props: propTypes.objectOf(propTypes.string).def({
     src: 'src',
     height: 'height'
-  })
+  }),
+  loadingText: propTypes.string.def('加载中...'),
+  loading: propTypes.bool.def(false),
+  end: propTypes.bool.def(false),
+  endText: propTypes.string.def('没有更多了')
 })
 
 const wrapEl = ref<HTMLDivElement>()
 
 const heights = ref<number[]>([])
 
+const wrapHeight = ref(0)
+
+const wrapWidth = ref(0)
+
+const loadMore = ref<HTMLDivElement>()
+
 // 首先确定列数 = 页面宽度 / 图片宽度
 const cols = ref(0)
 
+const filterData = ref<any[]>([])
+
 const filterWaterfall = async () => {
-  const { props, width, gap, getContainer, height } = prop
+  const { props, width, gap } = prop
   const data = prop.data as any[]
   await nextTick()
 
-  const container = (getContainer?.() || unref(wrapEl)) as HTMLElement
+  const container = unref(wrapEl) as HTMLElement
   if (!container) return
   cols.value = Math.floor(container.clientWidth / (width + gap))
 
   const length = data.length
   for (let i = 0; i < length; i++) {
-    if (i + 1 < unref(cols)) {
-      if (height || data[i][props.height as string]) {
-        // 如果有全局高度,就使用全局高度
-        // 如果 data[i][props.height as string] 是字符串,只保留数字字符串
-        const itemHeight = isString(data[i][props.height as string])
-          ? Number(data[i][props.height as string].replace(/[^0-9]/gi, ''))
-          : data[i][props.height as string]
-        heights.value[i] = height || itemHeight
-      } else {
-        // 说明在第一列
-        const itemEl = container.querySelector(`.${prefixCls}-item__${i}`)
-        itemEl?.addEventListener('load', () => {
-          const clientRect = itemEl?.getBoundingClientRect()
-          console.log(clientRect)
-        })
-        // const imgEl = new Image()
-        // imgEl.src = data[i][props.src as string]
-        // imgEl.onload = async () => {
-        //   // const itemEl = container.querySelector(`.${prefixCls}-item__${i}`)
-        //   const clientRect = itemEl?.getBoundingClientRect()
-        //   if (clientRect) {
-        //     heights.value[i] = clientRect?.height
-        //   }
-        // }
+    if (i < unref(cols)) {
+      heights.value[i] = data[i][props.height as string]
+      filterData.value.push({
+        ...data[i],
+        top: 0,
+        left: i * (width + gap)
+      })
+    } else {
+      // 其他行,先找出最矮的那一列 和 索引
+      // 假设最小高度是第一个元素
+      let minHeight = heights.value[0]
+      let index = 0
+      // 找出最小高度
+      for (let j = 1; j < cols.value; j++) {
+        if (unref(heights)[j] < minHeight) {
+          minHeight = unref(heights)[j]
+          index = j
+        }
       }
+
+      // 更新最矮高度
+      heights.value[index] += data[i][props.height as string] + gap
+      filterData.value.push({
+        ...data[i],
+        top: minHeight + gap,
+        left: index * (width + gap)
+      })
     }
   }
+  wrapHeight.value = Math.max(...unref(heights))
+  wrapWidth.value = unref(cols) * (width + gap) - gap
 }
 
+watch(
+  () => prop.data,
+  async () => {
+    await nextTick()
+    filterWaterfall()
+  },
+  {
+    immediate: true
+  }
+)
+
 onMounted(() => {
-  filterWaterfall()
+  if (unref(prop.reset)) {
+    useEventListener(window, 'resize', debounce(filterWaterfall, 300))
+  }
+  useIntersectionObserver(
+    unref(loadMore),
+    ([{ isIntersecting }]) => {
+      if (isIntersecting && !prop.loading && !prop.end) {
+        emit('loadMore')
+      }
+    },
+    {
+      threshold: 0.1
+    }
+  )
 })
 </script>
 
 <template>
-  <div :class="prefixCls" ref="wrapEl">
+  <div
+    :class="[prefixCls, 'flex', 'justify-center', 'items-center']"
+    ref="wrapEl"
+    :style="{
+      height: `${wrapHeight + 40}px`
+    }"
+  >
     <div
-      v-for="(item, $index) in data"
-      :class="`${prefixCls}-item__${$index}`"
-      :key="`water-${$index}`"
+      class="relative"
       :style="{
-        width: `${width}px`
+        width: `${wrapWidth}px`,
+        height: `${wrapHeight + 40}px`
       }"
     >
-      <img :src="item[props.src as string]" class="w-full block" alt="" srcset="" />
+      <div
+        v-for="(item, $index) in filterData"
+        :class="[`${prefixCls}-item__${$index}`, 'absolute']"
+        :key="`water-${$index}`"
+        :style="{
+          width: `${width}px`,
+          height: `${item[props.height as string]}px`,
+          top: `${item.top}px`,
+          left: `${item.left}px`
+        }"
+      >
+        <img :src="item[props.src as string]" class="w-full h-full block" alt="" srcset="" />
+      </div>
+      <div
+        ref="loadMore"
+        class="h-40px flex justify-center absolute w-full"
+        :style="{
+          top: `${wrapHeight + gap}px`
+        }"
+      >
+        {{ end ? endText : loadingText }}
+      </div>
     </div>
   </div>
 </template>

+ 1 - 1
src/config/axios/service.ts

@@ -32,7 +32,7 @@ axiosInstance.interceptors.response.use(
     return res
   },
   (error: AxiosError) => {
-    console.log('err' + error) // for debug
+    console.log('err' + error) // for debug
     ElMessage.error(error.message)
     return Promise.reject(error)
   }

+ 24 - 2
src/views/Components/Waterfall.vue

@@ -11,29 +11,51 @@ const data = ref<any>([])
 const getList = () => {
   const list: any = []
   for (let i = 0; i < 20; i++) {
+    // 随机 100, 500 之间的整数
+    const height = Mock.Random.integer(100, 500)
+    const width = Mock.Random.integer(100, 500)
     list.push(
       Mock.mock({
+        width,
+        height,
         id: toAnyString(),
-        image_uri: Mock.Random.image('@integer(100, 500)x@integer(100, 500)')
+        image_uri: Mock.Random.image(`${width}x${height}`)
       })
     )
   }
   data.value = [...unref(data), ...list]
-  console.log('【data】:', data.value)
+  if (unref(data).length >= 60) {
+    end.value = true
+  }
 }
 getList()
 
 const { t } = useI18n()
+
+const loading = ref(false)
+
+const end = ref(false)
+
+const loadMore = () => {
+  loading.value = true
+  setTimeout(() => {
+    getList()
+    loading.value = false
+  }, 1000)
+}
 </script>
 
 <template>
   <ContentWrap :title="t('router.waterfall')">
     <Waterfall
       :data="data"
+      :loading="loading"
+      :end="end"
       :props="{
         src: 'image_uri',
         height: 'height'
       }"
+      @load-more="loadMore"
     />
   </ContentWrap>
 </template>