# 聊天页面组件化重构方案 ## 📋 需求分析 ### 当前问题 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 ``` #### 参数来源 这些参数来自 `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 ``` #### 说明 - ✅ 所有参数写死,不做任何 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