---
title: "ChatComposer 消息输入"
description: "带上下文托盘的 AI 消息输入框"
category: AiChat
status: beta
since: 1.0.0
tags: [chat, composer, input, tray]
syncStatus: reviewed
verified: true
---

<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
const sent = ref<string[]>([])
function onSend(payload: { text: string }) {
  sent.value.unshift(payload.text)
  text.value = ''
}
</script>

## 用法

### 基础
:::TuffDemoWrapper{demo="ChatComposerChatComposerDemo" code-lang="vue"}
---
code: |
  <template>
    <TxChatComposer v-model="text" @send="onSend" />
  </template>
---
:::

### 托盘换位
`tray` 插槽在卡片外渲染托盘，`trayPlacement` 决定它在卡片下方（默认）或上方。
:::TuffDemoWrapper{demo="ChatComposerTrayDemo" code-lang="vue"}
---
code: |
  <template>
    <TxChatComposer v-model="text" :tray-placement="placement" tray-label="上下文" show-attachment-button>
      <template #tray>
        <TxModeChip v-if="placement === 'bottom'" icon="i-carbon-plug" label="连接应用" @click="placement = 'top'" />
        <TxModeChip v-else icon="i-carbon-folder" label="选择项目" @click="placement = 'bottom'" />
      </template>
    </TxChatComposer>
  </template>
---
:::

### 搭配模式芯片
把 [ModeChip 模式芯片](/docs/dev/components/mode-chip) 放进 `toolbar-left` 插槽。
:::TuffDemoWrapper{demo="ChatComposerModeChipDemo" code-lang="vue"}
---
code: |
  <template>
    <TxChatComposer v-model="text">
      <template #toolbar-left>
        <TxModeChip
          :icon="unrestricted ? 'i-carbon-unlocked' : 'i-carbon-touch-1'"
          :label="unrestricted ? '无限制访问' : '请求批准'"
          :tone="unrestricted ? 'danger' : 'muted'"
          @click="unrestricted = !unrestricted"
        />
      </template>
    </TxChatComposer>
  </template>
---
:::

### 最佳实践

- `send` 只表示发送意图：请求成功后再清空 `modelValue`，失败时保留文本供重试。
- 请求进行中设置 `submitting`，防止重复发送。
- `attachments` 只作状态 chip；上传进度、删除与重试放在宿主或 `attachments` 插槽中。
- 托盘内容保持一行且上下等高，换位时外框不变；托盘动作用 muted 色调的 `TxModeChip`。
- 托盘位置跟随语境切换（「连接应用」变为「选择项目」），不要循环换位作装饰。

## API 参考

### 属性

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `modelValue` | `string` | `''` | 输入框文本，配合 `v-model` 使用。 |
| `placeholder` | `string` | `'Message…'` | 占位文本。 |
| `ariaLabel` | `string` | - | 输入框的可访问名称；缺省时用 `placeholder`。 |
| `disabled` | `boolean` | `false` | 禁用输入、发送与附件操作。 |
| `submitting` | `boolean` | `false` | 请求进行中，阻止发送。 |
| `allowAttachmentWhileSubmitting` | `boolean` | `false` | `submitting` 期间仍允许附件操作。 |
| `minRows` | `number` | `3` | 静止高度（行），随内容增高。 |
| `maxRows` | `number` | `6` | 增高上限（行），超出后滚动；不低于 `minRows`。 |
| `sendOnEnter` | `boolean` | `true` | 启用键盘发送。 |
| `sendOnMetaEnter` | `boolean` | `true` | 键盘发送需按 Meta/Ctrl+Enter。 |
| `allowEmptySend` | `boolean` | `false` | 文本为空但有附件时允许发送。 |
| `sendButtonText` | `string` | `'Send'` | 发送图标按钮的可访问名称。 |
| `showAttachmentButton` | `boolean` | `false` | 显示默认的 `+` 附件按钮。 |
| `attachmentButtonText` | `string` | `'Attach'` | `+` 附件按钮的可访问名称。 |
| `attachments` | `ChatComposerAttachment[]` | `[]` | 输入框上方的附件 chip。 |
| `trayPlacement` | `'top' \| 'bottom'` | `'bottom'` | 托盘在卡片上方或下方；改变时播放换位动效。 |
| `trayLabel` | `string` | - | 设置后托盘渲染为带此名称的 `role="group"`。 |

### 事件

| 事件 | 参数 | 说明 |
|------|------|------|
| `update:modelValue` | `string` | 输入时触发。 |
| `send` | `{ text: string }` | 允许发送时触发，文本已 trim。 |
| `attachmentClick` | - | 允许附件操作时，点击附件按钮触发。 |
| `paste` | `ClipboardEvent` | 透传输入框的 paste。 |
| `attachmentAdd` | `File[]` | 粘贴或拖入文件时触发；上传由宿主负责。 |
| `focus` | `FocusEvent` | 透传输入框的 focus。 |
| `blur` | `FocusEvent` | 透传输入框的 blur。 |

### 插槽

| 插槽 | 作用域 | 说明 |
|------|--------|------|
| `tray` | - | 卡片外的托盘内容。 |
| `attachments` | `{ attachments }` | 替换默认附件 chip。 |
| `toolbar` | `{ send, disabled, attachmentClick }` | 替换默认操作行。 |
| `toolbar-left` | `{ disabled }` | 追加到默认操作行左侧。 |
| `actions` | `{ send, disabled }` | 追加到发送按钮之前。 |
| `footer` | - | 卡片内、操作行下方的内容。 |

### 类型

:::TuffCodeBlock{lang="ts"}
---
code: |
  interface ChatComposerAttachment {
    id: string // chip 的 key
    label: string // chip 文本，超宽时省略
    kind?: string // 文本后的类型标记，以大写显示
    pending?: boolean // 处理中，chip 改为琥珀色
  }
---
:::

### CSS 变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `--tx-chat-composer-radius` | `18px` | 外壳与卡片共用的圆角。 |

## 概述

- 文本 trim 后非空才能发送；`allowEmptySend` 为真且有附件时可只发附件。`disabled`、`submitting` 都阻止发送。
- 附件操作（按钮、粘贴、拖入文件）在 `disabled` 时被阻止；`submitting` 时也被阻止，除非设置 `allowAttachmentWhileSubmitting`。
- `sendOnMetaEnter` 为真时 Meta/Ctrl+Enter 发送；为假时 Enter 发送，Shift+Enter 换行。
- 输入框从 `minRows` 行随内容增高，到 `maxRows` 行后滚动（`field-sizing: content`）；不支持的浏览器固定为 `minRows` 行。
- 根元素是外壳，透传的 class 与属性落在外壳上，输入卡片是 `.tx-chat-composer__card`；增删托盘不重建 textarea，焦点与输入法状态保留。
- 托盘只在提供 `tray` 插槽时渲染。焦点在离开的托盘中时，换位后移到输入框；减少动态效果或无 Web Animations API 时直接落到终态。

## 技术实现

- 换位时旧托盘由 CSS 过渡淡出；卡片滑动与外壳高度是 Web Animations 驱动的 FLIP，延迟 70ms、时长 450ms、曲线 `cubic-bezier(0.65, 0.16, 0.1, 0.88)`。
- 动效参考：[@flohoeller 的 Chatbox 组件视频](https://x.com/flohoeller/status/2102660458658582913)。
- 源码：`packages/tuffex/packages/components/src/chat/src/TxChatComposer.vue`。

<TuffDocSourceLink />
