Browse Source
* feat: add preset alert, confirm, prompt components that can be simple called * fix: type definepull/5845/head
committed by
GitHub
23 changed files with 941 additions and 4 deletions
@ -0,0 +1,101 @@ |
|||
--- |
|||
outline: deep |
|||
--- |
|||
|
|||
# Vben Alert 轻量提示框 |
|||
|
|||
框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。 |
|||
|
|||
::: info 应用场景 |
|||
|
|||
Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal |
|||
|
|||
::: |
|||
|
|||
::: tip README |
|||
|
|||
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。 |
|||
|
|||
::: |
|||
|
|||
## 基础用法 |
|||
|
|||
使用 `alert` 创建只有一个确认按钮的提示框。 |
|||
|
|||
<DemoPreview dir="demos/vben-alert/alert" /> |
|||
|
|||
使用 `confirm` 创建有确认和取消按钮的提示框。 |
|||
|
|||
<DemoPreview dir="demos/vben-alert/confirm" /> |
|||
|
|||
使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。 |
|||
|
|||
<DemoPreview dir="demos/vben-alert/prompt" /> |
|||
|
|||
## 类型说明 |
|||
|
|||
```ts |
|||
/** 预置的图标类型 */ |
|||
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; |
|||
}; |
|||
|
|||
/** |
|||
* 函数签名 |
|||
* alert和confirm的函数签名相同。 |
|||
* confirm默认会显示取消按钮,而alert默认只有一个按钮 |
|||
* */ |
|||
export function alert(options: AlertProps): Promise<void>; |
|||
export function alert( |
|||
message: string, |
|||
options?: Partial<AlertProps>, |
|||
): Promise<void>; |
|||
export function alert( |
|||
message: string, |
|||
title?: string, |
|||
options?: Partial<AlertProps>, |
|||
): Promise<void>; |
|||
|
|||
/** |
|||
* 弹出输入框的函数签名。 |
|||
* 参数beforeClose会传入用户当前输入的值 |
|||
* component指定接受用户输入的组件,默认为Input |
|||
* componentProps 为输入组件设置的属性数据 |
|||
* defaultValue 默认的值 |
|||
* modelPropName 输入组件的值属性名称。默认为modelValue |
|||
*/ |
|||
export async function prompt<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>; |
|||
``` |
|||
@ -0,0 +1,31 @@ |
|||
<script lang="ts" setup> |
|||
import { h } from 'vue'; |
|||
|
|||
import { alert, VbenButton } from '@vben/common-ui'; |
|||
|
|||
import { Empty } from 'ant-design-vue'; |
|||
|
|||
function showAlert() { |
|||
alert('This is an alert message'); |
|||
} |
|||
|
|||
function showIconAlert() { |
|||
alert({ |
|||
content: 'This is an alert message with icon', |
|||
icon: 'success', |
|||
}); |
|||
} |
|||
|
|||
function showCustomAlert() { |
|||
alert({ |
|||
content: h(Empty, { description: '什么都没有' }), |
|||
}); |
|||
} |
|||
</script> |
|||
<template> |
|||
<div class="flex gap-4"> |
|||
<VbenButton @click="showAlert">Alert</VbenButton> |
|||
<VbenButton @click="showIconAlert">Alert With Icon</VbenButton> |
|||
<VbenButton @click="showCustomAlert">Alert With Custom Content</VbenButton> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,39 @@ |
|||
<script lang="ts" setup> |
|||
import { alert, confirm, VbenButton } from '@vben/common-ui'; |
|||
|
|||
function showConfirm() { |
|||
confirm('This is an alert message') |
|||
.then(() => { |
|||
alert('Confirmed'); |
|||
}) |
|||
.catch(() => { |
|||
alert('Canceled'); |
|||
}); |
|||
} |
|||
|
|||
function showIconConfirm() { |
|||
confirm({ |
|||
content: 'This is an alert message with icon', |
|||
icon: 'success', |
|||
}); |
|||
} |
|||
|
|||
function showAsyncConfirm() { |
|||
confirm({ |
|||
beforeClose() { |
|||
return new Promise((resolve) => setTimeout(resolve, 2000)); |
|||
}, |
|||
content: 'This is an alert message with async confirm', |
|||
icon: 'success', |
|||
}).then(() => { |
|||
alert('Confirmed'); |
|||
}); |
|||
} |
|||
</script> |
|||
<template> |
|||
<div class="flex gap-4"> |
|||
<VbenButton @click="showConfirm">Confirm</VbenButton> |
|||
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton> |
|||
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton> |
|||
</div> |
|||
</template> |
|||
@ -0,0 +1,41 @@ |
|||
<script lang="ts" setup> |
|||
import { alert, prompt, VbenButton } from '@vben/common-ui'; |
|||
|
|||
import { VbenSelect } from '@vben-core/shadcn-ui'; |
|||
|
|||
function showPrompt() { |
|||
prompt({ |
|||
content: '请输入一些东西', |
|||
}) |
|||
.then((val) => { |
|||
alert(`已收到你的输入:${val}`); |
|||
}) |
|||
.catch(() => { |
|||
alert('Canceled'); |
|||
}); |
|||
} |
|||
|
|||
function showSelectPrompt() { |
|||
prompt({ |
|||
component: VbenSelect, |
|||
componentProps: { |
|||
options: [ |
|||
{ label: 'Option 1', value: 'option1' }, |
|||
{ label: 'Option 2', value: 'option2' }, |
|||
{ label: 'Option 3', value: 'option3' }, |
|||
], |
|||
placeholder: '请选择', |
|||
}, |
|||
content: 'This is an alert message with icon', |
|||
icon: 'question', |
|||
}).then((val) => { |
|||
alert(`你选择的是${val}`); |
|||
}); |
|||
} |
|||
</script> |
|||
<template> |
|||
<div class="flex gap-4"> |
|||
<VbenButton @click="showPrompt">Prompt</VbenButton> |
|||
<VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton> |
|||
</div> |
|||
</template> |
|||
@ -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 = []; |
|||
} |
|||
@ -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; |
|||
}; |
|||
@ -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> |
|||
@ -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'; |
|||
|
|||
@ -0,0 +1,16 @@ |
|||
<script setup lang="ts"> |
|||
import type { AlertDialogEmits, AlertDialogProps } from 'radix-vue'; |
|||
|
|||
import { AlertDialogRoot, useForwardPropsEmits } from 'radix-vue'; |
|||
|
|||
const props = defineProps<AlertDialogProps>(); |
|||
const emits = defineEmits<AlertDialogEmits>(); |
|||
|
|||
const forwarded = useForwardPropsEmits(props, emits); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogRoot v-bind="forwarded"> |
|||
<slot></slot> |
|||
</AlertDialogRoot> |
|||
</template> |
|||
@ -0,0 +1,13 @@ |
|||
<script setup lang="ts"> |
|||
import type { AlertDialogActionProps } from 'radix-vue'; |
|||
|
|||
import { AlertDialogAction } from 'radix-vue'; |
|||
|
|||
const props = defineProps<AlertDialogActionProps>(); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogAction v-bind="props"> |
|||
<slot></slot> |
|||
</AlertDialogAction> |
|||
</template> |
|||
@ -0,0 +1,13 @@ |
|||
<script setup lang="ts"> |
|||
import type { AlertDialogCancelProps } from 'radix-vue'; |
|||
|
|||
import { AlertDialogCancel } from 'radix-vue'; |
|||
|
|||
const props = defineProps<AlertDialogCancelProps>(); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogCancel v-bind="props"> |
|||
<slot></slot> |
|||
</AlertDialogCancel> |
|||
</template> |
|||
@ -0,0 +1,91 @@ |
|||
<script setup lang="ts"> |
|||
import type { |
|||
AlertDialogContentEmits, |
|||
AlertDialogContentProps, |
|||
} from 'radix-vue'; |
|||
|
|||
import type { ClassType } from '@vben-core/typings'; |
|||
|
|||
import { computed, ref } from 'vue'; |
|||
|
|||
import { cn } from '@vben-core/shared/utils'; |
|||
|
|||
import { |
|||
AlertDialogContent, |
|||
AlertDialogPortal, |
|||
useForwardPropsEmits, |
|||
} from 'radix-vue'; |
|||
|
|||
import AlertDialogOverlay from './AlertDialogOverlay.vue'; |
|||
|
|||
const props = withDefaults( |
|||
defineProps< |
|||
AlertDialogContentProps & { |
|||
centered?: boolean; |
|||
class?: ClassType; |
|||
modal?: boolean; |
|||
open?: boolean; |
|||
overlayBlur?: number; |
|||
zIndex?: number; |
|||
} |
|||
>(), |
|||
{ modal: true }, |
|||
); |
|||
const emits = defineEmits< |
|||
AlertDialogContentEmits & { close: []; closed: []; opened: [] } |
|||
>(); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { class: _, modal: _modal, open: _open, ...delegated } = props; |
|||
|
|||
return delegated; |
|||
}); |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits); |
|||
|
|||
const contentRef = ref<InstanceType<typeof AlertDialogContent> | null>(null); |
|||
function onAnimationEnd(event: AnimationEvent) { |
|||
// 只有在 contentRef 的动画结束时才触发 opened/closed 事件 |
|||
if (event.target === contentRef.value?.$el) { |
|||
if (props.open) { |
|||
emits('opened'); |
|||
} else { |
|||
emits('closed'); |
|||
} |
|||
} |
|||
} |
|||
defineExpose({ |
|||
getContentRef: () => contentRef.value, |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogPortal> |
|||
<Transition name="fade"> |
|||
<AlertDialogOverlay |
|||
v-if="open && modal" |
|||
:style="{ |
|||
...(zIndex ? { zIndex } : {}), |
|||
position: 'fixed', |
|||
backdropFilter: |
|||
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none', |
|||
}" |
|||
@click="() => emits('close')" |
|||
/> |
|||
</Transition> |
|||
<AlertDialogContent |
|||
ref="contentRef" |
|||
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }" |
|||
@animationend="onAnimationEnd" |
|||
v-bind="forwarded" |
|||
:class=" |
|||
cn( |
|||
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl', |
|||
props.class, |
|||
) |
|||
" |
|||
> |
|||
<slot></slot> |
|||
</AlertDialogContent> |
|||
</AlertDialogPortal> |
|||
</template> |
|||
@ -0,0 +1,28 @@ |
|||
<script lang="ts" setup> |
|||
import type { AlertDialogDescriptionProps } from 'radix-vue'; |
|||
|
|||
import { computed } from 'vue'; |
|||
|
|||
import { cn } from '@vben-core/shared/utils'; |
|||
|
|||
import { AlertDialogDescription, useForwardProps } from 'radix-vue'; |
|||
|
|||
const props = defineProps<AlertDialogDescriptionProps & { class?: any }>(); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { class: _, ...delegated } = props; |
|||
|
|||
return delegated; |
|||
}); |
|||
|
|||
const forwardedProps = useForwardProps(delegatedProps); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogDescription |
|||
v-bind="forwardedProps" |
|||
:class="cn('text-muted-foreground text-sm', props.class)" |
|||
> |
|||
<slot></slot> |
|||
</AlertDialogDescription> |
|||
</template> |
|||
@ -0,0 +1,8 @@ |
|||
<script setup lang="ts"> |
|||
import { useScrollLock } from '@vben-core/composables'; |
|||
|
|||
useScrollLock(); |
|||
</script> |
|||
<template> |
|||
<div class="bg-overlay z-popup inset-0"></div> |
|||
</template> |
|||
@ -0,0 +1,30 @@ |
|||
<script setup lang="ts"> |
|||
import type { AlertDialogTitleProps } from 'radix-vue'; |
|||
|
|||
import { computed } from 'vue'; |
|||
|
|||
import { cn } from '@vben-core/shared/utils'; |
|||
|
|||
import { AlertDialogTitle, useForwardProps } from 'radix-vue'; |
|||
|
|||
const props = defineProps<AlertDialogTitleProps & { class?: any }>(); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { class: _, ...delegated } = props; |
|||
|
|||
return delegated; |
|||
}); |
|||
|
|||
const forwardedProps = useForwardProps(delegatedProps); |
|||
</script> |
|||
|
|||
<template> |
|||
<AlertDialogTitle |
|||
v-bind="forwardedProps" |
|||
:class=" |
|||
cn('text-lg font-semibold leading-none tracking-tight', props.class) |
|||
" |
|||
> |
|||
<slot></slot> |
|||
</AlertDialogTitle> |
|||
</template> |
|||
@ -0,0 +1,6 @@ |
|||
export { default as AlertDialog } from './AlertDialog.vue'; |
|||
export { default as AlertDialogAction } from './AlertDialogAction.vue'; |
|||
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'; |
|||
export { default as AlertDialogContent } from './AlertDialogContent.vue'; |
|||
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'; |
|||
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'; |
|||
Loading…
Reference in new issue