# 聊天页面组件化重构方案

## 📋 需求分析

### 当前问题

1. **chat.vue 在 tabBar 中**
   - 位于 pages.json 的 tabBar 列表中，作为"专属" tab
   - 从 drama 页面跳转到 chat.vue 时，底部会显示 tabBar
   - "专属" tab 会处于选中状态，用户体验不佳

2. **代码重复维护问题**
   - 如果为角色聊天单独创建页面，会与 chat.vue 产生大量重复代码
   - 聊天逻辑复杂（800+ 行），不便于维护

### 解决方案

将聊天核心逻辑提取为 **ChatBox.vue 组件**，实现代码复用：

- **chat.vue**：保留在 tabBar 中，固定显示蔚AI，参数写死
- **role-chat.vue**：新建独立页面，不在 tabBar 中，从 API 获取角色参数
- **ChatBox.vue**：核心聊天组件，两个页面共用

---

## 🎯 方案设计

### 架构图

```
┌─────────────────────────────────────────────┐
│         ChatBox.vue (核心聊天组件)            │
│         src/components/ChatBox.vue          │
│  - 消息管理（发送、接收、显示）                │
│  - API 调用（chatAPI、voiceAPI）             │
│  - 语音交互（录音、播放）                     │
│  - 会话管理（sessionId、历史加载）            │
│  - UI 渲染（消息气泡、输入框、导航栏）          │
└─────────────────────────────────────────────┘
            ▲                       ▲
            │                       │
    ┌───────┴────────┐      ┌──────┴─────────┐
    │                │      │                 │
┌───────────────────┐│  ┌──────────────────────┐
│ chat.vue          ││  │ role-chat.vue        │
│ (tabBar页面)      ││  │ (独立页面)            │
│─────────────────  ││  │──────────────────────│
│ ✓ 在 tabBar 中    ││  │ ✓ 不在 tabBar 中     │
│ ✓ 固定蔚AI        ││  │ ✓ 动态角色参数       │
│ ✓ 参数写死        ││  │ ✓ 从 URL 解析参数    │
│ ✓ 无返回按钮      ││  │ ✓ 显示返回按钮       │
│ ✓ 有底部导航      ││  │ ✓ 全屏沉浸式         │
└───────────────────┘│  └──────────────────────┘
        ▲            │           ▲
        │            │           │
  点击"专属" tab     │    drama/index.vue
                     │      跳转到此
                     │
                     └─────────共用组件────────┘
```

---

## 📁 文件结构

```
src/
├── components/
│   └── ChatBox.vue                    # ✅ 新建：核心聊天组件
├── pages/
    ├── chat/
    │   └── chat.vue                   # ✅ 改造：tabBar 页面（"专属"）
    ├── role-chat/
    │   └── role-chat.vue              # ✅ 新建：角色聊天页面
    └── drama/
        └── index.vue                  # ✅ 修改：跳转路径
```

---

## 🔧 详细实施步骤

### 步骤 1：创建 ChatBox.vue 核心聊天组件

**文件路径：** `src/components/ChatBox.vue`

#### 任务清单

- [ ] 从 `src/pages/chat/chat.vue` 复制所有代码
- [ ] 移除 `initPage()` 中的 URL 参数解析逻辑（第 252-348 行）
- [ ] 定义 Props 接收配置参数
- [ ] 添加 `watch` 监听 `characterConfig` 变化
- [ ] 保留所有其他逻辑不变

#### Props 定义

```javascript
const props = defineProps({
  // 角色配置
  characterConfig: {
    type: Object,
    required: true,
    default: () => ({
      id: 'wei-ai',
      roleId: null,
      name: '蔚AI',
      avatar: '/static/avatar/icon_hushi.jpg',
      greeting: '你好！我是蔚AI',
      roleDesc: ''
    })
  },

  // AI 模型配置
  aiConfig: {
    type: Object,
    default: () => ({
      modelId: 10,
      templateId: 6,
      ttsId: null,
      sttId: null,
      temperature: 0.7,
      topP: 0.9
    })
  },

  // UI 配置
  uiConfig: {
    type: Object,
    default: () => ({
      showBackButton: true  // 是否显示返回按钮
    })
  }
});
```

