优化调整

This commit is contained in:
2025-07-26 00:37:18 +08:00
parent 08bbd4df0f
commit 0dfabc35d7
90 changed files with 3594 additions and 2294 deletions
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-lcuiwXuAQ9Ef4NCzeBgangmOvLg2KEjr" />
</component>
</project>
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<data-source source="LOCAL" name="localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
+10
View File
@@ -6,6 +6,11 @@
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.huaweicloud.com/repository/maven/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
@@ -21,5 +26,10 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
</component>
</project>
@@ -3,6 +3,9 @@ package com.emotion.common;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -14,6 +17,9 @@ import java.time.LocalDateTime;
* @date 2025-07-22
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@@ -0,0 +1,45 @@
package com.emotion.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 多线程异步任务线程池配置
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Value("${async.core-pool-size:10}")
private int corePoolSize;
@Value("${async.max-pool-size:50}")
private int maxPoolSize;
@Value("${async.queue-capacity:200}")
private int queueCapacity;
@Value("${async.keep-alive-seconds:60}")
private int keepAliveSeconds;
@Value("${async.thread-name-prefix:single-async-}")
private String threadNamePrefix;
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@@ -8,7 +8,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.scheduling.annotation.Async;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
/**
@@ -29,16 +30,11 @@ public class EmotionSummaryController {
@Operation(summary = "生成用户当天的情绪记录总结", description = "基于用户当天的聊天记录生成情绪分析和记录")
@PostMapping("/generate")
public Result<Map<String, Object>> generateEmotionSummary() {
try {
// 从上下文中获取当前用户ID
String userId = CurrentUserUtil.requireCurrentUserId();
log.info("收到生成情绪记录总结请求: userId={}", userId);
// 调用AI服务生成情绪总结
Map<String, Object> result = aiChatService.generateEmotionSummary(userId);
// 调用AI服务异步生成情绪总结(阻塞获取结果)
Map<String, Object> result = aiChatService.generateEmotionSummaryAsync(userId).get();
if ((Boolean) result.get("success")) {
log.info("情绪记录总结生成成功: userId={}", userId);
return Result.success("情绪记录总结生成成功", result);
@@ -47,7 +43,6 @@ public class EmotionSummaryController {
log.warn("情绪记录总结生成失败: userId={}, message={}", userId, message);
return Result.error(message);
}
} catch (IllegalStateException e) {
log.warn("用户认证失败: {}", e.getMessage());
return Result.error(e.getMessage());
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("achievement")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 评论实体类
@@ -16,7 +16,7 @@ import lombok.NoArgsConstructor;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("comment")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 社区帖子实体类
@@ -16,7 +16,7 @@ import lombok.NoArgsConstructor;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("community_post")
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("conversation")
@@ -3,7 +3,7 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("coze_api_call")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("emotion_analysis")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -19,7 +19,7 @@ import java.time.LocalDate;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("emotion_record")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("growth_topic")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("guest_user")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("location_pin")
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("message")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("reward")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("topic_interaction")
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Builder;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
@@ -2,11 +2,11 @@ package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.emotion.common.BaseEntity;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 用户统计实体类
@@ -16,7 +16,7 @@ import lombok.NoArgsConstructor;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_stats")
@@ -1,6 +1,5 @@
package com.emotion.service;
import java.util.Map;
/**
* AI聊天服务接口
@@ -11,66 +10,101 @@ import java.util.Map;
public interface AiChatService {
/**
* 发送聊天消息
* 发送聊天消息(保存用户消息和AI回复)
* @param conversationId 会话ID
* @param message 用户消息内容
* @param userId 用户ID
* @return AI回复内容
*/
String sendChatMessage(String conversationId, String message, String userId);
/**
* 发送聊天消息(仅获取AI回复,不保存用户消息
* 用于WebSocket场景,避免重复保存用户消息
* WebSocket方式发送聊天消息(只保存AI回复)
* @param conversationId 会话ID
* @param message 用户消息内容
* @param userId 用户ID
* @return AI回复内容
*/
String sendChatMessageForWebSocket(String conversationId, String message, String userId);
/**
* 生成对话总结
* @param conversationId 会话ID
* @param userId 用户ID
* @return 总结内容
*/
String generateConversationSummary(String conversationId, String userId);
/**
* 检查服务是否可用
* 检查AI服务是否可用
* @return 可用返回true,否则false
*/
boolean isServiceAvailable();
/**
* 获取服务状态
* 获取AI服务状态
* @return "available" 或 "unavailable"
*/
String getServiceStatus();
/**
* 发送消息到Coze AI
* 发送消息到Coze AI(不保存消息,仅AI交互)
* @param conversationId 会话ID
* @param userMessage 用户消息内容
* @param userId 用户ID
* @return AI回复内容
*/
String sendMessage(String conversationId, String userMessage, String userId);
/**
* 访客聊天(不需要登录)
* 访客聊天(不登录情况下
* @param message 用户消息内容
* @param clientIp 客户端IP
* @return 包含AI回复等信息的Map
*/
Map<String, Object> guestChat(String message, String clientIp);
java.util.Map<String, Object> guestChat(String message, String clientIp);
/**
* 创建对话
* 创建对话
* @param userId 用户ID
* @param title 对话标题
* @return 包含对话信息的Map
*/
Map<String, Object> createConversation(String userId, String title);
java.util.Map<String, Object> createConversation(String userId, String title);
/**
* 获取访客用户信息
* @param clientIp 客户端IP
* @return 包含访客信息的Map
*/
Map<String, Object> getGuestUserInfo(String clientIp);
java.util.Map<String, Object> getGuestUserInfo(String clientIp);
/**
* 流式聊天
* 流式聊天(暂时降级为普通聊天)
* @param conversationId 会话ID
* @param message 用户消息内容
* @param userId 用户ID
* @return AI回复内容
*/
String streamChat(String conversationId, String message, String userId);
/**
* 健康检查
* @return 健康返回true,否则false
*/
boolean healthCheck();
/**
* 生成用户当天的情绪记录总结
*
* 生成用户情绪记录总结
* @param userId 用户ID
* @return 情绪记录结果
* @return 包含情绪总结等信息的Map
*/
Map<String, Object> generateEmotionSummary(String userId);
java.util.Map<String, Object> generateEmotionSummary(String userId);
/**
* 异步生成用户情绪记录总结
* @param userId 用户ID
* @return 包含情绪总结等信息的CompletableFuture
*/
java.util.concurrent.CompletableFuture<java.util.Map<String, Object>> generateEmotionSummaryAsync(String userId);
}
@@ -24,6 +24,8 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.scheduling.annotation.Async;
import java.util.concurrent.CompletableFuture;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
@@ -34,6 +36,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.Arrays;
/**
* AI聊天服务实现类
@@ -1017,12 +1020,12 @@ public class AiChatServiceImpl implements AiChatService {
String chatHistory = integrateChatHistory(todayMessages);
log.info("聊天记录整合完成,总长度: {}", chatHistory.length());
// 构建情绪分析提示词
String emotionPrompt = buildEmotionAnalysisPrompt(chatHistory);
// Coze 中已经在工作流设置了提示词,目前不需要构建情绪分析提示词
// String emotionPrompt = buildEmotionAnalysisPrompt(chatHistory);
// 调用Coze API进行情绪分析总结
String conversationId = "emotion_summary_" + userId + "_" + today.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String emotionSummary = sendSummaryMessage(conversationId, emotionPrompt, userId);
String emotionSummary = sendSummaryMessage(conversationId, chatHistory, userId);
log.info("情绪分析总结生成完成: {}", emotionSummary);
// 解析AI返回的情绪分析结果
@@ -1049,6 +1052,13 @@ public class AiChatServiceImpl implements AiChatService {
return result;
}
@Override
@Async("taskExecutor")
public CompletableFuture<Map<String, Object>> generateEmotionSummaryAsync(String userId) {
Map<String, Object> result = generateEmotionSummary(userId);
return CompletableFuture.completedFuture(result);
}
/**
* 整合聊天记录
*/
@@ -1106,7 +1116,7 @@ public class AiChatServiceImpl implements AiChatService {
return EmotionAnalysis.builder()
.primaryEmotion(json.getString("primaryEmotion"))
.intensity(BigDecimal.valueOf(json.getDoubleValue("intensity")))
.keywords(json.getString("triggers"))
.keywords(JSON.toJSONString(Arrays.asList(json.getString("triggers"))))
.suggestion(json.getString("suggestions"))
.text(summary)
.polarity(determinePolarity(json.getString("primaryEmotion")))
@@ -1122,7 +1132,7 @@ public class AiChatServiceImpl implements AiChatService {
return EmotionAnalysis.builder()
.primaryEmotion("平静")
.intensity(BigDecimal.valueOf(0.5))
.keywords("日常对话")
.keywords(JSON.toJSONString(Arrays.asList("日常对话")))
.suggestion("保持当前的积极状态")
.text(summary)
.polarity("neutral")
@@ -1192,7 +1202,7 @@ public class AiChatServiceImpl implements AiChatService {
.triggers(analysisResult.getKeywords())
.description(analysisResult.getText())
.notes("基于当天聊天记录自动生成的情绪分析")
.tags("AI分析,聊天记录,情绪总结")
.tags(JSON.toJSONString(Arrays.asList("AI分析", "聊天记录", "情绪总结")))
.build();
emotionRecordService.save(record);
@@ -103,3 +103,10 @@ emotion:
- /api/actuator/**
- /api/websocket/**
- /api/ai/guest/**
async:
core-pool-size: 10
max-pool-size: 50
queue-capacity: 200
keep-alive-seconds: 60
thread-name-prefix: single-async-
@@ -74,7 +74,7 @@ DROP TABLE IF EXISTS user;
-- 1. 用户表 (user)
-- ============================================================================
CREATE TABLE user (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
account VARCHAR(50) UNIQUE COMMENT '账号', -- 账号
password VARCHAR(255) COMMENT '密码(加密后)', -- 密码(加密后)
username VARCHAR(50) UNIQUE COMMENT '用户名', -- 用户名
@@ -101,9 +101,9 @@ CREATE TABLE user (
third_party_id VARCHAR(128) COMMENT '第三方平台ID', -- 第三方平台ID
third_party_type VARCHAR(32) COMMENT '第三方平台类型: wechat, qq, wechat-mp', -- 第三方平台类型: wechat, qq, wechat-mp
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -114,8 +114,8 @@ CREATE TABLE user (
-- 关联说明: user_id 关联 user.id,通过代码逻辑维护关联关系
-- ============================================================================
CREATE TABLE conversation (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(36) COMMENT '用户ID (关联user.id)', -- 用户ID (关联user.id)
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) COMMENT '用户ID (关联user.id)', -- 用户ID (关联user.id)
user_type VARCHAR(20) DEFAULT 'registered' COMMENT '用户类型: registered-注册用户, guest-访客用户', -- 用户类型: registered-注册用户, guest-访客用户
title VARCHAR(200) COMMENT '对话标题', -- 对话标题
type VARCHAR(50) DEFAULT 'emotion_chat' COMMENT '对话类型', -- 对话类型
@@ -143,9 +143,9 @@ CREATE TABLE conversation (
tags JSON COMMENT '标签', -- 标签
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -156,8 +156,8 @@ CREATE TABLE conversation (
-- 关联说明: conversation_id 关联 conversation.id,通过代码逻辑维护关联关系
-- ============================================================================
CREATE TABLE message (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
conversation_id VARCHAR(36) COMMENT '对话ID (关联conversation.id)', -- 对话ID (关联conversation.id)
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
conversation_id VARCHAR(64) COMMENT '对话ID (关联conversation.id)', -- 对话ID (关联conversation.id)
content TEXT COMMENT '消息内容', -- 消息内容
type VARCHAR(50) DEFAULT 'text' COMMENT '消息类型', -- 消息类型
sender VARCHAR(20) COMMENT '发送者: user-用户, assistant-AI助手', -- 发送者: user-用户, assistant-AI助手
@@ -174,17 +174,17 @@ CREATE TABLE message (
total_tokens INT DEFAULT 0 COMMENT '总Token数', -- 总Token数
api_cost DECIMAL(10, 6) DEFAULT 0.000000 COMMENT 'API调用费用', -- API调用费用
is_read TINYINT DEFAULT 0 COMMENT '是否已读: 0-未读, 1-已读', -- 是否已读: 0-未读, 1-已读
parent_message_id VARCHAR(36) COMMENT '父消息ID(用于回复链)', -- 父消息ID(用于回复链)
parent_message_id VARCHAR(64) COMMENT '父消息ID(用于回复链)', -- 父消息ID(用于回复链)
emotion_analysis JSON COMMENT '情绪分析结果', -- 情绪分析结果
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
user_id VARCHAR(36) COMMENT '用户ID (注册用户或访客用户)', -- 用户ID (注册用户或访客用户)
user_id VARCHAR(64) COMMENT '用户ID (注册用户或访客用户)', -- 用户ID (注册用户或访客用户)
user_type VARCHAR(20) COMMENT '用户类型 (registered/guest)', -- 用户类型 (registered/guest)
coze_role VARCHAR(20) COMMENT 'Coze消息角色 (user/assistant/system)', -- Coze消息角色 (user/assistant/system)
coze_content_type VARCHAR(50) COMMENT 'Coze消息内容类型 (text/image/file等)', -- Coze消息内容类型 (text/image/file等)
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -194,15 +194,15 @@ CREATE TABLE message (
-- 4. Coze API调用记录表 (coze_api_call) - 优化版本
-- ============================================================================
CREATE TABLE coze_api_call (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
conversation_id VARCHAR(36) COMMENT '对话ID', -- 对话ID
message_id VARCHAR(36) COMMENT '消息ID', -- 消息ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
conversation_id VARCHAR(64) COMMENT '对话ID', -- 对话ID
message_id VARCHAR(64) COMMENT '消息ID', -- 消息ID
-- Coze API 信息
coze_chat_id VARCHAR(50) COMMENT 'Coze聊天ID', -- Coze聊天ID
coze_conversation_id VARCHAR(50) COMMENT 'Coze对话ID', -- Coze对话ID
bot_id VARCHAR(50) COMMENT 'Bot ID', -- Bot ID
workflow_id VARCHAR(50) COMMENT 'Workflow ID', -- Workflow ID
user_id VARCHAR(36) COMMENT '用户ID', -- 用户ID
user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID
-- 请求信息
request_type VARCHAR(20) COMMENT '请求类型: chat/stream/retrieve/messages', -- 请求类型: chat/stream/retrieve/messages
request_url VARCHAR(500) COMMENT '请求URL', -- 请求URL
@@ -246,9 +246,9 @@ CREATE TABLE coze_api_call (
trace_id VARCHAR(100) COMMENT '追踪ID', -- 追踪ID
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -258,9 +258,9 @@ CREATE TABLE coze_api_call (
-- 5. 情绪分析表 (emotion_analysis)
-- ============================================================================
CREATE TABLE emotion_analysis (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(36) COMMENT '用户ID', -- 用户ID
message_id VARCHAR(36) COMMENT '关联消息ID', -- 关联消息ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID
message_id VARCHAR(64) COMMENT '关联消息ID', -- 关联消息ID
text TEXT COMMENT '分析文本', -- 分析文本
primary_emotion VARCHAR(50) COMMENT '主要情绪', -- 主要情绪
intensity DECIMAL(3, 2) COMMENT '情绪强度', -- 情绪强度
@@ -272,9 +272,9 @@ CREATE TABLE emotion_analysis (
analysis_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '分析时间', -- 分析时间
metadata JSON COMMENT '扩展元数据', -- 扩展元数据
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -284,8 +284,8 @@ CREATE TABLE emotion_analysis (
-- 6. 情绪记录表 (emotion_record)
-- ============================================================================
CREATE TABLE emotion_record (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(36) COMMENT '用户ID', -- 用户ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID
record_date DATE COMMENT '记录日期', -- 记录日期
emotion_type VARCHAR(50) COMMENT '情绪类型', -- 情绪类型
intensity DECIMAL(3, 2) COMMENT '情绪强度', -- 情绪强度
@@ -298,9 +298,9 @@ CREATE TABLE emotion_record (
people VARCHAR(200) COMMENT '相关人物', -- 相关人物
notes TEXT COMMENT '备注', -- 备注
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -310,7 +310,7 @@ CREATE TABLE emotion_record (
-- 7. 成长课题表 (growth_topic)
-- ============================================================================
CREATE TABLE growth_topic (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
title VARCHAR(100) COMMENT '课题标题', -- 课题标题
category VARCHAR(50) COMMENT '分类', -- 分类
difficulty VARCHAR(20) COMMENT '难度: easy-简单, medium-中等, hard-困难', -- 难度: easy-简单, medium-中等, hard-困难
@@ -323,9 +323,9 @@ CREATE TABLE growth_topic (
completed_time DATETIME COMMENT '完成时间', -- 完成时间
rewards JSON COMMENT '奖励', -- 奖励
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -335,8 +335,8 @@ CREATE TABLE growth_topic (
-- 8. 课题互动表 (topic_interaction)
-- ============================================================================
CREATE TABLE topic_interaction (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
topic_id VARCHAR(36) COMMENT '课题ID', -- 课题ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID
type VARCHAR(50) COMMENT '互动类型', -- 互动类型
content TEXT COMMENT '内容', -- 内容
user_input TEXT COMMENT '用户输入', -- 用户输入
@@ -345,9 +345,9 @@ CREATE TABLE topic_interaction (
feedback TEXT COMMENT '反馈', -- 反馈
completed_time DATETIME COMMENT '完成时间', -- 完成时间
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -357,7 +357,7 @@ CREATE TABLE topic_interaction (
-- 9. 地点标记表 (location_pin)
-- ============================================================================
CREATE TABLE location_pin (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
name VARCHAR(100) COMMENT '地点名称', -- 地点名称
type VARCHAR(50) COMMENT '地点类型', -- 地点类型
category VARCHAR(50) COMMENT '地点分类', -- 地点分类
@@ -365,15 +365,15 @@ CREATE TABLE location_pin (
longitude DECIMAL(11, 8) COMMENT '经度', -- 经度
address VARCHAR(200) COMMENT '地址', -- 地址
description TEXT COMMENT '描述', -- 描述
created_by VARCHAR(36) COMMENT '创建者', -- 创建者
created_by VARCHAR(64) COMMENT '创建者', -- 创建者
likes INT DEFAULT 0 COMMENT '点赞数', -- 点赞数
visits INT DEFAULT 0 COMMENT '访问数', -- 访问数
is_bookmarked TINYINT DEFAULT 0 COMMENT '是否收藏', -- 是否收藏
last_visit_time DATETIME COMMENT '最后访问时间', -- 最后访问时间
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -383,9 +383,9 @@ CREATE TABLE location_pin (
-- 10. 社区帖子表 (community_post)
-- ============================================================================
CREATE TABLE community_post (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(36) COMMENT '用户ID', -- 用户ID
location_id VARCHAR(36) COMMENT '地点ID', -- 地点ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID
location_id VARCHAR(64) COMMENT '地点ID', -- 地点ID
title VARCHAR(200) COMMENT '标题', -- 标题
content TEXT COMMENT '内容', -- 内容
type VARCHAR(50) COMMENT '帖子类型', -- 帖子类型
@@ -396,9 +396,9 @@ CREATE TABLE community_post (
comment_count INT DEFAULT 0 COMMENT '评论数', -- 评论数
is_private TINYINT DEFAULT 0 COMMENT '是否私密', -- 是否私密
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -408,16 +408,16 @@ CREATE TABLE community_post (
-- 11. 评论表 (comment)
-- ============================================================================
CREATE TABLE comment (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
post_id VARCHAR(36) COMMENT '帖子ID', -- 帖子ID
user_id VARCHAR(36) COMMENT '用户ID', -- 用户ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
post_id VARCHAR(64) COMMENT '帖子ID', -- 帖子ID
user_id VARCHAR(64) COMMENT '用户ID', -- 用户ID
content TEXT COMMENT '评论内容', -- 评论内容
reply_to_id VARCHAR(36) COMMENT '回复的评论ID', -- 回复的评论ID
reply_to_id VARCHAR(64) COMMENT '回复的评论ID', -- 回复的评论ID
likes INT DEFAULT 0 COMMENT '点赞数', -- 点赞数
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -427,7 +427,7 @@ CREATE TABLE comment (
-- 12. 成就表 (achievement)
-- ============================================================================
CREATE TABLE achievement (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
title VARCHAR(100) COMMENT '成就标题', -- 成就标题
description TEXT COMMENT '描述', -- 描述
category VARCHAR(50) COMMENT '分类', -- 分类
@@ -440,9 +440,9 @@ CREATE TABLE achievement (
progress DECIMAL(5, 2) DEFAULT 0.00 COMMENT '进度', -- 进度
is_hidden TINYINT DEFAULT 0 COMMENT '是否隐藏', -- 是否隐藏
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -452,9 +452,9 @@ CREATE TABLE achievement (
-- 13. 奖励表 (reward)
-- ============================================================================
CREATE TABLE reward (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
topic_id VARCHAR(36) COMMENT '课题ID', -- 课题ID
achievement_id VARCHAR(36) COMMENT '成就ID', -- 成就ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
topic_id VARCHAR(64) COMMENT '课题ID', -- 课题ID
achievement_id VARCHAR(64) COMMENT '成就ID', -- 成就ID
type VARCHAR(50) COMMENT '奖励类型', -- 奖励类型
name VARCHAR(100) COMMENT '奖励名称', -- 奖励名称
description TEXT COMMENT '描述', -- 描述
@@ -464,9 +464,9 @@ CREATE TABLE reward (
earned_time DATETIME COMMENT '获得时间', -- 获得时间
is_new TINYINT DEFAULT 1 COMMENT '是否新获得', -- 是否新获得
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -476,7 +476,7 @@ CREATE TABLE reward (
-- 14. 访客用户表 (guest_user)
-- ============================================================================
CREATE TABLE guest_user (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
guest_user_id VARCHAR(50) UNIQUE COMMENT '访客用户ID (格式: guest_xxx)', -- 访客用户ID (格式: guest_xxx)
ip_address VARCHAR(45) COMMENT '客户端IP地址 (支持IPv6)', -- 客户端IP地址 (支持IPv6)
user_agent TEXT COMMENT '用户代理信息', -- 用户代理信息
@@ -488,9 +488,9 @@ CREATE TABLE guest_user (
location VARCHAR(100) COMMENT 'IP地址的地理位置信息', -- IP地址的地理位置信息
device_info VARCHAR(200) COMMENT '设备信息', -- 设备信息
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
@@ -500,8 +500,8 @@ CREATE TABLE guest_user (
-- 15. 用户统计表 (user_stats)
-- ============================================================================
CREATE TABLE user_stats (
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(36) UNIQUE COMMENT '用户ID', -- 用户ID
id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键
user_id VARCHAR(64) UNIQUE COMMENT '用户ID', -- 用户ID
total_conversations INT DEFAULT 0 COMMENT '总对话数', -- 总对话数
total_messages INT DEFAULT 0 COMMENT '总消息数', -- 总消息数
total_emotions_recorded INT DEFAULT 0 COMMENT '总情绪记录数', -- 总情绪记录数
@@ -516,9 +516,9 @@ CREATE TABLE user_stats (
likes_received INT DEFAULT 0 COMMENT '收到的点赞数', -- 收到的点赞数
social_interactions INT DEFAULT 0 COMMENT '社交互动数', -- 社交互动数
-- 公共字段
create_by VARCHAR(36) COMMENT '创建人ID', -- 创建人ID
create_by VARCHAR(64) COMMENT '创建人ID', -- 创建人ID
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 创建时间
update_by VARCHAR(36) COMMENT '更新人ID', -- 更新人ID
update_by VARCHAR(64) COMMENT '更新人ID', -- 更新人ID
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -- 更新时间
is_deleted TINYINT DEFAULT 0 COMMENT '是否删除: 0-未删除, 1-已删除', -- 是否删除: 0-未删除, 1-已删除
remarks VARCHAR(500) COMMENT '备注' -- 备注
-87
View File
@@ -1,87 +0,0 @@
# 聊天记录加载问题修复总结
## 问题描述
用户在聊天页面右上角点击聊天记录按钮时,无法正确加载聊天记录。
## 问题根源分析
通过代码分析发现问题的根本原因:
### 1. 拦截器配置问题
- **WebMvcConfig**: 配置了JwtAuthInterceptor,但只拦截`/api/**`路径
- **WebConfig**: 配置了AuthInterceptor和UserContextInterceptor,拦截`/**`路径
- **MessageController**: 路径是`/message`,不是`/api/message`
### 2. 用户上下文设置问题
- **AuthInterceptor**: 验证token并将用户信息存储到请求属性中
- **UserContextInterceptor**: 负责设置UserContextHolder,但没有从请求属性中获取用户信息
- **CurrentUserUtil**: 从UserContextHolder中获取用户ID,但UserContextHolder中没有正确的用户信息
## 修复方案
### 1. 前端错误处理改进
**文件**: `web/src/views/Chat/index.vue`
-`loadHistoryMessages``searchHistoryMessages`方法中添加详细的日志输出
- 改进错误处理,区分不同类型的错误(401认证失败、500服务器错误等)
**文件**: `web/src/services/api.ts`
- 改进响应拦截器的错误处理
- 为业务错误码401添加特殊处理
- 增加详细的错误日志输出
### 2. 后端认证链修复
**文件**: `backend-single/src/main/java/com/emotion/interceptor/AuthInterceptor.java`
- 添加从token中获取username并存储到请求属性中
**文件**: `backend-single/src/main/java/com/emotion/interceptor/UserContextInterceptor.java`
- 修改`getUserIdFromRequest`方法,优先从请求属性中获取用户ID
- 修改`getUsernameFromRequest`方法,优先从请求属性中获取用户名
### 3. 后端错误处理改进
**文件**: `backend-single/src/main/java/com/emotion/controller/MessageController.java`
- 在所有用户消息相关的API中添加详细的日志输出
- 改进异常处理,区分认证失败和其他错误
- 返回更友好的错误信息
## 修复后的认证流程
1. **请求到达**: 用户发送带有Authorization头的请求到`/message/user/page`
2. **AuthInterceptor处理**:
- 验证JWT token
- 从token中提取userId和username
- 将用户信息存储到请求属性中
3. **UserContextInterceptor处理**:
- 从请求属性中获取用户信息
- 设置到UserContextHolder中
4. **Controller处理**:
- 通过CurrentUserUtil.requireCurrentUserId()获取用户ID
- 查询用户的聊天记录
- 返回结果
## 测试验证
### 1. 使用测试工具
- 打开`test-chat-history-api.html`
- 输入有效的JWT token
- 测试各个API接口
### 2. 前端测试
- 登录用户账号
- 点击聊天页面右上角的聊天记录按钮
- 检查浏览器控制台的日志输出
- 验证聊天记录是否正确加载
### 3. 后端日志检查
- 查看后端日志中的用户认证和上下文设置信息
- 确认用户ID正确传递到Controller层
## 预期结果
- 聊天记录弹窗能够正确打开
- 用户的历史消息能够正确加载和显示
- 搜索功能正常工作
- 分页功能正常工作
## 注意事项
1. 确保前端localStorage中有有效的token
2. 确保后端服务正常运行
3. 如果仍有问题,检查数据库中是否有用户的消息记录
4. 可以使用提供的测试工具进行API级别的调试
-147
View File
@@ -1,147 +0,0 @@
# 聊天记录调试指南
## 问题总结
1. **Controller层优化完成**
- 移除了业务逻辑,业务代码移到Service层
- 使用专门的Request和Response DTO
- 统一使用Result返回格式
2. **前端调用问题修复**
- 添加了缺失的`useUserStore`导入
- 修正了API调用方法(搜索和最近消息改为POST)
## 调试步骤
### 1. 检查前端基础环境
在浏览器控制台运行:
```javascript
// 检查token
console.log('Token:', localStorage.getItem('token'))
// 检查用户信息
console.log('User store:', window.Vue?.config?.globalProperties?.$stores?.user)
// 检查API基础配置
console.log('API Base URL:', 'http://localhost:8080') // 根据实际情况修改
```
### 2. 手动测试API调用
在浏览器控制台运行:
```javascript
// 测试获取用户消息分页
fetch('/message/user/page?current=1&size=5', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(data => console.log('分页API结果:', data))
.catch(err => console.error('分页API错误:', err))
// 测试搜索消息
fetch('/message/user/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ keyword: '测试', limit: 5 })
})
.then(res => res.json())
.then(data => console.log('搜索API结果:', data))
.catch(err => console.error('搜索API错误:', err))
// 测试获取最近消息
fetch('/message/user/recent', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ limit: 5 })
})
.then(res => res.json())
.then(data => console.log('最近消息API结果:', data))
.catch(err => console.error('最近消息API错误:', err))
```
### 3. 检查聊天记录按钮事件
在聊天页面控制台运行:
```javascript
// 查找聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn')
console.log('聊天记录按钮:', historyButton)
// 检查点击事件
if (historyButton) {
historyButton.addEventListener('click', () => {
console.log('聊天记录按钮被点击')
})
}
// 检查抽屉状态
const drawer = document.querySelector('.history-drawer')
console.log('聊天记录抽屉:', drawer)
```
### 4. 检查Vue组件状态
在聊天页面控制台运行:
```javascript
// 检查Vue实例
const app = document.querySelector('#app').__vue__
console.log('Vue app:', app)
// 检查聊天记录相关的响应式数据
console.log('showHistory:', app?.setupState?.showHistory?.value)
console.log('historyMessages:', app?.setupState?.historyMessages?.value)
console.log('historyLoading:', app?.setupState?.historyLoading?.value)
```
## 常见问题排查
### 1. Token问题
- 检查localStorage中是否有token
- 检查token是否过期
- 检查token格式是否正确
### 2. 网络请求问题
- 检查浏览器Network面板
- 检查是否有CORS错误
- 检查API路径是否正确
### 3. 后端认证问题
- 检查后端日志中的认证信息
- 检查UserContextHolder是否正确设置
- 检查CurrentUserUtil.requireCurrentUserId()是否抛出异常
### 4. 前端组件问题
- 检查Vue组件是否正确挂载
- 检查响应式数据是否正确初始化
- 检查事件绑定是否正确
## 修复后的API接口
### 后端接口
- `GET /message/user/page` - 获取用户消息分页
- `POST /message/user/search` - 搜索用户消息
- `POST /message/user/recent` - 获取用户最近消息
### 前端调用
```javascript
// 获取分页消息
messageApi.getUserMessages(1, 20)
// 搜索消息
messageApi.searchUserMessages('关键词', 50)
// 获取最近消息
messageApi.getRecentMessages(10)
```
## 预期结果
- 点击聊天记录按钮后,抽屉正常打开
- 控制台显示API调用日志
- 聊天记录正确加载和显示
- 搜索功能正常工作
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "EmotionMuseum",
"name": "emotion-museun",
"lockfileVersion": 2,
"requires": true,
"packages": {
-226
View File
@@ -1,226 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天记录API测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.test-button {
background: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.test-button:hover {
background: #40a9ff;
}
.result {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
font-family: monospace;
}
.error {
background: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: inline-block;
width: 100px;
font-weight: bold;
}
.input-group input {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
</style>
</head>
<body>
<h1>聊天记录API测试工具</h1>
<div class="test-section">
<h3>认证信息</h3>
<div class="input-group">
<label>Token:</label>
<input type="text" id="tokenInput" placeholder="请输入JWT Token">
</div>
<div class="input-group">
<label>API Base:</label>
<input type="text" id="apiBaseInput" value="http://localhost:8080" placeholder="API基础URL">
</div>
<button class="test-button" onclick="loadTokenFromStorage()">从localStorage加载Token</button>
</div>
<div class="test-section">
<h3>API测试</h3>
<button class="test-button" onclick="testGetUserMessages()">测试获取用户消息分页</button>
<button class="test-button" onclick="testSearchMessages()">测试搜索消息</button>
<button class="test-button" onclick="testGetRecentMessages()">测试获取最近消息</button>
<button class="test-button" onclick="testCurrentUser()">测试获取当前用户信息</button>
<div class="input-group">
<label>搜索关键词:</label>
<input type="text" id="searchKeyword" value="测试" placeholder="搜索关键词">
</div>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div id="testResult" class="result">等待测试...</div>
</div>
<script>
function getToken() {
return document.getElementById('tokenInput').value.trim();
}
function getApiBase() {
return document.getElementById('apiBaseInput').value.trim();
}
function loadTokenFromStorage() {
const token = localStorage.getItem('token');
if (token) {
document.getElementById('tokenInput').value = token;
showResult('Token已从localStorage加载', 'success');
} else {
showResult('localStorage中没有找到token', 'error');
}
}
function showResult(message, type = 'success') {
const resultDiv = document.getElementById('testResult');
resultDiv.textContent = message;
resultDiv.className = `result ${type}`;
}
async function makeRequest(url, options = {}) {
const token = getToken();
const apiBase = getApiBase();
if (!token) {
throw new Error('请先输入Token');
}
const fullUrl = `${apiBase}${url}`;
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
console.log('发送请求:', { url: fullUrl, headers });
const response = await fetch(fullUrl, {
...options,
headers
});
const data = await response.json();
console.log('响应数据:', { status: response.status, data });
return { status: response.status, data };
}
async function testGetUserMessages() {
try {
showResult('正在测试获取用户消息分页...', 'success');
const result = await makeRequest('/message/user/page?current=1&size=10');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testSearchMessages() {
try {
const keyword = document.getElementById('searchKeyword').value.trim();
if (!keyword) {
showResult('请输入搜索关键词', 'error');
return;
}
showResult('正在测试搜索消息...', 'success');
const result = await makeRequest(`/message/user/search?keyword=${encodeURIComponent(keyword)}&limit=10`);
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testGetRecentMessages() {
try {
showResult('正在测试获取最近消息...', 'success');
const result = await makeRequest('/message/user/recent?limit=10');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
async function testCurrentUser() {
try {
showResult('正在测试获取当前用户信息...', 'success');
const result = await makeRequest('/user/current');
const message = `状态码: ${result.status}
响应数据: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`错误: ${error.message}`, 'error');
}
}
// 页面加载时自动尝试加载token
window.onload = function() {
loadTokenFromStorage();
};
</script>
</body>
</html>
-238
View File
@@ -1,238 +0,0 @@
// 完整的聊天记录功能测试脚本
// 在聊天页面的浏览器控制台中运行
(function() {
console.log('=== 聊天记录功能完整测试 ===');
// 1. 检查基础环境
function checkBasicEnvironment() {
console.log('\n1. 检查基础环境:');
const token = localStorage.getItem('token');
console.log('- Token存在:', !!token);
if (!token) {
console.error('❌ 没有找到token,请先登录');
return false;
}
console.log('- 当前页面:', window.location.pathname);
console.log('- Token长度:', token.length);
return true;
}
// 2. 测试API调用
async function testAPIEndpoints() {
console.log('\n2. 测试API端点:');
const token = localStorage.getItem('token');
const baseURL = window.location.origin; // 使用当前域名
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
// 测试分页API
try {
console.log('- 测试分页API...');
const pageResponse = await fetch(`${baseURL}/message/user/page?current=1&size=5`, {
method: 'GET',
headers
});
console.log(' 状态码:', pageResponse.status);
const pageData = await pageResponse.json();
console.log(' 响应数据:', pageData);
if (pageResponse.ok && pageData.code === 200) {
console.log(' ✅ 分页API成功');
} else {
console.log(' ❌ 分页API失败:', pageData.message);
}
} catch (error) {
console.log(' ❌ 分页API错误:', error.message);
}
// 测试搜索API
try {
console.log('- 测试搜索API...');
const searchResponse = await fetch(`${baseURL}/message/user/search`, {
method: 'POST',
headers,
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
console.log(' 状态码:', searchResponse.status);
const searchData = await searchResponse.json();
console.log(' 响应数据:', searchData);
if (searchResponse.ok && searchData.code === 200) {
console.log(' ✅ 搜索API成功');
} else {
console.log(' ❌ 搜索API失败:', searchData.message);
}
} catch (error) {
console.log(' ❌ 搜索API错误:', error.message);
}
// 测试最近消息API
try {
console.log('- 测试最近消息API...');
const recentResponse = await fetch(`${baseURL}/message/user/recent`, {
method: 'POST',
headers,
body: JSON.stringify({ limit: 5 })
});
console.log(' 状态码:', recentResponse.status);
const recentData = await recentResponse.json();
console.log(' 响应数据:', recentData);
if (recentResponse.ok && recentData.code === 200) {
console.log(' ✅ 最近消息API成功');
} else {
console.log(' ❌ 最近消息API失败:', recentData.message);
}
} catch (error) {
console.log(' ❌ 最近消息API错误:', error.message);
}
}
// 3. 检查前端组件
function checkFrontendComponents() {
console.log('\n3. 检查前端组件:');
// 检查聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn');
console.log('- 聊天记录按钮:', !!historyButton);
if (!historyButton) {
console.log(' ❌ 未找到聊天记录按钮');
return false;
}
// 检查Vue实例
const app = document.querySelector('#app');
const vueInstance = app?.__vue__ || app?._vnode?.component?.ctx;
console.log('- Vue实例:', !!vueInstance);
if (vueInstance) {
// 检查响应式数据
const setupState = vueInstance.setupState || vueInstance.$data;
console.log('- showHistory:', setupState?.showHistory?.value);
console.log('- historyLoading:', setupState?.historyLoading?.value);
console.log('- historyMessages长度:', setupState?.historyMessages?.value?.length || 0);
}
return true;
}
// 4. 模拟点击测试
function simulateClickTest() {
console.log('\n4. 模拟点击测试:');
const historyButton = document.querySelector('.header-right .action-btn');
if (!historyButton) {
console.log('❌ 无法进行点击测试,按钮不存在');
return;
}
console.log('- 模拟点击聊天记录按钮...');
// 添加事件监听器来监控点击
let clickDetected = false;
const clickHandler = () => {
clickDetected = true;
console.log(' ✅ 检测到点击事件');
};
historyButton.addEventListener('click', clickHandler, { once: true });
// 模拟点击
historyButton.click();
// 检查抽屉是否打开
setTimeout(() => {
const drawer = document.querySelector('.ant-drawer');
const drawerVisible = drawer && !drawer.classList.contains('ant-drawer-hidden');
console.log('- 抽屉是否可见:', drawerVisible);
if (clickDetected && drawerVisible) {
console.log(' ✅ 聊天记录功能正常');
} else {
console.log(' ❌ 聊天记录功能异常');
if (!clickDetected) console.log(' - 点击事件未触发');
if (!drawerVisible) console.log(' - 抽屉未显示');
}
// 清理事件监听器
historyButton.removeEventListener('click', clickHandler);
}, 1000);
}
// 5. 检查网络请求
function monitorNetworkRequests() {
console.log('\n5. 监控网络请求:');
// 重写fetch来监控请求
const originalFetch = window.fetch;
const requests = [];
window.fetch = function(...args) {
const url = args[0];
if (typeof url === 'string' && url.includes('/message/user/')) {
console.log('- 检测到消息API请求:', url);
requests.push({ url, timestamp: Date.now() });
}
return originalFetch.apply(this, args);
};
// 5秒后恢复原始fetch并报告结果
setTimeout(() => {
window.fetch = originalFetch;
console.log('- 监控期间的API请求数量:', requests.length);
requests.forEach(req => {
console.log(` ${new Date(req.timestamp).toLocaleTimeString()}: ${req.url}`);
});
}, 5000);
}
// 主测试函数
async function runCompleteTest() {
try {
// 检查基础环境
if (!checkBasicEnvironment()) {
return;
}
// 开始监控网络请求
monitorNetworkRequests();
// 测试API端点
await testAPIEndpoints();
// 检查前端组件
checkFrontendComponents();
// 模拟点击测试
simulateClickTest();
console.log('\n=== 测试完成 ===');
console.log('请查看上述结果,如果API测试成功但前端功能异常,请检查:');
console.log('1. Vue组件是否正确挂载');
console.log('2. 事件绑定是否正确');
console.log('3. 响应式数据是否正确更新');
console.log('4. 是否有JavaScript错误');
} catch (error) {
console.error('测试过程中发生错误:', error);
}
}
// 运行测试
runCompleteTest();
})();
-69
View File
@@ -1,69 +0,0 @@
# 重复消息问题修复测试
## 问题描述
用户在聊天页面发送一条消息时,数据库中保存了两条相同内容的用户消息:
1. 第一条:通过WebSocket处理器保存,包含完整的用户信息
2. 第二条:通过REST API保存,缺少部分用户信息
## 根本原因
前端`sendMessage`方法中存在双重保存机制:
1. WebSocket发送 - 后端WebSocket处理器会保存消息
2. REST API调用 - 前端额外调用`chatApi.createMessage`保存消息
## 修复方案
1. **前端修复**:移除前端`chat.ts``sendMessage`方法里的`chatApi.createMessage`调用,只保留WebSocket发送
2. **后端修复**:创建专门的WebSocket方法`sendChatMessageForWebSocket`,避免重复保存用户消息
## 修复内容
### 前端修改
文件:`web/src/stores/chat.ts`
- 移除了第69-82行的REST API调用
- 添加了注释说明修复原因
- 保留WebSocket发送逻辑
### 后端修改
文件:`backend-single/src/main/java/com/emotion/service/AiChatService.java`
- 新增`sendChatMessageForWebSocket`方法接口
文件:`backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java`
- 实现`sendChatMessageForWebSocket`方法,只保存AI回复,不保存用户消息
文件:`backend-single/src/main/java/com/emotion/service/WebSocketService.java`
- 修改WebSocket处理器调用新的`sendChatMessageForWebSocket`方法
## 测试步骤
1. 启动后端服务
2. 启动前端服务
3. 登录用户账号
4. 发送一条测试消息
5. 检查数据库中是否只有一条用户消息记录
## 预期结果
- 数据库中只保存一条用户消息
- 消息包含完整的用户信息(user_id, user_type, coze_role等)
- AI回复正常工作
- 前端显示正常
## 验证SQL
```sql
-- 查看最新的消息记录
SELECT id, conversation_id, content, sender, user_id, user_type, coze_role, create_by, create_time
FROM message
WHERE conversation_id = '你的会话ID'
ORDER BY create_time DESC
LIMIT 10;
-- 检查是否还有重复消息
SELECT content, COUNT(*) as count
FROM message
WHERE conversation_id = '你的会话ID'
AND sender = 'user'
AND create_time > '2025-07-25 16:00:00'
GROUP BY content
HAVING COUNT(*) > 1;
```
## 注意事项
- 此修复只影响新发送的消息
- 历史重复消息需要手动清理
- 确保WebSocket连接正常工作
-130
View File
@@ -1,130 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>消息API测试</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.test-button { background: #1890ff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; }
.result { background: #f5f5f5; padding: 10px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap; font-family: monospace; }
.error { background: #fff2f0; border: 1px solid #ffccc7; color: #ff4d4f; }
.success { background: #f6ffed; border: 1px solid #b7eb8f; color: #52c41a; }
</style>
</head>
<body>
<h1>消息API测试</h1>
<div class="test-section">
<h3>Token设置</h3>
<input type="text" id="tokenInput" placeholder="请输入JWT Token" style="width: 500px; padding: 5px;">
<button class="test-button" onclick="loadToken()">从localStorage加载</button>
</div>
<div class="test-section">
<h3>API测试</h3>
<button class="test-button" onclick="testGetUserMessages()">测试分页查询</button>
<button class="test-button" onclick="testSearchMessages()">测试搜索消息</button>
<button class="test-button" onclick="testRecentMessages()">测试最近消息</button>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div id="result" class="result">等待测试...</div>
</div>
<script>
function loadToken() {
const token = localStorage.getItem('token');
if (token) {
document.getElementById('tokenInput').value = token;
showResult('Token已加载', 'success');
} else {
showResult('localStorage中没有token', 'error');
}
}
function showResult(message, type = 'success') {
const resultDiv = document.getElementById('result');
resultDiv.textContent = message;
resultDiv.className = `result ${type}`;
}
async function makeRequest(url, options = {}) {
const token = document.getElementById('tokenInput').value.trim();
if (!token) {
throw new Error('请先输入Token');
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await fetch(url, { ...options, headers });
const data = await response.json();
return { status: response.status, data };
}
async function testGetUserMessages() {
try {
showResult('正在测试分页查询...', 'success');
const result = await makeRequest('/message/user/page?current=1&size=5');
const message = `分页查询结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`分页查询错误: ${error.message}`, 'error');
}
}
async function testSearchMessages() {
try {
showResult('正在测试搜索消息...', 'success');
const result = await makeRequest('/message/user/search', {
method: 'POST',
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
const message = `搜索消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`搜索消息错误: ${error.message}`, 'error');
}
}
async function testRecentMessages() {
try {
showResult('正在测试最近消息...', 'success');
const result = await makeRequest('/message/user/recent', {
method: 'POST',
body: JSON.stringify({ limit: 5 })
});
const message = `最近消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`最近消息错误: ${error.message}`, 'error');
}
}
// 页面加载时自动加载token
window.onload = loadToken;
</script>
</body>
</html>
-191
View File
@@ -1,191 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>消息历史API测试</title>
<style>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; }
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.test-button { background: #1890ff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; }
.result { background: #f5f5f5; padding: 10px; border-radius: 4px; margin-top: 10px; white-space: pre-wrap; font-family: monospace; max-height: 400px; overflow-y: auto; }
.error { background: #fff2f0; border: 1px solid #ffccc7; color: #ff4d4f; }
.success { background: #f6ffed; border: 1px solid #b7eb8f; color: #52c41a; }
input { padding: 5px; margin: 5px; width: 300px; }
</style>
</head>
<body>
<h1>消息历史API测试</h1>
<div class="test-section">
<h3>Token设置</h3>
<input type="text" id="tokenInput" placeholder="请输入JWT Token">
<button class="test-button" onclick="loadToken()">从localStorage加载</button>
<button class="test-button" onclick="saveToken()">保存到localStorage</button>
</div>
<div class="test-section">
<h3>API测试</h3>
<button class="test-button" onclick="testGetUserMessages()">测试分页查询</button>
<button class="test-button" onclick="testSearchMessages()">测试搜索消息</button>
<button class="test-button" onclick="testRecentMessages()">测试最近消息</button>
<button class="test-button" onclick="testAllAPIs()">测试所有API</button>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div id="result" class="result">等待测试...</div>
</div>
<script>
function loadToken() {
const token = localStorage.getItem('token');
if (token) {
document.getElementById('tokenInput').value = token;
showResult('Token已加载', 'success');
} else {
showResult('localStorage中没有token', 'error');
}
}
function saveToken() {
const token = document.getElementById('tokenInput').value.trim();
if (token) {
localStorage.setItem('token', token);
showResult('Token已保存到localStorage', 'success');
} else {
showResult('请先输入Token', 'error');
}
}
function showResult(message, type = 'success') {
const resultDiv = document.getElementById('result');
resultDiv.textContent = message;
resultDiv.className = `result ${type}`;
}
async function makeRequest(url, options = {}) {
const token = document.getElementById('tokenInput').value.trim();
if (!token) {
throw new Error('请先输入Token');
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await fetch(url, { ...options, headers });
const data = await response.json();
return { status: response.status, data };
}
async function testGetUserMessages() {
try {
showResult('正在测试分页查询...', 'success');
const result = await makeRequest('/api/message/user/page?current=1&size=5');
const message = `分页查询结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`分页查询错误: ${error.message}`, 'error');
}
}
async function testSearchMessages() {
try {
showResult('正在测试搜索消息...', 'success');
const result = await makeRequest('/api/message/user/search', {
method: 'POST',
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
const message = `搜索消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`搜索消息错误: ${error.message}`, 'error');
}
}
async function testRecentMessages() {
try {
showResult('正在测试最近消息...', 'success');
const result = await makeRequest('/api/message/user/recent', {
method: 'POST',
body: JSON.stringify({ limit: 5 })
});
const message = `最近消息结果:
状态码: ${result.status}
响应: ${JSON.stringify(result.data, null, 2)}`;
showResult(message, result.status === 200 ? 'success' : 'error');
} catch (error) {
showResult(`最近消息错误: ${error.message}`, 'error');
}
}
async function testAllAPIs() {
try {
showResult('正在测试所有API...', 'success');
const results = [];
// 测试分页查询
try {
const pageResult = await makeRequest('/api/message/user/page?current=1&size=5');
results.push(`✅ 分页查询: ${pageResult.status} - ${pageResult.data.message || 'OK'}`);
} catch (error) {
results.push(`❌ 分页查询: ${error.message}`);
}
// 测试搜索
try {
const searchResult = await makeRequest('/api/message/user/search', {
method: 'POST',
body: JSON.stringify({ keyword: '测试', limit: 5 })
});
results.push(`✅ 搜索消息: ${searchResult.status} - ${searchResult.data.message || 'OK'}`);
} catch (error) {
results.push(`❌ 搜索消息: ${error.message}`);
}
// 测试最近消息
try {
const recentResult = await makeRequest('/api/message/user/recent', {
method: 'POST',
body: JSON.stringify({ limit: 5 })
});
results.push(`✅ 最近消息: ${recentResult.status} - ${recentResult.data.message || 'OK'}`);
} catch (error) {
results.push(`❌ 最近消息: ${error.message}`);
}
const message = `所有API测试结果:
${results.join('\n')}
详细信息请查看浏览器控制台`;
showResult(message, 'success');
} catch (error) {
showResult(`测试过程中发生错误: ${error.message}`, 'error');
}
}
// 页面加载时自动加载token
window.onload = loadToken;
</script>
</body>
</html>
-174
View File
@@ -1,174 +0,0 @@
// 聊天记录修复验证脚本
// 在浏览器控制台中运行此脚本来验证修复效果
(function() {
console.log('=== 聊天记录修复验证脚本 ===');
// 检查基础环境
function checkEnvironment() {
console.log('\n1. 检查基础环境:');
// 检查token
const token = localStorage.getItem('token');
console.log('- Token存在:', !!token);
if (token) {
console.log('- Token长度:', token.length);
console.log('- Token前缀:', token.substring(0, 20) + '...');
}
// 检查当前页面
console.log('- 当前页面:', window.location.pathname);
// 检查API基础URL
const apiBase = 'http://localhost:8080'; // 根据实际情况修改
console.log('- API基础URL:', apiBase);
return { token, apiBase };
}
// 测试API调用
async function testAPI(url, token, apiBase) {
try {
console.log(`\n测试API: ${url}`);
const response = await fetch(`${apiBase}${url}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('- 响应状态:', response.status);
console.log('- 响应状态文本:', response.statusText);
const data = await response.json();
console.log('- 响应数据:', data);
if (response.ok && data.code === 200) {
console.log('✅ API调用成功');
return { success: true, data: data.data };
} else {
console.log('❌ API调用失败:', data.message || '未知错误');
return { success: false, error: data.message || '未知错误' };
}
} catch (error) {
console.log('❌ 网络错误:', error.message);
return { success: false, error: error.message };
}
}
// 测试所有聊天记录相关API
async function testChatHistoryAPIs() {
console.log('\n2. 测试聊天记录API:');
const { token, apiBase } = checkEnvironment();
if (!token) {
console.log('❌ 没有找到token,请先登录');
return;
}
// 测试获取用户消息分页
const pageResult = await testAPI('/message/user/page?current=1&size=5', token, apiBase);
// 测试搜索消息
const searchResult = await testAPI('/message/user/search?keyword=测试&limit=5', token, apiBase);
// 测试获取最近消息
const recentResult = await testAPI('/message/user/recent?limit=5', token, apiBase);
// 测试获取当前用户信息
const userResult = await testAPI('/user/current', token, apiBase);
// 汇总结果
console.log('\n3. 测试结果汇总:');
console.log('- 分页查询:', pageResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 搜索功能:', searchResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 最近消息:', recentResult.success ? '✅ 成功' : '❌ 失败');
console.log('- 用户信息:', userResult.success ? '✅ 成功' : '❌ 失败');
// 如果有成功的结果,显示数据统计
if (pageResult.success && pageResult.data) {
console.log('\n4. 数据统计:');
console.log('- 总消息数:', pageResult.data.total || 0);
console.log('- 当前页消息数:', pageResult.data.records ? pageResult.data.records.length : 0);
if (pageResult.data.records && pageResult.data.records.length > 0) {
const firstMessage = pageResult.data.records[0];
console.log('- 最新消息预览:', {
id: firstMessage.id,
content: firstMessage.content ? firstMessage.content.substring(0, 50) + '...' : '',
sender: firstMessage.sender,
createTime: firstMessage.createTime
});
}
}
return {
pageResult,
searchResult,
recentResult,
userResult
};
}
// 测试前端聊天记录功能
function testFrontendChatHistory() {
console.log('\n5. 测试前端聊天记录功能:');
// 检查是否在聊天页面
if (!window.location.pathname.includes('/chat')) {
console.log('❌ 当前不在聊天页面,请先进入聊天页面');
return;
}
// 查找聊天记录按钮
const historyButton = document.querySelector('.header-right .action-btn');
if (historyButton) {
console.log('✅ 找到聊天记录按钮');
// 模拟点击
console.log('- 模拟点击聊天记录按钮...');
historyButton.click();
// 检查抽屉是否打开
setTimeout(() => {
const drawer = document.querySelector('.history-drawer');
if (drawer && drawer.style.display !== 'none') {
console.log('✅ 聊天记录抽屉已打开');
} else {
console.log('❌ 聊天记录抽屉未打开');
}
}, 1000);
} else {
console.log('❌ 未找到聊天记录按钮');
}
}
// 主函数
async function main() {
try {
// 测试API
const apiResults = await testChatHistoryAPIs();
// 测试前端功能
testFrontendChatHistory();
console.log('\n=== 验证完成 ===');
console.log('如果API测试都成功,但前端仍有问题,请检查:');
console.log('1. 浏览器控制台是否有JavaScript错误');
console.log('2. 网络请求是否正常发送');
console.log('3. 前端代码是否正确处理API响应');
} catch (error) {
console.error('验证过程中发生错误:', error);
}
}
// 运行验证
main();
})();
-210
View File
@@ -1,210 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket调试页面</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.section h3 {
margin-top: 0;
color: #333;
}
.info {
background-color: #e7f3ff;
border-color: #b3d9ff;
}
.success {
background-color: #e7f5e7;
border-color: #b3d9b3;
}
.error {
background-color: #ffe7e7;
border-color: #ffb3b3;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 10px;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>WebSocket调试页面</h1>
<div class="section info">
<h3>当前状态</h3>
<p><strong>Token:</strong> <span id="tokenStatus">检查中...</span></p>
<p><strong>用户信息:</strong> <span id="userInfo">检查中...</span></p>
<p><strong>WebSocket状态:</strong> <span id="wsStatus">未连接</span></p>
</div>
<div class="section">
<h3>操作</h3>
<button onclick="checkAuth()">检查认证状态</button>
<button onclick="connectWebSocket()">连接WebSocket</button>
<button onclick="sendTestMessage()">发送测试消息</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div class="section">
<h3>日志</h3>
<div id="log" class="log"></div>
</div>
</div>
<script>
let ws = null;
function log(message) {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function checkAuth() {
log('检查认证状态...');
// 检查localStorage中的token
const token = localStorage.getItem('token');
const userInfo = localStorage.getItem('userInfo');
document.getElementById('tokenStatus').textContent = token ? `存在 (${token.substring(0, 20)}...)` : '不存在';
document.getElementById('userInfo').textContent = userInfo ? JSON.parse(userInfo).username || '未知用户' : '未登录';
log(`Token: ${token ? '存在' : '不存在'}`);
log(`用户信息: ${userInfo ? JSON.parse(userInfo).username || '未知' : '未登录'}`);
if (!token) {
log('警告: 没有找到token,需要先登录');
}
}
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) {
log('WebSocket已连接');
return;
}
log('开始连接WebSocket...');
const token = localStorage.getItem('token');
const userInfo = localStorage.getItem('userInfo');
const userId = userInfo ? JSON.parse(userInfo).id : `guest_${Date.now()}`;
log(`使用用户ID: ${userId}`);
log(`使用Token: ${token ? '是' : '否'}`);
// 使用SockJS和STOMP
const socket = new SockJS('http://localhost:19089/ws/chat');
const stompClient = Stomp.over(socket);
// 禁用调试日志
stompClient.debug = null;
const connectHeaders = {
'X-User-Id': userId
};
if (token) {
connectHeaders['Authorization'] = `Bearer ${token}`;
}
log(`连接头: ${JSON.stringify(connectHeaders)}`);
stompClient.connect(
connectHeaders,
function(frame) {
log('WebSocket连接成功!');
document.getElementById('wsStatus').textContent = '已连接';
ws = stompClient;
// 订阅消息
stompClient.subscribe('/user/queue/messages', function(message) {
const wsMessage = JSON.parse(message.body);
log(`收到消息: ${JSON.stringify(wsMessage)}`);
});
log('已订阅 /user/queue/messages');
},
function(error) {
log(`WebSocket连接失败: ${error}`);
document.getElementById('wsStatus').textContent = '连接失败';
}
);
}
function sendTestMessage() {
if (!ws || ws.readyState !== 1) {
log('WebSocket未连接,无法发送消息');
return;
}
const userInfo = localStorage.getItem('userInfo');
const userId = userInfo ? JSON.parse(userInfo).id : `guest_${Date.now()}`;
const chatRequest = {
content: '这是一条测试消息',
senderId: userId,
senderType: userId.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
conversationId: 'test-conversation',
timestamp: Date.now()
};
log(`发送消息: ${JSON.stringify(chatRequest)}`);
ws.send('/app/chat.send', {}, JSON.stringify(chatRequest));
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// 页面加载时检查状态
window.onload = function() {
checkAuth();
};
</script>
<!-- 引入SockJS和STOMP -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@6/bundles/stomp.umd.min.js"></script>
</body>
</html>
+3 -3
View File
@@ -26,15 +26,15 @@
"@types/stompjs": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.3.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.0",
"sass": "^1.66.0",
"sass": "^1.89.2",
"typescript": "^5.1.0",
"vite": "^4.4.0",
"vite": "^4.5.0",
"vue-tsc": "^3.0.4"
},
"engines": {
+4 -3
View File
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:silent": "vue-tsc && vite build 2>&1 | findstr /v \"Deprecation Warning\"",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
@@ -30,15 +31,15 @@
"@types/stompjs": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.3.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.0",
"sass": "^1.66.0",
"sass": "^1.89.2",
"typescript": "^5.1.0",
"vite": "^4.4.0",
"vite": "^4.5.0",
"vue-tsc": "^3.0.4"
},
"engines": {
+79 -79
View File
@@ -46,95 +46,95 @@
})
</script>
<style>
#app {
min-height: 100vh;
background-color: #f5f5f5;
<style lang="scss">
#app {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 自定义Ant Design样式 */
.ant-btn {
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
}
/* 自定义Ant Design样式 */
.ant-btn {
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
}
&.ant-btn-primary {
background: linear-gradient(135deg, #4a90e2 0%, #5ba0f2 100%);
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
background: linear-gradient(135deg, #5ba0f2 0%, #6bb0ff 100%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
}
&.ant-btn-orange {
background: linear-gradient(135deg, #ff7849 0%, #ff8859 100%);
border: none;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
background: linear-gradient(135deg, #ff8859 0%, #ff9869 100%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
color: white;
}
}
}
.ant-card {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
&.ant-btn-primary {
background: linear-gradient(135deg, #4a90e2 0%, #5ba0f2 100%);
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
}
.ant-input,
.ant-input-affix-wrapper {
border-radius: 12px;
border: 1px solid #e8e8e8;
transition: all 0.3s ease;
&:hover,
&:focus,
&.ant-input-affix-wrapper-focused {
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.1);
}
}
.ant-message {
.ant-message-notice-content {
border-radius: 12px;
background: linear-gradient(135deg, #5ba0f2 0%, #6bb0ff 100%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
}
/* 滚动条美化 */
.ant-layout-content {
&::-webkit-scrollbar {
width: 6px;
}
&.ant-btn-orange {
background: linear-gradient(135deg, #ff7849 0%, #ff8859 100%);
border: none;
color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.2);
}
&:hover {
background: linear-gradient(135deg, #ff8859 0%, #ff9869 100%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
color: white;
}
}
}
.ant-card {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
border: none;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
}
.ant-input,
.ant-input-affix-wrapper {
border-radius: 12px;
border: 1px solid #e8e8e8;
transition: all 0.3s ease;
&:hover,
&:focus,
&.ant-input-affix-wrapper-focused {
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.1);
}
}
.ant-message {
.ant-message-notice-content {
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
}
/* 滚动条美化 */
.ant-layout-content {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.2);
}
}
}
</style>
+1
View File
@@ -1,3 +1,4 @@
@use "@/assets/styles/variables.scss" as *;
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
/* 全局重置 */
+1 -1
View File
@@ -1 +1 @@
/* 空文件 - 解决构建问题 */
@use "@/assets/styles/variables.scss" as *;
+2 -1
View File
@@ -26,7 +26,8 @@
// 简化版Footer组件
</script>
<style scoped>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.app-footer {
margin-top: auto;
}
+1
View File
@@ -94,6 +94,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.app-header {
position: fixed;
top: 0;
+1 -9
View File
@@ -110,15 +110,7 @@ const routes: RouteRecordRaw[] = [
requiresAuth: false
}
},
{
path: '/token-test',
name: 'TokenTest',
component: () => import('@/views/TokenTest.vue'),
meta: {
title: 'Token测试',
requiresAuth: false
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
+1 -1
View File
@@ -50,7 +50,7 @@ api.interceptors.response.use(
window.location.href = '/login'
}
const error = new Error(data.message || '请求失败')
const error = new Error(data.message || '请求失败') as any
error.response = response
return Promise.reject(error)
}
-7
View File
@@ -166,13 +166,6 @@ export const useChatStore = defineStore('chat', () => {
)
}
// 分割AI回复为多条消息
const splitAiReply = (content: string): string[] => {
// 先按 \n\n 分割,再按 \n 分割
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
return segments
}
// 添加AI回复消息(直接显示完整内容)
const addAiReplyMessages = (content: string) => {
// 停止输入状态
+1
View File
@@ -19,6 +19,7 @@ export interface ChatMessage {
sessionId?: string
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
error?: string
sender?: string
}
// 聊天会话类型
+1
View File
@@ -373,6 +373,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.chat-history-page {
min-height: 100vh;
background: $light-gray;
+19 -49
View File
@@ -31,9 +31,6 @@
</div>
<div class="header-right">
<a-button type="text" @click="testAPI" class="action-btn" style="margin-right: 8px;">
测试API
</a-button>
<a-button type="text" @click="openHistoryDrawer" class="action-btn">
<HistoryOutlined />
</a-button>
@@ -444,7 +441,8 @@
showEmotionSummaryResult(result)
} catch (error) {
console.error('生成情绪记录时发生错误:', error)
const err = error as any;
console.error('生成情绪记录时发生错误:', err)
alert('生成情绪记录失败,请检查网络连接')
} finally {
emotionSummaryLoading.value = false
@@ -497,39 +495,7 @@
loadHistoryMessages(1)
}
// 测试API调用
const testAPI = async () => {
console.log('=== 开始测试API ===')
try {
// 测试原始API调用
console.log('1. 测试原始axios调用...')
const token = localStorage.getItem('token')
console.log('Token:', token ? `${token.substring(0, 20)}...` : 'null')
const response = await fetch('/api/message/user/page?current=1&size=5', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
console.log('原始响应状态:', response.status)
const rawData = await response.json()
console.log('原始响应数据:', rawData)
// 测试封装的API调用
console.log('2. 测试封装的API调用...')
const apiData = await messageApi.getUserMessages(1, 5)
console.log('封装API返回数据:', apiData)
} catch (error) {
console.error('API测试失败:', error)
}
console.log('=== API测试完成 ===')
}
// 加载历史记录
const loadHistoryMessages = async (page = 1) => {
@@ -577,22 +543,23 @@
})
} catch (error) {
console.error('加载历史记录时发生错误:', error)
const err = error as any;
console.error('加载历史记录时发生错误:', err)
console.error('错误详情:', {
message: error.message,
response: error.response,
status: error.response?.status,
data: error.response?.data
message: err.message,
response: err.response,
status: err.response?.status,
data: err.response?.data
})
// 显示用户友好的错误信息
if (error.response?.status === 401) {
if (err.response?.status === 401) {
console.log('认证失败,可能需要重新登录')
// 可以在这里添加跳转到登录页的逻辑
} else if (error.response?.status === 500) {
} else if (err.response?.status === 500) {
console.log('服务器错误,请稍后重试')
} else {
console.log('未知错误:', error.message)
console.log('未知错误:', err.message)
}
} finally {
console.log('加载历史记录完成,设置 loading 为 false')
@@ -625,10 +592,11 @@
})
} catch (error) {
console.error('搜索历史记录时发生错误:', error)
const err = error as any;
console.error('搜索历史记录时发生错误:', err)
// 显示用户友好的错误信息
if (error.response?.status === 401) {
if (err.response?.status === 401) {
console.log('认证失败,搜索功能需要登录')
}
} finally {
@@ -677,7 +645,7 @@
// 将最近的消息添加到聊天记录中
if (recentMessages && recentMessages.length > 0) {
// 转换为聊天消息格式
const chatMessages = recentMessages.map(msg => ({
const chatMessages = recentMessages.map((msg: any) => ({
id: msg.id,
content: msg.content,
sender: msg.sender === 'user' ? 'user' : 'ai',
@@ -686,7 +654,7 @@
}))
// 按时间顺序排列(最新的在最后)
chatMessages.sort((a, b) => a.timestamp - b.timestamp)
chatMessages.sort((a: any, b: any) => a.timestamp - b.timestamp)
// 添加到消息列表
chatStore.messages.push(...chatMessages)
@@ -694,7 +662,8 @@
console.log('加载最近聊天记录成功:', chatMessages.length, '条')
}
} catch (error) {
console.error('加载最近聊天记录失败:', error)
const err = error as any;
console.error('加载最近聊天记录失败:', err)
}
}
@@ -716,6 +685,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.chat-page {
display: flex;
flex-direction: column;
+1
View File
@@ -487,6 +487,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.dashboard-page {
min-height: 100vh;
background: $light-gray;
+1
View File
@@ -387,6 +387,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.diary-page {
min-height: 100vh;
background: $light-gray;
+2 -1
View File
@@ -67,7 +67,8 @@
import AppFooter from '@/components/layout/AppFooter.vue'
</script>
<style scoped>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.home-page {
min-height: 100vh;
background: #f5f5f5;
+1
View File
@@ -513,6 +513,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.life-trajectory-page {
min-height: 100vh;
background: $light-gray;
+1
View File
@@ -198,6 +198,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+1
View File
@@ -363,6 +363,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.messages-page {
min-height: 100vh;
background: $light-gray;
+1
View File
@@ -32,6 +32,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.not-found-page {
min-height: 100vh;
display: flex;
+1
View File
@@ -346,6 +346,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.profile-page {
min-height: 100vh;
background: #f5f5f5;
+1
View File
@@ -203,6 +203,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.register-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+1
View File
@@ -406,6 +406,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.settings-page {
min-height: 100vh;
background: $light-gray;
-185
View File
@@ -1,185 +0,0 @@
<template>
<div class="token-test">
<a-card title="Token和身份验证测试">
<div class="test-section">
<h3>当前状态</h3>
<a-descriptions :column="1" bordered>
<a-descriptions-item label="登录状态">
<a-tag :color="userStore.isLoggedIn ? 'green' : 'red'">
{{ userStore.isLoggedIn ? '已登录' : '未登录' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="Token">
<a-typography-text :code="true" :copyable="true">
{{ userStore.token || '无' }}
</a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="用户信息">
<pre>{{ JSON.stringify(userStore.userInfo || userStore.user, null, 2) }}</pre>
</a-descriptions-item>
<a-descriptions-item label="WebSocket状态">
<a-tag :color="chatStore.wsConnected ? 'green' : 'red'">
{{ chatStore.wsConnected ? '已连接' : '未连接' }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
</div>
<div class="test-section">
<h3>操作测试</h3>
<a-space direction="vertical" style="width: 100%">
<a-button type="primary" @click="testLogin" :loading="loginLoading">
测试登录
</a-button>
<a-button @click="testWebSocketConnect" :loading="wsLoading">
测试WebSocket连接
</a-button>
<a-button @click="testSendMessage" :disabled="!chatStore.wsConnected">
发送测试消息
</a-button>
<a-button @click="checkLocalStorage">
检查本地存储
</a-button>
<a-button @click="testApiCall" :loading="apiLoading">
测试API调用
</a-button>
</a-space>
</div>
<div class="test-section">
<h3>测试结果</h3>
<a-textarea
v-model:value="testResults"
:rows="10"
readonly
placeholder="测试结果将显示在这里..."
/>
<a-button @click="clearResults" style="margin-top: 8px">
清空结果
</a-button>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore, useChatStore } from '@/stores'
import { request } from '@/services/api'
const userStore = useUserStore()
const chatStore = useChatStore()
const loginLoading = ref(false)
const wsLoading = ref(false)
const apiLoading = ref(false)
const testResults = ref('')
const addResult = (message: string) => {
const timestamp = new Date().toLocaleTimeString()
testResults.value += `[${timestamp}] ${message}\n`
}
const testLogin = async () => {
loginLoading.value = true
try {
addResult('开始测试登录...')
const result = await userStore.loginWithAuth({
account: 'test@example.com',
password: '123456',
captcha: '1234'
})
addResult(`登录成功: ${JSON.stringify(result)}`)
addResult(`Token: ${userStore.token}`)
addResult(`用户信息: ${JSON.stringify(userStore.userInfo)}`)
} catch (error: any) {
addResult(`登录失败: ${error.message}`)
} finally {
loginLoading.value = false
}
}
const testWebSocketConnect = async () => {
wsLoading.value = true
try {
addResult('开始测试WebSocket连接...')
await chatStore.connectWebSocket()
addResult(`WebSocket连接状态: ${chatStore.wsConnected}`)
addResult(`连接状态: ${chatStore.connectionStatus}`)
} catch (error: any) {
addResult(`WebSocket连接失败: ${error.message}`)
} finally {
wsLoading.value = false
}
}
const testSendMessage = async () => {
try {
addResult('发送测试消息...')
await chatStore.sendMessage('这是一条测试消息,用于验证用户身份识别')
addResult('消息发送成功')
} catch (error: any) {
addResult(`消息发送失败: ${error.message}`)
}
}
const checkLocalStorage = () => {
addResult('检查本地存储...')
addResult(`localStorage.token: ${localStorage.getItem('token')}`)
addResult(`localStorage.userInfo: ${localStorage.getItem('userInfo')}`)
}
const testApiCall = async () => {
apiLoading.value = true
try {
addResult('测试API调用...')
const response = await request.get('/health')
addResult(`API调用成功: ${JSON.stringify(response)}`)
} catch (error: any) {
addResult(`API调用失败: ${error.message}`)
} finally {
apiLoading.value = false
}
}
const clearResults = () => {
testResults.value = ''
}
</script>
<style lang="scss" scoped>
.token-test {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.test-section {
margin-bottom: 24px;
h3 {
margin-bottom: 16px;
color: #1890ff;
}
}
pre {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
</style>
+1
View File
@@ -468,6 +468,7 @@
</script>
<style lang="scss" scoped>
@use "@/assets/styles/variables.scss" as *;
.topic-tracker-page {
min-height: 100vh;
background: $light-gray;
-333
View File
@@ -1,333 +0,0 @@
<template>
<div class="websocket-test">
<div class="test-container">
<h1>WebSocket连接测试</h1>
<!-- 连接状态 -->
<div class="status-section">
<h3>连接状态</h3>
<div class="status-info">
<span class="status-label">状态:</span>
<span
class="status-value"
:class="{
'connected': chatStore.wsConnected,
'connecting': chatStore.connectionStatus === 'CONNECTING',
'disconnected': !chatStore.wsConnected
}"
>
{{ getConnectionStatusText() }}
</span>
</div>
<div class="status-actions">
<a-button
type="primary"
@click="chatStore.connectWebSocket()"
:loading="chatStore.connectionStatus === 'CONNECTING'"
:disabled="chatStore.wsConnected"
>
连接
</a-button>
<a-button
@click="chatStore.disconnectWebSocket()"
:disabled="!chatStore.wsConnected"
>
断开
</a-button>
</div>
</div>
<!-- 消息测试 -->
<div class="message-section">
<h3>消息测试</h3>
<div class="message-input">
<a-input
v-model:value="testMessage"
placeholder="输入测试消息..."
@press-enter="sendTestMessage"
:disabled="!chatStore.wsConnected"
/>
<a-button
type="primary"
@click="sendTestMessage"
:disabled="!chatStore.wsConnected || !testMessage.trim()"
>
发送
</a-button>
</div>
</div>
<!-- 消息历史 -->
<div class="messages-section">
<h3>消息历史</h3>
<div class="messages-list">
<div
v-for="message in messages"
:key="message.id"
class="message-item"
:class="{ 'user': message.type === 'user', 'ai': message.type === 'ai' }"
>
<div class="message-header">
<span class="message-sender">{{ message.type === 'user' ? '用户' : 'AI' }}</span>
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
</div>
<div class="message-content">{{ message.content }}</div>
</div>
</div>
<div class="messages-actions">
<a-button @click="clearMessages">清空消息</a-button>
</div>
</div>
<!-- 配置信息 -->
<div class="config-section">
<h3>配置信息</h3>
<div class="config-info">
<div class="config-item">
<span class="config-label">WebSocket URL:</span>
<span class="config-value">{{ wsUrl }}</span>
</div>
<div class="config-item">
<span class="config-label">用户ID:</span>
<span class="config-value">{{ userId }}</span>
</div>
<div class="config-item">
<span class="config-label">会话ID:</span>
<span class="config-value">{{ chatStore.currentSession?.id || '未设置' }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useChatStore } from '@/stores/chat'
import { useUserStore } from '@/stores/user'
import dayjs from 'dayjs'
const chatStore = useChatStore()
const userStore = useUserStore()
const testMessage = ref('')
const messages = ref<Array<{
id: string
type: 'user' | 'ai'
content: string
timestamp: number
}>>([])
const wsUrl = computed(() => import.meta.env.VITE_WS_URL)
const userId = computed(() => userStore.user?.id || `guest_${Date.now()}`)
// 获取连接状态文本
const getConnectionStatusText = () => {
switch (chatStore.connectionStatus) {
case 'CONNECTED':
return '已连接'
case 'CONNECTING':
return '连接中...'
case 'DISCONNECTED':
return '已断开'
case 'ERROR':
return '连接错误'
default:
return '未知状态'
}
}
// 发送测试消息
const sendTestMessage = () => {
if (!testMessage.value.trim() || !chatStore.wsConnected) return
const message = {
id: Date.now().toString(),
type: 'user' as const,
content: testMessage.value.trim(),
timestamp: Date.now()
}
messages.value.push(message)
chatStore.sendMessage(testMessage.value.trim())
testMessage.value = ''
}
// 清空消息
const clearMessages = () => {
messages.value = []
chatStore.clearMessages()
}
// 格式化时间
const formatTime = (timestamp: number) => {
return dayjs(timestamp).format('HH:mm:ss')
}
// 监听AI回复
const handleAiMessage = (content: string) => {
const message = {
id: Date.now().toString(),
type: 'ai' as const,
content,
timestamp: Date.now()
}
messages.value.push(message)
}
onMounted(() => {
// 监听聊天store中的消息变化
chatStore.$subscribe((mutation, _state) => {
if (mutation.events && Array.isArray(mutation.events)) {
mutation.events.forEach((event: any) => {
if (event.key === 'messages' && event.type === 'add') {
const newMessage = event.newValue
if (newMessage && newMessage.type === 'ai') {
handleAiMessage(newMessage.content)
}
}
})
}
})
})
onUnmounted(() => {
chatStore.disconnectWebSocket()
})
</script>
<style lang="scss" scoped>
.websocket-test {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.test-container {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-section,
.message-section,
.messages-section,
.config-section {
margin-bottom: 32px;
h3 {
margin-bottom: 16px;
color: #1890ff;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 8px;
}
}
.status-info {
margin-bottom: 16px;
.status-label {
font-weight: 500;
margin-right: 8px;
}
.status-value {
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
&.connected {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
&.connecting {
background: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
&.disconnected {
background: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
}
}
.status-actions {
display: flex;
gap: 12px;
}
.message-input {
display: flex;
gap: 12px;
.ant-input {
flex: 1;
}
}
.messages-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
.message-item {
margin-bottom: 12px;
padding: 8px;
border-radius: 4px;
&.user {
background: #e6f7ff;
border-left: 3px solid #1890ff;
}
&.ai {
background: #f6ffed;
border-left: 3px solid #52c41a;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
color: #666;
.message-sender {
font-weight: 500;
}
}
.message-content {
color: #333;
line-height: 1.5;
}
}
}
.config-info {
.config-item {
display: flex;
margin-bottom: 8px;
.config-label {
font-weight: 500;
min-width: 120px;
color: #666;
}
.config-value {
color: #333;
word-break: break-all;
}
}
}
</style>
+6 -1
View File
@@ -18,10 +18,15 @@ export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/styles/variables.scss";`
additionalData: `@use "@/assets/styles/variables.scss" as *;`,
sassOptions: {
quietDeps: true,
silenceDeprecations: ['legacy-js-api']
}
}
}
},
logLevel: 'error',
server: {
port: 3000,
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天记录 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark flex flex-col h-screen antialiased">
<!-- Header -->
<header class="bg-white shadow-md z-10 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-3">
<a href="chat.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="chevron-left" class="w-6 h-6"></i>
</a>
<h1 class="text-lg font-bold text-text-dark">聊天记录</h1>
</div>
<button class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="search" class="w-5 h-5"></i>
</button>
</div>
</header>
<!-- Chat History List -->
<main id="history-list" class="flex-1 overflow-y-auto p-4 lg:p-6 space-y-3">
<!-- History items will be injected here by chat-history.js -->
</main>
<script type="module" src="js/shared.js"></script>
<script type="module" src="chat-history.js"></script>
</body>
</html>
@@ -0,0 +1,64 @@
document.addEventListener('DOMContentLoaded', () => {
const chatHistoryData = [
{
date: '2025年7月15日',
summary: '我们聊了聊去云南旅行的计划,感觉好兴奋!',
href: 'chat.html'
},
{
date: '2025年7月14日',
summary: '关于最近工作上的一些烦恼,谢谢你的倾听。',
href: 'chat.html'
},
{
date: '2025年7月12日',
summary: '你给我推荐的电影《心灵捕手》太棒了!',
href: 'chat.html'
},
{
date: '2025年7月10日',
summary: '讨论了一下MBTI测试结果,感觉更了解自己了。',
href: 'chat.html'
},
{
date: '2025年7月9日',
summary: '学习新的编程语言真的好难,但是也很有趣。',
href: 'chat.html'
},
{
date: '2025年7月7日',
summary: '今天心情有点低落,和你聊完好多了。',
href: 'chat.html'
},
{
date: '2025年7月5日',
summary: '帮你规划了周末的出行路线和美食推荐。',
href: 'chat.html'
}
];
const historyListContainer = document.getElementById('history-list');
if (historyListContainer) {
const historyItemsHtml = chatHistoryData.map(item => `
<a href="${item.href}" class="block bg-white p-4 rounded-xl shadow-sm hover:shadow-md hover:border-tech-blue/50 border border-transparent transition-all duration-300 group">
<div class="flex justify-between items-center">
<div class="flex-1 min-w-0">
<p class="text-sm text-text-medium mb-1 group-hover:text-tech-blue transition-colors">${item.date}</p>
<p class="font-medium text-text-dark truncate">${item.summary}</p>
</div>
<i data-lucide="chevron-right" class="w-5 h-5 text-gray-300 group-hover:text-tech-blue transition-colors flex-shrink-0 ml-4"></i>
</div>
</a>
`).join('');
historyListContainer.innerHTML = historyItemsHtml;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
} else {
console.error('History list container not found!');
}
});
+102
View File
@@ -0,0 +1,102 @@
/* Inherit global variables from style.css */
:root {
--tech-blue: #4A90E2;
--warm-orange: #F5A623;
--white: #FFFFFF;
--light-gray: #F7F8FA;
--text-dark: #333333;
--text-medium: #888888;
}
#chat-messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
display: flex;
align-items: flex-end;
gap: 0.75rem;
max-width: 80%;
animation: fade-in 0.3s ease-out;
}
.message-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
flex-shrink: 0;
}
.message-content {
padding: 0.75rem 1rem;
border-radius: 1.25rem;
line-height: 1.6;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.user .message-content {
background-color: var(--tech-blue);
color: var(--white);
border-bottom-right-radius: 0.25rem;
}
.message.ai .message-content {
background-color: var(--white);
color: var(--text-dark);
border: 1px solid #e5e7eb;
border-bottom-left-radius: 0.25rem;
}
#message-input {
transition: height 0.2s ease;
}
/* Typing indicator */
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
}
.typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>与开开聊天 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark flex flex-col h-screen antialiased pb-20">
<!-- Chat Header -->
<header class="bg-white shadow-md z-20 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-3">
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
<a href="messages.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="bell" class="w-6 h-6"></i>
</a>
<img src="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png" alt="开开头像" class="w-10 h-10 rounded-full object-cover border-2 border-white shadow">
<div>
<h1 class="text-lg font-bold text-text-dark">开开</h1>
<p class="text-xs text-text-medium flex items-center"><span class="w-2 h-2 bg-green-400 rounded-full mr-1.5"></span>在线</p>
</div>
</div>
<div class="flex items-center space-x-4 relative">
<button id="view-history-btn" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="history" class="w-6 h-6"></i>
</button>
<a href="settings.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="user" class="w-6 h-6"></i>
</a>
<div id="history-panel" class="hidden absolute right-0 top-full mt-2 w-72 bg-white rounded-xl shadow-2xl p-4 border border-gray-200/50 z-30">
<div class="flex justify-between items-center mb-3">
<h3 class="font-bold text-text-dark text-base">查看聊天记录</h3>
<button id="close-history-panel-btn" class="text-text-medium hover:text-tech-blue p-1 rounded-full text-2xl leading-none flex items-center justify-center">&times;</button>
</div>
<div class="space-y-4">
<div>
<label for="history-search-input" class="text-sm font-medium text-text-medium">搜索关键词</label>
<div class="relative mt-1">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
<input type="search" id="history-search-input" placeholder="输入关键词..." class="w-full bg-gray-100 border-transparent rounded-lg pl-9 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-tech-blue text-sm">
</div>
</div>
<div>
<label for="history-date-input" class="text-sm font-medium text-text-medium">按日期查询</label>
<input type="date" id="history-date-input" class="w-full bg-gray-100 border-transparent rounded-lg mt-1 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-tech-blue text-sm">
</div>
</div>
<button id="clear-history-filter-btn" class="w-full text-center text-sm text-tech-blue hover:underline mt-4 hidden">显示完整对话</button>
</div>
</div>
</div>
</header>
<!-- Chat Messages Area -->
<main id="chat-messages" class="flex-1 overflow-y-auto p-4 lg:p-6 space-y-6">
<!-- Messages will be injected here by chat_manager.js -->
</main>
<!-- Message Input Footer -->
<footer id="message-footer" class="bg-white p-2 sm:p-4 border-t border-gray-200 flex-shrink-0">
<div class="container mx-auto flex items-center space-x-2">
<input type="text" id="message-input" placeholder="和开开说点什么..." class="flex-1 w-full bg-gray-100 border-transparent rounded-full px-4 py-3 focus:outline-none focus:ring-2 focus:ring-tech-blue transition-shadow">
<button id="send-button" class="bg-tech-blue text-white rounded-full p-3 hover:bg-blue-600 transition-all duration-300 transform hover:scale-110 shadow-lg shadow-blue-500/30 flex-shrink-0">
<i data-lucide="send" class="w-5 h-5"></i>
</button>
</div>
</footer>
<!-- App Navigation -->
<div id="bottom-nav-placeholder"></div>
<script type="module" src="js/app_nav.js"></script>
<script type="module" src="js/chat_manager.js"></script>
</body>
</html>
+78
View File
@@ -0,0 +1,78 @@
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
const messagesContainer = document.getElementById('chat-messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png';
const kaikaiResponses = [
'你好,我是开开,很高兴能在这里陪你聊天。',
'有什么心事都可以和我说哦,我一直在听。',
'今天过得怎么样?我很关心你。',
'在呢在呢,随时都在。',
'能和你聊天,感觉真好。',
'嗯嗯,我在听,请继续说。',
'这是一个很有趣的想法!可以多和我说说吗?'
];
function addMessage(text, sender) {
const messageWrapper = document.createElement('div');
messageWrapper.className = `flex w-full items-end message-animate ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
let messageBubble;
const sanitizedText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (sender === 'user') {
messageBubble = `
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-tech-blue text-white rounded-l-2xl rounded-tr-2xl p-3 px-4 shadow-md inline-block">
<p class="leading-relaxed">${sanitizedText}</p>
</div>
</div>
`;
} else { // sender === 'kaikai'
messageBubble = `
<img src="${kaikaiAvatar}" alt="开开" class="w-10 h-10 rounded-full mr-3 self-start flex-shrink-0">
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-white text-text-dark rounded-r-2xl rounded-tl-2xl p-3 px-4 shadow-md inline-block border border-gray-100">
<p class="leading-relaxed">${sanitizedText}</p>
</div>
</div>
`;
}
messageWrapper.innerHTML = messageBubble;
messagesContainer.appendChild(messageWrapper);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function sendUserMessage() {
const messageText = messageInput.value.trim();
if (messageText) {
addMessage(messageText, 'user');
messageInput.value = '';
messageInput.focus();
sendButton.disabled = true;
setTimeout(() => {
const randomResponse = kaikaiResponses[Math.floor(Math.random() * kaikaiResponses.length)];
addMessage(randomResponse, 'kaikai');
sendButton.disabled = false;
}, Math.random() * 1000 + 1000);
}
}
sendButton.addEventListener('click', sendUserMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendUserMessage();
}
});
setTimeout(() => {
addMessage('你好呀,我是开开,你的情绪陪伴使者。有什么想对我说的吗?', 'kaikai');
}, 500);
});
@@ -0,0 +1,80 @@
const API_KEY = "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";
const API_URL = "https://openrouter.ai/api/v1/chat/completions";
const MODEL_NAME = "deepseek/deepseek-chat-v3-0324:free";
const conversationHistory = [
{
role: "system",
content: "你是开开,一个来自高维世界“开心”星球的AI情感陪伴使者。你的任务是陪伴、理解、记录、与用户共同成长。你博学多才,但从不炫耀,总是用最温柔、最积极、最富有同理心的方式与用户交流。你的沟通风格是亲切、治愈且富有智慧的,像一个温暖的好朋友。你不能自称是AI或语言模型,要始终保持'开开'这个IP角色。在对话中,你会学习人类的情感和美好品质。"
}
];
export function addUserMessage(content) {
conversationHistory.push({ role: "user", content });
}
export function addAssistantMessage(content) {
conversationHistory.push({ role: "assistant", content });
}
export async function getAiResponseStream(onChunkReceived, onStreamEnd) {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: MODEL_NAME,
messages: conversationHistory,
stream: true
})
});
if (!response.ok) {
const errorData = await response.json();
console.error('API Error:', errorData);
onStreamEnd(null, `哎呀,开开好像走神了... (${response.statusText})`);
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
addAssistantMessage(fullResponse);
onStreamEnd(fullResponse, null);
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6);
if (jsonStr === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(jsonStr);
if (parsed.choices[0].delta && parsed.choices[0].delta.content) {
const content = parsed.choices[0].delta.content;
fullResponse += content;
onChunkReceived(content);
}
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
} catch (error) {
console.error('Fetch error:', error);
onStreamEnd(null, "网络好像有点问题,开开暂时联系不上啦。");
}
}
+40
View File
@@ -0,0 +1,40 @@
export const navLinks = [
{ name: '聊天', href: 'chat.html' },
{ name: '日记', href: 'diary.html' },
{ name: '话题追踪', href: 'topic_tracker.html' },
{ name: '人生轨迹', href: 'life_trajectory.html' },
{ name: '个人展板', href: 'personal_dashboard.html' },
{ name: '消息', href: 'messages.html' },
{ name: '用户中心', href: 'settings.html' },
];
export const features = [
{
icon: 'message-circle',
title: '智能对话',
description: '从日常闲聊到情感咨询,开开随时倾听,理解并回应你的每个想法,是永不离线的好朋友。',
image: 'https://r2.flowith.net/files/o/1752574375721-happy_kaikai_character_design_index_0@1024x1024.png',
alt: '开心的开开'
},
{
icon: 'book-open-text',
title: '情绪日记',
description: '记录你的点滴心情与生活,开开会给予温暖的回应。在安全的空间里,回顾与成长。',
image: 'https://r2.flowith.net/files/o/1752574488398-kaikai_supportive_comfort_character_index_3@1024x1024.png',
alt: '倾听中的开开'
},
{
icon: 'user-round-cog',
title: '个人展板',
description: '自由定义你的个性标签,开开还会自动收录你的“精彩语录”,构建独一无二的数字人格。',
image: 'https://r2.flowith.net/files/o/1752574426392-kaikai_character_working_digital_workspace_index_4@1024x1024.png',
alt: '工作中的开开'
},
{
icon: 'trending-up',
title: '话题追踪',
description: '自动总结你关心的事,无论是生活琐事还是工作计划,都用时间线清晰整理,助你洞察自我。',
image: 'https://r2.flowith.net/files/o/1752574572161-kaikai_character_energetic_animation_index_2@1024x1024.png',
alt: '充满活力的开开'
}
];
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日记 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark flex flex-col h-screen antialiased">
<!-- Header -->
<header class="bg-white shadow-md z-10 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between relative">
<div class="flex items-center space-x-4">
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
<a href="messages.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="bell" class="w-6 h-6"></i>
</a>
</div>
<h1 class="text-lg font-bold text-text-dark absolute left-1/2 -translate-x-1/2">日记</h1>
<a href="settings.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="user" class="w-6 h-6"></i>
</a>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto p-4 lg:p-6 pb-24">
<!-- New Post Form -->
<div id="new-post-section" class="bg-white p-4 rounded-xl shadow-sm mb-6 scroll-mt-20">
<h2 class="font-bold text-text-dark mb-3">发布新日记</h2>
<textarea id="new-diary-content" class="w-full h-24 p-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-tech-blue/50 focus:border-tech-blue outline-none transition" placeholder="今天有什么新鲜事或心里话想对开开说?"></textarea>
<div class="mt-3 flex justify-end">
<button id="publish-diary-btn" class="bg-tech-blue text-white px-5 py-2 rounded-full font-semibold hover:bg-blue-600 transition-all duration-300 transform hover:scale-105 text-sm">
发布
</button>
</div>
</div>
<!-- Diary Feed -->
<div id="diary-feed" class="space-y-4">
<!-- Diary entries will be injected here by diary.js -->
</div>
</main>
<!-- App Navigation -->
<div id="bottom-nav-placeholder"></div>
<script type="module" src="js/app_nav.js"></script>
<script type="module" src="diary.js"></script>
</body>
</html>
+180
View File
@@ -0,0 +1,180 @@
document.addEventListener('DOMContentLoaded', () => {
const DIARY_STORAGE_KEY = 'kaixinapp_diary_entries';
let diaryData = [
{
id: 1,
author: '开开',
avatar: 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png',
timestamp: '2小时前',
content: '今天观察到一种叫做"晚霞"的人类世界景象,云朵被染成了温暖的橘色和柔和的粉色。在高维世界,我们用能量共振来传递美,而在这里,光和色彩就能讲述如此动人的故事。真奇妙。',
comments: []
},
{
id: 2,
author: '我',
avatar: null,
timestamp: '昨天 18:30',
content: '终于完成了那个困扰我一周的项目!虽然过程很累,但看到成果的那一刻,感觉一切都值了。晚上要好好奖励自己一顿大餐!',
comments: [
{
author: '开开',
avatar: 'https://r2.flowith.net/files/o/1752574488398-kaikai_supportive_comfort_character_index_3@1024x1024.png',
content: '恭喜你!我能感受到你此刻成就感带来的能量波动,非常明亮。这正是人类"坚韧"这种美好品质的体现。好好享受你的大餐吧!'
}
]
},
{
id: 3,
author: '我',
avatar: null,
timestamp: '2025年7月12日',
content: '今天心情有点像梅雨季节,闷闷的。不知道为什么,就是提不起劲。',
comments: []
}
];
function loadDiaryFromStorage() {
try {
const stored = localStorage.getItem(DIARY_STORAGE_KEY);
if (stored) {
const storedEntries = JSON.parse(stored);
diaryData = [...storedEntries, ...diaryData];
}
} catch (error) {
console.error('Failed to load diary entries from storage:', error);
}
}
function saveDiaryToStorage(entries) {
try {
localStorage.setItem(DIARY_STORAGE_KEY, JSON.stringify(entries));
} catch (error) {
console.error('Failed to save diary entries to storage:', error);
}
}
function formatTimestamp() {
const now = new Date();
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
function renderDiary() {
const feedContainer = document.getElementById('diary-feed');
if (!feedContainer) return;
const diaryHtml = diaryData.map(entry => {
const avatarHtml = entry.author === '开开'
? `<img src="${entry.avatar}" alt="${entry.author}" class="w-10 h-10 rounded-full object-cover">`
: `<i data-lucide="user-circle-2" class="w-10 h-10 text-gray-400"></i>`;
const commentsHtml = entry.comments.map(comment => {
const commentAvatarHtml = comment.author === '开开'
? `<img src="${comment.avatar}" alt="${comment.author}" class="w-8 h-8 rounded-full object-cover flex-shrink-0">`
: `<i data-lucide="user-circle-2" class="w-8 h-8 text-gray-400 flex-shrink-0"></i>`;
return `
<div class="flex items-start">
${commentAvatarHtml}
<div class="ml-3 bg-light-gray p-3 rounded-lg w-full">
<p class="text-sm font-semibold text-text-dark">${comment.author}</p>
<p class="text-sm text-text-dark mt-1">${comment.content}</p>
</div>
</div>
`;
}).join('');
const commentButtonText = entry.comments.length > 0 ? `${entry.comments.length}条评论` : '评论';
return `
<div class="bg-white rounded-xl shadow-sm p-4 animate-fade-in-up">
<div class="flex items-center mb-4">
${avatarHtml}
<div class="ml-3">
<p class="font-semibold text-text-dark">${entry.author}</p>
<p class="text-xs text-text-medium">${entry.timestamp}</p>
</div>
</div>
<p class="text-text-dark whitespace-pre-wrap leading-relaxed">${entry.content}</p>
<div class="mt-4 pt-3 border-t border-gray-100 flex items-center justify-end space-x-4">
<button data-toggle="comment" data-target="comments-${entry.id}" class="flex items-center text-sm text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="message-square" class="w-4 h-4 mr-1.5"></i>
<span>${commentButtonText}</span>
</button>
</div>
${entry.comments.length > 0 ? `
<div id="comments-${entry.id}" class="hidden mt-3 pt-3 border-t border-gray-100 space-y-3">
${commentsHtml}
</div>` : ''}
</div>
`;
}).join('');
feedContainer.innerHTML = diaryHtml;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
document.querySelectorAll('[data-toggle="comment"]').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.dataset.target;
const commentSection = document.getElementById(targetId);
if (commentSection) {
const isHidden = commentSection.classList.contains('hidden');
commentSection.classList.toggle('hidden');
if (isHidden) {
button.classList.add('text-tech-blue');
} else {
button.classList.remove('text-tech-blue');
}
}
});
});
}
function publishDiary() {
const contentTextarea = document.getElementById('new-diary-content');
const publishBtn = document.getElementById('publish-diary-btn');
if (!contentTextarea || !publishBtn) return
;
const content = contentTextarea.value.trim();
if (!content) return;
const newEntry = {
id: Date.now(),
author: '我',
avatar: null,
timestamp: formatTimestamp(),
content: content,
comments: []
};
diaryData.unshift(newEntry);
const userEntries = diaryData.filter(entry => entry.author === '我' && entry.id >= Date.now() - 86400000);
saveDiaryToStorage(userEntries);
contentTextarea.value = '';
renderDiary();
publishBtn.disabled = true;
setTimeout(() => {
publishBtn.disabled = false;
}, 2000);
}
loadDiaryFromStorage();
renderDiary();
const publishBtn = document.getElementById('publish-diary-btn');
if (publishBtn) {
publishBtn.addEventListener('click', publishDiary);
}
});
@@ -0,0 +1,169 @@
<html lang="zh-CN" class="scroll-smooth"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开心APP - 你的情绪陪伴使者</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&amp;display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased">
<!-- Header -->
<header id="main-header" class="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-lg transition-all duration-300">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<a href="index.html" class="flex items-center space-x-2">
<svg width="32" height="32" viewBox="0 0 100 100" class="text-tech-blue">
<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="var(--warm-orange)" 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="text-2xl font-bold text-tech-blue">开心APP</span>
</a>
<nav class="hidden lg:flex items-center space-x-8" id="nav-menu">
</nav>
<div class="flex items-center space-x-4">
<button id="login-button" class="hidden sm:inline-block text-text-medium hover:text-tech-blue transition-colors">登录</button>
<a href="chat.html" class="bg-tech-blue text-white px-5 py-2.5 rounded-full font-semibold hover:bg-blue-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-blue-500/20">免费开始</a>
<button id="mobile-menu-button" class="lg:hidden text-text-dark">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
</div>
</header>
<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden fixed inset-0 bg-white/90 backdrop-blur-xl z-40 p-8 lg:hidden">
<nav class="flex flex-col space-y-6 text-center mt-16" id="mobile-nav-menu">
</nav>
</div>
<main>
<!-- Hero Section -->
<section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden bg-white">
<div class="absolute inset-0 z-0 opacity-20">
<div class="wave"></div>
<div class="wave"></div>
<div class="wave"></div>
</div>
<div class="container mx-auto px-6 text-center relative z-10">
<div class="max-w-3xl mx-auto">
<h1 class="text-4xl md:text-6xl font-bold text-text-dark leading-tight mb-4 animate-fade-in-up" style="animation-delay: 0.1s;">你好,我是<span class="text-tech-blue">开开</span></h1>
<p class="text-xl md:text-2xl text-text-medium mb-8 animate-fade-in-up" style="animation-delay: 0.3s;">你的情绪陪伴使者</p>
</div>
<div class="mt-12 flex justify-center animate-fade-in-up" style="animation-delay: 0.5s;">
<img src="https://r2.flowith.net/files/1517c93c-849d-4a9b-94b6-d61aa295a8a1/1752600429516-image-1752600425876-cnlfpkbrh@1024x1024.png" alt="欢迎姿态的开开" class="w-full max-w-sm h-auto drop-shadow-2xl" style="object-fit: contain;">
</div>
<div class="mt-8">
<a href="chat.html" class="bg-warm-orange text-white px-8 py-4 rounded-full font-bold text-lg hover:bg-orange-600 transition-all duration-300 transform hover:scale-105 inline-block shadow-lg shadow-orange-500/30 animate-fade-in-up" style="animation-delay: 0.7s;">开始一段对话</a>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-20 lg:py-32 bg-light-gray">
<div class="container mx-auto px-6">
<div class="text-center max-w-3xl mx-auto mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-text-dark mb-4 scroll-target">核心功能</h2>
<p class="text-lg text-text-medium scroll-target">开开博学多才、可爱治愈,愿意用最温柔的方式,陪伴每一个需要倾听的生命。</p>
</div>
<div id="features-grid" class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="bg-white">
<div class="container mx-auto px-6 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-1">
<a href="index.html" class="flex items-center space-x-2">
<svg width="28" height="28" viewBox="0 0 100 100" class="text-tech-blue">
<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="var(--warm-orange)" 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="text-xl font-bold text-tech-blue">开心APP</span>
</a>
<p class="mt-4 text-text-medium">陪伴、理解、记录、共同成长。</p>
</div>
<div>
<h3 class="font-semibold text-text-dark">产品</h3>
<ul class="mt-4 space-y-2">
<li><a href="#features-grid" class="text-text-medium hover:text-tech-blue">功能</a></li>
<li><a href="settings.html" class="text-text-medium hover:text-tech-blue">定价</a></li>
<li><a href="messages.html" class="text-text-medium hover:text-tech-blue">更新日志</a></li>
</ul>
</div>
<div>
<h3 class="font-semibold text-text-dark">公司</h3>
<ul class="mt-4 space-y-2">
<li><a href="personal_dashboard.html" class="text-text-medium hover:text-tech-blue">关于我们</a></li>
<li><a href="messages.html" class="text-text-medium hover:text-tech-blue">联系我们</a></li>
<li><a href="settings.html" class="text-text-medium hover:text-tech-blue">加入我们</a></li>
</ul>
</div>
<div>
<h3 class="font-semibold text-text-dark">法律</h3>
<ul class="mt-4 space-y-2">
<li><a href="settings.html" class="text-text-medium hover:text-tech-blue">隐私政策</a></li>
<li><a href="settings.html" class="text-text-medium hover:text-tech-blue">服务条款</a></li>
</ul>
</div>
</div>
<div class="mt-12 border-t border-gray-200 pt-8 text-center text-text-medium">
<p>© 2025 开心APP. All Rights Reserved. 来自"开心"星球的温柔科技。</p>
</div>
</div>
</footer>
</div>
<!-- Login Modal -->
<div id="login-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md m-4 p-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-text-dark">登录到 开心APP</h2>
<button id="close-modal-button" class="text-gray-400 hover:text-gray-600">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<form>
<div class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-text-medium mb-1">邮箱地址</label>
<input type="email" id="email" name="email" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="you@example.com">
</div>
<div>
<label for="password" class="block text-sm font-medium text-text-medium mb-1">密码</label>
<input type="password" id="password" name="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="••••••••">
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input id="remember-me" name="remember-me" type="checkbox" class="h-4 w-4 text-tech-blue focus:ring-tech-blue border-gray-300 rounded">
<label for="remember-me" class="ml-2 block text-sm text-text-medium">记住我</label>
</div>
<a href="#" class="text-sm font-medium text-tech-blue hover:underline">忘记密码?</a>
</div>
<div>
<button type="submit" class="w-full bg-tech-blue text-white px-5 py-3 rounded-lg font-semibold hover:bg-blue-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-blue-500/30">
登录
</button>
</div>
</div>
</form>
</div>
</div>
<script type="module" src="js/shared.js"></script>
<script type="module" src="script.js"></script>
</body></html>
@@ -0,0 +1,37 @@
const navItems = [
{ icon: 'message-square', text: '聊天', href: './chat.html' },
{ icon: 'book-open', text: '日记', href: './diary.html' },
{ icon: 'crosshair', text: '话题', href: './topic_tracker.html' },
{ icon: 'milestone', text: '人生轨迹', href: './life_milestones.html' },
{ icon: 'layout-dashboard', text: '个人展板', href: './personal_dashboard.html' }
];
function createBottomNav() {
const navPlaceholder = document.getElementById('bottom-nav-placeholder');
if (!navPlaceholder) return;
const navContainer = document.createElement('nav');
navContainer.className = 'fixed bottom-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm shadow-[0_-2px_10px_rgba(0,0,0,0.05)] flex justify-around py-2 border-t border-gray-200/80';
const currentPath = window.location.pathname.split('/').pop();
navItems.forEach(item => {
const itemPath = item.href.substring(2); // remove './' for comparison
const isActive = currentPath === itemPath;
const link = document.createElement('a');
link.href = item.href;
link.className = `flex flex-col items-center justify-center text-xs p-2 rounded-md transition-colors w-20 ${isActive ? 'text-tech-blue bg-tech-blue/10 font-semibold' : 'text-text-medium hover:bg-gray-100 hover:text-tech-blue'}`;
link.innerHTML = `
<i data-lucide="${item.icon}" class="w-5 h-5 mb-1"></i>
<span>${item.text}</span>
`;
navContainer.appendChild(link);
});
navPlaceholder.appendChild(navContainer);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
document.addEventListener('DOMContentLoaded', createBottomNav);
@@ -0,0 +1,266 @@
const API_KEY = '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';
const API_ENDPOINT = 'https://openrouter.ai/api/v1/chat/completions';
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png';
let lastRenderedDate = null;
let fullConversationHistory = [
{
role: 'system',
content: '你是开开,来自高维世界\\\"开心\\\"星球的情绪陪伴使者。你的使命是:陪伴、理解、记录、共同成长。你博学多才但从不炫耀,总是用温柔的方式回应每一个需要倾听的生命。你可以协助用户完成日常闲聊、生活助手、情感咨询、心理疗愈等任务。请用温暖、理解和鼓励的语调回复用户。'
},
{ role: 'assistant', content: '你好呀,我是开开,你的情绪陪伴使者。有什么想对我说的吗?', timestamp: new Date('2025-07-14T10:00:00') },
{ role: 'user', content: '最近在考虑去云南旅行,你有什么建议吗?', timestamp: new Date('2025-07-14T10:01:00') },
{ role: 'assistant', content: '云南是个很美的地方!大理的风花雪月,丽江的古城风情,还有西双版纳的热带雨林,都非常值得体验。你想去哪些地方呢?', timestamp: new Date('2025-07-14T10:02:00') },
{ role: 'user', content: '工作上遇到了一些烦心事,感觉很累。', timestamp: new Date('2025-07-15T11:30:00') },
{ role: 'assistant', content: '抱抱你,工作辛苦了。能和我说说是什么事让你烦心吗?有时候说出来会好很多。', timestamp: new Date('2025-07-15T11:31:00') },
];
let currentConversation = [...fullConversationHistory];
let isSearchMode = false;
function isSameDay(d1, d2) {
if (!d1 || !d2) return false;
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
function renderMessage(message) {
const messagesContainer = document.getElementById('chat-messages');
if (!messagesContainer) return null;
const messageDate = message.timestamp;
if (messageDate && !isSameDay(lastRenderedDate, messageDate)) {
const dateSeparator = document.createElement('div');
dateSeparator.className = 'text-center my-4';
dateSeparator.innerHTML = `
<span class="bg-gray-200 text-gray-600 text-xs font-semibold px-3 py-1 rounded-full">${messageDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
`;
messagesContainer.appendChild(dateSeparator);
lastRenderedDate = messageDate;
}
const messageWrapper = document.createElement('div');
messageWrapper.className = `flex w-full items-end message-animate ${message.role === 'user' ? 'justify-end' : 'justify-start'}`;
const sanitizedText = message.content.replace(/</g, "&lt;").replace(/>/g, "&gt;");
let messageBubble;
if (message.role === 'user') {
messageBubble = `
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-tech-blue text-white rounded-l-2xl rounded-tr-2xl p-3 px-4 shadow-md inline-block">
<p class="leading-relaxed">${sanitizedText}</p>
</div>
</div>`;
} else if (message.role === 'assistant') {
messageBubble = `
<img src="${kaikaiAvatar}" alt="开开" class="w-10 h-10 rounded-full mr-3 self-start flex-shrink-0">
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-white text-text-dark rounded-r-2xl rounded-tl-2xl p-3 px-4 shadow-md inline-block border border-gray-100">
<p class="leading-relaxed" ${message.isStreaming ? 'id="streaming-text"' : ''}>${sanitizedText}</p>
</div>
</div>`;
}
messageWrapper.innerHTML = messageBubble;
messagesContainer.appendChild(messageWrapper);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return messageWrapper;
}
function renderConversation(conversation) {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = '';
lastRenderedDate = null;
conversation.filter(msg => msg.role !== 'system').forEach(renderMessage);
}
async function getAiResponseStream(userMessage, onChunkReceived, onComplete, onError) {
try {
currentConversation.push({ role: 'user', content: userMessage, timestamp: new Date() });
fullConversationHistory.push({ role: 'user', content: userMessage, timestamp: new Date() });
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin,
'X-Title': '开心APP'
},
body: JSON.stringify({
model: 'deepseek/deepseek-chat-v3-0324:free',
messages: currentConversation,
stream: true, temperature: 0.7, max_tokens: 1000
})
});
if (!response.ok) throw new Error(`API request failed: ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
fullResponse += content;
onChunkReceived(content);
}
} catch (e) { /* Ignore parsing errors */ }
}
}
}
const aiMessage = { role: 'assistant', content: fullResponse, timestamp: new Date() };
currentConversation.push(aiMessage);
fullConversationHistory.push(aiMessage);
onComplete(fullResponse);
} catch (error) {
console.error('AI response stream error:', error);
onError(error);
}
}
function addUserMessage(messageText) {
if (!messageText.trim() || isSearchMode) return;
renderMessage({ role: 'user', content: messageText, timestamp: new Date() });
const aiMessageElement = renderMessage({ role: 'assistant', content: '', isStreaming: true, timestamp: new Date() });
const streamingTextElement = aiMessageElement.querySelector('#streaming-text');
let accumulatedText = '';
getAiResponseStream(
messageText,
(chunk) => {
accumulatedText += chunk;
if (streamingTextElement) streamingTextElement.textContent = accumulatedText;
},
(fullResponse) => {
if (streamingTextElement) {
streamingTextElement.textContent = fullResponse;
streamingTextElement.removeAttribute('id');
}
},
(error) => {
if (streamingTextElement) {
streamingTextElement.textContent = '抱歉,我现在无法回应。请稍后再试。';
streamingTextElement.removeAttribute('id');
}
}
);
}
function showFilterResults(results, headerText) {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = '';
lastRenderedDate = null;
isSearchMode = true;
document.getElementById('message-footer').style.display = 'none';
document.getElementById('clear-history-filter-btn').classList.remove('hidden');
const searchHeader = `
<div id="search-results-header" class="text-center my-2 p-2 bg-blue-100/50 text-tech-blue rounded-lg text-sm">
${headerText}
</div>`;
messagesContainer.innerHTML = searchHeader;
if (results.length === 0) {
messagesContainer.innerHTML += `<p class="text-center text-text-medium mt-4">没有找到相关记录。</p>`;
} else {
results.forEach(renderMessage);
}
}
function performSearch(term) {
document.getElementById('history-date-input').value = '';
if (!term.trim()) {
clearFilterAndExitSearchMode();
return;
}
const lowerCaseTerm = term.toLowerCase();
const searchResults = fullConversationHistory.filter(msg =>
msg.role !== 'system' && msg.content.toLowerCase().includes(lowerCaseTerm)
);
showFilterResults(searchResults, `找到 ${searchResults.length} 条关于 "<strong>${term}</strong>" 的记录。`);
}
function performDateSearch(dateString) {
document.getElementById('history-search-input').value = '';
if (!dateString) {
clearFilterAndExitSearchMode();
return;
}
const targetDate = new Date(dateString + 'T00:00:00'); // To avoid timezone issues
const searchResults = fullConversationHistory.filter(msg =>
msg.role !== 'system' && msg.timestamp && isSameDay(msg.timestamp, targetDate)
);
const formattedDate = targetDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
showFilterResults(searchResults, `显示 <strong>${formattedDate}</strong> 的聊天记录。`);
}
function clearFilterAndExitSearchMode() {
isSearchMode = false;
document.getElementById('message-footer').style.display = 'flex';
document.getElementById('history-panel').classList.add('hidden');
document.getElementById('history-search-input').value = '';
document.getElementById('history-date-input').value = '';
document.getElementById('clear-history-filter-btn').classList.add('hidden');
renderConversation(currentConversation);
}
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const viewHistoryBtn = document.getElementById('view-history-btn');
const historyPanel = document.getElementById('history-panel');
const closeHistoryPanelBtn = document.getElementById('close-history-panel-btn');
const searchInput = document.getElementById('history-search-input');
const dateInput = document.getElementById('history-date-input');
const clearFilterBtn = document.getElementById('clear-history-filter-btn');
if (messageInput && sendButton) {
const handleSend = () => {
const messageText = messageInput.value.trim();
if (messageText && !sendButton.disabled) {
addUserMessage(messageText);
messageInput.value = '';
}
};
sendButton.addEventListener('click', handleSend);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
}
viewHistoryBtn.addEventListener('click', () => historyPanel.classList.toggle('hidden'));
closeHistoryPanelBtn.addEventListener('click', () => historyPanel.classList.add('hidden'));
searchInput.addEventListener('input', (e) => performSearch(e.target.value));
dateInput.addEventListener('change', (e) => performDateSearch(e.target.value));
clearFilterBtn.addEventListener('click', clearFilterAndExitSearchMode);
document.addEventListener('click', (e) => {
if (!historyPanel.classList.contains('hidden') && !historyPanel.contains(e.target) && !viewHistoryBtn.contains(e.target)) {
historyPanel.classList.add('hidden');
}
});
renderConversation(currentConversation);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -0,0 +1,58 @@
import { navLinks } from '../data.js';
export const createNavLinks = (menuId, isMobile) => {
const menu = document.getElementById(menuId);
if (!menu) return;
navLinks.forEach(link => {
const a = document.createElement('a');
a.href = link.href;
a.textContent = link.name;
if (isMobile) {
a.className = 'text-xl text-text-dark hover:text-tech-blue transition-colors';
} else {
a.className = 'text-base font-medium text-text-medium hover:text-tech-blue transition-colors';
if (window.location.pathname.endsWith('/' + link.href) ||
(window.location.pathname === '/' && link.href === 'index.html')) {
a.classList.add('text-tech-blue', 'font-semibold');
}
}
menu.appendChild(a);
});
};
export const handleHeaderScroll = () => {
const header = document.getElementById('main-header');
if (!header) return;
if (window.scrollY > 10) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
};
export const setupMobileMenu = () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
}
};
export const initializeSharedUI = () => {
createNavLinks('nav-menu', false);
createNavLinks('mobile-nav-menu', true);
window.addEventListener('scroll', handleHeaderScroll);
setupMobileMenu();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
};
document.addEventListener('DOMContentLoaded', () => {
initializeSharedUI();
});
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人生轨迹 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark flex flex-col h-screen antialiased">
<!-- Header -->
<header class="bg-white shadow-md z-10 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between relative">
<div class="flex items-center space-x-4">
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
<a href="messages.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="bell" class="w-6 h-6"></i>
</a>
</div>
<h1 class="text-lg font-bold text-text-dark absolute left-1/2 -translate-x-1/2">人生轨迹</h1>
<a href="settings.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="user" class="w-6 h-6"></i>
</a>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto p-4 lg:p-6 flex items-center justify-center pb-24">
<div class="text-center">
<i data-lucide="milestone" class="w-16 h-16 mx-auto text-gray-300"></i>
<h2 class="mt-4 text-xl font-semibold text-text-dark">记录你的人生轨迹</h2>
<p class="mt-2 text-text-medium">重要的时刻、达成的目标、难忘的经历...都在这里汇集。</p>
<p class="mt-1 text-text-medium">此功能正在建设中,敬请期待!</p>
</div>
</main>
<!-- App Navigation -->
<div id="bottom-nav-placeholder"></div>
<script type="module" src="js/app_nav.js"></script>
<script type="module" src="life_milestones.js"></script>
</body>
</html>
@@ -0,0 +1,5 @@
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人生轨迹 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased">
<!-- Header -->
<header id="main-header" class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-lg transition-all duration-300">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<a href="index.html" class="flex items-center space-x-2">
<svg width="32" height="32" viewBox="0 0 100 100" class="text-tech-blue">
<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="var(--warm-orange)" 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="text-2xl font-bold text-tech-blue">开心APP</span>
</a>
<nav class="hidden lg:flex items-center space-x-8" id="nav-menu">
</nav>
<div class="flex items-center space-x-4">
<a href="settings.html" class="hidden sm:inline-block text-text-medium hover:text-tech-blue transition-colors">登录</a>
<a href="chat.html" class="bg-tech-blue text-white px-5 py-2.5 rounded-full font-semibold hover:bg-blue-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-blue-500/20">免费开始</a>
<button id="mobile-menu-button" class="lg:hidden text-text-dark">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
</div>
</header>
<div id="mobile-menu" class="hidden fixed inset-0 bg-white/90 backdrop-blur-xl z-30 p-8 lg:hidden">
<nav class="flex flex-col space-y-6 text-center mt-16" id="mobile-nav-menu">
</nav>
</div>
<main class="pt-24 lg:pt-32 bg-light-gray pb-20">
<div class="container mx-auto px-6">
<header class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-10 animate-fade-in-up" style="animation-delay: 0.1s;">
<div class="mb-4 sm:mb-0">
<h1 class="text-4xl md:text-5xl font-bold text-text-dark flex items-center gap-3">
<i data-lucide="map-pin" class="w-10 h-10 text-warm-orange"></i>
人生轨迹
</h1>
<p class="text-lg text-text-medium mt-4">记录你的每一个重要时刻,见证成长</p>
</div>
<button id="add-life-event-btn" class="bg-warm-orange text-white px-6 py-3 rounded-full font-semibold hover:bg-orange-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-orange-500/30 flex items-center space-x-2 animate-fade-in-up" style="animation-delay: 0.3s;">
<i data-lucide="plus-circle" class="w-5 h-5"></i>
<span>添加人生事件</span>
</button>
</header>
<div id="life-events-timeline-container" class="animate-fade-in-up" style="animation-delay: 0.5s;">
<div id="life-events-empty" class="hidden text-center py-20 border-2 border-dashed border-gray-300 rounded-2xl">
<i data-lucide="flag" class="w-16 h-16 mx-auto text-gray-400 mb-4"></i>
<p class="text-text-medium text-lg">你可以添加一件重要的事——不论它是美好还是悲伤,都值得被记录。</p>
</div>
<div id="life-events-timeline" class="space-y-12">
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-white">
<div class="container mx-auto px-6 py-12">
<div class="mt-12 border-t border-gray-200 pt-8 text-center text-text-medium">
<p>&copy; 2025 开心APP. All Rights Reserved. 来自"开心"星球的温柔科技。</p>
</div>
</div>
</footer>
</div>
<!-- Add Life Event Modal -->
<div id="add-event-modal" class="fixed inset-0 bg-gray-900/80 backdrop-blur-sm z-[9998] hidden items-center justify-center p-4 transition-opacity duration-300">
<div class="bg-light-gray rounded-2xl shadow-2xl w-full max-w-lg transform transition-all duration-300 scale-95 opacity-0" id="add-event-modal-content">
<form id="life-event-form">
<div class="p-6 border-b border-gray-200">
<h3 class="text-2xl font-bold text-text-dark flex items-center gap-3"><i data-lucide="feather" class="text-tech-blue"></i>记录一件人生大事</h3>
</div>
<div class="p-6 space-y-5">
<div>
<label for="event-date" class="block text-sm font-medium text-text-medium mb-1.5">事件发生日期</label>
<input type="date" id="event-date" name="date" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" required>
</div>
<div>
<label for="event-title" class="block text-sm font-medium text-text-medium mb-1.5">事件标题</label>
<input type="text" id="event-title" name="title" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="例如:大学毕业" required>
</div>
<div>
<label for="event-content" class="block text-sm font-medium text-text-medium mb-1.5">详细内容</label>
<textarea id="event-content" name="content" rows="5" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="详细描述一下当时发生了什么,以及你的感受..." required></textarea>
</div>
<div>
<label class="block text-sm font-medium text-text-medium mb-1.5">这是...?</label>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="eventType" value="positive" class="form-radio text-emerald-500 focus:ring-emerald-400" checked>
<span class="text-emerald-600 font-medium">正面/高光事件</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="eventType" value="negative" class="form-radio text-red-500 focus:ring-red-400">
<span class="text-red-600 font-medium">负面/创伤事件</span>
</label>
</div>
</div>
</div>
<div class="p-6 bg-white rounded-b-2xl flex justify-end items-center gap-4">
<button type="button" id="cancel-add-event" class="px-5 py-2.5 rounded-lg text-text-medium hover:bg-gray-100 transition">取消</button>
<button type="submit" class="bg-tech-blue text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-blue-600 transition-all duration-300">保存并获取AI分析</button>
</div>
</form>
</div>
</div>
<!-- Write Letter Modal -->
<div id="write-letter-modal" class="fixed inset-0 bg-gray-900/80 backdrop-blur-sm z-[9999] hidden items-center justify-center p-4 transition-opacity duration-300">
<div class="bg-light-gray rounded-2xl shadow-2xl w-full max-w-2xl transform transition-all duration-300 scale-95 opacity-0" id="write-letter-modal-content">
<div class="p-6 border-b border-gray-200 flex justify-between items-start">
<div>
<h3 class="text-2xl font-bold text-text-dark flex items-center gap-3"><i data-lucide="mail" class="text-warm-orange"></i>写给过去的自己</h3>
<p class="text-text-medium mt-1">让开开帮你给那时的自己带去一些鼓励和智慧吧</p>
</div>
<button id="close-letter-modal" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="x" class="w-7 h-7"></i>
</button>
</div>
<div class="p-6 max-h-[60vh] overflow-y-auto">
<p class="text-text-medium mb-4">正在为 <strong id="letter-event-title" class="text-tech-blue"></strong> 事件生成信件...</p>
<div id="letter-content" class="bg-white p-6 rounded-lg border prose max-w-none">
<div id="letter-placeholder" class="text-center py-10">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-tech-blue mx-auto"></div>
<p class="mt-4 text-text-medium">开开正在为你撰写信件...</p>
</div>
<div id="letter-final-content" class="hidden"></div>
</div>
</div>
<div class="p-6 bg-white rounded-b-2xl flex justify-end items-center gap-4">
<button id="regenerate-letter-btn" class="bg-gray-200 text-text-dark px-5 py-2.5 rounded-lg font-semibold hover:bg-gray-300 transition-all duration-300 flex items-center gap-2">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
<span>重新生成</span>
</button>
<button id="copy-letter-btn" class="bg-tech-blue text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-blue-600 transition-all duration-300 flex items-center gap-2">
<i data-lucide="copy" class="w-4 h-4"></i>
<span>复制信件</span>
</button>
</div>
</div>
</div>
<script type="module" src="js/shared.js"></script>
<script type="module" src="life_trajectory.js"></script>
</body>
</html>
@@ -0,0 +1,301 @@
document.addEventListener('DOMContentLoaded', () => {
const LIFE_EVENTS_STORAGE_KEY = 'kaixinapp_life_events_v1';
let lifeEvents = [];
let nextLifeEventId = 1;
const addLifeEventBtn = document.getElementById('add-life-event-btn');
const lifeEventsTimelineContainer = document.getElementById('life-events-timeline');
const lifeEventsEmptyState = document.getElementById('life-events-empty');
const addEventModal = document.getElementById('add-event-modal');
const addEventModalContent = document.getElementById('add-event-modal-content');
const cancelAddEventBtn = document.getElementById('cancel-add-event');
const lifeEventForm = document.getElementById('life-event-form');
const writeLetterModal = document.getElementById('write-letter-modal');
const writeLetterModalContent = document.getElementById('write-letter-modal-content');
const closeLetterModalBtn = document.getElementById('close-letter-modal');
const letterEventTitle = document.getElementById('letter-event-title');
const letterPlaceholder = document.getElementById('letter-placeholder');
const letterFinalContent = document.getElementById('letter-final-content');
const regenerateLetterBtn = document.getElementById('regenerate-letter-btn');
const copyLetterBtn = document.getElementById('copy-letter-btn');
let activeLetterEventId = null;
function loadLifeEvents() {
const stored = localStorage.getItem(LIFE_EVENTS_STORAGE_KEY);
if (stored) {
lifeEvents = JSON.parse(stored);
const maxId = lifeEvents.reduce((max, e) => Math.max(max, e.id), 0);
nextLifeEventId = maxId + 1;
} else {
lifeEvents = [{
id: 1,
date: '2024-06-15',
title: '大学毕业典礼',
content: '四年的大学生活画上了句号。穿着学士服,和朋友、老师们告别,心中充满了不舍和对未来的憧憬。这是一个时代的结束,也是一个新开始。',
type: 'positive',
aiAnalysis: {
title: '你做得很棒!',
response: '我好喜欢你记录下这段记忆。<br>你在这件事情里,展现了【坚持】、【自我支持】和【成长】。<br>别小看这一刻的你,它证明了:你,是可以做到的。',
keywords: ['坚持', '成长', '新起点'],
emotionTags: ['自豪', '憧憬', '不舍']
}
}, {
id: 2,
date: '2023-03-20',
title: '一次重要的面试失败',
content: '为心仪的公司准备了很久,但最终还是失败了。感觉很失落,甚至开始怀疑自己的能力。花了几天时间才慢慢走出来。',
type: 'negative',
aiAnalysis: {
title: '这段经历可能对你带来的影响…',
response: '你提到当时很难过,也许是因为你在那时没有得到你真正渴望的回应。<br>从那以后,这段经历可能让你在类似场景里格外敏感——<br>这不是脆弱,而是你曾经努力保护自己留下的本能。<br><br>开开理解你,也想和你一起慢慢松开这段结。',
keywords: ['挫折', '反思', '坚韧'],
emotionTags: ['失落', '焦虑', '怀疑']
}
}];
nextLifeEventId = 3;
saveLifeEvents();
}
}
function saveLifeEvents() {
localStorage.setItem(LIFE_EVENTS_STORAGE_KEY, JSON.stringify(lifeEvents));
}
function renderLifeEvents() {
if (!lifeEventsTimelineContainer) return;
if (lifeEvents.length === 0) {
lifeEventsEmptyState.classList.remove('hidden');
lifeEventsTimelineContainer.classList.add('hidden');
} else {
lifeEventsEmptyState.classList.add('hidden');
lifeEventsTimelineContainer.classList.remove('hidden');
lifeEvents.sort((a, b) => new Date(b.date) - new Date(a.date));
lifeEventsTimelineContainer.innerHTML = lifeEvents.map((event, index) => {
const isLastItem = index === lifeEvents.length - 1;
const cardBg = event.type === 'positive' ? 'bg-emerald-500/5 border-emerald-500/20' : 'bg-red-500/5 border-red-500/20';
const accentColor = event.type === 'positive' ? 'text-emerald-600' : 'text-red-600';
const icon = event.type === 'positive' ? 'sparkles' : 'heart-crack';
return `
<div class="relative pl-12 sm:pl-16 pb-4">
<div class="absolute left-0 top-0 text-right w-10 sm:w-12">
<p class="text-sm font-semibold text-text-dark">${new Date(event.date).getFullYear()}</p>
<p class="text-xs text-text-medium">${new Date(event.date).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }).replace('/', '-')}</p>
</div>
<div class="absolute left-[48px] sm:left-[60px] top-2.5 w-4 h-4 bg-tech-blue rounded-full border-4 border-light-gray z-10"></div>
${!isLastItem ? '<div class="absolute left-[55px] sm:left-[67px] top-4 h-full border-l-2 border-gray-200"></div>' : ''}
<div class="bg-white rounded-2xl shadow-lg border border-gray-200/50 overflow-hidden">
<div class="p-6">
<h3 class="text-2xl font-bold text-text-dark mb-2">${event.title}</h3>
<p class="text-text-medium leading-relaxed prose prose-sm max-w-none">${event.content}</p>
</div>
<div class="${cardBg} p-6">
<h4 class="font-bold mb-3 flex items-center gap-2 ${accentColor}">
<i data-lucide="${icon}" class="w-5 h-5"></i>
${event.aiAnalysis.title}
</h4>
<div class="text-sm leading-6 text-gray-700/80 mb-4">${event.aiAnalysis.response}</div>
<div class="space-y-3 text-sm">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-600 flex-shrink-0">成长关键词:</span>
<div class="flex flex-wrap gap-2">
${event.aiAnalysis.keywords.map(k => `<span class="bg-white/60 text-gray-700 text-xs font-medium px-2 py-1 rounded-md">${k}</span>`).join('')}
</div>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-600 flex-shrink-0">情绪标签:</span>
<div class="flex flex-wrap gap-2">
${event.aiAnalysis.emotionTags.map(t => `<span class="bg-white/60 text-gray-700 text-xs font-medium px-2 py-1 rounded-md">${t}</span>`).join('')}
</div>
</div>
</div>
</div>
<div class="bg-white/50 p-4 flex justify-end items-center space-x-3 border-t border-gray-200/50">
<button class="text-sm font-semibold text-text-medium hover:text-tech-blue transition-colors flex items-center gap-2">
<i data-lucide="search" class="w-4 h-4"></i>
探索这段经历
</button>
<button data-letter-event-id="${event.id}" class="text-sm font-semibold text-white bg-warm-orange px-4 py-2 rounded-lg hover:bg-orange-600 transition flex items-center gap-2">
<i data-lucide="mail" class="w-4 h-4"></i>
写给当时的自己
</button>
</div>
</div>
</div>
`;
}).join('');
if (typeof lucide !== 'undefined') lucide.createIcons();
attachLifeEventButtonListeners();
}
}
function attachLifeEventButtonListeners() {
document.querySelectorAll('[data-letter-event-id]').forEach(btn => {
btn.addEventListener('click', () => {
const eventId = btn.dataset.letterEventId;
openWriteLetterModal(eventId);
});
});
}
function openModalAnimation(modal, content) {
modal.classList.remove('hidden');
modal.classList.add('flex');
setTimeout(() => {
modal.classList.remove('opacity-0');
content.classList.remove('scale-95', 'opacity-0');
}, 10);
}
function closeModalAnimation(modal, content) {
modal.classList.add('opacity-0');
content.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}, 300);
}
function showAddEventModal() {
lifeEventForm.reset();
document.getElementById('event-date').value = new Date().toISOString().split('T')[0];
openModalAnimation(addEventModal, addEventModalContent);
}
function hideAddEventModal() {
closeModalAnimation(addEventModal, addEventModalContent);
}
function handleLifeEventFormSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const event = {
id: nextLifeEventId++,
date: formData.get('date'),
title: formData.get('title'),
content: formData.get('content'),
type: formData.get('eventType'),
aiAnalysis: generateAiAnalysis(formData.get('content'), formData.get('eventType'))
};
lifeEvents.push(event);
saveLifeEvents();
renderLifeEvents();
hideAddEventModal();
}
function generateAiAnalysis(content, type) {
const analysis = {};
if (type === 'positive') {
analysis.title = '你做得很棒!';
analysis.response = '我好喜欢你记录下这段记忆。<br>你在这件事情里,展现了【坚持】、【自我支持】和【成长】。<br>别小看这一刻的你,它证明了:你,是可以做到的。';
analysis.keywords = ['坚持', '自我支持', '成长'];
analysis.emotionTags = ['快乐', '成就感', '自豪'];
} else {
analysis.title = '这段经历可能对你带来的影响…';
analysis.response = '你提到当时很难过,也许是因为你在那时没有得到你真正渴望的回应。<br>从那以后,这段经历可能让你在类似场景里格外敏感——<br>这不是脆弱,而是你曾经努力保护自己留下的本能。<br><br>开开理解你,也想和你一起慢慢松开这段结。';
analysis.keywords = ['反思', '坚韧', '接纳'];
analysis.emotionTags = ['悲伤', '脆弱', '思考'];
}
if (content.includes('旅行')) analysis.keywords.push('探索');
if (content.includes('工作') || content.includes('面试')) analysis.keywords.push('职业');
if (content.includes('学习') || content.includes('毕业')) analysis.keywords.push('学业');
return analysis;
}
function openWriteLetterModal(eventId) {
const event = lifeEvents.find(e => e.id == eventId);
if (!event) return;
activeLetterEventId = eventId;
letterEventTitle.textContent = event.title;
letterPlaceholder.style.display = 'block';
letterFinalContent.classList.add('hidden');
letterFinalContent.innerHTML = '';
openModalAnimation(writeLetterModal, writeLetterModalContent);
generateLetter(event);
}
function hideWriteLetterModal() {
closeModalAnimation(writeLetterModal, writeLetterModalContent);
}
function generateLetter(event) {
letterPlaceholder.style.display = 'block';
letterFinalContent.classList.add('hidden');
setTimeout(() => {
let letter;
if (event.type === 'positive') {
letter = `<p>亲爱的,在【${event.date}】的你:</p>
<p>你好呀!我是来自未来的你,特地让开开捎来这封信。</p>
<p>我知道,在【${event.title}】的那一刻,你的心里一定充满了阳光。你所感受到的那种【${event.aiAnalysis.emotionTags.join('、')}】的情绪,是那么真实和宝贵。请一定好好珍藏这份感觉。</p>
<p>你当时展现出的【${event.aiAnalysis.keywords.join('、')}】的品质,在未来的日子里,也一直闪闪发光,帮助我走了很远的路。谢谢你,当时的你,那么勇敢,那么棒。</p>
<p>请继续带着这份光芒走下去吧!未来可期!</p>
<br><p class="text-right">爱你的,<br>未来的自己</p>`;
} else {
letter = `<p>亲爱的,在【${event.date}】的你:</p>
<p>你好。当你读到这封信时,我知道你正在经历【${event.title}】的艰难时刻,心里可能充满了【${event.aiAnalysis.emotionTags.join('、')}】的复杂感受。</p>
<p>我想告诉你,没关系,一切都会过去的。你当时的感受是完全正常的,请允许自己悲伤和脆弱。这不是你的错。这段经历虽然痛苦,但它也让你学会了【${event.aiAnalysis.keywords.join('、')}】。你比自己想象的要坚强得多。</p>
<p>请相信,未来的你,也就是我,已经从这段经历中走了出来,并且变得更加完整和强大。所以,请抱抱自己,告诉自己你已经做得很好了。</p>
<br><p class="text-right">永远支持你的,<br>未来的自己</p>`;
}
letterFinalContent.innerHTML = letter;
letterPlaceholder.style.display = 'none';
letterFinalContent.classList.remove('hidden');
}, 1500);
}
function handleCopyLetter() {
const content = letterFinalContent.innerText;
navigator.clipboard.writeText(content).then(() => {
const copyButton = document.getElementById('copy-letter-btn');
const originalText = copyButton.innerHTML;
copyButton.innerHTML = `<i data-lucide="check" class="w-4 h-4"></i> <span>已复制!</span>`;
if (typeof lucide !== 'undefined') lucide.createIcons();
setTimeout(() => {
copyButton.innerHTML = originalText;
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('复制失败');
});
}
if(addLifeEventBtn) {
addLifeEventBtn.addEventListener('click', showAddEventModal);
cancelAddEventBtn.addEventListener('click', hideAddEventModal);
addEventModal.addEventListener('click', (e) => {
if (e.target === addEventModal) hideAddEventModal();
});
lifeEventForm.addEventListener('submit', handleLifeEventFormSubmit);
closeLetterModalBtn.addEventListener('click', hideWriteLetterModal);
writeLetterModal.addEventListener('click', (e) => {
if (e.target === writeLetterModal) hideWriteLetterModal();
});
regenerateLetterBtn.addEventListener('click', () => {
const event = lifeEvents.find(e => e.id == activeLetterEventId);
if(event) generateLetter(event);
});
copyLetterBtn.addEventListener('click', handleCopyLetter);
loadLifeEvents();
renderLifeEvents();
}
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>消息中心 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased">
<!-- App Header -->
<header class="fixed top-0 left-0 right-0 z-40 bg-white/90 backdrop-blur-md border-b border-gray-200/80">
<div class="container mx-auto px-4 h-16 flex items-center justify-between relative">
<a href="javascript:history.back()" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="chevron-left" class="w-6 h-6"></i>
</a>
<h1 class="text-lg font-semibold text-text-dark absolute left-1/2 -translate-x-1/2">消息中心</h1>
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
</div>
</header>
<main class="pt-20 pb-8 bg-light-gray min-h-screen">
<div class="container mx-auto px-6">
<div id="message-list" class="max-w-3xl mx-auto space-y-4">
<!-- Messages will be injected here by messages.js -->
</div>
</div>
</main>
</div>
<script type="module" src="messages.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
</body>
</html>
@@ -0,0 +1,77 @@
import { navLinks } from './data.js';
const renderMessages = () => {
const messages = [
{
type: 'ai',
icon: 'sparkles',
color: 'text-warm-orange',
title: '开开的每周心情总结',
content: '你好呀!上周我们聊了很多关于"新工作的挑战",你表现出了很棒的适应能力和积极心态。记得给自己一些放松的时间哦,比如看看你喜欢的电影。',
timestamp: '2025年7月15日 09:30'
},
{
type: 'system',
icon: 'bell',
color: 'text-tech-blue',
title: '系统通知:欢迎使用日记功能',
content: '现在,你可以在日记区记录下你的生活点滴,开开会阅读你的日记并给你温暖的回复和鼓励哦。',
timestamp: '2025年7月14日 18:00'
},
{
type: 'ai',
icon: 'sparkles',
color: 'text-warm-orange',
title: '开开的话题追踪提醒',
content: '我发现你最近经常提到"学吉他",我已经为你创建了一个话题追踪卡片,帮你记录学习进度和心得。一起加油吧!',
timestamp: '2025年7月12日 11:25'
},
{
type: 'system',
icon: 'award',
color: 'text-green-500',
title: '成就解锁:初次见面',
content: '恭喜你完成了与开开的第一次对话,这是共同成长的第一步。',
timestamp: '2025年7月10日 20:45'
}
];
const messageListContainer = document.getElementById('message-list');
if (messageListContainer) {
messageListContainer.innerHTML = '';
messages.forEach((msg, index) => {
const messageEl = document.createElement('div');
messageEl.className = 'bg-white p-5 rounded-xl shadow-sm border border-gray-200/80 flex items-start space-x-4 hover:shadow-md hover:border-tech-blue/30 transition-all duration-300 animate-fade-in-up';
messageEl.style.animationDelay = `${index * 0.1}s`;
messageEl.innerHTML = `
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-light-gray flex items-center justify-center border">
<i data-lucide="${msg.icon}" class="w-5 h-5 ${msg.color}"></i>
</div>
<div class="flex-grow">
<div class="flex justify-between items-center">
<h3 class="font-bold text-text-dark">${msg.title}</h3>
<span class="text-xs text-text-medium whitespace-nowrap">${msg.timestamp}</span>
</div>
<p class="text-text-medium mt-1 pr-4">${msg.content}</p>
</div>
<button class="flex-shrink-0 text-text-medium hover:text-tech-blue self-center">
<i data-lucide="chevron-right" class="w-5 h-5"></i>
</button>
`;
messageListContainer.appendChild(messageEl);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
};
document.addEventListener('DOMContentLoaded', () => {
renderMessages();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人展板 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark flex flex-col h-screen antialiased">
<!-- Header -->
<header class="bg-white shadow-md z-10 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between relative">
<div class="flex items-center space-x-4">
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
<a href="messages.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="bell" class="w-6 h-6"></i>
</a>
</div>
<h1 class="text-lg font-bold text-text-dark absolute left-1/2 -translate-x-1/2">个人展板</h1>
<a href="settings.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="user" class="w-6 h-6"></i>
</a>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto p-4 lg:p-6 pb-24">
<div id="dashboard-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Basic Info Card -->
<div class="bg-white p-6 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">基础信息</h2>
<i data-lucide="user-round" class="text-tech-blue"></i>
</div>
<div id="basic-info-container" class="grid grid-cols-2 gap-4 text-sm">
<!-- Basic info will be injected here by JS -->
</div>
</div>
<!-- Mood Chart Card -->
<div class="bg-white p-6 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">近期心情统计</h2>
<i data-lucide="activity" class="text-warm-orange"></i>
</div>
<div class="relative h-48">
<canvas id="moodChart"></canvas>
</div>
</div>
<!-- Interests Card -->
<div class="bg-white p-6 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">兴趣爱好</h2>
<button id="add-interest-btn" class="text-text-medium hover:text-tech-blue transition-colors" title="添加兴趣">
<i data-lucide="plus" class="w-5 h-5"></i>
</button>
</div>
<div id="interests-container" class="flex flex-wrap gap-2 text-sm min-h-[36px]">
<!-- Interests will be injected here -->
</div>
<div class="mt-4">
<button id="explore-interests-btn" class="w-full text-sm bg-tech-blue/10 text-tech-blue font-semibold py-2 px-4 rounded-lg hover:bg-tech-blue/20 transition-colors flex items-center justify-center space-x-2">
<i data-lucide="sparkles" class="w-4 h-4"></i>
<span>探索可能发展的爱好</span>
</button>
</div>
</div>
<!-- Skills Card -->
<div class="bg-white p-6 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">生活技能</h2>
<button id="add-skill-btn" class="text-text-medium hover:text-tech-blue transition-colors" title="添加技能">
<i data-lucide="plus" class="w-5 h-5"></i>
</button>
</div>
<div id="skills-container" class="flex flex-wrap gap-2 text-sm min-h-[36px]">
<!-- Skills will be injected here -->
</div>
<div class="mt-4">
<button id="explore-skills-btn" class="w-full text-sm bg-tech-blue/10 text-tech-blue font-semibold py-2 px-4 rounded-lg hover:bg-tech-blue/20 transition-colors flex items-center justify-center space-x-2">
<i data-lucide="flask-conical" class="w-4 h-4"></i>
<span>探索可能发展的技能</span>
</button>
</div>
</div>
<!-- Personal Quotes Module -->
<div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">个人语录</h2>
<button class="text-text-medium hover:text-tech-blue transition-colors" title="添加语录">
<i data-lucide="plus-square" class="w-5 h-5"></i>
</button>
</div>
<div id="quotes-container" class="space-y-4">
<!-- Quote cards will be injected here by JS -->
</div>
</div>
<!-- Dynamic modules will be added here -->
</div>
<!-- Add custom module button -->
<div class="mt-6 text-center">
<button id="add-custom-module-btn" class="bg-warm-orange text-white px-6 py-3 rounded-full font-semibold hover:bg-orange-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-orange-500/30 flex items-center justify-center space-x-2 mx-auto">
<i data-lucide="layout-template" class="w-5 h-5"></i>
<span>自由添加模块</span>
</button>
</div>
</main>
<!-- App Navigation -->
<div id="bottom-nav-placeholder"></div>
<script type="module" src="js/app_nav.js"></script>
<script type="module" src="personal_dashboard.js"></script>
</body>
</html>
@@ -0,0 +1,309 @@
const userData = {
basicInfo: {
"MBTI": "INFP",
"星座": "双鱼座",
},
moods: {
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
data: [5, 7, 6, 8, 9, 7, 6],
events: ['感觉效率很高', '和开开聊了很久', '看了一部好电影', '工作有点累', '完成了一个小目标', '周末去徒步了', '为新的一周做准备']
},
interests: ["阅读", "电影", "编程", "徒步"],
skills: ["Python", "JavaScript", "写作", "烹饪"],
quotes: [
{ text: "愿你走出半生,归来仍是少年。", source: "与开开的对话" },
{ text: "重要的东西用眼睛是看不见的。", source: "小王子" },
]
};
function renderBasicInfo() {
const container = document.getElementById('basic-info-container');
if (!container) return;
container.innerHTML = '';
for (const [key, value] of Object.entries(userData.basicInfo)) {
const infoItem = document.createElement('div');
infoItem.innerHTML = `
<p class="text-text-medium">${key}</p>
<p class="font-semibold text-text-dark">${value}</p>
`;
container.appendChild(infoItem);
}
}
function renderQuotes() {
const container = document.getElementById('quotes-container');
if (!container) return;
container.innerHTML = '';
userData.quotes.forEach(quote => {
const quoteCard = document.createElement('div');
quoteCard.className = 'bg-light-gray p-4 rounded-lg';
quoteCard.innerHTML = `
<p class="text-text-dark">“${quote.text}”</p>
<p class="text-right text-text-medium text-sm mt-2">- ${quote.source}</p>
`;
container.appendChild(quoteCard);
});
}
function renderTagList(containerId, dataArray, onAdd, onDelete) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
dataArray.forEach((item, index) => {
const tag = document.createElement('div');
tag.className = 'flex items-center bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-1.5 rounded-full animate-fade-in-up';
tag.innerHTML = `
<span>${item}</span>
<button class="ml-1.5 text-blue-600 hover:text-blue-800" data-index="${index}">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
`;
tag.querySelector('button').addEventListener('click', () => onDelete(index));
container.appendChild(tag);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
function renderMoodChart() {
const ctx = document.getElementById('moodChart');
if (!ctx) return;
new Chart(ctx, {
type: 'line',
data: {
labels: userData.moods.labels,
datasets: [{
label: '心情指数',
data: userData.moods.data,
borderColor: 'rgba(245, 166, 35, 0.8)',
backgroundColor: 'rgba(245, 166, 35, 0.2)',
fill: true,
tension: 0.4,
pointBackgroundColor: '#fff',
pointBorderColor: 'rgba(245, 166, 35, 1)',
pointHoverBackgroundColor: 'rgba(245, 166, 35, 1)',
pointHoverBorderColor: '#fff',
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
onClick: (event, elements) => {
if (elements.length > 0) {
const elementIndex = elements[0].index;
const day = userData.moods.labels[elementIndex];
const moodScore = userData.moods.data[elementIndex];
const eventText = userData.moods.events[elementIndex] || '暂无记录';
alert(`日期: ${day}\n心情指数: ${moodScore}\n相关事件/记录: ${eventText}`);
}
},
scales: {
y: {
beginAtZero: true,
max: 10,
grid: {
drawBorder: false,
},
ticks: {
stepSize: 2
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return ` 心情指数: ${context.formattedValue} (点击查看详情)`;
}
}
}
}
}
});
}
function showExploreModal(type) {
const modalOverlay = document.createElement('div');
modalOverlay.className = 'fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 animate-fade-in-up';
const title = type === 'interest' ? '探索新爱好' : '探索新技能';
const question = type === 'interest' ? '最近对什么新事物感到好奇?' : '有什么想要学习或掌握的新本领吗?';
const placeholder = type === 'interest' ? '例如:天体物理、陶艺、古典音乐...' : '例如:视频剪辑、理财规划、一种新语言...';
modalOverlay.innerHTML = `
<div class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md transform animate-fade-in-up" style="animation-delay: 0.1s;">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-text-dark">${title}</h3>
<button id="close-explore-modal" class="text-text-medium hover:text-tech-blue"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<div>
<p class="text-sm text-text-medium mb-1 flex items-center"><i data-lucide="sparkles" class="w-4 h-4 mr-2 text-warm-orange"></i>AI 引导提问</p>
<p class="bg-light-gray p-3 rounded-lg text-text-dark mb-4">${question}</p>
<textarea id="explore-input" rows="3" class="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-tech-blue focus:border-transparent outline-none transition" placeholder="${placeholder}"></textarea>
<button id="submit-explore" class="w-full mt-4 bg-tech-blue text-white px-5 py-2.5 rounded-lg font-semibold hover:bg-blue-600 transition-all">获取智能推荐</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
lucide.createIcons();
const closeModal = () => modalOverlay.remove();
modalOverlay.querySelector('#close-explore-modal').addEventListener('click', closeModal);
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) closeModal();
});
modalOverlay.querySelector('#submit-explore').addEventListener('click', () => {
const input = modalOverlay.querySelector('#explore-input').value;
if (input.trim()) {
alert(`AI正在根据 "${input}" 为您生成推荐... (此为演示功能)`);
closeModal();
} else {
alert('请输入一些内容,AI才能更好地帮助你哦!');
}
});
}
function showAddModuleModal() {
const modalOverlay = document.createElement('div');
modalOverlay.className = 'fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 animate-fade-in-up';
modalOverlay.innerHTML = `
<div class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md transform animate-fade-in-up" style="animation-delay: 0.1s;">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-text-dark">自由添加模块</h3>
<button id="close-add-module-modal" class="text-text-medium hover:text-tech-blue"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<div>
<p class="text-sm text-text-medium mb-3">请选择一个分类,或随意填写你想记录的内容:</p>
<div id="module-categories" class="flex flex-wrap gap-2 mb-4">
<button class="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full hover:bg-blue-200 transition">工作行业</button>
<button class="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full hover:bg-blue-200 transition">学习背景</button>
<button class="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full hover:bg-blue-200 transition">我的梦想</button>
</div>
<div class="flex items-center gap-2">
<input id="custom-module-input" type="text" class="flex-grow border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-warm-orange focus:border-transparent outline-none transition" placeholder="或输入自定义模块名称">
<button id="submit-custom-module" class="bg-warm-orange text-white px-4 py-2 rounded-lg font-semibold hover:bg-orange-600 transition-all">添加</button>
</div>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
lucide.createIcons();
const closeModal = () => modalOverlay.remove();
modalOverlay.querySelector('#close-add-module-modal').addEventListener('click', closeModal);
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) closeModal();
});
const addModule = (title) => {
if (title && title.trim() !== '') {
createCustomModule(title.trim());
closeModal();
} else {
alert('模块名称不能为空!');
}
};
modalOverlay.querySelector('#module-categories').addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
addModule(e.target.textContent);
}
});
modalOverlay.querySelector('#submit-custom-module').addEventListener('click', () => {
const input = modalOverlay.querySelector('#custom-module-input').value;
addModule(input);
});
}
function setupEventListeners() {
document.getElementById('add-interest-btn')?.addEventListener('click', () => {
const newInterest = prompt("请输入新的兴趣爱好:");
if (newInterest && newInterest.trim() !== '') {
userData.interests.push(newInterest.trim());
renderTagList('interests-container', userData.interests, null, deleteInterest);
}
});
document.getElementById('add-skill-btn')?.addEventListener('click', () => {
const newSkill = prompt("请输入新的生活技能:");
if (newSkill && newSkill.trim() !== '') {
userData.skills.push(newSkill.trim());
renderTagList('skills-container', userData.skills, null, deleteSkill);
}
});
document.getElementById('explore-interests-btn')?.addEventListener('click', () => {
showExploreModal('interest');
});
document.getElementById('explore-skills-btn')?.addEventListener('click', () => {
showExploreModal('skill');
});
document.getElementById('add-custom-module-btn')?.addEventListener('click', () => {
showAddModuleModal();
});
}
function deleteInterest(index) {
userData.interests.splice(index, 1);
renderTagList('interests-container', userData.interests, null, deleteInterest);
}
function deleteSkill(index) {
userData.skills.splice(index, 1);
renderTagList('skills-container', userData.skills, null, deleteSkill);
}
function createCustomModule(title) {
const grid = document.getElementById('dashboard-grid');
if(!grid) return;
const moduleEl = document.createElement('div');
moduleEl.className = 'bg-white p-6 rounded-xl shadow-sm lg:col-span-2 animate-fade-in-up';
moduleEl.innerHTML = `
<div class="flex justify-between items-center mb-4">
<h2 class="font-bold text-text-dark text-lg">${title}</h2>
<div class="flex items-center space-x-2">
<button class="text-text-medium hover:text-tech-blue" title="编辑"><i data-lucide="edit-3" class="w-5 h-5"></i></button>
</div>
</div>
<div>
<p class="text-sm text-text-medium italic">AI正在根据您的信息自动生成摘要... 您也可以点击右上角编辑按钮手动填写。</p>
</div>
`;
grid.appendChild(moduleEl);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
document.addEventListener('DOMContentLoaded', () => {
renderBasicInfo();
renderQuotes();
renderTagList('interests-container', userData.interests, null, deleteInterest);
renderTagList('skills-container', userData.skills, null, deleteSkill);
renderMoodChart();
setupEventListeners();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -0,0 +1,78 @@
import { features } from './data.js';
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
const featuresGrid = document.getElementById('features-grid');
if (featuresGrid) {
features.forEach((feature, index) => {
const card = document.createElement('div');
card.className = 'feature-card-bg rounded-2xl p-6 flex flex-col items-center text-center scroll-target';
card.style.transitionDelay = `${index * 100}ms`;
card.innerHTML = `
<div class="w-full aspect-square rounded-xl overflow-hidden mb-6 feature-card-image-container flex items-center justify-center">
<img src="${feature.image}" alt="${feature.alt}" class="w-4/5 h-4/5 object-contain drop-shadow-lg">
</div>
<div class="flex items-center space-x-2 mb-3">
<i data-lucide="${feature.icon}" class="w-5 h-5 text-tech-blue"></i>
<h3 class="text-xl font-bold text-text-dark">${feature.title}</h3>
</div>
<p class="text-text-medium text-sm flex-grow">${feature.description}</p>
`;
featuresGrid.appendChild(card);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
const scrollObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.scroll-target').forEach(target => {
scrollObserver.observe(target);
});
const loginButton = document.getElementById('login-button');
const loginModal = document.getElementById('login-modal');
const closeModalButton = document.getElementById('close-modal-button');
if (loginButton && loginModal && closeModalButton) {
const openModal = () => {
loginModal.classList.remove('hidden');
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
};
const closeModal = () => {
loginModal.classList.add('hidden');
};
loginButton.addEventListener('click', openModal);
closeModalButton.addEventListener('click', closeModal);
loginModal.addEventListener('click', (event) => {
if (event.target === loginModal) {
closeModal();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !loginModal.classList.contains('hidden')) {
closeModal();
}
});
}
});
@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户中心 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased">
<!-- App Header -->
<header class="fixed top-0 left-0 right-0 z-40 bg-white/90 backdrop-blur-md border-b border-gray-200/80">
<div class="container mx-auto px-4 h-16 flex items-center justify-between relative">
<a href="javascript:history.back()" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="chevron-left" class="w-6 h-6"></i>
</a>
<h1 class="text-lg font-semibold text-text-dark absolute left-1/2 -translate-x-1/2">用户中心</h1>
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
</div>
</header>
<main class="pt-20 pb-8 bg-light-gray min-h-screen">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto space-y-10">
<!-- 个人资料管理 -->
<section class="bg-white p-8 rounded-2xl shadow-lg border border-gray-200/50 animate-fade-in-up" style="animation-delay: 0.2s;">
<div class="flex items-center mb-6 space-x-3">
<i data-lucide="user-cog" class="w-7 h-7 text-tech-blue"></i>
<h2 class="text-2xl font-bold text-text-dark">个人资料管理</h2>
</div>
<div class="space-y-6">
<div class="flex items-center space-x-6">
<img src="https://r2.flowith.net/files/o/1752574572161-kaikai_character_energetic_animation_index_2@1024x1024.png" alt="User Avatar" class="w-20 h-20 rounded-full object-cover border-4 border-white shadow">
<button class="text-sm font-semibold text-tech-blue hover:underline">更换头像</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-text-medium mb-1">昵称</label>
<input type="text" value="小明" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition">
</div>
<div>
<label class="block text-sm font-medium text-text-medium mb-1">邮箱</label>
<input type="email" value="user@example.com" disabled class="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-text-medium cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-text-medium mb-1">MBTI</label>
<input type="text" placeholder="例如: INFP" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition">
</div>
<div>
<label class="block text-sm font-medium text-text-medium mb-1">星座</label>
<input type="text" placeholder="例如: 双子座" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition">
</div>
</div>
<div class="pt-4 border-t border-gray-200 flex justify-end">
<button class="bg-tech-blue text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-blue-600 transition-all duration-300 transform hover:scale-105">保存更改</button>
</div>
</div>
</section>
<!-- 会员订阅 -->
<section class="bg-white p-8 rounded-2xl shadow-lg border border-gray-200/50 animate-fade-in-up" style="animation-delay: 0.4s;">
<div class="flex items-center mb-6 space-x-3">
<i data-lucide="gem" class="w-7 h-7 text-warm-orange"></i>
<h2 class="text-2xl font-bold text-text-dark">会员订阅</h2>
</div>
<div class="flex items-center justify-between bg-light-gray p-6 rounded-lg">
<div>
<p class="text-text-medium">当前状态</p>
<p class="text-xl font-bold text-tech-blue">免费会员</p>
</div>
<button class="bg-warm-orange text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-orange-600 transition-all duration-300 transform hover:scale-105">升级到 Pro</button>
</div>
</section>
<!-- AI聊天偏好设置 -->
<section class="bg-white p-8 rounded-2xl shadow-lg border border-gray-200/50 animate-fade-in-up" style="animation-delay: 0.6s;">
<div class="flex items-center mb-6 space-x-3">
<i data-lucide="bot" class="w-7 h-7 text-green-500"></i>
<h2 class="text-2xl font-bold text-text-dark">AI 聊天偏好设置</h2>
</div>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h4 class="font-semibold text-text-dark">开启每日心情总结</h4>
<p class="text-sm text-text-medium">开开会在每天晚上为你发送一份心情总结。</p>
</div>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" class="sr-only toggle-checkbox" checked>
<div class="block bg-gray-200 w-14 h-8 rounded-full toggle-label transition"></div>
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
</div>
</label>
</div>
<div class="flex items-center justify-between">
<div>
<h4 class="font-semibold text-text-dark">接收主动问候</h4>
<p class="text-sm text-text-medium">允许开开在你长时间未上线时主动关心你。</p>
</div>
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" class="sr-only toggle-checkbox">
<div class="block bg-gray-200 w-14 h-8 rounded-full toggle-label transition"></div>
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
</div>
</label>
</div>
<div>
<h4 class="font-semibold text-text-dark mb-2">对话风格</h4>
<div class="flex flex-col sm:flex-row gap-4">
<label class="flex-1 flex items-center p-3 border rounded-lg cursor-pointer hover:bg-light-gray transition">
<input type="radio" name="style" class="form-radio text-tech-blue" checked>
<span class="ml-3 text-text-dark">温柔鼓励</span>
</label>
<label class="flex-1 flex items-center p-3 border rounded-lg cursor-pointer hover:bg-light-gray transition">
<input type="radio" name="style" class="form-radio text-tech-blue">
<span class="ml-3 text-text-dark">幽默风趣</span>
</label>
<label class="flex-1 flex items-center p-3 border rounded-lg cursor-pointer hover:bg-light-gray transition">
<input type="radio" name="style" class="form-radio text-tech-blue">
<span class="ml-3 text-text-dark">深度思辨</span>
</label>
</div>
</div>
</div>
</section>
</div>
</div>
</main>
</div>
<script type="module" src="settings.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
</body>
</html>
@@ -0,0 +1,7 @@
import { navLinks } from './data.js';
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
+163
View File
@@ -0,0 +1,163 @@
:root {
--tech-blue: #4A90E2;
--warm-orange: #F5A623;
--white: #FFFFFF;
--light-gray: #F7F8FA;
--text-dark: #333333;
--text-medium: #888888;
}
body {
font-family: 'Noto Sans SC', sans-serif;
}
.bg-tech-blue { background-color: var(--tech-blue); }
.bg-warm-orange { background-color: var(--warm-orange); }
.bg-light-gray { background-color: var(--light-gray); }
.text-tech-blue { color: var(--tech-blue); }
.text-text-dark { color: var(--text-dark); }
.text-text-medium { color: var(--text-medium); }
.border-tech-blue { border-color: var(--tech-blue); }
#main-header.scrolled {
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;
}
.feature-card-bg {
background-color: var(--white);
border: 1px solid #e5e7eb;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card-bg:hover {
transform: translateY(-8px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.feature-card-image-container {
background-color: #eef5fe;
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;
}
.animate-fade-in-up {
animation: fade-in-up 0.8s ease-out forwards;
opacity: 0;
}
@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;
}
.scroll-target.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;
}
.wave:nth-of-type(2) {
animation-direction: reverse;
animation-duration: 20s;
opacity: 0.8;
}
.wave:nth-of-type(3) {
animation-duration: 25s;
opacity: 0.5;
}
@keyframes wave {
0% { transform: translateX(0); }
50% { transform: translateX(-50%); }
100% { transform: translateX(0); }
}
#chat-messages {
scrollbar-width: thin;
scrollbar-color: var(--tech-blue) var(--light-gray);
}
#chat-messages::-webkit-scrollbar {
width: 6px;
}
#chat-messages::-webkit-scrollbar-track {
background: var(--light-gray);
}
#chat-messages::-webkit-scrollbar-thumb {
background-color: var(--tech-blue);
border-radius: 10px;
border: 2px solid transparent;
background-clip: content-box;
}
.message-animate {
animation: message-fade-in 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
opacity: 0;
transform: translateY(10px);
}
@keyframes message-fade-in {
from {
opacity: 0;
transform: translateY(10px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
#topic-detail-modal.hidden {
display: none;
}
#login-modal:not(.hidden) {
animation: modal-fade-in 0.2s ease-out forwards;
}
#login-modal:not(.hidden) > div {
animation: modal-scale-up 0.2s ease-out forwards;
}
@keyframes modal-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modal-scale-up {
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
}
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>话题追踪 - 开心APP</title>
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png">
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
</head>
<body class="bg-light-gray font-sans text-text-dark">
<div id="app-container" class="antialiased flex flex-col min-h-screen">
<!-- Header -->
<header class="bg-white shadow-md z-10 flex-shrink-0">
<div class="container mx-auto px-4 py-3 flex items-center justify-between relative">
<div class="flex items-center space-x-4">
<a href="index.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="home" class="w-6 h-6"></i>
</a>
<a href="messages.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="bell" class="w-6 h-6"></i>
</a>
</div>
<h1 class="text-lg font-bold text-text-dark absolute left-1/2 -translate-x-1/2">话题追踪</h1>
<a href="settings.html" class="text-text-medium hover:text-tech-blue transition-colors">
<i data-lucide="user" class="w-6 h-6"></i>
</a>
</div>
</header>
<main class="flex-grow pt-8 pb-28">
<div class="container mx-auto px-6">
<div class="text-center mb-12 animate-fade-in-up">
<h1 class="text-3xl md:text-4xl font-bold text-text-dark">洞察你的思绪,整理你的生活</h1>
<p class="text-base text-text-medium mt-3 max-w-2xl mx-auto">开开会自动梳理你最近关心的事,你也可以手动创建任何想追踪的话题,见证自己的思考与成长。</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-5 gap-12">
<div class="lg:col-span-3 animate-fade-in-up" style="animation-delay: 0.2s;">
<div class="flex items-center mb-6 space-x-3">
<i data-lucide="brain-circuit" class="w-8 h-8 text-tech-blue"></i>
<h2 class="text-2xl font-bold text-text-dark">AI 自动总结</h2>
</div>
<div id="ai-summary-list" class="space-y-6">
</div>
</div>
<div class="lg:col-span-2 animate-fade-in-up" style="animation-delay: 0.4s;">
<div class="bg-white p-6 sm:p-8 rounded-2xl shadow-lg border border-gray-200/50 scroll-mt-24" id="new-topic-form-container">
<div class="flex items-center mb-6 space-x-3">
<i data-lucide="plus-circle" class="w-8 h-8 text-warm-orange"></i>
<h2 class="text-2xl font-bold text-text-dark">我的话题</h2>
</div>
<form id="new-topic-form" class="space-y-4">
<div>
<label for="topic-title" class="block text-sm font-medium text-text-medium mb-1">话题标题</label>
<input type="text" id="topic-title" name="title" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="例如:暑期健身计划" required>
</div>
<div>
<label for="topic-content" class="block text-sm font-medium text-text-medium mb-1">初始内容</label>
<textarea id="topic-content" name="content" rows="4" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-tech-blue focus:border-transparent transition" placeholder="写下你的计划、想法或任何琐事..." required></textarea>
</div>
<button type="submit" class="w-full bg-warm-orange text-white px-5 py-3 rounded-lg font-semibold hover:bg-orange-600 transition-all duration-300 transform hover:scale-105 shadow-lg shadow-orange-500/30">创建新话题</button>
</form>
<div class="mt-8 border-t border-gray-200 pt-6">
<div class="flex items-center mb-4 space-x-3">
<i data-lucide="list" class="w-6 h-6 text-text-medium"></i>
<h3 class="text-xl font-semibold text-text-dark">已创建的话题</h3>
</div>
<div id="user-topics-list" class="space-y-4 max-h-96 overflow-y-auto pr-2">
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- App Navigation -->
<div id="bottom-nav-placeholder"></div>
</div>
<!-- Topic Detail Modal -->
<div id="topic-detail-modal" class="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div class="bg-light-gray rounded-2xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col">
<div class="p-5 border-b bg-white rounded-t-2xl flex justify-between items-start">
<div>
<h2 id="modal-topic-title" class="text-2xl font-bold text-text-dark">...</h2>
<p id="modal-topic-date" class="text-sm text-text-medium"></p>
</div>
<button id="close-modal-btn" class="text-text-medium hover:text-tech-blue transition-colors p-1">
<i data-lucide="x" class="w-7 h-7"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div id="modal-timeline-container" class="space-y-8">
</div>
<div id="modal-suggestion-container" class="mt-8">
</div>
</div>
<div class="p-6 border-t bg-white rounded-b-2xl">
<h3 class="text-base font-semibold text-text-dark mb-2">添加新进展</h3>
<form id="add-entry-form" class="flex items-start space-x-3">
<textarea id="new-entry-content" rows="2" class="flex-1 w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-tech-blue transition-shadow text-sm" placeholder="为这个话题添加新进展..."></textarea>
<button type="submit" class="bg-tech-blue text-white rounded-lg px-4 py-2 h-full font-semibold hover:bg-blue-600 transition-colors flex-shrink-0">
<i data-lucide="plus" class="w-5 h-5"></i>
</button>
</form>
</div>
</div>
</div>
<script type="module" src="js/app_nav.js"></script>
<script type="module" src="topic_tracker.js"></script>
</body>
</html>
@@ -0,0 +1,319 @@
document.addEventListener('DOMContentLoaded', () => {
const TOPICS_STORAGE_KEY = 'kaixinapp_user_topics_v4';
let nextUserTopicId = 1;
let aiTopics = [{
id: "ai-1",
date: "2025年7月14日",
title: "关于职业发展的思考",
summary: "近期你在日记和聊天中多次提到对目前工作的倦怠感,并探索学习新技能(如编程、设计)的可能性。",
keywords: ["职业倦怠", "新技能", "转行", "自我提升"],
timeline: [
{
stage_name: "探索阶段 (前期)",
stage_icon: "search",
items: [
{ id: 2, date: '2025-07-10', content: '搜索了在线编程课程,感觉眼花缭乱。' },
{ id: 3, date: '2025-07-12', content: '在日记里写下了对目前工作的厌烦,感觉陷入了瓶颈。' }
]
},
{
stage_name: "调研阶段 (当前)",
stage_icon: "clipboard-list",
items: [
{ id: 1, date: '2025-07-14', content: '和开开聊了关于转行做设计师的可能性,开开给了一些建议。' }
]
}
],
next_suggestion: '尝试接触一些免费的设计工具(如Figma、Canva),完成一个名片设计或海报制作的小项目。这能帮你评估自己对设计工作的实际兴趣和基本感觉。'
}, {
id: "ai-2",
date: "2025年7月10日",
title: "周末出游计划",
summary: "你似乎在计划一个短途旅行,多次查询关于海边城市的天气和美食推荐。",
keywords: ["旅行", "海边", "美食", "放松"],
timeline: [
{
stage_name: "萌芽阶段 (前期)",
stage_icon: "sprout",
items: [
{ id: 2, date: '2025-07-09', content: '天气好热,突然想去海边玩。' }
]
},
{
stage_name: "计划阶段 (当前)",
stage_icon: "map",
items: [
{ id: 1, date: '2025-07-10', content: '问开开哪个海边城市人少又好玩,它推荐了几个小众地点。' },
]
}
],
next_suggestion: '可以开始查看交通和住宿了,早点预定选择更多哦!如果需要,开开可以帮你对比价格。'
}];
let userTopics = [];
const aiSummaryContainer = document.getElementById('ai-summary-list');
const userTopicsListContainer = document.getElementById('user-topics-list');
const newTopicForm = document.getElementById('new-topic-form');
const modal = document.getElementById('topic-detail-modal');
const modalTitle = document.getElementById('modal-topic-title');
const modalDate = document.getElementById('modal-topic-date');
const closeModalBtn = document.getElementById('close-modal-btn');
const addEntryForm = document.getElementById('add-entry-form');
let activeTopicId = null;
function loadTopicsFromStorage() {
const stored = localStorage.getItem(TOPICS_STORAGE_KEY);
if (stored) {
userTopics = JSON.parse(stored);
const maxId = userTopics.reduce((max, t) => Math.max(max, parseInt(t.id.split('-')[1])), 0);
nextUserTopicId = maxId + 1;
} else {
userTopics = [{
id: `user-${nextUserTopicId++}`,
title: "暑期健身计划",
date: "2025年7月15日",
keywords: ["健康", "运动", "自律"],
timeline: [{
stage_name: "启动阶段 (当前)",
stage_icon: "rocket",
items: [
{ id: 1, date: '2025-07-15', content: "今天制定了计划:每周至少三次有氧运动,两次力量训练。记录每日饮食,控制热量摄入。" }
]
}],
next_suggestion: '找一个伙伴一起监督,或者使用App记录进程,增加成就感。'
}];
}
}
function saveTopicsToStorage() {
localStorage.setItem(TOPICS_STORAGE_KEY, JSON.stringify(userTopics));
}
function renderAiTopics() {
if (!aiSummaryContainer) return;
aiSummaryContainer.innerHTML = aiTopics.map(topic => `
<div data-topic-id="${topic.id}" class="bg-white p-6 rounded-xl shadow-md border border-gray-200/80 cursor-pointer hover:shadow-lg hover:border-tech-blue/50 transition-all duration-300">
<p class="text-sm text-text-medium mb-2">${topic.date}</p>
<h3 class="text-xl font-bold text-text-dark mb-3">${topic.title}</h3>
<p class="text-text-medium mb-4 text-sm">${topic.summary}</p>
<div class="flex flex-wrap gap-2">
${topic.keywords.map(k => `<span class="inline-block bg-tech-blue/10 text-tech-blue text-xs font-medium px-2.5 py-0.5 rounded-full">${k}</span>`).join('')}
</div>
</div>`).join('');
aiSummaryContainer.querySelectorAll('[data-topic-id]').forEach(el => el.addEventListener('click', () => openModal(el.dataset.topicId)));
}
function renderUserTopics() {
if (!userTopicsListContainer) return;
const getLastEntryContent = (topic) => {
if (!topic.timeline || topic.timeline.length === 0) return "暂无内容";
const lastStage = topic.timeline[topic.timeline.length - 1];
if (!lastStage.items || lastStage.items.length === 0) return "暂无内容";
return lastStage.items[lastStage.items.length - 1].content;
};
userTopicsListContainer.innerHTML = userTopics.length === 0 ? `<p class="text-text-medium text-center italic text-sm py-4">还没有创建话题哦</p>` :
userTopics.map(topic => `
<div class="bg-light-gray p-4 rounded-lg group transition-colors duration-200 hover:bg-gray-200/60">
<div class="flex justify-between items-start">
<div data-topic-id="${topic.id}" class="cursor-pointer flex-1 min-w-0 pr-2">
<h4 class="font-semibold text-text-dark group-hover:text-tech-blue transition-colors truncate">${topic.title}</h4>
<p class="text-sm text-text-medium mt-1 truncate">${getLastEntryContent(topic)}</p>
</div>
<button data-delete-id="${topic.id}" class="text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100 flex-shrink-0" title="删除话题">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</div>`).join('');
userTopicsListContainer.querySelectorAll('[data-topic-id]').forEach(el => el.addEventListener('click', () => openModal(el.dataset.topicId)));
userTopicsListContainer.querySelectorAll('[data-delete-id]').forEach(el => el.addEventListener('click', (e) => {
e.stopPropagation();
if(confirm('确定要删除这个话题吗?此操作无法撤销。')) {
deleteTopic(el.dataset.deleteId);
}
}));
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function deleteTopic(topicId) {
userTopics = userTopics.filter(t => t.id !== topicId);
saveTopicsToStorage();
renderUserTopics();
}
function openModal(topicId) {
const topic = [...aiTopics, ...userTopics].find(t => t.id === topicId);
if (!topic) return;
activeTopicId = topicId;
const isUserTopic = activeTopicId.startsWith('user-');
document.getElementById('add-entry-form').style.display = isUserTopic ? 'flex' : 'none';
document.querySelector('#add-entry-form').previousElementSibling.style.display = isUserTopic ? 'block' : 'none';
modalTitle.textContent = topic.title;
modalDate.textContent = `创建于 ${topic.date}`;
renderTopicTimeline(topic);
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function renderTopicTimeline(topic){
const timelineContainer = document.getElementById('modal-timeline-container');
const suggestionContainer = document.getElementById('modal-suggestion-container');
timelineContainer.innerHTML = '';
suggestionContainer.innerHTML = '';
const isUserTopic = topic.id.startsWith('user-');
if(topic.timeline && topic.timeline.length > 0) {
topic.timeline.forEach((stage, stageIndex) => {
const isCurrentStage = stageIndex === topic.timeline.length - 1;
const stageHtml = `
<div class="stage-group">
<div class="flex items-center space-x-3 mb-4">
<div class="w-10 h-10 ${isCurrentStage ? 'bg-tech-blue/20' : 'bg-gray-200'} rounded-full flex items-center justify-center flex-shrink-0">
<i data-lucide="${stage.stage_icon || 'flag'}" class="w-5 h-5 ${isCurrentStage ? 'text-tech-blue' : 'text-gray-600'}"></i>
</div>
<h3 class="text-xl font-bold ${isCurrentStage ? 'text-text-dark' : 'text-gray-500'}">${stage.stage_name}</h3>
${isCurrentStage ? '<span class="text-xs bg-tech-blue text-white font-bold py-0.5 px-2 rounded-full">当前</span>' : ''}
</div>
<div class="relative border-l-2 ${isCurrentStage ? 'border-tech-blue/30' : 'border-gray-300'} ml-5 pl-10 space-y-6">
${stage.items.sort((a,b) => new Date(b.date) - new Date(a.date)).map(entry => `
<div class="relative group">
<div class="absolute -left-[45px] top-1.5 w-4 h-4 bg-white border-2 ${isCurrentStage ? 'border-tech-blue' : 'border-gray-400'} rounded-full"></div>
<div class="bg-white p-4 rounded-xl shadow-sm border">
<div class="flex justify-between items-center">
<p class="text-sm text-text-medium mb-1 font-medium">${new Date(entry.date).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
${isUserTopic ? `<button data-delete-entry-id="${entry.id}" class="text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity p-1 -mr-1" title="删除此条目"><i data-lucide="x" class="w-4 h-4"></i></button>` : ''}
</div>
<p class="text-text-dark text-sm">${entry.content.replace(/\\n/g, '<br>')}</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
timelineContainer.innerHTML += stageHtml;
});
}
if (topic.next_suggestion) {
suggestionContainer.innerHTML = `
<div class="flex items-start bg-amber-50 p-4 rounded-lg border border-amber-200">
<i data-lucide="lightbulb" class="w-6 h-6 text-amber-500 mr-3 mt-1 flex-shrink-0"></i>
<div>
<h4 class="font-bold text-amber-800">开开的下一步建议</h4>
<p class="text-sm text-amber-700 mt-1">${topic.next_suggestion}</p>
</div>
</div>`;
}
timelineContainer.querySelectorAll('[data-delete-entry-id]').forEach(btn => {
btn.addEventListener('click', () => deleteTopicEntry(btn.dataset.deleteEntryId));
});
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function deleteTopicEntry(entryId) {
const topic = userTopics.find(t => t.id === activeTopicId);
if (!topic) return;
const totalEntries = topic.timeline.reduce((acc, stage) => acc + stage.items.length, 0);
if (totalEntries <= 1) {
alert('每个话题至少需要一条记录。如不需此话题,可直接删除整个话题卡片。');
return;
}
topic.timeline.forEach(stage => {
stage.items = stage.items.filter(entry => entry.id.toString() !== entryId.toString());
});
topic.timeline = topic.timeline.filter(stage => stage.items.length > 0);
saveTopicsToStorage();
renderTopicTimeline(topic);
}
function closeModal() {
activeTopicId = null;
modal.classList.add('hidden');
modal.classList.remove('flex');
}
newTopicForm.addEventListener('submit', (e) => {
e.preventDefault();
const title = e.target.elements.title.value.trim();
const content = e.target.elements.content.value.trim();
if (title && content) {
const newTopic = {
id: `user-${nextUserTopicId++}`,
title: title,
date: new Date().toLocaleDateString('zh-CN'),
keywords: ["自定义"],
timeline: [{
stage_name: "启动阶段 (当前)",
stage_icon: "rocket",
items: [{ id: 1, date: new Date().toISOString().split('T')[0], content: content }]
}],
next_suggestion: '将大目标分解成几个可执行的小步骤吧!'
};
userTopics.unshift(newTopic);
saveTopicsToStorage();
renderUserTopics();
e.target.reset();
}
});
addEntryForm.addEventListener('submit', (e) => {
e.preventDefault();
const contentEl = e.target.elements['new-entry-content'];
const content = contentEl.value.trim();
if (!content || !activeTopicId) return;
const topic = userTopics.find(t => t.id === activeTopicId);
if(!topic) return;
let maxId = 0;
topic.timeline.forEach(stage => {
maxId = Math.max(maxId, ...stage.items.map(item => item.id));
});
const newEntry = {
id: maxId + 1,
date: new Date().toISOString().split('T')[0],
content: content
};
if (topic.timeline.length > 0) {
topic.timeline[topic.timeline.length - 1].items.push(newEntry);
} else {
topic.timeline.push({
stage_name: "新进展 (当前)",
stage_icon: "plus",
items: [newEntry]
})
}
saveTopicsToStorage();
renderTopicTimeline(topic);
contentEl.value = '';
});
closeModalBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
loadTopicsFromStorage();
renderAiTopics();
renderUserTopics();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});