Vue倒计时模态框组件实现

倒计时模态框是一种在页面中以模态形式弹出的用户界面组件,带有倒计时功能,用于在特定时间范围内向用户传递重要信息或要求用户执行某些操作。模态框具有以下特点:

  1. 模态形式
    弹出时会覆盖页面其他内容,通常会锁定用户操作,要求优先处理模态框中的内容,确保信息传递的显著性。
  2. 倒计时功能
    在模态框中嵌入动态倒计时,实时更新剩余时间,用于提示操作的时效性或限定可执行的时间范围。

组件设计

该倒计时模态框组件的设计目的是为用户提供一个直观的交互界面,在特定情境下通过倒计时提示用户做出决策。组件支持以下功能:

  • 倒计时显示:倒计时时间由父组件通过属性传入,倒计时结束后自动触发操作(如控制按钮样式改变)。
  • 标题与文本内容定制:支持通过属性和插槽自定义模态框的标题及内容,便于满足不同场景的展示需求。
  • 确认与取消操作:提供按钮用于执行确认或取消操作,并通过事件通知父组件处理结果。
  • 交互可选项:动态控制是否显示取消按钮,以及自定义确认与取消按钮的文案。

代码实现

基础结构

遮罩层

提供模态框背景遮罩,阻止用户与其他页面内容交互。

遮罩层结构

1
2
3
4
5
<div v-if="isShow" class="modal-mask">
<div class="modal">
...
</div>
</div>
  • 使用 v-if 指令控制遮罩层的显示与隐藏:当 isShowtrue 时,显示整个模态框组件;否则隐藏。
  • modal-mask 是遮罩层的样式类,用于设置其样式和位置。

遮罩层样式

添加遮罩层后,将无法点击页面后面的元素,是因为遮罩层的 全屏覆盖 样式属性阻止了鼠标事件传递。这是模态框设计中的一种默认行为,用于引导用户专注于模态框中的内容。

1
2
3
4
5
6
7
8
9
10
11
12
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}

功能点

  1. 固定定位 (position: fixed)遮罩层覆盖整个视口,确保始终位于页面顶部,无论滚动条位置如何。
  2. 全屏覆盖 (top, left, right, bottom)设置为 0,使遮罩层充满整个屏幕。
  3. 背景效果 (background-color)使用 rgba(0, 0, 0, 0.5) 实现黑色半透明背景,营造模态感。
  4. 层级控制 (z-index)设置 z-index: 999,确保遮罩层位于页面内容之上。

模态框主体

包含头部标题、内容插槽、底部操作按钮的基本布局。

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
 <div class="modal">
<div class="modal-header">{{ title }}</div>
<div class="modal-content">
<slot></slot>
</div>
<div class="modal-footer">
<!--left-->
<button
v-if="showCancel"
class="cancel-btn"
@click="onCancel"
>
{{ cancelText }}
</button>

<!--right-->
<button
:class="['confirm-btn', { 'centered-btn': !showCancel }]"
@click="onConfirm"
:disabled="isDisabled"
>
{{ isDisabled ? `${confirmText} (${localCountDown}s)` : confirmText }}
</button>
</div>
</div>
  • title:通过 prop 传入,支持动态更新模态框的标题内容。
  • 内容插槽 :为模态框提供灵活的内容区域,允许开发者自定义展示内容。
  • 按钮文案:通过外部数据动态绑定,cancelTextconfirmText 分别控制取消和确认按钮的显示文案。
  • 取消按钮:通过 showCancel 控制是否显示,提供灵活的配置。

确认按钮的样式会根据不同状态动态变化,大致设计风格可以参照:

  • 默认样式:暖色渐变背景,按钮显眼,提示操作。
  • 倒计时禁用样式:灰色背景+降低透明度,突出不可用状态。
  • 居中样式:按钮独占空间时居中对齐,增强布局美观。

倒计时功能

初始化倒计时

