通用组件实现
# 前言
主要介绍下Alert
、Avatar
、 Backtop
、Badge
、Divider
、Card
、Message
的实现原理,不会逐行解释,毕竟相同的组件不同的人的实现思路是不一样的。
# Alert
先回顾下,Alert
组件的使用方式。
之前的文章已经提到了每个组件的目录结构是这样的:
├── index.js
└── main.vue
2
index.js
文件我们就不加赘述了,主要说一下 main.vue
的定义:
<template>
<div class="ml-alert" :class="[typeClass]" v-show="visible">
<i class="ml-alert__icon" :class="[iconClass]" v-if="showIcon"></i>
<div class="ml-alert__content">
<span class="ml-alert__title" v-if="title || $slots.title">
<slot name="title">{{ title }}</slot>
</span>
<i
class="ml-alert__closebtn"
:class="{
'ml-icon-close': closeText === '',
}"
v-show="closable"
@click="close()"
>{{ closeText }}</i
>
</div>
</div>
</template>
<script>
const TYPE_CLASSES_MAP = {
success: "ml-icon-success",
warning: "ml-icon-warning",
error: "ml-icon-error",
};
export default {
name: "MlAlert",
props: {
title: { // 自定义展示的文案
type: String,
default: "",
},
type: {
type: String, // success、warning、error
default: "info",
},
closable: { // 是否展示关闭按钮
type: Boolean,
default: true,
},
closeText: { // 关闭按钮的提示文字
type: String,
default: "",
},
showIcon: Boolean, // 是否展示icon
center: Boolean, // 是否居中展示
},
data() {
return {
visible: true,
};
},
methods: {
// 关闭Card实例
close() {
this.visible = false;
this.$emit("close");
},
},
computed: {
// 根据 type 动态计算类名
typeClass() {
return `ml-alert--${this.type}`;
},
// 根据 type 动态计算 icon 类名
iconClass() {
return TYPE_CLASSES_MAP[this.type] || "ml-icon-info";
},
},
};
</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
结构很简单,如下图:
结构主要分为三个部分:
- 红色框是最外层的父容器,主要是控制
Alert
组件的显示和隐藏 - 蓝色框是 icon 放置区域
- 黄色框是 自定义内容区域
再看看样式,样式文件在 src/theme/alert.scss
目录下:
@import "mixins/mixins";
@import "common/var";
@include b(alert) {
width: 100%;
padding: $--alert-padding;
margin: 0;
box-sizing: border-box;
border-radius: $--alert-border-radius;
position: relative;
color: $--color-success;
background-color: $--alert-success-color;
overflow: hidden;
opacity: 1;
display: flex;
align-items: center;
transition: opacity .2s;
& [class*="ml-icon-"] {
& + div {
margin-left: 5px;
}
}
@include m(success) {
&.is-light {
background-color: $--alert-success-color;
color: $--color-success;
.ml-alert__description {
color: $--color-success;
}
}
}
@include m(info) {
&.is-light {
background-color: $--alert-info-color;
color: $--color-info;
}
&.is-dark {
background-color: $--color-info;
color: $--color-white;
}
.el-alert__description {
color: $--color-info;
}
}
@include m(warning) {
&.is-light {
background-color: $--alert-warning-color;
color: $--color-warning;
.el-alert__description {
color: $--color-warning;
}
}
&.is-dark {
background-color: $--color-warning;
color: $--color-white;
}
}
@include m(error) {
&.is-light {
background-color: $--alert-danger-color;
color: $--color-danger;
.el-alert__description {
color: $--color-danger;
}
}
&.is-dark {
background-color: $--color-danger;
color: $--color-white;
}
}
@include e(icon) {
font-size: $--alert-icon-size;
width: $--alert-icon-size;
}
@include e(title) {
font-size: $--alert-title-font-size;
line-height: 18px;
@include when(bold) {
font-weight: bold;
}
}
@include e(closebtn) {
font-size: $--alert-close-font-size;
opacity: 1;
position: absolute;
top: 12px;
right: 15px;
cursor: pointer;
}
}
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
样式需要重点关注的是 box-sizing: border-box;
这句代码。在css默认的盒模型 (opens new window)中,你对一个元素所设置的 width
与 height
只会应用到这个元素的内容区。如果这个元素有任何的 border
或 padding
,绘制到屏幕上时的盒子宽度和高度会加上设置的边框和内边距值。这意味着当你调整一个元素的宽度和高度时需要时刻注意到这个元素的边框和内边距。当我们实现响应式布局时,这个特点尤其烦人。所以这里,将 box-sizing
设置为 border-box
,也就是明确告诉浏览器:
你想要设置的边框和内边距的值是包含在width内的。也就是说,如果你将一个元素的width设为100px,那么这100px会包含它的border和padding,内容区的实际宽度是width减去(border + padding)的值。大多数情况下,这使得我们更容易地设定一个元素的宽高。
注意
border-box不包含margin
# Avatar
Avatar
组件用于头像、图标的展示。
我们看下是如何实现的,main.vue
的定义:
<template>
<span :class="[avatarClass]" :style="[sizeStyle]">
<img
v-if="isImageExist && src"
:src="src"
:onError="handleError"
:alt="alt"
:style="[{ 'object-fit': fit }]"
/>
<i v-if="icon && !isImageExist && !src" :class="icon" />
<slot v-else />
</span>
</template>
<script>
export default {
name: "MlAvatar",
props: {
shape: {
// 设置图像的形状
type: String,
default: "circle",
validator(val) {
return ["circle", "square"].includes(val);
},
},
size: [Number, String], // 头像的大小
icon: String, // 设置图标的头像类型
src: String, // 图片头像的资源地址
alt: String, // 描述图像的替换文本
error: Function, // 图像加载失败回调
fit: {
// 同原生的fit属性
type: String,
default: "cover",
},
},
data() {
return {
isImageExist: true, // 图像是否存在
};
},
methods: {
// 图片加载错误处理函数
handleError() {
const { error } = this;
const errorFlag = error ? error() : undefined;
if (errorFlag !== false) {
this.isImageExist = false;
}
},
},
computed: {
// 根据icon、shape 计算样式名
avatarClass() {
const { icon, shape } = this;
let classList = ["ml-avatar"];
if (icon) {
classList.push("ml-avatar--icon");
}
if (shape) {
classList.push(`ml-avatar--${shape}`);
}
return classList;
},
// 头像 或者 图标 的尺寸
sizeStyle() {
const { size } = this;
return typeof size === "number"
? {
height: `${size || 60}px`,
width: `${size || 60}px`,
lineHeight: `${size || 60}px`,
}
: {};
},
},
};
</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
主要就是展示图片或者图标,然后用了一个默认插槽,方便用户自定义其他展示的内容。值得注意的是 isImageExist
函数,这个函数是处理图片加载错误时触发用户定义的处理函数。
再看下样式文件:
@import "mixins/mixins";
@import "common/var";
@include b(avatar) {
display: inline-block;
box-sizing: border-box;
text-align: center;
overflow: hidden;
color: $--avatar-font-color;
width: $--avatar-large-size;
height: $--avatar-large-size;
background: $--avatar-background-color;
line-height: $--avatar-large-size;
font-size: $--avatar-text-font-size;
> img {
display: block;
height: 100%;
vertical-align: middle;
}
@include m(circle) {
border-radius: 50%;
}
@include m(square) {
border-radius: $--avatar-border-radius;
}
@include m(icon) {
font-size: $--avatar-icon-font-size;
}
}
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
# Badge
Badge
组件用于展示消息数量或者按钮的状态,看看基本使用:
main.vue
中的定义:
<template>
<div class="ml-badge">
<slot></slot>
<transition name="ml-zoom-in-center">
<sup
v-show="!hidden && (content || content === 0 || isDot)"
v-text="content"
class="ml-badge__content"
:class="[
'ml-badge__content--' + type,
{
'is-fixed': $slots.default,
'is-dot': isDot,
},
]"
>
</sup>
</transition>
</div>
</template>
<script>
export default {
name: "MlBadge",
props: {
value: [String, Number], // 右上角展示的值
max: Number, // 展示的最大值
isDot: Boolean, // 展示小圆点
hidden: Boolean, // 是否隐藏
type: { // 展示的状态
type: String,
validator(val) {
return (
["primary", "success", "warning", "info", "danger"].indexOf(val) > -1
);
},
},
},
computed: {
// badge展示的内容
// 如果传入了最大值,超过最大值就默认展示最大值
content() {
if (this.isDot) return;
const value = this.value;
const max = this.max;
if (typeof value === "number" && typeof max === "number") {
return max < value ? `${max}+` : value;
}
return value;
},
},
};
</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
看看样式文件:
@import "mixins/mixins";
@import "common/var";
@include b(badge) {
position: relative;
vertical-align: middle;
display: inline-block;
@include e(content) {
background-color: $--badge-background-color;
border-radius: $--badge-radius;
color: $--color-white;
display: inline-block;
font-size: $--badge-font-size;
height: $--badge-size;
line-height: $--badge-size;
padding: 0 $--badge-padding;
text-align: center;
white-space: nowrap;
border: 1px solid $--color-white;
@include when(fixed) {
position: absolute;
top: 0;
right: #{1 + $--badge-size / 2};
transform: translateY(-50%) translateX(100%);
@include when(dot) {
right: 5px;
}
}
@include when(dot) {
height: 8px;
width: 8px;
padding: 0;
right: 0;
border-radius: 50%;
}
@each $type in (primary, success, warning, info, danger) {
@include m($type) {
@if $type == primary {
background-color: $--color-primary;
} @else if $type == success {
background-color: $--color-success;
} @else if $type == warning {
background-color: $--color-warning;
} @else if $type == info {
background-color: $--color-info;
} @else {
background-color: $--color-danger;
}
}
}
}
}
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
# Backtop
Backtop
组件是用于回到顶部,看看基本使用:
<template>
<ml-backtop></ml-backtop>
</template>
<script>
export default {
data () {}
}
</script>
<style>
</style>
2
3
4
5
6
7
8
9
10
11
12
main.vue
的定义:
<template>
<ml-badge></ml-badge>
</template>
<script>
// 使用throttle防抖
import { throttle } from "throttle-debounce";
// 使用贝塞尔曲线 使得动画更加平滑
const cubic = (value) => Math.pow(value, 3);
const easeInOutCubic = (value) =>
value < 0.5 ? cubic(value * 2) / 2 : 1 - cubic((1 - value) * 2) / 2;
export default {
name: "MlBacktop",
props: {
visibilityHeight: {
type: Number, // 返回按钮出现的高度
default: 200,
},
target: [String], // 触发滚动的对象
right: {
type: Number,
default: 40, // 右边距
},
bottom: {
type: Number,
default: 40, // 底边距
},
},
data() {
return {
el: null,
container: null,
visible: false,
};
},
computed: {
styleBottom() {
return `${this.bottom}px`;
},
styleRight() {
return `${this.right}px`;
},
},
mounted() {
// 初始化 获取监听的目标元素
this.init();
// 防抖 避免多次点击
this.throttledScrollHandler = throttle(300, this.onScroll);
// 监听滚动事件
this.container.addEventListener("scroll", this.throttledScrollHandler);
},
methods: {
init() {
this.container = document;
this.el = document.documentElement;
if (this.target) {
this.el = document.querySelector(this.target);
if (!this.el) {
throw new Error(`target is not existed: ${this.target}`);
}
this.container = this.el;
}
},
// 根据滚动高度 判断backtop按钮是否显示
onScroll() {
const scrollTop = this.el.scrollTop;
this.visible = scrollTop >= this.visibilityHeight;
},
// 点击回到顶部
handleClick(e) {
this.scrollToTop();
this.$emit("click", e);
},
// 回到顶部的逻辑
// 实际上就是将 元素的 scrollTop 设置为 0
scrollToTop() {
const el = this.el;
const beginTime = Date.now();
const beginValue = el.scrollTop;
const rAF =
window.requestAnimationFrame || ((func) => setTimeout(func, 16));
// requestAnimationFrame 这里用来执行动画 更加流畅
const frameFunc = () => {
// 动画执行时间为500ms
const progress = (Date.now() - beginTime) / 500;
if (progress < 1) {
el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
rAF(frameFunc);
} else {
el.scrollTop = 0;
}
};
rAF(frameFunc);
},
},
// 页面销毁时 移除滚动事件的监听
beforeDestroy() {
this.container.removeEventListener("scroll", this.throttledScrollHandler);
},
};
</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
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
思路就是,监听目标元素的滚动高度,当超过限定的高度时,展示回到顶部功能,点击回到顶部按钮,将目标元素的 scrollTop 设置为 0。需要注意的是,为了用户的使用体验,加了动画的效果。主要用到的是 requestAnimationFrame (opens new window)
关于 requestAnimationFrame
:
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
浏览器执行动画,通常是每秒60次,但是使用 setInterval
和setTimeOut
无法保证和浏览器执行的频率一致,setInterval制作动画的时候会出现丢帧和动画效果生硬不连贯等情况。requestAnimationFrame
是HTML5新出的特性,就是为来解决这个问题。
# Divider
Divider
组件用于展示下划线和中划线。
看下 main.vue
中的定义:
<template functional>
<div :class="['ml-divider', `ml-divider--${props.direction}`]">
<div
v-if="slots().default && props.direction !== 'vertical'"
:class="['ml-divider__text', `is-${props.contentPosition}`]"
>
<slot />
</div>
</div>
</template>
<script>
// 这是函数式组件
export default {
name: "MlDivider",
props: {
direction: { // 线的方向 水平 or 垂直
type: String,
default: "horizontal",
validator(val) {
return ["horizontal", "vertical"].indexOf(val) !== -1;
},
},
contentPosition: { // 线的位置 左 中 右
type: String,
default: "center",
validator(val) {
return ["left", "center", "right"].indexOf(val) !== -1;
},
},
},
};
</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
divider
是一个函数式组件 (opens new window),函数式组件
的特点式不管理任何状态,也不监听任何状态,也没有生命周期函数,就是作为数据渲染的纯展示组件。对于 divider
来说,也确实只有展示的功能,不需要管理什么状态。
再看下样式:
@import "common/var";
@import "mixins/mixins";
@include b(divider) {
background-color: $--border-color-base;
position: relative;
@include m(horizontal) {
display: block;
height: 1px;
width: 100%;
margin: 24px 0;
}
@include m(vertical) {
display: inline-block;
width: 1px;
height: 1em;
margin: 0 8px;
vertical-align: middle;
position: relative;
}
@include e(text) {
position: absolute;
background-color: $--color-white;
padding: 0 20px;
font-weight: 500;
color: $--color-text-primary;
font-size: 14px;
@include when(left) {
left: 20px;
transform: translateY(-50%);
}
@include when(center) {
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
@include when(right) {
right: 20px;
transform: translateY(-50%);
}
}
}
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
# Card
Card
组件用于使用卡片展示信息。
先看看基本使用。
看下 main.vue
的定义:
<template>
<div
class="ml-card"
:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
<div class="ml-card__header" v-if="$slots.header || header">
<slot name="header">{{ header }}</slot>
</div>
<div class="ml-card__body" :style="bodyStyle">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "MlCard",
props: {
header: {},
bodyStyle: {},
shadow: {
type: String,
},
},
};
</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
结构没有什么可说的,看看样式文件。
@import "mixins/mixins";
@import "common/var";
@include b(card) {
border-radius: $--card-border-radius;
border: 1px solid $--card-border-color;
background-color: $--color-white;
overflow: hidden;
color: $--color-text-primary;
transition: 0.3s;
@include when(always-shadow) {
box-shadow: $--box-shadow-light;
}
@include when(hover-shadow) {
&:hover,
&:focus {
box-shadow: $--box-shadow-light;
}
}
@include e(header) {
padding: #{$--card-padding - 2 $--card-padding};
border-bottom: 1px solid $--card-border-color;
box-sizing: border-box;
}
@include e(body) {
padding: $--card-padding;
}
}
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
# Message
Message
组件用于反馈给用户某些信息。看看基本使用:
Message
组件的封装很有意思,我们一起来看看。main.vue
中的定义:
<template>
<transition name="ml-message-fade" @after-leave="handleAfterLeave">
<div
:class="[
'ml-message',
type && !iconClass ? `ml-message--${type}` : '',
center ? 'is-center' : '',
showClose ? 'is-closable' : '',
customClass,
]"
:style="positionStyle"
v-show="visible"
@mouseenter="clearTimer"
@mouseleave="startTimer"
>
<i :class="iconClass" v-if="iconClass"></i>
<i :class="typeClass" v-else></i>
<slot>
<p class="ml-message__content">
{{ message }}
</p>
</slot>
<i
v-if="showClose"
class="ml-message__closeBtn ml-icon-close"
@click="close"
></i>
</div>
</transition>
</template>
<script>
const typeMap = {
success: "success",
info: "info",
warning: "warning",
error: "error",
};
export default {
name: "MlMessage",
data() {
return {
visible: false, // 是否显示
message: "", // 提示的文字
duration: 3000, // 动画的时间
type: "info",
iconClass: "",
customClass: "", // 自定义的类名
onClose: null, // 关闭的回调函数
showClose: false,
closed: false, // 是否关闭
verticalOffset: 20, // 距离顶部的距离
timer: null, // 计时器
dangerouslyUseHTMLString: false, // 是否作为HTML片段展示
center: false, // 是否居中显示
};
},
computed: {
// 根据传入的 type 计算触发的message的类型
typeClass() {
return this.type && !this.iconClass
? `ml-message__icon ml-icon-${typeMap[this.type]}`
: "";
},
// 每个Message之间的间隔
positionStyle() {
return {
top: `${this.verticalOffset}px`,
};
},
},
watch: {
// 监听closed属性 一旦变化 隐藏Message
closed(newVal) {
if (newVal) {
this.visible = false;
}
},
},
methods: {
// 动画结束后销毁当前实例
handleAfterLeave() {
this.$destroy(true);
this.$el.parentNode.removeChild(this.$el);
},
// 关闭message
close() {
this.closed = true;
if (typeof this.onClose === "function") {
this.onClose(this);
}
},
// 清除自动关闭message的定时器
clearTimer() {
clearTimeout(this.timer);
},
// 计时器 固定的时间关闭message
startTimer() {
if (this.duration > 0) {
this.timer = setTimeout(() => {
if (!this.closed) {
this.close();
}
}, this.duration);
}
},
// 使用ESC键可以关闭message
keydown(e) {
if (e.keyCode === 27) {
// esc关闭消息
if (!this.closed) {
this.close();
}
}
},
},
mounted() {
this.startTimer();
document.addEventListener("keydown", this.keydown);
},
beforeDestroy() {
document.removeEventListener("keydown", this.keydown);
},
};
</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
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
主要逻辑还是在 index.js
中:
// @ 代表mssui源码中的 src
import Vue from "vue";
import Main from "./main.vue";
import { PopupManager } from "@/utils/popup";
let MessageConstructor = Vue.extend(Main);
let instance; // 当前的message实例
let instances = []; // 保存所有未关闭的message实例
let seed = 1; // 每个message独一无而的id
//
const Message = function (options) {
options = options || {};
// 支持this.$message("这是一条提示")
if (typeof options === "string") {
options = {
message: options,
};
}
let userOnClose = options.onClose;
let id = "message_" + seed++;
// message关闭时的回调函数
options.onClose = function () {
Message.close(id, userOnClose);
};
instance = new MessageConstructor({
data: options,
});
instance.id = id;
instance.$mount();
document.body.appendChild(instance.$el);
let verticalOffset = options.offset || 20;
instances.forEach((item) => {
verticalOffset += item.$el.offsetHeight + 16;
});
// 给当前message实例的data属性赋值
instance.verticalOffset = verticalOffset;
instance.visible = true;
// 使用PopupManager全局管理message,每触发一个message
// 该message的z-index都会增1
instance.$el.style.zIndex = PopupManager.nextZIndex();
instances.push(instance);
return instance;
};
// 给 Message 绑定四个函数分别是 success、warning、info、error
// 所以使用的时候可以 this.$message.success('这是一个提示!!') 这种方式使用
["success", "warning", "info", "error"].forEach((type) => {
Message[type] = (options) => {
if (typeof options === "string") {
options = {
message: options,
};
}
options.type = type;
return Message(options);
};
});
// close函数主要做两件事:
// 1.从instances删除关闭的message实例
// 2.instances中剩下的message实例向上移动 (自身的高度 + 16)的距离
Message.close = function (id, userOnClose) {
let len = instances.length;
let index = -1;
let removedHeight;
for (let i = 0; i < len; i++) {
if (id === instances[i].id) {
removedHeight = instances[i].$el.offsetHeight;
index = i;
if (typeof userOnClose === "function") {
userOnClose(instances[i]);
}
instances.splice(i, 1);
break;
}
}
if (len <= 1 || index === -1 || index > instances.length - 1) return;
// 每一个message实例关闭时,后面的message需要向上移动(removedHeight - 16)的距离
for (let i = index; i < len - 1; i++) {
let dom = instances[i].$el;
dom.style["top"] =
parseInt(dom.style["top"], 10) - removedHeight - 16 + "px";
}
};
Message.closeAll = function () {
for (let i = instances.length - 1; i >= 0; i--) {
instances[i].close();
}
};
export default Message;
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