fix: improve prompt component (#5879)
* fix: prompt component render fixed * fix: alert buttonAlign default value
This commit is contained in:
parent
d216fdca44
commit
71e8d12b70
@ -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) => {
|
||||
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;
|
||||
}
|
||||
|
@ -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
Block a user