From fc1d7da91915e6ef1d32b018139100ebc8c63902 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 23 Oct 2025 12:39:43 +0800 Subject: [PATCH] 1 --- frontend/package.json | 4 +- frontend/src/components/ui/command.tsx | 13 +- .../Design/components/NodeConfigModal.tsx | 142 ++++++++++++++++++ .../Workflow/Design/nodes/ApprovalNode.tsx | 27 +++- .../Design/nodes/components/BaseNode.tsx | 79 +++++++--- .../Workflow/Design/utils/dataSourceLoader.ts | 36 ++--- 6 files changed, 252 insertions(+), 49 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 952970b7..cd5b78f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx index 49a2921a..0db642a6 100644 --- a/frontend/src/components/ui/command.tsx +++ b/frontend/src/components/ui/command.tsx @@ -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, @@ -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 ( - + {children} @@ -43,7 +42,7 @@ const CommandInput = React.forwardRef< ) => 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 = ({ 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 ( +
+ + + + + + + + 未找到选项 + + {options.map((option) => ( + handleSelect(option.value)} + > + + {option.label} + + ))} + + + + + + {/* 已选择的标签 */} + {value.length > 0 && ( +
+ {value.map((val: any) => { + const option = options.find((opt: any) => opt.value === val); + return ( + + {option?.label || val} + + + ); + })} +
+ )} +
+ ); +}; + const NodeConfigModal: React.FC = ({ visible, node, @@ -294,6 +386,56 @@ const NodeConfigModal: React.FC = ({ // 动态数据源 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 ( + + ); + } + + // ✅ 单选类型 return renderSelect( options, field, diff --git a/frontend/src/pages/Workflow/Design/nodes/ApprovalNode.tsx b/frontend/src/pages/Workflow/Design/nodes/ApprovalNode.tsx index 02d67f02..037933f0 100644 --- a/frontend/src/pages/Workflow/Design/nodes/ApprovalNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/ApprovalNode.tsx @@ -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: { diff --git a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx index a9d00258..43691bd1 100644 --- a/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/components/BaseNode.tsx @@ -18,6 +18,34 @@ const BaseNode: React.FC = ({ 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 = ({ data, selected }) => { return null; } - 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)); - - return { - key, - title: fieldSchema.title || key, - description: fieldSchema.description, - isFilled, - }; - }); + const allInputs = Object.keys(schema.properties) + .map(key => { + const fieldSchema = schema.properties![key]; + const fieldValue = inputMapping[key]; + + // ✅ 检查 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]; + + // 如果条件不满足,跳过该字段 + 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 => input !== null); // 过滤掉条件不满足的字段 if (allInputs.length === 0) { return null; diff --git a/frontend/src/pages/Workflow/Design/utils/dataSourceLoader.ts b/frontend/src/pages/Workflow/Design/utils/dataSourceLoader.ts index c6b1442b..101776cc 100644 --- a/frontend/src/pages/Workflow/Design/utils/dataSourceLoader.ts +++ b/frontend/src/pages/Workflow/Design/utils/dataSourceLoader.ts @@ -102,35 +102,37 @@ export const DATA_SOURCE_REGISTRY: Record = { } }, [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 })); } }