Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { Component } from 'vue';
|
||||
import type { Component, VNode } from 'vue';
|
||||
|
||||
import type { Recordable } from '@vben-core/typings';
|
||||
|
||||
import type { AlertProps, BeforeCloseScope } from './alert';
|
||||
import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
|
||||
|
||||
import { h, ref, render } from 'vue';
|
||||
import { h, nextTick, ref, render } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import { Input } from '@vben-core/shadcn-ui';
|
||||
@@ -130,40 +130,58 @@ export function vbenConfirm(
|
||||
}
|
||||
|
||||
export async function vbenPrompt<T = any>(
|
||||
options: Omit<AlertProps, 'beforeClose'> & {
|
||||
beforeClose?: (scope: {
|
||||
isConfirm: boolean;
|
||||
value: T | undefined;
|
||||
}) => boolean | Promise<boolean | undefined> | undefined;
|
||||
component?: Component;
|
||||
componentProps?: Recordable<any>;
|
||||
defaultValue?: T;
|
||||
modelPropName?: string;
|
||||
},
|
||||
options: PromptProps<T>,
|
||||
): Promise<T | undefined> {
|
||||
const {
|
||||
component: _component,
|
||||
componentProps: _componentProps,
|
||||
componentSlots,
|
||||
content,
|
||||
defaultValue,
|
||||
modelPropName: _modelPropName,
|
||||
...delegated
|
||||
} = options;
|
||||
const contents: Component[] = [];
|
||||
|
||||
const modelValue = ref<T | undefined>(defaultValue);
|
||||
const inputComponentRef = ref<null | VNode>(null);
|
||||
const staticContents: Component[] = [];
|
||||
|
||||
if (isString(content)) {
|
||||
contents.push(h('span', content));
|
||||
} else {
|
||||
contents.push(content);
|
||||
staticContents.push(h('span', content));
|
||||
} else if (content) {
|
||||
staticContents.push(content as Component);
|
||||
}
|
||||
const componentProps = _componentProps || {};
|
||||
|
||||
const modelPropName = _modelPropName || 'modelValue';
|
||||
componentProps[modelPropName] = modelValue.value;
|
||||
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
|
||||
modelValue.value = val;
|
||||
const componentProps = { ..._componentProps };
|
||||
|
||||
// 每次渲染时都会重新计算的内容函数
|
||||
const contentRenderer = () => {
|
||||
const currentProps = { ...componentProps };
|
||||
|
||||
// 设置当前值
|
||||
currentProps[modelPropName] = modelValue.value;
|
||||
|
||||
// 设置更新处理函数
|
||||
currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
|
||||
modelValue.value = val;
|
||||
};
|
||||
|
||||
// 创建输入组件
|
||||
inputComponentRef.value = h(
|
||||
_component || Input,
|
||||
currentProps,
|
||||
componentSlots,
|
||||
);
|
||||
|
||||
// 返回包含静态内容和输入组件的数组
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'flex flex-col gap-2' },
|
||||
{ default: () => [...staticContents, inputComponentRef.value] },
|
||||
);
|
||||
};
|
||||
const componentRef = h(_component || Input, componentProps);
|
||||
contents.push(componentRef);
|
||||
|
||||
const props: AlertProps & Recordable<any> = {
|
||||
...delegated,
|
||||
async beforeClose(scope: BeforeCloseScope) {
|
||||
@@ -174,23 +192,46 @@ export async function vbenPrompt<T = any>(
|
||||
});
|
||||
}
|
||||
},
|
||||
content: h(
|
||||
'div',
|
||||
{ class: 'flex flex-col gap-2' },
|
||||
{ default: () => contents },
|
||||
),
|
||||
onOpened() {
|
||||
// 组件挂载完成后,自动聚焦到输入组件
|
||||
if (
|
||||
componentRef.component?.exposed &&
|
||||
isFunction(componentRef.component.exposed.focus)
|
||||
) {
|
||||
componentRef.component.exposed.focus();
|
||||
} else if (componentRef.el && isFunction(componentRef.el.focus)) {
|
||||
componentRef.el.focus();
|
||||
// 使用函数形式,每次渲染都会重新计算内容
|
||||
content: contentRenderer,
|
||||
contentMasking: true,
|
||||
async onOpened() {
|
||||
await nextTick();
|
||||
const componentRef: null | VNode = inputComponentRef.value;
|
||||
if (componentRef) {
|
||||
if (
|
||||
componentRef.component?.exposed &&
|
||||
isFunction(componentRef.component.exposed.focus)
|
||||
) {
|
||||
componentRef.component.exposed.focus();
|
||||
} else {
|
||||
if (componentRef.el) {
|
||||
if (
|
||||
isFunction(componentRef.el.focus) &&
|
||||
['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(
|
||||
componentRef.el.tagName,
|
||||
)
|
||||
) {
|
||||
componentRef.el.focus();
|
||||
} else if (isFunction(componentRef.el.querySelector)) {
|
||||
const focusableElement = componentRef.el.querySelector(
|
||||
'input, select, textarea, button',
|
||||
);
|
||||
if (focusableElement && isFunction(focusableElement.focus)) {
|
||||
focusableElement.focus();
|
||||
}
|
||||
} else if (
|
||||
componentRef.el.nextElementSibling &&
|
||||
isFunction(componentRef.el.nextElementSibling.focus)
|
||||
) {
|
||||
componentRef.el.nextElementSibling.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await vbenConfirm(props);
|
||||
return modelValue.value;
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import type { Component } from 'vue';
|
||||
import type { Component, VNode, VNodeArrayChildren } from 'vue';
|
||||
|
||||
import type { Recordable } from '@vben-core/typings';
|
||||
|
||||
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
||||
|
||||
@@ -13,6 +15,11 @@ export type AlertProps = {
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
/** 边框 */
|
||||
bordered?: boolean;
|
||||
/**
|
||||
* 按钮对齐方式
|
||||
* @default 'end'
|
||||
*/
|
||||
buttonAlign?: 'center' | 'end' | 'start';
|
||||
/** 取消按钮的标题 */
|
||||
cancelText?: string;
|
||||
/** 是否居中显示 */
|
||||
@@ -25,6 +32,8 @@ export type AlertProps = {
|
||||
content: Component | string;
|
||||
/** 弹窗内容的额外样式 */
|
||||
contentClass?: string;
|
||||
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
||||
contentMasking?: boolean;
|
||||
/** 弹窗的图标(在标题的前面) */
|
||||
icon?: Component | IconType;
|
||||
/** 是否显示取消按钮 */
|
||||
@@ -32,3 +41,26 @@ export type AlertProps = {
|
||||
/** 弹窗标题 */
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/** Prompt属性 */
|
||||
export type PromptProps<T = any> = {
|
||||
/** 关闭前的回调,如果返回false,则终止关闭 */
|
||||
beforeClose?: (scope: {
|
||||
isConfirm: boolean;
|
||||
value: T | undefined;
|
||||
}) => boolean | Promise<boolean | undefined> | undefined;
|
||||
/** 用于接受用户输入的组件 */
|
||||
component?: Component;
|
||||
/** 输入组件的属性 */
|
||||
componentProps?: Recordable<any>;
|
||||
/** 输入组件的插槽 */
|
||||
componentSlots?:
|
||||
| (() => any)
|
||||
| Recordable<unknown>
|
||||
| VNode
|
||||
| VNodeArrayChildren;
|
||||
/** 默认值 */
|
||||
defaultValue?: T;
|
||||
/** 输入组件的值属性名 */
|
||||
modelPropName?: string;
|
||||
} & Omit<AlertProps, 'beforeClose'>;
|
||||
|
@@ -30,6 +30,7 @@ import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = withDefaults(defineProps<AlertProps>(), {
|
||||
bordered: true,
|
||||
buttonAlign: 'end',
|
||||
centered: true,
|
||||
containerClass: 'w-[520px]',
|
||||
});
|
||||
@@ -154,9 +155,9 @@ async function handleOpenChange(val: boolean) {
|
||||
<div class="m-4 mb-6 min-h-[30px]">
|
||||
<VbenRenderContent :content="content" render-br />
|
||||
</div>
|
||||
<VbenLoading v-if="loading" :spinning="loading" />
|
||||
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
|
||||
</AlertDialogDescription>
|
||||
<div class="flex justify-end gap-x-2">
|
||||
<div class="flex justify-end gap-x-2" :class="`justify-${buttonAlign}`">
|
||||
<AlertDialogCancel v-if="showCancel" :disabled="loading">
|
||||
<component
|
||||
:is="components.DefaultButton || VbenButton"
|
||||
|
@@ -6,11 +6,11 @@ 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 { VbenRenderContent } from '../render-content';
|
||||
import VbenButtonGroup from './button-group.vue';
|
||||
import Button from './button.vue';
|
||||
|
||||
|
@@ -17,6 +17,14 @@
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./es/tippy": {
|
||||
"types": "./src/components/tippy/index.ts",
|
||||
"default": "./src/components/tippy/index.ts"
|
||||
},
|
||||
"./es/loading": {
|
||||
"types": "./src/components/loading/index.ts",
|
||||
"default": "./src/components/loading/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
@@ -5,9 +5,11 @@ import type {
|
||||
RouteLocationNormalizedLoadedGeneric,
|
||||
} from 'vue-router';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { storeToRefs, useTabbarStore } from '@vben/stores';
|
||||
import { RouterView } from 'vue-router';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
|
||||
@@ -19,6 +21,15 @@ const { keepAlive } = usePreferences();
|
||||
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
||||
storeToRefs(tabbarStore);
|
||||
|
||||
/**
|
||||
* 是否使用动画
|
||||
*/
|
||||
const getEnabledTransition = computed(() => {
|
||||
const { transition } = preferences;
|
||||
const transitionName = transition.name;
|
||||
return transitionName && transition.enable;
|
||||
});
|
||||
|
||||
// 页面切换动画
|
||||
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
|
||||
// 如果偏好设置未设置,则不使用动画
|
||||
@@ -89,7 +100,12 @@ function transformComponent(
|
||||
<div class="relative h-full">
|
||||
<IFrameRouterView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
||||
<Transition
|
||||
v-if="getEnabledTransition"
|
||||
:name="getTransitionName(route)"
|
||||
appear
|
||||
mode="out-in"
|
||||
>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:exclude="getExcludeCachedTabs"
|
||||
@@ -108,6 +124,25 @@ function transformComponent(
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</Transition>
|
||||
<template v-else>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:exclude="getExcludeCachedTabs"
|
||||
:include="getCachedTabs"
|
||||
>
|
||||
<component
|
||||
:is="transformComponent(Component, route)"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else-if="renderRouteView"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,6 +1,3 @@
|
||||
import type { ClassType, DeepPartial } from '@vben/types';
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
import type { Ref } from 'vue';
|
||||
import type {
|
||||
VxeGridListeners,
|
||||
VxeGridPropTypes,
|
||||
@@ -8,6 +5,12 @@ import type {
|
||||
VxeUIExport,
|
||||
} from 'vxe-table';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { ClassType, DeepPartial } from '@vben/types';
|
||||
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
|
||||
import type { VxeGridApi } from './api';
|
||||
|
||||
import { useVbenForm } from '@vben-core/form-ui';
|
||||
@@ -28,6 +31,10 @@ export interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {
|
||||
toolbarConfig?: ToolbarConfigOptions;
|
||||
}
|
||||
|
||||
export interface SeparatorOptions {
|
||||
show?: boolean;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
export interface VxeGridProps {
|
||||
/**
|
||||
* 标题
|
||||
@@ -61,13 +68,17 @@ export interface VxeGridProps {
|
||||
* 显示搜索表单
|
||||
*/
|
||||
showSearchForm?: boolean;
|
||||
/**
|
||||
* 搜索表单与表格主体之间的分隔条
|
||||
*/
|
||||
separator?: boolean | SeparatorOptions;
|
||||
}
|
||||
|
||||
export type ExtendedVxeGridApi = {
|
||||
export type ExtendedVxeGridApi = VxeGridApi & {
|
||||
useStore: <T = NoInfer<VxeGridProps>>(
|
||||
selector?: (state: NoInfer<VxeGridProps>) => T,
|
||||
) => Readonly<Ref<T>>;
|
||||
} & VxeGridApi;
|
||||
};
|
||||
|
||||
export interface SetupVxeTable {
|
||||
configVxeTable: (ui: VxeUIExport) => void;
|
||||
|
@@ -29,7 +29,13 @@ import { usePriorityValues } from '@vben/hooks';
|
||||
import { EmptyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
import { cloneDeep, cn, isEqual, mergeWithArrayOverride } from '@vben/utils';
|
||||
import {
|
||||
cloneDeep,
|
||||
cn,
|
||||
isBoolean,
|
||||
isEqual,
|
||||
mergeWithArrayOverride,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
|
||||
|
||||
@@ -67,10 +73,30 @@ const {
|
||||
tableTitle,
|
||||
tableTitleHelp,
|
||||
showSearchForm,
|
||||
separator,
|
||||
} = usePriorityValues(props, state);
|
||||
|
||||
const { isMobile } = usePreferences();
|
||||
|
||||
const isSeparator = computed(() => {
|
||||
if (
|
||||
!formOptions.value ||
|
||||
showSearchForm.value === false ||
|
||||
separator.value === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (separator.value === true || separator.value === undefined) {
|
||||
return true;
|
||||
}
|
||||
return separator.value.show !== false;
|
||||
});
|
||||
const separatorBg = computed(() => {
|
||||
return !separator.value ||
|
||||
isBoolean(separator.value) ||
|
||||
!separator.value.backgroundColor
|
||||
? undefined
|
||||
: separator.value.backgroundColor;
|
||||
});
|
||||
const slots: SetupContext['slots'] = useSlots();
|
||||
|
||||
const [Form, formApi] = useTableForm({
|
||||
@@ -380,7 +406,18 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="formOptions"
|
||||
v-show="showSearchForm !== false"
|
||||
:class="cn('relative rounded py-3', isCompactForm ? 'pb-8' : 'pb-4')"
|
||||
:class="
|
||||
cn(
|
||||
'relative rounded py-3',
|
||||
isCompactForm
|
||||
? isSeparator
|
||||
? 'pb-8'
|
||||
: 'pb-4'
|
||||
: isSeparator
|
||||
? 'pb-4'
|
||||
: 'pb-0',
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot name="form">
|
||||
<Form>
|
||||
@@ -409,6 +446,10 @@ onUnmounted(() => {
|
||||
</Form>
|
||||
</slot>
|
||||
<div
|
||||
v-if="isSeparator"
|
||||
:style="{
|
||||
...(separatorBg ? { backgroundColor: separatorBg } : undefined),
|
||||
}"
|
||||
class="bg-background-deep z-100 absolute -left-2 bottom-1 h-2 w-[calc(100%+1rem)] overflow-hidden md:bottom-2 md:h-3"
|
||||
></div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user