初始版本提交
This commit is contained in:
10
src/components/Container/index.ts
Normal file
10
src/components/Container/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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';
|
||||
145
src/components/Container/src/LazyContainer.vue
Normal file
145
src/components/Container/src/LazyContainer.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<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 { Skeleton } from 'ant-design-vue';
|
||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||
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>
|
||||
98
src/components/Container/src/ScrollContainer.vue
Normal file
98
src/components/Container/src/ScrollContainer.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<Scrollbar ref="scrollbarRef" class="scroll-container" v-bind="$attrs">
|
||||
<slot></slot>
|
||||
</Scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref, nextTick } from 'vue';
|
||||
import { Scrollbar, ScrollbarType } from '/@/components/Scrollbar';
|
||||
import { useScrollTo } from '/@/hooks/event/useScrollTo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScrollContainer',
|
||||
components: { Scrollbar },
|
||||
setup() {
|
||||
const scrollbarRef = ref<Nullable<ScrollbarType>>(null);
|
||||
|
||||
/**
|
||||
* Scroll to the specified position
|
||||
*/
|
||||
function scrollTo(to: number, duration = 500) {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
const wrap = unref(scrollbar.wrap);
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
const { start } = useScrollTo({
|
||||
el: wrap,
|
||||
to,
|
||||
duration,
|
||||
});
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
function getScrollWrap() {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) {
|
||||
return null;
|
||||
}
|
||||
return scrollbar.wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the bottom
|
||||
*/
|
||||
function scrollBottom() {
|
||||
const scrollbar = unref(scrollbarRef);
|
||||
if (!scrollbar) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
const wrap = unref(scrollbar.wrap) as any;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
const scrollHeight = wrap.scrollHeight as number;
|
||||
const { start } = useScrollTo({
|
||||
el: wrap,
|
||||
to: scrollHeight,
|
||||
});
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
scrollbarRef,
|
||||
scrollTo,
|
||||
scrollBottom,
|
||||
getScrollWrap,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.scrollbar__wrap {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.scrollbar__view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-body > .scroll-container {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
111
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
111
src/components/Container/src/collapse/CollapseContainer.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<CollapseHeader v-bind="props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
|
||||
<template #title>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
<template #action>
|
||||
<slot name="action"></slot>
|
||||
</template>
|
||||
</CollapseHeader>
|
||||
|
||||
<div class="p-2">
|
||||
<CollapseTransition :enable="canExpan">
|
||||
<Skeleton v-if="loading" :active="loading" />
|
||||
<div :class="`${prefixCls}__body`" v-else v-show="show">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</CollapseTransition>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__footer`" v-if="$slots.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { isNil } from 'lodash-es';
|
||||
// component
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
import { CollapseTransition } from '/@/components/Transition';
|
||||
import CollapseHeader from './CollapseHeader.vue';
|
||||
import { triggerWindowResize } from '/@/utils/event';
|
||||
// hook
|
||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
loading: { type: Boolean },
|
||||
/**
|
||||
* Can it be expanded
|
||||
*/
|
||||
canExpan: { type: Boolean, default: true },
|
||||
/**
|
||||
* Warm reminder on the right side of the title
|
||||
*/
|
||||
helpMessage: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* Whether to trigger window.resize when expanding and contracting,
|
||||
* Can adapt to tables and forms, when the form shrinks, the form triggers resize to adapt to the height
|
||||
*/
|
||||
triggerWindowResize: { type: Boolean },
|
||||
/**
|
||||
* Delayed loading time
|
||||
*/
|
||||
lazyTime: { type: Number, default: 0 },
|
||||
hasLeftBorder: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const show = ref(true);
|
||||
|
||||
const { prefixCls } = useDesign('collapse-container');
|
||||
|
||||
/**
|
||||
* @description: Handling development events
|
||||
*/
|
||||
function handleExpand(val: boolean) {
|
||||
show.value = isNil(val) ? !show.value : val;
|
||||
if (props.triggerWindowResize) {
|
||||
// 200 milliseconds here is because the expansion has animation,
|
||||
useTimeoutFn(triggerWindowResize, 200);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleExpand,
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-collapse-container';
|
||||
|
||||
.@{prefix-cls} {
|
||||
background-color: @component-background;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid @border-color-light;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 1px solid @border-color-light;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
49
src/components/Container/src/collapse/CollapseHeader.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="[`${prefixCls}__header pr-2 py-5`, $attrs.class]">
|
||||
<BasicTitle :helpMessage="helpMessage" normal>
|
||||
<template v-if="title">
|
||||
<div :class="{ 'header-title': hasLeftBorder }">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
</BasicTitle>
|
||||
<div :class="`${prefixCls}__action`">
|
||||
<slot name="action"></slot>
|
||||
<BasicArrow v-if="canExpan" up :expand="show" @click="$emit('expand')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { BasicArrow, BasicTitle } from '/@/components/Basic';
|
||||
|
||||
const props = {
|
||||
prefixCls: { type: String },
|
||||
helpMessage: {
|
||||
type: [Array, String] as PropType<string[] | string>,
|
||||
default: '',
|
||||
},
|
||||
title: { type: String },
|
||||
show: { type: Boolean },
|
||||
canExpan: { type: Boolean },
|
||||
hasLeftBorder: { type: Boolean },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { BasicArrow, BasicTitle },
|
||||
inheritAttrs: false,
|
||||
props,
|
||||
emits: ['expand'],
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
padding-left: 6px;
|
||||
border-left: 6px solid #5e95ff;
|
||||
}
|
||||
</style>
|
||||
17
src/components/Container/src/typing.ts
Normal file
17
src/components/Container/src/typing.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type ScrollType = 'default' | 'main';
|
||||
|
||||
export interface CollapseContainerOptions {
|
||||
canExpand?: boolean;
|
||||
title?: string;
|
||||
helpMessage?: Array<any> | string;
|
||||
}
|
||||
export interface ScrollContainerOptions {
|
||||
enableScroll?: boolean;
|
||||
type?: ScrollType;
|
||||
}
|
||||
|
||||
export type ScrollActionType = RefType<{
|
||||
scrollBottom: () => void;
|
||||
getScrollWrap: () => Nullable<HTMLElement>;
|
||||
scrollTo: (top: number) => void;
|
||||
}>;
|
||||
Reference in New Issue
Block a user