Parcourir la source

feat: add preset alert, confirm, prompt components that can be simple called (#5843)

* feat: add preset alert, confirm, prompt components that can be simple called

* fix: type define
Netfan il y a 5 mois
Parent
commit
44138f578f
23 fichiers modifiés avec 941 ajouts et 4 suppressions
  1. 4 0
      docs/.vitepress/config/zh.mts
  2. 101 0
      docs/src/components/common-ui/vben-alert.md
  3. 31 0
      docs/src/demos/vben-alert/alert/index.vue
  4. 39 0
      docs/src/demos/vben-alert/confirm/index.vue
  5. 41 0
      docs/src/demos/vben-alert/prompt/index.vue
  6. 2 0
      packages/@core/base/icons/src/lucide.ts
  7. 2 0
      packages/@core/composables/src/use-simple-locale/messages.ts
  8. 203 0
      packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts
  9. 28 0
      packages/@core/ui-kit/popup-ui/src/alert/alert.ts
  10. 181 0
      packages/@core/ui-kit/popup-ui/src/alert/alert.vue
  11. 9 0
      packages/@core/ui-kit/popup-ui/src/alert/index.ts
  12. 1 0
      packages/@core/ui-kit/popup-ui/src/index.ts
  13. 19 2
      packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue
  14. 16 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue
  15. 13 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue
  16. 13 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue
  17. 91 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue
  18. 28 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue
  19. 8 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue
  20. 30 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue
  21. 6 0
      packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts
  22. 1 0
      packages/@core/ui-kit/shadcn-ui/src/ui/index.ts
  23. 74 2
      playground/src/views/examples/modal/index.vue

+ 4 - 0
docs/.vitepress/config/zh.mts

@@ -168,6 +168,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
           link: 'common-ui/vben-api-component',
           text: 'ApiComponent Api组件包装器',
         },
