11 changed files with 2 additions and 382 deletions
@ -1,10 +1,8 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import collapseContainer from './src/collapse/CollapseContainer.vue'; |
|||
import scrollContainer from './src/ScrollContainer.vue'; |
|||
import lazyContainer from './src/LazyContainer.vue'; |
|||
|
|||
export const CollapseContainer = withInstall(collapseContainer); |
|||
export const ScrollContainer = withInstall(scrollContainer); |
|||
export const LazyContainer = withInstall(lazyContainer); |
|||
|
|||
export * from './src/typing'; |
|||
|
|||
@ -1,145 +0,0 @@ |
|||
<template> |
|||
<transition-group |
|||
class="h-full w-full" |
|||
v-bind="$attrs" |
|||
ref="elRef" |
|||
:name="transitionName" |
|||
:tag="tag" |
|||
mode="out-in" |
|||
> |
|||
<div key="component" v-if="isInit"> |
|||
<slot :loading="loading"></slot> |
|||
</div> |
|||
<div key="skeleton" v-else> |
|||
<slot name="skeleton" v-if="$slots.skeleton"></slot> |
|||
<Skeleton v-else /> |
|||
</div> |
|||
</transition-group> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { PropType } from 'vue'; |
|||
import { defineComponent, reactive, onMounted, ref, toRef, toRefs } from 'vue'; |
|||
import { useTimeoutFn } from '@vben/hooks'; |
|||
import { Skeleton } from 'ant-design-vue'; |
|||
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver'; |
|||
|
|||
interface State { |
|||
isInit: boolean; |
|||
loading: boolean; |
|||
intersectionObserverInstance: IntersectionObserver | null; |
|||
} |
|||
|
|||
const props = { |
|||
/** |
|||
* Waiting time, if the time is specified, whether visible or not, it will be automatically loaded after the specified time |
|||
*/ |
|||
timeout: { type: Number }, |
|||
/** |
|||
* The viewport where the component is located. |
|||
* If the component is scrolling in the page container, the viewport is the container |
|||
*/ |
|||
viewport: { |
|||
type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>, |
|||
default: () => null, |
|||
}, |
|||
/** |
|||
* Preload threshold, css unit |
|||
*/ |
|||
threshold: { type: String, default: '0px' }, |
|||
/** |
|||
* The scroll direction of the viewport, vertical represents the vertical direction, horizontal represents the horizontal direction |
|||
*/ |
|||
direction: { |
|||
type: String, |
|||
default: 'vertical', |
|||
validator: (v) => ['vertical', 'horizontal'].includes(v), |
|||
}, |
|||
/** |
|||
* The label name of the outer container that wraps the component |
|||
*/ |
|||
tag: { type: String, default: 'div' }, |
|||
maxWaitingTime: { type: Number, default: 80 }, |
|||
/** |
|||
* transition name |
|||
*/ |
|||
transitionName: { type: String, default: 'lazy-container' }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'LazyContainer', |
|||
components: { Skeleton }, |
|||
inheritAttrs: false, |
|||
props, |
|||
emits: ['init'], |
|||
setup(props, { emit }) { |
|||
const elRef = ref(); |
|||
const state = reactive<State>({ |
|||
isInit: false, |
|||
loading: false, |
|||
intersectionObserverInstance: null, |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
immediateInit(); |
|||
initIntersectionObserver(); |
|||
}); |
|||
|
|||
// If there is a set delay time, it will be executed immediately |
|||
function immediateInit() { |
|||
const { timeout } = props; |
|||
timeout && |
|||
useTimeoutFn(() => { |
|||
init(); |
|||
}, timeout); |
|||
} |
|||
|
|||
function init() { |
|||
state.loading = true; |
|||
|
|||
useTimeoutFn(() => { |
|||
if (state.isInit) return; |
|||
state.isInit = true; |
|||
emit('init'); |
|||
}, props.maxWaitingTime || 80); |
|||
} |
|||
|
|||
function initIntersectionObserver() { |
|||
const { timeout, direction, threshold } = props; |
|||
if (timeout) return; |
|||
// According to the scrolling direction to construct the viewport margin, used to load in advance |
|||
let rootMargin = '0px'; |
|||
switch (direction) { |
|||
case 'vertical': |
|||
rootMargin = `${threshold} 0px`; |
|||
break; |
|||
case 'horizontal': |
|||
rootMargin = `0px ${threshold}`; |
|||
break; |
|||
} |
|||
|
|||
try { |
|||
const { stop, observer } = useIntersectionObserver({ |
|||
rootMargin, |
|||
target: toRef(elRef.value, '$el'), |
|||
onIntersect: (entries: any[]) => { |
|||
const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio; |
|||
if (isIntersecting) { |
|||
init(); |
|||
if (observer) { |
|||
stop(); |
|||
} |
|||
} |
|||
}, |
|||
root: toRef(props, 'viewport'), |
|||
}); |
|||
} catch (e) { |
|||
init(); |
|||
} |
|||
} |
|||
return { |
|||
elRef, |
|||
...toRefs(state), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,48 +0,0 @@ |
|||
import { Ref, watchEffect, ref } from 'vue'; |
|||
|
|||
interface IntersectionObserverProps { |
|||
target: Ref<Element | null | undefined>; |
|||
root?: Ref<any>; |
|||
onIntersect: IntersectionObserverCallback; |
|||
rootMargin?: string; |
|||
threshold?: number; |
|||
} |
|||
|
|||
export function useIntersectionObserver({ |
|||
target, |
|||
root, |
|||
onIntersect, |
|||
rootMargin = '0px', |
|||
threshold = 0.1, |
|||
}: IntersectionObserverProps) { |
|||
let cleanup = () => {}; |
|||
const observer: Ref<Nullable<IntersectionObserver>> = ref(null); |
|||
const stopEffect = watchEffect(() => { |
|||
cleanup(); |
|||
|
|||
observer.value = new IntersectionObserver(onIntersect, { |
|||
root: root ? root.value : null, |
|||
rootMargin, |
|||
threshold, |
|||
}); |
|||
|
|||
const current = target.value; |
|||
|
|||
current && observer.value.observe(current); |
|||
|
|||
cleanup = () => { |
|||
if (observer.value) { |
|||
observer.value.disconnect(); |
|||
target.value && observer.value.unobserve(target.value); |
|||
} |
|||
}; |
|||
}); |
|||
|
|||
return { |
|||
observer, |
|||
stop: () => { |
|||
cleanup(); |
|||
stopEffect(); |
|||
}, |
|||
}; |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
<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> |
|||
@ -1,77 +0,0 @@ |
|||
<template> |
|||
<PageWrapper title="懒加载自定义动画示例" content="懒加载组件显示动画"> |
|||
<div class="lazy-base-demo-wrap"> |
|||
<h1>向下滚动</h1> |
|||
|
|||
<div class="lazy-base-demo-box"> |
|||
<LazyContainer transitionName="custom"> |
|||
<TargetContent /> |
|||
</LazyContainer> |
|||
</div> |
|||
</div> |
|||
</PageWrapper> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
import TargetContent from './TargetContent.vue'; |
|||
import { LazyContainer } from '/@/components/Container/index'; |
|||
import { PageWrapper } from '/@/components/Page'; |
|||
|
|||
export default defineComponent({ |
|||
components: { LazyContainer, TargetContent, PageWrapper }, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
.lazy-base-demo { |
|||
&-wrap { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 50%; |
|||
height: 2000px; |
|||
margin: 20px auto; |
|||
background-color: @component-background; |
|||
text-align: center; |
|||
} |
|||
|
|||
&-box { |
|||
width: 300px; |
|||
height: 300px; |
|||
} |
|||
|
|||
h1 { |
|||
height: 1300px; |
|||
margin: 20px 0; |
|||
} |
|||
} |
|||
|
|||
.custom-enter { |
|||
transform: scale(0.4) translate(100%); |
|||
opacity: 0; |
|||
} |
|||
|
|||
.custom-enter-to { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.custom-enter-active { |
|||
position: absolute; |
|||
top: 0; |
|||
width: 100%; |
|||
transition: all 0.5s; |
|||
} |
|||
|
|||
.custom-leave { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.custom-leave-to { |
|||
transform: scale(0.4) translate(-100%); |
|||
opacity: 0; |
|||
} |
|||
|
|||
.custom-leave-active { |
|||
transition: all 0.5s; |
|||
} |
|||
</style> |
|||
@ -1,52 +0,0 @@ |
|||
<template> |
|||
<PageWrapper title="懒加载基础示例" content="向下滚动到可见区域才会加载组件"> |
|||
<div class="lazy-base-demo-wrap"> |
|||
<h1>向下滚动</h1> |
|||
|
|||
<div class="lazy-base-demo-box"> |
|||
<LazyContainer> |
|||
<TargetContent /> |
|||
<template #skeleton> |
|||
<Skeleton :rows="10" /> |
|||
</template> |
|||
</LazyContainer> |
|||
</div> |
|||
</div> |
|||
</PageWrapper> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent } from 'vue'; |
|||
import { Skeleton } from 'ant-design-vue'; |
|||
import TargetContent from './TargetContent.vue'; |
|||
import { LazyContainer } from '/@/components/Container/index'; |
|||
import { PageWrapper } from '/@/components/Page'; |
|||
|
|||
export default defineComponent({ |
|||
components: { LazyContainer, PageWrapper, TargetContent, Skeleton }, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
.lazy-base-demo { |
|||
&-wrap { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 50%; |
|||
height: 2000px; |
|||
margin: 20px auto; |
|||
background-color: @component-background; |
|||
text-align: center; |
|||
} |
|||
|
|||
&-box { |
|||
width: 300px; |
|||
height: 300px; |
|||
} |
|||
|
|||
h1 { |
|||
height: 1300px; |
|||
margin: 20px 0; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue