Skip to content

Commit 71e8d12

Browse files
authored
fix: improve prompt component (vbenjs#5879)
* fix: prompt component render fixed * fix: alert buttonAlign default value
1 parent d216fdc commit 71e8d12

File tree

6 files changed

+193
-48
lines changed

6 files changed

+193
-48
lines changed

docs/src/components/common-ui/vben-alert.md

+26
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,18 @@ export type BeforeCloseScope = {
4343
isConfirm: boolean;
4444
};
4545

46+
/**
47+
* alert 属性
48+
*/
4649
export type AlertProps = {
4750
/** 关闭前的回调,如果返回false,则终止关闭 */
4851
beforeClose?: (
4952
scope: BeforeCloseScope,
5053
) => boolean | Promise<boolean | undefined> | undefined;
5154
/** 边框 */
5255
bordered?: boolean;
56+
/** 按钮对齐方式 */
57+
buttonAlign?: 'center' | 'end' | 'start';
5358
/** 取消按钮的标题 */
5459
cancelText?: string;
5560
/** 是否居中显示 */
@@ -62,6 +67,8 @@ export type AlertProps = {
6267
content: Component | string;
6368
/** 弹窗内容的额外样式 */
6469
contentClass?: string;
70+
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
71+
contentMasking?: boolean;
6572
/** 弹窗的图标(在标题的前面) */
6673
icon?: Component | IconType;
6774
/** 是否显示取消按钮 */
@@ -70,6 +77,25 @@ export type AlertProps = {
7077
title?: string;
7178
};
7279

80+
/** prompt 属性 */
81+
export type PromptProps<T = any> = {
82+
/** 关闭前的回调,如果返回false,则终止关闭 */
83+
beforeClose?: (scope: {
84+
isConfirm: boolean;
85+
value: T | undefined;
86+
}) => boolean | Promise<boolean | undefined> | undefined;
87+
/** 用于接受用户输入的组件 */
88+
component?: Component;
89+
/** 输入组件的属性 */
90+
componentProps?: Recordable<any>;
91+
/** 输入组件的插槽 */
92+
componentSlots?: Recordable<Component>;
93+
/** 默认值 */
94+
defaultValue?: T;
95+
/** 输入组件的值属性名 */
96+
modelPropName?: string;
97+
} & Omit<AlertProps, 'beforeClose'>;
98+
7399
/**
74100
* 函数签名
75101
* alert和confirm的函数签名相同。

docs/src/demos/vben-alert/alert/index.vue

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { h } from 'vue';
33
44
import { alert, VbenButton } from '@vben/common-ui';
55
6-
import { Empty } from 'ant-design-vue';
6+
import { Result } from 'ant-design-vue';
77
88
function showAlert() {
99
alert('This is an alert message');
@@ -18,7 +18,12 @@ function showIconAlert() {
1818
1919
function showCustomAlert() {
2020
alert({
21-
content: h(Empty, { description: '什么都没有' }),
21+
buttonAlign: 'center',
22+
content: h(Result, {
23+
status: 'success',
24+
subTitle: '已成功创建订单。订单ID:2017182818828182881',
25+
title: '操作成功',
26+
}),
2227
});
2328
}
2429
</script>

docs/src/demos/vben-alert/prompt/index.vue

+46-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<script lang="ts" setup>
2+
import { h } from 'vue';
3+
24
import { alert, prompt, VbenButton } from '@vben/common-ui';
35
4-
import { VbenSelect } from '@vben-core/shadcn-ui';
6+
import { Input, RadioGroup } from 'ant-design-vue';
7+
import { BadgeJapaneseYen } from 'lucide-vue-next';
58
69
function showPrompt() {
710
prompt({
@@ -17,25 +20,62 @@ function showPrompt() {
1720
1821
function showSelectPrompt() {
1922
prompt({
20-
component: VbenSelect,
23+
component: Input,
24+
componentProps: {
25+
placeholder: '请输入',
26+
prefix: '充值金额',
27+
type: 'number',
28+
},
29+
componentSlots: {
30+
addonAfter: () => h(BadgeJapaneseYen),
31+
},
32+
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
33+
icon: 'question',
34+
modelPropName: 'value',
35+
}).then((val) => {
36+
if (val) alert(`你输入的是${val}`);
37+
});
38+
}
39+
40+
function sleep(ms: number) {
41+
return new Promise((resolve) => setTimeout(resolve, ms));
42+
}
43+
44+
function showAsyncPrompt() {
45+
prompt({
46+
async beforeClose(scope) {
47+
console.log(scope);
48+
if (scope.isConfirm) {
49+
if (scope.value) {
50+
// 模拟异步操作,如果不成功,可以返回false
51+
await sleep(2000);
52+
} else {
53+
alert('请选择一个选项');
54+
return false;
55+
}
56+
}
57+
},
58+
component: RadioGroup,
2159
componentProps: {
60+
class: 'flex flex-col',
2261
options: [
2362
{ label: 'Option 1', value: 'option1' },
2463
{ label: 'Option 2', value: 'option2' },
2564
{ label: 'Option 3', value: 'option3' },
2665
],
27-
placeholder: '请选择',
2866
},
29-
content: 'This is an alert message with icon',
67+
content: '选择一个选项后再点击[确认]',
3068
icon: 'question',
69+
modelPropName: 'value',
3170
}).then((val) => {
32-
alert(`你选择的是${val}`);
71+
alert(`${val} 已设置。`);
3372
});
3473
}
3574
</script>
3675
<template>
3776
<div class="flex gap-4">
3877
<VbenButton @click="showPrompt">Prompt</VbenButton>
39-
<VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton>
78+
<VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
79+
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
4080
</div>
4181
</template>

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

+78-37
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Component } from 'vue';
1+
import type { Component, VNode } from 'vue';
22

33
import type { Recordable } from '@vben-core/typings';
44

5-
import type { AlertProps, BeforeCloseScope } from './alert';
5+
import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
66

7-
import { h, ref, render } from 'vue';
7+
import { h, nextTick, ref, render } from 'vue';
88

99
import { useSimpleLocale } from '@vben-core/composables';
1010
import { Input } from '@vben-core/shadcn-ui';
@@ -130,40 +130,58 @@ export function vbenConfirm(
130130
}
131131

132132
export async function vbenPrompt<T = any>(
133-
options: Omit<AlertProps, 'beforeClose'> & {
134-
beforeClose?: (scope: {
135-
isConfirm: boolean;
136-
value: T | undefined;
137-
}) => boolean | Promise<boolean | undefined> | undefined;
138-
component?: Component;
139-
componentProps?: Recordable<any>;
140-
defaultValue?: T;
141-
modelPropName?: string;
142-
},
133+
options: PromptProps<T>,
143134
): Promise<T | undefined> {
144135
const {
145136
component: _component,
146137
componentProps: _componentProps,
138+
componentSlots,
147139
content,
148140
defaultValue,
149141
modelPropName: _modelPropName,
150142
...delegated
151143
} = options;
152-
const contents: Component[] = [];
144+
153145
const modelValue = ref<T | undefined>(defaultValue);
146+
const inputComponentRef = ref<null | VNode>(null);
147+
const staticContents: Component[] = [];
148+
154149
if (isString(content)) {
155-
contents.push(h('span', content));
156-
} else {
157-
contents.push(content);
150+
staticContents.push(h('span', content));
151+
} else if (content) {
152+
staticContents.push(content as Component);
158153
}
159-
const componentProps = _componentProps || {};
154+
160155
const modelPropName = _modelPropName || 'modelValue';
161-
componentProps[modelPropName] = modelValue.value;
162-
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
163-
modelValue.value = val;
156+
const componentProps = { ..._componentProps };
157+
158+
// 每次渲染时都会重新计算的内容函数
159+
const contentRenderer = () => {
160+
const currentProps = { ...componentProps };
161+
162+
// 设置当前值
163+
currentProps[modelPropName] = modelValue.value;
164+
165+
// 设置更新处理函数
166+
currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
167+
modelValue.value = val;
168+
};
169+
170+
// 创建输入组件
171+
inputComponentRef.value = h(
172+
_component || Input,
173+
currentProps,
174+
componentSlots,
175+
);
176+
177+
// 返回包含静态内容和输入组件的数组
178+
return h(
179+
'div',
180+
{ class: 'flex flex-col gap-2' },
181+
{ default: () => [...staticContents, inputComponentRef.value] },
182+
);
164183
};
165-
const componentRef = h(_component || Input, componentProps);
166-
contents.push(componentRef);
184+
167185
const props: AlertProps & Recordable<any> = {
168186
...delegated,
169187
async beforeClose(scope: BeforeCloseScope) {
@@ -174,23 +192,46 @@ export async function vbenPrompt<T = any>(
174192
});
175193
}
176194
},
177-
content: h(
178-
'div',
179-
{ class: 'flex flex-col gap-2' },
180-
{ default: () => contents },
181-
),
182-
onOpened() {
183-
// 组件挂载完成后,自动聚焦到输入组件
184-
if (
185-
componentRef.component?.exposed &&
186-
isFunction(componentRef.component.exposed.focus)
187-
) {
188-
componentRef.component.exposed.focus();
189-
} else if (componentRef.el && isFunction(componentRef.el.focus)) {
190-
componentRef.el.focus();
195+
// 使用函数形式,每次渲染都会重新计算内容
196+
content: contentRenderer,
197+
contentMasking: true,
198+
async onOpened() {
199+
await nextTick();
200+
const componentRef: null | VNode = inputComponentRef.value;
201+
if (componentRef) {
202+
if (
203+
componentRef.component?.exposed &&
204+
isFunction(componentRef.component.exposed.focus)
205+
) {
206+
componentRef.component.exposed.focus();
207+
} else {
208+
if (componentRef.el) {
209+
if (
210+
isFunction(componentRef.el.focus) &&
211+
['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(
212+
componentRef.el.tagName,
213+
)
214+
) {
215+
componentRef.el.focus();
216+
} else if (isFunction(componentRef.el.querySelector)) {
217+
const focusableElement = componentRef.el.querySelector(
218+
'input, select, textarea, button',
219+
);
220+
if (focusableElement && isFunction(focusableElement.focus)) {
221+
focusableElement.focus();
222+
}
223+
} else if (
224+
componentRef.el.nextElementSibling &&
225+
isFunction(componentRef.el.nextElementSibling.focus)
226+
) {
227+
componentRef.el.nextElementSibling.focus();
228+
}
229+
}
230+
}
191231
}
192232
},
193233
};
234+
194235
await vbenConfirm(props);
195236
return modelValue.value;
196237
}

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { Component } from 'vue';
1+
import type { Component, VNode, VNodeArrayChildren } from 'vue';
2+
3+
import type { Recordable } from '@vben-core/typings';
24

35
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
46

@@ -13,6 +15,11 @@ export type AlertProps = {
1315
) => boolean | Promise<boolean | undefined> | undefined;
1416
/** 边框 */
1517
bordered?: boolean;
18+
/**
19+
* 按钮对齐方式
20+
* @default 'end'
21+
*/
22+
buttonAlign?: 'center' | 'end' | 'start';
1623
/** 取消按钮的标题 */
1724
cancelText?: string;
1825
/** 是否居中显示 */
@@ -25,10 +32,35 @@ export type AlertProps = {
2532
content: Component | string;
2633
/** 弹窗内容的额外样式 */
2734
contentClass?: string;
35+
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
36+
contentMasking?: boolean;
2837
/** 弹窗的图标(在标题的前面) */
2938
icon?: Component | IconType;
3039
/** 是否显示取消按钮 */
3140
showCancel?: boolean;
3241
/** 弹窗标题 */
3342
title?: string;
3443
};
44+
45+
/** Prompt属性 */
46+
export type PromptProps<T = any> = {
47+
/** 关闭前的回调,如果返回false,则终止关闭 */
48+
beforeClose?: (scope: {
49+
isConfirm: boolean;
50+
value: T | undefined;
51+
}) => boolean | Promise<boolean | undefined> | undefined;
52+
/** 用于接受用户输入的组件 */
53+
component?: Component;
54+
/** 输入组件的属性 */
55+
componentProps?: Recordable<any>;
56+
/** 输入组件的插槽 */
57+
componentSlots?:
58+
| (() => any)
59+
| Recordable<unknown>
60+
| VNode
61+
| VNodeArrayChildren;
62+
/** 默认值 */
63+
defaultValue?: T;
64+
/** 输入组件的值属性名 */
65+
modelPropName?: string;
66+
} & Omit<AlertProps, 'beforeClose'>;

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { cn } from '@vben-core/shared/utils';
3030
3131
const props = withDefaults(defineProps<AlertProps>(), {
3232
bordered: true,
33+
buttonAlign: 'end',
3334
centered: true,
3435
containerClass: 'w-[520px]',
3536
});
@@ -154,9 +155,9 @@ async function handleOpenChange(val: boolean) {
154155
<div class="m-4 mb-6 min-h-[30px]">
155156
<VbenRenderContent :content="content" render-br />
156157
</div>
157-
<VbenLoading v-if="loading" :spinning="loading" />
158+
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
158159
</AlertDialogDescription>
159-
<div class="flex justify-end gap-x-2">
160+
<div class="flex justify-end gap-x-2" :class="`justify-${buttonAlign}`">
160161
<AlertDialogCancel v-if="showCancel" :disabled="loading">
161162
<component
162163
:is="components.DefaultButton || VbenButton"

0 commit comments

Comments
 (0)