Selaa lähdekoodia

Merge remote-tracking branch 'upstream/master'

zhangcl 4 kuukautta sitten
vanhempi
commit
3f7666b7e6
24 muutettua tiedostoa jossa 954 lisäystä ja 352 poistoa
  1. 122 0
      src/components/DeptSelectForm/index.vue
  2. 3 2
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  3. 6 0
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  4. 57 14
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  5. 6 5
      src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
  6. 2 1
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
  7. 4 3
      src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
  8. 11 3
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  9. 11 3
      src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
  10. 14 3
      src/router/modules/remaining.ts
  11. 54 0
      src/views/ai/workflow/form/BasicInfo.vue
  12. 49 0
      src/views/ai/workflow/form/WorkflowDesign.vue
  13. 235 0
      src/views/ai/workflow/form/index.vue
  14. 28 34
      src/views/ai/workflow/index.vue
  15. 0 106
      src/views/ai/workflow/manager/WorkflowForm.vue
  16. 0 83
      src/views/ai/workflow/manager/WorkflowModelForm.vue
  17. 14 1
      src/views/bpm/model/CategoryDraggableModel.vue
  18. 73 1
      src/views/bpm/model/form/BasicInfo.vue
  19. 81 5
      src/views/bpm/model/form/ExtraSettings.vue
  20. 1 0
      src/views/bpm/model/form/ProcessDesign.vue
  21. 12 3
      src/views/bpm/model/form/index.vue
  22. 152 85
      src/views/bpm/oa/leave/create.vue
  23. 17 0
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  24. 2 0
      src/views/bpm/simple/SimpleModelDesign.vue

