diff --git a/.gitignore b/.gitignore index 7e0a76db..e6d79c3d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ build/ *.db ### macOS ### -.DS_Store \ No newline at end of file +.DS_Store + +### Worktrees ### +.worktrees/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 4bd6093a..bfc2dc56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,8 +137,43 @@ flow-types (类型定义) - **与用户沟通及编写文档时,所有内容必须使用中文表述** - 前端包管理使用 pnpm(根据用户配置) - 前端文件命名规范:使用小写字母 + 下划线组合(如 `script_editor.tsx`、`variable_picker.tsx`) +- **前端导入规范**:使用 `@/` 路径别名导入项目内部模块,避免使用相对路径导入 - 设计涉及流程或 UML 图形的解决方案时,使用 Mermaid Markdown 语法 - 在编写计划的时候要遵循 TDD 的开发规范,务必要在方案中进行对实现代码逻辑的单元测试设计。 - 在设计计划方案或执行方案过程中,对于代码的设计规划与调整修改要遵循本项目的代码风格和架构设计规则 - 设计的计划要保存到本地的 `docs/` 目录下,每一个计划以时间+标题的方式命名创建文件夹,例如 `2026-02-26-xxxx`,文件夹下内容分为 `plan.md` 以及其他设计文件(如设计文件 xxx.pen 或其他设计内容信息) +```typescript +// ✅ 正确:使用 @/ 路径别名 +import { GroovySyntaxConverter } from '@/components/design-editor/script/service/groovy-syntax-converter'; +import { ScriptType } from '@/components/design-editor/typings/groovy-script'; + +// ❌ 错误:避免使用相对路径 +import { GroovySyntaxConverter } from '../../../src/components/...'; +``` + +### 面向对象开发规范 + +除前端 **.tsx 组件** 以外的所有代码(Java 后端、TypeScript 非组件文件)均采用面向对象方式开发: + +- **Service 层**:使用 class 定义,通过依赖注入或单例模式使用 +- **工具类**:使用 class 定义,提供实例方法或静态方法 +- **适配器/转换器**:使用 class 实现,支持扩展 + +```typescript +// ✅ 正确:使用 class 定义 Service +export class GroovySyntaxConverter { + private adapters: Map = new Map(); + + public registerAdapter(adapter: ScriptAdapter): void { + this.adapters.set(adapter.scriptType, adapter); + } + + public toScript(...): string { ... } +} + +// ❌ 错误:避免直接使用函数导出 +export function toScript(...): string { ... } +export const toScript = (...): string => { ... }; +``` + diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/ConditionScript.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/ConditionScript.java index dd5f37ae..c8c506c8 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/ConditionScript.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/ConditionScript.java @@ -1,5 +1,6 @@ package com.codingapi.flow.script.node; +import com.codingapi.flow.script.request.ConditionGroovyRequest; import com.codingapi.flow.script.runtime.ScriptRuntimeContext; import com.codingapi.flow.session.FlowSession; import lombok.AllArgsConstructor; @@ -8,12 +9,13 @@ @AllArgsConstructor public class ConditionScript { - public static final String SCRIPT_DEFAULT = "def run(session){return true}"; + public static final String SCRIPT_DEFAULT = "def run(request){return true}"; @Getter private final String script; - public boolean execute(FlowSession request) { + public boolean execute(FlowSession session) { + ConditionGroovyRequest request = new ConditionGroovyRequest(session); return ScriptRuntimeContext.getInstance().run(script, Boolean.class, request); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/NodeTitleScript.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/NodeTitleScript.java index 0686ec9d..d62808d9 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/NodeTitleScript.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/NodeTitleScript.java @@ -1,7 +1,7 @@ package com.codingapi.flow.script.node; import com.codingapi.flow.script.runtime.ScriptRuntimeContext; -import com.codingapi.flow.script.runtime.TitleGroovyRequest; +import com.codingapi.flow.script.request.TitleGroovyRequest; import com.codingapi.flow.session.FlowSession; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,16 +18,7 @@ public class NodeTitleScript { private final String script; public String execute(FlowSession session) { - TitleGroovyRequest request = session.createTitleRequest(); - return execute(request); - } - - /** - * 执行标题脚本(直接使用TitleGroovyRequest) - * @param request 标题请求对象 - * @return 标题字符串 - */ - public String execute(TitleGroovyRequest request) { + TitleGroovyRequest request = new TitleGroovyRequest(session); return ScriptRuntimeContext.getInstance().run(script, String.class, request); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/OperatorLoadScript.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/OperatorLoadScript.java index 5e3e6dcd..5202ce28 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/OperatorLoadScript.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/node/OperatorLoadScript.java @@ -1,6 +1,7 @@ package com.codingapi.flow.script.node; import com.codingapi.flow.operator.IFlowOperator; +import com.codingapi.flow.script.request.OperatorLoadGroovyRequest; import com.codingapi.flow.script.runtime.ScriptRuntimeContext; import com.codingapi.flow.session.FlowSession; import lombok.AllArgsConstructor; @@ -20,7 +21,8 @@ public class OperatorLoadScript { private final String script; @SuppressWarnings("unchecked") - public List execute(FlowSession request) { + public List execute(FlowSession session) { + OperatorLoadGroovyRequest request = new OperatorLoadGroovyRequest(session); return ScriptRuntimeContext.getInstance().run(script, List.class, request); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/BaseGroovyRequest.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/BaseGroovyRequest.java new file mode 100644 index 00000000..f40a5c2c --- /dev/null +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/BaseGroovyRequest.java @@ -0,0 +1,95 @@ +package com.codingapi.flow.script.request; + +import com.codingapi.flow.session.FlowSession; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * Groovy脚本请求对象抽象基类 + * 从FlowSession中提取数据供脚本使用 + */ +@Getter +@Setter +public abstract class BaseGroovyRequest { + + /** + * 当前操作人姓名 + */ + protected String operatorName; + + /** + * 当前操作人ID + */ + protected Integer operatorId; + + /** + * 是否流程管理员 + */ + protected Boolean isFlowManager; + + /** + * 流程标题 + */ + protected String workflowTitle; + + /** + * 流程编码 + */ + protected String workflowCode; + + /** + * 流程编号 + */ + protected String workCode; + + /** + * 流程创建人姓名 + */ + protected String creatorName; + + /** + * 表单字段值 + */ + protected Map formData; + + /** + * 从FlowSession构建请求对象(模板方法模式) + * @param session 流程会话(不能为null) + */ + public BaseGroovyRequest(FlowSession session) { + // 提取操作人信息 + if (session.getCurrentOperator() != null) { + this.operatorName = session.getCurrentOperator().getName(); + this.operatorId = (int) session.getCurrentOperator().getUserId(); + this.isFlowManager = session.getCurrentOperator().isFlowManager(); + } + + // 提取流程信息 + if (session.getWorkflow() != null) { + this.workflowTitle = session.getWorkflow().getTitle(); + this.workflowCode = session.getWorkflow().getCode(); + if (session.getWorkflow().getCreatedOperator() != null) { + this.creatorName = session.getWorkflow().getCreatedOperator().getName(); + } + } + + // 提取表单数据 + if (session.getFormData() != null) { + this.formData = session.getFormData().toMapData(); + } + } + + /** + * 获取表单字段值(Groovy脚本调用) + * @param key 字段key + * @return 字段值 + */ + public Object getFormData(String key) { + if (formData == null) { + return null; + } + return formData.get(key); + } +} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/ConditionGroovyRequest.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/ConditionGroovyRequest.java new file mode 100644 index 00000000..bbd6eb4b --- /dev/null +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/ConditionGroovyRequest.java @@ -0,0 +1,32 @@ +package com.codingapi.flow.script.request; + +import com.codingapi.flow.session.FlowSession; +import lombok.Getter; +import lombok.Setter; + +/** + * 条件表达式Groovy脚本请求对象 + * 提供给ConditionScript使用的上下文数据 + * 继承BaseGroovyRequest,从FlowSession自动提取通用数据 + */ +@Getter +@Setter +public class ConditionGroovyRequest extends BaseGroovyRequest { + + /** + * 当前节点名称 + */ + private String nodeName; + + /** + * 从FlowSession构建ConditionGroovyRequest + * @param session 流程会话(不能为null) + */ + public ConditionGroovyRequest(FlowSession session) { + super(session); + // 提取节点信息 + if (session.getCurrentNode() != null) { + this.nodeName = session.getCurrentNode().getName(); + } + } +} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/OperatorLoadGroovyRequest.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/OperatorLoadGroovyRequest.java new file mode 100644 index 00000000..9cab1540 --- /dev/null +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/OperatorLoadGroovyRequest.java @@ -0,0 +1,33 @@ +package com.codingapi.flow.script.request; + +import com.codingapi.flow.operator.IFlowOperator; +import com.codingapi.flow.session.FlowSession; +import lombok.Getter; +import lombok.Setter; + +/** + * 人员加载Groovy脚本请求对象 + * 提供给OperatorLoadScript使用的上下文数据 + * 继承BaseGroovyRequest,从FlowSession自动提取通用数据 + */ +@Getter +@Setter +public class OperatorLoadGroovyRequest extends BaseGroovyRequest { + + /** + * 流程创建人 + */ + private IFlowOperator createdOperator; + + /** + * 从FlowSession构建OperatorLoadGroovyRequest + * @param session 流程会话(不能为null) + */ + public OperatorLoadGroovyRequest(FlowSession session) { + super(session); + // 提取创建人信息 + if (session.getWorkflow() != null && session.getWorkflow().getCreatedOperator() != null) { + this.createdOperator = session.getWorkflow().getCreatedOperator(); + } + } +} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/TitleGroovyRequest.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/TitleGroovyRequest.java new file mode 100644 index 00000000..86accad5 --- /dev/null +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/script/request/TitleGroovyRequest.java @@ -0,0 +1,42 @@ +package com.codingapi.flow.script.request; + +import com.codingapi.flow.session.FlowSession; +import lombok.Getter; +import lombok.Setter; + +/** + * 标题表达式Groovy脚本请求对象 + * 提供给NodeTitleScript使用的上下文数据 + * 继承BaseGroovyRequest,从FlowSession自动提取通用数据 + */ +@Getter +@Setter +public class TitleGroovyRequest extends BaseGroovyRequest { + + /** + * 当前节点名称 + */ + private String nodeName; + + /** + * 当前节点类型 + */ + private String nodeType; + + /** + * 从FlowSession构建TitleGroovyRequest + * @param session 流程会话 + */ + public TitleGroovyRequest(FlowSession session) { + super(session); + // 提取节点信息 + if (session != null && session.getCurrentNode() != null) { + this.nodeName = session.getCurrentNode().getName(); + this.nodeType = session.getCurrentNode().getType(); + } + // 提取流程编号(从record获取) + if (session != null && session.getCurrentRecord() != null) { + this.workCode = session.getCurrentRecord().getWorkCode(); + } + } +} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/TitleGroovyRequest.java b/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/TitleGroovyRequest.java deleted file mode 100644 index e0ad003c..00000000 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/script/runtime/TitleGroovyRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.codingapi.flow.script.runtime; - -import lombok.Data; -import java.util.Map; - -/** - * 标题表达式Groovy脚本请求对象 - * 提供给NodeTitleScript使用的上下文数据 - */ -@Data -public class TitleGroovyRequest { - - // ========== 操作人信息 ========== - /** - * 当前操作人姓名 - */ - private String operatorName; - - /** - * 当前操作人ID - */ - private Integer operatorId; - - /** - * 是否流程管理员 - */ - private Boolean isFlowManager; - - // ========== 流程信息 ========== - /** - * 流程标题 - */ - private String workflowTitle; - - /** - * 流程编码 - */ - private String workflowCode; - - /** - * 当前节点名称 - */ - private String nodeName; - - /** - * 当前节点类型 - */ - private String nodeType; - - // ========== 创建人信息 ========== - /** - * 流程创建人姓名 - */ - private String creatorName; - - // ========== 表单数据 ========== - /** - * 表单字段值 - */ - private Map formData; - - // ========== 流程编号 ========== - /** - * 流程编号 - */ - private String workCode; - - /** - * 获取表单字段值(Groovy脚本调用) - */ - public Object getFormData(String key) { - if (formData == null) { - return null; - } - return formData.get(key); - } -} diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/session/FlowSession.java b/flow-engine-framework/src/main/java/com/codingapi/flow/session/FlowSession.java index a2874a06..3c083b87 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/session/FlowSession.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/session/FlowSession.java @@ -8,7 +8,7 @@ import com.codingapi.flow.pojo.request.FlowActionRequest; import com.codingapi.flow.pojo.request.FlowCreateRequest; import com.codingapi.flow.record.FlowRecord; -import com.codingapi.flow.script.runtime.TitleGroovyRequest; +import com.codingapi.flow.script.request.TitleGroovyRequest; import com.codingapi.flow.workflow.Workflow; import lombok.Getter; import lombok.Setter; @@ -242,40 +242,6 @@ public FlowSession updateSession(IFlowOperator currentOperator) { * 从当前session构建TitleGroovyRequest */ public TitleGroovyRequest createTitleRequest() { - TitleGroovyRequest request = new TitleGroovyRequest(); - - // 操作人信息 - if (currentOperator != null) { - request.setOperatorName(currentOperator.getName()); - request.setOperatorId((int) currentOperator.getUserId()); - request.setIsFlowManager(currentOperator.isFlowManager()); - } - - // 流程信息 - if (workflow != null) { - request.setWorkflowTitle(workflow.getTitle()); - request.setWorkflowCode(workflow.getCode()); - } - - // 节点信息 - if (currentNode != null) { - request.setNodeName(currentNode.getName()); - request.setNodeType(currentNode.getType()); - } - - // 创建人信息 - if (workflow != null && workflow.getCreatedOperator() != null) { - request.setCreatorName(workflow.getCreatedOperator().getName()); - } - - // 表单数据 - request.setFormData(formData != null ? formData.toMapData() : null); - - // 流程编号(从record获取) - if (currentRecord != null) { - request.setWorkCode(currentRecord.getWorkCode()); - } - - return request; + return new TitleGroovyRequest(this); } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/integration/NodeTitleIntegrationTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/integration/NodeTitleIntegrationTest.java index 007a9b0d..58216370 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/integration/NodeTitleIntegrationTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/integration/NodeTitleIntegrationTest.java @@ -1,12 +1,17 @@ package com.codingapi.flow.integration; +import com.codingapi.flow.form.FormData; +import com.codingapi.flow.form.FormMeta; +import com.codingapi.flow.form.FormMetaBuilder; +import com.codingapi.flow.node.nodes.EndNode; +import com.codingapi.flow.node.nodes.StartNode; import com.codingapi.flow.script.node.NodeTitleScript; -import com.codingapi.flow.script.runtime.TitleGroovyRequest; +import com.codingapi.flow.session.FlowSession; +import com.codingapi.flow.user.User; +import com.codingapi.flow.workflow.Workflow; +import com.codingapi.flow.workflow.WorkflowBuilder; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -21,10 +26,10 @@ void testTitleGenerationWithOperatorName() { "def run(request){return \"审批人:\" + request.getOperatorName()}" ); - TitleGroovyRequest request = new TitleGroovyRequest(); - request.setOperatorName("张三"); + User user = new User(1, "张三"); + FlowSession session = new FlowSession(user, null, null, null, null, null, null, 0, null); - String result = script.execute(request); + String result = script.execute(session); assertEquals("审批人:张三", result); } @@ -34,12 +39,30 @@ void testTitleGenerationWithFormData() { "def run(request){return \"请假\" + request.getFormData(\"days\") + \"天\"}" ); - TitleGroovyRequest request = new TitleGroovyRequest(); - Map formData = new HashMap<>(); - formData.put("days", 5); - request.setFormData(formData); - - String result = script.execute(request); + User user = new User(1, "张三"); + FormMeta form = FormMetaBuilder.builder() + .name("请假流程") + .code("leave") + .addField("请假天数", "days", "int") + .build(); + + StartNode startNode = StartNode.builder().build(); + EndNode endNode = EndNode.builder().build(); + Workflow workflow = WorkflowBuilder.builder() + .title("请假流程") + .code("leave") + .createdOperator(user) + .form(form) + .addNode(startNode) + .addNode(endNode) + .build(); + + FormData data = new FormData(form); + data.getDataBody().set("days", 5); + + FlowSession session = new FlowSession(user, workflow, startNode, startNode.getActions().get(0), data, null, null, 0, null); + + String result = script.execute(session); assertEquals("请假5天", result); } @@ -49,13 +72,30 @@ void testTitleGenerationWithMultipleVariables() { "def run(request){return \"你好,\" + request.getOperatorName() + \",请假\" + request.getFormData(\"days\") + \"天\"}" ); - TitleGroovyRequest request = new TitleGroovyRequest(); - request.setOperatorName("李四"); - Map formData = new HashMap<>(); - formData.put("days", 3); - request.setFormData(formData); - - String result = script.execute(request); + User user = new User(1, "李四"); + FormMeta form = FormMetaBuilder.builder() + .name("请假流程") + .code("leave") + .addField("请假天数", "days", "int") + .build(); + + StartNode startNode = StartNode.builder().build(); + EndNode endNode = EndNode.builder().build(); + Workflow workflow = WorkflowBuilder.builder() + .title("请假流程") + .code("leave") + .createdOperator(user) + .form(form) + .addNode(startNode) + .addNode(endNode) + .build(); + + FormData data = new FormData(form); + data.getDataBody().set("days", 3); + + FlowSession session = new FlowSession(user, workflow, startNode, startNode.getActions().get(0), data, null, null, 0, null); + + String result = script.execute(session); assertEquals("你好,李四,请假3天", result); } @@ -65,9 +105,10 @@ void testTitleGenerationWithSimpleText() { "def run(request){return \"你有一条待办\"}" ); - TitleGroovyRequest request = new TitleGroovyRequest(); + User user = new User(1, "张三"); + FlowSession session = new FlowSession(user, null, null, null, null, null, null, 0, null); - String result = script.execute(request); + String result = script.execute(session); assertEquals("你有一条待办", result); } @@ -77,11 +118,27 @@ void testTitleGenerationWithWorkflowTitle() { "def run(request){return request.getWorkflowTitle() + \" - \" + request.getOperatorName()}" ); - TitleGroovyRequest request = new TitleGroovyRequest(); - request.setWorkflowTitle("请假审批"); - request.setOperatorName("王五"); - - String result = script.execute(request); + User user = new User(1, "王五"); + FormMeta form = FormMetaBuilder.builder() + .name("请假流程") + .code("leave") + .addField("请假人", "name", "string") + .build(); + + StartNode startNode = StartNode.builder().build(); + EndNode endNode = EndNode.builder().build(); + Workflow workflow = WorkflowBuilder.builder() + .title("请假审批") + .code("leave") + .createdOperator(user) + .form(form) + .addNode(startNode) + .addNode(endNode) + .build(); + + FlowSession session = new FlowSession(user, workflow, startNode, startNode.getActions().get(0), null, null, null, 0, null); + + String result = script.execute(session); assertEquals("请假审批 - 王五", result); } } diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/script/node/NodeTitleScriptTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/script/node/NodeTitleScriptTest.java index 70adfc42..d7da4532 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/script/node/NodeTitleScriptTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/script/node/NodeTitleScriptTest.java @@ -5,7 +5,7 @@ import com.codingapi.flow.form.FormMetaBuilder; import com.codingapi.flow.node.nodes.EndNode; import com.codingapi.flow.node.nodes.StartNode; -import com.codingapi.flow.script.runtime.TitleGroovyRequest; +import com.codingapi.flow.script.request.TitleGroovyRequest; import com.codingapi.flow.session.FlowSession; import com.codingapi.flow.user.User; import com.codingapi.flow.workflow.Workflow; diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/script/runtime/TitleGroovyRequestTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/script/runtime/TitleGroovyRequestTest.java deleted file mode 100644 index 16d998f2..00000000 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/script/runtime/TitleGroovyRequestTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.codingapi.flow.script.runtime; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashMap; -import java.util.Map; - -class TitleGroovyRequestTest { - - @Test - void testGetOperatorName() { - TitleGroovyRequest request = new TitleGroovyRequest(); - request.setOperatorName("张三"); - assertEquals("张三", request.getOperatorName()); - } - - @Test - void testGetFormData() { - TitleGroovyRequest request = new TitleGroovyRequest(); - Map formData = new HashMap<>(); - formData.put("days", 5); - request.setFormData(formData); - assertEquals(5, request.getFormData("days")); - } -} diff --git a/frontend/packages/flow-pc/flow-pc-design/src/components/design-editor/node-components/scripts/components/script-config-modal.tsx b/frontend/packages/flow-pc/flow-pc-design/src/components/design-editor/node-components/scripts/components/script-config-modal.tsx new file mode 100644 index 00000000..d1019d5b --- /dev/null +++ b/frontend/packages/flow-pc/flow-pc-design/src/components/design-editor/node-components/scripts/components/script-config-modal.tsx @@ -0,0 +1,262 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Modal, Input, Alert, Button, Space, message } from 'antd'; +import { EditOutlined, CodeOutlined } from '@ant-design/icons'; +import { GroovyVariableMapping } from '@flow-engine/flow-types'; +import { ScriptType } from '@/components/design-editor/typings/groovy-script'; +import { groovySyntaxConverter } from '@/components/design-editor/script/service/groovy-syntax-converter'; +import { ScriptEditor } from './script-editor'; + +const { TextArea } = Input; + +export interface ScriptConfigModalProps { + /** 脚本类型 */ + scriptType: ScriptType; + /** 当前脚本 */ + script: string; + /** 变量映射列表 */ + variables: GroovyVariableMapping[]; + /** 确认回调 */ + onConfirm: (script: string) => void; + /** 取消回调 */ + onCancel: () => void; + /** 弹框标题 */ + title?: string; +} + +/** + * 通用脚本配置弹框 + * 支持普通模式和高级模式切换 + */ +export const ScriptConfigModal: React.FC = ({ + scriptType, + script, + variables, + onConfirm, + onCancel, + title = '脚本配置', +}) => { + const [mode, setMode] = useState<'normal' | 'advanced'>('normal'); + const [content, setContent] = useState(''); + const [cursorPosition, setCursorPosition] = useState(0); + const [variablePickerOpen, setVariablePickerOpen] = useState(false); + const textareaRef = useRef(null); + const userModifiedModeRef = useRef(false); + + useEffect(() => { + if (userModifiedModeRef.current) { + return; + } + + const isAdvanced = groovySyntaxConverter.isAdvancedMode(script); + const parsedMode = isAdvanced ? 'advanced' : 'normal'; + + if (parsedMode === 'normal') { + const labelExpr = groovySyntaxConverter.toExpression(scriptType, script, variables); + setContent(labelExpr || ''); + } else { + setContent(script); + } + + setMode(parsedMode); + }, [script]); + + // 切换到高级模式 + const handleSwitchToAdvanced = () => { + const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables); + setContent(groovyScript); + setMode('advanced'); + userModifiedModeRef.current = true; + }; + + // 切换到普通模式 + const handleSwitchToNormal = () => { + const labelExpr = groovySyntaxConverter.toExpression(scriptType, content, variables); + if (labelExpr === null) { + message.error('当前脚本无法转换为可视化表达式,请检查语法'); + return; + } + setContent(labelExpr); + setMode('normal'); + userModifiedModeRef.current = true; + }; + + // 确认 + const handleConfirm = () => { + if (mode === 'normal') { + const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables); + onConfirm(groovyScript); + } else { + onConfirm(content); + } + }; + + // 插入变量 + const handleInsertVariable = (mapping: GroovyVariableMapping) => { + const variableText = `\${${mapping.label}}`; + const start = cursorPosition; + const newContent = content.substring(0, start) + variableText + content.substring(start); + setContent(newContent); + setCursorPosition(start + variableText.length); + setVariablePickerOpen(false); + + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.setSelectionRange(start + variableText.length, start + variableText.length); + } + }, 0); + }; + + // 预览 + const handlePreview = () => { + if (mode === 'normal') { + const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables); + const labelExpr = groovySyntaxConverter.toExpression(scriptType, groovyScript, variables); + message.info(labelExpr || '预览: ' + groovyScript); + } else { + message.info('预览: ' + content); + } + }; + + // 变量选择器 + const renderVariablePicker = () => { + if (!variablePickerOpen) { + return null; + } + + // 按 tag 分组 + const groups = new Map(); + for (const v of variables) { + const group = groups.get(v.tag) || []; + group.push(v); + groups.set(v.tag, group); + } + + return ( +
+ {Array.from(groups.entries()).map(([tag, vars]) => ( +
+
{tag}
+ {vars.map(v => ( +
{ + handleInsertVariable(v); + setVariablePickerOpen(false); + }} + style={{ + padding: '4px 8px', + cursor: 'pointer', + borderRadius: '4px', + }} + onMouseEnter={e => e.currentTarget.style.background = '#f5f5f5'} + onMouseLeave={e => e.currentTarget.style.background = 'transparent'} + > +
{v.label}
+
{v.value}
+
+ ))} +
+ ))} +
+ ); + }; + + return ( + +
+ {/* 模式切换 */} +
+ + + + +
+ + {/* 编辑器区域 */} + {mode === 'normal' ? ( +
+