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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ←
+ 返回
+
+ 清空
+
+
+
+ {{ currentCharacter.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ message.content }}
+
+
+
+
+
+
+
+
+
+
+ {{ message.content }}
+
+
+
+
+ {{ message.time }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送
+
+
+
+
+
+
+
+ {{ isActuallyRecording ? '松手发送...' : '按住说话' }}
+
+
+
+
+
+
+
+
+
+
+ {{ recordingTipText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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: []
+ };
+ }
}
};