+ 122 - 0
src/components/DeptSelectForm/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <Dialog v-model="dialogVisible" title="部门选择" width="600">
+    <el-row v-loading="formLoading">
+      <el-col :span="24">
+        <ContentWrap class="h-1/1">
+          <el-tree
+            ref="treeRef"
+            :data="deptTree"
+            :props="defaultProps"
+            show-checkbox
+            :check-strictly="checkStrictly"
+            check-on-click-node
+            default-expand-all
+            highlight-current
+            node-key="id"
+            @check="handleCheck"
+          />
+        </ContentWrap>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button
+        :disabled="formLoading || !selectedDeptIds?.length"
+        type="primary"
+        @click="submitForm"
+      >
+        确 定
+      </el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+defineOptions({ name: 'DeptSelectForm' })
+
+const emit = defineEmits<{
+  confirm: [deptList: any[]]
+}>()
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  // 是否严格的遵循父子不互相关联
+  checkStrictly: {
+    type: Boolean,
+    default: false
+  },
+  // 是否支持多选
+  multiple: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const treeRef = ref()
+const deptTree = ref<Tree[]>([]) // 部门树形结构
+const selectedDeptIds = ref<number[]>([]) // 选中的部门 ID 列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+
+/** 打开弹窗 */
+const open = async (selectedList?: DeptApi.DeptVO[]) => {
+  resetForm()
+  formLoading.value = true
+  try {
+    // 加载部门列表
+    const deptData = await DeptApi.getSimpleDeptList()
+    deptTree.value = handleTree(deptData)
+  } finally {
+    formLoading.value = false
+  }
+  dialogVisible.value = true
+  // 设置已选择的部门
+  if (selectedList?.length) {
+    await nextTick()
+    const selectedIds = selectedList
+      .map((dept) => dept.id)
+      .filter((id): id is number => id !== undefined)
+    selectedDeptIds.value = selectedIds
+    treeRef.value?.setCheckedKeys(selectedIds)
+  }
+}
+
+/** 处理选中状态变化 */
+const handleCheck = (data: any, checked: any) => {
+  selectedDeptIds.value = treeRef.value.getCheckedKeys()
+  if (!props.multiple && selectedDeptIds.value.length > 1) {
+    // 单选模式下,只保留最后选择的节点
+    const lastSelectedId = selectedDeptIds.value[selectedDeptIds.value.length - 1]
+    selectedDeptIds.value = [lastSelectedId]
+    treeRef.value.setCheckedKeys([lastSelectedId])
+  }
+}
+
+/** 提交选择 */
+const submitForm = async () => {
+  try {
+    // 获取选中的完整部门数据
+    const checkedNodes = treeRef.value.getCheckedNodes()
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    emit('confirm', checkedNodes)
+  } finally {
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  deptTree.value = []
+  selectedDeptIds.value = []
+  if (treeRef.value) {
+    treeRef.value.setCheckedKeys([])
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 3 - 2
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -91,6 +91,7 @@ import {
   DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
 import { generateUUID } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 defineOptions({
   name: 'NodeHandler'
@@ -184,7 +185,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {
@@ -242,7 +243,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {

+ 6 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -59,6 +59,11 @@ const props = defineProps({
   startUserIds: {
     type: Array,
     required: false
+  },
+  // 可发起流程的部门编号
+  startDeptIds: {
+    type: Array,
+    required: false
   }
 })
 
@@ -82,6 +87,7 @@ provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
 provide('startUserIds', props.startUserIds)
+provide('startDeptIds', props.startDeptIds)
 provide('tasks', [])
 provide('processInstance', {})
 const message = useMessage() // 国际化

+ 57 - 14
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -25,21 +25,46 @@
     </template>
     <el-tabs type="border-card" v-model="activeTabName">
       <el-tab-pane label="权限" name="user">
-        <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
+          v-if="
+            (!startUserIds || startUserIds.length === 0) &&
+            (!startDeptIds || startDeptIds.length === 0)
+          "
+        >
+          全部成员可以发起流程
         </el-text>
+        <div v-else-if="startUserIds && startUserIds.length > 0">
+          <el-text v-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>
+        </div>
+        <div v-else-if="startDeptIds && startDeptIds.length > 0">
+          <el-text v-if="startDeptIds.length == 1">
+            {{ getDeptNames(startDeptIds) }} 可发起流程
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="getDeptNames(startDeptIds)"
+            >
+              {{ getDeptNames(startDeptIds.slice(0, 2)) }} 等
+              {{ startDeptIds.length }} 个部门可发起流程
+            </el-tooltip>
+          </el-text>
+        </div>
       </el-tab-pane>
       <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
         <div class="field-setting-pane">
@@ -107,6 +132,7 @@
 import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
 import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 defineOptions({
   name: 'StartUserNodeConfig'
 })
@@ -118,8 +144,12 @@ const props = defineProps({
 })
 // 可发起流程的用户编号
 const startUserIds = inject<Ref<any[]>>('startUserIds')
+// 可发起流程的部门编号
+const startDeptIds = inject<Ref<any[]>>('startDeptIds')
 // 用户列表
 const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
+// 部门列表
+const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList')
 // 抽屉配置
 const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
@@ -145,6 +175,19 @@ const getUserNicknames = (userIds: number[]): string => {
   })
   return nicknames.join(',')
 }
+const getDeptNames = (deptIds: number[]): string => {
+  if (!deptIds || deptIds.length === 0) {
+    return ''
+  }
+  const deptNames: string[] = []
+  deptIds.forEach((deptId) => {
+    const found = deptOptions?.value.find((item) => item.id === deptId)
+    if (found && found.name) {
+      deptNames.push(found.name)
+    }
+  })
+  return deptNames.join(',')
+}
 // 保存配置
 const saveConfig = async () => {
   activeTabName.value = 'user'

+ 6 - 5
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue

@@ -254,6 +254,7 @@ import {
 import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
 import HttpRequestSetting from './components/HttpRequestSetting.vue'
 import ConditionDialog from './components/ConditionDialog.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 
 defineOptions({
@@ -290,7 +291,7 @@ const configForm = ref<TriggerSetting>({
   },
   formSettings: [
     {
-      conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+      conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
       updateFormFields: {},
       deleteFields: []
     }
@@ -346,7 +347,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: {},
               deleteFields: []
             }
@@ -361,7 +362,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: undefined,
               deleteFields: []
             }
@@ -374,7 +375,7 @@ const changeTriggerType = () => {
 /** 添加新的修改表单设置 */
 const addFormSetting = () => {
   configForm.value.formSettings!.push({
-    conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+    conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
     updateFormFields: {},
     deleteFields: []
   })
@@ -509,7 +510,7 @@ const showTriggerNodeConfig = (node: SimpleFlowNode) => {
       },
       formSettings: node.triggerSetting.formSettings || [
         {
-          conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+          conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
           updateFormFields: {},
           deleteFields: []
         }

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -154,6 +154,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 
 const props = defineProps({
   modelValue: {
@@ -196,7 +197,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 4 - 3
src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue

@@ -1,5 +1,5 @@
 <!-- TODO @jason:有可能,它里面套 Condition 么?  -->
-<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion --> 
+<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion -->
 <template>
   <Dialog v-model="dialogVisible" title="条件配置" width="600px" :fullscreen="false">
     <div class="h-410px">
@@ -165,6 +165,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 defineOptions({
   name: 'ConditionDialog'
 })
@@ -175,7 +176,7 @@ const condition = ref<{
   conditionGroups?: ConditionGroup
 }>({
   conditionType: ConditionType.RULE,
-  conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+  conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
 })
 
 const emit = defineEmits<{
@@ -210,7 +211,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -108,11 +108,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'ExclusiveNode'
@@ -149,7 +156,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -181,7 +189,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -110,11 +110,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { useTaskStatusClass } from '../node'
 import { getDefaultInclusiveConditionNodeName } from '../utils'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'InclusiveNode'
@@ -153,7 +160,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -185,7 +193,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 14 - 3
src/router/modules/remaining.ts

@@ -810,16 +810,27 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/ai/knowledge'
         }
       },
-      // TODO @lesan::type =》 design 设计 AI 工作流
+      {
+        path: 'console/workflow/create',
+        component: () => import('@/views/ai/workflow/form/index.vue'),
+        name: 'AiWorkflowCreate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '设计 AI 工作流',
+          activeMenu: '/ai/console/workflow'
+        }
+      },
       {
         path: 'console/workflow/:type/:id',
-        component: () => import('@/views/ai/workflow/manager/WorkflowModelForm.vue'),
+        component: () => import('@/views/ai/workflow/form/index.vue'),
         name: 'AiWorkflowUpdate',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          title: '修改 AI 工作流',
+          title: '设计 AI 工作流',
           activeMenu: '/ai/console/workflow'
         }
       }

+ 54 - 0
src/views/ai/workflow/form/BasicInfo.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="formRules" label-width="120px">
+    <el-row>
+      <el-col :span="24">
+        <el-form-item label="流程标识" prop="code">
+          <el-input v-model="modelData.code" placeholder="请输入流程标识" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="流程名称" prop="name">
+          <el-input v-model="modelData.name" placeholder="请输入流程名称" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="modelData.status" placeholder="请选择状态">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24">
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="modelData.remark" :rows="2" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { FormRules } from 'element-plus'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const modelData = defineModel<any>()
+
+const formRef = ref() // 表单 Ref
+const formRules = reactive<FormRules>({
+  code: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+})
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+defineExpose({
+  validate
+})
+</script>

+ 49 - 0
src/views/ai/workflow/form/WorkflowDesign.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="relative" style="width: 100%; height: 700px">
+    <Tinyflow
+      v-if="workflowData"
+      ref="tinyflowRef"
+      :className="'custom-class'"
+      :style="{ width: '100%', height: '100%' }"
+      :data="workflowData"
+      :provider="provider"
+    />
+    <div class="absolute top-30px right-30px">
+      <el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
+        测试
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
+
+defineProps<{
+  provider: any
+}>()
+
+const tinyflowRef = ref()
+const workflowData = inject('workflowData') as Ref
+
+const testWorkflowModel = () => {
+  // TODO @lesan 测试
+}
+
+/** 表单校验 */
+const validate = async () => {
+  try {
+    // 获取最新的流程数据
+    if (!workflowData.value) {
+      throw new Error('请设计流程')
+    }
+    workflowData.value = tinyflowRef.value.getData()
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+defineExpose({
+  validate
+})
+</script>

+ 235 - 0
src/views/ai/workflow/form/index.vue

@@ -0,0 +1,235 @@
+<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 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" ref="basicInfoRef" />
+        </div>
+
+        <!-- 第二步:工作流设计 -->
+        <WorkflowDesign
+          v-if="currentStep === 1"
+          v-model="formData"
+          :provider="provider"
+          ref="workflowDesignRef"
+        />
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as WorkflowApi from '@/api/ai/workflow'
+import BasicInfo from './BasicInfo.vue'
+import WorkflowDesign from './WorkflowDesign.vue'
+import { ApiKeyApi } from '@/api/ai/model/apiKey'
+
+const router = useRouter()
+const { delView } = useTagsViewStore()
+const route = useRoute()
+const message = useMessage()
+
+const basicInfoRef = ref()
+const workflowDesignRef = ref()
+
+const validateBasic = async () => {
+  await basicInfoRef.value?.validate()
+}
+const validateWorkflow = async () => {
+  await workflowDesignRef.value?.validate()
+}
+
+const currentStep = ref(-1)
+const steps = [
+  { title: '基本信息', validator: validateBasic },
+  { title: '工作流设计', validator: validateWorkflow }
+]
+
+const formData: any = ref({
+  id: undefined,
+  name: '',
+  code: '',
+  remark: '',
+  graph: '',
+  status: CommonStatusEnum.ENABLE
+})
+// TODO @lesan:待接入
+const provider = ref<any>()
+const workflowData = ref<any>({})
+provide('workflowData', workflowData)
+
+/** 初始化数据 */
+const actionType = route.params.type as string
+const initData = async () => {
+  if (actionType === 'update') {
+    const workflowId = route.params.id as string
+    formData.value = await WorkflowApi.getWorkflow(workflowId)
+    workflowData.value = JSON.parse(formData.value.graph)
+  }
+
+  const apiKeys = await ApiKeyApi.getApiKeySimpleList()
+  provider.value = {
+    llm: () =>
+      apiKeys.map(({ id, name }) => ({
+        value: id,
+        label: name
+      })),
+    knowledge: () => [],
+    internal: () => []
+  }
+
+  currentStep.value = 0
+}
+
+/** 校验所有步骤数据是否完整 */
+const validateAllSteps = async () => {
+  try {
+    // 基本信息校验
+    try {
+      await validateBasic()
+    } catch (error) {
+      currentStep.value = 0
+      throw new Error('请完善基本信息')
+    }
+
+    // 工作流设计校验
+    try {
+      await validateWorkflow()
+    } catch (error) {
+      currentStep.value = 1
+      throw new Error('请完善工作流信息')
+    }
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  try {
+    // 保存前校验所有步骤的数据
+    await validateAllSteps()
+
+    // 更新表单数据
+    const data = {
+      ...formData.value
+    }
+
+    data.graph = JSON.stringify(workflowData.value)
+
+    if (actionType === 'update') {
+      await WorkflowApi.updateWorkflow(data)
+    } else {
+      await WorkflowApi.createWorkflow(data)
+    }
+
+    delView(unref(router.currentRoute))
+    await router.push({ name: 'AiWorkflow' })
+  } catch (error: any) {
+    console.error('保存失败:', error)
+    message.warning(error.message || '请完善所有步骤的必填信息')
+  }
+}
+
+/** 步骤切换处理 */
+const handleStepClick = async (index: number) => {
+  try {
+    if (index !== 0) {
+      await validateBasic()
+    }
+    if (index !== 1) {
+      await validateWorkflow()
+    }
+
+    // 切换步骤
+    currentStep.value = index
+  } catch (error) {
+    console.error('步骤切换失败:', error)
+    message.warning('请先完善当前步骤必填信息')
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'AiWorkflow' })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+</script>
+
+<!-- TODO @lesan:可以用 cursor 搞成 unocss 哈 -->
+<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>

+ 28 - 34
src/views/ai/workflow/manager/index.vue → src/views/ai/workflow/index.vue

@@ -1,4 +1,3 @@
-<!-- TODO @lesan:要不直接放到 workflow 根目录 -->
 <template>
   <!-- 搜索工作栏 -->
   <ContentWrap>
@@ -9,9 +8,9 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="流程标识" prop="definitionKey">
+      <el-form-item label="流程标识" prop="code">
         <el-input
-          v-model="queryParams.definitionKey"
+          v-model="queryParams.code"
           placeholder="请输入流程标识"
           clearable
           @keyup.enter="handleQuery"
@@ -27,6 +26,16 @@
           class="!w-240px"
         />
       </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -56,22 +65,22 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" :show-overflow-tooltip="true" />
-      <el-table-column
-        label="流程标识"
-        align="center"
-        prop="definitionKey"
-        :show-overflow-tooltip="true"
-      />
-      <el-table-column label="流程名称" align="center" prop="name" :show-overflow-tooltip="true" />
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="流程标识" align="center" prop="code" />
+      <el-table-column label="流程名称" align="center" prop="name" />
       <el-table-column
         label="创建时间"
         align="center"
         prop="createTime"
         :formatter="dateFormatter"
-        width="180"
       />
-      <el-table-column label="操作" align="center" width="220" fixed="right">
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="状态" align="center" key="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right">
         <template #default="scope">
           <el-button
             type="primary"
@@ -81,14 +90,6 @@
           >
             修改
           </el-button>
-          <el-button
-            type="primary"
-            link
-            @click="openModelForm('update', scope.row.id)"
-            v-hasPermi="['ai:workflow:update']"
-          >
-            流程图
-          </el-button>
           <el-button
             link
             type="danger"
@@ -110,16 +111,14 @@
   </ContentWrap>
 
   <!-- 添加或修改工作流对话框 -->
-  <WorkflowForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as WorkflowApi from '@/api/ai/workflow'
 import { dateFormatter } from '@/utils/formatTime'
-import WorkflowForm from './WorkflowForm.vue'
 
-/** AI 绘画 列表 */
-defineOptions({ name: 'AiWorkflowManager' })
+defineOptions({ name: 'AiWorkflow' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -131,8 +130,9 @@ const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  definitionKey: '',
+  code: '',
   name: '',
+  status: undefined,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
@@ -175,13 +175,7 @@ const handleDelete = async (id: number) => {
 }
 
 /** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 修改流程模型弹窗 */
-const openModelForm = async (type: string, id?: number) => {
+const openForm = async (type: string, id?: number) => {
   if (type === 'create') {
     await push({ name: 'AiWorkflowCreate' })
   } else {
@@ -193,7 +187,7 @@ const openModelForm = async (type: string, id?: number) => {
 }
 
 /** 初始化 **/
-onMounted(async () => {
+onMounted(() => {
   getList()
 })
 </script>

+ 0 - 106
src/views/ai/workflow/manager/WorkflowForm.vue

@@ -1,106 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px"
-    >
-      <el-row>
-        <el-col :span="24">
-          <el-form-item label="流程标识" prop="definitionKey">
-            <el-input v-model="formData.definitionKey" placeholder="请输入流程标识" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="24">
-          <el-form-item label="流程名称" prop="name">
-            <el-input v-model="formData.name" placeholder="请输入流程名称" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import * as WorkflowApi from '@/api/ai/workflow'
-import { FormRules } from 'element-plus'
-
-defineOptions({ name: 'AiWorkflowForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  definitionKey: '',
-  name: ''
-})
-const formRules = reactive<FormRules>({
-  definitionKey: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await WorkflowApi.getWorkflow(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value
-    if (formType.value === 'create') {
-      await WorkflowApi.createWorkflow(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await WorkflowApi.updateWorkflow(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    definitionKey: '',
-    name: ''
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 0 - 83
src/views/ai/workflow/manager/WorkflowModelForm.vue

@@ -1,83 +0,0 @@
-<!-- TODO @lesan:要不叫搞个 design 单独一个路由 -->
-<template>
-  <div style="width: 100%; height: calc(100vh - 160px)">
-    <Tinyflow
-      ref="tinyflowRef"
-      :className="'custom-class'"
-      :style="{ width: '100%', height: '100%' }"
-      v-if="initialData"
-      :data="initialData"
-      :provider="provider"
-    />
-  </div>
-  <div class="absolute top-30px right-30px">
-    <el-button @click="updateWorkflowModel" type="primary" v-hasPermi="['ai:workflow:update']">
-      保存
-    </el-button>
-    <el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
-      测试
-    </el-button>
-  </div>
-</template>
-
-<script setup lang="ts">
-import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
-import * as WorkflowApi from '@/api/ai/workflow'
-import { ApiKeyApi } from '@/api/ai/model/apiKey'
-
-const route = useRoute()
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
-const tinyflowRef = ref()
-// TODO @lesan:待接入
-const provider = ref({ llm: () => [], knowledge: () => [], internal: () => [] })
-const initialData = ref()
-
-const loadData = async () => {
-  try {
-    const [apiKeys, flowData] = await Promise.all([
-      ApiKeyApi.getApiKeySimpleList(),
-      WorkflowApi.getWorkflow(route.params.id)
-    ])
-
-    // 更新 provider
-    provider.value = {
-      llm: () =>
-        apiKeys.map(({ id, name }) => ({
-          value: id,
-          label: name
-        })),
-      knowledge: () => [],
-      internal: () => []
-    }
-
-    // 更新流程图数据
-    initialData.value = JSON.parse(flowData.model)
-  } catch {}
-}
-
-const updateWorkflowModel = async () => {
-  try {
-    const model = tinyflowRef.value.getData()
-    const data = {
-      model: JSON.stringify(model),
-      id: route.params.id
-    }
-    await message.confirm('确认保存流程模型?')
-    await WorkflowApi.updateWorkflowModel(data)
-    message.success(t('common.updateSuccess'))
-    await loadData()
-  } catch {}
-}
-
-const testWorkflowModel = () => {
-  // TODO @lesan 测试
-}
-
-watchEffect(() => {
-  if (route.params.id) {
-    loadData()
-  }
-})
-</script>

+ 14 - 1
src/views/bpm/model/CategoryDraggableModel.vue

@@ -97,10 +97,23 @@
         </el-table-column>
         <el-table-column label="可见范围" prop="startUserIds" min-width="150">
           <template #default="{ row }">
-            <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+            <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 全部可见 </el-text>
             <el-text v-else-if="row.startUsers.length === 1">
               {{ row.startUsers[0].nickname }}
             </el-text>
+            <el-text v-else-if="row.startDepts?.length === 1">
+              {{ row.startDepts[0].name }}
+            </el-text>
+            <el-text v-else-if="row.startDepts?.length > 1">
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="row.startDepts.map((dept: any) => dept.name).join('、')"
+              >
+                {{ row.startDepts[0].name }}等 {{ row.startDepts.length }} 个部门可见
+              </el-tooltip>
+            </el-text>
             <el-text v-else>
               <el-tooltip
                 class="box-item"

+ 73 - 1
src/views/bpm/model/form/BasicInfo.vue

@@ -77,6 +77,7 @@
       >
         <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
@@ -99,6 +100,24 @@
           <Icon icon="ep:plus" /> 选择人员
         </el-button>
       </div>
+      <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="dept in selectedStartDepts" 
+          :key="dept.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <Icon icon="ep:office-building" class="!m-5px text-20px" />
+          {{ dept.name }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartDept(dept)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartDeptSelect">
+          <Icon icon="ep:plus" /> 选择部门
+        </el-button>
+      </div>
     </el-form-item>
     <el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
       <div class="flex flex-wrap gap-2">
@@ -127,11 +146,19 @@
 
   <!-- 用户选择弹窗 -->
   <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+  <!-- 部门选择弹窗 -->
+  <DeptSelectForm
+    ref="deptSelectFormRef"
+    :multiple="true"
+    :check-strictly="true"
+    @confirm="handleDeptSelectConfirm"
+  />
 </template>
 
 <script lang="ts" setup>
 import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { UserVO } from '@/api/system/user'
+import { DeptVO } from '@/api/system/dept'
 import { CategoryVO } from '@/api/bpm/category'
 
 const props = defineProps({
@@ -142,13 +169,19 @@ const props = defineProps({
   userList: {
     type: Array,
     required: true
+  },
+  deptList: {
+    type: Array,
+    required: true
   }
 })
 
 const formRef = ref()
 const selectedStartUsers = ref<UserVO[]>([])
+const selectedStartDepts = ref<DeptVO[]>([])
 const selectedManagerUsers = ref<UserVO[]>([])
 const userSelectFormRef = ref()
+const deptSelectFormRef = ref()
 const currentSelectType = ref<'start' | 'manager'>('start')
 
 const rules = {
@@ -174,6 +207,13 @@ watch(
     } else {
       selectedStartUsers.value = []
     }
+    if (newVal.startDeptIds?.length) {
+      selectedStartDepts.value = props.deptList.filter((dept: DeptVO) =>
+        newVal.startDeptIds.includes(dept.id)
+      ) as DeptVO[]
+    } else {
+      selectedStartDepts.value = []
+    }
     if (newVal.managerUserIds?.length) {
       selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
         newVal.managerUserIds.includes(user.id)
@@ -193,6 +233,11 @@ const openStartUserSelect = () => {
   userSelectFormRef.value.open(0, selectedStartUsers.value)
 }
 
+/** 打开部门选择 */
+const openStartDeptSelect = () => {
+  deptSelectFormRef.value.open(selectedStartDepts.value)
+}
+
 /** 打开管理员选择 */
 const openManagerUserSelect = () => {
   currentSelectType.value = 'manager'
@@ -214,9 +259,28 @@ const handleUserSelectConfirm = (_, users: UserVO[]) => {
   }
 }
 
+/** 处理部门选择确认 */
+const handleDeptSelectConfirm = (depts: DeptVO[]) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: depts.map((d) => d.id)
+  }
+}
+
 /** 处理发起人类型变化 */
 const handleStartUserTypeChange = (value: number) => {
-  if (value !== 1) {
+  if (value === 0) {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: [],
+      startDeptIds: []
+    }
+  } else if (value === 1) {
+    modelData.value = {
+      ...modelData.value,
+      startDeptIds: []
+    }
+  } else if (value === 2) {
     modelData.value = {
       ...modelData.value,
       startUserIds: []
@@ -232,6 +296,14 @@ const handleRemoveStartUser = (user: UserVO) => {
   }
 }
 
+/** 移除部门 */
+const handleRemoveStartDept = (dept: DeptVO) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: modelData.value.startDeptIds.filter((id: number) => id !== dept.id)
+  }
+}
+
 /** 移除管理员 */
 const handleRemoveManagerUser = (user: UserVO) => {
   modelData.value = {

+ 81 - 5
src/views/bpm/model/form/ExtraSettings.vue

@@ -148,7 +148,7 @@
         <div class="flex">
           <el-switch
             v-model="processBeforeTriggerEnable"
-            @change="handlePreProcessNotifyEnableChange"
+            @change="handleProcessBeforeTriggerEnableChange"
           />
           <div class="ml-80px">流程启动后通知</div>
         </div>
@@ -168,9 +168,9 @@
         <div class="flex">
           <el-switch
             v-model="processAfterTriggerEnable"
-            @change="handlePostProcessNotifyEnableChange"
+            @change="handleProcessAfterTriggerEnableChange"
           />
-          <div class="ml-80px">流程启动后通知</div>
+          <div class="ml-80px">流程结束后通知</div>
         </div>
         <HttpRequestSetting
           v-if="processAfterTriggerEnable"
@@ -180,6 +180,46 @@
         />
       </div>
     </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskBeforeTriggerEnable"
+            @change="handleTaskBeforeTriggerEnableChange"
+          />
+          <div class="ml-80px">任务执行时通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskBeforeTriggerEnable"
+          v-model:setting="modelData.taskBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskAfterTriggerEnable"
+            @change="handleTaskAfterTriggerEnableChange"
+          />
+          <div class="ml-80px">任务结束后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskAfterTriggerEnable"
+          v-model:setting="modelData.taskAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
   </el-form>
 </template>
 
@@ -248,7 +288,7 @@ const numberExample = computed(() => {
 
 /** 是否开启流程前置通知 */
 const processBeforeTriggerEnable = ref(false)
-const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessBeforeTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processBeforeTriggerSetting = {
       url: '',
@@ -263,7 +303,7 @@ const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
 
 /** 是否开启流程后置通知 */
 const processAfterTriggerEnable = ref(false)
-const handlePostProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessAfterTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processAfterTriggerSetting = {
       url: '',
@@ -276,6 +316,36 @@ const handlePostProcessNotifyEnableChange = (val: boolean | string | number) =>
   }
 }
 
+/** 是否开启任务前置通知 */
+const taskBeforeTriggerEnable = ref(false)
+const handleTaskBeforeTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启任务后置通知 */
+const taskAfterTriggerEnable = ref(false)
+const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskAfterTriggerSetting = null
+  }
+}
+
 /** 表单选项 */
 const formField = ref<Array<{ field: string; title: string }>>([])
 const formFieldOptions4Title = computed(() => {
@@ -341,6 +411,12 @@ const initData = () => {
   if (modelData.value.processAfterTriggerSetting) {
     processAfterTriggerEnable.value = true
   }
+  if (modelData.value.taskBeforeTriggerSetting) {
+    taskBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.taskAfterTriggerSetting) {
+    taskAfterTriggerEnable.value = true
+  }
 }
 defineExpose({ initData })
 

+ 1 - 0
src/views/bpm/model/form/ProcessDesign.vue

@@ -18,6 +18,7 @@
       :model-key="modelData.key"
       :model-name="modelData.name"
       :start-user-ids="modelData.startUserIds"
+      :start-dept-ids="modelData.startDeptIds"
       @success="handleDesignSuccess"
     />
   </template>

+ 12 - 3
src/views/bpm/model/form/index.vue

@@ -62,6 +62,7 @@
             v-model="formData"
             :categoryList="categoryList"
             :userList="userList"
+            :deptList="deptList"
             ref="basicInfoRef"
           />
         </div>
@@ -92,6 +93,7 @@ import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 import * as DefinitionApi from '@/api/bpm/definition'
 import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
 import BasicInfo from './BasicInfo.vue'
@@ -153,6 +155,7 @@ const formData: any = ref({
   visible: true,
   startUserType: undefined,
   startUserIds: [],
+  startDeptIds: [],
   managerUserIds: [],
   allowCancelRunningProcess: true,
   processIdRule: {
@@ -183,6 +186,7 @@ provide('modelData', formData)
 const formList = ref([])
 const categoryList = ref<CategoryVO[]>([])
 const userList = ref<UserApi.UserVO[]>([])
+const deptList = ref<DeptApi.DeptVO[]>([])
 
 /** 初始化数据 */
 const actionType = route.params.type as string
@@ -200,14 +204,17 @@ const initData = async () => {
       data.simpleModel = JSON.parse(data.simpleModel)
     }
     formData.value = data
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
   } else if (['update', 'copy'].includes(actionType)) {
     // 情况二:修改场景/复制场景
     const modelId = route.params.id as string
     formData.value = await ModelApi.getModel(modelId)
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+
     // 特殊:复制场景
-    if (actionType === 'copy') {
+    if (route.params.type === 'copy') {
       delete formData.value.id
       formData.value.name += '副本'
       formData.value.key += '_copy'
@@ -225,6 +232,8 @@ const initData = async () => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
   // 获取用户列表
   userList.value = await UserApi.getSimpleUserList()
+  // 获取部门列表
+  deptList.value = await DeptApi.getSimpleDeptList()
 
   // 最终,设置 currentStep 切换到第一步
   currentStep.value = 0

+ 152 - 85
src/views/bpm/oa/leave/create.vue

@@ -1,83 +1,78 @@
 <template>
-  <el-form
-    ref="formRef"
-    v-loading="formLoading"
-    :model="formData"
-    :rules="formRules"
-    label-width="80px"
-  >
-    <el-form-item label="请假类型" prop="type">
-      <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
-        <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="开始时间" prop="startTime">
-      <el-date-picker
-        v-model="formData.startTime"
-        clearable
-        placeholder="请选择开始时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="结束时间" prop="endTime">
-      <el-date-picker
-        v-model="formData.endTime"
-        clearable
-        placeholder="请选择结束时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="原因" prop="reason">
-      <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
-    </el-form-item>
-    <el-col v-if="startUserSelectTasks.length > 0">
-      <el-card class="mb-10px">
-        <template #header>指定审批人</template>
+  <el-row :gutter="20">
+    <el-col :span="16">
+      <ContentWrap title="申请信息">
         <el-form
-          :model="startUserSelectAssignees"
-          :rules="startUserSelectAssigneesFormRules"
-          ref="startUserSelectAssigneesFormRef"
+          ref="formRef"
+          v-loading="formLoading"
+          :model="formData"
+          :rules="formRules"
+          label-width="80px"
         >
-          <el-form-item
-            v-for="userTask in startUserSelectTasks"
-            :key="userTask.id"
-            :label="`任务【${userTask.name}】`"
-            :prop="userTask.id"
-          >
-            <el-select
-              v-model="startUserSelectAssignees[userTask.id]"
-              multiple
-              placeholder="请选择审批人"
-            >
+          <el-form-item label="请假类型" prop="type">
+            <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
               <el-option
-                v-for="user in userList"
-                :key="user.id"
-                :label="user.nickname"
-                :value="user.id"
+                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
               />
             </el-select>
           </el-form-item>
+          <el-form-item label="开始时间" prop="startTime">
+            <el-date-picker
+              v-model="formData.startTime"
+              clearable
+              placeholder="请选择开始时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="结束时间" prop="endTime">
+            <el-date-picker
+              v-model="formData.endTime"
+              clearable
+              placeholder="请选择结束时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="原因" prop="reason">
+            <el-input v-model="formData.reason" placeholder="请输入请假原因" type="textarea" />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="submitForm">
+              确 定
+            </el-button>
+          </el-form-item>
         </el-form>
-      </el-card>
+      </ContentWrap>
     </el-col>
-    <el-form-item>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-    </el-form-item>
-  </el-form>
+
+    <!-- 审批相关:流程信息 -->
+    <el-col :span="8">
+      <ContentWrap title="审批流程" :bodyStyle="{ padding: '0 20px 0' }">
+        <ProcessInstanceTimeline
+          ref="timelineRef"
+          :activity-nodes="activityNodes"
+          :show-status-icon="false"
+          @select-user-confirm="selectUserConfirm"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as LeaveApi from '@/api/bpm/leave'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 审批相关:import
 import * as DefinitionApi from '@/api/bpm/definition'
-import * as UserApi from '@/api/system/user'
+import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
 
 defineOptions({ name: 'BpmOALeaveCreate' })
 
@@ -100,30 +95,37 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 
-// 指定审批人
+// 审批相关:变量
 const processDefineKey = 'oa_leave' // 流程定义 Key
 const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
 const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+const processDefinitionId = ref('')
 
 /** 提交表单 */
 const submitForm = async () => {
-  // 校验表单
+  // 1.1 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
-  // 校验指定审批人
+  // 1.2 审批相关:校验指定审批人
   if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      ) {
+        return message.warning(`请选择${userTask.name}的审批人`)
+      }
+    }
   }
 
-  // 提交请求
+  // 2. 提交请求
   formLoading.value = true
   try {
     const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
-    // 设置指定审批人
+    // 审批相关:设置指定审批人
     if (startUserSelectTasks.value?.length > 0) {
       data.startUserSelectAssignees = startUserSelectAssignees.value
     }
@@ -137,28 +139,93 @@ const submitForm = async () => {
   }
 }
 
+/** 审批相关:获取审批详情 */
+const getApprovalDetail = async () => {
+  try {
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: processDefinitionId.value,
+      // TODO 小北:可以支持 processDefinitionKey 查询
+      activityId: NodeId.START_USER_NODE_ID,
+      processVariablesStr: JSON.stringify({ day: daysDifference() }) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+    })
+
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        if (
+          tempStartUserSelectAssignees.value[node.id] &&
+          tempStartUserSelectAssignees.value[node.id].length > 0
+        ) {
+          startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+        } else {
+          startUserSelectAssignees.value[node.id] = []
+        }
+      }
+    }
+  } finally {
+  }
+}
+
+/** 审批相关:选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+// 计算天数差
+// TODO @小北:可以搞到 formatTime 里面去,然后看看 dayjs 里面有没有现成的方法,或者辅助计算的方法。
+const daysDifference = () => {
+  const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
+  const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
+  return Math.floor(diffTime / oneDay)
+}
+
 /** 初始化 */
 onMounted(async () => {
+  // TODO @小北:这里可以简化,统一通过 getApprovalDetail 处理么?
   const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
     undefined,
     processDefineKey
   )
+
   if (!processDefinitionDetail) {
     message.error('OA 请假的流程模型未配置,请检查!')
     return
   }
+  processDefinitionId.value = processDefinitionDetail.id
   startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
-  // 设置指定审批人
-  if (startUserSelectTasks.value?.length > 0) {
-    // 设置校验规则
-    for (const userTask of startUserSelectTasks.value) {
-      startUserSelectAssignees.value[userTask.id] = []
-      startUserSelectAssigneesFormRules.value[userTask.id] = [
-        { required: true, message: '请选择审批人', trigger: 'blur' }
-      ]
+
+  // 审批相关:加载最新的审批详情,主要用于节点预测
+  await getApprovalDetail()
+})
+
+/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
+watch(
+  formData.value,
+  (newValue, oldValue) => {
+    if (!oldValue) {
+      return
     }
-    // 加载用户列表
-    userList.value = await UserApi.getSimpleUserList()
+    if (newValue && Object.keys(newValue).length > 0) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情,主要用于节点预测
+      getApprovalDetail()
+    }
+  },
+  {
+    immediate: true
   }
-})
+)
 </script>

+ 17 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -37,6 +37,11 @@
             {{ getApprovalNodeTime(activity) }}
           </div>
         </div>
+        <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
+          <el-button type="primary" plain size="small" @click="handleChildProcess(activity)">
+            查看子流程
+          </el-button>
+        </div>
         <!-- 需要自定义选择审批人 -->
         <div
           class="flex flex-wrap gap2 items-center"
@@ -194,6 +199,7 @@ withDefaults(
     showStatusIcon: true // 默认值为 true
   }
 )
+const { push } = useRouter() // 路由
 
 // 审批节点
 const statusIconMap2 = {
@@ -310,4 +316,15 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
   customApproveUsers.value[activityId] = userList || []
   emit('selectUserConfirm', activityId, userList)
 }
+
+/** 跳转子流程 */
+const handleChildProcess = (activity: any) => {
+  // TODO @lesan:貌似跳不过去?!
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: activity.processInstanceId
+    }
+  })
+}
 </script>

+ 2 - 0
src/views/bpm/simple/SimpleModelDesign.vue

@@ -6,6 +6,7 @@
       :model-name="modelName"
       @success="handleSuccess"
       :start-user-ids="startUserIds"
+      :start-dept-ids="startDeptIds"
       ref="designerRef"
     />
   </ContentWrap>
@@ -22,6 +23,7 @@ defineProps<{
   modelKey?: string
   modelName?: string
   startUserIds?: number[]
+  startDeptIds?: number[]
 }>()
 
 const emit = defineEmits(['success'])