Bubble 对话气泡 🔥
📌 Warning
1.1.6 版本
继承打字器雾化属性。请及时更新尝试
🐵 此温馨提示更新时间:2025-04-13
介绍
Bubble
是一个对话气泡组件,常用于聊天的时候。它可以展示对话内容,支持自定义头像、头部、内容、底部,并且具备打字效果和加载状态展示。该组件内置 Typewriter
打字器组件,能够实现文本的打字动画效果。
代码演示
基本使用
最简化的集成方式。
<script setup lang="ts">
const content = ref('hello world !');
</script>
<template>
<Bubble :content="content" />
</template>
2
3
4
5
6
7
头像、位置

通过 #avatar
设置自定义头像。通过 placement
属性设置位置,提供了 start
、end
两个选项值。
💡 Tip
😸 内置 element-plus
el-avatar
组件。但是为避免属性名重复,例如:el-avatar
和 Bubble
的 shape
属性。你需要用以下属性设置
- 属性
avatar
设置头像占位图片avatar-size
设置头像占位大小 👉这个属性在el-avatar组件
是number类型
,这里注意在此组件上是string类型
以更好自定义样式属性😊avatar-gap
设置头像和气泡之间的距离avatar-shape
设置头像形状avatar-icon
设置头像占位图标avatar-src-set
设置头像图片 srcset 属性avatar-alt
设置头像图片的 alt 属性avatar-fit
设置头像占位图片的填充模式
- 事件
@avatar-error
当头像加载失败时触发。
<script setup lang="ts">
const avatarAI =
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
const avatarUser = 'https://avatars.githubusercontent.com/u/76239030?v=4';
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<!-- Avatar and Placement 左侧 -->
<Bubble
content="Good morning, how are you?"
placement="start"
:avatar="avatarAI"
avatar-size="48px"
/>
<!-- avatar-size 设置头像占位空间 -->
<Bubble
content="What a beautiful day!"
placement="start"
avatar-size="48px"
/>
<!-- Avatar and Placement 右侧 -->
<Bubble content="Hi, good morning, I'm fine!" placement="end">
<template #avatar>
<el-avatar :size="32" :src="avatarUser" />
</template>
</Bubble>
<!-- avatar-gap 属性控制 气泡与头像的距离 -->
<Bubble
content="Hi, good morning, I'm fine! Thank you!"
placement="end"
avatar-size="0px"
avatar-gap="0px"
/>
</div>
</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
29
30
31
32
33
34
35
36
37
38
39
头部、底部

通过 #header
和 #footer
插槽 来自定义气泡的头部和底部。
<script setup lang="ts">
import { DocumentCopy, Refresh, Search, Star } from '@element-plus/icons-vue';
const content = ref(
'嗨!你好,欢迎使用 Element Plus X,有什么问题,可以问我哦~'
);
const avatarAI =
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
</script>
<template>
<Bubble :content="content">
<template #avatar>
<el-avatar :src="avatarAI" />
</template>
<template #header>
<span>Element Plus X</span>
</template>
<template #footer>
<div class="footer-container">
<el-button type="info" :icon="Refresh" size="small" circle />
<el-button type="success" :icon="Search" size="small" circle />
<el-button type="warning" :icon="Star" size="small" circle />
<el-button color="#626aef" :icon="DocumentCopy" size="small" circle />
</div>
</template>
</Bubble>
</template>
<style scoped lang="less">
.footer-container {
:deep(.el-button + .el-button) {
margin-left: 8px;
}
}
</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
加载状态
通过 loading
属性设置加载中状态。支持通过 #loading
插槽自定义加载中状态内容展示。
💌 Info
#loading
插槽 优先级更高,内置的加载中样式将失效。但 loading
属性任然可以控制 加载中状态。
<script setup lang="ts">
const loading = ref(true);
const content = ref('hello world !');
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 10px">
<Bubble :content="content" :loading="loading" />
<Bubble :content="content" :loading="loading">
<template #loading>
<div>loading...</div>
</template>
</Bubble>
<Bubble :content="content" :loading="loading">
<template #loading>
<div>感谢使用 Element-Plus-X 🌹 请稍后...</div>
</template>
</Bubble>
<div style="display: flex; align-items: center">
<span>状态:</span>
<el-switch v-model="loading" />
</div>
</div>
</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
打字器配置

