diff --git a/apps/web-antd/src/components/dept-select-modal/dept-select-modal.vue b/apps/web-antd/src/components/dept-select-modal/dept-select-modal.vue index 561ab24e4..8fc25665e 100644 --- a/apps/web-antd/src/components/dept-select-modal/dept-select-modal.vue +++ b/apps/web-antd/src/components/dept-select-modal/dept-select-modal.vue @@ -4,7 +4,7 @@ import type { DataNode } from 'ant-design-vue/es/tree'; import type { SystemDeptApi } from '#/api/system/dept'; -import { defineProps, ref } from 'vue'; +import { ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { handleTree } from '@vben/utils'; diff --git a/apps/web-antd/src/components/simple-process-design/components/nodes/end-event-node.vue b/apps/web-antd/src/components/simple-process-design/components/nodes/end-event-node.vue new file mode 100644 index 000000000..cccca4dbb --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/nodes/end-event-node.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/web-antd/src/components/simple-process-design/components/nodes/node-handler.vue b/apps/web-antd/src/components/simple-process-design/components/nodes/node-handler.vue new file mode 100644 index 000000000..01ab889dc --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/nodes/node-handler.vue @@ -0,0 +1,338 @@ + + + + diff --git a/apps/web-antd/src/components/simple-process-design/components/nodes/start-user-node.vue b/apps/web-antd/src/components/simple-process-design/components/nodes/start-user-node.vue new file mode 100644 index 000000000..748acef32 --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/nodes/start-user-node.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/web-antd/src/components/simple-process-design/components/process-node-tree.vue b/apps/web-antd/src/components/simple-process-design/components/process-node-tree.vue new file mode 100644 index 000000000..3e9e7de1e --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/process-node-tree.vue @@ -0,0 +1,147 @@ + + + diff --git a/apps/web-antd/src/components/simple-process-design/components/simple-process-designer.vue b/apps/web-antd/src/components/simple-process-design/components/simple-process-designer.vue new file mode 100644 index 000000000..f817963c0 --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/simple-process-designer.vue @@ -0,0 +1,246 @@ + + diff --git a/apps/web-antd/src/components/simple-process-design/components/simple-process-model.vue b/apps/web-antd/src/components/simple-process-design/components/simple-process-model.vue new file mode 100644 index 000000000..2495a68fd --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/components/simple-process-model.vue @@ -0,0 +1,286 @@ + + + diff --git a/apps/web-antd/src/components/simple-process-design/consts.ts b/apps/web-antd/src/components/simple-process-design/consts.ts new file mode 100644 index 000000000..74c910f60 --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/consts.ts @@ -0,0 +1,1010 @@ +// TODO 芋艿 这些 常量是不是可以共享 + +interface DictDataType { + label: string; + value: number | string; +} + +// 用户任务的审批类型。 【参考飞书】 +export enum ApproveType { + /** + * 自动通过 + */ + AUTO_APPROVE = 2, + /** + * 自动拒绝 + */ + AUTO_REJECT = 3, + /** + * 人工审批 + */ + USER = 1, +} + +// 多人审批方式类型枚举 ( 用于审批节点 ) +export enum ApproveMethodType { + /** + * 多人或签(通过只需一人,拒绝只需一人) + */ + ANY_APPROVE = 3, + + /** + * 多人会签(按通过比例) + */ + APPROVE_BY_RATIO = 2, + + /** + * 随机挑选一人审批 + */ + RANDOM_SELECT_ONE_APPROVE = 1, + /** + * 多人依次审批 + */ + SEQUENTIAL_APPROVE = 4, +} + +/** + * 任务状态枚举 + */ +export enum TaskStatusEnum { + /** + * 审批通过 + */ + APPROVE = 2, + + /** + * 审批通过中 + */ + APPROVING = 7, + /** + * 已取消 + */ + CANCEL = 4, + /** + * 未开始 + */ + NOT_START = -1, + + /** + * 审批不通过 + */ + REJECT = 3, + + /** + * 已退回 + */ + RETURN = 5, + /** + * 审批中 + */ + RUNNING = 1, + /** + * 待审批 + */ + WAIT = 0, +} + +/** + * 节点类型 + */ +export enum NodeType { + /** + * 子流程节点 + */ + CHILD_PROCESS_NODE = 20, + /** + * 条件分支节点 (对应排他网关) + */ + CONDITION_BRANCH_NODE = 51, + /** + * 条件节点 + */ + CONDITION_NODE = 50, + + /** + * 抄送人节点 + */ + COPY_TASK_NODE = 12, + + /** + * 延迟器节点 + */ + DELAY_TIMER_NODE = 14, + + /** + * 结束节点 + */ + END_EVENT_NODE = 1, + + /** + * 包容分支节点 (对应包容网关) + */ + INCLUSIVE_BRANCH_NODE = 53, + + /** + * 并行分支节点 (对应并行网关) + */ + PARALLEL_BRANCH_NODE = 52, + + /** + * 路由分支节点 + */ + ROUTER_BRANCH_NODE = 54, + /** + * 发起人节点 + */ + START_USER_NODE = 10, + /** + * 办理人节点 + */ + TRANSACTOR_NODE = 13, + + /** + * 触发器节点 + */ + TRIGGER_NODE = 15, + /** + * 审批人节点 + */ + USER_TASK_NODE = 11, +} + +export enum NodeId { + /** + * 发起人节点 Id + */ + END_EVENT_NODE_ID = 'EndEvent', + + /** + * 发起人节点 Id + */ + START_USER_NODE_ID = 'StartUserNode', +} + +// 条件配置类型 ( 用于条件节点配置 ) +export enum ConditionType { + /** + * 条件表达式 + */ + EXPRESSION = 1, + + /** + * 条件规则 + */ + RULE = 2, +} + +// 操作按钮类型枚举 (用于审批节点) +export enum OperationButtonType { + /** + * 加签 + */ + ADD_SIGN = 5, + /** + * 通过 + */ + APPROVE = 1, + /** + * 抄送 + */ + COPY = 7, + /** + * 委派 + */ + DELEGATE = 4, + /** + * 拒绝 + */ + REJECT = 2, + /** + * 退回 + */ + RETURN = 6, + /** + * 转办 + */ + TRANSFER = 3, +} + +// 审批拒绝类型枚举 +export enum RejectHandlerType { + /** + * 结束流程 + */ + FINISH_PROCESS = 1, + /** + * 驳回到指定节点 + */ + RETURN_USER_TASK = 2, +} + +// 用户任务超时处理类型枚举 +export enum TimeoutHandlerType { + /** + * 自动同意 + */ + APPROVE = 2, + /** + * 自动拒绝 + */ + REJECT = 3, + /** + * 自动提醒 + */ + REMINDER = 1, +} + +// 用户任务的审批人为空时,处理类型枚举 +export enum AssignEmptyHandlerType { + /** + * 自动通过 + */ + APPROVE = 1, + /** + * 转交给流程管理员 + */ + ASSIGN_ADMIN = 4, + /** + * 指定人员审批 + */ + ASSIGN_USER = 3, + /** + * 自动拒绝 + */ + REJECT = 2, +} + +// 用户任务的审批人与发起人相同时,处理类型枚举 +export enum AssignStartUserHandlerType { + /** + * 转交给部门负责人审批 + */ + ASSIGN_DEPT_LEADER = 3, + /** + * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过 + */ + SKIP = 2, + /** + * 由发起人对自己审批 + */ + START_USER_AUDIT = 1, +} + +// 时间单位枚举 +export enum TimeUnitType { + /** + * 天 + */ + DAY = 3, + /** + * 小时 + */ + HOUR = 2, + /** + * 分钟 + */ + MINUTE = 1, +} + +/** + * 表单权限的枚举 + */ +export enum FieldPermissionType { + /** + * 隐藏 + */ + NONE = '3', + /** + * 只读 + */ + READ = '1', + /** + * 编辑 + */ + WRITE = '2', +} + +/** + * 延迟类型 + */ +export enum DelayTypeEnum { + /** + * 固定日期时间 + */ + FIXED_DATE_TIME = 2, + /** + * 固定时长 + */ + FIXED_TIME_DURATION = 1, +} + +/** + * 触发器类型枚举 + */ +export enum TriggerTypeEnum { + /** + * 表单数据删除触发器 + */ + FORM_DELETE = 11, + /** + * 表单数据更新触发器 + */ + FORM_UPDATE = 10, + /** + * 接收 HTTP 回调请求触发器 + */ + HTTP_CALLBACK = 2, + /** + * 发送 HTTP 请求触发器 + */ + HTTP_REQUEST = 1, +} + +export enum ChildProcessStartUserTypeEnum { + /** + * 表单 + */ + FROM_FORM = 2, + /** + * 同主流程发起人 + */ + MAIN_PROCESS_START_USER = 1, +} + +export enum ChildProcessStartUserEmptyTypeEnum { + /** + * 子流程管理员 + */ + CHILD_PROCESS_ADMIN = 2, + /** + * 主流程管理员 + */ + MAIN_PROCESS_ADMIN = 3, + /** + * 同主流程发起人 + */ + MAIN_PROCESS_START_USER = 1, +} + +export enum ChildProcessMultiInstanceSourceTypeEnum { + /** + * 固定数量 + */ + FIXED_QUANTITY = 1, + /** + * 多选表单 + */ + MULTIPLE_FORM = 3, + /** + * 数字表单 + */ + NUMBER_FORM = 2, +} + +// 候选人策略枚举 ( 用于审批节点。抄送节点 ) +export enum CandidateStrategy { + /** + * 审批人自选 + */ + APPROVE_USER_SELECT = 34, + /** + * 部门的负责人 + */ + DEPT_LEADER = 21, + /** + * 部门成员 + */ + DEPT_MEMBER = 20, + /** + * 流程表达式 + */ + EXPRESSION = 60, + /** + * 表单内部门负责人 + */ + FORM_DEPT_LEADER = 51, + /** + * 表单内用户字段 + */ + FORM_USER = 50, + /** + * 连续多级部门的负责人 + */ + MULTI_LEVEL_DEPT_LEADER = 23, + /** + * 指定岗位 + */ + POST = 22, + /** + * 指定角色 + */ + ROLE = 10, + /** + * 发起人自己 + */ + START_USER = 36, + /** + * 发起人部门负责人 + */ + START_USER_DEPT_LEADER = 37, + /** + * 发起人连续多级部门的负责人 + */ + START_USER_MULTI_LEVEL_DEPT_LEADER = 38, + /** + * 发起人自选 + */ + START_USER_SELECT = 35, + /** + * 指定用户 + */ + USER = 30, + /** + * 指定用户组 + */ + USER_GROUP = 40, +} + +export enum BpmHttpRequestParamTypeEnum { + /** + * 固定值 + */ + FIXED_VALUE = 1, + /** + * 表单 + */ + FROM_FORM = 2, +} + +// 这里定义 HTTP 请求参数类型 +export type HttpRequestParam = { + key: string; + type: number; + value: string; +}; + +// 监听器结构定义 +export type ListenerHandler = { + body?: HttpRequestParam[]; + enable: boolean; + header?: HttpRequestParam[]; + path?: string; +}; + +/** + * 条件规则结构定义 + */ +export type ConditionRule = { + leftSide: string; + opCode: string; + rightSide: string; +}; + +/** + * 条件结构定义 + */ +export type Condition = { + // 条件规则的逻辑关系是否为且 + and: boolean; + rules: ConditionRule[]; +}; + +/** + * 条件组结构定义 + */ +export type ConditionGroup = { + // 条件组的逻辑关系是否为且 + and: boolean; + // 条件数组 + conditions: Condition[]; +}; + +/** + * 条件节点设置结构定义,用于条件节点 + */ +export type ConditionSetting = { + // 条件表达式 + conditionExpression?: string; + // 条件组 + conditionGroups?: ConditionGroup; + // 条件类型 + conditionType?: ConditionType; + // 是否默认的条件 + defaultFlow?: boolean; +}; + +/** + * 审批拒绝结构定义 + */ +export type RejectHandler = { + // 退回节点 Id + returnNodeId?: string; + // 审批拒绝类型 + type: RejectHandlerType; +}; + +/** + * 审批超时结构定义 + */ +export type TimeoutHandler = { + // 是否开启超时处理 + enable: boolean; + // 执行动作是自动提醒, 最大提醒次数 + maxRemindCount?: number; + // 超时时间设置 + timeDuration?: string; + // 超时执行的动作 + type?: number; +}; + +/** + * 审批人为空的结构定义 + */ +export type AssignEmptyHandler = { + // 审批人为空的处理类型 + type: AssignEmptyHandlerType; + // 指定用户的编号数组 + userIds?: number[]; +}; + +/** + * 延迟设置 + */ +export type DelaySetting = { + // 延迟时间表达式 + delayTime: string; + // 延迟类型 + delayType: number; +}; + +/** + * 路由分支结构定义 + */ +export type RouterSetting = { + conditionExpression: string; + conditionGroups: ConditionGroup; + conditionType: ConditionType; + nodeId: string; +}; + +/** + * 操作按钮权限结构定义 + */ +export type ButtonSetting = { + displayName: string; + enable: boolean; + id: OperationButtonType; +}; + +/** + * HTTP 请求触发器结构定义 + */ +export type HttpRequestTriggerSetting = { + // 请求体参数设置 + body?: HttpRequestParam[]; + // 请求头参数设置 + header?: HttpRequestParam[]; + // 请求响应设置 + response?: Record[]; + // 请求 URL + url: string; +}; + +/** + * 流程表单触发器配置结构定义 + */ +export type FormTriggerSetting = { + // 条件表达式 + conditionExpression?: string; + // 条件组 + conditionGroups?: ConditionGroup; + // 条件类型 + conditionType?: ConditionType; + // 删除表单字段配置 + deleteFields?: string[]; + // 更新表单字段配置 + updateFormFields?: Record; +}; + +/** + * 触发器节点结构定义 + */ +export type TriggerSetting = { + formSettings?: FormTriggerSetting[]; + httpRequestSetting?: HttpRequestTriggerSetting; + type: TriggerTypeEnum; +}; + +export type IOParameter = { + source: string; + target: string; +}; + +export type StartUserSetting = { + emptyType?: ChildProcessStartUserEmptyTypeEnum; + formField?: string; + type: ChildProcessStartUserTypeEnum; +}; + +export type TimeoutSetting = { + enable: boolean; + timeExpression?: string; + type?: DelayTypeEnum; +}; + +export type MultiInstanceSetting = { + approveRatio?: number; + enable: boolean; + sequential?: boolean; + source?: string; + sourceType?: ChildProcessMultiInstanceSourceTypeEnum; +}; + +/** + * 子流程节点结构定义 + */ +export type ChildProcessSetting = { + async: boolean; + calledProcessDefinitionKey: string; + calledProcessDefinitionName: string; + inVariables?: IOParameter[]; + multiInstanceSetting: MultiInstanceSetting; + outVariables?: IOParameter[]; + skipStartUserNode: boolean; + startUserSetting: StartUserSetting; + timeoutSetting: TimeoutSetting; +}; + +/** + * 节点结构定义 + */ +export interface SimpleFlowNode { + id: string; + type: NodeType; + name: string; + showText?: string; + // 孩子节点 + childNode?: SimpleFlowNode; + // 条件节点 + conditionNodes?: SimpleFlowNode[]; + // 审批类型 + approveType?: ApproveType; + // 候选人策略 + candidateStrategy?: number; + // 候选人参数 + candidateParam?: string; + // 多人审批方式 + approveMethod?: ApproveMethodType; + // 通过比例 + approveRatio?: number; + // 审批按钮设置 + buttonsSetting?: any[]; + // 表单权限 + fieldsPermission?: Array>; + // 审批任务超时处理 + timeoutHandler?: TimeoutHandler; + // 审批任务拒绝处理 + rejectHandler?: RejectHandler; + // 审批人为空的处理 + assignEmptyHandler?: AssignEmptyHandler; + // 审批节点的审批人与发起人相同时,对应的处理类型 + assignStartUserHandlerType?: number; + // 创建任务监听器 + taskCreateListener?: ListenerHandler; + // 创建任务监听器 + taskAssignListener?: ListenerHandler; + // 创建任务监听器 + taskCompleteListener?: ListenerHandler; + // 条件设置 + conditionSetting?: ConditionSetting; + // 活动的状态,用于前端节点状态展示 + activityStatus?: TaskStatusEnum; + // 延迟设置 + delaySetting?: DelaySetting; + // 路由分支 + routerGroups?: RouterSetting[]; + defaultFlowId?: string; + // 签名 + signEnable?: boolean; + // 审批意见 + reasonRequire?: boolean; + // 触发器设置 + triggerSetting?: TriggerSetting; + // 子流程 + childProcessSetting?: ChildProcessSetting; +} + +/** + * 条件组默认值 + */ +export const DEFAULT_CONDITION_GROUP_VALUE = { + and: true, + conditions: [ + { + and: true, + rules: [ + { + opCode: '==', + leftSide: '', + rightSide: '', + }, + ], + }, + ], +}; + +export const NODE_DEFAULT_TEXT = new Map(); +NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人'); +NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人'); +NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件'); +NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人'); +NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器'); +NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点'); +NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器'); +NODE_DEFAULT_TEXT.set(NodeType.TRANSACTOR_NODE, '请设置办理人'); +NODE_DEFAULT_TEXT.set(NodeType.CHILD_PROCESS_NODE, '请设置子流程'); + +export const NODE_DEFAULT_NAME = new Map(); +NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人'); +NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人'); +NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件'); +NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人'); +NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器'); +NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支'); +NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器'); +NODE_DEFAULT_NAME.set(NodeType.TRANSACTOR_NODE, '办理人'); +NODE_DEFAULT_NAME.set(NodeType.CHILD_PROCESS_NODE, '子流程'); + +// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 +export const CANDIDATE_STRATEGY: DictDataType[] = [ + { label: '指定成员', value: CandidateStrategy.USER as any }, + { label: '指定角色', value: CandidateStrategy.ROLE as any }, + { label: '指定岗位', value: CandidateStrategy.POST as any }, + { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER as any }, + { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER as any }, + { + label: '连续多级部门负责人', + value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER as any, + }, + { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT as any }, + { label: '审批人自选', value: CandidateStrategy.APPROVE_USER_SELECT as any }, + { label: '发起人本人', value: CandidateStrategy.START_USER as any }, + { + label: '发起人部门负责人', + value: CandidateStrategy.START_USER_DEPT_LEADER as any, + }, + { + label: '发起人连续部门负责人', + value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER as any, + }, + { label: '用户组', value: CandidateStrategy.USER_GROUP as any }, + { label: '表单内用户字段', value: CandidateStrategy.FORM_USER as any }, + { + label: '表单内部门负责人', + value: CandidateStrategy.FORM_DEPT_LEADER as any, + }, + { label: '流程表达式', value: CandidateStrategy.EXPRESSION as any }, +]; +// 审批节点 的审批类型 +export const APPROVE_TYPE: DictDataType[] = [ + { label: '人工审批', value: ApproveType.USER as any }, + { label: '自动通过', value: ApproveType.AUTO_APPROVE as any }, + { label: '自动拒绝', value: ApproveType.AUTO_REJECT as any }, +]; + +export const APPROVE_METHODS: DictDataType[] = [ + { + label: '按顺序依次审批', + value: ApproveMethodType.SEQUENTIAL_APPROVE as any, + }, + { + label: '会签(可同时审批,至少 % 人必须审批通过)', + value: ApproveMethodType.APPROVE_BY_RATIO as any, + }, + { + label: '或签(可同时审批,有一人通过即可)', + value: ApproveMethodType.ANY_APPROVE as any, + }, + { + label: '随机挑选一人审批', + value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE as any, + }, +]; + +export const CONDITION_CONFIG_TYPES: DictDataType[] = [ + { label: '条件规则', value: ConditionType.RULE as any }, + { label: '条件表达式', value: ConditionType.EXPRESSION as any }, +]; + +// 时间单位类型 +export const TIME_UNIT_TYPES: DictDataType[] = [ + { label: '分钟', value: TimeUnitType.MINUTE as any }, + { label: '小时', value: TimeUnitType.HOUR as any }, + { label: '天', value: TimeUnitType.DAY as any }, +]; +// 超时处理执行动作类型 +export const TIMEOUT_HANDLER_TYPES: DictDataType[] = [ + { label: '自动提醒', value: 1 }, + { label: '自动同意', value: 2 }, + { label: '自动拒绝', value: 3 }, +]; +export const REJECT_HANDLER_TYPES: DictDataType[] = [ + { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS as any }, + { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK as any }, + // { label: '结束任务', value: RejectHandlerType.FINISH_TASK } +]; +export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataType[] = [ + { label: '自动通过', value: 1 }, + { label: '自动拒绝', value: 2 }, + { label: '指定成员审批', value: 3 }, + { label: '转交给流程管理员', value: 4 }, +]; +export const ASSIGN_START_USER_HANDLER_TYPES: DictDataType[] = [ + { label: '由发起人对自己审批', value: 1 }, + { label: '自动跳过', value: 2 }, + { label: '转交给部门负责人审批', value: 3 }, +]; + +// 比较运算符 +export const COMPARISON_OPERATORS: DictDataType[] = [ + { + value: '==', + label: '等于', + }, + { + value: '!=', + label: '不等于', + }, + { + value: '>', + label: '大于', + }, + { + value: '>=', + label: '大于等于', + }, + { + value: '<', + label: '小于', + }, + { + value: '<=', + label: '小于等于', + }, +]; +// 审批操作按钮名称 +export const OPERATION_BUTTON_NAME = new Map(); +OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过'); +OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝'); +OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办'); +OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派'); +OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签'); +OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回'); +OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送'); + +// 默认的按钮权限设置 +export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [ + { id: OperationButtonType.APPROVE, displayName: '通过', enable: true }, + { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true }, + { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true }, + { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true }, + { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true }, + { id: OperationButtonType.RETURN, displayName: '退回', enable: true }, +]; + +// 办理人默认的按钮权限设置 +export const TRANSACTOR_DEFAULT_BUTTON_SETTING: ButtonSetting[] = [ + { id: OperationButtonType.APPROVE, displayName: '办理', enable: true }, + { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false }, + { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, + { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, + { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, + { id: OperationButtonType.RETURN, displayName: '退回', enable: false }, +]; + +// 发起人的按钮权限。暂时定死,不可以编辑 +export const START_USER_BUTTON_SETTING: ButtonSetting[] = [ + { id: OperationButtonType.APPROVE, displayName: '提交', enable: true }, + { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false }, + { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false }, + { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false }, + { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false }, + { id: OperationButtonType.RETURN, displayName: '退回', enable: false }, +]; + +export const MULTI_LEVEL_DEPT: DictDataType[] = [ + { label: '第 1 级部门', value: 1 }, + { label: '第 2 级部门', value: 2 }, + { label: '第 3 级部门', value: 3 }, + { label: '第 4 级部门', value: 4 }, + { label: '第 5 级部门', value: 5 }, + { label: '第 6 级部门', value: 6 }, + { label: '第 7 级部门', value: 7 }, + { label: '第 8 级部门', value: 8 }, + { label: '第 9 级部门', value: 9 }, + { label: '第 10 级部门', value: 10 }, + { label: '第 11 级部门', value: 11 }, + { label: '第 12 级部门', value: 12 }, + { label: '第 13 级部门', value: 13 }, + { label: '第 14 级部门', value: 14 }, + { label: '第 15 级部门', value: 15 }, +]; + +/** + * 流程实例的变量枚举 + */ +export enum ProcessVariableEnum { + /** + * 流程定义名称 + */ + PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME', + /** + * 发起时间 + */ + START_TIME = 'PROCESS_START_TIME', + /** + * 发起用户 ID + */ + START_USER_ID = 'PROCESS_START_USER_ID', +} + +export const DELAY_TYPE = [ + { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION }, + { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }, +]; + +export const BPM_HTTP_REQUEST_PARAM_TYPES = [ + { + value: 1, + label: '固定值', + }, + { + value: 2, + label: '表单', + }, +]; + +export const TRIGGER_TYPES: DictDataType[] = [ + { label: '发送 HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST as any }, + { label: '接收 HTTP 回调', value: TriggerTypeEnum.HTTP_CALLBACK as any }, + { label: '修改表单数据', value: TriggerTypeEnum.FORM_UPDATE as any }, + { label: '删除表单数据', value: TriggerTypeEnum.FORM_DELETE as any }, +]; + +export const CHILD_PROCESS_START_USER_TYPE = [ + { + label: '同主流程发起人', + value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER, + }, + { label: '表单', value: ChildProcessStartUserTypeEnum.FROM_FORM }, +]; + +export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [ + { + label: '同主流程发起人', + value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER, + }, + { + label: '子流程管理员', + value: ChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN, + }, + { + label: '主流程管理员', + value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN, + }, +]; + +export const CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = [ + { + label: '固定数量', + value: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY, + }, + { + label: '数字表单', + value: ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM, + }, + { + label: '多选表单', + value: ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM, + }, +]; diff --git a/apps/web-antd/src/components/simple-process-design/helpers.ts b/apps/web-antd/src/components/simple-process-design/helpers.ts new file mode 100644 index 000000000..281572815 --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/helpers.ts @@ -0,0 +1,739 @@ +import type { Ref } from 'vue'; + +import type { + ConditionGroup, + HttpRequestParam, + SimpleFlowNode, +} from './consts'; + +import type { BpmUserGroupApi } from '#/api/bpm/userGroup'; +import type { SystemDeptApi } from '#/api/system/dept'; +import type { SystemPostApi } from '#/api/system/post'; +import type { SystemRoleApi } from '#/api/system/role'; +import type { SystemUserApi } from '#/api/system/user'; + +import { inject, ref, toRaw, unref, watch } from 'vue'; + +import { + ApproveMethodType, + AssignEmptyHandlerType, + AssignStartUserHandlerType, + CandidateStrategy, + COMPARISON_OPERATORS, + ConditionType, + FieldPermissionType, + NODE_DEFAULT_NAME, + NodeType, + ProcessVariableEnum, + RejectHandlerType, + TaskStatusEnum, +} from './consts'; + +export function useWatchNode(props: { + flowNode: SimpleFlowNode; +}): Ref { + const node = ref(props.flowNode); + watch( + () => props.flowNode, + (newValue) => { + node.value = newValue; + }, + ); + return node; +} + +// 解析 formCreate 所有表单字段, 并返回 +const parseFormCreateFields = (formFields?: string[]) => { + const result: Array> = []; + if (formFields) { + formFields.forEach((fieldStr: string) => { + parseFormFields(JSON.parse(fieldStr), result); + }); + } + return result; +}; + +/** + * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件) + * + * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule + * @param fields 解析后表单组件字段 + * @param parentTitle 如果是子表单,子表单的标题,默认为空 + */ +export const parseFormFields = ( + rule: Record, + fields: Array> = [], + parentTitle: string = '', +) => { + const { type, field, $required, title: tempTitle, children } = rule; + if (field && tempTitle) { + let title = tempTitle; + if (parentTitle) { + title = `${parentTitle}.${tempTitle}`; + } + let required = false; + if ($required) { + required = true; + } + fields.push({ + field, + title, + type, + required, + }); + // TODO 子表单 需要处理子表单字段 + // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) { + // // 解析子表单的字段 + // rule.props.rule.forEach((item) => { + // parseFields(item, fieldsPermission, title) + // }) + // } + } + if (children && Array.isArray(children)) { + children.forEach((rule) => { + parseFormFields(rule, fields); + }); + } +}; + +/** + * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点 + */ +export function useFormFieldsPermission( + defaultPermission: FieldPermissionType, +) { + // 字段权限配置. 需要有 field, title, permissioin 属性 + const fieldsPermissionConfig = ref>>([]); + + const formType = inject>('formType', ref()); // 表单类型 + + const formFields = inject>('formFields', ref([])); // 流程表单字段 + + const getNodeConfigFormFields = ( + nodeFormFields?: Array>, + ) => { + nodeFormFields = toRaw(nodeFormFields); + fieldsPermissionConfig.value = + !nodeFormFields || nodeFormFields.length === 0 + ? getDefaultFieldsPermission(unref(formFields)) + : mergeFieldsPermission(nodeFormFields, unref(formFields)); + }; + // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段) + const mergeFieldsPermission = ( + formFieldsPermisson: Array>, + formFields?: string[], + ) => { + let mergedFieldsPermission: Array> = []; + if (formFields) { + mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => { + const found = formFieldsPermisson.find( + (fieldPermission) => fieldPermission.field === item.field, + ); + return { + field: item.field, + title: item.title, + permission: found ? found.permission : defaultPermission, + }; + }); + } + return mergedFieldsPermission; + }; + + // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 + const getDefaultFieldsPermission = (formFields?: string[]) => { + let defaultFieldsPermission: Array> = []; + if (formFields) { + defaultFieldsPermission = parseFormCreateFields(formFields).map( + (item) => { + return { + field: item.field, + title: item.title, + permission: defaultPermission, + }; + }, + ); + } + return defaultFieldsPermission; + }; + + // 获取表单的所有字段,作为下拉框选项 + const formFieldOptions = parseFormCreateFields(unref(formFields)); + + return { + formType, + fieldsPermissionConfig, + formFieldOptions, + getNodeConfigFormFields, + }; +} + +/** + * @description 获取流程表单的字段 + */ +export function useFormFields() { + const formFields = inject>('formFields', ref([])); // 流程表单字段 + return parseFormCreateFields(unref(formFields)); +} + +// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。 +/** + * @description 获取流程表单的字段和发起人字段 + */ +export function useFormFieldsAndStartUser() { + const injectFormFields = inject>('formFields', ref([])); // 流程表单字段 + const formFields = parseFormCreateFields(unref(injectFormFields)); + // 添加发起人 + formFields.unshift({ + field: ProcessVariableEnum.START_USER_ID, + title: '发起人', + required: true, + }); + return formFields; +} + +export type UserTaskFormType = { + approveMethod: ApproveMethodType; + approveRatio?: number; + assignEmptyHandlerType?: AssignEmptyHandlerType; + assignEmptyHandlerUserIds?: number[]; + assignStartUserHandlerType?: AssignStartUserHandlerType; + buttonsSetting: any[]; + candidateStrategy: CandidateStrategy; + deptIds?: number[]; // 部门 + deptLevel?: number; // 部门层级 + expression?: string; // 流程表达式 + formDept?: string; // 表单内部门字段 + formUser?: string; // 表单内用户字段 + maxRemindCount?: number; + postIds?: number[]; // 岗位 + reasonRequire: boolean; + rejectHandlerType?: RejectHandlerType; + returnNodeId?: string; + roleIds?: number[]; // 角色 + signEnable: boolean; + taskAssignListener?: { + body: HttpRequestParam[]; + header: HttpRequestParam[]; + }; + taskAssignListenerEnable?: boolean; + taskAssignListenerPath?: string; + taskCompleteListener?: { + body: HttpRequestParam[]; + header: HttpRequestParam[]; + }; + taskCompleteListenerEnable?: boolean; + taskCompleteListenerPath?: string; + taskCreateListener?: { + body: HttpRequestParam[]; + header: HttpRequestParam[]; + }; + taskCreateListenerEnable?: boolean; + taskCreateListenerPath?: string; + timeDuration?: number; + timeoutHandlerEnable?: boolean; + timeoutHandlerType?: number; + userGroups?: number[]; // 用户组 + userIds?: number[]; // 用户 +}; + +export type CopyTaskFormType = { + candidateStrategy: CandidateStrategy; + deptIds?: number[]; // 部门 + deptLevel?: number; // 部门层级 + expression?: string; // 流程表达式 + formDept?: string; // 表单内部门字段 + formUser?: string; // 表单内用户字段 + postIds?: number[]; // 岗位 + roleIds?: number[]; // 角色 + userGroups?: number[]; // 用户组 + userIds?: number[]; // 用户 +}; + +/** + * @description 节点表单数据。 用于审批节点、抄送节点 + */ +export function useNodeForm(nodeType: NodeType) { + const roleOptions = inject>('roleList', ref([])); // 角色列表 + const postOptions = inject>('postList', ref([])); // 岗位列表 + const userOptions = inject>('userList', ref([])); // 用户列表 + const deptOptions = inject>('deptList', ref([])); // 部门列表 + const userGroupOptions = inject>( + 'userGroupList', + ref([]), + ); // 用户组列表 + const deptTreeOptions = inject>( + 'deptTree', + ref([]), + ); // 部门树 + const formFields = inject>('formFields', ref([])); // 流程表单字段 + const configForm = ref(); + + // eslint-disable-next-line unicorn/prefer-ternary + if ( + nodeType === NodeType.USER_TASK_NODE || + nodeType === NodeType.TRANSACTOR_NODE + ) { + configForm.value = { + candidateStrategy: CandidateStrategy.USER, + approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, + approveRatio: 100, + rejectHandlerType: RejectHandlerType.FINISH_PROCESS, + assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, + returnNodeId: '', + timeoutHandlerEnable: false, + timeoutHandlerType: 1, + timeDuration: 6, // 默认 6小时 + maxRemindCount: 1, // 默认 提醒 1次 + buttonsSetting: [], + }; + } else { + configForm.value = { + candidateStrategy: CandidateStrategy.USER, + }; + } + + const getShowText = (): string => { + let showText = ''; + // 指定成员 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.USER && + configForm.value?.userIds?.length > 0 + ) { + const candidateNames: string[] = []; + userOptions?.value.forEach((item: any) => { + if (configForm.value?.userIds?.includes(item.id)) { + candidateNames.push(item.nickname); + } + }); + showText = `指定成员:${candidateNames.join(',')}`; + } + // 指定角色 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.ROLE && + configForm.value.roleIds?.length > 0 + ) { + const candidateNames: string[] = []; + roleOptions?.value.forEach((item: any) => { + if (configForm.value?.roleIds?.includes(item.id)) { + candidateNames.push(item.name); + } + }); + showText = `指定角色:${candidateNames.join(',')}`; + } + // 指定部门 + if ( + (configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER || + configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER || + configForm.value?.candidateStrategy === + CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) && + configForm.value?.deptIds?.length > 0 + ) { + const candidateNames: string[] = []; + deptOptions?.value.forEach((item) => { + if (configForm.value?.deptIds?.includes(item.id)) { + candidateNames.push(item.name); + } + }); + if ( + configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER + ) { + showText = `部门成员:${candidateNames.join(',')}`; + } else if ( + configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER + ) { + showText = `部门的负责人:${candidateNames.join(',')}`; + } else { + showText = `多级部门的负责人:${candidateNames.join(',')}`; + } + } + + // 指定岗位 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.POST && + configForm.value.postIds?.length > 0 + ) { + const candidateNames: string[] = []; + postOptions?.value.forEach((item) => { + if (configForm.value?.postIds?.includes(item.id)) { + candidateNames.push(item.name); + } + }); + showText = `指定岗位: ${candidateNames.join(',')}`; + } + // 指定用户组 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP && + configForm.value?.userGroups?.length > 0 + ) { + const candidateNames: string[] = []; + userGroupOptions?.value.forEach((item) => { + if (configForm.value?.userGroups?.includes(item.id)) { + candidateNames.push(item.name); + } + }); + showText = `指定用户组: ${candidateNames.join(',')}`; + } + + // 表单内用户字段 + if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) { + const formFieldOptions = parseFormCreateFields(unref(formFields)); + const item = formFieldOptions.find( + (item) => item.field === configForm.value?.formUser, + ); + showText = `表单用户:${item?.title}`; + } + + // 表单内部门负责人 + if ( + configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER + ) { + showText = `表单内部门负责人`; + } + + // 审批人自选 + if ( + configForm.value?.candidateStrategy === + CandidateStrategy.APPROVE_USER_SELECT + ) { + showText = `审批人自选`; + } + + // 发起人自选 + if ( + configForm.value?.candidateStrategy === + CandidateStrategy.START_USER_SELECT + ) { + showText = `发起人自选`; + } + // 发起人自己 + if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) { + showText = `发起人自己`; + } + // 发起人的部门负责人 + if ( + configForm.value?.candidateStrategy === + CandidateStrategy.START_USER_DEPT_LEADER + ) { + showText = `发起人的部门负责人`; + } + // 发起人的部门负责人 + if ( + configForm.value?.candidateStrategy === + CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER + ) { + showText = `发起人连续部门负责人`; + } + // 流程表达式 + if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) { + showText = `流程表达式:${configForm.value.expression}`; + } + return showText; + }; + + /** + * 处理候选人参数的赋值 + */ + const handleCandidateParam = () => { + let candidateParam: string | undefined; + if (!configForm.value) { + return candidateParam; + } + switch (configForm.value.candidateStrategy) { + case CandidateStrategy.DEPT_LEADER: + case CandidateStrategy.DEPT_MEMBER: { + candidateParam = configForm.value.deptIds?.join(','); + break; + } + case CandidateStrategy.EXPRESSION: { + candidateParam = configForm.value.expression; + break; + } + // 表单内部门的负责人 + case CandidateStrategy.FORM_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级 + const deptFieldOnForm = configForm.value.formDept; + candidateParam = deptFieldOnForm?.concat( + `|${configForm.value.deptLevel}`, + ); + break; + } + case CandidateStrategy.FORM_USER: { + candidateParam = configForm.value?.formUser; + break; + } + // 指定连续多级部门的负责人 + case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 + const deptIds = configForm.value.deptIds?.join(','); + candidateParam = deptIds?.concat(`|${configForm.value.deptLevel}`); + break; + } + case CandidateStrategy.POST: { + candidateParam = configForm.value.postIds?.join(','); + break; + } + case CandidateStrategy.ROLE: { + candidateParam = configForm.value.roleIds?.join(','); + break; + } + // 发起人部门负责人 + case CandidateStrategy.START_USER_DEPT_LEADER: + case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: { + candidateParam = `${configForm.value.deptLevel}`; + break; + } + case CandidateStrategy.USER: { + candidateParam = configForm.value.userIds?.join(','); + break; + } + case CandidateStrategy.USER_GROUP: { + candidateParam = configForm.value.userGroups?.join(','); + break; + } + default: { + break; + } + } + return candidateParam; + }; + /** + * 解析候选人参数 + */ + const parseCandidateParam = ( + candidateStrategy: CandidateStrategy, + candidateParam: string | undefined, + ) => { + if (!configForm.value || !candidateParam) { + return; + } + switch (candidateStrategy) { + case CandidateStrategy.DEPT_LEADER: + case CandidateStrategy.DEPT_MEMBER: { + configForm.value.deptIds = candidateParam + .split(',') + .map((item) => +item); + break; + } + case CandidateStrategy.EXPRESSION: { + configForm.value.expression = candidateParam; + break; + } + // 表单内的部门负责人 + case CandidateStrategy.FORM_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级 + const paramArray = candidateParam.split('|'); + if (paramArray.length > 1) { + configForm.value.formDept = paramArray[0]; + if (paramArray[1]) configForm.value.deptLevel = +paramArray[1]; + } + break; + } + case CandidateStrategy.FORM_USER: { + configForm.value.formUser = candidateParam; + break; + } + // 指定连续多级部门的负责人 + case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { + // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级 + const paramArray = candidateParam.split('|') as string[]; + if (paramArray.length > 1) { + configForm.value.deptIds = paramArray[0] + ?.split(',') + .map((item) => +item); + if (paramArray[1]) configForm.value.deptLevel = +paramArray[1]; + } + break; + } + case CandidateStrategy.POST: { + configForm.value.postIds = candidateParam + .split(',') + .map((item) => +item); + break; + } + case CandidateStrategy.ROLE: { + configForm.value.roleIds = candidateParam + .split(',') + .map((item) => +item); + break; + } + // 发起人部门负责人 + case CandidateStrategy.START_USER_DEPT_LEADER: + case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: { + configForm.value.deptLevel = +candidateParam; + break; + } + case CandidateStrategy.USER: { + configForm.value.userIds = candidateParam + .split(',') + .map((item) => +item); + break; + } + case CandidateStrategy.USER_GROUP: { + configForm.value.userGroups = candidateParam + .split(',') + .map((item) => +item); + break; + } + default: { + break; + } + } + }; + return { + configForm, + roleOptions, + postOptions, + userOptions, + userGroupOptions, + deptTreeOptions, + handleCandidateParam, + parseCandidateParam, + getShowText, + }; +} + +/** + * @description 抽屉配置 + */ +export function useDrawer() { + // 抽屉配置是否可见 + const settingVisible = ref(false); + // 关闭配置抽屉 + const closeDrawer = () => { + settingVisible.value = false; + }; + // 打开配置抽屉 + const openDrawer = () => { + settingVisible.value = true; + }; + return { + settingVisible, + closeDrawer, + openDrawer, + }; +} + +/** + * @description 节点名称配置 + */ +export function useNodeName(nodeType: NodeType) { + // 节点名称 + const nodeName = ref(); + // 节点名称输入框 + const showInput = ref(false); + // 点击节点名称编辑图标 + const clickIcon = () => { + showInput.value = true; + }; + // 节点名称输入框失去焦点 + const blurEvent = () => { + showInput.value = false; + nodeName.value = + nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string); + }; + return { + nodeName, + showInput, + clickIcon, + blurEvent, + }; +} + +export function useNodeName2(node: Ref, nodeType: NodeType) { + // 显示节点名称输入框 + const showInput = ref(false); + // 节点名称输入框失去焦点 + const blurEvent = () => { + showInput.value = false; + node.value.name = + node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string); + }; + // 点击节点标题进行输入 + const clickTitle = () => { + showInput.value = true; + }; + return { + showInput, + clickTitle, + blurEvent, + }; +} + +/** + * @description 根据节点任务状态,获取节点任务状态样式 + */ +export function useTaskStatusClass( + taskStatus: TaskStatusEnum | undefined, +): string { + if (!taskStatus) { + return ''; + } + if (taskStatus === TaskStatusEnum.APPROVE) { + return 'status-pass'; + } + if (taskStatus === TaskStatusEnum.RUNNING) { + return 'status-running'; + } + if (taskStatus === TaskStatusEnum.REJECT) { + return 'status-reject'; + } + if (taskStatus === TaskStatusEnum.CANCEL) { + return 'status-cancel'; + } + return ''; +} + +/** 条件组件文字展示 */ +export function getConditionShowText( + conditionType: ConditionType | undefined, + conditionExpression: string | undefined, + conditionGroups: ConditionGroup | undefined, + fieldOptions: Array>, +) { + let showText: string | undefined; + if (conditionType === ConditionType.EXPRESSION && conditionExpression) { + showText = `表达式:${conditionExpression}`; + } + if (conditionType === ConditionType.RULE) { + // 条件组是否为与关系 + const groupAnd = conditionGroups?.and; + let warningMessage: string | undefined; + const conditionGroup = conditionGroups?.conditions.map((item) => { + return `(${item.rules + .map((rule) => { + if (rule.leftSide && rule.rightSide) { + return `${getFormFieldTitle( + fieldOptions, + rule.leftSide, + )} ${getOpName(rule.opCode)} ${rule.rightSide}`; + } else { + // 有一条规则不完善。提示错误 + warningMessage = '请完善条件规则'; + return ''; + } + }) + .join(item.and ? ' 且 ' : ' 或 ')} ) `; + }); + showText = warningMessage + ? '' + : conditionGroup?.join(groupAnd ? ' 且 ' : ' 或 '); + } + return showText; +} + +/** 获取表单字段名称*/ +const getFormFieldTitle = ( + fieldOptions: Array>, + field: string, +) => { + const item = fieldOptions.find((item) => item.field === field); + return item?.title; +}; + +/** 获取操作符名称 */ +const getOpName = (opCode: string): string | undefined => { + const opName = COMPARISON_OPERATORS.find( + (item: any) => item.value === opCode, + ); + return opName?.label; +}; diff --git a/apps/web-antd/src/components/simple-process-design/index.ts b/apps/web-antd/src/components/simple-process-design/index.ts new file mode 100644 index 000000000..05fee448b --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/index.ts @@ -0,0 +1,3 @@ +import './styles/simple-process-designer.scss'; + +export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue'; diff --git a/apps/web-antd/src/components/simple-process-design/styles/iconfont.ttf b/apps/web-antd/src/components/simple-process-design/styles/iconfont.ttf new file mode 100644 index 000000000..06f4e31c4 Binary files /dev/null and b/apps/web-antd/src/components/simple-process-design/styles/iconfont.ttf differ diff --git a/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff b/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff new file mode 100644 index 000000000..0724e7505 Binary files /dev/null and b/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff differ diff --git a/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff2 b/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff2 new file mode 100644 index 000000000..c904bb67d Binary files /dev/null and b/apps/web-antd/src/components/simple-process-design/styles/iconfont.woff2 differ diff --git a/apps/web-antd/src/components/simple-process-design/styles/simple-process-designer.scss b/apps/web-antd/src/components/simple-process-design/styles/simple-process-designer.scss new file mode 100644 index 000000000..817e7a766 --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/styles/simple-process-designer.scss @@ -0,0 +1,829 @@ +// TODO 整个样式是不是要重新优化一下 +// iconfont 样式 +@font-face { + font-family: iconfont; /* Project id 4495938 */ + src: + url('iconfont.woff2?t=1737639517142') format('woff2'), + url('iconfont.woff?t=1737639517142') format('woff'), + url('iconfont.ttf?t=1737639517142') format('truetype'); +} +// 配置节点头部 +.config-header { + display: flex; + flex-direction: column; + + .node-name { + display: flex; + align-items: center; + height: 24px; + font-size: 16px; + line-height: 24px; + cursor: pointer; + } + + .divide-line { + width: 100%; + height: 1px; + margin-top: 16px; + background: #eee; + } + + .config-editable-input { + max-width: 510px; + height: 24px; + font-size: 16px; + line-height: 24px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + outline: 0; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); + } + } +} + +// 表单字段权限 +.field-setting-pane { + display: flex; + flex-direction: column; + font-size: 14px; + + .field-setting-desc { + padding-right: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 700; + } + + .field-permit-title { + display: flex; + align-items: center; + justify-content: space-between; + height: 45px; + padding-left: 12px; + line-height: 45px; + background-color: #f8fafc0a; + border: 1px solid #1f38581a; + + .first-title { + text-align: left !important; + } + + .other-titles { + display: flex; + justify-content: space-between; + } + + .setting-title-label { + display: inline-block; + width: 110px; + padding: 5px 0; + font-size: 13px; + font-weight: 700; + color: #000; + text-align: center; + } + } + + .field-setting-item { + display: flex; + align-items: center; + justify-content: space-between; + height: 38px; + padding-left: 12px; + border: 1px solid #1f38581a; + border-top: 0; + + .field-setting-item-label { + display: inline-block; + width: 110px; + min-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + } + + .field-setting-item-group { + display: flex; + justify-content: space-between; + + .item-radio-wrap { + display: inline-block; + width: 110px; + text-align: center; + } + } + } +} + +// 节点连线气泡卡片样式 +.handler-item-wrapper { + display: flex; + flex-wrap: wrap; + width: 320px; + cursor: pointer; + + .handler-item { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 12px; + } + + .handler-item-icon { + width: 50px; + height: 50px; + text-align: center; + user-select: none; + background: #fff; + border: 1px solid #e2e2e2; + border-radius: 50%; + + &:hover { + background: #e2e2e2; + box-shadow: 0 2px 4px 0 rgb(0 0 0 / 10%); + } + + .icon-size { + font-size: 25px; + line-height: 50px; + } + } + + .approve { + color: #ff943e; + } + + .copy { + color: #3296fa; + } + + .condition { + color: #67c23a; + } + + .parallel { + color: #626aef; + } + + .inclusive { + color: #345da2; + } + + .delay { + color: #e47470; + } + + .trigger { + color: #3373d2; + } + + .router { + color: #ca3a31; + } + + .transactor { + color: #309; + } + + .child-process { + color: #963; + } + + .async-child-process { + color: #066; + } + + .handler-item-text { + width: 80px; + margin-top: 4px; + font-size: 13px; + text-align: center; + } +} +// Simple 流程模型样式 +.simple-process-model-container { + width: 100%; + height: 100%; + overflow-x: auto; + background: url('./svg/simple-process-bg.svg') 0 0 repeat; + background-color: #fafafa; + + .simple-process-model { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: fit-content; + transform: scale(1); + transform-origin: 50% 0 0; + transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + // 节点容器 定义节点宽度 + .node-container { + width: 200px; + } + // 节点 + .node-box { + position: relative; + display: flex; + flex-direction: column; + min-height: 70px; + padding: 5px 10px 8px; + cursor: pointer; + background-color: #fff; + border: 2px solid transparent; + border-radius: 8px; + box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%); + transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); + + &.status-pass { + background-color: #a9da90; + border-color: #67c23a; + } + + &.status-pass:hover { + border-color: #67c23a; + } + + &.status-running { + background-color: #e7f0fe; + border-color: #5a9cf8; + } + + &.status-running:hover { + border-color: #5a9cf8; + } + + &.status-reject { + background-color: #f6e5e5; + border-color: #e47470; + } + + &.status-reject:hover { + border-color: #e47470; + } + + &:hover { + border-color: #0089ff; + + .node-toolbar { + opacity: 1; + } + + .branch-node-move { + display: flex; + } + } + + // 普通节点标题 + .node-title-container { + display: flex; + align-items: center; + padding: 4px; + cursor: pointer; + border-radius: 4px 4px 0 0; + + .node-title-icon { + display: flex; + align-items: center; + + &.user-task { + color: #ff943e; + } + + &.copy-task { + color: #3296fa; + } + + &.start-user { + color: #676565; + } + + &.delay-node { + color: #e47470; + } + + &.trigger-node { + color: #3373d2; + } + + &.router-node { + color: #ca3a31; + } + + &.transactor-task { + color: #309; + } + + &.child-process { + color: #963; + } + + &.async-child-process { + color: #066; + } + } + + .node-title { + margin-left: 4px; + overflow: hidden; + text-overflow: ellipsis; + font-size: 14px; + font-weight: 600; + line-height: 18px; + color: #1f1f1f; + white-space: nowrap; + + &:hover { + border-bottom: 1px dashed #f60; + } + } + } + + // 条件节点标题 + .branch-node-title-container { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; + cursor: pointer; + border-radius: 4px 4px 0 0; + + .input-max-width { + max-width: 115px !important; + } + + .branch-title { + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; + font-weight: 600; + color: #f60; + white-space: nowrap; + + &:hover { + border-bottom: 1px dashed #000; + } + } + + .branch-priority { + min-width: 50px; + font-size: 12px; + } + } + + .node-content { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 32px; + padding: 4px 8px; + margin-top: 4px; + line-height: 32px; + color: #111f2c; + background: rgb(0 0 0 / 3%); + border-radius: 4px; + + .node-text { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ + font-size: 14px; + line-height: 24px; + word-break: break-all; + -webkit-box-orient: vertical; + } + } + + //条件节点内容 + .branch-node-content { + display: flex; + align-items: center; + min-height: 32px; + padding: 4px 0; + margin-top: 4px; + line-height: 32px; + color: #111f2c; + border-radius: 4px; + + .branch-node-text { + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ + font-size: 12px; + line-height: 24px; + word-break: break-all; + -webkit-box-orient: vertical; + } + } + + // 节点操作 :删除 + .node-toolbar { + position: absolute; + top: -20px; + right: 0; + display: flex; + opacity: 0; + + .toolbar-icon { + vertical-align: middle; + text-align: center; + } + } + + // 条件节点左右移动 + .branch-node-move { + position: absolute; + display: none; + align-items: center; + justify-content: center; + width: 10px; + height: 100%; + cursor: pointer; + } + + .move-node-left { + top: 0; + left: -2px; + background: rgb(126 134 142 / 8%); + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + } + + .move-node-right { + top: 0; + right: -2px; + background: rgb(126 134 142 / 8%); + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + .node-config-error { + border-color: #ff5219 !important; + } + // 普通节点包装 + .node-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + // 节点连线处理 + .node-handler-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 70px; + user-select: none; + + &::before { + position: absolute; + top: 0; + z-index: 0; + width: 2px; + height: 100%; + margin: auto; + content: ''; + background-color: #dedede; + } + + .node-handler { + .add-icon { + position: relative; + top: -5px; + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + color: #fff; + cursor: pointer; + background-color: #0089ff; + border-radius: 50%; + + &:hover { + transform: scale(1.1); + } + } + } + + .node-handler-arrow { + position: absolute; + bottom: 0; + left: 50%; + display: flex; + transform: translateX(-50%); + } + } + + // 条件节点包装 + .branch-node-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 16px; + + .branch-node-container { + position: relative; + display: flex; + min-width: fit-content; + + &::before { + position: absolute; + left: 50%; + width: 4px; + height: 100%; + content: ''; + background-color: #fafafa; + transform: translate(-50%); + } + + .branch-node-add { + position: absolute; + top: -18px; + left: 50%; + z-index: 1; + height: 36px; + padding: 0 10px; + font-size: 12px; + line-height: 36px; + border: 2px solid #dedede; + border-radius: 18px; + transform: translateX(-50%); + transform-origin: center center; + } + + .branch-node-readonly { + position: absolute; + top: -18px; + left: 50%; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background-color: #fff; + border: 2px solid #dedede; + border-radius: 50%; + transform: translateX(-50%); + transform-origin: center center; + + &.status-pass { + background-color: #e9f4e2; + border-color: #6bb63c; + } + + &.status-pass:hover { + border-color: #6bb63c; + } + + .icon-size { + font-size: 22px; + + &.condition { + color: #67c23a; + } + + &.parallel { + color: #626aef; + } + + &.inclusive { + color: #345da2; + } + } + } + + .branch-node-item { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + align-items: center; + min-width: 280px; + padding: 40px 40px 0; + background: transparent; + border-top: 2px solid #dedede; + border-bottom: 2px solid #dedede; + + &::before { + position: absolute; + inset: 0; + width: 2px; + height: 100%; + margin: auto; + content: ''; + background-color: #dedede; + } + } + // 覆盖条件节点第一个节点左上角的线 + .branch-line-first-top { + position: absolute; + top: -5px; + left: -1px; + width: 50%; + height: 7px; + content: ''; + background-color: #fafafa; + } + // 覆盖条件节点第一个节点左下角的线 + .branch-line-first-bottom { + position: absolute; + bottom: -5px; + left: -1px; + width: 50%; + height: 7px; + content: ''; + background-color: #fafafa; + } + // 覆盖条件节点最后一个节点右上角的线 + .branch-line-last-top { + position: absolute; + top: -5px; + right: -1px; + width: 50%; + height: 7px; + content: ''; + background-color: #fafafa; + } + // 覆盖条件节点最后一个节点右下角的线 + .branch-line-last-bottom { + position: absolute; + right: -1px; + bottom: -5px; + width: 50%; + height: 7px; + content: ''; + background-color: #fafafa; + } + } + } + + .node-fixed-name { + display: inline-block; + width: auto; + padding: 0 4px; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + white-space: nowrap; + } + // 开始节点包装 + .start-node-wrapper { + position: relative; + margin-top: 16px; + + .start-node-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .start-node-box { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + width: 90px; + height: 36px; + padding: 3px 4px; + color: #212121; + cursor: pointer; + background: #fafafa; + border-radius: 30px; + box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); + } + } + } + + // 结束节点包装 + .end-node-wrapper { + margin-bottom: 16px; + + .end-node-box { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + width: 80px; + height: 36px; + color: #212121; + border: 2px solid #fafafa; + border-radius: 30px; + box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); + + &.status-pass { + background-color: #a9da90; + border-color: #6bb63c; + } + + &.status-pass:hover { + border-color: #6bb63c; + } + + &.status-reject { + background-color: #f6e5e5; + border-color: #e47470; + } + + &.status-reject:hover { + border-color: #e47470; + } + + &.status-cancel { + background-color: #eaeaeb; + border-color: #919398; + } + + &.status-cancel:hover { + border-color: #919398; + } + } + } + + // 可编辑的 title 输入框 + .editable-title-input { + max-width: 145px; + height: 20px; + margin-left: 4px; + font-size: 12px; + line-height: 20px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; + + &:focus { + outline: 0; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); + } + } + } +} + +.iconfont { + font-family: iconfont !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-trigger::before { + content: '\e6d3'; +} + +.icon-router::before { + content: '\e6b2'; +} + +.icon-delay::before { + content: '\e600'; +} + +.icon-start-user::before { + content: '\e679'; +} + +.icon-inclusive::before { + content: '\e602'; +} + +.icon-copy::before { + content: '\e7eb'; +} + +.icon-transactor::before { + content: '\e61c'; +} + +.icon-exclusive::before { + content: '\e717'; +} + +.icon-approve::before { + content: '\e715'; +} + +.icon-parallel::before { + content: '\e688'; +} + +.icon-async-child-process::before { + content: '\e6f2'; +} + +.icon-child-process::before { + content: '\e6c1'; +} diff --git a/apps/web-antd/src/components/simple-process-design/styles/svg/simple-process-bg.svg b/apps/web-antd/src/components/simple-process-design/styles/svg/simple-process-bg.svg new file mode 100644 index 000000000..eb23ab5ae --- /dev/null +++ b/apps/web-antd/src/components/simple-process-design/styles/svg/simple-process-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web-antd/src/views/bpm/model/form/index.vue b/apps/web-antd/src/views/bpm/model/form/index.vue index c0b45af1f..0ea84ac7d 100644 --- a/apps/web-antd/src/views/bpm/model/form/index.vue +++ b/apps/web-antd/src/views/bpm/model/form/index.vue @@ -29,6 +29,7 @@ import { getSimpleUserList } from '#/api/system/user'; import BasicInfo from './modules/basic-info.vue'; import FormDesign from './modules/form-design.vue'; +import ProcessDesign from './modules/process-design.vue'; defineOptions({ name: 'BpmModelCreate' }); @@ -69,6 +70,8 @@ const userStore = useUserStore(); const basicInfoRef = ref>(); // 表单设计组件引用 const formDesignRef = ref>(); +// 流程设计组件引用 +const processDesignRef = ref>(); /** 步骤校验函数 */ const validateBasic = async () => { @@ -82,7 +85,7 @@ const validateForm = async () => { /** 流程设计校验 */ const validateProcess = async () => { - // TODO + await processDesignRef.value?.validate(); }; const currentStep = ref(-1); // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成 @@ -102,7 +105,7 @@ const formData: any = ref({ category: undefined, icon: undefined, description: '', - type: BpmModelType.BPMN, + type: BpmModelType.SIMPLE, formType: BpmModelFormType.NORMAL, formId: '', formCustomCreatePath: '', @@ -190,7 +193,7 @@ const initData = async () => { } else { // 情况三:新增场景 formData.value.startUserType = 0; // 全体 - formData.value.managerUserIds.push(userStore.userInfo?.userId); + formData.value.managerUserIds.push(userStore.userInfo?.id); } // 获取表单列表 @@ -352,6 +355,7 @@ const handleDeploy = async () => { /** 步骤切换处理 */ const handleStepClick = async (index: number) => { try { + console.warn('handleStepClick', index); if (index !== 0) { await validateBasic(); } @@ -401,7 +405,7 @@ onBeforeUnmount(() => { // 清理所有的引用 basicInfoRef.value = undefined; formDesignRef.value = undefined; - // processDesignRef.value = null; + processDesignRef.value = undefined; }); @@ -486,7 +490,7 @@ onBeforeUnmount(() => { /> -
+
{ />
- + + -
+
diff --git a/apps/web-antd/src/views/bpm/model/form/modules/basic-info.vue b/apps/web-antd/src/views/bpm/model/form/modules/basic-info.vue index 2e246141e..f31ad4d7c 100644 --- a/apps/web-antd/src/views/bpm/model/form/modules/basic-info.vue +++ b/apps/web-antd/src/views/bpm/model/form/modules/basic-info.vue @@ -65,7 +65,6 @@ const rules: Record = { category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], type: [{ required: true, message: '流程类型不能为空', trigger: 'blur' }], visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], - // TODO 这个的校验好像没有起作用 managerUserIds: [ { required: true, message: '流程管理员不能为空', trigger: 'blur' }, ], @@ -282,10 +281,12 @@ defineExpose({ validate }); + {{ dict.label }} diff --git a/apps/web-antd/src/views/bpm/model/form/modules/process-design.vue b/apps/web-antd/src/views/bpm/model/form/modules/process-design.vue new file mode 100644 index 000000000..10f99f8d0 --- /dev/null +++ b/apps/web-antd/src/views/bpm/model/form/modules/process-design.vue @@ -0,0 +1,67 @@ + + diff --git a/apps/web-antd/src/views/bpm/model/form/modules/simple-model-design.vue b/apps/web-antd/src/views/bpm/model/form/modules/simple-model-design.vue new file mode 100644 index 000000000..fb606c048 --- /dev/null +++ b/apps/web-antd/src/views/bpm/model/form/modules/simple-model-design.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue index 7b005fcf6..2583fc867 100644 --- a/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue +++ b/apps/web-antd/src/views/bpm/model/modules/category-draggable-model.vue @@ -54,35 +54,39 @@ const columns = [ dataIndex: 'name', key: 'name', align: 'left' as const, - minWidth: 250, + ellipsis: true, + width: 250, }, { title: '可见范围', dataIndex: 'startUserIds', key: 'startUserIds', align: 'center' as const, - minWidth: 150, + ellipsis: true, + width: 150, }, { title: '流程类型', dataIndex: 'type', key: 'type', align: 'center' as const, - minWidth: 120, + ellipsis: true, + width: 120, }, { title: '表单信息', dataIndex: 'formType', key: 'formType', align: 'center' as const, - minWidth: 150, + ellipsis: true, + width: 150, }, { title: '最后发布', dataIndex: 'deploymentTime', key: 'deploymentTime', align: 'center' as const, - minWidth: 250, + width: 250, }, { title: '操作', @@ -316,6 +320,7 @@ const handleRenameSuccess = () => { :columns="columns" :pagination="false" :custom-row="customRow" + :scroll="{ x: '100%' }" row-key="id" >