已添加7个文件
已修改37个文件
3193 ■■■■ 文件已修改
package.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/model/sche/scheme/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/delay.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echart/src/Echart.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RouterSearch/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue 213 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/consts.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/node.ts 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UserSelectForm/index.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/process-designer.scss 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directives/permission/hasPermi.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Footer/src/Footer.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/remaining.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/routerHelper.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/CategoryDraggableModel.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/ModelForm.vue 189 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/editor/index.vue 258 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/BasicInfo.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/FormDesign.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/ProcessDesign.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/form/index.vue 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/simple/SimpleModelDesign.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/point/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/file/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/model/ScheduleModelForm.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/model/sche/scheme/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/area/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
src/api/model/sche/scheme/index.ts
@@ -45,3 +45,16 @@
export const deleteScheduleScheme = (id: number) => {
  return request.delete({ url: '/model/sche/scheme/delete?id=' + id })
}
// 启用
export const enable = (ids) => {
  const data = ids
  return request.put({ url: '/model/sche/scheme/enable', data })
}
// 禁用
export const disable = (ids) => {
  const data = ids
  return request.put({ url: '/model/sche/scheme/disable', data })
}
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>
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(() => {
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
    // 这里可以执行相应的操作(例如打开搜索框等)
  }
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>
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({
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>
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>
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 }
]
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>> = []
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
}
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>
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
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>
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;
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 方法,用于打开弹窗
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 || '&nbsp;'
// }
onBeforeMount(() => {
  console.log(props, 'propspropspropsprops')
})
onMounted(() => {
  initBpmnModeler()
  createNewDiagram(props.value)
})
onBeforeUnmount(() => {
  // this.$once('hook:beforeDestroy', () => {
  // })
  if (bpmnModeler) bpmnModeler.destroy()
  emit('destroy', bpmnModeler)
  bpmnModeler = null
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(
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;
    // }
  }
}
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)
  })
}
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>
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'
        }
      }
    ]
  },
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 包含 ?,则表示需要传递参数
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 })
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>
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>
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>
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>
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>
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>
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 }
    })
  }
}
/** 流程表单的详情按钮操作 */
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;
  }
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()
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 &&
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;
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>
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>
src/views/data/point/index.vue
@@ -295,9 +295,8 @@
    let ids = dataListSelections.map(item => {
      return item.id
    })
    // 启用的二次确认
    await message.enableConfirm(ids)
    // 二次确认
    await message.confirm('确认要开启所选测点?')
    await DaPoint.enable(ids)
    message.success(t('common.enableSuccess'))
    await getList()
@@ -307,9 +306,8 @@
    let ids = dataListSelections.map(item => {
      return item.id
    })
    // 启用的二次确认
    await message.disableConfirm(ids,)
    // 二次确认
    await message.confirm('确认要禁用所选测点?')
    await DaPoint.disable(ids)
    message.success(t('common.disableSuccess'))
    await getList()
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 {
src/views/model/sche/model/ScheduleModelForm.vue
@@ -210,6 +210,7 @@
        <el-table-column
          prop=""
          label="键"
          min-width="150"
          align="center">
          <template #default="scope">
            <el-input v-model="scope.row.key" maxlength="20" clearable :disabled="true"
@@ -219,6 +220,7 @@
        <el-table-column
          prop=""
          label="名称"
          min-width="150"
          align="center">
          <template #default="scope">
            <el-input v-model="scope.row.name" maxlength="20" clearable :disabled="true"
@@ -228,6 +230,7 @@
        <el-table-column
          prop=""
          label="类型"
          min-width="100"
          align="center">
          <template #default="scope">
            <el-select v-model="scope.row.valuetype" placeholder="请选择" :disabled="true">
@@ -243,34 +246,14 @@
        <el-table-column
          prop=""
          label="值"
          min-width="300"
          align="center">
          <template #default="scope">
            <el-input v-model="scope.row.value" maxlength="256" clearable  :disabled="scope.row.key === 'pyFile'"
            <el-input v-model="scope.row.value" maxlength="256" clearable
                      :disabled="scope.row.key === 'pyFile_BAK'"
                      style="width:100%;hight:100%"/>
          </template>
        </el-table-column>
<!--        <el-table-column-->
<!--          prop=""-->
<!--          label="操作"-->
<!--          width="100"-->
<!--          align="center">-->
<!--          <template #default="scope">-->
<!--            <el-button-->
<!--              @click.prevent="addRow(scope.$index, formData.settingList)"-->
<!--              link-->
<!--              type="primary"-->
<!--              size="small">-->
<!--              添加-->
<!--            </el-button>-->
<!--            <el-button-->
<!--              @click.prevent="deleteRow(scope.$index, formData.settingList)"-->
<!--              link-->
<!--              type="primary"-->
<!--              size="small">-->
<!--              删除-->
<!--            </el-button>-->
<!--          </template>-->
<!--        </el-table-column>-->
      </el-table>
      <el-divider content-position="left">模型下发配置</el-divider>
      <el-row :gutter="20">
@@ -334,6 +317,7 @@
        <el-table-column prop="disturbancePointNo’" label="无扰切换点位" align="center" min-width="200">
          <template #default="scope">
            <el-select v-model="scope.row.disturbancePointNo"
                       clearable
                       filterable
                       placeholder="请选择">
              <el-option
src/views/model/sche/scheme/index.vue
@@ -36,6 +36,20 @@
          重置
        </el-button>
        <el-button
          type="success"
          plain
          @click="enable"
          v-hasPermi="['sche:scheme:update']"
        >启用
        </el-button>
        <el-button
          type="danger"
          plain
          @click="disable"
          v-hasPermi="['sche:scheme:update']"
        >禁用
        </el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
@@ -50,7 +64,8 @@
  <!-- 列表 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
    <el-table v-loading="loading" :data="list" @selection-change="selectionChangeHandle">
      <el-table-column type="selection" header-align="center" align="center" fixed="left" width="50"/>
      <el-table-column label="方案编号" align="center" prop="code" min-width="100"/>
      <el-table-column label="方案名称" header-align="center" align="left" prop="name" min-width="100"/>
      <el-table-column label="触发方式" align="center" prop="triggerMethod" min-width="100">
@@ -124,6 +139,9 @@
  import * as ScheduleSchemeApi from '@/api/model/sche/scheme'
  import ScheduleSchemeForm from './ScheduleSchemeForm.vue'
  import RecordList from  './record/index.vue'
  import * as DaPoint from "@/api/data/da/point";
  import {reactive} from "vue";
  import {InfraJobStatusEnum} from "@/utils/constants";
  defineOptions({name: 'ScheduleScheme'})
@@ -192,6 +210,34 @@
    recordRef.value.open(id)
  }
  let dataListSelections = reactive([])
  // 多选
  function selectionChangeHandle (val) {
    dataListSelections = val
  }
  // 启用
  async function enable() {
    let ids = dataListSelections.map(item => {
      return item.id
    })
    // 二次确认
    await message.confirm('是否确认要启用所选调度方案?')
    await ScheduleSchemeApi.enable(ids)
    message.success(t('common.enableSuccess'))
    await getList()
  }
  // 禁用
  async function disable(){
    let ids = dataListSelections.map(item => {
      return item.id
    })
    // 二次确认
    await message.confirm('确认要禁用所选调度方案?')
    await ScheduleSchemeApi.disable(ids)
    message.success(t('common.disableSuccess'))
    await getList()
  }
  /** 初始化 **/
  onMounted(async () => {
    await getList()
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
  }
}
/** 添加/修改操作 */
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()