MessageBox 对话框组件

MessageBox 对话框组件


MessageBox.vue

script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick, type Ref } from 'vue';
import { isFunction, isNil } from 'lodash-es';
import type { MessageBoxProps, MessageBoxAction } from './types';
import VrOverlay from '../Overlay/Overlay.vue';
import VrIcon from '../Icon/Icon.vue';
import VrButton from '../Button/Button.vue';
import { useId, useZIndex } from '@veyra/hooks';
import { typeIconMap } from '@veyra/utils';

defineOptions({
name: 'VrMessageBox', // 定义组件名称
inheritAttrs: false, // 禁用继承属性
});

const props = withDefaults(defineProps<MessageBoxProps>(), {
lockScroll: true,
showClose: true,
closeOnClickModal: true,
confirmButtonType: 'primary',
roundButton: false,
boxType: '',
inputValue: '',
inputPlaceholder: 'Please input...',
confirmButtonText: 'Ok',
cancelButtonText: 'Cancel',
showConfirmButton: true,
});

const { doAction } = props;

const inputId = useId();
const { nextZIndex } = useZIndex();

const state = reactive({
...props,
zIndex: nextZizonIndex(),
});

const hasMessage = computed(() => !!state.message);
const iconComponent = computed(
() => state.icon ?? typeIconMap.get(state.type ?? '')
);

// 监听 visible 变化,更新 zIndex 并聚焦输入框
watch(
() => props.visible?.value,
(val) => {
if (val) state.zIndex = nextZIndex();
if (props.boxType !== 'prompt') return;
if (!val) return;
nextTick(() => {
// inputRef.value?.focus();
});
}
);

// 点击遮罩层触发关闭
function handleWrapperClick() {
props.closeOnClickModal && handleAction('close');
}

// 输入框回车触发确认
function handleInputEnter(e: KeyboardEvent) {
if (state.inputType === 'textarea') return;
e.preventDefault();
handleAction('confirm');
}

// 触发操作(确认/取消/关闭)
function handleAction(action: MessageBoxAction) {
if (isFunction(props.beforeClose)) {
props.beforeClose(action, state, () => doAction(action, state.inputValue));
} else {
doAction(action, state.inputValue);
}
}

// 关闭对话框
function handleClose() {
handleAction('close');
}
</script>

关键解释

defineOptions

  • 定义组件名称为 VrMessageBox,禁用属性继承:
    1
    2
    3
    4
    defineOptions({
    name: 'VrMessageBox',
    inheritAttrs: false,
    });

withDefaults

props 设置默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
const props = withDefaults(defineProps<MessageBoxProps>(), {
lockScroll: true,
showClose: true,
closeOnClickModal: true,
confirmButtonType: 'primary',
roundButton: false,
boxType: '',
inputValue: '',
inputPlaceholder: 'Please input...',
confirmButtonText: 'Ok',
cancelButtonText: 'Cancel',
showConfirmButton: true,
});

useZIndex

通过 useZIndex 管理对话框层级:

1
2
const { nextZIndex } = useZIndex();
const state = reactive({ ...props, zIndex: nextZIndex() });

handleAction

统一处理按钮点击和输入回车操作:

1
2
3
4
5
6
7
function handleAction(action: MessageBoxAction) {
if (isFunction(props.beforeClose)) {
props.beforeClose(action, state, () => doAction(action, state.inputValue));
} else {
doAction(action, state.inputValue);
}
}

