Files
webUI/chat-refactor-plan.md

748 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 聊天页面组件化重构方案
## 📋 需求分析
### 当前问题
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