ソースを参照

feat(流程设计器): 新增流程设计器核心组件及功能

zhanghaijun 1 年間 前
親
コミット
c10cc96515

+ 31 - 15
.vscode/vue3.code-snippets

@@ -1,23 +1,39 @@
 {
-  "Uni-Plus Vue3 SFC": {
+  "Vue3 Component": {
     "scope": "vue",
     "prefix": "vue3",
-    "description": "vue3 的 文件模板",
+    "description": "Vue3 + TypeScript 组件模板",
     "body": [
-      "<!--",
-      "@description: $1",
-      "@creationTime: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
-      "-->\n",
+     "<!--",
+      "  @desc: $1",
+      "  @date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
+      "-->",
+      "",
       "<template>",
-      "  <div>$3</div>",
-      "</template>\n",
-      "<script lang=\"ts\" setup>",
-      "defineOptions({ name: '$1' })",
-      "/* ------------------------ 导入 与 引用 ----------------------------------- */",
-      "/* ------------------------ 函数 与 方法 ----------------------------------- */",
-      "/* ------------------------- 生命周期 -------------------------------------- */",
-      "</script>\n",
-      "<style lang=\"scss\" scoped></style>\n"
+      "  <div class=\"$TM_FILENAME_BASE\">{{ title }} </div>",
+      "</template>",
+      "",
+      "<script setup lang=\"ts\">",
+      "  /** -------------------- 组件参数 -------------------- */",
+      "  interface Props {",
+      "    title?: string",
+      "  }",
+      "  const props = withDefaults(defineProps<Props>(), {",
+      "    title: 'hello'",
+      "  })",
+      "",
+      "  /** -------------------- 生命周期 ----------------------- */",
+      "  onMounted(() => {",
+      "    console.log(`${props.title} mounted`)",
+      "  })",
+      "</script>",
+      "",
+      "<style scoped lang=\"scss\">",
+      "  .${TM_FILENAME_BASE} {",
+      "    padding: 10px;",
+      "  }",
+      "</style>",
+      ""
     ]
   }
 }

+ 6 - 24
src/api/sys/dept.ts

@@ -3,49 +3,31 @@ import request from '@/utils/http'
 export class DeptService {
   // 分页查询
   static page(params: any) {
-    return request.post({
-      url: '/sys/dept/page',
-      params
-    })
+    return request.post({ url: '/sys/dept/page', params })
   }
 
   // 树形接口
   static tree(params: any) {
-    return request.post({
-      url: '/sys/dept/tree',
-      params
-    })
+    return request.post({ url: '/sys/dept/tree', params })
   }
 
   // 添加
   static save(params: any) {
-    return request.post({
-      url: '/sys/dept/save',
-      params
-    })
+    return request.post({ url: '/sys/dept/save', params })
   }
 
   // 修改
   static update(params: any) {
-    return request.post({
-      url: '/sys/dept/update',
-      params
-    })
+    return request.post({ url: '/sys/dept/update', params })
   }
 
   // 删除
   static remove(params: any) {
-    return request.post({
-      url: '/sys/dept/remove',
-      params
-    })
+    return request.post({ url: '/sys/dept/remove', params })
   }
 
   // 详情
   static detail(params: any) {
-    return request.post({
-      url: '/sys/dept/detail',
-      params
-    })
+    return request.post({ url: '/sys/dept/detail', params })
   }
 }

+ 3 - 3
src/components/business/EasyProcess/common/common.util.ts

@@ -10,10 +10,10 @@ export const getUUID = (): string => {
  * 复制对象(深拷贝)
  * @param source
  */
-export const copy = (source: object): string | null => {
+export const copy = <T>(source: T): T => {
   if (source) {
     const str = JSON.stringify(source)
-    return JSON.parse(str)
+    return JSON.parse(str) as T
   }
-  return null
+  return source
 }

+ 5 - 0
src/components/business/EasyProcess/common/data.ts

@@ -1,5 +1,10 @@
 import { EasyProcess } from './data.type'
 
+export const ProcessConstant = {
+  KEY_VALIDATOR: 'validator',
+  KEY_PROCESS_DATA: 'processData'
+}
+
 export const nodeConfigData: Record<EasyProcess.NodeType, EasyProcess.Node> = {
   start: {
     title: '开始',

+ 7 - 0
src/components/business/EasyProcess/core/node-add.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 7 - 0
src/components/business/EasyProcess/core/node-router.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 7 - 0
src/components/business/EasyProcess/core/node-wrap.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 29 - 0
src/components/business/EasyProcess/core/node_end.vue

@@ -0,0 +1,29 @@
+<template>
+  <!-- 结束节点 -->
+  <div class="ep-node-end">
+    <div class="ep-node-end-icon"></div>
+    <div class="ep-node-end-text">流程结束</div>
+  </div>
+</template>
+
+<script setup lang="ts" name="EndNode"></script>
+
+<style lang="scss" scoped>
+  .ep-node-end {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .ep-node-end-icon {
+      width: 15px;
+      height: 15px;
+      border-radius: 10px;
+      background-color: #cacaca;
+    }
+    .ep-node-end-text {
+      color: #5a5e66;
+      margin-bottom: 100px;
+    }
+  }
+</style>

+ 7 - 0
src/components/business/EasyProcess/core/node_start.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 113 - 0
src/components/business/EasyProcess/index2.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="ep-container">
+    <!-- 流程 -->
+    <div class="ep-process" :style="`transform: scale(${zoom / 100});`">
+      <!-- 递归节点 -->
+      <NodeWrap v-if="processData" :node="processData" />
+      <!-- 结束节点 -->
+      <NodeEnd />
+    </div>
+    <!-- 缩放 -->
+    <div class="ep-zoom">
+      <el-icon class="ep-zoom-icon" color="#ffffff" :class="zoom == 50 && 'disabled'" @click="setZoom(-10)">
+        <Minus />
+      </el-icon>
+      <span>{{ zoom }}%</span>
+      <el-icon class="ep-zoom-icon" color="#ffffff" :class="zoom == 300 && 'disabled'" @click="setZoom(10)">
+        <Plus />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="ProcessDesigner">
+  import { ref, provide, nextTick, watch } from 'vue'
+  import { Minus, Plus } from '@element-plus/icons-vue'
+  import NodeWrap from './core/node-wrap.vue'
+  import NodeEnd from './core/node-end.vue'
+  import { copy } from './common/common.util'
+  import { Validator } from './common/validator.util'
+  import { blankProcessData, ProcessConstant } from './common/data'
+  import { EasyProcess } from './common/data.type'
+
+  const props = defineProps<{ data?: EasyProcess.NodeConfig }>()
+
+  const processData = ref<EasyProcess.NodeConfig>(props.data || blankProcessData)
+  provide(ProcessConstant.KEY_PROCESS_DATA, processData)
+
+  // 初始化流程数据
+  const initProcess = (data?: EasyProcess.NodeConfig) => {
+    processData.value = copy(data || blankProcessData)
+    nextTick(() => validate())
+  }
+  watch(
+    () => props.data,
+    (val) => {
+      if (val && Object.keys(val).length) {
+        initProcess(val)
+      }
+    },
+    { immediate: true, deep: true }
+  )
+
+  /* ====== 节点数据验证 ====== start */
+  // 注册节点验证器
+  let validator = new Validator()
+  provide(ProcessConstant.KEY_VALIDATOR, validator)
+  // 验证流程节点数据
+  const validate = (callback?: (valid: boolean, messages: string[]) => void) => {
+    let result = validator.validate()
+    if (callback && typeof callback === 'function') {
+      callback(result.valid, result.messages)
+    }
+  }
+  /* ====== 节点数据验证 ====== end */
+
+  /* ====== 缩放 ====== start */
+  const zoom = ref<number>(100)
+  const setZoom = (step: number) => (zoom.value = Math.min(300, Math.max(50, zoom.value + step)))
+  /** ====== 缩放 ====== end */
+
+  // 暴露方法给父组件使用
+  defineExpose({ validate, getResult: () => copy(processData.value) })
+</script>
+<style lang="scss" scoped>
+  .ep-container {
+    position: relative;
+    box-sizing: border-box;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    padding: 16px;
+    overflow: auto;
+    background-color: #f5f5f7;
+  }
+
+  .ep-process {
+    position: relative;
+    height: 100%;
+  }
+
+  .ep-zoom {
+    position: absolute;
+    top: 70px;
+    right: 40px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 125px;
+    height: 40px;
+  }
+
+  .ep-zoom-icon {
+    width: 25px;
+    height: 25px;
+    color: #fff;
+    cursor: pointer;
+    background-color: #5a5e66;
+    border: 1px solid #5a5e66;
+    border-radius: 5px;
+  }
+</style>

+ 15 - 79
src/views/sys/dept/schemas/index.tsx

@@ -33,24 +33,10 @@ export const crudSchema: CrudSchema = {
   },
 
   columns: [
-    {
-      type: 'selection'
-    },
-    {
-      prop: 'name',
-      label: '部门名称',
-      component: 'Input'
-    },
-    {
-      prop: 'code',
-      label: '唯一编码',
-      component: 'Input'
-    },
-    {
-      prop: 'sort',
-      label: '排序',
-      component: 'InputNumber'
-    },
+    { type: 'selection' },
+    { prop: 'name', label: '部门名称', component: 'Input' },
+    { prop: 'code', label: '唯一编码', component: 'Input' },
+    { prop: 'sort', label: '排序', component: 'InputNumber' },
     {
       prop: 'enabled',
       label: '状态',
@@ -74,14 +60,7 @@ export const crudSchema: CrudSchema = {
     showDefaultActions: false,
     labelWidth: '100px',
     schema: [
-      {
-        label: '部门ID',
-        prop: 'id',
-        component: 'Input',
-        colSpan: 24,
-        show: false,
-        ifDetail: false
-      },
+      { label: '部门ID', prop: 'id', component: 'Input', colSpan: 24, show: false, ifDetail: false },
       {
         label: '父级部门',
         prop: 'parentId',
@@ -96,12 +75,7 @@ export const crudSchema: CrudSchema = {
           placeholder: '请选择父级部门'
         },
         colSpan: 24,
-        rules: [
-          {
-            required: true,
-            message: '请选择父级部门'
-          }
-        ]
+        rules: [{ required: true, message: '请选择父级部门' }]
       },
       {
         label: '部门名称',
@@ -111,12 +85,7 @@ export const crudSchema: CrudSchema = {
           placeholder: '请输入部门名称'
         },
         colSpan: 24,
-        rules: [
-          {
-            required: true,
-            message: '请输入部门名称'
-          }
-        ]
+        rules: [{ required: true, message: '请输入部门名称' }]
       },
       {
         label: '唯一编码',
@@ -126,76 +95,43 @@ export const crudSchema: CrudSchema = {
           placeholder: '请输入唯一编码'
         },
         colSpan: 24,
-        rules: [
-          {
-            required: true,
-            message: '请输入唯一编码'
-          }
-        ]
+        rules: [{ required: true, message: '请输入唯一编码' }]
       },
       {
         label: '排序',
         prop: 'sort',
         component: 'InputNumber',
-        componentProps: {
-          min: 0,
-          max: 9999
-        },
+        componentProps: { min: 0, max: 9999 },
         colSpan: 24,
-        rules: [
-          {
-            required: true,
-            message: '请输入排序值'
-          }
-        ]
+        rules: [{ required: true, message: '请输入排序值' }]
       },
       {
         label: '是否启用',
         prop: 'enabled',
         component: 'ApiDict',
-        componentProps: {
-          code: 'yes_no',
-          isBtn: true,
-          renderType: 'RadioGroup'
-        },
+        componentProps: { code: 'yes_no', isBtn: true, renderType: 'RadioGroup' },
         colSpan: 24,
-        rules: [
-          {
-            required: true,
-            message: '请选择启用状态'
-          }
-        ]
+        rules: [{ required: true, message: '请选择启用状态' }]
       },
       {
         label: '部门负责人',
         prop: 'leaderIdList',
         component: 'ApiSelect',
-        componentProps: {
-          api: '/sys/user/select',
-          placeholder: '请选择负责人',
-          multiple: true
-        },
+        componentProps: { api: '/sys/user/select', placeholder: '请选择负责人', multiple: true },
         colSpan: 24
       },
       {
         label: '分管领导',
         prop: 'mainLeaderId',
         component: 'ApiSelect',
-        componentProps: {
-          api: '/sys/user/select',
-          placeholder: '请选择分管领导'
-        },
+        componentProps: { api: '/sys/user/select', placeholder: '请选择分管领导' },
         colSpan: 24
       },
       {
         label: '备注',
         prop: 'remark',
         component: 'Input',
-        componentProps: {
-          type: 'textarea',
-          rows: 3,
-          placeholder: '请输入备注'
-        },
+        componentProps: { type: 'textarea', rows: 3, placeholder: '请输入备注' },
         colSpan: 24
       }
     ]