feat: 完成角色卡,智能体配置,加载,历史记录,清空等功能
This commit is contained in:
747
chat-refactor-plan.md
Normal file
747
chat-refactor-plan.md
Normal file
@@ -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
|
||||
<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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
24
src/App.vue
24
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 () {
|
||||
|
||||
1683
src/components/ChatBox.vue
Normal file
1683
src/components/ChatBox.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,12 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/role-chat/role-chat",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
|
||||
@@ -14,19 +14,22 @@
|
||||
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-left" @tap="goBack" v-if="showBackButton">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-text">返回</text>
|
||||
<view class="navbar-left">
|
||||
<view v-if="showBackButton" @tap="goBack" class="left-btn">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-text">返回</text>
|
||||
</view>
|
||||
<text class="clear-btn" @tap="clearContext">清空</text>
|
||||
</view>
|
||||
<view class="navbar-center" :class="{'centered': !showBackButton}">
|
||||
<image class="character-avatar" :src="currentCharacter.avatar" mode="aspectFill" />
|
||||
<text class="character-name">{{ currentCharacter.name }}</text>
|
||||
</view>
|
||||
<view class="navbar-right">
|
||||
<text class="status-dot" :class="{'online': isOnline}"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 聊天消息区域 - 使用 Ant Design X Bubble.List -->
|
||||
<scroll-view
|
||||
class="chat-messages"
|
||||
@@ -257,16 +260,23 @@ const initPage = async () => {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -897,13 +1078,19 @@ page {
|
||||
left: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
gap: 16rpx;
|
||||
color: #f9e076;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 10rpx rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.left-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
@@ -938,13 +1125,22 @@ page {
|
||||
.navbar-right {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(249, 224, 118, 0.3);
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #f9e076;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 8rpx rgba(249, 224, 118, 0.3);
|
||||
padding: 8rpx 16rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
@@ -962,6 +1158,7 @@ page {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 聊天消息区域 */
|
||||
.chat-messages {
|
||||
height: calc(100vh - 100rpx - 200rpx - env(safe-area-inset-top));
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<view class="floral-container">
|
||||
<!-- 夜空装饰背景 -->
|
||||
<view class="night-sky-decoration">
|
||||
<view class="star star-1"></view>
|
||||
<view class="star star-2"></view>
|
||||
<view class="star star-3"></view>
|
||||
<view class="star star-4"></view>
|
||||
<view class="star star-5"></view>
|
||||
<view class="star star-6"></view>
|
||||
<view class="star star-7"></view>
|
||||
<view class="star star-8"></view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<view class="navbar-stars">
|
||||
<view class="navbar-star star-left"></view>
|
||||
<view class="navbar-star star-right"></view>
|
||||
</view>
|
||||
<view class="navbar-title">剧情角色</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -18,7 +34,9 @@
|
||||
<view class="column column-left">
|
||||
<block v-for="(item, idx) in leftColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="cover-container">
|
||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
@@ -34,7 +52,9 @@
|
||||
<view class="column column-right">
|
||||
<block v-for="(item, idx) in rightColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="cover-container">
|
||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
@@ -69,8 +89,8 @@
|
||||
<view class="detail-cover-container">
|
||||
<image
|
||||
class="detail-cover"
|
||||
:src="selectedItem?.cover"
|
||||
mode="aspectFit"
|
||||
:src="getResourceUrl(selectedItem?.cover)"
|
||||
mode="aspectFill"
|
||||
:class="{'cover-zoomed': showDetailModal}"
|
||||
/>
|
||||
</view>
|
||||
@@ -85,7 +105,7 @@
|
||||
</scroll-view>
|
||||
<view class="detail-actions">
|
||||
<button class="floral-btn outline detail-btn cancel" @click="closeDetail">取消</button>
|
||||
<button class="floral-btn detail-btn confirm" @click="useFromDetail">💝 去使用</button>
|
||||
<button class="floral-btn detail-btn confirm" @click="useFromDetail">✨ 去使用</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -96,7 +116,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
import { roleAPI } from '@/utils/api.js';
|
||||
import { roleAPI, getResourceUrl } from '@/utils/api.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const showLoginModal = ref(false);
|
||||
@@ -334,8 +354,9 @@ const loadDramaList = async () => {
|
||||
dramaList.value = roles.map(role => ({
|
||||
id: role.roleId,
|
||||
roleId: role.roleId,
|
||||
cover: role.avatar || '/static/default-avatar.png',
|
||||
cover: role.backgroundImage || role.avatar || '/static/default-avatar.png', // 使用背景图片作为封面,如果没有则使用头像
|
||||
avatar: role.avatar || '/static/default-avatar.png',
|
||||
backgroundImage: role.backgroundImage || '', // 背景图片字段
|
||||
tag: role.tag || '角色',
|
||||
title: role.roleName || '未命名角色',
|
||||
roleName: role.roleName || '未命名角色',
|
||||
@@ -413,7 +434,7 @@ const handleUse = (item) => {
|
||||
.join('&');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.navigateTo({ url: `/pages/chat/chat?${queryString}` });
|
||||
uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
|
||||
};
|
||||
|
||||
const showLoginTip = () => { showLoginModal.value = true; };
|
||||
@@ -423,27 +444,62 @@ const useFromDetail = () => { if (selectedItem.value) { const v = selectedItem.v
|
||||
const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/pages/mine/mine' }); };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.floral-container {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 120rpx;
|
||||
overflow: hidden;
|
||||
padding-bottom: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 夜空装饰 */
|
||||
.night-sky-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 2s infinite;
|
||||
box-shadow: 0 0 10rpx #f9e076;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.star-1 { top: 10%; left: 15%; animation-delay: 0s; }
|
||||
.star-2 { top: 20%; left: 80%; animation-delay: 0.3s; }
|
||||
.star-3 { top: 30%; left: 45%; animation-delay: 0.6s; }
|
||||
.star-4 { top: 50%; left: 25%; animation-delay: 0.9s; }
|
||||
.star-5 { top: 60%; left: 70%; animation-delay: 1.2s; }
|
||||
.star-6 { top: 70%; left: 40%; animation-delay: 1.5s; }
|
||||
.star-7 { top: 15%; left: 60%; animation-delay: 0.4s; }
|
||||
.star-8 { top: 85%; left: 55%; animation-delay: 1.8s; }
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.95) 0%, rgba(74, 30, 109, 0.95) 25%, rgba(107, 44, 156, 0.95) 50%, rgba(138, 43, 226, 0.95) 75%, rgba(75, 0, 130, 0.95) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
|
||||
background: rgba(26, 11, 46, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@@ -452,40 +508,81 @@ const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/p
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24rpx;
|
||||
padding: 0 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-stars {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navbar-star {
|
||||
position: absolute;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 15rpx #f9e076;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.star-left {
|
||||
top: 50%;
|
||||
left: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.star-right {
|
||||
top: 50%;
|
||||
right: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #ffffff;
|
||||
color: #f9e076;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
text-shadow: 0 0 10px rgba(249, 224, 118, 0.5);
|
||||
font-size: 36rpx;
|
||||
text-shadow: 0 0 20rpx rgba(249, 224, 118, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.two-column-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
gap: 12rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
width: calc(50% - 8rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.floral-grid-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.2);
|
||||
border-radius: 25rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(138, 43, 226, 0.15);
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -493,97 +590,244 @@ const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/p
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floral-grid-card:active {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 15rpx 50rpx rgba(249, 224, 118, 0.2);
|
||||
border-color: rgba(249, 224, 118, 0.4);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.floral-tag {
|
||||
position: absolute;
|
||||
margin: 12rpx;
|
||||
background: linear-gradient(135deg, #f9e076, #f59e0b);
|
||||
color: #4b0082;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
|
||||
top: 12rpx;
|
||||
left: 12rpx;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4rpx 12rpx rgba(249, 224, 118, 0.4);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content-area { padding: 16rpx; }
|
||||
.title { font-size: 28rpx; color: #333; font-weight: bold; }
|
||||
.card-bottom { margin-top: 8rpx; display: flex; justify-content: flex-end; }
|
||||
.content-area {
|
||||
padding: 20rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
color: #f9e076;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.floral-btn {
|
||||
background: linear-gradient(135deg, #8a2be2, #6b2c9c);
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
box-shadow: 0 4rpx 15rpx rgba(138, 43, 226, 0.3);
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 12rpx 24rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(249, 224, 118, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floral-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 3rpx 8rpx rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.floral-btn.outline {
|
||||
background: transparent;
|
||||
color: #8a2be2;
|
||||
border: 2rpx solid #8a2be2;
|
||||
color: #f9e076;
|
||||
border: 2rpx solid #f9e076;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.floral-grid-card:hover {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 12rpx 40rpx rgba(138, 43, 226, 0.2);
|
||||
border-color: #8a2be2;
|
||||
.use-btn {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.use-btn { font-size: 24rpx; }
|
||||
.bottom-spacing { height: 40rpx; }
|
||||
.bottom-spacing {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 登录弹窗 */
|
||||
.floral-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.floral-modal-content {
|
||||
width: 80vw;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.98) 0%, rgba(45, 27, 78, 0.98) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
border-radius: 30rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(249, 224, 118, 0.8);
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background: transparent;
|
||||
color: rgba(249, 224, 118, 0.6);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.confirm {
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
}
|
||||
.modal-title { font-size: 30rpx; font-weight: bold; margin-bottom: 12rpx; }
|
||||
.modal-text { font-size: 24rpx; color: #666; margin-bottom: 16rpx; }
|
||||
.modal-btns { display: flex; gap: 20rpx; justify-content: flex-end; }
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 2100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-modal-content {
|
||||
width: 86vw;
|
||||
max-height: 80vh;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.98) 0%, rgba(45, 27, 78, 0.98) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
border-radius: 30rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.detail-cover-container {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cover-zoomed {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
padding: 30rpx;
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
transition: all 0.3s ease 0.1s;
|
||||
}
|
||||
|
||||
.info-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx;
|
||||
color: #1a0b2e;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #f9e076;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-description {
|
||||
max-height: 200rpx;
|
||||
margin-bottom: 30rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(249, 224, 118, 0.8);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
flex: 1;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
.detail-cover-container { width: 100%; height: 320rpx; overflow: hidden; }
|
||||
.detail-cover { width: 100%; height: 100%; }
|
||||
.detail-info { padding: 20rpx; }
|
||||
.detail-tag { font-size: 22rpx; color: #8a2be2; margin-bottom: 8rpx; }
|
||||
.detail-title { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 12rpx; }
|
||||
.detail-description { max-height: 200rpx; }
|
||||
.description-text { font-size: 24rpx; color: #555; line-height: 1.6; }
|
||||
.detail-actions { margin-top: 12rpx; display: flex; justify-content: flex-end; gap: 20rpx; }
|
||||
.detail-btn { padding: 10rpx 22rpx; }
|
||||
</style>
|
||||
|
||||
57
src/pages/role-chat/role-chat.vue
Normal file
57
src/pages/role-chat/role-chat.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<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>
|
||||
BIN
src/static/avatar/icon_hushi.jpg
Normal file
BIN
src/static/avatar/icon_hushi.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -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
|
||||
|
||||
108
src/utils/api.js
108
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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user