From c9a6f7cbb5209415e626df29c7572cf40fe92b66 Mon Sep 17 00:00:00 2001 From: houzhongjian <houzhongyi@126.com> Date: 星期一, 06 一月 2025 11:44:50 +0800 Subject: [PATCH] 1、工作流程功能优化,解决流程图模拟功能simulation不生效的bug 2、system menu等页面修改 3、Footer copyright年份修改为动态 --- src/components/UserSelectForm/index.vue | 39 src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue | 85 - src/directives/permission/hasPermi.ts | 21 src/views/bpm/model/ModelForm.vue | 189 +++ src/views/bpm/model/index.vue | 10 src/utils/routerHelper.ts | 2 src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue | 119 + src/views/bpm/processInstance/detail/index.vue | 4 src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue | 213 +++ src/views/bpm/model/form/FormDesign.vue | 137 ++ src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue | 2 src/views/bpm/model/form/ProcessDesign.vue | 235 ++++ src/components/SimpleProcessDesignerV2/src/NodeHandler.vue | 17 src/views/system/menu/index.vue | 200 ++- src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue | 46 src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss | 7 src/views/bpm/model/form/BasicInfo.vue | 301 +++++ src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue | 4 src/views/bpm/simple/SimpleModelDesign.vue | 148 ++ src/components/RouterSearch/index.vue | 7 src/views/bpm/model/form/index.vue | 439 ++++++++ src/components/Echart/src/Echart.vue | 8 src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue | 189 +++ src/layout/components/Footer/src/Footer.vue | 6 src/router/modules/remaining.ts | 24 src/components/SimpleProcessDesignerV2/src/consts.ts | 36 src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue | 36 src/views/bpm/model/CategoryDraggableModel.vue | 50 src/views/infra/file/index.vue | 10 src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue | 98 + src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue | 7 src/views/bpm/model/editor/index.vue | 258 +++- src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue | 20 src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue | 47 package.json | 4 src/views/system/area/index.vue | 22 src/components/SimpleProcessDesignerV2/src/node.ts | 31 src/assets/svgs/bpm/delay.svg | 1 src/views/bpm/task/done/index.vue | 2 src/components/bpmnProcessDesigner/package/theme/process-designer.scss | 18 40 files changed, 2,664 insertions(+), 428 deletions(-) diff --git a/package.json b/package.json index a7c562a..f904721 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "animate.css": "^4.1.1", "axios": "^1.6.8", "benz-amr-recorder": "^1.1.5", - "bpmn-js-token-simulation": "^0.10.0", + "bpmn-js-token-simulation": "^0.36.0", "camunda-bpmn-moddle": "^7.0.1", "cropperjs": "^1.6.1", "crypto-js": "^4.2.0", @@ -47,7 +47,7 @@ "driver.js": "^1.3.1", "echarts": "^5.5.0", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.8.4", + "element-plus": "2.9.1", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", diff --git a/src/assets/svgs/bpm/delay.svg b/src/assets/svgs/bpm/delay.svg new file mode 100644 index 0000000..cbc31df --- /dev/null +++ b/src/assets/svgs/bpm/delay.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg> \ No newline at end of file diff --git a/src/components/Echart/src/Echart.vue b/src/components/Echart/src/Echart.vue index fd3342d..02738ca 100644 --- a/src/components/Echart/src/Echart.vue +++ b/src/components/Echart/src/Echart.vue @@ -9,6 +9,10 @@ import { isString } from '@/utils/is' import { useDesign } from '@/hooks/web/useDesign' +import 'echarts/lib/component/markPoint' +import 'echarts/lib/component/markLine' +import 'echarts/lib/component/markArea' + defineOptions({ name: 'EChart' }) const { getPrefixCls, variables } = useDesign() @@ -94,13 +98,13 @@ contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0] unref(contentEl) && - (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) + (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler) }) onBeforeUnmount(() => { window.removeEventListener('resize', resizeHandler) unref(contentEl) && - (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) + (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler) }) onActivated(() => { diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue index 3fa35f6..42a4174 100644 --- a/src/components/RouterSearch/index.vue +++ b/src/components/RouterSearch/index.vue @@ -79,7 +79,12 @@ function handleChange(path) { router.push({ path }) + hiddenSearch() hiddenTopSearch() +} + +function hiddenSearch() { + showSearch.value = false } function hiddenTopSearch() { @@ -99,6 +104,8 @@ // 监听 ctrl + k function listenKey(event) { if ((event.ctrlKey || event.metaKey) && event.key === 'k') { + // 阻止触发浏览器默认事件 + event.preventDefault() showSearch.value = !showSearch.value // 这里可以执行相应的操作(例如打开搜索框等) } diff --git a/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue index 853a0aa..4dfd51a 100644 --- a/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue +++ b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue @@ -39,6 +39,13 @@ </div> <div class="handler-item-text">包容分支</div> </div> + <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)"> + <!-- TODO @芋艿 需要更换一下iconfont的图标 --> + <div class="handler-item-icon copy"> + <span class="iconfont icon-size icon-copy"></span> + </div> + <div class="handler-item-text">延迟器</div> + </div> </div> <template #reference> <div class="add-icon"><Icon icon="ep:plus" /></div> @@ -208,6 +215,16 @@ } emits('update:childNode', data) } + if (type === NodeType.DELAY_TIMER_NODE) { + const data: SimpleFlowNode = { + id: 'Activity_' + generateUUID(), + name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string, + showText: '', + type: NodeType.DELAY_TIMER_NODE, + childNode: props.childNode + } + emits('update:childNode', data) + } } </script> diff --git a/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue index 9b1d65a..419501a 100644 --- a/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue +++ b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue @@ -38,6 +38,12 @@ @update:model-value="handleModelValueUpdate" @find:parent-node="findFromParentNode" /> + <!-- 延迟器节点 --> + <DelayTimerNode + v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE" + :flow-node="currentNode" + @update:flow-node="handleModelValueUpdate" + /> <!-- 递归显示孩子节点 --> <ProcessNodeTree v-if="currentNode && currentNode.childNode" @@ -60,6 +66,7 @@ import ExclusiveNode from './nodes/ExclusiveNode.vue' import ParallelNode from './nodes/ParallelNode.vue' import InclusiveNode from './nodes/InclusiveNode.vue' +import DelayTimerNode from './nodes/DelayTimerNode.vue' import { SimpleFlowNode, NodeType } from './consts' import { useWatchNode } from './node' defineOptions({ diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue index 6b5ff99..22e6073 100644 --- a/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue +++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue @@ -1,6 +1,7 @@ <template> <div v-loading="loading" class="overflow-auto"> <SimpleProcessModel + ref="simpleProcessModelRef" v-if="processNodeTree" :flow-node="processNodeTree" :readonly="false" @@ -38,12 +39,30 @@ defineOptions({ name: 'SimpleProcessDesigner' }) -const emits = defineEmits(['success']) // 保存成功事件 + +const emits = defineEmits(['success', 'init-finished']) // 保存成功事件 const props = defineProps({ modelId: { type: String, - required: true + required: false + }, + modelKey: { + type: String, + required: false + }, + modelName: { + type: String, + required: false + }, + // 可发起流程的人员编号 + startUserIds : { + type: Array, + required: false + }, + value: { + type: [String, Object], + required: false } }) @@ -56,6 +75,10 @@ const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 const deptTreeOptions = ref() const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 + +// 添加当前值的引用 +const currentValue = ref<SimpleFlowNode | undefined>() + provide('formFields', formFields) provide('formType', formType) provide('roleList', roleOptions) @@ -64,33 +87,101 @@ provide('deptList', deptOptions) provide('userGroupList', userGroupOptions) provide('deptTree', deptTreeOptions) +provide('startUserIds', props.startUserIds) const message = useMessage() // 国际化 const processNodeTree = ref<SimpleFlowNode | undefined>() const errorDialogVisible = ref(false) let errorNodes: SimpleFlowNode[] = [] -const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { - if (!simpleModelNode) { - message.error('模型数据为空') - return - } - try { - loading.value = true - const data = { - id: props.modelId, - simpleModel: simpleModelNode + +// 添加更新模型的方法 +const updateModel = () => { + if (!processNodeTree.value) { + processNodeTree.value = { + name: '发起人', + type: NodeType.START_USER_NODE, + id: NodeId.START_USER_NODE_ID, + childNode: { + id: NodeId.END_EVENT_NODE_ID, + name: '结束', + type: NodeType.END_EVENT_NODE + } } - const result = await updateBpmSimpleModel(data) - if (result) { - message.success('修改成功') - emits('success') - } else { - message.alert('修改失败') - } - } finally { - loading.value = false + // 初始化时也触发一次保存 + saveSimpleFlowModel(processNodeTree.value) } } + +// 加载流程数据 +const loadProcessData = async (data: any) => { + try { + if (data) { + const parsedData = typeof data === 'string' ? JSON.parse(data) : data + processNodeTree.value = parsedData + currentValue.value = parsedData + // 确保数据加载后刷新视图 + await nextTick() + if (simpleProcessModelRef.value?.refresh) { + await simpleProcessModelRef.value.refresh() + } + } + } catch (error) { + console.error('加载流程数据失败:', error) + } +} + +// 监听属性变化 +watch( + () => props.value, + async (newValue, oldValue) => { + if (newValue && newValue !== oldValue) { + await loadProcessData(newValue) + } + }, + { immediate: true, deep: true } +) + +// 监听流程节点树变化,自动保存 +watch( + () => processNodeTree.value, + async (newValue, oldValue) => { + if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) { + await saveSimpleFlowModel(newValue) + } + }, + { deep: true } +) + +const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { + if (!simpleModelNode) { + return + } + + // 校验节点 + errorNodes = [] + validateNode(simpleModelNode, errorNodes) + if (errorNodes.length > 0) { + errorDialogVisible.value = true + return + } + + try { + if (props.modelId) { + // 编辑模式 + const data = { + id: props.modelId, + simpleModel: simpleModelNode + } + await updateBpmSimpleModel(data) + } + // 无论是编辑还是新建模式,都更新当前值并触发事件 + currentValue.value = simpleModelNode + emits('success', simpleModelNode) + } catch (error) { + console.error('保存失败:', error) + } +} + // 校验节点设置。 暂时以 showText 为空 未节点错误配置 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { if (node) { @@ -134,12 +225,14 @@ try { loading.value = true // 获取表单字段 - const bpmnModel = await getModel(props.modelId) - if (bpmnModel) { - formType.value = bpmnModel.formType - if (formType.value === 10) { - const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO - formFields.value = bpmnForm?.fields + if (props.modelId) { + const bpmnModel = await getModel(props.modelId) + if (bpmnModel) { + formType.value = bpmnModel.formType + if (formType.value === 10) { + const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO + formFields.value = bpmnForm?.fields + } } } // 获得角色列表 @@ -150,30 +243,64 @@ userOptions.value = await UserApi.getSimpleUserList() // 获得部门列表 deptOptions.value = await DeptApi.getSimpleDeptList() - deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id') // 获取用户组列表 userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() - //获取 SIMPLE 设计器模型 - const result = await getBpmSimpleModel(props.modelId) - if (result) { - processNodeTree.value = result - } else { - // 初始值 - processNodeTree.value = { - name: '发起人', - type: NodeType.START_USER_NODE, - id: NodeId.START_USER_NODE_ID, - childNode: { - id: NodeId.END_EVENT_NODE_ID, - name: '结束', - type: NodeType.END_EVENT_NODE - } + // 加载流程数据 + if (props.modelId) { + // 获取 SIMPLE 设计器模型 + const result = await getBpmSimpleModel(props.modelId) + if (result) { + await loadProcessData(result) + } else { + updateModel() } + } else if (props.value) { + await loadProcessData(props.value) + } else { + updateModel() } } finally { loading.value = false + emits('init-finished') } }) + +const simpleProcessModelRef = ref() + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + if (simpleProcessModelRef.value) { + const data = await simpleProcessModelRef.value.getCurrentFlowData() + if (data) { + currentValue.value = data + return data + } + } + return currentValue.value + } catch (error) { + console.error('获取流程数据失败:', error) + return currentValue.value + } +} + +// 刷新方法 +const refresh = async () => { + try { + if (currentValue.value) { + await loadProcessData(currentValue.value) + } + } catch (error) { + console.error('刷新失败:', error) + } +} + +defineExpose({ + getCurrentFlowData, + updateModel, + loadProcessData, + refresh +}) </script> diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue index 3a18227..ccd1f10 100644 --- a/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue +++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue @@ -8,15 +8,6 @@ <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button> <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" /> </el-button-group> - <el-button - v-if="!readonly" - size="default" - class="ml-4px" - type="primary" - :icon="Select" - @click="saveSimpleFlowModel" - >保存模型</el-button - > </el-row> </div> <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`"> @@ -42,7 +33,8 @@ import ProcessNodeTree from './ProcessNodeTree.vue' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts' import { useWatchNode } from './node' -import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' +import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' + defineOptions({ name: 'SimpleProcessModel' }) @@ -58,6 +50,7 @@ default: true } }) + const emits = defineEmits<{ 'save': [node: SimpleFlowNode | undefined] }>() @@ -68,6 +61,7 @@ let scaleValue = ref(100) const MAX_SCALE_VALUE = 200 const MIN_SCALE_VALUE = 50 + // 放大 const zoomIn = () => { if (scaleValue.value == MAX_SCALE_VALUE) { @@ -75,6 +69,7 @@ } scaleValue.value += 10 } + // 缩小 const zoomOut = () => { if (scaleValue.value == MIN_SCALE_VALUE) { @@ -82,21 +77,14 @@ } scaleValue.value -= 10 } + const processReZoom = () => { scaleValue.value = 100 } const errorDialogVisible = ref(false) let errorNodes: SimpleFlowNode[] = [] -const saveSimpleFlowModel = async () => { - errorNodes = [] - validateNode(processNodeTree.value, errorNodes) - if (errorNodes.length > 0) { - errorDialogVisible.value = true - return - } - emits('save', processNodeTree.value) -} + // 校验节点设置。 暂时以 showText 为空 未节点错误配置 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { if (node) { @@ -135,6 +123,26 @@ } } } + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + errorNodes = [] + validateNode(processNodeTree.value, errorNodes) + if (errorNodes.length > 0) { + errorDialogVisible.value = true + return undefined + } + return processNodeTree.value + } catch (error) { + console.error('获取流程数据失败:', error) + return undefined + } +} + +defineExpose({ + getCurrentFlowData +}) </script> <style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/consts.ts b/src/components/SimpleProcessDesignerV2/src/consts.ts index b81aa94..10d8a21 100644 --- a/src/components/SimpleProcessDesignerV2/src/consts.ts +++ b/src/components/SimpleProcessDesignerV2/src/consts.ts @@ -24,6 +24,11 @@ COPY_TASK_NODE = 12, /** + * 延迟器节点 + */ + DELAY_TIMER_NODE = 14, + + /** * 条件节点 */ CONDITION_NODE = 50, @@ -98,6 +103,8 @@ defaultFlow?: boolean // 活动的状态,用于前端节点状态展示 activityStatus?: TaskStatusEnum + // 延迟设置 + delaySetting?: DelaySetting } // 候选人策略枚举 ( 用于审批节点。抄送节点 ) export enum CandidateStrategy { @@ -413,12 +420,14 @@ 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, '请设置延迟器') export const NODE_DEFAULT_NAME = new Map<number, string>() 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, '延迟器') // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 export const CANDIDATE_STRATEGY: DictDataVO[] = [ @@ -568,3 +577,30 @@ */ START_USER_ID = 'PROCESS_START_USER_ID' } + +/** + * 延迟设置 + */ +export type DelaySetting = { + // 延迟类型 + delayType: number + // 延迟时间表达式 + delayTime: string +} +/** + * 延迟类型 + */ +export enum DelayTypeEnum { + /** + * 固定时长 + */ + FIXED_TIME_DURATION = 1, + /** + * 固定日期时间 + */ + FIXED_DATE_TIME = 2 +} +export const DELAY_TYPE = [ + { label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION }, + { label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME } +] diff --git a/src/components/SimpleProcessDesignerV2/src/node.ts b/src/components/SimpleProcessDesignerV2/src/node.ts index 4cbac6e..282e81b 100644 --- a/src/components/SimpleProcessDesignerV2/src/node.ts +++ b/src/components/SimpleProcessDesignerV2/src/node.ts @@ -1,4 +1,3 @@ -import { cloneDeep } from 'lodash-es' import { TaskStatusEnum } from '@/api/bpm/task' import * as RoleApi from '@/api/system/role' import * as DeptApi from '@/api/system/dept' @@ -14,7 +13,7 @@ NODE_DEFAULT_NAME, AssignStartUserHandlerType, AssignEmptyHandlerType, - FieldPermissionType, + FieldPermissionType } from './consts' import { parseFormFields } from '@/components/FormCreate/src/utils/index' export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { @@ -52,9 +51,33 @@ const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => { nodeFormFields = toRaw(nodeFormFields) - fieldsPermissionConfig.value = - cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields)) + if (!nodeFormFields || nodeFormFields.length === 0) { + fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields)) + } else { + fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields)) + } } + // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段) + const mergeFieldsPermission = ( + formFieldsPermisson: Array<Record<string, string>>, + formFields?: string[] + ) => { + let mergedFieldsPermission: Array<Record<string, any>> = [] + 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<Record<string, any>> = [] diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue index 49e5d9f..ae93172 100644 --- a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue @@ -381,7 +381,7 @@ /** 获取字段名称 */ const getFieldTitle = (field: string) => { - const item = fieldsInfo.find((item) => item.field === field) + const item = fieldOptions.value.find((item) => item.field === field) return item?.title } diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue new file mode 100644 index 0000000..27a351b --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue @@ -0,0 +1,189 @@ +<template> + <el-drawer + :append-to-body="true" + v-model="settingVisible" + :show-close="false" + :size="550" + :before-close="saveConfig" + > + <template #header> + <div class="config-header"> + <input + v-if="showInput" + type="text" + class="config-editable-input" + @blur="blurEvent()" + v-mountedFocus + v-model="nodeName" + :placeholder="nodeName" + /> + <div v-else class="node-name"> + {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" /> + </div> + <div class="divide-line"></div> + </div> + </template> + <div> + <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules"> + <el-form-item label="延迟时间" prop="delayType"> + <el-radio-group v-model="configForm.delayType"> + <el-radio-button + v-for="item in DELAY_TYPE" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-radio-group> + </el-form-item> + <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION"> + <el-form-item prop="timeDuration"> + <el-input-number + class="mr-2" + :style="{ width: '100px' }" + v-model="configForm.timeDuration" + :min="1" + controls-position="right" + /> + </el-form-item> + <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }"> + <el-option + v-for="item in TIME_UNIT_TYPES" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + <el-text>后进入下一节点</el-text> + </el-form-item> + <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime"> + <el-date-picker + class="mr-2" + v-model="configForm.dateTime" + type="datetime" + placeholder="请选择日期和时间" + value-format="YYYY-MM-DDTHH:mm:ss" + /> + <el-text>后进入下一节点</el-text> + </el-form-item> + </el-form> + </div> + <template #footer> + <el-divider /> + <div> + <el-button type="primary" @click="saveConfig">确 定</el-button> + <el-button @click="closeDrawer">取 消</el-button> + </div> + </template> + </el-drawer> +</template> +<script setup lang="ts"> +import { + SimpleFlowNode, + NodeType, + TIME_UNIT_TYPES, + TimeUnitType, + DelayTypeEnum, + DELAY_TYPE +} from '../consts' +import { useWatchNode, useDrawer, useNodeName } from '../node' +import { convertTimeUnit } from '../utils' +defineOptions({ + name: 'DelayTimerNodeConfig' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 抽屉配置 +const { settingVisible, closeDrawer, openDrawer } = useDrawer() +// 当前节点 +const currentNode = useWatchNode(props) +// 节点名称 +const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE) +// 抄送人表单配置 +const formRef = ref() // 表单 Ref +// 表单校验规则 +const formRules = reactive({ + delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], + timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }], + dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }] +}) +// 配置表单数据 +const configForm = ref({ + delayType: DelayTypeEnum.FIXED_TIME_DURATION, + timeDuration: 1, + timeUnit: TimeUnitType.HOUR, + dateTime: '' +}) +// 保存配置 +const saveConfig = async () => { + if (!formRef) return false + const valid = await formRef.value.validate() + if (!valid) return false + const showText = getShowText() + if (!showText) return false + currentNode.value.showText = showText + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + currentNode.value.delaySetting = { + delayType: configForm.value.delayType, + delayTime: getIsoTimeDuration() + } + } + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + currentNode.value.delaySetting = { + delayType: configForm.value.delayType, + delayTime: configForm.value.dateTime + } + } + settingVisible.value = false + return true +} +const getShowText = (): string => { + let showText = '' + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}` + } + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}` + } + return showText +} +const getIsoTimeDuration = () => { + let strTimeDuration = 'PT' + if (configForm.value.timeUnit === TimeUnitType.MINUTE) { + strTimeDuration += configForm.value.timeDuration + 'M' + } + if (configForm.value.timeUnit === TimeUnitType.HOUR) { + strTimeDuration += configForm.value.timeDuration + 'H' + } + if (configForm.value.timeUnit === TimeUnitType.DAY) { + strTimeDuration += configForm.value.timeDuration + 'D' + } + return strTimeDuration +} +// 显示延迟器节点配置, 由父组件传过来 +const showDelayTimerNodeConfig = (node: SimpleFlowNode) => { + nodeName.value = node.name + if (node.delaySetting) { + configForm.value.delayType = node.delaySetting.delayType + // 固定时长 + if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) { + const strTimeDuration = node.delaySetting.delayTime + let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1) + let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1) + configForm.value.timeDuration = parseInt(parseTime) + configForm.value.timeUnit = convertTimeUnit(parseTimeUnit) + } + // 固定日期时间 + if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) { + configForm.value.dateTime = node.delaySetting.delayTime + } + } +} + +defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件 +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue index e43a351..26c8e13 100644 --- a/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue +++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue @@ -25,7 +25,20 @@ </template> <el-tabs type="border-card" v-model="activeTabName"> <el-tab-pane label="权限" name="user"> - <div> 待实现 </div> + <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text> + <el-text v-else-if="startUserIds.length == 1"> + {{ getUserNicknames(startUserIds) }} 可发起流程 + </el-text> + <el-text v-else> + <el-tooltip + class="box-item" + effect="dark" + placement="top" + :content="getUserNicknames(startUserIds)" + > + {{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程 + </el-tooltip> + </el-text> </el-tab-pane> <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> <div class="field-setting-pane"> @@ -86,7 +99,7 @@ <script setup lang="ts"> import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts' import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node' - +import * as UserApi from '@/api/system/user' defineOptions({ name: 'StartUserNodeConfig' }) @@ -96,6 +109,10 @@ required: true } }) +// 可发起流程的用户编号 +const startUserIds = inject<Ref<any[]>>('startUserIds') +// 用户列表 +const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 抽屉配置 const { settingVisible, closeDrawer, openDrawer } = useDrawer() // 当前节点 @@ -108,12 +125,23 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( FieldPermissionType.WRITE ) - +const getUserNicknames = (userIds: number[]): string => { + if (!userIds || userIds.length === 0) { + return '' + } + const nicknames: string[] = [] + userIds.forEach((userId) => { + const found = userOptions?.value.find((item) => item.id === userId) + if (found && found.nickname) { + nicknames.push(found.nickname) + } + }) + return nicknames.join(',') +} // 保存配置 const saveConfig = async () => { activeTabName.value = 'user' currentNode.value.name = nodeName.value! - // TODO 暂时写死。后续可以显示谁有权限可以发起 currentNode.value.showText = '已设置' // 设置表单权限 currentNode.value.fieldsPermission = fieldsPermissionConfig.value diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue new file mode 100644 index 0000000..94f9c41 --- /dev/null +++ b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue @@ -0,0 +1,98 @@ +<template> + <div class="node-wrapper"> + <div class="node-container"> + <div + class="node-box" + :class="[ + { 'node-config-error': !currentNode.showText }, + `${useTaskStatusClass(currentNode?.activityStatus)}` + ]" + > + <div class="node-title-container"> + <!-- TODO @芋艿 需要更换图标 --> + <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div> + <input + v-if="!readonly && showInput" + type="text" + class="editable-title-input" + @blur="blurEvent()" + v-mountedFocus + v-model="currentNode.name" + :placeholder="currentNode.name" + /> + <div v-else class="node-title" @click="clickTitle"> + {{ currentNode.name }} + </div> + </div> + <div class="node-content" @click="openNodeConfig"> + <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText"> + {{ currentNode.showText }} + </div> + <div class="node-text" v-else> + {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }} + </div> + <Icon v-if="!readonly" icon="ep:arrow-right-bold" /> + </div> + <div v-if="!readonly" class="node-toolbar"> + <div class="toolbar-icon" + ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode" + /></div> + </div> + </div> + + <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> + <NodeHandler + v-if="currentNode" + v-model:child-node="currentNode.childNode" + :current-node="currentNode" + /> + </div> + <DelayTimerNodeConfig + v-if="!readonly && currentNode" + ref="nodeSetting" + :flow-node="currentNode" + /> + </div> +</template> +<script setup lang="ts"> +import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts' +import NodeHandler from '../NodeHandler.vue' +import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node' +import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue' +defineOptions({ + name: 'DelayTimerNode' +}) +const props = defineProps({ + flowNode: { + type: Object as () => SimpleFlowNode, + required: true + } +}) +// 定义事件,更新父组件。 +const emits = defineEmits<{ + 'update:flowNode': [node: SimpleFlowNode | undefined] +}>() +// 是否只读 +const readonly = inject<Boolean>('readonly') +// 监控节点的变化 +const currentNode = useWatchNode(props) +// 节点名称编辑 +const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE) + +const nodeSetting = ref() +// 打开节点配置 +const openNodeConfig = () => { + if (readonly) { + return + } + nodeSetting.value.showDelayTimerNodeConfig(currentNode.value) + nodeSetting.value.openDrawer() +} + +// 删除节点。更新当前节点为孩子节点 +const deleteNode = () => { + emits('update:flowNode', currentNode.value.childNode) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss index 516756e..8cf2681 100644 --- a/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss +++ b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss @@ -173,13 +173,16 @@ height: 100%; padding-top: 32px; background-color: #fafafa; + overflow-x: auto; + width: 100%; + .simple-process-model { display: flex; flex-direction: column; justify-content: center; align-items: center; transform-origin: 50% 0 0; - overflow: auto; + min-width: fit-content; transform: scale(1); transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat; @@ -473,6 +476,7 @@ .branch-node-container { position: relative; display: flex; + min-width: fit-content; &::before { position: absolute; @@ -548,6 +552,7 @@ background: transparent; border-top: 2px solid #dedede; border-bottom: 2px solid #dedede; + flex-shrink: 0; &::before { position: absolute; diff --git a/src/components/UserSelectForm/index.vue b/src/components/UserSelectForm/index.vue index 801489b..5ed99f8 100644 --- a/src/components/UserSelectForm/index.vue +++ b/src/components/UserSelectForm/index.vue @@ -39,7 +39,7 @@ </Dialog> </template> <script lang="ts" setup> -import { defaultProps, findTreeNode, handleTree } from '@/utils/tree' +import { defaultProps, handleTree } from '@/utils/tree' import * as DeptApi from '@/api/system/dept' import * as UserApi from '@/api/system/user' @@ -50,6 +50,7 @@ const { t } = useI18n() // 国际 const message = useMessage() // 消息弹窗 const deptTree = ref<Tree[]>([]) // 部门树形结构化 +const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据 const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表 const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表 const selectedUserIdList: any = ref([]) // 选中的用户列表 @@ -79,7 +80,9 @@ resetForm() // 加载部门、用户列表 - deptTree.value = handleTree(await DeptApi.getSimpleDeptList()) + const deptData = await DeptApi.getSimpleDeptList() + deptList.value = deptData // 保存扁平结构的部门数据 + deptTree.value = handleTree(deptData) // 转换成树形结构 userList.value = await UserApi.getSimpleUserList() // 初始状态下,过滤列表等于所有用户列表 @@ -88,16 +91,31 @@ dialogVisible.value = true } +/** 获取指定部门及其所有子部门的ID列表 */ +const getChildDeptIds = (deptId: number, deptList: any[]): number[] => { + const ids = [deptId] + const children = deptList.filter((dept) => dept.parentId === deptId) + children.forEach((child) => { + ids.push(...getChildDeptIds(child.id, deptList)) + }) + return ids +} + /** 获取部门过滤后的用户列表 */ -const getUserList = async (deptId?: number) => { +const filterUserList = async (deptId?: number) => { formLoading.value = true try { - // @ts-ignore - // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤 - // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList - const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId }) - // 更新过滤后的用户列表 - filteredUserList.value = data.list + if (!deptId) { + // 如果没有选择部门,显示所有用户 + filteredUserList.value = [...userList.value] + return + } + + // 直接使用已保存的部门列表数据进行过滤 + const deptIds = getChildDeptIds(deptId, deptList.value) + + // 过滤出这些部门下的用户 + filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId)) } finally { formLoading.value = false } @@ -121,6 +139,7 @@ /** 重置表单 */ const resetForm = () => { deptTree.value = [] + deptList.value = [] userList.value = [] filteredUserList.value = [] selectedUserIdList.value = [] @@ -128,7 +147,7 @@ /** 处理部门被点击 */ const handleNodeClick = (row: { [key: string]: any }) => { - getUserList(row.id) + filterUserList(row.id) } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue index 6cbe11f..9d2fa5b 100644 --- a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue +++ b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue @@ -160,13 +160,6 @@ <XButton preIcon="ep:refresh" @click="processRestart()" /> </el-tooltip> </ElButtonGroup> - <XButton - preIcon="ep:plus" - title="保存模型" - @click="processSave" - :type="props.headerButtonType" - :disabled="simulationStatus" - /> </template> <!-- 用于打开本地文件--> <input @@ -314,6 +307,28 @@ ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1 } }) + +// 监听value变化,重新加载流程图 +watch( + () => props.value, + (newValue) => { + if (newValue && bpmnModeler) { + createNewDiagram(newValue) + } + }, + { immediate: true } +) + +// 监听processId和processName变化 +watch( + [() => props.processId, () => props.processName], + ([newId, newName]) => { + if (newId && newName && !props.value) { + createNewDiagram(null) + } + }, + { immediate: true } +) provide('configGlobal', props) let bpmnModeler: any = null @@ -592,16 +607,6 @@ defaultZoom.value = newZoom bpmnModeler.get('canvas').zoom(defaultZoom.value) } -// const processZoomTo = (newZoom = 1) => { -// if (newZoom < 0.2) { -// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') -// } -// if (newZoom > 4) { -// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') -// } -// defaultZoom = newZoom -// bpmnModeler.get('canvas').zoom(newZoom) -// } const processReZoom = () => { defaultZoom.value = 1 bpmnModeler.get('canvas').zoom('fit-viewport', 'auto') @@ -640,63 +645,19 @@ } const previewProcessJson = () => { bpmnModeler.saveXML({ format: true }).then(({ xml }) => { - // console.log(xml, 'xml') - - // const rootNode = parseXmlString(xml) - // console.log(rootNode, 'rootNoderootNode') const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml)) - // console.log(rootNodes, 'rootNodesrootNodesrootNodes') - // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()') - // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()') - // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()') - - // const parser = new xml2js.XMLParser() - // let jObj = parser.parse(xml) - // console.log(jObj, 'jObjjObjjObjjObjjObj') - // const builder = new xml2js.XMLBuilder(xml) - // const xmlContent = builder - // console.log(xmlContent, 'xmlContent') - // console.log(xml2js, 'convertconvertconvert') previewResult.value = rootNodes.parent?.toJSON() as unknown as string - // previewResult.value = jObj - // previewResult.value = convert.xml2json(xml, {explicitArray : false},{ spaces: 2 }) previewType.value = 'json' previewModelVisible.value = true }) } + /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ -const processSave = async () => { - // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler') - const { err, xml } = await bpmnModeler.saveXML() - // console.log(err, 'errerrerrerrerr') - // console.log(xml, 'xmlxmlxmlxmlxml') - // 读取异常时抛出异常 - if (err) { - // this.$modal.msgError('保存模型失败,请重试!') - alert('保存模型失败,请重试!') - return - } - // 触发 save 事件 - emit('save', xml) -} -/** 高亮显示 */ -// const highlightedCode = (previewType, previewResult) => { -// console.log(previewType, 'previewType, previewResult') -// console.log(previewResult, 'previewType, previewResult') -// console.log(hljs.highlight, 'hljs.highlight') -// const result = hljs.highlight(previewType, previewResult.value || '', true) -// return result.value || ' ' -// } -onBeforeMount(() => { - console.log(props, 'propspropspropsprops') -}) onMounted(() => { initBpmnModeler() createNewDiagram(props.value) }) onBeforeUnmount(() => { - // this.$once('hook:beforeDestroy', () => { - // }) if (bpmnModeler) bpmnModeler.destroy() emit('destroy', bpmnModeler) bpmnModeler = null diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue index d2409ee..e426eb6 100644 --- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue +++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue @@ -1,6 +1,6 @@ <template> - <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '700px' }"> - <el-collapse v-model="activeTab"> + <div class="process-panel__container" :style="{ width: `${width}px` }"> + <el-collapse v-model="activeTab" v-if="isReady"> <el-collapse-item name="base"> <!-- class="panel-tab__title" --> <template #title> @@ -28,7 +28,7 @@ </el-collapse-item> <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task"> <template #title - ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template + ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template > <element-task :id="elementId" :type="elementType" /> </el-collapse-item> @@ -119,24 +119,16 @@ const conditionFormVisible = ref(false) // 流转条件设置 const formVisible = ref(false) // 表单配置 const bpmnElement = ref() +const isReady = ref(false) provide('prefix', props.prefix) provide('width', props.width) -const bpmnInstances = () => (window as any)?.bpmnInstances -// 监听 props.bpmnModeler 然后 initModels -const unwatchBpmn = watch( - () => props.bpmnModeler, - () => { - // 避免加载时 流程图 并未加载完成 - if (!props.bpmnModeler) { - console.log('缺少props.bpmnModeler') - return - } - - console.log('props.bpmnModeler 有值了!!!') - const w = window as any - w.bpmnInstances = { +// 初始化 bpmnInstances +const initBpmnInstances = () => { + if (!props.bpmnModeler) return false + try { + const instances = { modeler: props.bpmnModeler, modeling: props.bpmnModeler.get('modeling'), moddle: props.bpmnModeler.get('moddle'), @@ -148,9 +140,45 @@ selection: props.bpmnModeler.get('selection') } - console.log(bpmnInstances(), 'window.bpmnInstances') - getActiveElement() - unwatchBpmn() + // 检查所有实例是否都存在 + const allInstancesExist = Object.values(instances).every(instance => instance) + if (allInstancesExist) { + const w = window as any + w.bpmnInstances = instances + return true + } + return false + } catch (error) { + console.error('初始化 bpmnInstances 失败:', error) + return false + } +} + +const bpmnInstances = () => (window as any)?.bpmnInstances + +// 监听 props.bpmnModeler 然后 initModels +const unwatchBpmn = watch( + () => props.bpmnModeler, + async () => { + // 避免加载时 流程图 并未加载完成 + if (!props.bpmnModeler) { + console.log('缺少props.bpmnModeler') + return + } + + try { + // 等待 modeler 初始化完成 + await nextTick() + if (initBpmnInstances()) { + isReady.value = true + await nextTick() + getActiveElement() + } else { + console.error('modeler 实例未完全初始化') + } + } catch (error) { + console.error('初始化失败:', error) + } }, { immediate: true @@ -158,6 +186,8 @@ ) const getActiveElement = () => { + if (!isReady.value || !props.bpmnModeler) return + // 初始第一个选中元素 bpmn:Process initFormOnChanged(null) props.bpmnModeler.on('import.done', (e) => { @@ -175,8 +205,11 @@ } }) } + // 初始化数据 const initFormOnChanged = (element) => { + if (!isReady.value || !bpmnInstances()) return + let activatedElement = element if (!activatedElement) { activatedElement = @@ -184,32 +217,36 @@ bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration') } if (!activatedElement) return - console.log(` - ---------- - select element changed: - id: ${activatedElement.id} - type: ${activatedElement.businessObject.$type} - ---------- - `) - console.log('businessObject: ', activatedElement.businessObject) - bpmnInstances().bpmnElement = activatedElement - bpmnElement.value = activatedElement - elementId.value = activatedElement.id - elementType.value = activatedElement.type.split(':')[1] || '' - elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) - conditionFormVisible.value = !!( - elementType.value === 'SequenceFlow' && - activatedElement.source && - activatedElement.source.type.indexOf('StartEvent') === -1 - ) - formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' + + try { + console.log(` + ---------- + select element changed: + id: ${activatedElement.id} + type: ${activatedElement.businessObject.$type} + ---------- + `) + console.log('businessObject: ', activatedElement.businessObject) + bpmnInstances().bpmnElement = activatedElement + bpmnElement.value = activatedElement + elementId.value = activatedElement.id + elementType.value = activatedElement.type.split(':')[1] || '' + elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) + conditionFormVisible.value = !!( + elementType.value === 'SequenceFlow' && + activatedElement.source && + activatedElement.source.type.indexOf('StartEvent') === -1 + ) + formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' + } catch (error) { + console.error('初始化表单数据失败:', error) + } } onBeforeUnmount(() => { const w = window as any w.bpmnInstances = null - console.log(props, 'props1') - console.log(props.bpmnModeler, 'props.bpmnModeler1') + isReady.value = false }) watch( diff --git a/src/components/bpmnProcessDesigner/package/theme/process-designer.scss b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss index 6af945d..ac2976b 100644 --- a/src/components/bpmnProcessDesigner/package/theme/process-designer.scss +++ b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss @@ -1,6 +1,4 @@ -@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; -@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css'; -@import 'bpmn-js-token-simulation/assets/css/normalize.css'; +@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css'; // 边框被 token-simulation 样式覆盖了 .djs-palette { @@ -83,7 +81,7 @@ height: 100%; position: relative; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') - repeat !important; + repeat !important; div.toggle-mode { display: none; } @@ -97,12 +95,12 @@ box-sizing: border-box; } } - svg { - width: 100%; - height: 100%; - min-height: 100%; - overflow: hidden; - } + // svg { + // width: 100%; + // height: 100%; + // min-height: 100%; + // overflow: hidden; + // } } } diff --git a/src/directives/permission/hasPermi.ts b/src/directives/permission/hasPermi.ts index 931f44b..0ef3c50 100644 --- a/src/directives/permission/hasPermi.ts +++ b/src/directives/permission/hasPermi.ts @@ -5,18 +5,10 @@ export function hasPermi(app: App<Element>) { app.directive('hasPermi', (el, binding) => { - const { wsCache } = useCache() const { value } = binding - const all_permission = '*:*:*' - const userInfo = wsCache.get(CACHE_KEY.USER) - const permissions = userInfo?.permissions || [] if (value && value instanceof Array && value.length > 0) { - const permissionFlag = value - - const hasPermissions = permissions.some((permission: string) => { - return all_permission === permission || permissionFlag.includes(permission) - }) + const hasPermissions = hasPermission(value) if (!hasPermissions) { el.parentNode && el.parentNode.removeChild(el) @@ -26,3 +18,14 @@ } }) } + +export const hasPermission = (permission: string[]) => { + const { wsCache } = useCache() + const all_permission = '*:*:*' + const userInfo = wsCache.get(CACHE_KEY.USER) + const permissions = userInfo?.permissions || [] + + return permissions.some((p: string) => { + return all_permission === p || permission.includes(p) + }) +} diff --git a/src/layout/components/Footer/src/Footer.vue b/src/layout/components/Footer/src/Footer.vue index 62302fc..c5a1d1a 100644 --- a/src/layout/components/Footer/src/Footer.vue +++ b/src/layout/components/Footer/src/Footer.vue @@ -12,6 +12,10 @@ const appStore = useAppStore() const title = computed(() => appStore.getTitle) + + +// 添加当前年份计算属性 +const currentYear = computed(() => new Date().getFullYear()) </script> <template> @@ -19,6 +23,6 @@ :class="prefixCls" class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden" > - <span class="text-14px">Copyright ©2022-{{ title }}</span> + <span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span> </div> </template> diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 6b64a95..f603ca5 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -351,6 +351,30 @@ title: '查看 OA 请假', activeMenu: '/bpm/oa/leave' } + }, + { + path: 'manager/model/create', + component: () => import('@/views/bpm/model/form/index.vue'), + name: 'BpmModelCreate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '创建流程', + activeMenu: '/bpm/manager/model' + } + }, + { + path: 'manager/model/update/:id', + component: () => import('@/views/bpm/model/form/index.vue'), + name: 'BpmModelUpdate', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: '修改流程', + activeMenu: '/bpm/manager/model' + } } ] }, diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts index b65f93a..a4b2295 100644 --- a/src/utils/routerHelper.ts +++ b/src/utils/routerHelper.ts @@ -73,7 +73,7 @@ noCache: !route.keepAlive, alwaysShow: route.children && - route.children.length === 1 && + route.children.length > 0 && (route.alwaysShow !== undefined ? route.alwaysShow : true) } as any // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 diff --git a/src/views/bpm/model/CategoryDraggableModel.vue b/src/views/bpm/model/CategoryDraggableModel.vue index 7bd58d7..f3b5a42 100644 --- a/src/views/bpm/model/CategoryDraggableModel.vue +++ b/src/views/bpm/model/CategoryDraggableModel.vue @@ -64,6 +64,7 @@ </div> </div> </div> + <!-- 模型列表 --> <el-collapse-transition> <div v-show="isExpand"> @@ -90,7 +91,7 @@ </div> </template> </el-table-column> - <el-table-column label="可见范围" prop="startUserIds" min-width="100"> + <el-table-column label="可见范围" prop="startUserIds" min-width="150"> <template #default="scope"> <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> 全部可见 @@ -110,7 +111,7 @@ </el-text> </template> </el-table-column> - <el-table-column label="表单信息" prop="formType" min-width="200"> + <el-table-column label="表单信息" prop="formType" min-width="150"> <template #default="scope"> <el-button v-if="scope.row.formType === BpmModelFormType.NORMAL" @@ -161,16 +162,6 @@ :disabled="!isManagerUser(scope.row)" > 修改 - </el-button> - <el-button - link - class="!ml-5px" - type="primary" - @click="handleDesign(scope.row)" - v-hasPermi="['bpm:model:update']" - :disabled="!isManagerUser(scope.row)" - > - 设计 </el-button> <el-button link @@ -236,11 +227,6 @@ </template> </Dialog> - <!-- 弹窗:表单详情 --> - <Dialog title="表单详情" v-model="formDetailVisible" width="800"> - <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" /> - </Dialog> - <!-- 表单弹窗:添加流程模型 --> <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" /> </template> @@ -254,7 +240,7 @@ import * as ModelApi from '@/api/bpm/model' import * as FormApi from '@/api/bpm/form' import { setConfAndFields2 } from '@/utils/formCreate' -import { BpmModelFormType, BpmModelType } from '@/utils/constants' +import { BpmModelFormType } from '@/utils/constants' import { checkPermi } from '@/utils/permission' import { useUserStoreWithOut } from '@/store/modules/user' import { useAppStore } from '@/store/modules/app' @@ -340,25 +326,6 @@ // 刷新列表 emit('success') } catch {} -} - -/** 设计流程 */ -const handleDesign = (row: any) => { - if (row.type == BpmModelType.BPMN) { - push({ - name: 'BpmModelEditor', - query: { - modelId: row.id - } - }) - } else { - push({ - name: 'SimpleModelDesign', - query: { - modelId: row.id - } - }) - } } /** 发布流程 */ @@ -501,7 +468,14 @@ /** 添加流程模型弹窗 */ const modelFormRef = ref() const openModelForm = (type: string, id?: number) => { - modelFormRef.value.open(type, id) + if (type === 'create') { + push({ name: 'BpmModelCreate' }) + } else { + push({ + name: 'BpmModelUpdate', + params: { id } + }) + } } watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true }) diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue index 16d3c1d..095b0ac 100644 --- a/src/views/bpm/model/ModelForm.vue +++ b/src/views/bpm/model/ModelForm.vue @@ -123,29 +123,69 @@ </el-radio> </el-radio-group> </el-form-item> - <el-form-item label="谁可以发起" prop="startUserIds"> + <el-form-item label="谁可以发起" prop="startUserType"> <el-select - v-model="formData.startUserIds" - multiple - placeholder="请选择可发起人,默认(不选择)则所有人都可以发起" + v-model="formData.startUserType" + placeholder="请选择谁可以发起" + @change="handleStartUserTypeChange" > - <el-option - v-for="user in userList" - :key="user.id" - :label="user.nickname" - :value="user.id" - /> + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> </el-select> + <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedStartUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveStartUser(user)" + /> + </div> + <el-button type="primary" link @click="openStartUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> </el-form-item> - <el-form-item label="流程管理员" prop="managerUserIds"> - <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员"> - <el-option - v-for="user in userList" - :key="user.id" - :label="user.nickname" - :value="user.id" - /> + <el-form-item label="流程管理员" prop="managerUserType"> + <el-select + v-model="formData.managerUserType" + placeholder="请选择流程管理员" + @change="handleManagerUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> </el-select> + <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedManagerUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveManagerUser(user)" + /> + </div> + <el-button type="primary" link @click="openManagerUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> </el-form-item> </el-form> <template #footer> @@ -153,6 +193,7 @@ <el-button @click="dialogVisible = false">取 消</el-button> </template> </Dialog> + <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> </template> <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' @@ -160,11 +201,12 @@ import { ElMessageBox } from 'element-plus' import * as ModelApi from '@/api/bpm/model' import * as FormApi from '@/api/bpm/form' -import { CategoryApi } from '@/api/bpm/category' +import { CategoryApi, CategoryVO } from '@/api/bpm/category' import { BpmModelFormType, BpmModelType } from '@/utils/constants' import { UserVO } from '@/api/system/user' import * as UserApi from '@/api/system/user' import { useUserStoreWithOut } from '@/store/modules/user' +import { FormVO } from '@/api/bpm/form' defineOptions({ name: 'ModelForm' }) @@ -178,7 +220,7 @@ const dialogTitle = ref('') // 弹窗的标题 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ +const formData: any = ref({ id: undefined, name: '', key: '', @@ -191,6 +233,8 @@ formCustomCreatePath: '', formCustomViewPath: '', visible: true, + startUserType: undefined, + managerUserType: undefined, startUserIds: [], managerUserIds: [] }) @@ -208,9 +252,13 @@ managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const formList = ref([]) // 流程表单的下拉框的数据 -const categoryList = ref([]) // 流程分类列表 +const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据 +const categoryList = ref<CategoryVO[]>([]) // 流程分类列表 const userList = ref<UserVO[]>([]) // 用户列表 +const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表 +const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表 +const userSelectFormRef = ref() // 用户选择弹窗 ref +const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员 /** 打开弹窗 */ const open = async (type: string, id?: string) => { @@ -225,6 +273,19 @@ formData.value = await ModelApi.getModel(id) } finally { formLoading.value = false + } + // 加载数据时,根据已有的用户ID列表初始化已选用户 + if (formData.value.startUserIds?.length) { + formData.value.startUserType = 1 + selectedStartUsers.value = userList.value.filter((user) => + formData.value.startUserIds.includes(user.id) + ) + } + if (formData.value.managerUserIds?.length) { + formData.value.managerUserType = 1 + selectedManagerUsers.value = userList.value.filter((user) => + formData.value.managerUserIds.includes(user.id) + ) } } else { formData.value.managerUserIds.push(userStore.getUser.id) @@ -257,9 +318,9 @@ // 提示,引导用户做后续的操作 await ElMessageBox.alert( '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' + - '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + - '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + - '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', + '<div>1. 点击【设计流程】按钮,绘制流程图</div>' + + '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' + + '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!', '重要提示', { dangerouslyUseHTMLString: true, @@ -293,9 +354,87 @@ formCustomCreatePath: '', formCustomViewPath: '', visible: true, + startUserType: undefined, + managerUserType: undefined, startUserIds: [], managerUserIds: [] } formRef.value?.resetFields() + selectedStartUsers.value = [] + selectedManagerUsers.value = [] +} + +/** 处理发起人类型变化 */ +const handleStartUserTypeChange = (value: number) => { + if (value !== 1) { + selectedStartUsers.value = [] + formData.value.startUserIds = [] + } +} + +/** 处理管理员类型变化 */ +const handleManagerUserTypeChange = (value: number) => { + if (value !== 1) { + selectedManagerUsers.value = [] + formData.value.managerUserIds = [] + } +} + +/** 打开发起人选择 */ +const openStartUserSelect = () => { + currentSelectType.value = 'start' + userSelectFormRef.value.open(0, selectedStartUsers.value) +} + +/** 打开管理员选择 */ +const openManagerUserSelect = () => { + currentSelectType.value = 'manager' + userSelectFormRef.value.open(0, selectedManagerUsers.value) +} + +/** 处理用户选择确认 */ +const handleUserSelectConfirm = (_, users: UserVO[]) => { + if (currentSelectType.value === 'start') { + selectedStartUsers.value = users + formData.value.startUserIds = users.map((u) => u.id) + } else { + selectedManagerUsers.value = users + formData.value.managerUserIds = users.map((u) => u.id) + } +} + +/** 移除发起人 */ +const handleRemoveStartUser = (user: UserVO) => { + selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) + formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id) +} + +/** 移除管理员 */ +const handleRemoveManagerUser = (user: UserVO) => { + selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) + formData.value.managerUserIds = formData.value.managerUserIds.filter( + (id: number) => id !== user.id + ) } </script> + +<style lang="scss" scoped> +.bg-gray-100 { + background-color: #f5f7fa; + transition: all 0.3s; + + &:hover { + background-color: #e6e8eb; + } + + .ep-close { + font-size: 14px; + color: #909399; + transition: color 0.3s; + + &:hover { + color: #f56c6c; + } + } +} +</style> diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue index 1a41a50..37eff73 100644 --- a/src/views/bpm/model/editor/index.vue +++ b/src/views/bpm/model/editor/index.vue @@ -3,7 +3,6 @@ <!-- 流程设计器,负责绘制流程等 --> <MyProcessDesigner key="designer" - v-if="xmlString !== undefined" v-model="xmlString" :value="xmlString" v-bind="controlForm" @@ -11,12 +10,14 @@ ref="processDesigner" @init-finished="initModeler" :additionalModel="controlForm.additionalModel" + :model="model" @save="save" /> <!-- 流程属性器,负责编辑每个流程节点的属性 --> <MyProcessPenal + v-if="isModelerReady && modeler" key="penal" - :bpmnModeler="modeler as any" + :bpmnModeler="modeler" :prefix="controlForm.prefix" class="process-panel" :model="model" @@ -31,12 +32,17 @@ // 自定义左侧菜单(修改 默认任务 为 用户任务) import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette' import * as ModelApi from '@/api/bpm/model' -import { getForm, FormVO } from '@/api/bpm/form' defineOptions({ name: 'BpmModelEditor' }) -const router = useRouter() // 路由 -const { query } = useRoute() // 路由的查询 +const props = defineProps<{ + modelId?: string + modelKey?: string + modelName?: string + value?: string +}>() + +const emit = defineEmits(['success', 'init-finished']) const message = useMessage() // 国际化 // 表单信息 @@ -45,8 +51,10 @@ provide('formFields', formFields) provide('formType', formType) -const xmlString = ref(undefined) // BPMN XML -const modeler = ref(null) // BPMN Modeler +const xmlString = ref<string>('') // BPMN XML +const modeler = shallowRef() // BPMN Modeler +const processDesigner = ref() +const isModelerReady = ref(false) const controlForm = ref({ simulation: true, labelEditing: false, @@ -57,73 +65,215 @@ }) const model = ref<ModelApi.ModelVO>() // 流程模型的信息 +// 初始化 bpmnInstances +const initBpmnInstances = () => { + if (!modeler.value) return false + try { + const instances = { + modeler: modeler.value, + modeling: modeler.value.get('modeling'), + moddle: modeler.value.get('moddle'), + eventBus: modeler.value.get('eventBus'), + bpmnFactory: modeler.value.get('bpmnFactory'), + elementFactory: modeler.value.get('elementFactory'), + elementRegistry: modeler.value.get('elementRegistry'), + replace: modeler.value.get('replace'), + selection: modeler.value.get('selection') + } + + // 检查所有实例是否都存在 + return Object.values(instances).every((instance) => instance) + } catch (error) { + console.error('初始化 bpmnInstances 失败:', error) + return false + } +} + /** 初始化 modeler */ -const initModeler = (item) => { - setTimeout(() => { +const initModeler = async (item) => { + try { modeler.value = item - }, 10) + // 等待 modeler 初始化完成 + await nextTick() + + // 确保 modeler 的所有实例都已经准备好 + if (initBpmnInstances()) { + isModelerReady.value = true + emit('init-finished') + + // 初始化完成后,设置初始值 + if (props.modelId) { + // 编辑模式 + const data = await ModelApi.getModel(props.modelId) + model.value = { + ...data, + bpmnXml: undefined // 清空 bpmnXml 属性 + } + xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name) + } else if (props.modelKey && props.modelName) { + // 新建模式 + xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName) + model.value = { + key: props.modelKey, + name: props.modelName + } as ModelApi.ModelVO + } + + // 导入XML并刷新视图 + await nextTick() + try { + await modeler.value.importXML(xmlString.value) + if (processDesigner.value?.refresh) { + processDesigner.value.refresh() + } + } catch (error) { + console.error('导入XML失败:', error) + } + } else { + console.error('modeler 实例未完全初始化') + } + } catch (error) { + console.error('初始化 modeler 失败:', error) + } +} + +/** 获取默认的BPMN XML */ +const getDefaultBpmnXml = (key: string, name: string) => { + return `<?xml version="1.0" encoding="UTF-8"?> +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> + <process id="${key}" name="${name}" isExecutable="true" /> + <bpmndi:BPMNDiagram id="BPMNDiagram"> + <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" /> + </bpmndi:BPMNDiagram> +</definitions>` } /** 添加/修改模型 */ const save = async (bpmnXml: string) => { - const data = { - ...model.value, - bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 - } as unknown as ModelApi.ModelVO - // 提交 - if (data.id) { - await ModelApi.updateModelBpmn(data) - message.success('修改成功') - } else { - await ModelApi.updateModelBpmn(data) - message.success('新增成功') + try { + xmlString.value = bpmnXml + if (props.modelId) { + // 编辑模式 + const data = { + ...model.value, + bpmnXml: bpmnXml + } as unknown as ModelApi.ModelVO + await ModelApi.updateModelBpmn(data) + emit('success') + } else { + // 新建模式,直接返回XML + emit('success', bpmnXml) + } + } catch (error) { + console.error('保存失败:', error) + message.error('保存失败') } - // 跳转回去 - close() } -/** 关闭按钮 */ -const close = () => { - router.push({ path: '/bpm/manager/model' }) +// 监听 key、name 和 value 的变化 +watch( + [() => props.modelKey, () => props.modelName, () => props.value], + async ([newKey, newName, newValue]) => { + if (!props.modelId && isModelerReady.value) { + let shouldRefresh = false + + if (newKey && newName) { + const newXml = newValue || getDefaultBpmnXml(newKey, newName) + if (newXml !== xmlString.value) { + xmlString.value = newXml + shouldRefresh = true + } + model.value = { + ...model.value, + key: newKey, + name: newName + } as ModelApi.ModelVO + } else if (newValue && newValue !== xmlString.value) { + xmlString.value = newValue + shouldRefresh = true + } + + if (shouldRefresh) { + // 确保更新后重新渲染 + await nextTick() + if (processDesigner.value?.refresh) { + try { + await modeler.value?.importXML(xmlString.value) + processDesigner.value.refresh() + } catch (error) { + console.error('导入XML失败:', error) + } + } + } + } + }, + { deep: true } +) + +// 在组件卸载时清理 +onBeforeUnmount(() => { + isModelerReady.value = false + modeler.value = null + // 清理全局实例 + const w = window as any + if (w.bpmnInstances) { + w.bpmnInstances = null + } +}) + +/** 获取 XML 字符串 */ +const saveXML = async () => { + if (!modeler.value) { + return { xml: xmlString.value } + } + try { + const result = await modeler.value.saveXML({ format: true }) + xmlString.value = result.xml + return result + } catch (error) { + console.error('获取XML失败:', error) + return { xml: xmlString.value } + } } -/** 初始化 */ -onMounted(async () => { - const modelId = query.modelId as unknown as number - if (!modelId) { - message.error('缺少模型 modelId 编号') - return +/** 获取SVG字符串 */ +const saveSVG = async () => { + if (!modeler.value) { + return { svg: undefined } } - // 查询模型 - const data = await ModelApi.getModel(modelId) - if (!data.bpmnXml) { - // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的 - data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?> -<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef"> - <process id="${data.key}" name="${data.name}" isExecutable="true" /> - <bpmndi:BPMNDiagram id="BPMNDiagram"> - <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" /> - </bpmndi:BPMNDiagram> -</definitions>` + try { + return await modeler.value.saveSVG() + } catch (error) { + console.error('获取SVG失败:', error) + return { svg: undefined } } +} - formType.value = data.formType - if (data.formType === 10) { - const bpmnForm = (await getForm(data.formId)) as unknown as FormVO - formFields.value = bpmnForm?.fields +/** 刷新视图 */ +const refresh = async () => { + if (processDesigner.value?.refresh && modeler.value) { + try { + await modeler.value.importXML(xmlString.value) + processDesigner.value.refresh() + } catch (error) { + console.error('刷新视图失败:', error) + } } +} - model.value = { - ...data, - bpmnXml: undefined // 清空 bpmnXml 属性 - } - xmlString.value = data.bpmnXml +// 暴露必要的属性和方法给父组件 +defineExpose({ + modeler, + isModelerReady, + saveXML, + saveSVG, + refresh }) </script> <style lang="scss"> .process-panel__container { position: absolute; - top: 90px; - right: 60px; + top: 172px; + right: 70px; } </style> diff --git a/src/views/bpm/model/form/BasicInfo.vue b/src/views/bpm/model/form/BasicInfo.vue new file mode 100644 index 0000000..0359ea8 --- /dev/null +++ b/src/views/bpm/model/form/BasicInfo.vue @@ -0,0 +1,301 @@ +<template> + <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> + <el-form-item label="流程标识" prop="key" class="mb-20px"> + <div class="flex items-center"> + <el-input + class="!w-440px" + v-model="modelData.key" + :disabled="!!modelData.id" + placeholder="请输入流标标识" + /> + <el-tooltip + class="item" + :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'" + effect="light" + placement="top" + > + <Icon icon="ep:question-filled" class="ml-5px" /> + </el-tooltip> + </div> + </el-form-item> + <el-form-item label="流程名称" prop="name" class="mb-20px"> + <el-input + v-model="modelData.name" + :disabled="!!modelData.id" + clearable + placeholder="请输入流程名称" + /> + </el-form-item> + <el-form-item label="流程分类" prop="category" class="mb-20px"> + <el-select + class="!w-full" + v-model="modelData.category" + clearable + placeholder="请选择流程分类" + > + <el-option + v-for="category in categoryList" + :key="category.code" + :label="category.name" + :value="category.code" + /> + </el-select> + </el-form-item> + <el-form-item label="流程图标" prop="icon" class="mb-20px"> + <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" /> + </el-form-item> + <el-form-item label="流程描述" prop="description" class="mb-20px"> + <el-input v-model="modelData.description" clearable type="textarea" /> + </el-form-item> + <el-form-item label="流程类型" prop="type" class="mb-20px"> + <el-radio-group v-model="modelData.type"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="是否可见" prop="visible" class="mb-20px"> + <el-radio-group v-model="modelData.visible"> + <el-radio + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px"> + <el-select + v-model="modelData.startUserType" + placeholder="请选择谁可以发起" + @change="handleStartUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedStartUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveStartUser(user)" + /> + </div> + <el-button type="primary" link @click="openStartUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </el-form-item> + <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px"> + <el-select + v-model="modelData.managerUserType" + placeholder="请选择流程管理员" + @change="handleManagerUserTypeChange" + > + <el-option label="全员" :value="0" /> + <el-option label="指定人员" :value="1" /> + <el-option label="均不可提交" :value="2" /> + </el-select> + <div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2"> + <div + v-for="user in selectedManagerUsers" + :key="user.id" + class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative" + > + <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" /> + <el-avatar class="!m-5px" :size="28" v-else> + {{ user.nickname.substring(0, 1) }} + </el-avatar> + {{ user.nickname }} + <Icon + icon="ep:close" + class="ml-2 cursor-pointer hover:text-red-500" + @click="handleRemoveManagerUser(user)" + /> + </div> + <el-button type="primary" link @click="openManagerUserSelect"> + <Icon icon="ep:plus" />选择人员 + </el-button> + </div> + </el-form-item> + </el-form> + + <!-- 用户选择弹窗 --> + <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" /> +</template> + +<script lang="ts" setup> +import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' +import { UserVO } from '@/api/system/user' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + }, + categoryList: { + type: Array, + required: true + }, + userList: { + type: Array, + required: true + } +}) + +const emit = defineEmits(['update:modelValue']) + +const formRef = ref() +const selectedStartUsers = ref<UserVO[]>([]) +const selectedManagerUsers = ref<UserVO[]>([]) +const userSelectFormRef = ref() +const currentSelectType = ref<'start' | 'manager'>('start') + +const rules = { + name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }], + key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }], + category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], + icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }], + type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], + managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] +} + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 初始化选中的用户 +watch( + () => props.modelValue, + (newVal) => { + if (newVal.startUserIds?.length) { + selectedStartUsers.value = props.userList.filter((user: UserVO) => + newVal.startUserIds.includes(user.id) + ) as UserVO[] + } + if (newVal.managerUserIds?.length) { + selectedManagerUsers.value = props.userList.filter((user: UserVO) => + newVal.managerUserIds.includes(user.id) + ) as UserVO[] + } + }, + { immediate: true } +) + +/** 打开发起人选择 */ +const openStartUserSelect = () => { + currentSelectType.value = 'start' + userSelectFormRef.value.open(0, selectedStartUsers.value) +} + +/** 打开管理员选择 */ +const openManagerUserSelect = () => { + currentSelectType.value = 'manager' + userSelectFormRef.value.open(0, selectedManagerUsers.value) +} + +/** 处理用户选择确认 */ +const handleUserSelectConfirm = (_, users: UserVO[]) => { + if (currentSelectType.value === 'start') { + selectedStartUsers.value = users + emit('update:modelValue', { + ...modelData.value, + startUserIds: users.map((u) => u.id) + }) + } else { + selectedManagerUsers.value = users + emit('update:modelValue', { + ...modelData.value, + managerUserIds: users.map((u) => u.id) + }) + } +} + +/** 处理发起人类型变化 */ +const handleStartUserTypeChange = (value: number) => { + if (value !== 1) { + selectedStartUsers.value = [] + emit('update:modelValue', { + ...modelData.value, + startUserIds: [] + }) + } +} + +/** 处理管理员类型变化 */ +const handleManagerUserTypeChange = (value: number) => { + if (value !== 1) { + selectedManagerUsers.value = [] + emit('update:modelValue', { + ...modelData.value, + managerUserIds: [] + }) + } +} + +/** 移除发起人 */ +const handleRemoveStartUser = (user: UserVO) => { + selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id) + emit('update:modelValue', { + ...modelData.value, + startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id) + }) +} + +/** 移除管理员 */ +const handleRemoveManagerUser = (user: UserVO) => { + selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id) + emit('update:modelValue', { + ...modelData.value, + managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id) + }) +} + +/** 表单校验 */ +const validate = async () => { + await formRef.value?.validate() +} + +defineExpose({ + validate +}) +</script> + +<style lang="scss" scoped> +.bg-gray-100 { + background-color: #f5f7fa; + transition: all 0.3s; + + &:hover { + background-color: #e6e8eb; + } + + .ep-close { + font-size: 14px; + color: #909399; + transition: color 0.3s; + + &:hover { + color: #f56c6c; + } + } +} +</style> diff --git a/src/views/bpm/model/form/FormDesign.vue b/src/views/bpm/model/form/FormDesign.vue new file mode 100644 index 0000000..98aee6d --- /dev/null +++ b/src/views/bpm/model/form/FormDesign.vue @@ -0,0 +1,137 @@ +<template> + <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px"> + <el-form-item label="表单类型" prop="formType" class="mb-20px"> + <el-radio-group v-model="modelData.formType"> + <el-radio + v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)" + :key="dict.value" + :value="dict.value" + > + {{ dict.label }} + </el-radio> + </el-radio-group> + </el-form-item> + <el-form-item v-if="modelData.formType === 10" label="流程表单" prop="formId"> + <el-select v-model="modelData.formId" clearable style="width: 100%"> + <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> + </el-select> + </el-form-item> + <el-form-item v-if="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath"> + <el-input + v-model="modelData.formCustomCreatePath" + placeholder="请输入表单提交路由" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue" + effect="light" + placement="top" + > + <Icon icon="ep:question" class="ml-5px" /> + </el-tooltip> + </el-form-item> + <el-form-item v-if="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath"> + <el-input + v-model="modelData.formCustomViewPath" + placeholder="请输入表单查看的组件地址" + style="width: 330px" + /> + <el-tooltip + class="item" + content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue" + effect="light" + placement="top" + > + <Icon icon="ep:question" class="ml-5px" /> + </el-tooltip> + </el-form-item> + <!-- 表单预览 --> + <div + v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0" + class="mt-20px" + > + <div class="flex items-center mb-15px"> + <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div> + <span class="text-15px font-bold">表单预览</span> + </div> + <form-create + v-model="formPreview.formData" + :rule="formPreview.rule" + :option="formPreview.option" + /> + </div> + </el-form> +</template> + +<script lang="ts" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import * as FormApi from '@/api/bpm/form' +import { setConfAndFields2 } from '@/utils/formCreate' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + }, + formList: { + type: Array, + required: true + } +}) + +const emit = defineEmits(['update:modelValue']) + +const formRef = ref() + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 表单预览数据 +const formPreview = ref({ + formData: {}, + rule: [], + option: { + submitBtn: false, + resetBtn: false, + formData: {} + } +}) + +// 监听表单ID变化,加载表单数据 +watch( + () => modelData.value.formId, + async (newFormId) => { + if (newFormId && modelData.value.formType === 10) { + const data = await FormApi.getForm(newFormId) + setConfAndFields2(formPreview.value, data.conf, data.fields) + // 设置只读 + formPreview.value.rule.forEach((item: any) => { + item.props = { ...item.props, disabled: true } + }) + } else { + formPreview.value.rule = [] + } + }, + { immediate: true } +) + +const rules = { + formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }], + formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }], + formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }], + formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }] +} + +/** 表单校验 */ +const validate = async () => { + await formRef.value?.validate() +} + +defineExpose({ + validate +}) +</script> diff --git a/src/views/bpm/model/form/ProcessDesign.vue b/src/views/bpm/model/form/ProcessDesign.vue new file mode 100644 index 0000000..40d35ab --- /dev/null +++ b/src/views/bpm/model/form/ProcessDesign.vue @@ -0,0 +1,235 @@ +<template> + <!-- BPMN设计器 --> + <template v-if="modelData.type === BpmModelType.BPMN"> + <BpmModelEditor + v-if="showDesigner" + :model-id="modelData.id" + :model-key="modelData.key" + :model-name="modelData.name" + :value="currentBpmnXml" + ref="bpmnEditorRef" + @success="handleDesignSuccess" + @init-finished="handleEditorInit" + /> + </template> + + <!-- Simple设计器 --> + <template v-else> + <SimpleModelDesign + v-if="showDesigner" + :model-id="modelData.id" + :model-key="modelData.key" + :model-name="modelData.name" + :start-user-ids="modelData.startUserIds" + :value="currentSimpleModel" + ref="simpleEditorRef" + @success="handleDesignSuccess" + @init-finished="handleEditorInit" + /> + </template> +</template> + +<script lang="ts" setup> +import { BpmModelType } from '@/utils/constants' +import BpmModelEditor from '../editor/index.vue' +import SimpleModelDesign from '../../simple/SimpleModelDesign.vue' + +const props = defineProps({ + modelValue: { + type: Object, + required: true + } +}) + +const emit = defineEmits(['update:modelValue', 'success']) + +const bpmnEditorRef = ref() +const simpleEditorRef = ref() +const isEditorInitialized = ref(false) + +// 创建本地数据副本 +const modelData = computed({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +// 保存当前的流程XML或数据 +const currentBpmnXml = ref('') +const currentSimpleModel = ref('') + +// 初始化或更新当前的XML数据 +const initOrUpdateXmlData = () => { + if (modelData.value) { + if (modelData.value.type === BpmModelType.BPMN) { + currentBpmnXml.value = modelData.value.bpmnXml || '' + } else { + currentSimpleModel.value = modelData.value.simpleModel || '' + } + } +} + +// 监听modelValue的变化,更新数据 +watch( + () => props.modelValue, + (newVal) => { + if (newVal) { + if (newVal.type === BpmModelType.BPMN) { + if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) { + currentBpmnXml.value = newVal.bpmnXml + // 如果编辑器已经初始化,刷新视图 + if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) { + nextTick(() => { + bpmnEditorRef.value.refresh() + }) + } + } + } else { + if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) { + currentSimpleModel.value = newVal.simpleModel + // 如果编辑器已经初始化,刷新视图 + if (isEditorInitialized.value && simpleEditorRef.value?.refresh) { + nextTick(() => { + simpleEditorRef.value.refresh() + }) + } + } + } + } + }, + { immediate: true, deep: true } +) + +/** 编辑器初始化完成的回调 */ +const handleEditorInit = async () => { + isEditorInitialized.value = true + + // 等待下一个tick,确保编辑器已经准备好 + await nextTick() + + // 初始化完成后,设置初始值 + if (modelData.value.type === BpmModelType.BPMN) { + if (modelData.value.bpmnXml) { + currentBpmnXml.value = modelData.value.bpmnXml + if (bpmnEditorRef.value?.refresh) { + await nextTick() + bpmnEditorRef.value.refresh() + } + } + } else { + if (modelData.value.simpleModel) { + currentSimpleModel.value = modelData.value.simpleModel + if (simpleEditorRef.value?.refresh) { + await nextTick() + simpleEditorRef.value.refresh() + } + } + } +} + +/** 获取当前流程数据 */ +const getProcessData = async () => { + try { + if (modelData.value.type === BpmModelType.BPMN) { + if (!bpmnEditorRef.value || !isEditorInitialized.value) { + return currentBpmnXml.value || undefined + } + const { xml } = await bpmnEditorRef.value.saveXML() + if (xml) { + currentBpmnXml.value = xml + return xml + } + } else { + if (!simpleEditorRef.value || !isEditorInitialized.value) { + return currentSimpleModel.value || undefined + } + const flowData = await simpleEditorRef.value.getCurrentFlowData() + if (flowData) { + currentSimpleModel.value = flowData + return flowData + } + } + return modelData.value.type === BpmModelType.BPMN + ? currentBpmnXml.value + : currentSimpleModel.value + } catch (error) { + console.error('获取流程数据失败:', error) + return modelData.value.type === BpmModelType.BPMN + ? currentBpmnXml.value + : currentSimpleModel.value + } +} + +/** 表单校验 */ +const validate = async () => { + try { + // 获取最新的流程数据 + const processData = await getProcessData() + if (!processData) { + throw new Error('请设计流程') + } + return true + } catch (error) { + throw error + } +} + +/** 处理设计器保存成功 */ +const handleDesignSuccess = async (data?: any) => { + if (data) { + if (modelData.value.type === BpmModelType.BPMN) { + currentBpmnXml.value = data + } else { + currentSimpleModel.value = data + } + + // 创建新的对象以触发响应式更新 + const newModelData = { + ...modelData.value, + bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, + simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data + } + + // 使用emit更新父组件的数据 + await nextTick() + emit('update:modelValue', newModelData) + emit('success', data) + } +} + +/** 是否显示设计器 */ +const showDesigner = computed(() => { + return Boolean(modelData.value?.key && modelData.value?.name) +}) + +// 组件创建时初始化数据 +onMounted(() => { + initOrUpdateXmlData() +}) + +// 组件卸载前保存数据 +onBeforeUnmount(async () => { + try { + // 获取并保存最新的流程数据 + const data = await getProcessData() + if (data) { + // 创建新的对象以触发响应式更新 + const newModelData = { + ...modelData.value, + bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, + simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data + } + + // 使用emit更新父组件的数据 + await nextTick() + emit('update:modelValue', newModelData) + } + } catch (error) { + console.error('保存数据失败:', error) + } +}) + +defineExpose({ + validate, + getProcessData +}) +</script> diff --git a/src/views/bpm/model/form/index.vue b/src/views/bpm/model/form/index.vue new file mode 100644 index 0000000..4585fc6 --- /dev/null +++ b/src/views/bpm/model/form/index.vue @@ -0,0 +1,439 @@ +<template> + <ContentWrap> + <div class="mx-auto"> + <!-- 头部导航栏 --> + <div + class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px" + > + <!-- 左侧标题 --> + <div class="w-200px flex items-center overflow-hidden"> + <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" /> + <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'"> + {{ formData.name || '创建流程' }} + </span> + </div> + + <!-- 步骤条 --> + <div class="flex-1 flex items-center justify-center h-full"> + <div class="w-400px flex items-center justify-between h-full"> + <div + v-for="(step, index) in steps" + :key="index" + class="flex items-center cursor-pointer mx-15px relative h-full" + :class="[ + currentStep === index + ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid' + : 'text-gray-500' + ]" + @click="handleStepClick(index)" + > + <div + class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px" + :class="[ + currentStep === index + ? 'bg-[#3473ff] text-white border-[#3473ff]' + : 'border-gray-300 bg-white text-gray-500' + ]" + > + {{ index + 1 }} + </div> + <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span> + </div> + </div> + </div> + + <!-- 右侧按钮 --> + <div class="w-200px flex items-center justify-end gap-2"> + <el-button v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button> + <el-button type="primary" @click="handleSave">保 存</el-button> + </div> + </div> + + <!-- 主体内容 --> + <div class="mt-50px"> + <!-- 第一步:基本信息 --> + <div v-if="currentStep === 0" class="mx-auto w-560px"> + <BasicInfo + v-model="formData" + :categoryList="categoryList" + :userList="userList" + ref="basicInfoRef" + /> + </div> + + <!-- 第二步:表单设计 --> + <div v-if="currentStep === 1" class="mx-auto w-560px"> + <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" /> + </div> + + <!-- 第三步:流程设计 --> + <ProcessDesign + v-if="currentStep === 2" + v-model="formData" + ref="processDesignRef" + @success="handleDesignSuccess" + /> + </div> + </div> + </ContentWrap> +</template> + +<script lang="ts" setup> +import { useRoute, useRouter } from 'vue-router' +import { useMessage } from '@/hooks/web/useMessage' +import * as ModelApi from '@/api/bpm/model' +import * as FormApi from '@/api/bpm/form' +import { CategoryApi } from '@/api/bpm/category' +import * as UserApi from '@/api/system/user' +import { useUserStoreWithOut } from '@/store/modules/user' +import { BpmModelFormType, BpmModelType } from '@/utils/constants' +import BasicInfo from './BasicInfo.vue' +import FormDesign from './FormDesign.vue' +import ProcessDesign from './ProcessDesign.vue' +import { useTagsViewStore } from '@/store/modules/tagsView' + +const router = useRouter() +const { delView } = useTagsViewStore() // 视图操作 +const route = useRoute() +const message = useMessage() +const userStore = useUserStoreWithOut() + +// 组件引用 +const basicInfoRef = ref() +const formDesignRef = ref() +const processDesignRef = ref() + +/** 步骤校验函数 */ +const validateBasic = async () => { + await basicInfoRef.value?.validate() +} + +/** 表单设计校验 */ +const validateForm = async () => { + await formDesignRef.value?.validate() +} + +/** 流程设计校验 */ +const validateProcess = async () => { + await processDesignRef.value?.validate() +} + +const currentStep = ref(0) // 步骤控制 +const steps = [ + { title: '基本信息', validator: validateBasic }, + { title: '表单设计', validator: validateForm }, + { title: '流程设计', validator: validateProcess } +] + +// 表单数据 +const formData: any = ref({ + id: undefined, + name: '', + key: '', + category: undefined, + icon: undefined, + description: '', + type: BpmModelType.BPMN, + formType: BpmModelFormType.NORMAL, + formId: '', + formCustomCreatePath: '', + formCustomViewPath: '', + visible: true, + startUserType: undefined, + managerUserType: undefined, + startUserIds: [], + managerUserIds: [] +}) + +// 数据列表 +const formList = ref([]) +const categoryList = ref([]) +const userList = ref<UserApi.UserVO[]>([]) + +/** 初始化数据 */ +const initData = async () => { + const modelId = route.params.id as string + if (modelId) { + // 修改场景 + formData.value = await ModelApi.getModel(modelId) + } else { + // 新增场景 + formData.value.managerUserIds.push(userStore.getUser.id) + } + + // 获取表单列表 + formList.value = await FormApi.getFormSimpleList() + // 获取分类列表 + categoryList.value = await CategoryApi.getCategorySimpleList() + // 获取用户列表 + userList.value = await UserApi.getSimpleUserList() +} + +/** 校验所有步骤数据是否完整 */ +const validateAllSteps = async () => { + try { + // 基本信息校验 + await basicInfoRef.value?.validate() + if (!formData.value.key || !formData.value.name || !formData.value.category) { + currentStep.value = 0 + throw new Error('请完善基本信息') + } + + // 表单设计校验 + await formDesignRef.value?.validate() + if (formData.value.formType === 10 && !formData.value.formId) { + currentStep.value = 1 + throw new Error('请选择流程表单') + } + if ( + formData.value.formType === 20 && + (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath) + ) { + currentStep.value = 1 + throw new Error('请完善自定义表单信息') + } + + // 流程设计校验 + // 如果已经有流程数据,则不需要重新校验 + if (!formData.value.bpmnXml && !formData.value.simpleModel) { + // 如果当前不在第三步,需要先保存当前步骤数据 + if (currentStep.value !== 2) { + await steps[currentStep.value].validator() + // 切换到第三步 + currentStep.value = 2 + // 等待组件渲染完成 + await nextTick() + } + + // 校验流程设计 + await processDesignRef.value?.validate() + const processData = await processDesignRef.value?.getProcessData() + if (!processData) { + throw new Error('请设计流程') + } + + // 保存流程数据 + if (formData.value.type === BpmModelType.BPMN) { + formData.value.bpmnXml = processData + formData.value.simpleModel = null + } else { + formData.value.bpmnXml = null + formData.value.simpleModel = processData + } + } + + return true + } catch (error) { + throw error + } +} + +/** 保存操作 */ +const handleSave = async () => { + try { + // 保存前校验所有步骤的数据 + await validateAllSteps() + + // 更新表单数据 + const modelData = { + ...formData.value + } + + // 如果当前在第三步,获取最新的流程设计数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + modelData.bpmnXml = processData + modelData.simpleModel = null + } else { + modelData.bpmnXml = null + modelData.simpleModel = processData + } + } + } + + if (formData.value.id) { + // 修改场景 + await ModelApi.updateModel(modelData) + // 询问是否发布流程 + try { + await message.confirm('修改流程成功,是否发布流程?') + // 用户点击确认,执行发布 + await handleDeploy() + } catch { + // 用户点击取消,停留在当前页面 + } + } else { + // 新增场景 + formData.value.id = await ModelApi.createModel(modelData) + message.success('新增成功') + try { + await message.confirm('创建流程成功,是否继续编辑?') + // 用户点击继续编辑,跳转到编辑页面 + await nextTick() + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 跳转到编辑页面 + await router.push({ + name: 'BpmModelUpdate', + params: { id: formData.value.id } + }) + } catch { + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 用户点击返回列表 + await router.push({ name: 'BpmModel' }) + } + } + } catch (error: any) { + console.error('保存失败:', error) + message.warning(error.message || '请完善所有步骤的必填信息') + } +} + +/** 发布操作 */ +const handleDeploy = async () => { + try { + // 修改场景下直接发布,新增场景下需要先确认 + if (!formData.value.id) { + await message.confirm('是否确认发布该流程?') + } + + // 校验所有步骤 + await validateAllSteps() + + // 更新表单数据 + const modelData = { + ...formData.value + } + + // 如果当前在第三步,获取最新的流程设计数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + modelData.bpmnXml = processData + modelData.simpleModel = null + } else { + modelData.bpmnXml = null + modelData.simpleModel = processData + } + } + } + + // 先保存所有数据 + if (formData.value.id) { + await ModelApi.updateModel(modelData) + } else { + const result = await ModelApi.createModel(modelData) + formData.value.id = result.id + } + + // 发布 + await ModelApi.deployModel(formData.value.id) + message.success('发布成功') + // 返回列表页 + await router.push({ name: 'BpmModel' }) + } catch (error: any) { + console.error('发布失败:', error) + message.warning(error.message || '发布失败') + } +} + +/** 步骤切换处理 */ +const handleStepClick = async (index: number) => { + try { + // 如果是切换到第三步(流程设计),需要校验key和name + if (index === 2) { + if (!formData.value.key || !formData.value.name) { + message.warning('请先填写流程标识和流程名称') + return + } + } + + // 保存当前步骤的数据 + if (currentStep.value === 2) { + const processData = await processDesignRef.value?.getProcessData() + if (processData) { + if (formData.value.type === BpmModelType.BPMN) { + formData.value.bpmnXml = processData + formData.value.simpleModel = null + } else { + formData.value.bpmnXml = null + formData.value.simpleModel = processData + } + } + } else { + // 只有在向后切换时才进行校验 + if (index > currentStep.value) { + if (typeof steps[currentStep.value].validator === 'function') { + await steps[currentStep.value].validator() + } + } + } + + // 切换步骤 + currentStep.value = index + + // 如果切换到流程设计步骤,等待组件渲染完成后刷新设计器 + if (index === 2) { + await nextTick() + // 等待更长时间确保组件完全初始化 + await new Promise(resolve => setTimeout(resolve, 200)) + if (processDesignRef.value?.refresh) { + await processDesignRef.value.refresh() + } + } + } catch (error) { + console.error('步骤切换失败:', error) + message.warning('请先完善当前步骤必填信息') + } +} + +/** 处理设计器保存成功 */ +const handleDesignSuccess = (bpmnXml?: string) => { + if (bpmnXml) { + formData.value.bpmnXml = bpmnXml + } +} + +/** 返回列表页 */ +const handleBack = () => { + // 先删除当前页签 + delView(unref(router.currentRoute)) + // 跳转到列表页 + router.push({ name: 'BpmModel' }) +} + +/** 初始化 */ +onMounted(async () => { + await initData() +}) + +// 添加组件卸载前的清理代码 +onBeforeUnmount(() => { + // 清理所有的引用 + basicInfoRef.value = null + formDesignRef.value = null + processDesignRef.value = null +}) +</script> + +<style lang="scss" scoped> +.border-bottom { + border-bottom: 1px solid #dcdfe6; +} + +.text-primary { + color: #3473ff; +} + +.bg-primary { + background-color: #3473ff; +} + +.border-primary { + border-color: #3473ff; +} +</style> diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue index bf43d29..c7d9417 100644 --- a/src/views/bpm/model/index.vue +++ b/src/views/bpm/model/index.vue @@ -106,6 +106,7 @@ defineOptions({ name: 'BpmModel' }) +const { push } = useRouter() const message = useMessage() // 消息弹窗 const loading = ref(true) // 列表的加载中 const isCategorySorting = ref(false) // 是否 category 正处于排序状态 @@ -124,7 +125,14 @@ /** 添加/修改操作 */ const formRef = ref() const openForm = (type: string, id?: number) => { - formRef.value.open(type, id) + if (type === 'create') { + push({ name: 'BpmModelCreate' }) + } else { + push({ + name: 'BpmModelUpdate', + params: { id } + }) + } } /** 流程表单的详情按钮操作 */ diff --git a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue index 3800f19..7eaf0f4 100644 --- a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue +++ b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue @@ -8,7 +8,7 @@ <!-- 中间主要内容 tab 栏 --> <el-tabs v-model="activeTab"> <!-- 表单信息 --> - <el-tab-pane label="表单填写" name="form" > + <el-tab-pane label="表单填写" name="form"> <div class="form-scroll-area" v-loading="processInstanceStartLoading"> <el-scrollbar> <el-row> @@ -75,7 +75,11 @@ <script lang="ts" setup> import { decodeFields, setConfAndFields2 } from '@/utils/formCreate' import { BpmModelType } from '@/utils/constants' -import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts' +import { + CandidateStrategy, + NodeId, + FieldPermissionType +} from '@/components/SimpleProcessDesignerV2/src/consts' import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue' import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue' import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue' @@ -129,8 +133,10 @@ } } setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables) + await nextTick() fApi.value?.btn.show(false) // 隐藏提交按钮 + // 获取流程审批信息 await getApprovalDetail(row) @@ -152,7 +158,12 @@ /** 获取审批详情 */ const getApprovalDetail = async (row: any) => { try { - const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id }) + // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效) + const data = await ProcessInstanceApi.getApprovalDetail({ + processDefinitionId: row.id, + activityId: NodeId.START_USER_NODE_ID + }) + if (!data) { message.error('查询不到审批详情信息!') return @@ -170,7 +181,33 @@ // 获取审批节点,显示 Timeline 的数据 activityNodes.value = data.activityNodes + // 获取表单字段权限 + const formFieldsPermission = data.formFieldsPermission + // 设置表单字段权限 + if (formFieldsPermission) { + Object.keys(formFieldsPermission).forEach((item) => { + setFieldPermission(item, formFieldsPermission[item]) + }) + } } finally { + } +} + +/** + * 设置表单权限 + */ +const setFieldPermission = (field: string, permission: string) => { + if (permission === FieldPermissionType.READ) { + //@ts-ignore + fApi.value?.disabled(true, field) + } + if (permission === FieldPermissionType.WRITE) { + //@ts-ignore + fApi.value?.disabled(false, field) + } + if (permission === FieldPermissionType.NONE) { + //@ts-ignore + fApi.value?.hidden(true, field) } } @@ -243,11 +280,11 @@ .form-scroll-area { height: calc( 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - - $process-header-height - 40px + $process-header-height - 40px ); max-height: calc( 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - - $process-header-height - 40px + $process-header-height - 40px ); overflow: auto; } diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue index 894a5d4..1b3ebc5 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue @@ -588,7 +588,7 @@ }) const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({ deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }], - reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], + reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }], }) // 退回表单 @@ -627,11 +627,11 @@ const openPopover = async (type: string) => { if (type === 'approve') { // 校验流程表单 - const valid = await validateNormalForm(); - if (!valid) { + const valid = await validateNormalForm(); + if (!valid) { message.warning('表单校验不通过,请先完善表单!!') return; - } + } } if (type === 'return') { // 获取退回节点 @@ -652,7 +652,7 @@ const closePropover = (type: string, formRef: FormInstance | undefined) => { if (formRef) { formRef.resetFields() - } + } popOverVisible.value[type] = false } @@ -664,8 +664,8 @@ if (!formRef) return await formRef.validate() if (pass) { - // 获取修改的流程变量, 暂时只支持流程表单 - const variables = getUpdatedProcessInstanceVaiables(); + // 获取修改的流程变量, 暂时只支持流程表单 + const variables = getUpdatedProcessInstanceVaiables(); // 审批通过数据 const data = { id: runningTask.value.id, @@ -684,8 +684,8 @@ popOverVisible.value.approve = false message.success('审批通过成功') } else { - // 审批不通过数据 - const data = { + // 审批不通过数据 + const data = { id: runningTask.value.id, reason: rejectReasonForm.reason, } @@ -752,7 +752,7 @@ const handleDelegate = async () => { formLoading.value = true try { - + // 1.1 校验表单 if (!delegateFormRef.value) return await delegateFormRef.value.validate() diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue index 56b466a..e24316c 100644 --- a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue +++ b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue @@ -25,7 +25,7 @@ </div> </div> </template> - <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`"> + <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`"> <!-- 第一行:节点名称、时间 --> <div class="flex w-full"> <div class="font-bold"> {{ activity.name }}</div> @@ -113,7 +113,7 @@ </div> </div> </div> - <teleport defer :to="`#activity-task-${activity.id}`"> + <teleport defer :to="`#activity-task-${activity.id}-${index}`"> <div v-if=" task.reason && diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue index 9809f7a..a6ed3b5 100644 --- a/src/views/bpm/processInstance/detail/index.vue +++ b/src/views/bpm/processInstance/detail/index.vue @@ -325,11 +325,11 @@ display: flex; height: calc( 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - - $process-header-height - 40px + $process-header-height - 40px ); max-height: calc( 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px - - $process-header-height - 40px + $process-header-height - 40px ); overflow: auto; flex-direction: column; diff --git a/src/views/bpm/simple/SimpleModelDesign.vue b/src/views/bpm/simple/SimpleModelDesign.vue index 1740e03..eed0099 100644 --- a/src/views/bpm/simple/SimpleModelDesign.vue +++ b/src/views/bpm/simple/SimpleModelDesign.vue @@ -1,6 +1,15 @@ <template> <ContentWrap :bodyStyle="{ padding: '20px 16px' }"> - <SimpleProcessDesigner :model-id="modelId" @success="close" /> + <SimpleProcessDesigner + :model-id="modelId" + :model-key="modelKey" + :model-name="modelName" + :value="currentValue" + @success="handleSuccess" + @init-finished="handleInit" + :start-user-ids="startUserIds" + ref="designerRef" + /> </ContentWrap> </template> <script setup lang="ts"> @@ -9,11 +18,138 @@ defineOptions({ name: 'SimpleModelDesign' }) -const router = useRouter() // 路由 -const { query } = useRoute() // 路由的查询 -const modelId = query.modelId as string -const close = () => { - router.push({ path: '/bpm/manager/model' }) + +const props = defineProps<{ + modelId?: string + modelKey?: string + modelName?: string + value?: string + startUserIds?: number[] +}>() + +const emit = defineEmits(['success', 'init-finished']) +const designerRef = ref() +const isInitialized = ref(false) +const currentValue = ref('') + +// 初始化或更新当前值 +const initOrUpdateValue = async () => { + console.log('initOrUpdateValue', props.value) + if (props.value) { + currentValue.value = props.value + // 如果设计器已经初始化,立即加载数据 + if (isInitialized.value && designerRef.value) { + try { + await designerRef.value.loadProcessData(props.value) + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } catch (error) { + console.error('加载流程数据失败:', error) + } + } + } } + +// 监听属性变化 +watch( + [() => props.modelKey, () => props.modelName, () => props.value], + async ([newKey, newName, newValue], [oldKey, oldName, oldValue]) => { + if (designerRef.value && isInitialized.value) { + try { + if (newKey && newName && (newKey !== oldKey || newName !== oldName)) { + await designerRef.value.updateModel(newKey, newName) + } + if (newValue && newValue !== oldValue) { + currentValue.value = newValue + await designerRef.value.loadProcessData(newValue) + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } + } catch (error) { + console.error('更新流程数据失败:', error) + } + } + }, + { deep: true, immediate: true } +) + +// 初始化完成回调 +const handleInit = async () => { + try { + isInitialized.value = true + emit('init-finished') + + // 等待下一个tick,确保设计器已经准备好 + await nextTick() + + // 初始化完成后,设置初始值 + if (props.modelKey && props.modelName) { + await designerRef.value.updateModel(props.modelKey, props.modelName) + } + if (props.value) { + currentValue.value = props.value + await designerRef.value.loadProcessData(props.value) + // 再次刷新确保数据正确加载 + await nextTick() + if (designerRef.value.refresh) { + await designerRef.value.refresh() + } + } + } catch (error) { + console.error('初始化流程数据失败:', error) + } +} + +// 修改成功回调 +const handleSuccess = (data?: any) => { + console.warn('handleSuccess', data) + if (data && data !== currentValue.value) { + currentValue.value = data + emit('success', data) + } +} + +/** 获取当前流程数据 */ +const getCurrentFlowData = async () => { + try { + if (designerRef.value) { + const data = await designerRef.value.getCurrentFlowData() + if (data) { + currentValue.value = data + } + return data + } + return currentValue.value || undefined + } catch (error) { + console.error('获取流程数据失败:', error) + return currentValue.value || undefined + } +} + +// 组件创建时初始化数据 +onMounted(() => { + initOrUpdateValue() +}) + +// 组件卸载前保存数据 +onBeforeUnmount(async () => { + try { + const data = await getCurrentFlowData() + if (data) { + emit('success', data) + } + } catch (error) { + console.error('保存数据失败:', error) + } +}) + +defineExpose({ + getCurrentFlowData, + refresh: () => designerRef.value?.refresh?.() +}) </script> <style lang="scss" scoped></style> diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue index 1365104..e83e9ed 100644 --- a/src/views/bpm/task/done/index.vue +++ b/src/views/bpm/task/done/index.vue @@ -103,7 +103,7 @@ <el-button @click="handleQuery"> 确认</el-button> <el-button @click="showPopover = false"> 取消</el-button> <el-button @click="resetQuery"> 清空</el-button> - </el-form-item> + </el-form-item> </el-popover> </el-form-item> diff --git a/src/views/infra/file/index.vue b/src/views/infra/file/index.vue index e1d2ffd..3598bfa 100644 --- a/src/views/infra/file/index.vue +++ b/src/views/infra/file/index.vue @@ -91,6 +91,9 @@ /> <el-table-column label="操作" align="center"> <template #default="scope"> + <el-button link type="primary" @click="copyToClipboard(scope.row.url)"> + 复制链接 + </el-button> <el-button link type="danger" @@ -168,6 +171,13 @@ formRef.value.open() } +/** 复制到剪贴板方法 */ +const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + message.success('复制成功') + }) +} + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { diff --git a/src/views/system/area/index.vue b/src/views/system/area/index.vue index f3289a0..339ecef 100644 --- a/src/views/system/area/index.vue +++ b/src/views/system/area/index.vue @@ -1,4 +1,6 @@ <template> + <doc-alert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" /> + <!-- 操作栏 --> <ContentWrap> <el-button type="primary" plain @click="openForm()"> @@ -14,6 +16,7 @@ <template #default="{ height, width }"> <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> <el-table-v2 + v-loading="loading" :columns="columns" :data="list" :width="width" @@ -29,7 +32,7 @@ <AreaForm ref="formRef" /> </template> <script setup lang="tsx"> -import type { Column } from 'element-plus' +import { Column } from 'element-plus' import AreaForm from './AreaForm.vue' import * as AreaApi from '@/api/system/area' @@ -38,7 +41,7 @@ // 表格的 column 字段 const columns: Column[] = [ { - dataKey: 'id', // 需要渲染当前列的数据字段。例如说:{id:9527, name:'Mike'},则填 id + dataKey: 'id', // 需要渲染当前列的数据字段 title: '编号', // 显示在单元格表头的文本 width: 400, // 当前列的宽度,必须设置 fixed: true, // 是否固定列 @@ -50,14 +53,17 @@ width: 200 } ] -// 表格的数据 -const list = ref([]) +const loading = ref(true) // 列表的加载中 +const list = ref([]) // 表格的数据 -/** - * 获得数据列表 - */ +/** 获得数据列表 */ const getList = async () => { - list.value = await AreaApi.getAreaTree() + loading.value = true + try { + list.value = await AreaApi.getAreaTree() + } finally { + loading.value = false + } } /** 添加/修改操作 */ diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index a8fcc0e..03d352f 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -50,10 +50,6 @@ <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> - <el-button plain type="danger" @click="toggleExpandAll"> - <Icon class="mr-5px" icon="ep:sort" /> - 展开/折叠 - </el-button> <el-button plain @click="refreshMenu"> <Icon class="mr-5px" icon="ep:refresh" /> 刷新菜单缓存 @@ -64,57 +60,22 @@ <!-- 列表 --> <ContentWrap> - <el-table - v-if="refreshTable" - v-loading="loading" - :data="list" - :default-expand-all="isExpandAll" - row-key="id" - > - <el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" /> - <el-table-column align="center" label="图标" prop="icon" width="100"> - <template #default="scope"> - <Icon :icon="scope.row.icon" /> + <div style="height: 700px"> + <!-- AutoResizer 自动调节大小 --> + <el-auto-resizer> + <template #default="{ height, width }"> + <!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 --> + <el-table-v2 + v-loading="loading" + :columns="columns" + :data="list" + :width="width" + :height="height" + expand-column-key="name" + /> </template> - </el-table-column> - <el-table-column label="排序" prop="sort" width="60" /> - <el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" /> - <el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" /> - <el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" /> - <el-table-column label="状态" prop="status" width="80"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column align="center" label="操作"> - <template #default="scope"> - <el-button - v-hasPermi="['system:menu:update']" - link - type="primary" - @click="openForm('update', scope.row.id)" - > - 修改 - </el-button> - <el-button - v-hasPermi="['system:menu:create']" - link - type="primary" - @click="openForm('create', undefined, scope.row.id)" - > - 新增 - </el-button> - <el-button - v-hasPermi="['system:menu:delete']" - link - type="danger" - @click="handleDelete(scope.row.id)" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> + </el-auto-resizer> + </div> </ContentWrap> <!-- 表单弹窗:添加/修改 --> @@ -124,8 +85,14 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { handleTree } from '@/utils/tree' import * as MenuApi from '@/api/system/menu' +import { MenuVO } from '@/api/system/menu' import MenuForm from './MenuForm.vue' -import {CACHE_KEY, useCache, useSessionCache} from '@/hooks/web/useCache' +import { CACHE_KEY, useCache, useSessionCache } from '@/hooks/web/useCache' +import { h } from 'vue' +import { Column, ElButton } from 'element-plus' +import { Icon } from '@/components/Icon' +import { hasPermission } from '@/directives/permission/hasPermi' +import { CommonStatusEnum } from '@/utils/constants' defineOptions({ name: 'SystemMenu' }) @@ -134,6 +101,101 @@ const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 +// 表格的 column 字段 +const columns: Column[] = [ + { + dataKey: 'name', + title: '菜单名称', + width: 250 + }, + { + dataKey: 'icon', + title: '图标', + width: 150, + cellRenderer: ({ rowData }) => { + return h(Icon, { + icon: rowData.icon + }) + } + }, + { + dataKey: 'sort', + title: '排序', + width: 100 + }, + { + dataKey: 'permission', + title: '权限标识', + width: 240 + }, + { + dataKey: 'component', + title: '组件路径', + width: 240 + }, + { + dataKey: 'componentName', + title: '组件名称', + width: 240 + }, + { + dataKey: 'status', + title: '状态', + width: 160, + cellRenderer: ({ rowData }) => { + return h(ElSwitch, { + modelValue: rowData.status, + activeValue: CommonStatusEnum.ENABLE, + inactiveValue: CommonStatusEnum.DISABLE, + loading: menuStatusUpdating.value[rowData.id], + disabled: !hasPermission(['system:menu:update']), + onChange: (val) => handleStatusChanged(rowData, val as number) + }) + } + }, + { + dataKey: 'operation', + title: '操作', + width: 200, + cellRenderer: ({ rowData }) => { + return h( + 'div', + [ + hasPermission(['system:menu:update']) && + h( + ElButton, + { + link: true, + type: 'primary', + onClick: () => openForm('update', rowData.id) + }, + '修改' + ), + hasPermission(['system:menu:create']) && + h( + ElButton, + { + link: true, + type: 'primary', + onClick: () => openForm('create', undefined, rowData.id) + }, + '新增' + ), + hasPermission(['system:menu:delete']) && + h( + ElButton, + { + link: true, + type: 'danger', + onClick: () => handleDelete(rowData.id) + }, + '删除' + ) + ].filter(Boolean) + ) + } + } +] const loading = ref(true) // 列表的加载中 const list = ref<any>([]) // 列表的数据 const queryParams = reactive({ @@ -141,8 +203,6 @@ status: undefined }) const queryFormRef = ref() // 搜索的表单 -const isExpandAll = ref(false) // 是否展开,默认全部折叠 -const refreshTable = ref(true) // 重新渲染表格状态 /** 查询列表 */ const getList = async () => { @@ -172,21 +232,14 @@ formRef.value.open(type, id, parentId) } -/** 展开/折叠操作 */ -const toggleExpandAll = () => { - refreshTable.value = false - isExpandAll.value = !isExpandAll.value - nextTick(() => { - refreshTable.value = true - }) -} - /** 刷新菜单缓存按钮操作 */ const refreshMenu = async () => { try { await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存') // 清空,从而触发刷新 wsCache.delete(CACHE_KEY.USER) + wsCache.delete(CACHE_KEY.ROLE_ROUTERS) + wsSessionCache.delete(CACHE_KEY.USER) wsSessionCache.delete(CACHE_KEY.ROLE_ROUTERS) // 刷新浏览器 location.reload() @@ -206,6 +259,21 @@ } catch {} } +/** 开启/关闭菜单的状态 */ +const menuStatusUpdating = ref({}) // 菜单状态更新中的 menu 映射。key:菜单编号,value:是否更新中 +const handleStatusChanged = async (menu: MenuVO, val: number) => { + // 1. 标记 menu.id 更新中 + menuStatusUpdating.value[menu.id] = true + try { + // 2. 发起更新状态 + menu.status = val + await MenuApi.updateMenu(menu) + } finally { + // 3. 标记 menu.id 更新完成 + menuStatusUpdating.value[menu.id] = false + } +} + /** 初始化 **/ onMounted(() => { getList() -- Gitblit v1.9.3