diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6d996124..7a5aba08 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3989,7 +3989,6 @@ "resolved": "https://registry.npmmirror.com/less/-/less-4.2.1.tgz", "integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", diff --git a/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less new file mode 100644 index 00000000..525ce3a1 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less @@ -0,0 +1,125 @@ +.nodeTabs { + height: 100%; + + :global { + .ant-tabs { + height: 100%; + + .ant-tabs-nav { + margin-bottom: 12px; + padding: 0 12px; + background: #fafafa; + border-bottom: 1px solid #f0f0f0; + + .ant-tabs-tab { + padding: 12px 0; + font-size: 14px; + transition: all 0.3s; + + &:hover { + color: #1890ff; + } + + .anticon { + margin-right: 8px; + } + } + + .ant-tabs-tab-active { + .ant-tabs-tab-btn { + color: #1890ff; + font-weight: 500; + } + } + + .ant-tabs-ink-bar { + background: #1890ff; + } + } + } + + .ant-tabs-content { + height: calc(100% - 46px); + padding: 0 12px 12px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 3px; + + &:hover { + background: #999; + } + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; + } + } + + .ant-empty { + margin: 32px 0; + color: #999; + } + } +} + +.nodeCard { + margin-bottom: 16px; + cursor: move; + border-radius: 4px; + transition: all 0.3s; + background: #fff; + border: 1px solid #f0f0f0; + padding: 12px; + + &:hover { + border-color: #1890ff; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); + transform: translateY(-1px); + } + + .nodeIcon { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 8px; + color: #fff; + font-size: 20px; + transition: all 0.3s; + + .anticon { + transition: all 0.3s; + } + } + + &:hover .nodeIcon { + transform: scale(1.1); + + .anticon { + transform: scale(1.1); + } + } + + .nodeName { + font-size: 14px; + color: #333; + text-align: center; + line-height: 1.5; + margin: 0; + font-weight: 500; + } + + &:active { + transform: scale(0.98); + } +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less.d.ts b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less.d.ts new file mode 100644 index 00000000..48263c58 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.module.less.d.ts @@ -0,0 +1,13 @@ +declare namespace NodePanelModuleLessNamespace { + export interface INodePanelModuleLess { + nodeTabs: string; + nodeCard: string; + nodeIcon: string; + nodeName: string; + } +} + +declare module '*.less' { + const content: NodePanelModuleLessNamespace.INodePanelModuleLess; + export default content; +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/NodePanel.tsx b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.tsx new file mode 100644 index 00000000..eb8b85f2 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/NodePanel.tsx @@ -0,0 +1,121 @@ +import React, {useEffect, useState} from 'react'; +import {Card, Empty, Spin, Tabs, Tooltip} from 'antd'; +import {NodeCategory, NodeType} from '../../types'; +import {getNodeTypes} from '../../service'; +import * as Icons from '@ant-design/icons'; +import styles from './NodePanel.module.less'; + +interface NodePanelProps { + onNodeDragStart: (nodeType: NodeType) => void; +} + +const NodePanel: React.FC = ({onNodeDragStart}) => { + const [loading, setLoading] = useState(false); + const [nodeTypes, setNodeTypes] = useState([]); + + // 获取节点类型列表 + const fetchNodeTypes = async () => { + setLoading(true); + try { + const response = await getNodeTypes({enabled: true}); + if (response) { + setNodeTypes(response); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchNodeTypes(); + }, []); + + // 按分类分组节点类型 + const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => { + const category = nodeType.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(nodeType); + return acc; + }, {} as Record); + + // 渲染图标 + const renderIcon = (iconName: string) => { + // @ts-ignore + const Icon = Icons[iconName]; + return Icon ? : null; + }; + + // 渲染节点类型卡片 + const renderNodeTypeCard = (nodeType: NodeType) => ( + + { + e.dataTransfer.setData('nodeType', JSON.stringify(nodeType)); + onNodeDragStart(nodeType); + }} + > +
+ {renderIcon(nodeType.icon)} +
+
{nodeType.name}
+
+
+ ); + + const categoryIcons = { + [NodeCategory.TASK]: renderIcon('CodeOutlined'), + [NodeCategory.EVENT]: renderIcon('ThunderboltOutlined'), + [NodeCategory.GATEWAY]: renderIcon('ForkOutlined') + }; + + const categoryLabels = { + [NodeCategory.TASK]: '任务节点', + [NodeCategory.EVENT]: '事件节点', + [NodeCategory.GATEWAY]: '网关节点' + }; + + const tabItems = Object.values(NodeCategory).map(category => ({ + key: category, + label: ( + + {categoryIcons[category]} {categoryLabels[category]} + + ({groupedNodeTypes[category]?.length || 0}) + + + ), + children: groupedNodeTypes[category]?.map(renderNodeTypeCard) || ( + + ) + })); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + + ); +}; + +export default NodePanel; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.module.less b/frontend/src/pages/Workflow/Definition/Designer/index.module.less new file mode 100644 index 00000000..3cc6f143 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/index.module.less @@ -0,0 +1,30 @@ +.container { + height: calc(100vh - 180px); + border: 1px solid #f0f0f0; + border-radius: 2px; + background: #fff; + + .sider { + border-right: 1px solid #f0f0f0; + background: #fff; + + .nodePanel { + padding: 16px; + height: 100%; + overflow-y: auto; + } + } + + .content { + position: relative; + background: #fafafa; + + .graph { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts b/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts new file mode 100644 index 00000000..ae182ace --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts @@ -0,0 +1,14 @@ +declare namespace IndexModuleLessNamespace { + export interface IIndexModuleLess { + container: string; + sider: string; + nodePanel: string; + content: string; + graph: string; + } +} + +declare module '*.less' { + const content: IndexModuleLessNamespace.IIndexModuleLess; + export default content; +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx new file mode 100644 index 00000000..3d885aca --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -0,0 +1,235 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; +import {Button, Card, Layout, message, Space, Spin} from 'antd'; +import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; +import {getDefinition, updateDefinition} from '../../service'; +import {WorkflowDefinition, WorkflowStatus, NodeType} from '../../types'; +import {Graph} from '@antv/x6'; +import '@antv/x6-react-shape'; +import styles from './index.module.less'; +import NodePanel from './NodePanel'; + +const {Sider, Content} = Layout; + +const FlowDesigner: React.FC = () => { + const navigate = useNavigate(); + const {id} = useParams<{ id: string }>(); + const [loading, setLoading] = useState(false); + const [detail, setDetail] = useState(); + const containerRef = useRef(null); + const graphRef = useRef(); + + // 初始化图形 + const initGraph = () => { + if (!containerRef.current) return; + + // 创建画布 + const graph = new Graph({ + container: containerRef.current, + width: 800, + height: 600, + grid: { + size: 10, + visible: true, + type: 'dot', + args: { + color: '#ccc', + thickness: 1, + }, + }, + connecting: { + router: 'manhattan', + connector: { + name: 'rounded', + args: { + radius: 8, + }, + }, + anchor: 'center', + connectionPoint: 'anchor', + allowBlank: false, + snap: { + radius: 20, + }, + createEdge() { + return this.createEdge({ + attrs: { + line: { + stroke: '#5F95FF', + strokeWidth: 1, + targetMarker: { + name: 'classic', + size: 8, + }, + }, + }, + router: { + name: 'manhattan', + }, + connector: { + name: 'rounded', + args: { + radius: 8, + }, + }, + }); + }, + }, + highlighting: { + magnetAvailable: { + name: 'stroke', + args: { + padding: 4, + attrs: { + strokeWidth: 4, + stroke: '#52c41a', + }, + }, + }, + }, + mousewheel: { + enabled: true, + modifiers: ['ctrl', 'meta'], + minScale: 0.5, + maxScale: 2, + }, + interacting: { + nodeMovable: true, + edgeMovable: true, + edgeLabelMovable: true, + arrowheadMovable: true, + vertexMovable: true, + vertexAddable: true, + vertexDeletable: true, + }, + scroller: { + enabled: true, + pannable: true, + pageVisible: false, + pageBreak: false, + }, + history: { + enabled: true, + }, + clipboard: { + enabled: true, + }, + keyboard: { + enabled: true, + }, + selecting: { + enabled: true, + multiple: true, + rubberband: true, + movable: true, + showNodeSelectionBox: true, + }, + }); + + graphRef.current = graph; + + // 加载流程图数据 + if (detail) { + try { + const graphData = JSON.parse(detail.graphDefinition); + graph.fromJSON(graphData); + } catch (error) { + message.error('加载流程图数据失败'); + } + } + }; + + // 获取详情 + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + try { + const response = await getDefinition(parseInt(id)); + if (response) { + setDetail(response); + } + } finally { + setLoading(false); + } + }; + + // 处理保存 + const handleSave = async () => { + if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return; + + try { + const graphData = graphRef.current.toJSON(); + const data = { + ...detail, + graphDefinition: JSON.stringify(graphData) + }; + await updateDefinition(parseInt(id), data); + message.success('保存成功'); + } catch (error) { + // 错误已在请求拦截器中处理 + } + }; + + // 处理返回 + const handleBack = () => { + navigate('/workflow/definition'); + }; + + // 首次加载 + useEffect(() => { + fetchDetail(); + }, [id]); + + // 初始化图形 + useEffect(() => { + if (detail && containerRef.current) { + initGraph(); + } + }, [detail, containerRef.current]); + + // 处理节点拖拽开始 + const handleNodeDragStart = (nodeType: NodeType) => { + // TODO: 实现节点拖拽创建 + console.log('Node drag start:', nodeType); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + + + + + } + > + + + + + +
+ + + + ); +}; + +export default FlowDesigner; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Edit/index.tsx b/frontend/src/pages/Workflow/Definition/Edit/index.tsx new file mode 100644 index 00000000..ccd90774 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Edit/index.tsx @@ -0,0 +1,159 @@ +import React, {useEffect, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; +import {Button, Card, Form, Input, message, Space, Spin} from 'antd'; +import {getDefinition, updateDefinition} from '../../service'; +import {UpdateWorkflowDefinitionRequest, WorkflowDefinition, WorkflowStatus} from '../../types'; +import {ArrowLeftOutlined} from '@ant-design/icons'; +import {WorkflowConfigUtils} from '../../../Workflow/utils'; + +const WorkflowDefinitionEdit: React.FC = () => { + const navigate = useNavigate(); + const {id} = useParams<{ id: string }>(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [detail, setDetail] = useState(); + + // 获取详情 + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + try { + const response = await getDefinition(parseInt(id)); + if (response) { + setDetail(response); + // 设置表单初始值 + form.setFieldsValue({ + code: response.code, + name: response.name, + description: response.description, + }); + } + } finally { + setLoading(false); + } + }; + + // 首次加载 + useEffect(() => { + fetchDetail(); + }, [id]); + + // 处理保存 + const handleSave = async (values: any) => { + if (!id || !detail) return; + try { + // 保留原有配置和状态 + const data: UpdateWorkflowDefinitionRequest = { + ...values, + status: detail.status, + version: detail.version, + enabled: detail.enabled, + nodeConfig: JSON.stringify(WorkflowConfigUtils.parseNodeConfig(detail.nodeConfig)), + transitionConfig: JSON.stringify(WorkflowConfigUtils.parseTransitionConfig(detail.transitionConfig)), + formDefinition: JSON.stringify(WorkflowConfigUtils.parseFormDefinition(detail.formDefinition)), + graphDefinition: JSON.stringify(WorkflowConfigUtils.parseGraphDefinition(detail.graphDefinition)) + }; + await updateDefinition(parseInt(id), data); + message.success('保存成功'); + navigate('/workflow/definition'); + } catch (error) { + // 错误已在请求拦截器中处理 + } + }; + + // 处理取消 + const handleCancel = () => { + navigate('/workflow/definition'); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + } onClick={handleCancel}> + 返回 + + } + > +
+ {detail?.status !== WorkflowStatus.DRAFT && ( +
+ 注意:非草稿状态的流程定义不能修改! +
+ )} + +
+ + + + + + + + + + + + + + + + +
+
+
+ ); +}; + +export default WorkflowDefinitionEdit; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/index.tsx b/frontend/src/pages/Workflow/Definition/index.tsx new file mode 100644 index 00000000..44f3c4e9 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/index.tsx @@ -0,0 +1,366 @@ +import React, {useEffect, useState} from 'react'; +import {Button, Card, Form, Input, message, Modal, Select, Space, Table, Tag} from 'antd'; +import {useNavigate} from 'react-router-dom'; +import { + createDefinition, + deleteDefinition, + disableDefinition, + enableDefinition, + getDefinitions, + publishDefinition +} from '../service'; +import { + CreateWorkflowDefinitionRequest, + WorkflowDefinition, + WorkflowStatus, + WorkflowDefinitionBase +} from '../types'; +import {DeleteOutlined, EditOutlined, PlusOutlined} from '@ant-design/icons'; + +const {confirm} = Modal; + +const WorkflowDefinitionList: React.FC = () => { + const navigate = useNavigate(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [current, setCurrent] = useState(1); + const [size, setSize] = useState(10); + const [createModalVisible, setCreateModalVisible] = useState(false); + + // 获取列表数据 + const fetchList = async (page = current, pageSize = size) => { + setLoading(true); + try { + const params = { + page: page - 1, // 后端页码从0开始 + size: pageSize, + ...form.getFieldsValue() + }; + const response = await getDefinitions(params); + if (response?.content) { + setList(response.content); + setTotal(response.totalElements); + } + } finally { + setLoading(false); + } + }; + + // 首次加载 + useEffect(() => { + fetchList(); + }, []); + + // 处理表格变化 + const handleTableChange = (pagination: any) => { + setCurrent(pagination.current); + setSize(pagination.pageSize); + fetchList(pagination.current, pagination.pageSize); + }; + + // 处理搜索 + const handleSearch = () => { + setCurrent(1); + fetchList(1); + }; + + // 处理重置 + const handleReset = () => { + form.resetFields(); + setCurrent(1); + fetchList(1); + }; + + // 处理创建 + const handleCreate = async (values: WorkflowDefinitionBase) => { + try { + // 设置系统字段的默认值 + const data = { + ...values, + status: WorkflowStatus.DRAFT, + version: 1, + enabled: true, + nodeConfig: JSON.stringify({ + nodes: [] + }), + transitionConfig: JSON.stringify({ + transitions: [] + }), + formDefinition: JSON.stringify({}), + graphDefinition: JSON.stringify({}) + }; + await createDefinition(data); + message.success('创建成功'); + setCreateModalVisible(false); + fetchList(); + } catch (error) { + // 错误已在请求拦截器中处理 + } + }; + + // 处理删除 + const handleDelete = (record: WorkflowDefinition) => { + confirm({ + title: '确认删除', + content: `确定要删除工作流"${record.name}"吗?`, + onOk: async () => { + try { + await deleteDefinition(record.id); + message.success('删除成功'); + fetchList(); + } catch (error) { + // 错误已在请求拦截器中处理 + } + } + }); + }; + + // 处理发布 + const handlePublish = async (record: WorkflowDefinition) => { + try { + await publishDefinition(record.id); + message.success('发布成功'); + fetchList(); + } catch (error) { + // 错误已在请求拦截器中处理 + } + }; + + // 处理启用/禁用 + const handleToggleEnable = async (record: WorkflowDefinition) => { + try { + if (record.enabled) { + await disableDefinition(record.id); + message.success('禁用成功'); + } else { + await enableDefinition(record.id); + message.success('启用成功'); + } + fetchList(); + } catch (error) { + // 错误已在请求拦截器中处理 + } + }; + + // 表格列定义 + const columns = [ + { + title: '编码', + dataIndex: 'code', + key: 'code', + width: 150, + ellipsis: true, + }, + { + title: '名称', + dataIndex: 'name', + key: 'name', + width: 150, + ellipsis: true, + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + width: 200, + ellipsis: true, + }, + { + title: '版本', + dataIndex: 'version', + key: 'version', + width: 80, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (status: WorkflowStatus) => { + const statusMap = { + [WorkflowStatus.DRAFT]: {color: 'default', text: '草稿'}, + [WorkflowStatus.PUBLISHED]: {color: 'success', text: '已发布'}, + [WorkflowStatus.DISABLED]: {color: 'error', text: '已禁用'}, + }; + const {color, text} = statusMap[status]; + return {text}; + }, + }, + { + title: '是否启用', + dataIndex: 'enabled', + key: 'enabled', + width: 100, + render: (enabled: boolean) => ( + {enabled ? '已启用' : '已禁用'} + ), + }, + { + title: '创建时间', + dataIndex: 'createTime', + key: 'createTime', + width: 180, + }, + { + title: '操作', + key: 'action', + fixed: 'right' as const, + width: 380, + render: (_: any, record: WorkflowDefinition) => ( + + + + {record.status === WorkflowStatus.DRAFT && ( + + )} + + + + ), + }, + ]; + + return ( + + {/* 搜索表单 */} +
+ + + + + + + + + + + + +
+ + {/* 工具栏 */} +
+ +
+ + {/* 列表 */} + + + {/* 创建表单弹窗 */} + setCreateModalVisible(false)} + footer={null} + > +
+ + + + + + + + + + + + + + + + +
+ + ); +}; + +export default WorkflowDefinitionList; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Instance/index.tsx b/frontend/src/pages/Workflow/Instance/index.tsx new file mode 100644 index 00000000..114424c3 --- /dev/null +++ b/frontend/src/pages/Workflow/Instance/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {Card} from 'antd'; + +const WorkflowInstance: React.FC = () => { + return ( + +
流程实例列表页面
+
+ ); +}; + +export default WorkflowInstance; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Monitor/index.tsx b/frontend/src/pages/Workflow/Monitor/index.tsx new file mode 100644 index 00000000..7e8ec2da --- /dev/null +++ b/frontend/src/pages/Workflow/Monitor/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {Card} from 'antd'; + +const WorkflowMonitor: React.FC = () => { + return ( + +
流程监控页面
+
+ ); +}; + +export default WorkflowMonitor; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/service.ts b/frontend/src/pages/Workflow/service.ts new file mode 100644 index 00000000..649b707b --- /dev/null +++ b/frontend/src/pages/Workflow/service.ts @@ -0,0 +1,56 @@ +import request from '../../utils/request'; +import { + CreateWorkflowDefinitionRequest, + UpdateWorkflowDefinitionRequest, + WorkflowDefinition, + WorkflowDefinitionPage, + WorkflowDefinitionQuery, + NodeType, + NodeTypeQuery +} from './types'; + +const WORKFLOW_DEFINITION_URL = '/api/v1/workflow-definitions'; +const NODE_TYPE_URL = '/api/v1/node-types'; + +// 创建工作流定义 +export const createDefinition = (data: CreateWorkflowDefinitionRequest) => + request.post(WORKFLOW_DEFINITION_URL, data); + +// 更新工作流定义 +export const updateDefinition = (id: number, data: UpdateWorkflowDefinitionRequest) => + request.put(`${WORKFLOW_DEFINITION_URL}/${id}`, data); + +// 获取工作流定义列表 +export const getDefinitions = (params?: WorkflowDefinitionQuery) => + request.get(`${WORKFLOW_DEFINITION_URL}/page`, { params }); + +// 获取工作流定义详情 +export const getDefinition = (id: number) => + request.get(`${WORKFLOW_DEFINITION_URL}/${id}`); + +// 删除工作流定义 +export const deleteDefinition = (id: number) => + request.delete(`${WORKFLOW_DEFINITION_URL}/${id}`); + +// 发布工作流定义 +export const publishDefinition = (id: number) => + request.post(`${WORKFLOW_DEFINITION_URL}/${id}/publish`); + +// 禁用工作流定义 +export const disableDefinition = (id: number) => + request.post(`${WORKFLOW_DEFINITION_URL}/${id}/disable`); + +// 启用工作流定义 +export const enableDefinition = (id: number) => + request.post(`${WORKFLOW_DEFINITION_URL}/${id}/enable`); + +// 创建新版本 +export const createVersion = (id: number) => + request.post(`${WORKFLOW_DEFINITION_URL}/${id}/versions`); + +// 节点类型相关接口 +export const getNodeTypes = (params?: NodeTypeQuery) => + request.get(NODE_TYPE_URL, { params }); + +export const getNodeTypeExecutors = (code: string) => + request.get(`${NODE_TYPE_URL}/${code}/executors`); \ No newline at end of file diff --git a/frontend/src/pages/Workflow/types.ts b/frontend/src/pages/Workflow/types.ts new file mode 100644 index 00000000..c8809f2d --- /dev/null +++ b/frontend/src/pages/Workflow/types.ts @@ -0,0 +1,151 @@ +import {Page} from '../../types/base'; + +// 工作流状态枚举 +export enum WorkflowStatus { + DRAFT = 'DRAFT', // 草稿 + PUBLISHED = 'PUBLISHED', // 已发布 + DISABLED = 'DISABLED' // 已禁用 +} + +// 节点类型分类枚举 +export enum NodeCategory { + TASK = 'TASK', // 任务节点 + EVENT = 'EVENT', // 事件节点 + GATEWAY = 'GATEWAY' // 网关节点 +} + +// 节点类型查询参数 +export interface NodeTypeQuery { + enabled?: boolean; + category?: NodeCategory; +} + +// 节点执行器 +export interface NodeExecutor { + code: string; + name: string; + description: string; + configSchema: string; // JSON Schema + defaultConfig?: string; +} + +// 节点类型 +export interface NodeType { + id: number; + code: string; + name: string; + category: NodeCategory; + description: string; + enabled: boolean; + icon: string; + color: string; + executors: NodeExecutor[]; + configSchema: string; // JSON Schema + defaultConfig: string; + createTime: string; + updateTime: string; +} + +// 工作流定义查询参数 +export interface WorkflowDefinitionQuery { + page?: number; + size?: number; + sort?: string[]; + keyword?: string; + status?: WorkflowStatus; + enabled?: boolean; +} + +// 节点配置 +export interface NodeConfig { + nodes: { + id: string; + type: string; + name: string; + config: Record; + }[]; +} + +// 流转配置 +export interface TransitionConfig { + transitions: { + from: string; + to: string; + condition: string; + }[]; +} + +// 工作流定义基本信息 +export interface WorkflowDefinitionBase { + code: string; + name: string; + description?: string; +} + +// 工作流定义 +export interface WorkflowDefinition extends WorkflowDefinitionBase { + id: number; + version: number; + status: WorkflowStatus; + enabled: boolean; + nodeConfig: string; // JSON string of NodeConfig + transitionConfig: string; // JSON string of TransitionConfig + formDefinition: string; // JSON string + graphDefinition: string; // JSON string + createTime: string; + updateTime: string; +} + +// 创建工作流定义请求 +export interface CreateWorkflowDefinitionRequest extends WorkflowDefinitionBase { + status: WorkflowStatus; + version: number; + enabled: boolean; + nodeConfig: string; // JSON string of NodeConfig + transitionConfig: string; // JSON string of TransitionConfig + formDefinition: string; // JSON string + graphDefinition: string; // JSON string +} + +// 更新工作流定义请求 +export interface UpdateWorkflowDefinitionRequest extends WorkflowDefinitionBase { + nodeConfig?: string; // JSON string of NodeConfig + transitionConfig?: string; // JSON string of TransitionConfig + formDefinition?: string; // JSON string + graphDefinition?: string; // JSON string +} + +// 分页响应 +export type WorkflowDefinitionPage = Page; + +// 工作流配置的解析和序列化工具 +export const WorkflowConfigUtils = { + parseNodeConfig: (config: string): NodeConfig => { + try { + return JSON.parse(config); + } catch { + return { nodes: [] }; + } + }, + parseTransitionConfig: (config: string): TransitionConfig => { + try { + return JSON.parse(config); + } catch { + return { transitions: [] }; + } + }, + parseFormDefinition: (config: string): Record => { + try { + return JSON.parse(config); + } catch { + return {}; + } + }, + parseGraphDefinition: (config: string): Record => { + try { + return JSON.parse(config); + } catch { + return {}; + } + } +}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/utils.ts b/frontend/src/pages/Workflow/utils.ts new file mode 100644 index 00000000..dfb56713 --- /dev/null +++ b/frontend/src/pages/Workflow/utils.ts @@ -0,0 +1,33 @@ +import {NodeConfig, TransitionConfig} from './types'; + +// 工作流配置的解析和序列化工具 +export const WorkflowConfigUtils = { + parseNodeConfig: (config: string): NodeConfig => { + try { + return JSON.parse(config); + } catch { + return {nodes: []}; + } + }, + parseTransitionConfig: (config: string): TransitionConfig => { + try { + return JSON.parse(config); + } catch { + return {transitions: []}; + } + }, + parseFormDefinition: (config: string): Record => { + try { + return JSON.parse(config); + } catch { + return {}; + } + }, + parseGraphDefinition: (config: string): Record => { + try { + return JSON.parse(config); + } catch { + return {}; + } + } +}; \ No newline at end of file diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 5766dee3..06403ac1 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -32,11 +32,11 @@ const Menu = lazy(() => import('../pages/System/Menu')); const Department = lazy(() => import('../pages/System/Department')); const External = lazy(() => import('../pages/System/External')); const X6Test = lazy(() => import('../pages/X6Test')); -// const WorkflowDefinition = lazy(() => import('../pages/Workflow/Definition')); -// const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/Edit')); -// const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance')); -// const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor')); -// const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer')); +const WorkflowDefinition = lazy(() => import('../pages/Workflow/Definition')); +const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/Edit')); +const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance')); +const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor')); +const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer')); // 创建路由 const router = createBrowserRouter([ @@ -117,56 +117,56 @@ const router = createBrowserRouter([ ) }, - // { - // path: 'workflow', - // children: [ - // { - // path: 'definition', - // children: [ - // { - // path: '', - // element: ( - // }> - // - // - // ) - // }, - // { - // path: 'edit/:id?', - // element: ( - // }> - // - // - // ) - // }, - // { - // path: 'designer/:id?', - // element: ( - // }> - // - // - // ) - // } - // ] - // }, - // { - // path: 'instance', - // element: ( - // }> - // - // - // ) - // }, - // { - // path: 'monitor', - // element: ( - // }> - // - // - // ) - // } - // ] - // }, + { + path: 'workflow', + children: [ + { + path: 'definition', + children: [ + { + path: '', + element: ( + }> + + + ) + }, + { + path: 'edit/:id?', + element: ( + }> + + + ) + }, + { + path: 'designer/:id?', + element: ( + }> + + + ) + } + ] + }, + { + path: 'instance', + element: ( + }> + + + ) + }, + { + path: 'monitor', + element: ( + }> + + + ) + } + ] + }, { path: '*', element: diff --git a/frontend/src/types/base.ts b/frontend/src/types/base.ts index cc05cfe2..2c62aea1 100644 --- a/frontend/src/types/base.ts +++ b/frontend/src/types/base.ts @@ -13,4 +13,22 @@ export interface BaseQuery { enabled?: boolean; startTime?: string; endTime?: string; +} + +export interface Page { + content: T[]; + totalElements: number; + totalPages: number; + size: number; + number: number; + empty: boolean; + first: boolean; + last: boolean; +} + +export interface Response { + code: number; + message: string; + data: T; + success: boolean; } \ No newline at end of file