Compare commits

...

4 Commits

@ -55,7 +55,8 @@
"react-redux": "^7.2.6",
"recorder-core": "^1.3.25011100",
"redux": "^4.1.2",
"remark-gfm": "^4.0.1"
"remark-gfm": "^4.0.1",
"utif": "^3.1.0"
},
"devDependencies": {
"@svgr/webpack": "^5.5.0",

@ -5,6 +5,9 @@ import DynamicIcon from '@/components/DynamicIcon';
import NodeContentImage from '@/pages/flowEditor/components/nodeContentImage';
import NodeStatusIndicator, { NodeStatus } from '@/components/FlowEditor/NodeStatusIndicator';
import { useStore as useFlowStore } from '@xyflow/react';
import { useSelector } from 'react-redux';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getRuntimeImageUrl } from '@/utils/flow/imageRuntimeData';
const setIcon = () => {
return <DynamicIcon type="IconImage" style={{ fontSize: '16px', marginRight: '5px' }} />;
@ -12,6 +15,12 @@ const setIcon = () => {
const ImageNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '图片展示';
const { currentAppData, appRuntimeData } = useSelector((state: any) => state.ideContainer);
const currentAppKey = getCurrentAppKey(currentAppData);
const runtimeNodeData = currentAppKey
? appRuntimeData[currentAppKey]?.nodeData
: [];
const imageUrl = getRuntimeImageUrl(runtimeNodeData, id);
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
@ -35,9 +44,9 @@ const ImageNode = ({ data, id }: { data: any; id: string }) => {
{title}
<NodeStatusIndicator status={nodeStatus} isVisible={isStatusVisible} />
</div>
<NodeContentImage data={data} />
<NodeContentImage data={data} imageUrl={imageUrl} />
</div>
);
};
export default ImageNode;
export default ImageNode;

@ -105,6 +105,40 @@
min-height: 10px;
}
.node-image-box {
width: 150px;
padding: 6px;
margin: -1px auto 0;
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 3px;
:global(.arco-image) {
display: block;
max-width: 100%;
}
:global(.arco-image-img) {
max-width: 100%;
max-height: 120px;
object-fit: contain;
}
.node-image-loading,
.node-image-download {
display: block;
color: #86909c;
font-size: 12px;
line-height: 22px;
text-align: center;
word-break: break-all;
}
.node-image-download {
color: #165dff;
}
}
.node-content-box {
padding: 10px;
margin: -1px auto;
@ -182,4 +216,4 @@
100% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
}
}
}

