Browse Source

fix: improve prompt component (#5879)

* fix: prompt component render fixed

* fix: alert buttonAlign default value
Netfan 5 tháng trước cách đây
mục cha
commit
71e8d12b70

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

@@ -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的函数签名相同。

+ 7 - 2
docs/src/demos/vben-alert/alert/index.vue

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

+ 46 - 6
docs/src/demos/vben-alert/prompt/index.vue

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

+ 78 - 37
packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts

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

+ 33 - 1
packages/@core/ui-kit/popup-ui/src/alert/alert.ts

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

+ 3 - 2
packages/@core/ui-kit/popup-ui/src/alert/alert.vue

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