#### 初始化逻辑改造

**原逻辑（chat.vue）：**
```javascript
onMounted(() => {
  initPage();      // 解析 URL 参数、初始化角色
  initRecorder();  // 初始化录音器
});
```

**新逻辑（ChatBox.vue）：**
```javascript
// 监听角色配置变化，触发初始化
watch(() => props.characterConfig, async (newConfig) => {
  if (!newConfig || !newConfig.id) return;

  // 设置当前角色
  currentCharacter.value = {
    id: newConfig.id,
    roleId: newConfig.roleId,
    name: newConfig.name,
    avatar: newConfig.avatar,
    greeting: newConfig.greeting,
    roleDesc: newConfig.roleDesc,
    // 合并 AI 配置
    modelId: props.aiConfig.modelId,
    templateId: props.aiConfig.templateId,
    ttsId: props.aiConfig.ttsId,
    sttId: props.aiConfig.sttId,
    temperature: props.aiConfig.temperature,
    topP: props.aiConfig.topP
  };

  // 加载 AI 配置
  await loadAIConfigs();

  // 创建/获取会话
  createNewConversation(newConfig.id || newConfig.roleId);

  // 加载历史消息
  await loadHistoryMessages();

  // 如果没有历史消息，显示欢迎消息
  if (messages.value.length === 0) {
    addMessage('ai', currentCharacter.value.greeting);
    if (!isLoggedIn.value) {
      addMessage('system', '您当前处于未登录状态，将使用本地模拟回复。登录后可享受完整的AI对话功能。');
    }
  }
}, { immediate: true, deep: true });

onMounted(() => {
  checkLoginStatus();  // 检查登录状态
  initRecorder();      // 初始化录音器
});
```

#### 关键改动点

1. **移除 getCurrentPages() 逻辑**
   ```javascript
   // ❌ 删除这部分代码（第 250-256 行）
   const pages = getCurrentPages();
   const currentPage = pages[pages.length - 1];
   const options = currentPage.options || {};
   ```

2. **移除 URL 参数判断逻辑**
   ```javascript
   // ❌ 删除这部分代码（第 254-343 行）
   if (!options.characterId && !options.roleId) { ... }
   if (options.characterId === 'wei-ai' || !options.characterId) { ... }
   else if (options.roleId) { ... }
   else { ... }
   ```

3. **保留所有业务逻辑**
   - ✅ 消息管理（messages、addMessage、addSegmentedAIResponse）
   - ✅ API 调用（chatAPI.syncChat、voiceAPI.voiceChat）
   - ✅ 录音功能（recorderManager、语音处理）
   - ✅ 会话管理（conversationId、createNewConversation、loadHistoryMessages）
   - ✅ UI 状态（isTyping、isLoading、scrollTop）

4. **showBackButton 改为从 props 获取**
   ```javascript
   // 原代码（第 217 行）
   const showBackButton = ref(true);

   // 改为
   const showBackButton = computed(() => props.uiConfig.showBackButton);
   ```

---

### 步骤 2：创建 role-chat.vue 角色聊天页面

**文件路径：** `src/pages/role-chat/role-chat.vue`

#### 完整代码

```vue
<template>
  <ChatBox
    :character-config="characterConfig"
    :ai-config="aiConfig"
    :ui-config="uiConfig"
  />
</template>

<script setup>
import { ref, onMounted } from 'vue';
import ChatBox from '@/components/ChatBox.vue';

// 角色配置
const characterConfig = ref({});

// AI 配置
const aiConfig = ref({
  modelId: 10,
  templateId: 6
});

// UI 配置
const uiConfig = ref({
  showBackButton: true  // 角色聊天页面显示返回按钮
});

// 初始化：解析 URL 参数
onMounted(() => {
  const pages = getCurrentPages();
  const currentPage = pages[pages.length - 1];
  const options = currentPage.options || {};

  // 组装角色配置
  characterConfig.value = {
    id: options.characterId || '',
    roleId: options.roleId || '',
    name: decodeURIComponent(options.roleName || 'AI角色'),
    avatar: decodeURIComponent(options.avatar || '/static/logo.png'),
    greeting: decodeURIComponent(options.greeting || '你好！很高兴认识你！'),
    roleDesc: decodeURIComponent(options.roleDesc || '')
  };

  // 组装 AI 配置
  aiConfig.value = {
    modelId: options.modelId ? parseInt(options.modelId) : 10,
    templateId: options.templateId ? parseInt(options.templateId) : (options.roleId ? parseInt(options.roleId) : 6),
    ttsId: options.ttsId || null,
    sttId: options.sttId || null,
    temperature: options.temperature ? parseFloat(options.temperature) : 0.7,
    topP: options.topP ? parseFloat(options.topP) : 0.9
  };
});
</script>

<style scoped>
/* 无需额外样式，所有样式在 ChatBox 组件中 */
</style>
```

