This commit is contained in:
dengqichen 2024-12-19 18:18:06 +08:00
parent 684e682bcd
commit 64220aef44
6 changed files with 219 additions and 248 deletions

View File

@ -155,6 +155,25 @@ export const getCurrentUserMenus = async () => {
] ]
}; };
// 添加X6测试菜单
// const x6Test: MenuResponse = {
// id: -1,
// createTime: new Date().toISOString(),
// updateTime: new Date().toISOString(),
// version: 0,
// name: "X6测试",
// path: "/x6-test",
// component: "/X6Test/index",
// icon: "experiment",
// type: MenuTypeEnum.MENU,
// parentId: 0,
// sort: 1,
// hidden: false,
// enabled: true,
// createBy: "system",
// updateBy: "system"
// };
// 处理组件路径格式 // 处理组件路径格式
const processMenu = (menu: MenuResponse): MenuResponse => { const processMenu = (menu: MenuResponse): MenuResponse => {
const processed = { ...menu }; const processed = { ...menu };

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { Row, Col, Menu, Tabs, Card } from 'antd'; import { Row, Col, Menu, Tabs, Card, Button, message } from 'antd';
import { BetaSchemaForm, ProForm, ProFormGroup } from '@ant-design/pro-form'; import { BetaSchemaForm, ProForm, ProFormGroup, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
import type { NodeDefinitionData } from './types'; import type { NodeDefinitionData } from './types';
import * as service from './service'; import * as service from './service';
@ -22,6 +22,9 @@ const NodeDesignForm: React.FC = () => {
// 当前选中的 tab // 当前选中的 tab
const [activeTab, setActiveTab] = useState<string>('panel'); const [activeTab, setActiveTab] = useState<string>('panel');
const formRef = useRef<any>();
const uiFormRef = useRef<any>(); // 添加 UI 配置表单的 ref
// 加载节点定义数据 // 加载节点定义数据
useEffect(() => { useEffect(() => {
const loadNodeDefinitions = async () => { const loadNodeDefinitions = async () => {
@ -173,6 +176,22 @@ const NodeDesignForm: React.FC = () => {
'boolean': 'switch' 'boolean': 'switch'
}; };
// 判断是否是颜色字段
const isColorField = value.type === 'string' &&
(value.title?.includes('颜色') || value.description?.includes('颜色'));
if (isColorField) {
return {
...baseField,
valueType: 'color',
placeholder: `请选择${value.title}`,
fieldProps: {
showText: true,
format: 'hex'
}
};
}
return { return {
...baseField, ...baseField,
valueType: typeMap[value.type] || 'text', valueType: typeMap[value.type] || 'text',
@ -182,127 +201,175 @@ const NodeDesignForm: React.FC = () => {
}; };
// 渲染 Tab 内容 // 渲染 Tab 内容
const renderTabContent = (tabKey: string) => { const renderTabContent = (schemaKey: string) => {
if (!selectedNode) return null; if (!selectedNode) return null;
const schema = selectedNode[schemaKey as keyof NodeDefinitionData];
const content = selectedNode[tabKey as keyof NodeDefinitionData]; if (!schema) return null;
if (!content) return null;
const currentTab = TAB_CONFIG.find(tab => tab.schemaKey === tabKey);
const isReadOnly = currentTab?.readonly ?? true;
// 判断是否是 schema 格式
const isSchema = typeof content === 'object' && 'type' in content && 'properties' in content;
console.log('Is Schema:', isSchema);
console.log('Content:', content);
const schema = isSchema ? content : {
type: 'object',
properties: Object.entries(content).reduce((acc, [key, value]) => ({
...acc,
[key]: {
title: key,
type: typeof value,
default: value
}
}), {})
};
console.log('Final Schema:', schema);
// 获取默认值
const defaultValues = getDefaultValues(schema);
console.log('Default Values:', defaultValues);
return ( return (
<Card bordered={false}> <BetaSchemaForm
<BetaSchemaForm formRef={schemaKey === 'uiVariables' ? uiFormRef : undefined}
layoutType="Form" layoutType="Form"
readonly={isReadOnly} initialValues={getDefaultValues(schema)}
onFinish={handleFormSubmit} columns={convertToProFormSchema(schema)}
columns={convertToProFormSchema(schema)} submitter={false}
initialValues={defaultValues} />
submitter={!isReadOnly} );
grid={false} };
layout="vertical"
style={{ maxWidth: '100%' }} // 将扁平的对象转换为嵌套对象
formItemProps={{ const flatToNested = (obj: Record<string, any>) => {
labelCol: { span: 24 }, const result: Record<string, any> = {};
wrapperCol: { span: 24 },
style: { Object.keys(obj).forEach(key => {
marginBottom: 16 const parts = key.split('.');
} let current = result;
}}
/> parts.forEach((part, index) => {
</Card> if (index === parts.length - 1) {
current[part] = obj[key];
} else {
current[part] = current[part] || {};
current = current[part];
}
});
});
return result;
};
const handleSave = async () => {
if (!selectedNode) return;
try {
const formValues = await formRef.current?.validateFields();
const uiValues = await uiFormRef.current?.validateFields();
const saveData = {
nodeType: formValues.nodeType,
nodeCode: formValues.nodeCode,
nodeName: formValues.nodeName,
category: formValues.category,
description: formValues.description,
uiVariables: flatToNested(uiValues), // 将扁平结构转换为嵌套结构
localVariablesSchema: selectedNode.localVariablesSchema,
panelVariablesSchema: selectedNode.panelVariablesSchema,
formVariablesSchema: selectedNode.formVariablesSchema
};
await service.saveNodeDefinition(saveData);
message.success('保存成功');
} catch (error) {
console.error('Save failed:', error);
message.error('保存失败');
}
};
const renderBasicForm = () => {
if (!selectedNode) return null;
return (
<ProForm
formRef={formRef}
key={selectedNode.nodeCode}
layout="horizontal"
submitter={false}
initialValues={{
nodeType: selectedNode.nodeCode,
nodeCode: selectedNode.nodeCode,
nodeName: selectedNode.nodeName,
category: selectedNode.category,
}}
>
<ProForm.Group>
<ProFormText
name="nodeType"
label="节点类型"
disabled
colProps={{ span: 12 }}
/>
<ProFormText
name="nodeCode"
label="节点编码"
disabled
colProps={{ span: 12 }}
/>
</ProForm.Group>
<ProForm.Group>
<ProFormText
name="nodeName"
label="节点名称"
disabled
colProps={{ span: 12 }}
/>
<ProFormText
name="category"
label="节点类别"
disabled
colProps={{ span: 12 }}
/>
</ProForm.Group>
<ProForm.Group>
<ProFormTextArea
name="description"
label="节点描述"
placeholder="请输入节点描述"
colProps={{ span: 24 }}
/>
</ProForm.Group>
</ProForm>
); );
}; };
return ( return (
<PageContainer <PageContainer
loading={loading} loading={loading}
header={{ extra={[
title: '节点设计', <Button
breadcrumb: { key="save"
items: [ type="primary"
{ title: '工作流' }, onClick={handleSave}
{ title: '节点设计', path: '/workflow/node-design' } disabled={!selectedNode}
], >
},
}} </Button>
]}
> >
<div style={{ background: '#fff', minHeight: 'calc(100vh - 200px)' }}> <div style={{ display: 'flex', height: '100%' }}>
<Row> <div style={{ width: 200, borderRight: '1px solid #f0f0f0', marginRight: 24 }}>
<Col span={5} style={{ borderRight: '1px solid #f0f0f0', padding: '16px 0' }}> <div style={{
<div style={{ padding: '8px 16px',
padding: '0 24px 16px', fontWeight: 'bold',
fontSize: '16px', borderBottom: '1px solid #f0f0f0'
fontWeight: 500, }}>
color: '#000000d9',
borderBottom: '1px solid #f0f0f0', </div>
marginBottom: '8px' <Menu
}}> mode="vertical"
selectedKeys={[selectedNode?.nodeCode || '']}
</div> items={nodeDefinitions.map(node => ({
<Menu key: node.nodeCode,
mode="vertical" label: `${node.nodeName}${node.nodeCode}`,
selectedKeys={[selectedNode?.nodeCode || '']} }))}
items={nodeDefinitions.map(node => ({ onClick={({ key }) => handleNodeSelect(key)}
key: node.nodeCode, style={{
label: node.nodeName, border: 'none',
}))} padding: '0 16px'
onClick={({ key }) => handleNodeSelect(key)} }}
style={{ />
border: 'none', </div>
padding: '0 16px' <div style={{ flex: 1 }}>
}} {renderBasicForm()}
/> <Tabs
</Col> activeKey={activeTab}
<Col span={19}> onChange={handleTabChange}
<div style={{ padding: '16px 24px' }}> items={getAvailableTabs(selectedNode).map(tab => ({
{selectedNode && getAvailableTabs(selectedNode).length > 0 ? ( key: tab.schemaKey,
<Tabs label: tab.label,
activeKey={activeTab} children: renderTabContent(tab.schemaKey)
onChange={handleTabChange} }))}
type="card" />
items={getAvailableTabs(selectedNode).map(tab => ({ </div>
key: tab.key,
label: tab.label,
children: renderTabContent(tab.schemaKey)
}))}
/>
) : (
<div style={{
textAlign: 'center',
padding: '32px',
color: '#00000073',
fontSize: '14px'
}}>
</div>
)}
</div>
</Col>
</Row>
</div> </div>
</PageContainer> </PageContainer>
); );

View File

@ -100,20 +100,13 @@ const NodeDesignList: React.FC = () => {
...rest, ...rest,
}); });
return { return {
data: response.list, data: response.content,
success: true, success: true,
total: response.total, total: response.totalElements
}; };
}} }}
rowKey="nodeCode" rowKey="nodeCode"
search={{ search={false}
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="节点设计列表"
toolBarRender={() => [ toolBarRender={() => [
<Button <Button
key="create" key="create"
@ -121,15 +114,7 @@ const NodeDesignList: React.FC = () => {
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => navigate('/workflow/node-design/create')} onClick={() => navigate('/workflow/node-design/create')}
> >
</Button>,
<Button
key="export"
onClick={() => {
// TODO: 实现导出功能
}}
>
</Button>, </Button>,
]} ]}
/> />

View File

@ -3,20 +3,24 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { NodeDesignQuery, NodeDesignResponse, NodeDesignData, NodeDefinitionData } from './types'; import type { NodeDesignQuery, NodeDesignResponse, NodeDesignData, NodeDefinitionData } from './types';
const BASE_URL = '/api/v1/workflow'; const BASE_URL = '/api/v1/workflow/node-definition';
// 获取节点设计列表 // 获取节点设计列表
export const getNodeDesigns = (params: NodeDesignQuery) => export const getNodeDesigns = (params: NodeDesignQuery) =>
request.get<NodeDesignResponse>(`${BASE_URL}/node-design/list`, { params }); request.get<NodeDesignResponse>(`${BASE_URL}/page`, { params });
// 获取节点设计详情 // 获取节点设计详情
export const getNodeDesign = (nodeCode: string) => export const getNodeDesign = (nodeCode: string) =>
request.get<NodeDesignData>(`${BASE_URL}/node-design/${nodeCode}`); request.get<NodeDesignData>(`${BASE_URL}/${nodeCode}`);
// 更新节点UI配置 // 更新节点UI配置
export const updateNodeUIConfig = (nodeCode: string, uiVariables: any) => export const updateNodeUIConfig = (nodeCode: string, uiVariables: any) =>
request.put(`${BASE_URL}/node-design/${nodeCode}/ui`, uiVariables); request.put(`${BASE_URL}/${nodeCode}/ui`, uiVariables);
// 获取已定义的节点类型配置 // 获取已定义的节点类型配置
export const getNodeDefinitionsDefined = () => export const getNodeDefinitionsDefined = () =>
request.get<NodeDefinitionData[]>(`${BASE_URL}/node-definition/defined`); request.get<NodeDefinitionData[]>(`${BASE_URL}/defined`);
// 保存节点定义
export const saveNodeDefinition = (data: NodeDefinitionData) =>
request.post<void>(`${BASE_URL}`, data);

View File

@ -1,6 +1,8 @@
// 节点设计相关类型定义 // 节点设计相关类型定义
// 基础Schema接口 // 基础Schema接口
import {BaseQuery, BaseResponse} from "@/types/base";
export interface BaseSchema { export interface BaseSchema {
type: string; type: string;
properties: Record<string, any>; properties: Record<string, any>;
@ -68,20 +70,11 @@ export interface UIVariables extends BaseSchema {
style: NodeStyle; style: NodeStyle;
} }
// 节点定义数据接口
export interface NodeDefinitionData {
nodeCode: string;
nodeName: string;
panelVariablesSchema: NodeVariablesSchema | null;
localVariablesSchema: NodeVariablesSchema | null;
formVariablesSchema: NodeVariablesSchema | null;
uiVariables: UIVariables;
}
// 节点设计数据 // 节点设计数据
export interface NodeDesignData { export interface NodeDesignData {
nodeCode: string; nodeCode: string;
nodeName: string; nodeName: string;
category: string;
panelVariablesSchema: NodeVariablesSchema | null; panelVariablesSchema: NodeVariablesSchema | null;
localVariablesSchema: NodeVariablesSchema | null; localVariablesSchema: NodeVariablesSchema | null;
formVariablesSchema: NodeVariablesSchema | null; formVariablesSchema: NodeVariablesSchema | null;
@ -96,15 +89,12 @@ export enum NodeTypeEnum {
} }
// 查询参数接口 // 查询参数接口
export interface NodeDesignQuery { export interface NodeDesignQuery extends BaseQuery{
nodeCode?: string; nodeCode?: string;
nodeName?: string; nodeName?: string;
pageSize?: number;
current?: number;
} }
// 分页响应接口 // 分页响应接口
export interface NodeDesignResponse { export interface NodeDesignResponse extends BaseResponse{
total: number;
list: NodeDesignData[];
} }

View File

@ -1,94 +0,0 @@
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { message } from 'antd';
interface Response<T = any> {
code: number;
message: string;
data: T;
success: boolean;
}
class Http {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse<Response>) => {
const { data: res } = response;
if (res.success) {
return res.data;
}
message.error(res.message);
return Promise.reject(new Error(res.message));
},
(error) => {
if (error.response?.status === 401) {
// 处理未授权
localStorage.removeItem('token');
window.location.href = '/login';
}
message.error(error.response?.data?.message || '网络错误');
return Promise.reject(error);
}
);
}
get<T = any>(url: string, config?: AxiosRequestConfig) {
return this.instance.get<Response<T>, T>(url, config);
}
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
return this.instance.post<Response<T>, T>(url, data, config);
}
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
return this.instance.put<Response<T>, T>(url, data, config);
}
delete<T = any>(url: string, config?: AxiosRequestConfig) {
return this.instance.delete<Response<T>, T>(url, config);
}
download(url: string, filename?: string, config?: AxiosRequestConfig) {
return this.instance
.get(url, { ...config, responseType: 'blob' })
.then((response) => {
const blob = new Blob([response]);
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename || 'download';
link.click();
window.URL.revokeObjectURL(link.href);
});
}
}
export const http = new Http();