diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc index cef0288..122df57 100644 --- a/.cursor/rules/rules.mdc +++ b/.cursor/rules/rules.mdc @@ -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、repository(mapper/dao)、domain/entity、converter、config、client(feign)、event、facade、task、util、constant。 - - DTO/Request/Response 与 Entity 分离;Controller 不做对象互转,统一在 Service/Converter 层完成。 - -### 二、分层架构规范 - -- Controller 层职责 - - 接收请求、参数校验(Jakarta Validation)、鉴权校验(由网关/拦截器)、调用 Service,统一使用 `Result` 返回。 - - 禁止:业务逻辑、事务控制、数据访问、实体与请求/响应的相互转换。 - -- 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 与 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 层使用 MockMvc;Repository 层覆盖复杂 SQL 与多条件查询。 - -- 重构指导原则 - - 小步快跑、单一职责、先加测试再重构;保持对外行为一致(新增特性使用特性开关/版本控制)。 - - 严禁大规模跨模块重构合并在单次 PR;拆分为多个可审查的独立提交。 - ---- - -## 三、变更管理流程 - -- 变更前影响分析 - - 维度:接口兼容性(路径/入参/语义)、数据库(表/字段/索引/数据迁移)、配置(Nacos/环境变量)、依赖版本、性能、容量与并发、安全与合规、可回滚性。 - - 产出:影响分析文档与回滚方案(包含回滚脚本/开关)。 - -- 回归测试策略 - - 冒烟覆盖主干流程;模块级回归覆盖变更影响域;跨服务联调验证接口契约(通过网关)。 - - 回归范围基于依赖关系与影响分析确定;优先自动化回归,必要时补充手工用例。 - -- 版本管理与发布 - - 分支策略建议: - - 主分支:master; - - 功能分支:feature/{module}-{short-desc}; - - 预发布:release/x.y.z; - - 紧急修复:hotfix/x.y.z; - - 版本号:语义化版本 SemVer(MAJOR.MINOR.PATCH),发布时打 Tag;所有服务端口遵循 280xx 约定(配置文件统一)。 - -- 紧急修复与常规开发 - - 紧急修复:创建 hotfix → 修复 → 快速测试 → 合并 master 与 release → 立即发布 → 追补测试与文档。 - - 常规开发:feature → PR 审核 → 合并 → 集成测试 → 发布。 - ---- - -## 四、持续改进机制 - -- 规范评审与更新 - - 每月一次质量例会,评审新增问题与改进项;规范变更需在本文件留痕(版本/日期/变更点)。 - -- 质量指标监控(建议在 Sonar/日志/监控平台看板化) - - 构建成功率、静态问题趋势、单元测试覆盖率、平均修复时长、缺陷密度、接口成功率、P95/P99 响应时间、数据库慢查询比例。 - -- 团队培训计划 - - 新人入项必读规范与代码演练;关键模块轮训;每季度至少一次专题分享(性能调优/安全加固/重构实践)。 - -- 反馈闭环 - - 通过代码评审、事后复盘(Postmortem)、问题工单收敛到规范条款;指定责任人与截止时间;下次评审验证落地结果。 - ---- - -## 附:标准检查清单(节选) - -- PR 自检 - - 命名/注释达标;无魔法值;日志分级正确且无敏感信息;空指针风险已处理;删除代码/文件已获批准。 - - Controller 不含业务逻辑;路由命名与网关规范一致;统一 Result 返回;参数校验齐全;不使用 PathVariable。 - - Service 事务边界清晰;仅更新非空字段;分页/批量/幂等校验到位。 - - Repository 使用 LambdaQueryWrapper;避免硬编码字段;SQL 有必要索引;避免 N+1。 - - 测试覆盖关键路径与边界;新增功能附带测试;CI 静态扫描无阻断级问题。 - -- 回滚准备 - - 是否具备配置开关/灰度发布策略;是否提供回滚脚本;数据库迁移是否可逆(或给出补偿方案)。 diff --git a/.qoder/rules/rules.md b/.qoder/rules/rules.md index 347f543..0860d24 100644 --- a/.qoder/rules/rules.md +++ b/.qoder/rules/rules.md @@ -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、repository(mapper/dao)、domain/entity、converter、config、client(feign)、event、facade、task、util、constant。 - - DTO/Request/Response 与 Entity 分离;Controller 不做对象互转,统一在 Service/Converter 层完成。 - -### 二、分层架构规范 - -- Controller 层职责 - - 接收请求、参数校验(Jakarta Validation)、鉴权校验(由网关/拦截器)、调用 Service,统一使用 `Result` 返回。 - - 禁止:业务逻辑、事务控制、数据访问、实体与请求/响应的相互转换。 - -- 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 与 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 层使用 MockMvc;Repository 层覆盖复杂 SQL 与多条件查询。 - -- 重构指导原则 - - 小步快跑、单一职责、先加测试再重构;保持对外行为一致(新增特性使用特性开关/版本控制)。 - - 严禁大规模跨模块重构合并在单次 PR;拆分为多个可审查的独立提交。 - ---- - -## 三、变更管理流程 - -- 变更前影响分析 - - 维度:接口兼容性(路径/入参/语义)、数据库(表/字段/索引/数据迁移)、配置(Nacos/环境变量)、依赖版本、性能、容量与并发、安全与合规、可回滚性。 - - 产出:影响分析文档与回滚方案(包含回滚脚本/开关)。 - -- 回归测试策略 - - 冒烟覆盖主干流程;模块级回归覆盖变更影响域;跨服务联调验证接口契约(通过网关)。 - - 回归范围基于依赖关系与影响分析确定;优先自动化回归,必要时补充手工用例。 - -- 版本管理与发布 - - 分支策略建议: - - 主分支:master; - - 功能分支:feature/{module}-{short-desc}; - - 预发布:release/x.y.z; - - 紧急修复:hotfix/x.y.z; - - 版本号:语义化版本 SemVer(MAJOR.MINOR.PATCH),发布时打 Tag;所有服务端口遵循 280xx 约定(配置文件统一)。 - -- 紧急修复与常规开发 - - 紧急修复:创建 hotfix → 修复 → 快速测试 → 合并 master 与 release → 立即发布 → 追补测试与文档。 - - 常规开发:feature → PR 审核 → 合并 → 集成测试 → 发布。 - ---- - -## 四、持续改进机制 - -- 规范评审与更新 - - 每月一次质量例会,评审新增问题与改进项;规范变更需在本文件留痕(版本/日期/变更点)。 - -- 质量指标监控(建议在 Sonar/日志/监控平台看板化) - - 构建成功率、静态问题趋势、单元测试覆盖率、平均修复时长、缺陷密度、接口成功率、P95/P99 响应时间、数据库慢查询比例。 - -- 团队培训计划 - - 新人入项必读规范与代码演练;关键模块轮训;每季度至少一次专题分享(性能调优/安全加固/重构实践)。 - -- 反馈闭环 - - 通过代码评审、事后复盘(Postmortem)、问题工单收敛到规范条款;指定责任人与截止时间;下次评审验证落地结果。 - ---- - -## 附:标准检查清单(节选) - -- PR 自检 - - 命名/注释达标;无魔法值;日志分级正确且无敏感信息;空指针风险已处理;删除代码/文件已获批准。 - - Controller 不含业务逻辑;路由命名与网关规范一致;统一 Result 返回;参数校验齐全;不使用 PathVariable。 - - Service 事务边界清晰;仅更新非空字段;分页/批量/幂等校验到位。 - - Repository 使用 LambdaQueryWrapper;避免硬编码字段;SQL 有必要索引;避免 N+1。 - - 测试覆盖关键路径与边界;新增功能附带测试;CI 静态扫描无阻断级问题。 - -- 回滚准备 - - 是否具备配置开关/灰度发布策略;是否提供回滚脚本;数据库迁移是否可逆(或给出补偿方案)。 - diff --git a/backend-single/deploy.py b/backend-single/deploy.py old mode 100644 new mode 100755 diff --git a/backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java b/backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java index c85c1dc..18c88f1 100644 --- a/backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java +++ b/backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java @@ -73,4 +73,19 @@ public class EpicScriptUpdateRequest extends BaseRequest { * 是否当前选中 */ private Boolean isSelected; + + /** + * 角色信息(用于AI重新生成) + */ + private String characterInfo; + + /** + * 过往经历摘要(用于AI重新生成) + */ + private String lifeEventsSummary; + + /** + * 是否需要重新生成AI内容 + */ + private Boolean regenerateContent; } diff --git a/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java index d776618..146751f 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/CommentServiceImpl.java @@ -118,9 +118,8 @@ public class CommentServiceImpl extends ServiceImpl impl return false; } - // 逻辑删除 - comment.setIsDeleted(1); - return this.updateById(comment); + // 使用 MyBatis-Plus 的 removeById 方法,自动处理逻辑删除 + return this.removeById(id); } /** diff --git a/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java index 1795e43..bc9501c 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/CommunityPostServiceImpl.java @@ -140,9 +140,8 @@ public class CommunityPostServiceImpl extends ServiceImpl 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 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 @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 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 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(LifePath::getUserId, currentUserId) - .eq(LifePath::getScriptId, scriptId) - .eq(LifePath::getIsDeleted, 0); + .eq(LifePath::getScriptId, scriptId); - List paths = this.list(wrapper); - for (LifePath path : paths) { - path.setIsDeleted(1); - this.updateById(path); - } - return true; + // 使用 MyBatis-Plus 的 remove 方法,自动处理逻辑删除 + return this.remove(wrapper); } /** diff --git a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java index 20cda27..02b6cdc 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java @@ -430,13 +430,12 @@ public class MessageServiceImpl extends ServiceImpl 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); } /** diff --git a/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java index ff00c78..5e08192 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/RewardServiceImpl.java @@ -326,13 +326,12 @@ public class RewardServiceImpl extends ServiceImpl 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); } /** diff --git a/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java index eb5888b..898c6c1 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/TopicInteractionServiceImpl.java @@ -127,11 +127,11 @@ public class TopicInteractionServiceImpl extends ServiceImpl 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 diff --git a/life-script/deploy.sh b/life-script/deploy.sh old mode 100644 new mode 100755 diff --git a/life-script/src/App.jsx b/life-script/src/App.jsx index 4c53421..51cdf30 100644 --- a/life-script/src/App.jsx +++ b/life-script/src/App.jsx @@ -139,7 +139,7 @@ function App() { {/* 主容器 */} -
+
diff --git a/life-script/src/components/Modal.jsx b/life-script/src/components/Modal.jsx index 460f221..165eb6e 100644 --- a/life-script/src/components/Modal.jsx +++ b/life-script/src/components/Modal.jsx @@ -42,7 +42,10 @@ const Modal = ({ {/* 内容区 */} - + {/* 关闭按钮 */} - - + {/* 标题 */} {title && ( {title} )} - + {/* 内容 */}
{children} diff --git a/life-script/src/index.css b/life-script/src/index.css index b972ec1..177eb77 100644 --- a/life-script/src/index.css +++ b/life-script/src/index.css @@ -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 { diff --git a/life-script/src/pages/DashboardPage.jsx b/life-script/src/pages/DashboardPage.jsx index dbb9bcf..a2608b9 100644 --- a/life-script/src/pages/DashboardPage.jsx +++ b/life-script/src/pages/DashboardPage.jsx @@ -40,23 +40,23 @@ const DashboardPage = () => { }; return ( - <> - {/* 头部 */} -
setIsProfileOpen(true)} +
+ {/* 头部 - 固定高度 */} +
setIsProfileOpen(true)} /> - {/* 主内容区 */} -
+ {/* 主内容区 - 占据剩余高度 */} +
- {/* 侧边栏 */} - - {/* 内容区 */} + {/* 内容区 - 独立滚动 */}
{ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.3, ease: 'easeOut' }} - className="h-full" > {renderView()} @@ -75,11 +74,11 @@ const DashboardPage = () => {
{/* 用户资料模态框 */} - setIsProfileOpen(false)} + setIsProfileOpen(false)} /> - +
); }; diff --git a/life-script/src/services/epicScript.js b/life-script/src/services/epicScript.js index e82a730..e727f7e 100644 --- a/life-script/src/services/epicScript.js +++ b/life-script/src/services/epicScript.js @@ -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 }; }; diff --git a/life-script/src/store/useStore.js b/life-script/src/store/useStore.js index 490a02e..f0940be 100644 --- a/life-script/src/store/useStore.js +++ b/life-script/src/store/useStore.js @@ -270,6 +270,39 @@ const useStore = create( } }, + /** + * 更新生命事件 + * @param {Object} event - 事件数据(必须包含 id) + * @returns {Promise} 更新后的事件 + */ + 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} 更新后的剧本 + */ + 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; + } + }, + /** * 删除剧本 */ @@ -380,24 +444,16 @@ const useStore = create( const newScripts = state.scripts.filter(s => s.id !== id); return { scripts: newScripts, - selectedScriptId: state.selectedScriptId === id - ? (newScripts[0]?.id || null) + selectedScriptId: state.selectedScriptId === id + ? (newScripts[0]?.id || null) : state.selectedScriptId, loading: false }; }); } 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; } }, @@ -427,12 +483,12 @@ const useStore = create( */ setPath: async (pathContent, scriptId) => { set({ selectedPath: pathContent }); - + if (scriptId) { try { // 检查是否已有路径 const existingPath = await lifePathService.getPathByScriptId(scriptId).catch(() => null); - + if (existingPath?.data?.id) { // 更新 await lifePathService.updatePath({ @@ -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 }); + } + }, + /** * 清除所有数据 */ diff --git a/life-script/src/views/PathView.jsx b/life-script/src/views/PathView.jsx index 0992343..bf2324d 100644 --- a/life-script/src/views/PathView.jsx +++ b/life-script/src/views/PathView.jsx @@ -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 ( -
- {/* 标题区域 */} -
-
-

实现路径

-

- 基于《{selectedScript.theme}》,拆解达成目标的每一步。 -

+ <> +
+ {/* 标题区域 */} +
+
+

实现路径

+

+ 基于《{selectedScript.theme}》,拆解达成目标的每一步。 +

+
+
+ {selectedPath && ( + 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="删除路径" + > + + + )} + + {isLoading ? ( + <> + + 规划中... + + ) : ( + selectedPath ? '重新推演' : '开启人生导航' + )} + +
- - {isLoading ? ( - <> - - 规划中... - + + {/* 路径步骤展示 */} +
+ {pathSteps.length > 0 ? ( + pathSteps.map((step, index) => ( + + +
+ + {step.index} + + {step.title} +
+
+ {step.content} +
+
+
+ )) ) : ( - selectedPath ? '重新推演' : '开启人生导航' +
+ 等待开启人生导航... +
)} - +
- {/* 路径步骤展示 */} -
- {pathSteps.length > 0 ? ( - pathSteps.map((step, index) => ( - setShowDeleteConfirm(false)} title="确认删除" maxWidth="sm"> +
+

+ 确定要删除当前的实现路径吗?此操作不可恢复。 +

+
+ setShowDeleteConfirm(false)} + className="flex-1" > - -
- - {step.index} - - {step.title} -
-
- {step.content} -
-
- - )) - ) : ( -
- 等待开启人生导航... + 取消 + + + 确认删除 +
- )} -
-
+
+ + ); }; diff --git a/life-script/src/views/ScriptView.jsx b/life-script/src/views/ScriptView.jsx index af0968b..bd62908 100644 --- a/life-script/src/views/ScriptView.jsx +++ b/life-script/src/views/ScriptView.jsx @@ -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'; @@ -11,12 +12,14 @@ import { scriptStyles, scriptLengths } from '../utils/constants'; * @param {Function} props.onOpenProfile - 打开用户资料模态框回调 */ const ScriptView = ({ onOpenProfile }) => { - const { - registrationData, - lifeEvents, - scripts, - selectedScriptId, - addScript, + const { + registrationData, + lifeEvents, + 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); + /** * 处理剧本生成 */ @@ -45,17 +60,17 @@ const ScriptView = ({ onOpenProfile }) => { } setIsLoading(true); - + try { // 直接调用后端创建接口,由后端调用AI生成 - await addScript({ - theme, - style, - length, + await addScript({ + theme, + style, + length, character: registrationData, events: lifeEvents }); - + setTheme(''); } catch (error) { console.error('Failed to generate script:', error); @@ -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) => (
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' : ''} `} > -
{script.theme}
-
- {script.style} | {script.length} - {script.date} + {/* 操作按钮 */} +
+ + +
+ +
setSelectedScriptId(script.id)}> +
{script.theme}
+
+ {script.style} | {script.length} + {script.date} +
)) @@ -189,9 +290,18 @@ const ScriptView = ({ onOpenProfile }) => {
{selectedScript ? ( - + + {/* 编辑按钮 */} + +
-
+

{selectedScript.theme}

@@ -200,7 +310,7 @@ const ScriptView = ({ onOpenProfile }) => {

-
@@ -214,6 +324,62 @@ const ScriptView = ({ onOpenProfile }) => { )}
+ + {/* 编辑剧本模态框 */} + +
+ setEditForm(prev => ({ ...prev, theme: v }))} + /> + setEditForm(prev => ({ ...prev, style: v }))} + /> + setEditForm(prev => ({ ...prev, length: v }))} + /> + + {isLoading ? '正在重新编撰...' : '重新生成剧本'} + +
+
+ + {/* 删除确认模态框 */} + setDeleteConfirmId(null)} title="确认删除" maxWidth="sm"> +
+

