1
This commit is contained in:
parent
e18874688e
commit
fd0b615e6b
2532
frontend/package-lock.json
generated
2532
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,37 +12,28 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/pro-components": "^2.8.2",
|
||||
"@antv/layout": "^1.2.14-beta.8",
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@antv/x6-plugin-clipboard": "^2.1.6",
|
||||
"@antv/x6-plugin-export": "^2.1.6",
|
||||
"@antv/x6-plugin-history": "^2.2.4",
|
||||
"@antv/x6-plugin-keyboard": "^2.2.3",
|
||||
"@antv/x6-plugin-minimap": "^2.0.7",
|
||||
"@antv/x6-plugin-selection": "^2.2.2",
|
||||
"@antv/x6-plugin-snapline": "^2.1.7",
|
||||
"@antv/x6-plugin-transform": "^2.1.8",
|
||||
"@antv/x6-react-shape": "^2.2.3",
|
||||
"@formily/antd-v5": "^1.2.3",
|
||||
"@formily/core": "^2.3.2",
|
||||
"@formily/react": "^2.3.2",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@types/recharts": "^1.8.29",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
|
||||
57
frontend/src/components/ui/accordion.tsx
Normal file
57
frontend/src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
||||
29
frontend/src/components/ui/tooltip.tsx
Normal file
29
frontend/src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import type { FlowEdge } from '../types';
|
||||
|
||||
interface EdgeConfigModalProps {
|
||||
@ -15,9 +24,31 @@ export interface EdgeCondition {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
// Zod 表单验证 Schema
|
||||
const edgeConditionSchema = z.object({
|
||||
type: z.enum(['EXPRESSION', 'DEFAULT'], {
|
||||
required_error: '请选择条件类型',
|
||||
}),
|
||||
expression: z.string().optional(),
|
||||
priority: z.number()
|
||||
.min(1, '优先级最小为 1')
|
||||
.max(999, '优先级最大为 999'),
|
||||
}).refine((data) => {
|
||||
// 如果是表达式类型,expression 必填
|
||||
if (data.type === 'EXPRESSION') {
|
||||
return data.expression && data.expression.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: '请输入条件表达式',
|
||||
path: ['expression'],
|
||||
});
|
||||
|
||||
type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
|
||||
|
||||
/**
|
||||
* 边条件配置弹窗
|
||||
* 复刻 Workflow/Design/ExpressionModal 功能
|
||||
* 使用 shadcn/ui Dialog + react-hook-form
|
||||
*/
|
||||
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
visible,
|
||||
@ -25,123 +56,178 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
onOk,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const { toast } = useToast();
|
||||
const [conditionType, setConditionType] = useState<'EXPRESSION' | 'DEFAULT'>('EXPRESSION');
|
||||
|
||||
// 当edge变化时,更新表单初始值
|
||||
const form = useForm<EdgeConditionFormValues>({
|
||||
resolver: zodResolver(edgeConditionSchema),
|
||||
defaultValues: {
|
||||
type: 'EXPRESSION',
|
||||
expression: '',
|
||||
priority: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 当 edge 变化时,更新表单值
|
||||
useEffect(() => {
|
||||
if (visible && edge) {
|
||||
const condition = edge.data?.condition;
|
||||
form.setFieldsValue({
|
||||
type: condition?.type || 'EXPRESSION',
|
||||
const values = {
|
||||
type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
|
||||
expression: condition?.expression || '',
|
||||
priority: condition?.priority || 10
|
||||
});
|
||||
priority: condition?.priority || 10,
|
||||
};
|
||||
form.reset(values);
|
||||
setConditionType(values.type);
|
||||
}
|
||||
}, [visible, edge, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
const handleSubmit = (values: EdgeConditionFormValues) => {
|
||||
if (!edge) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证表达式
|
||||
if (values.type === 'EXPRESSION') {
|
||||
if (!values.expression || values.expression.trim() === '') {
|
||||
message.error('请输入条件表达式');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否包含变量引用 ${...}
|
||||
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
|
||||
if (!hasVariable) {
|
||||
message.warning('表达式建议包含变量引用,格式:${变量名}');
|
||||
}
|
||||
|
||||
// 检查表达式是否包含变量引用
|
||||
if (values.type === 'EXPRESSION' && values.expression) {
|
||||
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
|
||||
if (!hasVariable) {
|
||||
toast({
|
||||
title: '提示',
|
||||
description: '表达式建议包含变量引用,格式:${变量名}',
|
||||
});
|
||||
}
|
||||
|
||||
onOk(edge.id, values);
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
}
|
||||
|
||||
onOk(edge.id, values);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
setConditionType('EXPRESSION');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="配置边条件"
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
type: 'EXPRESSION',
|
||||
expression: '',
|
||||
priority: 10
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="条件类型"
|
||||
rules={[{ required: true, message: '请选择条件类型' }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="EXPRESSION">表达式</Radio>
|
||||
<Radio value="DEFAULT">默认路径</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>配置边条件</DialogTitle>
|
||||
<DialogDescription>
|
||||
设置流程分支的条件表达式和优先级
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const type = getFieldValue('type');
|
||||
return type === 'EXPRESSION' ? (
|
||||
<Form.Item
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>条件类型</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="type-expression"
|
||||
value="EXPRESSION"
|
||||
checked={field.value === 'EXPRESSION'}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setConditionType('EXPRESSION');
|
||||
}}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="type-expression" className="cursor-pointer font-normal">
|
||||
表达式
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="type-default"
|
||||
value="DEFAULT"
|
||||
checked={field.value === 'DEFAULT'}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
setConditionType('DEFAULT');
|
||||
}}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="type-default" className="cursor-pointer font-normal">
|
||||
默认路径
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{conditionType === 'EXPRESSION' ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expression"
|
||||
label="条件表达式"
|
||||
rules={[{ required: true, message: '请输入条件表达式' }]}
|
||||
extra="支持使用 ${变量名} 引用流程变量,例如:${amount} > 1000"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件表达式,如:${amount} > 1000"
|
||||
rows={4}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>条件表达式</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="请输入条件表达式,如:${amount} > 1000"
|
||||
rows={4}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
支持使用 {'${变量名}'} 引用流程变量,例如:{'${amount} > 1000'}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
||||
默认路径:当没有其他条件分支满足时,将执行此路径
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="priority"
|
||||
label="优先级"
|
||||
rules={[{ required: true, message: '请设置优先级' }]}
|
||||
extra="数字越小优先级越高(1-999)"
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={999}
|
||||
className="w-full"
|
||||
placeholder="请输入优先级"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>优先级</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
placeholder="请输入优先级"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
数字越小优先级越高(1-999)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
确定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Drawer, Tabs, Button, Space, message } from 'antd';
|
||||
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5';
|
||||
import { createForm } from '@formily/core';
|
||||
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
|
||||
import { Save, RotateCcw } from 'lucide-react';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import type { FlowNode, FlowNodeData } from '../types';
|
||||
import type { WorkflowNodeDefinition } from '../nodes/types';
|
||||
import { isConfigurableNode } from '../nodes/types';
|
||||
@ -35,7 +38,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
onOk
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('config');
|
||||
const { toast } = useToast();
|
||||
|
||||
// 获取节点定义
|
||||
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
|
||||
@ -118,11 +121,18 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
};
|
||||
|
||||
onOk(node.id, updatedData);
|
||||
message.success('节点配置保存成功');
|
||||
toast({
|
||||
title: "保存成功",
|
||||
description: "节点配置已成功保存",
|
||||
});
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "保存失败",
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -133,7 +143,10 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
configForm.reset();
|
||||
inputForm.reset();
|
||||
outputForm.reset();
|
||||
message.info('已重置为初始值');
|
||||
toast({
|
||||
title: "已重置",
|
||||
description: "表单已重置为初始值",
|
||||
});
|
||||
};
|
||||
|
||||
// 将JSON Schema转换为Formily Schema(扩展配置)
|
||||
@ -248,120 +261,111 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建Tabs配置
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'config',
|
||||
label: '基本配置',
|
||||
children: activeTab === 'config' ? (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<FormProvider form={configForm}>
|
||||
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</div>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
// 如果是可配置节点,添加输入和输出映射TAB
|
||||
if (isConfigurableNode(nodeDefinition)) {
|
||||
if (nodeDefinition.inputMappingSchema) {
|
||||
tabItems.push({
|
||||
key: 'input',
|
||||
label: '输入映射',
|
||||
children: activeTab === 'input' ? (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<FormProvider form={inputForm}>
|
||||
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</div>
|
||||
) : null,
|
||||
});
|
||||
}
|
||||
|
||||
if (nodeDefinition.outputMappingSchema) {
|
||||
tabItems.push({
|
||||
key: 'output',
|
||||
label: '输出映射',
|
||||
children: activeTab === 'output' ? (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<FormProvider form={outputForm}>
|
||||
<FormLayout labelCol={6} wrapperCol={18} labelAlign="right" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</div>
|
||||
) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600' }}>
|
||||
编辑节点 - {nodeDefinition.nodeName}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={onCancel}
|
||||
size="small"
|
||||
/>
|
||||
<Sheet open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<SheetContent className="w-[720px] sm:max-w-[720px] flex flex-col p-0">
|
||||
{/* Header - 固定在顶部 */}
|
||||
<div className="px-6 pt-6 pb-4 border-b">
|
||||
<SheetHeader className="space-y-2">
|
||||
<SheetTitle className="text-xl font-semibold">
|
||||
编辑节点 - {nodeDefinition.nodeName}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="text-sm text-muted-foreground">
|
||||
配置节点的参数和映射关系
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
width={720}
|
||||
open={visible}
|
||||
onClose={onCancel}
|
||||
closeIcon={null}
|
||||
styles={{
|
||||
body: { padding: '0 24px 24px' },
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
||||
}}
|
||||
footer={
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 0',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
|
||||
{/* Content - 可滚动区域 */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{/* 使用 Accordion 支持多个面板同时展开,默认展开基本配置 */}
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={["config"]}
|
||||
className="w-full"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Space>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
{/* 基本配置 - 始终显示 */}
|
||||
<AccordionItem value="config">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
基本配置
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-1">
|
||||
<FormProvider form={configForm}>
|
||||
<FormLayout layout="vertical" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* 输入映射 - 条件显示 */}
|
||||
{isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema && (
|
||||
<AccordionItem value="input">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
输入映射
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-1">
|
||||
<FormProvider form={inputForm}>
|
||||
<FormLayout layout="vertical" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{/* 输出映射 - 条件显示 */}
|
||||
{isConfigurableNode(nodeDefinition) && nodeDefinition.outputMappingSchema && (
|
||||
<AccordionItem value="output">
|
||||
<AccordionTrigger className="text-base font-semibold">
|
||||
输出映射
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-1">
|
||||
<FormProvider form={outputForm}>
|
||||
<FormLayout layout="vertical" colon={false}>
|
||||
<SchemaField schema={convertToFormilySchema(nodeDefinition.outputMappingSchema)} />
|
||||
</FormLayout>
|
||||
</FormProvider>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</Accordion>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ paddingTop: '16px' }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
{/* Footer - 固定在底部 */}
|
||||
<div className="px-6 py-4 border-t bg-background">
|
||||
<SheetFooter className="flex-row justify-between gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
className="gap-2"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{loading ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Button, Divider, Tooltip } from 'antd';
|
||||
import {
|
||||
SaveOutlined,
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
ExpandOutlined,
|
||||
ArrowLeftOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Save, Undo, Redo, ZoomIn, ZoomOut, Maximize2, ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
interface WorkflowToolbarProps {
|
||||
title?: string;
|
||||
@ -40,131 +39,124 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`workflow-toolbar ${className}`}
|
||||
style={{
|
||||
height: '56px',
|
||||
background: 'white',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
{/* 左侧:返回按钮和标题 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Tooltip title="返回列表">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onBack}
|
||||
style={{ color: '#6b7280' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" />
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮区域 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
{/* 撤销/重做 */}
|
||||
<Tooltip title="撤销 (Ctrl+Z)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
size="middle"
|
||||
style={{
|
||||
color: canUndo ? '#374151' : '#d1d5db',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重做 (Ctrl+Shift+Z)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
size="middle"
|
||||
style={{
|
||||
color: canRedo ? '#374151' : '#d1d5db',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" style={{ margin: '0 4px' }} />
|
||||
|
||||
{/* 视图操作 */}
|
||||
<Tooltip title="放大 (+)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={onZoomIn}
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="缩小 (-)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomOutOutlined />}
|
||||
onClick={onZoomOut}
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="适应视图">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ExpandOutlined />}
|
||||
onClick={onFitView}
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 缩放比例显示 */}
|
||||
<div style={{
|
||||
background: '#f8fafc',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#475569',
|
||||
fontFamily: 'ui-monospace, monospace',
|
||||
minWidth: '60px',
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
border: '1px solid #e2e8f0',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
{Math.round(zoom * 100)}%
|
||||
<TooltipProvider>
|
||||
<div
|
||||
className={`workflow-toolbar flex items-center justify-between h-14 px-4 bg-background border-b shadow-sm ${className}`}
|
||||
>
|
||||
{/* 左侧:返回按钮和标题 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>返回列表</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
|
||||
<h2 className="text-base font-semibold text-foreground m-0">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" style={{ margin: '0 8px' }} />
|
||||
{/* 右侧:操作按钮区域 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* 撤销/重做 */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>撤销 (Ctrl+Z)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>重做 (Ctrl+Shift+Z)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* 保存按钮(最右侧) */}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onSave}
|
||||
size="middle"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
fontWeight: '500',
|
||||
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
{/* 视图操作 */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onZoomIn}
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>放大 (+)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onZoomOut}
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>缩小 (-)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onFitView}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>适应视图</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* 缩放比例显示 */}
|
||||
<div className="ml-1 px-3 py-1.5 bg-muted text-muted-foreground text-xs font-mono font-semibold rounded-md border min-w-[60px] text-center">
|
||||
{Math.round(zoom * 100)}%
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-6 mx-2" />
|
||||
|
||||
{/* 保存按钮(最右侧) */}
|
||||
<Button
|
||||
onClick={onSave}
|
||||
className="gap-1.5 font-medium shadow-md"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -580,20 +580,20 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
initialEdges={initialEdges}
|
||||
onNodeClick={handleNodeClick}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onDrop={handleDrop}
|
||||
onDrop={handleDrop}
|
||||
onViewportChange={handleViewportChange}
|
||||
className="workflow-canvas"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 节点配置弹窗 */}
|
||||
<NodeConfigModal
|
||||
visible={configModalVisible}
|
||||
visible={configModalVisible}
|
||||
node={configNode}
|
||||
onCancel={handleCloseConfigModal}
|
||||
onOk={handleNodeConfigUpdate}
|
||||
/>
|
||||
onOk={handleNodeConfigUpdate}
|
||||
/>
|
||||
|
||||
{/* 边条件配置弹窗 */}
|
||||
<EdgeConfigModal
|
||||
|
||||
Loading…
Reference in New Issue
Block a user