diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index d2dad44e..fe7d9650 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { MotionPlugin } from '@vben/plugins/motion'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; @@ -31,6 +31,12 @@ async function bootstrap(namespace: string) { const app = createApp(App); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index 1e4cc3ef..7e9e1cd4 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { MotionPlugin } from '@vben/plugins/motion'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; @@ -33,6 +33,12 @@ async function bootstrap(namespace: string) { // 注册Element Plus提供的v-loading指令 app.directive('loading', ElLoading.directive); + // 注册Vben提供的v-loading和v-spinning指令 + registerLoadingDirective(app, { + loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); diff --git a/apps/web-naive/src/bootstrap.ts b/apps/web-naive/src/bootstrap.ts index cc3e775a..b8c21ad2 100644 --- a/apps/web-naive/src/bootstrap.ts +++ b/apps/web-naive/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { MotionPlugin } from '@vben/plugins/motion'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; @@ -31,6 +31,12 @@ async function bootstrap(namespace: string) { const app = createApp(App); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue index 756fa0fd..79dbda1a 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue @@ -31,7 +31,7 @@ const props = withDefaults(defineProps(), { }); // const startTime = ref(0); const showSpinner = ref(false); -const renderSpinner = ref(true); +const renderSpinner = ref(false); const timer = ref>(); watch( @@ -69,7 +69,7 @@ function onTransitionEnd() {
- - - + + + + +
{{ text }}
+
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue index 58eab2eb..cde39014 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue @@ -25,7 +25,7 @@ const props = withDefaults(defineProps(), { }); // const startTime = ref(0); const showSpinner = ref(false); -const renderSpinner = ref(true); +const renderSpinner = ref(false); const timer = ref>(); watch( @@ -74,6 +74,7 @@ function onTransitionEnd() { >
diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts index 3fed8ee8..9c883077 100644 --- a/packages/effects/common-ui/src/components/index.ts +++ b/packages/effects/common-ui/src/components/index.ts @@ -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'; diff --git a/packages/effects/common-ui/src/components/loading/directive.ts b/packages/effects/common-ui/src/components/loading/directive.ts new file mode 100644 index 00000000..973a605d --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/directive.ts @@ -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, + ); + } +} diff --git a/packages/effects/common-ui/src/components/loading/index.ts b/packages/effects/common-ui/src/components/loading/index.ts new file mode 100644 index 00000000..2fbfb047 --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/index.ts @@ -0,0 +1,3 @@ +export * from './directive'; +export { default as Loading } from './loading.vue'; +export { default as Spinner } from './spinner.vue'; diff --git a/packages/effects/common-ui/src/components/loading/loading.vue b/packages/effects/common-ui/src/components/loading/loading.vue new file mode 100644 index 00000000..6c19a12f --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/loading.vue @@ -0,0 +1,19 @@ + + diff --git a/packages/effects/common-ui/src/components/loading/spinner.vue b/packages/effects/common-ui/src/components/loading/spinner.vue new file mode 100644 index 00000000..e4a22cd7 --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/spinner.vue @@ -0,0 +1,14 @@ + + diff --git a/playground/src/bootstrap.ts b/playground/src/bootstrap.ts index bfa6e4cb..cecb3cf6 100644 --- a/playground/src/bootstrap.ts +++ b/playground/src/bootstrap.ts @@ -1,7 +1,7 @@ import { createApp, watchEffect } from 'vue'; import { registerAccessDirective } from '@vben/access'; -import { initTippy } from '@vben/common-ui'; +import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { MotionPlugin } from '@vben/plugins/motion'; import { preferences } from '@vben/preferences'; import { initStores } from '@vben/stores'; @@ -32,6 +32,12 @@ async function bootstrap(namespace: string) { const app = createApp(App); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); + // 国际化 i18n 配置 await setupI18n(app); diff --git a/playground/src/router/routes/modules/examples.ts b/playground/src/router/routes/modules/examples.ts index e131bf8b..e91c31ad 100644 --- a/playground/src/router/routes/modules/examples.ts +++ b/playground/src/router/routes/modules/examples.ts @@ -290,6 +290,15 @@ const routes: RouteRecordRaw[] = [ title: 'CountTo', }, }, + { + name: 'Loading', + path: '/examples/loading', + component: () => import('#/views/examples/loading/index.vue'), + meta: { + icon: 'mdi:circle-double', + title: 'Loading', + }, + }, ], }, ]; diff --git a/playground/src/views/examples/loading/index.vue b/playground/src/views/examples/loading/index.vue new file mode 100644 index 00000000..c31f7844 --- /dev/null +++ b/playground/src/views/examples/loading/index.vue @@ -0,0 +1,101 @@ + +