diff --git a/chat-refactor-plan.md b/chat-refactor-plan.md new file mode 100644 index 0000000..afc7ba6 --- /dev/null +++ b/chat-refactor-plan.md @@ -0,0 +1,747 @@ +# 聊天页面组件化重构方案 + +## 📋 需求分析 + +### 当前问题 + +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 diff --git a/package.json b/package.json index 1934906..7459470 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@dcloudio/uni-mp-xhs": "3.0.0-4060420250429001", "@dcloudio/uni-quickapp-webview": "3.0.0-4060420250429001", "ant-design-vue": "^4.2.6", - "ant-design-x-vue": "^1.3.2", "pinia": "^2.1.7", "vue": "^3.4.21", "vue-i18n": "^9.1.9" @@ -66,6 +65,7 @@ "@dcloudio/uni-stacktracey": "3.0.0-4060420250429001", "@dcloudio/vite-plugin-uni": "3.0.0-4060420250429001", "@vue/runtime-core": "^3.4.21", + "sass": "^1.93.3", "vite": "5.2.8" } } diff --git a/src/App.vue b/src/App.vue index 533c3c0..79be953 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,19 +3,23 @@ export default { onLaunch: function () { console.log('App Launch'); + // 暂时跳过欢迎页面 - 直接标记为已显示过启动页和已同意协议 + uni.setStorageSync('hasShownSplash', 'true'); + uni.setStorageSync('hasAgreedToTerms', 'true'); + // 检查是否是首次启动或需要显示启动页 - const hasShownSplash = uni.getStorageSync('hasShownSplash'); - const hasAgreedToTerms = uni.getStorageSync('hasAgreedToTerms'); + // const hasShownSplash = uni.getStorageSync('hasShownSplash'); + // const hasAgreedToTerms = uni.getStorageSync('hasAgreedToTerms'); // 如果未显示过启动页或未同意协议,则跳转到启动页 - if (!hasShownSplash || hasAgreedToTerms !== 'true') { - // 延迟一下确保页面已加载 - setTimeout(() => { - uni.redirectTo({ - url: '/pages/splash/splash' - }); - }, 100); - } + // if (!hasShownSplash || hasAgreedToTerms !== 'true') { + // // 延迟一下确保页面已加载 + // setTimeout(() => { + // uni.redirectTo({ + // url: '/pages/splash/splash' + // }); + // }, 100); + // } }, onShow: function () { diff --git a/src/components/ChatBox.vue b/src/components/ChatBox.vue new file mode 100644 index 0000000..a034a5d --- /dev/null +++ b/src/components/ChatBox.vue @@ -0,0 +1,1683 @@ + + + + + diff --git a/src/pages.json b/src/pages.json index 155158e..63970ee 100644 --- a/src/pages.json +++ b/src/pages.json @@ -18,6 +18,12 @@ "navigationStyle": "custom" } }, + { + "path": "pages/role-chat/role-chat", + "style": { + "navigationStyle": "custom" + } + }, { "path": "pages/mine/mine", "style": { diff --git a/src/pages/chat/chat.vue b/src/pages/chat/chat.vue index 825be08..ac68197 100644 --- a/src/pages/chat/chat.vue +++ b/src/pages/chat/chat.vue @@ -14,19 +14,22 @@ - - - 返回 + + + + 返回 + + 清空 {{ currentCharacter.name }} - + { currentCharacter.value = { id: 'wei-ai', name: options.characterName || '蔚AI', - avatar: options.characterAvatar || '/static/logo.png', + avatar: options.characterAvatar || '/static/avatar/icon_hushi.jpg', greeting: decodeURIComponent(options.introMessage || '你好!我是蔚AI,很高兴为您服务!') }; await loadAIConfigs(); - await createNewConversation('wei-ai'); - addMessage('ai', currentCharacter.value.greeting); + createNewConversation('wei-ai'); - if (!isLoggedIn.value) { - addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + // 尝试加载历史消息 + await loadHistoryMessages(); + + // 如果没有历史消息,显示欢迎消息 + if (messages.value.length === 0) { + addMessage('ai', currentCharacter.value.greeting); + + if (!isLoggedIn.value) { + addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + } } } // AI角色 @@ -295,11 +305,18 @@ const initPage = async () => { currentTemplateId.value = parseInt(options.templateId); } - await createNewConversation(options.roleId); - addMessage('ai', currentCharacter.value.greeting); + createNewConversation(options.roleId); - if (!isLoggedIn.value) { - addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + // 尝试加载历史消息 + await loadHistoryMessages(); + + // 如果没有历史消息,显示欢迎消息 + if (messages.value.length === 0) { + addMessage('ai', currentCharacter.value.greeting); + + if (!isLoggedIn.value) { + addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + } } } // 默认角色 @@ -309,11 +326,18 @@ const initPage = async () => { if (character) { currentCharacter.value = character; await loadAIConfigs(); - await createNewConversation(characterId); - addMessage('ai', character.greeting); + createNewConversation(characterId); - if (!isLoggedIn.value) { - addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + // 尝试加载历史消息 + await loadHistoryMessages(); + + // 如果没有历史消息,显示欢迎消息 + if (messages.value.length === 0) { + addMessage('ai', character.greeting); + + if (!isLoggedIn.value) { + addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。'); + } } } } @@ -461,17 +485,115 @@ const sendMessage = async () => { } }; -// 创建新对话 -const createNewConversation = async (characterId) => { +// 创建或获取会话ID(基于角色持久化存储) +const createNewConversation = (characterId) => { + // 生成存储key:根据角色类型区分 + let storageKey = ''; + if (characterId === 'wei-ai' || !currentCharacter.value.roleId) { + // 蔚AI或默认角色 + storageKey = `session_weiai`; + } else { + // 剧情角色 + storageKey = `session_role_${currentCharacter.value.roleId}`; + } + + // 尝试从本地存储获取已有的sessionId + let existingSessionId = uni.getStorageSync(storageKey); + + if (existingSessionId) { + // 已有sessionId,直接使用(保持上下文) + conversationId.value = existingSessionId; + console.log('使用已有sessionId:', existingSessionId, 'storageKey:', storageKey); + } else { + // 生成新的sessionId + 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; + console.log('创建新sessionId:', newSessionId, 'storageKey:', storageKey); + } +}; + +// 加载历史消息 +const loadHistoryMessages = async () => { + if (!conversationId.value || !isLoggedIn.value) { + console.log('没有sessionId或用户未登录,跳过加载历史消息'); + return; + } + try { - const result = await chatAPI.createConversation(characterId); - if (result.success) { - conversationId.value = result.data.conversationId; + console.log('开始加载历史消息,sessionId:', conversationId.value); + const result = await chatAPI.getHistoryMessages(conversationId.value); + + if (result.success && result.data && result.data.length > 0) { + console.log('获取到历史消息:', result.data.length, '条'); + + // 将后端消息格式转换为前端格式,保持与API回复一致的“清理后回复”逻辑 + const historyMessages = []; + + result.data.forEach(msg => { + // 格式化时间 + let timeStr = ''; + if (msg.createTime) { + const date = new Date(msg.createTime); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + timeStr = `${hours}:${minutes}`; + } + + const messageType = msg.sender === 'assistant' ? 'ai' : (msg.sender === 'user' ? 'user' : 'system'); + const rawContent = msg.message || ''; + + // 与API一致:先清理文本,再根据 & 分段 + const cleanedContent = cleanText(rawContent); + + if (messageType === 'ai' && cleanedContent.includes('&')) { + const segments = cleanedContent + .split('&') + .map(s => s.trim()) + .filter(Boolean); + + segments.forEach(segment => { + historyMessages.push({ + type: messageType, + content: segment, + time: timeStr + }); + }); + } else { + // 非 AI 或无分隔符,直接使用清理后的内容 + const content = cleanedContent.trim(); + if (content) { + historyMessages.push({ + type: messageType, + content, + time: timeStr + }); + } + } + }); + + // 按时间排序(从旧到新) + historyMessages.sort((a, b) => { + if (!a.time || !b.time) return 0; + return a.time.localeCompare(b.time); + }); + + // 清空当前消息列表,加载历史消息 + messages.value = historyMessages; + console.log('历史消息加载完成,共', messages.value.length, '条'); + + // 滚动到底部 + await nextTick(); + scrollToBottom(); } else { - conversationId.value = `local_${Date.now()}`; + console.log('没有历史消息或获取失败'); } } catch (error) { - conversationId.value = `local_${Date.now()}`; + console.error('加载历史消息失败:', error); } }; @@ -814,6 +936,65 @@ const handleInputFocus = () => { const goBack = () => { uni.navigateBack(); }; + + +// 清空对话上下文 +const clearContext = async () => { + uni.showModal({ + title: '清空对话', + content: '确定要清空与该角色的所有对话记录吗?此操作不可恢复。', + confirmText: '确定清空', + cancelText: '取消', + success: async (res) => { + if (res.confirm) { + try { + uni.showLoading({ title: '清空中...' }); + + // 1. 调用后端清除session(如果用户已登录) + if (conversationId.value && isLoggedIn.value) { + try { + const result = await chatAPI.clearSession(conversationId.value); + console.log('后端清除会话结果:', result); + } catch (error) { + console.log('后端清除失败,继续本地清除:', error); + } + } + + // 2. 删除本地存储的sessionId + let storageKey = ''; + if (currentCharacter.value.id === 'wei-ai' || !currentCharacter.value.roleId) { + storageKey = `session_weiai`; + } else { + storageKey = `session_role_${currentCharacter.value.roleId}`; + } + uni.removeStorageSync(storageKey); + console.log('删除本地sessionId:', storageKey); + + // 3. 生成新的sessionId + const characterIdForSession = currentCharacter.value.id || currentCharacter.value.roleId || 'default'; + createNewConversation(characterIdForSession); + + // 4. 清空消息列表(保留欢迎消息) + messages.value = []; + addMessage('ai', currentCharacter.value.greeting || '你好!很高兴再次见到你!'); + + uni.hideLoading(); + uni.showToast({ + title: '对话已清空', + icon: 'success' + }); + } catch (error) { + console.error('清空对话失败:', error); + uni.hideLoading(); + uni.showToast({ + title: '清空失败', + icon: 'none' + }); + } + } + } + }); +}; diff --git a/src/pages/role-chat/role-chat.vue b/src/pages/role-chat/role-chat.vue new file mode 100644 index 0000000..4099488 --- /dev/null +++ b/src/pages/role-chat/role-chat.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/static/avatar/icon_hushi.jpg b/src/static/avatar/icon_hushi.jpg new file mode 100644 index 0000000..5581656 Binary files /dev/null and b/src/static/avatar/icon_hushi.jpg differ diff --git a/src/stores/user.js b/src/stores/user.js index 8054df4..4c668a9 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -101,7 +101,7 @@ export const useUserStore = defineStore('user', () => { function wxLogin(code, userInfo) { return new Promise((resolve, reject) => { uni.request({ - url: 'http://localhost:8091/app/login', + url: 'http://192.168.1.2:8091/app/login', method: 'POST', data: { code diff --git a/src/utils/api.js b/src/utils/api.js index 1fbc293..62b5aa6 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,6 +1,31 @@ // API服务文件 import { useUserStore } from '@/stores/user.js'; +// 图片URL处理函数 - 处理小程序中图片路径问题 +export const getResourceUrl = (url) => { + if (!url || typeof url !== 'string') { + return '/static/default-avatar.png'; + } + + // 如果是完整的http/https URL,直接返回 + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + // 如果是相对路径,拼接完整的服务器地址 + if (url.startsWith('/file/')) { + return BASE_URL + url; + } + + // 如果是其他相对路径,也拼接服务器地址 + if (url.startsWith('/')) { + return BASE_URL + url; + } + + // 默认返回原路径 + return url; +}; + // 文本清理函数 - 只保留文字和标点符号 export const cleanText = (text) => { if (!text || typeof text !== 'string') { @@ -49,7 +74,7 @@ export const cleanText = (text) => { }; // 基础配置 -const BASE_URL = 'http://192.168.3.243:8091'; // 根据后端地址调整 +const BASE_URL = 'http://192.168.1.2:8091'; // 根据后端地址调整 // 检查用户登录状态 const checkLoginStatus = () => { @@ -150,7 +175,7 @@ export const chatAPI = { useFunctionCall: false, modelId: params.modelId || null, // 支持传入modelId,默认为null使用后端默认 templateId: params.templateId || params.characterId, // 支持templateId参数 - sessionId: params.sessionId || null // 支持sessionId参数 + sessionId: params.sessionId || params.conversationId || null // 支持sessionId参数,conversationId作为备选 }; console.log('发送AI聊天请求,参数:', requestData); @@ -317,7 +342,7 @@ export const chatAPI = { characterId: characterId } }); - + return { success: true, data: response @@ -329,6 +354,83 @@ export const chatAPI = { error: error }; } + }, + + // 清空会话上下文 + clearSession: async (sessionId) => { + try { + const response = await request({ + url: `/api/chat/session/${sessionId}`, + method: 'DELETE' + }); + + return { + success: true, + data: response + }; + } catch (error) { + console.error('清空会话API调用失败:', error); + return { + success: false, + error: error + }; + } + }, + + // 获取历史消息(根据sessionId查询全部) + getHistoryMessages: async (sessionId) => { + const loginStatus = checkLoginStatus(); + + // 如果用户未登录,直接返回空数组 + if (!loginStatus.isLoggedIn) { + console.log('用户未登录,无法获取历史消息'); + return { + success: true, + data: [], + isAnonymous: true + }; + } + + try { + const response = await request({ + url: '/app/message/history', + method: 'GET', + data: { + sessionId: sessionId + } + }); + + console.log('历史消息API响应:', response); + + // 处理响应数据 + let messageList = []; + if (response && response.data) { + // 直接是数组 + if (Array.isArray(response.data)) { + messageList = response.data; + } + // 可能嵌套在data字段中 + else if (response.data.data && Array.isArray(response.data.data)) { + messageList = response.data.data; + } + } else if (Array.isArray(response)) { + messageList = response; + } + + console.log('解析后的历史消息数量:', messageList.length); + + return { + success: true, + data: messageList + }; + } catch (error) { + console.error('获取历史消息失败:', error); + return { + success: false, + error: error, + data: [] + }; + } } };