Commit fdeaa00bf24b0710ca341fafba8327c786ab9879

Authored by vben
1 parent a0c31974

feat: add lazyContainer comp and demo

CHANGELOG.zh_CN.md
... ... @@ -8,6 +8,7 @@
8 8 - 表单新增 submitOnReset 控制是否在重置时重新发起请求
9 9 - 表格新增`sortFn`支持自定义排序
10 10 - 新增动画组件及示例
  11 +- 新增懒加载/延时加载组件及示例
11 12  
12 13 ### ✨ Refactor
13 14  
... ...
src/components/Container/index.ts
1 1 export { default as ScrollContainer } from './src/ScrollContainer.vue';
2 2 export { default as CollapseContainer } from './src/collapse/CollapseContainer.vue';
3   -export { default as LazyContainer } from './src/LazyContainer';
  3 +export { default as LazyContainer } from './src/LazyContainer.vue';
4 4  
5 5 export * from './src/types.d';
... ...
src/components/Container/src/LazyContainer.less deleted 100644 → 0
1   -.lazy-container-enter {
2   - opacity: 0;
3   -}
4   -
5   -.lazy-container-enter-to {
6   - opacity: 1;
7   -}
8   -
9   -.lazy-container-enter-from,
10   -.lazy-container-enter-active {
11   - position: absolute;
12   - top: 0;
13   - width: 100%;
14   - transition: opacity 0.3s 0.2s;
15   -}
16   -
17   -.lazy-container-leave {
18   - opacity: 1;
19   -}
20   -
21   -.lazy-container-leave-to {
22   - opacity: 0;
23   -}
24   -
25   -.lazy-container-leave-active {
26   - transition: opacity 0.5s;
27   -}
src/components/Container/src/LazyContainer.tsx renamed to src/components/Container/src/LazyContainer.vue
1   -import type { PropType } from 'vue';
2   -
3   -import {
4   - defineComponent,
5   - reactive,
6   - onMounted,
7   - ref,
8   - unref,
9   - onUnmounted,
10   - TransitionGroup,
11   -} from 'vue';
12   -
13   -import { Skeleton } from 'ant-design-vue';
14   -import { useRaf } from '/@/hooks/event/useRaf';
15   -import { useTimeout } from '/@/hooks/core/useTimeout';
16   -import { getListeners, getSlot } from '/@/utils/helper/tsxHelper';
17   -
18   -import './LazyContainer.less';
19   -
20   -interface State {
21   - isInit: boolean;
22   - loading: boolean;
23   - intersectionObserverInstance: IntersectionObserver | null;
24   -}
25   -export default defineComponent({
26   - name: 'LazyContainer',
27   - emits: ['before-init', 'init'],
28   - props: {
29   - // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
30   - timeout: {
31   - type: Number as PropType<number>,
32   - default: 8000,
33   - // default: 8000,
34   - },
35   - // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
36   - viewport: {
37   - type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
38   - default: () => null,
39   - },
40   - // 预加载阈值, css单位
41   - threshold: {
42   - type: String as PropType<string>,
43   - default: '0px',
  1 +<template>
  2 + <transition-group v-bind="$attrs" ref="elRef" :name="transitionName" :tag="tag">
  3 + <div key="component" v-if="isInit">
  4 + <slot :loading="loading" />
  5 + </div>
  6 + <div key="skeleton">
  7 + <slot name="skeleton" v-if="$slots.skeleton" />
  8 + <Skeleton v-else />
  9 + </div>
  10 + </transition-group>
  11 +</template>
  12 +<script lang="ts">
  13 + import type { PropType } from 'vue';
  14 +
  15 + import { defineComponent, reactive, onMounted, ref, unref, onUnmounted, toRefs } from 'vue';
  16 +
  17 + import { Skeleton } from 'ant-design-vue';
  18 + import { useRaf } from '/@/hooks/event/useRaf';
  19 + import { useTimeout } from '/@/hooks/core/useTimeout';
  20 +
  21 + interface State {
  22 + isInit: boolean;
  23 + loading: boolean;
  24 + intersectionObserverInstance: IntersectionObserver | null;
  25 + }
  26 + export default defineComponent({
  27 + name: 'LazyContainer',
  28 + components: { Skeleton },
  29 + props: {
  30 + // 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载
  31 + timeout: {
  32 + type: Number as PropType<number>,
  33 + default: 8000,
  34 + // default: 8000,
  35 + },
  36 + // 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器
  37 + viewport: {
  38 + type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<
  39 + HTMLElement
  40 + >,
  41 + default: () => null,
  42 + },
  43 + // 预加载阈值, css单位
  44 + threshold: {
  45 + type: String as PropType<string>,
  46 + default: '0px',
  47 + },
  48 +
  49 + // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
  50 + direction: {
  51 + type: String as PropType<'vertical' | 'horizontal'>,
  52 + default: 'vertical',
  53 + },
  54 + // 包裹组件的外层容器的标签名
  55 + tag: {
  56 + type: String as PropType<string>,
  57 + default: 'div',
  58 + },
  59 +
  60 + maxWaitingTime: {
  61 + type: Number as PropType<number>,
  62 + default: 80,
  63 + },
  64 +
  65 + // // 是否在不可见的时候销毁
  66 + // autoDestory: {
  67 + // type: Boolean as PropType<boolean>,
  68 + // default: false,
  69 + // },
  70 +
  71 + // transition name
  72 + transitionName: {
  73 + type: String as PropType<string>,
  74 + default: 'lazy-container',
  75 + },
44 76 },
  77 + emits: ['before-init', 'init'],
  78 + setup(props, { emit, slots }) {
  79 + const elRef = ref<any>(null);
  80 + const state = reactive<State>({
  81 + isInit: false,
  82 + loading: false,
  83 + intersectionObserverInstance: null,
  84 + });
45 85  
46   - // 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向
47   - direction: {
48   - type: String as PropType<'vertical' | 'horizontal'>,
49   - default: 'vertical',
50   - },
51   - // 包裹组件的外层容器的标签名
52   - tag: {
53   - type: String as PropType<string>,
54   - default: 'div',
55   - },
  86 + immediateInit();
  87 + onMounted(() => {
  88 + initIntersectionObserver();
  89 + });
  90 + onUnmounted(() => {
  91 + // Cancel the observation before the component is destroyed
  92 + if (state.intersectionObserverInstance) {
  93 + const el = unref(elRef);
  94 + state.intersectionObserverInstance.unobserve(el.$el);
  95 + }
  96 + });
56 97  
57   - maxWaitingTime: {
58   - type: Number as PropType<number>,
59   - default: 80,
60   - },
  98 + // If there is a set delay time, it will be executed immediately
  99 + function immediateInit() {
  100 + const { timeout } = props;
  101 + timeout &&
  102 + useTimeout(() => {
  103 + init();
  104 + }, timeout);
  105 + }
61 106  
62   - // 是否在不可见的时候销毁
63   - autoDestory: {
64   - type: Boolean as PropType<boolean>,
65   - default: false,
66   - },
  107 + function init() {
  108 + // At this point, the skeleton component is about to be switched
  109 + emit('before-init');
  110 + // At this point you can prepare to load the resources of the lazy-loaded component
  111 + state.loading = true;
67 112  
68   - // transition name
69   - transitionName: {
70   - type: String as PropType<string>,
71   - default: 'lazy-container',
72   - },
73   - },
74   - setup(props, { attrs, emit, slots }) {
75   - const elRef = ref<any>(null);
76   - const state = reactive<State>({
77   - isInit: false,
78   - loading: false,
79   - intersectionObserverInstance: null,
80   - });
81   -
82   - // If there is a set delay time, it will be executed immediately
83   - function immediateInit() {
84   - const { timeout } = props;
85   - timeout &&
  113 + requestAnimationFrameFn(() => {
  114 + state.isInit = true;
  115 + emit('init');
  116 + });
  117 + }
  118 +
  119 + function requestAnimationFrameFn(callback: () => any) {
  120 + // Prevent waiting too long without executing the callback
  121 + // Set the maximum waiting time
86 122 useTimeout(() => {
87   - init();
88   - }, timeout);
89   - }
90   -
91   - function init() {
92   - // At this point, the skeleton component is about to be switched
93   - emit('before-init');
94   - // At this point you can prepare to load the resources of the lazy-loaded component
95   - state.loading = true;
96   -
97   - requestAnimationFrameFn(() => {
98   - state.isInit = true;
99   - emit('init');
100   - });
101   - }
102   - function requestAnimationFrameFn(callback: () => any) {
103   - // Prevent waiting too long without executing the callback
104   - // Set the maximum waiting time
105   - useTimeout(() => {
106   - if (state.isInit) {
107   - return;
108   - }
109   - callback();
110   - }, props.maxWaitingTime || 80);
  123 + if (state.isInit) {
  124 + return;
  125 + }
  126 + callback();
  127 + }, props.maxWaitingTime || 80);
111 128  
112   - const { requestAnimationFrame } = useRaf();
  129 + const { requestAnimationFrame } = useRaf();
113 130  
114   - return requestAnimationFrame;
115   - }
116   - function initIntersectionObserver() {
117   - const { timeout, direction, threshold, viewport } = props;
118   - if (timeout) {
119   - return;
  131 + return requestAnimationFrame;
120 132 }
121   - // According to the scrolling direction to construct the viewport margin, used to load in advance
122   - let rootMargin;
123   - switch (direction) {
124   - case 'vertical':
125   - rootMargin = `${threshold} 0px`;
126   - break;
127   - case 'horizontal':
128   - rootMargin = `0px ${threshold}`;
129   - break;
130   - }
131   - try {
132   - // Observe the intersection of the viewport and the component container
133   - state.intersectionObserverInstance = new window.IntersectionObserver(intersectionHandler, {
134   - rootMargin,
135   - root: viewport,
136   - threshold: [0, Number.MIN_VALUE, 0.01],
137   - });
138 133  
139   - const el = unref(elRef);
  134 + function initIntersectionObserver() {
  135 + const { timeout, direction, threshold, viewport } = props;
  136 + if (timeout) {
  137 + return;
  138 + }
  139 + // According to the scrolling direction to construct the viewport margin, used to load in advance
  140 + let rootMargin;
  141 + switch (direction) {
  142 + case 'vertical':
  143 + rootMargin = `${threshold} 0px`;
  144 + break;
  145 + case 'horizontal':
  146 + rootMargin = `0px ${threshold}`;
  147 + break;
  148 + }
  149 + try {
  150 + // Observe the intersection of the viewport and the component container
  151 + state.intersectionObserverInstance = new window.IntersectionObserver(
  152 + intersectionHandler,
  153 + {
  154 + rootMargin,
  155 + root: viewport,
  156 + threshold: [0, Number.MIN_VALUE, 0.01],
  157 + }
  158 + );
140 159  
141   - state.intersectionObserverInstance.observe(el.$el);
142   - } catch (e) {
143   - init();
144   - }
145   - }
146   - // Cross-condition change handling function
147   - function intersectionHandler(entries: any[]) {
148   - const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
149   - if (isIntersecting) {
150   - init();
151   - if (state.intersectionObserverInstance) {
152 160 const el = unref(elRef);
153   - state.intersectionObserverInstance.unobserve(el.$el);
154   - }
155   - }
156   - // else {
157   - // const { autoDestory } = props;
158   - // autoDestory && destory();
159   - // }
160   - }
161   - // function destory() {
162   - // emit('beforeDestory');
163   - // state.loading = false;
164   - // nextTick(() => {
165   - // emit('destory');
166   - // });
167   - // }
168   -
169   - immediateInit();
170   - onMounted(() => {
171   - initIntersectionObserver();
172   - });
173   - onUnmounted(() => {
174   - // Cancel the observation before the component is destroyed
175   - if (state.intersectionObserverInstance) {
176   - const el = unref(elRef);
177   - state.intersectionObserverInstance.unobserve(el.$el);
178   - }
179   - });
180 161  
181   - function renderContent() {
182   - const { isInit, loading } = state;
183   - if (isInit) {
184   - return <div key="component">{getSlot(slots, 'default', { loading })}</div>;
  162 + state.intersectionObserverInstance.observe(el.$el);
  163 + } catch (e) {
  164 + init();
  165 + }
185 166 }
186   - if (slots.skeleton) {
187   - return <div key="skeleton">{getSlot(slots, 'skeleton') || <Skeleton />}</div>;
  167 + // Cross-condition change handling function
  168 + function intersectionHandler(entries: any[]) {
  169 + const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio;
  170 + if (isIntersecting) {
  171 + init();
  172 + if (state.intersectionObserverInstance) {
  173 + const el = unref(elRef);
  174 + state.intersectionObserverInstance.unobserve(el.$el);
  175 + }
  176 + }
188 177 }
189   - return null;
190   - }
191   - return () => {
192   - const { tag, transitionName } = props;
193   - return (
194   - <TransitionGroup ref={elRef} name={transitionName} tag={tag} {...getListeners(attrs)}>
195   - {() => renderContent()}
196   - </TransitionGroup>
197   - );
198   - };
199   - },
200   -});
  178 + return {
  179 + elRef,
  180 + ...toRefs(state),
  181 + };
  182 + },
  183 + });
  184 +</script>
  185 +<style lang="less">
  186 + .lazy-container-enter {
  187 + opacity: 0;
  188 + }
  189 +
  190 + .lazy-container-enter-to {
  191 + opacity: 1;
  192 + }
  193 +
  194 + .lazy-container-enter-from,
  195 + .lazy-container-enter-active {
  196 + position: absolute;
  197 + top: 0;
  198 + width: 100%;
  199 + transition: opacity 0.3s 0.2s;
  200 + }
  201 +
  202 + .lazy-container-leave {
  203 + opacity: 1;
  204 + }
  205 +
  206 + .lazy-container-leave-to {
  207 + opacity: 0;
  208 + }
  209 +
  210 + .lazy-container-leave-active {
  211 + transition: opacity 0.5s;
  212 + }
  213 +</style>
... ...
src/router/menus/modules/demo/comp.ts
... ... @@ -49,6 +49,10 @@ const menu: MenuModule = {
49 49 name: '详情组件',
50 50 },
51 51 {
  52 + path: 'lazy',
  53 + name: '懒加载组件',
  54 + },
  55 + {
52 56 path: 'verify',
53 57 name: '验证组件',
54 58 children: [
... ...
src/router/routes/modules/demo/comp.ts
... ... @@ -99,7 +99,14 @@ export default {
99 99 title: '详情组件',
100 100 },
101 101 },
102   -
  102 + {
  103 + path: '/lazy',
  104 + name: 'lazyDemo',
  105 + component: () => import('/@/views/demo/comp/lazy/index.vue'),
  106 + meta: {
  107 + title: '懒加载组件',
  108 + },
  109 + },
103 110 {
104 111 path: '/verify',
105 112 name: 'VerifyDemo',
... ...
src/views/demo/comp/lazy/TargetContent.vue 0 → 100644
  1 +<template>
  2 + <Card hoverable :style="{ width: '240px', background: '#fff' }">
  3 + <template #cover>
  4 + <img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />
  5 + </template>
  6 + <CardMeta title="懒加载组件" />
  7 + </Card>
  8 +</template>
  9 +<script lang="ts">
  10 + import { defineComponent } from 'vue';
  11 + import { Card } from 'ant-design-vue';
  12 +
  13 + export default defineComponent({
  14 + components: { CardMeta: Card.Meta, Card },
  15 + setup() {
  16 + return {};
  17 + },
  18 + });
  19 +</script>
... ...
src/views/demo/comp/lazy/index.vue 0 → 100644
  1 +<template>
  2 + <div class="p-4 lazy-base-demo">
  3 + <Alert message="基础示例" description="向下滚动到可见区域才会加载组件" type="info" show-icon />
  4 + <div class="lazy-base-demo-wrap">
  5 + <h1>向下滚动</h1>
  6 + <LazyContainer @init="() => {}">
  7 + <TargetContent />
  8 + <template #skeleton>
  9 + <Skeleton :rows="10" />
  10 + </template>
  11 + </LazyContainer>
  12 + </div>
  13 + </div>
  14 +</template>
  15 +<script lang="ts">
  16 + import { defineComponent } from 'vue';
  17 + import { Skeleton, Alert } from 'ant-design-vue';
  18 + import TargetContent from './TargetContent.vue';
  19 + import { LazyContainer } from '/@/components/Container/index';
  20 + export default defineComponent({
  21 + components: { LazyContainer, TargetContent, Skeleton, Alert },
  22 + setup() {
  23 + return {};
  24 + },
  25 + });
  26 +</script>
  27 +<style lang="less" scoped>
  28 + .lazy-base-demo {
  29 + &-wrap {
  30 + display: flex;
  31 + width: 50%;
  32 + height: 2000px;
  33 + margin: 20px auto;
  34 + text-align: center;
  35 + background: #fff;
  36 + justify-content: center;
  37 + flex-direction: column;
  38 + align-items: center;
  39 + }
  40 +
  41 + h1 {
  42 + height: 1300px;
  43 + margin: 20px 0;
  44 + }
  45 + }
  46 +</style>
... ...