Sender 输入框 💭
📌 Warning
1.1.6 版本
新增
- 变体
上下结构
- 自定义底部
#footer
插槽 - 自定义指令
弹框
和 触发指令的回调事件
🐵 此温馨提示更新时间:2025-04-16
介绍
Sender
是用于聊天的输入框组件。具备丰富的交互功能和自定义特性。它支持语音输入、清空输入内容、多种提交方式,并且允许用户自定义头部、前缀和操作列表等内容。同时,组件提供了焦点控制、提交回调等功能,可满足多样化的输入场景需求。
代码演示
基础用法
这是一个Sender
输入框,最简单的使用例子。
<template>
<Sender />
</template>
2
3
提示语
可以通过 placeholder
设置输入框的提示语。
<template>
<Sender placeholder="💌 欢迎使用 Element-Plus-X ~" />
</template>
2
3
双向绑定(未绑定,值不会变)
可以通过 v-model
绑定组件的 value
属性。
📌 Warning
- 在提交时,需要有内容,才会进行提交。
- 内容为空时,提交按钮会被禁用,且使用组件实例提交会失效。
💌 Info
- 通过
v-model
属性,可以自动绑定输入框的值。不用赋值数据到v-model
中。 - 通过
@submit
事件,可以触发输入框的提交事件,回传一个value
参数,你可以在此处理提交的数据。 - 通过
@cancel
事件,可以触发loading
按钮的点击事件。在这里你可以中止提交的操作。
你也可以通过组件 ref 实例对象进行调用
senderRef.value.submit()
触发提交senderRef.value.cancel()
触发取消senderRef.value.clear()
重置输入框的值
<script setup lang="ts">
const senderRef = ref();
const timeValue = ref<NodeJS.Timeout | null>(null);
const senderValue = ref('');
const senderLoading = ref(false);
const submitBtnDisabled = ref(true);
function handleSubmit(value: string) {
ElMessage.info(`发送中`);
senderLoading.value = true;
timeValue.value = setTimeout(() => {
// 可以在控制台 查看打印结果
console.log('submit-> value:', value);
console.log('submit-> senderValue', senderValue.value);
senderLoading.value = false;
ElMessage.success(`发送成功`);
}, 3500);
}
function handleCancel() {
senderLoading.value = false;
if (timeValue.value)
clearTimeout(timeValue.value);
timeValue.value = null;
ElMessage.info(`取消发送`);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex">
<el-button
type="primary"
style="width: fit-content"
@click="senderRef.clear()"
>
使用组件实例清空
</el-button>
<el-button
type="primary"
style="width: fit-content"
:disabled="!senderValue"
@click="senderRef.submit()"
>
使用组件实例提交
</el-button>
<el-button
type="primary"
style="width: fit-content"
@click="senderRef.cancel()"
>
使用组件实例取消
</el-button>
</div>
<Sender
ref="senderRef"
v-model="senderValue"
:submit-btn-disabled="submitBtnDisabled"
:loading="senderLoading"
clearable
@submit="handleSubmit"
@cancel="handleCancel"
/>
</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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
提交按钮禁用状态
可以通过 submit-btn-disabled
自定义 是否让发送按钮禁用。当禁用时,组件实例的 submit
方法将失效。
📌 Warning
组件内置的 发送按钮,是更据 v-model
绑定的,所以,当 v-model
绑定的值为空时,发送按钮将处于禁用状态。
但是,有这么一个场景。用户通过上传了文件,但是没有输入内容,此时,发送按钮依然处于禁用状态。
所以,为了 禁用逻辑的解耦,组件提供了 submit-btn-disabled
属性,用于自主控制发送按钮的禁用状态。
自定义 #action-list
时,此属性对 submit 事件同样生效。
<script setup lang="ts">
const senderRef = ref();
const timeValue = ref<NodeJS.Timeout | null>(null);
const senderValue = ref('');
const senderLoading = ref(false);
const submitBtnDisabled = ref(true);
function handleSubmit(value: string) {
ElMessage.info(`发送中`);
senderLoading.value = true;
timeValue.value = setTimeout(() => {
// 可以在控制台 查看打印结果
console.log('submit-> value:', value);
console.log('submit-> senderValue', senderValue.value);
senderLoading.value = false;
ElMessage.success(`发送成功`);
}, 3500);
}
function handleCancel() {
senderLoading.value = false;
if (timeValue.value)
clearTimeout(timeValue.value);
timeValue.value = null;
ElMessage.info(`取消发送`);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<span>这是内置的禁用逻辑:</span>
<Sender
ref="senderRef"
v-model="senderValue"
:loading="senderLoading"
clearable
@submit="handleSubmit"
@cancel="handleCancel"
/>
<span>自定义禁用逻辑:</span>
<Sender
ref="senderRef"
v-model="senderValue"
:submit-btn-disabled="submitBtnDisabled"
:loading="senderLoading"
clearable
@submit="handleSubmit"
@cancel="handleCancel"
/>
</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
43
44
45
46
47
48
49
50
51
52
自定义最大行数和最小行数
可以通过 autosize
设置输入框的最小展示行数和最大展示行数。 autosize
是一个对象 默认值为 { minRows: 1, maxRows: 6 }
。超出最大行数时,输入框会自动出现滚动条。
<script setup lang="ts">
const longerValue = `💌 欢迎使用 Element-Plus-X ~`.repeat(30);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<Sender :auto-size="{ minRows: 2, maxRows: 5 }" />
<Sender v-model="longerValue" />
</div>
</template>
2
3
4
5
6
7
8
9
10
输入框组件各种状态
可以通过简单属性是,实现组件的状态
💌 Info
- 通过
loading
属性,可以控制输入框是否加载中。 - 通过
readOnly
属性,可以控制输入框是否可编辑。 - 通过
disabled
属性,可以控制输入框是否禁用。 - 通过
clearable
属性,可以控制输入框是否出现删除按钮,实现清空。 - 通过
inputWidth
属性,可以控制输入框的宽度。默认为100%
。
<script setup lang="ts">
const senderReadOnlyValue = ref(`只读:💌 欢迎使用 Element-Plus-X ~`);
const senderClearableValue = ref(`可删除:💌 欢迎使用 Element-Plus-X ~`);
function handleSubmit(value: string) {
console.log(value);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<Sender loading placeholder="加载中..." @submit="handleSubmit" />
<Sender v-model="senderReadOnlyValue" read-only @submit="handleSubmit" />
<Sender
value="禁用:💌 欢迎使用 Element-Plus-X ~"
disabled
@submit="handleSubmit"
/>
<Sender v-model="senderClearableValue" clearable @submit="handleSubmit" />
<Sender
style="width: fit-content"
value="输入框最大宽度:💌 欢迎使用 Element-Plus-X ~"
input-width="150px"
@submit="handleSubmit"
/>
</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
提交方式
通过 submitType
控制换行与提交模式。默认 'enter'
。即 回车提交,'shift + Enter'
换行。
💌 Info
submitType='enter'
设置 回车提交,'shift + Enter'
换行。submitType='shiftEnter'
设置'shift + Enter'
提交,回车换行。submitType='cmdOrCtrlEnter'
设置'cmd + Enter'
或'ctrl + Enter'
提交,回车换行。submitType='altEnter'
设置'alt + Enter'
提交,回车换行。
<script setup lang="ts">
import type { SenderProps } from 'vue-element-plus-x/types/Sender';
const activeName = ref<SenderProps['submitType']>('enter');
const senderValue = ref('');
const senderLoading = ref(false);
function handleSubmit(value: string) {
ElMessage.info(`发送中`);
senderLoading.value = true;
setTimeout(() => {
// 可以在控制台 查看打印结果
console.log('submit-> value:', value);
console.log('submit-> senderValue', senderValue.value);
senderLoading.value = false;
ElMessage.success(`发送成功`);
}, 2000);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<el-radio-group v-model="activeName">
<el-radio-button value="enter">
enter
</el-radio-button>
<el-radio-button value="shiftEnter">
shiftEnter
</el-radio-button>
<el-radio-button value="cmdOrCtrlEnter">
cmdOrCtrlEnter
</el-radio-button>
<el-radio-button value="altEnter">
altEnter
</el-radio-button>
</el-radio-group>
<Sender
v-model="senderValue"
:submit-type="activeName"
:loading="senderLoading"
@submit="handleSubmit"
/>
</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
43
语音识别
📌 Warning
浏览器内置语音识别 API,可以使用组件库内置的 useRecord
hooks 更方便内置语音识别集成和控制
内置 语音识别
功能,通过 allowSpeech
属性开启即可。调用浏览器原生的语音识别 API,在 谷歌浏览器
中使用,需要在 🪄魔法环境
中才能正常使用。
💌 Info
如果你不想使用内置的 语音识别
功能,可以通过 @recording-change
事件来监听录音状态,自行实现语音识别功能。
你也可以通过组件 ref 实例对象进行调用
senderRef.value.startRecognition()
触发开始录音senderRef.value.stopRecognition()
触发结束录音
<script setup lang="ts">
const senderRef = ref();
const senderValue = ref('');
function onRecordingChange(recording: boolean) {
if (recording) {
ElMessage.success('开始录音');
}
else {
ElMessage.success('结束录音');
}
}
function onsubmit() {
ElMessage.success('发送成功');
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<span>内置语音识别:</span>
<Sender v-model="senderValue" allow-speech @submit="onsubmit" />
<span>自定义语音识别:</span>
<div style="display: flex">
<el-button
type="primary"
style="width: fit-content"
@click="senderRef.startRecognition()"
>
使用组件实例 开始录音
</el-button>
<el-button
type="primary"
style="width: fit-content"
@click="senderRef.stopRecognition()"
>
使用组件实例 结束录音
</el-button>
</div>
<Sender
ref="senderRef"
v-model="senderValue"
allow-speech
@recording-change="onRecordingChange"
/>
</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
43
44
45
46
47
变体-垂直样式
通过 variant
属性设置输入框的变体。默认 'default' | 上下结构 'updown'
这个属性,将左右结构的 输入框,变成 上下结构的 输入框。上面为 输入框,下面为 内置的 前缀和操作列表栏
<script setup lang="ts">
import { ElementPlus, Paperclip, Promotion } from '@element-plus/icons-vue';
const senderValue = ref('');
const isSelect = ref(false);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 20px">
<Sender v-model="senderValue" variant="updown" />
<Sender v-model="senderValue" variant="updown" clearable />
<Sender v-model="senderValue" variant="updown" clearable allow-speech />
<Sender
v-model="senderValue"
variant="updown"
:auto-size="{ minRows: 2, maxRows: 5 }"
clearable
allow-speech
placeholder="💌 在这里你可以自定义变体后的 prefix 和 action-list"
>
<template #prefix>
<div
style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
>
<el-button round plain color="#626aef">
<el-icon><Paperclip /></el-icon>
</el-button>
<div
:class="{ isSelect }"
style="
display: flex;
align-items: center;
gap: 4px;
padding: 2px 12px;
border: 1px solid silver;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
"
@click="isSelect = !isSelect"
>
<el-icon><ElementPlus /></el-icon>
<span>深度思考</span>
</div>
左边是自定义 prefix 前缀 右边是自定义 操作列表
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px">
<el-button round color="#626aef">
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="scss">
.isSelect {
color: #626aef;
border: 1px solid #626aef !important;
border-radius: 15px;
padding: 3px 12px;
font-weight: 700;
}
</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
自定义操作列表
📌 Warning
1.0.81 版本
前,在自定义插槽的时候,会牺牲内置的操作按钮。我们在 1.0.81 版本
推出了流式请求的 hooks,可以让用户更好的控制流式请求,从而更好的自己定义 #action-list
插槽。详情请查看我们的项目模版中主推的一个请求库,对标 Axios hook-fetch。
此温馨提示更新时间:2025-07-05
通过 #action-list
插槽用于自定义输入框的操作列表内容。
💌 Info
当你使用 #action-list
插槽时,会隐藏内置的输入框的操作按钮。你可以通过和 组件实例方法
相结合,实现更丰富的操作。
<script setup lang="ts">
import {
Delete,
Loading,
Operation,
Position,
Promotion,
Right,
Setting
} from '@element-plus/icons-vue';
const senderRef = ref();
const senderValue = ref('');
const loading = ref(false);
function handleSubmit() {
console.log('submit', senderValue.value);
senderRef.value.submit();
loading.value = true;
}
function handleCancel() {
console.log('cancel');
loading.value = false;
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<Sender>
<!-- 自定义操作列表 -->
<template #action-list>
<div class="action-list-self-wrap">
<el-button type="danger" circle>
<el-icon><Delete /></el-icon>
</el-button>
<el-button type="primary" circle style="rotate: -45deg">
<el-icon><Position /></el-icon>
</el-button>
</div>
</template>
</Sender>
<Sender>
<!-- 自定义操作列表 -->
<template #action-list>
<div class="action-list-self-wrap">
<el-button type="primary" plain circle color="#626aef">
<el-icon><Operation /></el-icon>
</el-button>
<el-button type="primary" circle color="#626aef">
<el-icon><Right /></el-icon>
</el-button>
</div>
</template>
</Sender>
<Sender>
<!-- 自定义操作列表 -->
<template #action-list>
<div class="action-list-self-wrap">
<el-button plain circle color="#eebe77">
<el-icon><Setting /></el-icon>
</el-button>
<el-button type="primary" plain circle>
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
</Sender>
<Sender ref="senderRef" v-model="senderValue" :loading="loading">
<!-- 自定义操作列表 -->
<template #action-list>
<div class="action-list-self-wrap">
<el-button
v-if="loading"
type="primary"
plain
circle
@click="handleCancel"
>
<el-icon class="is-loaidng">
<Loading />
</el-icon>
</el-button>
<el-button v-else plain circle @click="handleSubmit">
<el-icon><Position /></el-icon>
</el-button>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="less">
.action-list-self-wrap {
display: flex;
align-items: center;
& > span {
width: 120px;
font-weight: 600;
color: var(--el-color-primary);
}
}
.is-loaidng {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</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
自定义前缀
通过 #prefix
插槽用于自定义输入框的前缀内容。
<script setup lang="ts">
import { CircleClose, Link } from '@element-plus/icons-vue';
const senderRef = ref();
const senderValue = ref('');
const showHeaderFlog = ref(false);
onMounted(() => {
showHeaderFlog.value = true;
senderRef.value.openHeader();
});
function openCloseHeader() {
if (!showHeaderFlog.value) {
senderRef.value.openHeader();
}
else {
senderRef.value.closeHeader();
}
showHeaderFlog.value = !showHeaderFlog.value;
}
function closeHeader() {
showHeaderFlog.value = false;
senderRef.value.closeHeader();
}
</script>
<template>
<div
style="
display: flex;
flex-direction: column;
gap: 12px;
height: 230px;
justify-content: flex-end;
"
>
<Sender ref="senderRef" v-model="senderValue">
<template #header>
<div class="header-self-wrap">
<div class="header-self-title">
<div class="header-left">
💯 欢迎使用 Element Plus X
</div>
<div class="header-right">
<el-button @click.stop="closeHeader">
<el-icon><CircleClose /></el-icon>
<span>关闭头部</span>
</el-button>
</div>
</div>
<div class="header-self-content">
🦜 自定义头部内容
</div>
</div>
</template>
<!-- 自定义前缀 -->
<template #prefix>
<div class="prefix-self-wrap">
<el-button dark>
<el-icon><Link /></el-icon>
<span>自定义前缀</span>
</el-button>
<el-button color="#626aef" :dark="true" @click="openCloseHeader">
打开/关闭头部
</el-button>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="less">
.header-self-wrap {
display: flex;
flex-direction: column;
padding: 16px;
height: 200px;
.header-self-title {
width: 100%;
display: flex;
height: 30px;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
.header-self-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #626aef;
font-weight: 600;
}
}
.prefix-self-wrap {
display: flex;
}
</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
自定义头部
通过 #header
插槽用于自定义输入框的头部内容。
💌 Info
通过组件实例控制 头部容器 展开收起
senderRef.value.openHeader()
打开头部容器senderRef.value.closeHeader()
关闭头部容器
<script setup lang="ts">
import { CircleClose } from '@element-plus/icons-vue';
const senderRef = ref();
const senderValue = ref('');
const showHeaderFlog = ref(false);
onMounted(() => {
showHeaderFlog.value = true;
senderRef.value.openHeader();
});
function openCloseHeader() {
if (!showHeaderFlog.value) {
senderRef.value.openHeader();
}
else {
senderRef.value.closeHeader();
}
showHeaderFlog.value = !showHeaderFlog.value;
}
function closeHeader() {
showHeaderFlog.value = false;
senderRef.value.closeHeader();
}
</script>
<template>
<div
style="
display: flex;
flex-direction: column;
gap: 12px;
height: 300px;
justify-content: space-between;
"
>
<el-button style="width: fit-content" @click="openCloseHeader">
{{ showHeaderFlog ? '关闭头部' : '打开头部' }}
</el-button>
<Sender ref="senderRef" v-model="senderValue">
<template #header>
<div class="header-self-wrap">
<div class="header-self-title">
<div class="header-left">
💯 欢迎使用 Element Plus X
</div>
<div class="header-right">
<el-button @click.stop="closeHeader">
<el-icon><CircleClose /></el-icon>
<span>关闭头部</span>
</el-button>
</div>
</div>
<div class="header-self-content">
🦜 自定义头部内容
</div>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="less">
.header-self-wrap {
display: flex;
flex-direction: column;
padding: 16px;
height: 200px;
.header-self-title {
width: 100%;
display: flex;
height: 30px;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
.header-self-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #626aef;
font-weight: 600;
}
}
</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
自定义底部
通过 #footer
插槽设置输入框 底部内容
💌 Info
如果你想要设置 #footer
插槽,不想要 updown 变体的内置布局,可以再添加 showUpdown
属性,隐藏 updown 变体的内置布局
<script setup lang="ts">
import { ElementPlus, Paperclip, Promotion } from '@element-plus/icons-vue';
const senderValue = ref('');
const isSelect = ref(false);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 20px">
<Sender
v-model="senderValue"
:auto-size="{ minRows: 1, maxRows: 5 }"
clearable
allow-speech
placeholder="💌 欢迎使用 Element-Plus-X"
>
<template #prefix>
<div
style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
>
<el-button round plain color="#626aef">
<el-icon><Paperclip /></el-icon>
</el-button>
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px">
<el-button round color="#626aef">
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
<!-- 自定义 底部插槽 -->
<template #footer>
<div
style="
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
"
>
默认变体 自定义底部
</div>
</template>
</Sender>
<Sender
v-model="senderValue"
variant="updown"
:auto-size="{ minRows: 2, maxRows: 5 }"
clearable
allow-speech
placeholder="💌 在这里你可以自定义变体后的 prefix 和 action-list"
>
<template #prefix>
<div
style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
>
<el-button round plain color="#626aef">
<el-icon><Paperclip /></el-icon>
</el-button>
<div
:class="{ isSelect }"
style="
display: flex;
align-items: center;
gap: 4px;
padding: 2px 12px;
border: 1px solid silver;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
"
@click="isSelect = !isSelect"
>
<el-icon><ElementPlus /></el-icon>
<span>深度思考</span>
</div>
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px">
<el-button round color="#626aef">
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
<!-- 自定义 底部插槽 -->
<template #footer>
<div
style="
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
"
>
updown 变体 自定义底部
</div>
</template>
</Sender>
<Sender
v-model="senderValue"
variant="updown"
:auto-size="{ minRows: 2, maxRows: 5 }"
clearable
allow-speech
placeholder="💌 通过设置 showUpdown 为 false 隐藏 updown 变体的内置布局"
:show-updown="false"
>
<template #prefix>
<div
style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
>
<el-button round plain color="#626aef">
<el-icon><Paperclip /></el-icon>
</el-button>
<div
:class="{ isSelect }"
style="
display: flex;
align-items: center;
gap: 4px;
padding: 2px 12px;
border: 1px solid silver;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
"
@click="isSelect = !isSelect"
>
<el-icon><ElementPlus /></el-icon>
<span>深度思考</span>
</div>
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px">
<el-button round color="#626aef">
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
<!-- 自定义 底部插槽 -->
<template #footer>
<div
style="
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
text-align: center;
"
>
showUpdown 属性 隐藏 updown 变体内置布局样式 + #footer
底部插槽结合,完全让你来控制底部内容
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="scss">
.isSelect {
color: #626aef;
border: 1px solid #626aef !important;
border-radius: 15px;
padding: 3px 12px;
font-weight: 700;
}
</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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
自定义输入框样式
通过 input-style
方便对输入框的样式透传
<script setup lang="ts">
import { ElementPlus, Paperclip, Promotion } from '@element-plus/icons-vue';
const senderValue = ref('这是自定义输入框样式');
const isSelect = ref(false);
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 20px">
<Sender
v-model="senderValue"
variant="updown"
:input-style="{
backgroundColor: 'rgb(243 244 246)',
color: '#626aef',
fontSize: '24px',
fontWeight: 700
}"
style="background: rgb(243 244 246); border-radius: 8px"
/>
<Sender
v-model="senderValue"
variant="updown"
:input-style="{
backgroundColor: 'transparent',
color: '#F0F2F5',
fontSize: '24px',
fontWeight: 700
}"
style="
background-image: linear-gradient(to left, #434343 0%, black 100%);
border-radius: 8px;
"
/>
<Sender
v-model="senderValue"
:input-style="{
backgroundColor: 'transparent',
color: '#FF5454',
fontSize: '20px',
fontWeight: 700
}"
style="
background-image: linear-gradient(
to top,
#fdcbf1 0%,
#fdcbf1 1%,
#e6dee9 100%
);
border-radius: 8px;
"
/>
<Sender
v-model="senderValue"
variant="updown"
:input-style="{
backgroundColor: 'transparent',
color: '#303133',
fontSize: '16px',
fontWeight: 700
}"
style="
background-image: linear-gradient(
to top,
#d5d4d0 0%,
#d5d4d0 1%,
#eeeeec 31%,
#efeeec 75%,
#e9e9e7 100%
);
border-radius: 8px;
"
>
<template #prefix>
<div
style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
>
<el-button round plain color="#626aef">
<el-icon><Paperclip /></el-icon>
</el-button>
<div
:class="{ isSelect }"
style="
display: flex;
align-items: center;
gap: 4px;
padding: 2px 12px;
border: 1px solid black;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
color: black;
"
@click="isSelect = !isSelect"
>
<el-icon><ElementPlus /></el-icon>
<span>深度思考</span>
</div>
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px">
<el-button round color="#626aef">
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="scss">
.isSelect {
color: #626aef !important;
border: 1px solid #626aef !important;
border-radius: 15px;
padding: 3px 12px;
font-weight: 700;
}
</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
触发指令
输入框,内置指令弹框,方便调用指令操作
💌 Info
- 通过
triggerStrings
属性,设置 指令触发字符,类型一个 字符数组 ['/', '@'] - 通过 v-model 绑定
triggerPopoverVisible
属性,控制指令弹框是否可见
v-model:trigger-popover-visible="triggerVisible"
- 通过
triggerPopoverWidth
属性,设置指令弹框宽度 默认'fit-content'
- 通过
triggerPopoverLeft
属性,设置指令弹框距离左侧距离 默认'0px'
- 通过
triggerPopoverOffset
属性,设置指令弹框和输入框的距离 默认8
- 通过
triggerPopoverPlacement
属性,设置指令弹框弹出位置 同 el-popover 的 placement 属性一致,默认'top-start'
取值 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end'
@trigger
设置指令弹框显示隐藏发生改变的回调方法
💡 Tip
@trigger
当你要在指令被触发的时候做某些事,但是不想要内置的弹框样式时,可以不用 v-model:trigger-popover-visible="triggerVisible",这样 内置弹框 就不会出现
<script setup lang="ts">
import type { TriggerEvent } from 'vue-element-plus-x/types/Sender';
const senderValue = ref('');
const senderValue1 = ref('');
const triggerVisible = ref(false);
const dialogVisible = ref(false);
function onTrigger(event: TriggerEvent) {
console.log('onTrigger', event);
}
function onTrigger1(event: TriggerEvent) {
console.log('onTrigger1', event);
dialogVisible.value = event.isOpen;
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 20px">
<Sender
v-model="senderValue"
v-model:trigger-popover-visible="triggerVisible"
placeholder="输入 / 和 @ 触发指令弹框"
clearable
:trigger-strings="['/', '@']"
trigger-popover-width="400px"
trigger-popover-left="0px"
:trigger-popover-offset="10"
trigger-popover-placement="top-start"
@trigger="onTrigger"
/>
<Sender
v-model="senderValue1"
placeholder="输入 XXX 和 QQ 触发指令弹框 在这里不使用 v-model:trigger-popover-visible 绑定,也可以触发 @trigger 事件 请在控制台查看触发事件"
clearable
:trigger-strings="['XXX', 'QQ']"
trigger-popover-width="400px"
trigger-popover-left="0px"
:trigger-popover-offset="30"
trigger-popover-placement="top-start"
@trigger="onTrigger1"
/>
<el-dialog
v-model="dialogVisible"
title="💖 欢迎使用 Element-Plus-X"
width="500"
>
<span>触发事件已经执行,可以是打开弹框、打开抽屉、任何你需要的事件 ~</span>
</el-dialog>
</div>
</template>
<style scoped lang="scss"></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
自定义指令弹框
💡 Tip
自定义弹框内容,如果你只是简单匹配开头某个字符,可以这个组件。
📌 Warning
💌 温馨提示:V1.3.1 开始,组件 ref 可以获取弹框打开状态属性 popoverVisible
,和弹框内置输入框的实例 inputInstance
。
意味着:
- 可以通过弹框的是否打开装填进行一些判断处理。
- 弹框将可以支持更丰富的自定义事件。
该温馨提示时间 2025-07-21
<script setup lang="ts">
import type { TriggerEvent } from 'vue-element-plus-x/types/Sender';
import { ElMessage } from 'element-plus';
import { Sender } from 'vue-element-plus-x';
const senderValue = ref('');
const triggerVisible = ref(false);
const senderRef = ref<InstanceType<typeof Sender>>();
onMounted(() => {
window.addEventListener('keydown', handleWindowKeydown);
senderRef.value?.inputInstance.addEventListener(
'keydown',
handleInputKeydown
);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleWindowKeydown);
senderRef.value?.inputInstance.removeEventListener(
'keydown',
handleInputKeydown
);
});
function onTrigger(event: TriggerEvent) {
ElMessage.success('指令被触发了');
console.log('onTrigger', event);
}
function handleWindowKeydown(e: KeyboardEvent) {
switch (e.key) {
case 'w':
ElMessage.success(`w 被按下,输入框不受影响`);
console.log('w 被按下');
break;
case 'a':
ElMessage.success(`a 被按下,输入框不受影响`);
console.log('a 被按下');
break;
case 's':
ElMessage.success(`s 被按下,输入框不受影响`);
console.log('s 被按下');
break;
case 'd':
ElMessage.success(`d 被按下,输入框不受影响`);
console.log('d 被按下');
break;
}
}
// 当弹框显示时,阻止输入框的部分按键事件,避免和提及弹框的全局自定义键盘事件冲突
function handleInputKeydown(e: KeyboardEvent) {
if (['w', 'a', 's', 'd'].includes(e.key)) {
e.preventDefault();
}
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 20px">
<Sender
ref="senderRef"
v-model="senderValue"
v-model:trigger-popover-visible="triggerVisible"
placeholder="输入 / 和 @ 触发指令弹框"
clearable
:trigger-strings="['/', '@']"
trigger-popover-width="400px"
trigger-popover-left="0px"
:trigger-popover-offset="10"
trigger-popover-placement="top-start"
@trigger="onTrigger"
>
<!-- 自定义 提及弹框 -->
<template #trigger-popover="{ triggerString }">
当前触发的字符为:{{ `${triggerString}` }}
这是我自定义的弹框,在这里你可以自定义弹框内容。包括对弹框做一些按键控制的自定义操作。请尝试控制方向
w/a/s/d 这几个按键。
</template>
</Sender>
</div>
</template>
<style scoped lang="scss"></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
输入框聚焦控制
通过 ref 选项控制聚焦。
💌 Info
通过组件实例控制
senderRef.value.focus('all')
聚焦到整个文本 (默认)senderRef.value.focus('start')
聚焦到文本最前方senderRef.value.focus('end')
聚焦到文本最后方senderRef.value.blur()
失去焦点
<script setup lang="ts">
const senderRef = ref();
const senderValue = ref('🐳 欢迎使用 Element Plus X');
function blur() {
senderRef.value.blur();
}
function focus(type = 'all') {
senderRef.value.focus(type);
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex">
<el-button dark type="success" plain @click="focus('start')">
文本最前方
</el-button>
<el-button dark type="success" plain @click="focus('end')">
文本最后方
</el-button>
<el-button dark type="success" plain @click="focus('all')">
整个文本
</el-button>
<el-button dark type="success" plain @click="blur">
失去焦点
</el-button>
</div>
<Sender ref="senderRef" v-model="senderValue" />
</div>
</template>
<style scoped lang="less">
.header-self-wrap {
display: flex;
flex-direction: column;
padding: 16px;
height: 200px;
.header-self-title {
width: 100%;
display: flex;
height: 30px;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
.header-self-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #626aef;
font-weight: 600;
}
}
</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
黏贴文件
使用 pasteFile
获取黏贴的文件,配合 Attachments
进行文件上传展示。
<script setup lang="ts">
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import { CloseBold, Link } from '@element-plus/icons-vue';
const senderRef = ref();
const senderValue = ref('');
const showHeaderFlog = ref(false);
type SelfFilesCardProps = FilesCardProps & {
id?: number | string;
};
const files = ref<SelfFilesCardProps[]>([]);
function handleOpenHeader() {
if (!showHeaderFlog.value) {
senderRef.value.openHeader();
}
else {
senderRef.value.closeHeader();
}
showHeaderFlog.value = !showHeaderFlog.value;
}
function closeHeader() {
showHeaderFlog.value = false;
senderRef.value.closeHeader();
}
function handlePasteFile(firstFile: File, fileList: FileList) {
showHeaderFlog.value = true;
senderRef.value.openHeader();
const fileArray = Array.from(fileList);
fileArray.forEach((file, index) => {
files.value.push({
id: index,
uid: index + '_' + file.name + '_' + file.size,
name: file.name,
fileSize: file.size,
imgFile: file,
showDelIcon: true,
imgVariant: 'square'
});
});
}
async function handleHttpRequest(options: any) {
const formData = new FormData();
formData.append('file', options.file);
ElMessage.info('上传中...');
setTimeout(() => {
const res = {
message: '文件上传成功',
fileName: options.file.name,
uid: options.file.uid,
fileSize: options.file.size,
imgFile: options.file
};
files.value.push({
id: files.value.length,
uid: res.uid,
name: res.fileName,
fileSize: res.fileSize,
imgFile: res.imgFile,
showDelIcon: true,
imgVariant: 'square'
});
ElMessage.success('上传成功');
}, 1000);
}
function handleDeleteCard(item: SelfFilesCardProps) {
files.value = files.value.filter((items: any) => items.id !== item.id);
ElMessage.success('删除成功');
}
</script>
<template>
<div
style="
display: flex;
flex-direction: column;
gap: 12px;
height: 230px;
justify-content: flex-end;
"
>
<Sender ref="senderRef" v-model="senderValue" @paste-file="handlePasteFile">
<template #header>
<div class="header-self-wrap">
<div class="header-self-title">
<div class="header-left">
Attachments
</div>
<div class="header-right">
<el-button @click.stop="closeHeader">
<el-icon><CloseBold /></el-icon>
</el-button>
</div>
</div>
<Attachments
:items="files"
:http-request="handleHttpRequest"
@delete-card="handleDeleteCard"
/>
</div>
</template>
<!-- 自定义前缀 -->
<template #prefix>
<div class="prefix-self-wrap">
<el-button @click="handleOpenHeader">
<el-icon><Link /></el-icon>
</el-button>
</div>
</template>
</Sender>
</div>
</template>
<style scoped lang="less">
.header-self-wrap {
display: flex;
flex-direction: column;
padding: 16px;
height: 200px;
.header-self-title {
width: 100%;
display: flex;
height: 30px;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
.header-self-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #626aef;
font-weight: 600;
}
}
.prefix-self-wrap {
display: flex;
}
</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
142
143
144
145
146
147
148
149
150
151
152
153
属性
属性名 | 类型 | 是否必填 | 默认值 | 说明 |
---|---|---|---|---|
v-model | String | 否 | '' | 输入框的绑定值,使用 v-model 进行双向绑定。 |
placeholder | String | 否 | '' | 输入框的提示语文本。 |
auto-size | Object | 否 | { minRows:1, maxRows:6 } | 设置输入框的最小展示行数和最大展示行数。 |
read-only | Boolean | 否 | false | 输入框是否为只读状态。 |
disabled | Boolean | 否 | false | 输入框是否为禁用状态。 |
submitBtnDisabled | Boolean | undefined | 否 | undefined | 内置发送按钮禁用状态。(注意使用场景) |
loading | Boolean | 否 | false | 是否显示加载状态。为 true 时,输入框会显示加载动画。 |
clearable | Boolean | 否 | false | 输入框是否可清空内容。展示默认清空按钮 |
allowSpeech | Boolean | 否 | false | 是否允许语音输入。默认展示内置语音识别按钮,内置浏览器内置语音识别 API |
submitType | String | 否 | 'enter' | 提交方式,支持 'shiftEnter' (按 Shift + Enter 提交)、 'cmdOrCtrlEnter' (按 Command + Enter 或 Ctrl + Enter 提交)、 'altEnter' (按 Alt + Enter 提交)。 |
headerAnimationTimer | Number | 否 | 300 | 输入框的自定义头部显示时长,单位为 ms 。 |
inputWidth | String | 否 | '100%' | 输入框的宽度。 |
variant | String | 否 | 'default' | 输入框的变体类型,支持 'default' 、'updown' 。 |
showUpdown | Boolean | 否 | true | 当变体为 updown 时,是否展示内置样式。 |
inputStyle | Object | 否 | {} | 输入框的样式。 |
triggerStrings | string[] | 否 | [] | 触发指令的 字符串数组 。 |
triggerPopoverVisible | Boolean | 否 | false | 触发指令的 弹框 是否可见。需要使用 v-model:triggerPopoverVisible 进行控制。 |
triggerPopoverWidth | String | 否 | 'fit-content' | 触发指令的 弹框 的宽度。可使用百分比等css单位。 |
triggerPopoverLeft | String | 否 | '0px' | 触发指令的 弹框 的左边距。可使用百分比等css单位。 |
triggerPopoverOffset | Number | 否 | 8 | 触发指令的 弹框 的间距。只能是数字类型,单位px |
triggerPopoverPlacement | String | 否 | 'top-start' | 触发指令的 弹框 的位置。取值:'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end' |
事件
事件名 | 说明 | 回调参数 |
---|---|---|
submit | 内置 提交按钮 提交时触发的事件。 | 无 |
cancel | 内置 loading按钮 点击时触发的事件。 | 无 |
recordingChange | 内置语音识别状态变化时触发的事件。 | 无 |
trigger | 指令弹框发生变化时触发的事件。 | interface TriggerEvent{oldValue: string; newValue: string; isOpen: boolean; } |
pasteFile | 黏贴文件时触发的事件 | interface PasteFileEvent{firstFile: File; fileList: FileList} |
Ref 实例方法
属性名 | 类型 | 描述 |
---|---|---|
openHeader | Function | 打开输入框的自定义头部。 |
closeHeader | Function | 关闭输入框的自定义头部。 |
clear | Function | 清空输入框的内容。 |
blur | Function | 移除输入框的焦点。 |
focus | Function | 聚焦输入框。 默认 focus('all') 聚焦整个文本,focus('start') 聚焦文本最前方,focus('end') 聚焦文本最后方。 |
submit | Function | 提交输入内容。 |
cancel | Function | 取消加载状态。 |
startRecognition | Function | 开始语音识别。 |
stopRecognition | Function | 停止语音识别。 |
popoverVisible | Boolean | 触发指令的 弹框 可见性。 |
inputInstance | Object | 输入框实例。 |
插槽
插槽名 | 参数 | 类型 | 描述 |
---|---|---|---|
#header | - | Slot | 用于自定义输入框的头部内容。 |
#prefix | - | Slot | 用于自定义输入框的前缀内容。 |
#action-list | - | Slot | 用于自定义输入框的操作列表内容。 |
#footer | - | Slot | 用于自定义输入框的尾部内容。 |
功能特性
- 焦点控制:支持将焦点设置到文本最前方、最后方或选中整个文本,也可取消焦点。
- 自定义内容:提供头部、前缀、操作列表等插槽,允许用户自定义这些部分的内容。
- 提交功能:支持按
Shift + Enter
提交输入内容,提交后可执行自定义操作。 - 加载状态:可显示加载状态,模拟提交处理过程。
- 语音输入:支持语音输入功能,提升输入的便捷性。
- 清空功能:输入框可清空内容,方便用户重新输入。