---
title: "令牌输入"
description: "TokenInput 在可编辑文本里混排令牌：提及、文件、技能引用整体移动、删除、复制，并可序列化为文本。"
---

> Documentation Index
> Fetch the complete documentation index at: https://ui-sdk.ndjp.net/llms.txt
> Use this file to discover all available pages before exploring further.

# 令牌输入

`TokenInput`（令牌输入）是一个文本框，但文本里可以嵌入令牌，例如消息里提到的人、提示词里附带的文件。每个令牌作为一个整体移动、删除与复制。只需要纯文本时用[多行文本](https://ui-sdk.ndjp.net/docs/components/textarea)；只需要在键入触发字符时给建议、不需要令牌时用[触发菜单](https://ui-sdk.ndjp.net/docs/components/trigger-menu)配普通输入。

## 组合

```text
TokenInput
├── TokenInputEditor
└── TriggerMenu
```

`TokenInput` 持有值，`TokenInputEditor` 是人真正编辑的文本区。放在它里面的[触发菜单](https://ui-sdk.ndjp.net/docs/components/trigger-menu)在键入 `@`、`/` 等触发字符时给出候选。编辑器自己管理内容、选区与撤销，所以让它渲染内容，用 `renderToken` 定制令牌外观：通常是 `TokenInputToken` 包一个 `TokenInputTokenRemove`。

## 用法

```tsx
import { TokenInput, TokenInputEditor, type TokenInputValue } from "@ui-sdk/react/token-input";

const draft: TokenInputValue = ["解释一下 ", { type: "file", value: "src/routes.ts", label: "routes.ts" }];

<TokenInput defaultValue={draft}>
  <TokenInputEditor aria-label="提示词" />
</TokenInput>;
```

值是文本与令牌组成的数组。每个令牌有 `type`（种类）、`value`（应用用的 id）、`label`（读者看到的文字），可选 `id`、`status`（`"pending"` / `"unresolved"`）与 `data`（渲染用的附加数据，不参与序列化）。

## 配合触发菜单

由代码提供令牌时可以只用 `TokenInput`；需要候选列表就在它里面放一个触发菜单：

```tsx
import { TokenInput, TokenInputEditor, TokenInputToken } from "@ui-sdk/react/token-input";
import { TriggerMenu, TriggerMenuContent, TriggerMenuItem, TriggerMenuList, type TriggerMenuDataItem } from "@ui-sdk/react/trigger-menu";

const people = [{ value: "hollis", label: "何雨" }, { value: "ines", label: "伊娜" }];

<TokenInput renderToken={(token) => <TokenInputToken>{token.label}</TokenInputToken>}>
  <TokenInputEditor placeholder="键入 @ 提及某人…" />
  <TriggerMenu triggers={[{ char: "@", type: "mention", items: people }]}>
    <TriggerMenuContent>
      <TriggerMenuList>
        {(state) =>
          (state.items as TriggerMenuDataItem[]).map((item) => (
            <TriggerMenuItem key={item.value} value={item.value} label={item.label}>{item.label}</TriggerMenuItem>
          ))
        }
      </TriggerMenuList>
    </TriggerMenuContent>
  </TriggerMenu>
</TokenInput>;
```

键入 `@` 后用方向键与 `Enter` 选中一个人，它就变成一枚令牌。菜单在令牌输入内部时会自动找到编辑器；放在外部时把控制器的 `editor` 传给它。

## 令牌外观与删除方式

`renderToken` 收到令牌，返回它的内容，通常是 `TokenInputToken` 加一个 `TokenInputTokenRemove`。可以按 `type` 选图标，或读 `data` 显示运行状态。不传 `renderToken` 时每个令牌显示自己的 `label`。

```tsx
<TokenInput tokenDeletion="select" renderToken={(token) => (
  <TokenInputToken>
    {token.label}
    <TokenInputTokenRemove>×</TokenInputTokenRemove>
  </TokenInputToken>
)}>
  <TokenInputEditor aria-label="消息" />
</TokenInput>
```

`tokenDeletion` 决定光标贴着令牌时 `Backspace` 的行为：`"whole"` 一次按键删掉整个令牌；`"select"` 先选中它，再按一次才删除；`"revert"` 把它还原成可编辑文本，触发菜单会再次在它上面打开。

`TokenInputTokenRemove` 按下即删除该令牌并把焦点留在编辑器；它渲染 `span` 且对辅助技术隐藏，因为键盘用户用 `Backspace` 删除。

## 文本互转与复制

codec 负责令牌与文本之间的转换，用于剪贴板、表单与草稿。默认格式是 `[@label](type:value)`，复制到其他应用再粘回来能还原令牌。

```tsx
import { fromText, toText, type TokenCodec } from "@ui-sdk/react/token-input";

const chat: TokenCodec = {
  format: (token) => `<@${token.value}>`,
  parse: (text) =>
    text.split(/(<@[\w.-]+>)/).map((part) => {
      const id = /^<@([\w.-]+)>$/.exec(part)?.[1];
      return id ? { type: "mention", value: id, label: `@${id}` } : part;
    }),
};

<TokenInput codec={chat} copy="label" defaultValue={fromText("通知 <@hollis> 和 <@ines> 上线", chat)}>
  <TokenInputEditor aria-label="消息" />
</TokenInput>;
```

`toText`、`fromText`、`toLabel` 可自行转换值。`copy="codec"` 保留可跨应用还原的格式，`copy="label"` 复制出人所读到的文字，粘回令牌输入时仍能还原令牌。codec 还可以提供 `escape`，决定纯文本如何写出才能被 `parse` 读回为文本。默认解析器只接受字母、数字、`_`、`-` 组成的令牌类型，其他类型需要自定义 codec。

## 受控、控制器与提交

`value` / `defaultValue` 加 `onValueChange` 即可受控。回调的 `details.reason` 区分 `input-change`、`paste`、`drop`、`cut`、`undo` / `redo`、`remove-press`、`item-press`、`reset`、`restore`、`imperative`，`details.removed` 列出被删掉的令牌，`details.cancel()` 可拦截。参见[状态与事件](https://ui-sdk.ndjp.net/docs/concepts/state-and-events)。

`createTokenInputController()` 创建的控制器能在外部读写：`getValue / setValue / getText / getLabel / insert / updateToken / removeToken / undo / redo / canUndo / canRedo / clear / focus`。`insert` 支持 `{ at: "caret" | "end" }` 与 `resolve`：传入 promise 时令牌先处于 pending，resolve 后更新、reject 则移除；`updateToken(id, patch)` 改状态或 `data`，不产生撤销步骤、不移动光标。

`submitShortcut="enter"` 时 `Enter` 提交、`Shift` + `Enter` 换行；`"mod+enter"` 时相反，更适合中日文输入。候选菜单打开或输入法正在组字时，按键优先交给它们。传 `onSubmit` 可自行处理提交，此时表单不提交；不传则提交所在表单。给根传 `name` 才会提交 codec 序列化后的文本，`required` 阻止空内容提交，`form` 关联其他表单 id，重置回到 `defaultValue`。参见[控制器](https://ui-sdk.ndjp.net/docs/concepts/controllers)与[表单](https://ui-sdk.ndjp.net/docs/concepts/forms)。

## 样式与状态

- 编辑器 `token-input-editor`：`data-empty`（内容为空）、`data-disabled`、`data-readonly`。占位符通过 `aria-placeholder` 提供，用 `::before` 画出来。
- 令牌 `token-input-token`：`data-token-type`（如 `file`、`mention`）、`data-pending`、`data-unresolved`、`data-selected`（在选区内或被选中待删）、`data-disabled`、`data-readonly`。
- 删除按钮 `token-input-token-remove`：`data-disabled`、`data-readonly`。
- 不写 `renderToken` 时，承载令牌的 `token-input-host` 带有同样的令牌状态属性，因此不用自定义渲染也能画出外观。

```css
[data-slot="token-input-editor"][data-empty]::before {
  content: attr(aria-placeholder);
  color: var(--color-muted-foreground);
}
[data-slot="token-input-token"] {
  border-radius: 6px;
  background: var(--color-brand-wash);
  padding-inline: 0.375rem;
}
[data-slot="token-input-token"][data-selected] {
  outline: 2px solid var(--color-brand);
}
[data-slot="token-input-token"][data-pending] {
  opacity: 0.6;
}
[data-slot="token-input-token"][data-unresolved] {
  background: var(--color-warning-muted);
}
```

详见[样式与动画](https://ui-sdk.ndjp.net/docs/concepts/styling-and-animation)。

## API

`TokenInput` 常用属性：

| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `value` / `defaultValue` | `TokenInputValue` | 文本与令牌的数组，受控与非受控。 |
| `onValueChange` | `(value, details) => void` | 值变化时触发，含 `removed`，可取消。 |
| `renderToken` | `(token) => ReactNode` | 渲染每个令牌的内容。 |
| `tokenDeletion` | `"whole" \| "select" \| "revert"` | 退格键如何处理令牌。 |
| `copy` | `"codec" \| "label"` | 复制写出的纯文本格式。 |
| `codec` | `TokenCodec` | 令牌与文本的互转规则。 |
| `submitShortcut` | `"enter" \| "mod+enter"` | 提交快捷键。 |
| `getTokenDescription` | `(token) => string` | 令牌除标签外要朗读的信息，如种类与状态。 |
| `history` | `UndoStackOptions` | 撤销步数与「停顿多久算新一步」。 |
| `name` / `form` | `string` | 提交字段名与关联表单 id。 |
| `disabled` / `readOnly` / `required` | `boolean` | 禁用、只读、必填。 |
| `controller` | `object` | `createTokenInputController()` 创建的控制器。 |

`TokenInputEditor` 另有 `placeholder`（`string`）。

## 键盘与无障碍

编辑器是多行文本框：`textbox` 角色加 `aria-multiline`；内部有触发菜单时再加上 `aria-autocomplete` 与 `aria-haspopup`。用[字段](https://ui-sdk.ndjp.net/docs/components/field)的可见标签或 `aria-label` 命名。每个令牌读作它的标签加种类（例如「routes.ts 文件」），插入与删除都会被播报，浏览器自带的拼写检查、自动纠错与输入法照常可用。

| 按键 | 行为 |
| --- | --- |
| `Backspace` / `Delete` | 按 `tokenDeletion` 删除光标前或后的令牌。 |
| `ArrowLeft` / `ArrowRight` | 一次按键跨过整个令牌，光标不会停在令牌内部。 |
| `Enter` | 提交；触发菜单打开时选中高亮候选。 |
| `Shift` + `Enter` | 换行。 |
| Command / Control + `Enter` | `submitShortcut="mod+enter"` 时提交，此时 `Enter` 换行。 |
| Command / Control + `Z` | 撤销上一步，连续键入合并为一步，粘贴与令牌改动各算一步。 |
| Shift + Command + `Z`、Control + `Y` | 重做。 |

## 相关组件

- [触发菜单](https://ui-sdk.ndjp.net/docs/components/trigger-menu)：键入触发字符时的候选列表，也可配普通输入。
- [多行文本](https://ui-sdk.ndjp.net/docs/components/textarea)：纯多行文本，共享提交快捷键约定。
- [组合框](https://ui-sdk.ndjp.net/docs/components/combobox)：只需要选一个值、不需要混排文本时用它。

Source: https://ui-sdk.ndjp.net/docs/components/token-input/index.mdx
