Compare commits

...

43 Commits

Author SHA1 Message Date
钟良源 52c55b5988 pref: 优化状态指示器的渲染时机 1 month ago
鱼星 bc02892413 Merge remote-tracking branch 'origin/production' into production 1 month ago
鱼星 f23d0774f8 feat: 修复图片展示组件 1 month ago
钟良源 bea38a7e36 feat: 增加流程列表状态与画布运行状态的关联 1 month ago
钟良源 294894ea56 fix: 处理频繁更新导致节点闪烁的问题 1 month ago
钟良源 406b88e0cf Merge branch 'master' into production 1 month ago
钟良源 43fddf44b8 fix: 修复图片组件 dataIns 问题 1 month ago
鱼星 40bc22f6e8 fix: 修复重连后多画布空白 1 month ago
鱼星 faaaf2d5bb feat: 实现重连功能 1 month ago
钟良源 6543571b96 Merge branch 'master' into production 1 month ago
钟良源 2a21018544 feat(componentList): 增加表单校验规则 1 month ago
钟良源 923df3cc56 Merge branch 'master' into production 1 month ago
钟良源 d31d00c0e7 fix(componentList): 修复同步事件节点问题 1 month ago
钟良源 4599b3d39b Merge branch 'master' into production 3 months ago
钟良源 8273b7eebb feat(componentList): 导入组件增加多选导入功能 3 months ago
钟良源 ded88c7721 Merge branch 'master' into production 3 months ago
钟良源 fd610e2bd8 feat(event): 替换删除事件的接口入参id 3 months ago
钟良源 abe0fc554a fix(flowEditor): 修复粘贴逻辑无效的问题 3 months ago
钟良源 f1fe33942e Merge branch 'master' into production 3 months ago
钟良源 755afe1483 feat(scripts): 优化git脚本,使用prompts替换readline,提升cli的交互 3 months ago
钟良源 84835607d3 Merge branch 'master' into production 3 months ago
钟良源 2abcd07184 feat(scripts): 优化git脚本,增加cli交互式功能 3 months ago
钟良源 ab4a0e99cc Merge branch 'master' into production 3 months ago
钟良源 3bb7164b4f feat(globalVar): 变量名增加正则限制 3 months ago
钟良源 4a320f62f2 feat(event): 事件标识增加正则限制 3 months ago
钟良源 9037901d03 Merge branch 'master' into production 3 months ago
钟良源 8fcf17568b pref: 清除无用console.log 3 months ago
钟良源 84736d074e fix(useFlowCallback): 修复onConnect中自旋连接的问题 3 months ago
钟良源 c6937367c4 Merge branch 'master' into production 3 months ago
钟良源 861b829bc5 style: 恢复原本项目主题名称 3 months ago
钟良源 107ca923fa style: 修改项目主题名称 3 months ago
钟良源 9f974861e5 feat(market): 修改组件作者的取值字段 3 months ago
钟良源 347becdec0 feat(market): 修改组件仓库中组件列表的布局,增加新内容渲染以及组件语言渲染 3 months ago
钟良源 8bc11c0de1 fix(overview): 修复首页未能正确展示用户名的问题 3 months ago
钟良源 6dad941b96 fix(componentDeploy): 修复实例编译状态枚举表 3 months ago
钟良源 697f13d6c3 feat(useWebSocket): 修改socket默认重连次数 3 months ago
钟良源 2657452977 feat(componentDeployment): 实例编译状态增加新的枚举值 3 months ago
钟良源 e410fabf20 feat(componentDeployment): socket消息增加新字段取值 3 months ago
钟良源 8e70ea94ef feat(componentDeployment): socket美化,增加序号以及信息提取 3 months ago
钟良源 020b9235a8 fix(componentDeployment): 修复启停按钮异常消失的问题 3 months ago
钟良源 e9f19eb810 style(componentDeployment): 隐藏组件资源按钮 3 months ago
钟良源 38b59621e3 fix(componentDeployment): 修改socket入参 3 months ago
钟良源 a664e6bd90 feat(componentDeployment): 增加组件编译日志功能,优化编辑按钮,优化运行日志和启动按钮渲染逻辑 3 months ago

@ -78,6 +78,7 @@
"postcss-less": "^5.0.0",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.2",
"prompts": "^2.4.2",
"stylelint": "^14.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^24.0.0",

@ -192,6 +192,9 @@ importers:
pretty-quick:
specifier: ^3.1.2
version: 3.3.1(prettier@2.8.8)
prompts:
specifier: ^2.4.2
version: 2.4.2
stylelint:
specifier: ^14.1.0
version: 14.16.1
@ -3312,6 +3315,10 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
@ -4021,6 +4028,10 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@ -4475,6 +4486,9 @@ packages:
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
size-sensor@1.0.2:
resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==}
@ -9351,6 +9365,8 @@ snapshots:
kind-of@6.0.3: {}
kleur@3.0.3: {}
kleur@4.1.5: {}
klona@2.0.6: {}
@ -10375,6 +10391,11 @@ snapshots:
progress@2.0.3: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@ -10945,6 +10966,8 @@ snapshots:
dependencies:
is-arrayish: 0.3.4
sisteransi@1.0.5: {}
size-sensor@1.0.2: {}
skmeans@0.9.7: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 B

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

