Skip to content

Commit cc8c46a

Browse files
author
weilei
committed
feat: 添加 ElXBubble 组件及样式
1 parent aa24476 commit cc8c46a

File tree

2 files changed

+416
-0
lines changed

2 files changed

+416
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<script>
2+
import Typewriter from './Typewriter.vue'
3+
4+
export default {
5+
name: 'ElXBubble',
6+
components: {
7+
Typewriter,
8+
},
9+
props: {
10+
content: {
11+
type: String,
12+
default: '',
13+
},
14+
reasoning_content: {
15+
type: String,
16+
default: '',
17+
},
18+
avatar: {
19+
type: String,
20+
default: '',
21+
},
22+
placement: {
23+
type: String,
24+
default: 'start',
25+
},
26+
variant: {
27+
type: String,
28+
default: 'filled',
29+
},
30+
maxWidth: {
31+
type: String,
32+
default: '500px',
33+
},
34+
avatarSize: {
35+
type: String,
36+
default: '',
37+
},
38+
avatarGap: {
39+
type: String,
40+
default: '12px',
41+
},
42+
avatarShape: {
43+
type: String,
44+
default: 'circle',
45+
},
46+
avatarIcon: {
47+
type: String,
48+
default: '',
49+
},
50+
avatarSrcSet: {
51+
type: String,
52+
default: '',
53+
},
54+
avatarAlt: {
55+
type: String,
56+
default: '',
57+
},
58+
avatarFit: {
59+
type: String,
60+
default: 'cover',
61+
},
62+
noStyle: {
63+
type: Boolean,
64+
default: false,
65+
},
66+
typing: {
67+
type: [Boolean, Object],
68+
default: undefined,
69+
},
70+
loading: {
71+
type: Boolean,
72+
default: false,
73+
},
74+
shape: {
75+
type: String,
76+
default: '',
77+
},
78+
isMarkdown: {
79+
type: Boolean,
80+
default: false,
81+
},
82+
isFog: {
83+
type: Boolean,
84+
default: false,
85+
},
86+
},
87+
data() {
88+
return {
89+
internalDestroyed: false,
90+
isTypingClass: false,
91+
}
92+
},
93+
computed: {
94+
_step() {
95+
if (typeof this.typing === 'object' && this.typing.step) return this.typing.step
96+
return 2
97+
},
98+
_suffix() {
99+
if (typeof this.typing === 'object' && this.typing.suffix) return this.typing.suffix
100+
return '|'
101+
},
102+
_interval() {
103+
if (typeof this.typing === 'object' && this.typing.interval) return this.typing.interval
104+
return 50
105+
},
106+
_typing() {
107+
if (typeof this.typing === 'undefined') {
108+
return false
109+
} else if (typeof this.typing === 'boolean') {
110+
return this.typing
111+
} else {
112+
return {
113+
suffix: this._suffix,
114+
step: this._step,
115+
interval: this._interval,
116+
}
117+
}
118+
},
119+
dots() {
120+
return [1, 2, 3]
121+
},
122+
},
123+
watch: {
124+
content(newVal, oldVal) {
125+
if (newVal !== oldVal && this.internalDestroyed) {
126+
this.restart()
127+
}
128+
},
129+
},
130+
methods: {
131+
onStart(instance) {
132+
this.$emit('start', instance)
133+
},
134+
onFinish(instance) {
135+
this.isTypingClass = false
136+
this.$emit('finish', instance)
137+
},
138+
onWriting(instance) {
139+
this.isTypingClass = true
140+
this.$emit('writing', instance)
141+
},
142+
avatarError(e) {
143+
this.$emit('avatarError', e)
144+
},
145+
interrupt() {
146+
this.$refs.typewriterRef && this.$refs.typewriterRef.interrupt()
147+
},
148+
continueTyping() {
149+
this.$refs.typewriterRef && this.$refs.typewriterRef.continue()
150+
},
151+
restart() {
152+
this.internalDestroyed = false
153+
this.$refs.typewriterRef && this.$refs.typewriterRef.restart()
154+
},
155+
destroy() {
156+
this.$refs.typewriterRef && this.$refs.typewriterRef.destroy()
157+
this.internalDestroyed = true
158+
},
159+
},
160+
beforeDestroy() {
161+
this.destroy()
162+
},
163+
}
164+
</script>
165+
166+
<template>
167+
<div
168+
v-if="!internalDestroyed"
169+
class="el-x-bubble"
170+
:class="{
171+
'el-x-bubble-start': placement === 'start',
172+
'el-x-bubble-end': placement === 'end',
173+
'el-x-bubble-no-style': noStyle,
174+
'el-x-bubble-is-typing': isTypingClass,
175+
}"
176+
:style="{
177+
'--el-box-shadow-tertiary': `0 1px 2px 0 rgba(0, 0, 0, 0.03),
178+
0 1px 6px -1px rgba(0, 0, 0, 0.02),
179+
0 2px 4px 0 rgba(0, 0, 0, 0.02)`,
180+
'--bubble-content-max-width': `${maxWidth}`,
181+
'--el-x-bubble-avatar-placeholder-width': `${$slots.avatar ? '' : avatarSize}`,
182+
'--el-x-bubble-avatar-placeholder-height': `${$slots.avatar ? '' : avatarSize}`,
183+
'--el-x-bubble-avatar-placeholder-gap': `${avatarGap}`,
184+
}"
185+
>
186+
<!-- 头像 -->
187+
<div v-if="!$slots.avatar && avatar" class="el-x-bubble-avatar el-x-bubble-avatar-size">
188+
<el-avatar :size="0" :src="avatar" :shape="avatarShape" :icon="avatarIcon" :src-set="avatarSrcSet" :alt="avatarFit" @error="avatarError" />
189+
</div>
190+
191+
<!-- 头像属性进行占位 -->
192+
<div v-if="!$slots.avatar && !avatar && avatarSize" class="el-x-bubble-avatar-placeholder" />
193+
194+
<div v-if="$slots.avatar" class="el-x-bubble-avatar">
195+
<slot name="avatar" />
196+
</div>
197+
198+
<!-- 内容 -->
199+
<div class="el-x-bubble-content-wrapper">
200+
<!-- 头部内容 -->
201+
<div v-if="$slots.header" class="el-x-bubble-header">
202+
<slot name="header" />
203+
</div>
204+
205+
<div
206+
class="el-x-bubble-content"
207+
:class="{
208+
'el-x-bubble-content-loading': loading,
209+
'el-x-bubble-content-round': shape === 'round',
210+
'el-x-bubble-content-corner': shape === 'corner',
211+
'el-x-bubble-content-filled': variant === 'filled',
212+
'el-x-bubble-content-borderless': variant === 'borderless',
213+
'el-x-bubble-content-outlined': variant === 'outlined',
214+
'el-x-bubble-content-shadow': variant === 'shadow',
215+
}"
216+
>
217+
<div v-if="!loading" class="el-typewriter" :class="{
218+
'no-content': !content,
219+
}">
220+
<Typewriter
221+
v-if="!$slots.content && content"
222+
ref="typewriterRef"
223+
:typing="_typing"
224+
:content="content"
225+
:is-markdown="isMarkdown"
226+
:is-fog="isFog"
227+
@start="onStart"
228+
@writing="onWriting"
229+
@finish="onFinish"
230+
/>
231+
</div>
232+
233+
<!-- 内容-自定义 -->
234+
<slot v-if="!internalDestroyed && $slots.content && !loading" name="content" />
235+
236+
<!-- 加载中-默认 -->
237+
<div v-if="loading && !$slots.loading" class="el-x-bubble-loading-wrap">
238+
<div v-for="(_, index) in dots" :key="index" class="dot" :style="{ animationDelay: `${index * 0.2}s` }" />
239+
</div>
240+
241+
<!-- 加载中-自定义 -->
242+
<div v-if="loading && $slots.loading" class="el-x-bubble-loading-wrap">
243+
<slot name="loading" />
244+
</div>
245+
</div>
246+
247+
<div v-if="$slots.footer" class="el-x-bubble-footer">
248+
<slot name="footer" />
249+
</div>
250+
</div>
251+
</div>
252+
</template>
253+
254+
255+
<style lang="scss" scoped>
256+
@import '../styles/Bubble.scss';
257+
</style>

0 commit comments

Comments
 (0)