diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 95472d96..80f1ced6 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -131,7 +131,13 @@ async function initComponentAdapter() { IconPicker: (props, { attrs, slots }) => { return h( IconPicker, - { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs }, + { + iconSlot: 'addonAfter', + inputComponent: Input, + modelValueProp: 'value', + ...props, + ...attrs, + }, slots, ); }, diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index 6abc2bbf..35b9924b 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'; @@ -34,6 +34,11 @@ async function bootstrap(namespace: string) { // 全局组件 setupGlobalComponent(app); + // 注册v-loading指令 + registerLoadingDirective(app, { + loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令 + spinning: 'spinning', + }); // 国际化 i18n 配置 await setupI18n(app); diff --git a/docs/src/components/common-ui/vben-count-to-animator.md b/docs/src/components/common-ui/vben-count-to-animator.md index 301e1a41..5f3ec188 100644 --- a/docs/src/components/common-ui/vben-count-to-animator.md +++ b/docs/src/components/common-ui/vben-count-to-animator.md @@ -42,11 +42,18 @@ outline: deep | transition | 动画效果 | `string` | `linear` | | decimals | 保留小数点位数 | `number` | `0` | +### Events + +| 事件名 | 描述 | 类型 | +| -------------- | -------------- | -------------- | +| started | 动画已开始 | `()=>void` | +| finished | 动画已结束 | `()=>void` | +| ~~onStarted~~ | ~~动画已开始~~ | ~~`()=>void`~~ | +| ~~onFinished~~ | ~~动画已结束~~ | ~~`()=>void`~~ | + ### Methods -以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。 - -| 事件名 | 描述 | 类型 | +| 方法名 | 描述 | 类型 | | ------ | ------------ | ---------- | | start | 开始执行动画 | `()=>void` | | reset | 重置 | `()=>void` | diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index ecaae6f8..7ad64136 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -445,9 +445,9 @@ export interface FormSchema< /** 字段名,也作为自定义插槽的名称 */ fieldName: string; /** 帮助信息 */ - help?: string; - /** 表单项 */ - label?: string; + help?: CustomRenderType; + /** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */ + label?: CustomRenderType; /** 自定义组件内部渲染 */ renderComponentContent?: RenderComponentContentType; /** 字段规则 */ diff --git a/docs/src/guide/essentials/settings.md b/docs/src/guide/essentials/settings.md index 3669a771..a75838f6 100644 --- a/docs/src/guide/essentials/settings.md +++ b/docs/src/guide/essentials/settings.md @@ -538,4 +538,6 @@ interface Preferences { - `overridesPreferences`方法只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置。 - 任何配置项都可以覆盖,只需要在`overridesPreferences`方法内覆盖即可,不要修改默认配置文件。 -- 更改配置后请清空缓存,否则可能不生效。::: +- 更改配置后请清空缓存,否则可能不生效。 + +::: diff --git a/packages/@core/base/icons/src/lucide.ts b/packages/@core/base/icons/src/lucide.ts index 21a1beff..fe0e08fd 100644 --- a/packages/@core/base/icons/src/lucide.ts +++ b/packages/@core/base/icons/src/lucide.ts @@ -1,9 +1,7 @@ export { ArrowDown, ArrowLeft, - ArrowLeftFromLine as MdiMenuOpen, ArrowLeftToLine, - ArrowRightFromLine as MdiMenuClose, ArrowRightLeft, ArrowRightToLine, ArrowUp, @@ -16,6 +14,8 @@ export { ChevronRight, ChevronsLeft, ChevronsRight, + Circle, + CircleCheckBig, CircleHelp, Copy, CornerDownLeft, @@ -29,6 +29,7 @@ export { Github, Grip, GripVertical, + Menu as IconDefault, Info, InspectionPanel, Languages, @@ -37,7 +38,8 @@ export { LogOut, MailCheck, Maximize, - Menu as IconDefault, + ArrowRightFromLine as MdiMenuClose, + ArrowLeftFromLine as MdiMenuOpen, Menu, Minimize, Minimize2, diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue index 90019361..191c3019 100644 --- a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue +++ b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue @@ -193,7 +193,7 @@ const fieldProps = computed(() => { const rules = fieldRules.value; return { keepValue: true, - label, + label: isString(label) ? label : '', ...(rules ? { rules } : {}), ...(formFieldProps as Record), }; @@ -285,7 +285,7 @@ function autofocus() { 'pb-6': !compact, 'pb-2': compact, }" - class="flex" + class="relative flex" v-bind="$attrs" > -
- - - +
+
+ + - - - - - - -
- + + + + + + +
+ + +
+ +
+ + +
- - - - - +
diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue index f4876b52..7ab3538a 100644 --- a/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue +++ b/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue @@ -1,10 +1,18 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts b/packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts index 967e012c..f91e9d05 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts +++ b/packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts @@ -1,4 +1,5 @@ import type { AsTag } from 'radix-vue'; + import type { Component } from 'vue'; import type { ButtonVariants, ButtonVariantSize } from '../../ui'; @@ -21,3 +22,21 @@ export interface VbenButtonProps { size?: ButtonVariantSize; variant?: ButtonVariants; } + +export type CustomRenderType = (() => Component | string) | string; + +export type ValueType = boolean | number | string; + +export interface VbenButtonGroupProps + extends Pick { + beforeChange?: ( + value: ValueType, + isChecked: boolean, + ) => boolean | PromiseLike | undefined; + btnClass?: any; + gap?: number; + multiple?: boolean; + options?: { label: CustomRenderType; value: ValueType }[]; + showIcon?: boolean; + size?: 'large' | 'middle' | 'small'; +} diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue b/packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue new file mode 100644 index 00000000..a7477f07 --- /dev/null +++ b/packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue @@ -0,0 +1,163 @@ + + + diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts index 4afd1c25..aa3d9ef8 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts +++ b/packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts @@ -1,3 +1,5 @@ export type * from './button'; +export { default as VbenButtonGroup } from './button-group.vue'; export { default as VbenButton } from './button.vue'; +export { default as VbenCheckButtonGroup } from './check-button-group.vue'; export { default as VbenIconButton } from './icon-button.vue'; diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue b/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue index f252049c..50fece66 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue @@ -37,7 +37,18 @@ const props = withDefaults(defineProps(), { useEasing: true, }); -const emit = defineEmits(['onStarted', 'onFinished']); +const emit = defineEmits<{ + finished: []; + /** + * @deprecated 请使用{@link finished}事件 + */ + onFinished: []; + /** + * @deprecated 请使用{@link started}事件 + */ + onStarted: []; + started: []; +}>(); const source = ref(props.startVal); const disabled = ref(false); @@ -73,8 +84,14 @@ function run() { outputValue = useTransition(source, { disabled, duration: props.duration, - onFinished: () => emit('onFinished'), - onStarted: () => emit('onStarted'), + onFinished: () => { + emit('finished'); + emit('onFinished'); + }, + onStarted: () => { + emit('started'); + emit('onStarted'); + }, ...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}), 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..0e1aa810 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 }}
+
{{ 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/count-to/count-to.vue b/packages/effects/common-ui/src/components/count-to/count-to.vue index 0d73e3a9..d6b0943d 100644 --- a/packages/effects/common-ui/src/components/count-to/count-to.vue +++ b/packages/effects/common-ui/src/components/count-to/count-to.vue @@ -53,7 +53,7 @@ const numMain = computed(() => { const result = currentValue.value .toFixed(props.decimals) .split('.')[0] - ?.replaceAll(/\B(?=(\d{3})+(?!\d))/g, ','); + ?.replaceAll(/\B(?=(\d{3})+(?!\d))/g, props.separator); return result; }); diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts index ca22c32a..dc8f0f63 100644 --- a/packages/effects/common-ui/src/components/index.ts +++ b/packages/effects/common-ui/src/components/index.ts @@ -7,6 +7,7 @@ export * from './ellipsis-text'; export * from './icon-picker'; export * from './json-preview'; export * from './json-viewer'; +export * from './loading'; export * from './markdown'; export * from './page'; export * from './resize'; @@ -17,6 +18,8 @@ export * from '@vben-core/popup-ui'; // 给文档用 export { VbenButton, + VbenButtonGroup, + VbenCheckButtonGroup, VbenCountToAnimator, VbenInputPassword, VbenLoading, 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..faf49e60 --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/loading.vue @@ -0,0 +1,39 @@ + + 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..caee2bbc --- /dev/null +++ b/packages/effects/common-ui/src/components/loading/spinner.vue @@ -0,0 +1,28 @@ + + diff --git a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue index b96b31de..1d73f717 100644 --- a/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue +++ b/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue @@ -355,7 +355,7 @@ onUnmounted(() => {
diff --git a/packages/utils/src/helpers/generate-routes-backend.ts b/packages/utils/src/helpers/generate-routes-backend.ts index f64a69ab..e5d87809 100644 --- a/packages/utils/src/helpers/generate-routes-backend.ts +++ b/packages/utils/src/helpers/generate-routes-backend.ts @@ -1,9 +1,10 @@ +import type { RouteRecordRaw } from 'vue-router'; + import type { ComponentRecordType, GenerateMenuAndRoutesOptions, RouteRecordStringComponent, } from '@vben-core/typings'; -import type { RouteRecordRaw } from 'vue-router'; import { mapTree } from '@vben-core/shared/utils'; diff --git a/playground/src/adapter/component/index.ts b/playground/src/adapter/component/index.ts index ebe59934..607130b3 100644 --- a/playground/src/adapter/component/index.ts +++ b/playground/src/adapter/component/index.ts @@ -3,11 +3,15 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { BaseFormComponentType } from '@vben/common-ui'; import type { Component, SetupContext } from 'vue'; +import type { BaseFormComponentType } from '@vben/common-ui'; + +import { h } from 'vue'; + import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; + import { AutoComplete, Button, @@ -32,7 +36,6 @@ import { TreeSelect, Upload, } from 'ant-design-vue'; -import { h } from 'vue'; const withDefaultPlaceholder = ( component: T, @@ -123,7 +126,13 @@ async function initComponentAdapter() { IconPicker: (props, { attrs, slots }) => { return h( IconPicker, - { iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs }, + { + iconSlot: 'addonAfter', + inputComponent: Input, + modelValueProp: 'value', + ...props, + ...attrs, + }, slots, ); }, 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/locales/langs/en-US/examples.json b/playground/src/locales/langs/en-US/examples.json index 8131380f..1a25a983 100644 --- a/playground/src/locales/langs/en-US/examples.json +++ b/playground/src/locales/langs/en-US/examples.json @@ -12,6 +12,7 @@ "form": { "title": "Form", "basic": "Basic Form", + "layout": "Custom Layout", "query": "Query Form", "rules": "Form Rules", "dynamic": "Dynamic Form", @@ -62,5 +63,8 @@ }, "layout": { "col-page": "ColPage Layout" + }, + "button-group": { + "title": "Button Group" } } diff --git a/playground/src/locales/langs/zh-CN/examples.json b/playground/src/locales/langs/zh-CN/examples.json index fea65a70..8f15d020 100644 --- a/playground/src/locales/langs/zh-CN/examples.json +++ b/playground/src/locales/langs/zh-CN/examples.json @@ -15,6 +15,7 @@ "form": { "title": "表单", "basic": "基础表单", + "layout": "自定义布局", "query": "查询表单", "rules": "表单校验", "dynamic": "动态表单", @@ -62,5 +63,8 @@ }, "layout": { "col-page": "双列布局" + }, + "button-group": { + "title": "按钮组" } } diff --git a/playground/src/router/routes/modules/examples.ts b/playground/src/router/routes/modules/examples.ts index cc86838e..1e2461c5 100644 --- a/playground/src/router/routes/modules/examples.ts +++ b/playground/src/router/routes/modules/examples.ts @@ -53,6 +53,14 @@ const routes: RouteRecordRaw[] = [ title: $t('examples.form.dynamic'), }, }, + { + name: 'FormLayoutExample', + path: '/examples/form/custom-layout', + component: () => import('#/views/examples/form/custom-layout.vue'), + meta: { + title: $t('examples.form.layout'), + }, + }, { name: 'FormCustomExample', path: '/examples/form/custom', @@ -282,6 +290,24 @@ const routes: RouteRecordRaw[] = [ title: 'CountTo', }, }, + { + name: 'Loading', + path: '/examples/loading', + component: () => import('#/views/examples/loading/index.vue'), + meta: { + icon: 'mdi:circle-double', + title: 'Loading', + }, + }, + { + name: 'ButtonGroup', + path: '/examples/button-group', + component: () => import('#/views/examples/button-group/index.vue'), + meta: { + icon: 'mdi:check-circle', + title: $t('examples.button-group.title'), + }, + }, ], }, ]; diff --git a/playground/src/views/examples/button-group/index.vue b/playground/src/views/examples/button-group/index.vue new file mode 100644 index 00000000..47388963 --- /dev/null +++ b/playground/src/views/examples/button-group/index.vue @@ -0,0 +1,194 @@ + + diff --git a/playground/src/views/examples/form/basic.vue b/playground/src/views/examples/form/basic.vue index cde1a6d5..5d734d43 100644 --- a/playground/src/views/examples/form/basic.vue +++ b/playground/src/views/examples/form/basic.vue @@ -1,15 +1,17 @@ + + diff --git a/playground/src/views/examples/form/rules.vue b/playground/src/views/examples/form/rules.vue index dd95894c..7ec95493 100644 --- a/playground/src/views/examples/form/rules.vue +++ b/playground/src/views/examples/form/rules.vue @@ -150,7 +150,9 @@ const [Form, formApi] = useVbenForm({ default: () => ['我已阅读并同意'], }; }, - rules: 'selectRequired', + rules: z.boolean().refine((value) => value, { + message: '请勾选', + }), }, { component: 'DatePicker', 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 @@ + +