优化CozeAPI调用保存逻辑和前端图标修复
- 修复前端TrendingUpOutlined图标导入错误,改为LineChartOutlined - 优化CozeAPI调用记录保存逻辑: * 正确保存创建人和更新人字段为当前用户ID * 正确传递和保存message_id字段 * 新增带messageId的WebSocket聊天方法重载 - 修复WebSocket处理器中的用户消息保存逻辑 - 确保CozeApiCallService正确设置创建人和更新人字段 - 改进AI回复保存时的创建人设置逻辑
This commit is contained in:
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -27,6 +27,16 @@ public interface AiChatService {
|
||||
*/
|
||||
String sendChatMessageForWebSocket(String conversationId, String message, String userId);
|
||||
|
||||
/**
|
||||
* WebSocket方式发送聊天消息(只保存AI回复,带messageId)
|
||||
* @param conversationId 会话ID
|
||||
* @param messageId 用户消息ID
|
||||
* @param message 用户消息内容
|
||||
* @param userId 用户ID
|
||||
* @return AI回复内容
|
||||
*/
|
||||
String sendChatMessageForWebSocket(String conversationId, String messageId, String message, String userId);
|
||||
|
||||
/**
|
||||
* 生成对话总结
|
||||
* @param conversationId 会话ID
|
||||
|
||||
@@ -149,7 +149,7 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
log.info("WebSocket发送聊天消息: conversationId={}, userId={}, message={}", conversationId, userId, message);
|
||||
|
||||
try {
|
||||
// 调用Coze API
|
||||
// 调用Coze API(不带messageId)
|
||||
String aiReply = sendMessage(conversationId, message, userId);
|
||||
|
||||
// 注意:不保存用户消息,因为WebSocket处理器已经保存了
|
||||
@@ -172,6 +172,38 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sendChatMessageForWebSocket(String conversationId, String messageId, String message, String userId) {
|
||||
log.info("WebSocket发送聊天消息(带messageId): conversationId={}, messageId={}, userId={}, message={}",
|
||||
conversationId, messageId, userId, message);
|
||||
|
||||
try {
|
||||
// 调用Coze API(带messageId)
|
||||
String aiReply = sendMessageWithMessageId(conversationId, messageId, message, userId);
|
||||
|
||||
// 注意:不保存用户消息,因为WebSocket处理器已经保存了
|
||||
// 只保存AI回复
|
||||
Message aiMessage = new Message();
|
||||
aiMessage.setConversationId(conversationId);
|
||||
aiMessage.setCreateBy(userId); // 设置创建人为当前用户
|
||||
aiMessage.setContent(aiReply);
|
||||
aiMessage.setType("text");
|
||||
aiMessage.setSender("ai");
|
||||
aiMessage.setCozeRole("assistant");
|
||||
aiMessage.setCozeContentType("text");
|
||||
aiMessage = messageService.createMessage(aiMessage);
|
||||
|
||||
log.info("WebSocket聊天消息处理完成(带messageId): userMessageId={}, aiMessageId={}",
|
||||
messageId, aiMessage.getId());
|
||||
|
||||
return aiReply;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket发送聊天消息失败(带messageId)", e);
|
||||
return "抱歉,我暂时无法回复,请稍后再试。";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateConversationSummary(String conversationId, String userId) {
|
||||
log.info("生成对话总结: conversationId={}, userId={}", conversationId, userId);
|
||||
|
||||
@@ -244,6 +244,8 @@ public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeA
|
||||
.requestBody(requestBody)
|
||||
.status("pending")
|
||||
.startTime(LocalDateTime.now())
|
||||
.createBy(userId) // 设置创建人为当前用户
|
||||
.updateBy(userId) // 设置更新人为当前用户
|
||||
.build();
|
||||
this.save(apiCall);
|
||||
return apiCall;
|
||||
|
||||
@@ -244,6 +244,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
Message userMessage = new Message();
|
||||
userMessage.setConversationId(conversationId);
|
||||
userMessage.setUserId(userId);
|
||||
userMessage.setCreateBy(userId); // 设置创建人为当前用户
|
||||
userMessage
|
||||
.setUserType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
|
||||
userMessage.setContent(request.getContent());
|
||||
@@ -251,11 +252,12 @@ public class WebSocketServiceImpl implements WebSocketService {
|
||||
userMessage.setSender("user");
|
||||
userMessage.setCozeRole("user");
|
||||
userMessage.setCozeContentType("text");
|
||||
messageService.createMessage(userMessage);
|
||||
userMessage = messageService.createMessage(userMessage);
|
||||
|
||||
// 调用AI服务(WebSocket专用方法,不重复保存用户消息)
|
||||
// 调用AI服务(WebSocket专用方法,传递messageId)
|
||||
String aiReply = aiChatService.sendChatMessageForWebSocket(
|
||||
conversationId,
|
||||
userMessage.getId(), // 传递用户消息ID
|
||||
request.getContent(),
|
||||
userId
|
||||
);
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// 动画效果样式文件
|
||||
|
||||
/* 淡入向上动画 */
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.8s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.delay-100 { animation-delay: 0.1s; }
|
||||
.delay-200 { animation-delay: 0.2s; }
|
||||
.delay-300 { animation-delay: 0.3s; }
|
||||
.delay-400 { animation-delay: 0.4s; }
|
||||
.delay-500 { animation-delay: 0.5s; }
|
||||
.delay-600 { animation-delay: 0.6s; }
|
||||
.delay-700 { animation-delay: 0.7s; }
|
||||
.delay-800 { animation-delay: 0.8s; }
|
||||
.delay-900 { animation-delay: 0.9s; }
|
||||
.delay-1000 { animation-delay: 1s; }
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动触发动画 */
|
||||
.scroll-target {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 波浪动画 */
|
||||
.wave {
|
||||
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTQ0MCAxNDciIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPmdyb3VwPC90aXRsZT48ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBpZD0iQ29tcG9uZW50LS0tV2F2ZS1Cb3R0b20iIGZpbGw9IiM0QTkwRTIiPjxwYXRoIGQ9Ik0wLDc0LjgzMjk0MTIgQzM2MCw3NC44MzI5NDEyIDM2MCwxNDcgNzIwLDE0NyBDMTA4MCwxNDcgMTA4MCw3NC44MzI5NDEyIDE0NDAsNzQuODMyOTQxMiBMMTQ0MCwxNDcgTDAsMTQ3IEwwLDc0LjgzMjk0MTIgWiIgaWQ9IldhdmUiIG9wYWNpdHk9IjAuMSI+PC9wYXRoPjwvZz48L2c+PC9zdmc+");
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 200%;
|
||||
height: 147px;
|
||||
animation: wave 15s linear infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-direction: reverse;
|
||||
animation-duration: 20s;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-duration: 25s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: translateX(0); }
|
||||
50% { transform: translateX(-50%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-scale {
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮动画 */
|
||||
.btn-bounce {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.loading-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滑入动画 */
|
||||
.slide-in-left {
|
||||
animation: slide-in-left 0.5s ease-out;
|
||||
}
|
||||
|
||||
.slide-in-right {
|
||||
animation: slide-in-right 0.5s ease-out;
|
||||
}
|
||||
|
||||
.slide-in-down {
|
||||
animation: slide-in-down 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-in-left {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹性动画 */
|
||||
.bounce-in {
|
||||
animation: bounce-in 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 渐变背景动画 */
|
||||
.gradient-animation {
|
||||
background: linear-gradient(-45deg, #4A90E2, #F5A623, #4A90E2, #F5A623);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-shift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
.typewriter {
|
||||
overflow: hidden;
|
||||
border-right: 0.15em solid #4A90E2;
|
||||
white-space: nowrap;
|
||||
margin: 0 auto;
|
||||
letter-spacing: 0.15em;
|
||||
animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-caret {
|
||||
from, to {
|
||||
border-color: transparent;
|
||||
}
|
||||
50% {
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式动画控制 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<header class="app-header" :class="{ 'scrolled': isScrolled }">
|
||||
<div class="header-content">
|
||||
<!-- Logo -->
|
||||
<router-link to="/" class="logo">
|
||||
<svg width="32" height="32" viewBox="0 0 100 100" class="logo-icon">
|
||||
<path fill="currentColor" d="M85.4,37.3C85.4,37.3,85.4,37.3,85.4,37.3c-2.8-9.9-10-17.7-19.1-21.2c-0.2-0.1-0.5-0.1-0.7-0.2c-0.1,0-0.2-0.1-0.3-0.1 c-1.2-0.4-2.5-0.8-3.7-1.1c-1-0.2-2-0.4-3-0.6c-1.1-0.2-2.1-0.3-3.2-0.4c-1.2-0.1-2.4-0.2-3.6-0.2c-0.1,0-0.2,0-0.3,0h-0.1 c-0.1,0-0.2,0-0.3,0c-1.2,0-2.4,0.1-3.6,0.2c-1.1,0.1-2.1,0.2-3.2,0.4c-1,0.2-2,0.4-3,0.6c-1.3,0.3-2.5,0.6-3.7,1.1 c-0.1,0-0.2,0.1-0.3,0.1c-0.2,0.1-0.5,0.1-0.7,0.2C21.6,19.6,14.4,27.4,11.6,37.3c0,0,0,0.1-0.1,0.1C8,47.7,8,58.8,11.5,69.2 c0,0.1,0.1,0.1,0.1,0.2c2.8,9.9,10,17.7,19.1,21.2c0.2,0.1,0.5,0.1,0.7,0.2c0.1,0,0.2,0.1,0.3,0.1c1.2,0.4,2.5,0.8,3.7,1.1 c1,0.2,2,0.4,3,0.6c1.1,0.2,2.1,0.3,3.2,0.4c1.2,0.1,2.4,0.2,3.6,0.2c0.1,0,0.2,0,0.3,0h0.1c0.1,0,0.2,0,0.3,0 c1.2,0,2.4-0.1,3.6-0.2c-1.1-0.1-2.1-0.2-3.2-0.4c1-0.2,2-0.4,3-0.6c1.3-0.3,2.5-0.6,3.7-1.1c0.1,0,0.2-0.1,0.3-0.1 c0.2-0.1,0.5-0.1,0.7-0.2c9.1-3.5,16.3-11.3,19.1-21.2c0-0.1,0.1-0.1,0.1-0.2C89,58.8,89,47.7,85.4,37.3z M50,77.9 c-15.4,0-27.9-12.5-27.9-27.9S34.6,22.1,50,22.1s27.9,12.5,27.9,27.9S65.4,77.9,50,77.9z"></path>
|
||||
<path fill="#F5A623" d="M50,88.8c-21.4,0-38.8-17.4-38.8-38.8S28.6,11.2,50,11.2s38.8,17.4,38.8,38.8S71.4,88.8,50,88.8z M50,16.2 c-18.7,0-33.8,15.1-33.8,33.8S31.3,83.8,50,83.8s33.8-15.1,33.8-33.8S68.7,16.2,50,16.2z"></path>
|
||||
</svg>
|
||||
<span class="logo-text">开心APP</span>
|
||||
</router-link>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu">
|
||||
<router-link to="/chat" class="nav-link">聊天</router-link>
|
||||
<router-link to="/diary" class="nav-link">日记</router-link>
|
||||
<router-link to="/dashboard" class="nav-link">展板</router-link>
|
||||
<nav class="nav-menu" :class="{ 'mobile-hidden': !mobileMenuVisible }">
|
||||
<router-link to="/chat" class="nav-link" @click="closeMobileMenu">聊天</router-link>
|
||||
<router-link to="/diary" class="nav-link" @click="closeMobileMenu">日记</router-link>
|
||||
<router-link to="/dashboard" class="nav-link" @click="closeMobileMenu">展板</router-link>
|
||||
<router-link to="/topic-tracker" class="nav-link" @click="closeMobileMenu">话题追踪</router-link>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
@@ -62,25 +67,66 @@
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<a-button
|
||||
type="text"
|
||||
class="mobile-menu-btn"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
<MenuOutlined v-if="!mobileMenuVisible" />
|
||||
<CloseOutlined v-else />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div v-if="mobileMenuVisible" class="mobile-menu">
|
||||
<nav class="mobile-nav">
|
||||
<router-link to="/chat" class="mobile-nav-link" @click="closeMobileMenu">聊天</router-link>
|
||||
<router-link to="/diary" class="mobile-nav-link" @click="closeMobileMenu">日记</router-link>
|
||||
<router-link to="/dashboard" class="mobile-nav-link" @click="closeMobileMenu">展板</router-link>
|
||||
<router-link to="/topic-tracker" class="mobile-nav-link" @click="closeMobileMenu">话题追踪</router-link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
UserOutlined,
|
||||
DownOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined
|
||||
LogoutOutlined,
|
||||
MenuOutlined,
|
||||
CloseOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 响应式状态
|
||||
const isScrolled = ref(false)
|
||||
const mobileMenuVisible = ref(false)
|
||||
|
||||
// 滚动监听
|
||||
const handleScroll = () => {
|
||||
isScrolled.value = window.scrollY > 50
|
||||
}
|
||||
|
||||
// 移动端菜单控制
|
||||
const toggleMobileMenu = () => {
|
||||
mobileMenuVisible.value = !mobileMenuVisible.value
|
||||
}
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
mobileMenuVisible.value = false
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
@@ -91,42 +137,85 @@
|
||||
message.error('退出登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
handleScroll() // 初始检查
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/assets/styles/variables.scss" as *;
|
||||
.app-header {
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 0;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
}
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.header-content {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
&.scrolled {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
border-bottom-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
padding: 16px 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.logo {
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: #4A90E2;
|
||||
font-weight: bold;
|
||||
color: $tech-blue;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: $tech-blue;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
|
||||
+406
-31
@@ -3,59 +3,72 @@
|
||||
<!-- 头部导航 -->
|
||||
<AppHeader />
|
||||
|
||||
<div style="padding: 100px 20px 20px; background: white; text-align: center;">
|
||||
<h1 style="color: #4A90E2; font-size: 3rem; margin-bottom: 20px;">
|
||||
你好,我是开开
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="wave-background">
|
||||
<div class="wave"></div>
|
||||
<div class="wave"></div>
|
||||
<div class="wave"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-text">
|
||||
<h1 class="hero-title animate-fade-in-up">
|
||||
你好,我是<span class="highlight">开开</span>
|
||||
</h1>
|
||||
<p style="font-size: 1.5rem; color: #888; margin-bottom: 40px;">
|
||||
<p class="hero-subtitle animate-fade-in-up delay-300">
|
||||
你的情绪陪伴使者
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="hero-image animate-fade-in-up delay-500">
|
||||
<img
|
||||
src="https://r2.flowith.net/files/1517c93c-849d-4a9b-94b6-d61aa295a8a1/1752600429516-image-1752600425876-cnlfpkbrh@1024x1024.png"
|
||||
alt="开开"
|
||||
style="width: 300px; height: auto; margin-bottom: 40px; border-radius: 20px;"
|
||||
alt="欢迎姿态的开开"
|
||||
class="kaikai-image"
|
||||
/>
|
||||
|
||||
<div>
|
||||
</div>
|
||||
<div class="hero-action animate-fade-in-up delay-700">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="$router.push('/chat')"
|
||||
style="background: #F5A623; border: none; border-radius: 20px; padding: 12px 32px; font-size: 18px;"
|
||||
class="start-chat-btn"
|
||||
>
|
||||
开始一段对话
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="padding: 80px 20px; background: #F7F8FA;">
|
||||
<div style="text-align: center; margin-bottom: 60px;">
|
||||
<h2 style="font-size: 2rem; color: #333; margin-bottom: 16px;">发现你的专属陪伴</h2>
|
||||
<p style="font-size: 18px; color: #888;">
|
||||
开开博学多才,从不炫耀,愿意用最温柔的方式,陪伴每一个需要倾听的生命。
|
||||
<!-- Features Section -->
|
||||
<section class="features-section">
|
||||
<div class="container">
|
||||
<div class="features-header">
|
||||
<h2 class="features-title scroll-target">核心功能</h2>
|
||||
<p class="features-subtitle scroll-target">
|
||||
开开博学多才、可爱治愈,愿意用最温柔的方式,陪伴每一个需要倾听的生命。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 32px; max-width: 1200px; margin: 0 auto;">
|
||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<h3 style="color: #333; margin-bottom: 16px;">智能对话</h3>
|
||||
<p style="color: #888; line-height: 1.6;">从日常闲聊到情感咨询,开开随时倾听,理解并回应你的每个想法。</p>
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="(feature, index) in features"
|
||||
:key="feature.title"
|
||||
class="feature-card scroll-target"
|
||||
:style="{ animationDelay: `${index * 100}ms` }"
|
||||
>
|
||||
<div class="feature-image-container">
|
||||
<img :src="feature.image" :alt="feature.alt" class="feature-image" />
|
||||
</div>
|
||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<h3 style="color: #333; margin-bottom: 16px;">情绪日记</h3>
|
||||
<p style="color: #888; line-height: 1.6;">记录你的点滴心情与生活,开开会给予温暖的回应。</p>
|
||||
<div class="feature-content">
|
||||
<div class="feature-header">
|
||||
<component :is="feature.icon" class="feature-icon" />
|
||||
<h3 class="feature-title">{{ feature.title }}</h3>
|
||||
</div>
|
||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<h3 style="color: #333; margin-bottom: 16px;">个人展板</h3>
|
||||
<p style="color: #888; line-height: 1.6;">自由定义你的个性标签,构建独一无二的数字人格。</p>
|
||||
</div>
|
||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<h3 style="color: #333; margin-bottom: 16px;">话题追踪</h3>
|
||||
<p style="color: #888; line-height: 1.6;">自动总结你关心的事,助你洞察自我。</p>
|
||||
<p class="feature-description">{{ feature.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 底部 -->
|
||||
<AppFooter />
|
||||
@@ -63,14 +76,376 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { MessageOutlined, BookOutlined, UserOutlined, LineChartOutlined } from '@ant-design/icons-vue'
|
||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||
import AppFooter from '@/components/layout/AppFooter.vue'
|
||||
|
||||
// 功能特性数据
|
||||
const features = [
|
||||
{
|
||||
icon: MessageOutlined,
|
||||
title: '智能对话',
|
||||
description: '从日常闲聊到情感咨询,开开随时倾听,理解并回应你的每个想法,是永不离线的好朋友。',
|
||||
image: 'https://r2.flowith.net/files/o/1752574375721-happy_kaikai_character_design_index_0@1024x1024.png',
|
||||
alt: '开心的开开'
|
||||
},
|
||||
{
|
||||
icon: BookOutlined,
|
||||
title: '情绪日记',
|
||||
description: '记录你的点滴心情与生活,开开会给予温暖的回应。在安全的空间里,回顾与成长。',
|
||||
image: 'https://r2.flowith.net/files/o/1752574488398-kaikai_supportive_comfort_character_index_3@1024x1024.png',
|
||||
alt: '倾听中的开开'
|
||||
},
|
||||
{
|
||||
icon: UserOutlined,
|
||||
title: '个人展板',
|
||||
description: '自由定义你的个性标签,开开还会自动收录你的"精彩语录",构建独一无二的数字人格。',
|
||||
image: 'https://r2.flowith.net/files/o/1752574426392-kaikai_character_working_digital_workspace_index_4@1024x1024.png',
|
||||
alt: '工作中的开开'
|
||||
},
|
||||
{
|
||||
icon: LineChartOutlined,
|
||||
title: '话题追踪',
|
||||
description: '自动总结你关心的事,无论是生活琐事还是工作计划,都用时间线清晰整理,助你洞察自我。',
|
||||
image: 'https://r2.flowith.net/files/o/1752574572161-kaikai_character_energetic_animation_index_2@1024x1024.png',
|
||||
alt: '充满活力的开开'
|
||||
}
|
||||
]
|
||||
|
||||
// 滚动动画观察器
|
||||
let scrollObserver: IntersectionObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化滚动动画
|
||||
initScrollAnimations()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (scrollObserver) {
|
||||
scrollObserver.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
const initScrollAnimations = () => {
|
||||
scrollObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible')
|
||||
scrollObserver?.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
// 观察所有需要动画的元素
|
||||
document.querySelectorAll('.scroll-target').forEach((target) => {
|
||||
scrollObserver?.observe(target)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/assets/styles/variables.scss" as *;
|
||||
.home-page {
|
||||
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
padding: 128px 24px 80px;
|
||||
background: white;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 100px 16px 60px;
|
||||
min-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.2;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.wave {
|
||||
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTQ0MCAxNDciIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPmdyb3VwPC90aXRsZT48ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyBpZD0iQ29tcG9uZW50LS0tV2F2ZS1Cb3R0b20iIGZpbGw9IiM0QTkwRTIiPjxwYXRoIGQ9Ik0wLDc0LjgzMjk0MTIgQzM2MCw3NC44MzI5NDEyIDM2MCwxNDcgNzIwLDE0NyBDMTA4MCwxNDcgMTA4MCw3NC44MzI5NDEyIDE0NDAsNzQuODMyOTQxMiBMMTQ0MCwxNDcgTDAsMTQ3IEwwLDc0LjgzMjk0MTIgWiIgaWQ9IldhdmUiIG9wYWNpdHk9IjAuMSI+PC9wYXRoPjwvZz48L2c+PC9zdmc+");
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 200%;
|
||||
height: 147px;
|
||||
animation: wave 15s linear infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-direction: reverse;
|
||||
animation-duration: 20s;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-duration: 25s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: translateX(0); }
|
||||
50% { transform: translateX(-50%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: $tech-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: $text-medium;
|
||||
margin-bottom: 48px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
margin-bottom: 32px;
|
||||
|
||||
.kaikai-image {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.1));
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.start-chat-btn {
|
||||
background: $warm-orange !important;
|
||||
border: none !important;
|
||||
border-radius: 50px !important;
|
||||
padding: 16px 32px !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: 600 !important;
|
||||
height: auto !important;
|
||||
box-shadow: 0 8px 24px rgba(245, 166, 35, 0.3) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
|
||||
&:hover {
|
||||
background: #e6951f !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 12px 32px rgba(245, 166, 35, 0.4) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 24px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Features Section */
|
||||
.features-section {
|
||||
padding: 80px 24px;
|
||||
background: $light-gray;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.features-header {
|
||||
text-align: center;
|
||||
max-width: 768px;
|
||||
margin: 0 auto 64px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.features-subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: $text-medium;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 32px;
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-image-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, #eef5fe 0%, #f0f8ff 100%);
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M-10 10 C 20 20, 40 0, 60 10 S 100 0, 120 10" stroke="%234A90E2" fill="none" stroke-width="2" stroke-opacity="0.2"/></svg>');
|
||||
background-size: 50px;
|
||||
background-repeat: repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feature-image {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feature-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: $tech-blue;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: $text-medium;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.8s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.delay-300 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.delay-500 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.delay-700 {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-target {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user