fix: improve prompt component (#5879)

* fix: prompt component render fixed

* fix: alert buttonAlign default value
This commit is contained in:
Netfan 2025-04-07 01:21:30 +08:00 committed by GitHub
parent d216fdca44
commit 71e8d12b70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 193 additions and 48 deletions

View File

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

View File

@ -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: '已成功创建订单。订单ID2017182818828182881',
title: '操作成功',
}),
}); });
} }
</script> </script>

View File

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

View File

@ -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) => {
// 每次渲染时都会重新计算的内容函数
const contentRenderer = () => {
const currentProps = { ...componentProps };
// 设置当前值
currentProps[modelPropName] = modelValue.value;
// 设置更新处理函数
currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
modelValue.value = val; 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> = { 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 {
if (componentRef.el) {
if (
isFunction(componentRef.el.focus) &&
['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(
componentRef.el.tagName,
)
) {
componentRef.el.focus(); 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;
} }

View File

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

View File

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