优化处理

This commit is contained in:
2025-10-26 16:59:50 +08:00
parent fdac026720
commit 2e243c7671
45 changed files with 346 additions and 3757 deletions
+2 -6
View File
@@ -1,15 +1,13 @@
<?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>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
@@ -19,12 +17,10 @@
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend/emotion-websocket/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://47.111.10.27:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT</jdbc-url>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
-267
View File
@@ -1,267 +0,0 @@
# MVP功能需求文档
**生成时间**: 2025-07-03 11:31:17
**源文件**: MVP功能.pdf
**总页数**: 7
MVP功能
你已经为MVP构想出一个⾮常完整、用户体验导向的原型了!下面我将按照你提出的四个页面结构,
把功能要点进行**模块化梳理**,方便你用来指引设计开发,也方便你在Cursor⾥逐步实现每个部分
的前端/后端逻辑。
### ✅整体结构:底部导航栏4个主页面
导航项:聊天|开心||探索|我的
## 📘页面一:记录(情绪主页+聊天入⼝)
### ✅核心功能:
- 左上⻆入⼝:聊天内容回顾页(情绪对话记录索引页)。
- 右上⻆设置(主题切换)
- ⽇历组件(单行,可展开):支持情绪记录、历史回顾标记。
- 中心展示AI疗愈师3DIP(静态MVP阶段可用插图代替)
- 底部对话框输入:
- 支持语⾳、文字、图⽚输入
- 点发送后进入全屏对话模式(AI聊天),AIIP缩为头像
**技术建议:**
| MVP功能 你已经为MVP构想出一个⾮常完整、用户体验导向的原型了!下面我将按照你提出的四个页面结构, 把功能要点进行**模块化梳理**,方便你用来指引设计开发,也方便你在Cursor⾥逐步实现每个部分 的前端/后端逻辑。 |
| --- |
| ✅整体结构:底部导航栏4个主页面 导航项:聊天|开心||探索|我的 |
| 📘页面一:记录(情绪主页+聊天入⼝)核心功能: • 左上⻆入⼝:聊天内容回顾页(情绪对话记录索引页)。 • 右上⻆设置(主题切换) • ⽇历组件(单行,可展开):支持情绪记录、历史回顾标记。 • 中心展示AI疗愈师3DIP(静态MVP阶段可用插图代替) • 底部对话框输入: • 支持语⾳、文字、图⽚输入 • 点发送后进入全屏对话模式(AI聊天),AIIP缩为头像 技术建议: |
---
- 可用 做语⾳转文本MVP
react-speech-recognition
- 全屏聊天页面:使用GPT-4或现成情绪对话接⼝+表情动画反馈
①左上⻆
②右上⻆
(⽇记本图 ③单行⽇历 折叠/展开 选择⽇期
(设置图标)
标)
可滚动页面 ⽇历数字变 弹出可选择的 主题设置
AI对话记录 为心情图标 选择心情图标 心情图标 ⾳乐设置
⾳效设置
选择记录 选择主题
调整⾳量
聊天记录
调整⾳量
随机打招呼文案
跳转聊天详情 界面主题变化
⾳乐⾳量变化
⾳效⾳量变化
AI总结
查看课题 表情动作变化
点击产生交互 ⽓泡文案变化
(跳转课题系统)
语⾳跟随⽓泡
跟随手指移动
+ 图⽚
语⾳
输入文字...
⻨克⻛
记录 聊天
AI 探索 发现 我的
分析心情 反馈⾄③,⽇历数字变为心情图标
AI智能分析
记录 内容收录⾄①
A
反馈⾄①,未读提示
分析内容
反馈⾄课题系统(若有))
语⾳ 聊天已收录
AI智能分析 顶部 反馈⾄页面2
聊天
A 弹窗 未读提示
解锁新课题
文字
课题进度有更新
---
语⾳聊天 文字聊天
切换全屏 切换全屏
收起 X
切换文字聊天 收起
可滚动页面
+ 图⽚
语⾳
挂断 文本输入框
⻨克⻛
## 📗页面二:治愈(个人成长档案)
### ✅模块结构:
A.情绪洞察与成长课题板块
- 来自:
1. ⽇常聊天自动总结
2. 主动探索测试
3. 命盘(出生数据生成)
B.课题标签系统
- 每个课题一个标签,包含:
- 当前等级/进度
| 语⾳聊天 文字聊天 切换全屏 切换全屏 收起 X 切换文字聊天 收起 可滚动页面 + 图⽚ 语⾳ 挂断 文本输入框 ⻨克⻛ |
| --- |
| 📗页面二:治愈(个人成长档案)模块结构: A.情绪洞察与成长课题板块 • 来自: 1. ⽇常聊天自动总结 2. 主动探索测试 3. 命盘(出生数据生成) B.课题标签系统 • 每个课题一个标签,包含: • 当前等级/进度 |
---
- 可点入:AI对话、知识文章、行动建议
- 完成一次互动:课题升级/积分/⽪肤/称号掉落
C.用户画像五维图(或视觉更优形式)
- 根据用户的成长路径动态生成
- 示例:自我感知|情绪韧性|行动力|共情力|生活热度
**技术建议:**
- 使用radarchart(五边图)或Tag系统管理成长维度
- Tag模块可作为数据库中的 表维护
UserGrowthTopic
| • 可点入:AI对话、知识文章、行动建议 • 完成一次互动:课题升级/积分/⽪肤/称号掉落 C.用户画像五维图(或视觉更优形式) • 根据用户的成长路径动态生成 • 示例:自我感知|情绪韧性|行动力|共情力|生活热度 技术建议: • 使用radarchart(五边图)或Tag系统管理成长维度 • Tag模块可作为数据库中的 表维护 UserGrowthTopic |
| --- |
| |
| |
| |
### 页面图片信息
- 图片 1: X163
---
## 📙页面三:探索(情绪地图+笔记分享)
### ✅地图展示+推荐逻辑
- 地图上两类地标颜⾊:
1. 用户自⼰收藏/计划出行地
2. AI根据情绪推荐场景(匹配情绪+地点)
- 地标弹窗内容:
- 场景图⽚
- 评论(系统/用户)
- 社区笔记
- 收藏/加入计划
### ✅分享视图切换
- 地图左上⻆切换为**社区模式**(美好⻆落分享)
- 用户上传图⽚+文字标记城市疗愈⻆落
- 可点赞、评论、转发
---
**技术建议:**
- 使用Leaflet/Mapbox/⾼德地图开放平台
- 笔记内容结构可参考小红书feed设计(图文瀑布流)
## 📕页面四:我的
### ✅用户信息模块
- 用户基础资料(注册天数)
- 会员中心(MVP可为静态页)
- 邀请好友模块(可做邀请返积分)
### ✅成就总结
- 本周:
| |
| --- |
| |
| 技术建议: • 使用Leaflet/Mapbox/⾼德地图开放平台 • 笔记内容结构可参考小红书feed设计(图文瀑布流) |
| 📕页面四:我的用户信息模块 • 用户基础资料(注册天数) • 会员中心(MVP可为静态页) • 邀请好友模块(可做邀请返积分) ✅成就总结 • 本周: |
### 页面图片信息
- 图片 1: X176
---
- 心情指数
- 聊天次数
- 成长轨迹
- 打卡记录:
- 疗愈地标打卡数
- 分享数
- 他人访问你分享的次数
#### 🧠额外建议(冷启动优先级)
**MVP推荐优先搭建模块顺序:**
1. 记录页(对话+⽇历)
2. 治愈页(课题总结+标签互动)
3. 探索页(地图基础+笔记系统)
4. 我的页(基础资料+成就统计)
如你愿意,我可以现在就帮你用Cursor建一个最小结构的React项目框架,含底部导航和主页面布
局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分?
| • 心情指数 • 聊天次数 • 成长轨迹 • 打卡记录: • 疗愈地标打卡数 • 分享数 • 他人访问你分享的次数 |
| --- |
| 🧠额外建议(冷启动优先级) MVP推荐优先搭建模块顺序: 1. 记录页(对话+⽇历) 2. 治愈页(课题总结+标签互动) 3. 探索页(地图基础+笔记系统) 4. 我的页(基础资料+成就统计) |
| 如你愿意,我可以现在就帮你用Cursor建一个最小结构的React项目框架,含底部导航和主页面布 局,并生成页面组件文件名结构、推荐依赖包清单和初步布局代码。你要不要我开始搭建这部分? |
+2 -2
View File
@@ -5,11 +5,11 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.emotion</groupId>
<artifactId>emotion-single</artifactId>
<artifactId>backend-single</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>emotion-single</name>
<name>backend-single</name>
<description>情感博物馆单体服务</description>
<properties>
@@ -23,7 +23,7 @@ public class EmotionSimpleApplication {
System.out.println("========================================");
System.out.println("🎉 情感博物馆服务启动成功!");
System.out.println("📋 服务信息:");
System.out.println(" - 服务名称: emotion-single");
System.out.println(" - 服务名称: backend-single");
System.out.println(" - 服务端口: 19089");
System.out.println(" - 环境配置: " + System.getProperty("spring.profiles.active"));
System.out.println(" - API文档: http://localhost:19089/api/health");
@@ -20,17 +20,18 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注意:已配置 server.servlet.context-path=/api,拦截器路径匹配不需要再带 /api 前缀
registry.addInterceptor(jwtAuthInterceptor)
.addPathPatterns("/api/**") // 拦截所有API请求
.addPathPatterns("/**") // 拦截应用内的所有请求(已去掉 /api 前缀)
.excludePathPatterns(
"/api/auth/login", // 登录接口
"/api/auth/register", // 注册接口
"/api/auth/captcha", // 图形验证码接口
"/api/auth/sms-code", // 短信验证码接口(免登录)
"/api/auth/refresh-token", // 刷新token接口
"/api/auth/resetPassword", // 重置密码接口(免登录)
"/api/health", // 健康检查接口
"/api/ws/**", // WebSocket接口
"/auth/login", // 登录接口
"/auth/register", // 注册接口
"/auth/captcha", // 图形验证码接口
"/auth/sms-code", // 短信验证码接口(免登录)
"/auth/refresh-token", // 刷新token接口
"/auth/resetPassword", // 重置密码接口(免登录)
"/health", // 健康检查接口
"/ws/**", // WebSocket接口
"/swagger-ui/**", // Swagger UI
"/v3/api-docs/**", // API文档
"/actuator/**" // 监控端点
@@ -28,7 +28,7 @@ public class HealthController {
log.info("健康检查请求");
Map<String, Object> response = new HashMap<>();
response.put("service", "emotion-single");
response.put("service", "backend-single");
response.put("message", "情感博物馆单体服务运行正常");
response.put("version", "1.0.0");
response.put("status", "UP");
@@ -45,7 +45,7 @@ public class HealthController {
log.info("服务信息请求");
Map<String, Object> response = new HashMap<>();
response.put("service", "emotion-single");
response.put("service", "backend-single");
response.put("description", "情感博物馆单体服务");
response.put("version", "1.0.0");
response.put("author", "emotion-museum");
@@ -0,0 +1,42 @@
package com.emotion.dto.request;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
/**
* 重置密码请求对象
* 用于通过手机号+验证码重置用户登录密码(验证码本期固定为 123456)
*
* 作者: emotion-museum
* 日期: 2025-10-26
* 版本: 1.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ResetPasswordRequest extends BaseRequest {
/**
* 手机号
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 新密码(6-20位)
*/
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "新密码长度必须在6-20个字符之间")
private String newPassword;
/**
* 验证码(本期固定为 123456)
*/
@NotBlank(message = "验证码不能为空")
private String captcha;
}
@@ -0,0 +1,21 @@
package com.emotion.dto.response;
import lombok.Data;
/**
* 重置密码响应对象
*
* 作者: emotion-museum
* 日期: 2025-10-26
* 版本: 1.0
*/
@Data
public class ResetPasswordResponse {
/** 是否成功 */
private boolean success;
/** 提示信息 */
private String message;
}
@@ -3,14 +3,10 @@ package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.Message;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息Mapper接口
* 说明:统一使用 MyBatis-Plus 的 BaseMapper 与 Service 层的 LambdaQueryWrapper 构建条件,
*
* @author emotion-museum
* @date 2025-07-23
@@ -18,66 +14,4 @@ import java.util.List;
@Mapper
public interface MessageMapper extends BaseMapper<Message> {
/**
* 根据用户ID和时间范围查询消息
* 通过conversation表关联查询
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.create_time BETWEEN #{startTime} AND #{endTime} " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time ASC")
List<Message> getByUserIdAndTimeRange(@Param("userId") String userId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 根据用户ID分页查询消息
* 通过conversation表关联查询
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time DESC " +
"LIMIT #{offset}, #{size}")
List<Message> getByUserIdWithPageList(@Param("userId") String userId,
@Param("offset") Integer offset,
@Param("size") Integer size);
/**
* 统计用户消息总数
*/
@Select("SELECT COUNT(*) FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.is_deleted = 0")
Long countByUserId(@Param("userId") String userId);
/**
* 根据用户ID和关键词搜索消息
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.content LIKE CONCAT('%', #{keyword}, '%') " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time DESC " +
"LIMIT #{limit}")
List<Message> searchByUserIdAndKeyword(@Param("userId") String userId,
@Param("keyword") String keyword,
@Param("limit") Integer limit);
/**
* 根据用户ID获取最近的消息
*/
@Select("SELECT m.* FROM message m " +
"INNER JOIN conversation c ON m.conversation_id = c.id " +
"WHERE c.user_id = #{userId} " +
"AND m.is_deleted = 0 " +
"ORDER BY m.create_time DESC " +
"LIMIT #{limit}")
List<Message> getRecentByUserId(@Param("userId") String userId,
@Param("limit") Integer limit);
}
@@ -195,35 +195,49 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
@Override
public List<Message> getByUserIdAndTimeRange(String userId, LocalDateTime startTime, LocalDateTime endTime) {
// 由于Message表没有直接的userId字段,需要通过conversation表关联查询
// 这里先通过conversationService获取用户的所有对话ID,然后查询这些对话的消息
return this.baseMapper.getByUserIdAndTimeRange(userId, startTime, endTime);
// 使用 MyBatis-Plus 条件构造器,直接根据消息表的 user_id 字段查询
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.between(Message::getCreateTime, startTime, endTime)
.eq(Message::getIsDeleted, 0)
.orderByAsc(Message::getCreateTime);
return this.list(wrapper);
}
@Override
public IPage<Message> getByUserIdWithPage(String userId, Integer current, Integer size) {
// 手动实现分页
Integer offset = (current - 1) * size;
List<Message> records = this.baseMapper.getByUserIdWithPageList(userId, offset, size);
Long total = this.baseMapper.countByUserId(userId);
// 使用 MyBatis-Plus 分页 + 条件构造器
Page<Message> page = new Page<>(current, size);
page.setRecords(records);
page.setTotal(total);
return page;
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getCreateTime);
return this.page(page, wrapper);
}
@Override
public List<Message> searchByUserIdAndKeyword(String userId, String keyword, Integer limit) {
// 通过conversation表关联查询用户的消息,根据关键词搜索
return this.baseMapper.searchByUserIdAndKeyword(userId, keyword, limit);
// 使用 MyBatis-Plus 分页 + 条件构造器,避免硬编码 SQL
Page<Message> page = new Page<>(1, limit);
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.like(StringUtils.hasText(keyword), Message::getContent, keyword)
.orderByDesc(Message::getCreateTime);
IPage<Message> result = this.page(page, wrapper);
return result.getRecords();
}
@Override
public List<Message> getRecentByUserId(String userId, Integer limit) {
// 获取用户最近消息,按时间倒序
return this.baseMapper.getRecentByUserId(userId, limit);
// 使用 MyBatis-Plus 分页查询最近消息
Page<Message> page = new Page<>(1, limit);
LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Message::getUserId, userId)
.eq(Message::getIsDeleted, 0)
.orderByDesc(Message::getCreateTime);
IPage<Message> result = this.page(page, wrapper);
return result.getRecords();
}
@Override
@@ -0,0 +1,49 @@
package com.emotionmuseum.auth.request;
import com.emotionmuseum.common.request.BaseRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 重置密码请求
*
* <p>用于未登录情况下通过手机号与验证码(本期固定为 123456)设置新密码。</p>
*
* @author emotion-museum
* @since 2025-10-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "重置密码请求")
public class ResetPasswordRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
/**
* 手机号
*/
@Schema(description = "手机号", example = "13800138000")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 新密码
*/
@Schema(description = "新密码", example = "NewPass_123")
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String newPassword;
/**
* 验证码(本期为固定值 123456)
*/
@Schema(description = "验证码(固定为123456", example = "123456")
@NotBlank(message = "验证码不能为空")
private String captcha;
}
@@ -0,0 +1,26 @@
package com.emotionmuseum.auth.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 重置密码响应
*
* <p>不返回敏感信息,仅提供结果标识与提示。</p>
*
* @author emotion-museum
* @since 2025-10-26
*/
@Data
@Schema(description = "重置密码响应")
public class ResetPasswordResponse {
/** 是否重置成功 */
@Schema(description = "是否重置成功", example = "true")
private boolean success;
/** 提示信息 */
@Schema(description = "提示信息", example = "重置密码成功")
private String message;
}
+14 -13
View File
@@ -52,7 +52,8 @@ export class ChatApiService {
async createSession(userId: string, title?: string): Promise<ChatSession> {
try {
console.log('📝 创建会话API调用:', { userId, title })
const response = await http.post<ConversationResponse>('/conversation', {
// backend-single: POST /conversation/create
const response = await http.post<ConversationResponse>('/conversation/create', {
userId,
title: title || `对话${Date.now()}`
})
@@ -88,25 +89,23 @@ export class ChatApiService {
async getUserSessions(userId: string): Promise<ChatSession[]> {
try {
console.log('📂 获取用户会话API调用:', { userId })
const response = await http.get<ConversationResponse[]>(`/conversation/user/${userId}`)
// backend-single: GET /conversation/page?userId=xxx
const response = await http.get<any>('/conversation/page', { params: { userId, current: 1, size: 100 } })
console.log('📂 获取用户会话API响应:', response)
// 处理HTTP响应的data字段
const data = (response as any).data || response
// 处理HTTP响应的data字段PageResult
const pageData = (response as any).data || response
const records = pageData.records || []
// 后端返回ConversationResponse数组,需要转换为ChatSession格式
if (Array.isArray(data)) {
return data.map((conv: any) => ({
// 转换为ChatSession数组
return records.map((conv: any) => ({
id: conv.id,
title: conv.title,
userId: conv.userId || conv.user_id, // 兼容不同的字段名
userId: conv.userId || conv.user_id,
createTime: conv.createTime || conv.create_time,
updateTime: conv.updateTime || conv.update_time,
messageCount: conv.messageCount || conv.message_count || 0
}))
}
return []
} catch (error) {
console.error('❌ 获取用户会话失败:', error)
return []
@@ -133,7 +132,8 @@ export class ChatApiService {
async deleteSession(sessionId: string): Promise<void> {
try {
console.log('🗑️ 删除会话API调用:', { sessionId })
await http.delete(`/conversation/${sessionId}`)
// backend-single: DELETE /conversation/delete?id=xxx
await http.delete('/conversation/delete', { params: { id: sessionId } })
console.log('✅ 删除会话成功')
} catch (error) {
console.error('❌ 删除会话失败:', error)
@@ -147,7 +147,8 @@ export class ChatApiService {
async updateSessionTitle(sessionId: string, title: string): Promise<void> {
try {
console.log('✏️ 更新会话标题API调用:', { sessionId, title })
await http.put(`/conversation/${sessionId}`, { title })
// backend-single: PUT /conversation/update 传id和title
await http.put('/conversation/update', { id: sessionId, title })
console.log('✅ 更新会话标题成功')
} catch (error) {
console.error('❌ 更新会话标题失败:', error)
+18 -9
View File
@@ -57,7 +57,8 @@ export const messageApi = {
// 获取用户消息分页
getUserMessages: async (current: number = 1, size: number = 20) => {
console.log('📨 调用getUserMessages API:', { current, size })
const response = await http.get(`/message/user/page`, { params: { current, size } })
// backend-single: GET /message/page (后端根据token识别用户)
const response = await http.get(`/message/page`, { params: { current, size } })
console.log('📨 getUserMessages API响应:', response)
return response
},
@@ -65,23 +66,31 @@ export const messageApi = {
// 搜索用户消息
searchUserMessages: async (keyword: string, limit: number = 50) => {
console.log('🔍 调用searchUserMessages API:', { keyword, limit })
const response = await http.post(`/message/user/search`, { keyword, limit })
console.log('🔍 searchUserMessages API响应:', response)
return response
// backend-single: POST /message/search
const resp = await http.post(`/message/search`, { keyword, limit })
console.log('🔍 searchUserMessages API响应:', resp)
// 统一返回数组,兼容控制器返回 PageResult 结构
const data: any = (resp as any).data || resp
const records = data.records || data
return Array.isArray(records) ? records : []
},
// 获取用户最近的聊天记录 - 修复:使用POST请求匹配后端接口
// 获取用户最近的聊天记录 - 返回数组,兼容后端 PageResult 结构
getRecentMessages: async (limit: number = 10) => {
console.log('📝 调用getRecentMessages API:', { limit })
const response = await http.post(`/message/user/recent`, { limit })
console.log('📝 getRecentMessages API响应:', response)
return response
// backend-single: POST /message/recent
const resp = await http.post(`/message/recent`, { limit })
console.log('📝 getRecentMessages API响应:', resp)
const data: any = (resp as any).data || resp
const records = data.records || data
return Array.isArray(records) ? records : []
},
// 获取消息详情
getMessageById: async (id: string) => {
console.log('📄 调用getMessageById API:', { id })
const response = await http.get(`/message/${id}`)
// backend-single: GET /message/detail?id=xxx
const response = await http.get(`/message/detail`, { params: { id } })
console.log('📄 getMessageById API响应:', response)
return response
}
+3 -3
View File
@@ -207,9 +207,9 @@ export class StompWebSocketService {
console.log('📤 准备发送的聊天请求:', chatRequest)
try {
// 发送到后端的/app/chat.send端点
// 发送到后端的/app/chat/send端点(对应 @MessageMapping("/chat") + @MessageMapping("/send")
this.client.publish({
destination: '/app/chat.send',
destination: '/app/chat/send',
body: JSON.stringify(chatRequest)
})
console.log('✅ STOMP聊天消息发送成功:', chatRequest)
@@ -324,7 +324,7 @@ export class StompWebSocketService {
try {
this.client.publish({
destination: '/app/chat.connect',
destination: '/app/chat/connect',
body: JSON.stringify(connectRequest)
})
console.log('✅ STOMP连接消息发送成功:', connectRequest)
+13 -9
View File
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import type { ChatMessage, ChatSession } from '@/types'
import { stompWebSocketService, type WebSocketMessage, type ConnectionStatus } from '@/services/stomp-websocket'
import { useAuthStore } from './auth'
@@ -381,31 +381,35 @@ export const useChatStore = defineStore('chat', () => {
}
// 添加AI回复消息(直接显示完整内容)
const addAiReplyMessages = (content: string) => {
const addAiReplyMessages = async (content: string) => {
// 停止输入状态
isTyping.value = false
// 使用 nextTick 确保 DOM 更新的顺序性,避免与定时同步并发
await nextTick()
// 直接添加完整的AI回复
const aiMessage = addMessage({
content: content.trim(),
type: 'ai',
sessionId: currentSession.value?.id
conversationId: currentSession.value?.id
})
// 强制触发响应式更新
console.log('AI消息已添加,当前消息总数:', messages.value.length)
console.log('最新AI消息:', aiMessage)
console.log('AI消息已添加,当前消息总数:', messages.value.length)
console.log('📝 最新AI消息:', aiMessage)
console.log('📊 所有消息:', messages.value)
}
// WebSocket消息处理
let handleWebSocketMessage = (wsMessage: WebSocketMessage) => {
let handleWebSocketMessage = async (wsMessage: WebSocketMessage) => {
console.log('收到WebSocket消息:', wsMessage.type, wsMessage.senderType)
switch (wsMessage.type) {
case 'TEXT':
if (wsMessage.senderType === 'AI') {
// AI回复消息 - 支持分段显示
addAiReplyMessages(wsMessage.content)
await addAiReplyMessages(wsMessage.content)
}
break
@@ -602,8 +606,8 @@ export const useChatStore = defineStore('chat', () => {
onMessage: (callback: (message: any) => void) => {
// 简单的消息监听实现
const originalHandler = handleWebSocketMessage
handleWebSocketMessage = (message: any) => {
originalHandler(message)
handleWebSocketMessage = async (message: any) => {
await originalHandler(message)
callback(message)
}
}
+28 -128
View File
@@ -67,8 +67,8 @@
<p class="mt-2 text-gray-600">加载对话记录中...</p>
</div>
<!-- 欢迎消息 -->
<div v-else-if="messages.length === 0" class="text-center py-12">
<!-- 欢迎消息使用 v-if避免与列表互斥切换引发的 DOM 竞态 -->
<div v-if="messages.length === 0" class="text-center py-12">
<img
:src="kaikaiAvatar"
alt="开开"
@@ -81,16 +81,16 @@
</p>
</div>
<!-- 消息列表 -->
<div v-else class="space-y-4">
<!-- 消息列表使用 v-show 保持节点稳定移除key避免频繁重新挂载 -->
<div v-show="messages.length > 0" class="space-y-4">
<div
v-for="(message, index) in messages"
:key="`msg-${message.id}-${index}`"
v-for="message in messages"
:key="message.id"
class="flex w-full items-end mb-4"
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
:class="message.type === 'user' ? 'justify-end' : 'justify-start'"
>
<!-- AI消息 -->
<template v-if="message.role === 'assistant'">
<template v-if="message.type === 'ai'">
<img
:src="kaikaiAvatar"
alt="开开"
@@ -105,7 +105,7 @@
</template>
<!-- 用户消息 -->
<template v-else-if="message.role === 'user'">
<template v-else-if="message.type === 'user'">
<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">
<p class="leading-relaxed whitespace-pre-wrap">{{ message.content }}</p>
@@ -122,27 +122,6 @@
</div>
</template>
</div>
<!-- AI正在输入指示器 -->
<div v-if="chatStore.isTyping" class="flex w-full items-end justify-start">
<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 border border-gray-100">
<div class="flex items-center space-x-1">
<div class="flex space-x-1">
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<span class="text-sm text-gray-500 ml-2">开开正在输入...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
@@ -193,13 +172,16 @@ import BottomNavigation from '@/components/layout/BottomNavigation.vue'
const chatStore = useChatStore()
// 响应式数据
const messages = ref<ChatMessage[]>([])
// 直接使用 chatStore.messages,避免计算属性导致的重新计算
const messages = computed(() => chatStore.messages)
const inputMessage = ref('')
const sending = ref(false)
const loading = ref(false)
const messagesContainer = ref<HTMLElement>()
const messageInput = ref<HTMLTextAreaElement>()
const lastSyncedMessageCount = ref(0) // 记录上次同步的消息数量
// 定时同步句柄(避免在 onMounted 内部注册 onUnmounted
let syncInterval: any = null
// 头像
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
@@ -295,20 +277,15 @@ const loadMessages = async () => {
return parseTime(a.timestamp) - parseTime(b.timestamp)
})
messages.value = chatMessages
// 初始化同步计数
lastSyncedMessageCount.value = chatStore.messages.length
// 将消息添加到 chatStore
chatStore.messages.splice(0, chatStore.messages.length, ...chatMessages)
// 强制滚动到底部
await nextTick()
forceScrollToBottom()
} else {
messages.value = []
}
} catch (error) {
console.error('❌ 加载消息失败:', error)
messages.value = []
} finally {
loading.value = false
}
@@ -325,12 +302,8 @@ const sendMessage = async () => {
sending.value = true
try {
// 强制滚动到底部(为即将到来的消息做准备)
await nextTick()
forceScrollToBottom()
// 直接通过WebSocket发送消息,让chatStore处理消息添加
// 这样避免重复添加消息
// 计算属性会自动响应 chatStore 的变化
await chatStore.sendMessage(content)
} catch (error) {
@@ -340,54 +313,12 @@ const sendMessage = async () => {
}
}
// 从chatStore同步消息(完全重新构建消息列表)
const syncWithChatStore = () => {
const storeMessages = chatStore.messages
// 如果store消息数量没有变化,跳过同步
if (storeMessages.length === lastSyncedMessageCount.value) {
return
}
console.log('🔄 同步chatStore消息,数量:', storeMessages.length)
// 转换所有store消息
const convertedMessages = storeMessages.map(msg => ({
id: msg.id,
content: msg.content,
role: msg.type === 'user' ? 'user' : 'assistant',
type: msg.type,
timestamp: msg.timestamp,
status: msg.status || 'sent',
sender: msg.type === 'user' ? 'user' : 'ai'
} as ChatMessage))
// 按时间排序
convertedMessages.sort((a, b) => {
const parseTime = (timestamp: string | Date) => {
if (timestamp instanceof Date) return timestamp.getTime()
if (typeof timestamp === 'string') {
if (timestamp.includes(' ') && !timestamp.includes('T')) {
return new Date(timestamp.replace(' ', 'T')).getTime()
}
return new Date(timestamp).getTime()
}
return new Date().getTime()
}
return parseTime(a.timestamp) - parseTime(b.timestamp)
// 监听消息变化,自动滚动到底部
watch(() => messages.value.length, async () => {
await nextTick()
forceScrollToBottom()
})
// 完全替换消息列表
messages.value = convertedMessages
// 更新同步计数
lastSyncedMessageCount.value = storeMessages.length
// 强制滚动到底部
nextTick(() => forceScrollToBottom())
}
// 调整文本框高度
const adjustTextareaHeight = () => {
if (messageInput.value) {
@@ -412,38 +343,16 @@ onMounted(async () => {
// 监听WebSocket消息
try {
chatStore.onMessage((message: any) => {
// 创建AI消息
const aiMessage: ChatMessage = {
id: message.id || `ai_${Date.now()}`,
content: message.content || message.message || String(message),
role: 'assistant',
type: 'ai',
timestamp: message.timestamp || new Date().toISOString(),
status: 'sent',
sender: 'ai'
}
// 添加到消息列表
messages.value.push(aiMessage)
// 强制滚动到底部
nextTick(() => forceScrollToBottom())
chatStore.onMessage(async (_message: any) => {
// 消息已经被添加到 chatStore,计算属性会自动更新
console.log('📨 Chat页面收到WebSocket消息回调')
await nextTick()
forceScrollToBottom()
})
} catch (error) {
console.warn('⚠️ 设置WebSocket监听器失败:', error)
}
// 定期同步chatStore消息(确保不遗漏)
const syncInterval = setInterval(() => {
syncWithChatStore()
}, 1000)
// 组件卸载时清理定时器
onUnmounted(() => {
clearInterval(syncInterval)
})
// 确保初始化完成后滚动到底部
await nextTick()
setTimeout(() => forceScrollToBottom(), 100)
@@ -456,18 +365,9 @@ onUnmounted(() => {
chatStore.disconnectWebSocket()
})
// 监听消息变化,自动滚动
watch(() => messages.value.length, () => {
nextTick(() => forceScrollToBottom())
})
// 监听chatStore消息变化(移除,避免与 onMessage/定时同步重复触发导致渲染竞态)
// 保留通过 onMessage 事件与定时器同步的方式,减少同一 tick 内的多次 DOM 更新
// 监听chatStore消息变化
watch(() => chatStore.messages.length, (newLength, oldLength) => {
if (newLength > oldLength) {
// 有新消息时同步
syncWithChatStore()
}
}, { immediate: false })
</script>
<style scoped>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="forgot-page">
<div class="card">
<h2 class="title">重置密码</h2>
<el-form :model="form" :rules="rules" ref="formRef" label-width="0">
<el-form-item prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item prop="newPassword">
<el-input v-model="form.newPassword" placeholder="请输入新密码" show-password clearable />
</el-form-item>
<el-form-item prop="captcha">
<el-input v-model="form.captcha" placeholder="请输入验证码(123456" clearable />
</el-form-item>
<el-button type="primary" class="w-full" :loading="submitting" @click="onSubmit">提交</el-button>
</el-form>
<div class="mt-4 text-center">
<router-link to="/login">返回登录</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import AuthService from '@/services/auth'
import type { ResetPasswordRequest } from '@/types/auth'
const formRef = ref<FormInstance>()
const submitting = ref(false)
const form = ref<ResetPasswordRequest>({ phone: '', newPassword: '', captcha: '' })
const rules: FormRules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: ['blur', 'change'] }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度6-20位', trigger: ['blur', 'change'] }
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
const onSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
try {
submitting.value = true
await AuthService.resetPassword(form.value)
ElMessage.success('重置密码成功,请使用新密码登录')
} catch (e) {
ElMessage.error('重置密码失败,请稍后重试')
} finally {
submitting.value = false
}
})
}
</script>
<style scoped>
.forgot-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.card { width: 360px; background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
.title { text-align: center; margin-bottom: 16px; }
.w-full { width: 100%; }
</style>
@@ -1,43 +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>聊天记录 - 开心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>
@@ -1,64 +0,0 @@
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
@@ -1,102 +0,0 @@
/* 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);
}
}
@@ -1,89 +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>与开开聊天 - 开心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
@@ -1,78 +0,0 @@
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);
});
@@ -1,80 +0,0 @@
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
@@ -1,40 +0,0 @@
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: '充满活力的开开'
}
];
@@ -1,63 +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>日记 - 开心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
@@ -1,180 +0,0 @@
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);
}
});
@@ -1,169 +0,0 @@
<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>
@@ -1,37 +0,0 @@
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);
@@ -1,266 +0,0 @@
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();
}
});
@@ -1,58 +0,0 @@
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();
});
@@ -1,51 +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>人生轨迹 - 开心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>
@@ -1,5 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
@@ -1,166 +0,0 @@
<!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>
@@ -1,301 +0,0 @@
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();
}
});
@@ -1,53 +0,0 @@
<!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>
@@ -1,77 +0,0 @@
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();
}
});
@@ -1,136 +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>个人展板 - 开心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>
@@ -1,309 +0,0 @@
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();
}
});
@@ -1,78 +0,0 @@
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();
}
});
}
});
@@ -1,153 +0,0 @@
<!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>
@@ -1,7 +0,0 @@
import { navLinks } from './data.js';
document.addEventListener('DOMContentLoaded', () => {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
-163
View File
@@ -1,163 +0,0 @@
: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);
}
}
@@ -1,125 +0,0 @@
<!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>
@@ -1,319 +0,0 @@
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();
}
});