+ 确定要删除这个剧本吗?此操作不可恢复,关联的实现路径也将被删除。 +

+
+ setDeleteConfirmId(null)} + className="flex-1" + > + 取消 + + handleDeleteConfirm(deleteConfirmId)} + className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20" + > + 确认删除 + +
+
+
); }; diff --git a/life-script/src/views/TimelineView.jsx b/life-script/src/views/TimelineView.jsx index c783315..fae6f4b 100644 --- a/life-script/src/views/TimelineView.jsx +++ b/life-script/src/views/TimelineView.jsx @@ -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(() => { @@ -87,11 +87,17 @@ const TimelineView = () => { // 后端不可用时忽略错误 }); }, [loadLifeEvents]); - + // 模态框状态 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) { @@ -109,23 +147,45 @@ 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 = () => {

塑造你的每一刻,都被星辰见证。

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" > 记录足迹 @@ -159,17 +219,35 @@ const TimelineView = () => { {/* 时间线容器 */}
{sortedEvents.length > 0 &&
} - +
{sortedEvents.length > 0 ? ( sortedEvents.map((event) => (
{/* 时间线点 */}
- + {/* 事件卡片 */} - -
+ + {/* 操作按钮 */} +
+ + +
+ +

{event.title}

{event.tags && event.tags.length > 0 && ( @@ -185,7 +263,7 @@ const TimelineView = () => {

{event.content}

- + {/* AI 反馈区域 - 仅在有反馈时显示 */} {event.aiFeedback && (
@@ -211,8 +289,8 @@ const TimelineView = () => {
- {/* 添加事件模态框 */} - setIsModalOpen(false)} title="记录足迹"> + {/* 添加/编辑事件模态框 */} +
{
+ + {/* 删除确认模态框 */} + setDeleteConfirmId(null)} title="确认删除" maxWidth="sm"> +
+

+ 确定要删除这段人生足迹吗?此操作不可恢复,相关的 AI 洞察也将被删除。 +

+
+ setDeleteConfirmId(null)} + className="flex-1" + > + 取消 + + handleDeleteConfirm(deleteConfirmId)} + className="flex-1 bg-red-500/10 text-red-400 border-red-400/20 hover:bg-red-500/20" + > + 确认删除 + +
+
+
); };