#### 参数来源

这些参数来自 `drama/index.vue` 的 `handleUse()` 方法：

```javascript
// drama/index.vue 第 389-418 行
const params = {
  characterId: item.id,
  roleId: item.roleId,
  roleName: item.roleName || item.title,
  roleDesc: item.roleDesc,
  avatar: item.avatar || item.cover,
  greeting: item.greeting,
  modelId: item.modelId || '',
  templateId: item.templateId || '',
  ttsId: item.ttsId || '',
  sttId: item.sttId || '',
  temperature: item.temperature || '',
  topP: item.topP || ''
};
```

---

### 步骤 3：改造 chat.vue 为蔚AI专属页面

**文件路径：** `src/pages/chat/chat.vue`

#### 完整代码

```vue
<template>
  <ChatBox
    :character-config="characterConfig"
    :ai-config="aiConfig"
    :ui-config="uiConfig"
  />
</template>

<script setup>
import { ref } from 'vue';
import ChatBox from '@/components/ChatBox.vue';

// 固定配置：蔚AI
const characterConfig = ref({
  id: 'wei-ai',
  roleId: null,
  name: '蔚AI',
  avatar: '/static/avatar/icon_hushi.jpg',
  greeting: '你好！我是蔚AI，很高兴为您服务！',
  roleDesc: ''
});

// 固定配置：AI 模型
const aiConfig = ref({
  modelId: 10,
  templateId: 6,
  ttsId: null,
  sttId: null,
  temperature: 0.7,
  topP: 0.9
});

// UI 配置
const uiConfig = ref({
  showBackButton: false  // tabBar 页面不显示返回按钮
});
</script>

<style scoped>
/* 无需额外样式，所有样式在 ChatBox 组件中 */
</style>
```

#### 说明

- ✅ 所有参数写死，不做任何 URL 解析
- ✅ `showBackButton: false`（因为是 tabBar 页面）
- ✅ 固定显示蔚AI
- ✅ 代码极简（仅 40 行）

---

### 步骤 4：修改 drama/index.vue 跳转路径

**文件路径：** `src/pages/drama/index.vue`

#### 修改位置

第 417 行，`handleUse()` 方法中的跳转路径：

```javascript
// 原代码
uni.navigateTo({ url: `/pages/chat/chat?${queryString}` });

// 改为
uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
```

#### 完整的 handleUse 方法

```javascript
const handleUse = (item) => {
  if (!item || !item.id) {
    uni.showToast({ title: '角色信息无效', icon: 'none' });
    return;
  }
  uni.showLoading({ title: '正在设置角色...' });

  // 构建完整的角色参数，包括模型和模板信息
  const params = {
    characterId: item.id,
    roleId: item.roleId,
    roleName: item.roleName || item.title,
    roleDesc: item.roleDesc,
    avatar: item.avatar || item.cover,
    greeting: item.greeting,
    modelId: item.modelId || '',
    templateId: item.templateId || '',
    ttsId: item.ttsId || '',
    sttId: item.sttId || '',
    temperature: item.temperature || '',
    topP: item.topP || ''
  };

  const queryString = Object.keys(params)
    .map(key => `${key}=${encodeURIComponent(params[key] || '')}`)
    .join('&');

  uni.hideLoading();
  // ✅ 修改跳转路径
  uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
};
```

---

### 步骤 5：更新 pages.json 路由配置

**文件路径：** `src/pages.json`

#### 添加路由配置

在 `pages` 数组中添加 role-chat 页面配置，建议放在 `pages/chat/chat` 后面：