@ -1,8 +1,9 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import styles from '@/components/FlowEditor/node/style/baseOther.module.less';
import { Handle, Position, useStore } from '@xyflow/react';
import { Handle, Position } from '@xyflow/react';
import { Image } from '@arco-design/web-react';
import { formatDataType } from '@/utils/common';
import UTIF from 'utif';
interface NodeContentData {
parameters?: {
@ -103,7 +104,64 @@ const renderRegularNodeHandles = (dataIns: any[], dataOuts: any[], apiIns: any[]
};
const NodeContent = ({ data }: { data: NodeContentData }) => {
const getImageBlobType = (url: string, blobType: string) => {
if (blobType && blobType !== 'application/octet-stream') {
return blobType;
}
const cleanUrl = url.split('?')[0].toLowerCase();
if (cleanUrl.endsWith('.png')) return 'image/png';
if (cleanUrl.endsWith('.jpg') || cleanUrl.endsWith('.jpeg')) return 'image/jpeg';
if (cleanUrl.endsWith('.gif')) return 'image/gif';
if (cleanUrl.endsWith('.webp')) return 'image/webp';
if (cleanUrl.endsWith('.bmp')) return 'image/bmp';
if (cleanUrl.endsWith('.svg')) return 'image/svg+xml';
if (cleanUrl.endsWith('.tif') || cleanUrl.endsWith('.tiff')) return 'image/tiff';
return blobType || 'application/octet-stream';
};
const isTiffImage = (url: string, blobType: string) => {
const imageType = getImageBlobType(url, blobType).toLowerCase();
const cleanUrl = url.split('?')[0].toLowerCase();
return (
imageType === 'image/tiff' ||
cleanUrl.endsWith('.tif') ||
cleanUrl.endsWith('.tiff')
);
};
const createTiffPreviewUrl = async (url: string, blob: Blob) => {
const buffer = await blob.arrayBuffer();
const ifds = UTIF.decode(buffer);
const firstImage = ifds[0];
if (!firstImage) {
throw new Error('empty tiff image');
}
UTIF.decodeImage(buffer, firstImage);
const rgba = UTIF.toRGBA8(firstImage);
const canvas = document.createElement('canvas');
canvas.width = firstImage.width;
canvas.height = firstImage.height;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('canvas context unavailable');
}
const imageData = context.createImageData(firstImage.width, firstImage.height);
imageData.data.set(rgba);
context.putImageData(imageData, 0, 0);
return canvas.toDataURL('image/png');
};
const NodeContent = ({ data, imageUrl = '' }: { data: NodeContentData; imageUrl?: string }) => {
const [previewUrl, setPreviewUrl] = useState('');
const [imageLoadError, setImageLoadError] = useState(false);
const apiIns = data.parameters?.apiIns || [];
const apiOuts = data.parameters?.apiOuts || [];
const dataIns = data.parameters?.dataIns || [];
@ -114,6 +172,60 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
const isEndNode = data.type === 'end';
const isSpecialNode = isStartNode || isEndNode;
useEffect(() => {
let canceled = false;
let objectUrl = '';
setPreviewUrl('');
setImageLoadError(false);
if (!imageUrl) {
return undefined;
}
const loadImageBlob = async () => {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`load image failed: ${response.status}`);
}
const blob = await response.blob();
if (isTiffImage(imageUrl, blob.type)) {
const tiffPreviewUrl = await createTiffPreviewUrl(imageUrl, blob);
if (!canceled) {
setPreviewUrl(tiffPreviewUrl);
}
return;
}
const typedBlob = new Blob([blob], {
type: getImageBlobType(imageUrl, blob.type),
});
objectUrl = URL.createObjectURL(typedBlob);
if (!canceled) {
setPreviewUrl(objectUrl);
}
} catch (error) {
console.error('图片下载预览失败:', error);
if (!canceled) {
setPreviewUrl(imageUrl);
setImageLoadError(true);
}
}
};
loadImageBlob();
return () => {
canceled = true;
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [imageUrl]);
return (
<>
{/*content栏-api部分*/}
@ -178,14 +290,31 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
</>
)}
{/*图片展示 TODO 需要对接接口*/}
{/*<div className={styles['node-image-box']}>*/}
{/* <Image*/}
{/* width={150}*/}
{/* src=""*/}
{/* alt="lamp"*/}
{/* />*/}
{/*</div>*/}
{imageUrl && (
<div className={styles['node-image-box']}>
{previewUrl ? (
<Image
width={150}
src={previewUrl}
alt="图片展示"
style={{ display: 'block' }}
onError={() => setImageLoadError(true)}
/>
) : (
<div className={styles['node-image-loading']}></div>
)}
{imageLoadError && (
<a
className={styles['node-image-download']}
href={imageUrl}
target="_blank"
rel="noreferrer"
>
</a>
)}
</div>
)}
{/* 根据节点类型渲染不同的句柄 */}
{renderRegularNodeHandles(dataIns, dataOuts, apiIns, apiOuts)}
@ -193,4 +322,4 @@ const NodeContent = ({ data }: { data: NodeContentData }) => {
);
};
export default NodeContent;
export default NodeContent;

