提交 fdeaa00b 作者: vben

feat: add lazyContainer comp and demo

上级 a0c31974
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
- 表单新增 submitOnReset 控制是否在重置时重新发起请求 - 表单新增 submitOnReset 控制是否在重置时重新发起请求
- 表格新增`sortFn`支持自定义排序 - 表格新增`sortFn`支持自定义排序
- 新增动画组件及示例 - 新增动画组件及示例
- 新增懒加载/延时加载组件及示例
### ✨ Refactor ### ✨ Refactor
......
export { default as ScrollContainer } from './src/ScrollContainer.vue'; export { default as ScrollContainer } from './src/ScrollContainer.vue';
export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue'; export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
export { default as LazyContainer } from './src/LazyContainer'; export { default as LazyContainer } from './src/LazyContainer.vue';
export * from './src/types.d'; export * from './src/types.d';
.lazy-container-enter {
opacity: 0;
}
.lazy-container-enter-to {
opacity: 1;
}
.lazy-container-enter-from,
.lazy-container-enter-active {
position: absolute;
top: 0;
width: 100%;
transition: opacity 0.3s 0.2s;
}
.lazy-container-leave {
opacity: 1;
}
.lazy-container-leave-to {
opacity: 0;
}
.lazy-container-leave-active {
transition: opacity 0.5s;
}
import type { PropType } from 'vue'; <template>
<transition-group v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag">
import { <div key="component" v-if="isInit">
defineComponent, <slot :loading="loading" />
reactive, </div>
onMounted, <div key="skeleton">
ref, <slot name="skeleton" v-if="$slots.skeleton" />
unref, <Skeleton v-else />
onUnmounted, </div>
TransitionGroup, </transition-group>
} from 'vue'; </template>
<script lang="ts">
import { Skeleton } from 'ant-design-vue'; import type { PropType } from 'vue';
import { useRaf } from '/@/hooks/event/useRaf';
import { useTimeout } from '/@/hooks/core/useTimeout'; import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
import { Skeleton } from 'ant-design-vue';
import './LazyContainer.less'; import { useRaf } from '/@/hooks/event/useRaf';
import { useTimeout } from '/@/hooks/core/useTimeout';
interface State {
interface State {
isInit: boolean; isInit: boolean;
loading: boolean; loading: boolean;
intersectionObserverInstance: IntersectionObserver | null; intersectionObserverInstance: IntersectionObserver | null;
} }
export default defineComponent({ export default defineComponent({
name: 'LazyContainer', name: 'LazyContainer',
emits: ['before-init', 'init'], components: { Skeleton },
props: { props: {
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
timeout: { timeout: {
...@@ -34,7 +35,9 @@ export default defineComponent({ ...@@ -34,7 +35,9 @@ export default defineComponent({
}, },
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
viewport: { viewport: {
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>, type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<
HTMLElement
>,
default: () => null, default: () => null,
}, },
// 预加载阈值, css单位 // 预加载阈值, css单位
...@@ -59,11 +62,11 @@ export default defineComponent({ ...@@ -59,11 +62,11 @@ export default defineComponent({
default: 80, default: 80,
}, },
// 是否在不可见的时候销毁 // // 是否在不可见的时候销毁
autoDestory: { // autoDestory: {
type: Boolean as PropType<boolean>, // type: Boolean as PropType<boolean>,
default: false, // default: false,
}, // },
// transition name // transition name
transitionName: { transitionName: {
...@@ -71,7 +74,8 @@ export default defineComponent({ ...@@ -71,7 +74,8 @@ export default defineComponent({
default: 'lazy-container', default: 'lazy-container',
}, },
}, },
setup(props, { attrs, emit, slots }) { emits: ['before-init', 'init'],
setup(props, { emit, slots }) {
const elRef = ref<any>(null); const elRef = ref<any>(null);
const state = reactive<State>({ const state = reactive<State>({
isInit: false, isInit: false,
...@@ -79,6 +83,18 @@ export default defineComponent({ ...@@ -79,6 +83,18 @@ export default defineComponent({
intersectionObserverInstance: null, intersectionObserverInstance: null,
}); });
immediateInit();
onMounted(() => {
initIntersectionObserver();
});
onUnmounted(() => {
// Cancel the observation before the component is destroyed
if (state.intersectionObserverInstance) {
const el = unref(elRef);
state.intersectionObserverInstance.unobserve(el.$el);
}
});
// If there is a set delay time, it will be executed immediately // If there is a set delay time, it will be executed immediately
function immediateInit() { function immediateInit() {
const { timeout } = props; const { timeout } = props;
...@@ -99,6 +115,7 @@ export default defineComponent({ ...@@ -99,6 +115,7 @@ export default defineComponent({
emit('init'); emit('init');
}); });
} }
function requestAnimationFrameFn(callback: () => any) { function requestAnimationFrameFn(callback: () => any) {
// Prevent waiting too long without executing the callback // Prevent waiting too long without executing the callback
// Set the maximum waiting time // Set the maximum waiting time
...@@ -113,6 +130,7 @@ export default defineComponent({ ...@@ -113,6 +130,7 @@ export default defineComponent({
return requestAnimationFrame; return requestAnimationFrame;
} }
function initIntersectionObserver() { function initIntersectionObserver() {
const { timeout, direction, threshold, viewport } = props; const { timeout, direction, threshold, viewport } = props;
if (timeout) { if (timeout) {
...@@ -130,11 +148,14 @@ export default defineComponent({ ...@@ -130,11 +148,14 @@ export default defineComponent({
} }
try { try {
// Observe the intersection of the viewport and the component container // Observe the intersection of the viewport and the component container
state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, { state.intersectionObserverInstance = new window.IntersectionObserver(
intersectionHandler,
{
rootMargin, rootMargin,
root: viewport, root: viewport,
threshold: [0, Number.MIN_VALUE, 0.01], threshold: [0, Number.MIN_VALUE, 0.01],
}); }
);
const el = unref(elRef); const el = unref(elRef);
...@@ -153,48 +174,40 @@ export default defineComponent({ ...@@ -153,48 +174,40 @@ export default defineComponent({
state.intersectionObserverInstance.unobserve(el.$el); state.intersectionObserverInstance.unobserve(el.$el);
} }
} }
// else {
// const { autoDestory } = props;
// autoDestory && destory();
// }
} }
// function destory() { return {
// emit('beforeDestory'); elRef,
// state.loading = false; ...toRefs(state),
// nextTick(() => { };
// emit('destory'); },
// });
// }
immediateInit();
onMounted(() => {
initIntersectionObserver();
}); });
onUnmounted(() => { </script>
// Cancel the observation before the component is destroyed <style lang="less">
if (state.intersectionObserverInstance) { .lazy-container-enter {
const el = unref(elRef); opacity: 0;
state.intersectionObserverInstance.unobserve(el.$el);
} }
});
function renderContent() { .lazy-container-enter-to {
const { isInit, loading } = state; opacity: 1;
if (isInit) {
return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
} }
if (slots.skeleton) {
return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>; .lazy-container-enter-from,
.lazy-container-enter-active {
position: absolute;
top: 0;
width: 100%;
transition: opacity 0.3s 0.2s;
} }
return null;
.lazy-container-leave {
opacity: 1;
} }
return () => {
const { tag, transitionName } = props; .lazy-container-leave-to {
return ( opacity: 0;
<TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}> }
{() => renderContent()}
</TransitionGroup> .lazy-container-leave-active {
); transition: opacity 0.5s;
}; }
}, </style>
});
...@@ -49,6 +49,10 @@ const menu: MenuModule = { ...@@ -49,6 +49,10 @@ const menu: MenuModule = {
name: '详情组件', name: '详情组件',
}, },
{ {
path: 'lazy',
name: '懒加载组件',
},
{
path: 'verify', path: 'verify',
name: '验证组件', name: '验证组件',
children: [ children: [
......
...@@ -99,7 +99,14 @@ export default { ...@@ -99,7 +99,14 @@ export default {
title: '详情组件', title: '详情组件',
}, },
}, },
{
path: '/lazy',
name: 'lazyDemo',
component: () => import('/@/views/demo/comp/lazy/index.vue'),
meta: {
title: '懒加载组件',
},
},
{ {
path: '/verify', path: '/verify',
name: 'VerifyDemo', name: 'VerifyDemo',
......
<template>
<Card hoverable :style="{ width: '240px', background: '#fff' }">
<template #cover>
<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />
</template>
<CardMeta title="懒加载组件" />
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Card } from 'ant-design-vue';
export default defineComponent({
components: { CardMeta: Card.Meta, Card },
setup() {
return {};
},
});
</script>
<template>
<div class="p-4 lazy-base-demo">
<Alert message="基础示例" description="向下滚动到可见区域才会加载组件" type="info" show-icon />
<div class="lazy-base-demo-wrap">
<h1>向下滚动</h1>
<LazyContainer @init="() => {}">
<TargetContent />
<template #skeleton>
<Skeleton :rows="10" />
</template>
</LazyContainer>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Skeleton, Alert } from 'ant-design-vue';
import TargetContent from './TargetContent.vue';
import { LazyContainer } from '/@/components/Container/index';
export default defineComponent({
components: { LazyContainer, TargetContent, Skeleton, Alert },
setup() {
return {};
},
});
</script>
<style lang="less" scoped>
.lazy-base-demo {
&-wrap {
display: flex;
width: 50%;
height: 2000px;
margin: 20px auto;
text-align: center;
background: #fff;
justify-content: center;
flex-direction: column;
align-items: center;
}
h1 {
height: 1300px;
margin: 20px 0;
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论