Skip to content

Commit 579b1b4

Browse files
authored
feat: loading and spinner component with directive (#5587)
* 添加loading和spinner组件,以及对应的vue指令
1 parent eba3720 commit 579b1b4

File tree

13 files changed

+321
-14
lines changed

13 files changed

+321
-14
lines changed

apps/web-antd/src/bootstrap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createApp, watchEffect } from 'vue';
22

33
import { registerAccessDirective } from '@vben/access';
4-
import { initTippy } from '@vben/common-ui';
4+
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
55
import { MotionPlugin } from '@vben/plugins/motion';
66
import { preferences } from '@vben/preferences';
77
import { initStores } from '@vben/stores';
@@ -31,6 +31,12 @@ async function bootstrap(namespace: string) {
3131

3232
const app = createApp(App);
3333

34+
// 注册v-loading指令
35+
registerLoadingDirective(app, {
36+
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
37+
spinning: 'spinning',
38+
});
39+
3440
// 国际化 i18n 配置
3541
await setupI18n(app);
3642

apps/web-ele/src/bootstrap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createApp, watchEffect } from 'vue';
22

33
import { registerAccessDirective } from '@vben/access';
4-
import { initTippy } from '@vben/common-ui';
4+
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
55
import { MotionPlugin } from '@vben/plugins/motion';
66
import { preferences } from '@vben/preferences';
77
import { initStores } from '@vben/stores';
@@ -33,6 +33,12 @@ async function bootstrap(namespace: string) {
3333
// 注册Element Plus提供的v-loading指令
3434
app.directive('loading', ElLoading.directive);
3535

36+
// 注册Vben提供的v-loading和v-spinning指令
37+
registerLoadingDirective(app, {
38+
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
39+
spinning: 'spinning',
40+
});
41+
3642
// 国际化 i18n 配置
3743
await setupI18n(app);
3844

apps/web-naive/src/bootstrap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createApp, watchEffect } from 'vue';
22

