优化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);
|
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
|
* @param conversationId 会话ID
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
log.info("WebSocket发送聊天消息: conversationId={}, userId={}, message={}", conversationId, userId, message);
|
log.info("WebSocket发送聊天消息: conversationId={}, userId={}, message={}", conversationId, userId, message);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用Coze API
|
// 调用Coze API(不带messageId)
|
||||||
String aiReply = sendMessage(conversationId, message, userId);
|
String aiReply = sendMessage(conversationId, message, userId);
|
||||||
|
|
||||||
// 注意:不保存用户消息,因为WebSocket处理器已经保存了
|
// 注意:不保存用户消息,因为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
|
@Override
|
||||||
public String generateConversationSummary(String conversationId, String userId) {
|
public String generateConversationSummary(String conversationId, String userId) {
|
||||||
log.info("生成对话总结: conversationId={}, userId={}", conversationId, userId);
|
log.info("生成对话总结: conversationId={}, userId={}", conversationId, userId);
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeA
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CozeApiCall createApiCall(String conversationId, String messageId, String userId,
|
public CozeApiCall createApiCall(String conversationId, String messageId, String userId,
|
||||||
String requestType, String requestUrl, String requestBody) {
|
String requestType, String requestUrl, String requestBody) {
|
||||||
CozeApiCall apiCall = CozeApiCall.builder()
|
CozeApiCall apiCall = CozeApiCall.builder()
|
||||||
.conversationId(conversationId)
|
.conversationId(conversationId)
|
||||||
@@ -244,6 +244,8 @@ public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeA
|
|||||||
.requestBody(requestBody)
|
.requestBody(requestBody)
|
||||||
.status("pending")
|
.status("pending")
|
||||||
.startTime(LocalDateTime.now())
|
.startTime(LocalDateTime.now())
|
||||||
|
.createBy(userId) // 设置创建人为当前用户
|
||||||
|
.updateBy(userId) // 设置更新人为当前用户
|
||||||
.build();
|
.build();
|
||||||
this.save(apiCall);
|
this.save(apiCall);
|
||||||
return apiCall;
|
return apiCall;
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ public class WebSocketServiceImpl implements WebSocketService {
|
|||||||
Message userMessage = new Message();
|
Message userMessage = new Message();
|
||||||
userMessage.setConversationId(conversationId);
|
userMessage.setConversationId(conversationId);
|
||||||
userMessage.setUserId(userId);
|
userMessage.setUserId(userId);
|
||||||
|
userMessage.setCreateBy(userId); // 设置创建人为当前用户
|
||||||
userMessage
|
userMessage
|
||||||
.setUserType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
|
.setUserType(request.getSenderType() == ChatRequest.SenderType.USER ? "registered" : "guest");
|
||||||
userMessage.setContent(request.getContent());
|
userMessage.setContent(request.getContent());
|
||||||
@@ -251,11 +252,12 @@ public class WebSocketServiceImpl implements WebSocketService {
|
|||||||
userMessage.setSender("user");
|
userMessage.setSender("user");
|
||||||
userMessage.setCozeRole("user");
|
userMessage.setCozeRole("user");
|
||||||
userMessage.setCozeContentType("text");
|
userMessage.setCozeContentType("text");
|
||||||
messageService.createMessage(userMessage);
|
userMessage = messageService.createMessage(userMessage);
|
||||||
|
|
||||||
// 调用AI服务(WebSocket专用方法,不重复保存用户消息)
|
// 调用AI服务(WebSocket专用方法,传递messageId)
|
||||||
String aiReply = aiChatService.sendChatMessageForWebSocket(
|
String aiReply = aiChatService.sendChatMessageForWebSocket(
|
||||||
conversationId,
|
conversationId,
|
||||||
|
userMessage.getId(), // 传递用户消息ID
|
||||||
request.getContent(),
|
request.getContent(),
|
||||||
userId
|
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>
|
<template>
|
||||||
<header class="app-header">
|
<header class="app-header" :class="{ 'scrolled': isScrolled }">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<router-link to="/" class="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>
|
<span class="logo-text">开心APP</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- 导航菜单 -->
|
<!-- 导航菜单 -->
|
||||||
<nav class="nav-menu">
|
<nav class="nav-menu" :class="{ 'mobile-hidden': !mobileMenuVisible }">
|
||||||
<router-link to="/chat" class="nav-link">聊天</router-link>
|
<router-link to="/chat" class="nav-link" @click="closeMobileMenu">聊天</router-link>
|
||||||
<router-link to="/diary" class="nav-link">日记</router-link>
|
<router-link to="/diary" class="nav-link" @click="closeMobileMenu">日记</router-link>
|
||||||
<router-link to="/dashboard" class="nav-link">展板</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>
|
</nav>
|
||||||
|
|
||||||
<!-- 右侧操作区 -->
|
<!-- 右侧操作区 -->
|
||||||
@@ -62,25 +67,66 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 移动端菜单按钮 -->
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
class="mobile-menu-btn"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>
|
||||||
|
<MenuOutlined v-if="!mobileMenuVisible" />
|
||||||
|
<CloseOutlined v-else />
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined
|
LogoutOutlined,
|
||||||
|
MenuOutlined,
|
||||||
|
CloseOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
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 () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -91,42 +137,85 @@
|
|||||||
message.error('退出登录失败')
|
message.error('退出登录失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
handleScroll() // 初始检查
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "@/assets/styles/variables.scss" as *;
|
@use "@/assets/styles/variables.scss" as *;
|
||||||
.app-header {
|
|
||||||
position: fixed;
|
.app-header {
|
||||||
top: 0;
|
position: fixed;
|
||||||
left: 0;
|
top: 0;
|
||||||
right: 0;
|
left: 0;
|
||||||
z-index: 1000;
|
right: 0;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
z-index: 1000;
|
||||||
backdrop-filter: blur(16px);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
border-bottom: 1px solid #e8e8e8;
|
backdrop-filter: blur(16px);
|
||||||
padding: 0;
|
border-bottom: 1px solid transparent;
|
||||||
height: 64px;
|
transition: all 0.3s ease;
|
||||||
line-height: 64px;
|
|
||||||
|
&.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 {
|
@media (max-width: 768px) {
|
||||||
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;
|
|
||||||
font-size: 20px;
|
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 {
|
.nav-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+426
-51
@@ -3,59 +3,72 @@
|
|||||||
<!-- 头部导航 -->
|
<!-- 头部导航 -->
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<div style="padding: 100px 20px 20px; background: white; text-align: center;">
|
<!-- Hero Section -->
|
||||||
<h1 style="color: #4A90E2; font-size: 3rem; margin-bottom: 20px;">
|
<section class="hero-section">
|
||||||
你好,我是开开
|
<div class="wave-background">
|
||||||
</h1>
|
<div class="wave"></div>
|
||||||
<p style="font-size: 1.5rem; color: #888; margin-bottom: 40px;">
|
<div class="wave"></div>
|
||||||
你的情绪陪伴使者
|
<div class="wave"></div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="hero-content">
|
||||||
|
<div class="hero-text">
|
||||||
<div style="padding: 80px 20px; background: #F7F8FA;">
|
<h1 class="hero-title animate-fade-in-up">
|
||||||
<div style="text-align: center; margin-bottom: 60px;">
|
你好,我是<span class="highlight">开开</span>
|
||||||
<h2 style="font-size: 2rem; color: #333; margin-bottom: 16px;">发现你的专属陪伴</h2>
|
</h1>
|
||||||
<p style="font-size: 18px; color: #888;">
|
<p class="hero-subtitle animate-fade-in-up delay-300">
|
||||||
开开博学多才,从不炫耀,愿意用最温柔的方式,陪伴每一个需要倾听的生命。
|
你的情绪陪伴使者
|
||||||
</p>
|
</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>
|
</div>
|
||||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
<div class="hero-image animate-fade-in-up delay-500">
|
||||||
<h3 style="color: #333; margin-bottom: 16px;">情绪日记</h3>
|
<img
|
||||||
<p style="color: #888; line-height: 1.6;">记录你的点滴心情与生活,开开会给予温暖的回应。</p>
|
src="https://r2.flowith.net/files/1517c93c-849d-4a9b-94b6-d61aa295a8a1/1752600429516-image-1752600425876-cnlfpkbrh@1024x1024.png"
|
||||||
|
alt="欢迎姿态的开开"
|
||||||
|
class="kaikai-image"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
<div class="hero-action animate-fade-in-up delay-700">
|
||||||
<h3 style="color: #333; margin-bottom: 16px;">个人展板</h3>
|
<a-button
|
||||||
<p style="color: #888; line-height: 1.6;">自由定义你的个性标签,构建独一无二的数字人格。</p>
|
type="primary"
|
||||||
</div>
|
size="large"
|
||||||
<div style="background: white; padding: 32px; border-radius: 16px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
@click="$router.push('/chat')"
|
||||||
<h3 style="color: #333; margin-bottom: 16px;">话题追踪</h3>
|
class="start-chat-btn"
|
||||||
<p style="color: #888; line-height: 1.6;">自动总结你关心的事,助你洞察自我。</p>
|
>
|
||||||
|
开始一段对话
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</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 />
|
<AppFooter />
|
||||||
@@ -63,14 +76,376 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 AppHeader from '@/components/layout/AppHeader.vue'
|
||||||
import AppFooter from '@/components/layout/AppFooter.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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "@/assets/styles/variables.scss" as *;
|
@use "@/assets/styles/variables.scss" as *;
|
||||||
.home-page {
|
|
||||||
min-height: 100vh;
|
.home-page {
|
||||||
background: #f5f5f5;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user