mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-23 17:50:25 +08:00
feat: add lazyContainer comp and demo
This commit is contained in:
parent
a0c3197454
commit
fdeaa00bf2
@ -8,6 +8,7 @@
|
||||
- 表单新增 submitOnReset 控制是否在重置时重新发起请求
|
||||
- 表格新增`sortFn`支持自定义排序
|
||||
- 新增动画组件及示例
|
||||
- 新增懒加载/延时加载组件及示例
|
||||
|
||||
### ✨ Refactor
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
export { default as ScrollContainer } from './src/ScrollContainer.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';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
213
src/components/Container/src/LazyContainer.vue
Normal file
213
src/components/Container/src/LazyContainer.vue
Normal file
@ -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',
|
||||
name: '详情组件',
|
||||
},
|
||||
{
|
||||
path: 'lazy',
|
||||
name: '懒加载组件',
|
||||
},
|
||||
{
|
||||
path: 'verify',
|
||||
name: '验证组件',
|
||||
|
@ -99,7 +99,14 @@ export default {
|
||||
title: '详情组件',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
path: '/lazy',
|
||||
name: 'lazyDemo',
|
||||
component: () => import('/@/views/demo/comp/lazy/index.vue'),
|
||||
meta: {
|
||||
title: '懒加载组件',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/verify',
|
||||
name: 'VerifyDemo',
|
||||
|
19
src/views/demo/comp/lazy/TargetContent.vue
Normal file
19
src/views/demo/comp/lazy/TargetContent.vue
Normal file
@ -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>
|
46
src/views/demo/comp/lazy/index.vue
Normal file
46
src/views/demo/comp/lazy/index.vue
Normal file
@ -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
Block a user