@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { ResizeBox, Tabs } from '@arco-design/web-react';
import styles from './style/logBar.module.less';
import { updateLogBarStatus } from '@/store/ideContainer';
import { updateLogBarStatus, updateRuntimeNodeData } from '@/store/ideContainer';
import { useSelector, useDispatch } from 'react-redux';
import { getNodeData } from '@/api/appIns';
import RunTimeData from './components/runTimeData';
@ -165,27 +165,32 @@ const LogBar: React.FC<LogBarProps> = () => {
// 获取当前应用的运行状态
const currentAppKey = getCurrentAppKey(currentAppData);
const isRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey]?.isRunning;
const currentRunId = currentAppKey ? appRuntimeData[currentAppKey]?.runId : '';
// 实现轮询获取运行数据 - 只在应用运行时轮询
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
const appKey = getCurrentAppKey(currentAppData);
const appKey = currentAppKey;
// 只有在应用正在运行且有 runId 时才开始轮询
if (
appKey &&
appRuntimeData[appKey]?.isRunning &&
appRuntimeData[appKey]?.runId
currentAppIsRunning &&
currentRunId
) {
const fetchRuntimeData = async () => {
try {
setLoading(true);
const response = await getNodeData(appRuntimeData[appKey].runId);
const response = await getNodeData(currentRunId);
setRuntimeData((prev) => ({
...prev,
[appKey]: response.data,
}));
dispatch(updateRuntimeNodeData({
appId: appKey,
nodeData: response.data
}));
} catch (error) {
console.error('获取运行数据失败:', error);
} finally {
@ -206,12 +211,12 @@ const LogBar: React.FC<LogBarProps> = () => {
clearInterval(intervalId);
}
};
}, [currentAppData, appRuntimeData]);
}, [currentAppKey, currentAppIsRunning, currentRunId, dispatch]);
// 当应用停止运行时,清除运行数据
useEffect(() => {
const appKey = getCurrentAppKey(currentAppData);
if (appKey && !appRuntimeData[appKey]?.isRunning) {
const appKey = currentAppKey;
if (appKey && !currentAppIsRunning) {
// 清除当前应用的运行数据
setRuntimeData((prev) => {
const newData = { ...prev };
@ -219,7 +224,7 @@ const LogBar: React.FC<LogBarProps> = () => {
return newData;
});
}
}, [isRunning, currentAppData]);
}, [currentAppKey, currentAppIsRunning]);
// 渲染校验日志内容
const renderValidationLogs = () => {

@ -28,6 +28,7 @@ interface IDEContainerState {
isPaused: boolean;
logs: any[];
runId: string;
nodeData: any[];
eventSendNodeList: any[]; // [{nodeID:topic}]
eventlisteneList: any[]; // [{nodeID:topic}]
}
@ -212,6 +213,19 @@ const ideContainerSlice = createSlice({
state.appRuntimeData[appId].logs = [];
}
},
// 更新运行节点数据
updateRuntimeNodeData: (state, { payload }) => {
const { appId, nodeData } = payload;
if (!appId) {
return;
}
if (!state.appRuntimeData[appId]) {
state.appRuntimeData[appId] = createDefaultAppRuntimeState();
}
state.appRuntimeData[appId].nodeData = Array.isArray(nodeData)
? nodeData
: [];
},
// 更新组件编码路径
updateComponentCodingPath(state, action) {
state.componentCoding = { ...action.payload };
@ -241,6 +255,7 @@ export const {
updateEventNodeList,
addRuntimeLog,
clearRuntimeLogs,
updateRuntimeNodeData,
updateComponentCodingPath,
} = ideContainerSlice.actions;

@ -0,0 +1,40 @@
export interface RuntimeNodeData {
nodeId?: string;
input?: Record<string, any>;
[key: string]: any;
}
const isRuntimeNode = (item: any): item is RuntimeNodeData => {
return !!item && typeof item === 'object' && typeof item.nodeId === 'string';
};
export const flattenRuntimeNodeData = (runtimeData: any): RuntimeNodeData[] => {
if (!Array.isArray(runtimeData)) {
return [];
}
return runtimeData.reduce<RuntimeNodeData[]>((nodes, item) => {
if (isRuntimeNode(item)) {
nodes.push(item);
return nodes;
}
if (Array.isArray(item?.nodes)) {
nodes.push(...item.nodes.filter(isRuntimeNode));
}
return nodes;
}, []);
};
export const getRuntimeImageUrl = (
runtimeData: any,
nodeId: string
): string => {
const nodeData = flattenRuntimeNodeData(runtimeData).find(
(item) => item.nodeId === nodeId
);
const imageUrl = nodeData?.input?.in;
return typeof imageUrl === 'string' ? imageUrl.trim() : '';
};

@ -9,6 +9,7 @@ export interface AppRuntimeState {
isPaused: boolean;
logs: any[];
runId: string;
nodeData: any[];
eventSendNodeList: any[];
eventlisteneList: any[];
}
@ -29,6 +30,7 @@ export const createDefaultAppRuntimeState = (): AppRuntimeState => ({
isPaused: false,
logs: [],
runId: '',
nodeData: [],
eventSendNodeList: [],
eventlisteneList: [],
});

Loading…
Cancel
Save