优化CozeAPI调用保存逻辑和前端图标修复

- 修复前端TrendingUpOutlined图标导入错误,改为LineChartOutlined
- 优化CozeAPI调用记录保存逻辑:
  * 正确保存创建人和更新人字段为当前用户ID
  * 正确传递和保存message_id字段
  * 新增带messageId的WebSocket聊天方法重载
- 修复WebSocket处理器中的用户消息保存逻辑
- 确保CozeApiCallService正确设置创建人和更新人字段
- 改进AI回复保存时的创建人设置逻辑
This commit is contained in:
2025-07-26 10:46:47 +08:00
parent 0dfabc35d7
commit 6903ac1c0d
8 changed files with 856 additions and 90 deletions
+1 -1
View File
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
);
+256
View File
@@ -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;
}
}
+123 -34
View File
@@ -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 {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(16px);
border-bottom: 1px solid #e8e8e8;
padding: 0;
height: 64px;
line-height: 64px;
.app-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(16px);
border-bottom: 1px solid transparent;
transition: all 0.3s ease;
&.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;
padding: 16px 24px;
max-width: 1200px;
margin: 0 auto;
@media (max-width: 768px) {
padding: 12px 16px;
}
}
.logo {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: $tech-blue;
font-weight: 700;
font-size: 24px;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
.header-content {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.logo {
display: flex;
align-items: center;
text-decoration: none;
color: #4A90E2;
font-weight: bold;
@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;
+426 -51
View File
@@ -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;">
你好我是开开
</h1>
<p style="font-size: 1.5rem; color: #888; margin-bottom: 40px;">
你的情绪陪伴使者
</p>
<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;"
/>
<div>
<a-button
type="primary"
size="large"
@click="$router.push('/chat')"
style="background: #F5A623; border: none; border-radius: 20px; padding: 12px 32px; font-size: 18px;"
>
开始一段对话
</a-button>
<!-- Hero Section -->
<section class="hero-section">
<div class="wave-background">
<div class="wave"></div>
<div class="wave"></div>
<div class="wave"></div>
</div>
</div>
<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;">
开开博学多才从不炫耀愿意用最温柔的方式陪伴每一个需要倾听的生命
</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="hero-content">
<div class="hero-text">
<h1 class="hero-title animate-fade-in-up">
你好我是<span class="highlight">开开</span>
</h1>
<p class="hero-subtitle animate-fade-in-up delay-300">
你的情绪陪伴使者
</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>
<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="欢迎姿态的开开"
class="kaikai-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>
<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="hero-action animate-fade-in-up delay-700">
<a-button
type="primary"
size="large"
@click="$router.push('/chat')"
class="start-chat-btn"
>
开始一段对话
</a-button>
</div>
</div>
</div>
</section>
<!-- 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 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 class="feature-content">
<div class="feature-header">
<component :is="feature.icon" class="feature-icon" />
<h3 class="feature-title">{{ feature.title }}</h3>
</div>
<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 {
min-height: 100vh;
background: #f5f5f5;
.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>