---
author: claude
created_at: 2026-06-27
purpose: 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮，并修复 story-card 展开失效 bug
---

# AI 消息卡片功能按钮统一设计

## 背景

在小程序「实现心愿」页面（`mini-program/src/pages/main/ScriptView.vue`）中：

1. **第一个 AI 回复卡片**（`story-card`）拥有完整的功能按钮：展开/收起、复制、换个方向、不像我、继续生成、播放。
2. **后续 AI 消息**（`result-chat-list`）只有基础的展开/收起，缺少复制、换个方向、不像我、继续生成、播放。
3. **story-card 展开失效**：当用户触发过重新生成（`resultHasScriptReplies` 为 `true`）后，`<template v-if="!resultHasScriptReplies">` 会隐藏整个故事正文和底部展开按钮，导致点击展开无效果。

## 目标

- 每条 AI 回复消息卡片都拥有完整的功能按钮（展开/收起、复制、换个方向、不像我、继续生成、播放）
- 修复 story-card 展开失效的 bug
- 所有按钮功能正常可用

## 方案选择

采用 **方案 A：在现有单文件组件内修复 + 扩展**

- 不提取独立组件，保持现有文件结构
- 改动最小、风险低、不引入新文件
- 后续如有更复杂的差异化需求再升级

## 详细设计

### 1. 修复 story-card 展开失效

**根因**：`ScriptView.vue` 第 192 行的条件包裹

```vue
<template v-if="!resultHasScriptReplies">
  <scroll-view v-if="storyCollapsed" ...>
    <text class="story-body">{{ displayedResultContent }}</text>
  </scroll-view>
  <text v-else class="story-body">{{ displayedResultContent }}</text>
  <view class="collapse-row" @click="toggleStoryCollapse">...</view>
</template>
```

当 `resultHasScriptReplies` 为 `true`（有 `kind: 'script'` 的消息）时，整个故事正文和底部展开按钮被隐藏。

**修复**：去掉 `<template v-if="!resultHasScriptReplies">` 条件包裹，让故事正文始终由 `storyCollapsed` 控制展开/收起。

**副作用**：story-card 始终显示完整故事正文，与 result-chat-list 中的 script 消息内容重复。这是预期行为——两个卡片独立操作各自内容。

### 2. 给每条 assistant 消息加功能按钮组

**布局**：在每条 assistant 消息的内容 + 展开/收起按钮之后，追加一行网格按钮组，2 列布局（与 story-card 的 `.result-actions` 一致）。

**按钮清单**（按顺序）：

| 按钮 | 作用 | 实现方式 |
|---|---|---|
| 复制 | 复制该消息正文到剪贴板 | `uni.setClipboardData` |
| 换个方向 | 进入「换方向」修订确认 | 复用现有 `changeDirection()` |
| 不像我 | 进入「不像我」修订确认 | 复用现有 `notLikeMe()` |
| 继续生成 | 收起故事 + 聚焦输入框 | 复用现有 `continueInChat()` |
| 播放 | 播放 TTS 音频 | 复用 `ttsPlayer.playSource`，传 `currentResult.id` |

**按钮条件**：
- 展开/收起、复制：所有 assistant 消息都有
- 换个方向、不像我、继续生成、播放：所有 assistant 消息都有（本质上是全局操作，作用在 `currentResult` 上）
- 正在 `pending` 的 assistant 消息不显示按钮组（保持现有逻辑，`isAssistantMessage` 已排除 `pending`）

### 3. 新增/修改的事件处理器

1. **`copyMessageContent(message)`**（新增）：
   - 复制 `message.content` 到剪贴板
   - 空内容时 `showToast` 提示「暂无可复制内容」
   - 埋点 `script_message_copy_click`

2. **`playMessageTts(message)`**（新增）：
   - 调用 `ttsPlayer.playSource(currentResult.value?.id || '')`
   - 所有消息都播放同一个故事的 TTS（TTS 基于 `currentResult` 生成）
   - 埋点 `script_message_tts_click`

3. **`toggleMessageCollapse(message)`**（修改）：
   - 补充埋点 `script_message_collapse_toggle`（与 `toggleStoryCollapse` 对齐）

### 4. 样式

为消息卡片内的按钮组新建 `.message-actions` 样式，复用 `.result-actions` 和 `.action-btn` 的视觉风格：

```css
.message-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14rpx;
  margin-top: 16rpx;
}
```

按钮直接复用 `.action-btn` 类名，确保视觉与主卡片一致。展开/收起按钮（`.message-toggle`）保留现有金色调样式，与功能按钮组做视觉区分。

### 5. 边界情况

| 场景 | 行为 |
|---|---|
| 空内容的 assistant 消息 | 复制按钮 `showToast` 提示「暂无可复制内容」；其他按钮正常可用 |
| pending 中的消息 | 不显示按钮组（现有逻辑） |
| user 消息 | 不显示按钮组 |
| 无 `currentResult` 时的播放 | 按钮仍可点击，`playSource` 会走内部兜底（传空 id） |

## 修改范围

仅修改一个文件：`mini-program/src/pages/main/ScriptView.vue`

### 模板修改
1. 去掉第 192 行 `<template v-if="!resultHasScriptReplies">` 条件包裹
2. 在 `result-chat-list` 的 assistant 消息中添加功能按钮组

### 脚本修改
1. 新增 `copyMessageContent(message)` 方法
2. 新增 `playMessageTts(message)` 方法
3. 修改 `toggleMessageCollapse(message)` 补充埋点

### 样式修改
1. 新增 `.message-actions` 样式
