对话框组件
# 自定义可拖拽对话框封装
# 一、封装自定义可拖拽对话框组件
在实际的项目中,经常需要对话框组件具备灵活的配置,满足各种业务需求。本文将详细介绍如何在 Vue 3
中使用 Element Plus
封装一个可配置、可拖拽的对话框组件,并且通过 v-model
实现显示状态的双向绑定。这个对话框组件支持如下功能:
- 支持设置标题、宽度、高度;
- 是否允许拖拽;
- 是否全屏显示;
- 点击空白区域关闭对话框;
- 提供自定义内容和底部插槽;
- 对外暴露事件接口(
confirm
、cancel
、update:visible
)。
# 1.1 组件基本结构
自定义对话框组件的基础是 Element Plus
的 el-dialog
组件。我们将这个对话框进行扩展,并允许传递多个配置选项,如 title
、width
、draggable
等。对话框的显示与隐藏通过 v-model
实现,父组件和子组件可以双向同步。
<template>
<el-dialog
v-model="isVisible" <!-- 通过v-model绑定显示状态 -->
:title="dialogTitle" <!-- 对话框标题 -->
:width="dialogWidth" <!-- 对话框宽度 -->
:custom-class="customClass" <!-- 自定义类 -->
:draggable="draggable" <!-- 是否允许拖拽 -->
:fullscreen="fullscreen" <!-- 是否全屏 -->
:append-to-body="appendToBody" <!-- 是否追加到body -->
:close-on-click-modal="closeOnClickModal" <!-- 是否点击空白处关闭 -->
@open="handleOpen" <!-- 对话框打开时的事件 -->
@close="handleClose" <!-- 对话框关闭时的事件 -->
:before-close="beforeClose" <!-- 自定义关闭前的逻辑 -->
>
<!-- 对话框内容区域,支持自定义插槽 -->
<div :style="contentStyle" class="dialog-content">
<slot></slot> <!-- 用于用户自定义内容的插槽 -->
</div>
<!-- 底部区域,支持用户自定义按钮,默认显示“取消”和“确定”按钮 -->
<template #footer>
<slot name="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmDialog">确定</el-button>
</slot>
</template>
</el-dialog>
</template>
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
# 1.2 组件的核心逻辑
在 script
部分,我们定义了组件的多个 props
,这些属性控制对话框的显示、宽度、是否可拖拽等。同时,对话框的 confirm
、cancel
、update:visible
事件通过 emit
向父组件传递,确保组件与外部的交互。
<script setup>
import { ref, computed, watch } from 'vue';
// 组件的属性 (props)
const props = defineProps({
visible: {
type: Boolean,
required: true, // 控制对话框显示状态
},
title: {
type: String,
default: '对话框标题', // 对话框标题
},
width: {
type: String,
default: '50%', // 对话框默认宽度
},
maxHeight: {
type: String,
default: '400px', // 对话框最大高度
},
draggable: {
type: Boolean,
default: false, // 是否允许拖拽
},
customClass: {
type: String,
default: '', // 自定义类
},
fullscreen: {
type: Boolean,
default: false, // 是否全屏显示
},
appendToBody: {
type: Boolean,
default: true, // 是否追加到body
},
closeOnClickModal: {
type: Boolean,
default: false, // 是否点击空白处关闭对话框
}
});
// 定义 emits,用于父组件和子组件的通信
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
// 本地响应式变量,保持与props同步
const isVisible = ref(props.visible);
const dialogTitle = ref(props.title);
const dialogWidth = ref(props.width);
// 监听 visible 属性,确保父组件和子组件状态同步
watch(() => props.visible, (newValue) => {
isVisible.value = newValue;
});
// 动态计算内容区域样式,控制内容最大高度并支持滚动
const contentStyle = computed(() => ({
maxHeight: props.height, // 根据传入的高度或默认值设置内容区域的最大高度
overflowY: 'auto', // 当内容超出高度时显示滚动条
}));
// 自定义关闭前的逻辑
const beforeClose = (done) => {
isVisible.value = false; // 隐藏对话框
emit('update:visible', false); // 触发父组件更新visible状态
done(); // 调用done完成关闭动作
};
// 关闭对话框的逻辑
const closeDialog = () => {
isVisible.value = false; // 设置对话框不可见
emit('update:visible', false); // 向父组件发送事件
emit('cancel'); // 触发取消事件
};
// 确认按钮的逻辑
const confirmDialog = () => {
emit('confirm'); // 触发确认事件
};
// 处理对话框打开时的逻辑
const handleOpen = () => {
console.log('对话框已打开');
};
// 处理对话框关闭时的逻辑
const handleClose = () => {
console.log('对话框已关闭');
};
</script>
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
# 1.3 对外暴露的属性和事件
# Props(属性)
visible
:控制对话框的显示状态,父组件通过v-model
双向绑定。title
:对话框标题,默认值为“对话框标题”。width
:对话框宽度,默认值为“50%”。height
:对话框内容区域的最大高度,默认值为“400px”。draggable
:是否允许拖拽,默认为false
。customClass
:自定义对话框的样式类。fullscreen
:是否全屏显示对话框,默认为false
。appendToBody
:是否将对话框追加到 body 标签下,默认为true
。closeOnClickModal
:是否点击空白处关闭对话框,默认为true
。
# Emits(事件)
update:visible
:更新对话框的显示状态,确保父组件和子组件同步。confirm
:点击确认按钮时触发,父组件可通过监听此事件执行逻辑。cancel
:点击取消按钮或关闭对话框时触发,父组件可通过监听此事件执行逻辑。
为什么 appendToBody 属性?
- 控制对话框的渲染位置:当
appendToBody
设置为true
时,对话框会被渲染并插入到body
标签下,避免被父组件或外层容器的样式(例如overflow:hidden
)限制。 - 解决层级问题:在某些情况下,如果对话框被嵌套在父容器中,可能会因为父容器的
z-index
或布局问题导致对话框无法正确显示在最前方。将对话框追加到body
中,可以确保它拥有正确的层级。
# 1.4 样式自定义
为了增强对话框的用户体验,我们对对话框的滚动条样式进行了自定义,使其更具美观性。
<style scoped>
/* 自定义对话框样式 */
.custom-dialog {
border-radius: 8px;
background-color: white;
padding: 20px;
}
/* 自定义滚动条样式 */
.dialog-content::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dialog-content::-webkit-scrollbar-thumb {
background-color: #bfbfbf;
border-radius: 10px;
}
.dialog-content::-webkit-scrollbar-track {
background-color: #f0f0f0;
}
/* 确保当内容过多时出现滚动条 */
.dialog-content {
padding: 20px;
scrollbar-width: thin;
scrollbar-color: #bfbfbf #f0f0f0;
/* 添加上下分隔线 */
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
}
</style>
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
# 1.5 完整代码
<template>
<el-dialog
v-model="isVisible"
:title="dialogTitle"
:width="dialogWidth"
:custom-class="customClass"
:draggable="draggable"
:fullscreen="fullscreen"
:append-to-body="appendToBody"
:close-on-click-modal="closeOnClickModal"
@open="handleOpen"
@close="handleClose"
:before-close="beforeClose"
>
<!-- 对话框内容区域,提供滚动条支持 -->
<div :style="contentStyle" class="dialog-content">
<slot></slot> <!-- 用户自定义内容 -->
</div>
<!-- 底部按钮,支持自定义插槽,默认显示 "取消" 和 "确定" -->
<template #footer>
<slot name="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmDialog">确定</el-button>
</slot>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
// 组件的属性(props)
const props = defineProps({
visible: {
type: Boolean,
required: true, // 控制对话框显示状态
},
title: {
type: String,
default: '对话框标题', // 对话框标题
},
width: {
type: String,
default: '50%', // 对话框宽度,默认50%
},
maxHeight: {
type: String,
default: '400px', // 对话框高度,默认400px
},
draggable: {
type: Boolean,
default: false, // 是否允许拖拽
},
customClass: {
type: String,
default: '', // 自定义类
},
fullscreen: {
type: Boolean,
default: false, // 是否全屏
},
appendToBody: {
type: Boolean,
default: true, // 是否将对话框追加到body
},
closeOnClickModal: {
type: Boolean,
default: false, // 新增:是否点击空白处关闭
}
});
// 定义 emits,用于父组件和子组件通信
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
// 本地响应式变量
const isVisible = ref(props.visible);
const dialogTitle = ref(props.title);
const dialogWidth = ref(props.width);
// 监听 visible 属性,确保父组件与子组件的同步状态
watch(() => props.visible, (newValue) => {
isVisible.value = newValue;
});
// 动态计算内容区域的样式,设置高度并允许滚动
const contentStyle = computed(() => ({
maxHeight: props.maxHeight, // 使用传入或默认的高度作为最大高度
overflowY: 'auto', // 当内容超出高度时启用滚动条
}));
// beforeClose事件,用于捕获所有关闭操作
const beforeClose = (done) => {
isVisible.value = false; // 手动设置对话框为不可见
emit('update:visible', false); // 确保父组件同步状态
done(); // 调用done关闭对话框
};
// 关闭对话框的方法
const closeDialog = () => {
isVisible.value = false; // 设置对话框为不可见
emit('update:visible', false); // 触发关闭事件
emit('cancel'); // 触发取消事件
};
// 确认按钮的逻辑
const confirmDialog = () => {
emit('confirm'); // 触发确认事件
};
// 打开对话框时的操作
const handleOpen = () => {
console.log('对话框已打开');
};
// 关闭对话框时的操作
const handleClose = () => {
console.log('对话框已关闭');
};
</script>
<style scoped>
/* 自定义对话框样式 */
.custom-dialog {
border-radius: 8px;
background-color: white;
padding: 20px;
}
/* 自定义滚动条 */
.dialog-content::-webkit-scrollbar {
width: 8px; /* 滚动条宽度 */
height: 8px;
}
.dialog-content::-webkit-scrollbar-thumb {
background-color: #bfbfbf; /* 滚动条滑块颜色 */
border-radius: 10px; /* 圆角滑块 */
}
.dialog-content::-webkit-scrollbar-track {
background-color: #f0f0f0; /* 滚动条轨道背景 */
}
/* 设置滚动区域的样式,确保当内容过多时出现滚动条 */
.dialog-content {
padding: 20px;
scrollbar-width: thin; /* Firefox 滚动条宽度 */
scrollbar-color: #bfbfbf #f0f0f0; /* Firefox 滚动条颜色 */
/* 添加上下分隔线 */
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
}
</style>
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
# 二、自定义对话框组件插槽的使用
在这个对话框组件中,我们定义了两个插槽:
- 默认插槽:用于自定义对话框的主体内容。
- 命名插槽
footer
:用于自定义对话框底部的按钮区域。
通过这些插槽,开发者可以完全控制对话框的内部布局和交互逻辑。
# 插槽的定义
# 1. 默认插槽
<div :style="contentStyle" class="dialog-content">
<slot></slot> <!-- 默认插槽,用于自定义对话框内容 -->
</div>
2
3
- 说明:默认插槽位于对话框的主要内容区域内。如果父组件不指定任何内容,对话框主体部分将为空;如果父组件提供内容,该内容将显示在对话框主体部分。
- 使用方式:父组件可以直接将自定义内容插入到
CustomDialog
组件的标签内,默认插槽会将这些内容显示出来。
# 2. 命名插槽 footer
<template #footer>
<slot name="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmDialog">确定</el-button>
</slot>
</template>
2
3
4
5
6
- 说明:命名插槽
footer
主要用于自定义对话框底部的操作按钮。如果父组件未提供footer
插槽的内容,组件将默认显示 "取消" 和 "确定" 按钮。如果父组件提供了footer
插槽的内容,那么父组件提供的内容将替代默认的按钮。 - 使用方式:父组件可以使用
<template #footer>
提供自定义的按钮区域。
# 父组件中插槽的使用
# 1. 使用默认插槽
父组件直接在 CustomDialog
标签内插入自定义内容,默认插槽会自动显示这些内容。例如:
<template>
<CustomDialog
v-model:visible="isDialogVisible"
title="自定义对话框"
width="60%"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<!-- 这里是插入到默认插槽中的内容 -->
<p>这是对话框的自定义内容,可以是任何 HTML 元素。</p>
</CustomDialog>
</template>
2
3
4
5
6
7
8
9
10
11
12
在这个示例中,<p>
标签内的文本将作为对话框的主体内容显示。
# 2. 使用命名插槽 footer
如果你希望自定义对话框底部的按钮,而不是使用默认的 "取消" 和 "确定" 按钮,你可以使用命名插槽 footer
来插入自己的内容:
<template>
<CustomDialog
v-model:visible="isDialogVisible"
title="自定义对话框"
width="60%"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<p>这是对话框的自定义内容。</p>
<!-- 使用命名插槽自定义底部按钮 -->
<template #footer>
<el-button type="danger" @click="closeDialog">关闭</el-button>
<el-button type="success" @click="handleCustomConfirm">提交</el-button>
</template>
</CustomDialog>
</template>
<script setup>
const handleCustomConfirm = () => {
console.log('自定义确认操作');
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在这个示例中,#footer
插槽被用于提供自定义的按钮。父组件可以根据自己的需求定义按钮样式和功能逻辑。
插槽总结
- 默认插槽:可以插入任何内容,用于自定义对话框主体部分。
- 命名插槽
footer
:可用于自定义对话框底部的按钮区域,如果未使用此插槽,将显示默认的 "取消" 和 "确定" 按钮。
# 三、组件的使用
# 3.1 在父组件中使用自定义对话框
封装好的对话框组件可以在父组件中轻松使用,父组件通过 v-model
控制其显示和隐藏,同时可以传递 title
、width
等属性。对话框的 confirm
和 cancel
事件可以用于处理用户的操作。
<template>
<div>
<!-- 触发打开对话框 -->
<el-button type="primary" @click="openDialog">打开自定义对话框</el-button>
<!-- 使用自定义封装的对话框组件 -->
<CustomDialog
v-model:visible="isDialogVisible"
title="自定义标题"
width="60%"
maxHeight="500px"
:draggable="true"
custom-class="custom-dialog"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<!-- 插入到对话框中的自定义内容 -->
<p>这里是对话框内容。</p>
</CustomDialog>
</div>
</template>
<script setup>
import { ref } from 'vue';
import CustomDialog from '@/components/CustomDialog.vue'; // 引入封装好的对话框组件
// 控制对话框是否可见
const isDialogVisible = ref(false);
// 打开对话框
const openDialog = () => {
isDialogVisible.value = true;
};
// 处理确认事件
const handleConfirm = () => {
console.log('确认操作');
isDialogVisible.value = false; // 关闭对话框
};
// 处理取消事件
const handleCancel = () => {
console.log('取消操作');
isDialogVisible.value = false; // 关闭对话框
};
</script>
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
# 3.2 父组件中的事件处理
在父组件中,监听自定义对话框的 confirm
和 cancel
事件,分别处理确认和取消操作。你可以在事件处理函数中执行任何逻辑,如表单提交、数据保存等操作。