From 71ffa0a816ad4521b4509882dacc678788236b55 Mon Sep 17 00:00:00 2001 From: qupan li Date: Sat, 8 Nov 2025 21:01:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8D=A1=EF=BC=8C=E6=99=BA=E8=83=BD=E4=BD=93=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=8A=A0=E8=BD=BD=EF=BC=8C=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E6=B8=85=E7=A9=BA=E7=AD=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat-refactor-plan.md | 747 +++++++++++++ package.json | 2 +- src/App.vue | 24 +- src/components/ChatBox.vue | 1683 +++++++++++++++++++++++++++++ src/pages.json | 6 + src/pages/chat/chat.vue | 257 ++++- src/pages/drama/index.vue | 386 +++++-- src/pages/role-chat/role-chat.vue | 57 + src/static/avatar/icon_hushi.jpg | Bin 0 -> 36966 bytes src/stores/user.js | 2 +- src/utils/api.js | 108 +- 11 files changed, 3156 insertions(+), 116 deletions(-) create mode 100644 chat-refactor-plan.md create mode 100644 src/components/ChatBox.vue create mode 100644 src/pages/role-chat/role-chat.vue create mode 100644 src/static/avatar/icon_hushi.jpg 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 0000000000000000000000000000000000000000..55816566d6ec8f2df28d6cd98db515b37d3e7fd6 GIT binary patch literal 36966 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 O!+hMJf==WKXs7_(yV`dE literal 0 HcmV?d00001 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: [] + }; + } } };