```json
{
  "pages": [
    // ... 其他页面 ...
    {
      "path": "pages/chat/chat",
      "style": {
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/role-chat/role-chat",
      "style": {
        "navigationStyle": "custom"
      }
    },
    // ... 其他页面 ...
  ],
  // tabBar 配置保持不变
  "tabBar": {
    "list": [
      { "pagePath": "pages/drama/index", "text": "发现" },
      { "pagePath": "pages/device/index", "text": "设备" },
      { "pagePath": "pages/chat/chat", "text": "专属" },  // ✅ 保持不变
      { "pagePath": "pages/mine/mine", "text": "我的" }
    ]
  }
}
```

---

## 🔍 技术细节

### 1. 会话 ID 管理

ChatBox 组件继续使用原有的会话 ID 管理逻辑：

```javascript
const createNewConversation = (characterId) => {
  let storageKey = '';
  if (characterId === 'wei-ai' || !currentCharacter.value.roleId) {
    // 蔚AI 或默认角色
    storageKey = `session_weiai`;
  } else {
    // 剧情角色
    storageKey = `session_role_${currentCharacter.value.roleId}`;
  }

  let existingSessionId = uni.getStorageSync(storageKey);
  if (existingSessionId) {
    conversationId.value = existingSessionId;
  } else {
    const userId = userStore.userInfo?.openid || userStore.userInfo?.userId || 'guest';
    const timestamp = Date.now();
    const newSessionId = `session_${characterId}_${userId}_${timestamp}`;
    uni.setStorageSync(storageKey, newSessionId);
    conversationId.value = newSessionId;
  }
};
```

**说明：**
- 蔚AI 的会话存储在 `session_weiai`
- 每个角色的会话存储在 `session_role_${roleId}`
- 不同页面使用同一角色时会共享会话历史

---

### 2. 参数编码/解码

#### drama/index.vue 编码

```javascript
const queryString = Object.keys(params)
  .map(key => `${key}=${encodeURIComponent(params[key] || '')}`)
  .join('&');
```

#### role-chat.vue 解码

```javascript
characterConfig.value = {
  name: decodeURIComponent(options.roleName || 'AI角色'),
  avatar: decodeURIComponent(options.avatar || '/static/logo.png'),
  greeting: decodeURIComponent(options.greeting || '你好！很高兴认识你！'),
  roleDesc: decodeURIComponent(options.roleDesc || '')
};
```

---

### 3. AI 配置优先级

在 ChatBox 组件中，AI 配置的优先级：

```javascript
const requestParams = {
  message: userMessage,
  characterId: currentCharacter.value.id,
  conversationId: conversationId.value,
  modelId: 10,      // 默认值
  templateId: 6     // 默认值
};

// 如果角色有自定义配置，则覆盖默认值
if (currentCharacter.value.roleId) {
  if (currentCharacter.value.modelId) {
    requestParams.modelId = currentCharacter.value.modelId;
  }
  if (currentCharacter.value.templateId) {
    requestParams.templateId = currentCharacter.value.templateId;
  } else {
    requestParams.templateId = currentCharacter.value.roleId;
  }
}
```

---

## ✅ 测试验证

### 测试场景

#### 场景 1：点击"专属" tab

**步骤：**
1. 启动应用
2. 点击底部"专属" tab

**预期结果：**
- ✅ 显示蔚AI聊天界面
- ✅ 底部显示 tabBar，"专属"选中
- ✅ 顶部导航栏不显示返回按钮
- ✅ 显示欢迎消息："你好！我是蔚AI，很高兴为您服务！"
- ✅ 可以正常发送消息、接收回复

---

#### 场景 2：从 drama 页面选择角色

**步骤：**
1. 点击底部"发现" tab
2. 选择任意角色
3. 点击"去使用"

**预期结果：**
- ✅ 跳转到 role-chat 页面
- ✅ 底部不显示 tabBar（全屏）
- ✅ 顶部导航栏显示返回按钮
- ✅ 显示角色头像、名称
- ✅ 显示角色的欢迎消息
- ✅ 可以正常发送消息、接收回复

---

#### 场景 3：会话持久化

**步骤：**
1. 从 drama 选择角色 A，发送消息
2. 返回，再次选择角色 A