通过设置 typing
属性,开启打字效果。 更新 content
如果是之前的子集,则会继续输出,否则会重新输出。
💌 Info
🙊 当使用 #content
插槽,去自定义内容时。typing
属性将失效。如果你想让你的内容字符串,重新实现打字效果,可以与 Typewriter 打字器
组件 结合使用。
💡 Tip
typing
属性接受一个对象,包含以下属性:
step
: 每次打字的吐字字符数,默认为 2interval
: 打字间隔(毫秒),默认为 50suffix
: 结尾字符,默认为|
<script setup lang="ts">
const num = ref(1);
const content = computed(() =>
'🥰 感谢使用 Element-Plus-X ! 你的支持,是我们开源的最强动力 ~ '.repeat(
num.value
)
);
const avatarAI =
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
function changeContent() {
num.value++;
if (num.value > 3)
num.value = 1;
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<el-button style="width: fit-content" @click="changeContent">
设置 text
</el-button>
<Bubble
:content="content"
:typing="{ step: 1, interval: 100, suffix: '💩' }"
>
<template #avatar>
<el-avatar :src="avatarAI" />
</template>
</Bubble>
</div>
</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
29
30
31
32
33
开启Markdown渲染
通过设置 is-markdown
属性,开启 markdown
文本内容渲染模式。 更新 content
如果是之前的子集,则会继续输出,否则会重新输出。
<script setup lang="ts">
const avatarUser = 'https://avatars.githubusercontent.com/u/76239030?v=4';
const content = ref(
`## 🔥Element-Plus-X \n 🥰 感谢使用 Element-Plus-X! \n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n`
);
const num = ref(1);
function changeContent() {
num.value++;
content.value = content.value.repeat(num.value);
if (num.value > 2) {
num.value = 1;
content.value = `## 🔥Element-Plus-X \n 🥰 感谢使用 Element-Plus-X! \n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n`;
}
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<el-button style="width: fit-content" @click="changeContent">
设置 markdown
</el-button>
<Bubble :content="content" typing is-markdown>
<template #avatar>
<el-avatar :size="32" :src="avatarUser" />
</template>
</Bubble>
</div>
</template>
<style scoped lang="less">
:deep(.markdown-body) {
background-color: transparent;
}
</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
继承打字器的图表和md样式
通过设置 is-markdown
属性,开启 markdown
文本内容渲染模式。 更新 content
如果是之前的子集,则会继续输出,否则会重新输出。
<script setup lang="ts">
const avatarUser = 'https://avatars.githubusercontent.com/u/76239030?v=4';
const markdownText =
ref(`#### 标题 \n 这是一个 Markdown 示例。\n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n \`\`\`mermaid
pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15
\n
\`\`\`
\`\`\`mermaid
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
\n
\`\`\`
`);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<Bubble :content="markdownText" typing is-markdown>
<template #avatar>
<el-avatar :size="32" :src="avatarUser" />
</template>
</Bubble>
</div>
</template>
<style scoped lang="less">
:deep(.markdown-body) {
background-color: transparent;
}
</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
雾化效果
开启打字器时,继承打字器雾化属性。通过设置 is-fog
属性,开启雾化打字器渲染模式。 兼容 Markdown 样式。注意,开启雾化后,typing
的后缀 suffix
属性将会失效。
is-fog
默认为 false,可以设置为 true
或者 { bgColor: '#f5f5f5', width: '80px' }
。设置雾化背景颜色,可以更好的匹配自定义的样式。
<script setup lang="ts">
const avatarUser = 'https://avatars.githubusercontent.com/u/76239030?v=4';
const content = ref(
`## 🔥Element-Plus-X \n 🥰 感谢使用 Element-Plus-X! \n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n`
);
function changeContent(type: number) {
content.value = '';
setTimeout(() => {
if (type === 1) {
content.value = `## 🔥Element-Plus-X \n 🥰 感谢使用 Element-Plus-X! \n - 列表项 1 \n - 列表项 2 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n console.log('Hello, world!'); \n \`\`\` \n`;
}
else if (type === 2) {
content.value = `🔥Element-Plus-X `.repeat(10);
}
}, 80);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; gap: 12px">
<el-button style="width: fit-content" @click="changeContent(1)">
雾化 markdown
</el-button>
<el-button style="width: fit-content" @click="changeContent(2)">
雾化 text
</el-button>
</div>
<Bubble
:content="content"
:typing="{ step: 3, interval: 80, suffix: '💩' }"
is-markdown
:is-fog="{ bgColor: '#f5f5f5' }"
>
<template #avatar>
<el-avatar :size="32" :src="avatarUser" />
</template>
</Bubble>
</div>
</template>
<style scoped lang="less">
:deep(.markdown-body) {
background-color: transparent;
}
</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
自定义内容

通过 #content
插槽,自定义气泡内容。
💌 Info
#content
插槽 优先级更高,content
属性将失效。 no-padding
属性可以禁用气泡内容内边距。
<script setup lang="ts">
const avatarSize = '48px';
const avatarAI =
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png';
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<Bubble
content="欢迎使用 element-plus-x。"
typing
:avatar="avatarAI"
:avatar-size="avatarSize"
no-style
>
<template #content>
<div class="content-container">
😊 欢迎使用 element-plus-x,我是自定义气泡
</div>
</template>
</Bubble>
<Bubble :avatar-size="avatarSize" typing no-style variant="borderless">
<template #header>
<div class="content-container-header">
推荐内容 自定义气泡
</div>
</template>
<template #content>
<div class="content-borderless-container">
🥤 长时间工作后如何有效休息?
</div>
</template>
</Bubble>
<Bubble :avatar-size="avatarSize" typing no-style variant="borderless">
<template #content>
<div class="content-borderless-container">
💌 保持积极心态的秘诀是什么?
</div>
</template>
</Bubble>
<Bubble :avatar-size="avatarSize" typing no-style variant="borderless">
<template #content>
<div class="content-borderless-container">
🔥 如何在巨大的压力下保持冷静?
</div>
</template>
</Bubble>
</div>
</template>
<style scoped>
.content-container {
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
}
.content-container-header {
font-size: 12px;
color: #909399;
}
.content-borderless-container {
user-select: none;
padding: 12px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #ebeef5;
}
}
</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
变体和形状
通过 variant
属性设置气泡的填内置样式格式。通过 shape
属性设置气泡的形状。当然你也可以两两结合,搭配使用
💌 Info
默认情况下,variant
为 filled
,shape
为 round
。
shape
为 corner
时,placement="end"
会自动将气泡翻转,使得右上角的 弧度针
指向用户。
<script setup lang="ts"></script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="filled" variant="filled" />
<Bubble content="filled + round" variant="filled" shape="round" />
<Bubble content="filled + corner" variant="filled" shape="corner" />
</div>
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="borderless" variant="borderless" />
<Bubble content="borderless + round" variant="borderless" shape="round" />
<Bubble
content="borderless + corner"
variant="borderless"
shape="corner"
/>
</div>
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="outlined" variant="outlined" />
<Bubble content="outlined + round" variant="outlined" shape="round" />
<Bubble content="outlined + corner" variant="outlined" shape="corner" />
</div>
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="shadow" variant="shadow" />
<Bubble content="shadow + round" variant="shadow" shape="round" />
<Bubble content="shadow + corner" variant="shadow" shape="corner" />
</div>
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="round" shape="round" />
</div>
<div style="display: flex; gap: 12px; align-items: center">
<Bubble content="corner" shape="corner" />
<Bubble content="placement end" shape="corner" placement="end" />
</div>
</div>
</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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
控制打字
💩 更好的控制中断输出、继续打字和销毁等操作
💡 Tip
😸 内置 Typewriter
组件。将 Typewriter
组件内的所有属性方法挂载到 Bubble
组件上,方便在敏捷开发中使用。
💌 Info
🐒 如果你觉得内置的 Typewriter
组件,不能满足你的需求,还可以 使用 #content
插槽对 Bubble
组件进行定制化开发。
使用 #content
, 内置的 Typewriter
组件将会失效。在插槽中,你也可以自行和 Typewriter
组合使用,也可以自定义 流式请求
、 流式渲染
等个性化操作。
<script setup lang="ts">
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';
import {
Delete,
RefreshLeft,
VideoPause,
VideoPlay
} from '@element-plus/icons-vue';
const markdownContent = ref(
`# 🔥 Bubble 实例方法-事件 \n 😄 使你的打字器可高度定制化。\n - 更方便的控制打字器的状态 \n - 列表项 **粗体文本** 和 *斜体文本* \n \`\`\`javascript \n // 🙉 控制台可以查看相关打日志\n console.log('Hello, world!'); \n \`\`\``
);
const isTypingValue = ref(false);
const progressValue = ref(0);
const bubbleRef = ref();
// 开始打字的监听方法
function onStart(instance: TypewriterInstance) {
console.log('开始打字:组件 ref 实例', unref(instance));
isTypingValue.value = true;
}
// 打字中,进度监听方法
function onWriting(instance: TypewriterInstance) {
const progress: number = instance.progress.value;
// 避免打印打多次 onWriting 事件 😂
if (progress > 90 && progress < 100) {
// 可以直接获取打字进度,可以根据打字进度,设置更炫酷的样式
// console.log('Writing', `${progress}%`)
console.log(
'打字中 isTyping:',
instance.isTyping.value,
'progress:',
progress
);
}
if (~~progress === 80) {
console.log(
'打字中 progress 为 80% 时候的内容',
instance.renderedContent.value
);
}
isTypingValue.value = true;
progressValue.value = ~~progress; // 通过运算符~~取整 💩
}
// 监听打字结束事件
function onFinish(instance: TypewriterInstance) {
isTypingValue.value = false;
console.log(
'打字结束 isTyping',
instance.isTyping.value,
'progress:',
instance.progress.value
);
}
// 组件实例方法,控制 暂停打字
function onInterrupt() {
bubbleRef.value.interrupt();
isTypingValue.value = false;
}
function onDestroy() {
bubbleRef.value.destroy();
isTypingValue.value = false;
progressValue.value = 0;
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex">
<el-button
v-if="isTypingValue"
type="warning"
style="width: fit-content"
@click="onInterrupt"
>
<el-icon :size="18">
<VideoPause />
</el-icon>
<span>暂停</span>
</el-button>
<el-button
v-if="!isTypingValue && progressValue !== 0 && progressValue !== 100"
type="success"
style="width: fit-content"
@click="bubbleRef?.continue()"
>
<el-icon :size="18">
<VideoPlay />
</el-icon>
<span>继续</span>
</el-button>
<el-button
v-if="!isTypingValue && (progressValue === 0 || progressValue === 100)"
type="primary"
style="width: fit-content"
@click="bubbleRef?.restart()"
>
<el-icon :size="18">
<RefreshLeft />
</el-icon>
<span>重播</span>
</el-button>
<el-button type="danger" style="width: fit-content" @click="onDestroy">
<el-icon>
<Delete />
</el-icon>
<span>销毁</span>
</el-button>
</div>
<el-progress
v-if="progressValue > 0 && progressValue !== 100"
:duration="0"
:percentage="progressValue"
/>
<el-progress
v-if="progressValue === 100"
:percentage="100"
status="success"
/>
<!-- 这里展示了如果是 markdown 的话,typing.suffix 会被忽略 -->
<Bubble
ref="bubbleRef"
:content="markdownContent"
:typing="{ suffix: '💩', interval: 40 }"
:is-markdown="true"
@start="onStart"
@writing="onWriting"
@finish="onFinish"
/>
</div>
</template>
<style scoped lang="less">
// 避免 markdown-body 样式被覆盖
:deep(.markdown-body) {
background: transparent;
}
</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
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
content | String | '' | 气泡内要展示的文本内容 |
placement | String | 'start' | 气泡的位置,可选值为 'start' 或 'end' ,分别表示左侧和右侧。 |
avatar | String | '' | 气泡头像的图片地址 |
loading | Boolean | false | 是否显示加载状态。为 true 时,气泡内会显示加载状态。 |
shape | String | null | 气泡的形状,可选值为 'round' (圆角)或 'corner' (有角)。 |
variant | String | 'filled' | 气泡的样式变体,可选值为 'filled' (填充)、'borderless' (无边框)、'outlined' (轮廓)、'shadow' (阴影)。 |
noStyle | Boolean | false | 是否去除样式,为 true 时,将去除气泡内置 padding 和 背景色 |
isMarkdown | Boolean | false | 是否将 content 内容作为 Markdown 格式处理。 |
typing | Boolean | Object | false | 是否开启打字效果。若为对象,可设置 step (每次渲染的字符数)和 suffix (打字光标后缀内容)。interval 表示打字间隔时间,单位为 ms 。 |
maxWidth | String | '500px' | 气泡内容的最大宽度。 |
avatar-size | String | '' | 设置头像占位大小 |
avatar-gap | String | '12px' | 设置头像和气泡之间的 gap 值 |
avatar-shape | String | '' | 头像形状,可选值为 'circle' (圆形)或 'square' (方形)。 |
avatar-icon | String | '' | 头像图标,优先级高于 avatar ,支持传入图标名称,如 'user' 。 |
avatar-src-set | String | '' | 设置头像图片 srcset 属性 |
avatar-alt | String | '' | 设置头像图片 alt 属性 |
avatar-fit | String | 'cover' | 设置头像图片的 object-fit 属性,可选属性值:'cover' 、'contain' 、'fill' 、'none' 、'scale-down' |
事件
事件名 | 参数 | 类型 | 描述 |
---|---|---|---|
@start | ref 实例 | Function | 打字效果开始时触发 |
@finish | ref 实例 | Function | 打字效果完成时触发 |
@writing | ref 实例 | Function | 打字中实时触发 |
@avatarError | ref 实例 | Function | 头像加载失败时触发 |
Ref 实例方法
属性名 | 类型 | 描述 |
---|---|---|
interrupt | Function | 中断打字。 |
continue | Function | 继续未完成的打字。 |
restart | Function | 重新开始打字。 |
destroy | Function | 主动销毁 Bubble 组件。 |
renderedContent | String | 获取打字组件渲染的内容。 |
isTyping | Boolean | 是否正在打字。 |
progress | Number | 打字进度,取值范围 0 - 100。 |
插槽
插槽名 | 参数 | 类型 | 描述 |
---|---|---|---|
#avatar | - | Slot | 自定义头像展示内容 |
#header | - | Slot | 自定义气泡顶部展示内容 |
#content | - | Slot | 自定义气泡展示内容 |
#loading | - | Slot | 自定义气泡加载状态展示内容 |
#footer | - | Slot | 自定义气泡底部展示内容 |
功能特性
- 布局方向 - 支持左对齐(
start
)和右对齐(end
) - 内容类型 - 支持纯文本、Markdown、自定义插槽内容
- 加载状态 - 内置加载动画,支持自定义加载内容
- 视觉效果 - 提供多种形状和变体(圆角/直角、填充/描边/阴影等)
- 打字动画 - 支持渐进式文字输出效果
- 灵活插槽 - 提供头像、头部、内容、底部、加载状态等插槽