+        {
+          link: 'common-ui/vben-alert',
+          text: 'Alert 轻量提示框',
+        },
         {
           link: 'common-ui/vben-modal',
           text: 'Modal 模态框',

+ 101 - 0
docs/src/components/common-ui/vben-alert.md

@@ -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>;
+```

+ 31 - 0
docs/src/demos/vben-alert/alert/index.vue

@@ -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>

+ 39 - 0
docs/src/demos/vben-alert/confirm/index.vue

@@ -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>

+ 41 - 0
docs/src/demos/vben-alert/prompt/index.vue

@@ -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>

+ 2 - 0
packages/@core/base/icons/src/lucide.ts

@@ -15,8 +15,10 @@ export {
   ChevronsLeft,
   ChevronsRight,
   Circle,
+  CircleAlert,
   CircleCheckBig,
   CircleHelp,
+  CircleX,
   Copy,
   CornerDownLeft,
   Ellipsis,

+ 2 - 0
packages/@core/composables/src/use-simple-locale/messages.ts

@@ -6,6 +6,7 @@ export const messages: Record<Locale, Record<string, string>> = {
     collapse: 'Collapse',
     confirm: 'Confirm',
     expand: 'Expand',
+    prompt: 'Prompt',
     reset: 'Reset',
     submit: 'Submit',
   },
@@ -14,6 +15,7 @@ export const messages: Record<Locale, Record<string, string>> = {
     collapse: '收起',
     confirm: '确认',
     expand: '展开',
+    prompt: '提示',
     reset: '重置',
     submit: '提交',
   },

+ 203 - 0
packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts

@@ -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 - 0
packages/@core/ui-kit/popup-ui/src/alert/alert.ts

@@ -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 - 0
packages/@core/ui-kit/popup-ui/src/alert/alert.vue

@@ -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 - 0
packages/@core/ui-kit/popup-ui/src/alert/index.ts

@@ -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 - 0
packages/@core/ui-kit/popup-ui/src/index.ts

@@ -1,2 +1,3 @@
+export * from './alert';
 export * from './drawer';
 export * from './modal';

+ 19 - 2
packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue

@@ -3,7 +3,7 @@ import type { Component, PropType } from 'vue';
 
 import { defineComponent, h } from 'vue';
 
-import { isFunction, isObject } from '@vben-core/shared/utils';
+import { isFunction, isObject, isString } from '@vben-core/shared/utils';
 
 export default defineComponent({
   name: 'RenderContent',
@@ -14,6 +14,10 @@ export default defineComponent({
         | undefined,
       type: [Object, String, Function],
     },
+    renderBr: {
+      default: false,
+      type: Boolean,
+    },
   },
   setup(props, { attrs, slots }) {
     return () => {
@@ -24,7 +28,20 @@ export default defineComponent({
         (isObject(props.content) || isFunction(props.content)) &&
         props.content !== null;
       if (!isComponent) {
-        return props.content;
+        if (props.renderBr && isString(props.content)) {
+          const lines = props.content.split('\n');
+          const result = [];
+          for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            result.push(h('span', { key: i }, line));
+            if (i < lines.length - 1) {
+              result.push(h('br'));
+            }
+          }
+          return result;
+        } else {
+          return props.content;
+        }
       }
       return h(props.content as never, {
         ...attrs,

+ 16 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue

@@ -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>

+ 13 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue

@@ -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>

+ 13 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue

@@ -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>

+ 91 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue

@@ -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>

+ 28 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue

@@ -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>

+ 8 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue

@@ -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>

+ 30 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue

@@ -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>

+ 6 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts

@@ -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';

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/src/ui/index.ts

@@ -1,4 +1,5 @@
 export * from './accordion';
+export * from './alert-dialog';
 export * from './avatar';
 export * from './badge';
 export * from './breadcrumb';

+ 74 - 2
playground/src/views/examples/modal/index.vue

@@ -1,7 +1,16 @@
 <script lang="ts" setup>
-import { Page, useVbenModal } from '@vben/common-ui';
+import { onBeforeUnmount } from 'vue';
 
-import { Button, Card, Flex } from 'ant-design-vue';
+import {
+  alert,
+  clearAllAlerts,
+  confirm,
+  Page,
+  prompt,
+  useVbenModal,
+} from '@vben/common-ui';
+
+import { Button, Card, Flex, message } from 'ant-design-vue';
 
 import DocButton from '../doc-button.vue';
 import AutoHeightDemo from './auto-height-demo.vue';
@@ -103,6 +112,61 @@ function openFormModal() {
     })
     .open();
 }
+
+function openAlert() {
+  alert({
+    content: '这是一个弹窗',
+    icon: 'success',
+  }).then(() => {
+    message.info('用户关闭了弹窗');
+  });
+}
+
+onBeforeUnmount(() => {
+  // 清除所有弹窗
+  clearAllAlerts();
+});
+
+function openConfirm() {
+  confirm({
+    beforeClose() {
+      // 这里可以做一些异步操作
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          resolve(true);
+        }, 1000);
+      });
+    },
+    content: '这是一个确认弹窗',
+    icon: 'question',
+  })
+    .then(() => {
+      message.success('用户确认了操作');
+    })
+    .catch(() => {
+      message.error('用户取消了操作');
+    });
+}
+
+async function openPrompt() {
+  prompt<string>({
+    async beforeClose(val) {
+      if (val === '芝士') {
+        message.error('不能吃芝士');
+        return false;
+      }
+    },
+    componentProps: { placeholder: '不能吃芝士...' },
+    content: '中午吃了什么?',
+    icon: 'question',
+  })
+    .then((res) => {
+      message.success(`用户输入了:${res}`);
+    })
+    .catch(() => {
+      message.error('用户取消了输入');
+    });
+}
 </script>
 
 <template>
@@ -195,6 +259,14 @@ function openFormModal() {
           <Button type="primary" @click="openBlurModal">打开弹窗</Button>
         </template>
       </Card>
+      <Card class="w-[300px]" title="轻量提示弹窗">
+        <p>通过快捷方法创建动态提示弹窗,适合一些轻量的提示和确认、输入等</p>
+        <template #actions>
+          <Button type="primary" @click="openAlert">Alert</Button>
+          <Button type="primary" @click="openConfirm">Confirm</Button>
+          <Button type="primary" @click="openPrompt">Prompt</Button>
+        </template>
+      </Card>
     </Flex>
   </Page>
 </template>