**预期结果：**
- ✅ 加载之前的聊天历史
- ✅ 消息顺序正确
- ✅ 时间戳正确

---

#### 场景 4：清空对话

**步骤：**
1. 在聊天页面点击"清空"按钮
2. 确认清空

**预期结果：**
- ✅ 历史消息清空
- ✅ 重新显示欢迎消息
- ✅ 生成新的 sessionId

---

#### 场景 5：语音交互（如果启用）

**步骤：**
1. 在聊天页面按住录音按钮
2. 说话后松开

**预期结果：**
- ✅ 显示录音动画
- ✅ 识别语音内容
- ✅ 显示 AI 回复
- ✅ 播放语音（如果角色配置了 TTS）

---

#### 场景 6：多角色会话隔离

**步骤：**
1. 选择角色 A，发送消息
2. 返回，选择角色 B，发送消息
3. 返回，再次选择角色 A

**预期结果：**
- ✅ 角色 A 和角色 B 的会话独立
- ✅ 重新进入角色 A 时，显示之前的消息
- ✅ sessionId 不同（`session_role_A` vs `session_role_B`）

---

## 📊 代码变更统计

| 文件 | 类型 | 行数变化 | 说明 |
|------|------|----------|------|
| `src/components/ChatBox.vue` | 新建 | +850 | 核心聊天组件 |
| `src/pages/role-chat/role-chat.vue` | 新建 | +60 | 角色聊天页面 |
| `src/pages/chat/chat.vue` | 改造 | -960 / +40 | 简化为容器页面 |
| `src/pages/drama/index.vue` | 修改 | ~1 | 修改跳转路径 |
| `src/pages.json` | 修改 | +6 | 添加路由配置 |
| **总计** | - | **约 -1000 行** | 代码复用显著 |

---

## 🎯 方案优势

### 1. 代码复用
- ✅ 聊天逻辑只维护一份（ChatBox 组件）
- ✅ 减少约 1000 行重复代码
- ✅ bug 修复和功能增强只需改组件

### 2. 职责清晰
- ✅ chat.vue → "专属" tab，固定蔚AI
- ✅ role-chat.vue → 角色聊天，动态参数
- ✅ ChatBox.vue → 核心逻辑，通用组件

### 3. 用户体验优化
- ✅ 角色聊天全屏沉浸式（无 tabBar）
- ✅ "专属" tab 不受干扰
- ✅ 清晰的导航层级（返回按钮控制）

### 4. 可扩展性
- ✅ 未来新增聊天场景，只需创建页面使用 ChatBox
- ✅ 组件化后易于添加新功能（如多模态、附件等）

---

## ⚠️ 注意事项

### 1. 兼容性测试
- 确保在 H5 和微信小程序平台都正常工作
- 测试录音功能（仅微信小程序支持）

### 2. 数据迁移
- 如果用户已有聊天记录，确保 sessionId 逻辑不变
- 蔚AI 的 sessionId 保持为 `session_weiai`

### 3. 性能优化
- ChatBox 组件较大（800+ 行），注意内存管理
- 考虑按需加载语音模块（条件编译）

### 4. 错误处理
- 确保 URL 参数缺失时有合理的降级处理
- API 失败时显示友好的错误提示

---

## 🚀 后续优化建议

### 短期优化（可选）

1. **添加加载状态**
   - 组件初始化时显示 loading
   - 避免短暂的空白页面

2. **优化参数传递**
   - 考虑使用 Vuex/Pinia 传递复杂参数
   - 避免 URL 过长（目前约 10 个参数）

3. **添加错误边界**
   - 组件内部捕获异常
   - 防止整个页面崩溃

### 长期优化（未来规划）

1. **消息组件化**
   - 将消息气泡提取为独立组件
   - 支持更多消息类型（图片、文件等）

2. **语音模块拆分**
   - 将录音、播放逻辑提取为 hooks
   - 方便在其他页面复用

3. **性能监控**
   - 添加页面加载耗时监控
   - 优化首屏渲染速度

---

## 📞 联系与支持

如有问题或建议，请联系开发团队。

---

**文档版本：** v1.0
**最后更新：** 2025-11-08
**编写人员：** Claude Code
