fix: improve prompt component (#5879)
* fix: prompt component render fixed * fix: alert buttonAlign default valuepull/73/head
							parent
							
								
									5cc93d2efe
								
							
						
					
					
						commit
						a1e761b9f8
					
				| 
						 | 
				
			
			@ -43,6 +43,9 @@ export type BeforeCloseScope = {
 | 
			
		|||
  isConfirm: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * alert 属性
 | 
			
		||||
 */
 | 
			
		||||
export type AlertProps = {
 | 
			
		||||
  /** 关闭前的回调,如果返回false,则终止关闭 */
 | 
			
		||||
  beforeClose?: (
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +53,8 @@ export type AlertProps = {
 | 
			
		|||
  ) => boolean | Promise<boolean | undefined> | undefined;
 | 
			
		||||
  /** 边框 */
 | 
			
		||||
  bordered?: boolean;
 | 
			
		||||
  /** 按钮对齐方式 */
 | 
			
		||||
  buttonAlign?: 'center' | 'end' | 'start';
 | 
			
		||||
  /** 取消按钮的标题 */
 | 
			
		||||
  cancelText?: string;
 | 
			
		||||
  /** 是否居中显示 */
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +67,8 @@ export type AlertProps = {
 | 
			
		|||
  content: Component | string;
 | 
			
		||||
  /** 弹窗内容的额外样式 */
 | 
			
		||||
  contentClass?: string;
 | 
			
		||||
  /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
 | 
			
		||||
  contentMasking?: boolean;
 | 
			
		||||
  /** 弹窗的图标(在标题的前面) */
 | 
			
		||||
  icon?: Component | IconType;
 | 
			
		||||
  /** 是否显示取消按钮 */
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +77,25 @@ 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?: Recordable<Component>;
 | 
			
		||||
  /** 默认值 */
 | 
			
		||||
  defaultValue?: T;
 | 
			
		||||
  /** 输入组件的值属性名 */
 | 
			
		||||
  modelPropName?: string;
 | 
			
		||||
} & Omit<AlertProps, 'beforeClose'>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 函数签名
 | 
			
		||||
 * alert和confirm的函数签名相同。
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { h } from 'vue';
 | 
			
		|||
 | 
			
		||||
import { alert, VbenButton } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { Empty } from 'ant-design-vue';
 | 
			
		||||
import { Result } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
function showAlert() {
 | 
			
		||||
  alert('This is an alert message');
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,12 @@ function showIconAlert() {
 | 
			
		|||
 | 
			
		||||
function showCustomAlert() {
 | 
			
		||||
  alert({
 | 
			
		||||
    content: h(Empty, { description: '什么都没有' }),
 | 
			
		||||
    buttonAlign: 'center',
 | 
			
		||||
    content: h(Result, {
 | 
			
		||||
      status: 'success',
 | 
			
		||||
      subTitle: '已成功创建订单。订单ID:2017182818828182881',
 | 
			
		||||
      title: '操作成功',
 | 
			
		||||
    }),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { h } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { alert, prompt, VbenButton } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { VbenSelect } from '@vben-core/shadcn-ui';
 | 
			
		||||
import { Input, RadioGroup } from 'ant-design-vue';
 | 
			
		||||
import { BadgeJapaneseYen } from 'lucide-vue-next';
 | 
			
		||||
 | 
			
		||||
function showPrompt() {
 | 
			
		||||
  prompt({
 | 
			
		||||
| 
						 | 
				
			
			@ -17,25 +20,62 @@ function showPrompt() {
 | 
			
		|||
 | 
			
		||||
function showSelectPrompt() {
 | 
			
		||||
  prompt({
 | 
			
		||||
    component: VbenSelect,
 | 
			
		||||
    component: Input,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      placeholder: '请输入',
 | 
			
		||||
      prefix: '充值金额',
 | 
			
		||||
      type: 'number',
 | 
			
		||||
    },
 | 
			
		||||
    componentSlots: {
 | 
			
		||||
      addonAfter: () => h(BadgeJapaneseYen),
 | 
			
		||||
    },
 | 
			
		||||
    content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
 | 
			
		||||
    icon: 'question',
 | 
			
		||||
    modelPropName: 'value',
 | 
			
		||||
  }).then((val) => {
 | 
			
		||||
    if (val) alert(`你输入的是${val}`);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sleep(ms: number) {
 | 
			
		||||
  return new Promise((resolve) => setTimeout(resolve, ms));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showAsyncPrompt() {
 | 
			
		||||
  prompt({
 | 
			
		||||
    async beforeClose(scope) {
 | 
			
		||||
      console.log(scope);
 | 
			
		||||
      if (scope.isConfirm) {
 | 
			
		||||
        if (scope.value) {
 | 
			
		||||
          // 模拟异步操作,如果不成功,可以返回false
 | 
			
		||||
          await sleep(2000);
 | 
			
		||||
        } else {
 | 
			
		||||
          alert('请选择一个选项');
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    component: RadioGroup,
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      class: 'flex flex-col',
 | 
			
		||||
      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',
 | 
			
		||||
    content: '选择一个选项后再点击[确认]',
 | 
			
		||||
    icon: 'question',
 | 
			
		||||
    modelPropName: 'value',
 | 
			
		||||
  }).then((val) => {
 | 
			
		||||
    alert(`你选择的是${val}`);
 | 
			
		||||
    alert(`${val} 已设置。`);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex gap-4">
 | 
			
		||||
    <VbenButton @click="showPrompt">Prompt</VbenButton>
 | 
			
		||||
    <VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton>
 | 
			
		||||
    <VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
 | 
			
		||||
    <VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) => {
 | 
			
		||||
  const componentProps = { ..._componentProps };
 | 
			
		||||
 | 
			
		||||
  // 每次渲染时都会重新计算的内容函数
 | 
			
		||||
  const contentRenderer = () => {
 | 
			
		||||
    const currentProps = { ...componentProps };
 | 
			
		||||
 | 
			
		||||
    // 设置当前值
 | 
			
		||||
    currentProps[modelPropName] = modelValue.value;
 | 
			
		||||
 | 
			
		||||
    // 设置更新处理函数
 | 
			
		||||
    currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
 | 
			
		||||
      modelValue.value = val;
 | 
			
		||||
    };
 | 
			
		||||
  const componentRef = h(_component || Input, componentProps);
 | 
			
		||||
  contents.push(componentRef);
 | 
			
		||||
 | 
			
		||||
    // 创建输入组件
 | 
			
		||||
    inputComponentRef.value = h(
 | 
			
		||||
      _component || Input,
 | 
			
		||||
      currentProps,
 | 
			
		||||
      componentSlots,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 返回包含静态内容和输入组件的数组
 | 
			
		||||
    return h(
 | 
			
		||||
      'div',
 | 
			
		||||
      { class: 'flex flex-col gap-2' },
 | 
			
		||||
      { default: () => [...staticContents, inputComponentRef.value] },
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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() {
 | 
			
		||||
      // 组件挂载完成后,自动聚焦到输入组件
 | 
			
		||||
    // 使用函数形式,每次渲染都会重新计算内容
 | 
			
		||||
    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 && isFunction(componentRef.el.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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue