---
author: claude
created_at: 2026-06-27
purpose: 提取 MessageCard 组件，统一所有 AI 消息卡片样式，按内容长度阈值（100 字）决定是否显示完整功能按钮
---

# MessageCard 组件统一设计

## 背景

在上一轮实现中，result-chat-list 中的 assistant 消息使用了 `.chat-bubble.system` 气泡样式，与 `story-card` 的卡片样式不一致。用户要求：

1. 所有 AI 回复消息卡片的样式、功能、布局、按钮等，都与第一次生成剧本的 `story-card` 完全一致
2. 内容 < 100 字的 assistant 消息：只显示消息内容，无功能按钮
3. 内容 ≥ 100 字的 assistant 消息：必须与 `story-card` 完全一致

## 目标

- 提取独立的 `MessageCard.vue` 组件
- story-card 和 result-chat-list 都复用该组件
- 按 100 字阈值自动切换显示模式

## 方案选择

采用 **方案 B：提取独立 MessageCard.vue 组件**

- 代码复用，维护性好
- 改动范围：新建 1 个组件 + 修改 ScriptView.vue

## 详细设计

### 1. 组件接口

**文件**：`mini-program/src/components/MessageCard.vue`

**Props：**

| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
| `content` | String | 是 | 消息/故事正文内容 |
| `title` | String | 否 | 卡片标题，默认 `'我的人生剧本'` |
| `tags` | Array | 否 | 标签列表 |
| `collapsed` | Boolean | 是 | 当前折叠状态 |
| `contentLength` | Number | 是 | 内容字数（用于阈值判断） |
| `isShortMessage` | Boolean | 是 | `contentLength < 100` 时为 true |
| `ttsIcon` | String | 否 | TTS 按钮图标（`▶` 或 `Ⅱ`） |
| `ttsText` | String | 否 | TTS 按钮文字（`播放` 或 `暂停`） |

**Events：**

| 事件 | 说明 |
|---|---|
| `toggle-collapse` | 展开/收起 |
| `copy` | 复制内容 |
| `change-direction` | 换个方向 |
| `not-like-me` | 不像我 |
| `continue` | 继续生成 |
| `play-tts` | 播放 TTS |

### 2. 组件内部结构

**短消息（`isShortMessage = true`）**：

```vue
<view class="chat-bubble system">
  <text>{{ content }}</text>
</view>
```

只显示纯文本气泡，无标题、无标签、无按钮。

**长消息（`isShortMessage = false`）**：

完整复用现有 `story-card` 的 HTML 结构：

- 标题（`.story-title`）+ 标签（`.tag-row`）
- 头部操作区：展开/收起按钮 + 复制按钮
- 正文：展开时用 scroll-view，收起时用 text
- 底部：展开全文/收起全文按钮
- 功能按钮组（2 列网格）：换个方向、不像我、继续生成、播放

### 3. ScriptView.vue 中的使用

**替换现有 story-card**：

```vue
<MessageCard
  :content="displayedResultContent"
  :title="currentResult?.title"
  :tags="resultTags"
  :collapsed="storyCollapsed"
  :content-length="displayedResultContent.length"
  :is-short-message="false"
  :tts-icon="ttsActionIcon"
  :tts-text="ttsActionText"
  @toggle-collapse="toggleStoryCollapse"
  @copy="copyResultContent"
  @change-direction="changeDirection"
  @not-like-me="notLikeMe"
  @continue="continueInChat"
  @play-tts="trackTtsClick"
/>
```

**替换 result-chat-list 中的 assistant 消息**：

```vue
<view v-for="message in resultMessages" :key="message.id">
  <MessageCard
    v-if="isAssistantMessage(message)"
    :content="message.content"
    :collapsed="isMessageCollapsed(message)"
    :content-length="message.content.length"
    :is-short-message="message.content.length < 100"
    :tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
    :tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
    @toggle-collapse="toggleMessageCollapse(message)"
    @copy="copyMessageContent(message)"
    @change-direction="changeDirection"
    @not-like-me="notLikeMe"
    @continue="continueInChat"
    @play-tts="playMessageTts(message)"
  />
  <view v-else class="chat-bubble user">
    <text>{{ message.content }}</text>
    <text class="bubble-time">{{ message.time }}</text>
  </view>
</view>
```

### 4. 样式迁移

将以下 CSS 类从 `ScriptView.vue` 迁移到 `MessageCard.vue` 的 `<style scoped>` 中：

- `.story-card`、`.story-card.collapsed`
- `.story-head`、`.story-head-actions`、`.story-title-wrap`
- `.story-title`、`.tag-row`、`.tag`
- `.collapse-icon`、`.copy-card-btn`、`.collapse-row`
- `.collapse-chevron`、`.story-body`、`.story-body-scroll`
- `.result-actions`、`.action-btn`

`ScriptView.vue` 中删除这些已迁移的样式。

### 5. 边界情况

| 场景 | 行为 |
|---|---|
| content 刚好 100 字 | `isShortMessage = false`，显示完整卡片 |
| content 为空 | 显示空卡片（与 story-card 现有行为一致） |
| pending 中的消息 | 不使用 MessageCard，保持 thinking-dots 渲染 |
| user 消息 | 不使用 MessageCard，保持 `.chat-bubble.user` 气泡 |

## 修改范围

| 文件 | 操作 |
|---|---|
| `mini-program/src/components/MessageCard.vue` | 新建 |
| `mini-program/src/pages/main/ScriptView.vue` | 修改（模板 + 脚本 + 样式） |
