diff --git a/designs/approval.pen b/designs/approval.pen new file mode 100644 index 00000000..c199d832 --- /dev/null +++ b/designs/approval.pen @@ -0,0 +1,973 @@ +{ + "version": "2.8", + "children": [ + { + "type": "frame", + "id": "H0Aq7", + "x": 0, + "y": 0, + "name": "Design System", + "width": 400, + "layout": "vertical", + "gap": 40, + "padding": 28, + "children": [ + { + "type": "frame", + "id": "r5fJ4", + "name": "Button/Primary", + "reusable": true, + "height": 40, + "fill": "$--color-accent", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-accent" + }, + "gap": 8, + "padding": [ + 10, + 20 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "geiia", + "name": "ButtonText", + "fill": "#FFFFFF", + "content": "通过", + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "Space Grotesk", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "id": "fyr3p", + "type": "ref", + "ref": "r5fJ4", + "name": "Button/Secondary", + "y": 108, + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-accent" + } + }, + { + "id": "d3jNf", + "type": "ref", + "ref": "r5fJ4", + "name": "Button/Outline", + "y": 188, + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + } + }, + { + "id": "2rwqv", + "type": "ref", + "ref": "r5fJ4", + "name": "Button/Ghost", + "y": 268, + "fill": "none", + "stroke": { + "thickness": 0, + "fill": "transparent" + }, + "padding": [ + 10, + 12 + ] + }, + { + "type": "frame", + "id": "Y0xmz", + "name": "StatusIcon/Completed", + "reusable": true, + "width": 24, + "height": 24, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yOY6V", + "name": "Icon", + "fill": "$--color-success", + "content": "✓", + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "Inter", + "fontSize": 16 + } + ] + }, + { + "type": "frame", + "id": "v7PGP", + "name": "StatusIcon/Current", + "reusable": true, + "width": 24, + "height": 24, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "7jOsJ", + "name": "Dot", + "fill": "$--color-accent", + "width": 16, + "height": 16 + } + ] + }, + { + "type": "frame", + "id": "MWEbp", + "name": "StatusIcon/Pending", + "reusable": true, + "width": 24, + "height": 24, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "jNZdZ", + "name": "Circle", + "fill": "none", + "width": 16, + "height": 16, + "stroke": { + "align": "center", + "thickness": 2, + "fill": "$--color-text-muted" + } + } + ] + }, + { + "type": "frame", + "id": "Q7nd7", + "name": "FormField/ReadOnly", + "reusable": true, + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "L7fix", + "name": "Label", + "fill": "$--color-text-secondary", + "content": "字段名称", + "fontFamily": "Inter", + "fontSize": 13 + }, + { + "type": "frame", + "id": "GoHrJ", + "name": "ValueContainer", + "width": "fill_container", + "height": 40, + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "padding": [ + 10, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HP9Uk", + "name": "Value", + "fill": "$--color-text-primary", + "content": "字段值", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + } + ] + }, + { + "type": "frame", + "id": "jALdQ", + "name": "ApprovalNodeItem", + "reusable": true, + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "UGfOf", + "name": "IconContainer", + "width": 24, + "height": "fit_content(0)", + "layout": "vertical", + "alignItems": "center" + }, + { + "type": "frame", + "id": "OT9MM", + "name": "Content", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "W0CwU", + "name": "Header", + "width": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "152m4", + "name": "NodeName", + "fill": "$--color-text-primary", + "content": "节点名称", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "UKK6F", + "name": "StatusBadge", + "reusable": true, + "width": 60, + "height": 24, + "fill": "$--color-success", + "padding": [ + 4, + 10 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z8Mks", + "name": "StatusText", + "fill": "#FFFFFF", + "content": "通过", + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BVFyN", + "name": "Info", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "QQCRM", + "name": "Approver", + "fill": "$--color-text-secondary", + "content": "审批人: 张三", + "fontFamily": "Inter", + "fontSize": 13 + }, + { + "type": "text", + "id": "YbMfY", + "name": "Time", + "fill": "$--color-text-muted", + "content": "2024-02-25 10:30", + "fontFamily": "Inter", + "fontSize": 12 + } + ] + }, + { + "type": "frame", + "id": "ZLJZr", + "name": "CommentArea", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "4dp39", + "name": "CommentLabel", + "fill": "$--color-text-secondary", + "content": "审批意见", + "fontFamily": "Inter", + "fontSize": 13 + }, + { + "type": "frame", + "id": "hJVqy", + "name": "CommentText", + "width": "fill_container", + "fill": "$--color-surface-tint", + "padding": 12, + "children": [ + { + "type": "text", + "id": "jOsZn", + "name": "Content", + "fill": "$--color-text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "同意采购,请财务部门审核。", + "fontFamily": "Inter", + "fontSize": 13 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "MTclw", + "name": "CollapseButton", + "reusable": true, + "width": 32, + "height": 32, + "fill": "none", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V6fOi", + "name": "Icon", + "fill": "$--color-text-secondary", + "content": "◀", + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "vo8wW", + "name": "ExpandButton", + "reusable": true, + "width": 32, + "height": 32, + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5QBgk", + "name": "Icon", + "fill": "$--color-text-secondary", + "content": "▶", + "textAlign": "center", + "textAlignVertical": "middle", + "fontFamily": "Inter", + "fontSize": 14 + } + ] + }, + { + "type": "frame", + "id": "taMBt", + "name": "CollapsedStrip", + "reusable": true, + "width": 48, + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "layout": "vertical", + "gap": 16, + "padding": [ + 12, + 8 + ], + "children": [ + { + "id": "5IDWO", + "type": "ref", + "ref": "vo8wW", + "name": "StripBtn" + }, + { + "type": "text", + "id": "eTqHS", + "name": "StripLabel", + "fill": "$--color-text-primary", + "content": "审批\n历史", + "lineHeight": 1.4, + "textAlign": "center", + "fontFamily": "Space Grotesk", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Eiyti", + "x": 500, + "y": 0, + "name": "Approval Screen", + "width": 1440, + "fill": "#ffffffff", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "lb1ge", + "name": "Header", + "width": "fill_container", + "height": 64, + "stroke": { + "thickness": 1, + "fill": "$--color-border" + }, + "padding": [ + 20, + 32 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HO6K1", + "name": "HeaderLeft", + "height": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jSWs6", + "name": "Title", + "fill": "$--color-text-primary", + "content": "审批详情", + "textAlignVertical": "middle", + "fontFamily": "Space Grotesk", + "fontSize": 18, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "hAhBA", + "name": "HeaderRight", + "height": "fill_container", + "gap": 12, + "alignItems": "center", + "children": [ + { + "id": "wMIrq", + "type": "ref", + "ref": "r5fJ4", + "name": "PassButton" + }, + { + "id": "FtQF8", + "type": "ref", + "ref": "r5fJ4", + "name": "RejectButton", + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-accent" + }, + "descendants": { + "geiia": { + "fill": "$--color-accent", + "content": "驳回" + } + } + }, + { + "id": "pKiQj", + "type": "ref", + "ref": "r5fJ4", + "name": "RevokeButton", + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "descendants": { + "geiia": { + "fill": "$--color-text-primary", + "content": "撤回" + } + } + }, + { + "id": "tQJQj", + "type": "ref", + "ref": "r5fJ4", + "name": "CloseButton", + "fill": "none", + "stroke": { + "thickness": 0, + "fill": "transparent" + }, + "padding": [ + 10, + 12 + ], + "descendants": { + "geiia": { + "fill": "$--color-text-secondary", + "content": "关闭" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "300ij", + "name": "Body", + "width": "fill_container", + "gap": 24, + "padding": [ + 40, + 48, + 48, + 48 + ], + "children": [ + { + "type": "frame", + "id": "SJri1", + "name": "LeftContent", + "width": 880, + "layout": "vertical", + "gap": 24, + "children": [ + { + "type": "frame", + "id": "GCd39", + "name": "FormCard", + "width": "fill_container", + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "layout": "vertical", + "padding": 28, + "children": [ + { + "type": "text", + "id": "rK77e", + "name": "Title", + "fill": "$--color-text-primary", + "content": "流程表单", + "fontFamily": "Space Grotesk", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "7iOiP", + "name": "FormFields", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "id": "qluNt", + "type": "ref", + "ref": "Q7nd7", + "name": "Field1", + "width": "fill_container", + "descendants": { + "L7fix": { + "content": "申请标题" + }, + "HP9Uk": { + "content": "采购办公用品申请" + } + } + }, + { + "id": "88OuI", + "type": "ref", + "ref": "Q7nd7", + "name": "Field2", + "width": "fill_container", + "descendants": { + "L7fix": { + "content": "申请部门" + }, + "HP9Uk": { + "content": "技术部" + } + } + }, + { + "id": "JJzOr", + "type": "ref", + "ref": "Q7nd7", + "name": "Field3", + "width": "fill_container", + "descendants": { + "L7fix": { + "content": "申请金额" + }, + "HP9Uk": { + "content": "¥15,000.00" + } + } + }, + { + "id": "aOrE2", + "type": "ref", + "ref": "Q7nd7", + "name": "Field4", + "width": "fill_container", + "descendants": { + "L7fix": { + "content": "申请日期" + }, + "HP9Uk": { + "content": "2024-02-24" + } + } + }, + { + "id": "VfIko", + "type": "ref", + "ref": "Q7nd7", + "name": "Field5", + "width": "fill_container", + "descendants": { + "L7fix": { + "content": "申请事由" + }, + "HP9Uk": { + "content": "部门日常办公需要,采购电脑配件及文具用品" + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "AxmH9", + "name": "RightContent", + "width": 420, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "jh1VJ", + "name": "HistoryCard", + "width": "fill_container", + "fill": "#FFFFFF", + "stroke": { + "align": "center", + "thickness": 1, + "fill": "$--color-border" + }, + "layout": "vertical", + "gap": 20, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "BVaLL", + "name": "TitleBar", + "width": "fill_container", + "gap": 12, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P5RbQ", + "name": "Title", + "fill": "$--color-text-primary", + "content": "审批节点历史记录", + "fontFamily": "Space Grotesk", + "fontSize": 18, + "fontWeight": "600" + }, + { + "id": "0bCKW", + "type": "ref", + "ref": "MTclw", + "name": "CollapseBtn" + } + ] + }, + { + "type": "frame", + "id": "yrBm2", + "name": "HistoryList", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "id": "bCwzp", + "type": "ref", + "ref": "jALdQ", + "name": "HistoryItem1", + "width": "fill_container", + "descendants": { + "UGfOf": { + "children": [ + { + "id": "iMe5r", + "type": "ref", + "ref": "Y0xmz", + "name": "StatusIcon" + } + ] + }, + "152m4": { + "content": "部门审批" + }, + "QQCRM": { + "content": "审批人: 王经理" + }, + "YbMfY": { + "content": "2024-02-24 14:30" + } + } + }, + { + "id": "IzCjx", + "type": "ref", + "ref": "jALdQ", + "name": "HistoryItem2", + "width": "fill_container", + "descendants": { + "UGfOf": { + "children": [ + { + "id": "JRtZQ", + "type": "ref", + "ref": "v7PGP", + "name": "StatusIcon" + } + ] + }, + "152m4": { + "content": "财务审批" + }, + "UKK6F": { + "fill": "$--color-accent" + }, + "Z8Mks": { + "content": "待审批" + }, + "QQCRM": { + "content": "审批人: 李总监" + }, + "YbMfY": { + "content": "等待处理" + } + } + }, + { + "id": "b3lJU", + "type": "ref", + "ref": "jALdQ", + "name": "HistoryItem3", + "width": "fill_container", + "descendants": { + "UGfOf": { + "children": [ + { + "id": "YPYal", + "type": "ref", + "ref": "MWEbp", + "name": "StatusIcon" + } + ] + }, + "152m4": { + "content": "总经理审批" + }, + "UKK6F": { + "fill": "$--color-text-secondary" + }, + "Z8Mks": { + "content": "待处理" + }, + "QQCRM": { + "content": "审批人: 张总" + }, + "YbMfY": { + "content": "待审批人处理" + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "hX1PR", + "x": 2000, + "y": 0, + "name": "Demo Area", + "width": 500, + "gap": 40, + "padding": 48, + "children": [ + { + "type": "text", + "id": "FWWsO", + "name": "Label", + "fill": "$--color-text-muted", + "content": "展开状态", + "fontFamily": "Space Grotesk", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "L9mDo", + "type": "ref", + "ref": "taMBt", + "name": "CollapsedDemo" + }, + { + "type": "text", + "id": "C8Mwq", + "name": "Label", + "fill": "$--color-text-muted", + "content": "收缩状态", + "fontFamily": "Space Grotesk", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ], + "variables": { + "--color-accent": { + "type": "color", + "value": "#E42313" + }, + "--color-bg": { + "type": "color", + "value": "#FFFFFF" + }, + "--color-border": { + "type": "color", + "value": "#E8E8E8" + }, + "--color-success": { + "type": "color", + "value": "#22C55E" + }, + "--color-surface-tint": { + "type": "color", + "value": "#FAFAFA" + }, + "--color-text-muted": { + "type": "color", + "value": "#B0B0B0" + }, + "--color-text-primary": { + "type": "color", + "value": "#0D0D0D" + }, + "--color-text-secondary": { + "type": "color", + "value": "#7A7A7A" + }, + "--font-body": { + "type": "string", + "value": "Inter" + }, + "--font-heading": { + "type": "string", + "value": "Space Grotesk" + }, + "--spacing-card-padding": { + "type": "number", + "value": 28 + }, + "--spacing-content-padding-h": { + "type": "number", + "value": 48 + }, + "--spacing-content-padding-v": { + "type": "number", + "value": 40 + }, + "--spacing-header-height": { + "type": "number", + "value": 64 + }, + "--spacing-section-gap": { + "type": "number", + "value": 24 + } + } +} \ No newline at end of file diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/ProcessNode.java b/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/ProcessNode.java index 2dd5028b..5da9e391 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/ProcessNode.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/pojo/response/ProcessNode.java @@ -16,6 +16,11 @@ @Data @NoArgsConstructor public class ProcessNode { + + public final static int STATE_HISTORY = -1; + public final static int STATE_CURRENT = 0; + public final static int STATE_NEXT = 1; + /** * 节点名称 */ @@ -30,9 +35,12 @@ public class ProcessNode { private String nodeType; /** - * 是否历史记录 + * 记录状态 + * -1 为历史状态 + * 0 为当前状态 + * 1 为后续状态 */ - private boolean history; + private int state; /** * 节点审批人 @@ -40,13 +48,17 @@ public class ProcessNode { private List operators; + public boolean isHistory(){ + return this.state == STATE_HISTORY; + } + public ProcessNode(FlowRecord flowRecord, Workflow workflow) { this.nodeId = flowRecord.getNodeId(); IFlowNode flowNode = workflow.getFlowNode(this.nodeId); this.nodeName = flowNode.getName(); this.nodeType = flowNode.getType(); this.operators = new ArrayList<>(); - this.history = true; + this.state = STATE_HISTORY; this.operators.add(new FlowOperatorBody(flowRecord)); } @@ -56,7 +68,16 @@ public ProcessNode(IFlowNode flowNode, List operators) { this.nodeName = flowNode.getName(); this.nodeType = flowNode.getType(); this.operators = operators.stream().map(FlowOperatorBody::new).toList(); - this.history = false; + this.state = STATE_NEXT; + } + + + public boolean isFlowNode(IFlowNode currentNode) { + return this.nodeId.equals(currentNode.getId()); + } + + public void setCurrentState() { + this.state = STATE_CURRENT; } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java b/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java index 61dd930a..c3da7dd6 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowProcessNodeService.java @@ -122,6 +122,9 @@ private void fetchNextNode(FlowSession flowSession, List nexNodes) { operators = operatorManager.getOperators(); } ProcessNode processNode = new ProcessNode(flowNode,operators); + if(processNode.isFlowNode(this.currentNode)){ + processNode.setCurrentState(); + } this.nodeList.add(processNode); List nextNodes = workflow.nextNodes(flowNode); this.fetchNextNode(flowSession.updateSession(flowNode), nextNodes); diff --git a/frontend/.claude/commands/git-push.md b/frontend/.claude/commands/git-push.md new file mode 100644 index 00000000..40f55f37 --- /dev/null +++ b/frontend/.claude/commands/git-push.md @@ -0,0 +1,3 @@ +# git push command + +根据当前调整内容,并创建git的提交指令,并添加对应的提交信息,然后执行git push命令将本地的提交推送到远程仓库。 \ No newline at end of file diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 00000000..51811900 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,466 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Flow Engine frontend is a React/TypeScript monorepo built with Rsbuild/Rspack. It provides a visual workflow designer (flow-design) and runtime applications (app-pc, app-mobile) that integrate with the Java backend workflow engine. + +## Common Commands + +### Package Manager + +This project uses **pnpm workspaces**. Always use `pnpm` for package management. + +```bash +# Install all dependencies +pnpm install + +# Add a dependency to a specific package +pnpm -F @flow-engine/flow-design add +``` + +### Building + +```bash +# Build all packages in order +pnpm run build + +# Build individual packages +pnpm run build:flow-types # Build types first (no dependencies) +pnpm run build:flow-core # Build core API library +pnpm run build:flow-engine # Build flow-design (depends on flow-types, flow-core) +pnpm run build:app-pc # Build PC application + +# Watch mode for development +pnpm run watch:flow-design # Rebuild flow-design on changes +``` + +### Development + +```bash +pnpm run dev:app-pc # Run PC app on localhost:3000 +pnpm run dev:app-mobile # Run mobile app +``` + +### Testing + +```bash +# Run tests (uses @rstest/core) +cd packages/flow-design +pnpm run test + +# Currently: Only one basic test exists in flow-design/tests/demo.test.ts +``` + +## Monorepo Structure + +``` +frontend/ +├── apps/ # Runtime applications +│ ├── app-pc/ # PC client (React Router, proxies to localhost:8090) +│ └── app-mobile/ # Mobile client (in development) +│ +├── packages/ # Shared libraries +│ ├── flow-types/ # TypeScript definitions (no dependencies) +│ ├── flow-core/ # API client + service interfaces +│ └── flow-design/ # Workflow designer component library +│ +└── pnpm-workspace.yaml # Workspace configuration +``` + +### Package Dependencies + +``` +flow-types (no dependencies) + ↓ +flow-core (depends on: flow-types) + ↓ +flow-design (depends on: flow-types, flow-core) + ↓ ↓ +app-pc app-mobile +``` + +When making changes: +1. **flow-types**: Rebuild everything that depends on it +2. **flow-core**: Rebuild flow-design and apps +3. **flow-design**: Rebuild apps + +## Architecture + +### @flowgram.ai Integration + +The flow-design package is built on top of `@flowgram.ai/fixed-layout-editor`: + +- **fixed-layout-editor**: Core editor that handles the visual canvas, node rendering, and drag-drop +- **fixed-semi-materials**: Semi Design UI components for node configuration +- **panel-manager-plugin**: Manages side panels (left/right panels) +- **minimap-plugin**: Adds minimap overview +- **export-plugin**: Workflow export functionality + +**Critical**: The editor uses a **fixed layout system**, not absolute positioning like traditional flowchart libraries. Nodes are positioned using a coordinate system managed by the editor. + +### Redux Architecture (flow-design) + +State management follows a **presenter pattern** with Redux Toolkit: + +``` +Context (useDesignContext) + └── state: { workflow, view } + └── presenter: DesignPanelPresenter + ├── getWorkflowPresenter() + ├── getNodePresenter() + ├── getFlowActionPresenter() + └── getFormPresenter() +``` + +- **Context**: Created via `createDesignContext(props)`, provides state and presenter +- **Presenters**: Handle business logic and API calls +- **Store**: Uses Redux Toolkit slices (`workflowSlice`, `viewSlice`) +- **Selectors**: Access state via `useDesignContext()` hook + +### Design Panel Structure + +The `DesignPanelLayout` follows a Header-Body-Footer pattern using Ant Design components: + +``` +DesignPanelLayout +├── Header - Tabs (base/form/flow/setting) + action buttons +├── Body - Tab content components +└── Footer - Empty (reserved) +``` + +**Tabs**: +- `base` - Basic workflow info (name, code) +- `form` - Form design with field configuration +- `flow` - Visual workflow design with @flowgram.ai editor +- `setting` - Advanced workflow parameters + +### Approval Layout Structure + +The `ApprovalLayout` uses Ant Design Layout with collapsible sidebar: + +``` +ApprovalLayout +├── Header - Typography.Title "审批详情" + Button group +└── Body (Layout) + ├── Content - Card with FormViewComponent + └── Sider - Collapsible sidebar with FlowNodeHistory (Timeline) +``` + +**Key Ant Design Components Used**: +- `Layout`, `Content`, `Sider` - Main layout structure +- `Flex` - Flexible box layout for headers and button groups +- `Card` - Form container with title +- `Timeline` - Approval history display +- `Tag` - Status badges (color prop for different states) +- `Typography` - Text styling with `type="secondary"` for muted text +- `Button` - Action buttons with `type="primary"`, `danger`, `ghost` +- `Space` - Button grouping with spacing +- `Empty` - Empty state display + +## Key Components + +### flow-design Package + +**Directory Structure**: +``` +packages/flow-design/src/components/ +├── design-panel/ # Main design panel +│ ├── layout/ # Layout components (Header, Body, Footer) +│ ├── tabs/ # Tab content components +│ ├── context/ # DesignPanelContext +│ ├── hooks/ # use-design-context +│ ├── store/ # Redux store and slices +│ └── types.ts # TypeScript interfaces +│ +├── design-editor/ # @flowgram.ai editor wrapper +│ └── index.tsx # Editor initialization +│ +└── flow-approval/ # Approval workflow components + ├── layout/ # ApprovalLayout (Header, Body) + ├── components/ # FormViewComponent, FlowNodeHistory + ├── context/ # ApprovalContext + ├── hooks/ # use-approval-context + └── typings/ # TypeScript interfaces +``` + +### Type Definitions (flow-types) + +**Critical Types** in `packages/flow-types/src/pages/design-panel/types.ts`: + +```typescript +interface Workflow { + id: string; + title: string; + code: string; + form: FlowForm; + strategies?: any[]; + nodes?: FlowNode[]; +} + +interface FlowNode { + id: string; + name: string; + type: NodeType; + blocks?: FlowNode[]; // HIERARCHICAL: child nodes + strategies?: any[]; + actions?: FlowAction[]; +} + +interface FlowForm { + name: string; + code: string; + fields: FormField[]; + subForms: FlowForm[]; +} +``` + +**Important**: `FlowNode.blocks` is the hierarchical structure - NOT edge-based connections. + +## State Management Patterns + +### DesignPanel Context + +```typescript +const {state, context} = useDesignContext(); + +// Access state +state.workflow.title +state.view.tabPanel + +// Use presenter to trigger actions +context.getPresenter().updateTitle("New Title"); +context.getPresenter().getFlowActionPresenter().action(actionId); +``` + +### Approval Context + +```typescript +const {state, context} = useApprovalContext(); + +// Process nodes (approval history) +context.getPresenter().processNodes().then(nodes => {...}); + +// Action execution +context.getPresenter().getFlowActionPresenter().action(actionId); +``` + +## UI Component Conventions + +### Using Ant Design Components + +This project uses **Ant Design 6.x** as the primary UI library. Follow these patterns: + +**Layout Components**: +```typescript +import { Layout, Flex, Space } from 'antd'; + + + + {/* Left content */} + {/* Right content */} + + +``` + +**Buttons**: +```typescript +import { Button } from 'antd'; + +// Primary action (first button) + + +// Secondary/Destructive action + + +// Tertiary/Ghost action + + +// Button group with spacing + + + + +``` + +**Card with Title**: +```typescript +import { Card } from 'antd'; + + + {/* Content */} + +``` + +**Collapsible Sidebar**: +```typescript +import { Layout, Sider } from 'antd'; + + + {/* Main content */} + + {/* Sidebar content */} + + +``` + +**Typography**: +```typescript +import { Typography } from 'antd'; + +const { Title, Text } = Typography; + +审批详情 +辅助文本 +``` + +**Timeline (Approval History)**: +```typescript +import { Timeline, Tag } from 'antd'; +import { + CheckCircleFilled, + ClockCircleOutlined, + LoadingOutlined +} from '@ant-design/icons'; + + + } + > +
节点名称
+ 通过 +
+
+``` + +**Tag Colors for Status**: +- `color="success"` - Completed/通过 +- `color="error"` - Current/待审批 +- `color="default"` - Pending/待处理 + +**Spacing**: +- Use Ant Design's standard spacing: 8, 16, 24, 32, 48 +- Use `Space` component for consistent gaps +- Use `Flex` with `gap` prop for layout spacing + +### Component Patterns + +**Header Pattern**: +```typescript + + 页面标题 + + + + + +``` + +**Sidebar Pattern**: +```typescript + + {!collapsed ? ( +
+ 侧边栏标题 + {/* Content */} +
+ ) : ( +
+ {/* Collapsed content */} +
+ )} +
+``` + +## Path Aliases + +All packages use `@/` as an alias for `src/`: + +```typescript +import {Header} from "@/components/design-panel/layout/header"; +import {DesignPanelTypes} from "@/components/design-panel/types"; +``` + +## API Integration + +### Backend Proxy + +During development, API requests are proxied to the backend: + +```javascript +// apps/app-pc/rsbuild.config.ts +proxy: { + '/api': 'http://localhost:8090', + '/open': 'http://localhost:8090', + '/user': 'http://localhost:8090', +} +``` + +### flow-core API Client + +The `flow-core` package provides the HTTP client and service interfaces: + +```typescript +// API client setup in flow-core +const client = axios.create({ + baseURL: '/api', + timeout: 30000, +}); +``` + +## Build System (Rsbuild/Rspack) + +- **Rsbuild**: High-level build configuration (like Vite) +- **Rspack**: Rust-powered bundler (5-10x faster than webpack) +- **Rslib**: Library mode for building packages + +**Configuration Files**: +- Apps: `rsbuild.config.ts` +- Libraries: `rslib.config.ts` + +**Key Features**: +- TypeScript support with strict mode +- Sass/Less support for styling +- ES module output +- Tree-shaking enabled +- React Fast Refresh in dev mode + +## Extension Points + +### Adding New Node Types + +1. Update `NodeType` in `flow-types/src/pages/design-panel/types.ts` +2. Create node configuration component in `flow-design/src/components/design-editor/` +3. Register in `@flowgram.ai` editor materials + +### Adding New Layout Components + +Follow Ant Design patterns: +1. Use Ant Design Layout components (Layout, Content, Sider) +2. Use Flex for responsive layouts +3. Use Space for consistent spacing +4. Use Card for content containers +5. Use Typography for text styling +6. Follow existing component patterns in flow-approval/layout/ + +## Documentation References + +- **flow-design/README.md** - Component library documentation +- **apps/app-pc/AGENTS.md** - App development guidelines +- **Ant Design**: https://ant.design/components/overview-cn/ +- **Rsbuild**: https://rsbuild.rs/llms.txt +- **Rspack**: https://rspack.rs/llms.txt +- **Parent CLAUDE.md** (../CLAUDE.md) - Backend architecture and Java patterns diff --git a/frontend/apps/app-pc/src/pages/todo.tsx b/frontend/apps/app-pc/src/pages/todo.tsx index 9188829d..c27611c0 100644 --- a/frontend/apps/app-pc/src/pages/todo.tsx +++ b/frontend/apps/app-pc/src/pages/todo.tsx @@ -180,6 +180,7 @@ const TodoPage: React.FC = () => { key={"create"} type={'primary'} onClick={() => { + setCurrentRecordId(''); setSelectVisible(true); }}>发起流程 ) diff --git a/frontend/packages/flow-design/src/components/flow-approval/components/flow-node-history.tsx b/frontend/packages/flow-design/src/components/flow-approval/components/flow-node-history.tsx new file mode 100644 index 00000000..040adb0a --- /dev/null +++ b/frontend/packages/flow-design/src/components/flow-approval/components/flow-node-history.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import {useApprovalContext} from "@/components/flow-approval/hooks/use-approval-context"; +import {ProcessNode} from "@/components/flow-approval/typings"; +import {Timeline, Tag, Empty, Typography} from "antd"; +import {CheckCircleFilled, ClockCircleOutlined, LoadingOutlined, SyncOutlined} from "@ant-design/icons"; + +const {Text} = Typography; + +export interface FlowNodeHistoryAction{ + refresh:()=>void; +} + +interface FlowNodeHistoryProps{ + actionRef?:React.Ref; +} + +// 获取节点状态 +const getNodeStatus = (node: ProcessNode): 'completed' | 'current' | 'pending' => { + if (node.state===-1) { + return 'completed'; + } + // 非历史节点,检查是否有审批人 + if (node.state === 0) { + return 'current'; + } + return 'pending'; +}; + +// 获取状态配置 +const getStatusConfig = (status: 'completed' | 'current' | 'pending') => { + switch (status) { + case 'completed': + return { + color: 'success', + label: '已通过', + icon: + }; + case 'current': + return { + color: 'processing', + label: '待审批', + icon: + }; + case 'pending': + return { + color: 'default', + label: '未执行', + icon: + }; + } +}; + +export const FlowNodeHistory: React.FC = (props) => { + const {context} = useApprovalContext(); + const [processNodes, setProcessNodes] = React.useState([]); + + const triggerProcessNodes = () => { + context.getPresenter().processNodes().then(nodes => { + setProcessNodes(nodes); + }); + } + + React.useEffect(()=>{ + triggerProcessNodes(); + },[]); + + React.useImperativeHandle(props.actionRef,()=>{ + return { + refresh:()=>{ + triggerProcessNodes(); + } + } + },[]); + + return ( + <> + {processNodes.length > 0 ? ( + ({ + icon: getStatusConfig(getNodeStatus(node)).icon, + content: ( +
+
+ {node.nodeName} + + {getStatusConfig(getNodeStatus(node)).label} + +
+ {node.state===-1 && node.operators?.[0] && ( + <> + + 审批人: {node.operators[0].flowOperator.name} + + {node.operators[0].approveTime > 0 && ( + + {new Date(node.operators[0].approveTime).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + })} + + )} + {node.operators[0].advice && ( +
+ + {node.operators[0].advice} + +
+ )} + + )} + {node.state!==-1 && node.operators?.[0] && ( + + 待审批人: {node.operators[0].flowOperator.name} + + )} +
+ ) + }))} /> + ) : ( + + )} + + ) +} \ No newline at end of file diff --git a/frontend/packages/flow-design/src/components/flow-approval/components/form-view-component.tsx b/frontend/packages/flow-design/src/components/flow-approval/components/form-view-component.tsx new file mode 100644 index 00000000..0379b1f1 --- /dev/null +++ b/frontend/packages/flow-design/src/components/flow-approval/components/form-view-component.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import {useApprovalContext} from "@/components/flow-approval/hooks/use-approval-context"; +import {ViewPlugin} from "@/plugins/view"; +import { Form } from "antd"; + +interface FormViewComponentProps{ + onValuesChange?:(values:any)=>void; +} + +export const FormViewComponent: React.FC = (props) => { + const {state, context} = useApprovalContext(); + const ViewComponent = ViewPlugin.getInstance().get(state.flow?.view || 'default'); + // 是否可合并审批 + const mergeable = state.flow?.mergeable || false; + const todos = state.flow?.todos || []; + const viewForms = todos.length>0?todos.map(item => { + return { + instance: Form.useForm()[0], + data: item.data, + } + }):[ + { + instance: Form.useForm()[0], + data: undefined, + } + ] + + React.useEffect(() => { + viewForms.forEach(item => { + const formInstance = item.instance; + const data = item.data; + context.getPresenter().getFormActionContext().addAction({ + save(): any { + return formInstance.getFieldsValue(); + }, + key(): string { + return 'view-form' + } + }); + formInstance.setFieldsValue(data); + }); + }, []); + + if (ViewComponent) { + if (mergeable) { + return ( +
+

合并审批

+
+ ) + } + return ( + <> + {viewForms.map((item, index) => ( + + ))} + + ) + } +} diff --git a/frontend/packages/flow-design/src/components/flow-approval/index.tsx b/frontend/packages/flow-design/src/components/flow-approval/index.tsx index 0b709524..6cc743b2 100644 --- a/frontend/packages/flow-design/src/components/flow-approval/index.tsx +++ b/frontend/packages/flow-design/src/components/flow-approval/index.tsx @@ -6,27 +6,27 @@ import {ApprovalLayout} from "@/components/flow-approval/layout"; interface ApprovalPanelProps { workflowCode?: string; - recordId?:string; + recordId?: string; onClose?: () => void; } export const ApprovalPanel: React.FC = (props) => { - const [content,dispatch] = React.useState(undefined); + const [content, dispatch] = React.useState(undefined); - React.useEffect(()=>{ - const id = props.recordId || props.workflowCode || ''; - detail(id).then(res=>{ - if(res.success){ + React.useEffect(() => { + const id = props.recordId || props.workflowCode || ''; + detail(id).then(res => { + if (res.success) { dispatch(res.data); } }); - },[]); + }, []); return ( - <> +
{content && } - +
) } @@ -41,6 +41,12 @@ export const ApprovalPanelDrawer: React.FC = (props) = void; -} - -const FormViewComponent: React.FC = (props) => { - const {state, context} = useApprovalContext(); - const ViewComponent = ViewPlugin.getInstance().get(state.flow?.view || 'default'); - // 是否可合并审批 - const mergeable = state.flow?.mergeable || false; - const todos = state.flow?.todos || []; - const viewForms = todos.length>0?todos.map(item => { - return { - instance: Form.useForm()[0], - data: item.data, - } - }):[ - { - instance: Form.useForm()[0], - data: undefined, - } - ] - - React.useEffect(() => { - viewForms.forEach(item => { - const formInstance = item.instance; - const data = item.data; - context.getPresenter().getFormActionContext().addAction({ - save(): any { - return formInstance.getFieldsValue(); - }, - key(): string { - return 'view-form' - } - }); - formInstance.setFieldsValue(data); - }); - }, []); - - if (ViewComponent) { - if (mergeable) { - return ( -
-

合并审批

-
- ) - } - return ( - <> - {viewForms.map((item, index) => ( - - ))} - - ) - } -} +const {Sider, Content} = Layout; +const {Title} = Typography; export const Body = () => { - - const {state, context} = useApprovalContext(); + const [collapsed, setCollapsed] = useState(false); + const flowNodeHistoryAction = React.useRef(null); const handleValuesChange = (values:any) => { - context.getPresenter().processNodes().then(nodes => { - console.log('流程节点:', nodes); - }); + flowNodeHistoryAction.current?.refresh(); } return ( - - - 表单详情 - - - - 流转历史 - - + + + 流程表单} + style={{height: '100%', borderRadius: 8}} + styles={{body: {padding: 24}}} + > + + + + + + {!collapsed && ( + + + 流程记录 + + ))} - ) - })} - - + > + 关闭 + + + + ) } \ No newline at end of file diff --git a/frontend/packages/flow-design/src/components/flow-approval/layout/index.tsx b/frontend/packages/flow-design/src/components/flow-approval/layout/index.tsx index 6de74580..0e3049a8 100644 --- a/frontend/packages/flow-design/src/components/flow-approval/layout/index.tsx +++ b/frontend/packages/flow-design/src/components/flow-approval/layout/index.tsx @@ -7,23 +7,29 @@ import {createApprovalContext} from "@/components/flow-approval/hooks/use-approv import {Header} from "@/components/flow-approval/layout/header"; import {Body} from "@/components/flow-approval/layout/body"; - const ApprovalLayoutScope: React.FC = (props) => { const {context} = createApprovalContext(props); return ( -
- +
+
+ +
) } export const ApprovalLayout: React.FC = (props) => { - return ( ) -}; +} diff --git a/frontend/packages/flow-design/src/components/flow-approval/model.ts b/frontend/packages/flow-design/src/components/flow-approval/model.ts index a921c5ff..c61f004e 100644 --- a/frontend/packages/flow-design/src/components/flow-approval/model.ts +++ b/frontend/packages/flow-design/src/components/flow-approval/model.ts @@ -17,7 +17,7 @@ export class FlowApprovalApiImpl implements FlowApprovalApi { processNodes =async (body: Record)=> { const response = await postProcessNodes(body); if(response.success){ - return response.data; + return response.data.list; } } diff --git a/frontend/packages/flow-design/src/components/flow-approval/typings/index.ts b/frontend/packages/flow-design/src/components/flow-approval/typings/index.ts index b8bc758d..12685a5e 100644 --- a/frontend/packages/flow-design/src/components/flow-approval/typings/index.ts +++ b/frontend/packages/flow-design/src/components/flow-approval/typings/index.ts @@ -64,7 +64,7 @@ export interface ProcessNode{ nodeId:string; nodeName:string; nodeType:string; - history:boolean; + state:number; operators:FlowOperatorBody[] } @@ -92,6 +92,13 @@ export interface ApprovalLayoutProps { onClose?:() => void; } +// Layout constants +export const ApprovalLayoutHeight = 64; +export const ApprovalContentPaddingV = 24; +export const ApprovalContentPaddingH = 24; +export const ApprovalSidebarWidth = 250; +export const ApprovalSidebarCollapsedWidth = 48; + export type State = { flow?:FlowContent;