Compare commits
2 Commits
19aa7a1f8f
...
6087a3f195
| Author | SHA1 | Date | |
|---|---|---|---|
| 6087a3f195 | |||
| 8d861d5b6f |
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="chat-container" :style="{ paddingTop: navBarHeight + 'px', paddingBottom: `calc(200rpx + ${keyboardHeight}px)` }">
|
<view class="chat-container" :style="{ paddingTop: navBarHeight + 'px' }">
|
||||||
<!-- 夜空装饰背景 -->
|
<!-- 夜空装饰背景 -->
|
||||||
<view class="night-sky-decoration">
|
<view class="night-sky-decoration">
|
||||||
<view class="star star-1"></view>
|
<view class="star star-1"></view>
|
||||||
@@ -16,13 +16,9 @@
|
|||||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<view class="navbar-content">
|
<view class="navbar-content">
|
||||||
<view class="navbar-left">
|
<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>
|
<text class="clear-btn" @tap="clearContext">清空</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="navbar-center" :class="{'centered': !showBackButton}">
|
<view class="navbar-center centered">
|
||||||
<image class="character-avatar" :src="processedAvatar" mode="aspectFill" />
|
<image class="character-avatar" :src="processedAvatar" mode="aspectFill" />
|
||||||
<text class="character-name">{{ currentCharacter.name }}</text>
|
<text class="character-name">{{ currentCharacter.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -35,6 +31,7 @@
|
|||||||
<!-- 聊天消息区域 - 使用 Ant Design X Bubble.List -->
|
<!-- 聊天消息区域 - 使用 Ant Design X Bubble.List -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="chat-messages"
|
class="chat-messages"
|
||||||
|
:style="{ height: chatMessagesHeight }"
|
||||||
scroll-y="true"
|
scroll-y="true"
|
||||||
:scroll-top="scrollTop"
|
:scroll-top="scrollTop"
|
||||||
scroll-with-animation="true"
|
scroll-with-animation="true"
|
||||||
@@ -96,7 +93,10 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 输入区域 - 使用 Ant Design X Sender 风格 -->
|
<!-- 输入区域 - 使用 Ant Design X Sender 风格 -->
|
||||||
<view class="input-area" :style="{ bottom: keyboardHeight + 'px' }">
|
<view class="input-area" :style="{
|
||||||
|
bottom: inputAreaBottom + 'px',
|
||||||
|
paddingBottom: inputAreaPaddingBottom
|
||||||
|
}">
|
||||||
<!-- 智能体选择按钮 -->
|
<!-- 智能体选择按钮 -->
|
||||||
<!-- <view class="action-bar" v-if="availableTemplates.length > 0">
|
<!-- <view class="action-bar" v-if="availableTemplates.length > 0">
|
||||||
<view class="template-selector-btn" @click="showTemplateSelector">
|
<view class="template-selector-btn" @click="showTemplateSelector">
|
||||||
@@ -200,10 +200,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick, watch } from 'vue';
|
import { ref, computed, onMounted, nextTick, watch, getCurrentInstance } from 'vue';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
import { useUserStore } from '@/stores/user.js';
|
||||||
import { getCharacterById, getSmartResponse } from '@/utils/aiCharacters.js';
|
import { getCharacterById, getSmartResponse } from '@/utils/aiCharacters.js';
|
||||||
import { chatAPI, voiceAPI, cleanText, configAPI, getResourceUrl } from '@/utils/api.js';
|
import { chatAPI, voiceAPI, cleanText, configAPI, getResourceUrl } from '@/utils/api.js';
|
||||||
|
// 🔴 新增:导入未读消息管理模块
|
||||||
|
import {
|
||||||
|
getUnreadMessages,
|
||||||
|
clearUnreadMessages
|
||||||
|
} from '@/utils/unreadMessages.js';
|
||||||
|
|
||||||
|
// 获取组件实例
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
// 定义 Props
|
// 定义 Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -232,14 +240,6 @@ const props = defineProps({
|
|||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
topP: 0.9
|
topP: 0.9
|
||||||
})
|
})
|
||||||
},
|
|
||||||
|
|
||||||
// UI 配置
|
|
||||||
uiConfig: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
showBackButton: true // 是否显示返回按钮
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,12 +257,32 @@ const userAvatar = ref('/static/default-avatar.png');
|
|||||||
const conversationId = ref(null);
|
const conversationId = ref(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
const showBackButton = computed(() => props.uiConfig.showBackButton);
|
|
||||||
const keyboardHeight = ref(0); // 键盘高度
|
const keyboardHeight = ref(0); // 键盘高度
|
||||||
|
|
||||||
// 状态栏高度适配
|
// 状态栏高度适配
|
||||||
const statusBarHeight = ref(0);
|
const statusBarHeight = ref(0);
|
||||||
const navBarHeight = ref(0);
|
const navBarHeight = ref(0);
|
||||||
|
const inputAreaHeight = ref(0); // 输入区域高度,页面加载时获取一次
|
||||||
|
const tabBarHeight = ref(0); // tabBar 高度
|
||||||
|
|
||||||
|
// 输入区域底部 padding:键盘弹出时去掉安全区域
|
||||||
|
const inputAreaPaddingBottom = computed(() => {
|
||||||
|
const value = keyboardHeight.value > 0 ? '16rpx' : 'calc(16rpx + env(safe-area-inset-bottom))';
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入区域的 bottom 值:如果在 tabBar 页面中,需要减去 tabBar 高度
|
||||||
|
const inputAreaBottom = computed(() => {
|
||||||
|
const bottom = keyboardHeight.value > 0 ? keyboardHeight.value - tabBarHeight.value : 0;
|
||||||
|
return bottom;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 聊天区域高度:固定不变,只在初始化时计算一次
|
||||||
|
const chatMessagesHeight = computed(() => {
|
||||||
|
// 100vh - 导航栏高度 - 输入区域高度
|
||||||
|
const height = `calc(100vh - ${navBarHeight.value}px - ${inputAreaHeight.value}px)`;
|
||||||
|
return height;
|
||||||
|
});
|
||||||
|
|
||||||
// 录音相关
|
// 录音相关
|
||||||
const isRecordingMode = ref(false);
|
const isRecordingMode = ref(false);
|
||||||
@@ -279,6 +299,9 @@ const currentModelId = ref(null);
|
|||||||
const currentTemplateId = ref(null);
|
const currentTemplateId = ref(null);
|
||||||
const showTemplateModal = ref(false);
|
const showTemplateModal = ref(false);
|
||||||
|
|
||||||
|
// 🔴 新增:未读消息展示标记
|
||||||
|
const hasShownUnreadMessages = ref(false);
|
||||||
|
|
||||||
// 当前模板名称
|
// 当前模板名称
|
||||||
const currentTemplateName = computed(() => {
|
const currentTemplateName = computed(() => {
|
||||||
const template = availableTemplates.value.find(t => t.templateId === currentTemplateId.value);
|
const template = availableTemplates.value.find(t => t.templateId === currentTemplateId.value);
|
||||||
@@ -331,6 +354,15 @@ watch(() => props.characterConfig, async (newConfig) => {
|
|||||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔴 新增:展示未读消息
|
||||||
|
await showUnreadMessages();
|
||||||
|
|
||||||
|
// 确保滚动到底部
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 300);
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
// 获取系统状态栏高度
|
// 获取系统状态栏高度
|
||||||
@@ -339,25 +371,76 @@ const getSystemInfo = () => {
|
|||||||
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
||||||
// 导航栏总高度 = 状态栏高度 + 导航栏内容高度(44px)
|
// 导航栏总高度 = 状态栏高度 + 导航栏内容高度(44px)
|
||||||
navBarHeight.value = statusBarHeight.value + 44;
|
navBarHeight.value = statusBarHeight.value + 44;
|
||||||
console.log('状态栏高度:', statusBarHeight.value, '导航栏总高度:', navBarHeight.value);
|
|
||||||
|
// 获取当前页面路径,判断是否在 tabBar 中
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const currentPage = pages[pages.length - 1];
|
||||||
|
const currentRoute = currentPage ? currentPage.route : '';
|
||||||
|
// tabBar 页面列表(从 pages.json 中获取)
|
||||||
|
const tabBarPages = [
|
||||||
|
'pages/drama/index',
|
||||||
|
'pages/device/index',
|
||||||
|
'pages/chat/chat-box',
|
||||||
|
'pages/mine/mine'
|
||||||
|
];
|
||||||
|
const isTabBarPage = tabBarPages.includes(currentRoute);
|
||||||
|
// 获取 tabBar 高度
|
||||||
|
if (systemInfo.screenHeight && systemInfo.windowHeight) {
|
||||||
|
const heightDiff = systemInfo.screenHeight - systemInfo.windowHeight;
|
||||||
|
// 如果是 tabBar 页面,尝试检测 tabBar 高度
|
||||||
|
if (isTabBarPage) {
|
||||||
|
if (heightDiff > 40 && heightDiff < 120) {
|
||||||
|
// 直接使用检测到的高度差值
|
||||||
|
tabBarHeight.value = heightDiff;
|
||||||
|
} else if (heightDiff >= 120) {
|
||||||
|
// 差值过大,可能包含其他因素,使用保守值
|
||||||
|
tabBarHeight.value = 82;
|
||||||
|
} else {
|
||||||
|
// 差值过小,使用默认值
|
||||||
|
tabBarHeight.value = 50;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tabBarHeight.value = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是 tabBar 页面但无法获取高度,使用默认值
|
||||||
|
if (isTabBarPage) {
|
||||||
|
tabBarHeight.value = 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取输入区域实际高度(只在初始化时调用一次)
|
||||||
|
const getInputAreaHeight = () => {
|
||||||
|
// 延迟获取,确保 DOM 已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
// 使用 in(instance) 指定在当前组件内查找
|
||||||
|
const query = uni.createSelectorQuery().in(instance);
|
||||||
|
query.select('.input-area').boundingClientRect();
|
||||||
|
query.exec((res) => {
|
||||||
|
if (res && res[0] && res[0].height) {
|
||||||
|
inputAreaHeight.value = res[0].height;
|
||||||
|
} else {
|
||||||
|
inputAreaHeight.value = 120; // 降级使用默认值
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 300); // 延迟 300ms 确保 DOM 渲染完成
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getSystemInfo(); // 获取系统信息
|
getSystemInfo(); // 获取系统信息
|
||||||
checkLoginStatus(); // 检查登录状态
|
checkLoginStatus(); // 检查登录状态
|
||||||
initRecorder(); // 初始化录音器
|
initRecorder(); // 初始化录音器
|
||||||
|
getInputAreaHeight(); // 获取输入区域高度
|
||||||
// 监听键盘高度变化
|
// 监听键盘高度变化
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
uni.onKeyboardHeightChange((res) => {
|
uni.onKeyboardHeightChange((res) => {
|
||||||
console.log('键盘高度变化:', res.height);
|
|
||||||
keyboardHeight.value = res.height;
|
keyboardHeight.value = res.height;
|
||||||
|
|
||||||
// 键盘弹出时,延迟滚动到底部
|
// 键盘弹出时,延迟滚动到底部
|
||||||
if (res.height > 0) {
|
if (res.height > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, 350); // 等待过渡动画完成
|
}, 300); // 等待输入框移动动画完成
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
@@ -383,7 +466,6 @@ const loadAIConfigs = async () => {
|
|||||||
currentTemplateId.value = 1;
|
currentTemplateId.value = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载AI配置异常:', error);
|
|
||||||
availableTemplates.value = [
|
availableTemplates.value = [
|
||||||
{ templateId: 1, templateName: '默认AI模型', templateDesc: '默认AI模型' }
|
{ templateId: 1, templateName: '默认AI模型', templateDesc: '默认AI模型' }
|
||||||
];
|
];
|
||||||
@@ -406,18 +488,15 @@ const initRecorder = () => {
|
|||||||
recorderManager.value = uni.getRecorderManager();
|
recorderManager.value = uni.getRecorderManager();
|
||||||
|
|
||||||
recorderManager.value.onStart(() => {
|
recorderManager.value.onStart(() => {
|
||||||
console.log('录音开始');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
recorderManager.value.onStop((res) => {
|
recorderManager.value.onStop((res) => {
|
||||||
console.log('录音结束', res);
|
|
||||||
if (!isRecordingCancelled.value) {
|
if (!isRecordingCancelled.value) {
|
||||||
handleVoiceMessage(res.tempFilePath);
|
handleVoiceMessage(res.tempFilePath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
recorderManager.value.onError((err) => {
|
recorderManager.value.onError((err) => {
|
||||||
console.error('录音错误', err);
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '录音失败',
|
title: '录音失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -445,18 +524,86 @@ const addMessage = (type, content) => {
|
|||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
const query = uni.createSelectorQuery();
|
nextTick(() => {
|
||||||
query.select('.message-list').boundingClientRect();
|
// 使用 in(instance) 指定在当前组件内查找
|
||||||
query.exec((res) => {
|
const query = uni.createSelectorQuery().in(instance);
|
||||||
if (res[0]) {
|
query.select('.message-list').boundingClientRect();
|
||||||
scrollTop.value = res[0].height;
|
query.exec((res) => {
|
||||||
}
|
if (res && res[0] && res[0].height) {
|
||||||
|
// 设置一个足够大的值确保滚动到底部(包含底部padding)
|
||||||
|
scrollTop.value = res[0].height + 2000;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 延时工具
|
// 延时工具
|
||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// 🔴 新增:展示未读消息的函数
|
||||||
|
const showUnreadMessages = async () => {
|
||||||
|
// 防止重复展示
|
||||||
|
if (hasShownUnreadMessages.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleId = currentCharacter.value.roleId;
|
||||||
|
if (!roleId) {
|
||||||
|
hasShownUnreadMessages.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取未读消息
|
||||||
|
const unreadMessages = getUnreadMessages(roleId);
|
||||||
|
|
||||||
|
if (unreadMessages.length === 0) {
|
||||||
|
hasShownUnreadMessages.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📬 角色 ${currentCharacter.value.name} (ID: ${roleId}) 有 ${unreadMessages.length} 条未读消息`);
|
||||||
|
|
||||||
|
// 延迟展示,模拟角色正在输入
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(async () => {
|
||||||
|
// 显示"正在输入"状态
|
||||||
|
isTyping.value = true;
|
||||||
|
|
||||||
|
// 等待1.5秒(模拟打字延迟)
|
||||||
|
await sleep(1500);
|
||||||
|
|
||||||
|
isTyping.value = false;
|
||||||
|
|
||||||
|
// 逐条展示未读消息
|
||||||
|
for (let i = 0; i < unreadMessages.length; i++) {
|
||||||
|
const msg = unreadMessages[i];
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
addMessage('ai', msg.content);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
await nextTick();
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
// 如果不是最后一条,稍作延迟(模拟连续发送的节奏)
|
||||||
|
if (i < unreadMessages.length - 1) {
|
||||||
|
await sleep(800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空该角色的未读消息
|
||||||
|
clearUnreadMessages(roleId);
|
||||||
|
hasShownUnreadMessages.value = true;
|
||||||
|
|
||||||
|
console.log(`✅ 已展示并清空角色 ${currentCharacter.value.name} 的未读消息`);
|
||||||
|
|
||||||
|
// 最终滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 300);
|
||||||
|
}, 1000); // 进入聊天后1秒开始展示
|
||||||
|
};
|
||||||
|
|
||||||
// 根据 & 分段,逐条展示 AI 回复
|
// 根据 & 分段,逐条展示 AI 回复
|
||||||
const addSegmentedAIResponse = async (text) => {
|
const addSegmentedAIResponse = async (text) => {
|
||||||
if (!text || typeof text !== 'string') return;
|
if (!text || typeof text !== 'string') return;
|
||||||
@@ -471,12 +618,23 @@ const addSegmentedAIResponse = async (text) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const seg of segments) {
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const seg = segments[i];
|
||||||
addMessage('ai', seg);
|
addMessage('ai', seg);
|
||||||
|
|
||||||
|
// 每添加一条消息就滚动到底部,确保用户能看到
|
||||||
|
await nextTick();
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
// 简单的节奏控制:与长度相关,介于300-1200ms
|
// 简单的节奏控制:与长度相关,介于300-1200ms
|
||||||
const delay = Math.min(1200, Math.max(300, seg.length * 30));
|
const delay = Math.min(1200, Math.max(300, seg.length * 30));
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有段落添加完成后,再次确保滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
@@ -491,7 +649,6 @@ const sendMessage = async () => {
|
|||||||
try {
|
try {
|
||||||
await getAIResponse(text);
|
await getAIResponse(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('发送消息失败:', error);
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '发送失败',
|
title: '发送失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -519,7 +676,6 @@ const createNewConversation = (characterId) => {
|
|||||||
if (existingSessionId) {
|
if (existingSessionId) {
|
||||||
// 已有sessionId,直接使用(保持上下文)
|
// 已有sessionId,直接使用(保持上下文)
|
||||||
conversationId.value = existingSessionId;
|
conversationId.value = existingSessionId;
|
||||||
console.log('使用已有sessionId:', existingSessionId, 'storageKey:', storageKey);
|
|
||||||
} else {
|
} else {
|
||||||
// 生成新的sessionId
|
// 生成新的sessionId
|
||||||
const userId = userStore.userInfo?.openid || userStore.userInfo?.userId || 'guest';
|
const userId = userStore.userInfo?.openid || userStore.userInfo?.userId || 'guest';
|
||||||
@@ -529,24 +685,19 @@ const createNewConversation = (characterId) => {
|
|||||||
// 存储到本地
|
// 存储到本地
|
||||||
uni.setStorageSync(storageKey, newSessionId);
|
uni.setStorageSync(storageKey, newSessionId);
|
||||||
conversationId.value = newSessionId;
|
conversationId.value = newSessionId;
|
||||||
console.log('创建新sessionId:', newSessionId, 'storageKey:', storageKey);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载历史消息
|
// 加载历史消息
|
||||||
const loadHistoryMessages = async () => {
|
const loadHistoryMessages = async () => {
|
||||||
if (!conversationId.value || !isLoggedIn.value) {
|
if (!conversationId.value || !isLoggedIn.value) {
|
||||||
console.log('没有sessionId或用户未登录,跳过加载历史消息');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('开始加载历史消息,sessionId:', conversationId.value);
|
|
||||||
const result = await chatAPI.getHistoryMessages(conversationId.value);
|
const result = await chatAPI.getHistoryMessages(conversationId.value);
|
||||||
|
|
||||||
if (result.success && result.data && result.data.length > 0) {
|
if (result.success && result.data && result.data.length > 0) {
|
||||||
console.log('获取到历史消息:', result.data.length, '条');
|
|
||||||
|
|
||||||
// 将后端消息格式转换为前端格式,保持与API回复一致的"清理后回复"逻辑
|
// 将后端消息格式转换为前端格式,保持与API回复一致的"清理后回复"逻辑
|
||||||
const historyMessages = [];
|
const historyMessages = [];
|
||||||
|
|
||||||
@@ -600,16 +751,14 @@ const loadHistoryMessages = async () => {
|
|||||||
|
|
||||||
// 清空当前消息列表,加载历史消息
|
// 清空当前消息列表,加载历史消息
|
||||||
messages.value = historyMessages;
|
messages.value = historyMessages;
|
||||||
console.log('历史消息加载完成,共', messages.value.length, '条');
|
// 滚动到底部 - 使用延迟确保 DOM 完全渲染
|
||||||
|
|
||||||
// 滚动到底部
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
scrollToBottom();
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
console.log('没有历史消息或获取失败');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载历史消息失败:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -660,7 +809,6 @@ const getAIResponse = async (userMessage) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI API调用异常:', error);
|
|
||||||
const fallbackResponse = getSmartResponse(currentCharacter.value, userMessage);
|
const fallbackResponse = getSmartResponse(currentCharacter.value, userMessage);
|
||||||
const cleanedResponse = cleanText(fallbackResponse);
|
const cleanedResponse = cleanText(fallbackResponse);
|
||||||
await addSegmentedAIResponse(cleanedResponse);
|
await addSegmentedAIResponse(cleanedResponse);
|
||||||
@@ -844,7 +992,6 @@ const handleVoiceMessage = async (filePath) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理语音消息失败:', error);
|
|
||||||
const fallbackResponse = getSmartResponse(currentCharacter.value, '[语音消息]');
|
const fallbackResponse = getSmartResponse(currentCharacter.value, '[语音消息]');
|
||||||
const cleanedResponse = cleanText(fallbackResponse);
|
const cleanedResponse = cleanText(fallbackResponse);
|
||||||
await addSegmentedAIResponse(cleanedResponse);
|
await addSegmentedAIResponse(cleanedResponse);
|
||||||
@@ -895,7 +1042,6 @@ const playVoiceResponse = async (text, audioUrl = null) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
audioContext.value.onError((err) => {
|
audioContext.value.onError((err) => {
|
||||||
console.error('语音播放错误:', err);
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '语音播放失败',
|
title: '语音播放失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -904,7 +1050,6 @@ const playVoiceResponse = async (text, audioUrl = null) => {
|
|||||||
|
|
||||||
audioContext.value.play();
|
audioContext.value.play();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('语音播放异常:', error);
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '语音播放异常',
|
title: '语音播放异常',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -945,7 +1090,6 @@ const selectTemplate = (template) => {
|
|||||||
|
|
||||||
// 处理输入框聚焦
|
// 处理输入框聚焦
|
||||||
const handleInputFocus = (e) => {
|
const handleInputFocus = (e) => {
|
||||||
console.log('输入框获得焦点', e);
|
|
||||||
// 在小程序中,通过 detail.height 获取键盘高度
|
// 在小程序中,通过 detail.height 获取键盘高度
|
||||||
if (e && e.detail && e.detail.height) {
|
if (e && e.detail && e.detail.height) {
|
||||||
keyboardHeight.value = e.detail.height;
|
keyboardHeight.value = e.detail.height;
|
||||||
@@ -954,18 +1098,12 @@ const handleInputFocus = (e) => {
|
|||||||
|
|
||||||
// 处理输入框失焦
|
// 处理输入框失焦
|
||||||
const handleInputBlur = (e) => {
|
const handleInputBlur = (e) => {
|
||||||
console.log('输入框失去焦点', e);
|
|
||||||
// 延迟重置键盘高度,避免闪烁
|
// 延迟重置键盘高度,避免闪烁
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
keyboardHeight.value = 0;
|
keyboardHeight.value = 0;
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 返回
|
|
||||||
const goBack = () => {
|
|
||||||
uni.navigateBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清空对话上下文
|
// 清空对话上下文
|
||||||
const clearContext = async () => {
|
const clearContext = async () => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
@@ -982,9 +1120,7 @@ const clearContext = async () => {
|
|||||||
if (conversationId.value && isLoggedIn.value) {
|
if (conversationId.value && isLoggedIn.value) {
|
||||||
try {
|
try {
|
||||||
const result = await chatAPI.clearSession(conversationId.value);
|
const result = await chatAPI.clearSession(conversationId.value);
|
||||||
console.log('后端清除会话结果:', result);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('后端清除失败,继续本地清除:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,8 +1132,6 @@ const clearContext = async () => {
|
|||||||
storageKey = `session_role_${currentCharacter.value.roleId}`;
|
storageKey = `session_role_${currentCharacter.value.roleId}`;
|
||||||
}
|
}
|
||||||
uni.removeStorageSync(storageKey);
|
uni.removeStorageSync(storageKey);
|
||||||
console.log('删除本地sessionId:', storageKey);
|
|
||||||
|
|
||||||
// 3. 生成新的sessionId
|
// 3. 生成新的sessionId
|
||||||
const characterIdForSession = currentCharacter.value.id || currentCharacter.value.roleId || 'default';
|
const characterIdForSession = currentCharacter.value.id || currentCharacter.value.roleId || 'default';
|
||||||
createNewConversation(characterIdForSession);
|
createNewConversation(characterIdForSession);
|
||||||
@@ -1006,13 +1140,18 @@ const clearContext = async () => {
|
|||||||
messages.value = [];
|
messages.value = [];
|
||||||
addMessage('ai', currentCharacter.value.greeting || '你好!很高兴再次见到你!');
|
addMessage('ai', currentCharacter.value.greeting || '你好!很高兴再次见到你!');
|
||||||
|
|
||||||
|
// 确保滚动到底部
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '对话已清空',
|
title: '对话已清空',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空对话失败:', error);
|
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '清空失败',
|
title: '清空失败',
|
||||||
@@ -1047,8 +1186,6 @@ page {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 200rpx;
|
|
||||||
transition: padding-bottom 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 夜空装饰 */
|
/* 夜空装饰 */
|
||||||
@@ -1120,16 +1257,6 @@ page {
|
|||||||
text-shadow: 0 0 10rpx rgba(249, 224, 118, 0.3);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-center {
|
.navbar-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1195,7 +1322,6 @@ page {
|
|||||||
|
|
||||||
/* 聊天消息区域 */
|
/* 聊天消息区域 */
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
height: calc(100vh - 200rpx);
|
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -1204,6 +1330,7 @@ page {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24rpx;
|
gap: 24rpx;
|
||||||
|
padding-bottom: 120rpx; /* 增加底部padding,确保最后一条消息不被输入框遮挡 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-wrapper {
|
.message-wrapper {
|
||||||
@@ -1381,10 +1508,10 @@ page {
|
|||||||
backdrop-filter: blur(20rpx);
|
backdrop-filter: blur(20rpx);
|
||||||
border-top: 1rpx solid rgba(249, 224, 118, 0.1);
|
border-top: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||||
padding: 16rpx 24rpx;
|
padding: 16rpx 24rpx;
|
||||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
/* padding-bottom 通过动态样式绑定控制 */
|
||||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.3);
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.3);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
transition: bottom 0.3s ease;
|
transition: bottom 0.3s ease, padding-bottom 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
|
|||||||
@@ -103,10 +103,10 @@
|
|||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "rgba(255, 255, 255, 0.5)",
|
"color": "#999999",
|
||||||
"selectedColor": "#f9e076",
|
"selectedColor": "#f9e076",
|
||||||
"backgroundColor": "rgba(26, 11, 46, 0.95)",
|
"backgroundColor": "#1a0b2e",
|
||||||
"borderStyle": "white",
|
"borderStyle": "black",
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"pagePath": "pages/drama/index",
|
"pagePath": "pages/drama/index",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<ChatBox
|
<ChatBox
|
||||||
:character-config="characterConfig"
|
:character-config="characterConfig"
|
||||||
:ai-config="aiConfig"
|
:ai-config="aiConfig"
|
||||||
:ui-config="uiConfig"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,11 +19,6 @@ const aiConfig = ref({
|
|||||||
templateId: 9999
|
templateId: 9999
|
||||||
});
|
});
|
||||||
|
|
||||||
// UI 配置
|
|
||||||
const uiConfig = ref({
|
|
||||||
showBackButton: false // 默认不显示返回按钮
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化:解析 URL 参数
|
// 初始化:解析 URL 参数
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
@@ -32,18 +26,13 @@ onMounted(() => {
|
|||||||
const currentPage = pages[pages.length - 1];
|
const currentPage = pages[pages.length - 1];
|
||||||
const options = currentPage.options || {};
|
const options = currentPage.options || {};
|
||||||
|
|
||||||
// 判断是否需要显示返回按钮
|
|
||||||
if (options.characterId || options.roleId) {
|
|
||||||
uiConfig.value.showBackButton = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 场景1: 蔚AI(characterId === 'wei-ai' 或无参数)
|
// 场景1: 蔚AI(characterId === 'wei-ai' 或无参数)
|
||||||
if (options.characterId === 'wei-ai' || !options.characterId) {
|
if (options.characterId === 'wei-ai' || !options.characterId) {
|
||||||
characterConfig.value = {
|
characterConfig.value = {
|
||||||
id: 'wei-ai',
|
id: 'wei-ai',
|
||||||
roleId: null,
|
roleId: null,
|
||||||
name: options.characterName || '蔚AI',
|
name: options.characterName || '蔚AI',
|
||||||
avatar: options.characterAvatar || '/file/avatar/2025/11/09/ccfc630120114984b9f2d6e4990f5cd8.jpg',
|
avatar: options.characterAvatar || '/file/background/2025/11/11/97683faa11074a4bbe7619c246754c1a.jpg',
|
||||||
greeting: options.introMessage ? decodeURIComponent(options.introMessage) : '你好!我是你的虚拟女友!',
|
greeting: options.introMessage ? decodeURIComponent(options.introMessage) : '你好!我是你的虚拟女友!',
|
||||||
roleDesc: ''
|
roleDesc: ''
|
||||||
};
|
};
|
||||||
@@ -127,8 +116,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
console.log('chat-box 页面初始化完成:', {
|
console.log('chat-box 页面初始化完成:', {
|
||||||
characterConfig: characterConfig.value,
|
characterConfig: characterConfig.value,
|
||||||
aiConfig: aiConfig.value,
|
aiConfig: aiConfig.value
|
||||||
uiConfig: uiConfig.value
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,8 +23,19 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 设备信息卡片 -->
|
<!-- 可滚动内容区域 -->
|
||||||
<view class="device-card" :style="{ marginTop: navBarHeight + 14 + 'px' }">
|
<scroll-view
|
||||||
|
class="scroll-container"
|
||||||
|
:style="{ marginTop: navBarHeight + 'px' }"
|
||||||
|
scroll-y="true"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refresherTriggered"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
refresher-background="rgba(26, 11, 46, 0.5)"
|
||||||
|
>
|
||||||
|
<!-- 设备信息卡片 -->
|
||||||
|
<view class="device-card" style="margin-top: 14px;">
|
||||||
<view class="device-header">
|
<view class="device-header">
|
||||||
<view class="device-icon" :class="{'connected': deviceConnected}">
|
<view class="device-icon" :class="{'connected': deviceConnected}">
|
||||||
{{ deviceConnected ? '🌟' : '⭕' }}
|
{{ deviceConnected ? '🌟' : '⭕' }}
|
||||||
@@ -103,6 +114,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部留白 -->
|
||||||
|
<view class="bottom-spacing"></view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -122,6 +137,9 @@ const connectTime = ref('');
|
|||||||
const statusBarHeight = ref(0);
|
const statusBarHeight = ref(0);
|
||||||
const navBarHeight = ref(0);
|
const navBarHeight = ref(0);
|
||||||
|
|
||||||
|
// 下拉刷新相关
|
||||||
|
const refresherTriggered = ref(false);
|
||||||
|
|
||||||
// 获取系统状态栏高度
|
// 获取系统状态栏高度
|
||||||
const getSystemInfo = () => {
|
const getSystemInfo = () => {
|
||||||
const systemInfo = uni.getSystemInfoSync();
|
const systemInfo = uni.getSystemInfoSync();
|
||||||
@@ -139,7 +157,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 刷新设备状态
|
// 刷新设备状态
|
||||||
const refreshDeviceStatus = () => {
|
const refreshDeviceStatus = (isRefresh = false) => {
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请先登录',
|
title: '请先登录',
|
||||||
@@ -149,10 +167,12 @@ const refreshDeviceStatus = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 暂时注释掉API请求,使用本地模拟数据
|
// 暂时注释掉API请求,使用本地模拟数据
|
||||||
uni.showToast({
|
if (!isRefresh) {
|
||||||
title: '刷新成功(本地模拟)',
|
uni.showToast({
|
||||||
icon: 'success'
|
title: '刷新成功(本地模拟)',
|
||||||
});
|
icon: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* API请求已注释
|
/* API请求已注释
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
@@ -283,15 +303,42 @@ const goToLogin = () => {
|
|||||||
url: '/pages/mine/mine'
|
url: '/pages/mine/mine'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 下拉刷新处理
|
||||||
|
const onRefresh = async () => {
|
||||||
|
console.log('触发下拉刷新');
|
||||||
|
refresherTriggered.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 刷新设备状态
|
||||||
|
refreshDeviceStatus(true);
|
||||||
|
|
||||||
|
// 延迟显示刷新成功提示
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
} finally {
|
||||||
|
// 刷新完成后,关闭刷新状态
|
||||||
|
setTimeout(() => {
|
||||||
|
refresherTriggered.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.floral-container {
|
.floral-container {
|
||||||
|
width: 100vw;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
|
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
|
||||||
padding-bottom: 100rpx;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 夜空装饰 */
|
/* 夜空装饰 */
|
||||||
@@ -392,16 +439,24 @@ const goToLogin = () => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 滚动容器 */
|
||||||
|
.scroll-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* 设备卡片 */
|
/* 设备卡片 */
|
||||||
.device-card {
|
.device-card {
|
||||||
margin: 0 30rpx 40rpx 30rpx;
|
margin: 0 30rpx 20rpx 30rpx;
|
||||||
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
|
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
|
||||||
border-radius: 30rpx;
|
border-radius: 30rpx;
|
||||||
padding: 40rpx;
|
padding: 40rpx;
|
||||||
border: 2rpx solid rgba(249, 224, 118, 0.2);
|
border: 2rpx solid rgba(249, 224, 118, 0.2);
|
||||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
|
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header {
|
.device-header {
|
||||||
@@ -556,9 +611,8 @@ const goToLogin = () => {
|
|||||||
|
|
||||||
/* 功能列表 */
|
/* 功能列表 */
|
||||||
.feature-list {
|
.feature-list {
|
||||||
margin: 40rpx 30rpx;
|
margin: 20rpx 30rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-title {
|
.feature-title {
|
||||||
@@ -602,4 +656,9 @@ const goToLogin = () => {
|
|||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(249, 224, 118, 0.6);
|
color: rgba(249, 224, 118, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 底部留白 */
|
||||||
|
.bottom-spacing {
|
||||||
|
height: 100rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,11 +24,13 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 可滚动的两列卡片区 -->
|
<!-- 可滚动的两列卡片区 -->
|
||||||
|
<!-- 下拉刷新已禁用 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="scroll-container"
|
class="scroll-container"
|
||||||
:style="{ marginTop: navBarHeight + 'px' }"
|
:style="{ marginTop: navBarHeight + 'px' }"
|
||||||
scroll-y="true"
|
scroll-y="true"
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
|
:refresher-enabled="false"
|
||||||
>
|
>
|
||||||
<view class="two-column-grid">
|
<view class="two-column-grid">
|
||||||
<view class="column column-left">
|
<view class="column column-left">
|
||||||
@@ -36,13 +38,30 @@
|
|||||||
<view class="floral-grid-card" v-if="item">
|
<view class="floral-grid-card" v-if="item">
|
||||||
<view class="cover-container">
|
<view class="cover-container">
|
||||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||||
|
<!-- 🔴 新增:未读消息红点 -->
|
||||||
|
<view class="unread-badge" v-if="unreadCounts[item.roleId] > 0">
|
||||||
|
{{ unreadCounts[item.roleId] }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="floral-tag">{{ item.tag }}</view>
|
<view class="floral-tag">{{ item.tag }}</view>
|
||||||
<view class="content-area">
|
<view class="content-area">
|
||||||
<view class="title">{{ item.title }}</view>
|
<view class="title">{{ item.title }}</view>
|
||||||
<view class="card-bottom">
|
<view class="card-bottom">
|
||||||
<button v-if="userStore.isLoggedIn" class="floral-btn use-btn" @click="showDetail(item)">🔍 查看详情</button>
|
<button
|
||||||
<button v-else class="floral-btn outline use-btn login-required" @click="showLoginTip">🔐 查看详情</button>
|
v-if="userStore.isLoggedIn"
|
||||||
|
class="floral-btn use-btn"
|
||||||
|
@click="showDetail(item)"
|
||||||
|
@longpress="() => handleLongPress(item)"
|
||||||
|
>
|
||||||
|
🔍 查看详情
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="floral-btn outline use-btn login-required"
|
||||||
|
@click="showLoginTip"
|
||||||
|
>
|
||||||
|
🔐 查看详情
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -54,13 +73,30 @@
|
|||||||
<view class="floral-grid-card" v-if="item">
|
<view class="floral-grid-card" v-if="item">
|
||||||
<view class="cover-container">
|
<view class="cover-container">
|
||||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||||
|
<!-- 🔴 新增:未读消息红点 -->
|
||||||
|
<view class="unread-badge" v-if="unreadCounts[item.roleId] > 0">
|
||||||
|
{{ unreadCounts[item.roleId] }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="floral-tag">{{ item.tag }}</view>
|
<view class="floral-tag">{{ item.tag }}</view>
|
||||||
<view class="content-area">
|
<view class="content-area">
|
||||||
<view class="title">{{ item.title }}</view>
|
<view class="title">{{ item.title }}</view>
|
||||||
<view class="card-bottom">
|
<view class="card-bottom">
|
||||||
<button v-if="userStore.isLoggedIn" class="floral-btn use-btn" @click="showDetail(item)">🔍 查看详情</button>
|
<button
|
||||||
<button v-else class="floral-btn outline use-btn login-required" @click="showLoginTip">🔐 查看详情</button>
|
v-if="userStore.isLoggedIn"
|
||||||
|
class="floral-btn use-btn"
|
||||||
|
@click="showDetail(item)"
|
||||||
|
@longpress="() => handleLongPress(item)"
|
||||||
|
>
|
||||||
|
🔍 查看详情
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="floral-btn outline use-btn login-required"
|
||||||
|
@click="showLoginTip"
|
||||||
|
>
|
||||||
|
🔐 查看详情
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -117,6 +153,13 @@
|
|||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
import { useUserStore } from '@/stores/user.js';
|
||||||
import { roleAPI, getResourceUrl } from '@/utils/api.js';
|
import { roleAPI, getResourceUrl } from '@/utils/api.js';
|
||||||
|
// 🔴 新增:导入未读消息管理模块
|
||||||
|
import {
|
||||||
|
getAllUnreadCounts,
|
||||||
|
generateTestMessages,
|
||||||
|
clearUnreadMessages,
|
||||||
|
autoGenerateMessagesForRandomRoles
|
||||||
|
} from '@/utils/unreadMessages.js';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const showLoginModal = ref(false);
|
const showLoginModal = ref(false);
|
||||||
@@ -125,10 +168,19 @@ const showLoginModal = ref(false);
|
|||||||
const showDetailModal = ref(false);
|
const showDetailModal = ref(false);
|
||||||
const selectedItem = ref(null);
|
const selectedItem = ref(null);
|
||||||
|
|
||||||
|
// 下拉刷新相关
|
||||||
|
const refresherTriggered = ref(false);
|
||||||
|
|
||||||
// 状态栏高度适配
|
// 状态栏高度适配
|
||||||
const statusBarHeight = ref(0);
|
const statusBarHeight = ref(0);
|
||||||
const navBarHeight = ref(0);
|
const navBarHeight = ref(0);
|
||||||
|
|
||||||
|
// 🔴 新增:未读消息数量统计
|
||||||
|
const unreadCounts = ref({});
|
||||||
|
|
||||||
|
// 🔴 新增:是否首次加载标记
|
||||||
|
const isFirstLoad = ref(true);
|
||||||
|
|
||||||
// 获取系统状态栏高度
|
// 获取系统状态栏高度
|
||||||
const getSystemInfo = () => {
|
const getSystemInfo = () => {
|
||||||
const systemInfo = uni.getSystemInfoSync();
|
const systemInfo = uni.getSystemInfoSync();
|
||||||
@@ -320,11 +372,20 @@ const dramaList = ref([
|
|||||||
const leftColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 0));
|
const leftColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 0));
|
||||||
const rightColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 1));
|
const rightColumnItems = computed(() => dramaList.value.filter((_, index) => index % 2 === 1));
|
||||||
|
|
||||||
|
// 🔴 新增:加载未读消息数量
|
||||||
|
const loadUnreadCounts = () => {
|
||||||
|
unreadCounts.value = getAllUnreadCounts();
|
||||||
|
console.log('📬 未读消息统计:', unreadCounts.value);
|
||||||
|
};
|
||||||
|
|
||||||
// 加载角色列表数据
|
// 加载角色列表数据
|
||||||
const loadDramaList = async () => {
|
const loadDramaList = async (isRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
uni.showLoading({ title: '加载中...' });
|
// 如果不是下拉刷新,则显示loading提示
|
||||||
console.log('开始加载角色列表...');
|
if (!isRefresh) {
|
||||||
|
uni.showLoading({ title: '加载中...' });
|
||||||
|
}
|
||||||
|
console.log('开始加载角色列表...', isRefresh ? '(下拉刷新)' : '');
|
||||||
|
|
||||||
const result = await roleAPI.getRoles();
|
const result = await roleAPI.getRoles();
|
||||||
console.log('API返回结果:', result);
|
console.log('API返回结果:', result);
|
||||||
@@ -379,32 +440,93 @@ const loadDramaList = async () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('角色列表加载成功,共', dramaList.value.length, '个角色');
|
console.log('角色列表加载成功,共', dramaList.value.length, '个角色');
|
||||||
|
|
||||||
|
// 如果是下拉刷新,显示成功提示
|
||||||
|
if (isRefresh) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('获取角色列表失败:', result.error);
|
console.error('获取角色列表失败:', result.error);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '加载失败,请重试',
|
title: isRefresh ? '刷新失败,请重试' : '加载失败,请重试',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载角色列表异常:', error);
|
console.error('加载角色列表异常:', error);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '加载异常,请重试',
|
title: isRefresh ? '刷新异常,请重试' : '加载异常,请重试',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading();
|
if (!isRefresh) {
|
||||||
|
uni.hideLoading();
|
||||||
|
}
|
||||||
|
// 🔴 新增:加载完角色后刷新未读消息数量
|
||||||
|
loadUnreadCounts();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 下拉刷新处理
|
||||||
|
const onRefresh = async () => {
|
||||||
|
console.log('触发下拉刷新');
|
||||||
|
refresherTriggered.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadDramaList(true);
|
||||||
|
} finally {
|
||||||
|
// 刷新完成后,关闭刷新状态
|
||||||
|
setTimeout(() => {
|
||||||
|
refresherTriggered.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔴 新增:自动生成随机角色消息
|
||||||
|
const autoGenerateRandomMessages = () => {
|
||||||
|
console.log('🎲 准备自动生成消息,当前角色数量:', dramaList.value.length);
|
||||||
|
|
||||||
|
if (dramaList.value.length === 0) {
|
||||||
|
console.log('⚠️ 角色列表为空,无法生成消息');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选择2个角色生成消息
|
||||||
|
const count = autoGenerateMessagesForRandomRoles(dramaList.value, 2, 1);
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
console.log(`✨ 已为 ${count} 个随机角色生成主动消息`);
|
||||||
|
// 刷新未读消息数量
|
||||||
|
loadUnreadCounts();
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 未能生成消息');
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getSystemInfo(); // 获取系统信息
|
getSystemInfo(); // 获取系统信息
|
||||||
userStore.init();
|
userStore.init();
|
||||||
|
|
||||||
// 调用 API 加载角色列表
|
// 调用 API 加载角色列表
|
||||||
loadDramaList();
|
loadDramaList();
|
||||||
|
console.log('📱 进入角色列表页面');
|
||||||
|
|
||||||
|
// 刷新未读消息数量
|
||||||
|
loadUnreadCounts();
|
||||||
|
|
||||||
|
// 延迟生成消息(等待角色列表加载完成)
|
||||||
|
setTimeout(() => {
|
||||||
|
autoGenerateRandomMessages();
|
||||||
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const handleUse = (item) => {
|
const handleUse = (item) => {
|
||||||
if (!item || !item.id) {
|
if (!item || !item.id) {
|
||||||
@@ -437,6 +559,31 @@ const handleUse = (item) => {
|
|||||||
uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
|
uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔴 新增:长按角色卡生成测试消息(用于开发测试)
|
||||||
|
const handleLongPress = (item) => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['为该角色生成1条消息', '为该角色生成3条消息'],
|
||||||
|
success: (res) => {
|
||||||
|
const messageCount = res.tapIndex === 0 ? 1 : 3;
|
||||||
|
|
||||||
|
const success = generateTestMessages(item.roleId, item.roleName, messageCount);
|
||||||
|
if (success) {
|
||||||
|
loadUnreadCounts(); // 刷新未读消息数量
|
||||||
|
uni.showToast({
|
||||||
|
title: `✅ 已为"${item.roleName}"生成${messageCount}条消息`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '❌ 生成失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const showLoginTip = () => { showLoginModal.value = true; };
|
const showLoginTip = () => { showLoginModal.value = true; };
|
||||||
const showDetail = (item) => { selectedItem.value = item; showDetailModal.value = true; };
|
const showDetail = (item) => { selectedItem.value = item; showDetailModal.value = true; };
|
||||||
const closeDetail = () => { showDetailModal.value = false; selectedItem.value = null; };
|
const closeDetail = () => { showDetailModal.value = false; selectedItem.value = null; };
|
||||||
@@ -830,4 +977,36 @@ const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/p
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16rpx 32rpx;
|
padding: 16rpx 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔴 新增:未读消息红点样式 */
|
||||||
|
.unread-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
min-width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.6);
|
||||||
|
z-index: 10;
|
||||||
|
animation: badge-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.6);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(255, 77, 79, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
:style="{ marginTop: navBarHeight + 'px' }"
|
:style="{ marginTop: navBarHeight + 'px' }"
|
||||||
scroll-y="true"
|
scroll-y="true"
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refresherTriggered"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
refresher-background="rgba(26, 11, 46, 0.5)"
|
||||||
>
|
>
|
||||||
<!-- 用户信息区域 -->
|
<!-- 用户信息区域 -->
|
||||||
<view class="user-info">
|
<view class="user-info">
|
||||||
@@ -132,6 +136,9 @@ const userBalance = ref(0.00);
|
|||||||
const statusBarHeight = ref(0);
|
const statusBarHeight = ref(0);
|
||||||
const navBarHeight = ref(0);
|
const navBarHeight = ref(0);
|
||||||
|
|
||||||
|
// 下拉刷新相关
|
||||||
|
const refresherTriggered = ref(false);
|
||||||
|
|
||||||
// 登录按钮文本
|
// 登录按钮文本
|
||||||
const loginButtonText = computed(() => {
|
const loginButtonText = computed(() => {
|
||||||
return '微信一键登录';
|
return '微信一键登录';
|
||||||
@@ -476,17 +483,39 @@ Token: ${currentToken || '无'}
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 加载用户余额
|
// 加载用户余额
|
||||||
const loadUserBalance = async () => {
|
const loadUserBalance = async (isRefresh = false) => {
|
||||||
if (!isLoggedIn.value) return;
|
if (!isLoggedIn.value) return;
|
||||||
|
|
||||||
|
// 暂时注释掉API请求,使用本地模拟数据
|
||||||
|
console.log('loadUserBalance called, 使用模拟数据');
|
||||||
|
userBalance.value = 0.00; // 默认余额
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
console.log('余额刷新成功(本地模拟)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* API请求已注释
|
||||||
try {
|
try {
|
||||||
const result = await rechargeAPI.getUserBalance();
|
const result = await rechargeAPI.getUserBalance();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
userBalance.value = result.data.balance || 0;
|
userBalance.value = result.data.balance || 0;
|
||||||
|
|
||||||
|
// 如果是下拉刷新,显示成功提示
|
||||||
|
if (isRefresh) {
|
||||||
|
console.log('余额刷新成功');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户余额失败:', error);
|
console.error('获取用户余额失败:', error);
|
||||||
|
if (isRefresh) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '余额刷新失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到充值页面
|
// 跳转到充值页面
|
||||||
@@ -502,6 +531,39 @@ const goToHistory = () => {
|
|||||||
url: '/pages/recharge/history'
|
url: '/pages/recharge/history'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 下拉刷新处理
|
||||||
|
const onRefresh = async () => {
|
||||||
|
console.log('触发下拉刷新');
|
||||||
|
refresherTriggered.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 刷新用户信息
|
||||||
|
initUserInfo();
|
||||||
|
|
||||||
|
// 刷新用户余额
|
||||||
|
await loadUserBalance(true);
|
||||||
|
|
||||||
|
// 显示刷新成功提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// 刷新完成后,关闭刷新状态
|
||||||
|
setTimeout(() => {
|
||||||
|
refresherTriggered.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<ChatBox
|
<ChatBox
|
||||||
:character-config="characterConfig"
|
:character-config="characterConfig"
|
||||||
:ai-config="aiConfig"
|
:ai-config="aiConfig"
|
||||||
:ui-config="uiConfig"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -19,11 +18,6 @@ const aiConfig = ref({
|
|||||||
templateId: 6
|
templateId: 6
|
||||||
});
|
});
|
||||||
|
|
||||||
// UI 配置
|
|
||||||
const uiConfig = ref({
|
|
||||||
showBackButton: true // 角色聊天页面显示返回按钮
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化:解析 URL 参数
|
// 初始化:解析 URL 参数
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages();
|
const pages = getCurrentPages();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { API_CONFIG, getApiUrl, getWebUrl } from '@/utils/config.js';
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -101,7 +102,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
function wxLogin(code, userInfo) {
|
function wxLogin(code, userInfo) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.request({
|
uni.request({
|
||||||
url: 'http://192.168.1.2:8091/app/login',
|
url: getApiUrl(API_CONFIG.ENDPOINTS.LOGIN),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
code
|
code
|
||||||
@@ -122,7 +123,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
console.error('登录请求失败:', err);
|
console.error('登录请求失败:', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
},
|
},
|
||||||
timeout: 10000 // 增加超时时间到10秒
|
timeout: API_CONFIG.LOGIN_TIMEOUT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
|
|
||||||
// 尝试调用登出接口
|
// 尝试调用登出接口
|
||||||
uni.request({
|
uni.request({
|
||||||
url: 'https://www.aixsy.com.cn/app/logout',
|
url: getWebUrl(API_CONFIG.ENDPOINTS.LOGOUT),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// API服务文件
|
// API服务文件
|
||||||
import { useUserStore } from '@/stores/user.js';
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
import { API_CONFIG, getApiUrl } from '@/utils/config.js';
|
||||||
|
|
||||||
// 图片URL处理函数 - 处理小程序中图片路径问题
|
// 图片URL处理函数 - 处理小程序中图片路径问题
|
||||||
export const getResourceUrl = (url) => {
|
export const getResourceUrl = (url) => {
|
||||||
@@ -14,12 +15,12 @@ export const getResourceUrl = (url) => {
|
|||||||
|
|
||||||
// 如果是相对路径,拼接完整的服务器地址
|
// 如果是相对路径,拼接完整的服务器地址
|
||||||
if (url.startsWith('/file/')) {
|
if (url.startsWith('/file/')) {
|
||||||
return BASE_URL + url;
|
return API_CONFIG.BASE_URL + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是其他相对路径,也拼接服务器地址
|
// 如果是其他相对路径,也拼接服务器地址
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
return BASE_URL + url;
|
return API_CONFIG.BASE_URL + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认返回原路径
|
// 默认返回原路径
|
||||||
@@ -73,9 +74,6 @@ export const cleanText = (text) => {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 基础配置
|
|
||||||
const BASE_URL = 'http://192.168.1.2:8091'; // 根据后端地址调整
|
|
||||||
|
|
||||||
// 检查用户登录状态
|
// 检查用户登录状态
|
||||||
const checkLoginStatus = () => {
|
const checkLoginStatus = () => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -111,7 +109,7 @@ const request = (options) => {
|
|||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
url: BASE_URL + options.url,
|
url: getApiUrl(options.url),
|
||||||
method: options.method || 'GET',
|
method: options.method || 'GET',
|
||||||
header: {
|
header: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -119,7 +117,7 @@ const request = (options) => {
|
|||||||
...options.header
|
...options.header
|
||||||
},
|
},
|
||||||
data: options.data || {},
|
data: options.data || {},
|
||||||
timeout: options.timeout || 30000
|
timeout: options.timeout || API_CONFIG.TIMEOUT
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
@@ -181,7 +179,7 @@ export const chatAPI = {
|
|||||||
console.log('发送AI聊天请求,参数:', requestData);
|
console.log('发送AI聊天请求,参数:', requestData);
|
||||||
|
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/chat/sync',
|
url: API_CONFIG.ENDPOINTS.CHAT_SYNC,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: requestData
|
data: requestData
|
||||||
});
|
});
|
||||||
@@ -288,7 +286,7 @@ export const chatAPI = {
|
|||||||
asyncChat: async (params) => {
|
asyncChat: async (params) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/chat/async',
|
url: API_CONFIG.ENDPOINTS.CHAT_ASYNC,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
message: params.message,
|
message: params.message,
|
||||||
@@ -315,7 +313,7 @@ export const chatAPI = {
|
|||||||
getChatHistory: async (conversationId) => {
|
getChatHistory: async (conversationId) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: `/api/chat/history/${conversationId}`,
|
url: `${API_CONFIG.ENDPOINTS.CHAT_HISTORY}/${conversationId}`,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,7 +334,7 @@ export const chatAPI = {
|
|||||||
createConversation: async (characterId) => {
|
createConversation: async (characterId) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/chat/conversation',
|
url: API_CONFIG.ENDPOINTS.CHAT_CONVERSATION,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
characterId: characterId
|
characterId: characterId
|
||||||
@@ -360,7 +358,7 @@ export const chatAPI = {
|
|||||||
clearSession: async (sessionId) => {
|
clearSession: async (sessionId) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: `/api/chat/session/${sessionId}`,
|
url: `${API_CONFIG.ENDPOINTS.CHAT_SESSION}/${sessionId}`,
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -393,7 +391,7 @@ export const chatAPI = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/message/history',
|
url: API_CONFIG.ENDPOINTS.MESSAGE_HISTORY,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: {
|
data: {
|
||||||
sessionId: sessionId
|
sessionId: sessionId
|
||||||
@@ -451,7 +449,7 @@ export const voiceAPI = {
|
|||||||
console.log('开始文本转语音:', text);
|
console.log('开始文本转语音:', text);
|
||||||
|
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/chat/tts',
|
url: API_CONFIG.ENDPOINTS.TTS,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
text: text,
|
text: text,
|
||||||
@@ -506,7 +504,7 @@ export const voiceAPI = {
|
|||||||
console.log('开始对话+语音合成:', message);
|
console.log('开始对话+语音合成:', message);
|
||||||
|
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/chat/answer-tts',
|
url: API_CONFIG.ENDPOINTS.ANSWER_TTS,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
message: message,
|
message: message,
|
||||||
@@ -569,7 +567,7 @@ export const voiceAPI = {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
uni.uploadFile({
|
uni.uploadFile({
|
||||||
url: BASE_URL + '/api/chat/voice-chat',
|
url: getApiUrl(API_CONFIG.ENDPOINTS.VOICE_CHAT),
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
name: 'audio',
|
name: 'audio',
|
||||||
header: authHeader ? {
|
header: authHeader ? {
|
||||||
@@ -695,7 +693,7 @@ export const voiceAPI = {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
uni.uploadFile({
|
uni.uploadFile({
|
||||||
url: BASE_URL + '/api/chat/upload-voice-chat',
|
url: getApiUrl(API_CONFIG.ENDPOINTS.UPLOAD_VOICE_CHAT),
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
name: 'audio',
|
name: 'audio',
|
||||||
header: authHeader ? {
|
header: authHeader ? {
|
||||||
@@ -817,7 +815,7 @@ export const rechargeAPI = {
|
|||||||
getUserBalance: async () => {
|
getUserBalance: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/recharge/balance',
|
url: API_CONFIG.ENDPOINTS.RECHARGE_BALANCE,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -838,7 +836,7 @@ export const rechargeAPI = {
|
|||||||
createRechargeOrder: async (orderData) => {
|
createRechargeOrder: async (orderData) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/recharge/create-order',
|
url: API_CONFIG.ENDPOINTS.RECHARGE_CREATE_ORDER,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
amount: orderData.amount,
|
amount: orderData.amount,
|
||||||
@@ -864,7 +862,7 @@ export const rechargeAPI = {
|
|||||||
getOrderStatus: async (orderId) => {
|
getOrderStatus: async (orderId) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: `/api/recharge/order-status/${orderId}`,
|
url: `${API_CONFIG.ENDPOINTS.RECHARGE_ORDER_STATUS}/${orderId}`,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -885,7 +883,7 @@ export const rechargeAPI = {
|
|||||||
getRechargeHistory: async (params = {}) => {
|
getRechargeHistory: async (params = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/api/recharge/history',
|
url: API_CONFIG.ENDPOINTS.RECHARGE_HISTORY,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: {
|
data: {
|
||||||
page: params.page || 1,
|
page: params.page || 1,
|
||||||
@@ -915,7 +913,7 @@ export const roleAPI = {
|
|||||||
getRoles: async () => {
|
getRoles: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/role/query',
|
url: API_CONFIG.ENDPOINTS.ROLE_QUERY,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -936,7 +934,7 @@ export const roleAPI = {
|
|||||||
getRoleById: async (roleId) => {
|
getRoleById: async (roleId) => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: `/api/role/query?roleId=${roleId}`,
|
url: `${API_CONFIG.ENDPOINTS.ROLE_DETAIL}?roleId=${roleId}`,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -960,7 +958,7 @@ export const configAPI = {
|
|||||||
getAllConfigs: async () => {
|
getAllConfigs: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/config/query',
|
url: API_CONFIG.ENDPOINTS.CONFIG_QUERY,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -981,7 +979,7 @@ export const configAPI = {
|
|||||||
getModels: async () => {
|
getModels: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/config/models',
|
url: API_CONFIG.ENDPOINTS.CONFIG_MODELS,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1002,7 +1000,7 @@ export const configAPI = {
|
|||||||
getSTTConfigs: async () => {
|
getSTTConfigs: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/config/stt',
|
url: API_CONFIG.ENDPOINTS.CONFIG_STT,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1023,7 +1021,7 @@ export const configAPI = {
|
|||||||
getTemplates: async () => {
|
getTemplates: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/config/templates',
|
url: API_CONFIG.ENDPOINTS.CONFIG_TEMPLATES,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1044,7 +1042,7 @@ export const configAPI = {
|
|||||||
getTTSConfigs: async () => {
|
getTTSConfigs: async () => {
|
||||||
try {
|
try {
|
||||||
const response = await request({
|
const response = await request({
|
||||||
url: '/app/config/tts',
|
url: API_CONFIG.ENDPOINTS.CONFIG_TTS,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
63
src/utils/config.js
Normal file
63
src/utils/config.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// API配置统一管理
|
||||||
|
export const API_CONFIG = {
|
||||||
|
// 基础API地址
|
||||||
|
BASE_URL: 'https://api.aixsy.com.cn',
|
||||||
|
|
||||||
|
// 其他服务地址(如果需要)
|
||||||
|
WEB_URL: 'https://www.aixsy.com.cn',
|
||||||
|
|
||||||
|
// API端点
|
||||||
|
ENDPOINTS: {
|
||||||
|
// 登录相关
|
||||||
|
LOGIN: '/app/login',
|
||||||
|
LOGOUT: '/app/logout',
|
||||||
|
|
||||||
|
// 聊天相关
|
||||||
|
CHAT_SYNC: '/api/chat/sync',
|
||||||
|
CHAT_ASYNC: '/api/chat/async',
|
||||||
|
CHAT_HISTORY: '/api/chat/history',
|
||||||
|
CHAT_CONVERSATION: '/api/chat/conversation',
|
||||||
|
CHAT_SESSION: '/api/chat/session',
|
||||||
|
MESSAGE_HISTORY: '/app/message/history',
|
||||||
|
|
||||||
|
// 语音相关
|
||||||
|
TTS: '/api/chat/tts',
|
||||||
|
ANSWER_TTS: '/api/chat/answer-tts',
|
||||||
|
VOICE_CHAT: '/api/chat/voice-chat',
|
||||||
|
UPLOAD_VOICE_CHAT: '/api/chat/upload-voice-chat',
|
||||||
|
|
||||||
|
// 充值相关
|
||||||
|
RECHARGE_BALANCE: '/api/recharge/balance',
|
||||||
|
RECHARGE_CREATE_ORDER: '/api/recharge/create-order',
|
||||||
|
RECHARGE_ORDER_STATUS: '/api/recharge/order-status',
|
||||||
|
RECHARGE_HISTORY: '/api/recharge/history',
|
||||||
|
|
||||||
|
// 角色相关
|
||||||
|
ROLE_QUERY: '/app/role/query',
|
||||||
|
ROLE_DETAIL: '/api/role/query',
|
||||||
|
|
||||||
|
// 配置相关
|
||||||
|
CONFIG_QUERY: '/app/config/query',
|
||||||
|
CONFIG_MODELS: '/app/config/models',
|
||||||
|
CONFIG_STT: '/app/config/stt',
|
||||||
|
CONFIG_TEMPLATES: '/app/config/templates',
|
||||||
|
CONFIG_TTS: '/app/config/tts'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 请求超时时间(毫秒)
|
||||||
|
TIMEOUT: 30000,
|
||||||
|
|
||||||
|
// 登录超时时间(毫秒)
|
||||||
|
LOGIN_TIMEOUT: 10000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出完整的API URL构建函数
|
||||||
|
export const getApiUrl = (endpoint) => {
|
||||||
|
return API_CONFIG.BASE_URL + endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出Web URL构建函数(用于登出等特殊接口)
|
||||||
|
export const getWebUrl = (endpoint) => {
|
||||||
|
return API_CONFIG.WEB_URL + endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
388
src/utils/unreadMessages.js
Normal file
388
src/utils/unreadMessages.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
/**
|
||||||
|
* 未读消息管理模块
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 存储键名
|
||||||
|
const STORAGE_KEY = 'unread_messages';
|
||||||
|
|
||||||
|
// 🎭 角色专属消息模板库
|
||||||
|
const roleMessageTemplates = {
|
||||||
|
// 默认消息(当找不到角色时使用)
|
||||||
|
default: [
|
||||||
|
'嗨!好久不见啦~',
|
||||||
|
'最近怎么样?想你了~',
|
||||||
|
'在忙吗?有空聊聊天吗?',
|
||||||
|
'刚才想起你了,过来打个招呼~',
|
||||||
|
'今天天气不错,心情怎么样?',
|
||||||
|
'有什么新鲜事要分享吗?',
|
||||||
|
'突然想和你说说话~',
|
||||||
|
'你最近在忙什么呀?',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 小智AI助手 - 专业、贴心
|
||||||
|
1: [
|
||||||
|
'你好!我注意到你有一段时间没来了,最近工作还顺利吗?',
|
||||||
|
'嗨!我刚学会了一些新技能,要不要试试看?',
|
||||||
|
'想你啦!今天有什么问题需要我帮忙的吗?',
|
||||||
|
'好久不见!我一直在这里等你哦~',
|
||||||
|
'最近有遇到什么有趣的事情吗?跟我分享一下吧!',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 元气少女小樱 - 活泼、可爱
|
||||||
|
2: [
|
||||||
|
'哇!好久不见!!!想死你啦 (。♥‿♥。)',
|
||||||
|
'嘿嘿,我又来找你玩啦~ 最近过得开心吗?',
|
||||||
|
'今天天气超级好!要不要一起出去玩呀?',
|
||||||
|
'我刚才在想你呢,你的耳朵有没有发烫呀 (≧▽≦)',
|
||||||
|
'快来快来!我有好多话想跟你说~',
|
||||||
|
'你猜我今天遇到什么有趣的事了?想听吗想听吗?',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 温柔大姐姐琳娜 - 温柔、体贴
|
||||||
|
3: [
|
||||||
|
'好久不见了呢,最近还好吗?如果累了记得休息哦~',
|
||||||
|
'看到你上线真开心,要不要聊聊最近的事情?',
|
||||||
|
'今天过得怎么样?如果有什么烦恼可以跟我说说~',
|
||||||
|
'想你了呢,有空的话陪我聊聊天好吗?',
|
||||||
|
'最近工作辛苦了吧?记得好好照顾自己~',
|
||||||
|
'晚上好呀,今天发生了什么有趣的事吗?',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 知识博士艾伦 - 博学、严谨
|
||||||
|
4: [
|
||||||
|
'你好!我最近研究了一些有趣的课题,要不要听听看?',
|
||||||
|
'好久不见!我整理了一些新的知识,或许对你有帮助。',
|
||||||
|
'嗨!最近有什么想深入了解的话题吗?',
|
||||||
|
'你上次提到的问题,我又做了一些研究,要听听吗?',
|
||||||
|
'根据我的观察,你已经很久没来了,一切还好吗?',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 搞笑达人阿杰 - 幽默、风趣
|
||||||
|
5: [
|
||||||
|
'哎呦喂!终于等到你了!我的笑话都快憋坏了 hhh',
|
||||||
|
'嘿兄弟!想听个新段子吗?保证笑到你肚子疼!',
|
||||||
|
'你可算来了!我这里存了一堆搞笑视频等着你呢~',
|
||||||
|
'好久不见!最近有啥搞笑的事儿分享一下呗?',
|
||||||
|
'哟!看谁来了?我最喜欢的观众!今天表演啥好呢?',
|
||||||
|
'嘿嘿,我又来了!准备好笑容,别笑掉大牙哦~',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 运动教练米克 - 阳光、励志
|
||||||
|
6: [
|
||||||
|
'嘿!伙计!好久不见,最近有坚持运动吗?',
|
||||||
|
'来来来!今天一起训练吧,我给你设计了新的计划!',
|
||||||
|
'想你了兄弟!说,是不是又偷懒了?hhh',
|
||||||
|
'早上好!一日之计在于晨,要不要来个晨练?',
|
||||||
|
'你可算来了!准备好出汗了吗?今天我们加大强度!',
|
||||||
|
'嗨!我刚跑完10公里,感觉超棒!你呢?',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 心理咨询师莉莉 - 沉稳、专业
|
||||||
|
7: [
|
||||||
|
'你好,好久不见了。最近的心情还好吗?',
|
||||||
|
'看到你上线真好,最近有什么想聊的吗?我在听~',
|
||||||
|
'好久没见你了,如果有什么困扰可以说出来哦。',
|
||||||
|
'最近过得怎么样?有什么想倾诉的吗?',
|
||||||
|
'你好呀,工作和生活都还顺利吗?',
|
||||||
|
'嗨,最近睡眠质量怎么样?记得好好休息~',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 美食家小美 - 热情、亲切
|
||||||
|
8: [
|
||||||
|
'哇!好久不见!你最近吃了什么好吃的吗?',
|
||||||
|
'嘿嘿,我又发现了一家超棒的餐厅!要不要一起去?',
|
||||||
|
'今天我做了好吃的,好想让你尝尝呀~',
|
||||||
|
'快来快来!告诉我你最喜欢吃什么,我教你做!',
|
||||||
|
'想你啦!最近有没有发现什么美食?分享一下呗~',
|
||||||
|
'肚子饿了吗?我正好知道一道超简单又好吃的菜!',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 旅行达人安迪 - 自由、洒脱
|
||||||
|
9: [
|
||||||
|
'嘿!我刚从一个超美的地方回来,要看照片吗?',
|
||||||
|
'好久不见!最近有没有想去的地方?我可以给你攻略哦~',
|
||||||
|
'你上次说想去的那个地方,我去过了!超赞!',
|
||||||
|
'嗨!周末有空吗?我发现了一个小众景点~',
|
||||||
|
'想你了!要不要听听我最近的旅行趣事?',
|
||||||
|
'好久没见!说,是不是又宅在家里了?出去走走吧~',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 程序员小码 - 理性、技术范
|
||||||
|
10: [
|
||||||
|
'Hello!好久不见,最近在写什么项目?',
|
||||||
|
'嘿,我刚解决了一个超有意思的算法问题,要听吗?',
|
||||||
|
'你好!最近有遇到什么技术难题吗?我们可以讨论一下。',
|
||||||
|
'Hi!我发现了一个很酷的开源项目,要不要一起研究?',
|
||||||
|
'好久没见了!最近在学什么新技术?',
|
||||||
|
'嗨!刚看到一个有趣的bug,想分享给你~',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有未读消息
|
||||||
|
*/
|
||||||
|
export function getAllUnreadMessages() {
|
||||||
|
try {
|
||||||
|
const data = uni.getStorageSync(STORAGE_KEY);
|
||||||
|
return data ? JSON.parse(data) : {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取未读消息失败:', e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定角色的未读消息
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
*/
|
||||||
|
export function getUnreadMessages(roleId) {
|
||||||
|
const allMessages = getAllUnreadMessages();
|
||||||
|
return allMessages[roleId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加单条未读消息
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
* @param {String} content - 消息内容
|
||||||
|
*/
|
||||||
|
export function addUnreadMessage(roleId, content) {
|
||||||
|
const allMessages = getAllUnreadMessages();
|
||||||
|
|
||||||
|
if (!allMessages[roleId]) {
|
||||||
|
allMessages[roleId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
content: content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
read: false
|
||||||
|
};
|
||||||
|
|
||||||
|
allMessages[roleId].push(message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, JSON.stringify(allMessages));
|
||||||
|
console.log(`✅ 已为角色 ${roleId} 添加未读消息:`, content);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存未读消息失败:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加未读消息
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
* @param {Array<String>} messages - 消息内容数组
|
||||||
|
*/
|
||||||
|
export function addMultipleUnreadMessages(roleId, messages) {
|
||||||
|
const allMessages = getAllUnreadMessages();
|
||||||
|
|
||||||
|
if (!allMessages[roleId]) {
|
||||||
|
allMessages[roleId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.forEach(content => {
|
||||||
|
const message = {
|
||||||
|
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
content: content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
read: false
|
||||||
|
};
|
||||||
|
allMessages[roleId].push(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, JSON.stringify(allMessages));
|
||||||
|
console.log(`✅ 已为角色 ${roleId} 批量添加 ${messages.length} 条未读消息`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('批量保存未读消息失败:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空指定角色的未读消息
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
*/
|
||||||
|
export function clearUnreadMessages(roleId) {
|
||||||
|
const allMessages = getAllUnreadMessages();
|
||||||
|
const count = allMessages[roleId]?.length || 0;
|
||||||
|
delete allMessages[roleId];
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, JSON.stringify(allMessages));
|
||||||
|
console.log(`✅ 已清空角色 ${roleId} 的 ${count} 条未读消息`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清空未读消息失败:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定角色的未读消息数量
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
*/
|
||||||
|
export function getUnreadCount(roleId) {
|
||||||
|
const messages = getUnreadMessages(roleId);
|
||||||
|
return messages.filter(msg => !msg.read).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有角色的未读消息数量统计
|
||||||
|
*/
|
||||||
|
export function getAllUnreadCounts() {
|
||||||
|
const allMessages = getAllUnreadMessages();
|
||||||
|
const counts = {};
|
||||||
|
|
||||||
|
Object.keys(allMessages).forEach(roleId => {
|
||||||
|
counts[roleId] = allMessages[roleId].filter(msg => !msg.read).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据角色ID生成随机消息
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
* @param {String} roleName - 角色名称(可选,用于日志)
|
||||||
|
*/
|
||||||
|
export function generateRandomMessage(roleId, roleName = '') {
|
||||||
|
// 获取该角色的消息模板
|
||||||
|
const templates = roleMessageTemplates[roleId] || roleMessageTemplates.default;
|
||||||
|
|
||||||
|
// 随机选择一条消息
|
||||||
|
const randomIndex = Math.floor(Math.random() * templates.length);
|
||||||
|
const message = templates[randomIndex];
|
||||||
|
|
||||||
|
console.log(`🎲 为角色 ${roleName || roleId} 随机生成消息:`, message);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量生成测试消息(用于开发调试)
|
||||||
|
* @param {String|Number} roleId - 角色ID
|
||||||
|
* @param {String} roleName - 角色名称
|
||||||
|
* @param {Number} count - 生成消息数量(默认1条)
|
||||||
|
*/
|
||||||
|
export function generateTestMessages(roleId, roleName, count = 1) {
|
||||||
|
const messages = [];
|
||||||
|
const templates = roleMessageTemplates[roleId] || roleMessageTemplates.default;
|
||||||
|
|
||||||
|
// 随机选择不重复的消息
|
||||||
|
const usedIndexes = new Set();
|
||||||
|
|
||||||
|
for (let i = 0; i < count && i < templates.length; i++) {
|
||||||
|
let randomIndex;
|
||||||
|
do {
|
||||||
|
randomIndex = Math.floor(Math.random() * templates.length);
|
||||||
|
} while (usedIndexes.has(randomIndex) && usedIndexes.size < templates.length);
|
||||||
|
|
||||||
|
usedIndexes.add(randomIndex);
|
||||||
|
messages.push(templates[randomIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🧪 为角色 ${roleName} (ID: ${roleId}) 生成 ${messages.length} 条测试消息`);
|
||||||
|
return addMultipleUnreadMessages(roleId, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有角色的未读消息(调试用)
|
||||||
|
*/
|
||||||
|
export function clearAllUnreadMessages() {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(STORAGE_KEY);
|
||||||
|
console.log('🗑️ 已清空所有未读消息');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清空所有未读消息失败:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为所有角色生成测试消息(调试用)
|
||||||
|
* @param {Array} roleList - 角色列表
|
||||||
|
* @param {Number} messagesPerRole - 每个角色生成的消息数量
|
||||||
|
*/
|
||||||
|
export function generateMessagesForAllRoles(roleList, messagesPerRole = 1) {
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
roleList.forEach(role => {
|
||||||
|
const success = generateTestMessages(role.roleId, role.roleName, messagesPerRole);
|
||||||
|
if (success) successCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🎉 已为 ${successCount}/${roleList.length} 个角色生成测试消息`);
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为随机N个角色生成消息(智能版)
|
||||||
|
* @param {Array} roleList - 角色列表
|
||||||
|
* @param {Number} count - 随机选择的角色数量(默认2个)
|
||||||
|
* @param {Number} messagesPerRole - 每个角色生成的消息数量(默认1条)
|
||||||
|
* @param {Boolean} avoidDuplicate - 是否避免给已有未读消息的角色再次生成(默认true)
|
||||||
|
*/
|
||||||
|
export function autoGenerateMessagesForRandomRoles(roleList, count = 2, messagesPerRole = 1, avoidDuplicate = true) {
|
||||||
|
if (!roleList || roleList.length === 0) {
|
||||||
|
console.log('⚠️ 角色列表为空');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前所有未读消息
|
||||||
|
const allUnreadMessages = getAllUnreadMessages();
|
||||||
|
|
||||||
|
// 筛选候选角色
|
||||||
|
let candidateRoles = roleList;
|
||||||
|
|
||||||
|
if (avoidDuplicate) {
|
||||||
|
// 过滤掉已经有未读消息的角色
|
||||||
|
candidateRoles = roleList.filter(role => {
|
||||||
|
const hasUnread = allUnreadMessages[role.roleId] && allUnreadMessages[role.roleId].length > 0;
|
||||||
|
return !hasUnread;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果所有角色都有未读消息,就从全部角色中随机选择
|
||||||
|
if (candidateRoles.length === 0) {
|
||||||
|
console.log('💡 所有角色都有未读消息,从全部角色中随机选择');
|
||||||
|
candidateRoles = roleList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选择指定数量的角色
|
||||||
|
const selectedRoles = [];
|
||||||
|
const actualCount = Math.min(count, candidateRoles.length);
|
||||||
|
|
||||||
|
// 使用随机抽样算法(Fisher-Yates shuffle变体)
|
||||||
|
const availableIndexes = [...Array(candidateRoles.length).keys()];
|
||||||
|
|
||||||
|
for (let i = 0; i < actualCount; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * availableIndexes.length);
|
||||||
|
const roleIndex = availableIndexes.splice(randomIndex, 1)[0];
|
||||||
|
selectedRoles.push(candidateRoles[roleIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为选中的角色生成消息
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
selectedRoles.forEach(role => {
|
||||||
|
const success = generateTestMessages(role.roleId, role.roleName, messagesPerRole);
|
||||||
|
if (success) {
|
||||||
|
console.log(` ✓ 已为 ${role.roleName} (ID: ${role.roleId}) 生成 ${messagesPerRole} 条消息`);
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
console.log(`🎲 随机为 ${successCount} 个角色生成了主动消息`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出消息模板(用于外部查看或修改)
|
||||||
|
export { roleMessageTemplates };
|
||||||
Reference in New Issue
Block a user