feat(flowEditor): 添加只读模式并实现实例画布查看功能

master
钟良源 1 week ago
parent 9719597be2
commit 3979b107ce

@ -0,0 +1,310 @@
.instanceCanvas {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-fill-2);
overflow: hidden;
}
.header {
height: 50px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border-2);
display: flex;
align-items: center;
padding: 0 20px;
flex-shrink: 0;
gap: 12px;
}
.backButton {
flex-shrink: 0;
}
.title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
flex: 1;
}
.mainContent {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.canvasArea {
flex: 1;
overflow: hidden;
position: relative;
background-color: var(--color-bg-1);
}
.logBarContainer {
position: relative;
width: 100%;
flex-shrink: 0;
}
.logBar {
width: 100%;
height: 100%;
background-color: var(--color-bg-2);
border-top: 1px solid var(--color-border-2);
overflow: hidden;
display: flex;
flex-direction: column;
}
.logTabs {
height: 100%;
display: flex;
flex-direction: column;
:global {
.arco-tabs-content {
flex: 1;
overflow: hidden;
padding: 0;
}
.arco-tabs-content-list {
height: 100%;
}
.arco-tabs-pane {
height: 100%;
overflow: auto;
}
.arco-tabs-nav {
padding: 0 16px;
margin: 0;
}
}
}
.collapsedTabBar {
width: 100%;
background-color: var(--color-bg-2);
border-top: 1px solid var(--color-border-2);
}
.collapsedTabs {
:global {
.arco-tabs-content {
display: none;
}
.arco-tabs-nav {
padding: 0 16px;
margin: 0;
}
}
}
.logContent {
padding: 12px 16px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--color-text-2);
background-color: var(--color-bg-1);
height: 100%;
overflow-y: auto;
}
.logSection {
margin-bottom: 16px;
padding: 12px;
background-color: var(--color-fill-2);
border-radius: 4px;
border-left: 3px solid var(--color-primary-6);
}
.logHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--color-border-2);
}
.logNodeName {
font-weight: 600;
color: var(--color-text-1);
font-size: 14px;
}
.logDuration {
color: var(--color-text-3);
font-size: 12px;
}
.logState {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.success {
background-color: var(--color-success-light-1);
color: var(--color-success-6);
}
&.failed {
background-color: var(--color-danger-light-1);
color: var(--color-danger-6);
}
&.running {
background-color: var(--color-primary-light-1);
color: var(--color-primary-6);
}
}
.logMessage {
color: var(--color-text-2);
font-size: 12px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-all;
div {
margin: 2px 0;
}
}
.logItem {
margin-bottom: 4px;
word-break: break-all;
}
.logTimestamp {
color: var(--color-text-3);
margin-right: 8px;
}
.dataContent {
padding: 12px 16px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--color-text-1);
background-color: var(--color-bg-1);
height: 100%;
overflow-y: auto;
}
.dataSection {
margin-bottom: 20px;
padding: 16px;
background-color: var(--color-fill-2);
border-radius: 6px;
border: 1px solid var(--color-border-2);
}
.dataHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 2px solid var(--color-border-3);
}
.dataNodeName {
font-weight: 600;
color: var(--color-text-1);
font-size: 15px;
}
.dataDuration {
color: var(--color-text-3);
font-size: 12px;
}
.dataState {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.success {
background-color: var(--color-success-light-1);
color: var(--color-success-6);
}
&.failed {
background-color: var(--color-danger-light-1);
color: var(--color-danger-6);
}
&.running {
background-color: var(--color-primary-light-1);
color: var(--color-primary-6);
}
}
.dataBlock {
margin-top: 12px;
background-color: var(--color-bg-1);
border-radius: 4px;
padding: 12px;
border: 1px solid var(--color-border-1);
}
.dataBlockTitle {
font-weight: 600;
color: var(--color-text-1);
margin-bottom: 8px;
font-size: 13px;
}
.dataBlockContent {
margin: 0;
padding: 8px;
background-color: var(--color-fill-1);
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-2);
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
}
.emptyLog {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-3);
font-size: 14px;
background-color: var(--color-bg-1);
}
.loadingContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: var(--color-bg-1);
}
.emptyContainer {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-3);
font-size: 16px;
background-color: var(--color-bg-1);
}

