Pārlūkot izejas kodu

添加审批流模块组件

jzy 1 gadu atpakaļ
vecāks
revīzija
9070b5b758

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
     "vue": "^3.2.45",
     "vue-cropper": "^1.0.5",
     "vue-router": "^4.1.6",
+    "vuedraggable": "^4.1.0",
     "vxe-table": "^4.3.9",
     "xe-utils": "^3.5.7"
   },

+ 6 - 0
src/api/employees.ts

@@ -0,0 +1,6 @@
+import request from '@/utils/request'
+
+// 获取人员列表
+export function getUserList(data: any) {
+  return request.post('/sys/user/list', data)
+}

+ 191 - 0
src/api/workflow.ts

@@ -0,0 +1,191 @@
+import request from '@/utils/request'
+//新增表单
+export function addForm(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      workflowId: '7a4f8b64eb79492894a1cdcc303ff4a0',
+      errCause: '',
+      errCode: 0,
+      msg: '创建工作流自定义表单成功',
+      success: true
+    })
+  })
+}
+//修改表单
+export function putForm(data?: any) {
+  return new Promise(resolve => {
+    resolve({
+      workflowId: '7a4f8b64eb79492894a1cdcc303ff4a0',
+      errCause: '',
+      errCode: 0,
+      msg: '创建工作流自定义表单成功',
+      success: true
+    })
+  })
+}
+//根据流程执行id查询流程详情
+export function getExecuteFlow(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      flowExpr:
+        '{"tableId":1,"workFlowDef":{"name":""},"directorMaxLevel":4,"flowPermission":[],"nodeConfig":{"nodeName":"流程发起","nodeType":0,"priorityLevel":"","approvalType":"","subjects":[],"copyTo":[],"settype":"","selectRange":"","directorLevel":"","examineMode":"","noHanderAction":"","examineEndDirectorLevel":"","ccSelfSelectFlag":"","condGroup":{"type":"and","items":[]},"nodeUserList":[],"childNode":{"nodeName":"经理会签","error":false,"nodeType":1,"settype":1,"selectMode":1,"selectRange":0,"directorLevel":1,"examineMode":1,"noHanderAction":1,"examineEndDirectorLevel":0,"childNode":{"nodeName":"抄送人","nodeType":4,"ccSelfSelectFlag":1,"copyTo":[{"type":3,"subjectId":"fcf93841f7b4438f889dd2fdbba6a135","subjectName":"崔超"}],"childNode":{},"subjects":[],"error":false},"subjects":[{"type":3,"subjectId":"b5a619c86201443c972e5e20715470bb","subjectName":"吉宇晟"},{"type":3,"subjectId":"fcf93841f7b4438f889dd2fdbba6a135","subjectName":"崔超"}],"approvalType":"and"},"conditionNodes":[]}}',
+      msg: '获取流程成功',
+      success: true
+    })
+  })
+}
+//查询表单
+export function getWorkflows(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      data: {
+        pageNo: 1,
+        totalCount: 1,
+        workflows: [
+          {
+            createTime: '2023-03-22 13:43',
+            id: '6aa8c6ded96d4689a05f97df66703cfd',
+            name: '最新测试',
+            state: 2,
+            stateRemark: '正常'
+          }
+        ]
+      },
+      errCause: '',
+      errCode: 0,
+      msg: '获取审批流成功',
+      success: true
+    })
+  })
+}
+//查询表单详情
+export function getForm(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      form: {
+        formExpr:
+          '{"props":{"labelWidth":100,"labelPosition":"top","size":"large"},"formItems":[{"id":"8MC6Mpt5","label":"存款","value":"","name":"input-number_8MC6Mpt5","type":"input-number","children":[],"options":[],"search":false,"list":true,"required":false,"rules":[],"props":{},"span":24},{"id":"WqGBzMmx","label":"金额","value":"","name":"input_WqGBzMmx","type":"input","children":[],"options":[],"search":false,"list":true,"required":false,"rules":[],"props":{},"span":24},{"id":"AFUov14b","label":"名称","value":"","name":"input_AFUov14b","type":"input","children":[],"options":[],"search":false,"list":true,"required":false,"rules":[],"props":{},"span":24}],"span":24}',
+        version: 1,
+        workflowId: '6aa8c6ded96d4689a05f97df66703cfd'
+      },
+      msg: '创建工作流自定义流程成功',
+      success: true
+    })
+  })
+}
+
+//新增流程
+export function addFlow(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      msg: '创建工作流自定义流程成功',
+      success: true
+    })
+  })
+}
+//修改流程
+export function putFlow(data?: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      msg: '创建工作流自定义流程成功',
+      success: true
+    })
+  })
+}
+//删除流程
+export function delFlow(data?: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      msg: '创建工作流自定义流程成功',
+      success: true
+    })
+  })
+}
+//查询流程详情
+export function getFlow(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      errCause: '',
+      errCode: 0,
+      flowExpr:
+        '{"tableId":1,"workFlowDef":{"name":""},"directorMaxLevel":4,"flowPermission":[],"nodeConfig":{"nodeName":"流程发起","nodeType":0,"priorityLevel":"","approvalType":"","subjects":[],"copyTo":[],"settype":"","selectRange":"","directorLevel":"","examineMode":"","noHanderAction":"","examineEndDirectorLevel":"","ccSelfSelectFlag":"","condGroup":{"type":"and","items":[]},"nodeUserList":[],"childNode":{"nodeName":"经理会签","error":false,"nodeType":1,"settype":1,"selectMode":1,"selectRange":0,"directorLevel":1,"examineMode":1,"noHanderAction":1,"examineEndDirectorLevel":0,"childNode":{"nodeName":"抄送人","nodeType":4,"ccSelfSelectFlag":1,"copyTo":[{"type":3,"subjectId":"fcf93841f7b4438f889dd2fdbba6a135","subjectName":"崔超"}],"childNode":{},"subjects":[],"error":false},"subjects":[{"type":3,"subjectId":"b5a619c86201443c972e5e20715470bb","subjectName":"吉宇晟"},{"type":3,"subjectId":"fcf93841f7b4438f889dd2fdbba6a135","subjectName":"崔超"}],"approvalType":"and"},"conditionNodes":[]}}',
+      msg: '获取流程成功',
+      success: true
+    })
+  })
+}
+
+//创建流程查询表单字段
+export function getFormField(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      fields: [
+        {
+          fieldIdentity: 'input_1N5KzwbC',
+          fieldName: '文本输入'
+        },
+        {
+          fieldIdentity: 'input_klQH1Qb0',
+          fieldName: '文本输入'
+        }
+      ],
+      errCause: '',
+      errCode: 0,
+      msg: '获取表单字段成功',
+      success: true
+    })
+  })
+}
+//创建流程查询表单字段
+export function getExecuteFormField(data: any) {
+  return new Promise(resolve => {
+    resolve({
+      fields: [
+        {
+          fieldIdentity: 'input_1N5KzwbC',
+          fieldName: '文本输入'
+        },
+        {
+          fieldIdentity: 'input_klQH1Qb0',
+          fieldName: '文本输入'
+        }
+      ],
+      errCause: '',
+      errCode: 0,
+      msg: '获取表单字段成功',
+      success: true
+    })
+  })
+}
+
+//查询人员列表
+export function getTangent() {
+  return new Promise(resolve => {
+    resolve({
+      infos: [
+        {
+          id: '2d53723d31424e199e765db602355ba6',
+          name: '杨立锐'
+        },
+        {
+          id: 'e8b3be5c33f2430abea14b73484e1c47',
+          name: '贾森岩'
+        }
+      ],
+      errCause: '',
+      errCode: 0,
+      msg: '获取用户成功',
+      success: true
+    })
+  })
+}

BIN
src/assets/images/iosBg.png


+ 18 - 0
src/components.d.ts

@@ -7,14 +7,28 @@ export {}
 
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
+    AddNode: typeof import('./components/workflow/addNode.vue')['default']
     AdvancedForm: typeof import('./components/form/AdvancedForm.vue')['default']
+    ApproverDrawer: typeof import('./components/workflow/drawer/approverDrawer.vue')['default']
     BasicForm: typeof import('./components/form/BasicForm.vue')['default']
+    ConditionDrawer: typeof import('./components/workflow/drawer/conditionDrawer.vue')['default']
+    CopyerDrawer: typeof import('./components/workflow/drawer/copyerDrawer.vue')['default']
     Cropper: typeof import('./components/avatar/cropper.vue')['default']
+    DesignerComp: typeof import('./components/designer/DesignerComp.vue')['default']
+    DesignerHeader: typeof import('./components/designer/DesignerHeader.vue')['default']
+    DesignerRender: typeof import('./components/designer/DesignerRender.vue')['default']
+    DesignerSetting: typeof import('./components/designer/DesignerSetting.vue')['default']
     DialogForm: typeof import('./components/form/DialogForm.vue')['default']
+    DynamicFormEdit: typeof import('./components/DynamicFormEdit.vue')['default']
     ElDict: typeof import('./components/ElDict.vue')['default']
     ElEditor: typeof import('./components/ElEditor.vue')['default']
+    ElEmployees: typeof import('./components/ElEmployees.vue')['default']
+    EmployeesDialog: typeof import('./components/workflow/dialog/employeesDialog.vue')['default']
+    EmployeesRoleDialog: typeof import('./components/workflow/dialog/employeesRoleDialog.vue')['default']
+    ErrorDialog: typeof import('./components/workflow/dialog/errorDialog.vue')['default']
     Exception: typeof import('./components/Exception.vue')['default']
     FormComp: typeof import('./components/form/FormComp.vue')['default']
+    FormDesigner: typeof import('./components/designer/FormDesigner.vue')['default']
     GlobalAside: typeof import('./components/GlobalAside.vue')['default']
     GlobalFooter: typeof import('./components/GlobalFooter.vue')['default']
     GlobalHeader: typeof import('./components/GlobalHeader.vue')['default']
