477 lines
18 KiB
TypeScript
477 lines
18 KiB
TypeScript
import React, {useState, useEffect} from 'react';
|
|
import {PageContainer} from '@ant-design/pro-layout';
|
|
import {Button, Card, Form, Input, InputNumber, Select, Switch, Space, Menu, Tabs, Row, Col, message, ColorPicker} from 'antd';
|
|
import type {NodeDesignDataResponse} from './types';
|
|
import * as service from './service';
|
|
|
|
// Tab 配置
|
|
const TAB_CONFIG = [
|
|
{key: 'panel', label: '属性(预览)', schemaKey: 'panelVariablesSchema', readonly: true},
|
|
{key: 'local', label: '环境变量(预览)', schemaKey: 'localVariablesSchema', readonly: true},
|
|
{key: 'form', label: '表单(预览)', schemaKey: 'formVariablesSchema', readonly: true},
|
|
{key: 'ui', label: 'UI配置', schemaKey: 'uiVariables', readonly: false}
|
|
];
|
|
|
|
// 渲染具体的表单控件
|
|
const renderField = (schema: any, fieldPath: string) => {
|
|
const commonProps = {
|
|
placeholder: `请输入${schema.title}`,
|
|
};
|
|
|
|
// 颜色字段路径列表
|
|
const colorFields = [
|
|
'ports.groups.in.attrs.circle.fill',
|
|
'ports.groups.in.attrs.circle.stroke',
|
|
'ports.groups.out.attrs.circle.fill',
|
|
'ports.groups.out.attrs.circle.stroke',
|
|
'style.fill',
|
|
'style.stroke',
|
|
'style.iconColor'
|
|
];
|
|
|
|
// 检查是否是颜色字段
|
|
if (colorFields.some(path => fieldPath.endsWith(path))) {
|
|
return <ColorPicker showText/>;
|
|
}
|
|
|
|
if (schema.enum) {
|
|
return (
|
|
<Select
|
|
{...commonProps}
|
|
options={schema.enum.map((value: string, index: number) => ({
|
|
label: schema.enumNames?.[index] || value,
|
|
value
|
|
}))}
|
|
/>
|
|
);
|
|
}
|
|
|
|
switch (schema.type) {
|
|
case 'string':
|
|
return <Input {...commonProps} />;
|
|
case 'integer':
|
|
case 'number':
|
|
return <InputNumber {...commonProps} style={{width: '100%'}}/>;
|
|
case 'boolean':
|
|
return <Switch/>;
|
|
default:
|
|
return <Input {...commonProps} />;
|
|
}
|
|
};
|
|
|
|
// 表单渲染器组件
|
|
const FormRenderer: React.FC<{
|
|
schema: any;
|
|
path?: string;
|
|
readOnly?: boolean;
|
|
}> = ({schema, path = '', readOnly = false}) => {
|
|
if (!schema || !schema.properties) return null;
|
|
const renderPortConfig = (portSchema: any, portPath: string) => {
|
|
if (!portSchema || !portSchema.properties) return null;
|
|
|
|
const {position, attrs} = portSchema.properties;
|
|
return (
|
|
<>
|
|
{/* 先渲染端口位置 */}
|
|
<Form.Item
|
|
name={`${portPath}.position`}
|
|
label={
|
|
<span style={{fontSize: '14px'}}>
|
|
{position.title}
|
|
</span>
|
|
}
|
|
tooltip={position.description}
|
|
required={portSchema.required?.includes('position')}
|
|
initialValue={'default' in position ? position.default : undefined}
|
|
style={{marginBottom: 16}}
|
|
>
|
|
{readOnly ? (
|
|
<span style={{color: '#666'}}>{position.default || '-'}</span>
|
|
) : (
|
|
renderField(position, `${portPath}.position`)
|
|
)}
|
|
</Form.Item>
|
|
|
|
{/* 再渲染端口属性 */}
|
|
{attrs && (
|
|
<Card
|
|
title={attrs.title}
|
|
size="small"
|
|
style={{
|
|
marginBottom: 16,
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
|
borderRadius: '8px',
|
|
border: '1px solid #f0f0f0'
|
|
}}
|
|
styles={{
|
|
body: {padding: '16px'}
|
|
}}
|
|
>
|
|
<FormRenderer
|
|
schema={attrs}
|
|
path={`${portPath}.attrs`}
|
|
readOnly={readOnly}
|
|
/>
|
|
</Card>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{padding: '8px 0'}}>
|
|
{Object.entries(schema.properties).map(([key, value]: [string, any]) => {
|
|
const fieldPath = path ? `${path}.${key}` : key;
|
|
if (value.type === 'object') {
|
|
// 特殊处理端口组配置
|
|
if (key === 'groups' && path.endsWith('ports')) {
|
|
const inPort = value.properties?.in;
|
|
const outPort = value.properties?.out;
|
|
|
|
return (
|
|
<Row key={fieldPath} gutter={16}>
|
|
<Col span={12}>
|
|
<Card
|
|
title={inPort.title}
|
|
size="small"
|
|
style={{
|
|
marginBottom: 16,
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
|
borderRadius: '8px',
|
|
border: '1px solid #f0f0f0'
|
|
}}
|
|
styles={{
|
|
body: {padding: '16px'}
|
|
}}
|
|
>
|
|
{renderPortConfig(inPort, `${fieldPath}.in`)}
|
|
</Card>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Card
|
|
title={outPort.title}
|
|
size="small"
|
|
style={{
|
|
marginBottom: 16,
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
|
borderRadius: '8px',
|
|
border: '1px solid #f0f0f0'
|
|
}}
|
|
styles={{
|
|
body: {padding: '16px'}
|
|
}}
|
|
>
|
|
{renderPortConfig(outPort, `${fieldPath}.out`)}
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
key={fieldPath}
|
|
title={
|
|
<span style={{fontSize: '14px', fontWeight: 500}}>
|
|
{value.title}
|
|
</span>
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginBottom: 16,
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
|
borderRadius: '8px',
|
|
border: '1px solid #f0f0f0'
|
|
}}
|
|
styles={{
|
|
body: {padding: '16px'}
|
|
}}
|
|
>
|
|
<FormRenderer
|
|
schema={value}
|
|
path={fieldPath}
|
|
readOnly={readOnly}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Form.Item
|
|
key={fieldPath}
|
|
name={fieldPath}
|
|
label={
|
|
<span style={{fontSize: '14px'}}>
|
|
{value.title}
|
|
</span>
|
|
}
|
|
tooltip={value.description}
|
|
required={schema.required?.includes(key)}
|
|
initialValue={'default' in value ? value.default : undefined}
|
|
style={{marginBottom: 16}}
|
|
>
|
|
{readOnly ? (
|
|
<span style={{color: '#666'}}>{value.default || '-'}</span>
|
|
) : (
|
|
renderField(value, fieldPath)
|
|
)}
|
|
</Form.Item>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const NodeDesignForm: React.FC = () => {
|
|
const [nodeDefinitionsDefined, setNodeDefinitionsDefined] = useState<NodeDesignDataResponse[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [selectedNode, setSelectedNode] = useState<NodeDesignDataResponse | null>(null);
|
|
const [activeTab, setActiveTab] = useState<string>('panel');
|
|
const [form] = Form.useForm();
|
|
|
|
// 加载节点定义数据
|
|
useEffect(() => {
|
|
const loadNodeDefinitions = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await service.getNodeDefinitionsDefined();
|
|
setNodeDefinitionsDefined(data);
|
|
// 自动选中第一个节点
|
|
if (data && data.length > 0) {
|
|
handleNodeSelect(data[0]);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载节点定义失败:', error);
|
|
message.error('加载节点定义失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadNodeDefinitions();
|
|
}, []);
|
|
|
|
// 获取当前节点可用的 Tab 列表
|
|
const getAvailableTabs = (node: NodeDesignDataResponse | null) => {
|
|
if (!node) return [];
|
|
return TAB_CONFIG.filter(tab => {
|
|
const value = node[tab.schemaKey as keyof NodeDesignDataResponse];
|
|
return value !== null && value !== undefined;
|
|
});
|
|
};
|
|
|
|
// 处理节点选择
|
|
const handleNodeSelect = (node: NodeDesignDataResponse) => {
|
|
setSelectedNode(node);
|
|
// 更新表单数据
|
|
form.setFieldsValue({
|
|
'base.nodeType': node.nodeCode, // 使用 nodeCode 作为节点类型
|
|
'base.nodeCode': node.nodeCode,
|
|
'base.nodeName': node.nodeName,
|
|
'base.category': node.category,
|
|
'base.description': node.description
|
|
});
|
|
};
|
|
|
|
// 处理 Tab 切换
|
|
const handleTabChange = (key: string) => {
|
|
setActiveTab(key);
|
|
// 不需要重置表单
|
|
};
|
|
|
|
// 处理保存
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
console.log('Form values:', values);
|
|
|
|
// 提取基本信息字段
|
|
const baseFields = {
|
|
nodeType: values['base.nodeType'],
|
|
nodeCode: values['base.nodeCode'],
|
|
nodeName: values['base.nodeName'],
|
|
category: values['base.category'],
|
|
description: values['base.description']
|
|
};
|
|
|
|
// 移除基本信息字段,处理 UI 配置
|
|
const uiValues = {...values};
|
|
delete uiValues['base.nodeType'];
|
|
delete uiValues['base.nodeCode'];
|
|
delete uiValues['base.nodeName'];
|
|
delete uiValues['base.category'];
|
|
delete uiValues['base.description'];
|
|
|
|
// 将扁平的键值对转换为嵌套对象
|
|
const convertToNestedObject = (flatObj: any) => {
|
|
const result: any = {};
|
|
|
|
Object.keys(flatObj).forEach(key => {
|
|
const parts = key.split('.');
|
|
let current = result;
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
current[parts[i]] = current[parts[i]] || {};
|
|
current = current[parts[i]];
|
|
}
|
|
|
|
current[parts[parts.length - 1]] = flatObj[key];
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
const saveData = {
|
|
...selectedNode,
|
|
...baseFields,
|
|
uiVariables: convertToNestedObject(uiValues)
|
|
};
|
|
|
|
console.log('Save data:', saveData);
|
|
await service.saveNodeDefinition(saveData);
|
|
message.success('保存成功');
|
|
} catch (error) {
|
|
console.error('保存失败:', error);
|
|
message.error('保存失败');
|
|
}
|
|
};
|
|
|
|
// 获取当前 schema
|
|
const getCurrentSchema = () => {
|
|
if (!selectedNode) return null;
|
|
const currentTab = TAB_CONFIG.find(tab => tab.key === activeTab);
|
|
if (!currentTab) return null;
|
|
return selectedNode[currentTab.schemaKey as keyof NodeDesignDataResponse];
|
|
};
|
|
|
|
return (
|
|
<PageContainer
|
|
header={{
|
|
title: '节点设计',
|
|
extra: [
|
|
<Button
|
|
key="save"
|
|
type="primary"
|
|
onClick={handleSave}
|
|
>
|
|
保存
|
|
</Button>
|
|
],
|
|
}}
|
|
>
|
|
<div style={{display: 'flex', gap: '24px', padding: '24px'}}>
|
|
<Tabs
|
|
tabPosition="left"
|
|
type="card"
|
|
activeKey={selectedNode?.nodeCode || ''}
|
|
onChange={(key) => {
|
|
const node = nodeDefinitionsDefined.find(n => n.nodeCode === key);
|
|
if (node) {
|
|
handleNodeSelect(node);
|
|
}
|
|
}}
|
|
items={nodeDefinitionsDefined.map(node => ({
|
|
key: node.nodeCode,
|
|
label: (
|
|
<div style={{padding: '4px 0'}}>
|
|
<div style={{fontSize: '14px', fontWeight: 500}}>
|
|
{node.nodeName}
|
|
</div>
|
|
<div style={{
|
|
fontSize: '12px',
|
|
color: '#666',
|
|
marginTop: '4px'
|
|
}}>
|
|
{node.nodeCode}
|
|
</div>
|
|
</div>
|
|
)
|
|
}))}
|
|
/>
|
|
<Card
|
|
style={{
|
|
flex: 1,
|
|
}}
|
|
>
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
key={`${selectedNode?.nodeCode}-${activeTab}`}
|
|
>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="base.nodeType"
|
|
label="节点类型"
|
|
rules={[{required: true}]}
|
|
>
|
|
<Input disabled/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="base.nodeCode"
|
|
label="节点编码"
|
|
rules={[{required: true}]}
|
|
>
|
|
<Input disabled/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="base.nodeName"
|
|
label="节点名称"
|
|
rules={[{required: true}]}
|
|
>
|
|
<Input disabled/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="base.category"
|
|
label="节点类别"
|
|
>
|
|
<Input disabled/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Form.Item
|
|
name="base.description"
|
|
label="节点描述"
|
|
>
|
|
<Input.TextArea rows={4}/>
|
|
</Form.Item>
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={handleTabChange}
|
|
items={getAvailableTabs(selectedNode).map(tab => ({
|
|
key: tab.key,
|
|
label: (
|
|
<span style={{fontSize: '14px'}}>
|
|
{tab.label}
|
|
</span>
|
|
),
|
|
children: tab.key === activeTab ? (
|
|
<div style={{
|
|
padding: '16px 0',
|
|
minHeight: '400px'
|
|
}}>
|
|
<FormRenderer
|
|
schema={getCurrentSchema()}
|
|
readOnly={tab.readonly}
|
|
/>
|
|
</div>
|
|
) : null
|
|
}))}
|
|
/>
|
|
</Form>
|
|
</Card>
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default NodeDesignForm;
|