Bubble 对话气泡 🔥
📌 注意
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
两个选项值。
💡 提示
😸 内置 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
头部、底部
通过 #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
加载状态
通过 loading
属性设置加载中状态。支持通过 #loading
插槽自定义加载中状态内容展示。
💌 消息
#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
如果是之前的子集,则会继续输出,否则会重新输出。
💌 消息
🙊 当使用 #content
插槽,去自定义内容时。typing
属性将失效。如果你想让你的内容字符串,重新实现打字效果,可以与 Typewriter 打字器
组件 结合使用。
💡 提示
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
开启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
继承打字器的图表和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
40
41
雾化效果
开启打字器时,继承打字器雾化属性。通过设置 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
自定义内容
通过 #content
插槽,自定义气泡内容。
💌 消息
#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
变体和形状
通过 variant
属性设置气泡的填内置样式格式。通过 shape
属性设置气泡的形状。当然你也可以两两结合,搭配使用
💌 消息
默认情况下,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
控制打字
💩 更好的控制中断输出、继续打字和销毁等操作
💡 提示
😸 内置 Typewriter
组件。将 Typewriter
组件内的所有属性方法挂载到 Bubble
组件上,方便在敏捷开发中使用。
💌 消息
🐒 如果你觉得内置的 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
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
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、自定义插槽内容
- 加载状态 - 内置加载动画,支持自定义加载内容
- 视觉效果 - 提供多种形状和变体(圆角/直角、填充/描边/阴影等)
- 打字动画 - 支持渐进式文字输出效果
- 灵活插槽 - 提供头像、头部、内容、底部、加载状态等插槽