feat: 完成角色卡,智能体配置,加载,历史记录,清空等功能
This commit is contained in:
24
src/App.vue
24
src/App.vue
@@ -3,19 +3,23 @@ export default {
|
||||
onLaunch: function () {
|
||||
console.log('App Launch');
|
||||
|
||||
// 暂时跳过欢迎页面 - 直接标记为已显示过启动页和已同意协议
|
||||
uni.setStorageSync('hasShownSplash', 'true');
|
||||
uni.setStorageSync('hasAgreedToTerms', 'true');
|
||||
|
||||
// 检查是否是首次启动或需要显示启动页
|
||||
const hasShownSplash = uni.getStorageSync('hasShownSplash');
|
||||
const hasAgreedToTerms = uni.getStorageSync('hasAgreedToTerms');
|
||||
// const hasShownSplash = uni.getStorageSync('hasShownSplash');
|
||||
// const hasAgreedToTerms = uni.getStorageSync('hasAgreedToTerms');
|
||||
|
||||
// 如果未显示过启动页或未同意协议,则跳转到启动页
|
||||
if (!hasShownSplash || hasAgreedToTerms !== 'true') {
|
||||
// 延迟一下确保页面已加载
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/splash/splash'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
// if (!hasShownSplash || hasAgreedToTerms !== 'true') {
|
||||
// // 延迟一下确保页面已加载
|
||||
// setTimeout(() => {
|
||||
// uni.redirectTo({
|
||||
// url: '/pages/splash/splash'
|
||||
// });
|
||||
// }, 100);
|
||||
// }
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
|
||||
1683
src/components/ChatBox.vue
Normal file
1683
src/components/ChatBox.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,12 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/role-chat/role-chat",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
|
||||
@@ -14,19 +14,22 @@
|
||||
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-left" @tap="goBack" v-if="showBackButton">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-text">返回</text>
|
||||
<view class="navbar-left">
|
||||
<view v-if="showBackButton" @tap="goBack" class="left-btn">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-text">返回</text>
|
||||
</view>
|
||||
<text class="clear-btn" @tap="clearContext">清空</text>
|
||||
</view>
|
||||
<view class="navbar-center" :class="{'centered': !showBackButton}">
|
||||
<image class="character-avatar" :src="currentCharacter.avatar" mode="aspectFill" />
|
||||
<text class="character-name">{{ currentCharacter.name }}</text>
|
||||
</view>
|
||||
<view class="navbar-right">
|
||||
<text class="status-dot" :class="{'online': isOnline}"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 聊天消息区域 - 使用 Ant Design X Bubble.List -->
|
||||
<scroll-view
|
||||
class="chat-messages"
|
||||
@@ -257,16 +260,23 @@ const initPage = async () => {
|
||||
currentCharacter.value = {
|
||||
id: 'wei-ai',
|
||||
name: options.characterName || '蔚AI',
|
||||
avatar: options.characterAvatar || '/static/logo.png',
|
||||
avatar: options.characterAvatar || '/static/avatar/icon_hushi.jpg',
|
||||
greeting: decodeURIComponent(options.introMessage || '你好!我是蔚AI,很高兴为您服务!')
|
||||
};
|
||||
|
||||
await loadAIConfigs();
|
||||
await createNewConversation('wei-ai');
|
||||
addMessage('ai', currentCharacter.value.greeting);
|
||||
createNewConversation('wei-ai');
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
// 尝试加载历史消息
|
||||
await loadHistoryMessages();
|
||||
|
||||
// 如果没有历史消息,显示欢迎消息
|
||||
if (messages.value.length === 0) {
|
||||
addMessage('ai', currentCharacter.value.greeting);
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
}
|
||||
}
|
||||
}
|
||||
// AI角色
|
||||
@@ -295,11 +305,18 @@ const initPage = async () => {
|
||||
currentTemplateId.value = parseInt(options.templateId);
|
||||
}
|
||||
|
||||
await createNewConversation(options.roleId);
|
||||
addMessage('ai', currentCharacter.value.greeting);
|
||||
createNewConversation(options.roleId);
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
// 尝试加载历史消息
|
||||
await loadHistoryMessages();
|
||||
|
||||
// 如果没有历史消息,显示欢迎消息
|
||||
if (messages.value.length === 0) {
|
||||
addMessage('ai', currentCharacter.value.greeting);
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
}
|
||||
}
|
||||
}
|
||||
// 默认角色
|
||||
@@ -309,11 +326,18 @@ const initPage = async () => {
|
||||
if (character) {
|
||||
currentCharacter.value = character;
|
||||
await loadAIConfigs();
|
||||
await createNewConversation(characterId);
|
||||
addMessage('ai', character.greeting);
|
||||
createNewConversation(characterId);
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
// 尝试加载历史消息
|
||||
await loadHistoryMessages();
|
||||
|
||||
// 如果没有历史消息,显示欢迎消息
|
||||
if (messages.value.length === 0) {
|
||||
addMessage('ai', character.greeting);
|
||||
|
||||
if (!isLoggedIn.value) {
|
||||
addMessage('system', '您当前处于未登录状态,将使用本地模拟回复。登录后可享受完整的AI对话功能。');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,17 +485,115 @@ const sendMessage = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新对话
|
||||
const createNewConversation = async (characterId) => {
|
||||
// 创建或获取会话ID(基于角色持久化存储)
|
||||
const createNewConversation = (characterId) => {
|
||||
// 生成存储key:根据角色类型区分
|
||||
let storageKey = '';
|
||||
if (characterId === 'wei-ai' || !currentCharacter.value.roleId) {
|
||||
// 蔚AI或默认角色
|
||||
storageKey = `session_weiai`;
|
||||
} else {
|
||||
// 剧情角色
|
||||
storageKey = `session_role_${currentCharacter.value.roleId}`;
|
||||
}
|
||||
|
||||
// 尝试从本地存储获取已有的sessionId
|
||||
let existingSessionId = uni.getStorageSync(storageKey);
|
||||
|
||||
if (existingSessionId) {
|
||||
// 已有sessionId,直接使用(保持上下文)
|
||||
conversationId.value = existingSessionId;
|
||||
console.log('使用已有sessionId:', existingSessionId, 'storageKey:', storageKey);
|
||||
} else {
|
||||
// 生成新的sessionId
|
||||
const userId = userStore.userInfo?.openid || userStore.userInfo?.userId || 'guest';
|
||||
const timestamp = Date.now();
|
||||
const newSessionId = `session_${characterId}_${userId}_${timestamp}`;
|
||||
|
||||
// 存储到本地
|
||||
uni.setStorageSync(storageKey, newSessionId);
|
||||
conversationId.value = newSessionId;
|
||||
console.log('创建新sessionId:', newSessionId, 'storageKey:', storageKey);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载历史消息
|
||||
const loadHistoryMessages = async () => {
|
||||
if (!conversationId.value || !isLoggedIn.value) {
|
||||
console.log('没有sessionId或用户未登录,跳过加载历史消息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await chatAPI.createConversation(characterId);
|
||||
if (result.success) {
|
||||
conversationId.value = result.data.conversationId;
|
||||
console.log('开始加载历史消息,sessionId:', conversationId.value);
|
||||
const result = await chatAPI.getHistoryMessages(conversationId.value);
|
||||
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
console.log('获取到历史消息:', result.data.length, '条');
|
||||
|
||||
// 将后端消息格式转换为前端格式,保持与API回复一致的“清理后回复”逻辑
|
||||
const historyMessages = [];
|
||||
|
||||
result.data.forEach(msg => {
|
||||
// 格式化时间
|
||||
let timeStr = '';
|
||||
if (msg.createTime) {
|
||||
const date = new Date(msg.createTime);
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
timeStr = `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
const messageType = msg.sender === 'assistant' ? 'ai' : (msg.sender === 'user' ? 'user' : 'system');
|
||||
const rawContent = msg.message || '';
|
||||
|
||||
// 与API一致:先清理文本,再根据 & 分段
|
||||
const cleanedContent = cleanText(rawContent);
|
||||
|
||||
if (messageType === 'ai' && cleanedContent.includes('&')) {
|
||||
const segments = cleanedContent
|
||||
.split('&')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
segments.forEach(segment => {
|
||||
historyMessages.push({
|
||||
type: messageType,
|
||||
content: segment,
|
||||
time: timeStr
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 非 AI 或无分隔符,直接使用清理后的内容
|
||||
const content = cleanedContent.trim();
|
||||
if (content) {
|
||||
historyMessages.push({
|
||||
type: messageType,
|
||||
content,
|
||||
time: timeStr
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 按时间排序(从旧到新)
|
||||
historyMessages.sort((a, b) => {
|
||||
if (!a.time || !b.time) return 0;
|
||||
return a.time.localeCompare(b.time);
|
||||
});
|
||||
|
||||
// 清空当前消息列表,加载历史消息
|
||||
messages.value = historyMessages;
|
||||
console.log('历史消息加载完成,共', messages.value.length, '条');
|
||||
|
||||
// 滚动到底部
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
} else {
|
||||
conversationId.value = `local_${Date.now()}`;
|
||||
console.log('没有历史消息或获取失败');
|
||||
}
|
||||
} catch (error) {
|
||||
conversationId.value = `local_${Date.now()}`;
|
||||
console.error('加载历史消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -814,6 +936,65 @@ const handleInputFocus = () => {
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
|
||||
// 清空对话上下文
|
||||
const clearContext = async () => {
|
||||
uni.showModal({
|
||||
title: '清空对话',
|
||||
content: '确定要清空与该角色的所有对话记录吗?此操作不可恢复。',
|
||||
confirmText: '确定清空',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '清空中...' });
|
||||
|
||||
// 1. 调用后端清除session(如果用户已登录)
|
||||
if (conversationId.value && isLoggedIn.value) {
|
||||
try {
|
||||
const result = await chatAPI.clearSession(conversationId.value);
|
||||
console.log('后端清除会话结果:', result);
|
||||
} catch (error) {
|
||||
console.log('后端清除失败,继续本地清除:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 删除本地存储的sessionId
|
||||
let storageKey = '';
|
||||
if (currentCharacter.value.id === 'wei-ai' || !currentCharacter.value.roleId) {
|
||||
storageKey = `session_weiai`;
|
||||
} else {
|
||||
storageKey = `session_role_${currentCharacter.value.roleId}`;
|
||||
}
|
||||
uni.removeStorageSync(storageKey);
|
||||
console.log('删除本地sessionId:', storageKey);
|
||||
|
||||
// 3. 生成新的sessionId
|
||||
const characterIdForSession = currentCharacter.value.id || currentCharacter.value.roleId || 'default';
|
||||
createNewConversation(characterIdForSession);
|
||||
|
||||
// 4. 清空消息列表(保留欢迎消息)
|
||||
messages.value = [];
|
||||
addMessage('ai', currentCharacter.value.greeting || '你好!很高兴再次见到你!');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '对话已清空',
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清空对话失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -897,13 +1078,19 @@ page {
|
||||
left: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
gap: 16rpx;
|
||||
color: #f9e076;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 10rpx rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.left-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
@@ -938,13 +1125,22 @@ page {
|
||||
.navbar-right {
|
||||
position: absolute;
|
||||
right: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(249, 224, 118, 0.3);
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #f9e076;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 8rpx rgba(249, 224, 118, 0.3);
|
||||
padding: 8rpx 16rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
@@ -962,6 +1158,7 @@ page {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 聊天消息区域 */
|
||||
.chat-messages {
|
||||
height: calc(100vh - 100rpx - 200rpx - env(safe-area-inset-top));
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<view class="floral-container">
|
||||
<!-- 夜空装饰背景 -->
|
||||
<view class="night-sky-decoration">
|
||||
<view class="star star-1"></view>
|
||||
<view class="star star-2"></view>
|
||||
<view class="star star-3"></view>
|
||||
<view class="star star-4"></view>
|
||||
<view class="star star-5"></view>
|
||||
<view class="star star-6"></view>
|
||||
<view class="star star-7"></view>
|
||||
<view class="star star-8"></view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="navbar-content">
|
||||
<view class="navbar-stars">
|
||||
<view class="navbar-star star-left"></view>
|
||||
<view class="navbar-star star-right"></view>
|
||||
</view>
|
||||
<view class="navbar-title">剧情角色</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -18,7 +34,9 @@
|
||||
<view class="column column-left">
|
||||
<block v-for="(item, idx) in leftColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="cover-container">
|
||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
@@ -34,7 +52,9 @@
|
||||
<view class="column column-right">
|
||||
<block v-for="(item, idx) in rightColumnItems" :key="item ? item.id : idx">
|
||||
<view class="floral-grid-card" v-if="item">
|
||||
<image class="cover" :src="item.cover" mode="aspectFit" />
|
||||
<view class="cover-container">
|
||||
<image class="cover" :src="getResourceUrl(item.cover)" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="floral-tag">{{ item.tag }}</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
@@ -69,8 +89,8 @@
|
||||
<view class="detail-cover-container">
|
||||
<image
|
||||
class="detail-cover"
|
||||
:src="selectedItem?.cover"
|
||||
mode="aspectFit"
|
||||
:src="getResourceUrl(selectedItem?.cover)"
|
||||
mode="aspectFill"
|
||||
:class="{'cover-zoomed': showDetailModal}"
|
||||
/>
|
||||
</view>
|
||||
@@ -85,7 +105,7 @@
|
||||
</scroll-view>
|
||||
<view class="detail-actions">
|
||||
<button class="floral-btn outline detail-btn cancel" @click="closeDetail">取消</button>
|
||||
<button class="floral-btn detail-btn confirm" @click="useFromDetail">💝 去使用</button>
|
||||
<button class="floral-btn detail-btn confirm" @click="useFromDetail">✨ 去使用</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -96,7 +116,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
import { roleAPI } from '@/utils/api.js';
|
||||
import { roleAPI, getResourceUrl } from '@/utils/api.js';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const showLoginModal = ref(false);
|
||||
@@ -334,8 +354,9 @@ const loadDramaList = async () => {
|
||||
dramaList.value = roles.map(role => ({
|
||||
id: role.roleId,
|
||||
roleId: role.roleId,
|
||||
cover: role.avatar || '/static/default-avatar.png',
|
||||
cover: role.backgroundImage || role.avatar || '/static/default-avatar.png', // 使用背景图片作为封面,如果没有则使用头像
|
||||
avatar: role.avatar || '/static/default-avatar.png',
|
||||
backgroundImage: role.backgroundImage || '', // 背景图片字段
|
||||
tag: role.tag || '角色',
|
||||
title: role.roleName || '未命名角色',
|
||||
roleName: role.roleName || '未命名角色',
|
||||
@@ -413,7 +434,7 @@ const handleUse = (item) => {
|
||||
.join('&');
|
||||
|
||||
uni.hideLoading();
|
||||
uni.navigateTo({ url: `/pages/chat/chat?${queryString}` });
|
||||
uni.navigateTo({ url: `/pages/role-chat/role-chat?${queryString}` });
|
||||
};
|
||||
|
||||
const showLoginTip = () => { showLoginModal.value = true; };
|
||||
@@ -423,27 +444,62 @@ const useFromDetail = () => { if (selectedItem.value) { const v = selectedItem.v
|
||||
const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/pages/mine/mine' }); };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.floral-container {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a0b2e 0%, #4a1e6d 25%, #6b2c9c 50%, #8a2be2 75%, #4b0082 100%);
|
||||
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 120rpx;
|
||||
overflow: hidden;
|
||||
padding-bottom: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 夜空装饰 */
|
||||
.night-sky-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 2s infinite;
|
||||
box-shadow: 0 0 10rpx #f9e076;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.star-1 { top: 10%; left: 15%; animation-delay: 0s; }
|
||||
.star-2 { top: 20%; left: 80%; animation-delay: 0.3s; }
|
||||
.star-3 { top: 30%; left: 45%; animation-delay: 0.6s; }
|
||||
.star-4 { top: 50%; left: 25%; animation-delay: 0.9s; }
|
||||
.star-5 { top: 60%; left: 70%; animation-delay: 1.2s; }
|
||||
.star-6 { top: 70%; left: 40%; animation-delay: 1.5s; }
|
||||
.star-7 { top: 15%; left: 60%; animation-delay: 0.4s; }
|
||||
.star-8 { top: 85%; left: 55%; animation-delay: 1.8s; }
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.95) 0%, rgba(74, 30, 109, 0.95) 25%, rgba(107, 44, 156, 0.95) 50%, rgba(138, 43, 226, 0.95) 75%, rgba(75, 0, 130, 0.95) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1rpx solid rgba(251, 191, 36, 0.3);
|
||||
background: rgba(26, 11, 46, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(249, 224, 118, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@@ -452,40 +508,81 @@ const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/p
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24rpx;
|
||||
padding: 0 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-stars {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navbar-star {
|
||||
position: absolute;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
background: #f9e076;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 15rpx #f9e076;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.star-left {
|
||||
top: 50%;
|
||||
left: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.star-right {
|
||||
top: 50%;
|
||||
right: 30rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #ffffff;
|
||||
color: #f9e076;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
text-shadow: 0 0 10px rgba(249, 224, 118, 0.5);
|
||||
font-size: 36rpx;
|
||||
text-shadow: 0 0 20rpx rgba(249, 224, 118, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.two-column-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
gap: 12rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1;
|
||||
width: calc(50% - 8rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.floral-grid-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2rpx solid rgba(138, 43, 226, 0.3);
|
||||
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.2);
|
||||
border-radius: 25rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(138, 43, 226, 0.15);
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -493,97 +590,244 @@ const goToLogin = () => { showLoginModal.value = false; uni.switchTab({ url: '/p
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floral-grid-card:active {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 15rpx 50rpx rgba(249, 224, 118, 0.2);
|
||||
border-color: rgba(249, 224, 118, 0.4);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.floral-tag {
|
||||
position: absolute;
|
||||
margin: 12rpx;
|
||||
background: linear-gradient(135deg, #f9e076, #f59e0b);
|
||||
color: #4b0082;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.3);
|
||||
top: 12rpx;
|
||||
left: 12rpx;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4rpx 12rpx rgba(249, 224, 118, 0.4);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content-area { padding: 16rpx; }
|
||||
.title { font-size: 28rpx; color: #333; font-weight: bold; }
|
||||
.card-bottom { margin-top: 8rpx; display: flex; justify-content: flex-end; }
|
||||
.content-area {
|
||||
padding: 20rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
color: #f9e076;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.floral-btn {
|
||||
background: linear-gradient(135deg, #8a2be2, #6b2c9c);
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
box-shadow: 0 4rpx 15rpx rgba(138, 43, 226, 0.3);
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 12rpx 24rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(249, 224, 118, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.floral-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 3rpx 8rpx rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.floral-btn.outline {
|
||||
background: transparent;
|
||||
color: #8a2be2;
|
||||
border: 2rpx solid #8a2be2;
|
||||
color: #f9e076;
|
||||
border: 2rpx solid #f9e076;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.floral-grid-card:hover {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 12rpx 40rpx rgba(138, 43, 226, 0.2);
|
||||
border-color: #8a2be2;
|
||||
.use-btn {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.use-btn { font-size: 24rpx; }
|
||||
.bottom-spacing { height: 40rpx; }
|
||||
.bottom-spacing {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 登录弹窗 */
|
||||
.floral-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.floral-modal-content {
|
||||
width: 80vw;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.98) 0%, rgba(45, 27, 78, 0.98) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
border-radius: 30rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #f9e076;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(249, 224, 118, 0.8);
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background: transparent;
|
||||
color: rgba(249, 224, 118, 0.6);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
}
|
||||
|
||||
.confirm {
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
color: #1a0b2e;
|
||||
}
|
||||
.modal-title { font-size: 30rpx; font-weight: bold; margin-bottom: 12rpx; }
|
||||
.modal-text { font-size: 24rpx; color: #666; margin-bottom: 16rpx; }
|
||||
.modal-btns { display: flex; gap: 20rpx; justify-content: flex-end; }
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 2100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-modal-content {
|
||||
width: 86vw;
|
||||
max-height: 80vh;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, rgba(26, 11, 46, 0.98) 0%, rgba(45, 27, 78, 0.98) 100%);
|
||||
border: 2rpx solid rgba(249, 224, 118, 0.3);
|
||||
border-radius: 30rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.detail-cover-container {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cover-zoomed {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
padding: 30rpx;
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
transition: all 0.3s ease 0.1s;
|
||||
}
|
||||
|
||||
.info-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx;
|
||||
color: #1a0b2e;
|
||||
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #f9e076;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.detail-description {
|
||||
max-height: 200rpx;
|
||||
margin-bottom: 30rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(249, 224, 118, 0.8);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
flex: 1;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
.detail-cover-container { width: 100%; height: 320rpx; overflow: hidden; }
|
||||
.detail-cover { width: 100%; height: 100%; }
|
||||
.detail-info { padding: 20rpx; }
|
||||
.detail-tag { font-size: 22rpx; color: #8a2be2; margin-bottom: 8rpx; }
|
||||
.detail-title { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 12rpx; }
|
||||
.detail-description { max-height: 200rpx; }
|
||||
.description-text { font-size: 24rpx; color: #555; line-height: 1.6; }
|
||||
.detail-actions { margin-top: 12rpx; display: flex; justify-content: flex-end; gap: 20rpx; }
|
||||
.detail-btn { padding: 10rpx 22rpx; }
|
||||
</style>
|
||||
|
||||
57
src/pages/role-chat/role-chat.vue
Normal file
57
src/pages/role-chat/role-chat.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<ChatBox
|
||||
:character-config="characterConfig"
|
||||
:ai-config="aiConfig"
|
||||
:ui-config="uiConfig"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import ChatBox from '@/components/ChatBox.vue';
|
||||
|
||||
// 角色配置
|
||||
const characterConfig = ref({});
|
||||
|
||||
// AI 配置
|
||||
const aiConfig = ref({
|
||||
modelId: 10,
|
||||
templateId: 6
|
||||
});
|
||||
|
||||
// UI 配置
|
||||
const uiConfig = ref({
|
||||
showBackButton: true // 角色聊天页面显示返回按钮
|
||||
});
|
||||
|
||||
// 初始化:解析 URL 参数
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options || {};
|
||||
|
||||
// 组装角色配置
|
||||
characterConfig.value = {
|
||||
id: options.characterId || '',
|
||||
roleId: options.roleId || '',
|
||||
name: decodeURIComponent(options.roleName || 'AI角色'),
|
||||
avatar: decodeURIComponent(options.avatar || '/static/logo.png'),
|
||||
greeting: decodeURIComponent(options.greeting || '你好!很高兴认识你!'),
|
||||
roleDesc: decodeURIComponent(options.roleDesc || '')
|
||||
};
|
||||
|
||||
// 组装 AI 配置
|
||||
aiConfig.value = {
|
||||
modelId: options.modelId ? parseInt(options.modelId) : 10,
|
||||
templateId: options.templateId ? parseInt(options.templateId) : (options.roleId ? parseInt(options.roleId) : 6),
|
||||
ttsId: options.ttsId || null,
|
||||
sttId: options.sttId || null,
|
||||
temperature: options.temperature ? parseFloat(options.temperature) : 0.7,
|
||||
topP: options.topP ? parseFloat(options.topP) : 0.9
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 无需额外样式,所有样式在 ChatBox 组件中 */
|
||||
</style>
|
||||
BIN
src/static/avatar/icon_hushi.jpg
Normal file
BIN
src/static/avatar/icon_hushi.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -101,7 +101,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
function wxLogin(code, userInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: 'http://localhost:8091/app/login',
|
||||
url: 'http://192.168.1.2:8091/app/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code
|
||||
|
||||
108
src/utils/api.js
108
src/utils/api.js
@@ -1,6 +1,31 @@
|
||||
// API服务文件
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
// 图片URL处理函数 - 处理小程序中图片路径问题
|
||||
export const getResourceUrl = (url) => {
|
||||
if (!url || typeof url !== 'string') {
|
||||
return '/static/default-avatar.png';
|
||||
}
|
||||
|
||||
// 如果是完整的http/https URL,直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 如果是相对路径,拼接完整的服务器地址
|
||||
if (url.startsWith('/file/')) {
|
||||
return BASE_URL + url;
|
||||
}
|
||||
|
||||
// 如果是其他相对路径,也拼接服务器地址
|
||||
if (url.startsWith('/')) {
|
||||
return BASE_URL + url;
|
||||
}
|
||||
|
||||
// 默认返回原路径
|
||||
return url;
|
||||
};
|
||||
|
||||
// 文本清理函数 - 只保留文字和标点符号
|
||||
export const cleanText = (text) => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
@@ -49,7 +74,7 @@ export const cleanText = (text) => {
|
||||
};
|
||||
|
||||
// 基础配置
|
||||
const BASE_URL = 'http://192.168.3.243:8091'; // 根据后端地址调整
|
||||
const BASE_URL = 'http://192.168.1.2:8091'; // 根据后端地址调整
|
||||
|
||||
// 检查用户登录状态
|
||||
const checkLoginStatus = () => {
|
||||
@@ -150,7 +175,7 @@ export const chatAPI = {
|
||||
useFunctionCall: false,
|
||||
modelId: params.modelId || null, // 支持传入modelId,默认为null使用后端默认
|
||||
templateId: params.templateId || params.characterId, // 支持templateId参数
|
||||
sessionId: params.sessionId || null // 支持sessionId参数
|
||||
sessionId: params.sessionId || params.conversationId || null // 支持sessionId参数,conversationId作为备选
|
||||
};
|
||||
|
||||
console.log('发送AI聊天请求,参数:', requestData);
|
||||
@@ -317,7 +342,7 @@ export const chatAPI = {
|
||||
characterId: characterId
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
@@ -329,6 +354,83 @@ export const chatAPI = {
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 清空会话上下文
|
||||
clearSession: async (sessionId) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/api/chat/session/${sessionId}`,
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('清空会话API调用失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取历史消息(根据sessionId查询全部)
|
||||
getHistoryMessages: async (sessionId) => {
|
||||
const loginStatus = checkLoginStatus();
|
||||
|
||||
// 如果用户未登录,直接返回空数组
|
||||
if (!loginStatus.isLoggedIn) {
|
||||
console.log('用户未登录,无法获取历史消息');
|
||||
return {
|
||||
success: true,
|
||||
data: [],
|
||||
isAnonymous: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/app/message/history',
|
||||
method: 'GET',
|
||||
data: {
|
||||
sessionId: sessionId
|
||||
}
|
||||
});
|
||||
|
||||
console.log('历史消息API响应:', response);
|
||||
|
||||
// 处理响应数据
|
||||
let messageList = [];
|
||||
if (response && response.data) {
|
||||
// 直接是数组
|
||||
if (Array.isArray(response.data)) {
|
||||
messageList = response.data;
|
||||
}
|
||||
// 可能嵌套在data字段中
|
||||
else if (response.data.data && Array.isArray(response.data.data)) {
|
||||
messageList = response.data.data;
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
messageList = response;
|
||||
}
|
||||
|
||||
console.log('解析后的历史消息数量:', messageList.length);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: messageList
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取历史消息失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user