template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<template>
<Transition
name="fade-in-linear"
@after-leave="destroy"
>
<VrOverlay
v-show="(visible as Ref).value"
:z-index="state.zIndex"
mask
>
<div
role="dialog"
class="vr-overlay-message-box"
@click="handleWrapperClick"
>
<div
ref="rootRef"
class="vr-message-box"
:class="{ 'is-center': state.center }"
@click.stop
>
<!-- 标题栏 -->
<div
v-if="!isNil(state.title)"
ref="headerRef"
class="vr-message-box__header"
:class="{ 'show-close': state.showClose }"
>
<div class="vr-message-box__title">
<VrIcon
v-if="iconComponent && state.center"
:class="{ [`vr-icon-${state.type}`]: state.type }"
:icon="iconComponent"
/>
{{ state.title }}
</div>
<button
v-if="showClose"
class="vr-message-box__header-btn"
@click.stop="handleClose"
>
<VrIcon icon="xmark" />
</button>
</div>

<!-- 内容区 -->
<div class="vr-message-box__content">
<VrIcon
v-if="iconComponent && !state.center && hasMessage"
:class="{ [`vr-icon-${state.type}`]: state.type }"
:icon="iconComponent"
/>
<div v-if="hasMessage" class="vr-message-box__message">
<slot>
<component
:is="state.showInput ? 'label' : 'p'"
:for="state.showInput ? inputId : void 0"
>
{{ state.message }}
</component>
</slot>
</div>
</div>

<!-- 输入框 -->
<div v-show="state.showInput" class="vr-message-box__input">
<VrInput
v-model="state.inputValue"
:placeholder="state.inputPlaceholder"
:type="state.inputType"
@keyup.enter="handleInputEnter"
/>
</div>

<!-- 按钮区 -->
<div class="vr-message-box__footer">
<VrButton
v-if="state.showCancelButton"
class="vr-message-box__footer-btn vr-message-box__cancel-btn"
:type="state.cancelButtonType"
:round="state.roundButton"
:loading="state.cancelButtonLoading"
@click="handleAction('cancel')"
>
{{ state.cancelButtonText }}
</VrButton>
<VrButton
v-show="state.showConfirmButton"
class="vr-message-box__footer-btn vr-message-box__confirm-btn"
:type="state.confirmButtonType"
:round="state.roundButton"
:loading="state.confirmButtonLoading"
@click="handleAction('confirm')"
>
{{ state.confirmButtonText }}
</VrButton>
</div>
</div>
</div>
</VrOverlay>
</Transition>
</template>

关键解释

Transition

  • 使用 fade-in-linear 动画,动画结束后触发 destroy 移除 DOM。

VrOverlay

  • 包裹对话框的遮罩层,支持 mask 属性控制遮罩是否可点击。

输入框与按钮

  • state.showInput 控制输入框显示(如 prompt 类型)。
  • 按钮根据 showCancelButton/showConfirmButton 动态显示。

标题与内容

  • center 模式下图标居中显示在标题栏内。
  • hasMessage 检测是否存在消息内容。

methods.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import {
createVNode,
isVNode,
ref,
render,
nextTick,
type ComponentPublicInstance,
type VNode,
type VNodeProps,
} from 'vue';
import type {
MessageBoxAction,
MessageBoxOptions,
MessageBoxData,
MessageBoxCallback,
MessageBoxProps,
IVrMessageBox,
} from './types';
import MessageBoxConstructor from './MessageBox.vue';
import { assign, each, isFunction, isObject, isString, isUndefined, set } from 'lodash-es';

const messageInstanceMap = new Map<
ComponentPublicInstance<{ doClose: () => void }>,
{
options: MessageBoxOptions;
callback: MessageBoxCallback | void;
resolve: (res: any) => void;
reject: (res: any) => void;
}
>();

// 创建对话框实例
function initInstance(props: MessageBoxProps, container: HTMLElement) {
const visible = ref(false);
const isVNodeMsg = isFunction(props?.message) || isVNode(props?.message);
const genDefaultSlot = (message: VNode | (() => VNode)) =>
isFunction(message) ? message : () => message;

const vnode = createVNode(
MessageBoxConstructor,
{ ...props, visible } as VNodeProps,
isVNodeMsg ? { default: genDefaultSlot(props.message as VNode) } : void 0
);
render(vnode, container);
document.body.appendChild(container.firstElementChild!);
return vnode.component;
}

