Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
203
packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts
Normal file
203
packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { Recordable } from '@vben-core/typings';
|
||||
|
||||
import type { AlertProps } from './alert';
|
||||
|
||||
import { h, ref, render } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import { Input } from '@vben-core/shadcn-ui';
|
||||
import { isFunction, isString } from '@vben-core/shared/utils';
|
||||
|
||||
import Alert from './alert.vue';
|
||||
|
||||
const alerts = ref<Array<{ container: HTMLElement; instance: Component }>>([]);
|
||||
|
||||
const { $t } = useSimpleLocale();
|
||||
|
||||
export function vbenAlert(options: AlertProps): Promise<void>;
|
||||
export function vbenAlert(
|
||||
message: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
export function vbenAlert(
|
||||
message: string,
|
||||
title?: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
|
||||
export function vbenAlert(
|
||||
arg0: AlertProps | string,
|
||||
arg1?: Partial<AlertProps> | string,
|
||||
arg2?: Partial<AlertProps>,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: AlertProps = isString(arg0)
|
||||
? {
|
||||
content: arg0,
|
||||
}
|
||||
: { ...arg0 };
|
||||
if (arg1) {
|
||||
if (isString(arg1)) {
|
||||
options.title = arg1;
|
||||
} else if (!isString(arg1)) {
|
||||
// 如果第二个参数是对象,则合并到选项中
|
||||
Object.assign(options, arg1);
|
||||
}
|
||||
}
|
||||
|
||||
if (arg2 && !isString(arg2)) {
|
||||
Object.assign(options, arg2);
|
||||
}
|
||||
// 创建容器元素
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 创建一个引用,用于在回调中访问实例
|
||||
const alertRef = { container, instance: null as any };
|
||||
|
||||
const props: AlertProps & Recordable<any> = {
|
||||
onClosed: (isConfirm: boolean) => {
|
||||
// 移除组件实例以及创建的所有dom(恢复页面到打开前的状态)
|
||||
// 从alerts数组中移除该实例
|
||||
alerts.value = alerts.value.filter((item) => item !== alertRef);
|
||||
|
||||
// 从DOM中移除容器
|
||||
render(null, container);
|
||||
if (container.parentNode) {
|
||||
container.remove();
|
||||
}
|
||||
|
||||
// 解析 Promise,传递用户操作结果
|
||||
if (isConfirm) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('dialog cancelled'));
|
||||
}
|
||||
},
|
||||
...options,
|
||||
open: true,
|
||||
title: options.title ?? $t.value('prompt'),
|
||||
};
|
||||
|
||||
// 创建Alert组件的VNode
|
||||
const vnode = h(Alert, props);
|
||||
|
||||
// 渲染组件到容器
|
||||
render(vnode, container);
|
||||
|
||||
// 保存组件实例引用
|
||||
alertRef.instance = vnode.component?.proxy as Component;
|
||||
|
||||
// 将实例和容器添加到alerts数组中
|
||||
alerts.value.push(alertRef);
|
||||
});
|
||||
}
|
||||
|
||||
export function vbenConfirm(options: AlertProps): Promise<void>;
|
||||
export function vbenConfirm(
|
||||
message: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
export function vbenConfirm(
|
||||
message: string,
|
||||
title?: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
|
||||
export function vbenConfirm(
|
||||
arg0: AlertProps | string,
|
||||
arg1?: Partial<AlertProps> | string,
|
||||
arg2?: Partial<AlertProps>,
|
||||
): Promise<void> {
|
||||
const defaultProps: Partial<AlertProps> = {
|
||||
showCancel: true,
|
||||
};
|
||||
if (!arg1) {
|
||||
return isString(arg0)
|
||||
? vbenAlert(arg0, defaultProps)
|
||||
: vbenAlert({ ...defaultProps, ...arg0 });
|
||||
} else if (!arg2) {
|
||||
return isString(arg1)
|
||||
? vbenAlert(arg0 as string, arg1, defaultProps)
|
||||
: vbenAlert(arg0 as string, { ...defaultProps, ...arg1 });
|
||||
}
|
||||
return vbenAlert(arg0 as string, arg1 as string, {
|
||||
...defaultProps,
|
||||
...arg2,
|
||||
});
|
||||
}
|
||||
|
||||
export async function vbenPrompt<T = any>(
|
||||
options: Omit<AlertProps, 'beforeClose'> & {
|
||||
beforeClose?: (
|
||||
val: T,
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
component?: Component;
|
||||
componentProps?: Recordable<any>;
|
||||
defaultValue?: T;
|
||||
modelPropName?: string;
|
||||
},
|
||||
): Promise<T | undefined> {
|
||||
const {
|
||||
component: _component,
|
||||
componentProps: _componentProps,
|
||||
content,
|
||||
defaultValue,
|
||||
modelPropName: _modelPropName,
|
||||
...delegated
|
||||
} = options;
|
||||
const contents: Component[] = [];
|
||||
const modelValue = ref<T | undefined>(defaultValue);
|
||||
if (isString(content)) {
|
||||
contents.push(h('span', content));
|
||||
} else {
|
||||
contents.push(content);
|
||||
}
|
||||
const componentProps = _componentProps || {};
|
||||
const modelPropName = _modelPropName || 'modelValue';
|
||||
componentProps[modelPropName] = modelValue.value;
|
||||
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
|
||||
modelValue.value = val;
|
||||
};
|
||||
const componentRef = h(_component || Input, componentProps);
|
||||
contents.push(componentRef);
|
||||
const props: AlertProps & Recordable<any> = {
|
||||
...delegated,
|
||||
async beforeClose() {
|
||||
if (delegated.beforeClose) {
|
||||
return await delegated.beforeClose(modelValue.value);
|
||||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
};
|
||||
await vbenConfirm(props);
|
||||
return modelValue.value;
|
||||
}
|
||||
|
||||
export function clearAllAlerts() {
|
||||
alerts.value.forEach((alert) => {
|
||||
// 从DOM中移除容器
|
||||
render(null, alert.container);
|
||||
if (alert.container.parentNode) {
|
||||
alert.container.remove();
|
||||
}
|
||||
});
|
||||
alerts.value = [];
|
||||
}
|
28
packages/@core/ui-kit/popup-ui/src/alert/alert.ts
Normal file
28
packages/@core/ui-kit/popup-ui/src/alert/alert.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
||||
|
||||
export type AlertProps = {
|
||||
/** 关闭前的回调,如果返回false,则终止关闭 */
|
||||
beforeClose?: () => boolean | Promise<boolean | undefined> | undefined;
|
||||
/** 边框 */
|
||||
bordered?: boolean;
|
||||
/** 取消按钮的标题 */
|
||||
cancelText?: string;
|
||||
/** 是否居中显示 */
|
||||
centered?: boolean;
|
||||
/** 确认按钮的标题 */
|
||||
confirmText?: string;
|
||||
/** 弹窗容器的额外样式 */
|
||||
containerClass?: string;
|
||||
/** 弹窗提示内容 */
|
||||
content: Component | string;
|
||||
/** 弹窗内容的额外样式 */
|
||||
contentClass?: string;
|
||||
/** 弹窗的图标(在标题的前面) */
|
||||
icon?: Component | IconType;
|
||||
/** 是否显示取消按钮 */
|
||||
showCancel?: boolean;
|
||||
/** 弹窗标题 */
|
||||
title?: string;
|
||||
};
|
181
packages/@core/ui-kit/popup-ui/src/alert/alert.vue
Normal file
181
packages/@core/ui-kit/popup-ui/src/alert/alert.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { AlertProps } from './alert';
|
||||
|
||||
import { computed, h, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { useSimpleLocale } from '@vben-core/composables';
|
||||
import {
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
CircleHelp,
|
||||
CircleX,
|
||||
Info,
|
||||
X,
|
||||
} from '@vben-core/icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
VbenButton,
|
||||
VbenLoading,
|
||||
VbenRenderContent,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { globalShareState } from '@vben-core/shared/global-state';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
const props = withDefaults(defineProps<AlertProps>(), {
|
||||
bordered: true,
|
||||
centered: true,
|
||||
containerClass: 'w-[520px]',
|
||||
});
|
||||
const emits = defineEmits(['closed', 'confirm', 'opened']);
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
const { $t } = useSimpleLocale();
|
||||
const components = globalShareState.getComponents();
|
||||
const isConfirm = ref(false);
|
||||
watch(open, async (val) => {
|
||||
await nextTick();
|
||||
if (val) {
|
||||
isConfirm.value = false;
|
||||
} else {
|
||||
emits('closed', isConfirm.value);
|
||||
}
|
||||
});
|
||||
const getIconRender = computed(() => {
|
||||
let iconRender: Component | null = null;
|
||||
if (props.icon) {
|
||||
if (typeof props.icon === 'string') {
|
||||
switch (props.icon) {
|
||||
case 'error': {
|
||||
iconRender = h(CircleX, {
|
||||
style: { color: 'hsl(var(--destructive))' },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'info': {
|
||||
iconRender = h(Info, { style: { color: 'hsl(var(--info))' } });
|
||||
break;
|
||||
}
|
||||
case 'question': {
|
||||
iconRender = CircleHelp;
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
iconRender = h(CircleCheckBig, {
|
||||
style: { color: 'hsl(var(--success))' },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'warning': {
|
||||
iconRender = h(CircleAlert, {
|
||||
style: { color: 'hsl(var(--warning))' },
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
iconRender = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
iconRender = props.icon ?? null;
|
||||
}
|
||||
return iconRender;
|
||||
});
|
||||
function handleConfirm() {
|
||||
isConfirm.value = true;
|
||||
emits('confirm');
|
||||
}
|
||||
function handleCancel() {
|
||||
open.value = false;
|
||||
}
|
||||
const loading = ref(false);
|
||||
async function handleOpenChange(val: boolean) {
|
||||
if (!val && props.beforeClose) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await props.beforeClose();
|
||||
if (res !== false) {
|
||||
open.value = false;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
open.value = val;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AlertDialog :open="open" @update:open="handleOpenChange">
|
||||
<AlertDialogContent
|
||||
:open="open"
|
||||
:centered="centered"
|
||||
@opened="emits('opened')"
|
||||
:class="
|
||||
cn(
|
||||
containerClass,
|
||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
|
||||
{
|
||||
'border-border border': bordered,
|
||||
'shadow-3xl': !bordered,
|
||||
'top-1/2 !-translate-y-1/2': centered,
|
||||
},
|
||||
)
|
||||
"
|
||||
>
|
||||
<div :class="cn('relative flex-1 overflow-y-auto p-3', contentClass)">
|
||||
<AlertDialogTitle v-if="title">
|
||||
<div class="flex items-center">
|
||||
<component :is="getIconRender" class="mr-2" />
|
||||
<span class="flex-auto">{{ $t(title) }}</span>
|
||||
<AlertDialogCancel v-if="showCancel">
|
||||
<VbenButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
:disabled="loading"
|
||||
>
|
||||
<X class="text-muted-foreground size-4" />
|
||||
</VbenButton>
|
||||
</AlertDialogCancel>
|
||||
</div>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div class="m-4 mb-6 min-h-[30px]">
|
||||
<VbenRenderContent :content="content" render-br />
|
||||
</div>
|
||||
<VbenLoading v-if="loading" :spinning="loading" />
|
||||
</AlertDialogDescription>
|
||||
<div class="flex justify-end gap-x-2">
|
||||
<AlertDialogCancel
|
||||
v-if="showCancel"
|
||||
@click="handleCancel"
|
||||
:disabled="loading"
|
||||
>
|
||||
<component
|
||||
:is="components.DefaultButton || VbenButton"
|
||||
variant="ghost"
|
||||
>
|
||||
{{ cancelText || $t('cancel') }}
|
||||
</component>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleConfirm">
|
||||
<component
|
||||
:is="components.PrimaryButton || VbenButton"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ confirmText || $t('confirm') }}
|
||||
</component>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
9
packages/@core/ui-kit/popup-ui/src/alert/index.ts
Normal file
9
packages/@core/ui-kit/popup-ui/src/alert/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './alert';
|
||||
|
||||
export { default as Alert } from './alert.vue';
|
||||
export {
|
||||
vbenAlert as alert,
|
||||
clearAllAlerts,
|
||||
vbenConfirm as confirm,
|
||||
vbenPrompt as prompt,
|
||||
} from './AlertBuilder';
|
@@ -1,2 +1,3 @@
|
||||
export * from './alert';
|
||||
export * from './drawer';
|
||||
export * from './modal';
|
||||
|
Reference in New Issue
Block a user