fix: improve prompt component (#5879)
* fix: prompt component render fixed * fix: alert buttonAlign default valuepull/78/MERGE
							parent
							
								
									d216fdca44
								
							
						
					
					
						commit
						71e8d12b70
					
				|  | @ -43,6 +43,9 @@ export type BeforeCloseScope = { | ||||||
|   isConfirm: boolean; |   isConfirm: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * alert 属性 | ||||||
|  |  */ | ||||||
| export type AlertProps = { | export type AlertProps = { | ||||||
|   /** 关闭前的回调,如果返回false,则终止关闭 */ |   /** 关闭前的回调,如果返回false,则终止关闭 */ | ||||||
|   beforeClose?: ( |   beforeClose?: ( | ||||||
|  | @ -50,6 +53,8 @@ export type AlertProps = { | ||||||
|   ) => boolean | Promise<boolean | undefined> | undefined; |   ) => boolean | Promise<boolean | undefined> | undefined; | ||||||
|   /** 边框 */ |   /** 边框 */ | ||||||
|   bordered?: boolean; |   bordered?: boolean; | ||||||
|  |   /** 按钮对齐方式 */ | ||||||
|  |   buttonAlign?: 'center' | 'end' | 'start'; | ||||||
|   /** 取消按钮的标题 */ |   /** 取消按钮的标题 */ | ||||||
|   cancelText?: string; |   cancelText?: string; | ||||||
|   /** 是否居中显示 */ |   /** 是否居中显示 */ | ||||||
|  | @ -62,6 +67,8 @@ export type AlertProps = { | ||||||
|   content: Component | string; |   content: Component | string; | ||||||
|   /** 弹窗内容的额外样式 */ |   /** 弹窗内容的额外样式 */ | ||||||
|   contentClass?: string; |   contentClass?: string; | ||||||
|  |   /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/ | ||||||
|  |   contentMasking?: boolean; | ||||||
|   /** 弹窗的图标(在标题的前面) */ |   /** 弹窗的图标(在标题的前面) */ | ||||||
|   icon?: Component | IconType; |   icon?: Component | IconType; | ||||||
|   /** 是否显示取消按钮 */ |   /** 是否显示取消按钮 */ | ||||||
|  | @ -70,6 +77,25 @@ export type AlertProps = { | ||||||
|   title?: string; |   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的函数签名相同。 |  * alert和confirm的函数签名相同。 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { h } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import { alert, VbenButton } from '@vben/common-ui'; | import { alert, VbenButton } from '@vben/common-ui'; | ||||||
| 
 | 
 | ||||||
| import { Empty } from 'ant-design-vue'; | import { Result } from 'ant-design-vue'; | ||||||
| 
 | 
 | ||||||
| function showAlert() { | function showAlert() { | ||||||
|   alert('This is an alert message'); |   alert('This is an alert message'); | ||||||
|  | @ -18,7 +18,12 @@ function showIconAlert() { | ||||||
| 
 | 
 | ||||||
| function showCustomAlert() { | function showCustomAlert() { | ||||||
|   alert({ |   alert({ | ||||||
|     content: h(Empty, { description: '什么都没有' }), |     buttonAlign: 'center', | ||||||
|  |     content: h(Result, { | ||||||
|  |       status: 'success', | ||||||
|  |       subTitle: '已成功创建订单。订单ID:2017182818828182881', | ||||||
|  |       title: '操作成功', | ||||||
|  |     }), | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | import { h } from 'vue'; | ||||||
|  | 
 | ||||||
| import { alert, prompt, VbenButton } from '@vben/common-ui'; | 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() { | function showPrompt() { | ||||||
|   prompt({ |   prompt({ | ||||||
|  | @ -17,25 +20,62 @@ function showPrompt() { | ||||||
| 
 | 
 | ||||||
| function showSelectPrompt() { | function showSelectPrompt() { | ||||||
|   prompt({ |   prompt({ | ||||||
|     component: VbenSelect, |     component: Input, | ||||||
|     componentProps: { |     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: [ |       options: [ | ||||||
|         { label: 'Option 1', value: 'option1' }, |         { label: 'Option 1', value: 'option1' }, | ||||||
|         { label: 'Option 2', value: 'option2' }, |         { label: 'Option 2', value: 'option2' }, | ||||||
|         { label: 'Option 3', value: 'option3' }, |         { label: 'Option 3', value: 'option3' }, | ||||||
|       ], |       ], | ||||||
|       placeholder: '请选择', |  | ||||||
|     }, |     }, | ||||||
|     content: 'This is an alert message with icon', |     content: '选择一个选项后再点击[确认]', | ||||||
|     icon: 'question', |     icon: 'question', | ||||||
|  |     modelPropName: 'value', | ||||||
|   }).then((val) => { |   }).then((val) => { | ||||||
|     alert(`你选择的是${val}`); |     alert(`${val} 已设置。`); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div class="flex gap-4"> |   <div class="flex gap-4"> | ||||||
|     <VbenButton @click="showPrompt">Prompt</VbenButton> |     <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> |   </div> | ||||||
| </template> | </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 { 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 { useSimpleLocale } from '@vben-core/composables'; | ||||||
| import { Input } from '@vben-core/shadcn-ui'; | import { Input } from '@vben-core/shadcn-ui'; | ||||||
|  | @ -130,40 +130,58 @@ export function vbenConfirm( | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function vbenPrompt<T = any>( | export async function vbenPrompt<T = any>( | ||||||
|   options: Omit<AlertProps, 'beforeClose'> & { |   options: PromptProps<T>, | ||||||
|     beforeClose?: (scope: { |  | ||||||
|       isConfirm: boolean; |  | ||||||
|       value: T | undefined; |  | ||||||
|     }) => boolean | Promise<boolean | undefined> | undefined; |  | ||||||
|     component?: Component; |  | ||||||
|     componentProps?: Recordable<any>; |  | ||||||
|     defaultValue?: T; |  | ||||||
|     modelPropName?: string; |  | ||||||
|   }, |  | ||||||
| ): Promise<T | undefined> { | ): Promise<T | undefined> { | ||||||
|   const { |   const { | ||||||
|     component: _component, |     component: _component, | ||||||
|     componentProps: _componentProps, |     componentProps: _componentProps, | ||||||
|  |     componentSlots, | ||||||
|     content, |     content, | ||||||
|     defaultValue, |     defaultValue, | ||||||
|     modelPropName: _modelPropName, |     modelPropName: _modelPropName, | ||||||
|     ...delegated |     ...delegated | ||||||
|   } = options; |   } = options; | ||||||
|   const contents: Component[] = []; | 
 | ||||||
|   const modelValue = ref<T | undefined>(defaultValue); |   const modelValue = ref<T | undefined>(defaultValue); | ||||||
|  |   const inputComponentRef = ref<null | VNode>(null); | ||||||
|  |   const staticContents: Component[] = []; | ||||||
|  | 
 | ||||||
|   if (isString(content)) { |   if (isString(content)) { | ||||||
|     contents.push(h('span', content)); |     staticContents.push(h('span', content)); | ||||||
|   } else { |   } else if (content) { | ||||||
|     contents.push(content); |     staticContents.push(content as Component); | ||||||
|   } |   } | ||||||
|   const componentProps = _componentProps || {}; | 
 | ||||||
|   const modelPropName = _modelPropName || 'modelValue'; |   const modelPropName = _modelPropName || 'modelValue'; | ||||||
|   componentProps[modelPropName] = modelValue.value; |   const componentProps = { ..._componentProps }; | ||||||
|   componentProps[`onUpdate:${modelPropName}`] = (val: any) => { | 
 | ||||||
|     modelValue.value = val; |   // 每次渲染时都会重新计算的内容函数
 | ||||||
|  |   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> = { |   const props: AlertProps & Recordable<any> = { | ||||||
|     ...delegated, |     ...delegated, | ||||||
|     async beforeClose(scope: BeforeCloseScope) { |     async beforeClose(scope: BeforeCloseScope) { | ||||||
|  | @ -174,23 +192,46 @@ export async function vbenPrompt<T = any>( | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     content: h( |     // 使用函数形式,每次渲染都会重新计算内容
 | ||||||
|       'div', |     content: contentRenderer, | ||||||
|       { class: 'flex flex-col gap-2' }, |     contentMasking: true, | ||||||
|       { default: () => contents }, |     async onOpened() { | ||||||
|     ), |       await nextTick(); | ||||||
|     onOpened() { |       const componentRef: null | VNode = inputComponentRef.value; | ||||||
|       // 组件挂载完成后,自动聚焦到输入组件
 |       if (componentRef) { | ||||||
|       if ( |         if ( | ||||||
|         componentRef.component?.exposed && |           componentRef.component?.exposed && | ||||||
|         isFunction(componentRef.component.exposed.focus) |           isFunction(componentRef.component.exposed.focus) | ||||||
|       ) { |         ) { | ||||||
|         componentRef.component.exposed.focus(); |           componentRef.component.exposed.focus(); | ||||||
|       } else if (componentRef.el && isFunction(componentRef.el.focus)) { |         } else { | ||||||
|         componentRef.el.focus(); |           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); |   await vbenConfirm(props); | ||||||
|   return modelValue.value; |   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'; | export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; | ||||||
| 
 | 
 | ||||||
|  | @ -13,6 +15,11 @@ export type AlertProps = { | ||||||
|   ) => boolean | Promise<boolean | undefined> | undefined; |   ) => boolean | Promise<boolean | undefined> | undefined; | ||||||
|   /** 边框 */ |   /** 边框 */ | ||||||
|   bordered?: boolean; |   bordered?: boolean; | ||||||
|  |   /** | ||||||
|  |    * 按钮对齐方式 | ||||||
|  |    * @default 'end' | ||||||
|  |    */ | ||||||
|  |   buttonAlign?: 'center' | 'end' | 'start'; | ||||||
|   /** 取消按钮的标题 */ |   /** 取消按钮的标题 */ | ||||||
|   cancelText?: string; |   cancelText?: string; | ||||||
|   /** 是否居中显示 */ |   /** 是否居中显示 */ | ||||||
|  | @ -25,6 +32,8 @@ export type AlertProps = { | ||||||
|   content: Component | string; |   content: Component | string; | ||||||
|   /** 弹窗内容的额外样式 */ |   /** 弹窗内容的额外样式 */ | ||||||
|   contentClass?: string; |   contentClass?: string; | ||||||
|  |   /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/ | ||||||
|  |   contentMasking?: boolean; | ||||||
|   /** 弹窗的图标(在标题的前面) */ |   /** 弹窗的图标(在标题的前面) */ | ||||||
|   icon?: Component | IconType; |   icon?: Component | IconType; | ||||||
|   /** 是否显示取消按钮 */ |   /** 是否显示取消按钮 */ | ||||||
|  | @ -32,3 +41,26 @@ export type AlertProps = { | ||||||
|   /** 弹窗标题 */ |   /** 弹窗标题 */ | ||||||
|   title?: string; |   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>(), { | const props = withDefaults(defineProps<AlertProps>(), { | ||||||
|   bordered: true, |   bordered: true, | ||||||
|  |   buttonAlign: 'end', | ||||||
|   centered: true, |   centered: true, | ||||||
|   containerClass: 'w-[520px]', |   containerClass: 'w-[520px]', | ||||||
| }); | }); | ||||||
|  | @ -154,9 +155,9 @@ async function handleOpenChange(val: boolean) { | ||||||
|           <div class="m-4 mb-6 min-h-[30px]"> |           <div class="m-4 mb-6 min-h-[30px]"> | ||||||
|             <VbenRenderContent :content="content" render-br /> |             <VbenRenderContent :content="content" render-br /> | ||||||
|           </div> |           </div> | ||||||
|           <VbenLoading v-if="loading" :spinning="loading" /> |           <VbenLoading v-if="loading && contentMasking" :spinning="loading" /> | ||||||
|         </AlertDialogDescription> |         </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"> |           <AlertDialogCancel v-if="showCancel" :disabled="loading"> | ||||||
|             <component |             <component | ||||||
|               :is="components.DefaultButton || VbenButton" |               :is="components.DefaultButton || VbenButton" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Netfan
						Netfan