This commit is contained in:
dengqichen 2025-10-21 13:18:50 +08:00
parent e18874688e
commit fd0b615e6b
8 changed files with 2177 additions and 1244 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,37 +12,28 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@ant-design/pro-components": "^2.8.2", "@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/antd-v5": "^1.2.3",
"@formily/core": "^2.3.2", "@formily/core": "^2.3.2",
"@formily/react": "^2.3.2", "@formily/react": "^2.3.2",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@monaco-editor/react": "^4.6.0", "@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-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-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1", "@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-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2", "@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-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@types/recharts": "^1.8.29", "@types/recharts": "^1.8.29",
"@xyflow/react": "^12.8.6", "@xyflow/react": "^12.8.6",

View 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 }

View 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 }

View File

@ -1,5 +1,14 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd'; 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'; import type { FlowEdge } from '../types';
interface EdgeConfigModalProps { interface EdgeConfigModalProps {
@ -15,9 +24,31 @@ export interface EdgeCondition {
priority: number; 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> = ({ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
visible, visible,
@ -25,123 +56,178 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
onOk, onOk,
onCancel 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(() => { useEffect(() => {
if (visible && edge) { if (visible && edge) {
const condition = edge.data?.condition; const condition = edge.data?.condition;
form.setFieldsValue({ const values = {
type: condition?.type || 'EXPRESSION', type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
expression: condition?.expression || '', expression: condition?.expression || '',
priority: condition?.priority || 10 priority: condition?.priority || 10,
}); };
form.reset(values);
setConditionType(values.type);
} }
}, [visible, edge, form]); }, [visible, edge, form]);
const handleOk = async () => { const handleSubmit = (values: EdgeConditionFormValues) => {
if (!edge) return; if (!edge) return;
try { // 检查表达式是否包含变量引用
const values = await form.validateFields(); if (values.type === 'EXPRESSION' && values.expression) {
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
// 验证表达式 if (!hasVariable) {
if (values.type === 'EXPRESSION') { toast({
if (!values.expression || values.expression.trim() === '') { title: '提示',
message.error('请输入条件表达式'); description: '表达式建议包含变量引用,格式:${变量名}',
return; });
}
// 检查是否包含变量引用 ${...}
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
if (!hasVariable) {
message.warning('表达式建议包含变量引用,格式:${变量名}');
}
} }
onOk(edge.id, values);
} catch (error) {
console.error('表单验证失败:', error);
} }
onOk(edge.id, values);
handleClose();
}; };
const handleCancel = () => { const handleClose = () => {
form.resetFields(); form.reset();
setConditionType('EXPRESSION');
onCancel(); onCancel();
}; };
return ( return (
<Modal <Dialog open={visible} onOpenChange={(open) => !open && handleClose()}>
title="配置边条件" <DialogContent className="sm:max-w-[600px]">
open={visible} <DialogHeader>
onOk={handleOk} <DialogTitle></DialogTitle>
onCancel={handleCancel} <DialogDescription>
width={600}
okText="确定" </DialogDescription>
cancelText="取消" </DialogHeader>
>
<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>
<Form.Item <Form {...form}>
noStyle <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type} <FormField
> control={form.control}
{({ getFieldValue }) => { name="type"
const type = getFieldValue('type'); render={({ field }) => (
return type === 'EXPRESSION' ? ( <FormItem className="space-y-3">
<Form.Item <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" name="expression"
label="条件表达式" render={({ field }) => (
rules={[{ required: true, message: '请输入条件表达式' }]} <FormItem>
extra="支持使用 ${变量名} 引用流程变量,例如:${amount} > 1000" <FormLabel></FormLabel>
> <FormControl>
<Input.TextArea <Textarea
placeholder="请输入条件表达式,如:${amount} > 1000" placeholder="请输入条件表达式,如:${amount} > 1000"
rows={4} rows={4}
autoComplete="off" {...field}
/> />
</Form.Item> </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> </div>
); )}
}}
</Form.Item>
<Form.Item <FormField
name="priority" control={form.control}
label="优先级" name="priority"
rules={[{ required: true, message: '请设置优先级' }]} render={({ field }) => (
extra="数字越小优先级越高1-999" <FormItem>
> <FormLabel></FormLabel>
<InputNumber <FormControl>
min={1} <Input
max={999} type="number"
className="w-full" min={1}
placeholder="请输入优先级" max={999}
/> placeholder="请输入优先级"
</Form.Item> {...field}
</Form> onChange={(e) => field.onChange(Number(e.target.value))}
</Modal> />
</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>
); );
}; };

View File

@ -1,9 +1,12 @@
import React, { useState, useEffect, useMemo } from 'react'; 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 { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { createSchemaField, FormProvider, ISchema } from '@formily/react'; 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 { FlowNode, FlowNodeData } from '../types';
import type { WorkflowNodeDefinition } from '../nodes/types'; import type { WorkflowNodeDefinition } from '../nodes/types';
import { isConfigurableNode } from '../nodes/types'; import { isConfigurableNode } from '../nodes/types';
@ -35,7 +38,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
onOk onOk
}) => { }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('config'); const { toast } = useToast();
// 获取节点定义 // 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null; const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
@ -118,11 +121,18 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
}; };
onOk(node.id, updatedData); onOk(node.id, updatedData);
message.success('节点配置保存成功'); toast({
title: "保存成功",
description: "节点配置已成功保存",
});
onCancel(); onCancel();
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
message.error(error.message); toast({
variant: "destructive",
title: "保存失败",
description: error.message,
});
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -133,7 +143,10 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
configForm.reset(); configForm.reset();
inputForm.reset(); inputForm.reset();
outputForm.reset(); outputForm.reset();
message.info('已重置为初始值'); toast({
title: "已重置",
description: "表单已重置为初始值",
});
}; };
// 将JSON Schema转换为Formily Schema扩展配置 // 将JSON Schema转换为Formily Schema扩展配置
@ -248,120 +261,111 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
return null; 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 ( return (
<Drawer <Sheet open={visible} onOpenChange={(open) => !open && onCancel()}>
title={ <SheetContent className="w-[720px] sm:max-w-[720px] flex flex-col p-0">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}> {/* Header - 固定在顶部 */}
<span style={{ fontSize: '16px', fontWeight: '600' }}> <div className="px-6 pt-6 pb-4 border-b">
- {nodeDefinition.nodeName} <SheetHeader className="space-y-2">
</span> <SheetTitle className="text-xl font-semibold">
<Button - {nodeDefinition.nodeName}
type="text" </SheetTitle>
icon={<CloseOutlined />} <SheetDescription className="text-sm text-muted-foreground">
onClick={onCancel}
size="small" </SheetDescription>
/> </SheetHeader>
</div> </div>
}
placement="right" {/* Content - 可滚动区域 */}
width={720} <div className="flex-1 overflow-y-auto px-6 py-4">
open={visible} {/* 使用 Accordion 支持多个面板同时展开,默认展开基本配置 */}
onClose={onCancel} <Accordion
closeIcon={null} type="multiple"
styles={{ defaultValue={["config"]}
body: { padding: '0 24px 24px' }, className="w-full"
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}
> >
{/* 基本配置 - 始终显示 */}
</Button> <AccordionItem value="config">
<Space> <AccordionTrigger className="text-base font-semibold">
<Button onClick={onCancel}>
</AccordionTrigger>
</Button> <AccordionContent className="px-1">
<Button <FormProvider form={configForm}>
type="primary" <FormLayout layout="vertical" colon={false}>
loading={loading} <SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
icon={<SaveOutlined />} </FormLayout>
onClick={handleSubmit} </FormProvider>
> </AccordionContent>
</AccordionItem>
</Button>
</Space> {/* 输入映射 - 条件显示 */}
{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>
}
> {/* Footer - 固定在底部 */}
<div style={{ paddingTop: '16px' }}> <div className="px-6 py-4 border-t bg-background">
<Tabs <SheetFooter className="flex-row justify-between gap-2">
activeKey={activeTab} <Button
onChange={setActiveTab} type="button"
items={tabItems} variant="outline"
size="large" onClick={handleReset}
/> className="gap-2"
</div> >
</Drawer> <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>
); );
}; };

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import { Button, Divider, Tooltip } from 'antd'; import { Save, Undo, Redo, ZoomIn, ZoomOut, Maximize2, ArrowLeft } from 'lucide-react';
import { import { Button } from '@/components/ui/button';
SaveOutlined, import { Separator } from '@/components/ui/separator';
UndoOutlined, import {
RedoOutlined, Tooltip,
ZoomInOutlined, TooltipContent,
ZoomOutOutlined, TooltipProvider,
ExpandOutlined, TooltipTrigger
ArrowLeftOutlined } from '@/components/ui/tooltip';
} from '@ant-design/icons';
interface WorkflowToolbarProps { interface WorkflowToolbarProps {
title?: string; title?: string;
@ -40,131 +39,124 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
className = '' className = ''
}) => { }) => {
return ( return (
<div <TooltipProvider>
className={`workflow-toolbar ${className}`} <div
style={{ className={`workflow-toolbar flex items-center justify-between h-14 px-4 bg-background border-b shadow-sm ${className}`}
height: '56px', >
background: 'white', {/* 左侧:返回按钮和标题 */}
borderBottom: '1px solid #e5e7eb', <div className="flex items-center gap-3">
display: 'flex', <Tooltip>
alignItems: 'center', <TooltipTrigger asChild>
justifyContent: 'space-between', <Button
padding: '0 16px', variant="ghost"
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' size="icon"
}} onClick={onBack}
> className="text-muted-foreground hover:text-foreground"
{/* 左侧:返回按钮和标题 */} >
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> <ArrowLeft className="h-4 w-4" />
<Tooltip title="返回列表"> </Button>
<Button </TooltipTrigger>
type="text" <TooltipContent></TooltipContent>
icon={<ArrowLeftOutlined />} </Tooltip>
onClick={onBack}
style={{ color: '#6b7280' }} <Separator orientation="vertical" className="h-6" />
/>
</Tooltip> <h2 className="text-base font-semibold text-foreground m-0">
<Divider type="vertical" /> {title}
<h2 style={{ </h2>
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)}%
</div> </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>
{/* 保存按钮(最右侧) */} <Separator orientation="vertical" className="h-6 mx-1" />
<Button
type="primary" {/* 视图操作 */}
icon={<SaveOutlined />} <Tooltip>
onClick={onSave} <TooltipTrigger asChild>
size="middle" <Button
style={{ variant="ghost"
borderRadius: '6px', size="icon"
fontWeight: '500', onClick={onZoomIn}
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)', >
}} <ZoomIn className="h-4 w-4" />
> </Button>
</TooltipTrigger>
</Button> <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>
</div> </TooltipProvider>
); );
}; };

View File

@ -580,20 +580,20 @@ const WorkflowDesignInner: React.FC = () => {
initialEdges={initialEdges} initialEdges={initialEdges}
onNodeClick={handleNodeClick} onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick} onEdgeClick={handleEdgeClick}
onDrop={handleDrop} onDrop={handleDrop}
onViewportChange={handleViewportChange} onViewportChange={handleViewportChange}
className="workflow-canvas" className="workflow-canvas"
/> />
</div> </div>
</div> </div>
{/* 节点配置弹窗 */} {/* 节点配置弹窗 */}
<NodeConfigModal <NodeConfigModal
visible={configModalVisible} visible={configModalVisible}
node={configNode} node={configNode}
onCancel={handleCloseConfigModal} onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate} onOk={handleNodeConfigUpdate}
/> />
{/* 边条件配置弹窗 */} {/* 边条件配置弹窗 */}
<EdgeConfigModal <EdgeConfigModal