吃透 Sender 交互逻辑:提交、快捷键、事件与方法实战运用
消息输入组件的交互逻辑决定了用户的使用体验。TinyRobot Sender 在提交方式、快捷键、事件体系和方法调用方面做了精心设计,本文带你深入理解每一项交互细节。
一、提交方式深度解析
Sender 提供三种提交方式,通过 submitType 属性灵活切换:
typescript
type SubmitTrigger = 'enter' | 'ctrlEnter' | 'shiftEnter'
三种提交方式对比
| 配置值 | 提交快捷键 | 换行快捷键 | 适用场景 |
|---|---|---|---|
enter |
Enter | Ctrl+Enter 或 Shift+Enter | 即时通讯、快速交互 |
ctrlEnter |
Ctrl+Enter | Enter | 长文本编辑、邮件场景 |
shiftEnter |
Shift+Enter | Enter | 与 ctrlEnter 互为替代 |
实战代码:动态切换提交方式
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, type SubmitTrigger } from '@opentiny/tiny-robot'
const content = ref('')
const submittedContent = ref('')
const submitType = ref<SubmitTrigger>('enter')
const handleSubmit = (value: string) => {
submittedContent.value = value
console.log('提交内容:', value)
}
</script>
<template>
<div class="options-panel">
<label>提交方式:</label>
<label><input type="radio" value="enter" v-model="submitType" /> Enter</label>
<label><input type="radio" value="ctrlEnter" v-model="submitType" /> Ctrl + Enter</label>
<label><input type="radio" value="shiftEnter" v-model="submitType" /> Shift + Enter</label>
</div>
<tr-sender v-model="content" :submitType="submitType" placeholder="请输入内容..." @submit="handleSubmit" />
<div v-if="submittedContent" class="result">
<strong>已提交:</strong> {{ submittedContent }}
</div>
</template>
单行模式下的特殊行为
在单行模式(mode="single")下使用换行快捷键时,会自动切换为多行模式:
ini
submitType="enter" + mode="single"
→ 按 Enter:提交
→ 按 Ctrl+Enter 或 Shift+Enter:自动切换到多行模式并换行
二、快捷键完整参考
| 快捷键 | 功能 | 适用条件 |
|---|---|---|
| Enter | 提交内容 / 换行 | submitType="enter" |
| Ctrl+Enter | 提交内容 / 换行 | submitType="ctrlEnter" / submitType="enter" |
| Shift+Enter | 提交内容 / 换行 | submitType="shiftEnter" / submitType="enter" |
| Tab | 选中联想项 | Suggestion 扩展开启时 |
| Esc | 关闭联想 | Suggestion 扩展开启时 |
| ↑ / ↓ | 导航联想项 | Suggestion 扩展开启时 |
| Backspace | 删除提及项 | Mention 扩展开启时 |
自定义联想选中按键
通过 Suggestion 扩展的 activeSuggestionKeys 配置自定义选中按键:
typescript
TrSender.Suggestion.configure({
items: suggestions,
activeSuggestionKeys: ['Enter', 'Tab'], // 默认支持 Enter 和 Tab
})
移动端键盘回车提示
通过 enterkeyhint 属性控制移动端虚拟键盘回车键的显示文本:
typescript
type EnterKeyHint = 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'
vue
<!-- 移动端场景:显示"发送"按钮 -->
<tr-sender enterkeyhint="send" mode="single" />
<!-- 搜索场景:显示"搜索"按钮 -->
<tr-sender enterkeyhint="search" mode="single" />
三、事件体系详解
核心事件
| 事件名 | 说明 | 回调参数 |
|---|---|---|
update:modelValue |
内容更新 | (value: string) |
submit |
提交内容 | (text: string, data?: StructuredData) |
clear |
清空内容 | () |
focus |
获得焦点 | (event: FocusEvent) |
blur |
失去焦点 | (event: FocusEvent) |
input |
输入变化 | (value: string) |
cancel |
取消操作(loading 状态下) | () |
submit 事件:纯文本 vs 结构化数据
submit 事件是 Sender 最核心的事件,它同时返回两个参数:
typescript
function handleSubmit(text: string, data?: StructuredData) {
// text: 纯文本内容,适用于简单场景
// data: 结构化数据数组,仅在使用 Template 或 Mention 扩展时返回
}
简单场景 --- 只使用 text:
vue
<script setup>
const handleSubmit = (text: string) => {
// 直接将纯文本发送给 AI
aiClient.send(text)
}
</script>
<template>
<tr-sender @submit="handleSubmit" />
</template>
复杂场景 --- 使用 data 提取特殊节点:
typescript
// Mention 扩展的结构化数据
function handleSubmit(text: string, data?: StructuredData) {
// text: "帮我分析 @张三 的周报"
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'mention', content: '张三', value: '用户ID' },
// { type: 'text', content: ' 的周报' }
// ]
// 提取所有提及项
const mentions = data?.filter(item => item.type === 'mention') || []
// 自定义 Slack 风格格式
const customText = data?.map(item =>
item.type === 'mention' ? `<@${item.value}>` : item.content
).join('')
}
typescript
// Template 扩展的结构化数据
function handleSubmit(text: string, data?: StructuredData) {
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'block', content: '张三' },
// { type: 'text', content: ' 的周报' }
// ]
// 提取所有模板块
const blocks = data?.filter(item => item.type === 'block') || []
// 自定义 Mustache 风格格式
const customText = data?.map(item =>
item.type === 'block' ? `{{${item.content}}}` : item.content
).join('')
}
cancel 事件实战
在 AI 响应场景中,cancel 事件用于取消正在进行的操作:
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const content = ref('')
const loading = ref(false)
const message = ref('')
const handleSubmit = (text: string) => {
loading.value = true
message.value = '正在处理...'
// 模拟 AI 响应
setTimeout(() => {
loading.value = false
message.value = `AI 回复: 收到您的消息 "${text}"`
content.value = ''
}, 3000)
}
const handleCancel = () => {
loading.value = false
message.value = '❌ 已取消响应'
}
</script>
<template>
<tr-sender
v-model="content"
:loading="loading"
stop-text="停止响应"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</template>
UploadButton 事件
| 事件 | 说明 | 回调参数 |
|---|---|---|
select |
文件选择成功 | (files: File[]) |
error |
文件验证失败 | (error: Error, file?: File) |
vue
<template>
<tr-sender>
<template #footer-right>
<UploadButton
accept="image/*"
:multiple="true"
@select="handleFiles"
@error="handleFileError"
/>
</template>
</tr-sender>
</template>
<script setup>
const handleFiles = (files: File[]) => {
console.log('选择了文件:', files)
}
const handleFileError = (error: Error, file?: File) => {
console.error('文件验证失败:', error.message)
}
</script>
VoiceButton 事件
| 事件 | 说明 | 回调参数 |
|---|---|---|
speech-start |
开始录音 | () |
speech-interim |
中间结果 | (transcript: string) |
speech-final |
最终结果 | (transcript: string) |
speech-end |
结束录音 | (transcript?: string) |
speech-error |
识别错误 | (error: Error) |
vue
<template>
<tr-sender>
<template #footer-right>
<VoiceButton
@speech-start="onStart"
@speech-interim="onInterim"
@speech-final="onFinal"
@speech-end="onEnd"
@speech-error="onError"
/>
</template>
</tr-sender>
</template>
四、方法调用实战
方法一览
| 方法 | 说明 | 参数 | 返回值 |
|---|---|---|---|
focus() |
使输入框获取焦点 | - | void |
blur() |
使输入框失去焦点 | - | void |
clear() |
清空输入内容 | - | void |
submit() |
手动触发提交 | - | void |
setContent(content) |
设置编辑器内容 | content: string |
void |
getContent() |
获取编辑器内容 | - | string |
cancel() |
手动触发取消 | - | void |
实战案例:通过按钮控制输入框
vue
<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const chatInputRef = ref()
const content = ref('')
const result = ref('')
const handleFocus = () => {
chatInputRef.value?.focus()
result.value = '已聚焦'
}
const handleBlur = () => {
chatInputRef.value?.blur()
result.value = '已失焦'
}
const handleSetContent = () => {
chatInputRef.value?.setContent('这是通过方法设置的内容')
result.value = '已设置内容'
}
const handleGetContent = () => {
const c = chatInputRef.value?.getContent()
result.value = `当前内容: ${c}`
}
const handleClear = () => {
chatInputRef.value?.clear()
result.value = '已清空'
}
const handleSubmitMethod = () => {
chatInputRef.value?.submit()
}
const onSubmit = (value: string) => {
result.value = `已提交: ${value}`
}
</script>
<template>
<div>
<div class="controls">
<button @click="handleFocus">聚焦</button>
<button @click="handleBlur">失焦</button>
<button @click="handleSetContent">设置内容</button>
<button @click="handleGetContent">获取内容</button>
<button @click="handleClear">清空</button>
<button @click="handleSubmitMethod">提交</button>
</div>
<tr-sender
ref="chatInputRef"
v-model="content"
placeholder="通过上方按钮控制输入框..."
mode="multiple"
@submit="onSubmit"
/>
</div>
</template>
UploadButton 方法
| 方法 | 说明 | 参数 | 返回值 |
|---|---|---|---|
open() |
打开文件选择器 | - | void |
vue
<script setup>
const uploadRef = ref()
const triggerUpload = () => uploadRef.value?.open()
</script>
<template>
<UploadButton ref="uploadRef" />
<button @click="triggerUpload">触发上传</button>
</template>
VoiceButton 方法
| 方法 | 说明 | 参数 | 返回值 |
|---|---|---|---|
start() |
开始录音 | - | void |
stop() |
停止录音 | - | void |
vue
<script setup>
const voiceRef = ref()
const triggerVoice = () => voiceRef.value?.start()
</script>
<template>
<VoiceButton ref="voiceRef" />
<button @click="triggerVoice">触发录音</button>
</template>
五、常见交互场景
场景1:表单验证 + 动态禁用
vue
<script setup>
import { ref, computed } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const content = ref('')
const isValid = computed(() => content.value.length >= 5)
const defaultActions = computed(() => ({
submit: {
disabled: !isValid.value,
tooltip: isValid.value ? '发送消息' : '请输入至少 5 个字符',
},
clear: { tooltip: '清空内容' },
}))
</script>
<template>
<tr-sender v-model="content" :default-actions="defaultActions" clearable />
</template>
场景2:AI 流式响应 + 取消操作
vue
<script setup>
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
const content = ref('')
const loading = ref(false)
const handleSubmit = async (text: string) => {
loading.value = true
try {
await sendToAI(text)
} finally {
loading.value = false
}
}
const handleCancel = () => {
abortAIRequest()
loading.value = false
}
</script>
<template>
<tr-sender
v-model="content"
:loading="loading"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</template>
场景3:外部按钮触发提交
vue
<script setup>
const senderRef = ref()
const handleExternalSubmit = () => {
senderRef.value?.submit()
}
</script>
<template>
<button @click="handleExternalSubmit">外部提交</button>
<tr-sender ref="senderRef" v-model="content" @submit="onSubmit" />
</template>
六、总结
Sender 的交互体系设计遵循"灵活但可控"的原则:
- 提交方式:三种快捷键配置覆盖主流输入场景
- 事件体系:submit 双参数设计兼顾简单和复杂场景
- 方法调用:focus/blur/setContent/getContent 提供完整的程序化控制
- 取消机制:loading + cancel 组合完美适配 AI 流式响应场景
- 结构化数据:Template 和 Mention 扩展自动产出结构化数据
掌握这些交互细节,你就能在实际项目中精确控制输入行为,打造流畅的用户体验。
🔗 TinyRobot 官网 :tiny-robot.opentiny.design
🔗 GitHub 仓库 :github.com/opentiny/ti...