在组件的 data 中定义了 localCountDown,用于保存当前倒计时的秒数,初始值由 countDown 属性传入。localCountDown 会随着倒计时的进行动态更新,实时显示剩余的时间。

倒计时启动

当模态框显示 (isShow 变为 true) 时,组件会通过 watch 监听到 isShow 属性的变化,进而调用 startCountDown 方法启动倒计时功能。

1
2
3
4
5
6
7
watch: {
isShow(newVal) {
if (newVal) {
this.startCountDown();
}
},
},

倒计时逻辑

startCountDown 方法中实现了倒计时的核心逻辑:

  • 判断倒计时是否大于零,如果是,则禁用确认按钮(isDisabled = true),防止用户在倒计时过程中点击。
  • 清除定时器:使用 clearInterval(this.interval) 来清除已有的定时器,确保每次调用 startCountDown 时都能启动一个新的定时器。避免多个定时器的重复运行。
  • 设置新定时器:使用 setInterval 每秒更新一次 localCountDown,并根据倒计时结束时(localCountDown === 0)清除定时器,恢复按钮的可点击状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
startCountDown() {
if (this.localCountDown > 0) {
this.isDisabled = true;
}

if (this.interval) clearInterval(this.interval); // 清除旧的定时器
this.interval = setInterval(() => {
this.localCountDown--; // 每秒减少倒计时
if (this.localCountDown === 0) {
clearInterval(this.interval); // 倒计时结束,清除定时器
this.isDisabled = false; // 恢复按钮可用
this.localCountDown = this.countDown; // 重置倒计时
}
}, 1000); // 每秒执行一次
},

清除定时器

在组件销毁之前(beforeUnmount 生命周期钩子),清除定时器,确保不会在组件卸载后继续运行,防止内存泄漏。

1
2
3
beforeUnmount() {
if (this.interval) clearInterval(this.interval); // 清除定时器
},

按钮交互事件

  • 点击“确认”按钮触发 confirm 事件(仅在倒计时结束时触发)

    需对 按钮禁用状态 进行判断,只有按钮未禁用时才通过 $emit 触发 confirm 事件。

  • 点击“取消”按钮通过 $emit触发 canceled 事件。

在父组件中,对于 confirm 事件的处理通常较为简单,一般用于关闭模态框即可。而对于 canceled 事件,可以结合业务需求进行更丰富的处理,比如提供进一步的提示信息,引导用户确认是否取消操作;或者在特定场景下,执行页面回退、返回上一级页面等操作,从而提升用户体验和交互的友好性。

完整代码参考

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
<template>
<div v-if="isShow" class="modal-mask">
<div class="modal">
<div class="modal-header">{{ title }}</div>
<div class="modal-content">
<slot></slot>
</div>
<div class="modal-footer">
<!--left-->
<button
v-if="showCancel"
class="cancel-btn"
@click="onCancel"
>
{{ cancelText }}
</button>

<!--right-->
<button
:class="['confirm-btn', { 'centered-btn': !showCancel }]"
@click="onConfirm"
:disabled="isDisabled"
>
{{ isDisabled ? `${confirmText} (${localCountDown}s)` : confirmText }}
</button>
</div>
</div>
</div>
</template>