@ -0,0 +1,377 @@
import React, { useState, useRef, useEffect } from 'react';
import { ResizeBox, Tabs, Message, Spin, Table, TableColumnProps, Button } from '@arco-design/web-react';
import { IconCheckCircleFill, IconLoading, IconCloseCircleFill, IconLeft } from '@arco-design/web-react/icon';
import FlowEditor from '@/pages/flowEditor/index';
import { getInstanceDefinition } from '@/api/appIns';
import { formatSeconds } from '@/utils/common';
import dayjs from 'dayjs';
import styles from './index.module.less';
const TabPane = Tabs.TabPane;
interface LogMessage {
id: number;
type: string;
message: string;
timestamp: string;
}
interface RuntimeData {
[key: string]: any;
}
interface InstanceCanvasProps {
instanceData: any; // 实例数据
title?: string; // 实例标题
onBack?: () => void; // 返回回调
}
const InstanceCanvas: React.FC<InstanceCanvasProps> = ({ instanceData, title, onBack }) => {
const [activeTab, setActiveTab] = useState('1');
const [logBarExpanded, setLogBarExpanded] = useState(true);
const [logContainerHeight, setLogContainerHeight] = useState('250px');
const [runtimeLogs, setRuntimeLogs] = useState<LogMessage[]>([]);
const [runtimeData, setRuntimeData] = useState<RuntimeData>({});
const [flowData, setFlowData] = useState<any>(null); // 流程数据
const [loading, setLoading] = useState(false); // 加载状态
const resizeBoxRef = useRef<HTMLDivElement>(null);
// 获取实例定义数据
const fetchInstanceDefinition = async () => {
if (!instanceData?.id) return;
setLoading(true);
try {
const res: any = await getInstanceDefinition(instanceData.id);
if (res.code === 200 && res.data) {
const components = res.data.main?.flow?.components;
// 包装数据格式,使其符合 FlowEditor 的期望
// FlowEditor 使用 useDefault=true 时,会调用 projectFlowHandle
// projectFlowHandle 期望 initialData?.main?.components 或 initialData?.components
if (components && typeof components === 'object') {
// 转换连接格式:将单 $ 转换为 $$
const convertedComponents = Object.entries(components).reduce((acc, [nodeId, nodeConfig]: [string, any]) => {
const converted = { ...nodeConfig };
// 转换 apiDownstream
if (converted.apiDownstream && Array.isArray(converted.apiDownstream)) {
converted.apiDownstream = converted.apiDownstream.map((targetArray: string[]) => {
if (Array.isArray(targetArray)) {
return targetArray.map((target: string) => {
if (typeof target === 'string' && target.includes('$') && !target.includes('$$')) {
return target.replace('$', '$$');
}
return target;
});
}
return targetArray;
});
}
// 转换 apiUpstream
if (converted.apiUpstream && Array.isArray(converted.apiUpstream)) {
converted.apiUpstream = converted.apiUpstream.map((sourceArray: string[]) => {
if (Array.isArray(sourceArray)) {
return sourceArray.map((source: string) => {
if (typeof source === 'string' && source.includes('$') && !source.includes('$$')) {
return source.replace('$', '$$');
}
return source;
});
}
return sourceArray;
});
}
acc[nodeId] = converted;
return acc;
}, {} as any);
setFlowData({
components: convertedComponents,
main: {
components: convertedComponents
}
});
}
// 处理运行日志
if (res.data.main?.nodeLogs && Array.isArray(res.data.main.nodeLogs) && res.data.main.nodeLogs.length > 0) {
const logs = res.data.main.nodeLogs.map((log: any, index: number) => ({
id: index,
type: 'runtime',
message: log.runLog || '',
timestamp: new Date(log.startTime).toLocaleString('zh-CN'),
nodeId: log.nodeId,
nodeName: log.name,
duration: log.duration,
state: log.state
}));
setRuntimeLogs(logs);
}
// 处理运行数据input/output
if (res.data.main?.nodeLogs && Array.isArray(res.data.main.nodeLogs) && res.data.main.nodeLogs.length > 0) {
const dataMap: RuntimeData = {};
res.data.main.nodeLogs.forEach((log: any) => {
if (log.input || log.output) {
dataMap[log.nodeId] = {
nodeName: log.name,
input: log.input || {},
output: log.output || {},
duration: log.duration,
startTime: log.startTime,
endTime: log.endTime,
state: log.state === 1 ? '成功' : log.state === -1 ? '失败' : '运行中'
};
}
});
setRuntimeData(dataMap);
}
}
else {
Message.error(res.msg || '获取实例数据失败');
}
} catch (error) {
console.error('获取实例定义失败:', error);
Message.error('获取实例数据失败');
} finally {
setLoading(false);
}
};
// 当 instanceData 变化时,重新获取数据
useEffect(() => {
fetchInstanceDefinition();
}, [instanceData?.id]);
// 处理 Tab 点击事件
const handleTabClick = (key: string) => {
if (key === activeTab) {
setLogBarExpanded(!logBarExpanded);
}
else {
setActiveTab(key);
setLogBarExpanded(true);
}
};
// 当展开/收起状态改变时,更新元素样式
useEffect(() => {
if (resizeBoxRef.current) {
resizeBoxRef.current.style.height = logBarExpanded ? logContainerHeight : '0px';
}
}, [logBarExpanded, logContainerHeight]);
// 处理 ResizeBox 手动调整大小事件
const handleResize = (e: MouseEvent, size: { width: number; height: number }) => {
if (size.height <= 40) {
setLogBarExpanded(false);
}
else {
setLogBarExpanded(true);
setLogContainerHeight(`${size.height}px`);
}
};
// 渲染运行日志内容
const renderRuntimeLogs = () => {
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
{runtimeLogs.length === 0 ? (
<p></p>
) : (
runtimeLogs.map((log: any) => (
<div key={log.id} style={{ marginBottom: '8px', padding: '4px', borderBottom: '1px solid #eee' }}>
<div style={{ fontSize: '12px', color: '#999' }}>
{log.timestamp}
</div>
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{log.message}
</div>
</div>
))
)}
</div>
);
};
// 定义运行数据表格列
const dataColumns: TableColumnProps[] = [
{
title: '节点',
dataIndex: 'nodeName',
width: 120
},
{
title: '时间',
width: 160,
render: (_: any, record: any) => (
<>
<span> {dayjs(record.startTime).format('MM-DD HH:mm:ss')}</span>
<br />
{record.endTime > 0 && <span> {dayjs(record.endTime).format('MM-DD HH:mm:ss')}</span>}
</>
)
},
{
title: '耗时',
dataIndex: 'duration',
width: 80,
render: (_: any, record: any) => (
record.duration > 1000 ?
<span>{formatSeconds((record.duration / 1000).toString())}</span> :
<span>{record.duration}ms</span>
)
},
{
title: '状态',
dataIndex: 'state',
width: 60,
align: 'center',
render: (_: any, record: any) => (
<>
{record.state === 1 && <IconCheckCircleFill fontSize={24} style={{ color: 'rgb(var(--green-6))' }} />}
{record.state === 0 && <IconLoading fontSize={24} style={{ color: 'rgb(var(--arcoblue-4))' }} />}
{record.state === -1 && <IconCloseCircleFill fontSize={24} style={{ color: 'rgb(var(--red-6))' }} />}
</>
)
},
{
title: '输入参数',
dataIndex: 'input',
width: 200,
render: (_: any, record: any) => (
<div style={{ wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{JSON.stringify(record.input)}</div>
)
},
{
title: '输出参数',
dataIndex: 'output',
width: 200,
render: (_: any, record: any) => (
<div style={{ wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{JSON.stringify(record.output)}</div>
)
}
];
// 渲染运行数据内容
const renderRuntimeData = () => {
// 将 runtimeData 对象转换为表格数据数组
const tableData = Object.entries(runtimeData).map(([nodeId, data]: [string, any]) => ({
key: nodeId,
nodeId,
nodeName: data.nodeName,
startTime: data.startTime || 0,
endTime: data.endTime || 0,
duration: data.duration,
state: data.state === '成功' ? 1 : data.state === '失败' ? -1 : 0,
input: data.input,
output: data.output
}));
return (
<div style={{ padding: '10px', height: 'calc(100% - 40px)', overflowY: 'auto' }}>
{tableData.length === 0 ? (
<p></p>
) : (
<Table
columns={dataColumns}
data={tableData}
tableLayoutFixed
scroll={{ x: 'max-content' }}
pagination={false}
/>
)}
</div>
);
};
return (
<div className={styles.instanceCanvas}>
{/* 顶部标题栏 */}
{title && (
<div className={styles.header}>
{onBack && (
<Button
type="text"
icon={<IconLeft />}
onClick={onBack}
className={styles.backButton}
>
</Button>
)}
<h2 className={styles.title}>{title}</h2>
</div>
)}
{/* 主内容区域 */}
<div className={styles.mainContent}>
{/* 流程画布 */}
<div className={styles.canvasArea}>
{loading ? (
<div className={styles.loadingContainer}>
<Spin size={40} />
<span style={{ marginTop: '16px', color: 'var(--color-text-3)' }}>...</span>
</div>
) : flowData ? (
<FlowEditor initialData={flowData} useDefault={true} readOnly={true} />
) : (
<div className={styles.emptyContainer}>
<span></span>
</div>
)}
</div>
{/* 底部日志栏 */}
<div className={styles.logBarContainer}>
<ResizeBox
directions={['top']}
onMoving={handleResize}
style={{
minHeight: '0px',
maxHeight: '80vh',
height: logBarExpanded ? logContainerHeight : '0px',
transition: logBarExpanded ? 'none' : 'height 0.3s ease'
}}
>
<div ref={resizeBoxRef} className={styles.logBar}>
<Tabs
activeTab={activeTab}
type="line"
className={styles.logTabs}
onClickTab={handleTabClick}
>
<TabPane key="1" title="运行日志">
{renderRuntimeLogs()}
</TabPane>
<TabPane key="2" title="运行数据">
{renderRuntimeData()}
</TabPane>
</Tabs>
</div>
</ResizeBox>
{/* Tab 标签栏(始终可见) */}
{!logBarExpanded && (
<div className={styles.collapsedTabBar}>
<Tabs
activeTab={activeTab}
type="line"
className={styles.collapsedTabs}
onClickTab={handleTabClick}
>
<TabPane key="1" title="运行日志" />
<TabPane key="2" title="运行数据" />
</Tabs>
</div>
)}
</div>
</div>
</div>
);
};
export default InstanceCanvas;

@ -39,6 +39,7 @@ interface FlowEditorMainProps {
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
useDefault: boolean;
readOnly?: boolean; // 新增:只读模式
reactFlowInstance: any;
reactFlowWrapper: React.RefObject<HTMLDivElement>;
menu: any;
@ -96,6 +97,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
setNodes,
setEdges,
useDefault,
readOnly = false, // 解构 readOnly默认为 false
reactFlowInstance,
reactFlowWrapper,
menu,
@ -355,7 +357,7 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
return {
...node,
draggable: !currentAppIsRunning,
draggable: readOnly ? false : !currentAppIsRunning, // readOnly 模式下禁止拖拽
style: {
...node.style,
...style
@ -387,12 +389,20 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
snapToGrid={true}
snapGrid={[2, 2]}
proOptions={{ hideAttribution: true }} // 隐藏水印
nodesConnectable={!currentAppIsRunning} // 运行时禁用节点连接
nodesDraggable={!currentAppIsRunning} // 运行时禁用节点拖拽
// elementsSelectable={!currentAppIsRunning} // 运行时禁用元素选择
connectOnClick={!currentAppIsRunning} // 运行时禁用点击连接
disableKeyboardA11y={currentAppIsRunning} // 运行时禁用键盘交互
nodesConnectable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用节点连接
nodesDraggable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用节点拖拽
elementsSelectable={!readOnly} // readOnly 模式下禁用元素选择
connectOnClick={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用点击连接
disableKeyboardA11y={readOnly || currentAppIsRunning} // readOnly 或运行时禁用键盘交互
edgesFocusable={!readOnly} // readOnly 模式下边不可聚焦
nodesFocusable={!readOnly} // readOnly 模式下节点不可聚焦
edgesReconnectable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用边重连
onBeforeDelete={async ({ nodes, edges }) => {
// readOnly 模式下禁止删除
if (readOnly) {
return false;
}
// 在应用编排模式下useDefault为false只允许删除边不允许删除节点
if (!useDefault && nodes.length > 0) {
console.warn('在应用编排模式下不允许删除节点');
@ -488,21 +498,21 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
setIsEditModalOpen(false);
}}
onNodesChange={!currentAppIsRunning ? onNodesChange : undefined} // 运行时禁用节点变更
onEdgesChange={!currentAppIsRunning ? onEdgesChange : undefined} // 运行时禁用边变更
onConnect={!currentAppIsRunning ? onConnect : undefined} // 运行时禁用连接
onReconnect={!currentAppIsRunning ? onReconnect : undefined} // 运行时禁用重新连接
onDragOver={!currentAppIsRunning ? onDragOver : undefined} // 运行时禁用拖拽
onDrop={!currentAppIsRunning ? onDrop : undefined} // 运行时禁用放置
onNodeDrag={!currentAppIsRunning ? onNodeDrag : undefined} // 运行时禁用节点拖拽
onNodesChange={readOnly ? undefined : (!currentAppIsRunning ? onNodesChange : undefined)} // readOnly 或运行时禁用节点变更
onEdgesChange={readOnly ? undefined : (!currentAppIsRunning ? onEdgesChange : undefined)} // readOnly 或运行时禁用边变更
onConnect={readOnly ? undefined : (!currentAppIsRunning ? onConnect : undefined)} // readOnly 或运行时禁用连接
onReconnect={readOnly ? undefined : (!currentAppIsRunning ? onReconnect : undefined)} // readOnly 或运行时禁用重新连接
onDragOver={readOnly ? undefined : (!currentAppIsRunning ? onDragOver : undefined)} // readOnly 或运行时禁用拖拽
onDrop={readOnly ? undefined : (!currentAppIsRunning ? onDrop : undefined)} // readOnly 或运行时禁用放置
onNodeDrag={readOnly ? undefined : (!currentAppIsRunning ? onNodeDrag : undefined)} // readOnly 或运行时禁用节点拖拽
connectionLineType={ConnectionLineType.SmoothStep}
connectionLineComponent={CustomConnectionLine}
onNodeDragStop={!currentAppIsRunning ? onNodeDragStop : undefined} // 运行时禁用节点拖拽停止
onNodeContextMenu={(useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined} // 应用编排模式下禁用节点上下文菜单,运行时仍可使用
onNodeDoubleClick={!currentAppIsRunning ? onNodeDoubleClick : undefined} // 运行时禁用节点双击
onEdgeContextMenu={(!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined} // 运行时或应用编排模式下禁用边上下文菜单
onNodeDragStop={readOnly ? undefined : (!currentAppIsRunning ? onNodeDragStop : undefined)} // readOnly 或运行时禁用节点拖拽停止
onNodeContextMenu={readOnly ? undefined : ((useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined)} // readOnly 或应用编排模式下禁用节点上下文菜单
onNodeDoubleClick={readOnly ? undefined : (!currentAppIsRunning ? onNodeDoubleClick : undefined)} // readOnly 或运行时禁用节点双击
onEdgeContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用边上下文菜单
onPaneClick={(useDefault || currentAppIsRunning) ? onPaneClick : undefined} // 运行时禁用面板点击
onPaneContextMenu={(!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined} // 运行时或应用编排模式下禁用面板上下文菜单
onPaneContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单
onEdgeMouseEnter={(_event, edge) => {
setEdges((eds) => eds.map(e => {
if (e.id === edge.id) {
@ -524,21 +534,23 @@ const FlowEditorMain: React.FC<FlowEditorMainProps> = (props) => {
selectionMode={!currentAppIsRunning ? SelectionMode.Partial : undefined} // 运行时禁用选择模式
>
<Background />
<Panel position="top-left">
<ActionBar
useDefault={useDefault}
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
onPause={handlePause}
onReRun={handleReRun}
isRunning={currentAppIsRunning}
></ActionBar>
</Panel>
{useDefault && <Panel position="top-right">
{!readOnly && (
<Panel position="top-left">
<ActionBar
useDefault={useDefault}
onSave={saveFlowDataToServer}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
onRun={handleRun}
onPause={handlePause}
onReRun={handleReRun}
isRunning={currentAppIsRunning}
></ActionBar>
</Panel>
)}
{useDefault && !readOnly && <Panel position="top-right">
<HandlerBar
onPublish={handlePublish}
isRunning={currentAppIsRunning}

@ -12,17 +12,17 @@ import FlowEditorMain from './FlowEditorMain';
import { useFlowEditorState } from '@/hooks/useFlowEditorState';
import { useFlowCallbacks } from '@/hooks/useFlowCallbacks';
const FlowEditorWithProvider: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ initialData, useDefault }) => {
const FlowEditorWithProvider: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?: boolean }> = ({ initialData, useDefault, readOnly }) => {
return (
<div style={{ width: '100%', height: '91vh', display: 'flex' }} onContextMenu={(e) => e.preventDefault()}>
<ReactFlowProvider>
<FlowEditor initialData={initialData} useDefault={useDefault} />
<FlowEditor initialData={initialData} useDefault={useDefault} readOnly={readOnly} />
</ReactFlowProvider>
</div>
);
};
const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ initialData, useDefault }) => {
const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?: boolean }> = ({ initialData, useDefault, readOnly = false }) => {
const reactFlowInstance = useReactFlow();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [menu, setMenu] = useState<{
@ -302,6 +302,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini
setNodes={setNodes}
setEdges={setEdges}
useDefault={useDefault}
readOnly={readOnly}
reactFlowInstance={reactFlowInstance}
reactFlowWrapper={reactFlowWrapper}
menu={menu}

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import styles from './style/index.module.less';
import CustomCard from '@/components/CustomCard';
import Overview from './overview';
import InstanceCanvas from '@/components/InstanceCanvas';
import { Space, Select, Switch, Table, TableColumnProps, Progress } from '@arco-design/web-react';
import {
IconLoading,
@ -19,6 +20,8 @@ import { getMyAppList } from '@/api/apps';
const Option = Select.Option;
// 将 checkInstance 函数移到组件外部,稍后会在组件内部重新定义
let checkInstanceHandler: (instance: any) => void;
const columns: TableColumnProps[] = [
{
@ -63,27 +66,23 @@ const columns: TableColumnProps[] = [
title: '运行状态',
dataIndex: 'state',
render: (col, record, index) => (
<span>
{formatInstanceStatus(record.state)}
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ marginRight: 5 }}>{formatInstanceStatus(record.state)}</div>
{getIcon(record.state)}
</span>
</div>
)
},
// {
// title: '操作',
// dataIndex: '',
// render: (col, record, index) => (
// <span>
// <IconEye style={{ fontSize: '26px', cursor: 'pointer' }} onClick={() => checkInstance(record)} />
// </span>
// )
// }
{
title: '操作',
dataIndex: '',
render: (col, record, index) => (
<span>
<IconEye style={{ fontSize: '26px', cursor: 'pointer' }} onClick={() => checkInstanceHandler(record)} />
</span>
)
}
];
function checkInstance(instance) {
console.log('instance:', instance);
}
function getIcon(type) {
switch (type) {
case 'RUNNING':
@ -104,6 +103,8 @@ function getIcon(type) {
}
const Instance = () => {
const [viewMode, setViewMode] = useState<'list' | 'canvas'>('list'); // 视图模式:列表或画布
const [selectedInstance, setSelectedInstance] = useState<any>(null); // 选中的实例
const [instanceData, setInstanceData] = useState({
list: [],
totalCount: 0,
@ -125,6 +126,21 @@ const Instance = () => {
durationDesc: false
});
// 定义 checkInstance 处理函数
const checkInstance = (instance: any) => {
setSelectedInstance(instance);
setViewMode('canvas');
};
// 将处理函数赋值给外部变量
checkInstanceHandler = checkInstance;
// 返回列表视图
const backToList = () => {
setViewMode('list');
setSelectedInstance(null);
};
const Selector = () => {
return (
@ -288,48 +304,60 @@ const Instance = () => {
return (
<>
<div className={styles['instanceContainer']}>
<CustomCard>
<Overview overviewData={overviewData} />
<Space
size={50}
style={{ marginTop: '20px', marginBottom: '30px' }}
>
<div className={styles['handleRow']}>
<span></span>
{Selector()}
</div>
<div className={styles['handleRow']}>
<span></span>
{RunStatus()}
</div>
<div className={styles['handleRow']}>
<span></span>
{Type()}
</div>
<div className={styles['handleRow']}>
<span></span>
<Switch
checked={searchParams.durationDesc}
onChange={(checked) => handleSearchParamChange('durationDesc', checked)}
/>
</div>
</Space>
<Table
columns={columns}
data={instanceData.list}
pagination={{
total: instanceData.totalCount,
current: instanceData.currentPage,
pageSize: instanceData.pageSize,
showTotal: true,
sizeOptions: [10, 20, 30, 50],
onChange: handlePageChange,
onPageSizeChange: handlePageSizeChange
}}
{viewMode === 'list' ? (
// 列表视图
<div className={styles['instanceContainer']}>
<CustomCard>
<Overview overviewData={overviewData} />
<Space
size={50}
style={{ marginTop: '20px', marginBottom: '30px' }}
>
<div className={styles['handleRow']}>
<span></span>
{Selector()}
</div>
<div className={styles['handleRow']}>
<span></span>
{RunStatus()}
</div>
<div className={styles['handleRow']}>
<span></span>
{Type()}
</div>
<div className={styles['handleRow']}>
<span></span>
<Switch
checked={searchParams.durationDesc}
onChange={(checked) => handleSearchParamChange('durationDesc', checked)}
/>
</div>
</Space>
<Table
columns={columns}
data={instanceData.list}
pagination={{
total: instanceData.totalCount,
current: instanceData.currentPage,
pageSize: instanceData.pageSize,
showTotal: true,
sizeOptions: [10, 20, 30, 50],
onChange: handlePageChange,
onPageSizeChange: handlePageSizeChange
}}
/>
</CustomCard>
</div>
) : (
// 画布视图
<div className={styles['canvasContainer']}>
<InstanceCanvas
instanceData={selectedInstance}
title={selectedInstance?.name || '实例详情'}
onBack={backToList}
/>
</CustomCard>
</div>
</div>
)}
</>
);
};

@ -1,3 +1,18 @@
.instanceContainer {
}
}
.canvasContainer {
width: 100%;
height: calc(100vh - 60px);
display: flex;
flex-direction: column;
background-color: var(--color-fill-2);
overflow: hidden;
}
.handleRow {
display: flex;
align-items: center;
gap: 8px;
}

@ -676,9 +676,12 @@ const getNodeApiIns = (nodeId: string, nodeConfig: any, currentProjectCompData:
return [{ name: 'end', desc: '', dataType: '', defaultValue: '' }];
}
else {
const comp = currentProjectCompData.filter(item => item.id === nodeConfig?.component?.compId);
const comp = currentProjectCompData.filter(item => {
return (item.id || item?.comp?.id) === nodeConfig?.component?.compId;
});
if (comp && comp.length > 0) {
return comp[0].def?.apis.map(v => {
const apiIns = comp[0].def?.apis || comp[0]?.comp.def?.apis;
return apiIns.map(v => {
return {
...v,
name: v.id,
@ -854,6 +857,82 @@ const getCurrentProjectStoreData = () => {
});
}
// 如果从 store 中没有获取到数据,尝试从 sessionStorage 中获取
if (result.length === 0) {
try {
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
const compLibsKey = `compLibs${userInfo.userId}`;
const compLibsData = sessionStorage.getItem(compLibsKey);
if (compLibsData) {
const { myLibs = [], pubLibs = [], teamLibs = [], myFlow = [], pubFlow = [] } = JSON.parse(compLibsData);
// 处理 myLibs我的组件库
if (Array.isArray(myLibs)) {
myLibs.forEach((lib: any) => {
if (lib.children && Array.isArray(lib.children)) {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'mineComp'
});
});
}
});
}
// 处理 pubLibs公共组件库
if (Array.isArray(pubLibs)) {
pubLibs.forEach((lib: any) => {
if (lib.children && Array.isArray(lib.children)) {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'pubComp'
});
});
}
});
}
// 处理 teamLibs协同组件库
if (Array.isArray(teamLibs)) {
teamLibs.forEach((lib: any) => {
if (lib.children && Array.isArray(lib.children)) {
lib.children.forEach((item: any) => {
result.push({
...item,
type: 'teamWorkComp'
});
});
}
});
}
// 处理 myFlow我的流程
if (Array.isArray(myFlow)) {
myFlow.forEach((item: any) => {
result.push({
...item,
type: 'mineFlow'
});
});
}
// 处理 pubFlow公共流程
if (Array.isArray(pubFlow)) {
pubFlow.forEach((item: any) => {
result.push({
...item,
type: 'pubFlow'
});
});
}
}
} catch (error) {
console.error('从 sessionStorage 获取组件数据失败:', error);
}
}
return result;
};

Loading…
Cancel
Save