// 创建容器
function genContainer() {
return document.createElement('div');
}

// 显示对话框
function showMessage(options: MessageBoxOptions) {
const container = genContainer();
const props: MessageBoxProps = {
...options,
doClose: () => {
vm.visible.value = false;
},
doAction: (action: MessageBoxAction, inputVal: string) => {
const currentMsg = messageInstanceMap.get(vm);
const resolve = options.showInput
? { value: inputVal, action }
: action;
nextTick(() => vm.doClose());
if (options.callback) {
options.callback(resolve);
return;
}
if (action === 'cancel' || action === 'close') {
currentMsg?.reject(action);
return;
}
currentMsg?.resolve(resolve);
},
destroy: () => {
render(null, container);
messageInstanceMap.delete(vm);
},
};

const instance = initInstance(props as MessageBoxProps, container);
const vm = instance?.proxy as ComponentPublicInstance<any>;
vm.visible.value = true;
return vm;
}

// 主函数
function MessageBox(options: MessageBoxOptions | string | VNode): Promise<any> {
let callback: MessageBoxCallback | void;
if (isString(options) || isVNode(options)) {
options = { message: options };
} else {
callback = options.callback;
}

return new Promise((resolve, reject) => {
const vm = showMessage(options);
messageInstanceMap.set(vm, { options, callback, resolve, reject });
});
}

// 预定义对话框类型
const MESSAGE_BOX_VARIANTS = ['alert', 'confirm', 'prompt'] as const;
const MESSAGE_BOX_DEFAULT_OPTS: Record<
(typeof MESSAGE_BOX_VARIANTS)[number],
Partial<MessageBoxOptions>
> = {
alert: { closeOnClickModal: false },
confirm: { showCancelButton: true },
prompt: { showCancelButton: true, showInput: true },
};

each(MESSAGE_BOX_VARIANTS, (type) => {
set(MessageBox, type, messageBoxFactory(type));
});

// 工厂函数生成不同类型的对话框
function messageBoxFactory(boxType: (typeof MESSAGE_BOX_VARIANTS)[number]) {
return (
message: string | VNode,
title: string | MessageBoxOptions,
options: MessageBoxOptions = {}
) => {
let titleOrOpts = '';
if (isObject(title)) {
options = title as MessageBoxOptions;
titleOrOpts = '';
} else if (isUndefined(title)) {
titleOrOpts = '';
} else {
titleOrOpts = title as string;
}

return MessageBox(
assign(
{
title: titleOrOpts,
message,
type: '',
boxType,
...MESSAGE_BOX_DEFAULT_OPTS[boxType],
},
options
)
);
};
}

// 关闭所有实例
set(MessageBox, 'close', () => {
messageInstanceMap.forEach((_, vm) => {
vm.doClose();
});
messageInstanceMap.clear();
});

export default MessageBox as IVrMessageBox;

关键解释

messageInstanceMap

存储所有对话框实例,支持批量操作:

1
2
3
4
5
6
7
8
const messageInstanceMap = new Map<
ComponentPublicInstance<{ doClose: () => void }>,
{
options: MessageBoxOptions;
resolve: (res: any) => void;
reject: (res: any) => void;
}
>();

showMessage

  • 根据 options 生成对话框实例。
  • doAction 处理用户操作并触发 resolve/reject

预定义对话框类型

  • alert:默认禁用遮罩层关闭。
  • confirm:默认显示取消按钮。
  • prompt:默认显示输入框和取消按钮。

types.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { VNode } from 'vue';
import { messageType } from '../Message/types';
import { ButtonType } from '../Button/types';

export type MessageBoxAction = 'confirm' | 'cancel' | 'close';
export type MessageBoxType = '' | 'prompt' | 'alert' | 'confirm';
export type MessageBoxCallback = (
action: MessageBoxAction | { value: string; action: MessageBoxAction }
) => void;

