diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa8505b..542d24d43 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,10 +12,6 @@ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - // md 文件按 prettier 规范格式化 - "[md]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, "prettier.printWidth": 180, // Enable eslint for all supported languages @@ -24,6 +20,6 @@ "javascriptreact", "typescript", "typescriptreact", - "vue", + "vue" ] } diff --git a/components/scrollbar/style/index.less b/components/scrollbar/style/index.less index 00d40414a..3dcfe0930 100644 --- a/components/scrollbar/style/index.less +++ b/components/scrollbar/style/index.less @@ -16,6 +16,7 @@ width: 100%; height: 100%; overflow: hidden; + .@{scrollbar-prefix-cls}-shadow-top { .shadow(@scrollbar-shadow-size, 'top') } @@ -32,6 +33,11 @@ width: 100%; height: 100%; overflow: auto; + // 启用硬件加速和平滑滚动 + transform: translate3d(0, 0, 0); + // 包含优化,减少重排重绘 + contain: layout style paint; + will-change: scroll-position; } &-content { box-sizing: border-box; diff --git a/components/virtual-list/listItem.ts b/components/virtual-list/listItem.ts index f8a0c10d4..74b3a346e 100644 --- a/components/virtual-list/listItem.ts +++ b/components/virtual-list/listItem.ts @@ -2,7 +2,7 @@ * item component, we need to know their size change at any time */ -import { cloneVNode, computed, defineComponent, ref } from 'vue'; +import { cloneVNode, computed, defineComponent, onBeforeUnmount, ref } from 'vue'; import useResize from '../_util/use/useResize'; import { getFirstValidNode } from '../_util/vnode'; import getElementFromVueInstance from '../_util/getElementFromVueInstance'; @@ -14,20 +14,51 @@ export const FVirtualListItem = defineComponent({ props: itemProps, setup(props, { attrs }) { const itemRef = ref(); + let lastReportedSize = 0; + let sizeCheckTimer: number | null = null; - // tell parent current size identify by unqiue key + // 优化的尺寸变化检测和上报 const dispatchSizeChange = () => { + if (!itemRef.value) { + return; + } + const shapeKey = props.horizontal ? 'offsetWidth' : 'offsetHeight'; - const s = itemRef.value ? itemRef.value[shapeKey] : 0; - (attrs as any).onItemResized(props.uniqueKey, s); + const currentSize = itemRef.value[shapeKey] || 0; + + // 只有当尺寸显著变化时才上报,避免频繁的微小变化 + // 阈值可以根据实际情况调整,例如设置为2px + const sizeThreshold = 2; + if (Math.abs(currentSize - lastReportedSize) >= sizeThreshold) { + lastReportedSize = currentSize; + (attrs as any).onItemResized(props.uniqueKey, currentSize); + } }; + // 防抖的尺寸检查,并确保在组件卸载时清除定时器 + const debouncedSizeCheck = () => { + if (sizeCheckTimer) { + clearTimeout(sizeCheckTimer); + } + // 稍微增加延迟,减少高频触发,例如 30ms + sizeCheckTimer = setTimeout(dispatchSizeChange, 16) as any; + }; + + // 使用原有的useResize,但添加防抖 useResize( itemRef, - dispatchSizeChange, + debouncedSizeCheck, computed(() => !props.observeResize), ); + // 清理定时器 + onBeforeUnmount(() => { + if (sizeCheckTimer) { + clearTimeout(sizeCheckTimer); + sizeCheckTimer = null; + } + }); + return { itemRef, }; diff --git a/components/virtual-scroller/virtual-scroller.tsx b/components/virtual-scroller/virtual-scroller.tsx index a27a8d20a..8502f5982 100644 --- a/components/virtual-scroller/virtual-scroller.tsx +++ b/components/virtual-scroller/virtual-scroller.tsx @@ -55,6 +55,14 @@ export const virtualScrollerProps = { renderItemList: { type: Function as PropType<(itemVNodes: VNodeChild) => VNodeChild>, }, + topThreshold: { + type: Number, + default: 0, + }, + bottomThreshold: { + type: Number, + default: 0, + }, } as const satisfies ComponentObjectPropsOptions; export type VirtualScrollerProps = ExtractPublicPropTypes; @@ -123,10 +131,10 @@ export default defineComponent({ virtualRef.value?.scrollBy(offset); }, getItemOffset: (index: number) => { - virtualRef.value?.getItemOffset(index); + return virtualRef.value?.getItemOffset(index); }, getItemSize: (index: number) => { - virtualRef.value?.getItemSize(index); + return virtualRef.value?.getItemSize(index); }, findStartIndex: () => { virtualRef.value?.findStartIndex(); @@ -134,17 +142,25 @@ export default defineComponent({ findEndIndex: () => { virtualRef.value?.findEndIndex(); }, + getOffset, + getScrollSize, + getClientSize, }); + let lastScrollTop = 0; const onScroll = (e: Event) => { emit('scroll', e); + const currentScrollTop = (e.target as HTMLElement).scrollTop; + const isScrollUp = currentScrollTop < lastScrollTop; + lastScrollTop = currentScrollTop; + const offset = getOffset(); const scrollSize = getScrollSize(); const clientSize = getClientSize(); - if (offset === 0) { + if (isScrollUp && offset <= props.topThreshold) { emit('toTop'); } - if (offset + clientSize >= scrollSize) { + if (!isScrollUp && offset + clientSize >= scrollSize - props.bottomThreshold) { emit('toBottom'); } }; diff --git a/docs/.vitepress/components/virtualList/common.vue b/docs/.vitepress/components/virtualList/common.vue index 1dde86f08..e609c45db 100644 --- a/docs/.vitepress/components/virtualList/common.vue +++ b/docs/.vitepress/components/virtualList/common.vue @@ -1,11 +1,11 @@ @@ -76,8 +76,6 @@ export default { diff --git a/docs/.vitepress/components/virtualList/maxHeight.vue b/docs/.vitepress/components/virtualList/maxHeight.vue index 39b123e1c..f57e125bf 100644 --- a/docs/.vitepress/components/virtualList/maxHeight.vue +++ b/docs/.vitepress/components/virtualList/maxHeight.vue @@ -32,7 +32,7 @@ @@ -57,9 +57,36 @@ export default { const maxHeight = ref(200); const vals = ref([]); - for (let i = 0; i < 6; ++i) { - vals.value.push(i); - } + const sentence3 = [ + 'BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。', + 'IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', + 'margin 重合,margin 塌陷', + 'html5IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)IFC(Inline Formatting Contexts)直译为”内联格式化上下文”,IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)', + ]; + const genUniqueId = (prefix) => { + return `${prefix}$${Math.random().toString(16).substr(9)}`; + }; + const getSentences = () => { + const index = Math.floor(Math.random() * (sentence3.length - 1)); + return sentence3[index]; + }; + + const createData = (length, startIndex, isAdd = true) => { + const result = []; + let count = length; + while (count--) { + const index = isAdd ? startIndex + length - count - 1 : startIndex - count - 1; + result.push({ + index, + name: `${Math.random()}`, + id: genUniqueId(index), + desc: getSentences(), + }); + } + return result; + }; + + vals.value = createData(1000, 1); watch( heightType, @@ -97,9 +124,7 @@ export default { width: 1000px; } .virtual-scroll-list-max-height .virtual-scroll-list-wrap .virtual-scroll-item { - height: 36px; - background: rgba(83, 132, 255, 0.06); - border-bottom: 2px solid #fff; + margin: 0.5em; } .virtual-scroll-list-max-height .virtual-scroll-list-wrap diff --git a/docs/.vitepress/components/virtualList/vertical.vue b/docs/.vitepress/components/virtualScroller/common.vue similarity index 98% rename from docs/.vitepress/components/virtualList/vertical.vue rename to docs/.vitepress/components/virtualScroller/common.vue index e609c45db..5cc290eed 100644 --- a/docs/.vitepress/components/virtualList/vertical.vue +++ b/docs/.vitepress/components/virtualScroller/common.vue @@ -1,10 +1,9 @@ diff --git a/docs/.vitepress/components/virtualScroller/horizontal.vue b/docs/.vitepress/components/virtualScroller/horizontal.vue new file mode 100644 index 000000000..59b26c9f9 --- /dev/null +++ b/docs/.vitepress/components/virtualScroller/horizontal.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/docs/.vitepress/components/virtualScroller/index.md b/docs/.vitepress/components/virtualScroller/index.md new file mode 100644 index 000000000..cb73018fb --- /dev/null +++ b/docs/.vitepress/components/virtualScroller/index.md @@ -0,0 +1,79 @@ +# VirtualScroller 虚拟滚动 + +比虚拟列表有更好的渲染性能。 + +## 组件注册 + +```js +import { FVirtualScroller } from 'fes-design'; + +app.use(FVirtualScroller); +``` + +## 代码演示 + +### 不规则纵向滚动 + +:::demo +common.vue +::: + +### 不规则横向滚动 + +:::demo +horizontal.vue +::: + +### 无限滚动 + +:::demo +infinite.vue +::: + +### 滚动操作 + +:::demo +scroll.vue +::: + +## VirtualScroller Props + +| 属性 | 说明 | 类型 | 默认值 | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ---------- | +| dataSources | 为列表生成的源数组,每个数组数据必须是一个对象 | Array\ | `-` | +| keeps | 您期望虚拟列表在真实 `dom` 中保持渲染的项目数量。 | number | 30 | +| itemSize | 每项的估计大小,如果它更接近平均大小,滚动条长度看起来更准确。建议指定自己计算的平均值 | number | 50 | +| itemTag | 子项的包裹元素 | number | `div` | +| itemProps | 子项包裹元素的属性 | function | `({ item, index }: { item: ItemData; index: number }) => any` | +| direction | 滚动的方向, 可选值为 `vertical` 和 `horizontal` | string | `vertical` | +| wrapTag | 列表包裹元素名称 | string | `div` | +| wrapClass | 列表包裹元素类名 | string | - | +| wrapStyle | 列表包裹元素内联样式 | object | `{}` | +| topThreshold | 触发`toTop` 事件的阈值 | number | 0 | +| bottomThreshold | 触发`toBottom` 事件的阈值 | number | 0 | +| scrollbarProps | 滚动条样式,参考滚动条组件 | object | - | + +## VirtualScroller Events + +| 事件名称 | 说明 | 回调参数 | +| -------- | ---------------------------------- | ----------------------------- | +| scroll | 滚动时触发 | (event: Event, range) => void | +| toTop | 当滚动到顶部或者左边时触发 | () => void | +| toBottom | 当滚动到底部或者右边时触发,无参数 | () => void | + +## VirtualScroller Methods + +| 名称 | 说明 | 参数 | +| -------------- | -------------------------------------------------------------------------------------- | ------------------------ | +| scrollRef | 滚动条 | - | +| scrollToIndex | 手动将滚动位置设置为指定索引 | () => void | +| scrollToBottom | 手动将滚动位置设置到最底部 | (index: number) => void | +| scrollBy | 手动将滚动位置设置为相对指定偏移量 | (offset: number) => void | +| scrollTo | 手动将滚动位置设置为指定偏移量 | (offset: number) => void | +| getItemOffset | 获取选项位置 | (index: number) => number | +| getItemSize | 获取选项大小 | (index: number) => number | +| getOffset | 获取当前滚动偏移量 | () => number | +| getClientSize | 获取包装器元素客户端视口大小(宽度或高度) | () => number | +| getScrollSize | 获取所有滚动大小(滚动高度或滚动宽度) | () => number | +| findStartIndex | 获取显示的初始元素索引 | () => number | +| findEndIndex | 获取显示的结束元素索引 | () => number | diff --git a/docs/.vitepress/components/virtualScroller/infinite.vue b/docs/.vitepress/components/virtualScroller/infinite.vue new file mode 100644 index 000000000..e3caae5d2 --- /dev/null +++ b/docs/.vitepress/components/virtualScroller/infinite.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/docs/.vitepress/components/virtualScroller/scroll.vue b/docs/.vitepress/components/virtualScroller/scroll.vue new file mode 100644 index 000000000..5341642bd --- /dev/null +++ b/docs/.vitepress/components/virtualScroller/scroll.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/docs/.vitepress/configs/navbar/index.ts b/docs/.vitepress/configs/navbar/index.ts index 7b5df9e8c..6a7f79425 100644 --- a/docs/.vitepress/configs/navbar/index.ts +++ b/docs/.vitepress/configs/navbar/index.ts @@ -1,4 +1,4 @@ -import { type DefaultTheme } from 'vitepress'; +import type { DefaultTheme } from 'vitepress'; import { getPackageJsonVersion } from '../../../../scripts/utils.js'; const currentVersion = getPackageJsonVersion(); diff --git a/docs/.vitepress/configs/sidebar/index.ts b/docs/.vitepress/configs/sidebar/index.ts index 53408b40f..e2e371c7b 100644 --- a/docs/.vitepress/configs/sidebar/index.ts +++ b/docs/.vitepress/configs/sidebar/index.ts @@ -221,6 +221,9 @@ const sidebarConfig: Record = { { text: 'VirtualList 虚拟列表', link: '/zh/components/virtualList', + }, { + text: 'VirtualScroller 虚拟滚动', + link: '/zh/components/virtualScroller', }, ], },