<script>
export default {
name: "CountdownModal",
props: {
isShow: {
type: Boolean,
default: false,
},

title: {
type: String,
default: "",
},

confirmText: {
type: String,
default: "",
},
cancelText: {
type: String,
default: "",
},
showCancel: {
type: Boolean,
default: false,
},
//倒计时秒数
countDown: {
type: Number,
default: 0,
},
},
data() {
return {
isDisabled: false, //禁止点击
localCountDown: this.countDown,
interval: null,
};
},
watch: {
isShow(newVal) {
if (newVal) {
this.startCountDown();
}
},
},
methods: {
onConfirm() {
if (!this.isDisabled) {
this.$emit("confirm");
}
},
onCancel() {
this.$emit("canceled");
},

startCountDown() {
if (this.localCountDown > 0) {
this.isDisabled = true;
}

if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => {
this.localCountDown--;
if (this.localCountDown === 0) {
clearInterval(this.interval);
this.isDisabled = false;
this.localCountDown = this.countDown;//重置时间
}
}, 1000);
},
},
beforeUnmount() {
if (this.interval) clearInterval(this.interval);
},
};
</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
<template>
<img class="background" src="../assets/mobile-bank.png" alt="">
<CountdownModal
:isShow="isShow"
:title="title"
:cancelText="cancel"
:showCancel="showCancel"
:confirmText="confirm"
:countDown="countDown"
@confirm="onConfirm"
@canceled="onCancel"
>
<div class="modal-text">
<div>
&nbsp;&nbsp;&nbsp;&nbsp;您当前有未领取的手机银行积分,选择确认后,积分将自动添加到您的账户余额中。
请确认您是否希望继续领取积分,若取消,则积分不会被领取。
</div>
</div>
</CountdownModal>
</template>

<script>
import CountdownModal from "@/components/CountdownModal/CountdownModal.vue";

export default {
name: "CountdownModalDemo",
components: {CountdownModal},
data() {
return {
isShow: false,
title: "积分领取",
confirm: "继续",
cancel: "取消",
showCancel: true,
countDown: 3
}
},
mounted() {
this.isShow = true
},
methods: {
onConfirm() {
this.isShow = false;
},
onCancel() {
this.isShow = false;
this.$router.back(); //回退上一级空白页面
}
}
}
</script>

演示过程

场景一:用户确认
模态框显示倒计时,用户可选择“继续”确认领取积分,或选择“取消”放弃操作。

showcancel

场景二:通知提醒
同样的,当需要简化交互为仅提示用户时,可隐藏取消按钮。

1
2
3
4
5
mounted() {
this.showCancel = false
this.confirm = "已知晓"
this.isShow = true
},

image-20250110111554514

两种用法可通过配置 showCancel 和按钮文案实现灵活切换。

常见问题

当提示模态框弹出时,通常会在页面加载时监听 isShow 属性的变化,并在其为 true 时启动倒计时。然而,如果父组件在 data 中直接初始化 isShowtrue 并传递给子组件,可能会出现 watch 无法监听到该值变化的问题。

  1. 在父组件的 mounted 中修改数据
1
2
3
mounted() {
this.isShow = true; // 子组件的 watch 已准备好,能正确响应
}

需要注意的是,如果将值修改放在父组件的 created 钩子中,子组件的 watch 不会被触发。因为此时子组件还没有创建,watch 没有机会监听到父组件传递的 props 变化。

  1. 在子组件的生命周期中手动检查初始值

除了使用 watch 之外,还可以在子组件的 mountedcreated 钩子中手动检查 props 的初始值并执行对应的逻辑,从而弥补 watch 的不足。

1
2
3
4
5
created() {
if (this.isShow) {
//调用初始逻辑
}
}

父子组件生命周期执行顺序

  1. 父组件的 data:当父组件的实例化时,data 会首先初始化。
  2. 父组件的 created:父组件实例化后,data 初始化完毕,接下来会执行 created 钩子。此时,父组件的数据已经可用,但 DOM 尚未渲染。
  3. 子组件的 props:在父组件的 created 钩子执行之后,父组件会将数据传递给子组件的 props。此时,子组件会接收到父组件传递的 props 数据。
  4. 子组件的 data:子组件的 data 会在子组件实例化时初始化,且其数据在 created 钩子中会可用。
  5. 子组件的 created:在子组件的实例化过程中,dataprops 都已设置完成,因此在子组件的 created 钩子中,子组件的状态和数据都已经准备好。
  6. 父组件的 mounted:父组件的模板已经渲染并挂载到 DOM 上时,mounted 钩子会执行。
  7. 子组件的 mounted:子组件的模板已经渲染并挂载到 DOM 上时,子组件的 mounted 钩子会执行。