export type MessageBoxInputData = {
value: string;
action: MessageBoxAction;
};
export type MessageBoxData = MessageBoxInputData & MessageBoxAction;

export interface MessageBoxOptions {
title?: string;
message?: string | VNode | (() => VNode);
type?: messageType;
boxType?: MessageBoxType;
icon?: string;
callback?: MessageBoxCallback;
showClose?: boolean;
showInput?: boolean;
showCancelButton?: boolean;
showConfirmButton?: boolean;
cancelButtonText?: string;
confirmButtonText?: string;
cancelButtonLoading?: boolean;
confirmButtonLoading?: boolean;
cancelButtonDisabled?: boolean;
confirmButtonDisabled?: boolean;
cancelButtonType?: ButtonType;
confirmButtonType?: ButtonType;
roundButton?: boolean;
center?: boolean;
lockScroll?: boolean;
closeOnClickModal?: boolean;
beforeClose?: (
action: MessageBoxAction,
instance: any,
done: () => void
) => void;
}

关键解释

MessageBoxOptions

  • boxType:指定对话框类型(alert/confirm/prompt)。
  • showInput:是否显示输入框(通常用于 prompt 类型)。
  • beforeClose:关闭前的钩子函数,需调用 done() 完成关闭。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script setup>
import { MessageBox } from 'veyra-ui';

// 基础 Alert
MessageBox.alert('操作成功', '提示', {
type: 'success',
});

// 确认对话框
MessageBox.confirm('确定删除该文件?', '警告', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).then(() => {
console.log('已确认');
});

// 输入对话框
MessageBox.prompt('请输入用户名', '输入框', {
inputPlaceholder: '请输入...',
inputType: 'text',
}).then(({ value, action }) => {
if (action === 'confirm') {
console.log('输入内容:', value);
}
});

// 关闭所有对话框
MessageBox.close();
</script>

核心 API

Props

属性 类型 默认值 说明
title string - 对话框标题
message `string VNode (() => VNode)`
type messageType '' 消息类型(info/success/warning/danger/error
boxType MessageBoxType '' 对话框类型(alert/confirm/prompt
showClose boolean true 是否显示关闭按钮
showInput boolean false 是否显示输入框(prompt 类型默认开启)
showCancelButton boolean false 是否显示取消按钮
showConfirmButton boolean true 是否显示确认按钮
confirmButtonText string 'Ok' 确认按钮文字
cancelButtonText string 'Cancel' 取消按钮文字
lockScroll boolean true 是否锁定页面滚动
closeOnClickModal boolean true 是否允许点击遮罩层关闭

方法

方法名 参数 说明
MessageBox() options: MessageBoxOptions 显示自定义对话框
alert() `message: string VNode, title?: string, options?: MessageBoxOptions`
confirm() `message: string VNode, title?: string, options?: MessageBoxOptions`
prompt() `message: string VNode, title?: string, options?: MessageBoxOptions`
close() - 关闭所有对话框

回调参数

场景 返回值类型 说明
alert string ('confirm'/'close') 用户操作类型
confirm/prompt { value: string; action: 'confirm' }'cancel'/'close' 包含输入值和操作类型(prompt 时返回输入值)

注意事项

  1. 类型与默认配置

    • alert 默认禁用遮罩层关闭,confirm 默认显示取消按钮,prompt 默认显示输入框和取消按钮。
  2. 异步处理

    • MessageBox 返回 Promise,需通过 .then()await 处理用户操作结果。
  3. 输入验证

    • prompt 类型可通过 beforeClose 钩子验证输入内容,阻止无效提交。
  4. 样式扩展

    • 按钮样式支持通过 confirmButtonType/cancelButtonType 指定类型(如 'primary'/'danger')。
  5. 锁屏滚动

    • lockScroll 启用时,页面滚动将被禁用,对话框居中显示。