1
This commit is contained in:
parent
8e5e97cb95
commit
fc1d7da919
@ -22,7 +22,7 @@
|
||||
"@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-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
@ -44,7 +44,7 @@
|
||||
"ajv-formats": "^3.0.1",
|
||||
"antd": "^5.23.1",
|
||||
"axios": "^1.6.2",
|
||||
"cmdk": "^1.0.4",
|
||||
"cmdk": "^1.1.1",
|
||||
"dagre": "^0.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"form-render": "^2.5.6",
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@ -20,12 +21,10 @@ const Command = React.forwardRef<
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</Command>
|
||||
@ -43,7 +42,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@ -114,7 +113,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -10,6 +10,10 @@ import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
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 type { FlowNode, FlowNodeData, FlowEdge } from '../types';
|
||||
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
|
||||
@ -28,6 +32,94 @@ interface NodeConfigModalProps {
|
||||
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> = ({
|
||||
visible,
|
||||
node,
|
||||
@ -294,6 +386,56 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
// 动态数据源
|
||||
if (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(
|
||||
options,
|
||||
field,
|
||||
|
||||
@ -63,40 +63,55 @@ export const ApprovalNodeDefinition: ConfigurableNodeDefinition = {
|
||||
approvers: {
|
||||
type: "array",
|
||||
title: "审批人列表",
|
||||
description: "选择具体的审批人。会签模式下:选中的所有用户都必须审批;或签模式下:任一用户审批即可",
|
||||
description: "单人审批:选择一个审批人;会签模式:选中的所有用户都必须审批;或签模式:任一用户审批即可",
|
||||
items: {
|
||||
type: "number"
|
||||
type: "string" // 后台使用 username(字符串)
|
||||
},
|
||||
'x-dataSource': DataSourceType.USERS,
|
||||
'x-condition': {
|
||||
field: 'approverType',
|
||||
value: 'USER'
|
||||
},
|
||||
'x-multiple-condition': {
|
||||
field: 'approvalMode',
|
||||
operator: 'in',
|
||||
value: ['ALL', 'ANY'] // 只有会签和或签才是多选
|
||||
}
|
||||
},
|
||||
approverRoles: {
|
||||
type: "array",
|
||||
title: "审批角色",
|
||||
description: "选择审批角色。会签模式下:拥有选中角色的所有用户都必须审批;或签模式下:任一拥有该角色的用户审批即可",
|
||||
description: "单人审批:选择一个角色;会签模式:拥有选中角色的所有用户都必须审批;或签模式:任一拥有该角色的用户审批即可",
|
||||
items: {
|
||||
type: "number"
|
||||
type: "string" // 使用 code(字符串)
|
||||
},
|
||||
'x-dataSource': DataSourceType.ROLES,
|
||||
'x-condition': {
|
||||
field: 'approverType',
|
||||
value: 'ROLE'
|
||||
},
|
||||
'x-multiple-condition': {
|
||||
field: 'approvalMode',
|
||||
operator: 'in',
|
||||
value: ['ALL', 'ANY']
|
||||
}
|
||||
},
|
||||
approverDepartments: {
|
||||
type: "array",
|
||||
title: "审批部门",
|
||||
description: "选择审批部门。会签模式下:选中部门的所有成员都必须审批;或签模式下:任一部门成员审批即可",
|
||||
description: "单人审批:选择一个部门;会签模式:选中部门的所有成员都必须审批;或签模式:任一部门成员审批即可",
|
||||
items: {
|
||||
type: "number"
|
||||
type: "string" // 使用 code(字符串)
|
||||
},
|
||||
'x-dataSource': DataSourceType.DEPARTMENTS,
|
||||
'x-condition': {
|
||||
field: 'approverType',
|
||||
value: 'DEPARTMENT'
|
||||
},
|
||||
'x-multiple-condition': {
|
||||
field: 'approvalMode',
|
||||
operator: 'in',
|
||||
value: ['ALL', 'ANY']
|
||||
}
|
||||
},
|
||||
approverVariable: {
|
||||
|
||||
@ -18,6 +18,34 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
|
||||
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)
|
||||
const renderInputSection = () => {
|
||||
if (!isConfigurableNode(definition) || !definition.inputMappingSchema) {
|
||||
@ -32,23 +60,40 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allInputs = Object.keys(schema.properties).map(key => {
|
||||
const fieldSchema = schema.properties![key];
|
||||
const fieldValue = inputMapping[key];
|
||||
const allInputs = Object.keys(schema.properties)
|
||||
.map(key => {
|
||||
const fieldSchema = schema.properties![key];
|
||||
const fieldValue = inputMapping[key];
|
||||
|
||||
// 检查字段是否已填写(非空、非null、非undefined、非空字符串)
|
||||
const isFilled = fieldValue !== undefined &&
|
||||
fieldValue !== null &&
|
||||
fieldValue !== '' &&
|
||||
(typeof fieldValue !== 'number' || !isNaN(fieldValue));
|
||||
// ✅ 检查 x-condition(字段是否应该显示)
|
||||
if (fieldSchema['x-condition']) {
|
||||
const condition = fieldSchema['x-condition'];
|
||||
const conditionField = condition.field;
|
||||
const conditionValue = condition.value;
|
||||
const conditionOperator = condition.operator || '===';
|
||||
const currentFieldValue = inputMapping[conditionField];
|
||||
|
||||
return {
|
||||
key,
|
||||
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) {
|
||||
return null;
|
||||
|
||||
@ -102,35 +102,37 @@ export const DATA_SOURCE_REGISTRY: Record<DataSourceType, DataSourceConfig> = {
|
||||
}
|
||||
},
|
||||
[DataSourceType.USERS]: {
|
||||
url: '/api/v1/system/users/page',
|
||||
params: {pageSize: 1000},
|
||||
transform: (data: any) => {
|
||||
const users = data.content || data;
|
||||
return users.map((item: any) => ({
|
||||
label: `${item.name} (${item.username})`,
|
||||
value: item.id,
|
||||
username: item.username,
|
||||
email: item.email
|
||||
url: '/api/v1/user/list',
|
||||
transform: (data: any[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: `${item.nickname} (${item.username})`,
|
||||
value: item.username, // 后台使用 username 进行审批
|
||||
id: item.id,
|
||||
email: item.email,
|
||||
departmentName: item.departmentName
|
||||
}));
|
||||
}
|
||||
},
|
||||
[DataSourceType.ROLES]: {
|
||||
url: '/api/v1/system/roles/list',
|
||||
url: '/api/v1/role/list',
|
||||
transform: (data: any[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
code: item.code
|
||||
label: `${item.name} (${item.code})`,
|
||||
value: item.code, // 使用 code 作为值
|
||||
id: item.id,
|
||||
description: item.description
|
||||
}));
|
||||
}
|
||||
},
|
||||
[DataSourceType.DEPARTMENTS]: {
|
||||
url: '/api/v1/system/departments/list',
|
||||
url: '/api/v1/department/list',
|
||||
transform: (data: any[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
code: item.code
|
||||
label: `${item.name} (${item.code})`,
|
||||
value: item.code, // 使用 code 作为值
|
||||
id: item.id,
|
||||
description: item.description,
|
||||
parentId: item.parentId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user