This commit is contained in:
dengqichen 2025-10-23 12:39:43 +08:00
parent 8e5e97cb95
commit fc1d7da919
6 changed files with 252 additions and 49 deletions

View File

@ -22,7 +22,7 @@
"@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.15",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
@ -44,7 +44,7 @@
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"antd": "^5.23.1", "antd": "^5.23.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"cmdk": "^1.0.4", "cmdk": "^1.1.1",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"form-render": "^2.5.6", "form-render": "^2.5.6",

View File

@ -1,9 +1,10 @@
import * as React from "react" import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { Search } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog" import { Dialog, DialogContent } from "@/components/ui/dialog"
import { type DialogProps } from "@radix-ui/react-dialog"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
@ -20,12 +21,10 @@ const Command = React.forwardRef<
)) ))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: DialogProps) => {
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children} {children}
</Command> </Command>
@ -43,7 +42,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
@ -114,7 +113,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className className
)} )}
{...props} {...props}

View File

@ -10,6 +10,10 @@ import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/ui/command';
import { Badge } from '@/components/ui/badge';
import { ChevronsUpDown, X } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import type { FlowNode, FlowNodeData, FlowEdge } from '../types'; import type { FlowNode, FlowNodeData, FlowEdge } from '../types';
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types'; import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
@ -28,6 +32,94 @@ interface NodeConfigModalProps {
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void; onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
} }
// ✅ 多选组件(使用 shadcn/ui
interface MultiSelectProps {
options: Array<{ label: string; value: any }>;
value: any[];
onChange: (value: any[]) => void;
placeholder?: string;
disabled?: boolean;
}
const MultiSelect: React.FC<MultiSelectProps> = ({ options, value = [], onChange, placeholder, disabled }) => {
const [open, setOpen] = React.useState(false);
const handleSelect = (optionValue: any) => {
const newValues = value.includes(optionValue)
? value.filter((v: any) => v !== optionValue)
: [...value, optionValue];
onChange(newValues);
};
const handleRemove = (optionValue: any) => {
onChange(value.filter((v: any) => v !== optionValue));
};
return (
<div className="space-y-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
disabled={disabled}
>
{value.length > 0
? `已选择 ${value.length}`
: placeholder || '请选择'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align="start">
<Command>
<CommandInput placeholder="搜索..." />
<CommandEmpty></CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => handleSelect(option.value)}
>
<Checkbox
checked={value.includes(option.value)}
className="mr-2"
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
{/* 已选择的标签 */}
{value.length > 0 && (
<div className="flex flex-wrap gap-1">
{value.map((val: any) => {
const option = options.find((opt: any) => opt.value === val);
return (
<Badge key={val} variant="secondary" className="px-2 py-1">
{option?.label || val}
<button
type="button"
className="ml-1 hover:text-destructive"
onClick={() => handleRemove(val)}
disabled={disabled}
>
<X className="h-3 w-3" />
</button>
</Badge>
);
})}
</div>
)}
</div>
);
};
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
visible, visible,
node, node,
@ -294,6 +386,56 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 动态数据源 // 动态数据源
if (prop['x-dataSource']) { if (prop['x-dataSource']) {
const options = dataSourceCache[prop['x-dataSource']] || []; const options = dataSourceCache[prop['x-dataSource']] || [];
// ✅ 数组类型 - 检查是否应该是多选
if (prop.type === 'array') {
// 检查 x-multiple-condition是否应该多选
let isMultiple = true; // 默认多选
if (prop['x-multiple-condition']) {
const multipleCondition = prop['x-multiple-condition'];
const conditionField = multipleCondition.field;
const conditionValue = multipleCondition.value;
const conditionOperator = multipleCondition.operator || '===';
// 获取条件字段的当前值
const currentFieldValue = inputForm.watch(conditionField);
// 评估条件
isMultiple = evaluateCondition(currentFieldValue, conditionValue, conditionOperator);
}
// 如果是单选模式
if (!isMultiple) {
return renderSelect(
options,
{
...field,
// 转换数组为单个值
value: Array.isArray(field.value) && field.value.length > 0 ? field.value[0] : field.value,
onChange: (value: any) => {
// 单选时,将值包装为数组
field.onChange([value]);
}
},
prop,
loadingDataSources || loading
);
}
// 多选模式
return (
<MultiSelect
options={options}
value={field.value || []}
onChange={field.onChange}
placeholder={prop.description || `请选择${prop.title || ''}`}
disabled={loadingDataSources || loading}
/>
);
}
// ✅ 单选类型
return renderSelect( return renderSelect(
options, options,
field, field,

View File

@ -63,40 +63,55 @@ export const ApprovalNodeDefinition: ConfigurableNodeDefinition = {
approvers: { approvers: {
type: "array", type: "array",
title: "审批人列表", title: "审批人列表",
description: "选择具体的审批人。会签模式下:选中的所有用户都必须审批;或签模式:任一用户审批即可", description: "单人审批:选择一个审批人;会签模式:选中的所有用户都必须审批;或签模式:任一用户审批即可",
items: { items: {
type: "number" type: "string" // 后台使用 username字符串
}, },
'x-dataSource': DataSourceType.USERS, 'x-dataSource': DataSourceType.USERS,
'x-condition': { 'x-condition': {
field: 'approverType', field: 'approverType',
value: 'USER' value: 'USER'
},
'x-multiple-condition': {
field: 'approvalMode',
operator: 'in',
value: ['ALL', 'ANY'] // 只有会签和或签才是多选
} }
}, },
approverRoles: { approverRoles: {
type: "array", type: "array",
title: "审批角色", title: "审批角色",
description: "选择审批角色。会签模式下:拥有选中角色的所有用户都必须审批;或签模式:任一拥有该角色的用户审批即可", description: "单人审批:选择一个角色;会签模式:拥有选中角色的所有用户都必须审批;或签模式:任一拥有该角色的用户审批即可",
items: { items: {
type: "number" type: "string" // 使用 code字符串
}, },
'x-dataSource': DataSourceType.ROLES, 'x-dataSource': DataSourceType.ROLES,
'x-condition': { 'x-condition': {
field: 'approverType', field: 'approverType',
value: 'ROLE' value: 'ROLE'
},
'x-multiple-condition': {
field: 'approvalMode',
operator: 'in',
value: ['ALL', 'ANY']
} }
}, },
approverDepartments: { approverDepartments: {
type: "array", type: "array",
title: "审批部门", title: "审批部门",
description: "选择审批部门。会签模式下:选中部门的所有成员都必须审批;或签模式:任一部门成员审批即可", description: "单人审批:选择一个部门;会签模式:选中部门的所有成员都必须审批;或签模式:任一部门成员审批即可",
items: { items: {
type: "number" type: "string" // 使用 code字符串
}, },
'x-dataSource': DataSourceType.DEPARTMENTS, 'x-dataSource': DataSourceType.DEPARTMENTS,
'x-condition': { 'x-condition': {
field: 'approverType', field: 'approverType',
value: 'DEPARTMENT' value: 'DEPARTMENT'
},
'x-multiple-condition': {
field: 'approvalMode',
operator: 'in',
value: ['ALL', 'ANY']
} }
}, },
approverVariable: { approverVariable: {

View File

@ -18,6 +18,34 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
const config = definition.renderConfig; const config = definition.renderConfig;
// 条件评估函数
const evaluateCondition = (currentValue: any, expectedValue: any, operator: string = '==='): boolean => {
switch (operator) {
case '===':
case '==':
return currentValue === expectedValue;
case '!==':
case '!=':
return currentValue !== expectedValue;
case '>':
return Number(currentValue) > Number(expectedValue);
case '<':
return Number(currentValue) < Number(expectedValue);
case '>=':
return Number(currentValue) >= Number(expectedValue);
case '<=':
return Number(currentValue) <= Number(expectedValue);
case 'includes':
return Array.isArray(currentValue) && currentValue.includes(expectedValue);
case 'notIncludes':
return Array.isArray(currentValue) && !currentValue.includes(expectedValue);
case 'in':
return Array.isArray(expectedValue) && expectedValue.includes(currentValue);
default:
return currentValue === expectedValue;
}
};
// 渲染输入字段标签(来自 inputMappingSchema // 渲染输入字段标签(来自 inputMappingSchema
const renderInputSection = () => { const renderInputSection = () => {
if (!isConfigurableNode(definition) || !definition.inputMappingSchema) { if (!isConfigurableNode(definition) || !definition.inputMappingSchema) {
@ -32,23 +60,40 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
return null; return null;
} }
const allInputs = Object.keys(schema.properties).map(key => { const allInputs = Object.keys(schema.properties)
const fieldSchema = schema.properties![key]; .map(key => {
const fieldValue = inputMapping[key]; const fieldSchema = schema.properties![key];
const fieldValue = inputMapping[key];
// 检查字段是否已填写非空、非null、非undefined、非空字符串
const isFilled = fieldValue !== undefined && // ✅ 检查 x-condition字段是否应该显示
fieldValue !== null && if (fieldSchema['x-condition']) {
fieldValue !== '' && const condition = fieldSchema['x-condition'];
(typeof fieldValue !== 'number' || !isNaN(fieldValue)); const conditionField = condition.field;
const conditionValue = condition.value;
return { const conditionOperator = condition.operator || '===';
key, const currentFieldValue = inputMapping[conditionField];
title: fieldSchema.title || key,
description: fieldSchema.description, // 如果条件不满足,跳过该字段
isFilled, if (!evaluateCondition(currentFieldValue, conditionValue, conditionOperator)) {
}; return null;
}); }
}
// 检查字段是否已填写非空、非null、非undefined、非空字符串、非空数组
const isFilled = fieldValue !== undefined &&
fieldValue !== null &&
fieldValue !== '' &&
!(Array.isArray(fieldValue) && fieldValue.length === 0) &&
(typeof fieldValue !== 'number' || !isNaN(fieldValue));
return {
key,
title: fieldSchema.title || key,
description: fieldSchema.description,
isFilled,
};
})
.filter((input): input is NonNullable<typeof input> => input !== null); // 过滤掉条件不满足的字段
if (allInputs.length === 0) { if (allInputs.length === 0) {
return null; return null;

View File

@ -102,35 +102,37 @@ export const DATA_SOURCE_REGISTRY: Record<DataSourceType, DataSourceConfig> = {
} }
}, },
[DataSourceType.USERS]: { [DataSourceType.USERS]: {
url: '/api/v1/system/users/page', url: '/api/v1/user/list',
params: {pageSize: 1000}, transform: (data: any[]) => {
transform: (data: any) => { return data.map((item: any) => ({
const users = data.content || data; label: `${item.nickname} (${item.username})`,
return users.map((item: any) => ({ value: item.username, // 后台使用 username 进行审批
label: `${item.name} (${item.username})`, id: item.id,
value: item.id, email: item.email,
username: item.username, departmentName: item.departmentName
email: item.email
})); }));
} }
}, },
[DataSourceType.ROLES]: { [DataSourceType.ROLES]: {
url: '/api/v1/system/roles/list', url: '/api/v1/role/list',
transform: (data: any[]) => { transform: (data: any[]) => {
return data.map((item: any) => ({ return data.map((item: any) => ({
label: item.name, label: `${item.name} (${item.code})`,
value: item.id, value: item.code, // 使用 code 作为值
code: item.code id: item.id,
description: item.description
})); }));
} }
}, },
[DataSourceType.DEPARTMENTS]: { [DataSourceType.DEPARTMENTS]: {
url: '/api/v1/system/departments/list', url: '/api/v1/department/list',
transform: (data: any[]) => { transform: (data: any[]) => {
return data.map((item: any) => ({ return data.map((item: any) => ({
label: item.name, label: `${item.name} (${item.code})`,
value: item.id, value: item.code, // 使用 code 作为值
code: item.code id: item.id,
description: item.description,
parentId: item.parentId
})); }));
} }
} }