---
title: "ChoiceCard 选项卡片"
description: "以图标、标题和说明行作答、可分步翻页的问题卡片"
category: AiChat
status: beta
since: 0.6.0
tags: [ai, choice, options, guide, steps]
syncStatus: reviewed
verified: true
---

## 安装

:::TuffCodeBlock{lang="bash"}
---
code: |
  pnpm add @talex-touch/tuffex
---
:::

:::TuffCodeBlock{lang="typescript"}
---
code: |
  import { TxChoiceCard } from '@talex-touch/tuffex/choice-card'
  import '@talex-touch/tuffex/choice-card/style.css'
  // 卡片内部渲染 TxIcon 与 TxSkeleton，它们的样式表需单独引入
  import '@talex-touch/tuffex/icon/style.css'
  import '@talex-touch/tuffex/skeleton/style.css'
  import '@talex-touch/tuffex/base.css' // 设计令牌与重置样式，全应用引入一次
---
:::

## 用法

### 单个问题
`selected` 标出已选项；设置 `disabled` 的选项不可选。
:::TuffDemoWrapper{demo="ChoiceCardSingleDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { ChoiceSelectPayload, ChoiceStep } from '@talex-touch/tuffex/choice-card'
  import { ref } from 'vue'

  const steps: ChoiceStep[] = [{
    id: 'start',
    title: '我们先从哪件事开始？',
    options: [
      { id: 'report', label: '写一份周报', description: '整理本周完成的事和下周计划', icon: 'i-carbon-report' },
      { id: 'plan', label: '规划明天', description: '按顺序列出三件最重要的事', icon: 'i-carbon-calendar' },
      { id: 'calendar', label: '同步日历', description: '先连接一个日历账户', icon: 'i-carbon-calendar-add-alt', disabled: true },
    ],
  }]

  const selected = ref<string>()

  function onSelect({ option }: ChoiceSelectPayload) {
    selected.value = option.id
  }
  </script>

  <template>
    <TxChoiceCard :steps="steps" :selected="selected" @select="onSelect" />
  </template>
---
:::

### 分步引导
宿主在 `select` 中更新 `v-model:step` 翻页；`columns="2"` 让选项两个一行。
:::TuffDemoWrapper{demo="ChoiceCardStepsDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { ChoiceSelectPayload, ChoiceStep } from '@talex-touch/tuffex/choice-card'
  import { computed, ref } from 'vue'

  const step = ref(0)
  const kind = ref<string>()
  const task = ref<string>()

  const kinds: ChoiceStep = {
    id: 'kind',
    title: '我们先从哪类事开始？',
    options: [
      { id: 'write', label: '写作', description: '报告、邮件、会议纪要', icon: 'i-carbon-pen' },
      { id: 'plan', label: '规划', description: '日程、优先级、复盘', icon: 'i-carbon-roadmap' },
      // …学习、整理
    ],
  }

  // 第二页跟着第一步的答案走：换一类就换一个 id
  const steps = computed<ChoiceStep[]>(() => kind.value
    ? [kinds, { id: `tasks-${kind.value}`, title: TITLES[kind.value], options: TASKS[kind.value] }]
    : [kinds])

  // 每一页标出自己那一步的答案
  const selected = computed(() => (step.value === 0 ? kind.value : task.value))

  function onSelect({ stepIndex, option }: ChoiceSelectPayload) {
    if (stepIndex === 0) {
      if (kind.value !== option.id)
        task.value = undefined
      kind.value = option.id
      step.value = 1 // 卡片不会自己翻页
    }
    else {
      task.value = option.id
    }
  }
  </script>

  <template>
    <TxChoiceCard
      v-model:step="step"
      :steps="steps"
      :selected="selected"
      :columns="2"
      @select="onSelect"
    />
  </template>
---
:::

### 加载中
`loading` 在选项行自己的盒子里绘制骨架行，选项到达时页面不跳动。
:::TuffDemoWrapper{demo="ChoiceCardLoadingDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { ChoiceOption, ChoiceStep } from '@talex-touch/tuffex/choice-card'
  import { useDeferredLoading } from '@talex-touch/tuffex/skeleton'
  import { computed, ref } from 'vue'

  const pending = ref(true)
  const picks = ref<ChoiceOption[]>([])
  // 前 150ms 不显示，显示后至少停留 400ms：快速返回的请求不会闪出骨架屏
  const skeleton = useDeferredLoading(pending)

  const steps = computed<ChoiceStep[]>(() => [{ id: 'for-you', title: '为你准备', options: picks.value }])

  fetchPicks().then((options) => {
    picks.value = options
    pending.value = false
  })
  </script>

  <template>
    <TxChoiceCard :steps="steps" :loading="skeleton" :loading-rows="3" />
  </template>
---
:::

### 最佳实践

- 每步只问一个问题，给三到六个答案。
- 在 `select` 中更新 `v-model:step` 翻页；`selected` 传当前页的答案，回到上一步时仍能看到。
- `loading` 经 `useDeferredLoading` 绑定，`loadingRows` 设为即将到来的答案数。
- 需要前置条件的选项保留为禁用，并在说明里写明条件。
- 按页面语言传入 `prevLabel`、`nextLabel`、`stepLabel`；卡片只带英文默认值。

## API 参考

### 属性

| 属性名 | 类型 | 默认值 | 说明 |
|------|------|---------|------|
| `steps` | `ChoiceStep[]` | - | 各页，必填；为空且不在加载时不渲染。 |
| `step` | `number` | `undefined` | 当前页（从 0 起），配合 `v-model:step`；越界时取最近的有效页。 |
| `selected` | `string` | `undefined` | 当前页要标出的答案 id。 |
| `loading` | `boolean` | `false` | 把选项换成骨架行，卡片加 `aria-busy`；标题与分页保留。 |
| `loadingRows` | `number` | `3` | `loading` 时绘制的骨架行数。 |
| `columns` | `1 \| 2` | `1` | `2` 时选项两个一行；卡片窄于 480px 时退回一列。 |
| `appear` | `boolean` | `true` | 首次渲染与 `loading` 结束时选项逐行浮现。 |
| `prevLabel` | `string` | `'Previous'` | 后退箭头的可访问名称。 |
| `nextLabel` | `string` | `'Next'` | 前进箭头的可访问名称。 |
| `stepLabel` | `(current: number, total: number) => string` | `` `${current} / ${total}` `` | 分页计数文字，两个数字都从 1 开始。 |

### 事件

| 事件名 | 参数 | 说明 |
|------|------|------|
| `select` | `(payload: ChoiceSelectPayload)` | 点击或用 Enter / 空格激活可用选项时触发；卡片不翻页。 |
| `update:step` | `(index: number)` | 箭头翻到第 `index` 页（从 0 起）时触发。 |

### 插槽

| 插槽 | 参数 | 说明 |
|------|------|------|
| `header` | `{ step: ChoiceStep, stepIndex: number, total: number }` | 替换默认 `<h3>` 标题；其内容即卡片的名称，需保留问题。 |

### 类型

`ChoiceStep`，`steps` 中的一项：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `string` | 页的标识；当前页换成新 id 时重放翻页入场。必填。 |
| `title` | `string` | 问题，即卡片标题与答案列表的名称。必填。 |
| `options` | `ChoiceOption[]` | 各答案。必填。 |

`ChoiceOption`，一个答案：

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `id` | `string` | - | 页内唯一标识，也是 `selected` 比对的值。必填。 |
| `label` | `string` | - | 选项标题与可访问名称。必填。 |
| `description` | `string` | - | 标题下的一行说明，作为选项描述朗读。 |
| `icon` | `TxIconSource \| string` | - | `TxIcon` 图标源，或图标类名如 `'i-carbon-edit'`。 |
| `disabled` | `boolean` | `false` | 暗淡显示；方向键跳过，点击不派发事件。 |

:::TuffCodeBlock{lang="ts"}
---
code: |
  import type {
    ChoiceCardColumns, // 1 | 2
    ChoiceCardEmits,
    ChoiceCardProps,
    ChoiceOption,
    ChoiceSelectPayload, // { step, stepIndex, option }
    ChoiceStep,
    ChoiceStepLabelFormatter, // (current, total) => string
    TxChoiceCardInstance,
  } from '@talex-touch/tuffex/choice-card'
---
:::

### CSS 变量

| 变量 | 说明 |
|------|------|
| `--tx-choice-card-pad` | 卡片边缘到选项的内距，默认 `8px`；卡片圆角随之变大。 |
| `--tx-choice-card-option-radius` | 选项圆角，默认 `10px`；卡片圆角为它加内距，保持同心。 |
| `--tx-choice-card-option-pad-x` | 选项与标题的水平内距，默认 `12px`。 |
| `--tx-choice-card-label-line` | 标题行高与图标框高度，默认 `20px`。 |
| `--tx-choice-card-desc-line` | 说明行高，默认 `18px`。 |

这些变量可设在卡片或任意祖先元素上；组件只在每行写入 `--tx-choice-card-index`（入场错落序号）。

## 概述

- 语义：以问题命名的 `<section>`，选项是 `<ul role="list">` 中的原生 `<button>`，名称取自标题、描述取自说明；已选项打勾并带 `aria-current="true"`，颜色不是唯一标记。
- `select` 从不翻页。不传 `step` 时卡片自行记页并派发 `update:step`；至少两页才渲染分页，计数是 `role="status"` 区域。
- 键盘：列表只占一个 Tab 落点（已选项，否则第一个可用项）；上下键循环移动，两列时左右键按阅读顺序移动，Home / End 到首尾，跳过禁用项。
- 翻页时焦点若在列表内，移到新页的落点；正在使用的箭头变为禁用时，焦点移到另一个箭头。
- 两列在卡片窄于 480px 时退回一列（容器查询），方向键按实际渲染的列数移动。
- 入场与翻页动画都是新节点上的 CSS 动画，选项第一帧即可点击；减少动态效果时不播放。

## 技术实现

- 骨架行沿用选项行自己的容器，内放 `TxSkeleton` 骨架条。
- 源码：`packages/tuffex/packages/components/src/choice-card/`。

<TuffDocSourceLink />

## 使用场景

- 助手的开场引导：「我们先从哪件事开始？」，每步一个问题。
- 卡片出现后才到达的「为你准备」推荐列表。
- 回看已回答的问题：`selected` 标出当时的选择。

## 相关组件

| 组件 | 适用于 |
|------|--------|
| [SuggestionChips](/docs/dev/components/suggestion-chips) | 单行追问建议 |
| [RecommendationCard](/docs/dev/components/recommendation-card) | 单个推荐答案与备选 |
| [ApprovalCard](/docs/dev/components/approval-card) | 多问题作答后统一发送 |
