diff --git a/frontend/src/pages/LogStream/components/LogViewer.tsx b/frontend/src/pages/LogStream/components/LogViewer.tsx new file mode 100644 index 00000000..88c7de15 --- /dev/null +++ b/frontend/src/pages/LogStream/components/LogViewer.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ILogData } from '../types'; +import { logStreamService } from '../service'; + +interface LogViewerProps { + processInstanceId: string; +} + +const LogViewer: React.FC = ({ processInstanceId }) => { + const [logs, setLogs] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(); + const scrollRef = useRef(null); + + useEffect(() => { + const handleLog = (log: ILogData) => { + setLogs((prevLogs) => [...prevLogs, log]); + // 自动滚动到底部 + setTimeout(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, 0); + }; + + const handleError = (error: any) => { + setError('日志流连接已断开'); + setIsConnected(false); + }; + + setIsConnected(true); + logStreamService.connect(processInstanceId, handleLog, handleError); + + return () => { + logStreamService.disconnect(); + setIsConnected(false); + }; + }, [processInstanceId]); + + return ( +
+
+

实时日志

+
+ + + {isConnected ? '已连接' : '未连接'} + +
+
+ +
+
+ {logs.map((log, index) => ( +
+ [{new Date(log.timestamp).toLocaleTimeString()}] + {log.data} +
+ ))} + {error &&
{error}
} +
+
+
+ ); +}; + +export default LogViewer; \ No newline at end of file diff --git a/frontend/src/pages/LogStream/index.tsx b/frontend/src/pages/LogStream/index.tsx new file mode 100644 index 00000000..f44c9392 --- /dev/null +++ b/frontend/src/pages/LogStream/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import LogViewer from './components/LogViewer'; + +const LogStreamPage: React.FC = () => { + const { processInstanceId } = useParams<{ processInstanceId: string }>(); + + if (!processInstanceId) { + return
流程实例ID不能为空
; + } + + return ( +
+ +
+ ); +}; + +export default LogStreamPage; \ No newline at end of file diff --git a/frontend/src/pages/LogStream/service.ts b/frontend/src/pages/LogStream/service.ts new file mode 100644 index 00000000..7e36887b --- /dev/null +++ b/frontend/src/pages/LogStream/service.ts @@ -0,0 +1,54 @@ +import { ILogData } from './types'; + +const LOG_STREAM_URL = '/api/v1/workflow/instance/log'; + +export class LogStreamService { + private eventSource: EventSource | null = null; + + connect(processInstanceId: string, onMessage: (log: ILogData) => void, onError?: (error: any) => void) { + if (this.eventSource) { + this.disconnect(); + } + + const token = localStorage.getItem('token'); + const url = new URL(`${window.location.origin}${LOG_STREAM_URL}/dc65ecc5-b6f3-11ef-840c-326a31fc0fe1`); + + if (token) { + url.searchParams.append('token', token); + } + + this.eventSource = new EventSource(url.toString()); + + this.eventSource.addEventListener('STDOUT', (event) => { + onMessage({ + type: 'STDOUT', + data: event.data, + timestamp: new Date().toISOString() + }); + }); + + this.eventSource.addEventListener('STDERR', (event) => { + onMessage({ + type: 'STDERR', + data: event.data, + timestamp: new Date().toISOString() + }); + }); + + this.eventSource.onerror = (error) => { + if (onError) { + onError(error); + } + this.disconnect(); + }; + } + + disconnect() { + if (this.eventSource) { + this.eventSource.close(); + this.eventSource = null; + } + } +} + +export const logStreamService = new LogStreamService(); \ No newline at end of file diff --git a/frontend/src/pages/LogStream/types.ts b/frontend/src/pages/LogStream/types.ts new file mode 100644 index 00000000..8bb1dcba --- /dev/null +++ b/frontend/src/pages/LogStream/types.ts @@ -0,0 +1,18 @@ +import { BaseResponse } from '@/types/base'; + +// 日志事件类型 +export type LogEventType = 'STDOUT' | 'STDERR'; + +// 日志数据接口 +export interface ILogData { + type: LogEventType; + data: string; + timestamp: string; +} + +// 日志流状态 +export interface ILogStreamState { + logs: ILogData[]; + isConnected: boolean; + error?: string; +} \ No newline at end of file diff --git a/frontend/src/pages/System/Menu/service.ts b/frontend/src/pages/System/Menu/service.ts index d0a566df..aa3a72bf 100644 --- a/frontend/src/pages/System/Menu/service.ts +++ b/frontend/src/pages/System/Menu/service.ts @@ -104,6 +104,23 @@ export const getCurrentUserMenus = async () => { enabled: true, createBy: "system", updateBy: "system" + }, + { + id: -24, + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + version: 0, + name: "日志流", + path: "/workflow/log-stream/:processInstanceId", + component: "/pages/LogStream/index", + icon: "file-text", + type: MenuTypeEnum.MENU, + parentId: -2, + sort: 3, + hidden: false, + enabled: true, + createBy: "system", + updateBy: "system" } ] }; diff --git a/frontend/src/pages/Workflow/Definition/index.tsx b/frontend/src/pages/Workflow/Definition/index.tsx index a2e2d19e..8df3d45d 100644 --- a/frontend/src/pages/Workflow/Definition/index.tsx +++ b/frontend/src/pages/Workflow/Definition/index.tsx @@ -146,11 +146,10 @@ const WorkflowDefinitionList: React.FC = () => { // 表格列定义 const columns = [ { - title: '编码', - dataIndex: 'code', - key: 'code', - width: 150, - ellipsis: true, + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, }, { title: '名称', @@ -160,16 +159,16 @@ const WorkflowDefinitionList: React.FC = () => { ellipsis: true, }, { - title: '描述', - dataIndex: 'description', - key: 'description', - width: 200, + title: '标识', + dataIndex: 'key', + key: 'key', + width: 150, ellipsis: true, }, { title: '版本', - dataIndex: 'version', - key: 'version', + dataIndex: 'flowVersion', + key: 'flowVersion', width: 80, }, { @@ -188,13 +187,11 @@ const WorkflowDefinitionList: React.FC = () => { }, }, { - title: '是否启用', - dataIndex: 'enabled', - key: 'enabled', - width: 100, - render: (enabled: boolean) => ( - {enabled ? '已启用' : '已禁用'} - ), + title: '描述', + dataIndex: 'description', + key: 'description', + width: 200, + ellipsis: true, }, { title: '创建时间', @@ -202,6 +199,24 @@ const WorkflowDefinitionList: React.FC = () => { key: 'createTime', width: 180, }, + { + title: '创建人', + dataIndex: 'createBy', + key: 'createBy', + width: 100, + }, + { + title: '更新时间', + dataIndex: 'updateTime', + key: 'updateTime', + width: 180, + }, + { + title: '更新人', + dataIndex: 'updateBy', + key: 'updateBy', + width: 100, + }, { title: '操作', key: 'action', @@ -341,7 +356,7 @@ const WorkflowDefinitionList: React.FC = () => { label="名称" rules={[{required: true, message: '请输入流程名称'}]} > - + diff --git a/frontend/src/pages/Workflow/service.ts b/frontend/src/pages/Workflow/service.ts index 649b707b..2313a203 100644 --- a/frontend/src/pages/Workflow/service.ts +++ b/frontend/src/pages/Workflow/service.ts @@ -9,7 +9,7 @@ import { NodeTypeQuery } from './types'; -const WORKFLOW_DEFINITION_URL = '/api/v1/workflow-definitions'; +const WORKFLOW_DEFINITION_URL = '/api/v1/workflow/definition'; const NODE_TYPE_URL = '/api/v1/node-types'; // 创建工作流定义 diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 06403ac1..dcbb2d3f 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -37,6 +37,7 @@ const WorkflowDefinitionEdit = lazy(() => import('../pages/Workflow/Definition/E const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance')); const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor')); const FlowDesigner = lazy(() => import('../pages/Workflow/Definition/Designer')); +const LogStreamPage = lazy(() => import('../pages/LogStream')); // 创建路由 const router = createBrowserRouter([ @@ -164,6 +165,14 @@ const router = createBrowserRouter([ ) + }, + { + path: 'log-stream/:processInstanceId', + element: ( + }> + + + ) } ] },