增加修改和删除功能

This commit is contained in:
2025-12-24 15:20:58 +08:00
parent 1aa39e11b4
commit 31cc78038b
26 changed files with 707 additions and 492 deletions
-150
View File
@@ -78,153 +78,3 @@ alwaysApply: true
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
---
## Java Spring Boot 项目开发与代码质量保障规范(扩展)
适用范围:本规范适用于 logistics-finance 项目所有后端模块(common、gateway、auth、user、order、waybill、vehicle、finance、report、ai、file)。若与本文件前文条款或现有项目规范冲突,以更严格者为准。
### 一、代码规范完善
- 编码标准
- 严格开启编译参数校验与空指针警告,禁止忽略 IDE/编译器提示。
- 代码提交前必须通过本地编译、单元测试、静态扫描(如存在)。
- 严禁出现魔法值:使用常量或配置项;跨模块常量放在 common 模块统一管理。
- 日志分级:业务关键信息用 info,调试用 debug(生产禁用),异常用 error,禁止打印敏感信息(密码、密钥、Token、手机号等)。
- 命名规范
- 类:PascalCase;方法/变量:camelCase;常量:UPPER_SNAKE_CASE;包名全小写。
- 接口路径参照现有规则(禁止 /api 前缀、禁止 PathVariable、Mapping value 必须显式且驼峰命名)。
- 注释规范
- 类注释需包含:职责/用途、作者、创建时间、版本;方法注释需包含:用途、参数、返回、可能抛出的异常。
- 复杂算法/易错逻辑必须添加行内注释;废弃方法使用 @Deprecated 并标注替代方案与移除计划。
- 代码结构规范(建议包结构)
- controller、service、service.impl、repositorymapper/dao)、domain/entity、converter、config、clientfeign)、event、facade、task、util、constant。
- DTO/Request/Response 与 Entity 分离;Controller 不做对象互转,统一在 Service/Converter 层完成。
### 二、分层架构规范
- Controller 层职责
- 接收请求、参数校验(Jakarta Validation)、鉴权校验(由网关/拦截器)、调用 Service,统一使用 `Result<T>` 返回。
- 禁止:业务逻辑、事务控制、数据访问、实体与请求/响应的相互转换。
- Service 层职责
- 聚合业务逻辑、领域编排、事务边界控制(@Transactional,读写分离时仅在写方法上标注),幂等与重试策略在此实现。
- 方法命名与 Controller 对应,入参使用 Request 对象,出参使用 Response 对象。
- Repository 层职责
- 仅做数据访问,统一使用 MyBatis-Plus(优先 LambdaQueryWrapper,避免硬编码字段)。
- Entity 规范
- 统一继承 BaseEntity,字段包含:created_by、create_time、updated_by、update_time、logical_delete、remark(与项目约定一致)。
- 禁止使用枚举作为实体/DTO 字段类型;以 String/Integer 存储并在注释中补充取值说明。
### 三、统一异常与返回结果
- 全局异常处理
- 继续使用 common-web 的 GlobalExceptionHandler;禁止在业务代码中大面积 try-catch,异常由全局处理。
- 业务异常一律抛出 BusinessException;校验异常统一转为 400 语义返回。
- 统一返回结构
- 统一使用 common-core 的 Result<T> 与 ResultCode;保证 traceId、timestamp 一致性。
- Controller 层仅返回 Result,不直接返回实体或集合。
- 参数校验
- 使用 jakarta.validation 注解(@NotNull、@Size 等),在 Request 对象上标注;Controller 使用 @Validated。
### 四、数据库操作规范
- 设计规范
- 数据表必须包含 create_time、update_time;删除优先逻辑删除 logical_delete;字段命名下划线风格。
- 建议仅创建必要索引;避免外键约束(由应用层保证一致性)。
- 查询构建
- 优先使用 LambdaQueryWrapper 和链式调用;分页查询必须使用分页插件;大数据量必须分页。
- 避免 N+1 查询;需要时使用批量查询或联表映射。
- 事务管理
- 仅在 Service 层声明事务;方法内仅包含数据库操作或与数据库一致性相关的远程调用;跨服务一致性使用可靠消息/补偿方案。
---
## 二、代码质量保障机制
- 代码审查流程(Code Review
- 所有变更必须走 PR;至少 1 名同组开发+1 名模块 Owner 审核通过方可合并。
- 审查清单:命名/注释/分层边界/异常处理/日志/性能/安全/接口兼容性/测试覆盖率/静态扫描告警。
- PR 模板需包含:改动描述、影响面、回归范围、测试说明、回滚方案。
- 静态代码分析与格式
- 集成 Checkstyle(代码风格)、SpotBugs/PMD(潜在缺陷)、SonarQube(质量门禁),EditorConfig/格式化工具保持一致风格。
- 质量门禁建议:
- 新增代码覆盖率 >= 80%,全局覆盖率 >= 70%
- Blocker/Critical 问题为 0
- 重复代码率 < 3%
- 圈复杂度阈值:方法 < 10,类 < 80。
- 测试规范
- 单元测试:命名 {ClassName}Test,方法名 should_xxx_when_xxx;使用 Mockito/AssertJ;隔离外部依赖。
- 集成测试:使用 @SpringBootTest,测试容器或嵌入式组件替代真实依赖;构造典型场景与边界场景。
- 覆盖率准则:核心业务与关键分支必须覆盖(正常/异常/边界/空数据)。
- 用例分层:Service 层单测覆盖业务规则;Controller 层使用 MockMvcRepository 层覆盖复杂 SQL 与多条件查询。
- 重构指导原则
- 小步快跑、单一职责、先加测试再重构;保持对外行为一致(新增特性使用特性开关/版本控制)。
- 严禁大规模跨模块重构合并在单次 PR;拆分为多个可审查的独立提交。
---
## 三、变更管理流程
- 变更前影响分析
- 维度:接口兼容性(路径/入参/语义)、数据库(表/字段/索引/数据迁移)、配置(Nacos/环境变量)、依赖版本、性能、容量与并发、安全与合规、可回滚性。
- 产出:影响分析文档与回滚方案(包含回滚脚本/开关)。
- 回归测试策略
- 冒烟覆盖主干流程;模块级回归覆盖变更影响域;跨服务联调验证接口契约(通过网关)。
- 回归范围基于依赖关系与影响分析确定;优先自动化回归,必要时补充手工用例。
- 版本管理与发布
- 分支策略建议:
- 主分支:master
- 功能分支:feature/{module}-{short-desc}
- 预发布:release/x.y.z
- 紧急修复:hotfix/x.y.z
- 版本号:语义化版本 SemVerMAJOR.MINOR.PATCH),发布时打 Tag;所有服务端口遵循 280xx 约定(配置文件统一)。
- 紧急修复与常规开发
- 紧急修复:创建 hotfix → 修复 → 快速测试 → 合并 master 与 release → 立即发布 → 追补测试与文档。
- 常规开发:feature → PR 审核 → 合并 → 集成测试 → 发布。
---
## 四、持续改进机制
- 规范评审与更新
- 每月一次质量例会,评审新增问题与改进项;规范变更需在本文件留痕(版本/日期/变更点)。
- 质量指标监控(建议在 Sonar/日志/监控平台看板化)
- 构建成功率、静态问题趋势、单元测试覆盖率、平均修复时长、缺陷密度、接口成功率、P95/P99 响应时间、数据库慢查询比例。
- 团队培训计划
- 新人入项必读规范与代码演练;关键模块轮训;每季度至少一次专题分享(性能调优/安全加固/重构实践)。
- 反馈闭环
- 通过代码评审、事后复盘(Postmortem)、问题工单收敛到规范条款;指定责任人与截止时间;下次评审验证落地结果。
---
## 附:标准检查清单(节选)
- PR 自检
- 命名/注释达标;无魔法值;日志分级正确且无敏感信息;空指针风险已处理;删除代码/文件已获批准。
- Controller 不含业务逻辑;路由命名与网关规范一致;统一 Result 返回;参数校验齐全;不使用 PathVariable。
- Service 事务边界清晰;仅更新非空字段;分页/批量/幂等校验到位。
- Repository 使用 LambdaQueryWrapper;避免硬编码字段;SQL 有必要索引;避免 N+1。
- 测试覆盖关键路径与边界;新增功能附带测试;CI 静态扫描无阻断级问题。
- 回滚准备
- 是否具备配置开关/灰度发布策略;是否提供回滚脚本;数据库迁移是否可逆(或给出补偿方案)。
-151
View File
@@ -80,154 +80,3 @@ alwaysApply: true
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
---
## Java Spring Boot 项目开发与代码质量保障规范(扩展)
适用范围:本规范适用于 logistics-finance 项目所有后端模块(common、gateway、auth、user、order、waybill、vehicle、finance、report、ai、file)。若与本文件前文条款或现有项目规范冲突,以更严格者为准。
### 一、代码规范完善
- 编码标准
- 严格开启编译参数校验与空指针警告,禁止忽略 IDE/编译器提示。
- 代码提交前必须通过本地编译、单元测试、静态扫描(如存在)。
- 严禁出现魔法值:使用常量或配置项;跨模块常量放在 common 模块统一管理。
- 日志分级:业务关键信息用 info,调试用 debug(生产禁用),异常用 error,禁止打印敏感信息(密码、密钥、Token、手机号等)。
- 命名规范
- 类:PascalCase;方法/变量:camelCase;常量:UPPER_SNAKE_CASE;包名全小写。
- 接口路径参照现有规则(禁止 /api 前缀、禁止 PathVariable、Mapping value 必须显式且驼峰命名)。
- 注释规范
- 类注释需包含:职责/用途、作者、创建时间、版本;方法注释需包含:用途、参数、返回、可能抛出的异常。
- 复杂算法/易错逻辑必须添加行内注释;废弃方法使用 @Deprecated 并标注替代方案与移除计划。
- 代码结构规范(建议包结构)
- controller、service、service.impl、repositorymapper/dao)、domain/entity、converter、config、clientfeign)、event、facade、task、util、constant。
- DTO/Request/Response 与 Entity 分离;Controller 不做对象互转,统一在 Service/Converter 层完成。
### 二、分层架构规范
- Controller 层职责
- 接收请求、参数校验(Jakarta Validation)、鉴权校验(由网关/拦截器)、调用 Service,统一使用 `Result<T>` 返回。
- 禁止:业务逻辑、事务控制、数据访问、实体与请求/响应的相互转换。
- Service 层职责
- 聚合业务逻辑、领域编排、事务边界控制(@Transactional,读写分离时仅在写方法上标注),幂等与重试策略在此实现。
- 方法命名与 Controller 对应,入参使用 Request 对象,出参使用 Response 对象。
- Repository 层职责
- 仅做数据访问,统一使用 MyBatis-Plus(优先 LambdaQueryWrapper,避免硬编码字段)。
- Entity 规范
- 统一继承 BaseEntity,字段包含:created_by、create_time、updated_by、update_time、logical_delete、remark(与项目约定一致)。
- 禁止使用枚举作为实体/DTO 字段类型;以 String/Integer 存储并在注释中补充取值说明。
### 三、统一异常与返回结果
- 全局异常处理
- 继续使用 common-web 的 GlobalExceptionHandler;禁止在业务代码中大面积 try-catch,异常由全局处理。
- 业务异常一律抛出 BusinessException;校验异常统一转为 400 语义返回。
- 统一返回结构
- 统一使用 common-core 的 Result<T> 与 ResultCode;保证 traceId、timestamp 一致性。
- Controller 层仅返回 Result,不直接返回实体或集合。
- 参数校验
- 使用 jakarta.validation 注解(@NotNull@Size 等),在 Request 对象上标注;Controller 使用 @Validated
### 四、数据库操作规范
- 设计规范
- 数据表必须包含 create_time、update_time;删除优先逻辑删除 logical_delete;字段命名下划线风格。
- 建议仅创建必要索引;避免外键约束(由应用层保证一致性)。
- 查询构建
- 优先使用 LambdaQueryWrapper 和链式调用;分页查询必须使用分页插件;大数据量必须分页。
- 避免 N+1 查询;需要时使用批量查询或联表映射。
- 事务管理
- 仅在 Service 层声明事务;方法内仅包含数据库操作或与数据库一致性相关的远程调用;跨服务一致性使用可靠消息/补偿方案。
---
## 二、代码质量保障机制
- 代码审查流程(Code Review
- 所有变更必须走 PR;至少 1 名同组开发+1 名模块 Owner 审核通过方可合并。
- 审查清单:命名/注释/分层边界/异常处理/日志/性能/安全/接口兼容性/测试覆盖率/静态扫描告警。
- PR 模板需包含:改动描述、影响面、回归范围、测试说明、回滚方案。
- 静态代码分析与格式
- 集成 Checkstyle(代码风格)、SpotBugs/PMD(潜在缺陷)、SonarQube(质量门禁),EditorConfig/格式化工具保持一致风格。
- 质量门禁建议:
- 新增代码覆盖率 >= 80%,全局覆盖率 >= 70%
- Blocker/Critical 问题为 0
- 重复代码率 < 3%
- 圈复杂度阈值:方法 < 10,类 < 80。
- 测试规范
- 单元测试:命名 {ClassName}Test,方法名 should_xxx_when_xxx;使用 Mockito/AssertJ;隔离外部依赖。
- 集成测试:使用 @SpringBootTest,测试容器或嵌入式组件替代真实依赖;构造典型场景与边界场景。
- 覆盖率准则:核心业务与关键分支必须覆盖(正常/异常/边界/空数据)。
- 用例分层:Service 层单测覆盖业务规则;Controller 层使用 MockMvcRepository 层覆盖复杂 SQL 与多条件查询。
- 重构指导原则
- 小步快跑、单一职责、先加测试再重构;保持对外行为一致(新增特性使用特性开关/版本控制)。
- 严禁大规模跨模块重构合并在单次 PR;拆分为多个可审查的独立提交。
---
## 三、变更管理流程
- 变更前影响分析
- 维度:接口兼容性(路径/入参/语义)、数据库(表/字段/索引/数据迁移)、配置(Nacos/环境变量)、依赖版本、性能、容量与并发、安全与合规、可回滚性。
- 产出:影响分析文档与回滚方案(包含回滚脚本/开关)。
- 回归测试策略
- 冒烟覆盖主干流程;模块级回归覆盖变更影响域;跨服务联调验证接口契约(通过网关)。
- 回归范围基于依赖关系与影响分析确定;优先自动化回归,必要时补充手工用例。
- 版本管理与发布
- 分支策略建议:
- 主分支:master
- 功能分支:feature/{module}-{short-desc}
- 预发布:release/x.y.z
- 紧急修复:hotfix/x.y.z
- 版本号:语义化版本 SemVerMAJOR.MINOR.PATCH),发布时打 Tag;所有服务端口遵循 280xx 约定(配置文件统一)。
- 紧急修复与常规开发
- 紧急修复:创建 hotfix → 修复 → 快速测试 → 合并 master 与 release → 立即发布 → 追补测试与文档。
- 常规开发:feature → PR 审核 → 合并 → 集成测试 → 发布。
---
## 四、持续改进机制
- 规范评审与更新
- 每月一次质量例会,评审新增问题与改进项;规范变更需在本文件留痕(版本/日期/变更点)。
- 质量指标监控(建议在 Sonar/日志/监控平台看板化)
- 构建成功率、静态问题趋势、单元测试覆盖率、平均修复时长、缺陷密度、接口成功率、P95/P99 响应时间、数据库慢查询比例。
- 团队培训计划
- 新人入项必读规范与代码演练;关键模块轮训;每季度至少一次专题分享(性能调优/安全加固/重构实践)。
- 反馈闭环
- 通过代码评审、事后复盘(Postmortem)、问题工单收敛到规范条款;指定责任人与截止时间;下次评审验证落地结果。
---
## 附:标准检查清单(节选)
- PR 自检
- 命名/注释达标;无魔法值;日志分级正确且无敏感信息;空指针风险已处理;删除代码/文件已获批准。
- Controller 不含业务逻辑;路由命名与网关规范一致;统一 Result 返回;参数校验齐全;不使用 PathVariable。
- Service 事务边界清晰;仅更新非空字段;分页/批量/幂等校验到位。
- Repository 使用 LambdaQueryWrapper;避免硬编码字段;SQL 有必要索引;避免 N+1。
- 测试覆盖关键路径与边界;新增功能附带测试;CI 静态扫描无阻断级问题。
- 回滚准备
- 是否具备配置开关/灰度发布策略;是否提供回滚脚本;数据库迁移是否可逆(或给出补偿方案)。
Regular → Executable
View File
@@ -73,4 +73,19 @@ public class EpicScriptUpdateRequest extends BaseRequest {
* 是否当前选中
*/
private Boolean isSelected;
/**
* 角色信息(用于AI重新生成)
*/
private String characterInfo;
/**
* 过往经历摘要(用于AI重新生成)
*/
private String lifeEventsSummary;
/**
* 是否需要重新生成AI内容
*/
private Boolean regenerateContent;
}
@@ -118,9 +118,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return false;
}
// 逻辑删除
comment.setIsDeleted(1);
return this.updateById(comment);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -140,9 +140,8 @@ public class CommunityPostServiceImpl extends ServiceImpl<CommunityPostMapper, C
return false;
}
// 逻辑删除
post.setIsDeleted(1);
return this.updateById(post);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -271,9 +271,8 @@ public class CozeApiCallServiceImpl extends ServiceImpl<CozeApiCallMapper, CozeA
return false;
}
// 逻辑删除
apiCall.setIsDeleted(1);
return this.updateById(apiCall);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
@@ -339,10 +339,127 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
script.setIsSelected(request.getIsSelected() ? 1 : 0);
}
// 如果需要重新生成AI内容
if (Boolean.TRUE.equals(request.getRegenerateContent())) {
String aiGeneratedContent = regenerateScriptByAi(request, script, currentUserId);
if (aiGeneratedContent != null) {
Map<String, Object> plotJson = script.getPlotJson();
if (plotJson == null) {
plotJson = new java.util.HashMap<>();
}
plotJson.put("fullContent", aiGeneratedContent);
script.setPlotJson(plotJson);
log.info("AI重新生成剧本内容成功,用户ID: {}, 剧本ID: {}", currentUserId, script.getId());
}
}
this.updateById(script);
return convertToResponse(script);
}
/**
* 调用Coze AI重新生成爽文剧本内容
*
* @param request 剧本更新请求
* @param script 原剧本实体
* @param userId 用户ID
* @return AI生成的剧本内容,失败时返回null
*/
private String regenerateScriptByAi(EpicScriptUpdateRequest request, EpicScript script, String userId) {
try {
// 组装AI输入
String input = assembleUpdateScriptInput(request, script);
log.info("开始调用AI重新生成剧本,用户ID: {}, 剧本ID: {}", userId, script.getId());
// 调用Coze工作流
String result = aiChatService.callWorkflowByConfigKey(COZE_EPIC_SCRIPT_CONFIG_KEY, input, userId);
log.info("AI重新生成剧本完成,用户ID: {}, 结果长度: {}", userId, result != null ? result.length() : 0);
return result;
} catch (Exception e) {
log.error("AI重新生成剧本失败,用户ID: {}, 剧本ID: {}, 错误: {}", userId, script.getId(), e.getMessage(), e);
return null;
}
}
/**
* 组装更新时的AI输入内容
*
* @param request 更新请求
* @param script 原剧本实体
* @return 格式化的输入字符串
*/
private String assembleUpdateScriptInput(EpicScriptUpdateRequest request, EpicScript script) {
StringBuilder sb = new StringBuilder();
// 角色信息(优先使用请求中的,否则使用原有的)
String characterInfo = StringUtils.hasText(request.getCharacterInfo())
? request.getCharacterInfo() : null;
if (characterInfo != null) {
sb.append("【角色信息】").append(characterInfo).append("\n");
}
// 过往经历
String lifeEventsSummary = StringUtils.hasText(request.getLifeEventsSummary())
? request.getLifeEventsSummary() : null;
if (lifeEventsSummary != null) {
sb.append("【过往经历】").append(lifeEventsSummary).append("\n");
}
// 标题
String title = StringUtils.hasText(request.getTitle()) ? request.getTitle() : script.getTitle();
if (StringUtils.hasText(title)) {
sb.append("【剧本标题】").append(title).append("\n");
}
// 主题/渴望
String theme = request.getTheme() != null ? request.getTheme() : script.getTheme();
if (StringUtils.hasText(theme)) {
sb.append("【主题渴望】").append(theme).append("\n");
}
// 风格
String style = StringUtils.hasText(request.getStyle()) ? request.getStyle() : script.getStyle();
if (StringUtils.hasText(style)) {
String styleDesc = getStyleDescription(style);
sb.append("【剧本风格】").append(styleDesc).append("\n");
}
// 篇幅
String length = StringUtils.hasText(request.getLength()) ? request.getLength() : script.getLength();
if (StringUtils.hasText(length)) {
String lengthDesc = getLengthDescription(length);
sb.append("【篇幅长度】").append(lengthDesc).append("\n");
}
// 序幕
String plotIntro = request.getPlotIntro() != null ? request.getPlotIntro() : script.getPlotIntro();
if (StringUtils.hasText(plotIntro)) {
sb.append("【序幕-低谷回响】").append(plotIntro).append("\n");
}
// 转折
String plotTurning = request.getPlotTurning() != null ? request.getPlotTurning() : script.getPlotTurning();
if (StringUtils.hasText(plotTurning)) {
sb.append("【转折-契机出现】").append(plotTurning).append("\n");
}
// 高潮
String plotClimax = request.getPlotClimax() != null ? request.getPlotClimax() : script.getPlotClimax();
if (StringUtils.hasText(plotClimax)) {
sb.append("【高潮-命运抉择】").append(plotClimax).append("\n");
}
// 结局
String plotEnding = request.getPlotEnding() != null ? request.getPlotEnding() : script.getPlotEnding();
if (StringUtils.hasText(plotEnding)) {
sb.append("【结局-新的开始】").append(plotEnding).append("\n");
}
return sb.toString().trim();
}
@Override
public EpicScriptResponse selectScript(String id) {
String currentUserId = UserContextHolder.getCurrentUserId();
@@ -378,7 +495,7 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
@Override
public boolean deleteScript(String id) {
EpicScript script = this.getById(id);
if (script == null || script.getIsDeleted() == 1) {
if (script == null) {
return false;
}
@@ -391,8 +508,8 @@ public class EpicScriptServiceImpl extends ServiceImpl<EpicScriptMapper, EpicScr
// 删除关联的路径
lifePathService.deleteByScriptId(id);
script.setIsDeleted(1);
return this.updateById(script);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -147,13 +147,12 @@ public class GrowthTopicServiceImpl extends ServiceImpl<GrowthTopicMapper, Growt
@Override
public boolean deleteGrowthTopic(String id) {
GrowthTopic topic = this.getById(id);
if (topic == null || topic.getIsDeleted() == 1) {
if (topic == null) {
return false;
}
// 逻辑删除
topic.setIsDeleted(1);
return this.updateById(topic);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -223,13 +223,10 @@ public class GuestUserServiceImpl extends ServiceImpl<GuestUserMapper, GuestUser
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<GuestUser> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(GuestUser::getLastActiveTime, expireTime)
.eq(GuestUser::getIsDeleted, 0);
wrapper.lt(GuestUser::getLastActiveTime, expireTime);
GuestUser updateUser = new GuestUser();
updateUser.setIsDeleted(1);
return this.update(updateUser, wrapper);
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
return this.remove(wrapper);
}
@Override
@@ -285,13 +282,12 @@ public class GuestUserServiceImpl extends ServiceImpl<GuestUserMapper, GuestUser
@Override
public boolean deleteGuestUser(String id) {
GuestUser guestUser = this.getById(id);
if (guestUser == null || guestUser.getIsDeleted() == 1) {
if (guestUser == null) {
return false;
}
// 逻辑删除
guestUser.setIsDeleted(1);
return this.updateById(guestUser);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -271,7 +271,7 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
@Override
public boolean deleteEvent(String id) {
LifeEvent event = this.getById(id);
if (event == null || event.getIsDeleted() == 1) {
if (event == null) {
return false;
}
@@ -281,8 +281,8 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
return false;
}
event.setIsDeleted(1);
return this.updateById(event);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -180,7 +180,7 @@ public class LifePathServiceImpl extends ServiceImpl<LifePathMapper, LifePath>
@Override
public boolean deletePath(String id) {
LifePath path = this.getById(id);
if (path == null || path.getIsDeleted() == 1) {
if (path == null) {
return false;
}
@@ -190,8 +190,8 @@ public class LifePathServiceImpl extends ServiceImpl<LifePathMapper, LifePath>
return false;
}
path.setIsDeleted(1);
return this.updateById(path);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
@@ -203,15 +203,10 @@ public class LifePathServiceImpl extends ServiceImpl<LifePathMapper, LifePath>
LambdaQueryWrapper<LifePath> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LifePath::getUserId, currentUserId)
.eq(LifePath::getScriptId, scriptId)
.eq(LifePath::getIsDeleted, 0);
.eq(LifePath::getScriptId, scriptId);
List<LifePath> paths = this.list(wrapper);
for (LifePath path : paths) {
path.setIsDeleted(1);
this.updateById(path);
}
return true;
// 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除
return this.remove(wrapper);
}
/**
@@ -430,13 +430,12 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
@Override
public boolean deleteMessage(String id) {
Message message = this.getById(id);
if (message == null || message.getIsDeleted() == 1) {
if (message == null) {
return false;
}
// 逻辑删除
message.setIsDeleted(1);
return this.updateById(message);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -326,13 +326,12 @@ public class RewardServiceImpl extends ServiceImpl<RewardMapper, Reward> impleme
@Override
public boolean deleteReward(String id) {
Reward reward = this.getById(id);
if (reward == null || reward.getIsDeleted() == 1) {
if (reward == null) {
return false;
}
// 逻辑删除
reward.setIsDeleted(1);
return this.updateById(reward);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -127,11 +127,11 @@ public class TopicInteractionServiceImpl extends ServiceImpl<TopicInteractionMap
@Override
public boolean deleteTopicInteraction(String id) {
TopicInteraction topicInteraction = this.getById(id);
if (topicInteraction == null || topicInteraction.getIsDeleted() == 1) {
if (topicInteraction == null) {
return false;
}
topicInteraction.setIsDeleted(1);
return this.updateById(topicInteraction);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
/**
@@ -238,11 +238,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Override
public boolean deleteUser(String id) {
User user = this.getById(id);
if (user == null || user.getIsDeleted() == 1) {
if (user == null) {
return false;
}
user.setIsDeleted(1);
return this.updateById(user);
// 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除
return this.removeById(id);
}
@Override
Regular → Executable
View File
+1 -1
View File
@@ -139,7 +139,7 @@ function App() {
<Background />
{/* 主容器 */}
<main className="relative z-10 min-h-screen flex flex-col items-center justify-center p-4 md:p-8">
<main className="relative z-10 h-screen flex flex-col items-center justify-center p-4 md:p-8 overflow-hidden">
<AnimatedRoutes />
</main>
</BrowserRouter>
+4 -1
View File
@@ -42,7 +42,10 @@ const Modal = ({
</Dialog.Overlay>
{/* 内容区 */}
<Dialog.Content asChild>
<Dialog.Content
asChild
aria-describedby={undefined}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
+2 -1
View File
@@ -1,4 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;600&family=Noto+Sans+SC:wght@300;400;500&display=swap');
/* 使用国内镜像加载字体,避免 Google Fonts 访问超时 */
@import url('https://fonts.loli.net/css2?family=Noto+Serif+SC:wght@300;600&family=Noto+Sans+SC:wght@300;400;500&display=swap');
@import "tailwindcss";
:root {
+7 -8
View File
@@ -40,23 +40,23 @@ const DashboardPage = () => {
};
return (
<>
{/* 头部 */}
<div className="flex flex-col h-screen max-h-screen overflow-hidden">
{/* 头部 - 固定高度 */}
<Header
showNav
onProfileClick={() => setIsProfileOpen(true)}
/>
{/* 主内容区 */}
<div className="glass-card w-full h-full overflow-hidden">
{/* 主内容区 - 占据剩余高度 */}
<div className="glass-card flex-1 overflow-hidden">
<div className="grid grid-cols-1 md:grid-cols-12 h-full">
{/* 侧边栏 */}
{/* 侧边栏 - 固定不滚动 */}
<Sidebar
activeView={activeView}
onViewChange={handleViewChange}
/>
{/* 内容区 */}
{/* 内容区 - 独立滚动 */}
<section className="md:col-span-9 p-8 overflow-y-auto custom-scrollbar relative">
<AnimatePresence mode="wait">
<motion.div
@@ -65,7 +65,6 @@ const DashboardPage = () => {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className="h-full"
>
{renderView()}
</motion.div>
@@ -79,7 +78,7 @@ const DashboardPage = () => {
isOpen={isProfileOpen}
onClose={() => setIsProfileOpen(false)}
/>
</>
</div>
);
};
+4 -2
View File
@@ -100,7 +100,8 @@ const transformToBackendFormat = (frontendData) => {
content,
isSelected,
character,
events
events,
regenerateContent = false
} = frontendData;
// 解析内容生成标题和各部分
@@ -191,7 +192,8 @@ const transformToBackendFormat = (frontendData) => {
plotJson: content ? { fullContent: content } : null,
isSelected,
characterInfo,
lifeEventsSummary
lifeEventsSummary,
regenerateContent
};
};
+89 -14
View File
@@ -270,6 +270,39 @@ const useStore = create(
}
},
/**
* 更新生命事件
* @param {Object} event - 事件数据(必须包含 id)
* @returns {Promise<Object>} 更新后的事件
*/
updateLifeEvent: async (event) => {
set({ loading: true, error: null });
try {
const response = await lifeEventService.updateEvent(event);
if (response.data) {
const updatedEvent = lifeEventService.transformToFrontendFormat(response.data);
set((state) => ({
lifeEvents: state.lifeEvents.map(e =>
e.id === updatedEvent.id ? updatedEvent : e
),
loading: false
}));
return updatedEvent;
}
set({ loading: false });
return null;
} catch (error) {
set({ loading: false, error: error.message });
// 降级到本地更新
set((state) => ({
lifeEvents: state.lifeEvents.map(e =>
e.id === event.id ? { ...e, ...event } : e
)
}));
return event;
}
},
/**
* 删除生命事件
*/
@@ -283,10 +316,8 @@ const useStore = create(
}));
} catch (error) {
set({ loading: false });
// 降级到本地删除
set((state) => ({
lifeEvents: state.lifeEvents.filter(e => e.id !== id)
}));
console.error('删除生命事件失败:', error);
throw error;
}
},
@@ -369,6 +400,39 @@ const useStore = create(
return state.scripts.find(s => s.id === state.selectedScriptId);
},
/**
* 更新剧本
* @param {Object} script - 剧本数据(必须包含 id)
* @returns {Promise<Object>} 更新后的剧本
*/
updateScript: async (script) => {
set({ loading: true, error: null });
try {
const response = await epicScriptService.updateScript(script);
if (response.data) {
const updatedScript = epicScriptService.transformToFrontendFormat(response.data);
set((state) => ({
scripts: state.scripts.map(s =>
s.id === updatedScript.id ? updatedScript : s
),
loading: false
}));
return updatedScript;
}
set({ loading: false });
return null;
} catch (error) {
set({ loading: false, error: error.message });
// 降级到本地更新
set((state) => ({
scripts: state.scripts.map(s =>
s.id === script.id ? { ...s, ...script } : s
)
}));
return script;
}
},
/**
* 删除剧本
*/
@@ -388,16 +452,8 @@ const useStore = create(
});
} catch (error) {
set({ loading: false });
// 降级到本地删除
set((state) => {
const newScripts = state.scripts.filter(s => s.id !== id);
return {
scripts: newScripts,
selectedScriptId: state.selectedScriptId === id
? (newScripts[0]?.id || null)
: state.selectedScriptId
};
});
console.error('删除剧本失败:', error);
throw error;
}
},
@@ -453,6 +509,25 @@ const useStore = create(
}
},
/**
* 删除路径
* @param {string} scriptId - 剧本ID
*/
deletePath: async (scriptId) => {
if (!scriptId) return;
try {
const existingPath = await lifePathService.getPathByScriptId(scriptId).catch(() => null);
if (existingPath?.data?.id) {
await lifePathService.deletePath(existingPath.data.id);
}
set({ selectedPath: null });
} catch {
// 忽略错误,本地已清除
set({ selectedPath: null });
}
},
/**
* 清除所有数据
*/
+101 -50
View File
@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { Map, Loader2 } from 'lucide-react';
import { Map, Loader2, Trash2 } from 'lucide-react';
import { motion } from 'framer-motion';
import { GlassCard, GlassButton } from '../components/ui';
import Modal from '../components/Modal';
import useStore from '../store/useStore';
import { generatePath } from '../services/ai';
@@ -12,8 +13,9 @@ import { generatePath } from '../services/ai';
* @param {Function} props.onGoToScript - 跳转到剧本视图回调
*/
const PathView = ({ onGoToScript }) => {
const { getSelectedScript, selectedPath, setPath, loadPath, selectedScriptId } = useStore();
const { getSelectedScript, selectedPath, setPath, loadPath, deletePath, selectedScriptId } = useStore();
const [isLoading, setIsLoading] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const selectedScript = getSelectedScript();
@@ -44,6 +46,18 @@ const PathView = ({ onGoToScript }) => {
}
};
/**
* 处理删除路径
*/
const handleDelete = async () => {
try {
await deletePath(selectedScriptId);
setShowDeleteConfirm(false);
} catch (error) {
console.error('Failed to delete path:', error);
}
};
/**
* 解析路径内容为步骤数组
*/
@@ -82,61 +96,98 @@ const PathView = ({ onGoToScript }) => {
}
return (
<div className="max-w-3xl mx-auto space-y-12 pb-20">
{/* 标题区域 */}
<div className="flex justify-between items-end">
<div>
<h3 className="text-4xl font-serif">实现路径</h3>
<p className="text-sm text-white/30 mt-2">
基于{selectedScript.theme}拆解达成目标的每一步
</p>
<>
<div className="max-w-3xl mx-auto space-y-12 pb-20">
{/* 标题区域 */}
<div className="flex justify-between items-end">
<div>
<h3 className="text-4xl font-serif">实现路径</h3>
<p className="text-sm text-white/30 mt-2">
基于{selectedScript.theme}拆解达成目标的每一步
</p>
</div>
<div className="flex gap-3">
{selectedPath && (
<GlassButton
onClick={() => setShowDeleteConfirm(true)}
className="px-4 py-3 rounded-full text-sm bg-red-400/5 text-red-300 border-red-400/20 hover:bg-red-400/10"
title="删除路径"
>
<Trash2 className="w-4 h-4" />
</GlassButton>
)}
<GlassButton
onClick={handleGenerate}
disabled={isLoading}
className="px-8 py-3 rounded-full text-sm font-bold bg-blue-400/5 text-blue-300 border-blue-400/20"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
规划中...
</>
) : (
selectedPath ? '重新推演' : '开启人生导航'
)}
</GlassButton>
</div>
</div>
<GlassButton
onClick={handleGenerate}
disabled={isLoading}
className="px-8 py-3 rounded-full text-sm font-bold bg-blue-400/5 text-blue-300 border-blue-400/20"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
规划中...
</>
{/* 路径步骤展示 */}
<div className="space-y-6">
{pathSteps.length > 0 ? (
pathSteps.map((step, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
<GlassCard className="border-l-4 border-l-blue-400/40 bg-blue-400/[0.01]" padding="lg">
<h5 className="text-blue-200 font-bold mb-4 flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-blue-400/20 text-[10px] flex items-center justify-center">
{step.index}
</span>
{step.title}
</h5>
<div className="text-white/60 text-sm leading-relaxed whitespace-pre-wrap">
{step.content}
</div>
</GlassCard>
</motion.div>
))
) : (
selectedPath ? '重新推演' : '开启人生导航'
<div className="py-20 text-center text-white/20 italic font-serif">
等待开启人生导航...
</div>
)}
</GlassButton>
</div>
</div>
{/* 路径步骤展示 */}
<div className="space-y-6">
{pathSteps.length > 0 ? (
pathSteps.map((step, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
{/* 删除确认模态框 */}
<Modal isOpen={showDeleteConfirm} onClose={() => setShowDeleteConfirm(false)} title="确认删除" maxWidth="sm">
<div className="space-y-6">
<p className="text-white/70 text-sm">
确定要删除当前的实现路径吗此操作不可恢复
</p>
<div className="flex gap-4">
<GlassButton
onClick={() => setShowDeleteConfirm(false)}
className="flex-1"
>
<GlassCard className="border-l-4 border-l-blue-400/40 bg-blue-400/[0.01]" padding="lg">
<h5 className="text-blue-200 font-bold mb-4 flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-blue-400/20 text-[10px] flex items-center justify-center">
{step.index}
</span>
{step.title}
</h5>
<div className="text-white/60 text-sm leading-relaxed whitespace-pre-wrap">
{step.content}
</div>
</GlassCard>
</motion.div>
))
) : (
<div className="py-20 text-center text-white/20 italic font-serif">
等待开启人生导航...
取消
</GlassButton>
<GlassButton
variant="primary"
onClick={handleDelete}
className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20"
>
确认删除
</GlassButton>
</div>
)}
</div>
</div>
</div>
</Modal>
</>
);
};
+175 -9
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { UserCog, PenTool, Sparkles, BookOpen, Loader2 } from 'lucide-react';
import { UserCog, PenTool, Sparkles, BookOpen, Loader2, Pencil, Trash2 } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassSelect } from '../components/ui';
import Modal from '../components/Modal';
import useStore from '../store/useStore';
import { scriptStyles, scriptLengths } from '../utils/constants';
@@ -17,6 +18,8 @@ const ScriptView = ({ onOpenProfile }) => {
scripts,
selectedScriptId,
addScript,
updateScript,
deleteScript,
setSelectedScriptId,
getSelectedScript,
loadScripts
@@ -35,6 +38,18 @@ const ScriptView = ({ onOpenProfile }) => {
const [length, setLength] = useState(scriptLengths[0].value);
const [isLoading, setIsLoading] = useState(false);
// 编辑模态框状态
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingScript, setEditingScript] = useState(null);
const [editForm, setEditForm] = useState({
theme: '',
style: '',
length: ''
});
// 删除确认状态
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
/**
* 处理剧本生成
*/
@@ -64,6 +79,73 @@ const ScriptView = ({ onOpenProfile }) => {
}
};
/**
* 打开编辑模态框
* @param {Object} script - 要编辑的剧本
*/
const openEditModal = (script) => {
setEditingScript(script);
setEditForm({
theme: script.theme || '',
style: script.style || scriptStyles[0].value,
length: script.length || scriptLengths[0].value
});
setIsEditModalOpen(true);
};
/**
* 关闭编辑模态框
*/
const closeEditModal = () => {
setIsEditModalOpen(false);
setEditingScript(null);
setEditForm({ theme: '', style: '', length: '' });
};
/**
* 处理编辑提交
*/
const handleEditSubmit = async () => {
if (!editForm.theme) {
alert('请输入主题');
return;
}
setIsLoading(true);
try {
await updateScript({
id: editingScript.id,
theme: editForm.theme,
style: editForm.style,
length: editForm.length,
character: registrationData,
events: lifeEvents,
regenerateContent: true // 标记需要重新生成AI内容
});
closeEditModal();
} catch (error) {
console.error('Failed to update script:', error);
} finally {
setIsLoading(false);
}
};
/**
* 处理删除确认
* @param {string} id - 剧本ID
*/
const handleDeleteConfirm = async (id) => {
try {
await deleteScript(id);
setDeleteConfirmId(null);
} catch (error) {
console.error('Failed to delete script:', error);
alert('删除失败,请稍后重试');
setDeleteConfirmId(null);
}
};
/**
* 格式化剧本内容,高亮【标题】
*/
@@ -165,16 +247,35 @@ const ScriptView = ({ onOpenProfile }) => {
scripts.map((script) => (
<div
key={script.id}
onClick={() => setSelectedScriptId(script.id)}
className={`
p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all
p-3 glass-card text-left cursor-pointer hover:bg-white/5 border-white/5 transition-all relative group
${script.id === selectedScriptId ? 'border-orange-200/30 bg-orange-200/5' : ''}
`}
>
<div className="text-[11px] text-white/80 truncate">{script.theme}</div>
<div className="text-[9px] text-white/30 flex justify-between mt-1">
<span>{script.style} | {script.length}</span>
<span>{script.date}</span>
{/* 操作按钮 */}
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => { e.stopPropagation(); openEditModal(script); }}
className="p-1.5 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all"
title="编辑"
>
<Pencil className="w-3 h-3" />
</button>
<button
onClick={(e) => { e.stopPropagation(); setDeleteConfirmId(script.id); }}
className="p-1.5 rounded-full bg-white/5 hover:bg-red-400/10 text-white/30 hover:text-red-400 transition-all"
title="删除"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
<div onClick={() => setSelectedScriptId(script.id)}>
<div className="text-[11px] text-white/80 truncate pr-12">{script.theme}</div>
<div className="text-[9px] text-white/30 flex justify-between mt-1">
<span>{script.style} | {script.length}</span>
<span>{script.date}</span>
</div>
</div>
</div>
))
@@ -189,9 +290,18 @@ const ScriptView = ({ onOpenProfile }) => {
<div className="lg:col-span-8">
<div className="h-full">
{selectedScript ? (
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in" padding="lg">
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in relative group" padding="lg">
{/* 编辑按钮 */}
<button
onClick={() => openEditModal(selectedScript)}
className="absolute top-4 right-4 p-2 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all opacity-0 group-hover:opacity-100"
title="修改剧本"
>
<Pencil className="w-4 h-4" />
</button>
<div className="prose prose-invert max-w-none">
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5 pr-10">
<div>
<h4 className="text-2xl font-serif text-orange-200">{selectedScript.theme}</h4>
<p className="text-[10px] text-white/30 mt-1 uppercase tracking-widest">
@@ -214,6 +324,62 @@ const ScriptView = ({ onOpenProfile }) => {
)}
</div>
</div>
{/* 编辑剧本模态框 */}
<Modal isOpen={isEditModalOpen} onClose={closeEditModal} title="修改剧本">
<div className="space-y-6">
<GlassInput
label="剧本主题"
placeholder="例如:我在职场逆袭了"
value={editForm.theme}
onChange={(v) => setEditForm(prev => ({ ...prev, theme: v }))}
/>
<GlassSelect
label="叙事风格"
options={scriptStyles}
value={editForm.style}
onChange={(v) => setEditForm(prev => ({ ...prev, style: v }))}
/>
<GlassSelect
label="剧本篇幅"
options={scriptLengths}
value={editForm.length}
onChange={(v) => setEditForm(prev => ({ ...prev, length: v }))}
/>
<GlassButton
variant="primary"
onClick={handleEditSubmit}
loading={isLoading}
className="w-full"
>
{isLoading ? '正在重新编撰...' : '重新生成剧本'}
</GlassButton>
</div>
</Modal>
{/* 删除确认模态框 */}
<Modal isOpen={!!deleteConfirmId} onClose={() => setDeleteConfirmId(null)} title="确认删除" maxWidth="sm">
<div className="space-y-6">
<p className="text-white/70 text-sm">
确定要删除这个剧本吗此操作不可恢复关联的实现路径也将被删除
</p>
<div className="flex gap-4">
<GlassButton
onClick={() => setDeleteConfirmId(null)}
className="flex-1"
>
取消
</GlassButton>
<GlassButton
variant="primary"
onClick={() => handleDeleteConfirm(deleteConfirmId)}
className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20"
>
确认删除
</GlassButton>
</div>
</div>
</Modal>
</div>
);
};
+117 -15
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Plus, Wind, Sparkles } from 'lucide-react';
import { Plus, Wind, Sparkles, Pencil, Trash2 } from 'lucide-react';
import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components/ui';
import Modal from '../components/Modal';
import useStore from '../store/useStore';
@@ -79,7 +79,7 @@ const FeedbackContent = ({ content }) => {
* 人生轨迹视图,显示和管理生命事件
*/
const TimelineView = () => {
const { lifeEvents, addLifeEvent, loadLifeEvents } = useStore();
const { lifeEvents, addLifeEvent, updateLifeEvent, deleteLifeEvent, loadLifeEvents } = useStore();
// 加载生命事件
useEffect(() => {
@@ -92,6 +92,12 @@ const TimelineView = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
const [editingEventId, setEditingEventId] = useState(null);
// 删除确认状态
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
// 表单状态
const [eventForm, setEventForm] = useState({
title: '',
@@ -100,7 +106,39 @@ const TimelineView = () => {
});
/**
* 处理表单提交
* 打开新增模态框
*/
const openAddModal = () => {
setEditingEventId(null);
setEventForm({ title: '', time: '', content: '' });
setIsModalOpen(true);
};
/**
* 打开编辑模态框
* @param {Object} event - 要编辑的事件
*/
const openEditModal = (event) => {
setEditingEventId(event.id);
setEventForm({
title: event.title || '',
time: event.time || '',
content: event.content || ''
});
setIsModalOpen(true);
};
/**
* 关闭模态框
*/
const closeModal = () => {
setIsModalOpen(false);
setEditingEventId(null);
setEventForm({ title: '', time: '', content: '' });
};
/**
* 处理表单提交(新增或编辑)
*/
const handleSubmit = async () => {
if (!eventForm.title || !eventForm.time || !eventForm.content) {
@@ -111,21 +149,43 @@ const TimelineView = () => {
setIsLoading(true);
try {
// 直接调用后端添加事件,由后端调用AI进行疗愈分析
await addLifeEvent({
...eventForm
});
if (editingEventId) {
// 编辑模式:调用更新接口
await updateLifeEvent({
id: editingEventId,
...eventForm
});
} else {
// 新增模式:调用添加接口
await addLifeEvent({
...eventForm
});
}
// 重置表单并关闭模态框
setEventForm({ title: '', time: '', content: '' });
setIsModalOpen(false);
closeModal();
} catch (error) {
console.error('Failed to add event:', error);
console.error('Failed to save event:', error);
} finally {
setIsLoading(false);
}
};
/**
* 处理删除确认
* @param {string} id - 事件ID
*/
const handleDeleteConfirm = async (id) => {
try {
await deleteLifeEvent(id);
setDeleteConfirmId(null);
} catch (error) {
console.error('Failed to delete event:', error);
alert('删除失败,请稍后重试');
setDeleteConfirmId(null);
}
};
/**
* 按事件时间倒序排列(最新的在最上面)
* 空日期的事件排在最后
@@ -149,7 +209,7 @@ const TimelineView = () => {
<p className="text-sm text-white/30 mt-2">塑造你的每一刻都被星辰见证</p>
</div>
<GlassButton
onClick={() => setIsModalOpen(true)}
onClick={openAddModal}
className="px-6 py-3 rounded-full text-sm font-bold flex items-center gap-2 bg-orange-200/5 text-orange-200 border-orange-200/20 shadow-lg"
>
<Plus className="w-4 h-4" /> 记录足迹
@@ -168,8 +228,26 @@ const TimelineView = () => {
<div className="timeline-dot absolute left-[-39px] top-6 z-10" />
{/* 事件卡片 */}
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700">
<div className="flex justify-between items-start mb-4">
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700 relative">
{/* 操作按钮 */}
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<button
onClick={() => openEditModal(event)}
className="p-2 rounded-full bg-white/5 hover:bg-orange-200/10 text-white/30 hover:text-orange-200 transition-all"
title="修改足迹"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => setDeleteConfirmId(event.id)}
className="p-2 rounded-full bg-white/5 hover:bg-red-400/10 text-white/30 hover:text-red-400 transition-all"
title="删除足迹"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex justify-between items-start mb-4 pr-20">
<div className="flex items-center gap-3">
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
{event.tags && event.tags.length > 0 && (
@@ -211,8 +289,8 @@ const TimelineView = () => {
</div>
</div>
{/* 添加事件模态框 */}
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} title="记录足迹">
{/* 添加/编辑事件模态框 */}
<Modal isOpen={isModalOpen} onClose={closeModal} title={editingEventId ? '修改足迹' : '记录足迹'}>
<div className="space-y-6">
<GlassInput
label="事件标题"
@@ -243,6 +321,30 @@ const TimelineView = () => {
</GlassButton>
</div>
</Modal>
{/* 删除确认模态框 */}
<Modal isOpen={!!deleteConfirmId} onClose={() => setDeleteConfirmId(null)} title="确认删除" maxWidth="sm">
<div className="space-y-6">
<p className="text-white/70 text-sm">
确定要删除这段人生足迹吗此操作不可恢复相关的 AI 洞察也将被删除
</p>
<div className="flex gap-4">
<GlassButton
onClick={() => setDeleteConfirmId(null)}
className="flex-1"
>
取消
</GlassButton>
<GlassButton
variant="primary"
onClick={() => handleDeleteConfirm(deleteConfirmId)}
className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20"
>
确认删除
</GlassButton>
</div>
</div>
</Modal>
</div>
);
};