33
import { registerAccessDirective } from '@vben/access';
4-
import { initTippy } from '@vben/common-ui';
4+
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
55
import { MotionPlugin } from '@vben/plugins/motion';
66
import { preferences } from '@vben/preferences';
77
import { initStores } from '@vben/stores';
@@ -31,6 +31,12 @@ async function bootstrap(namespace: string) {
3131

3232
const app = createApp(App);
3333

34+
// 注册v-loading指令
35+
registerLoadingDirective(app, {
36+
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
37+
spinning: 'spinning',
38+
});
39+
3440
// 国际化 i18n 配置
3541
await setupI18n(app);
3642

packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
3131
});
3232
// const startTime = ref(0);
3333
const showSpinner = ref(false);
34-
const renderSpinner = ref(true);
34+
const renderSpinner = ref(false);
3535
const timer = ref<ReturnType<typeof setTimeout>>();
3636
3737
watch(
@@ -69,7 +69,7 @@ function onTransitionEnd() {
6969
<div
7070
:class="
7171
cn(
72-
'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',
72+
'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',
7373
{
7474
'invisible opacity-0': !showSpinner,
7575
},
@@ -78,15 +78,18 @@ function onTransitionEnd() {
7878
"
7979
@transitionend="onTransitionEnd"
8080
>
81-
<span class="dot relative inline-block size-9 text-3xl">
82-
<i
83-
v-for="index in 4"
84-
:key="index"
85-
class="bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30"
86-
></i>
87-
</span>
81+
<slot name="icon" v-if="renderSpinner">
82+
<span class="dot relative inline-block size-9 text-3xl">
83+
<i
84+
v-for="index in 4"
85+
:key="index"
86+
class="bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30"
87+
></i>
88+
</span>
89+
</slot>
8890

8991
<div v-if="text" class="mt-4 text-xs">{{ text }}</div>
92+
<slot></slot>
9093
</div>
9194
</template>
9295

packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
2525
});
2626
// const startTime = ref(0);
2727
const showSpinner = ref(false);
28-
const renderSpinner = ref(true);
28+
const renderSpinner = ref(false);
2929
const timer = ref<ReturnType<typeof setTimeout>>();
3030
3131
watch(
@@ -74,6 +74,7 @@ function onTransitionEnd() {
7474
>
7575
<div
7676
:class="{ paused: !renderSpinner }"
77+
v-if="renderSpinner"
7778
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-['']"
7879
></div>
7980
</div>

packages/effects/common-ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './count-to';
55
export * from './ellipsis-text';
66
export * from './icon-picker';
77
export * from './json-viewer';
8+
export * from './loading';
89
export * from './page';
910
export * from './resize';
1011
export * from './tippy';
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { App, Directive, DirectiveBinding } from 'vue';
2+
3+
import { h, render } from 'vue';
4+
5+
import { VbenLoading, VbenSpinner } from '@vben-core/shadcn-ui';
6+
import { isString } from '@vben-core/shared/utils';
7+
8+
const LOADING_INSTANCE_KEY = Symbol('loading');
9+
const SPINNER_INSTANCE_KEY = Symbol('spinner');
10+
11+
const CLASS_NAME_RELATIVE = 'spinner-parent--relative';
12+
13+
const loadingDirective: Directive = {
14+
mounted(el, binding) {
15+
const instance = h(VbenLoading, getOptions(binding));
16+
render(instance, el);
17+
18+
el.classList.add(CLASS_NAME_RELATIVE);
19+
el[LOADING_INSTANCE_KEY] = instance;
20+
},
21+
unmounted(el) {
22+
const instance = el[LOADING_INSTANCE_KEY];
23+
el.classList.remove(CLASS_NAME_RELATIVE);
24+
render(null, el);
25+
instance.el.remove();
26+
27+
el[LOADING_INSTANCE_KEY] = null;
28+
},
29+
30+
updated(el, binding) {
31+
const instance = el[LOADING_INSTANCE_KEY];
32+
const options = getOptions(binding);
33+
if (options && instance?.component) {
34+
try {
35+
Object.keys(options).forEach((key) => {
36+
instance.component.props[key] = options[key];
37+
});
38+
instance.component.update();
39+
} catch (error) {
40+
console.error(
41+
'Failed to update loading component in directive:',
42+
error,
43+
);
44+
}
45+
}
46+
},
47+
};
48+
49+
function getOptions(binding: DirectiveBinding) {
50+
if (binding.value === undefined) {
51+
return { spinning: true };
52+
} else if (typeof binding.value === 'boolean') {
53+
return { spinning: binding.value };
54+
} else {
55+
return { ...binding.value };
56+
}
57+
}
58+
59+
const spinningDirective: Directive = {
60+
mounted(el, binding) {
61+
const instance = h(VbenSpinner, getOptions(binding));
62+
render(instance, el);
63+
64+
el.classList.add(CLASS_NAME_RELATIVE);
65+
el[SPINNER_INSTANCE_KEY] = instance;
66+
},
67+
unmounted(el) {
68+
const instance = el[SPINNER_INSTANCE_KEY];
69+
el.classList.remove(CLASS_NAME_RELATIVE);
70+
render(null, el);
71+
instance.el.remove();
72+
73+
el[SPINNER_INSTANCE_KEY] = null;
74+
},
75+
76+
updated(el, binding) {
77+
const instance = el[SPINNER_INSTANCE_KEY];
78+
const options = getOptions(binding);
79+
if (options && instance?.component) {
80+
try {
81+
Object.keys(options).forEach((key) => {
82+
instance.component.props[key] = options[key];
83+
});
84+
instance.component.update();
85+
} catch (error) {
86+
console.error(
87+
'Failed to update spinner component in directive:',
88+
error,
89+
);
90+
}
91+
}
92+
},
93+
};
94+
95+
type loadingDirectiveParams = {
96+
/** 是否注册loading指令。如果提供一个string,则将指令注册为指定的名称 */
97+
loading?: boolean | string;
98+
/** 是否注册spinning指令。如果提供一个string,则将指令注册为指定的名称 */
99+
spinning?: boolean | string;
100+
};
101+
102+
/**
103+
* 注册loading指令
104+
* @param app
105+
* @param params
106+
*/
107+
export function registerLoadingDirective(
108+
app: App,
109+
params?: loadingDirectiveParams,
110+
) {
111+
// 注入一个样式供指令使用,确保容器是相对定位
112+
const style = document.createElement('style');
113+
style.id = CLASS_NAME_RELATIVE;
114+
style.innerHTML = `
115+
.${CLASS_NAME_RELATIVE} {
116+
position: relative !important;
117+
}
118+
`;
119+
document.head.append(style);
120+
if (params?.loading !== false) {
121+
app.directive(
122+
isString(params?.loading) ? params.loading : 'loading',
123+
loadingDirective,
124+
);
125+
}
126+
if (params?.spinning !== false) {
127+
app.directive(
128+
isString(params?.spinning) ? params.spinning : 'spinning',
129+
spinningDirective,
130+
);
131+
}
132+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './directive';
2+
export { default as Loading } from './loading.vue';
3+
export { default as Spinner } from './spinner.vue';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts" setup>
2+
import { VbenLoading } from '@vben-core/shadcn-ui';
3+
4+
defineOptions({ name: 'Loading' });
5+
defineProps<{
6+
spinning: boolean;
7+
text?: string;
8+
}>();
9+
</script>
10+
<template>
11+
<div class="relative min-h-20">
12+
<slot></slot>
13+
<VbenLoading :spinning="spinning" :text="text">
14+
<template v-if="$slots.icon" #icon>
15+
<slot name="icon"></slot>
16+
</template>
17+
</VbenLoading>
18+
</div>
19+
</template>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts" setup>
2+
import { VbenSpinner } from '@vben-core/shadcn-ui';
3+
4+
defineOptions({ name: 'Spinner' });
5+
defineProps({
6+
spinning: Boolean,
7+
});
8+
</script>
9+
<template>
10+
<div class="relative min-h-20">
11+
<slot></slot>
12+
<VbenSpinner :spinning="spinning" />
13+
</div>
14+
</template>

0 commit comments

Comments
 (0)