feat: 完成角色卡,智能体配置,加载,历史记录,清空等功能

This commit is contained in:
2025-11-08 21:01:58 +08:00
parent 82546c4381
commit 71ffa0a816
11 changed files with 3156 additions and 116 deletions

747
chat-refactor-plan.md Normal file
View 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

View File

@@ -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"
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,12 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/role-chat/role-chat",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/mine",
"style": {

View File

@@ -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));

View File

@@ -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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -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

View File

@@ -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: []
};
}
}
};