feat: add lazyContainer comp and demo
This commit is contained in:
parent
a0c3197454
commit
fdeaa00bf2
|
|
@ -8,6 +8,7 @@
|
||||||
- 表单新增 submitOnReset 控制是否在重置时重新发起请求
|
- 表单新增 submitOnReset 控制是否在重置时重新发起请求
|
||||||
- 表格新增`sortFn`支持自定义排序
|
- 表格新增`sortFn`支持自定义排序
|
||||||
- 新增动画组件及示例
|
- 新增动画组件及示例
|
||||||
|
- 新增懒加载/延时加载组件及示例
|
||||||
|
|
||||||
### ✨ Refactor
|
### ✨ Refactor
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
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';
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
import type { PropType } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
defineComponent,
|
|
||||||
reactive,
|
|
||||||
onMounted,
|
|
||||||
ref,
|
|
||||||
unref,
|
|
||||||
onUnmounted,
|
|
||||||
TransitionGroup,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { Skeleton } from 'ant-design-vue';
|
|
||||||
import { useRaf } from '/@/hooks/event/useRaf';
|
|
||||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
|
||||||
import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
|
|
||||||
|
|
||||||
import './LazyContainer.less';
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
isInit: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
intersectionObserverInstance: IntersectionObserver | null;
|
|
||||||
}
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'LazyContainer',
|
|
||||||
emits: ['before-init', 'init'],
|
|
||||||
props: {
|
|
||||||
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
|
|
||||||
timeout: {
|
|
||||||
type: Number as PropType<number>,
|
|
||||||
default: 8000,
|
|
||||||
// default: 8000,
|
|
||||||
},
|
|
||||||
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
|
|
||||||
viewport: {
|
|
||||||
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
|
|
||||||
default: () => null,
|
|
||||||
},
|
|
||||||
// 预加载阈值, css单位
|
|
||||||
threshold: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: '0px',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
|
|
||||||
direction: {
|
|
||||||
type: String as PropType<'vertical' | 'horizontal'>,
|
|
||||||
default: 'vertical',
|
|
||||||
},
|
|
||||||
// 包裹组件的外层容器的标签名
|
|
||||||
tag: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: 'div',
|
|
||||||
},
|
|
||||||
|
|
||||||
maxWaitingTime: {
|
|
||||||
type: Number as PropType<number>,
|
|
||||||
default: 80,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 是否在不可见的时候销毁
|
|
||||||
autoDestory: {
|
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// transition name
|
|
||||||
transitionName: {
|
|
||||||
type: String as PropType<string>,
|
|
||||||
default: 'lazy-container',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, { attrs, emit, slots }) {
|
|
||||||
const elRef = ref<any>(null);
|
|
||||||
const state = reactive<State>({
|
|
||||||
isInit: false,
|
|
||||||
loading: false,
|
|
||||||
intersectionObserverInstance: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there is a set delay time, it will be executed immediately
|
|
||||||
function immediateInit() {
|
|
||||||
const { timeout } = props;
|
|
||||||
timeout &&
|
|
||||||
useTimeout(() => {
|
|
||||||
init();
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
// At this point, the skeleton component is about to be switched
|
|
||||||
emit('before-init');
|
|
||||||
// At this point you can prepare to load the resources of the lazy-loaded component
|
|
||||||
state.loading = true;
|
|
||||||
|
|
||||||
requestAnimationFrameFn(() => {
|
|
||||||
state.isInit = true;
|
|
||||||
emit('init');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function requestAnimationFrameFn(callback: () => any) {
|
|
||||||
// Prevent waiting too long without executing the callback
|
|
||||||
// Set the maximum waiting time
|
|
||||||
useTimeout(() => {
|
|
||||||
if (state.isInit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
}, props.maxWaitingTime || 80);
|
|
||||||
|
|
||||||
const { requestAnimationFrame } = useRaf();
|
|
||||||
|
|
||||||
return requestAnimationFrame;
|
|
||||||
}
|
|
||||||
function initIntersectionObserver() {
|
|
||||||
const { timeout, direction, threshold, viewport } = props;
|
|
||||||
if (timeout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
|
||||||
let rootMargin;
|
|
||||||
switch (direction) {
|
|
||||||
case 'vertical':
|
|
||||||
rootMargin = `${threshold} 0px`;
|
|
||||||
break;
|
|
||||||
case 'horizontal':
|
|
||||||
rootMargin = `0px ${threshold}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Observe the intersection of the viewport and the component container
|
|
||||||
state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {
|
|
||||||
rootMargin,
|
|
||||||
root: viewport,
|
|
||||||
threshold: [0, Number.MIN_VALUE, 0.01],
|
|
||||||
});
|
|
||||||
|
|
||||||
const el = unref(elRef);
|
|
||||||
|
|
||||||
state.intersectionObserverInstance.observe(el.$el);
|
|
||||||
} catch (e) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cross-condition change handling function
|
|
||||||
function intersectionHandler(entries: any[]) {
|
|
||||||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
|
||||||
if (isIntersecting) {
|
|
||||||
init();
|
|
||||||
if (state.intersectionObserverInstance) {
|
|
||||||
const el = unref(elRef);
|
|
||||||
state.intersectionObserverInstance.unobserve(el.$el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else {
|
|
||||||
// const { autoDestory } = props;
|
|
||||||
// autoDestory && destory();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
// function destory() {
|
|
||||||
// emit('beforeDestory');
|
|
||||||
// state.loading = false;
|
|
||||||
// nextTick(() => {
|
|
||||||
// emit('destory');
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
immediateInit();
|
|
||||||
onMounted(() => {
|
|
||||||
initIntersectionObserver();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
// Cancel the observation before the component is destroyed
|
|
||||||
if (state.intersectionObserverInstance) {
|
|
||||||
const el = unref(elRef);
|
|
||||||
state.intersectionObserverInstance.unobserve(el.$el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderContent() {
|
|
||||||
const { isInit, loading } = state;
|
|
||||||
if (isInit) {
|
|
||||||
return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
|
|
||||||
}
|
|
||||||
if (slots.skeleton) {
|
|
||||||
return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
const { tag, transitionName } = props;
|
|
||||||
return (
|
|
||||||
<TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
|
|
||||||
{() => renderContent()}
|
|
||||||
</TransitionGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<transition-group v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag">
|
||||||
|
<div key="component" v-if="isInit">
|
||||||
|
<slot :loading="loading" />
|
||||||
|
</div>
|
||||||
|
<div key="skeleton">
|
||||||
|
<slot name="skeleton" v-if="$slots.skeleton" />
|
||||||
|
<Skeleton v-else />
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
|
||||||
|
|
||||||
|
import { Skeleton } from 'ant-design-vue';
|
||||||
|
import { useRaf } from '/@/hooks/event/useRaf';
|
||||||
|
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isInit: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
intersectionObserverInstance: IntersectionObserver | null;
|
||||||
|
}
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LazyContainer',
|
||||||
|
components: { Skeleton },
|
||||||
|
props: {
|
||||||
|
// 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
|
||||||
|
timeout: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 8000,
|
||||||
|
// default: 8000,
|
||||||
|
},
|
||||||
|
// 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
|
||||||
|
viewport: {
|
||||||
|
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<
|
||||||
|
HTMLElement
|
||||||
|
>,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
// 预加载阈值, css单位
|
||||||
|
threshold: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
|
||||||
|
direction: {
|
||||||
|
type: String as PropType<'vertical' | 'horizontal'>,
|
||||||
|
default: 'vertical',
|
||||||
|
},
|
||||||
|
// 包裹组件的外层容器的标签名
|
||||||
|
tag: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: 'div',
|
||||||
|
},
|
||||||
|
|
||||||
|
maxWaitingTime: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
|
||||||
|
// // 是否在不可见的时候销毁
|
||||||
|
// autoDestory: {
|
||||||
|
// type: Boolean as PropType<boolean>,
|
||||||
|
// default: false,
|
||||||
|
// },
|
||||||
|
|
||||||
|
// transition name
|
||||||
|
transitionName: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: 'lazy-container',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['before-init', 'init'],
|
||||||
|
setup(props, { emit, slots }) {
|
||||||
|
const elRef = ref<any>(null);
|
||||||
|
const state = reactive<State>({
|
||||||
|
isInit: false,
|
||||||
|
loading: false,
|
||||||
|
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
|
||||||
|
function immediateInit() {
|
||||||
|
const { timeout } = props;
|
||||||
|
timeout &&
|
||||||
|
useTimeout(() => {
|
||||||
|
init();
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// At this point, the skeleton component is about to be switched
|
||||||
|
emit('before-init');
|
||||||
|
// At this point you can prepare to load the resources of the lazy-loaded component
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
requestAnimationFrameFn(() => {
|
||||||
|
state.isInit = true;
|
||||||
|
emit('init');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestAnimationFrameFn(callback: () => any) {
|
||||||
|
// Prevent waiting too long without executing the callback
|
||||||
|
// Set the maximum waiting time
|
||||||
|
useTimeout(() => {
|
||||||
|
if (state.isInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}, props.maxWaitingTime || 80);
|
||||||
|
|
||||||
|
const { requestAnimationFrame } = useRaf();
|
||||||
|
|
||||||
|
return requestAnimationFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initIntersectionObserver() {
|
||||||
|
const { timeout, direction, threshold, viewport } = props;
|
||||||
|
if (timeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// According to the scrolling direction to construct the viewport margin, used to load in advance
|
||||||
|
let rootMargin;
|
||||||
|
switch (direction) {
|
||||||
|
case 'vertical':
|
||||||
|
rootMargin = `${threshold} 0px`;
|
||||||
|
break;
|
||||||
|
case 'horizontal':
|
||||||
|
rootMargin = `0px ${threshold}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Observe the intersection of the viewport and the component container
|
||||||
|
state.intersectionObserverInstance = new window.IntersectionObserver(
|
||||||
|
intersectionHandler,
|
||||||
|
{
|
||||||
|
rootMargin,
|
||||||
|
root: viewport,
|
||||||
|
threshold: [0, Number.MIN_VALUE, 0.01],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = unref(elRef);
|
||||||
|
|
||||||
|
state.intersectionObserverInstance.observe(el.$el);
|
||||||
|
} catch (e) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cross-condition change handling function
|
||||||
|
function intersectionHandler(entries: any[]) {
|
||||||
|
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
|
||||||
|
if (isIntersecting) {
|
||||||
|
init();
|
||||||
|
if (state.intersectionObserverInstance) {
|
||||||
|
const el = unref(elRef);
|
||||||
|
state.intersectionObserverInstance.unobserve(el.$el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elRef,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -48,6 +48,10 @@ const menu: MenuModule = {
|
||||||
path: 'desc',
|
path: 'desc',
|
||||||
name: '详情组件',
|
name: '详情组件',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'lazy',
|
||||||
|
name: '懒加载组件',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'verify',
|
path: 'verify',
|
||||||
name: '验证组件',
|
name: '验证组件',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<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>
|
||||||
Loading…
Reference in New Issue