gaoluyang
16 小时以前 cb9cd49627b65a4c0e137e08063271a8cefe1826
src/views/im/home/components/paged-scroller.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<script lang="ts" setup generic="T">
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
defineOptions({ name: 'ImPagedScroller' })
const props = withDefaults(
  defineProps<{
    itemKey?: string // ä¸šåŠ¡ id å­—段名(如 'userId' / 'id');不传 / å­—段值非 string|number æ—¶å›žé€€ idx
    items: T[] // å…¨é‡æ•°æ®
    pageSize?: number // æ¯é¡µæ¸²æŸ“条数
    threshold?: number // è·åº•多少 px è§¦å‘下一页
  }>(),
  {
    itemKey: undefined,
    pageSize: 30,
    threshold: 30
  }
)
/** è§£æžæ¯æ¡ item çš„ :key:caller ä¼  itemKey åˆ™æŒ‰å­—段取,无效 / ç¼ºå¤±å›žé€€ç´¢å¼•,避免传错字段时全表 undefined key */
function resolveItemKey(item: T, idx: number): number | string {
  if (!props.itemKey || item == null || typeof item !== 'object') {
    return idx
  }
  const value = (item as Record<string, unknown>)[props.itemKey]
  return typeof value === 'string' || typeof value === 'number' ? value : idx
}
const scrollbarRef = useTemplateRef<HTMLDivElement>('scrollbarRef')
const page = ref(1)
const displayItems = computed(() => {
  const limit = Math.min(page.value * props.pageSize, props.items.length)
  return props.items.slice(0, limit)
})
const allLoaded = computed(() => displayItems.value.length >= props.items.length)
/** ä»…当超过一页时才显示「已到底部」,避免短列表也出现这条提示 */
const showFooter = computed(() => allLoaded.value && props.items.length > props.pageSize)
let wrapEl: HTMLElement | null = null
onMounted(() => {
  wrapEl = scrollbarRef.value
  wrapEl?.addEventListener('scroll', onScroll)
})
onBeforeUnmount(() => {
  wrapEl?.removeEventListener('scroll', onScroll)
})
/** åˆ‡æ¢æ•°æ®æºï¼ˆå¦‚切会话)时重置分页:避免新列表沿用旧 page,首屏出现空段 */
watch(
  () => props.items,
  () => {
    page.value = 1
  }
)
/** æ»šåˆ°è·åº• threshold å†…时自增 page,扩出下一段切片 */
function onScroll(e: Event) {
  const el = e.target as HTMLElement
  if (el.scrollTop + el.clientHeight < el.scrollHeight - props.threshold) {
    return
  }
  if (allLoaded.value) {
    return
  }
  page.value++
}
defineExpose({
  /** æ‰‹åŠ¨æ»šåˆ°é¡¶éƒ¨ */
  scrollTop: () => {
    if (wrapEl) {
      wrapEl.scrollTop = 0
    }
  },
  /** æ‰‹åŠ¨æ»šåˆ°åº•éƒ¨ */
  scrollBottom: () => {
    if (wrapEl) {
      wrapEl.scrollTop = wrapEl.scrollHeight
    }
  }
})
</script>
<template>
  <!--
    åˆ†é¡µå¢žé‡æ»šåЍ容噍
    - æ»šåˆ°åº•部自动 page++,直到全部渲染完
    - é€šè¿‡ slot æš´éœ²æ¯ä¸€é¡¹ï¼Œè®©è°ƒç”¨æ–¹è‡ªå·±å†³å®šæ¸²æŸ“
  -->
  <div ref="scrollbarRef" class="w-full h-full overflow-y-auto">
    <slot
      v-for="(item, idx) in displayItems"
      :item="item"
      :index="idx"
      :key="resolveItemKey(item, idx)"
    ></slot>
    <div
      v-if="showFooter"
      class="py-3 text-xs text-center text-[var(--ant-color-text-secondary)]"
    >
      å·²åˆ°åº•部
    </div>
  </div>
</template>