@@ -23,6 +37,7 @@ declare module '@vue/runtime-core' {
     GlobalSubMenu: typeof import('./components/GlobalSubMenu.vue')['default']
     GlobalTabs: typeof import('./components/GlobalTabs.vue')['default']
     ImageUpload: typeof import('./components/ImageUpload.vue')['default']
+    NodeWrap: typeof import('./components/workflow/nodeWrap.vue')['default']
     OrgLayout: typeof import('./components/org/OrgLayout.vue')['default']
     OrgList: typeof import('./components/org/OrgList.vue')['default']
     PaneModel: typeof import('./components/splitpanes/PaneModel.vue')['default']
@@ -31,7 +46,10 @@ declare module '@vue/runtime-core' {
     ProTable: typeof import('./components/ProTable.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SelectBox: typeof import('./components/workflow/selectBox.vue')['default']
+    SelectResult: typeof import('./components/workflow/selectResult.vue')['default']
     SelIcon: typeof import('./components/SelIcon.vue')['default']
     SvgIcon: typeof import('./components/SvgIcon.vue')['default']
+    WorkflowEdit: typeof import('./components/workflow/WorkflowEdit.vue')['default']
   }
 }

+ 171 - 0
src/components/DynamicFormEdit.vue

@@ -0,0 +1,171 @@
+<script setup lang="ts">
+import type { FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { useFormDesignerStore } from '@/stores/designer'
+// import WorkflowEdit from '@/components/workflow/WorkflowEdit.vue'
+import { addForm, putForm } from '@/api/workflow'
+const WorkflowEdit = defineAsyncComponent(() => import('@/components/workflow/WorkflowEdit.vue'))
+const emits = defineEmits(['click-change'])
+const dialogFormVisible = ref<boolean>(false)
+const active = ref<number>(0)
+const ruleFormRef = ref<any>()
+let workflowEdit = ref<any>(null)
+let workflowState = ref<number>(1)
+const formDesignerStore = useFormDesignerStore()
+const formConfig = computed(() => {
+  return {
+    props: formDesignerStore.formProps,
+    formItems: formDesignerStore.formItems,
+    span: formDesignerStore.formSpan
+  }
+})
+let ruleForm = ref<any>({
+  workflowName: ''
+})
+const init = (row: any) => {
+  ruleForm.value.workflowName = row.name
+  ruleForm.value.id = row.id
+  workflowState.value = row.state
+  if (row.state == 1) {
+    nextStep2()
+  }
+
+  dialogFormVisible.value = true
+}
+const rules = reactive<FormRules>({
+  workflowName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }]
+})
+
+const nextSet = async (formEl: any | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid: any) => {
+    if (valid) {
+      active.value = 1
+    }
+  })
+}
+
+const nextStep2 = () => {
+  active.value = 2
+  setTimeout(() => {
+    workflowEdit.value.init({ state: workflowState.value, id: ruleForm.value.id })
+  }, 500)
+}
+
+const nextStep = (e: any) => {
+  if (formConfig.value.formItems.length != 0) {
+    if (ruleForm.value.id) {
+      putForm({
+        workflowName: ruleForm.value.workflowName,
+        workflowId: ruleForm.value.id,
+        formExpr: JSON.stringify(formConfig.value)
+      }).then((res: any) => {
+        ElMessage({
+          type: 'success',
+          message: res.msg
+        })
+        active.value = 2
+        setTimeout(() => {
+          workflowEdit.value.init({ state: workflowState.value, id: ruleForm.value.id })
+        }, 500)
+      })
+    } else {
+      addForm({
+        workflowName: ruleForm.value.workflowName,
+        formExpr: JSON.stringify(formConfig.value)
+      }).then((res: any) => {
+        ElMessage({
+          type: 'success',
+          message: res.msg
+        })
+        ruleForm.value.id = res.workflowId
+        active.value = 2
+        setTimeout(() => {
+          workflowEdit.value.init({ state: workflowState.value, id: res.workflowId })
+        }, 500)
+      })
+    }
+  } else {
+    ElMessage({
+      type: 'error',
+      message: '表单不能为空'
+    })
+  }
+}
+
+const previousStep = (e: any) => {
+  if (active.value == 1) {
+    active.value = 0
+  } else {
+    active.value = 1
+  }
+}
+
+const closeDialog = () => {
+  emits('click-change')
+  active.value = 0
+  ruleForm.value.workflowName = ''
+  formDesignerStore.$reset()
+}
+
+const handleCircle = () => {
+  dialogFormVisible.value = false
+}
+
+defineExpose({
+  init
+})
+</script>
+<template>
+  <el-dialog v-model="dialogFormVisible" title="新增" width="1200" @close="closeDialog">
+    <el-steps :active="active" finish-status="success" simple style="margin-top: 20px">
+      <el-step title="流程类型设置" />
+      <el-step title="流程表单设置" />
+      <el-step title="流程审批设置" />
+    </el-steps>
+    <div v-if="active == 0" class="w-6/12 m-auto mt-10">
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-position="right"
+        label-width="100px"
+        status-icon
+        size="large"
+      >
+        <el-form-item class="w-full items-center" label="类型:" prop="workflowName">
+          <el-input
+            :disabled="ruleForm.id == '' ? false : true"
+            v-model="ruleForm.workflowName"
+            placeholder="请输入流程类型名称"
+          />
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <form-designer v-else-if="active == 1"></form-designer>
+
+    <WorkflowEdit
+      v-else-if="active == 2"
+      ref="workflowEdit"
+      @clickChange="handleCircle"
+      @previousStep="previousStep"
+    ></WorkflowEdit>
+
+    <template #footer>
+      <div class="flex justify-end w-full">
+        <el-button size="default" @click="previousStep" v-if="active != 0 && active != 2">上一步</el-button>
+        <el-button size="default" type="primary" v-if="active == 0" @click="nextSet(ruleFormRef)">下一步</el-button>
+        <el-button size="default" type="primary" v-if="active == 1 && ruleForm.id" @click="nextStep2">下一步</el-button>
+        <el-button size="default" type="primary" v-if="active == 1" @click="nextStep">{{
+          ruleForm.id ? '确认修改并继续' : '下一步'
+        }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+<style lang="scss" scoped>
+.error-modal-list {
+  width: 455px;
+}
+</style>

+ 47 - 0
src/components/ElEmployees.vue

@@ -0,0 +1,47 @@
+<script setup lang="ts">
+import { getUserList } from '@/api/employees'
+
+interface Props {
+  modelValue: any
+  type: string
+}
+const props = defineProps<Props>()
+const emits = defineEmits(['update:modelValue'])
+const list = ref<any>([])
+const userList = ref<any>([])
+const data = ref<any>([])
+const selectValue = computed({
+  get: () => props.modelValue,
+  set: value => {
+    emits('update:modelValue', value)
+  }
+})
+const visibleDialog = ref<boolean>(false)
+const employeesoption = ref<any>(null)
+const handleTestTasks = (res: any) => {
+  employeesoption.value.blur()
+  visibleDialog.value = true
+}
+const saveDialog = () => {
+  selectValue.value = userList.value
+  visibleDialog.value = false
+}
+getUserList(props.type).then(res => {
+  data.value = res
+})
+</script>
+
+<template>
+  <el-select ref="employeesoption" v-model="selectValue" @focus="handleTestTasks">
+    <el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id">{{ item.name }} </el-option>
+  </el-select>
+  <el-dialog title="选择成员" v-model="visibleDialog" append-to-body>
+    <el-transfer v-model="userList" filterable :data="data" />
+    <template #footer>
+      <el-button @click="visibleDialog = false">取 消</el-button>
+      <el-button type="primary" @click="saveDialog">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped></style>

+ 102 - 0
src/components/designer/DesignerComp.vue

@@ -0,0 +1,102 @@
+<script lang="ts" setup>
+import draggable from 'vuedraggable'
+import { comps } from './constant'
+import { useFormDesignerStore } from '@/stores/designer'
+import { compToFormItem } from '@/utils/utils'
+
+const formDesignerStore = useFormDesignerStore()
+const handleClick = (comp: any) => {
+  const formItem = compToFormItem(comp)
+  formDesignerStore.setCurFormItem(formItem)
+  formDesignerStore.setFormItems(formItem)
+}
+const handleClone = (comp: any) => {
+  const formItem = compToFormItem(comp)
+  formDesignerStore.setCurFormItem(formItem)
+  // formDesignerStore.setFormItems(formItem)
+  return formItem
+}
+</script>
+
+<template>
+  <div class="designer-comp">
+    <div class="designer-comp-box" v-for="item in comps" :key="item.label">
+      <div class="designer-comp-title">{{ item.label }}</div>
+      <div class="">
+        <draggable
+          class="grid grid-gap-10px"
+          chosen-class="chosen"
+          v-model="item.options"
+          :clone="handleClone"
+          :group="{ name: 'comp', pull: 'clone', put: false }"
+          :sort="false"
+          item-key="name"
+        >
+          <template #item="{ element }">
+            <div class="designer-comp-item" :key="element.name" @click="handleClick(element)">
+              <component :is="element.icon" size="16px" />
+              <div class="ml-5px">{{ element.name }}</div>
+            </div>
+          </template>
+        </draggable>
+        <!-- <div class="designer-comp-item" v-for="comp in item.options" :key="comp.name" @click="handleClick(comp)">
+          <component :is="comp.icon" size="16px" />
+          <div class="ml-5px">{{ comp.name }}</div>
+        </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.designer {
+  &-comp {
+    background-color: #fff;
+    padding: 10px;
+    height: 100%;
+
+    &-title {
+      height: 40px;
+      line-height: 40px;
+      font-weight: 500;
+      margin-bottom: 0;
+    }
+
+    &-item {
+      display: flex;
+      align-items: center;
+      height: 40px;
+      border-radius: 4px;
+      background: #f7f7f7;
+      padding: 0 7px;
+      margin-bottom: 8px;
+      cursor: move;
+      font-size: 14px;
+
+      &:hover {
+        color: var(--el-color-primary);
+        outline: 1px dashed currentColor;
+      }
+    }
+  }
+}
+.grid {
+  grid-template-columns: 1fr 1fr;
+}
+.i-icon {
+  display: inline-block;
+  color: inherit;
+  font-style: normal;
+  line-height: 0;
+  text-align: center;
+  text-transform: none;
+  vertical-align: -0.125em;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.chosen {
+  color: var(--el-color-primary);
+  border: 1px dashed currentColor;
+}
+</style>

+ 60 - 0
src/components/designer/DesignerHeader.vue

@@ -0,0 +1,60 @@
+<script lang="ts" setup>
+import { useFormDesignerStore,designerType } from '@/stores/designer'
+const formDesignerStore = useFormDesignerStore()
+const designerHeaderType = designerType()
+const formConfig = computed(() => {
+  return {
+    props: formDesignerStore.formProps,
+    formItems: formDesignerStore.formItems,
+    span: formDesignerStore.formSpan
+  }
+})
+
+const dialogVisible = ref(false)
+
+const handleRemove = () => {
+  formDesignerStore.$reset()
+}
+const handlePreview = () => {
+  dialogVisible.value = true
+}
+const handleChanging =(e) =>{
+    designerHeaderType.type = e
+}
+
+const formData = computed(() => {
+  const res: any = {}
+  formConfig.value.formItems.forEach(item => {
+    res[item.name] = item.value
+  })
+  return res
+})
+const create = (data:any) =>{
+   
+}
+const update = () =>{
+
+}
+
+</script>
+
+<template>
+  <div class="h-full bg-white flex items-center justify-end px-10px">
+    <el-button plain round type="danger" @click="handleRemove">清除</el-button>
+    <el-button plain round type="primary" @click="handleChanging(1)">桌面端</el-button>
+    <el-button plain round type="primary" @click="handleChanging(0)">移动端</el-button>
+    <el-button plain round type="primary" @click="handlePreview">预览</el-button>
+  </div>
+  <dialog-form
+      v-model="dialogVisible"
+      :formConfig="formConfig"
+      :formData="formData"
+      :create="create"
+      :update="update"
+      v-if="dialogVisible"
+    >
+    </dialog-form>
+</template>
+
+<style lang="scss">
+</style>

+ 186 - 0
src/components/designer/DesignerRender.vue

@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import draggable from 'vuedraggable'
+import { useFormDesignerStore, designerType } from '@/stores/designer'
+import type { BasicFormItem } from '@/types/form'
+import { copyFormItem } from '@/utils/utils'
+
+const formDesignerStore = useFormDesignerStore()
+const { formProps, formSpan } = storeToRefs(formDesignerStore)
+const designerHeaderType = designerType()
+const curFormItem = computed(() => formDesignerStore.curFormItem)
+const formItems = computed({
+  get() {
+    return formDesignerStore.formItems
+  },
+  set(val) {
+    formDesignerStore.formItems = val
+  }
+})
+const formData = computed(() => {
+  const res: any = {}
+  formItems.value.forEach(item => {
+    res[item.name] = item.value
+  })
+  return res
+})
+
+const handleClick = (item: BasicFormItem) => {
+  formDesignerStore.setCurFormItem(item)
+}
+const handleCopy = (item: BasicFormItem) => {
+  const formItem = copyFormItem(item)
+  formDesignerStore.setCurFormItem(formItem)
+  formDesignerStore.setFormItems(formItem)
+}
+const handleDel = (item: BasicFormItem, index: number) => {
+  formDesignerStore.formItems.splice(index, 1)
+  if (item.id === curFormItem.value.id) {
+    const length = formItems.value.length
+    if (length) {
+      formDesignerStore.setCurFormItem(formItems.value[index < length ? index : length - 1])
+    } else {
+      formDesignerStore.curFormItem = {}
+    }
+  }
+}
+</script>
+<template>
+  <div class="h-full bg-white overflow-x-hidden overflow-y-auto p-16px">
+    <div class="ios-bg ml-15" style="height: " v-if="designerHeaderType.type == 0">
+      <el-form v-bind="formProps" class="h-full overflow-auto pt-px">
+        <draggable
+          class="content-start"
+          style="min-height: 100%"
+          tag="el-row"
+          v-bind="{ gutter: 20 }"
+          v-model="formItems"
+          handle=".form-handle"
+          group="comp"
+          item-key="id"
+        >
+          <template #item="{ element, index }">
+            <el-col :span="24" :key="element.id" class="mb-10px" style="margin-left: 1px; max-width: 92%">
+              <div
+                class="form-item border-dashed border-1 border-gray-200"
+                :class="{ active: curFormItem.id === element.id }"
+                @click="handleClick(element)"
+              >
+                <el-form-item :label="element.label" :rules="element.rules" :prop="element.name">
+                  <form-comp :item="element" v-model="formData[element.name]"></form-comp>
+                </el-form-item>
+                <div class="form-handle" v-if="curFormItem.id === element.id">
+                  <el-icon color="#fff"><Rank /></el-icon>
+                </div>
+                <div class="form-btns" v-if="curFormItem.id === element.id">
+                  <el-button
+                    type="primary"
+                    size="small"
+                    icon="CopyDocument"
+                    circle
+                    title="复制"
+                    @click.stop="handleCopy(element)"
+                  />
+                  <el-button
+                    type="danger"
+                    size="small"
+                    icon="Delete"
+                    circle
+                    title="删除"
+                    @click.stop="handleDel(element, index)"
+                  />
+                </div>
+              </div>
+            </el-col>
+          </template>
+        </draggable>
+      </el-form>
+    </div>
+    <el-form v-else v-bind="formProps" class="h-full overflow-auto pt-px border-dashed border-1 border-gray-200">
+      <draggable
+        class="content-start"
+        style="min-height: 100%"
+        tag="el-row"
+        v-bind="{ gutter: 20 }"
+        v-model="formItems"
+        handle=".form-handle"
+        group="comp"
+        item-key="id"
+      >
+        <template #item="{ element, index }">
+          <el-col :span="element.span || formSpan || 12" :key="element.id" class="mb-10px p-1">
+            <div
+              class="form-item border-dashed border-1 border-gray-200"
+              :class="{ active: curFormItem.id === element.id }"
+              @click="handleClick(element)"
+            >
+              <el-form-item :label="element.label" :rules="element.rules" :prop="element.name">
+                <form-comp :item="element" v-model="formData[element.name]"></form-comp>
+              </el-form-item>
+              <div class="form-handle" v-if="curFormItem.id === element.id">
+                <el-icon color="#fff"><Rank /></el-icon>
+              </div>
+              <div class="form-btns" v-if="curFormItem.id === element.id">
+                <el-button
+                  type="primary"
+                  size="small"
+                  icon="CopyDocument"
+                  circle
+                  title="复制"
+                  @click.stop="handleCopy(element)"
+                />
+                <el-button
+                  type="danger"
+                  size="small"
+                  icon="Delete"
+                  circle
+                  title="删除"
+                  @click.stop="handleDel(element, index)"
+                />
+              </div>
+            </div>
+          </el-col>
+        </template>
+      </draggable>
+    </el-form>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.form-item {
+  padding: 10px;
+  position: relative;
+
+  &:hover {
+    color: var(--el-color-primary);
+    outline: 1px dashed currentColor;
+  }
+  &.active {
+    color: var(--el-color-primary);
+    outline: 1px solid currentColor;
+  }
+}
+.form-btns {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  z-index: 10;
+}
+.form-handle {
+  position: absolute;
+  top: 0;
+  left: 0;
+  background-color: var(--el-color-primary);
+  padding: 0 5px;
+  z-index: 10;
+  cursor: move;
+}
+:deep(.el-select) {
+  width: 100%;
+}
+.ios-bg {
+  height: 580px;
+  background: url('@/assets/images/iosBg.png') no-repeat;
+  background-size: contain;
+  padding: 60px 92px 60px 35px;
+}
+</style>

+ 343 - 0
src/components/designer/DesignerSetting.vue

@@ -0,0 +1,343 @@
+<script lang="ts" setup>
+import { comps } from './constant'
+import { useFormDesignerStore } from '@/stores/designer'
+import { compToFormItem, placeholder } from '@/utils/utils'
+
+const formDesignerStore = useFormDesignerStore()
+const { formProps, formSpan } = storeToRefs(formDesignerStore)
+
+// 组件属性
+const compData: any = computed(() => formDesignerStore.curFormItem)
+const handleCompTypeChange = (type: string) => {
+  let formItem: any
+  comps.forEach(comp => {
+    const item = comp.options.find(option => option.type === type)
+    if (item) {
+      formItem = item
+      return
+    }
+  })
+  formDesignerStore.setCurFormItem(compToFormItem(formItem))
+  // formDesignerStore.curFormItem.type = type
+}
+
+// 日期类型
+const dateType = ref(['year', 'month', 'date', 'dates', 'datetime', 'week', 'datetimerange', 'daterange', 'monthrange'])
+
+const activeName = computed({
+  get() {
+    return compData.value.type ? 'comp' : 'form'
+  },
+  set() {
+    // activeName.value = val
+  }
+})
+
+const handleAddOption = () => {
+  compData.value.options.push({})
+}
+const handleDelOption = (index: number) => {
+  compData.value.options.splice(index, 1)
+}
+
+const marks = ref({
+  6: '6',
+  8: '8',
+  12: '12',
+  24: '24'
+})
+
+watch(
+  () => compData.value.required,
+  (val: boolean) => {
+    if (val) {
+      compData.value.rules[0] = { required: true, message: placeholder(compData.value), trigger: 'blur' }
+    } else {
+      compData.value.rules && compData.value.rules.shift()
+    }
+  }
+)
+</script>
+
+<template>
+  <div class="h-full bg-white">
+    <el-tabs v-model="activeName" stretch>
+      <el-tab-pane label="组件属性" name="comp">
+        <el-form class="px-10px" label-width="90px">
+          <!-- 通用属性 begin -->
+          <el-form-item label="组件类型">
+            <el-select style="width: 100%" v-model="compData.type" @change="handleCompTypeChange">
+              <el-option-group v-for="group in comps" :key="group.label" :label="group.label">
+                <el-option v-for="item in group.options" :key="item.type" :label="item.name" :value="item.type" />
+              </el-option-group>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="字段标识">
+            <el-input v-model="compData.name" clearable></el-input>
+          </el-form-item>
+          <el-form-item label="标题">
+            <el-input v-model="compData.label" clearable></el-input>
+          </el-form-item>
+          <el-form-item label="组件栅格">
+            <el-slider :max="24" v-model="compData.span" :marks="marks" class="mb-10px"></el-slider>
+          </el-form-item>
+          <el-form-item label="默认值" v-if="compData.type !== 'date-picker'">
+            <el-input v-model="compData.value"></el-input>
+          </el-form-item>
+          <el-form-item label="列表显示">
+            <el-switch v-model="compData.list"></el-switch>
+          </el-form-item>
+          <el-form-item label="必填">
+            <el-switch v-model="compData.required"></el-switch>
+          </el-form-item>
+          <!-- 通用属性 end -->
+
+          <!-- input属性 begin -->
+          <template v-if="compData.type === 'input'">
+            <el-form-item label="最多输入">
+              <el-input v-model="compData.props.maxlength">
+                <template #append>个字符</template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="类型">
+              <el-select style="width: 100%" v-model="compData.props.type">
+                <el-option
+                  v-for="item in ['text', 'password', 'textarea', 'number']"
+                  :key="item"
+                  :label="item"
+                  :value="item"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="可清空">
+              <el-switch v-model="compData.props.clearable"></el-switch>
+            </el-form-item>
+            <el-form-item label="字数统计">
+              <el-switch v-model="compData.props.showWordLimit"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- input属性 end -->
+
+          <!-- input-number属性 begin -->
+          <template v-if="compData.type === 'input-number'">
+            <el-form-item label="步进">
+              <el-input-number v-model="compData.props.step"></el-input-number>
+            </el-form-item>
+            <el-form-item label="精度">
+              <el-input-number v-model="compData.props.precision"></el-input-number>
+            </el-form-item>
+          </template>
+          <!-- input-number属性 end -->
+
+          <!-- radio属性 begin -->
+          <template v-if="compData.type === 'radio-group'">
+            <el-form-item label="边框">
+              <el-switch v-model="compData.props.border"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- radio属性 end -->
+
+          <!-- checkbox属性 begin -->
+          <template v-if="compData.type === 'checkbox-group'">
+            <el-form-item label="最少可选">
+              <el-input-number v-model="compData.props.min"></el-input-number>
+            </el-form-item>
+            <el-form-item label="最多可选">
+              <el-input-number v-model="compData.props.max"></el-input-number>
+            </el-form-item>
+          </template>
+          <!-- checkbox end -->
+
+          <!-- cascader|select属性 begin -->
+          <template v-if="['cascader', 'select'].includes(compData.type)">
+            <el-form-item label="可清空">
+              <el-switch v-model="compData.props.clearable"></el-switch>
+            </el-form-item>
+            <el-form-item label="多选">
+              <el-switch v-model="compData.props.multiple"></el-switch>
+            </el-form-item>
+            <el-form-item label="多选合并" v-if="compData.type === 'select'">
+              <el-switch v-model="compData.props.collapseTags"></el-switch>
+            </el-form-item>
+            <el-form-item label="完整路径" v-if="compData.type === 'cascader'">
+              <el-switch v-model="compData.props.showAllLevels"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- cascader|select属性 end -->
+
+          <!-- checkbox|radio|select属性 begin -->
+          <template v-if="['checkbox-group', 'radio-group', 'select'].includes(compData.type)">
+            <el-divider>选项</el-divider>
+            <el-space v-for="(item, index) in compData.options" :key="index" class="mb-10px">
+              <el-input v-model="item.label" placeholder="选项名"></el-input>
+              <el-input v-model="item.value" placeholder="选项值"></el-input>
+              <el-icon @click="handleDelOption(index)" color="var(--el-color-danger)" size="20px"><Remove /></el-icon>
+            </el-space>
+            <el-button type="primary" text icon="Plus" @click="handleAddOption">添加选项</el-button>
+          </template>
+          <!-- checkbox|radio|select属性 end -->
+
+          <!-- transfer属性 begin -->
+          <template v-if="compData.type === 'transfer'">
+            <el-form-item label="可搜索">
+              <el-switch v-model="compData.props.filterable"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- transfer属性 end -->
+
+          <!-- date-picker属性 begin -->
+          <template v-if="compData.type === 'date-picker'">
+            <el-form-item label="显示类型">
+              <el-select style="width: 100%" v-model="compData.props.type">
+                <el-option v-for="item in dateType" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="日期格式">
+              <el-input v-model="compData.props.format" value="YYYY-MM-DD"></el-input>
+            </el-form-item>
+            <el-form-item label="默认值">
+              <el-date-picker
+                v-model="compData.value"
+                :format="compData.props.format"
+                :type="compData.props.type"
+                style="width: 100%"
+              ></el-date-picker>
+            </el-form-item>
+          </template>
+          <!-- date-picker属性 end -->
+
+          <!-- time-picker属性 begin -->
+          <template v-if="compData.type === 'time-picker'">
+            <el-form-item label="时间范围">
+              <el-switch v-model="compData.props.isRange"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- time-picker属性 end -->
+
+          <!-- time-select属性 begin -->
+          <template v-if="compData.type === 'time-select'">
+            <el-form-item label="起始时间">
+              <el-input v-model="compData.props.start" placeholder="08:30"></el-input>
+            </el-form-item>
+            <el-form-item label="结束时间">
+              <el-input v-model="compData.props.end" placeholder="18:30"></el-input>
+            </el-form-item>
+            <el-form-item label="步长">
+              <el-input v-model="compData.props.step" placeholder="00:15"></el-input>
+            </el-form-item>
+          </template>
+          <!-- time-select属性 begin -->
+
+          <!-- slider属性 begin -->
+          <template v-if="compData.type === 'slider'">
+            <el-form-item label="最小值">
+              <el-input-number v-model="compData.props.min"></el-input-number>
+            </el-form-item>
+            <el-form-item label="最大值">
+              <el-input-number v-model="compData.props.max"></el-input-number>
+            </el-form-item>
+            <el-form-item label="步长">
+              <el-input-number v-model="compData.props.step"></el-input-number>
+            </el-form-item>
+            <el-form-item label="间断点">
+              <el-switch v-model="compData.props.showStops"></el-switch>
+            </el-form-item>
+            <el-form-item label="输入框">
+              <el-switch v-model="compData.props.showInput"></el-switch>
+            </el-form-item>
+            <el-form-item label="范围选择">
+              <el-switch v-model="compData.props.range"></el-switch>
+            </el-form-item>
+            <el-form-item label="垂直模式">
+              <el-switch v-model="compData.props.vertical"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- slider属性 end -->
+
+          <!-- rate属性 begin -->
+          <template v-if="compData.type === 'rate'">
+            <el-form-item label="允许半选">
+              <el-switch v-model="compData.props.allowHalf"></el-switch>
+            </el-form-item>
+            <el-form-item label="可清空">
+              <el-switch v-model="compData.props.clearable"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- rate属性 end -->
+
+          <!-- color-picker属性 begin -->
+          <template v-if="compData.type === 'color-picker'">
+            <el-form-item label="透明度">
+              <el-switch v-model="compData.props.showAlpha"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- color-picker属性 end -->
+
+          <!-- switch属性 begin -->
+          <template v-if="compData.type === 'switch'">
+            <el-form-item label="激活文字">
+              <el-input v-model="compData.props.activeText"></el-input>
+            </el-form-item>
+            <el-form-item label="非激活文字">
+              <el-input v-model="compData.props.inactiveText"></el-input>
+            </el-form-item>
+            <el-form-item label="文字在点内">
+              <el-switch v-model="compData.props.inlinePrompt"></el-switch>
+            </el-form-item>
+          </template>
+          <!-- switch属性 end -->
+
+          <!-- 字典属性 begin -->
+          <template v-if="compData.type === 'dict'">
+            <el-form-item label="字典类型">
+              <el-input v-model="compData.props.type"></el-input>
+            </el-form-item>
+          </template>
+          <!-- 字典属性 end -->
+
+
+          <!-- <template v-if="compData.type === 'trends-select'">
+            <el-form-item label="远端方法">
+              <el-input v-model="compData.props.request"></el-input>
+            </el-form-item>
+
+          </template> -->
+
+
+        </el-form>
+      </el-tab-pane>
+      <el-tab-pane label="表单属性" name="form">
+        <el-form class="px-10px" label-width="80px">
+          <!-- <el-form-item label="表单类型">
+            <el-radio-group v-model="formData.type">
+              <el-radio-button label="basic">基础表单</el-radio-button>
+              <el-radio-button label="advanced">高级表单</el-radio-button>
+            </el-radio-group>
+          </el-form-item> -->
+          <el-form-item label="表单尺寸">
+            <el-radio-group v-model="formProps.size">
+              <el-radio-button label="large" />
+              <el-radio-button label="default" />
+              <el-radio-button label="small" />
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="表单栅格">
+            <el-slider :max="24" v-model="formSpan" :marks="marks" class="mb-10px"></el-slider>
+          </el-form-item>
+          <el-form-item label="标签对齐">
+            <el-radio-group v-model="formProps.labelPosition">
+              <el-radio-button label="left">左对齐</el-radio-button>
+              <el-radio-button label="right">右对齐</el-radio-button>
+              <el-radio-button label="top">顶部对齐</el-radio-button>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="标签宽度">
+            <el-input-number v-model="formProps.labelWidth"></el-input-number>
+          </el-form-item>
+        </el-form>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>

+ 33 - 0
src/components/designer/FormDesigner.vue

@@ -0,0 +1,33 @@
+<script lang="ts" setup>
+
+
+
+</script>
+
+<template>
+  <el-container class="layout-container el-dialog-div" >
+    <el-aside width="300px">
+      <designer-comp></designer-comp>
+    </el-aside>
+    <el-container>
+      <el-header>
+        <designer-header></designer-header>
+      </el-header>
+      <el-main style="padding-bottom: 0;" >
+        <designer-render ></designer-render>
+      </el-main>
+    </el-container>
+    <el-aside width="350px">
+      <designer-setting></designer-setting>
+    </el-aside>
+  </el-container>
+</template>
+
+<style lang="scss" scoped>
+.layout-container {
+  height: 100%;
+}
+.el-dialog-div{
+  height: 700px;
+}
+</style>

+ 92 - 0
src/components/designer/constant.ts

@@ -0,0 +1,92 @@
+
+export const comps = [
+  {
+    label: '输入型',
+    options: [
+      {
+        icon: 'icon-add-text-two',
+        name: '文本输入',
+        type: 'input'
+      },
+      {
+        icon: 'icon-add-text-two',
+        name: '数字输入',
+        type: 'input-number'
+      },
+    ]
+  },
+  {
+    label: '选择型',
+    options: [
+      {
+        icon: 'icon-radio-two',
+        name: '单选',
+        type: 'radio-group'
+      },
+      {
+        icon: 'icon-checkbox',
+        name: '多选',
+        type: 'checkbox-group'
+      },
+      {
+        icon: 'icon-drop-down-list',
+        name: '下拉',
+        type: 'select'
+      },
+      {
+        icon: 'icon-calendar-thirty-two',
+        name: '日期选择器',
+        type: 'date-picker',
+        props: {
+          type: 'date'
+        }
+      },
+      {
+        icon: 'icon-time',
+        name: '时间选择器',
+        type: 'time-picker'
+      },
+      {
+        icon: 'icon-timer',
+        name: '时间选择',
+        type: 'time-select'
+      },
+      {
+        icon: 'icon-upload-picture',
+        name: '图片上传',
+        type: 'image-upload'
+      },
+      {
+        icon: 'icon-switch-one',
+        name: '开关',
+        type: 'switch'
+      }
+    ]
+  },
+]
+
+export const typeMap = {
+  radio: '单选',
+  checkbox: '多选',
+  select: '下拉',
+  input: '单行文本',
+  textarea: '多行文本',
+}
+
+export const configMap = {
+  radio: {
+    label: '',
+    value: '',
+    name: '',
+    type: 'radio',
+    placeholder: '',
+    children: [],
+    options: [],
+    search: false,
+    required: false
+  },
+  checkbox: '多选',
+  select: '下拉',
+  input: '单行文本',
+  textarea: '多行文本'
+}

+ 207 - 0
src/components/workflow/WorkflowEdit.vue

@@ -0,0 +1,207 @@
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { addFlow, getFlow, putFlow, getExecuteFlow } from '@/api/workflow'
+import { useWorkflow } from '@/stores/workflow'
+const workFlow = useWorkflow()
+const emits = defineEmits(['click-change', 'previous-step'])
+const tipList = ref<any>([])
+let tipVisible = ref(false)
+let islook = ref(false)
+let nowVal = ref(100)
+let processConfig = ref<any>({
+  nodeConfig: {
+    nodeName: '流程发起',
+    nodeType: 0,
+    priorityLevel: '',
+    approvalType: 'or',
+    subjects: [],
+    copyTo: [],
+    settype: '',
+    directorLevel: '',
+    examineMode: '',
+    noHanderAction: '',
+    ccSelfSelectFlag: '',
+    condGroup: {
+      type: 'and',
+      items: []
+    },
+    nodeUserList: [],
+    childNode: {},
+    conditionNodes: []
+  }
+})
+let nodeConfig = ref({})
+let workflowId = ref('')
+let state = ref<any>('')
+let operateShow = ref(false)
+const init = (row: any) => {
+  workflowId = row.id
+  state = row.state
+  if (row.state == 1 || !row.state) {
+    // 新增流程
+    let { nodeConfig: nodes } = processConfig.value
+    nodeConfig.value = nodes
+  } else if (row.state == 2) {
+    operateShow.value = row.operateShow
+    islook.value = row.islook
+    getFlow({
+      workflowId: workflowId
+    }).then((res: any) => {
+      processConfig.value = JSON.parse(res.flowExpr)
+      let { nodeConfig: nodes } = JSON.parse(res.flowExpr)
+      nodeConfig.value = nodes
+    })
+  } else if (row.state == 3) {
+    operateShow.value = row.operateShow
+    islook.value = row.islook
+    getExecuteFlow({
+      workflowExecuteId: workflowId
+    }).then((res: any) => {
+      processConfig.value = JSON.parse(res.flowExpr)
+      let { nodeConfig: nodes } = JSON.parse(res.flowExpr)
+      nodeConfig.value = nodes
+    })
+  }
+}
+const reErr = (childNode: any) => {
+  if (childNode) {
+    let { nodeType, error, nodeName, conditionNodes } = childNode
+    if (nodeType == 1 || nodeType == 4) {
+      if (error) {
+        tipList.value.push({
+          name: nodeName,
+          type: ['', '审核人', '抄送人'][nodeType]
+        })
+      }
+      reErr(childNode)
+    } else if (nodeType == 3) {
+      reErr(childNode)
+    } else if (nodeType == 2) {
+      reErr(childNode)
+      for (var i = 0; i < conditionNodes.length; i++) {
+        if (conditionNodes[i].error) {
+          tipList.value.push({ name: conditionNodes[i].nodeName, type: '条件' })
+        }
+        reErr(conditionNodes[i])
+      }
+    }
+  } else {
+    childNode = null
+  }
+}
+const saveSet = async () => {
+  workFlow.setIsTried(true)
+  tipList.value = []
+  reErr(nodeConfig.value)
+  if (tipList.value.length != 0) {
+    tipVisible.value = true
+    return
+  }
+  processConfig.value.nodeConfig = nodeConfig.value
+  if (state == 1 || !state) {
+    addFlow({
+      workflowId: workflowId,
+      firstNode: processConfig.value.nodeConfig.childNode,
+      flowExpr: JSON.stringify(processConfig.value)
+    }).then((res: any) => {
+      ElMessage({
+        type: 'success',
+        message: res.msg
+      })
+      closeDialog()
+      emits('click-change')
+    })
+  } else {
+    putFlow({
+      workflowId: workflowId,
+      firstNode: processConfig.value.nodeConfig.childNode,
+      flowExpr: JSON.stringify(processConfig.value)
+    }).then((res: any) => {
+      ElMessage({
+        type: 'success',
+        message: res.msg
+      })
+      closeDialog()
+      emits('click-change')
+    })
+  }
+}
+const zoomSize = (type: any) => {
+  if (type == 1) {
+    if (nowVal.value == 50) {
+      return
+    }
+    nowVal.value -= 10
+  } else {
+    if (nowVal.value == 300) {
+      return
+    }
+    nowVal.value += 10
+  }
+}
+const resetForm = (formEl: any | undefined) => {
+  emits('previous-step')
+}
+const closeDialog = () => {
+  processConfig.value = {
+    nodeConfig: {
+      nodeName: '流程发起',
+      nodeType: 0,
+      priorityLevel: '',
+      approvalType: 'or',
+      subjects: [],
+      copyTo: [],
+      settype: '',
+      directorLevel: '',
+      examineMode: '',
+      noHanderAction: '',
+      ccSelfSelectFlag: '',
+      condGroup: {
+        type: 'and',
+        items: []
+      },
+      nodeUserList: [],
+      childNode: {},
+      conditionNodes: []
+    }
+  }
+}
+defineExpose({
+  init
+})
+</script>
+<template>
+  <div class="m-t-10">
+    <div style="height: 800px">
+      <section class="dingflow-design">
+        <div class="zoom">
+          <div class="zoom-out" :class="nowVal == 50 && 'disabled'" @click="zoomSize(1)"></div>
+          <span>{{ nowVal }}%</span>
+          <div class="zoom-in" :class="nowVal == 300 && 'disabled'" @click="zoomSize(2)"></div>
+        </div>
+        <div class="box-scale" :style="`transform: scale(${nowVal / 100});`">
+          <nodeWrap v-model:nodeConfig="nodeConfig" :operateShow="operateShow" />
+          <div class="end-node">
+            <div class="end-node-circle"></div>
+            <div class="end-node-text">
+              <el-button type="primary">流程结束</el-button>
+            </div>
+          </div>
+        </div>
+      </section>
+    </div>
+    <errorDialog v-model:visible="tipVisible" :list="tipList" />
+    <approverDrawer :operateShow="operateShow" />
+    <copyerDrawer :operateShow="operateShow" />
+    <conditionDrawer :workflowId="workflowId" :operateShow="operateShow" :islook="islook" />
+    <div class="flex justify-end w-full m-t-10" v-if="!operateShow">
+      <el-button size="default" @click="resetForm">上一步</el-button>
+      <el-button size="default" type="primary" @click="saveSet">确认</el-button>
+    </div>
+  </div>
+</template>
+<style lang="scss" scoped>
+.error-modal-list {
+  width: 455px;
+}
+</style>

+ 233 - 0
src/components/workflow/addNode.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="add-node-btn-box">
+    <div class="add-node-btn">
+      <el-popover placement="right-start" v-model="visible" width="auto" v-if="!props.operateShow">
+        <div class="add-node-popover-body">
+          <a class="add-node-popover-item approver" @click="addType(1)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>审批人</p>
+          </a>
+          <a class="add-node-popover-item notifier" @click="addType(4)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>抄送人</p>
+          </a>
+          <a class="add-node-popover-item condition" @click="addType(2)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>条件分支</p>
+          </a>
+        </div>
+        <template #reference>
+          <button class="btn" type="button">
+            <span class="iconfont"></span>
+          </button>
+        </template>
+      </el-popover>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+let props = defineProps({
+  childNodeP: {
+    type: Object,
+    default: () => ({})
+  },
+  operateShow: {
+    type: Boolean,
+    default: false
+  }
+})
+let emits = defineEmits(['update:childNodeP'])
+let visible = ref(false)
+const addType = type => {
+  visible.value = false
+  if (type != 2) {
+    var data
+    if (type == 1) {
+      data = {
+        nodeName: '',
+        error: true,
+        nodeType: 1,
+        settype: 1,
+        selectMode: 1,
+        directorLevel: 1,
+        approvalType: 'or',
+        examineMode: 1,
+        noHanderAction: 1,
+        childNode: props.childNodeP,
+        subjects: []
+      }
+    } else if (type == 4) {
+      data = {
+        nodeName: '抄送人',
+        nodeType: 4,
+        ccSelfSelectFlag: 1,
+        copyTo: [],
+        childNode: props.childNodeP,
+        subjects: []
+      }
+    }
+    emits('update:childNodeP', data)
+  } else {
+    emits('update:childNodeP', {
+      nodeName: '路由',
+      nodeType: 2,
+      childNode: null,
+      conditionNodes: [
+        {
+          nodeName: '条件1',
+          error: true,
+          nodeType: 3,
+          priorityLevel: 1,
+          condGroup: {
+            type: 'and',
+            items: []
+          },
+          subjects: [],
+          childNode: props.childNodeP
+        },
+        {
+          nodeName: '条件2',
+          nodeType: 3,
+          priorityLevel: 2,
+          condGroup: {
+            type: 'default',
+            items: []
+          },
+          subjects: [],
+          childNode: null
+        }
+      ]
+    })
+  }
+}
+</script>
+<style scoped lang="scss">
+.add-node-btn-box {
+  width: 240px;
+  display: -webkit-inline-box;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  -webkit-box-flex: 1;
+  -ms-flex-positive: 1;
+  position: relative;
+  &:before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: -1;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca;
+  }
+  .add-node-btn {
+    user-select: none;
+    width: 240px;
+    padding: 20px 0 32px;
+    display: flex;
+    -webkit-box-pack: center;
+    justify-content: center;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    flex-grow: 1;
+    .btn {
+      outline: none;
+      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      width: 30px;
+      height: 30px;
+      background: #3296fa;
+      border-radius: 50%;
+      position: relative;
+      border: none;
+      line-height: 30px;
+      -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      .iconfont {
+        color: #fff;
+        font-size: 16px;
+      }
+      &:hover {
+        transform: scale(1.3);
+        box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+      }
+      &:active {
+        transform: none;
+        background: #1e83e9;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+.add-node-popover-body {
+  display: flex;
+  .add-node-popover-item {
+    margin-right: 10px;
+    cursor: pointer;
+    text-align: center;
+    flex: 1;
+    color: #191f25 !important;
+    .item-wrapper {
+      user-select: none;
+      display: inline-block;
+      width: 80px;
+      height: 80px;
+      margin-bottom: 5px;
+      background: #fff;
+      border: 1px solid #e2e2e2;
+      border-radius: 50%;
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      .iconfont {
+        font-size: 35px;
+        line-height: 80px;
+      }
+    }
+    &.approver {
+      .item-wrapper {
+        color: #ff943e;
+      }
+    }
+    &.notifier {
+      .item-wrapper {
+        color: #3296fa;
+      }
+    }
+    &.condition {
+      .item-wrapper {
+        color: #15bc83;
+      }
+    }
+    &:hover {
+      .item-wrapper {
+        background: #3296fa;
+        box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
+      }
+      .iconfont {
+        color: #fff;
+      }
+    }
+    &:active {
+      .item-wrapper {
+        box-shadow: none;
+        background: #eaeaea;
+      }
+      .iconfont {
+        color: inherit;
+      }
+    }
+  }
+}
+</style>

+ 146 - 0
src/components/workflow/dialog/employeesRoleDialog.vue

@@ -0,0 +1,146 @@
+<template>
+  <el-dialog title="选择成员" v-model="visibleDialog" width="600px" append-to-body class="promoter_person">
+    <div class="person_body clear flex">
+      <div class="person_tree l">
+        <input type="text" placeholder="搜索成员" v-model="searchVal" @input="getDebounceData($event)" />
+        <el-tabs v-model="activeName" @tab-change="handleClick">
+          <el-tab-pane label="人员列表" name="3"></el-tab-pane>
+        </el-tabs>
+
+        <selectBox :list="!searchVal ? list : searchlist" style="height: 360px" />
+      </div>
+      <selectResult :total="total" @del="delList" :list="resList" />
+    </div>
+    <template #footer>
+      <el-button @click="$emit('update:visible', false)">取 消</el-button>
+      <el-button type="primary" @click="saveDialog">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import $func from '@/utils/preload.js'
+import { getTangent } from '@/api/workflow'
+let props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  data: {
+    type: Array,
+    default: () => []
+  },
+  isDepartment: {
+    type: Boolean,
+    default: false
+  }
+})
+let searchVal = ref<any>('')
+let emits = defineEmits(['update:visible', 'change'])
+let visibleDialog = computed({
+  get() {
+    return props.visible
+  },
+  set() {
+    closeDialog()
+  }
+})
+let departments = ref<any>({
+  titleDepartments: [],
+  childDepartments: [],
+  employees: []
+})
+let checkedRoleList = ref<any>([])
+let checkedEmployessList = ref<any>([])
+let checkedDepartmentList = ref<any>([])
+let activeName = ref<any>('3')
+let list = computed(() => {
+  return [
+    {
+      type: 'employee',
+      data: departments.value.employees,
+      isActive: (item: any) => $func.toggleClass(checkedEmployessList.value, item),
+      change: (item: any) => $func.toChecked(checkedEmployessList.value, item)
+    }
+  ]
+})
+let searchlist = computed(() => {
+  return [
+    {
+      type: 'employee',
+      data: departments.value.titleDepartments,
+      isActive: (item: any) => $func.toggleClass(checkedEmployessList.value, item),
+      change: (item: any) => $func.toChecked(checkedEmployessList.value, item)
+    }
+  ]
+})
+getTangent().then((res: any) => {
+  departments.value.employees = res.infos
+})
+const getDebounceData = (e: any) => {
+  departments.value.titleDepartments = departments.value.employees.filter((item: any) => {
+    if (item.name.indexOf(searchVal.value) != -1) {
+      return item
+    }
+  })
+}
+let resList = computed(() => {
+  let data = [
+    {
+      type: 'employee',
+      data: checkedEmployessList.value,
+      cancel: (item: any) => $func.removeEle(checkedEmployessList.value, item)
+    }
+  ]
+  if (props.isDepartment) {
+    data.splice(1, 0, {
+      type: 'department',
+      data: checkedDepartmentList.value,
+      cancel: item => $func.removeEle(checkedDepartmentList.value, item)
+    })
+  }
+  return data
+})
+watch(
+  () => props.visible,
+  (val: any) => {
+    if (val) {
+      activeName.value = '3'
+      searchVal.value = ''
+      checkedEmployessList.value = props.data.map(({ subjectName, subjectId }: any) => ({
+        name: subjectName,
+        id: subjectId
+      }))
+    }
+  }
+)
+let total = computed(() => {
+  return checkedEmployessList.value.length + checkedRoleList.value.length + checkedDepartmentList.value.length
+})
+
+const handleClick = () => {
+  searchVal.value = ''
+}
+const saveDialog = () => {
+  let checkedList = [...checkedRoleList.value, ...checkedEmployessList.value, ...checkedDepartmentList.value].map(
+    (item: any) => ({
+      type: item.employeeName ? 1 : item.roleName ? 2 : 3,
+      subjectId: item.id || item.roleId,
+      subjectName: item.name
+    })
+  )
+  emits('change', checkedList)
+}
+const delList = () => {
+  checkedEmployessList.value = []
+  checkedRoleList.value = []
+  checkedDepartmentList.value = []
+}
+const closeDialog = () => {
+  emits('update:visible', false)
+}
+</script>
+
+<style>
+@import '@/css/dialog.css';
+</style>

+ 76 - 0
src/components/workflow/dialog/errorDialog.vue

@@ -0,0 +1,76 @@
+<!--
+ * @Date: 2022-08-25 14:05:59
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2022-09-21 14:36:40
+ * @FilePath: /Workflow-Vue3/src/components/dialog/errorDialog.vue
+-->
+<template>
+  <el-dialog title="提示" v-model="visibleDialog">
+    <div class="ant-confirm-body">
+      <i class="anticon anticon-close-circle" style="color: #f00"></i>
+      <span class="ant-confirm-title">当前无法发布</span>
+      <div class="ant-confirm-content">
+        <div>
+          <p class="error-modal-desc">以下内容不完善,需进行修改</p>
+          <div class="error-modal-list">
+            <div class="error-modal-item" v-for="(item, index) in list" :key="index">
+              <div class="error-modal-item-label">流程设计</div>
+              <div class="error-modal-item-content">{{ item.name }} 未选择{{ item.type }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="visibleDialog = false">我知道了</el-button>
+      <el-button type="primary" @click="visibleDialog = false">前往修改</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+let props = defineProps({
+  list: {
+    type: Array,
+    default: () => []
+  },
+  visible: {
+    type: Boolean,
+    default: false
+  }
+})
+let emits = defineEmits(['update:visible'])
+
+let visibleDialog = computed({
+  get() {
+    return props.visible
+  },
+  set(val) {
+    emits('update:visible', val)
+  }
+})
+</script>
+
+<style scoped>
+.ant-confirm-body .ant-confirm-title {
+  color: rgba(0, 0, 0, 0.85);
+  font-weight: 500;
+  font-size: 16px;
+  line-height: 1.4;
+  display: block;
+  overflow: hidden;
+}
+
+.ant-confirm-body .ant-confirm-content {
+  margin-left: 38px;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.65);
+  margin-top: 8px;
+}
+
+.ant-confirm-body > .anticon {
+  font-size: 22px;
+  margin-right: 16px;
+  float: left;
+}
+</style>

+ 231 - 0
src/components/workflow/drawer/approverDrawer.vue

@@ -0,0 +1,231 @@
+<template>
+  <el-drawer title="审批人设置" v-model="visible" direction="rtl" class="set_promoter" size="650px">
+    <div class="demo-drawer__content">
+      <div class="drawer_content">
+        <div class="approver_some">
+          <el-form ref="formRef" :model="approverConfig" :rules="formRules">
+            <el-form-item class="w-full items-center" label="" prop="nodeName">
+              <p>
+                <el-icon color="#1890ff"><Operation /></el-icon>节点名称
+              </p>
+              <el-input placeholder="请输入节点名称" :disabled="props.operateShow" v-model="approverConfig.nodeName">
+              </el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="approver_content">
+          <el-radio-group
+            v-model="approverConfig.settype"
+            :disabled="props.operateShow"
+            class="clear"
+            @change="changeType"
+          >
+            <el-radio :label="1">指定成员</el-radio>
+            <el-radio :label="2">上级主管</el-radio>
+            <!-- <el-radio :label="4">发起人自选</el-radio> -->
+            <!-- <el-radio :label="3">指定角色</el-radio> -->
+            <!-- <el-radio :label="7">来自流程</el-radio> -->
+          </el-radio-group>
+        </div>
+        <div class="approver_manager approver_detail" v-if="approverConfig.settype == 1">
+          <el-button class="el-button el-button--text" @click="addApprover" v-if="!props.operateShow">
+            <el-icon><User color="#1890ff" /></el-icon>添加/修改成员
+          </el-button>
+          <a
+            class="ml-10"
+            v-if="approverConfig.subjects.length != 0 && !props.operateShow"
+            @click="approverConfig.subjects = []"
+            >清除</a
+          >
+          <p class="p-5 bg-white mt-5" v-if="approverConfig.settype == 1 && approverConfig.subjects.length != 0">
+            <el-tag
+              @close="$func.removeEle(approverConfig.subjects, item, 'subjectId')"
+              v-for="(item, index) in approverConfig.subjects"
+              class="mx-1"
+              :closable="!props.operateShow"
+              :disable-transitions="false"
+              :key="index"
+            >
+              {{ item.subjectName }}
+            </el-tag>
+          </p>
+        </div>
+
+        <div class="approver_some">
+          <p>
+            <el-icon color="#1890ff"><Operation /></el-icon>多人审批时采用的审批方式
+          </p>
+          <el-radio-group v-model="approverConfig.approvalType" :disabled="props.operateShow" class="clear">
+            <el-radio class="ml-10" label="or">或签(一名审批人同意或拒绝即可)</el-radio>
+            <br />
+            <el-radio class="ml-10" label="and">会签(须所有审批人同意)</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <div class="demo-drawer__footer clear float-right" v-if="!props.operateShow">
+        <el-button type="primary" @click="saveApprover">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+      <employees-role-dialog v-model:visible="approverVisible" v-model:data="checkedList" @change="sureApprover" />
+    </div>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import $func from '@/utils/preload.js'
+import { ElMessage } from 'element-plus'
+import type { FormInstance } from 'element-plus'
+import { useWorkflow } from '@/stores/workflow'
+const workFlow = useWorkflow()
+let props = defineProps({
+  operateShow: {
+    type: Boolean,
+    default: false
+  }
+})
+let emits = defineEmits(['update:nodeConfig'])
+let approverConfig = ref<any>({})
+let approverVisible = ref<any>(false)
+
+let checkedList = ref<any>([])
+let visible = computed({
+  get() {
+    return workFlow.approverDrawer
+  },
+  set() {
+    closeDrawer()
+  }
+})
+const formRef = ref<FormInstance>()
+const formRules = {
+  nodeName: {
+    required: true,
+    message: '请输入节点名称'
+  }
+}
+watch(workFlow, (val: any) => {
+  approverConfig.value = val.approverConfig1.value
+})
+const changeType = (val: any) => {
+  approverConfig.value.subjects = []
+  approverConfig.value.examineMode = 1
+  approverConfig.value.noHanderAction = 2
+  if (val == 2) {
+    approverConfig.value.directorLevel = 1
+    approverConfig.value.subjects = [
+      {
+        subjectName: '[上级主管]',
+        subjectId: '[上级主管]',
+        type: ''
+      }
+    ]
+  } else if (val == 4) {
+    approverConfig.value.selectMode = 1
+    approverConfig.value.selectRange = 1
+  }
+}
+// 添加成员
+const addApprover = () => {
+  approverVisible.value = true
+  checkedList.value = approverConfig.value.subjects
+}
+
+const sureApprover = (data: any) => {
+  approverConfig.value.subjects = data
+  approverVisible.value = false
+}
+
+const saveApprover = () => {
+  formRef.value?.validate(async (valid: any) => {
+    if (valid) {
+      try {
+        approverConfig.value.error = !$func.setApproverStr(approverConfig.value)
+        workFlow.setApproverConfig({
+          value: approverConfig.value,
+          flag: true,
+          id: workFlow.approverConfig1.id
+        })
+
+        emits('update:nodeConfig', approverConfig.value)
+        closeDrawer()
+      } catch (error) {
+        ElMessage.error('请输入节点名称。')
+      }
+    }
+  })
+}
+const closeDrawer = () => {
+  workFlow.setApprover(false)
+}
+</script>
+<style lang="scss">
+.set_promoter {
+  .approver_content {
+    padding-bottom: 10px;
+    border-bottom: 1px solid #f2f2f2;
+  }
+  .approver_self_select,
+  .approver_content {
+    .el-button {
+      margin-bottom: 20px;
+    }
+  }
+  .approver_content,
+  .approver_some,
+  .approver_self_select {
+    .el-radio-group {
+      display: unset;
+    }
+    .el-radio {
+      width: 27%;
+      margin-bottom: 20px;
+      height: 16px;
+    }
+  }
+  .approver_manager p {
+    line-height: 32px;
+  }
+  .approver_manager select {
+    width: 420px;
+    height: 32px;
+    background: rgba(255, 255, 255, 1);
+    border-radius: 4px;
+    border: 1px solid rgba(217, 217, 217, 1);
+  }
+  .approver_manager p.tip {
+    margin: 10px 0 22px 0;
+    font-size: 12px;
+    line-height: 16px;
+    color: #f8642d;
+  }
+  .approver_self {
+    padding: 28px 20px;
+  }
+  .approver_self_select,
+  .approver_manager,
+  .approver_content,
+  .approver_some {
+    padding: 20px 20px 0;
+  }
+  .approver_manager p:first-of-type,
+  .approver_some p {
+    line-height: 19px;
+    font-size: 14px;
+    margin-bottom: 14px;
+    display: flex;
+    align-items: center;
+  }
+  .approver_self_select h3 {
+    margin: 5px 0 20px;
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 19px;
+  }
+  .approver_detail {
+    margin-left: 20px;
+    background: #f4f8fd;
+    border-radius: 5px;
+    padding: 10px;
+    box-shadow: 0 -2px 8px #ccc;
+  }
+}
+</style>

+ 314 - 0
src/components/workflow/drawer/conditionDrawer.vue

@@ -0,0 +1,314 @@
+<template>
+  <el-drawer
+    title="条件设置"
+    v-model="visible"
+    direction="rtl"
+    class="condition_copyer"
+    size="550px"
+    :before-close="saveCondition"
+  >
+    <template #header="{ titleId, titleClass }">
+      <h3 :id="titleId" :class="titleClass">条件设置</h3>
+    </template>
+    <div class="demo-drawer__content">
+      <div class="condition_content drawer_content">
+        <p class="tip">配置条件</p>
+        <div class="approver_some">
+          <p>
+            <el-icon color="#1890ff"><Operation /></el-icon>需满足条件
+          </p>
+          <el-radio-group v-model="conditionConfig.condGroup.type" :disabled="props.operateShow" class="clear">
+            <el-radio class="ml-10" label="and">满足所有</el-radio>
+            <!-- <br/> -->
+            <el-radio class="ml-10" label="or">满足任一</el-radio>
+            <!-- <br/> -->
+            <el-radio class="ml-10" label="default">不满足其他分支条件时,进入该分支</el-radio>
+          </el-radio-group>
+        </div>
+        <ul v-if="conditionConfig.condGroup.type != 'default'">
+          <li v-for="(item, index) in conditionConfig.condGroup.items" :key="index">
+            <div>
+              <p>
+                <el-select
+                  v-model="item.fieldId"
+                  :disabled="props.operateShow"
+                  :style="'width:' + (item.optType == 6 ? 370 : 100) + 'px'"
+                  clearable
+                  filterable
+                  placeholder="条件选项"
+                >
+                  <el-option
+                    v-for="item in fieldList"
+                    :key="item.fieldIdentity"
+                    :label="item.fieldName"
+                    :value="item.fieldIdentity"
+                  />
+                </el-select>
+                :
+                <el-select
+                  v-model="item.operator"
+                  :disabled="props.operateShow"
+                  :style="'width:' + (item.optType == 6 ? 370 : 100) + 'px'"
+                  clearable
+                  filterable
+                  placeholder="请选择符号"
+                >
+                  <el-option value="<" label="小于"></el-option>
+                  <el-option value=">" label="大于"></el-option>
+                  <el-option value="<=" label="小于等于"></el-option>
+                  <el-option value="==" label="等于"></el-option>
+                  <el-option value=">=" label="大于等于"></el-option>
+                  <el-option value="!=" label="不等于"></el-option>
+                </el-select>
+                :
+                <el-input
+                  v-if="item.optType != 6"
+                  :disabled="props.operateShow"
+                  type="text"
+                  placeholder="请输入数字"
+                  style="width: 100px"
+                  v-model="item.rightValue"
+                ></el-input>
+              </p>
+            </div>
+            <a
+              v-if="(item.type == 1 || item.type == 2) && !props.operateShow"
+              @click="$func.removeEle(conditionConfig.condGroup.items, item, 'columnId')"
+              >删除</a
+            >
+          </li>
+        </ul>
+        <el-button
+          type="primary"
+          :disabled="props.operateShow"
+          @click="addCondition"
+          v-if="conditionConfig.condGroup.type != 'default'"
+          >添加条件</el-button
+        >
+      </div>
+      <div class="demo-drawer__footer clear" v-if="!props.operateShow">
+        <el-button type="primary" @click="saveCondition">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import $func from '@/utils/preload'
+
+import { getFormField, getExecuteFormField } from '@/api/workflow'
+import { useWorkflow } from '@/stores/workflow'
+const workFlow = useWorkflow()
+let conditionsConfig = ref<any>({
+  conditionNodes: []
+})
+let fieldList = ref<any>([])
+let conditionConfig = ref<any>({})
+let PriorityLevel = ref<any>('')
+
+let condGroup = ref({
+  type: 'and',
+  items: []
+})
+
+let props = defineProps({
+  workflowId: {
+    type: String,
+    default: ''
+  },
+  operateShow: {
+    type: Boolean,
+    default: false
+  },
+  islook: {
+    type: Boolean,
+    default: false
+  }
+})
+
+let visible = computed({
+  get() {
+    return workFlow.conditionDrawer
+  },
+  set() {
+    closeDrawer()
+  }
+})
+watch(workFlow, (val: any) => {
+  conditionsConfig.value = val.conditionsConfig1.value
+  PriorityLevel.value = val.conditionsConfig1.priorityLevel
+  conditionConfig.value = val.conditionsConfig1.priorityLevel
+    ? conditionsConfig.value.conditionNodes[val.conditionsConfig1.priorityLevel - 1]
+    : { subjects: [], condGroup: { type: '', items: [] } }
+})
+
+onBeforeUpdate(() => {
+  if (!props.operateShow || !props.islook) {
+    getFormField({
+      workflowId: props.workflowId
+    }).then((res: any) => {
+      fieldList = res.fields
+    })
+  } else {
+    getExecuteFormField({
+      workflowExecuteId: props.workflowId
+    }).then((res: any) => {
+      fieldList = res.fields
+    })
+  }
+})
+
+const addCondition = async () => {
+  conditionConfig.value.condGroup.items.push({
+    showType: 3,
+    columnId: '',
+    type: 2,
+    showName: '',
+    optType: '',
+    zdy1: '2',
+    opt1: '<',
+    zdy2: '',
+    opt2: '<',
+    columnDbname: '',
+    columnType: 'Double'
+  })
+}
+
+const saveCondition = () => {
+  closeDrawer()
+
+  var a = conditionsConfig.value.conditionNodes.splice(PriorityLevel.value - 1, 1) //截取旧下标
+  conditionsConfig.value.conditionNodes.splice(conditionConfig.value.priorityLevel - 1, 0, a[0]) //填充新下标
+  conditionsConfig.value.conditionNodes.map((item: any, index: any) => {
+    item.priorityLevel = index + 1
+  })
+  for (var i = 0; i < conditionsConfig.value.conditionNodes.length; i++) {
+    conditionsConfig.value.conditionNodes[i].error =
+      $func.conditionStr(conditionsConfig.value, i) == '请设置条件' &&
+      i != conditionsConfig.value.conditionNodes.length - 1
+  }
+  workFlow.setConditionsConfig({
+    value: conditionsConfig.value,
+    flag: true,
+    id: workFlow.conditionsConfig1.id
+  })
+}
+
+const closeDrawer = () => {
+  workFlow.conditionDrawer = false
+}
+</script>
+<style lang="scss">
+.condition_copyer {
+  .priority_level {
+    position: absolute;
+    top: 11px;
+    right: 30px;
+    width: 100px;
+    height: 32px;
+    background: rgba(255, 255, 255, 1);
+    border-radius: 4px;
+    border: 1px solid rgba(217, 217, 217, 1);
+    font-size: 12px;
+  }
+
+  .condition_content {
+    padding: 20px 20px 0;
+
+    p.tip {
+      margin: 20px 0;
+      text-indent: 17px;
+      line-height: 45px;
+      background: rgba(241, 249, 255, 1);
+      border: 1px solid rgba(64, 163, 247, 1);
+      color: #46a6fe;
+      font-size: 14px;
+    }
+
+    ul {
+      max-height: 500px;
+      overflow-y: scroll;
+      margin-bottom: 20px;
+
+      li {
+        & > span {
+          float: left;
+          margin-right: 8px;
+          width: 70px;
+          line-height: 32px;
+          text-align: right;
+        }
+
+        & > div {
+          display: inline-block;
+          width: 370px;
+
+          & > p:not(:last-child) {
+            margin-bottom: 10px;
+          }
+        }
+
+        &:not(:last-child) > div > p {
+          margin-bottom: 20px;
+        }
+
+        & > a {
+          float: right;
+          margin-right: 10px;
+          margin-top: 7px;
+        }
+
+        select,
+        input {
+          width: 100%;
+          height: 32px;
+          background: rgba(255, 255, 255, 1);
+          border-radius: 4px;
+          // border: 1px solid rgba(217, 217, 217, 1);
+        }
+
+        select + input {
+          width: 260px;
+        }
+
+        select {
+          margin-right: 10px;
+          width: 100px;
+        }
+
+        p.selected_list {
+          padding-left: 10px;
+          border-radius: 4px;
+          min-height: 32px;
+          border: 1px solid rgba(217, 217, 217, 1);
+          word-break: break-word;
+        }
+
+        p.check_box {
+          line-height: 32px;
+        }
+      }
+    }
+
+    .el-button {
+      margin-bottom: 20px;
+    }
+  }
+}
+
+.condition_list {
+  .el-dialog__body {
+    padding: 16px 26px;
+  }
+
+  p {
+    color: #666666;
+    margin-bottom: 10px;
+
+    & > .check_box {
+      margin-bottom: 0;
+      line-height: 36px;
+    }
+  }
+}
+</style>

+ 110 - 0
src/components/workflow/drawer/copyerDrawer.vue

@@ -0,0 +1,110 @@
+<!--
+ * @Date: 2022-08-25 14:05:59
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2022-09-21 14:36:45
+ * @FilePath: /Workflow-Vue3/src/components/drawer/copyerDrawer.vue
+-->
+<template>
+  <el-drawer
+    title="抄送人设置"
+    v-model="visible"
+    direction="rtl"
+    class="set_copyer"
+    size="550px"
+    :before-close="saveCopyer"
+  >
+    <div class="demo-drawer__content">
+      <div class="copyer_content drawer_content">
+        <el-button type="primary" @click="addCopyer" v-if="!props.operateShow">添加成员</el-button>
+        <p class="p-5 bg-white mt-5 border-gray-200" style="border: 1px solid #b5b5b5">
+          <el-tag
+            v-if="copyerConfig.copyTo.length != 0"
+            @close="$func.removeEle(copyerConfig.copyTo, item, 'subjectId')"
+            v-for="(item, index) in copyerConfig.copyTo"
+            class="mx-1 mb-5"
+            :closable="!props.operateShow"
+            :disable-transitions="false"
+            :key="index"
+          >
+            {{ item.subjectName }}
+          </el-tag>
+          <a
+            class="ml-10"
+            v-if="copyerConfig.copyTo.length != 0 && !props.operateShow"
+            @click="copyerConfig.copyTo = []"
+            >清除</a
+          >
+        </p>
+      </div>
+      <div class="demo-drawer__footer clear float-right mt-10" v-if="!props.operateShow">
+        <el-button @click="closeDrawer">取 消</el-button>
+        <el-button type="primary" @click="saveCopyer">确 定</el-button>
+      </div>
+      <employees-role-dialog v-model:visible="copyerVisible" v-model:data="checkedList" @change="sureCopyer" />
+    </div>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import $func from '@/utils/preload'
+import { useWorkflow } from '@/stores/workflow'
+const workFlow = useWorkflow()
+let copyerConfig = ref<any>({})
+let ccSelfSelectFlag = ref<any>([])
+let copyerVisible = ref<any>(false)
+let checkedList = ref<any>([])
+let visible = computed({
+  get() {
+    return workFlow.copyerDrawer
+  },
+  set() {
+    closeDrawer()
+  }
+})
+let props = defineProps({
+  operateShow: {
+    type: Boolean,
+    default: false
+  }
+})
+watch(workFlow, (val: any) => {
+  copyerConfig.value = val.copyerConfig1.value
+})
+
+const addCopyer = () => {
+  copyerVisible.value = true
+  checkedList.value = copyerConfig.value.copyTo
+}
+const sureCopyer = (data: any) => {
+  copyerConfig.value.copyTo = data
+  copyerVisible.value = false
+}
+const saveCopyer = () => {
+  copyerConfig.value.ccSelfSelectFlag = ccSelfSelectFlag.value.length == 0 ? 0 : 1
+  copyerConfig.value.error = !$func.copyerStr(copyerConfig.value)
+  workFlow.setCopyerConfig({
+    value: copyerConfig.value,
+    flag: true,
+    id: workFlow.copyerConfig1.id
+  })
+  closeDrawer()
+}
+const closeDrawer = () => {
+  workFlow.copyerDrawer = false
+}
+</script>
+
+<style lang="scss">
+.set_copyer {
+  .copyer_content {
+    padding: 20px 20px 0;
+
+    .el-button {
+      margin-bottom: 20px;
+    }
+
+    .el-checkbox {
+      margin-bottom: 20px;
+    }
+  }
+}
+</style>

+ 299 - 0
src/components/workflow/nodeWrap.vue

@@ -0,0 +1,299 @@
+<template>
+  <div class="node-wrap" v-if="nodeConfig.nodeType == 0">
+    <el-button type="primary">流程开始</el-button>
+    <addNode v-model:childNodeP="nodeConfig.childNode" :operateShow="operateShow" />
+  </div>
+  <div class="node-wrap" v-if="(nodeConfig.nodeType == 1 || nodeConfig.nodeType == 4) && nodeConfig.nodeType != 0">
+    <div class="node-wrap-box" :class="isTried && nodeConfig.error ? 'active error' : ''">
+      <div class="title" :style="`background: rgb(${bgColors[nodeConfig.nodeType]});`">
+        <template v-if="nodeConfig.nodeType != 0">
+          <span class="iconfont">{{ nodeConfig.nodeType == 1 ? '' : '' }}</span>
+          <input
+            v-if="isInput"
+            type="text"
+            class="ant-input editable-title-input"
+            :disabled="props.operateShow"
+            @blur="blurEvent()"
+            v-model="nodeConfig.nodeName"
+            :placeholder="defaultText"
+          />
+          <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
+          <span
+            v-if="nodeConfig.nodeName == '' && nodeConfig.nodeType == 1"
+            class="editable-title"
+            @click="clickEvent()"
+            >请输入节点名称</span
+          >
+
+          <i class="anticon anticon-close close" @click="delNode" v-if="!props.operateShow"></i>
+        </template>
+      </div>
+      <div class="content" @click="setPerson">
+        <div class="text">
+          <span class="placeholder" v-if="!showText">请选择{{ defaultText }}</span>
+          {{ showText }}
+        </div>
+        <i class="anticon anticon-right arrow"></i>
+      </div>
+      <div class="error_tip" v-if="isTried && nodeConfig.error">
+        <i class="anticon anticon-exclamation-circle"></i>
+      </div>
+    </div>
+    <addNode v-model:childNodeP="nodeConfig.childNode" :operateShow="operateShow" />
+  </div>
+  <div class="branch-wrap" v-if="nodeConfig.nodeType == 2">
+    <div class="branch-box-wrap">
+      <div class="branch-box">
+        <button class="add-branch" @click="addTerm" v-if="!props.operateShow">添加条件</button>
+        <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
+          <div class="condition-node">
+            <div class="condition-node-box">
+              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
+                <div class="sort-left" v-if="index != 0 && !props.operateShow" @click="arrTransfer(index, -1)">
+                  &lt;
+                </div>
+                <div class="title-wrapper">
+                  <input
+                    v-if="isInputList[index]"
+                    type="text"
+                    :disabled="props.operateShow"
+                    class="ant-input editable-title-input"
+                    @blur="blurEvent(index)"
+                    v-model="item.nodeName"
+                  />
+                  <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
+                  <span class="priority-title" @click="setPerson(item.priorityLevel)"
+                    >优先级{{ item.priorityLevel }}</span
+                  >
+                  <i class="anticon anticon-close close" v-if="!props.operateShow" @click="delTerm(index)"></i>
+                </div>
+                <div
+                  class="sort-right"
+                  v-if="index != nodeConfig.conditionNodes.length - 1 && !props.operateShow"
+                  @click="arrTransfer(index)"
+                >
+                  &gt;
+                </div>
+                <div class="content" @click="setPerson(item.priorityLevel)">
+                  {{ $func.conditionStr(nodeConfig, index) }}
+                </div>
+                <div class="error_tip" v-if="isTried && item.error">
+                  <i class="anticon anticon-exclamation-circle"></i>
+                </div>
+              </div>
+              <addNode v-model:childNodeP="item.childNode" :operateShow="operateShow" />
+            </div>
+          </div>
+          <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" :operateShow="operateShow" />
+          <template v-if="index == 0">
+            <div class="top-left-cover-line"></div>
+            <div class="bottom-left-cover-line"></div>
+          </template>
+          <template v-if="index == nodeConfig.conditionNodes.length - 1">
+            <div class="top-right-cover-line"></div>
+            <div class="bottom-right-cover-line"></div>
+          </template>
+        </div>
+      </div>
+      <addNode v-model:childNodeP="nodeConfig.childNode" :operateShow="operateShow" />
+    </div>
+  </div>
+  <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" :operateShow="operateShow" />
+</template>
+<script setup>
+import $func from '@/utils/preload'
+import addNode from './addNode.vue'
+import { useWorkflow } from '@/stores/workflow'
+const workFlow = useWorkflow()
+let _uid = getCurrentInstance().uid
+let bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
+let placeholderList = ['发起人', '', '抄送人']
+let props = defineProps({
+  nodeConfig: {
+    type: Object,
+    default: () => ({})
+  },
+  operateShow: {
+    type: Boolean,
+    default: false
+  }
+})
+
+let defaultText = computed(() => {
+  return placeholderList[props.nodeConfig.nodeType]
+})
+let showText = computed(() => {
+  if (props.nodeConfig.nodeType == 1) return $func.setApproverStr(props.nodeConfig)
+  return $func.copyerStr(props.nodeConfig)
+})
+
+let isInputList = ref([])
+let isInput = ref(false)
+const resetConditionNodesErr = () => {
+  for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
+    props.nodeConfig.conditionNodes[i].error =
+      $func.conditionStr(props.nodeConfig, i) == '请设置条件' && i != props.nodeConfig.conditionNodes.length - 1
+  }
+}
+onMounted(() => {
+  if (props.nodeConfig.nodeType == 1) {
+    props.nodeConfig.error = !$func.setApproverStr(props.nodeConfig)
+  } else if (props.nodeConfig.nodeType == 4) {
+    props.nodeConfig.error = !$func.copyerStr(props.nodeConfig)
+  } else if (props.nodeConfig.nodeType == 2) {
+    resetConditionNodesErr()
+  }
+})
+let emits = defineEmits(['update:nodeConfig'])
+
+watch(workFlow, val => {
+  if (val.approverConfig1.flag && val.approverConfig1.id === _uid) {
+    emits('update:nodeConfig', val.approverConfig1.value)
+  }
+  if (val.copyerConfig1.flag && val.copyerConfig1.id === _uid) {
+    emits('update:nodeConfig', val.copyerConfig1.value)
+  }
+  if (val.conditionsConfig1.flag && val.conditionsConfig1.id === _uid) {
+    emits('update:nodeConfig', val.conditionsConfig1.value)
+  }
+})
+
+const clickEvent = index => {
+  if (index || index === 0) {
+    isInputList.value[index] = true
+  } else {
+    isInput.value = true
+  }
+}
+const blurEvent = index => {
+  if (index || index === 0) {
+    isInputList.value[index] = false
+    props.nodeConfig.conditionNodes[index].nodeName = props.nodeConfig.conditionNodes[index].nodeName || '条件'
+  } else {
+    isInput.value = false
+    props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
+  }
+}
+const delNode = () => {
+  emits('update:nodeConfig', props.nodeConfig.childNode)
+}
+const addTerm = () => {
+  let len = props.nodeConfig.conditionNodes.length + 1
+  props.nodeConfig.conditionNodes.push({
+    nodeName: '条件' + len,
+    nodeType: 3,
+    priorityLevel: len,
+    condGroup: {
+      type: 'and',
+      items: []
+    },
+    subjects: [],
+    childNode: null
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+}
+const delTerm = index => {
+  props.nodeConfig.conditionNodes.splice(index, 1)
+  props.nodeConfig.conditionNodes.map((item, index) => {
+    item.priorityLevel = index + 1
+    item.nodeName = `条件${index + 1}`
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+  if (props.nodeConfig.conditionNodes.length == 1) {
+    if (props.nodeConfig.childNode) {
+      if (props.nodeConfig.conditionNodes[0].childNode) {
+        reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
+      } else {
+        props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
+      }
+    }
+    emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
+  }
+}
+const reData = (data, addData) => {
+  if (!data.childNode) {
+    data.childNode = addData
+  } else {
+    reData(data.childNode, addData)
+  }
+}
+const setPerson = priorityLevel => {
+  var { nodeType } = props.nodeConfig
+  if (nodeType == 1) {
+    workFlow.approverDrawer = true
+    workFlow.setApproverConfig({
+      value: {
+        ...JSON.parse(JSON.stringify(props.nodeConfig)),
+        ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
+      },
+      flag: false,
+      id: _uid
+    })
+  } else if (nodeType == 4) {
+    workFlow.copyerDrawer = true
+    workFlow.setCopyerConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      flag: false,
+      id: _uid
+    })
+  } else {
+    workFlow.conditionDrawer = true
+    workFlow.setConditionsConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      priorityLevel,
+      flag: false,
+      id: _uid
+    })
+  }
+}
+const arrTransfer = (index, type = 1) => {
+  //向左-1,向右1
+  props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
+    index + type,
+    1,
+    props.nodeConfig.conditionNodes[index]
+  )[0]
+  props.nodeConfig.conditionNodes.map((item, index) => {
+    item.priorityLevel = index + 1
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+}
+</script>
+<style lang="scss">
+@import '@/css/workflow.css';
+.error_tip {
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  transform: translate(150%, 0px);
+  font-size: 24px;
+}
+
+.promoter_person .el-dialog__body {
+  padding: 10px 20px 14px 20px;
+}
+
+.selected_list {
+  margin-bottom: 20px;
+  line-height: 30px;
+}
+
+.selected_list span {
+  margin-right: 10px;
+  padding: 3px 6px 3px 9px;
+  line-height: 12px;
+  white-space: nowrap;
+  border-radius: 2px;
+  border: 1px solid rgba(220, 220, 220, 1);
+}
+
+.selected_list img {
+  margin-left: 5px;
+  width: 7px;
+  height: 7px;
+  cursor: pointer;
+}
+</style>

+ 59 - 0
src/components/workflow/selectBox.vue

@@ -0,0 +1,59 @@
+<!--
+ * @Date: 2022-08-26 17:18:14
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2022-09-21 14:36:25
+ * @FilePath: /Workflow-Vue3/src/components/selectBox.vue
+-->
+
+<template>
+  <ul class="select-box">
+    <template v-for="(elem, i) in list" :key="i">
+      <li v-for="item in elem.data" :key="item.id" class="check_box">
+        <a :class="elem.isActive(item) && 'active'" @click="elem.change(item)" :title="item.departmentNames">
+          {{ item.name }}
+        </a>
+      </li>
+    </template>
+  </ul>
+</template>
+<script setup lang="ts">
+defineProps({
+  list: {
+    type: Array,
+    default: () => []
+  }
+})
+</script>
+<style lang="scss">
+.select-box {
+  height: 420px;
+  overflow-y: auto;
+
+  li {
+    padding: 5px 0;
+
+    i {
+      float: right;
+      padding-left: 24px;
+      padding-right: 10px;
+      color: #3195f8;
+      font-size: 12px;
+      cursor: pointer;
+      background: url(~@/assets/images/workflow/next_level_active.png) no-repeat 10px center;
+      border-left: 1px solid rgb(238, 238, 238);
+    }
+
+    a.active + i {
+      color: rgb(197, 197, 197);
+      background-image: url(~@/assets/images/workflow/next_level.png);
+      pointer-events: none;
+    }
+
+    img {
+      width: 14px;
+      vertical-align: middle;
+      margin-right: 5px;
+    }
+  }
+}
+</style>

+ 104 - 0
src/components/workflow/selectResult.vue

@@ -0,0 +1,104 @@
+<!--
+ * @Date: 2022-08-26 16:29:24
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2022-09-21 14:36:30
+ * @FilePath: /Workflow-Vue3/src/components/selectResult.vue
+-->
+<template>
+  <div class="select-result l">
+    <p class="clear">
+      已选({{ total }})
+      <a @click="emits('del')">清空</a>
+    </p>
+    <ul>
+      <template v-for="{ type, data, cancel } in list" :key="type">
+        <template v-if="type === 'role'">
+          <li v-for="item in data" :key="item.roleId">
+            <img src="@/assets/images/workflow/icon_role.png" />
+            <span>{{ item.roleName }}</span>
+            <img src="@/assets/images/workflow/cancel.png" @click="cancel(item)" />
+          </li>
+        </template>
+        <template v-if="type === 'department'">
+          <li v-for="item in data" :key="item.id">
+            <img src="@/assets/images/workflow/icon_file.png" />
+            <span>{{ item.departmentName }}</span>
+            <img src="@/assets/images/workflow/cancel.png" @click="cancel(item)" />
+          </li>
+        </template>
+        <template v-if="type === 'employee'">
+          <li v-for="item in data" :key="item.id">
+            <span>{{ item.name }}</span>
+
+            <icon-close-one theme="outline" size="17" fill="#333" @click="cancel(item)"></icon-close-one>
+            <!-- <img src="@/assets/images/cancel.png" @click="cancel(item)"> -->
+          </li>
+        </template>
+      </template>
+    </ul>
+  </div>
+</template>
+<script setup>
+import { CloseOne as iconCloseOne } from '@icon-park/vue-next'
+defineProps({
+  total: {
+    type: Number,
+    default: 0
+  },
+  list: {
+    type: Array,
+    default: () => [{ type: 'role', data, cancel }]
+  }
+})
+let emits = defineEmits(['del'])
+</script>
+
+<style lang="scss">
+.select-result {
+  width: 276px;
+  height: 100%;
+  font-size: 12px;
+
+  ul {
+    height: 460px;
+    overflow-y: auto;
+
+    li {
+      margin: 11px 26px 13px 19px;
+      line-height: 17px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      span {
+        vertical-align: middle;
+      }
+
+      img {
+        &:first-of-type {
+          width: 14px;
+          vertical-align: middle;
+          margin-right: 5px;
+        }
+
+        &:last-of-type {
+          float: right;
+          margin-top: 2px;
+          width: 14px;
+        }
+      }
+    }
+  }
+
+  p {
+    padding-left: 19px;
+    padding-right: 20px;
+    line-height: 37px;
+    border-bottom: 1px solid #f2f2f2;
+
+    a {
+      float: right;
+    }
+  }
+}
+</style>

+ 38 - 0
src/css/dialog.css

@@ -0,0 +1,38 @@
+.person_body {
+  border: 1px solid #f5f5f5;
+  height: 500px;
+}
+
+.tree_nav span {
+  display: inline-block;
+  padding-right: 10px;
+  margin-right: 5px;
+  max-width: 6em;
+  color: #38adff;
+  font-size: 12px;
+  cursor: pointer;
+  background: url(~@/assets/images/workflow/jiaojiao.png) no-repeat right center;
+}
+
+.tree_nav span:last-of-type {
+  background: none;
+}
+
+.person_tree {
+  padding: 10px 12px 0 8px;
+  width: 280px;
+  height: 100%;
+  border-right: 1px solid #f5f5f5;
+}
+
+.person_tree input {
+  padding-left: 22px;
+  width: 210px;
+  height: 30px;
+  font-size: 12px;
+  border-radius: 2px;
+  border: 1px solid #d5dadf;
+  background: url(~@/assets/images/workflow/list_search.png) no-repeat 10px center;
+  background-size: 14px 14px;
+  margin-bottom: 14px;
+}

+ 1708 - 0
src/css/workflow.css

@@ -0,0 +1,1708 @@
+body {
+    background: #eee
+}
+
+@font-face {
+    font-family: Chinese Quote;
+    src: local("PingFang SC"), local("SimSun");
+    unicode-range: u+2018, u+2019, u+201c, u+201d
+}
+
+
+
+input::-ms-clear,
+input::-ms-reveal {
+    display: none
+}
+
+*,
+:after,
+:before {
+    box-sizing: border-box
+}
+
+
+
+@-ms-viewport {
+    width: device-width
+}
+
+article,
+aside,
+dialog,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section {
+    display: block
+}
+
+body {
+    margin: 0;
+    font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+    font-size: 14px;
+    font-variant: tabular-nums;
+    line-height: 1.5;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff
+}
+
+[tabindex="-1"]:focus {
+    outline: none !important
+}
+
+hr {
+    box-sizing: content-box;
+    height: 0;
+    overflow: visible
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    margin-top: 0;
+    margin-bottom: .5em;
+    color: rgba(0, 0, 0, .85);
+    font-weight: 500
+}
+
+p {
+    margin-top: 0;
+    margin-bottom: 1em
+}
+
+abbr[data-original-title],
+abbr[title] {
+    text-decoration: underline;
+    text-decoration: underline dotted;
+    cursor: help;
+    border-bottom: 0
+}
+
+address {
+    margin-bottom: 1em;
+    font-style: normal;
+    line-height: inherit
+}
+
+input[type=number],
+input[type=password],
+input[type=text],
+textarea {
+    -webkit-appearance: none
+}
+
+dl,
+ol,
+ul {
+    margin-top: 0;
+    margin-bottom: 1em
+}
+
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+    margin-bottom: 0
+}
+
+dt {
+    font-weight: 500
+}
+
+dd {
+    margin-bottom: .5em;
+    margin-left: 0
+}
+
+blockquote {
+    margin: 0 0 1em
+}
+
+dfn {
+    font-style: italic
+}
+
+b,
+strong {
+    font-weight: bolder
+}
+
+small {
+    font-size: 80%
+}
+
+sub,
+sup {
+    position: relative;
+    font-size: 75%;
+    line-height: 0;
+    vertical-align: baseline
+}
+
+sub {
+    bottom: -.25em
+}
+
+sup {
+    top: -.5em
+}
+
+a {
+    color: #1890ff;
+    background-color: transparent;
+    text-decoration: none;
+    outline: none;
+    cursor: pointer;
+    transition: color .3s;
+    -webkit-text-decoration-skip: objects
+}
+
+a:focus {
+    text-decoration: underline;
+    text-decoration-skip: auto
+}
+
+a:hover {
+    color: #40a9ff
+}
+
+a:active {
+    color: #096dd9
+}
+
+a:active,
+a:hover {
+    outline: 0;
+    text-decoration: none
+}
+
+code,
+kbd,
+pre,
+samp {
+    font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
+    font-size: 1em
+}
+
+pre {
+    margin-top: 0;
+    margin-bottom: 1em;
+    overflow: auto
+}
+
+figure {
+    margin: 0 0 1em
+}
+
+img {
+    vertical-align: middle;
+    border-style: none
+}
+
+svg:not(:root) {
+    overflow: hidden
+}
+
+[role=button],
+a,
+area,
+button,
+input:not([type=range]),
+label,
+select,
+summary,
+textarea {
+    touch-action: manipulation
+}
+
+table {
+    border-collapse: collapse
+}
+
+caption {
+    padding-top: .75em;
+    padding-bottom: .3em;
+    color: rgba(0, 0, 0, .45);
+    text-align: left;
+    caption-side: bottom
+}
+
+th {
+    text-align: inherit
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+    margin: 0;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    color: inherit
+}
+
+button,
+input {
+    overflow: visible
+}
+
+button,
+select {
+    text-transform: none
+}
+
+[type=reset],
+[type=submit],
+button,
+html [type=button] {
+    -webkit-appearance: button
+}
+
+[type=button]::-moz-focus-inner,
+[type=reset]::-moz-focus-inner,
+[type=submit]::-moz-focus-inner,
+button::-moz-focus-inner {
+    padding: 0;
+    border-style: none
+}
+
+input[type=checkbox],
+input[type=radio] {
+    box-sizing: border-box;
+    padding: 0
+}
+
+input[type=date],
+input[type=datetime-local],
+input[type=month],
+input[type=time] {
+    -webkit-appearance: listbox
+}
+
+textarea {
+    overflow: auto;
+    resize: vertical
+}
+
+fieldset {
+    min-width: 0;
+    padding: 0;
+    margin: 0;
+    border: 0
+}
+
+legend {
+    display: block;
+    width: 100%;
+    max-width: 100%;
+    padding: 0;
+    margin-bottom: .5em;
+    font-size: 1.5em;
+    line-height: inherit;
+    color: inherit;
+    white-space: normal
+}
+
+progress {
+    vertical-align: baseline
+}
+
+[type=number]::-webkit-inner-spin-button,
+[type=number]::-webkit-outer-spin-button {
+    height: auto
+}
+
+[type=search] {
+    outline-offset: -2px;
+    -webkit-appearance: none
+}
+
+[type=search]::-webkit-search-cancel-button,
+[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+::-webkit-file-upload-button {
+    font: inherit;
+    -webkit-appearance: button
+}
+
+output {
+    display: inline-block
+}
+
+summary {
+    display: list-item
+}
+
+template {
+    display: none
+}
+
+[hidden] {
+    display: none !important
+}
+
+mark {
+    padding: .2em;
+    background-color: #feffe6
+}
+
+::selection {
+    background: #1890ff;
+    color: #fff
+}
+
+.clearfix {
+    zoom: 1
+}
+
+.clearfix:after,
+.clearfix:before {
+    content: "";
+    display: table
+}
+
+.clearfix:after {
+    clear: both
+}
+
+@font-face {
+    font-family: anticon;
+    font-display: fallback;
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot");
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg")
+}
+
+.anticon {
+    display: inline-block;
+    font-style: normal;
+    vertical-align: baseline;
+    text-align: center;
+    text-transform: none;
+    line-height: 1;
+    text-rendering: optimizeLegibility;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.anticon:before {
+    display: block;
+    font-family: anticon !important
+}
+
+.anticon-close:before {
+    content: "\E633"
+}
+
+.anticon-right:before {
+    content: "\E61F"
+}
+
+.anticon-exclamation-circle {
+    color: rgb(242, 86, 67)
+}
+
+.anticon-exclamation-circle:before {
+    content: "\E62C"
+}
+
+.anticon-left:before {
+    content: "\E620"
+}
+
+.anticon-close-circle:before {
+    content: "\E62E"
+}
+
+.ant-btn {
+    line-height: 1.5;
+    display: inline-block;
+    font-weight: 400;
+    text-align: center;
+    touch-action: manipulation;
+    cursor: pointer;
+    background-image: none;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    padding: 0 15px;
+    font-size: 14px;
+    border-radius: 4px;
+    height: 32px;
+    user-select: none;
+    transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    position: relative;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    border-color: #d9d9d9
+}
+
+.ant-btn>.anticon {
+    line-height: 1
+}
+
+.ant-btn,
+.ant-btn:active,
+.ant-btn:focus {
+    outline: 0
+}
+
+.ant-btn>a:only-child {
+    color: currentColor
+}
+
+.ant-btn>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn:focus,
+.ant-btn:hover {
+    color: #40a9ff;
+    background-color: #fff;
+    border-color: #40a9ff
+}
+
+.ant-btn:focus>a:only-child,
+.ant-btn:hover>a:only-child {
+    color: currentColor
+}
+
+.ant-btn:focus>a:only-child:after,
+.ant-btn:hover>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn.active,
+.ant-btn:active {
+    color: #096dd9;
+    background-color: #fff;
+    border-color: #096dd9
+}
+
+.ant-btn.active>a:only-child,
+.ant-btn:active>a:only-child {
+    color: currentColor
+}
+
+.ant-btn.active>a:only-child:after,
+.ant-btn:active>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btn.active,
+.ant-btn:active,
+.ant-btn:focus,
+.ant-btn:hover {
+    background: #fff;
+    text-decoration: none
+}
+
+.ant-btn>i,
+.ant-btn>span {
+    pointer-events: none
+}
+
+.ant-btn:before {
+    position: absolute;
+    top: -1px;
+    left: -1px;
+    bottom: -1px;
+    right: -1px;
+    background: #fff;
+    opacity: .35;
+    content: "";
+    border-radius: inherit;
+    z-index: 1;
+    transition: opacity .2s;
+    pointer-events: none;
+    display: none
+}
+
+.ant-btn .anticon {
+    transition: margin-left .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.ant-btn:active>span,
+.ant-btn:focus>span {
+    position: relative
+}
+
+.ant-btn>.anticon+span,
+.ant-btn>span+.anticon {
+    margin-left: 8px
+}
+
+.fd-nav-container {
+    display: inline-block;
+    position: relative
+}
+
+.fd-nav-container .ghost-bar {
+    position: absolute;
+    width: 150px;
+    height: 100%;
+    left: 0;
+    background: #1583f2;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.fd-nav-container .ghost-bar:after {
+    content: "";
+    position: absolute;
+    bottom: 0;
+    left: 50%;
+    margin-left: -5px;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 5px 6px;
+    border-color: transparent transparent #f6f6f6
+}
+
+.fd-nav-item {
+    position: relative;
+    cursor: pointer;
+    display: inline-block;
+    line-height: 60px;
+    width: 150px;
+    text-align: center;
+    white-space: nowrap
+}
+
+.fd-nav-item .order-num {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    line-height: 20px;
+    border: 1px solid #fff;
+    border-radius: 50%;
+    margin-right: 6px;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.fd-nav-item.active .order-num {
+    color: #1583f2;
+    background: #fff
+}
+
+.ant-input {
+    font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+    font-variant: tabular-nums;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    position: relative;
+    display: inline-block;
+    padding: 4px 11px;
+    width: 100%;
+    height: 32px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #d9d9d9;
+    border-radius: 4px;
+    transition: all .3s
+}
+
+.ant-input::-moz-placeholder {
+    color: #bfbfbf;
+    opacity: 1
+}
+
+.ant-input:-ms-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input::-webkit-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input:focus,
+.ant-input:hover {
+    border-color: #40a9ff;
+    border-right-width: 1px !important
+}
+
+.ant-input:focus {
+    outline: 0;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, .2)
+}
+
+textarea.ant-input {
+    max-width: 100%;
+    height: auto;
+    vertical-align: bottom;
+    transition: all .3s, height 0s;
+    min-height: 32px
+}
+
+a,
+abbr,
+acronym,
+address,
+applet,
+article,
+aside,
+audio,
+b,
+big,
+blockquote,
+body,
+canvas,
+caption,
+center,
+cite,
+code,
+dd,
+del,
+details,
+dfn,
+div,
+dl,
+dt,
+em,
+fieldset,
+figcaption,
+figure,
+footer,
+form,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+header,
+hgroup,
+html,
+i,
+iframe,
+img,
+ins,
+kbd,
+label,
+legend,
+li,
+mark,
+menu,
+nav,
+object,
+ol,
+p,
+pre,
+q,
+s,
+samp,
+section,
+small,
+span,
+strike,
+strong,
+sub,
+summary,
+sup,
+table,
+tbody,
+td,
+tfoot,
+th,
+thead,
+time,
+tr,
+tt,
+u,
+ul,
+var,
+video {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline
+}
+
+*,
+:after,
+:before {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box
+}
+
+html {
+    font-family: sans-serif;
+    -ms-text-size-adjust: 100%;
+    -webkit-text-size-adjust: 100%
+}
+
+body,
+html {
+    font-size: 14px
+}
+
+body {
+    font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif;
+    line-height: 1.6;
+    background-color: #fff;
+    position: static !important;
+    -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
+}
+
+ol,
+ul {
+    list-style-type: none
+}
+
+b,
+strong {
+    font-weight: 700
+}
+
+img {
+    border: 0
+}
+
+button,
+input,
+select,
+textarea {
+    font-family: inherit;
+    font-size: 100%;
+    margin: 0
+}
+
+textarea {
+    overflow: auto;
+    vertical-align: top;
+    -webkit-appearance: none
+}
+
+button,
+input {
+    line-height: normal
+}
+
+button,
+select {
+    text-transform: none
+}
+
+button,
+html input[type=button],
+input[type=reset],
+input[type=submit] {
+    -webkit-appearance: button;
+    cursor: pointer
+}
+
+input[type=search] {
+    -webkit-appearance: textfield;
+    -moz-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+    box-sizing: content-box
+}
+
+input[type=search]::-webkit-search-cancel-button,
+input[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0
+}
+
+table {
+    width: 100%;
+    border-spacing: 0;
+    border-collapse: collapse
+}
+
+table,
+td,
+th {
+    border: 0
+}
+
+td,
+th {
+    padding: 0;
+    vertical-align: top
+}
+
+th {
+    font-weight: 700;
+    text-align: left
+}
+
+thead th {
+    white-space: nowrap
+}
+
+a {
+    text-decoration: none;
+    cursor: pointer;
+    color: #3296fa
+}
+
+a:active,
+a:hover {
+    outline: 0;
+    color: #3296fa
+}
+
+small {
+    font-size: 80%
+}
+
+body,
+html {
+    font-size: 12px !important;
+    color: #191f25 !important;
+    background: #f6f6f6 !important
+}
+
+.wrap {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    height: 100%
+}
+
+@font-face {
+    font-family: IconFont;
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
+}
+
+.iconfont {
+    font-family: IconFont !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-stroke-width: .2px;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.fd-nav {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 997;
+    width: 100%;
+    height: 60px;
+    font-size: 14px;
+    color: #fff;
+    background: #3296fa;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav>* {
+    flex: 1;
+    width: 100%
+}
+
+.fd-nav .fd-nav-left {
+    display: -webkit-box;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav .fd-nav-center {
+    flex: none;
+    width: 600px;
+    text-align: center
+}
+
+.fd-nav .fd-nav-right {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    text-align: right
+}
+
+.fd-nav .fd-nav-back {
+    display: inline-block;
+    width: 60px;
+    height: 60px;
+    font-size: 22px;
+    border-right: 1px solid #1583f2;
+    text-align: center;
+    cursor: pointer
+}
+
+.fd-nav .fd-nav-back:hover {
+    background: #5af
+}
+
+.fd-nav .fd-nav-back:active {
+    background: #1583f2
+}
+
+.fd-nav .fd-nav-back .anticon {
+    line-height: 60px
+}
+
+.fd-nav .fd-nav-title {
+    width: 0;
+    flex: 1;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    padding: 0 15px
+}
+
+.fd-nav a {
+    color: #fff;
+    margin-left: 12px
+}
+
+.fd-nav .button-publish {
+    min-width: 80px;
+    margin-left: 4px;
+    margin-right: 15px;
+    color: #3296fa;
+    border-color: #fff
+}
+
+.fd-nav .button-publish.ant-btn:focus,
+.fd-nav .button-publish.ant-btn:hover {
+    color: #3296fa;
+    border-color: #fff;
+    box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3)
+}
+
+.fd-nav .button-publish.ant-btn:active {
+    color: #3296fa;
+    background: #d6eaff;
+    box-shadow: none
+}
+
+.fd-nav .button-preview {
+    min-width: 80px;
+    margin-left: 16px;
+    margin-right: 4px;
+    color: #fff;
+    border-color: #fff;
+    background: transparent
+}
+
+.fd-nav .button-preview.ant-btn:focus,
+.fd-nav .button-preview.ant-btn:hover {
+    color: #fff;
+    border-color: #fff;
+    background: #59acfc
+}
+
+.fd-nav .button-preview.ant-btn:active {
+    color: #fff;
+    border-color: #fff;
+    background: #2186ef
+}
+
+.fd-nav-content {
+    position: fixed;
+    top: 60px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    overflow-x: hidden;
+    overflow-y: auto;
+    padding-bottom: 30px
+}
+
+.error-modal-desc {
+    font-size: 13px;
+    color: rgba(25, 31, 37, .56);
+    line-height: 22px;
+    margin-bottom: 14px
+}
+
+.error-modal-list {
+    height: 200px;
+    overflow-y: auto;
+    margin-right: -25px;
+    padding-right: 25px
+}
+
+.error-modal-item {
+    padding: 10px 20px;
+    line-height: 21px;
+    background: #f6f6f6;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    border-radius: 4px
+}
+
+.error-modal-item-label {
+    flex: none;
+    font-size: 15px;
+    color: rgba(25, 31, 37, .56);
+    padding-right: 10px
+}
+
+.error-modal-item-content {
+    text-align: right;
+    flex: 1;
+    font-size: 13px;
+    color: #191f25
+}
+
+#body.blur {
+    -webkit-filter: blur(3px);
+    filter: blur(3px)
+}
+
+.zoom {
+    display: flex;
+    position: absolute;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -webkit-box-pack: justify;
+    -ms-flex-pack: justify;
+    justify-content: space-between;
+    height: 40px;
+    width: 125px;
+    right: 40px;
+    margin-top: 30px;
+    z-index: 10
+}
+
+.zoom .zoom-in,
+.zoom .zoom-out {
+    width: 30px;
+    height: 30px;
+    background: #fff;
+    color: #c1c1cd;
+    cursor: pointer;
+    background-size: 100%;
+    background-repeat: no-repeat
+}
+
+.zoom .zoom-out {
+    background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png)
+}
+
+.zoom .zoom-out.disabled {
+    opacity: .5
+}
+
+.zoom .zoom-in {
+    background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png)
+}
+
+.zoom .zoom-in.disabled {
+    opacity: .5
+}
+
+.auto-judge:hover .editable-title,
+.node-wrap-box:hover .editable-title {
+    border-bottom: 1px dashed #fff
+}
+
+.auto-judge:hover .editable-title.editing,
+.node-wrap-box:hover .editable-title.editing {
+    text-decoration: none;
+    border: 1px solid #d9d9d9
+}
+
+.auto-judge:hover .editable-title {
+    border-color: #15bc83
+}
+
+.editable-title {
+    line-height: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    border-bottom: 1px dashed transparent
+}
+
+.editable-title:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 40px
+}
+
+.editable-title:hover {
+    border-bottom: 1px dashed #fff
+}
+
+.editable-title-input {
+    flex: none;
+    height: 18px;
+    padding-left: 4px;
+    text-indent: 0;
+    font-size: 12px;
+    line-height: 18px;
+    z-index: 1
+}
+
+.editable-title-input:hover {
+    text-decoration: none
+}
+
+.ant-btn {
+    position: relative
+}
+
+.node-wrap-box {
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    position: relative;
+    width: 220px;
+    min-height: 72px;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    background: #fff;
+    border-radius: 4px;
+    cursor: pointer
+}
+
+.node-wrap-box:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box.active:after,
+.node-wrap-box:active:after,
+.node-wrap-box:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.node-wrap-box.active .close,
+.node-wrap-box:active .close,
+.node-wrap-box:hover .close {
+    display: block
+}
+
+.node-wrap-box.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box .title {
+    position: relative;
+    display: flex;
+    align-items: center;
+    padding-left: 16px;
+    padding-right: 30px;
+    width: 100%;
+    height: 24px;
+    line-height: 24px;
+    font-size: 12px;
+    color: #fff;
+    text-align: left;
+    background: #576a95;
+    border-radius: 4px 4px 0 0
+}
+
+.node-wrap-box .title .iconfont {
+    font-size: 12px;
+    margin-right: 5px
+}
+
+.node-wrap-box .placeholder {
+    color: #bfbfbf
+}
+
+.node-wrap-box .close {
+    display: none;
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: #fff;
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px
+}
+
+.node-wrap-box .content {
+    position: relative;
+    font-size: 14px;
+    padding: 16px;
+    padding-right: 30px
+}
+
+.node-wrap-box .content .text {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+
+.node-wrap-box .content .arrow {
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 14px;
+    font-size: 14px;
+    color: #979797
+}
+
+.start-node.node-wrap-box .content .text {
+    display: block;
+    white-space: nowrap
+}
+
+.node-wrap-box:before {
+    content: "";
+    position: absolute;
+    top: -12px;
+    left: 50%;
+    -webkit-transform: translateX(-50%);
+    transform: translateX(-50%);
+    width: 0;
+    height: 4px;
+    border-style: solid;
+    border-width: 8px 6px 4px;
+    border-color: #cacaca transparent transparent;
+    background: #f5f5f7
+}
+
+.node-wrap-box.start-node:before {
+    content: none
+}
+
+.top-left-cover-line {
+    left: -1px
+}
+
+.top-left-cover-line,
+.top-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    top: -4px
+}
+
+.top-right-cover-line {
+    right: -1px
+}
+
+.bottom-left-cover-line {
+    left: -1px
+}
+
+.bottom-left-cover-line,
+.bottom-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    bottom: -4px
+}
+
+.bottom-right-cover-line {
+    right: -1px
+}
+
+.dingflow-design {
+    background-color: #f5f5f7;
+    overflow: auto;
+    height: 800px;
+}
+
+.dingflow-design .box-scale {
+    transform: scale(1);
+    display: inline-block;
+    position: relative;
+    width: 100%;
+    padding: 54.5px 0;
+    -webkit-box-align: start;
+    -ms-flex-align: start;
+    align-items: flex-start;
+    -webkit-box-pack: center;
+    -ms-flex-pack: center;
+    justify-content: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    min-width: -webkit-min-content;
+    min-width: -moz-min-content;
+    min-width: min-content;
+    background-color: #f5f5f7;
+    transform-origin: 50% 0px 0px;
+}
+
+.dingflow-design .node-wrap {
+    flex-direction: column;
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    justify-content: flex-start;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    padding: 0 50px;
+    position: relative
+}
+
+.dingflow-design .branch-wrap,
+.dingflow-design .node-wrap {
+    display: inline-flex;
+    width: 100%
+}
+
+.dingflow-design .branch-box-wrap {
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    min-height: 270px;
+    width: 100%;
+    -ms-flex-negative: 0;
+    flex-shrink: 0
+}
+
+.dingflow-design .branch-box {
+    display: flex;
+    overflow: visible;
+    min-height: 180px;
+    height: auto;
+    border-bottom: 2px solid #ccc;
+    border-top: 2px solid #ccc;
+    position: relative;
+    margin-top: 15px
+}
+
+.dingflow-design .branch-box .col-box {
+    background: #f5f5f7
+}
+
+.dingflow-design .branch-box .col-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.dingflow-design .add-branch {
+    border: none;
+    outline: none;
+    user-select: none;
+    justify-content: center;
+    font-size: 12px;
+    padding: 0 10px;
+    height: 30px;
+    line-height: 30px;
+    border-radius: 15px;
+    color: #3296fa;
+    background: #fff;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
+    position: absolute;
+    top: -16px;
+    left: 50%;
+    transform: translateX(-50%);
+    transform-origin: center center;
+    cursor: pointer;
+    z-index: 1;
+    display: inline-flex;
+    align-items: center;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.dingflow-design .add-branch:hover {
+    transform: translateX(-50%) scale(1.1);
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .add-branch:active {
+    transform: translateX(-50%);
+    box-shadow: none
+}
+
+.dingflow-design .col-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-align: center;
+    align-items: center;
+    position: relative
+}
+
+.dingflow-design .condition-node {
+    min-height: 220px
+}
+
+.dingflow-design .condition-node,
+.dingflow-design .condition-node-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-flex: 1
+}
+
+.dingflow-design .condition-node-box {
+    padding-top: 30px;
+    padding-right: 50px;
+    padding-left: 50px;
+    -webkit-box-pack: center;
+    justify-content: center;
+    -webkit-box-align: center;
+    align-items: center;
+    flex-grow: 1;
+    position: relative
+}
+
+.dingflow-design .condition-node-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.dingflow-design .auto-judge {
+    position: relative;
+    width: 220px;
+    min-height: 72px;
+    background: #fff;
+    border-radius: 4px;
+    padding: 14px 19px;
+    cursor: pointer
+}
+
+.dingflow-design .auto-judge:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge.active:after,
+.dingflow-design .auto-judge:active:after,
+.dingflow-design .auto-judge:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.dingflow-design .auto-judge.active .close,
+.dingflow-design .auto-judge:active .close,
+.dingflow-design .auto-judge:hover .close {
+    display: block
+}
+
+.dingflow-design .auto-judge.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge .title-wrapper {
+    position: relative;
+    font-size: 12px;
+    color: #15bc83;
+    text-align: left;
+    line-height: 16px
+}
+
+.dingflow-design .auto-judge .title-wrapper .editable-title {
+    display: inline-block;
+    max-width: 120px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis
+}
+
+.dingflow-design .auto-judge .title-wrapper .priority-title {
+    display: inline-block;
+    float: right;
+    margin-right: 10px;
+    color: rgba(25, 31, 37, .56)
+}
+
+.dingflow-design .auto-judge .placeholder {
+    color: #bfbfbf
+}
+
+.dingflow-design .auto-judge .close {
+    display: none;
+    position: absolute;
+    right: -10px;
+    top: -10px;
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: rgba(0, 0, 0, .25);
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px;
+    z-index: 2
+}
+
+.dingflow-design .auto-judge .content {
+    font-size: 14px;
+    color: #191f25;
+    text-align: left;
+    margin-top: 6px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+
+.dingflow-design .auto-judge .sort-left,
+.dingflow-design .auto-judge .sort-right {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    display: none;
+    z-index: 1
+}
+
+.dingflow-design .auto-judge .sort-left {
+    left: 0;
+    border-right: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge .sort-right {
+    right: 0;
+    border-left: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge:hover .sort-left,
+.dingflow-design .auto-judge:hover .sort-right {
+    display: flex;
+    align-items: center
+}
+
+.dingflow-design .auto-judge .sort-left:hover,
+.dingflow-design .auto-judge .sort-right:hover {
+    background: #efefef
+}
+
+.dingflow-design .end-node {
+    border-radius: 50%;
+    font-size: 14px;
+    color: rgba(25, 31, 37, .4);
+    text-align: left
+}
+
+.dingflow-design .end-node .end-node-circle {
+    width: 10px;
+    height: 10px;
+    margin: auto;
+    border-radius: 50%;
+    background: #dbdcdc
+}
+
+.dingflow-design .end-node .end-node-text {
+    margin-top: 5px;
+    text-align: center
+}
+
+.approval-setting {
+    border-radius: 2px;
+    margin: 20px 0;
+    position: relative;
+    background: #fff
+}
+
+.ant-btn {
+    position: relative
+}

+ 9 - 0
src/router/asyncRouter.ts

@@ -19,6 +19,15 @@ const asyncRouter: RouteRecordRaw[] = [
       icon: 'Aim'
     }
   },
+  {
+    path: '/workflow/workflow',
+    name: 'workflow',
+    component: () => import('@/views/workflow/Workflow.vue'),
+    meta: {
+      title: '表单与流程设计',
+      icon: 'Notebook'
+    }
+  },
   {
     path: '/system',
     name: 'system',

+ 53 - 0
src/stores/designer.ts

@@ -0,0 +1,53 @@
+import type { BasicFormItem, BasicForm } from '@/types/form'
+import type { FormProps } from 'element-plus'
+
+export const useFormDesignerStore = defineStore({
+  id: 'formDesigner',
+  state: () => ({
+    formItems: <BasicFormItem[]>[],
+    formProps: <Partial<FormProps>>{
+      labelWidth: 100,
+      labelPosition: 'top',
+      size: 'large'
+    },
+    formSpan: 24,
+    curFormItem: <any>{}
+  }),
+  getters: {
+    tableItems: state => state.formItems.filter(item => item.list),
+    downloadFormItems: state => {
+      return state.formItems.map(item => {
+        const element = JSON.parse(JSON.stringify(item))
+        delete element.id
+        delete element.list
+        !element.children.length && delete element.children
+        !element.options.length && delete element.options
+        !element.required && delete element.required
+        !element.search && delete element.search
+        !element.request && delete element.request
+        if (element.span === 12) {
+          delete element.span
+        }
+        return element
+      })
+    }
+  },
+  actions: {
+    setFormItems(formItem: BasicFormItem) {
+      this.formItems.push(formItem)
+    },
+    setCurFormItem(formItem: BasicFormItem) {
+      this.curFormItem = formItem
+    }
+  }
+})
+
+
+export const designerType = defineStore({
+  id: 'Type',
+  state: () => ({
+    type:0
+  }),
+  getters: {},
+  actions: {}
+})

+ 43 - 0
src/stores/workflow.ts

@@ -0,0 +1,43 @@
+export const useWorkflow = defineStore({
+  id: 'work',
+  state: () => ({
+    isTried: false,
+    promoterDrawer: false,
+    approverDrawer: false,
+    approverConfig1: {},
+    approverConfig: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    copyerConfig:{},
+    conditionDrawer: false,
+    conditionsConfig1: {
+      conditionNodes: [],
+    },
+  }),
+  actions: {
+    setIsTried(isTried:any) {
+      this.isTried = isTried
+    },
+    setPromoter(promoterDrawer:any) {
+      this.promoterDrawer = promoterDrawer
+    },
+    setApprover(approverDrawer:any) {
+      this.approverDrawer = approverDrawer
+    },
+    setApproverConfig(approverConfig1:any) {
+      this.approverConfig1 = approverConfig1
+    },
+    setCopyer(copyerDrawer:any) {
+      this.copyerDrawer = copyerDrawer
+    },
+    setCopyerConfig(copyerConfig1:any) {
+      this.copyerConfig1 = copyerConfig1
+    },
+    setCondition(conditionDrawer:any) {
+      this.conditionDrawer = conditionDrawer
+    },
+    setConditionsConfig(conditionsConfig1:any) {
+      this.conditionsConfig1 = conditionsConfig1
+    },
+  }
+})

+ 108 - 0
src/utils/preload.ts

@@ -0,0 +1,108 @@
+function All() {}
+All.prototype = {
+  timer: '',
+  arrToStr(arr: any) {
+    if (arr) {
+      return arr
+        .map((item: any) => {
+          return item.subjectName
+        })
+        .toString()
+    }
+  },
+  toggleClass(arr: any, elem: any, key = 'id') {
+    return arr.some((item: any) => {
+      return item[key] == elem[key]
+    })
+  },
+  toChecked(arr: any, elem: any, key = 'id') {
+    var isIncludes = this.toggleClass(arr, elem, key)
+    !isIncludes ? arr.push(elem) : this.removeEle(arr, elem, key)
+  },
+  removeEle(arr: any, elem: any, key = 'id') {
+    var includesIndex
+    arr.map((item: any, index: any) => {
+      if (item[key] == elem[key]) {
+        includesIndex = index
+      }
+    })
+    arr.splice(includesIndex, 1)
+  },
+  setApproverStr(nodeConfig: any) {
+    if (nodeConfig.settype == 1 || nodeConfig.settype == 3) {
+      if (nodeConfig.subjects.length == 1) {
+        return nodeConfig.subjects[0].subjectName
+      } else if (nodeConfig.subjects.length > 1) {
+        if (nodeConfig.examineMode == 1) {
+          return this.arrToStr(nodeConfig.subjects)
+        } else if (nodeConfig.examineMode == 2) {
+          return nodeConfig.subjects.length + '人会签'
+        }
+      }
+    } else if (nodeConfig.settype == 2) {
+      let level = nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
+      if (nodeConfig.examineMode == 1) {
+        return level
+      } else if (nodeConfig.examineMode == 2) {
+        return level + '会签'
+      }
+    } else if (nodeConfig.settype == 4) {
+      if (nodeConfig.selectRange == 1) {
+        return '发起人自选'
+      } else {
+        if (nodeConfig.subjects.length > 0) {
+          if (nodeConfig.selectRange == 2) {
+            return '发起人自选'
+          } else {
+            return '发起人从' + nodeConfig.subjects[0].name + '中自选'
+          }
+        } else {
+          return ''
+        }
+      }
+    } else if (nodeConfig.settype == 5) {
+      return '发起人自己'
+    }
+  },
+  dealStr(str: any, obj: any) {
+    let arr = ref<any>([])
+    let list = str.split(',')
+    for (var elem in obj) {
+      list.map((item: any) => {
+        if (item == elem) {
+          arr.push(obj[elem].value)
+        }
+      })
+    }
+    return arr.join('或')
+  },
+  conditionStr(nodeConfig: any, index: any) {
+    var { condGroup, subjects } = nodeConfig.conditionNodes[index]
+    if (condGroup.items.length == 0) {
+      return index == nodeConfig.conditionNodes.length - 1 && nodeConfig.conditionNodes[0].condGroup.items.length != 0
+        ? '其他条件进入此流程'
+        : '请设置条件'
+    } else {
+      let str = '已设置条件'
+
+      return str ? str : '请设置条件'
+    }
+  },
+  copyerStr(nodeConfig: any) {
+    if (nodeConfig.copyTo.length != 0) {
+      return this.arrToStr(nodeConfig.copyTo)
+    } else {
+      if (nodeConfig.ccSelfSelectFlag == 1) {
+        return '发起人自选'
+      }
+    }
+  },
+  toggleStrClass(item: any, key: any) {
+    let a = item.zdy1 ? item.zdy1.split(',') : []
+    return a.some((item: any) => {
+      return item == key
+    })
+  }
+}
+
+export default new All()

+ 72 - 0
src/utils/utils.ts

@@ -1,4 +1,6 @@
 import dayjs from 'dayjs'
+import type { BasicFormItem } from '@/types/form'
+import { useFormDesignerStore } from '@/stores/designer'
 
 export const formatDate = (date: any, format = 'YYYY-MM-DD HH:mm') => {
   return dayjs(date).format(format)
@@ -7,3 +9,73 @@ export const formatDate = (date: any, format = 'YYYY-MM-DD HH:mm') => {
 export const isAbsolutePath = (path: string) => {
   return path.startsWith('http')
 }
+
+
+export const uuid = (len = 16) => {
+  const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
+  const uuid = []
+
+  if (len) {
+    // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
+    for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * chars.length)]
+  } else {
+    let r
+    // rfc4122标准要求返回的uuid中,某些位为固定的字符
+    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
+    uuid[14] = '4'
+
+    for (let i = 0; i < 36; i++) {
+      if (!uuid[i]) {
+        r = 0 | (Math.random() * 16)
+        uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]
+      }
+    }
+  }
+
+  return uuid.join('')
+}
+
+export const compToFormItem = (comp: any) => {
+  const id = uuid(8)
+  const formDesignerStore = useFormDesignerStore()
+  console.log("comp",comp);
+  
+  const formItem: BasicFormItem = {
+    id,
+    label: comp.name,
+    value: '',
+    name: comp.type + '_' + id,
+    type: comp.type,
+    children: [],
+    options: [],
+    search: false,
+    list: true,
+    required: false,
+    rules: [],
+    props: comp.props || {},
+    span: formDesignerStore.formSpan,
+    slots: comp.slots,
+    request:comp.request
+  }
+  if (['editor'].includes(comp.type)) {
+    formItem.span = 24
+  }
+  return formItem
+}
+
+export const copyFormItem = (formItem: BasicFormItem) => {
+  const id = uuid(8)
+  return {
+    ...JSON.parse(JSON.stringify(formItem)),
+    id,
+    name: formItem.type + '_' + id
+  }
+}
+
+export const placeholder = (item: BasicFormItem) => {
+  if (['select', 'cascader', 'date-picker', 'time-picker', 'time-select', 'dict'].includes(item.type)) {
+    return '请选择' + item.label
+  } else {
+    return '请输入' + item.label
+  }
+}

+ 90 - 0
src/views/workflow/Workflow.vue

@@ -0,0 +1,90 @@
+<script setup lang="ts">
+import type { BasicForm, ICRUD } from '@/types/form'
+import { getForm, getWorkflows } from '@/api/workflow'
+import { useFormDesignerStore } from '@/stores/designer'
+import DynamicFormEdit from '@/components/DynamicFormEdit.vue'
+let dynamicFormEdit = ref<any>(null)
+const tableRef = ref<any>(null)
+const selection = ref(false)
+const showToolbar = ref(false)
+const tableConfig = ref({
+  showEdit: false,
+  showDelete: true
+})
+const formDesignerStore = useFormDesignerStore()
+const CRUD: ICRUD = {
+  create(data: any) {},
+  update(data: any) {},
+  getList(data: any) {
+    return getWorkflows(data).then((res: any) => {
+      return {
+        list: res.data.workflows,
+        total: res.data.totalCount
+      }
+    })
+  },
+  delete(data: any) {}
+}
+const handleAdd = () => {
+  dynamicFormEdit.value.init({
+    id: ''
+  })
+}
+
+const handleEdit = (row: any) => {
+  getForm({ workflowId: row.id }).then((res: any) => {
+    formDesignerStore.formItems = JSON.parse(res.form.formExpr).formItems
+  })
+  dynamicFormEdit.value.init({
+    id: row.id,
+    name: row.name,
+    state: row.state
+  })
+}
+
+const formConfig = reactive<BasicForm>({
+  span: 12,
+  props: {
+    labelPosition: 'right',
+    size: 'default'
+  },
+  formItems: []
+})
+const handleCircle = (row: any) => {
+  tableRef.value.refresh()
+}
+</script>
+
+<template>
+  <pro-table
+    :crud="CRUD"
+    ref="tableRef"
+    :formConfig="formConfig"
+    :tableConfig="tableConfig"
+    :selection="selection"
+    :showToolbar="showToolbar"
+  >
+    <template #header>
+      <div class="flex justify-between mb-20px">
+        <div>
+          <el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
+        </div>
+      </div>
+    </template>
+    <vxe-column field="name" title="流程类型"></vxe-column>
+    <vxe-column field="stateRemark" title="状态">
+      <template #default="{ row }">
+        <el-tag :type="row.state == 1 ? 'danger' : 'success'">
+          {{ row.stateRemark }}
+        </el-tag>
+      </template>
+    </vxe-column>
+    <vxe-column field="createTime" title="创建时间"></vxe-column>
+    <template #operateBefore="{ row }">
+      <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
+    </template>
+  </pro-table>
+  <dynamic-form-edit ref="dynamicFormEdit" @clickChange="handleCircle"></dynamic-form-edit>
+</template>
+
+<style lang="scss" scoped></style>