feat: loading and spinner component with directive (#5587)
* 添加loading和spinner组件,以及对应的vue指令
This commit is contained in:
@@ -31,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
// const startTime = ref(0);
|
||||
const showSpinner = ref(false);
|
||||
const renderSpinner = ref(true);
|
||||
const renderSpinner = ref(false);
|
||||
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||
|
||||
watch(
|
||||
@@ -69,7 +69,7 @@ function onTransitionEnd() {
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'z-100 dark:bg-overlay bg-overlay-content pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
|
||||
'z-100 dark:bg-overlay bg-overlay-content absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
|
||||
{
|
||||
'invisible opacity-0': !showSpinner,
|
||||
},
|
||||
@@ -78,15 +78,18 @@ function onTransitionEnd() {
|
||||
"
|
||||
@transitionend="onTransitionEnd"
|
||||
>
|
||||
<span class="dot relative inline-block size-9 text-3xl">
|
||||
<i
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30"
|
||||
></i>
|
||||
</span>
|
||||
<slot name="icon" v-if="renderSpinner">
|
||||
<span class="dot relative inline-block size-9 text-3xl">
|
||||
<i
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30"
|
||||
></i>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
<div v-if="text" class="mt-4 text-xs">{{ text }}</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
// const startTime = ref(0);
|
||||
const showSpinner = ref(false);
|
||||
const renderSpinner = ref(true);
|
||||
const renderSpinner = ref(false);
|
||||
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||
|
||||
watch(
|
||||
@@ -74,6 +74,7 @@ function onTransitionEnd() {
|
||||
>
|
||||
<div
|
||||
:class="{ paused: !renderSpinner }"
|
||||
v-if="renderSpinner"
|
||||
class="loader before:bg-primary/50 after:bg-primary relative size-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:rounded-[50%] before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:rounded after:content-['']"
|
||||
></div>
|
||||
</div>
|
||||
|
@@ -5,6 +5,7 @@ export * from './count-to';
|
||||
export * from './ellipsis-text';
|
||||
export * from './icon-picker';
|
||||
export * from './json-viewer';
|
||||
export * from './loading';
|
||||
export * from './page';
|
||||
export * from './resize';
|
||||
export * from './tippy';
|
||||
|
132
packages/effects/common-ui/src/components/loading/directive.ts
Normal file
132
packages/effects/common-ui/src/components/loading/directive.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { App, Directive, DirectiveBinding } from 'vue';
|
||||
|
||||
import { h, render } from 'vue';
|
||||
|
||||
import { VbenLoading, VbenSpinner } from '@vben-core/shadcn-ui';
|
||||
import { isString } from '@vben-core/shared/utils';
|
||||
|
||||
const LOADING_INSTANCE_KEY = Symbol('loading');
|
||||
const SPINNER_INSTANCE_KEY = Symbol('spinner');
|
||||
|
||||
const CLASS_NAME_RELATIVE = 'spinner-parent--relative';
|
||||
|
||||
const loadingDirective: Directive = {
|
||||
mounted(el, binding) {
|
||||
const instance = h(VbenLoading, getOptions(binding));
|
||||
render(instance, el);
|
||||
|
||||
el.classList.add(CLASS_NAME_RELATIVE);
|
||||
el[LOADING_INSTANCE_KEY] = instance;
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = el[LOADING_INSTANCE_KEY];
|
||||
el.classList.remove(CLASS_NAME_RELATIVE);
|
||||
render(null, el);
|
||||
instance.el.remove();
|
||||
|
||||
el[LOADING_INSTANCE_KEY] = null;
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
const instance = el[LOADING_INSTANCE_KEY];
|
||||
const options = getOptions(binding);
|
||||
if (options && instance?.component) {
|
||||
try {
|
||||
Object.keys(options).forEach((key) => {
|
||||
instance.component.props[key] = options[key];
|
||||
});
|
||||
instance.component.update();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to update loading component in directive:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getOptions(binding: DirectiveBinding) {
|
||||
if (binding.value === undefined) {
|
||||
return { spinning: true };
|
||||
} else if (typeof binding.value === 'boolean') {
|
||||
return { spinning: binding.value };
|
||||
} else {
|
||||
return { ...binding.value };
|
||||
}
|
||||
}
|
||||
|
||||
const spinningDirective: Directive = {
|
||||
mounted(el, binding) {
|
||||
const instance = h(VbenSpinner, getOptions(binding));
|
||||
render(instance, el);
|
||||
|
||||
el.classList.add(CLASS_NAME_RELATIVE);
|
||||
el[SPINNER_INSTANCE_KEY] = instance;
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = el[SPINNER_INSTANCE_KEY];
|
||||
el.classList.remove(CLASS_NAME_RELATIVE);
|
||||
render(null, el);
|
||||
instance.el.remove();
|
||||
|
||||
el[SPINNER_INSTANCE_KEY] = null;
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
const instance = el[SPINNER_INSTANCE_KEY];
|
||||
const options = getOptions(binding);
|
||||
if (options && instance?.component) {
|
||||
try {
|
||||
Object.keys(options).forEach((key) => {
|
||||
instance.component.props[key] = options[key];
|
||||
});
|
||||
instance.component.update();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to update spinner component in directive:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
type loadingDirectiveParams = {
|
||||
/** 是否注册loading指令。如果提供一个string,则将指令注册为指定的名称 */
|
||||
loading?: boolean | string;
|
||||
/** 是否注册spinning指令。如果提供一个string,则将指令注册为指定的名称 */
|
||||
spinning?: boolean | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册loading指令
|
||||
* @param app
|
||||
* @param params
|
||||
*/
|
||||
export function registerLoadingDirective(
|
||||
app: App,
|
||||
params?: loadingDirectiveParams,
|
||||
) {
|
||||
// 注入一个样式供指令使用,确保容器是相对定位
|
||||
const style = document.createElement('style');
|
||||
style.id = CLASS_NAME_RELATIVE;
|
||||
style.innerHTML = `
|
||||
.${CLASS_NAME_RELATIVE} {
|
||||
position: relative !important;
|
||||
}
|
||||
`;
|
||||
document.head.append(style);
|
||||
if (params?.loading !== false) {
|
||||
app.directive(
|
||||
isString(params?.loading) ? params.loading : 'loading',
|
||||
loadingDirective,
|
||||
);
|
||||
}
|
||||
if (params?.spinning !== false) {
|
||||
app.directive(
|
||||
isString(params?.spinning) ? params.spinning : 'spinning',
|
||||
spinningDirective,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export * from './directive';
|
||||
export { default as Loading } from './loading.vue';
|
||||
export { default as Spinner } from './spinner.vue';
|
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenLoading } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({ name: 'Loading' });
|
||||
defineProps<{
|
||||
spinning: boolean;
|
||||
text?: string;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative min-h-20">
|
||||
<slot></slot>
|
||||
<VbenLoading :spinning="spinning" :text="text">
|
||||
<template v-if="$slots.icon" #icon>
|
||||
<slot name="icon"></slot>
|
||||
</template>
|
||||
</VbenLoading>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenSpinner } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({ name: 'Spinner' });
|
||||
defineProps({
|
||||
spinning: Boolean,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative min-h-20">
|
||||
<slot></slot>
|
||||
<VbenSpinner :spinning="spinning" />
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user