@ -1,6 +1,7 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const { spawnSync } = require('child_process');
const prompts = require('prompts');
const COLORS = {
red: '\x1b[31m',
@ -10,116 +11,323 @@ const COLORS = {
reset: '\x1b[0m'
};
class MergeConflictError extends Error {
constructor(branchName) {
super(`合并到 ${branchName} 时发生冲突`);
this.name = 'MergeConflictError';
this.branchName = branchName;
}
}
class PromptAbortedError extends Error {
constructor() {
super('操作已取消');
this.name = 'PromptAbortedError';
}
}
function log(message, color = 'reset') {
console.log(`${COLORS[color]}${message}${COLORS.reset}`);
}
function exec(command, options = {}) {
try {
return execSync(command, { encoding: 'utf-8', stdio: options.silent ? 'pipe' : 'inherit', ...options });
} catch (error) {
if (options.ignoreError) {
return error.stdout || '';
function runCommand(command, args, options = {}) {
const { silent = false, ignoreError = false } = options;
const result = spawnSync(command, args, {
encoding: 'utf-8',
stdio: silent ? ['inherit', 'pipe', 'pipe'] : 'inherit'
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
if (ignoreError) {
return result.stdout || '';
}
const error = new Error(`${command} ${args.join(' ')} failed`);
error.status = result.status;
error.stdout = result.stdout || '';
error.stderr = result.stderr || '';
throw error;
}
return result.stdout || '';
}
function git(args, options = {}) {
return runCommand('git', args, options);
}
function run() {
log('\n========================================', 'cyan');
log(' 合并 master 到 production 分支', 'cyan');
log('========================================\n', 'cyan');
function getCurrentBranch() {
return git(['branch', '--show-current'], { silent: true }).trim();
}
function getLocalBranches() {
return git(['branch', '--format=%(refname:short)'], { silent: true })
.split(/\r?\n/)
.map(item => item.trim())
.filter(Boolean);
}
// 获取当前分支名
const currentBranch = exec('git branch --show-current', { silent: true }).trim();
log(`当前分支: ${currentBranch}\n`, 'cyan');
function hasRemoteBranch(branchName) {
const output = git(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branchName}`], {
silent: true,
ignoreError: true
});
// 暂存未提交的更改
const status = exec('git status --porcelain', { silent: true });
const hasChanges = status.trim().length > 0;
if (hasChanges) {
log('>>> 暂存未提交的更改...', 'yellow');
exec('git stash push -m "暂存未提交的更改"');
return output.trim().length > 0;
}
function stashChanges(state) {
const status = git(['status', '--porcelain'], { silent: true });
state.hasChanges = status.trim().length > 0;
if (!state.hasChanges) {
return;
}
log('>>> 暂存未提交的改动...', 'yellow');
git(['stash', 'push', '-m', 'auto-stash-before-merge']);
state.stashCreated = true;
log('已暂存\n', 'green');
}
// 拉取最新代码
log('>>> 拉取远程最新代码...', 'yellow');
try {
exec('git fetch origin');
} catch (error) {
log('\n错误: 拉取远程代码失败', 'red');
process.exit(1);
function restoreWorkspace(state) {
if (state.currentBranch && getCurrentBranch() !== state.currentBranch) {
log(`>>> 切换回原分支 ${state.currentBranch}...`, 'yellow');
git(['checkout', state.currentBranch], { ignoreError: true });
}
if (state.stashCreated) {
log('>>> 恢复暂存的改动...', 'yellow');
git(['stash', 'pop'], { ignoreError: true });
log('已恢复\n', 'green');
}
}
function fetchOrigin() {
log('>>> 拉取远程最新信息...', 'yellow');
git(['fetch', 'origin']);
log('拉取完成\n', 'green');
}
// 切换到 master 并更新
log('>>> 切换到 master 分支并更新...', 'yellow');
try {
exec('git checkout master');
exec('git pull origin master');
} catch (error) {
log('\n错误: 更新 master 分支失败', 'red');
process.exit(1);
function checkoutBranch(branchName) {
log(`>>> 切换到 ${branchName} 分支...`, 'yellow');
git(['checkout', branchName]);
log(`已切换到 ${branchName}\n`, 'green');
}
log('master 分支已更新\n', 'green');
// 切换到 production 并更新
log('>>> 切换到 production 分支并更新...', 'yellow');
try {
exec('git checkout production');
exec('git pull origin production');
} catch (error) {
log('\n错误: 更新 production 分支失败,分支可能不存在', 'red');
log('请先创建 production 分支: git checkout -b production\n', 'yellow');
exec(`git checkout ${currentBranch}`, { ignoreError: true });
process.exit(1);
function pullBranchIfNeeded(branchName) {
if (!hasRemoteBranch(branchName)) {
log(`>>> 本地分支 ${branchName} 未关联 origin/${branchName},跳过 pull`, 'yellow');
return;
}
log('production 分支已更新\n', 'green');
// 合并 master 到 production
log('>>> 合并 master 到 production...', 'yellow');
try {
exec('git merge master --no-edit');
} catch (error) {
log('\n错误: 合并过程中发生冲突!', 'red');
log('请手动解决冲突后再提交', 'red');
log(`>>> 更新 ${branchName} 分支...`, 'yellow');
git(['pull', 'origin', branchName]);
log(`${branchName} 已更新\n`, 'green');
}
function pushBranchIfNeeded(branchName) {
if (!hasRemoteBranch(branchName)) {
log(`>>> 本地分支 ${branchName} 未关联 origin/${branchName},跳过 push`, 'yellow');
return;
}
log(`>>> 推送 ${branchName} 到远程...`, 'yellow');
git(['push', 'origin', branchName]);
log(`${branchName} 推送成功\n`, 'green');
}
function printConflictGuidance(branchName) {
log(`\n错误: 合并到 ${branchName} 时发生冲突`, 'red');
log('请手动解决冲突后再继续', 'red');
log('\n冲突文件:', 'yellow');
exec('git diff --name-only --diff-filter=U', { ignoreError: true });
log('\n解决冲突后运行:', 'yellow');
git(['diff', '--name-only', '--diff-filter=U'], { ignoreError: true });
log('\n解决冲突后可继续执行:', 'yellow');
log(' git add .', 'cyan');
log(' git commit', 'cyan');
log(' git push origin production\n', 'cyan');
log('或放弃合并:', 'yellow');
log(` git push origin ${branchName}\n`, 'cyan');
log('如需放弃本次合并:', 'yellow');
log(' git merge --abort\n', 'cyan');
process.exit(1);
}
log('合并成功\n', 'green');
// 推送到远程
log('>>> 推送到远程 production 分支...', 'yellow');
function mergeBranch(sourceBranch, targetBranch, state) {
log(`>>> 合并 ${sourceBranch} -> ${targetBranch}...`, 'yellow');
try {
exec('git push origin production');
git(['merge', sourceBranch, '--no-edit']);
} catch (error) {
log('\n错误: 推送失败', 'red');
process.exit(1);
state.shouldRestoreWorkspace = false;
throw new MergeConflictError(targetBranch);
}
log('推送成功\n', 'green');
// 切换回原分支
log(`>>> 切换回原分支 ${currentBranch}...`, 'yellow');
exec(`git checkout ${currentBranch}`, { ignoreError: true });
log('合并成功\n', 'green');
}
// 恢复暂存的更改
if (hasChanges) {
log('>>> 恢复暂存的更改...', 'yellow');
exec('git stash pop', { ignoreError: true });
log('已恢复\n', 'green');
async function promptValue(config) {
const response = await prompts(config, {
onCancel() {
throw new PromptAbortedError();
}
});
return response[config.name];
}
async function selectOption(title, options) {
log('', 'reset');
return promptValue({
type: 'select',
name: 'value',
message: title,
hint: '使用上下方向键选择,回车确认',
choices: options.map(option => ({
title: option.label,
value: option.value,
description: option.description
}))
});
}
async function selectBranch(title, branches, currentBranch) {
return selectOption(
title,
branches.map(branchName => ({
value: branchName,
label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName
}))
);
}
async function confirm(message) {
log('', 'reset');
return promptValue({
type: 'confirm',
name: 'confirmed',
message,
initial: true
});
}
async function runSingleBranchMerge(state) {
const branches = getLocalBranches();
if (branches.length < 2) {
throw new Error('本地分支数量不足,无法执行单分支合并');
}
const sourceBranch = await selectBranch('请选择来源分支', branches, state.currentBranch);
const targetCandidates = branches.filter(branchName => branchName !== sourceBranch);
const targetBranch = await selectBranch('请选择目标分支', targetCandidates, state.currentBranch);
log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan');
const accepted = await confirm('确认开始合并吗');
if (!accepted) {
log('已取消操作', 'yellow');
return;
}
stashChanges(state);
fetchOrigin();
checkoutBranch(targetBranch);
pullBranchIfNeeded(targetBranch);
mergeBranch(sourceBranch, targetBranch, state);
pushBranchIfNeeded(targetBranch);
log('\n========================================', 'green');
log(' master 已成功合并到 production!', 'green');
log(` 已完成 ${sourceBranch} -> ${targetBranch} 合并`, 'green');
log('========================================\n', 'green');
}
async function runDeployMerge(state) {
if (state.currentBranch === 'production') {
throw new Error('当前分支为 production不能执行部署合并');
}
log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan');
const accepted = await confirm('确认开始部署合并吗');
if (!accepted) {
log('已取消操作', 'yellow');
return;
}
stashChanges(state);
fetchOrigin();
checkoutBranch('master');
pullBranchIfNeeded('master');
if (state.currentBranch !== 'master') {
mergeBranch(state.currentBranch, 'master', state);
} else {
log('当前分支已是 master跳过 feature -> master 合并\n', 'yellow');
}
pushBranchIfNeeded('master');
checkoutBranch('production');
pullBranchIfNeeded('production');
mergeBranch('master', 'production', state);
pushBranchIfNeeded('production');
log('\n========================================', 'green');
log(` 已完成 ${state.currentBranch} -> master -> production`, 'green');
log('========================================\n', 'green');
}
async function run() {
const state = {
currentBranch: '',
hasChanges: false,
stashCreated: false,
shouldRestoreWorkspace: true
};
try {
log('\n========================================', 'cyan');
log(' Git 分支合并工具', 'cyan');
log('========================================\n', 'cyan');
state.currentBranch = getCurrentBranch();
if (!state.currentBranch) {
throw new Error('当前不在任何本地分支上,无法执行合并');
}
log(`当前分支: ${state.currentBranch}\n`, 'cyan');
const action = await selectOption('请选择操作类型', [
{ label: '单分支合并', value: 'single', description: '手动选择来源分支和目标分支' },
{ label: '部署合并', value: 'deploy', description: '将当前分支依次合并到 master 和 production' }
]);
if (action === 'single') {
await runSingleBranchMerge(state);
} else {
await runDeployMerge(state);
}
} catch (error) {
if (error instanceof MergeConflictError) {
printConflictGuidance(error.branchName);
} else if (error instanceof PromptAbortedError) {
log('\n已取消操作\n', 'yellow');
} else {
log(`\n错误: ${error.message}\n`, 'red');
}
process.exitCode = error instanceof PromptAbortedError ? 0 : 1;
} finally {
if (state.shouldRestoreWorkspace) {
restoreWorkspace(state);
}
}
}
run();

@ -332,6 +332,7 @@ const validateBasicParams = (nodeData: any): string[] => {
// 检查输入参数的完整性
if (nodeData.parameters?.dataIns) {
console.log("nodeData.parameters.dataIns", nodeData.parameters);
nodeData.parameters.dataIns.forEach((param: any, index: number) => {
if (!param.id) {
errors.push(`${index + 1}个输入参数的标识不能为空`);

@ -67,35 +67,51 @@ export const startStatusMap = startStatusDic.reduce((obj, item) => {
/**
*
* BUILT_DOING, BUILT_IGNORE,BUILT_DOCKER,BUILT_FAIL
* BUILT_IDLE
* BUILT_RUNNING
* BUILT_SUCCESS
* BUILT_FAILED
* BUILT_CANCELED
* BUILT_NEVER
*/
export const compileStatusConstant = {
BUILT_DOING: 'built-doing',
BUILT_IGNORE: 'built-ignore',
BUILT_DOCKER: 'built-docker',
BUILT_FAIL: 'built-fail',
// 前端单独使用
NOT_COMPILED: 'not-compiled',
get(type) {
return compileStatusConstant[type] || compileStatusConstant.NOT_COMPILED;
}
BUILT_IDLE: 'idle',
BUILT_RUNNING: 'running',
BUILT_SUCCESS: 'success',
BUILT_FAILED: 'failed',
BUILT_CANCELED: 'canceled',
BUILT_NEVER: 'never',
};
export const compileStatusDic = [
{
label: '编译中',
value: compileStatusConstant.BUILT_DOING
label: '未编译',
value: compileStatusConstant.BUILT_NEVER,
color: 'gray'
},
{
label: '免编译',
value: compileStatusConstant.BUILT_IGNORE
label: '空闲中',
value: compileStatusConstant.BUILT_IDLE,
color: 'blue'
},
{
label: '已编译',
value: compileStatusConstant.BUILT_DOCKER
label: '编译中',
value: compileStatusConstant.BUILT_RUNNING,
color: 'blue'
},
{
label: '编译成功',
value: compileStatusConstant.BUILT_SUCCESS,
color: 'green'
},
{
label: '编译失败',
value: compileStatusConstant.BUILT_FAIL
value: compileStatusConstant.BUILT_FAILED,
color: 'red'
},
{
label: '编译取消',
value: compileStatusConstant.BUILT_CANCELED,
color: 'orange'
}
];
export const compileStatusMap = startStatusDic.reduce((obj, item) => {

@ -63,6 +63,10 @@ import {
buildRuntimeNode,
resolveNodeDefinition,
} from '@/utils/flow/nodeOnboarding';
import {
shouldPersistCanvas,
shouldUseCachedCanvas,
} from '@/utils/flow/canvasCache';
import { Dispatch } from 'redux';
import {
@ -125,6 +129,10 @@ export const useFlowCallbacks = (
return getCurrentAppKey(currentAppData) || initialData?.appId;
}, [initialData]);
const refreshAppList = useCallback(() => {
document.dispatchEvent(new CustomEvent('refreshAppList'));
}, []);
// region 画布操作
// 节点变更处理,添加防抖机制
const onNodesChange = useCallback(
@ -187,6 +195,12 @@ export const useFlowCallbacks = (
if (!sourceNode || !targetNode) {
return;
}
// 不允许链接到自己的节点上
if (sourceNode.id === targetNode.id) {
console.warn("不允许自旋链接");
return;
}
// 获取源节点和目标节点的参数信息
const sourceParams: any = sourceNode.data?.parameters || {};
const targetParams: any = targetNode.data?.parameters || {};
@ -513,8 +527,9 @@ export const useFlowCallbacks = (
// 初始化画布数据
const initializeCanvasData = useCallback(() => {
const appKey = getCurrentFlowAppKey();
if (appKey && canvasDataMap[appKey]) {
const { edges, nodes } = canvasDataMap[appKey];
const cachedCanvas = appKey ? canvasDataMap[appKey] : null;
if (shouldUseCachedCanvas({ cachedCanvas, initialData, useDefault })) {
const { edges, nodes } = cachedCanvas;
setNodes(nodes);
setEdges(edges);
} else {
@ -533,11 +548,19 @@ export const useFlowCallbacks = (
// 标记历史记录已初始化
setHistoryInitialized(true);
}, [initialData, canvasDataMap, getCurrentFlowAppKey]);
}, [initialData, useDefault, canvasDataMap, getCurrentFlowAppKey]);
// 实时更新 canvasDataMap
const updateCanvasDataMapEffect = useCallback(() => {
const appKey = getCurrentFlowAppKey();
if (appKey) {
const { appRuntimeData } = store.getState().ideContainer;
const isCurrentAppRunning =
appKey && appRuntimeData[appKey]?.isRunning;
if (
appKey &&
!isCurrentAppRunning &&
shouldPersistCanvas({ nodes, edges })
) {
updateCanvasDataMapDebounced(
dispatch,
canvasDataMap,
@ -551,7 +574,13 @@ export const useFlowCallbacks = (
return () => {
// 取消防抖函数
};
}, [nodes, edges, dispatch, canvasDataMap, getCurrentFlowAppKey]);
}, [
nodes,
edges,
dispatch,
canvasDataMap,
getCurrentFlowAppKey,
]);
// 关闭编辑弹窗
const closeEditModal = useCallback(() => {
setIsEditModalOpen(false);
@ -1381,6 +1410,7 @@ export const useFlowCallbacks = (
// 更新运行ID
dispatch(updateRuntimeId(res.data));
refreshAppList();
// 开始运行时动画
setEdges((eds) =>
@ -1414,6 +1444,7 @@ export const useFlowCallbacks = (
// 更新运行ID
dispatch(updateRuntimeId(res.data));
refreshAppList();
// 开始运行时动画
setEdges((eds) =>
@ -1442,9 +1473,8 @@ export const useFlowCallbacks = (
} else {
// 特殊停止逻辑,持久化运行的应用使用这里的入参
await stopApp(currentAppData.instanceId);
// 特殊停止完成后触发事件,通知刷新应用列表
document.dispatchEvent(new CustomEvent('refreshAppList'));
}
refreshAppList();
// 重置节点状态
dispatch(resetNodeStatus());
@ -1470,7 +1500,7 @@ export const useFlowCallbacks = (
}
}
},
[getCurrentFlowAppKey]
[getCurrentFlowAppKey, refreshAppList]
);
// 暂停/恢复应用

@ -2,11 +2,58 @@ import { useState, useRef, useEffect, useMemo } from 'react';
import { Node, Edge } from '@xyflow/react';
import { debounce } from 'lodash';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { updateCanvasDataMap } from '@/store/ideContainer';
import { updateCanvasDataMap, updateNodeStatus } from '@/store/ideContainer';
import { getCurrentAppKey } from '@/utils/flow/runtime';
import { getNodeData } from '@/api/appIns';
import { Dispatch } from 'redux';
const getRuntimeNodeStatus = (state: any) => {
switch (state) {
case 0:
case '0':
return 'running';
case 1:
case '1':
return 'success';
case -1:
case '-1':
return 'failed';
default:
return '';
}
};
const collectRuntimeNodes = (runtimeData: any) => {
if (!runtimeData) {
return [];
}
if (Array.isArray(runtimeData)) {
return runtimeData.flatMap((item) => {
if (Array.isArray(item?.nodes)) {
return item.nodes;
}
return item?.nodeId || item?.id ? [item] : [];
});
}
if (Array.isArray(runtimeData?.main?.nodeLogs)) {
return runtimeData.main.nodeLogs;
}
if (Array.isArray(runtimeData?.nodes)) {
return runtimeData.nodes;
}
if (Array.isArray(runtimeData?.data)) {
return collectRuntimeNodes(runtimeData.data);
}
return [];
};
export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
@ -46,6 +93,11 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
// 在组件顶部添加历史记录相关状态
const [historyInitialized, setHistoryInitialized] = useState(false);
const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const syncedRuntimeKeyRef = useRef('');
const currentRunId =
currentAppKey && appRuntimeData[currentAppKey]
? appRuntimeData[currentAppKey].runId
: '';
// 更新节点状态将从store获取的状态应用到节点上
useEffect(() => {
@ -61,7 +113,9 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
Object.values(initialData.components).some((comp: any) => comp.status);
setNodes((prevNodes) => {
return prevNodes.map((node) => {
let hasChanges = false;
const nextNodes = prevNodes.map((node) => {
// 如果是只读模式(历史实例查看),优先使用节点自身的历史状态
// 如果是正常运行模式,只使用运行时状态
let nodeStatus = 'waiting';
@ -80,6 +134,14 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
showStatus = currentAppIsRunning;
}
if (
node.data.status === nodeStatus &&
node.data.isStatusVisible === showStatus
) {
return node;
}
hasChanges = true;
return {
...node,
data: {
@ -89,6 +151,8 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
},
};
});
return hasChanges ? nextNodes : prevNodes;
});
}, [
appRuntimeData,
@ -100,6 +164,70 @@ export const useFlowEditorState = (initialData?: any, readOnly?: boolean) => {
readOnly,
]);
useEffect(() => {
if (
readOnly ||
!currentAppKey ||
!currentAppIsRunning ||
!currentRunId ||
nodes.length === 0
) {
return;
}
const syncKey = `${currentAppKey}:${currentRunId}`;
if (syncedRuntimeKeyRef.current === syncKey) {
return;
}
let canceled = false;
syncedRuntimeKeyRef.current = syncKey;
const syncRuntimeNodeStatus = async () => {
try {
const nodeDataRes: any = await getNodeData(currentRunId);
const runtimeData = nodeDataRes?.data || nodeDataRes;
const runtimeNodes = collectRuntimeNodes(runtimeData);
if (canceled || runtimeNodes.length === 0) {
return;
}
runtimeNodes.forEach((node) => {
const nodeId = node?.nodeId || node?.id;
const status = getRuntimeNodeStatus(node?.state);
if (nodeId && status) {
dispatch(updateNodeStatus({
nodeId,
status,
appId: currentAppKey,
actionType: 'RUNTIME_RECONNECT_SYNC',
}));
}
});
} catch (error) {
if (!canceled) {
syncedRuntimeKeyRef.current = '';
console.error('同步运行实例节点状态失败:', error);
}
}
};
syncRuntimeNodeStatus();
return () => {
canceled = true;
};
}, [
currentAppKey,
currentAppIsRunning,
currentRunId,
dispatch,
nodes.length,
readOnly,
]);
const updateCanvasDataMapDebounced = useRef(
debounce(
(

@ -20,7 +20,7 @@ interface WebSocketHook {
const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
const {
reconnectInterval = 3000,
maxReconnectAttempts = 0,
maxReconnectAttempts = 99,
onOpen,
onClose,
onError,
@ -58,7 +58,8 @@ const useWebSocket = (options: WebSocketOptions = {}): WebSocketHook => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
wsRef.current.send(messageStr);
} else {
}
else {
console.warn('WebSocket is not connected. Cannot send message.');
}
}, []);

@ -25,9 +25,25 @@ interface CollapseListProps {
searchKeyword?: string;
runStatus?: string;
componentClassify?: string;
compileLogsMap: Record<string, string>;
compileStatusMap: Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>;
subscribeInstanceLog: (instanceId: string) => void;
unsubscribeInstanceLog: (instanceId: string) => void;
updateCompileStatus: (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => void;
isWsConnected: boolean;
}
const CollapseList: React.FC<CollapseListProps> = ({ searchKeyword, runStatus, componentClassify }) => {
const CollapseList: React.FC<CollapseListProps> = ({
searchKeyword,
runStatus,
componentClassify,
compileLogsMap,
compileStatusMap,
subscribeInstanceLog,
unsubscribeInstanceLog,
updateCompileStatus,
isWsConnected
}) => {
const [collapses, setCollapses] = useState([]);
const [visible, setVisible] = useState(false);
const [showOffSaleModal, setShowOffSaleModal] = useState(false);
@ -258,7 +274,15 @@ const CollapseList: React.FC<CollapseListProps> = ({ searchKeyword, runStatus, c
name={index.toString()}
extra={extraNode(item)}
>
<ListNode componentData={item} />
<ListNode
componentData={item}
compileLogsMap={compileLogsMap}
compileStatusMap={compileStatusMap}
subscribeInstanceLog={subscribeInstanceLog}
unsubscribeInstanceLog={unsubscribeInstanceLog}
updateCompileStatus={updateCompileStatus}
isWsConnected={isWsConnected}
/>
</CollapseItem>
))}
</Collapse>

@ -1,11 +1,14 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import styles from './style/index.module.less';
import { Button, Input, Modal, Radio, Space, Select } from '@arco-design/web-react';
import { Button, Input, Modal, Radio, Space, Select, Message } from '@arco-design/web-react';
import { IconSearch } from '@arco-design/web-react/icon';
import CollapseList from './collapseList';
import { startStatusConstant } from '@/const/isdp/componentDeploy';
import ConfigTutorial from '@/pages/componentDevelopment/componentEnv/configTutorial';
import { getComponentClassify } from '@/api/componentClassify';
import useWebSocket from '@/hooks/useWebSocket';
import { getToken } from '@/utils/auth';
import { isJSON } from '@/utils/common';
const ComponentDeployment = () => {
const [searchKeyword, setSearchKeyword] = useState('');
@ -15,6 +18,74 @@ const ComponentDeployment = () => {
const [selectedStatus, setSelectedStatus] = useState<string | undefined>(undefined);
const [tutorialVisible, setTutorialVisible] = useState(false);
// 编译日志相关状态 - 按实例 ID 分组存储
const [compileLogsMap, setCompileLogsMap] = useState<Record<string, string>>({});
// 编译状态管理 - 按实例 ID 存储状态
const [compileStatusMap, setCompileStatusMap] = useState<Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>>({});
const compileLogSequenceRef = useRef<Record<string, number>>({});
const currentCompileIdRef = useRef<string | null>(null);
// WebSocket 连接 - 用于接收编译日志
const { connect: wsConnect, disconnect: wsDisconnect, sendMessage, isConnected } = useWebSocket({
onMessage: (event) => {
const id = currentCompileIdRef.current;
if (!id) return;
try {
const currentSequence = (compileLogSequenceRef.current[id] || 0) + 1;
compileLogSequenceRef.current[id] = currentSequence;
if (isJSON(event.data)) {
const parseData = JSON.parse(event.data);
const message = parseData.message || parseData.line || event.data;
const sequenceMessage = `${currentSequence}. ${message}`;
setCompileLogsMap(prev => ({
...prev,
[id]: prev[id] ? prev[id] + '\n' + sequenceMessage : sequenceMessage
}));
}
else {
const data = `${currentSequence}. ${event.data}`;
setCompileLogsMap(prev => ({
...prev,
[id]: prev[id] ? prev[id] + '\n' + data : data
}));
}
} catch (error) {
console.error('解析 WebSocket 消息失败:', error);
}
},
onOpen: () => {
console.log('编译日志 WebSocket 连接成功');
},
onError: (error) => {
console.error('编译日志 WebSocket 错误:', error);
},
onClose: () => {
console.log('编译日志 WebSocket 连接已关闭');
}
});
// 页面挂载时连接 WebSocket
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
let wsUrl;
if (window.location.host.includes('localhost')) {
wsUrl = `${process.env.NEXT_PUBLIC_DEV_SOCKET_HOST}/ws/v1/bpms-workbench/instance-log?Authorization=Bearer ${getToken()}`;
}
else {
wsUrl = `${protocol}//${host}/ws/v1/bpms-workbench/instance-log?Authorization=Bearer ${getToken()}`;
}
wsConnect(wsUrl);
return () => {
wsDisconnect();
};
}, []);
useEffect(() => {
const timer = setTimeout(() => setDebouncedKeyword(searchKeyword), 500);
return () => clearTimeout(timer);
@ -28,6 +99,57 @@ const ComponentDeployment = () => {
});
}, []);
// 订阅实例编译日志
const subscribeInstanceLog = (instanceId: string) => {
currentCompileIdRef.current = instanceId;
compileLogSequenceRef.current[instanceId] = 0;
// 清空该实例之前的日志
setCompileLogsMap(prev => ({
...prev,
[instanceId]: ''
}));
// 设置编译状态为 running
setCompileStatusMap(prev => ({
...prev,
[instanceId]: 'running'
}));
if (isConnected) {
sendMessage({
instanceId,
type: 'subscribe'
});
}
else {
Message.warning('WebSocket 未连接,无法实时接收日志');
}
};
// 取消订阅实例编译日志
const unsubscribeInstanceLog = (instanceId: string) => {
if (isConnected && instanceId) {
sendMessage({
instanceId,
type: 'unsubscribe'
});
}
if (currentCompileIdRef.current === instanceId) {
currentCompileIdRef.current = null;
}
};
// 更新编译状态
const updateCompileStatus = (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => {
setCompileStatusMap(prev => ({
...prev,
[instanceId]: status
}));
};
// 状态选项配置
const statusOptions = [
{ label: '全部', value: undefined },
@ -88,8 +210,17 @@ const ComponentDeployment = () => {
</Space>
</div>
<div className={styles['content']}>
<CollapseList searchKeyword={debouncedKeyword} runStatus={selectedStatus}
componentClassify={selectedClassify} />
<CollapseList
searchKeyword={debouncedKeyword}
runStatus={selectedStatus}
componentClassify={selectedClassify}
compileLogsMap={compileLogsMap}
compileStatusMap={compileStatusMap}
subscribeInstanceLog={subscribeInstanceLog}
unsubscribeInstanceLog={unsubscribeInstanceLog}
updateCompileStatus={updateCompileStatus}
isWsConnected={isConnected}
/>
</div>
</div>

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Space,
@ -22,20 +22,41 @@ import {
refreshInstanceDependency,
getComponentResource
} from '@/api/componentInstance';
import { runStatusConstant, runStatusDic, runTypeConstant, runTypeDic } from '@/const/isdp/componentDeploy';
import {
compileStatusDic,
runStatusConstant,
runStatusDic,
runTypeConstant,
runTypeDic
} from '@/const/isdp/componentDeploy';
import dayjs from 'dayjs';
import EditInstanceModal from './editInstanceModal';
import EnvConfigModal from './envConfigModal';
import ResourceMonitorModal from '@/components/ResourceMonitorModal';
import { getToken } from '@/utils/auth';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
interface ListNodeProps {
componentData: any; // 组件数据
compileLogsMap: Record<string, string>;
compileStatusMap: Record<string, 'idle' | 'running' | 'success' | 'failed' | 'canceled'>;
subscribeInstanceLog: (instanceId: string) => void;
unsubscribeInstanceLog: (instanceId: string) => void;
updateCompileStatus: (instanceId: string, status: 'idle' | 'running' | 'success' | 'failed' | 'canceled') => void;
isWsConnected: boolean;
}
const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const ListNode: React.FC<ListNodeProps> = ({
componentData,
compileLogsMap,
compileStatusMap,
subscribeInstanceLog,
unsubscribeInstanceLog,
updateCompileStatus,
isWsConnected
}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshingIds, setRefreshingIds] = useState<Set<string>>(new Set()); // 记录正在刷新的实例ID
@ -65,6 +86,10 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const [resourceModalVisible, setResourceModalVisible] = useState(false);
const [resourceData, setResourceData] = useState<any>(null);
// 编译日志 Modal 相关状态
const [compileModalVisible, setCompileModalVisible] = useState(false);
const [compileInstance, setCompileInstance] = useState<any>(null);
// 获取实例列表
const fetchInstanceList = async () => {
if (!componentData?.identifier) return;
@ -164,35 +189,20 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
// 添加到刷新中的集合
setRefreshingIds(prev => new Set(prev).add(record.id));
// 显示提示消息
const messageKey = `refresh_${record.id}`;
Message.loading({
id: messageKey,
content: '正在编译组件,请稍候...',
duration: 0 // 不自动关闭
});
try {
// 调用编译接口
const res: any = await refreshInstanceDependency(record.id);
// 关闭 loading 消息
Message.clear();
if (res.code === 200 && res.data) {
Message.success('依赖刷新成功');
// 刷新列表
fetchInstanceList();
if (res.code === 200) {
Message.success('编译任务已提交');
}
else {
Message.error(res.msg || '依赖刷新失败');
Message.error(res.msg || '编译失败');
}
} catch (error) {
// 关闭 loading 消息
Message.clear();
console.error('编译组件失败:', error);
Message.error('依赖刷新失败,请稍后重试');
Message.error('编译组件失败,请稍后重试');
} finally {
// 从刷新中的集合移除
setRefreshingIds(prev => {
const newSet = new Set(prev);
newSet.delete(record.id);
@ -201,6 +211,27 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
}
};
// 打开编译日志 Modal
const handleOpenCompileLog = (record) => {
setCompileInstance(record);
setCompileModalVisible(true);
// 通过父组件传递的方法订阅该实例的编译日志
subscribeInstanceLog(record.id);
};
// 关闭编译日志 Modal
const handleCloseCompileModal = () => {
setCompileModalVisible(false);
// 取消订阅当前实例的日志
if (compileInstance?.id) {
unsubscribeInstanceLog(compileInstance.id);
}
setCompileInstance(null);
};
// 打开日志 Modal
const handleOpenLog = (record) => {
setCurrentInstance(record);
@ -278,7 +309,6 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
// 处理编辑实例确定
const handleEditOk = async (values: any) => {
try {
console.log('更新实例信息:', { ...editingInstance, ...values });
Message.success('更新成功');
setEditModalVisible(false);
setEditingInstance(null);
@ -319,7 +349,8 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
if (res.code === 200 && res.data) {
setResourceData(res.data);
setResourceModalVisible(true);
} else {
}
else {
Message.error(res.msg || '获取资源信息失败');
}
} catch (error) {
@ -338,7 +369,17 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
{
title: '组件标识',
dataIndex: 'identifier',
align: 'center'
align: 'center',
render: (identifier, record) => {
return (
<span
style={{ color: '#3491FA', cursor: 'pointer' }}
onClick={() => handleOpenEdit(record)}
>
{identifier}
</span>
);
}
},
{
title: '实例名称',
@ -355,7 +396,7 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
}
},
{
title: '运行状态',
title: '实例运行状态',
dataIndex: 'runStatus',
align: 'center',
render: (runStatus) => {
@ -363,6 +404,15 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
return item ? <Tag color={item.color}>{item.label}</Tag> : '-';
}
},
{
title: '实例编译状态',
dataIndex: 'currentTaskStatus',
align: 'center',
render: (currentTaskStatus) => {
const item = compileStatusDic.find(d => d.value === currentTaskStatus);
return item ? <Tag color={item.color}>{item.label}</Tag> : '-';
}
},
{
title: '创建时间',
dataIndex: 'createTime',
@ -381,6 +431,11 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
const isRefreshing = refreshingIds.has(record.id);
const isLocalRun = record.runType === 'LOCAL';
// 获取当前实例的编译状态
const compileStatus = compileStatusMap[record.currentTaskStatus] || 'idle';
// 编译状态为 running、failed、canceled 时不显示启停按钮
const hideStartStopButtons = compileStatus === 'running' || compileStatus === 'failed' || compileStatus === 'canceled';
return (
<div className={styles['table-handle-box']}>
<Space size={20}>
@ -394,21 +449,23 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
>
</Button>
<Button
type="text"
onClick={() => handleOpenCompileLog(record)}
>
</Button>
<Button type="text" onClick={() => handleOpenLog(record)}></Button>
</>
)}
{!isRunning && (
<Button type="text" onClick={() => handleOpenEnvConfig(record)}></Button>
)}
{!isLocalRun && isRunning && (
<Button type="text" onClick={() => handeViewResource(record)}></Button>
)}
<Button type="text"
onClick={() => handleOpenEdit(record)}
icon={<img
src={'/icons/editIcon.png'}
style={{ width: 16, height: 16, marginRight: 5, verticalAlign: 'middle' }} />}
></Button>
{/*{!isLocalRun && isRunning && (*/}
{/* <Button type="text" onClick={() => handeViewResource(record)}>组件资源</Button>*/}
{/*)}*/}
{!hideStartStopButtons && (
<>
{isRunning ? (
<Button
type="text"
@ -434,6 +491,8 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
loading={startingId === record.id && startLoading}
></Button>
)}
</>
)}
{!isRunning && (
<Button
type="text"
@ -518,6 +577,26 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
/>
</Modal>
{/* 编译日志 Modal */}
<Modal
title={`编译日志 - ${compileInstance?.name || compileInstance?.identifier || ''}`}
visible={compileModalVisible}
onCancel={handleCloseCompileModal}
footer={null}
style={{ width: '70%' }}
>
<TextArea
value={compileLogsMap[compileInstance?.id] || ''}
readOnly
placeholder="等待编译输出..."
style={{
minHeight: 400,
fontFamily: 'monospace',
fontSize: 13
}}
/>
</Modal>
{/* 编辑实例信息 Modal */}
<EditInstanceModal
visible={editModalVisible}

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Input, Message } from '@arco-design/web-react';
import EditableTable from '@/pages/componentDevelopment/componentList/editableTable';
import EditableTable, { validateEditableTableData } from '@/pages/componentDevelopment/componentList/editableTable';
import { updateComponentDesign } from '@/api/componentDevelopProcess';
const FormItem = Form.Item;
@ -18,10 +18,12 @@ const AddApiModal = ({
const [form] = Form.useForm();
const [parametersData, setParametersData] = useState([]);
const [responsesData, setResponsesData] = useState([]);
const [showTableValidationErrors, setShowTableValidationErrors] = useState(false);
// 当 visible 或 componentDesignProgress 变化时,设置表单初始值
useEffect(() => {
if (visible && componentDesignProgress) {
setShowTableValidationErrors(false);
// 设置表单字段值
form.setFieldsValue({
ident: componentDesignProgress.ident || '',
@ -51,6 +53,7 @@ const AddApiModal = ({
}
}
else if (visible) {
setShowTableValidationErrors(false);
// 重置表单和表格数据
form.resetFields();
setParametersData([]);
@ -62,6 +65,15 @@ const AddApiModal = ({
try {
await form.validate();
const formData = form.getFields();
const parametersValidation = validateEditableTableData(parametersData);
const responsesValidation = validateEditableTableData(responsesData);
if (!parametersValidation.valid || !responsesValidation.valid) {
setShowTableValidationErrors(true);
const firstError = parametersValidation.errors[0] || responsesValidation.errors[0];
Message.error(firstError || '请检查参数填写是否正确');
return;
}
// 构造要提交的数据
const params = {
@ -170,6 +182,7 @@ const AddApiModal = ({
onDataUpdate={setParametersData}
initialData={componentDesignProgress?.parameters || []}
visible={visible}
showValidationErrors={showTableValidationErrors}
/>
</FormItem>
<FormItem label="输出参数" field="responses">
@ -177,6 +190,7 @@ const AddApiModal = ({
onDataUpdate={setResponsesData}
initialData={componentDesignProgress?.responses || []}
visible={visible}
showValidationErrors={showTableValidationErrors}
/>
</FormItem>
</Form>

@ -22,20 +22,70 @@ const arrayTypeOptions = [
{ label: 'OBJECT', value: 'OBJECT' }
];
function EditableCell({ value, onChange, columnType, record, dataIndex }) {
const isEmptyValue = (value) => value === undefined || value === null || `${value}`.trim() === '';
const getFieldError = (record, dataIndex) => {
if (dataIndex === 'ident' && isEmptyValue(record.ident)) {
return '请输入名称';
}
if (dataIndex === 'type' && isEmptyValue(record.type)) {
return '请选择数据类型';
}
if (dataIndex === 'generic' && record.type === 'ARRAY' && isEmptyValue(record.generic)) {
return '请选择数组类型';
}
return '';
};
export const validateEditableTableData = (tableData = []) => {
const errors = [];
tableData.forEach((item, index) => {
const rowIndex = index + 1;
if (isEmptyValue(item.ident)) {
errors.push(`${rowIndex}行请输入名称`);
}
if (isEmptyValue(item.type)) {
errors.push(`${rowIndex}行请选择数据类型`);
}
if (item.type === 'ARRAY' && isEmptyValue(item.generic)) {
errors.push(`${rowIndex}行请选择数组类型`);
}
});
return {
valid: errors.length === 0,
errors
};
};
function EditableCell({ value, onChange, columnType, record, dataIndex, showError, onBlur }) {
const [error, setError] = useState('');
const requiredError = getFieldError(record, dataIndex);
const mergedError = error || (showError ? requiredError : '');
// 对于数组类型字段的特殊处理
if (dataIndex === 'generic') {
// 仅当数据类型为 ARRAY 时才可编辑
if (record.type === 'ARRAY') {
return (
<div>
<Select
value={value}
onChange={onChange}
onBlur={onBlur}
options={arrayTypeOptions}
placeholder="请选择数组类型"
status={mergedError ? 'error' : undefined}
/>
{mergedError && <div style={{ color: '#f53f3f', fontSize: 12, marginTop: 4 }}>{mergedError}</div>}
</div>
);
}
else {
@ -64,10 +114,11 @@ function EditableCell({ value, onChange, columnType, record, dataIndex }) {
<Input
value={value}
onChange={handleIdentChange}
onBlur={onBlur}
placeholder="请输入(仅支持英文字母、数字和下划线)"
status={error ? 'error' : undefined}
status={mergedError ? 'error' : undefined}
/>
{error && <div style={{ color: '#f53f3f', fontSize: 12, marginTop: 4 }}>{error}</div>}
{mergedError && <div style={{ color: '#f53f3f', fontSize: 12, marginTop: 4 }}>{mergedError}</div>}
</div>
);
}
@ -76,6 +127,7 @@ function EditableCell({ value, onChange, columnType, record, dataIndex }) {
<Input
value={value}
onChange={onChange}
onBlur={onBlur}
placeholder="请输入"
/>
);
@ -83,20 +135,39 @@ function EditableCell({ value, onChange, columnType, record, dataIndex }) {
if (columnType === 'select') {
return (
<div>
<Select
value={value}
onChange={onChange}
onBlur={onBlur}
options={dataTypeOptions}
placeholder="请选择数据类型"
status={mergedError ? 'error' : undefined}
/>
{mergedError && <div style={{ color: '#f53f3f', fontSize: 12, marginTop: 4 }}>{mergedError}</div>}
</div>
);
}
return <span>{value}</span>;
}
function EditableTable({ onDataUpdate, initialData = [], visible }) {
function EditableTable({ onDataUpdate, initialData = [], visible, showValidationErrors = false }) {
const [data, setData] = useState([]);
const [touchedFields, setTouchedFields] = useState({});
const getFieldKey = (key, dataIndex) => `${key}-${dataIndex}`;
const markFieldTouched = (key, dataIndex) => {
setTouchedFields(prev => ({
...prev,
[getFieldKey(key, dataIndex)]: true
}));
};
const shouldShowFieldError = (record, dataIndex) => {
return showValidationErrors || touchedFields[getFieldKey(record.key, dataIndex)];
};
// 当初始数据变化时,更新表格数据
useEffect(() => {
@ -112,6 +183,7 @@ function EditableTable({ onDataUpdate, initialData = [], visible }) {
useEffect(() => {
if (!visible) setData([]);
if (!visible) setTouchedFields({});
}, [visible]);
const handleValueChange = (key, dataIndex, value) => {
@ -144,6 +216,8 @@ function EditableTable({ onDataUpdate, initialData = [], visible }) {
columnType="input"
record={record}
dataIndex="ident"
showError={shouldShowFieldError(record, 'ident')}
onBlur={() => markFieldTouched(record.key, 'ident')}
/>
)
},
@ -157,6 +231,8 @@ function EditableTable({ onDataUpdate, initialData = [], visible }) {
columnType="select"
record={record}
dataIndex="type"
showError={shouldShowFieldError(record, 'type')}
onBlur={() => markFieldTouched(record.key, 'type')}
/>
)
},
@ -170,6 +246,8 @@ function EditableTable({ onDataUpdate, initialData = [], visible }) {
columnType="select"
record={record}
dataIndex="generic"
showError={shouldShowFieldError(record, 'generic')}
onBlur={() => markFieldTouched(record.key, 'generic')}
/>
)
},
@ -183,6 +261,8 @@ function EditableTable({ onDataUpdate, initialData = [], visible }) {
columnType="input"
record={record}
dataIndex="desc"
showError={false}
onBlur={() => markFieldTouched(record.key, 'desc')}
/>
)
},

@ -1,20 +1,24 @@
import React, { useState, useRef, useEffect } from 'react';
import { Modal, Button, Message, Divider, Tabs, Checkbox } from '@arco-design/web-react';
import { Modal, Button, Message, Tabs } from '@arco-design/web-react';
import { IconUpload, IconDelete, IconFile } from '@arco-design/web-react/icon';
import styles from './style/importComponentModal.module.less';
interface ParsedComponentInfo {
baseInfo?: any;
operates?: any[];
}
interface ImportComponentModalProps {
visible: boolean;
onCancel: () => void;
onOk: (files: FileItem[]) => void;
onFileSelect: (file: File) => void;
componentInfo: any; // 单次文件解析的结果
onFileSelect: (file: File) => Promise<ParsedComponentInfo[] | ParsedComponentInfo | null>;
loading: boolean;
}
interface FileItem {
file: File;
componentInfo: any[]; // 该文件包含的组件列表
componentInfo: ParsedComponentInfo[]; // 该文件包含的组件列表
}
const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
@ -22,12 +26,10 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
onCancel,
onOk,
onFileSelect,
componentInfo,
loading
}) => {
const [fileItems, setFileItems] = useState<FileItem[]>([]);
const [activeTabKey, setActiveTabKey] = useState<string>('0');
const [pendingFile, setPendingFile] = useState<File | null>(null); // 等待解析的文件
const fileInputRef = useRef<HTMLInputElement>(null);
// 当弹窗关闭时清空所有状态
@ -35,56 +37,60 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
if (!visible) {
setFileItems([]);
setActiveTabKey('0');
setPendingFile(null);
}
}, [visible]);
// 当 componentInfo 更新时,添加到 fileItems`
useEffect(() => {
if (pendingFile && componentInfo) {
const updateFileItem = (file: File, componentInfo: ParsedComponentInfo[] | ParsedComponentInfo) => {
const componentList = Array.isArray(componentInfo) ? componentInfo : [componentInfo];
let nextActiveKey = '0';
// 检查文件是否已存在
const existingIndex = fileItems.findIndex(item => item.file.name === pendingFile.name);
setFileItems((prev) => {
const existingIndex = prev.findIndex((item) => item.file.name === file.name);
if (existingIndex >= 0) {
// 更新现有文件
const newFileItems = [...fileItems];
newFileItems[existingIndex] = {
file: pendingFile,
nextActiveKey = String(existingIndex);
const nextItems = [...prev];
nextItems[existingIndex] = {
file,
componentInfo: componentList
};
setFileItems(newFileItems);
return nextItems;
}
else {
// 添加新文件
const newFileItem: FileItem = {
file: pendingFile,
nextActiveKey = String(prev.length);
return [
...prev,
{
file,
componentInfo: componentList
};
const updatedFileItems = [...fileItems, newFileItem];
setFileItems(updatedFileItems);
setActiveTabKey(String(fileItems.length)); // 切换到新添加的标签页
}
];
});
setPendingFile(null);
}
}, [componentInfo]);
setActiveTabKey(nextActiveKey);
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files || files.length === 0) return;
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(event.target.files ?? []);
if (selectedFiles.length === 0) return;
// 验证文件类型 (.zip)
const file = files[0];
if (!file.name.match(/\.zip$/i)) {
const validFiles = selectedFiles.filter((file) => file.name.match(/\.zip$/i));
const invalidFileCount = selectedFiles.length - validFiles.length;
if (invalidFileCount > 0) {
Message.error('请选择 ZIP 压缩包文件');
event.target.value = '';
return;
}
setPendingFile(file);
onFileSelect(file);
for (const file of validFiles) {
const parsedComponentInfo = await onFileSelect(file);
if (!parsedComponentInfo) {
continue;
}
updateFileItem(file, parsedComponentInfo);
}
event.target.value = '';
};
@ -116,7 +122,6 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
const handleClose = () => {
setFileItems([]);
setActiveTabKey('0');
setPendingFile(null);
onCancel();
};
@ -151,6 +156,7 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
>
<input
ref={fileInputRef}
multiple
type="file"
accept=".zip"
style={{ display: 'none' }}
@ -158,7 +164,7 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
/>
<div className={styles['import-modal-content']}>
{fileItems.length === 0 && !pendingFile && (
{fileItems.length === 0 && (
<div className={styles['empty-state']}>
<p className={styles['empty-text']}>&#34;&#34;</p>
<p className={styles['empty-hint']}> .zip </p>
@ -172,9 +178,9 @@ const ImportComponentModal: React.FC<ImportComponentModalProps> = ({
type="card"
>
{fileItems.map((fileItem, fileIndex) => {
const current = fileItems[activeTabKey];
const baseInfo = current.componentInfo[0].baseInfo;
const operates = current.componentInfo[0].operates;
const currentComponent = fileItem.componentInfo[0];
const baseInfo = currentComponent?.baseInfo || {};
const operates = currentComponent?.operates || [];
return (
<Tabs.TabPane
key={String(fileIndex)}

@ -84,7 +84,6 @@ const GlobalVarContainer = () => {
{ label: 'Python', value: 'Python' }
];
const [importModalVisible, setImportModalVisible] = useState(false); // 导入弹窗
const [importComponentInfo, setImportComponentInfo] = useState(null); // 导入组件信息
const [importLoading, setImportLoading] = useState(false); // 导入加载状态
const [showOffSaleModal, setShowOffSaleModal] = useState(false);
const [offSaleComponent, setOffSaleComponent] = useState(null);
@ -423,16 +422,16 @@ const GlobalVarContainer = () => {
const res: any = await getImportComponentInfo({ file });
if (res.code === 200) {
setImportComponentInfo(res.data);
Message.success('组件信息解析成功');
return res.data;
}
else {
Message.error(res.msg || '解析组件信息失败');
setImportComponentInfo(null);
return null;
}
} catch (error) {
Message.error('解析组件信息失败');
setImportComponentInfo(null);
return null;
} finally {
setImportLoading(false);
}
@ -453,7 +452,6 @@ const GlobalVarContainer = () => {
if (res.code === 200) {
Message.success('组件导入成功');
setImportModalVisible(false);
setImportComponentInfo(null);
fetchComponentData(); // 刷新列表
}
else {
@ -471,7 +469,6 @@ const GlobalVarContainer = () => {
// 关闭导入弹窗
const handleImportCancel = () => {
setImportModalVisible(false);
setImportComponentInfo(null);
};
// 查看审核历史
@ -890,7 +887,6 @@ const GlobalVarContainer = () => {
onCancel={handleImportCancel}
onOk={handleImportConfirm}
onFileSelect={handleImportFileSelect}
componentInfo={importComponentInfo}
loading={importLoading}
/>

@ -111,7 +111,6 @@ const TestInstance = ({ instance, parentId, onBack }: { instance: any; parentId:
else {
// WebSocket连接前置
const res: any = await startTestCase(instance.id);
console.log('res:', res);
// 构建WebSocket URL根据你的实际后端配置调整
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';

@ -61,7 +61,7 @@ function Overview() {
const [loading, setLoading] = useState(true);
const t = useLocale(locale);
const userInfo = useSelector((state: any) => state.userInfo || {});
const userInfo = useSelector((state: any) => state.user?.userInfo || {});
const fetchData = async () => {
setLoading(true);
@ -79,7 +79,7 @@ function Overview() {
<Card>
<Typography.Title heading={5}>
{t['workplace.welcomeBack']}
{userInfo.name}
{userInfo.username}
</Typography.Title>
<Divider />
<Row>

@ -210,15 +210,15 @@ const formatFooter = (data: any, eventListOld = []) => {
if (topic.includes('**empty**')) return '';
return `事件: ${name}`;
}
case 'EVENTSEND_SYNC':
const parsedData1 = isJSON(data.customDef) ? JSON.parse(data.customDef) : null;
if (parsedData1) {
const { eventName } = parsedData1;
return `事件: ${eventName}`;
}
else {
return '';
}
// case 'EVENTSEND_SYNC':
// const parsedData1 = isJSON(data.customDef) ? JSON.parse(data.customDef) : null;
// if (parsedData1) {
// const { eventName } = parsedData1;
// return `事件: ${eventName}`;
// }
// else {
// return '';
// }
case 'BASIC':
case 'BASIC_LOOP':
return data.compIdentifier ? `当前实例:${data.compIdentifier}` : '';

@ -29,7 +29,7 @@ const PaneContextMenu: React.FC<PaneContextMenuProps> = ({
}, []);
// 检查是否有复制的节点数据
const hasCopiedNode = !!localStorage.getItem('copiedNode');
const hasCopiedNode = !!localStorage.getItem('copiedNode') || !!localStorage.getItem('copiedFlowData');
return (
<Menu

@ -12,7 +12,11 @@ import FlowEditorMain from './FlowEditorMain';
import { useFlowEditorState } from '@/hooks/useFlowEditorState';
import { useFlowCallbacks } from '@/hooks/useFlowCallbacks';
const FlowEditorWithProvider: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?: boolean }> = ({ initialData, useDefault, readOnly }) => {
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>
@ -22,7 +26,11 @@ const FlowEditorWithProvider: React.FC<{ initialData?: any, useDefault?: boolean
);
};
const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?: boolean }> = ({ initialData, useDefault, readOnly = false }) => {
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<{
@ -205,30 +213,56 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?:
setMenu(null);
}, [setMenu]);
// 添加处理粘贴事件的函数
// 存储最后的鼠标位置
const lastMousePositionRef = useRef<{ x: number; y: number } | null>(null);
// 跟踪鼠标位置
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
const pane = reactFlowWrapper.current?.getBoundingClientRect();
if (pane && reactFlowInstance) {
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
lastMousePositionRef.current = position;
}
};
const canvasElement = reactFlowWrapper.current;
if (canvasElement) {
canvasElement.addEventListener('mousemove', handleMouseMove);
}
return () => {
if (canvasElement) {
canvasElement.removeEventListener('mousemove', handleMouseMove);
}
};
}, [reactFlowInstance]);
// 处理粘贴事件
const handlePasteNode = useCallback((event: ClipboardEvent) => {
// 检查是否是粘贴操作且在画布区域内
// 右键菜单触发的粘贴(有menu.position)
if (event.clipboardData?.getData('text') === 'paste-node' && menu?.type === 'pane' && menu.position) {
pasteNode(menu.position);
setMenu(null);
}
}, [menu, pasteNode]);
}, [menu, pasteNode, setMenu]);
// 添加键盘事件监听
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// 检查是否按下了 Ctrl+V (Windows/Linux) 或 Cmd+V (Mac)
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
// 触发自定义粘贴事件
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: new DataTransfer()
});
pasteEvent.clipboardData?.setData('text', 'paste-node');
document.dispatchEvent(pasteEvent);
// 键盘快捷键粘贴,使用最后的鼠标位置
if (lastMousePositionRef.current) {
pasteNode(lastMousePositionRef.current);
}
}
};
// 为画布添加键盘事件监听
// 为画布添加键盘和粘贴事件监听
const canvasElement = reactFlowWrapper.current;
if (canvasElement) {
canvasElement.addEventListener('keydown', handleKeyDown);
@ -241,7 +275,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean, readOnly?:
}
document.removeEventListener('paste', handlePasteNode);
};
}, [handlePasteNode]);
}, [pasteNode, handlePasteNode]);
// 监听边的变化,处理添加节点的触发
useEffect(() => {

@ -84,6 +84,7 @@ const imageParameters = {
defaultValue: ''
}],
dataIns: [{
id: 'in',
name: 'in',
desc: 'url',
dataType: 'STRING',

@ -1,8 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { updateSocketId, updateNodeStatus, updateEventListOld } from '@/store/ideContainer';
import {
updateSocketId,
updateNodeStatus,
updateEventListOld,
updateRuntimeId,
updateIsRunning
} from '@/store/ideContainer';
import useWebSocket from '@/hooks/useWebSocket';
import { isJSON, getUrlParams } from '@/utils/common';
import { buildRuntimeReconnectRequest } from '@/utils/runtimeReconnect';
import styles from './style/index.module.less';
import SideBar from './sideBar';
@ -17,7 +24,7 @@ import {
updateEventList,
updateGlobalVarList
} from '@/store/ideContainer';
import { getAppListBySceneId } from '@/api/apps';
import { getAppListBySceneId, reconnectRun } from '@/api/apps';
import { getProjectComp } from '@/api/scene';
import ProjectContainer from '@/pages/orchestration/project';
@ -57,6 +64,22 @@ const ALL_PATHS = [
'systemResource', 'appGuide'
];
const getRuntimeNodeStatus = (state: any) => {
switch (state) {
case 0:
case '0':
return 'running';
case 1:
case '1':
return 'success';
case -1:
case '-1':
return 'failed';
default:
return '';
}
};
function IDEContainer() {
const [selected, setSelected] = useState<Selected>({});
const [urlParams, setUrlParams] = useState<UrlParamsOptions>({});
@ -64,9 +87,10 @@ function IDEContainer() {
// 用于跟踪已打开的tab保持组件状态
const [openedTabs, setOpenedTabs] = useState<Set<string>>(new Set());
const [subMenuData, setSubMenuData] = useState<any>({});
const { menuData, flowData, currentAppData } = useSelector((state) => state.ideContainer);
const { menuData, flowData, currentAppData, socketId } = useSelector((state) => state.ideContainer);
const dispatch = useDispatch();
const navBarRef = useRef<NavBarRef>(null);
const lastReconnectKeyRef = useRef('');
// 初始化WebSocket hook
const ws = useWebSocket({
@ -86,25 +110,12 @@ function IDEContainer() {
// 处理节点状态更新
if (socketMessage?.nodeLog) {
const { nodeId, state, runLog, appId } = socketMessage.nodeLog;
// 将状态映射为前端使用的状态
let status = 'waiting';
switch (state) {
case 0: // 运行中
status = 'running';
break;
case 1: // 运行成功
status = 'success';
break;
case -1: // 运行失败
status = 'failed';
break;
default:// 等待运行
status = 'waiting';
break;
}
const status = getRuntimeNodeStatus(state);
// 更新节点状态使用特殊的actionType标记这是运行时状态更新
// 如果后端提供了 appId则传递给 action 以确保更新正确的应用状态
if (nodeId && status) {
dispatch(updateNodeStatus({ nodeId, status, appId, actionType: 'RUNTIME_UPDATE' }));
}
// 只有当存在runLog时才发送日志到logBar
if (runLog) {
@ -122,6 +133,52 @@ function IDEContainer() {
}
});
useEffect(() => {
const reconnectRequest = buildRuntimeReconnectRequest({
app: currentAppData,
socketId,
lastReconnectKey: lastReconnectKeyRef.current
});
if (!reconnectRequest) {
return;
}
let canceled = false;
lastReconnectKeyRef.current = reconnectRequest.reconnectKey;
const reconnectRuntime = async () => {
try {
dispatch(updateRuntimeId(reconnectRequest.instanceId));
dispatch(updateIsRunning(true));
const res: any = await reconnectRun({
instanceId: reconnectRequest.instanceId,
newSocketId: reconnectRequest.newSocketId
});
const reconnectResult = res?.data || res;
if (!canceled && reconnectResult?.success === false) {
lastReconnectKeyRef.current = '';
Message.error(reconnectResult.message || '运行实例重连失败');
return;
}
} catch (error) {
if (!canceled) {
lastReconnectKeyRef.current = '';
console.error('运行实例重连失败:', error);
Message.error('运行实例重连失败');
}
}
};
reconnectRuntime();
return () => {
canceled = true;
};
}, [currentAppData, socketId, dispatch]);
// 监听自定义事件,处理打开子节点标签页的逻辑
useEffect(() => {
const handleOpenSubNodeTab = async (event: CustomEvent) => {

@ -365,7 +365,18 @@ const Market: React.FC<MarketProps> = ({ updateProjectComp }) => {
<div className={styles['component-list']}>
<div className={styles['component-info']}>
<img src="/icons/compIcon.png" alt="" />
<span>{component.label || component.flowName}</span>
<div className={styles['component-text']}>
<div className={styles['component-header']}>
<span className={styles['component-flowName']}>{component.label || component.flowName}</span>
{component?.comp?.codeLanguage && (
<Tag size="small" color="arcoblue" className={styles['component-language-tag']}>
{component.comp.codeLanguage}
</Tag>
)}
</div>
<span className={styles['component-identifier']}>{component?.comp?.identifier}</span>
<span className={styles['component-author']}>{component?.author}</span>
</div>
</div>
{/*两种状态 未添加的是primary 已添加的是secondary*/}
{

@ -44,16 +44,57 @@
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding-top: 10px;
.component-info {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
img {
width: 40px;
height: 40px;
margin-right: 10px;
width: 45px;
height: 45px;
margin-right: 15px;
flex-shrink: 0;
}
.component-text {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.component-header {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
}
.component-flowName {
min-width: 0;
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.component-language-tag {
flex-shrink: 0;
}
.component-identifier,
.component-author {
font-size: 12px;
color: #adadad;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

@ -97,6 +97,9 @@ const HandleModal = ({ visible, onChangeVisible, onRefresh }) => {
if (value.includes('**empty**')) {
return cb('非法事件标识,请重新输入');
}
if (!/^[A-Za-z0-9_]+$/.test(value)) {
return cb('事件标识只能输入英文字母、数字和下划线');
}
return cb();
}
@ -232,7 +235,7 @@ const EventContainer = () => {
title="删除事件"
content="事件删除后无法恢复,请谨慎删除!"
onOk={async () => {
const res: any = await deleteEventItem(record.eventId);
const res: any = await deleteEventItem(record.topicId);
if (res && res.code === 200) {
Message.success('删除成功');
fetchEventData();

@ -151,6 +151,9 @@ const AddGlobalVarModal = ({ visible, onOk, onChangeVisible, form }) => {
if (!value) {
return cb('请填写变量名');
}
if (!/^[A-Za-z0-9_]+$/.test(value)) {
return cb('变量名只能输入英文字母、数字和下划线');
}
return cb();
}

@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
export interface UserState {
userInfo?: {
name?: string;
username?: string;
avatar?: string;
job?: string;
organization?: string;

@ -4,6 +4,28 @@ import LoopNode from '@/components/FlowEditor/node/loopNode/LoopNode';
import { updateEventNodeList } from '@/store/ideContainer';
import { resolveNodeComponent } from '@/utils/flow/nodeRegistry';
const runtimeToEditorNodeTypeMap: Record<string, string> = {
SHOW_IMAGE: 'IMAGE',
SHOW_RESULT: 'RESULT',
JSON_CONVERT: 'JSONCONVERT',
};
const editorToRuntimeNodeTypeMap: Record<string, string> = {
IMAGE: 'SHOW_IMAGE',
RESULT: 'SHOW_RESULT',
JSONCONVERT: 'JSON_CONVERT',
};
export const toEditorNodeType = (type?: string) => {
if (!type) return type;
return runtimeToEditorNodeTypeMap[type] || type;
};
export const toRuntimeComponentType = (type?: string) => {
if (!type) return type;
return editorToRuntimeNodeTypeMap[type] || type;
};
/**
* flow editor nodes edges
* @param flowData -
@ -117,19 +139,19 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
// 确定节点类型
let nodeType = 'BASIC';
const componentType = toEditorNodeType(nodeConfig.component?.type);
if (nodeId.includes('start')) {
nodeType = 'start';
} else if (nodeId.includes('end')) {
nodeType = 'end';
} else if (
nodeConfig.component?.type === 'LOOP_START' ||
nodeConfig.component?.type === 'LOOP_END'
componentType === 'LOOP_START' ||
componentType === 'LOOP_END'
) {
nodeType = 'LOOP';
} else {
nodeType = nodeConfig.component?.type || 'BASIC';
nodeType = componentType || 'BASIC';
}
// 解析位置信息
const position = nodeConfig.position || { x: 0, y: 0 };
@ -143,16 +165,16 @@ export const convertFlowData = (flowData: any, useDefault = true) => {
parameters: {
apiIns: getNodeApiIns(nodeId, nodeConfig, currentProjectCompData),
apiOuts: getNodeApiOuts(nodeId, nodeConfig, currentProjectCompData),
dataIns: nodeConfig.dataIns || [],
dataIns: getNodeDataIns(nodeConfig),
dataOuts: nodeConfig.dataOuts || [],
},
type: nodeConfig.component?.type || nodeType,
type: componentType || nodeType,
},
};
// 添加组件标识信息
if (nodeConfig.component) {
node.data.component = { ...nodeConfig.component };
node.data.component = { ...nodeConfig.component, type: componentType };
node.data.compId = nodeConfig.component.compId;
}
@ -471,7 +493,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
// 处理 component 信息
if (node.data?.component) {
nodeConfig.component = {
type: nodeType,
type: toRuntimeComponentType(nodeType),
compIdentifier: node.data.component.compIdentifier || '',
compInstanceIdentifier:
node.data.component.compInstanceIdentifier || '',
@ -482,7 +504,7 @@ export const revertFlowData = (nodes: any[], edges: any[]) => {
} else if (nodeType !== 'start' && nodeType !== 'end') {
// 对于非 start/end 节点,添加基本的 component 信息
nodeConfig.component = {
type: nodeType,
type: toRuntimeComponentType(nodeType),
};
}
if (['BASIC', 'SUB'].includes(nodeType))
@ -625,10 +647,13 @@ export const reverseConvertFlowData = (
}),
};
} else if (node.data?.component) {
nodeConfig.component = { ...node.data.component };
nodeConfig.component = {
...node.data.component,
type: toRuntimeComponentType(node.data.component.type || node.type),
};
} else {
nodeConfig.component = {
type: node.type,
type: toRuntimeComponentType(node.type),
};
}
@ -725,6 +750,27 @@ export const reverseConvertFlowData = (
return flowData;
};
// 获取节点的数据输入参数
const getNodeDataIns = (nodeConfig: any) => {
if (Array.isArray(nodeConfig.dataIns) && nodeConfig.dataIns.length > 0) {
return nodeConfig.dataIns;
}
if (nodeConfig.component?.type === 'EVENTSEND_SYNC') {
return [
{
arrayType: null,
dataType: 'STRING',
defaultValue: '',
desc: '输入',
id: 'in',
},
];
}
return [];
};
// 获取节点的API输入参数
const getNodeApiIns = (
nodeId: string,

@ -0,0 +1,48 @@
function hasObjectValues(value) {
return Boolean(value && typeof value === 'object' && Object.keys(value).length > 0);
}
function hasInitialCanvasData(initialData, useDefault) {
if (!initialData) {
return false;
}
if (Array.isArray(initialData)) {
return initialData.length > 0;
}
if (useDefault) {
return hasObjectValues(initialData?.main?.components) || hasObjectValues(initialData?.components);
}
return hasObjectValues(initialData);
}
function hasCanvasContent(canvas) {
return Boolean(
canvas &&
((Array.isArray(canvas.nodes) && canvas.nodes.length > 0) ||
(Array.isArray(canvas.edges) && canvas.edges.length > 0))
);
}
function shouldUseCachedCanvas({ cachedCanvas, initialData, useDefault }) {
if (!cachedCanvas) {
return false;
}
if (hasCanvasContent(cachedCanvas)) {
return true;
}
return !hasInitialCanvasData(initialData, useDefault);
}
function shouldPersistCanvas({ nodes, edges }) {
return hasCanvasContent({ nodes, edges });
}
module.exports = {
shouldUseCachedCanvas,
shouldPersistCanvas,
};

@ -0,0 +1,28 @@
function isScheduledRunning(app) {
return app && (app.scheduled === 1 || app.scheduled === '1' || app.scheduled === true);
}
function buildRuntimeReconnectRequest({ app, socketId, lastReconnectKey }) {
const instanceId = app && app.instanceId ? String(app.instanceId) : '';
const newSocketId = socketId ? String(socketId) : '';
const appKey = app && (app.id || app.key) ? String(app.id || app.key) : '';
if (!isScheduledRunning(app) || !instanceId || !newSocketId || !appKey) {
return null;
}
const reconnectKey = `${appKey}:${instanceId}:${newSocketId}`;
if (reconnectKey === lastReconnectKey) {
return null;
}
return {
instanceId,
newSocketId,
reconnectKey,
};
}
module.exports = {
buildRuntimeReconnectRequest,
};
Loading…
Cancel
Save