UI 框架-Dialog 组件

做好了 Button,知道有遮罩层这么一回事,就可以做 Dialog 组件了


需求分析

惯例先行需求分析

  1. 默认是不可见的,在用户触发某个动作后变为可见
  2. 自带白板卡片,分为上中下三个区域,分别放置标题、内容、操作
  3. 有两个基本操作:确定、取消
  4. 卡片后应放置淡黑色遮罩层,遮住原本网页内容
  5. 可以自定义是否允许取消
  6. 右上角提供小叉叉来允许关闭
  7. 允许通过点击遮罩层来关闭

那么可以整理出以下参数表格

参数 含义 类型 可选值 默认值
visible 是否可见 boolean false / true false
title 标题 string 任意字符串 必填
ok 确定回调 ()=>boolean 返回 boolean 的函数 ()=>true
cancel 取消回调 ()=>boolean 返回 boolean 的函数 ()=>true

第 3 条,可以通过设置返回值为 true 来允许事件发生,反之不允许

第 5 条,可以通过设置返回 false 来取消事件

第 6/7 条,直接与取消功能共用函数即可

骨架

可以复用已经制作好的 Button 组件

容易得到如下骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<template v-if="visible">
<div class="laby-dialog-overlay" @click="close"></div>
<div class="laby-dialog">
<header class="laby-dialog-header">
{{ title }}
<span class="laby-dialog-close" @click="close"></span>
</header>
<div class="laby-dialog-divider" />
<main class="laby-dialog-main">
<slot></slot>
</main>
<div class="laby-dialog-divider" />
<footer class="laby-dialog-footer">
<laby-button level="plain" @click="close">取消</laby-button>
&nbsp;&nbsp;&nbsp;&nbsp;
<laby-button @click="task" :loading="loading">确定</laby-button>
</footer>
</div>
</template>
</template>

但是我们一般不希望对话框弹窗在 DOM 树上的位置是非常下级的元素的子元素,而希望是 body 的直接子元素,那么我们可以使用 vue3 的 teleport 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<template v-if="visible">
<teleport to="body">
<div class="laby-dialog-overlay" @click="close"></div>
<div class="laby-dialog">
<header class="laby-dialog-header">
{{ title }}
<span class="laby-dialog-close" @click="close"></span>
</header>
<div class="laby-dialog-divider" />
<main class="laby-dialog-main">
<slot></slot>
</main>
<div class="laby-dialog-divider" />
<footer class="laby-dialog-footer">
<laby-button level="plain" @click="close">取消</laby-button>
&nbsp;&nbsp;&nbsp;&nbsp;
<laby-button @click="task" :loading="loading">确定</laby-button>
</footer>
</div>
</teleport>
</template>
</template>

这样,在渲染时,teleport 内部的内容就会出现在 body 的子级

功能

先 ts 声明

1
2
3
4
5
6
7
declare const props: {
visible: boolean;
title: string;
ok: () => boolean;
cancel: () => boolean;
};
declare const context: SetupContext;

然后在 export default 中,写入我们的参数

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
export default {
name: "LabyDialog",
props: {
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
required: true,
},
ok: {
type: Function,
default: () => {
return true;
},
},
cancel: {
type: Function,
default: () => {
return true;
},
},
},
components: {
LabyButton,
},
setup(){}
};

再补全 setup 方法,此处选用 Promise 制造提交等待响应的感觉

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
setup(props, context) {
const loading = ref(false);
const close = () => {
if (loading.value) {
return;
}
new Promise((resolve, reject) => {
resolve(props.cancel());
}).then((result) => {
if (result !== false) {
context.emit("update:visible", false);
}
});
};
const task = () => {
new Promise((resolve, reject) => {
loading.value = true;
resolve(props.ok());
}).then((result) => {
if (result === true) {
loading.value = false;
context.emit("update:visible", false);
}
});
};
return { loading, close, task };
}

样式表

然后补全样式表

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
.laby-dialog-overlay {
z-index: 20;
position: fixed;
left: 0;
top: 0;
background: fade-out($color: #000000, $amount: 0.7);
width: 100vw;
height: 100vh;
}
.laby-dialog {
z-index: 20;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
min-width: 300px;
min-height: 200px;
border-radius: 8px;
background: white;
display: flex;
flex-direction: column;
> * {
padding: 8px;
}
> .laby-dialog-divider {
border: 1px solid #ffb5dc;
padding: 0;
}
> .laby-dialog-header {
display: flex;
justify-content: space-between;
> .laby-dialog-close {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
cursor: pointer;
&::before,
&::after {
content: "";
position: absolute;
height: 1px;
background: black;
width: 100%;
top: 50%;
left: 50%;
}
&::before {
transform: translate(-50%, -50%) rotate(-45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(45deg);
}
}
}
> .laby-dialog-main {
flex-grow: 1;
background: white;
}
> .laby-dialog-footer {
display: flex;
justify-content: flex-end;
}
}

测试

引入文档页看一下

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
// src/components/Dialog.vue
<template>
<div>Dialog 文档</div>
<laby-button @click="visible = true">打开对话框</laby-button>
<laby-dialog v-model:visible="visible" title="标题" :ok="ok" :cancel="cancel">
<span> 内容 </span>
</laby-dialog>
</template>
<script lang="ts">
import LabyButton from "../lib/Button.vue";
import LabyDialog from "../lib/Dialog.vue";

import { ref } from "vue";
export default {
components: {
LabyButton,
LabyDialog,
},
setup() {
const visible = ref(false);
const ok = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("ok");
resolve(true);
}, 1000);
});
};
const cancel = () => {
console.log("cancel");
};
return { visible, ok, cancel };
},
};
</script>

效果如图

效果图

一行代码打开

有时候还会想,能不能不用组件式,直接用函数生成一个呢

其实是可以的,只要使用 vue3 提供的 createApph 函数就可以做到了

此处只给出一个示例,不多介绍

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
// createDialog.ts
import { createApp, h } from 'vue'
import LabyDialog from './Dialog.vue'
export const createDialog = options => {
const { title, content, ok, cancel } = options
const div = document.createElement('div')
document.body.appendChild(div)
const close = () => {
app.unmount(div)
div.remove()
}
const app = createApp({
render() {
return h(LabyDialog, {
visible: true,
'onUpdate:visible': newVisible => {
if (newVisible === false) {
close();
}
},
title,
ok, cancel
}, { default() { return content } })
}
})
app.mount(div)
}

在需要的地方使用

1
import {createDialog} from './createDialog.ts'

引入即可

该函数要求传入一个 options 对象,该对象包含 title, content, ok, cancel 等 4 个部分,content 指代组件式中的插槽,其余含义见需求分析

然后使用 h 函数渲染新 app 中的内容,并作为参数传入 createApp 函数用以创建新的 app,最后挂载到 DOM 树上即可


感谢阅读

--It's the end.Thanks for your read.--