Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -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,
|
||||
|
@@ -193,7 +193,7 @@ const fieldProps = computed(() => {
|
||||
const rules = fieldRules.value;
|
||||
return {
|
||||
keepValue: true,
|
||||
label,
|
||||
label: isString(label) ? label : '',
|
||||
...(rules ? { rules } : {}),
|
||||
...(formFieldProps as Record<string, any>),
|
||||
};
|
||||
@@ -285,7 +285,7 @@ function autofocus() {
|
||||
'pb-6': !compact,
|
||||
'pb-2': compact,
|
||||
}"
|
||||
class="flex"
|
||||
class="relative flex"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<FormLabel
|
||||
@@ -301,59 +301,64 @@ function autofocus() {
|
||||
)
|
||||
"
|
||||
:help="help"
|
||||
:colon="colon"
|
||||
:label="label"
|
||||
:required="shouldRequired && !hideRequiredMark"
|
||||
:style="labelStyle"
|
||||
>
|
||||
<template v-if="label">
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="colon" class="ml-[2px]">:</span>
|
||||
<VbenRenderContent :content="label" />
|
||||
</template>
|
||||
</FormLabel>
|
||||
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
||||
<FormControl :class="cn(controlClass)">
|
||||
<slot
|
||||
v-bind="{
|
||||
...slotProps,
|
||||
...createComponentProps(slotProps),
|
||||
disabled: shouldDisabled,
|
||||
isInValid,
|
||||
}"
|
||||
>
|
||||
<component
|
||||
:is="FieldComponent"
|
||||
ref="fieldComponentRef"
|
||||
:class="{
|
||||
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
|
||||
<div class="w-full overflow-hidden">
|
||||
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
||||
<div class="flex-auto overflow-hidden p-[2px]">
|
||||
<FormControl :class="cn(controlClass)">
|
||||
<slot
|
||||
v-bind="{
|
||||
...slotProps,
|
||||
...createComponentProps(slotProps),
|
||||
disabled: shouldDisabled,
|
||||
isInValid,
|
||||
}"
|
||||
v-bind="createComponentProps(slotProps)"
|
||||
:disabled="shouldDisabled"
|
||||
>
|
||||
<template
|
||||
v-for="name in renderContentKey"
|
||||
:key="name"
|
||||
#[name]="renderSlotProps"
|
||||
}"
|
||||
>
|
||||
<VbenRenderContent
|
||||
:content="customContentRender[name]"
|
||||
v-bind="{ ...renderSlotProps, formContext: slotProps }"
|
||||
/>
|
||||
</template>
|
||||
<!-- <slot></slot> -->
|
||||
</component>
|
||||
</slot>
|
||||
</FormControl>
|
||||
<!-- 自定义后缀 -->
|
||||
<div v-if="suffix" class="ml-1">
|
||||
<VbenRenderContent :content="suffix" />
|
||||
<component
|
||||
:is="FieldComponent"
|
||||
ref="fieldComponentRef"
|
||||
:class="{
|
||||
'border-destructive focus:border-destructive hover:border-destructive/80 focus:shadow-[0_0_0_2px_rgba(255,38,5,0.06)]':
|
||||
isInValid,
|
||||
}"
|
||||
v-bind="createComponentProps(slotProps)"
|
||||
:disabled="shouldDisabled"
|
||||
>
|
||||
<template
|
||||
v-for="name in renderContentKey"
|
||||
:key="name"
|
||||
#[name]="renderSlotProps"
|
||||
>
|
||||
<VbenRenderContent
|
||||
:content="customContentRender[name]"
|
||||
v-bind="{ ...renderSlotProps, formContext: slotProps }"
|
||||
/>
|
||||
</template>
|
||||
<!-- <slot></slot> -->
|
||||
</component>
|
||||
</slot>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<!-- 自定义后缀 -->
|
||||
<div v-if="suffix" class="ml-1">
|
||||
<VbenRenderContent :content="suffix" />
|
||||
</div>
|
||||
<FormDescription v-if="description" class="ml-1">
|
||||
<VbenRenderContent :content="description" />
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormDescription v-if="description">
|
||||
<VbenRenderContent :content="description" />
|
||||
</FormDescription>
|
||||
|
||||
<Transition name="slide-up">
|
||||
<FormMessage class="absolute -bottom-[22px]" />
|
||||
<FormMessage class="absolute bottom-1" />
|
||||
</Transition>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
@@ -1,10 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';
|
||||
import type { CustomRenderType } from '../types';
|
||||
|
||||
import {
|
||||
FormLabel,
|
||||
VbenHelpTooltip,
|
||||
VbenRenderContent,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
help?: string;
|
||||
colon?: boolean;
|
||||
help?: CustomRenderType;
|
||||
label?: CustomRenderType;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
@@ -20,6 +28,8 @@ const props = defineProps<Props>();
|
||||
<span class="whitespace-pre-line">
|
||||
{{ help }}
|
||||
</span>
|
||||
<VbenRenderContent :content="help" />
|
||||
</VbenHelpTooltip>
|
||||
<span v-if="colon && label" class="ml-[2px]">:</span>
|
||||
</FormLabel>
|
||||
</template>
|
||||
|
@@ -244,13 +244,13 @@ export interface FormSchema<
|
||||
/** 依赖 */
|
||||
dependencies?: FormItemDependencies;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
description?: CustomRenderType;
|
||||
/** 字段名 */
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: string;
|
||||
help?: CustomRenderType;
|
||||
/** 表单项 */
|
||||
label?: string;
|
||||
label?: CustomRenderType;
|
||||
// 自定义组件内部渲染
|
||||
renderComponentContent?: RenderComponentContentType;
|
||||
/** 字段规则 */
|
||||
|
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
defineOptions({ name: 'VbenButtonGroup' });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
border?: boolean;
|
||||
gap?: number;
|
||||
size?: 'large' | 'middle' | 'small';
|
||||
}>(),
|
||||
{ border: false, gap: 0, size: 'middle' },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'vben-button-group rounded-md',
|
||||
`size-${size}`,
|
||||
gap ? 'with-gap' : 'no-gap',
|
||||
$attrs.class as string,
|
||||
)
|
||||
"
|
||||
:style="{ gap: gap ? `${gap}px` : '0px' }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vben-button-group {
|
||||
display: inline-flex;
|
||||
|
||||
&.size-large :deep(button) {
|
||||
height: 2.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.4rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.size-middle :deep(button) {
|
||||
height: 2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.2rem;
|
||||
|
||||
svg {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.size-small :deep(button) {
|
||||
height: 1.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.65rem;
|
||||
line-height: 0.75rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.1rem;
|
||||
|
||||
svg {
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-gap > :deep(button):nth-of-type(1) {
|
||||
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
|
||||
}
|
||||
|
||||
&.no-gap > :deep(button):last-of-type {
|
||||
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
|
||||
}
|
||||
|
||||
&.no-gap {
|
||||
:deep(button + button) {
|
||||
border-left-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -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<VbenButtonProps, 'disabled'> {
|
||||
beforeChange?: (
|
||||
value: ValueType,
|
||||
isChecked: boolean,
|
||||
) => boolean | PromiseLike<boolean | undefined> | undefined;
|
||||
btnClass?: any;
|
||||
gap?: number;
|
||||
multiple?: boolean;
|
||||
options?: { label: CustomRenderType; value: ValueType }[];
|
||||
showIcon?: boolean;
|
||||
size?: 'large' | 'middle' | 'small';
|
||||
}
|
||||
|
@@ -0,0 +1,163 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Arrayable } from '@vueuse/core';
|
||||
|
||||
import type { ValueType, VbenButtonGroupProps } from './button';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
|
||||
import { VbenRenderContent } from '@vben-core/shadcn-ui';
|
||||
import { cn, isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
import { objectOmit } from '@vueuse/core';
|
||||
|
||||
import VbenButtonGroup from './button-group.vue';
|
||||
import Button from './button.vue';
|
||||
|
||||
const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
|
||||
gap: 0,
|
||||
multiple: false,
|
||||
showIcon: true,
|
||||
size: 'middle',
|
||||
});
|
||||
|
||||
const btnDefaultProps = computed(() => {
|
||||
return {
|
||||
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
|
||||
class: cn(props.btnClass),
|
||||
};
|
||||
});
|
||||
const modelValue = defineModel<Arrayable<ValueType> | undefined>();
|
||||
|
||||
const innerValue = ref<Array<ValueType>>([]);
|
||||
const loadingValues = ref<Array<ValueType>>([]);
|
||||
watch(
|
||||
() => props.multiple,
|
||||
(val) => {
|
||||
if (val) {
|
||||
modelValue.value = innerValue.value;
|
||||
} else {
|
||||
modelValue.value =
|
||||
innerValue.value.length > 0 ? innerValue.value[0] : undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
const arrVal = val.filter((v) => v !== undefined);
|
||||
if (arrVal.length > 0) {
|
||||
innerValue.value = props.multiple
|
||||
? [...arrVal]
|
||||
: [arrVal[0] as ValueType];
|
||||
} else {
|
||||
innerValue.value = [];
|
||||
}
|
||||
} else {
|
||||
innerValue.value = val === undefined ? [] : [val as ValueType];
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
async function onBtnClick(value: ValueType) {
|
||||
if (props.beforeChange && isFunction(props.beforeChange)) {
|
||||
try {
|
||||
loadingValues.value.push(value);
|
||||
const canChange = await props.beforeChange(
|
||||
value,
|
||||
!innerValue.value.includes(value),
|
||||
);
|
||||
if (canChange === false) {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
loadingValues.value.splice(loadingValues.value.indexOf(value), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.multiple) {
|
||||
if (innerValue.value.includes(value)) {
|
||||
innerValue.value = innerValue.value.filter((item) => item !== value);
|
||||
} else {
|
||||
innerValue.value.push(value);
|
||||
}
|
||||
modelValue.value = innerValue.value;
|
||||
} else {
|
||||
innerValue.value = [value];
|
||||
modelValue.value = value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VbenButtonGroup
|
||||
:size="props.size"
|
||||
:gap="props.gap"
|
||||
class="vben-check-button-group"
|
||||
>
|
||||
<Button
|
||||
v-for="(btn, index) in props.options"
|
||||
:key="index"
|
||||
:class="cn('border', props.btnClass)"
|
||||
:disabled="
|
||||
props.disabled ||
|
||||
loadingValues.includes(btn.value) ||
|
||||
(!props.multiple && loadingValues.length > 0)
|
||||
"
|
||||
v-bind="btnDefaultProps"
|
||||
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
<div class="icon-wrapper" v-if="props.showIcon">
|
||||
<LoaderCircle
|
||||
class="animate-spin"
|
||||
v-if="loadingValues.includes(btn.value)"
|
||||
/>
|
||||
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
|
||||
<Circle v-else />
|
||||
</div>
|
||||
<slot name="option" :label="btn.label" :value="btn.value">
|
||||
<VbenRenderContent :content="btn.label" />
|
||||
</slot>
|
||||
</Button>
|
||||
</VbenButtonGroup>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.vben-check-button-group {
|
||||
&:deep(.size-large) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.3rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:deep(.size-middle) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.2rem;
|
||||
|
||||
svg {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:deep(.size-small) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.1rem;
|
||||
|
||||
svg {
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -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';
|
||||
|
@@ -37,7 +37,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
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] }
|
||||
: {}),
|
||||
|
@@ -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>
|
||||
<div v-if="text" class="text-primary 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>
|
||||
|
@@ -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;
|
||||
});
|
||||
|
||||
|
@@ -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,
|
||||
|
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,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenLoading } from '@vben-core/shadcn-ui';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
interface LoadingProps {
|
||||
class?: string;
|
||||
/**
|
||||
* @zh_CN 最小加载时间
|
||||
* @en_US Minimum loading time
|
||||
*/
|
||||
minLoadingTime?: number;
|
||||
|
||||
/**
|
||||
* @zh_CN loading状态开启
|
||||
*/
|
||||
spinning?: boolean;
|
||||
/**
|
||||
* @zh_CN 文字
|
||||
*/
|
||||
text?: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'Loading' });
|
||||
const props = defineProps<LoadingProps>();
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cn('relative min-h-20', props.class)">
|
||||
<slot></slot>
|
||||
<VbenLoading
|
||||
:min-loading-time="props.minLoadingTime"
|
||||
:spinning="props.spinning"
|
||||
:text="props.text"
|
||||
>
|
||||
<template v-if="$slots.icon" #icon>
|
||||
<slot name="icon"></slot>
|
||||
</template>
|
||||
</VbenLoading>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenSpinner } from '@vben-core/shadcn-ui';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
interface SpinnerProps {
|
||||
class?: string;
|
||||
/**
|
||||
* @zh_CN 最小加载时间
|
||||
* @en_US Minimum loading time
|
||||
*/
|
||||
minLoadingTime?: number;
|
||||
/**
|
||||
* @zh_CN loading状态开启
|
||||
*/
|
||||
spinning?: boolean;
|
||||
}
|
||||
defineOptions({ name: 'Spinner' });
|
||||
const props = defineProps<SpinnerProps>();
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cn('relative min-h-20', props.class)">
|
||||
<slot></slot>
|
||||
<VbenSpinner
|
||||
:min-loading-time="props.minLoadingTime"
|
||||
:spinning="props.spinning"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -355,7 +355,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="formOptions"
|
||||
v-show="showSearchForm !== false"
|
||||
:class="cn('relative rounded py-3', isCompactForm ? 'pb-6' : 'pb-4')"
|
||||
:class="cn('relative rounded py-3', isCompactForm ? 'pb-8' : 'pb-4')"
|
||||
>
|
||||
<slot name="form">
|
||||
<Form>
|
||||
|
@@ -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';
|
||||
|
||||
|
Reference in New Issue
Block a user