From aebb04a7c2f6d97406c04d510b873c1045f7a9b8 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 00:08:44 +0800
Subject: [PATCH 01/49] =?UTF-8?q?2023=E5=B9=B406=E6=9C=8801=E6=97=A5?=
 =?UTF-8?q?=EF=BC=9AContentWrap=20=E6=94=AF=E6=8C=81=20header=20=E5=8C=BA?=
 =?UTF-8?q?=E5=9F=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/ContentWrap/src/ContentWrap.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue
index e3bd5972..454e95c9 100644
--- a/src/components/ContentWrap/src/ContentWrap.vue
+++ b/src/components/ContentWrap/src/ContentWrap.vue
@@ -25,6 +25,9 @@ defineProps({
           </template>
           <Icon :size="14" class="ml-5px" icon="ep:question-filled" />
         </ElTooltip>
+        <div class="flex flex-grow pl-20px">
+          <slot name="header"></slot>
+        </div>
       </div>
     </template>
     <div>

From 4231a7d1336d56593885b622e650abfaf925c432 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 00:10:24 +0800
Subject: [PATCH 02/49] =?UTF-8?q?2023=E5=B9=B406=E6=9C=8815=E6=97=A5?=
 =?UTF-8?q?=EF=BC=9Afix:=20expand=20clickable=20area=20of=20collapse-icon?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/layout/components/Collapse/src/Collapse.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/layout/components/Collapse/src/Collapse.vue b/src/layout/components/Collapse/src/Collapse.vue
index ecb6890f..a8fc7ee8 100644
--- a/src/layout/components/Collapse/src/Collapse.vue
+++ b/src/layout/components/Collapse/src/Collapse.vue
@@ -24,13 +24,12 @@ const toggleCollapse = () => {
 </script>
 
 <template>
-  <div :class="prefixCls">
+  <div :class="prefixCls" @click="toggleCollapse">
     <Icon
       :color="color"
       :icon="collapse ? 'ep:expand' : 'ep:fold'"
       :size="18"
       class="cursor-pointer"
-      @click="toggleCollapse"
     />
   </div>
 </template>

From 4e39c11d7c0189d741c3bf12b5f0f61c13b5c9d0 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 08:46:46 +0800
Subject: [PATCH 03/49] =?UTF-8?q?=E2=9C=A8=20=E5=8D=87=E7=BA=A7=20vite=20?=
 =?UTF-8?q?=E7=AD=89=E7=9B=B8=E5=85=B3=E7=9A=84=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json | 84 ++++++++++++++++++++++++++--------------------------
 1 file changed, 42 insertions(+), 42 deletions(-)

diff --git a/package.json b/package.json
index 9bcd5c65..cf7142a3 100644
--- a/package.json
+++ b/package.json
@@ -30,12 +30,12 @@
     "@form-create/element-ui": "^3.1.24",
     "@iconify/iconify": "^3.1.1",
     "@videojs-player/vue": "^1.0.0",
-    "@vueuse/core": "^10.6.1",
+    "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^3.0.4",
     "animate.css": "^4.1.1",
-    "axios": "^1.6.1",
+    "axios": "^1.6.7",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.10.0",
     "camunda-bpmn-moddle": "^7.0.1",
@@ -44,9 +44,9 @@
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
     "driver.js": "^1.3.1",
-    "echarts": "^5.4.3",
+    "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.4.2",
+    "element-plus": "2.4.4",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
@@ -60,71 +60,71 @@
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
     "video.js": "^7.21.5",
-    "vue": "^3.3.8",
+    "vue": "3.4.20",
     "vue-dompurify-html": "^4.1.4",
-    "vue-i18n": "^9.6.5",
-    "vue-router": "^4.2.5",
+    "vue-i18n": "9.9.1",
+    "vue-router": "^4.3.0",
     "vue-types": "^5.1.1",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"
   },
   "devDependencies": {
-    "@commitlint/cli": "^18.4.1",
-    "@commitlint/config-conventional": "^18.4.0",
-    "@iconify/json": "^2.2.142",
-    "@intlify/unplugin-vue-i18n": "^1.5.0",
+    "@commitlint/cli": "^19.0.1",
+    "@commitlint/config-conventional": "^19.0.0",
+    "@iconify/json": "^2.2.187",
+    "@intlify/unplugin-vue-i18n": "^2.0.0",
     "@purge-icons/generated": "^0.9.0",
-    "@types/lodash-es": "^4.17.11",
-    "@types/node": "^20.9.0",
+    "@types/lodash-es": "^4.17.12",
+    "@types/node": "^20.11.21",
     "@types/nprogress": "^0.2.3",
     "@types/qrcode": "^1.5.5",
-    "@types/qs": "^6.9.10",
-    "@typescript-eslint/eslint-plugin": "^6.11.0",
-    "@typescript-eslint/parser": "^6.11.0",
-    "@unocss/transformer-variant-group": "^0.57.4",
+    "@types/qs": "^6.9.12",
+    "@typescript-eslint/eslint-plugin": "^7.1.0",
+    "@typescript-eslint/parser": "^7.1.0",
+    "@unocss/transformer-variant-group": "^0.58.5",
     "@unocss/eslint-config": "^0.57.4",
-    "@vitejs/plugin-legacy": "^4.1.1",
-    "@vitejs/plugin-vue": "^4.4.1",
-    "@vitejs/plugin-vue-jsx": "^3.0.2",
-    "autoprefixer": "^10.4.16",
+    "@vitejs/plugin-legacy": "^5.3.1",
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "autoprefixer": "^10.4.17",
     "bpmn-js": "8.9.0",
     "bpmn-js-properties-panel": "0.46.0",
     "consola": "^3.2.3",
-    "eslint": "^8.53.0",
-    "eslint-config-prettier": "^9.0.0",
-    "eslint-define-config": "^1.24.1",
-    "eslint-plugin-prettier": "^5.0.1",
-    "eslint-plugin-vue": "^9.18.1",
-    "lint-staged": "^15.1.0",
-    "postcss": "^8.4.31",
-    "postcss-html": "^1.5.0",
+    "eslint": "^8.57.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-define-config": "^2.1.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-vue": "^9.22.0",
+    "lint-staged": "^15.2.2",
+    "postcss": "^8.4.35",
+    "postcss-html": "^1.6.0",
     "postcss-scss": "^4.0.9",
-    "prettier": "^3.1.0",
+    "prettier": "^3.2.5",
     "rimraf": "^5.0.5",
-    "rollup": "^4.4.1",
+    "rollup": "^4.12.0",
     "sass": "^1.69.5",
-    "stylelint": "^15.11.0",
+    "stylelint": "^16.2.1",
     "stylelint-config-html": "^1.1.0",
-    "stylelint-config-recommended": "^13.0.0",
-    "stylelint-config-standard": "^34.0.0",
-    "stylelint-order": "^6.0.3",
-    "terser": "^5.24.0",
-    "typescript": "5.2.2",
-    "unocss": "^0.57.4",
+    "stylelint-config-recommended": "^14.0.0",
+    "stylelint-config-standard": "^36.0.0",
+    "stylelint-order": "^6.0.4",
+    "terser": "^5.28.1",
+    "typescript": "5.3.3",
+    "unocss": "^0.58.5",
     "unplugin-auto-import": "^0.16.7",
     "unplugin-element-plus": "^0.8.0",
     "unplugin-vue-components": "^0.25.2",
-    "vite": "4.5.0",
+    "vite": "5.1.4",
     "vite-plugin-compression": "^0.5.1",
-    "vite-plugin-ejs": "^1.6.4",
+    "vite-plugin-ejs": "^1.7.0",
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-progress": "^0.0.7",
-    "vite-plugin-purge-icons": "^0.9.2",
+    "vite-plugin-purge-icons": "^0.10.0",
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-top-level-await": "^1.3.1",
     "vue-eslint-parser": "^9.3.2",
-    "vue-tsc": "^1.8.22"
+    "vue-tsc": "^1.8.27"
   },
   "license": "MIT",
   "repository": {

From ca8858a33fd1bacb90870d9396cfb8135356c7b1 Mon Sep 17 00:00:00 2001
From: moon69 <1016830869@qq.com>
Date: Thu, 29 Feb 2024 05:40:40 +0000
Subject: [PATCH 04/49] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A0=A1=E9=AA=8C?=
 =?UTF-8?q?=E6=96=87=E6=9C=AC=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: moon69 <1016830869@qq.com>
---
 src/views/system/role/RoleForm.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue
index 01f29b8b..161b7571 100644
--- a/src/views/system/role/RoleForm.vue
+++ b/src/views/system/role/RoleForm.vue
@@ -59,11 +59,11 @@ const formData = ref({
   remark: ''
 })
 const formRules = reactive({
-  name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }],
-  code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }],
-  sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }],
-  status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }],
-  remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }]
+  name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '角色标识不能为空', trigger: 'change' }],
+  sort: [{ required: true, message: '显示顺序不能为空', trigger: 'change' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
+  remark: [{ required: false, message: '备注不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 

From f8580fdf2a07f9f72eeb79e38a3f6ed79f0f9db9 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 18:52:46 +0800
Subject: [PATCH 05/49] =?UTF-8?q?=E2=9C=A8=202023-05-10=EF=BC=9Afeat:=20?=
 =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=94=81=E5=B1=8F=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                                  |   1 +
 src/assets/imgs/avatar.jpg                    | Bin 0 -> 6264 bytes
 src/components/Editor/src/Editor.vue          |   2 +-
 .../components/UserInfo/src/UserInfo.vue      |  45 ++-
 .../UserInfo/src/components/LockDialog.vue    |  98 +++++++
 .../UserInfo/src/components/LockPage.vue      | 272 ++++++++++++++++++
 .../UserInfo/src/components/useNow.ts         |  60 ++++
 src/locales/en.ts                             |  10 +
 src/locales/zh-CN.ts                          |  10 +
 src/store/index.ts                            |   2 +
 src/store/modules/lock.ts                     |  52 ++++
 src/utils/dateUtil.ts                         |  18 ++
 types/global.d.ts                             |   3 +
 13 files changed, 570 insertions(+), 3 deletions(-)
 create mode 100644 src/assets/imgs/avatar.jpg
 create mode 100644 src/layout/components/UserInfo/src/components/LockDialog.vue
 create mode 100644 src/layout/components/UserInfo/src/components/LockPage.vue
 create mode 100644 src/layout/components/UserInfo/src/components/useNow.ts
 create mode 100644 src/store/modules/lock.ts
 create mode 100644 src/utils/dateUtil.ts

diff --git a/package.json b/package.json
index cf7142a3..e6e33e39 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
+    "pinia-plugin-persist": "^1.0.0",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
     "steady-xml": "^0.1.0",
diff --git a/src/assets/imgs/avatar.jpg b/src/assets/imgs/avatar.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d46a70a46430e31744420865138cc7eedb8b77e1
GIT binary patch
literal 6264
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
BLq7lj

literal 0
HcmV?d00001

diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue
index ec40bca2..fd31bcd2 100644
--- a/src/components/Editor/src/Editor.vue
+++ b/src/components/Editor/src/Editor.vue
@@ -185,7 +185,7 @@ defineExpose({
     <Toolbar
       :editor="editorRef"
       :editorId="editorId"
-      class="border-0 b-b-1 border-[var(--el-border-color)] border-solid"
+      class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
     />
     <!-- 编辑器 -->
     <Editor
diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue
index 22acfd13..5c5e3732 100644
--- a/src/layout/components/UserInfo/src/UserInfo.vue
+++ b/src/layout/components/UserInfo/src/UserInfo.vue
@@ -5,6 +5,9 @@ import avatarImg from '@/assets/imgs/avatar.gif'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useUserStore } from '@/store/modules/user'
+import LockDialog from './components/LockDialog.vue'
+import LockPage from './components/LockPage.vue'
+import { useLockStore } from '@/store/modules/lock'
 
 defineOptions({ name: 'UserInfo' })
 
@@ -23,6 +26,14 @@ const prefixCls = getPrefixCls('user-info')
 const avatar = computed(() => userStore.user.avatar ?? avatarImg)
 const userName = computed(() => userStore.user.nickname ?? 'Admin')
 
+// 锁定屏幕
+const lockStore = useLockStore()
+const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
+const dialogVisible = ref<boolean>(false)
+const lockScreen = () => {
+  dialogVisible.value = true
+}
+
 const loginOut = async () => {
   try {
     await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
@@ -33,8 +44,7 @@ const loginOut = async () => {
     await userStore.loginOut()
     tagsViewStore.delAllViews()
     replace('/login?redirect=/index')
-  }
-  catch { }
+  } catch {}
 }
 const toProfile = async () => {
   push('/user/profile')
@@ -62,6 +72,10 @@ const toDocument = () => {
           <Icon icon="ep:menu" />
           <div @click="toDocument">{{ t('common.document') }}</div>
         </ElDropdownItem>
+        <ElDropdownItem divided>
+          <Icon icon="ep:lock" />
+          <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
+        </ElDropdownItem>
         <ElDropdownItem divided @click="loginOut">
           <Icon icon="ep:switch-button" />
           <div>{{ t('common.loginOut') }}</div>
@@ -69,4 +83,31 @@ const toDocument = () => {
       </ElDropdownMenu>
     </template>
   </ElDropdown>
+
+  <LockDialog v-if="dialogVisible" v-model="dialogVisible" />
+
+  <teleport to="body">
+    <transition name="fade-bottom" mode="out-in">
+      <LockPage v-if="getIsLock" />
+    </transition>
+  </teleport>
 </template>
+
+<style scoped lang="scss">
+.fade-bottom-enter-active,
+.fade-bottom-leave-active {
+  transition:
+    opacity 0.25s,
+    transform 0.3s;
+}
+
+.fade-bottom-enter-from {
+  opacity: 0;
+  transform: translateY(-10%);
+}
+
+.fade-bottom-leave-to {
+  opacity: 0;
+  transform: translateY(10%);
+}
+</style>
diff --git a/src/layout/components/UserInfo/src/components/LockDialog.vue b/src/layout/components/UserInfo/src/components/LockDialog.vue
new file mode 100644
index 00000000..f4ab7d4f
--- /dev/null
+++ b/src/layout/components/UserInfo/src/components/LockDialog.vue
@@ -0,0 +1,98 @@
+<script setup lang="ts">
+import { useValidator } from '@/hooks/web/useValidator'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useLockStore } from '@/store/modules/lock'
+import avatarImg from '@/assets/imgs/avatar.gif'
+import { useUserStore } from '@/store/modules/user'
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-dialog')
+
+const { required } = useValidator()
+
+const { t } = useI18n()
+
+const lockStore = useLockStore()
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean
+  }
+})
+
+const userStore = useUserStore()
+const avatar = computed(() => userStore.user.avatar ?? avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => {
+    console.log('set: ', val)
+    emit('update:modelValue', val)
+  }
+})
+
+const dialogTitle = ref(t('lock.lockScreen'))
+
+const formData = ref({
+  password: undefined
+})
+const formRules = reactive({
+  password: [required()]
+})
+
+const formRef = ref() // 表单 Ref
+const handleLock = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  dialogVisible.value = false
+  lockStore.setLockInfo({
+    ...formData.value,
+    isLock: true
+  })
+}
+</script>
+
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    width="500px"
+    max-height="170px"
+    :class="prefixCls"
+    :title="dialogTitle"
+  >
+    <div class="flex flex-col items-center">
+      <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+      <span class="text-14px my-10px text-[var(--top-header-text-color)]">
+        {{ userName }}
+      </span>
+    </div>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-form-item :label="t('lock.lockPassword')" prop="password">
+        <el-input
+          type="password"
+          v-model="formData.password"
+          :placeholder="'请输入' + t('lock.lockPassword')"
+          clearable
+          show-password
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+
+<style lang="scss" scoped>
+:global(.v-lock-dialog) {
+  @media (max-width: 767px) {
+    max-width: calc(100vw - 16px);
+  }
+}
+</style>
diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue
new file mode 100644
index 00000000..0e153e7e
--- /dev/null
+++ b/src/layout/components/UserInfo/src/components/LockPage.vue
@@ -0,0 +1,272 @@
+<script lang="ts" setup>
+import { resetRouter } from '@/router'
+import { useCache } from '@/hooks/web/useCache'
+import { useLockStore } from '@/store/modules/lock'
+import { useNow } from './useNow'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStore } from '@/store/modules/user'
+import avatarImg from '@/assets/imgs/avatar.gif'
+
+const tagsViewStore = useTagsViewStore()
+
+const { wsCache } = useCache()
+
+const { replace } = useRouter()
+
+const userStore = useUserStore()
+
+const password = ref('')
+const loading = ref(false)
+const errMsg = ref(false)
+const showDate = ref(true)
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-page')
+
+const avatar = computed(() => userStore.user.avatar ?? avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const lockStore = useLockStore()
+
+const { hour, month, minute, meridiem, year, day, week } = useNow(true)
+
+const { t } = useI18n()
+
+// 解锁
+async function unLock() {
+  if (!password.value) {
+    return
+  }
+  let pwd = password.value
+  try {
+    loading.value = true
+    const res = await lockStore.unLock(pwd)
+    errMsg.value = !res
+  } finally {
+    loading.value = false
+  }
+}
+
+// 返回登录
+async function goLogin() {
+  await userStore.loginOut().catch(() => {})
+  // 登出后清理
+  wsCache.clear()
+  tagsViewStore.delAllViews()
+  resetRouter() // 重置静态路由表
+  lockStore.resetLockInfo()
+  replace('/login')
+}
+
+function handleShowForm(show = false) {
+  showDate.value = show
+}
+</script>
+
+<template>
+  <div
+    :class="prefixCls"
+    class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
+  >
+    <div
+      :class="`${prefixCls}__unlock`"
+      class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
+      @click="handleShowForm(false)"
+      v-show="showDate"
+    >
+      <Icon icon="ep:lock" />
+      <span>{{ t('lock.unlock') }}</span>
+    </div>
+
+    <div class="flex w-screen h-screen justify-center items-center">
+      <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
+        <span>{{ hour }}</span>
+        <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
+          {{ meridiem }}
+        </span>
+      </div>
+      <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
+        <span> {{ minute }}</span>
+      </div>
+    </div>
+    <transition name="fade-slide">
+      <div :class="`${prefixCls}-entry`" v-show="!showDate">
+        <div :class="`${prefixCls}-entry-content`">
+          <div class="flex flex-col items-center">
+            <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+            <span class="text-14px my-10px text-[var(--logo-title-text-color)]">
+              {{ userName }}
+            </span>
+          </div>
+          <ElInput
+            type="password"
+            :placeholder="t('lock.placeholder')"
+            class="enter-x"
+            v-model="password"
+          />
+          <span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
+            {{ t('lock.message') }}
+          </span>
+          <div :class="`${prefixCls}-entry__footer enter-x`">
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="handleShowForm(true)"
+            >
+              {{ t('common.back') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="goLogin"
+            >
+              {{ t('lock.backToLogin') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              class="mt-2"
+              size="small"
+              link
+              @click="unLock()"
+              :disabled="loading"
+            >
+              {{ t('lock.entrySystem') }}
+            </ElButton>
+          </div>
+        </div>
+      </div>
+    </transition>
+
+    <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
+      <div class="text-5xl mb-4 enter-x" v-show="!showDate">
+        {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
+      </div>
+      <div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: '#{$namespace}-lock-page';
+
+// Small screen / tablet
+$screen-sm: 576px;
+
+// Medium screen / desktop
+$screen-md: 768px;
+
+// Large screen / wide desktop
+$screen-lg: 992px;
+
+// Extra large screen / full hd
+$screen-xl: 1200px;
+
+// Extra extra large screen / large desktop
+$screen-2xl: 1600px;
+
+$error-color: #ed6f6f;
+
+.#{$prefix-cls} {
+  z-index: 3000;
+
+  &__unlock {
+    transform: translate(-50%, 0);
+  }
+
+  &__hour,
+  &__minute {
+    display: flex;
+    font-weight: 700;
+    color: #bababa;
+    background-color: #141313;
+    border-radius: 30px;
+    justify-content: center;
+    align-items: center;
+
+    @media screen and (max-width: $screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (min-width: $screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (max-width: $screen-sm) {
+      span:not(.meridiem) {
+        font-size: 90px;
+      }
+    }
+    @media screen and (min-width: $screen-lg) {
+      span:not(.meridiem) {
+        font-size: 220px;
+      }
+    }
+
+    @media screen and (min-width: $screen-xl) {
+      span:not(.meridiem) {
+        font-size: 260px;
+      }
+    }
+    @media screen and (min-width: $screen-2xl) {
+      span:not(.meridiem) {
+        font-size: 320px;
+      }
+    }
+  }
+
+  &-entry {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    backdrop-filter: blur(8px);
+    justify-content: center;
+    align-items: center;
+
+    &-content {
+      width: 260px;
+    }
+
+    &__header {
+      text-align: center;
+
+      &-img {
+        width: 70px;
+        margin: 0 auto;
+        border-radius: 50%;
+      }
+
+      &-name {
+        margin-top: 5px;
+        font-weight: 500;
+        color: #bababa;
+      }
+    }
+
+    &__err-msg {
+      display: inline-block;
+      margin-top: 10px;
+      color: $error-color;
+    }
+
+    &__footer {
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+}
+</style>
diff --git a/src/layout/components/UserInfo/src/components/useNow.ts b/src/layout/components/UserInfo/src/components/useNow.ts
new file mode 100644
index 00000000..10795965
--- /dev/null
+++ b/src/layout/components/UserInfo/src/components/useNow.ts
@@ -0,0 +1,60 @@
+import { dateUtil } from '@/utils/dateUtil'
+import { reactive, toRefs } from 'vue'
+import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
+
+export function useNow(immediate = true) {
+  let timer: IntervalHandle
+
+  const state = reactive({
+    year: 0,
+    month: 0,
+    week: '',
+    day: 0,
+    hour: '',
+    minute: '',
+    second: 0,
+    meridiem: ''
+  })
+
+  const update = () => {
+    const now = dateUtil()
+
+    const h = now.format('HH')
+    const m = now.format('mm')
+    const s = now.get('s')
+
+    state.year = now.get('y')
+    state.month = now.get('M') + 1
+    state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
+    state.day = now.get('date')
+    state.hour = h
+    state.minute = m
+    state.second = s
+
+    state.meridiem = now.format('A')
+  }
+
+  function start() {
+    update()
+    clearInterval(timer)
+    timer = setInterval(() => update(), 1000)
+  }
+
+  function stop() {
+    clearInterval(timer)
+  }
+
+  tryOnMounted(() => {
+    immediate && start()
+  })
+
+  tryOnUnmounted(() => {
+    stop()
+  })
+
+  return {
+    ...toRefs(state),
+    start,
+    stop
+  }
+}
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 4f4d4895..6562c9b7 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -56,6 +56,16 @@ export default {
     copySuccess: 'Copy Success',
     copyError: 'Copy Error'
   },
+  lock: {
+    lockScreen: 'Lock screen',
+    lock: 'Lock',
+    lockPassword: 'Lock screen password',
+    unlock: 'Click to unlock',
+    backToLogin: 'Back to login',
+    entrySystem: 'Entry the system',
+    placeholder: 'Please enter the lock screen password',
+    message: 'Lock screen password error'
+  },
   error: {
     noPermission: `Sorry, you don't have permission to access this page.`,
     pageError: 'Sorry, the page you visited does not exist.',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 6346a3d3..0721651d 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -56,6 +56,16 @@ export default {
     copySuccess: '复制成功',
     copyError: '复制失败'
   },
+  lock: {
+    lockScreen: '锁定屏幕',
+    lock: '锁定',
+    lockPassword: '锁屏密码',
+    unlock: '点击解锁',
+    backToLogin: '返回登录',
+    entrySystem: '进入系统',
+    placeholder: '请输入锁屏密码',
+    message: '锁屏密码错误'
+  },
   error: {
     noPermission: `抱歉,您无权访问此页面。`,
     pageError: '抱歉,您访问的页面不存在。',
diff --git a/src/store/index.ts b/src/store/index.ts
index 65964ea8..7740bdd3 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,7 +1,9 @@
 import type { App } from 'vue'
 import { createPinia } from 'pinia'
+import piniaPersist from 'pinia-plugin-persist'
 
 const store = createPinia()
+store.use(piniaPersist)
 
 export const setupStore = (app: App<Element>) => {
   app.use(store)
diff --git a/src/store/modules/lock.ts b/src/store/modules/lock.ts
new file mode 100644
index 00000000..bf6ceb49
--- /dev/null
+++ b/src/store/modules/lock.ts
@@ -0,0 +1,52 @@
+import { defineStore } from 'pinia'
+import { store } from '@/store'
+
+interface lockInfo {
+  isLock?: boolean
+  password?: string
+}
+
+interface LockState {
+  lockInfo: lockInfo
+}
+
+// TODO 芋艿:【锁屏】这里有报错,后续解决下
+export const useLockStore = defineStore('lock', {
+  state: (): LockState => {
+    return {
+      lockInfo: {
+        // isLock: false, // 是否锁定屏幕
+        // password: '' // 锁屏密码
+      }
+    }
+  },
+  getters: {
+    getLockInfo(): lockInfo {
+      return this.lockInfo
+    }
+  },
+  actions: {
+    setLockInfo(lockInfo: lockInfo) {
+      this.lockInfo = lockInfo
+    },
+    resetLockInfo() {
+      this.lockInfo = {}
+    },
+    unLock(password: string) {
+      if (this.lockInfo?.password === password) {
+        this.resetLockInfo()
+        return true
+      } else {
+        return false
+      }
+    }
+  },
+  persist: {
+    enabled: true,
+    strategies: [{ key: 'lock', storage: localStorage }]
+  }
+})
+
+export const useLockStoreWithOut = () => {
+  return useLockStore(store)
+}
diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts
new file mode 100644
index 00000000..316b870b
--- /dev/null
+++ b/src/utils/dateUtil.ts
@@ -0,0 +1,18 @@
+/**
+ * Independent time operation tool to facilitate subsequent switch to dayjs
+ */
+// TODO 芋艿:【锁屏】可能后面删除掉
+import dayjs from 'dayjs'
+
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+const DATE_FORMAT = 'YYYY-MM-DD'
+
+export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
+  return dayjs(date).format(format)
+}
+
+export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
+  return dayjs(date).format(format)
+}
+
+export const dateUtil = dayjs
diff --git a/types/global.d.ts b/types/global.d.ts
index 5e292687..e91e1fe4 100644
--- a/types/global.d.ts
+++ b/types/global.d.ts
@@ -14,6 +14,9 @@ declare global {
 
   type LocaleType = 'zh-CN' | 'en'
 
+  declare type TimeoutHandle = ReturnType<typeof setTimeout>
+  declare type IntervalHandle = ReturnType<typeof setInterval>
+
   type AxiosHeaders =
     | 'application/json'
     | 'application/x-www-form-urlencoded'

From d884f71d5701bd3b9242d5a5076bbfd783ee9ba9 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 19:34:04 +0800
Subject: [PATCH 06/49] =?UTF-8?q?=E2=9C=A8=202023-06-25=EF=BC=9Aperf:=20?=
 =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=94=81=E5=B1=8F=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/Editor/src/Editor.vue          |  2 +-
 .../UserInfo/src/components/LockPage.vue      |  2 +-
 .../UserInfo/src/components/useNow.ts         | 60 -------------------
 3 files changed, 2 insertions(+), 62 deletions(-)
 delete mode 100644 src/layout/components/UserInfo/src/components/useNow.ts

diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue
index fd31bcd2..eff82745 100644
--- a/src/components/Editor/src/Editor.vue
+++ b/src/components/Editor/src/Editor.vue
@@ -180,7 +180,7 @@ defineExpose({
 </script>
 
 <template>
-  <div class="z-99 border-1 border-[var(--el-border-color)] border-solid">
+  <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
     <!-- 工具栏 -->
     <Toolbar
       :editor="editorRef"
diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue
index 0e153e7e..1de18fe6 100644
--- a/src/layout/components/UserInfo/src/components/LockPage.vue
+++ b/src/layout/components/UserInfo/src/components/LockPage.vue
@@ -2,7 +2,7 @@
 import { resetRouter } from '@/router'
 import { useCache } from '@/hooks/web/useCache'
 import { useLockStore } from '@/store/modules/lock'
-import { useNow } from './useNow'
+import { useNow } from '@/hooks/web/useNow'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useUserStore } from '@/store/modules/user'
diff --git a/src/layout/components/UserInfo/src/components/useNow.ts b/src/layout/components/UserInfo/src/components/useNow.ts
deleted file mode 100644
index 10795965..00000000
--- a/src/layout/components/UserInfo/src/components/useNow.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { dateUtil } from '@/utils/dateUtil'
-import { reactive, toRefs } from 'vue'
-import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
-
-export function useNow(immediate = true) {
-  let timer: IntervalHandle
-
-  const state = reactive({
-    year: 0,
-    month: 0,
-    week: '',
-    day: 0,
-    hour: '',
-    minute: '',
-    second: 0,
-    meridiem: ''
-  })
-
-  const update = () => {
-    const now = dateUtil()
-
-    const h = now.format('HH')
-    const m = now.format('mm')
-    const s = now.get('s')
-
-    state.year = now.get('y')
-    state.month = now.get('M') + 1
-    state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
-    state.day = now.get('date')
-    state.hour = h
-    state.minute = m
-    state.second = s
-
-    state.meridiem = now.format('A')
-  }
-
-  function start() {
-    update()
-    clearInterval(timer)
-    timer = setInterval(() => update(), 1000)
-  }
-
-  function stop() {
-    clearInterval(timer)
-  }
-
-  tryOnMounted(() => {
-    immediate && start()
-  })
-
-  tryOnUnmounted(() => {
-    stop()
-  })
-
-  return {
-    ...toRefs(state),
-    start,
-    stop
-  }
-}

From fe61119735b992918fe03cd9af26a8e7b4daa217 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 19:39:58 +0800
Subject: [PATCH 07/49] =?UTF-8?q?=E2=9C=A8=202023-06-25=EF=BC=9Aperf:=20Im?=
 =?UTF-8?q?ageViewer=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/ImageViewer/index.ts            | 4 ++--
 src/components/ImageViewer/src/ImageViewer.vue | 2 +-
 src/components/ImageViewer/src/types.ts        | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/ImageViewer/index.ts b/src/components/ImageViewer/index.ts
index 38681356..35764d6b 100644
--- a/src/components/ImageViewer/index.ts
+++ b/src/components/ImageViewer/index.ts
@@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
     initialIndex = 0,
     infinite = true,
     hideOnClickModal = false,
-    appendToBody = false,
+    teleported = false,
     zIndex = 2000,
     show = true
   } = options
@@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
   propsData.initialIndex = initialIndex
   propsData.infinite = infinite
   propsData.hideOnClickModal = hideOnClickModal
-  propsData.appendToBody = appendToBody
+  propsData.teleported = teleported
   propsData.zIndex = zIndex
   propsData.show = show
 
diff --git a/src/components/ImageViewer/src/ImageViewer.vue b/src/components/ImageViewer/src/ImageViewer.vue
index 5c4921ed..c84d06be 100644
--- a/src/components/ImageViewer/src/ImageViewer.vue
+++ b/src/components/ImageViewer/src/ImageViewer.vue
@@ -13,7 +13,7 @@ const props = defineProps({
   initialIndex: propTypes.number.def(0),
   infinite: propTypes.bool.def(true),
   hideOnClickModal: propTypes.bool.def(false),
-  appendToBody: propTypes.bool.def(false),
+  teleported: propTypes.bool.def(false),
   show: propTypes.bool.def(false)
 })
 
diff --git a/src/components/ImageViewer/src/types.ts b/src/components/ImageViewer/src/types.ts
index 1932d74d..2fff4c0a 100644
--- a/src/components/ImageViewer/src/types.ts
+++ b/src/components/ImageViewer/src/types.ts
@@ -4,6 +4,6 @@ export interface ImageViewerProps {
   initialIndex?: number
   infinite?: boolean
   hideOnClickModal?: boolean
-  appendToBody?: boolean
+  teleported?: boolean
   show?: boolean
 }

From 8d23c38f2f85cf9e990d60b225915fd3e517bdab Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 19:49:28 +0800
Subject: [PATCH 08/49] =?UTF-8?q?=E2=9C=A8=202023-06-25=EF=BC=9Astyle:=20?=
 =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/layout/components/Menu/src/Menu.vue | 32 -------------------------
 1 file changed, 32 deletions(-)

diff --git a/src/layout/components/Menu/src/Menu.vue b/src/layout/components/Menu/src/Menu.vue
index 9033616f..7254b735 100644
--- a/src/layout/components/Menu/src/Menu.vue
+++ b/src/layout/components/Menu/src/Menu.vue
@@ -124,16 +124,6 @@ export default defineComponent({
 <style lang="scss" scoped>
 $prefix-cls: #{$namespace}-menu;
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
-
 .#{$prefix-cls} {
   position: relative;
   transition: width var(--transition-time-02);
@@ -171,10 +161,6 @@ $prefix-cls: #{$namespace}-menu;
 
     .#{$elNamespace}-menu-item.is-active {
       position: relative;
-
-      &::after {
-        @extend .is-active--after;
-      }
     }
 
     // 设置子菜单的背景颜色
@@ -194,10 +180,6 @@ $prefix-cls: #{$namespace}-menu;
     & > .is-active > .#{$elNamespace}-sub-menu__title {
       position: relative;
       background-color: var(--left-menu-collapse-bg-active-color) !important;
-
-      &::after {
-        @extend .is-active--after;
-      }
     }
   }
 
@@ -245,16 +227,6 @@ $prefix-cls: #{$namespace}-menu;
 <style lang="scss">
 $prefix-cls: #{$namespace}-menu-popper;
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
-
 .#{$prefix-cls}--vertical,
 .#{$prefix-cls}--horizontal {
   // 设置选中时子标题的颜色
@@ -281,10 +253,6 @@ $prefix-cls: #{$namespace}-menu-popper;
     &:hover {
       background-color: var(--left-menu-bg-active-color) !important;
     }
-
-    &::after {
-      @extend .is-active--after;
-    }
   }
 }
 </style>

From f413556c7350cceb647d9c00d0898a654a2cdf5c Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 20:06:03 +0800
Subject: [PATCH 09/49] =?UTF-8?q?=E2=9C=A8=202023-08-10=EF=BC=9Astyle:=20?=
 =?UTF-8?q?=E4=BF=AE=E6=94=B9TabMenu=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/layout/components/TabMenu/src/TabMenu.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/layout/components/TabMenu/src/TabMenu.vue b/src/layout/components/TabMenu/src/TabMenu.vue
index c4f63a3f..055a6aff 100644
--- a/src/layout/components/TabMenu/src/TabMenu.vue
+++ b/src/layout/components/TabMenu/src/TabMenu.vue
@@ -139,7 +139,7 @@ export default defineComponent({
         id={`${variables.namespace}-menu`}
         class={[
           prefixCls,
-          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
+          'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
           {
             'w-[var(--tab-menu-max-width)]': !unref(collapse),
             'w-[var(--tab-menu-min-width)]': unref(collapse)

From 560a336f8cc17768de404cd2d754d090417a9a22 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 22:23:10 +0800
Subject: [PATCH 10/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7?=
 =?UTF-8?q?=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95=E6=97=B6=EF=BC=8C=E6=8A=8A?=
 =?UTF-8?q?=20LANG=E3=80=81THEME=E3=80=81LAYOUT=E3=80=81IS=5FDARK=20?=
 =?UTF-8?q?=E7=BB=99=E6=B8=85=E7=A9=BA=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/config/axios/service.ts                   |  5 ++---
 src/hooks/web/useCache.ts                     |  6 ++++++
 .../UserInfo/src/components/LockPage.vue      |  6 ++----
 src/store/modules/user.ts                     | 20 ++++++++++++++++---
 4 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts
index 19b8c979..25936068 100644
--- a/src/config/axios/service.ts
+++ b/src/config/axios/service.ts
@@ -13,7 +13,7 @@ import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } f
 import errorCode from './errorCode'
 
 import { resetRouter } from '@/router'
-import { useCache } from '@/hooks/web/useCache'
+import { deleteUserCache } from '@/hooks/web/useCache'
 
 const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
 const { result_code, base_url, request_timeout } = config
@@ -217,9 +217,8 @@ const handleAuthorized = () => {
       confirmButtonText: t('login.relogin'),
       type: 'warning'
     }).then(() => {
-      const { wsCache } = useCache()
       resetRouter() // 重置静态路由表
-      wsCache.clear()
+      deleteUserCache() // 删除用户缓存
       removeToken()
       isRelogin.show = false
       // 干掉token后再走一次路由让它过router.beforeEach的校验
diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts
index 6d2a9318..2850bd5c 100644
--- a/src/hooks/web/useCache.ts
+++ b/src/hooks/web/useCache.ts
@@ -25,3 +25,9 @@ export const useCache = (type: CacheType = 'localStorage') => {
     wsCache
   }
 }
+
+export const deleteUserCache = () => {
+  const { wsCache } = useCache()
+  wsCache.delete(CACHE_KEY.USER)
+  wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+}
diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue
index 1de18fe6..497dd37b 100644
--- a/src/layout/components/UserInfo/src/components/LockPage.vue
+++ b/src/layout/components/UserInfo/src/components/LockPage.vue
@@ -1,6 +1,6 @@
 <script lang="ts" setup>
 import { resetRouter } from '@/router'
-import { useCache } from '@/hooks/web/useCache'
+import { deleteUserCache } from '@/hooks/web/useCache'
 import { useLockStore } from '@/store/modules/lock'
 import { useNow } from '@/hooks/web/useNow'
 import { useDesign } from '@/hooks/web/useDesign'
@@ -10,8 +10,6 @@ import avatarImg from '@/assets/imgs/avatar.gif'
 
 const tagsViewStore = useTagsViewStore()
 
-const { wsCache } = useCache()
-
 const { replace } = useRouter()
 
 const userStore = useUserStore()
@@ -52,7 +50,7 @@ async function unLock() {
 async function goLogin() {
   await userStore.loginOut().catch(() => {})
   // 登出后清理
-  wsCache.clear()
+  deleteUserCache() // 清空用户缓存
   tagsViewStore.delAllViews()
   resetRouter() // 重置静态路由表
   lockStore.resetLockInfo()
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
index cb71a1a4..9e5caae8 100644
--- a/src/store/modules/user.ts
+++ b/src/store/modules/user.ts
@@ -1,7 +1,7 @@
-import { store } from '../index'
+import { store } from '@/store'
 import { defineStore } from 'pinia'
 import { getAccessToken, removeToken } from '@/utils/auth'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
 import { getInfo, loginOut } from '@/api/login'
 
 const { wsCache } = useCache()
@@ -13,11 +13,20 @@ interface UserVO {
   deptId: number
 }
 
+interface RememberMeInfo {
+  enable: boolean // 是否记住我
+  username: string
+  password: string
+}
+
 interface UserInfoVO {
+  // USER 缓存
   permissions: string[]
   roles: string[]
   isSetUser: boolean
   user: UserVO
+  // REMEMBER_ME 缓存
+  rememberMe: RememberMeInfo
 }
 
 export const useUserStore = defineStore('admin-user', {
@@ -30,6 +39,11 @@ export const useUserStore = defineStore('admin-user', {
       avatar: '',
       nickname: '',
       deptId: 0
+    },
+    rememberMe: {
+      enable: true,
+      username: '',
+      password: ''
     }
   }),
   getters: {
@@ -80,7 +94,7 @@ export const useUserStore = defineStore('admin-user', {
     async loginOut() {
       await loginOut()
       removeToken()
-      wsCache.clear()
+      deleteUserCache() // 删除用户缓存
       this.resetState()
     },
     resetState() {

From 1bc4eefeabf58c3a40c7205cd6ee4b53ea48ce49 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 22:42:10 +0800
Subject: [PATCH 11/49] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B0=E4=BD=8F?=
 =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=A4=B1=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/hooks/web/useCache.ts                | 12 ++++++---
 src/store/modules/user.ts                | 13 ----------
 src/utils/auth.ts                        | 33 +++++-------------------
 src/views/Login/components/LoginForm.vue |  8 +++---
 4 files changed, 19 insertions(+), 47 deletions(-)

diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts
index 2850bd5c..4f39f307 100644
--- a/src/hooks/web/useCache.ts
+++ b/src/hooks/web/useCache.ts
@@ -7,13 +7,18 @@ import WebStorageCache from 'web-storage-cache'
 type CacheType = 'localStorage' | 'sessionStorage'
 
 export const CACHE_KEY = {
-  IS_DARK: 'isDark',
+  // 用户相关
+  ROLE_ROUTERS: 'roleRouters',
   USER: 'user',
+  // 系统设置
+  IS_DARK: 'isDark',
   LANG: 'lang',
   THEME: 'theme',
   LAYOUT: 'layout',
-  ROLE_ROUTERS: 'roleRouters',
-  DICT_CACHE: 'dictCache'
+  DICT_CACHE: 'dictCache',
+  // 登录表单
+  LoginForm: 'loginForm',
+  TenantId: 'tenantId'
 }
 
 export const useCache = (type: CacheType = 'localStorage') => {
@@ -30,4 +35,5 @@ export const deleteUserCache = () => {
   const { wsCache } = useCache()
   wsCache.delete(CACHE_KEY.USER)
   wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  // 注意,不要清理 LoginForm 登录表单
 }
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
index 9e5caae8..b3861809 100644
--- a/src/store/modules/user.ts
+++ b/src/store/modules/user.ts
@@ -13,20 +13,12 @@ interface UserVO {
   deptId: number
 }
 
-interface RememberMeInfo {
-  enable: boolean // 是否记住我
-  username: string
-  password: string
-}
-
 interface UserInfoVO {
   // USER 缓存
   permissions: string[]
   roles: string[]
   isSetUser: boolean
   user: UserVO
-  // REMEMBER_ME 缓存
-  rememberMe: RememberMeInfo
 }
 
 export const useUserStore = defineStore('admin-user', {
@@ -39,11 +31,6 @@ export const useUserStore = defineStore('admin-user', {
       avatar: '',
       nickname: '',
       deptId: 0
-    },
-    rememberMe: {
-      enable: true,
-      username: '',
-      password: ''
     }
   }),
   getters: {
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index 7da49b08..c68a67a9 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -1,4 +1,4 @@
-import { useCache } from '@/hooks/web/useCache'
+import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
 import { TokenType } from '@/api/login/types'
 import { decrypt, encrypt } from '@/utils/jsencrypt'
 
@@ -36,8 +36,6 @@ export const formatToken = (token: string): string => {
 }
 // ========== 账号相关 ==========
 
-const LoginFormKey = 'LOGINFORM'
-
 export type LoginFormType = {
   tenantName: string
   username: string
@@ -46,7 +44,7 @@ export type LoginFormType = {
 }
 
 export const getLoginForm = () => {
-  const loginForm: LoginFormType = wsCache.get(LoginFormKey)
+  const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
   if (loginForm) {
     loginForm.password = decrypt(loginForm.password) as string
   }
@@ -55,38 +53,19 @@ export const getLoginForm = () => {
 
 export const setLoginForm = (loginForm: LoginFormType) => {
   loginForm.password = encrypt(loginForm.password) as string
-  wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 })
+  wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
 }
 
 export const removeLoginForm = () => {
-  wsCache.delete(LoginFormKey)
+  wsCache.delete(CACHE_KEY.LoginForm)
 }
 
 // ========== 租户相关 ==========
 
-const TenantIdKey = 'TENANT_ID'
-const TenantNameKey = 'TENANT_NAME'
-
-export const getTenantName = () => {
-  return wsCache.get(TenantNameKey)
-}
-
-export const setTenantName = (username: string) => {
-  wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 })
-}
-
-export const removeTenantName = () => {
-  wsCache.delete(TenantNameKey)
-}
-
 export const getTenantId = () => {
-  return wsCache.get(TenantIdKey)
+  return wsCache.get(CACHE_KEY.TenantId)
 }
 
 export const setTenantId = (username: string) => {
-  wsCache.set(TenantIdKey, username)
-}
-
-export const removeTenantId = () => {
-  wsCache.delete(TenantIdKey)
+  wsCache.set(CACHE_KEY.TenantId, username)
 }
diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue
index ef212505..bf102e04 100644
--- a/src/views/Login/components/LoginForm.vue
+++ b/src/views/Login/components/LoginForm.vue
@@ -188,7 +188,7 @@ const loginData = reactive({
     username: 'admin',
     password: 'admin123',
     captchaVerification: '',
-    rememberMe: false
+    rememberMe: true // 默认记录我。如果不需要,可手动修改
   }
 })
 
@@ -218,14 +218,14 @@ const getTenantId = async () => {
   }
 }
 // 记住我
-const getCookie = () => {
+const getLoginFormCache = () => {
   const loginForm = authUtil.getLoginForm()
   if (loginForm) {
     loginData.loginForm = {
       ...loginData.loginForm,
       username: loginForm.username ? loginForm.username : loginData.loginForm.username,
       password: loginForm.password ? loginForm.password : loginData.loginForm.password,
-      rememberMe: loginForm.rememberMe ? true : false,
+      rememberMe: loginForm.rememberMe,
       tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
     }
   }
@@ -320,7 +320,7 @@ watch(
   }
 )
 onMounted(() => {
-  getCookie()
+  getLoginFormCache()
   getTenantByWebsite()
 })
 </script>

From 8f14355903d0c92de7e05179f7b8c51990467f44 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 23:05:14 +0800
Subject: [PATCH 12/49] =?UTF-8?q?=E5=8D=87=E7=BA=A7=20element=20plus=20?=
 =?UTF-8?q?=E5=88=B0=202.5.3=EF=BC=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                          | 2 +-
 src/components/RouterSearch/index.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index e6e33e39..2b927a1c 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
     "driver.js": "^1.3.1",
     "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.4.4",
+    "element-plus": "2.5.3",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue
index e9310b8f..c0352422 100644
--- a/src/components/RouterSearch/index.vue
+++ b/src/components/RouterSearch/index.vue
@@ -26,7 +26,7 @@
       placeholder="请输入菜单内容"
       :remote-method="remoteMethod"
       class="overflow-hidden transition-all-600"
-      :class="showTopSearch ? 'w-220px ml2' : 'w-0'"
+      :class="showTopSearch ? '!w-220px ml2' : '!w-0'"
       @change="handleChange"
     >
       <el-option

From 3be088e3708c898e25ba868000b5fcaad4ab5847 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 23:14:47 +0800
Subject: [PATCH 13/49] =?UTF-8?q?2023-11-29=20feat:=20=E6=8C=81=E4=B9=85?=
 =?UTF-8?q?=E5=8C=96=E7=BC=93=E5=AD=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                    | 2 +-
 src/store/index.ts              | 4 ++--
 src/store/modules/app.ts        | 3 ++-
 src/store/modules/lock.ts       | 6 +-----
 src/store/modules/permission.ts | 3 ++-
 src/store/modules/tagsView.ts   | 3 ++-
 6 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index 2b927a1c..4cc257ba 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
-    "pinia-plugin-persist": "^1.0.0",
+    "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
     "steady-xml": "^0.1.0",
diff --git a/src/store/index.ts b/src/store/index.ts
index 7740bdd3..63f00452 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,9 +1,9 @@
 import type { App } from 'vue'
 import { createPinia } from 'pinia'
-import piniaPersist from 'pinia-plugin-persist'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 
 const store = createPinia()
-store.use(piniaPersist)
+store.use(piniaPluginPersistedstate)
 
 export const setupStore = (app: App<Element>) => {
   app.use(store)
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
index 1d0c797a..87336181 100644
--- a/src/store/modules/app.ts
+++ b/src/store/modules/app.ts
@@ -268,7 +268,8 @@ export const useAppStore = defineStore('app', {
     setFooter(footer: boolean) {
       this.footer = footer
     }
-  }
+  },
+  persist: false
 })
 
 export const useAppStoreWithOut = () => {
diff --git a/src/store/modules/lock.ts b/src/store/modules/lock.ts
index bf6ceb49..68ae1d7d 100644
--- a/src/store/modules/lock.ts
+++ b/src/store/modules/lock.ts
@@ -10,7 +10,6 @@ interface LockState {
   lockInfo: lockInfo
 }
 
-// TODO 芋艿:【锁屏】这里有报错,后续解决下
 export const useLockStore = defineStore('lock', {
   state: (): LockState => {
     return {
@@ -41,10 +40,7 @@ export const useLockStore = defineStore('lock', {
       }
     }
   },
-  persist: {
-    enabled: true,
-    strategies: [{ key: 'lock', storage: localStorage }]
-  }
+  persist: true
 })
 
 export const useLockStoreWithOut = () => {
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
index c729cea0..7406fa36 100644
--- a/src/store/modules/permission.ts
+++ b/src/store/modules/permission.ts
@@ -59,7 +59,8 @@ export const usePermissionStore = defineStore('permission', {
     setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
       this.menuTabRouters = routers
     }
-  }
+  },
+  persist: false
 })
 
 export const usePermissionStoreWithOut = () => {
diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts
index a60d0e45..25a3a1fd 100644
--- a/src/store/modules/tagsView.ts
+++ b/src/store/modules/tagsView.ts
@@ -132,7 +132,8 @@ export const useTagsViewStore = defineStore('tagsView', {
         }
       }
     }
-  }
+  },
+  persist: false
 })
 
 export const useTagsViewStoreWithOut = () => {

From db106834a7f56e5b6a755a92070e6e253ba3f825 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 29 Feb 2024 23:37:25 +0800
Subject: [PATCH 14/49] =?UTF-8?q?bugfix=20=E4=BF=AE=E5=A4=8D=E5=AD=90?=
 =?UTF-8?q?=E8=8F=9C=E5=8D=95=E9=80=89=E4=B8=AD=E6=97=B6=EF=BC=8C=E7=88=B6?=
 =?UTF-8?q?=E8=8F=9C=E5=8D=95=E4=B9=9F=E9=AB=98=E4=BA=AE=E7=9A=84=E9=97=AE?=
 =?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/layout/components/Menu/src/Menu.vue | 1 -
 src/store/modules/permission.ts         | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/layout/components/Menu/src/Menu.vue b/src/layout/components/Menu/src/Menu.vue
index 7254b735..466cca50 100644
--- a/src/layout/components/Menu/src/Menu.vue
+++ b/src/layout/components/Menu/src/Menu.vue
@@ -149,7 +149,6 @@ $prefix-cls: #{$namespace}-menu;
     }
 
     // 设置选中时的高亮背景和高亮颜色
-    .#{$elNamespace}-sub-menu.is-active,
     .#{$elNamespace}-menu-item.is-active {
       color: var(--left-menu-text-active-color) !important;
       background-color: var(--left-menu-bg-active-color) !important;
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
index 7406fa36..5e3287a7 100644
--- a/src/store/modules/permission.ts
+++ b/src/store/modules/permission.ts
@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { store } from '../index'
+import { store } from '@/store'
 import { cloneDeep } from 'lodash-es'
 import remainingRouter from '@/router/modules/remaining'
 import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'

From ff0566bb7fd2b571b8c617531b1f1a7af6bb1603 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 1 Mar 2024 00:14:23 +0800
Subject: [PATCH 15/49] =?UTF-8?q?2024-02-06=20fix:=20=E4=BF=AE=E5=A4=8DMen?=
 =?UTF-8?q?u=E7=BB=84=E4=BB=B6=E7=BC=A9=E7=95=A5=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=86=85=E6=A0=B7=E5=BC=8F=E4=B8=8D=E7=BB=9F?=
 =?UTF-8?q?=E4=B8=80=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../Menu/src/components/useRenderMenuItem.tsx | 89 +++++++++----------
 .../src/components/useRenderMenuTitle.tsx     |  9 +-
 2 files changed, 47 insertions(+), 51 deletions(-)

diff --git a/src/layout/components/Menu/src/components/useRenderMenuItem.tsx b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx
index 17a520a6..301313fe 100644
--- a/src/layout/components/Menu/src/components/useRenderMenuItem.tsx
+++ b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx
@@ -1,59 +1,50 @@
 import { ElSubMenu, ElMenuItem } from 'element-plus'
-import type { RouteMeta } from 'vue-router'
 import { hasOneShowingChild } from '../helper'
 import { isUrl } from '@/utils/is'
 import { useRenderMenuTitle } from './useRenderMenuTitle'
-import { useDesign } from '@/hooks/web/useDesign'
 import { pathResolve } from '@/utils/routerHelper'
 
-export const useRenderMenuItem = (
+const { renderMenuTitle } = useRenderMenuTitle()
+
+export const useRenderMenuItem = () =>
   // allRouters: AppRouteRecordRaw[] = [],
-  menuMode: 'vertical' | 'horizontal'
-) => {
-  const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
-    return routers.map((v) => {
-      const meta = (v.meta ?? {}) as RouteMeta
-      if (!meta.hidden) {
-        const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
-        const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
+  {
+    const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
+      return routers
+        .filter((v) => !v.meta?.hidden)
+        .map((v) => {
+          const meta = v.meta ?? {}
+          const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
+          const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
 
-        const { renderMenuTitle } = useRenderMenuTitle()
+          if (
+            oneShowingChild &&
+            (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
+            !meta?.alwaysShow
+          ) {
+            return (
+              <ElMenuItem
+                index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
+              >
+                {{
+                  default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
+                }}
+              </ElMenuItem>
+            )
+          } else {
+            return (
+              <ElSubMenu index={fullPath}>
+                {{
+                  title: () => renderMenuTitle(meta),
+                  default: () => renderMenuItem(v.children!, fullPath)
+                }}
+              </ElSubMenu>
+            )
+          }
+        })
+    }
 
-        if (
-          oneShowingChild &&
-          (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
-          !meta?.alwaysShow
-        ) {
-          return (
-            <ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
-              {{
-                default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
-              }}
-            </ElMenuItem>
-          )
-        } else {
-          const { getPrefixCls } = useDesign()
-
-          const preFixCls = getPrefixCls('menu-popper')
-          return (
-            <ElSubMenu
-              index={fullPath}
-              popperClass={
-                menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
-              }
-            >
-              {{
-                title: () => renderMenuTitle(meta),
-                default: () => renderMenuItem(v.children!, fullPath)
-              }}
-            </ElSubMenu>
-          )
-        }
-      }
-    })
+    return {
+      renderMenuItem
+    }
   }
-
-  return {
-    renderMenuItem
-  }
-}
diff --git a/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
index fc30b900..8941d9d7 100644
--- a/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
+++ b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
@@ -1,5 +1,6 @@
 import type { RouteMeta } from 'vue-router'
 import { Icon } from '@/components/Icon'
+import { useI18n } from '@/hooks/web/useI18n'
 
 export const useRenderMenuTitle = () => {
   const renderMenuTitle = (meta: RouteMeta) => {
@@ -9,10 +10,14 @@ export const useRenderMenuTitle = () => {
     return icon ? (
       <>
         <Icon icon={meta.icon}></Icon>
-        <span class="v-menu__title">{t(title as string)}</span>
+        <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+          {t(title as string)}
+        </span>
       </>
     ) : (
-      <span class="v-menu__title">{t(title as string)}</span>
+      <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+        {t(title as string)}
+      </span>
     )
   }
 

From 9b0890d19535b2c58b5b38c5f685330042474ba1 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 1 Mar 2024 01:05:03 +0800
Subject: [PATCH 16/49] =?UTF-8?q?1.=20=E5=BF=BD=E7=95=A5=20VITE=5FCJS=5FIG?=
 =?UTF-8?q?NORE=5FWARNING=20=E5=91=8A=E8=AD=A6=202.=20=E5=BF=BD=E7=95=A5?=
 =?UTF-8?q?=20@unocss/order=20=E5=92=8C=20@unocss/order-attributify=20?=
 =?UTF-8?q?=E5=91=8A=E8=AD=A6=203.=20=E4=BF=AE=E5=A4=8D=20https=20?=
 =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9=E7=9A=84=E5=91=8A=E8=AD=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .eslintrc.js   | 4 +++-
 package.json   | 2 +-
 vite.config.ts | 5 +----
 3 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index 70c91784..b28255ca 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -68,6 +68,8 @@ module.exports = defineConfig({
     ],
     'vue/multi-word-component-names': 'off',
     'vue/no-v-html': 'off',
-    'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
+    'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
+    '@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
+    '@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
   }
 })
diff --git a/package.json b/package.json
index 4cc257ba..3adc6750 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
   "private": false,
   "scripts": {
     "i": "pnpm install",
-    "dev": "vite --mode local-dev",
+    "dev": "VITE_CJS_IGNORE_WARNING=true vite --mode local-dev",
     "dev-server": "vite --mode dev",
     "ts:check": "vue-tsc --noEmit",
     "build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",
diff --git a/vite.config.ts b/vite.config.ts
index fe2d7131..8cba9150 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -25,10 +25,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     root: root,
     // 服务端渲染
     server: {
-      // 是否开启 https
-      https: false,
-      // 端口号
-      port: env.VITE_PORT,
+      port: env.VITE_PORT, // 端口号
       host: "0.0.0.0",
       open: env.VITE_OPEN === 'true',
       // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域

From 9ea698e86ca673ffb097f523a371473ded0d39f6 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 1 Mar 2024 12:44:33 +0800
Subject: [PATCH 17/49] =?UTF-8?q?bugfix=EF=BC=9A=E6=9A=82=E6=97=B6?=
 =?UTF-8?q?=E5=8E=BB=E6=8E=89=20VITE=5FCJS=5FIGNORE=5FWARNING=20=E5=8F=98?=
 =?UTF-8?q?=E9=87=8F=EF=BC=8C=E8=A7=A3=E5=86=B3=20Windows=20=E6=97=A0?=
 =?UTF-8?q?=E6=B3=95=E5=90=AF=E5=8A=A8=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3adc6750..4cc257ba 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
   "private": false,
   "scripts": {
     "i": "pnpm install",
-    "dev": "VITE_CJS_IGNORE_WARNING=true vite --mode local-dev",
+    "dev": "vite --mode local-dev",
     "dev-server": "vite --mode dev",
     "ts:check": "vue-tsc --noEmit",
     "build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",

From 6f38676af78349a79b5428bca082ea0d8b4f3142 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 1 Mar 2024 20:58:05 +0800
Subject: [PATCH 18/49] =?UTF-8?q?=F0=9F=8E=89=202.0.1=20=E7=89=88=E6=9C=AC?=
 =?UTF-8?q?=E5=8F=91=E5=B8=83=EF=BC=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4cc257ba..4b5816bc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.0.0-snapshot",
+  "version": "2.0.1-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,

From e4e37d0cb4cc7a92c84b0f5da862026aa216a413 Mon Sep 17 00:00:00 2001
From: moon69 <1016830869@qq.com>
Date: Sun, 3 Mar 2024 16:18:53 +0800
Subject: [PATCH 19/49] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=87=8D=E5=A4=8D?=
 =?UTF-8?q?=E7=9A=84=20pagination=20=E4=BA=8B=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/Pagination/index.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
index b88997b1..6bb00b3a 100644
--- a/src/components/Pagination/index.vue
+++ b/src/components/Pagination/index.vue
@@ -53,7 +53,7 @@ const props = defineProps({
   }
 })
 
-const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination'])
+const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
 const currentPage = computed({
   get() {
     return props.page

From feadd022e7c0e67e5176b0bddc0361f4ef90da4b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 13 Mar 2024 21:18:26 +0800
Subject: [PATCH 20/49] =?UTF-8?q?BPM=EF=BC=9A=E9=87=8D=E6=9E=84=E5=AE=A1?=
 =?UTF-8?q?=E6=89=B9=E4=BA=BA=E7=9A=84=E5=88=86=E9=85=8D=E8=A7=84=E5=88=99?=
 =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=8C=E7=A7=BB=E9=99=A4=20bpm=5Ftask=5Fas?=
 =?UTF-8?q?sign=5Frule=20=E8=A1=A8=EF=BC=8C=E5=AD=98=E5=82=A8=E5=9C=A8=20b?=
 =?UTF-8?q?pmn=20=E7=9A=84=20userTask=20=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/taskAssignRule/index.ts           |  29 --
 .../descriptor/activitiDescriptor.json        |  10 +
 .../plugins/descriptor/camundaDescriptor.json |  10 +
 .../descriptor/flowableDescriptor.json        |  10 +
 .../package/penal/PropertiesPanel.vue         |   4 +-
 .../package/penal/base/ElementBaseInfo.vue    |   2 +
 .../package/penal/task/ElementTask.vue        |   3 +-
 .../penal/task/task-components/UserTask.vue   | 235 +++++++++++-----
 src/router/modules/remaining.ts               |  11 -
 src/views/bpm/definition/index.vue            |  22 --
 src/views/bpm/model/index.vue                 |  18 --
 .../bpm/taskAssignRule/TaskAssignRuleForm.vue | 250 ------------------
 src/views/bpm/taskAssignRule/index.vue        | 136 ----------
 13 files changed, 210 insertions(+), 530 deletions(-)
 delete mode 100644 src/api/bpm/taskAssignRule/index.ts
 delete mode 100644 src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
 delete mode 100644 src/views/bpm/taskAssignRule/index.vue

diff --git a/src/api/bpm/taskAssignRule/index.ts b/src/api/bpm/taskAssignRule/index.ts
deleted file mode 100644
index 5fbe342d..00000000
--- a/src/api/bpm/taskAssignRule/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import request from '@/config/axios'
-
-export type TaskAssignVO = {
-  id: number
-  modelId: string
-  processDefinitionId: string
-  taskDefinitionKey: string
-  taskDefinitionName: string
-  options: string[]
-  type: number
-}
-
-export const getTaskAssignRuleList = async (params) => {
-  return await request.get({ url: '/bpm/task-assign-rule/list', params })
-}
-
-export const createTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.post({
-    url: '/bpm/task-assign-rule/create',
-    data: data
-  })
-}
-
-export const updateTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.put({
-    url: '/bpm/task-assign-rule/update',
-    data: data
-  })
-}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
index db5e4901..ef1371e2 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
@@ -332,6 +332,16 @@
           "name": "multiinstance_condition",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "assignType",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "assignOptions",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
index 79b86bca..ccf06d4e 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "assignType",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "assignOptions",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
index 7fe7ad14..3a80c232 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "assignType",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "assignOptions",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },
diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
index 377592f4..1165568e 100644
--- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
@@ -32,7 +32,7 @@
         替代,提供更好的表单设计功能
       </el-collapse-item>
       <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task">
-        <template #title><Icon icon="ep:checked" />任务</template>
+        <template #title><Icon icon="ep:checked" />任务(审批人)</template>
         <element-task :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item
@@ -40,7 +40,7 @@
         v-if="elementType.indexOf('Task') !== -1"
         key="multiInstance"
       >
-        <template #title><Icon icon="ep:help-filled" />多实例</template>
+        <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template>
         <element-multi-instance :business-object="elementBusinessObject" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="listeners" key="listeners">
diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 639c1cb2..03f82e76 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -138,6 +138,8 @@ const updateBaseInfo = (key) => {
     bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj)
   }
 }
+
+// TODO 芋艿:这里延迟,可能存在覆盖 userTask 的问题。。例如说,打开的时候,立马选中某个 usertask,则它的 id 会被覆盖。。。
 onMounted(() => {
   // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新
   setTimeout(() => {
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
index 33a12a74..e808af39 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
@@ -1,7 +1,8 @@
 <template>
   <div class="panel-tab__content">
     <el-form size="small" label-width="90px">
-      <el-form-item label="异步延续">
+      <!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none -->
+      <el-form-item label="异步延续" style="display: none">
         <el-checkbox
           v-model="taskConfigForm.asyncBefore"
           label="异步前"
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index 7b793dbc..aca53a9d 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -1,85 +1,183 @@
 <template>
-  <div style="margin-top: 16px">
-    <!--    <el-form-item label="处理用户">-->
-    <!--      <el-select v-model="userTaskForm.assignee" @change="updateElementTask('assignee')">-->
-    <!--        <el-option v-for="ak in mockData" :key="'ass-' + ak" :label="`用户${ak}`" :value="`user${ak}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选用户">-->
-    <!--      <el-select v-model="userTaskForm.candidateUsers" multiple collapse-tags @change="updateElementTask('candidateUsers')">-->
-    <!--        <el-option v-for="uk in mockData" :key="'user-' + uk" :label="`用户${uk}`" :value="`user${uk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选分组">-->
-    <!--      <el-select v-model="userTaskForm.candidateGroups" multiple collapse-tags @change="updateElementTask('candidateGroups')">-->
-    <!--        <el-option v-for="gk in mockData" :key="'ass-' + gk" :label="`分组${gk}`" :value="`group${gk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <el-form-item label="到期时间">
-      <el-input v-model="userTaskForm.dueDate" clearable @change="updateElementTask('dueDate')" />
-    </el-form-item>
-    <el-form-item label="跟踪时间">
-      <el-input
-        v-model="userTaskForm.followUpDate"
+  <el-form label-width="100px">
+    <el-form-item label="规则类型" prop="assignType">
+      <el-select
+        v-model="userTaskForm.assignType"
         clearable
-        @change="updateElementTask('followUpDate')"
+        style="width: 100%"
+        @change="changeAssignType"
+      >
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item v-if="userTaskForm.assignType == 10" label="指定角色" prop="assignOptions">
+      <el-select
+        v-model="userTaskForm.assignOptions"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+    </el-form-item>
+    <el-form-item
+      v-if="userTaskForm.assignType == 20 || userTaskForm.assignType == 21"
+      label="指定部门"
+      prop="assignOptions"
+      span="24"
+    >
+      <el-tree-select
+        ref="treeRef"
+        v-model="userTaskForm.assignOptions"
+        :data="deptTreeOptions"
+        :props="defaultProps"
+        empty-text="加载中,请稍后"
+        multiple
+        node-key="id"
+        show-checkbox
+        @change="updateElementTask"
       />
     </el-form-item>
-    <el-form-item label="优先级">
-      <el-input v-model="userTaskForm.priority" clearable @change="updateElementTask('priority')" />
+    <el-form-item
+      v-if="userTaskForm.assignType == 22"
+      label="指定岗位"
+      prop="assignOptions"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.assignOptions"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option v-for="item in postOptions" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
     </el-form-item>
-    友情提示:任务的分配规则,使用
-    <router-link target="_blank" :to="{ path: '/bpm/manager/model' }"
-      ><el-link type="danger">流程模型</el-link>
-    </router-link>
-    下的【分配规则】替代,提供指定角色、部门负责人、部门成员、岗位、工作组、自定义脚本等 7
-    种维护的任务分配维度,更加灵活!
-  </div>
+    <el-form-item
+      v-if="
+        userTaskForm.assignType == 30 ||
+        userTaskForm.assignType == 31 ||
+        userTaskForm.assignType == 32
+      "
+      label="指定用户"
+      prop="assignOptions"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.assignOptions"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="item in userOptions"
+          :key="item.id"
+          :label="item.nickname"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item v-if="userTaskForm.assignType === 40" label="指定用户组" prop="assignOptions">
+      <el-select
+        v-model="userTaskForm.assignOptions"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="item in userGroupOptions"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item v-if="userTaskForm.assignType === 50" label="指定脚本" prop="assignOptions">
+      <el-select
+        v-model="userTaskForm.assignOptions"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="dict in taskAssignScriptDictDatas"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+  </el-form>
 </template>
 
 <script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
   id: String,
   type: String
 })
-const defaultTaskForm = ref({
-  assignee: '',
-  candidateUsers: [],
-  candidateGroups: [],
-  dueDate: '',
-  followUpDate: '',
-  priority: ''
+const userTaskForm = ref({
+  assignType: undefined, // 分配规则
+  assignOptions: [] // 分配选项
 })
-const userTaskForm = ref<any>({})
 // const mockData=ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
 const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
 
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const deptTreeOptions = ref() // 部门树
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
+
 const resetTaskForm = () => {
-  for (let key in defaultTaskForm.value) {
-    let value
-    if (key === 'candidateUsers' || key === 'candidateGroups') {
-      value = bpmnElement.value?.businessObject[key]
-        ? bpmnElement.value.businessObject[key].split(',')
-        : []
-    } else {
-      value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
-    }
-    userTaskForm.value[key] = value
+  const businessObject = bpmnElement.value.businessObject
+  if (!businessObject) {
+    return
+  }
+  if (businessObject.assignType != undefined) {
+    userTaskForm.value.assignType = parseInt(businessObject.assignType) as any
+  } else {
+    userTaskForm.value.assignType = undefined
+  }
+  if (businessObject.assignOptions && businessObject.assignOptions.length > 0) {
+    userTaskForm.value.assignOptions = businessObject.assignOptions?.split(',').map((item) => +item)
+  } else {
+    userTaskForm.value.assignOptions = []
   }
 }
-const updateElementTask = (key) => {
-  const taskAttr = Object.create(null)
-  if (key === 'candidateUsers' || key === 'candidateGroups') {
-    taskAttr[key] =
-      userTaskForm.value[key] && userTaskForm.value[key].length
-        ? userTaskForm.value[key].join()
-        : null
-  } else {
-    taskAttr[key] = userTaskForm.value[key] || null
-  }
-  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+
+/** 更新 assignType 字段时,需要清空 assignOptions,并触发 bpmn 图更新 */
+const changeAssignType = () => {
+  userTaskForm.value.assignOptions = []
+  updateElementTask()
+}
+
+/** 选中某个 options 时候,更新 bpmn 图  */
+const updateElementTask = () => {
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    assignType: userTaskForm.value.assignType,
+    assignOptions: userTaskForm.value.assignOptions.join(',')
+  })
 }
 
 watch(
@@ -92,6 +190,21 @@ watch(
   },
   { immediate: true }
 )
+
+onMounted(async () => {
+  // 获得角色列表
+  roleOptions.value = await RoleApi.getSimpleRoleList()
+  // 获得部门列表
+  const deptOptions = await DeptApi.getSimpleDeptList()
+  deptTreeOptions.value = handleTree(deptOptions, 'id')
+  // 获得岗位列表
+  postOptions.value = await PostApi.getSimplePostList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得用户组列表
+  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
+})
+
 onBeforeUnmount(() => {
   bpmnElement.value = null
 })
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index f63bee6e..b08035de 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -278,17 +278,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/bpm/manager/model'
         }
       },
-      {
-        path: '/manager/task-assign-rule',
-        component: () => import('@/views/bpm/taskAssignRule/index.vue'),
-        name: 'BpmTaskAssignRuleList',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '任务分配规则'
-        }
-      },
       {
         path: '/process-instance/create',
         component: () => import('@/views/bpm/processInstance/create/index.vue'),
diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue
index 31ed8413..923a5901 100644
--- a/src/views/bpm/definition/index.vue
+++ b/src/views/bpm/definition/index.vue
@@ -57,18 +57,6 @@
         width="300"
         show-overflow-tooltip
       />
-      <el-table-column label="操作" align="center" width="150" fixed="right">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="handleAssignRule(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:query']"
-          >
-            分配规则
-          </el-button>
-        </template>
-      </el-table-column>
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -129,16 +117,6 @@ const getList = async () => {
   }
 }
 
-/** 点击任务分配按钮 */
-const handleAssignRule = (row) => {
-  push({
-    name: 'BpmTaskAssignRuleList',
-    query: {
-      modelId: row.id
-    }
-  })
-}
-
 /** 流程表单的详情按钮操作 */
 const formDetailVisible = ref(false)
 const formDetailPreview = ref({
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index c7318891..dc47ff64 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -161,14 +161,6 @@
           >
             设计流程
           </el-button>
-          <el-button
-            link
-            type="primary"
-            @click="handleAssignRule(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:query']"
-          >
-            分配规则
-          </el-button>
           <el-button
             link
             type="primary"
@@ -347,16 +339,6 @@ const handleDeploy = async (row) => {
   } catch {}
 }
 
-/** 点击任务分配按钮 */
-const handleAssignRule = (row) => {
-  push({
-    name: 'BpmTaskAssignRuleList',
-    query: {
-      modelId: row.id
-    }
-  })
-}
-
 /** 跳转到指定流程定义列表 */
 const handleDefinitionList = (row) => {
   push({
diff --git a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue b/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
deleted file mode 100644
index 9b215e0f..00000000
--- a/src/views/bpm/taskAssignRule/TaskAssignRuleForm.vue
+++ /dev/null
@@ -1,250 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="修改任务规则" width="600">
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
-      <el-form-item label="任务名称" prop="taskDefinitionName">
-        <el-input v-model="formData.taskDefinitionName" disabled placeholder="请输入流标标识" />
-      </el-form-item>
-      <el-form-item label="任务标识" prop="taskDefinitionKey">
-        <el-input v-model="formData.taskDefinitionKey" disabled placeholder="请输入任务标识" />
-      </el-form-item>
-      <el-form-item label="规则类型" prop="type">
-        <el-select v-model="formData.type" clearable style="width: 100%">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
-        <el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in roleOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 20 || formData.type === 21"
-        label="指定部门"
-        prop="deptIds"
-        span="24"
-      >
-        <el-tree-select
-          ref="treeRef"
-          v-model="formData.deptIds"
-          :data="deptTreeOptions"
-          :props="defaultProps"
-          empty-text="加载中,请稍后"
-          multiple
-          node-key="id"
-          show-checkbox
-        />
-      </el-form-item>
-      <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
-        <el-select v-model="formData.postIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in postOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
-        label="指定用户"
-        prop="userIds"
-        span="24"
-      >
-        <el-select v-model="formData.userIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userOptions"
-            :key="parseInt(item.id)"
-            :label="item.nickname"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
-        <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userGroupOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
-        <el-select v-model="formData.scripts" clearable multiple style="width: 100%">
-          <el-option
-            v-for="dict in taskAssignScriptDictDatas"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-    </el-form>
-    <!-- 操作按钮 -->
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-
-defineOptions({ name: 'BpmTaskAssignRuleForm' })
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref({
-  type: Number(undefined),
-  modelId: '',
-  options: [],
-  roleIds: [],
-  deptIds: [],
-  postIds: [],
-  userIds: [],
-  userGroupIds: [],
-  scripts: []
-})
-const formRules = reactive({
-  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
-  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
-  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
-  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
-  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
-  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
-  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const deptTreeOptions = ref() // 部门树
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 打开弹窗 */
-const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => {
-  // 1. 先重置表单
-  resetForm()
-  // 2. 再设置表单
-  formData.value = {
-    ...row,
-    modelId: modelId,
-    options: [],
-    roleIds: [],
-    deptIds: [],
-    postIds: [],
-    userIds: [],
-    userGroupIds: [],
-    scripts: []
-  }
-  // 将 options 赋值到对应的 roleIds 等选项
-  if (row.type === 10) {
-    formData.value.roleIds.push(...row.options)
-  } else if (row.type === 20 || row.type === 21) {
-    formData.value.deptIds.push(...row.options)
-  } else if (row.type === 22) {
-    formData.value.postIds.push(...row.options)
-  } else if (row.type === 30 || row.type === 31 || row.type === 32) {
-    formData.value.userIds.push(...row.options)
-  } else if (row.type === 40) {
-    formData.value.userGroupIds.push(...row.options)
-  } else if (row.type === 50) {
-    formData.value.scripts.push(...row.options)
-  }
-  // 打开弹窗
-  dialogVisible.value = true
-
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  deptTreeOptions.value = handleTree(deptOptions.value, 'id')
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-
-  // 构建表单
-  const form = {
-    ...formData.value,
-    taskDefinitionName: undefined
-  }
-  // 将 roleIds 等选项赋值到 options 中
-  if (form.type === 10) {
-    form.options = form.roleIds
-  } else if (form.type === 20 || form.type === 21) {
-    form.options = form.deptIds
-  } else if (form.type === 22) {
-    form.options = form.postIds
-  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
-    form.options = form.userIds
-  } else if (form.type === 40) {
-    form.options = form.userGroupIds
-  } else if (form.type === 50) {
-    form.options = form.scripts
-  }
-  form.roleIds = undefined
-  form.deptIds = undefined
-  form.postIds = undefined
-  form.userIds = undefined
-  form.userGroupIds = undefined
-  form.scripts = undefined
-
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = form as unknown as TaskAssignRuleApi.TaskAssignVO
-    if (!data.id) {
-      await TaskAssignRuleApi.createTaskAssignRule(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await TaskAssignRuleApi.updateTaskAssignRule(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formRef.value?.resetFields()
-}
-</script>
diff --git a/src/views/bpm/taskAssignRule/index.vue b/src/views/bpm/taskAssignRule/index.vue
deleted file mode 100644
index 0fe9bde6..00000000
--- a/src/views/bpm/taskAssignRule/index.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<template>
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="任务名" align="center" prop="taskDefinitionName" />
-      <el-table-column label="任务标识" align="center" prop="taskDefinitionKey" />
-      <el-table-column label="规则类型" align="center" prop="type">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE" :value="scope.row.type" />
-        </template>
-      </el-table-column>
-      <el-table-column label="规则范围" align="center" prop="options">
-        <template #default="scope">
-          <el-tag class="mr-5px" :key="option" v-for="option in scope.row.options">
-            {{ getAssignRuleOptionName(scope.row.type, option) }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column v-if="queryParams.modelId" label="操作" align="center">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm(scope.row)"
-            v-hasPermi="['bpm:task-assign-rule:update']"
-          >
-            修改
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </ContentWrap>
-  <!-- 添加/修改弹窗 -->
-  <TaskAssignRuleForm ref="formRef" @success="getList" />
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-import TaskAssignRuleForm from './TaskAssignRuleForm.vue'
-
-defineOptions({ name: 'BpmTaskAssignRule' })
-
-const { query } = useRoute() // 查询参数
-
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  modelId: query.modelId, // 流程模型的编号。如果 modelId 非空,则用于流程模型的查看与配置
-  processDefinitionId: query.processDefinitionId // 流程定义的编号。如果 processDefinitionId 非空,则用于流程定义的查看,不支持配置
-})
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    list.value = await TaskAssignRuleApi.getTaskAssignRuleList(queryParams)
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 翻译规则范围 */
-// TODO 芋艿:各种 ts 报错
-const getAssignRuleOptionName = (type, option) => {
-  if (type === 10) {
-    for (const roleOption of roleOptions.value) {
-      if (roleOption.id === option) {
-        return roleOption.name
-      }
-    }
-  } else if (type === 20 || type === 21) {
-    for (const deptOption of deptOptions.value) {
-      if (deptOption.id === option) {
-        return deptOption.name
-      }
-    }
-  } else if (type === 22) {
-    for (const postOption of postOptions.value) {
-      if (postOption.id === option) {
-        return postOption.name
-      }
-    }
-  } else if (type === 30 || type === 31 || type === 32) {
-    for (const userOption of userOptions.value) {
-      if (userOption.id === option) {
-        return userOption.nickname
-      }
-    }
-  } else if (type === 40) {
-    for (const userGroupOption of userGroupOptions.value) {
-      if (userGroupOption.id === option) {
-        return userGroupOption.name
-      }
-    }
-  } else if (type === 50) {
-    option = option + '' // 转换成 string
-    for (const dictData of taskAssignScriptDictDatas) {
-      if (dictData.value === option) {
-        return dictData.label
-      }
-    }
-  }
-  return '未知(' + option + ')'
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (row: TaskAssignRuleApi.TaskAssignVO) => {
-  formRef.value.open(queryParams.modelId, row)
-}
-
-/** 初始化 */
-onMounted(async () => {
-  await getList()
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-})
-</script>

From 960f27f6efd6e7768ab93ff5d4713cb0157981a5 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 14 Mar 2024 12:54:28 +0800
Subject: [PATCH 21/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=20flowable?=
 =?UTF-8?q?=20expression=20=E8=A1=A8=E8=BE=BE=E5=BC=8F=EF=BC=8C=E6=9B=BF?=
 =?UTF-8?q?=E4=BB=A3=E7=8E=B0=E6=9C=89=20BpmTaskAssignScript=EF=BC=8C?=
 =?UTF-8?q?=E6=9B=B4=E5=8A=A0=E7=81=B5=E6=B4=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../penal/task/task-components/UserTask.vue     | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index aca53a9d..18553b9f 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -117,6 +117,15 @@
         />
       </el-select>
     </el-form-item>
+    <el-form-item v-if="userTaskForm.assignType === 60" label="流程表达式" prop="assignOptions">
+      <el-input
+        type="textarea"
+        v-model="userTaskForm.assignOptions[0]"
+        clearable
+        style="width: 100%"
+        @change="updateElementTask"
+      />
+    </el-form-item>
   </el-form>
 </template>
 
@@ -160,7 +169,13 @@ const resetTaskForm = () => {
     userTaskForm.value.assignType = undefined
   }
   if (businessObject.assignOptions && businessObject.assignOptions.length > 0) {
-    userTaskForm.value.assignOptions = businessObject.assignOptions?.split(',').map((item) => +item)
+    if (userTaskForm.value.assignType === 60) {
+      userTaskForm.value.assignOptions = [businessObject.assignOptions]
+    } else {
+      userTaskForm.value.assignOptions = businessObject.assignOptions
+        .split(',')
+        .map((item) => +item)
+    }
   } else {
     userTaskForm.value.assignOptions = []
   }

From 5f7ccd4e7ce73ebc8f74825902cbcd13d4b68f4d Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 15 Mar 2024 00:19:23 +0800
Subject: [PATCH 22/49] =?UTF-8?q?BPM=EF=BC=9A=E9=87=8D=E6=9E=84=E6=B5=81?=
 =?UTF-8?q?=E7=A8=8B=E5=88=86=E9=85=8D=E4=BA=BA=E7=9A=84=E5=AE=9E=E7=8E=B0?=
 =?UTF-8?q?=EF=BC=8C=E9=80=9A=E8=BF=87=20BpmTaskCandidateStrategy=20?=
 =?UTF-8?q?=E7=AD=96=E7=95=A5=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../descriptor/activitiDescriptor.json        |   4 +-
 .../plugins/descriptor/camundaDescriptor.json |   4 +-
 .../descriptor/flowableDescriptor.json        |   4 +-
 .../penal/task/task-components/UserTask.vue   | 103 +++++++++---------
 src/utils/dict.ts                             |   3 +-
 .../detail/TaskCCDialogForm.vue               |   2 +-
 6 files changed, 57 insertions(+), 63 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
index ef1371e2..94ba8f6c 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
@@ -334,12 +334,12 @@
           "type": "String"
         },
         {
-          "name": "assignType",
+          "name": "candidateStrategy",
           "isAttr": true,
           "type": "String"
         },
         {
-          "name": "assignOptions",
+          "name": "candidateParam",
           "isAttr": true,
           "type": "String"
         }
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
index ccf06d4e..8322561e 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
@@ -321,12 +321,12 @@
           "type": "String"
         },
         {
-          "name": "assignType",
+          "name": "candidateStrategy",
           "isAttr": true,
           "type": "String"
         },
         {
-          "name": "assignOptions",
+          "name": "candidateParam",
           "isAttr": true,
           "type": "String"
         }
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
index 3a80c232..4ea632a0 100644
--- a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
@@ -321,12 +321,12 @@
           "type": "String"
         },
         {
-          "name": "assignType",
+          "name": "candidateStrategy",
           "isAttr": true,
           "type": "String"
         },
         {
-          "name": "assignOptions",
+          "name": "candidateParam",
           "isAttr": true,
           "type": "String"
         }
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index 18553b9f..013719ea 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -1,23 +1,27 @@
 <template>
   <el-form label-width="100px">
-    <el-form-item label="规则类型" prop="assignType">
+    <el-form-item label="规则类型" prop="candidateStrategy">
       <el-select
-        v-model="userTaskForm.assignType"
+        v-model="userTaskForm.candidateStrategy"
         clearable
         style="width: 100%"
-        @change="changeAssignType"
+        @change="changecandidateStrategy"
       >
         <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
           :key="dict.value"
           :label="dict.label"
           :value="dict.value"
         />
       </el-select>
     </el-form-item>
-    <el-form-item v-if="userTaskForm.assignType == 10" label="指定角色" prop="assignOptions">
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 10"
+      label="指定角色"
+      prop="candidateParam"
+    >
       <el-select
-        v-model="userTaskForm.assignOptions"
+        v-model="userTaskForm.candidateParam"
         clearable
         multiple
         style="width: 100%"
@@ -27,14 +31,14 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.assignType == 20 || userTaskForm.assignType == 21"
+      v-if="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21"
       label="指定部门"
-      prop="assignOptions"
+      prop="candidateParam"
       span="24"
     >
       <el-tree-select
         ref="treeRef"
-        v-model="userTaskForm.assignOptions"
+        v-model="userTaskForm.candidateParam"
         :data="deptTreeOptions"
         :props="defaultProps"
         empty-text="加载中,请稍后"
@@ -45,13 +49,13 @@
       />
     </el-form-item>
     <el-form-item
-      v-if="userTaskForm.assignType == 22"
+      v-if="userTaskForm.candidateStrategy == 22"
       label="指定岗位"
-      prop="assignOptions"
+      prop="candidateParam"
       span="24"
     >
       <el-select
-        v-model="userTaskForm.assignOptions"
+        v-model="userTaskForm.candidateParam"
         clearable
         multiple
         style="width: 100%"
@@ -62,16 +66,16 @@
     </el-form-item>
     <el-form-item
       v-if="
-        userTaskForm.assignType == 30 ||
-        userTaskForm.assignType == 31 ||
-        userTaskForm.assignType == 32
+        userTaskForm.candidateStrategy == 30 ||
+        userTaskForm.candidateStrategy == 31 ||
+        userTaskForm.candidateStrategy == 32
       "
       label="指定用户"
-      prop="assignOptions"
+      prop="candidateParam"
       span="24"
     >
       <el-select
-        v-model="userTaskForm.assignOptions"
+        v-model="userTaskForm.candidateParam"
         clearable
         multiple
         style="width: 100%"
@@ -85,9 +89,13 @@
         />
       </el-select>
     </el-form-item>
-    <el-form-item v-if="userTaskForm.assignType === 40" label="指定用户组" prop="assignOptions">
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === 40"
+      label="指定用户组"
+      prop="candidateParam"
+    >
       <el-select
-        v-model="userTaskForm.assignOptions"
+        v-model="userTaskForm.candidateParam"
         clearable
         multiple
         style="width: 100%"
@@ -101,26 +109,14 @@
         />
       </el-select>
     </el-form-item>
-    <el-form-item v-if="userTaskForm.assignType === 50" label="指定脚本" prop="assignOptions">
-      <el-select
-        v-model="userTaskForm.assignOptions"
-        clearable
-        multiple
-        style="width: 100%"
-        @change="updateElementTask"
-      >
-        <el-option
-          v-for="dict in taskAssignScriptDictDatas"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item v-if="userTaskForm.assignType === 60" label="流程表达式" prop="assignOptions">
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === 60"
+      label="流程表达式"
+      prop="candidateParam"
+    >
       <el-input
         type="textarea"
-        v-model="userTaskForm.assignOptions[0]"
+        v-model="userTaskForm.candidateParam[0]"
         clearable
         style="width: 100%"
         @change="updateElementTask"
@@ -144,10 +140,9 @@ const props = defineProps({
   type: String
 })
 const userTaskForm = ref({
-  assignType: undefined, // 分配规则
-  assignOptions: [] // 分配选项
+  candidateStrategy: undefined, // 分配规则
+  candidateParam: [] // 分配选项
 })
-// const mockData=ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
 const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
 
@@ -156,42 +151,42 @@ const deptTreeOptions = ref() // 部门树
 const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
 
 const resetTaskForm = () => {
   const businessObject = bpmnElement.value.businessObject
   if (!businessObject) {
     return
   }
-  if (businessObject.assignType != undefined) {
-    userTaskForm.value.assignType = parseInt(businessObject.assignType) as any
+  if (businessObject.candidateStrategy != undefined) {
+    userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any
   } else {
-    userTaskForm.value.assignType = undefined
+    userTaskForm.value.candidateStrategy = undefined
   }
-  if (businessObject.assignOptions && businessObject.assignOptions.length > 0) {
-    if (userTaskForm.value.assignType === 60) {
-      userTaskForm.value.assignOptions = [businessObject.assignOptions]
+  if (businessObject.candidateParam && businessObject.candidateParam.length > 0) {
+    if (userTaskForm.value.candidateStrategy === 60) {
+      // 特殊:流程表达式,只有一个 input 输入框
+      userTaskForm.value.candidateParam = [businessObject.candidateParam]
     } else {
-      userTaskForm.value.assignOptions = businessObject.assignOptions
+      userTaskForm.value.candidateParam = businessObject.candidateParam
         .split(',')
         .map((item) => +item)
     }
   } else {
-    userTaskForm.value.assignOptions = []
+    userTaskForm.value.candidateParam = []
   }
 }
 
-/** 更新 assignType 字段时,需要清空 assignOptions,并触发 bpmn 图更新 */
-const changeAssignType = () => {
-  userTaskForm.value.assignOptions = []
+/** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
+const changecandidateStrategy = () => {
+  userTaskForm.value.candidateParam = []
   updateElementTask()
 }
 
 /** 选中某个 options 时候,更新 bpmn 图  */
 const updateElementTask = () => {
   bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
-    assignType: userTaskForm.value.assignType,
-    assignOptions: userTaskForm.value.assignOptions.join(',')
+    candidateStrategy: userTaskForm.value.candidateStrategy,
+    candidateParam: userTaskForm.value.candidateParam.join(',')
   })
 }
 
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index cc1774b3..e6b82500 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -138,10 +138,9 @@ export enum DICT_TYPE {
   // ========== BPM 模块 ==========
   BPM_MODEL_CATEGORY = 'bpm_model_category',
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
-  BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
+  BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
   BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
-  BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
 
   // ========== PAY 模块 ==========
diff --git a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue b/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
index bdfecadb..be3bb4f5 100644
--- a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
+++ b/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
@@ -17,7 +17,7 @@
       <el-form-item label="规则类型" prop="type">
         <el-select v-model="formData.type" clearable style="width: 100%">
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"

From 33d59c8b7dbc85e7cebd9117944028ed934f71a2 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 16 Mar 2024 00:05:58 +0800
Subject: [PATCH 23/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=E9=A1=BA?=
 =?UTF-8?q?=E5=BA=8F=E4=BC=9A=E7=AD=BE=E3=80=81=E6=88=96=E7=AD=BE=E7=9A=84?=
 =?UTF-8?q?=E8=83=BD=E5=8A=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../multi-instance/ElementMultiInstance.vue   | 37 ++++++++++++++++---
 1 file changed, 32 insertions(+), 5 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
index 28db5aa7..a921b812 100644
--- a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
@@ -1,11 +1,16 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="90px">
-      <el-form-item label="回路特性">
+      <el-form-item label="快捷配置">
+        <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
+        <el-button size="small" @click="changeConfig('会签')">会签</el-button>
+        <el-button size="small" @click="changeConfig('或签')">或签</el-button>
+      </el-form-item>
+      <el-form-item label="会签类型">
         <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
           <el-option label="并行多重事件" value="ParallelMultiInstance" />
           <el-option label="时序多重事件" value="SequentialMultiInstance" />
-          <el-option label="循环事件" value="StandardLoop" />
+          <!--          <el-option label="循环事件" value="StandardLoop" />-->
           <el-option label="无" value="Null" />
         </el-select>
       </el-form-item>
@@ -15,7 +20,7 @@
           loopCharacteristics === 'SequentialMultiInstance'
         "
       >
-        <el-form-item label="循环基数" key="loopCardinality">
+        <el-form-item label="循环数量" key="loopCardinality">
           <el-input
             v-model="loopInstanceForm.loopCardinality"
             clearable
@@ -25,7 +30,8 @@
         <el-form-item label="集合" key="collection" v-show="false">
           <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
         </el-form-item>
-        <el-form-item label="元素变量" key="elementVariable">
+        <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="元素变量" key="elementVariable" style="display: none">
           <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
         </el-form-item>
         <el-form-item label="完成条件" key="completionCondition">
@@ -35,7 +41,8 @@
             @change="updateLoopCondition"
           />
         </el-form-item>
-        <el-form-item label="异步状态" key="async">
+        <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="异步状态" key="async" style="display: none">
           <el-checkbox
             v-model="loopInstanceForm.asyncBefore"
             label="异步前"
@@ -124,6 +131,7 @@ const getElementLoop = (businessObject) => {
       businessObject.loopCharacteristics.extensionElements.values[0].body
   }
 }
+
 const changeLoopCharacteristicsType = (type) => {
   // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
   // 取消多实例配置
@@ -160,6 +168,7 @@ const changeLoopCharacteristicsType = (type) => {
     loopCharacteristics: toRaw(multiLoopInstance.value)
   })
 }
+
 // 循环基数
 const updateLoopCardinality = (cardinality) => {
   let loopCardinality = null
@@ -176,6 +185,7 @@ const updateLoopCardinality = (cardinality) => {
     }
   )
 }
+
 // 完成条件
 const updateLoopCondition = (condition) => {
   let completionCondition = null
@@ -192,6 +202,7 @@ const updateLoopCondition = (condition) => {
     }
   )
 }
+
 // 重试周期
 const updateLoopTimeCycle = (timeCycle) => {
   const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
@@ -209,6 +220,7 @@ const updateLoopTimeCycle = (timeCycle) => {
     }
   )
 }
+
 // 直接更新的基础信息
 const updateLoopBase = () => {
   bpmnInstances().modeling.updateModdleProperties(
@@ -220,6 +232,7 @@ const updateLoopBase = () => {
     }
   )
 }
+
 // 各异步状态
 const updateLoopAsync = (key) => {
   const { asyncBefore, asyncAfter } = loopInstanceForm.value
@@ -238,6 +251,20 @@ const updateLoopAsync = (key) => {
   )
 }
 
+const changeConfig = (config) => {
+  if (config === '依次审批') {
+    changeLoopCharacteristicsType('SequentialMultiInstance')
+    updateLoopCardinality('1')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '会签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '或签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances > 0 }')
+  }
+}
+
 onBeforeUnmount(() => {
   multiLoopInstance.value = null
   bpmnElement.value = null

From 59c7c49efa5d2af1b0f509c5695a449ffdcb544e Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 16 Mar 2024 16:43:47 +0800
Subject: [PATCH 24/49] =?UTF-8?q?BPM=EF=BC=9A=E6=B5=81=E7=A8=8B=E5=AE=9E?=
 =?UTF-8?q?=E4=BE=8B=E7=9A=84=20`status`=20=E7=8A=B6=E6=80=81=E5=AE=9E?=
 =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E4=BD=BF=E7=94=A8=20Flowable=20=E7=9A=84=20`?=
 =?UTF-8?q?variables`=20=E5=AD=98=E5=82=A8=EF=BC=8C=E7=A7=BB=E9=99=A4=20`b?=
 =?UTF-8?q?pm=5Fprocess=5Finstance=5Fext`=20=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../multi-instance/ElementMultiInstance.vue   |  1 -
 src/views/bpm/processInstance/index.vue       | 29 ++++++-------------
 2 files changed, 9 insertions(+), 21 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
index a921b812..c0ec1cad 100644
--- a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
@@ -10,7 +10,6 @@
         <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
           <el-option label="并行多重事件" value="ParallelMultiInstance" />
           <el-option label="时序多重事件" value="SequentialMultiInstance" />
-          <!--          <el-option label="循环事件" value="StandardLoop" />-->
           <el-option label="无" value="Null" />
         </el-select>
       </el-form-item>
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 8b9f8a1a..5ef0edf8 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -43,8 +43,13 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+      <el-form-item label="流程状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-240px"
+        >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
             :key="dict.value"
@@ -53,16 +58,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="结果" prop="result">
-        <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
       <el-form-item label="提交时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -106,20 +101,15 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="状态" prop="status">
+      <el-table-column label="流程" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="结果" prop="result">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
-        </template>
-      </el-table-column>
       <el-table-column
         label="提交时间"
         align="center"
-        prop="createTime"
+        prop="startTime"
         width="180"
         :formatter="dateFormatter"
       />
@@ -183,7 +173,6 @@ const queryParams = reactive({
   processDefinitionId: undefined,
   category: undefined,
   status: undefined,
-  result: undefined,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单

From de29528165768de61d07b486f3fef5f0b102a093 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 17 Mar 2024 17:25:40 +0800
Subject: [PATCH 25/49] =?UTF-8?q?BPM=EF=BC=9A=E7=AE=80=E5=8C=96=20task=20?=
 =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=9A=84=20VO=EF=BC=8C=E5=81=9A=E4=B8=80?=
 =?UTF-8?q?=E4=BA=9B=E5=90=88=E5=B9=B6=E3=80=82=E8=99=BD=E7=84=B6=E5=AD=97?=
 =?UTF-8?q?=E6=AE=B5=E4=B8=8A=E6=9C=89=E5=86=97=E4=BD=99=EF=BC=8C=E4=BD=86?=
 =?UTF-8?q?=E6=98=AF=E8=AF=BB=E4=BB=A3=E7=A0=81=E9=9A=BE=E5=BA=A6=E9=99=8D?=
 =?UTF-8?q?=E4=BD=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/task/index.ts                     |  8 ++--
 .../package/designer/ProcessViewer.vue        | 19 ++++----
 .../ProcessInstanceChildrenTaskList.vue       |  6 +--
 .../detail/ProcessInstanceTaskList.vue        | 26 +++++------
 .../detail/TaskReturnDialogForm.vue           | 18 ++++----
 .../bpm/processInstance/detail/index.vue      |  7 ++-
 src/views/bpm/task/done/TaskDetail.vue        |  2 +-
 src/views/bpm/task/done/index.vue             |  4 +-
 src/views/bpm/task/todo/index.vue             | 46 +++++++++----------
 9 files changed, 69 insertions(+), 67 deletions(-)

diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts
index df6d8160..78bbb984 100644
--- a/src/api/bpm/task/index.ts
+++ b/src/api/bpm/task/index.ts
@@ -43,12 +43,12 @@ export const exportTask = async (params) => {
 }
 
 // 获取所有可回退的节点
-export const getReturnList = async (params) => {
-  return await request.get({ url: '/bpm/task/return-list', params })
+export const getTaskListByReturn = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
 }
 
 // 回退
-export const returnTask = async (data) => {
+export const returnTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/return', data })
 }
 
@@ -70,7 +70,7 @@ export const taskAddSign = async (data) => {
  * 获取减签任务列表
  */
 export const getChildrenTaskList = async (id: string) => {
-  return await request.get({ url: '/bpm/task/children-list?taskId=' + id })
+  return await request.get({ url: '/bpm/task/children-list?parentId=' + id })
 }
 
 /**
diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
index a7958adb..27e6151a 100644
--- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -121,13 +121,13 @@ const highlightDiagram = async () => {
         return
       }
       // 高亮任务
-      canvas.addMarker(n.id, getResultCss(task.result))
+      canvas.addMarker(n.id, getResultCss(task.status))
       //标记是否高亮了进行中任务
-      if (task.result === 1) {
+      if (task.status === 1) {
         findProcessTask = true
       }
       // 如果非通过,就不走后面的线条了
-      if (task.result !== 2) {
+      if (task.status !== 2) {
         return
       }
       // 处理 outgoing 出线
@@ -205,10 +205,10 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:EndEvent') {
       // 结束节点
-      if (!processInstance.value || processInstance.value.result === 1) {
+      if (!processInstance.value || processInstance.value.status === 1) {
         return
       }
-      canvas.addMarker(n.id, getResultCss(processInstance.value.result))
+      canvas.addMarker(n.id, getResultCss(processInstance.value.status))
     } else if (n.$type === 'bpmn:ServiceTask') {
       //服务任务
       if (activity.startTime > 0 && activity.endTime === 0) {
@@ -226,6 +226,7 @@ const highlightDiagram = async () => {
     }
   })
   if (!isEmpty(removeTaskDefinitionKeyList)) {
+    // TODO 芋艿:后面 .definitionKey 再看
     taskList.value = taskList.value.filter(
       (item) => !removeTaskDefinitionKeyList.includes(item.definitionKey)
     )
@@ -321,7 +322,7 @@ const elementHover = (element) => {
       let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == task.result) {
+        if (element.value == task.status) {
           dataResult = element.label
         }
       })
@@ -333,7 +334,7 @@ const elementHover = (element) => {
       //             <p>部门:${task.assigneeUser.deptName}</p>
       //             <p>结果:${getIntDictOptions(
       //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //               task.result
+      //               task.status
       //             )}</p>
       //             <p>创建时间:${formatDate(task.createTime)}</p>`
       if (task.endTime) {
@@ -354,14 +355,14 @@ const elementHover = (element) => {
       let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == processInstance.value.result) {
+        if (element.value == processInstance.value.status) {
           dataResult = element.label
         }
       })
       html = `<p>结果:${dataResult}</p>`
       // html = `<p>结果:${getIntDictOptions(
       //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //   processInstance.value.result
+      //   processInstance.value.status
       // )}</p>`
       if (processInstance.value.endTime) {
         html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
index 363874cf..02aab321 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
@@ -17,9 +17,9 @@
     <el-table :data="baseTask.children" style="width: 100%" row-key="id" border>
       <el-table-column prop="assigneeUser.nickname" label="审批人" />
       <el-table-column prop="assigneeUser.deptName" label="所在部门" />
-      <el-table-column label="审批状态" prop="result">
+      <el-table-column label="审批状态" prop="status">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column
@@ -88,7 +88,7 @@ const handleSubSign = (item) => {
 const isSubSignButtonVisible = (task: any) => {
   if (task && task.children && !isEmpty(task.children)) {
     // 有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮
-    const subTask = task.children.find((item) => item.result === 1 || item.result === 9)
+    const subTask = task.children.find((item) => item.status === 1 || item.status === 9)
     return !isEmpty(subTask)
   }
   return false
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index 97287e99..9876d5d3 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -14,7 +14,7 @@
           >
             <p style="font-weight: 700">
               任务:{{ item.name }}
-              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.result" />
+              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.status" />
               <el-button
                 style="margin-left: 5px"
                 v-if="!isEmpty(item.children)"
@@ -73,19 +73,19 @@ defineProps({
 
 /** 获得任务对应的 icon */
 const getTimelineItemIcon = (item) => {
-  if (item.result === 1) {
+  if (item.status === 1) {
     return 'el-icon-time'
   }
-  if (item.result === 2) {
+  if (item.status === 2) {
     return 'el-icon-check'
   }
-  if (item.result === 3) {
+  if (item.status === 3) {
     return 'el-icon-close'
   }
-  if (item.result === 4) {
+  if (item.status === 4) {
     return 'el-icon-remove-outline'
   }
-  if (item.result === 5) {
+  if (item.status === 5) {
     return 'el-icon-back'
   }
   return ''
@@ -93,25 +93,25 @@ const getTimelineItemIcon = (item) => {
 
 /** 获得任务对应的颜色 */
 const getTimelineItemType = (item) => {
-  if (item.result === 1) {
+  if (item.status === 1) {
     return 'primary'
   }
-  if (item.result === 2) {
+  if (item.status === 2) {
     return 'success'
   }
-  if (item.result === 3) {
+  if (item.status === 3) {
     return 'danger'
   }
-  if (item.result === 4) {
+  if (item.status === 4) {
     return 'info'
   }
-  if (item.result === 5) {
+  if (item.status === 5) {
     return 'warning'
   }
-  if (item.result === 6) {
+  if (item.status === 6) {
     return 'default'
   }
-  if (item.result === 7 || item.result === 8) {
+  if (item.status === 7 || item.status === 8) {
     return 'warning'
   }
   return ''
diff --git a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue b/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue
index f93bf2c5..82a8f960 100644
--- a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue
+++ b/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue
@@ -7,13 +7,13 @@
       :rules="formRules"
       label-width="110px"
     >
-      <el-form-item label="退回节点" prop="targetDefinitionKey">
-        <el-select v-model="formData.targetDefinitionKey" clearable style="width: 100%">
+      <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
+        <el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%">
           <el-option
             v-for="item in returnList"
-            :key="item.definitionKey"
+            :key="item.taskDefinitionKey"
             :label="item.name"
-            :value="item.definitionKey"
+            :value="item.taskDefinitionKey"
           />
         </el-select>
       </el-form-item>
@@ -35,19 +35,19 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  targetDefinitionKey: undefined,
+  targetTaskDefinitionKey: undefined,
   reason: ''
 })
 const formRules = ref({
-  targetDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
+  targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
   reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
-const returnList = ref([])
+const returnList = ref([] as any)
 /** 打开弹窗 */
 const open = async (id: string) => {
-  returnList.value = await TaskApi.getReturnList({ taskId: id })
+  returnList.value = await TaskApi.getTaskListByReturn(id)
   if (returnList.value.length === 0) {
     message.warning('当前没有可回退的节点')
     return false
@@ -82,7 +82,7 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    targetDefinitionKey: undefined,
+    targetTaskDefinitionKey: undefined,
     reason: ''
   }
   formRef.value?.resetFields()
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 074d1329..3ce665f6 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -254,7 +254,7 @@ const getTaskList = async () => {
     tasks.value = []
     // 1.1 移除已取消的审批
     data.forEach((task) => {
-      if (task.result !== 4) {
+      if (task.status !== 4) {
         tasks.value.push(task)
       }
     })
@@ -291,7 +291,10 @@ const loadRunningTask = (tasks) => {
       loadRunningTask(task.children)
     }
     // 2.1 只有待处理才需要
-    if (task.result !== 1 && task.result !== 6) {
+    // if (task.status !== 1 && task.status !== 6) {
+    //   return
+    // }
+    if (task.status !== 1 && task.status !== 6) {
       return
     }
     // 2.2 自己不是处理人
diff --git a/src/views/bpm/task/done/TaskDetail.vue b/src/views/bpm/task/done/TaskDetail.vue
index 5bc06f19..ff8f313d 100644
--- a/src/views/bpm/task/done/TaskDetail.vue
+++ b/src/views/bpm/task/done/TaskDetail.vue
@@ -14,7 +14,7 @@
         {{ detailData.processInstance.startUserNickname }}
       </el-descriptions-item>
       <el-descriptions-item label="状态">
-        <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="detailData.result" />
+        <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="detailData.status" />
       </el-descriptions-item>
       <el-descriptions-item label="原因">
         {{ detailData.reason }}
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index ee1e1d14..b190bd4f 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -50,9 +50,9 @@
       <el-table-column align="center" label="任务名称" prop="name" />
       <el-table-column align="center" label="所属流程" prop="processInstance.name" />
       <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
-      <el-table-column align="center" label="状态" prop="result">
+      <el-table-column align="center" label="审批状态" prop="status">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column align="center" label="原因" prop="reason" />
diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue
index 29ba73dd..43d29921 100644
--- a/src/views/bpm/task/todo/index.vue
+++ b/src/views/bpm/task/todo/index.vue
@@ -46,27 +46,33 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="任务编号" prop="id" width="300px" />
-      <el-table-column align="center" label="任务名称" prop="name" />
-      <el-table-column align="center" label="所属流程" prop="processInstance.name" />
-      <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="创建时间"
+        label="发起时间"
         prop="createTime"
         width="180"
       />
-      <el-table-column label="任务状态" prop="suspensionState">
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
         <template #default="scope">
-          <el-tag v-if="scope.row.suspensionState === 1" type="success">激活</el-tag>
-          <el-tag v-if="scope.row.suspensionState === 2" type="warning">挂起</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" label="操作">
-        <template #default="scope">
-          <el-button link type="primary" @click="handleAudit(scope.row)">审批进度</el-button>
-          <el-button link type="primary" @click="handleCC(scope.row)">抄送</el-button>
+          <el-button link type="primary" @click="handleAudit(scope.row)">办理</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -77,16 +83,14 @@
       :total="total"
       @pagination="getList"
     />
-    <TaskCCDialogForm ref="taskCCDialogForm" />
   </ContentWrap>
 </template>
 
 <script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
-import TaskCCDialogForm from '../../processInstance/detail/TaskCCDialogForm.vue'
 
-defineOptions({ name: 'BpmDoneTask' })
+defineOptions({ name: 'BpmTodoTask' })
 
 const { push } = useRouter() // 路由
 
@@ -126,7 +130,7 @@ const resetQuery = () => {
 }
 
 /** 处理审批按钮 */
-const handleAudit = (row) => {
+const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {
@@ -135,12 +139,6 @@ const handleAudit = (row) => {
   })
 }
 
-const taskCCDialogForm = ref()
-/** 处理抄送按钮 */
-const handleCC = (row) => {
-  taskCCDialogForm.value.open(row)
-}
-
 /** 初始化 **/
 onMounted(() => {
   getList()

From 03b8bd5e2236b969f70d1348272e5c363ca6cb51 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 17 Mar 2024 22:14:28 +0800
Subject: [PATCH 26/49] =?UTF-8?q?BPM=EF=BC=9A=E8=B0=83=E6=95=B4=E6=8A=84?=
 =?UTF-8?q?=E9=80=81=E9=80=BB=E8=BE=91=E7=9A=84=E5=AE=9E=E7=8E=B0=EF=BC=8C?=
 =?UTF-8?q?=E6=94=B9=E6=88=90=E5=AE=A1=E6=89=B9=E9=80=9A=E8=BF=87=E3=80=81?=
 =?UTF-8?q?=E4=B8=8D=E9=80=9A=E8=BF=87=E6=97=B6=EF=BC=8C=E5=8F=AF=E9=80=89?=
 =?UTF-8?q?=E6=8B=A9=E6=8A=84=E9=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/processInstance/index.ts          |  34 +--
 src/utils/formatTime.ts                       |  10 +-
 .../detail/TaskCCDialogForm.vue               | 242 ------------------
 .../bpm/processInstance/detail/index.vue      |  29 ++-
 src/views/bpm/task/{cc => copy}/index.vue     |  37 ++-
 src/views/bpm/task/done/TaskDetail.vue        |  51 ----
 src/views/bpm/task/done/index.vue             |  69 +++--
 7 files changed, 95 insertions(+), 377 deletions(-)
 delete mode 100644 src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
 rename src/views/bpm/task/{cc => copy}/index.vue (74%)
 delete mode 100644 src/views/bpm/task/done/TaskDetail.vue

diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts
index a937eae2..1a5b5ecb 100644
--- a/src/api/bpm/processInstance/index.ts
+++ b/src/api/bpm/processInstance/index.ts
@@ -20,14 +20,14 @@ export type ProcessInstanceVO = {
   endTime: string
 }
 
-export type ProcessInstanceCCVO = {
-  type: number,
-  taskName: string,
-  taskKey: string,
-  processInstanceName: string,
-  processInstanceKey: string,
-  startUserId: string,
-  options:string [],
+export type ProcessInstanceCopyVO = {
+  type: number
+  taskName: string
+  taskKey: string
+  processInstanceName: string
+  processInstanceKey: string
+  startUserId: string
+  options: string[]
   reason: string
 }
 
@@ -51,20 +51,6 @@ export const getProcessInstance = async (id: number) => {
   return await request.get({ url: '/bpm/process-instance/get?id=' + id })
 }
 
-/**
- * 抄送
- * @param data 抄送数据
- * @returns 是否抄送成功
- */
-export const createProcessInstanceCC = async (data) => {
-  return await request.post({ url: '/bpm/process-instance/cc/create', data: data })
+export const getProcessInstanceCopyPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/copy/page', params })
 }
-
-/**
- * 抄送列表
- * @param params 
- * @returns 
- */
-export const getProcessInstanceCCPage = async (params) => {
-  return await request.get({ url: '/bpm/process-instance/cc/my-page', params })
-}
\ No newline at end of file
diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts
index ed434cb0..134a986e 100644
--- a/src/utils/formatTime.ts
+++ b/src/utils/formatTime.ts
@@ -175,18 +175,18 @@ export function formatPast2(ms: number): string {
   const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60)
   const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60)
   if (day > 0) {
-    return day + '天' + hour + '小时' + minute + '分钟'
+    return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟'
   }
   if (hour > 0) {
-    return hour + '小时' + minute + '分钟'
+    return hour + ' 小时 ' + minute + ' 分钟'
   }
   if (minute > 0) {
-    return minute + '分钟'
+    return minute + ' 分钟'
   }
   if (second > 0) {
-    return second + '秒'
+    return second + ' 秒'
   } else {
-    return 0 + '秒'
+    return 0 + ' 秒'
   }
 }
 
diff --git a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue b/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
deleted file mode 100644
index be3bb4f5..00000000
--- a/src/views/bpm/processInstance/detail/TaskCCDialogForm.vue
+++ /dev/null
@@ -1,242 +0,0 @@
-<!-- TODO @kyle:需要在讨论下;可能直接选人更合适 -->
-<template>
-  <Dialog v-model="dialogVisible" title="修改任务规则" width="600">
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
-      <el-form-item label="任务名称" prop="taskName">
-        <el-input v-model="formData.taskName" disabled placeholder="请输入任务名称" />
-      </el-form-item>
-      <el-form-item label="任务标识" prop="taskKey">
-        <el-input v-model="formData.taskKey" disabled placeholder="请输入任务标识" />
-      </el-form-item>
-      <el-form-item label="流程名称" prop="processInstanceName">
-        <el-input v-model="formData.processInstanceName" disabled placeholder="请输入流程名称" />
-      </el-form-item>
-      <el-form-item label="流程标识" prop="processInstanceKey">
-        <el-input v-model="formData.processInstanceKey" disabled placeholder="请输入流程标识" />
-      </el-form-item>
-      <el-form-item label="规则类型" prop="type">
-        <el-select v-model="formData.type" clearable style="width: 100%">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
-        <el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in roleOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 20 || formData.type === 21"
-        label="指定部门"
-        prop="deptIds"
-        span="24"
-      >
-        <el-tree-select
-          ref="treeRef"
-          v-model="formData.deptIds"
-          :data="deptTreeOptions"
-          :props="defaultProps"
-          empty-text="加载中,请稍后"
-          multiple
-          node-key="id"
-          show-checkbox
-        />
-      </el-form-item>
-      <el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
-        <el-select v-model="formData.postIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in postOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
-        label="指定用户"
-        prop="userIds"
-        span="24"
-      >
-        <el-select v-model="formData.userIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userOptions"
-            :key="parseInt(item.id)"
-            :label="item.nickname"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
-        <el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
-          <el-option
-            v-for="item in userGroupOptions"
-            :key="parseInt(item.id)"
-            :label="item.name"
-            :value="parseInt(item.id)"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
-        <el-select v-model="formData.scripts" clearable multiple style="width: 100%">
-          <el-option
-            v-for="dict in taskAssignScriptDictDatas"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="抄送原因" prop="reason">
-        <el-input v-model="formData.reason" placeholder="请输入抄送原因" type="textarea" />
-      </el-form-item>
-    </el-form>
-    <!-- 操作按钮 -->
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { defaultProps, handleTree } from '@/utils/tree'
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import * as RoleApi from '@/api/system/role'
-import * as DeptApi from '@/api/system/dept'
-import * as PostApi from '@/api/system/post'
-import * as UserApi from '@/api/system/user'
-import * as UserGroupApi from '@/api/bpm/userGroup'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref({
-  type: Number(undefined),
-  taskName: '',
-  taskKey: '',
-  processInstanceName: '',
-  processInstanceKey: '',
-  startUserId: '',
-  options: [],
-  roleIds: [],
-  deptIds: [],
-  postIds: [],
-  userIds: [],
-  userGroupIds: [],
-  scripts: [],
-  reason: ''
-})
-const formRules = reactive({
-  type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
-  roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
-  deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
-  postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
-  userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
-  userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
-  scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }],
-  reason: [{ required: true, message: '抄送原因不能为空', trigger: 'change' }]
-})
-const formRef = ref() // 表单 Ref
-const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
-const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
-const deptTreeOptions = ref() // 部门树
-const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
-const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
-
-/** 打开弹窗 */
-const open = async (row) => {
-  // 1. 先重置表单
-  resetForm()
-  // 2. 再设置表单
-  if (row != null) {
-    formData.value.type = undefined as unknown as number
-    formData.value.taskName = row.name
-    formData.value.taskKey = row.id
-    formData.value.processInstanceName = row.processInstance.name
-    formData.value.processInstanceKey = row.processInstance.id
-    formData.value.startUserId = row.processInstance.startUserId
-  }
-  // 打开弹窗
-  dialogVisible.value = true
-
-  // 获得角色列表
-  roleOptions.value = await RoleApi.getSimpleRoleList()
-  // 获得部门列表
-  deptOptions.value = await DeptApi.getSimpleDeptList()
-  deptTreeOptions.value = handleTree(deptOptions.value, 'id')
-  // 获得岗位列表
-  postOptions.value = await PostApi.getSimplePostList()
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-  // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-
-  // 构建表单
-  const form = {
-    ...formData.value
-  }
-  // 将 roleIds 等选项赋值到 options 中
-  if (form.type === 10) {
-    form.options = form.roleIds
-  } else if (form.type === 20 || form.type === 21) {
-    form.options = form.deptIds
-  } else if (form.type === 22) {
-    form.options = form.postIds
-  } else if (form.type === 30 || form.type === 31 || form.type === 32) {
-    form.options = form.userIds
-  } else if (form.type === 40) {
-    form.options = form.userGroupIds
-  } else if (form.type === 50) {
-    form.options = form.scripts
-  }
-  form.roleIds = undefined
-  form.deptIds = undefined
-  form.postIds = undefined
-  form.userIds = undefined
-  form.userGroupIds = undefined
-  form.scripts = undefined
-
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = form as unknown as ProcessInstanceApi.ProcessInstanceCCVO
-    await ProcessInstanceApi.createProcessInstanceCC(data)
-    console.log(data)
-    message.success(t('common.createSuccess'))
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formRef.value?.resetFields()
-}
-</script>
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 3ce665f6..800ff2f3 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -31,6 +31,16 @@
               type="textarea"
             />
           </el-form-item>
+          <el-form-item label="抄送人" prop="copyUserIds">
+            <el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人">
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
         </el-form>
         <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
           <el-button type="success" @click="handleAudit(item, true)">
@@ -118,6 +128,7 @@ import TaskDelegateForm from './TaskDelegateForm.vue'
 import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import { isEmpty } from '@/utils/is'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
 
@@ -161,7 +172,8 @@ const handleAudit = async (task, pass) => {
   // 2.1 提交审批
   const data = {
     id: task.id,
-    reason: auditForms.value[index].reason
+    reason: auditForms.value[index].reason,
+    copyUserIds: auditForms.value[index].copyUserIds
   }
   if (pass) {
     await TaskApi.approveTask(data)
@@ -180,21 +192,20 @@ const openTaskUpdateAssigneeForm = (id: string) => {
   taskUpdateAssigneeFormRef.value.open(id)
 }
 
-const taskDelegateForm = ref()
 /** 处理审批退回的操作 */
+const taskDelegateForm = ref()
 const handleDelegate = async (task) => {
   taskDelegateForm.value.open(task.id)
 }
 
-//回退弹框组件
-const taskReturnDialogRef = ref()
 /** 处理审批退回的操作 */
+const taskReturnDialogRef = ref()
 const handleBack = async (task) => {
   taskReturnDialogRef.value.open(task.id)
 }
 
-const taskAddSignDialogForm = ref()
 /** 处理审批加签的操作 */
+const taskAddSignDialogForm = ref()
 const handleSign = async (task) => {
   taskAddSignDialogForm.value.open(task.id)
 }
@@ -304,13 +315,17 @@ const loadRunningTask = (tasks) => {
     // 2.3 添加到处理任务
     runningTasks.value.push({ ...task })
     auditForms.value.push({
-      reason: ''
+      reason: '',
+      copyUserIds: []
     })
   })
 }
 
 /** 初始化 */
-onMounted(() => {
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
   getDetail()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
 })
 </script>
diff --git a/src/views/bpm/task/cc/index.vue b/src/views/bpm/task/copy/index.vue
similarity index 74%
rename from src/views/bpm/task/cc/index.vue
rename to src/views/bpm/task/copy/index.vue
index 50ddf889..dd41b2e1 100644
--- a/src/views/bpm/task/cc/index.vue
+++ b/src/views/bpm/task/copy/index.vue
@@ -11,14 +11,6 @@
           placeholder="请输入流程名称"
         />
       </el-form-item>
-      <el-form-item label="所属流程" prop="processDefinitionId">
-        <el-input
-          v-model="queryParams.processInstanceId"
-          placeholder="请输入流程定义的编号"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="抄送时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -46,12 +38,17 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="所属流程" prop="processInstanceId" width="300px" />
-      <el-table-column align="center" label="流程名称" prop="processInstanceName" />
-      <el-table-column align="center" label="任务名称" prop="taskName" />
-      <el-table-column align="center" label="流程发起人" prop="startUserNickname" />
-      <el-table-column align="center" label="抄送发起人" prop="creatorNickname" />
-      <el-table-column align="center" label="抄送原因" prop="reason" />
+      <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
+      <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="流程发起时间"
+        prop="processInstanceStartTime"
+        width="180"
+      />
+      <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" />
+      <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" />
       <el-table-column
         align="center"
         label="抄送时间"
@@ -59,9 +56,9 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column align="center" label="操作">
+      <el-table-column align="center" label="操作" fixed="right" width="80">
         <template #default="scope">
-          <el-button link type="primary" @click="handleAudit(scope.row)">跳转待办</el-button>
+          <el-button link type="primary" @click="handleAudit(scope.row)">详情</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -78,14 +75,14 @@
 import { dateFormatter } from '@/utils/formatTime'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 
-defineOptions({ name: 'BpmCCProcessInstance' })
+defineOptions({ name: 'BpmProcessInstanceCopy' })
 
 const { push } = useRouter() // 路由
 
 const loading = ref(false) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const queryParams = ref({
+const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   processInstanceId: '',
@@ -98,7 +95,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await ProcessInstanceApi.getProcessInstanceCCPage(queryParams)
+    const data = await ProcessInstanceApi.getProcessInstanceCopyPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -118,7 +115,7 @@ const handleAudit = (row: any) => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.value.pageNo = 1
+  queryParams.pageNo = 1
   getList()
 }
 
diff --git a/src/views/bpm/task/done/TaskDetail.vue b/src/views/bpm/task/done/TaskDetail.vue
deleted file mode 100644
index ff8f313d..00000000
--- a/src/views/bpm/task/done/TaskDetail.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情">
-    <el-descriptions :column="1" border>
-      <el-descriptions-item label="任务编号" min-width="120">
-        {{ detailData.id }}
-      </el-descriptions-item>
-      <el-descriptions-item label="任务名称">
-        {{ detailData.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="所属流程">
-        {{ detailData.processInstance.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="流程发起人">
-        {{ detailData.processInstance.startUserNickname }}
-      </el-descriptions-item>
-      <el-descriptions-item label="状态">
-        <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="detailData.status" />
-      </el-descriptions-item>
-      <el-descriptions-item label="原因">
-        {{ detailData.reason }}
-      </el-descriptions-item>
-      <el-descriptions-item label="创建时间">
-        {{ formatDate(detailData.createTime) }}
-      </el-descriptions-item>
-    </el-descriptions>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
-import { formatDate } from '@/utils/formatTime'
-import * as TaskApi from '@/api/bpm/task'
-
-defineOptions({ name: 'BpmTaskDetail' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const detailLoading = ref(false) // 表单的加载中
-const detailData = ref() // 详情数据
-
-/** 打开弹窗 */
-const open = async (data: TaskApi.TaskVO) => {
-  dialogVisible.value = true
-  // 设置数据
-  detailLoading.value = true
-  try {
-    detailData.value = data
-  } finally {
-    detailLoading.value = false
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index b190bd4f..d9b32803 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -46,27 +46,50 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="任务编号" prop="id" width="300px" />
-      <el-table-column align="center" label="任务名称" prop="name" />
-      <el-table-column align="center" label="所属流程" prop="processInstance.name" />
-      <el-table-column align="center" label="流程发起人" prop="processInstance.startUserNickname" />
-      <el-table-column align="center" label="审批状态" prop="status">
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="发起时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务开始时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+        <template #default="scope">
+          {{ formatPast2(scope.row.durationInMillis) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="审批状态" prop="status" width="100">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="原因" prop="reason" />
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180"
-      />
-      <el-table-column align="center" label="操作">
+      <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
         <template #default="scope">
-          <el-button link type="primary" @click="openDetail(scope.row)">详情</el-button>
-          <el-button link type="primary" @click="handleAudit(scope.row)">流程</el-button>
+          <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -78,15 +101,11 @@
       @pagination="getList"
     />
   </ContentWrap>
-
-  <!-- 表单弹窗:详情 -->
-  <TaskDetail ref="detailRef" @success="getList" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import * as TaskApi from '@/api/bpm/task'
-import TaskDetail from './TaskDetail.vue'
 
 defineOptions({ name: 'BpmTodoTask' })
 
@@ -127,14 +146,8 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 详情操作 */
-const detailRef = ref()
-const openDetail = (row: TaskApi.TaskVO) => {
-  detailRef.value.open(row)
-}
-
 /** 处理审批按钮 */
-const handleAudit = (row) => {
+const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {

From 60ddc45b9bb444c4ada72611b454dcac1160aed4 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Mon, 18 Mar 2024 18:41:11 +0800
Subject: [PATCH 27/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=8A=A0=20task=20?=
 =?UTF-8?q?=E7=9A=84=E5=AE=A1=E6=89=B9=E5=BB=BA=E8=AE=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../detail/ProcessInstanceBpmnViewer.vue              |  6 ------
 .../detail/ProcessInstanceTaskList.vue                |  7 +++----
 src/views/bpm/processInstance/detail/index.vue        |  3 ---
 src/views/bpm/task/done/index.vue                     | 11 ++++++-----
 4 files changed, 9 insertions(+), 18 deletions(-)

diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
index 0a2057dd..dcf3bcc4 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
@@ -33,12 +33,6 @@ const bpmnControlForm = ref({
   prefix: 'flowable'
 })
 const activityList = ref([]) // 任务列表
-// const bpmnXML = computed(() => { // TODO 芋艿:不晓得为啊哈不能这么搞
-//   if (!props.processInstance || !props.processInstance.processDefinition) {
-//     return
-//   }
-//   return DefinitionApi.getProcessDefinitionBpmnXML(props.processInstance.processDefinition.id)
-// })
 
 /** 初始化 */
 onMounted(async () => {
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index 9876d5d3..e20872db 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -3,7 +3,7 @@
     <template #header>
       <span class="el-icon-picture-outline">审批记录</span>
     </template>
-    <el-col :offset="4" :span="16">
+    <el-col :offset="3" :span="17">
       <div class="block">
         <el-timeline>
           <el-timeline-item
@@ -20,8 +20,7 @@
                 v-if="!isEmpty(item.children)"
                 @click="openChildrenTask(item)"
               >
-                <Icon icon="ep:memo" />
-                子任务
+                <Icon icon="ep:memo" /> 子任务
               </el-button>
             </p>
             <el-card :body-style="{ padding: '10px' }">
@@ -92,7 +91,7 @@ const getTimelineItemIcon = (item) => {
 }
 
 /** 获得任务对应的颜色 */
-const getTimelineItemType = (item) => {
+const getTimelineItemType = (item: any) => {
   if (item.status === 1) {
     return 'primary'
   }
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 800ff2f3..a8c5f2a5 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -302,9 +302,6 @@ const loadRunningTask = (tasks) => {
       loadRunningTask(task.children)
     }
     // 2.1 只有待处理才需要
-    // if (task.status !== 1 && task.status !== 6) {
-    //   return
-    // }
     if (task.status !== 1 && task.status !== 6) {
       return
     }
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index d9b32803..7d8f905b 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -75,16 +75,17 @@
         prop="endTime"
         width="180"
       />
+      <el-table-column align="center" label="审批状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
       <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
         <template #default="scope">
           {{ formatPast2(scope.row.durationInMillis) }}
         </template>
       </el-table-column>
-      <el-table-column align="center" label="审批状态" prop="status" width="100">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
-        </template>
-      </el-table-column>
       <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" />
       <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
       <el-table-column align="center" label="操作" fixed="right" width="80">

From a40866e27f43505226977178772cc8fb6f1e3b27 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Mon, 18 Mar 2024 20:45:39 +0800
Subject: [PATCH 28/49] =?UTF-8?q?BPM=EF=BC=9A=E5=AE=8C=E5=96=84=20task=20?=
 =?UTF-8?q?=E8=BD=AC=E6=B4=BE=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/task/index.ts                     | 27 +++++--------------
 .../detail/ProcessInstanceTaskList.vue        |  4 +--
 .../detail/{ => dialog}/TaskDelegateForm.vue  |  9 ++++---
 .../TaskReturnForm.vue}                       |  2 +-
 .../TaskTransferForm.vue}                     | 18 ++++++++-----
 .../bpm/processInstance/detail/index.vue      | 26 +++++++++---------
 6 files changed, 40 insertions(+), 46 deletions(-)
 rename src/views/bpm/processInstance/detail/{ => dialog}/TaskDelegateForm.vue (92%)
 rename src/views/bpm/processInstance/detail/{TaskReturnDialogForm.vue => dialog/TaskReturnForm.vue} (97%)
 rename src/views/bpm/processInstance/detail/{TaskUpdateAssigneeForm.vue => dialog/TaskTransferForm.vue} (79%)

diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts
index 78bbb984..de575244 100644
--- a/src/api/bpm/task/index.ts
+++ b/src/api/bpm/task/index.ts
@@ -12,10 +12,6 @@ export const getDoneTaskPage = async (params) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
-export const completeTask = async (data) => {
-  return await request.put({ url: '/bpm/task/complete', data })
-}
-
 export const approveTask = async (data) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }
@@ -23,13 +19,6 @@ export const approveTask = async (data) => {
 export const rejectTask = async (data) => {
   return await request.put({ url: '/bpm/task/reject', data })
 }
-export const backTask = async (data) => {
-  return await request.put({ url: '/bpm/task/back', data })
-}
-
-export const updateTaskAssignee = async (data) => {
-  return await request.put({ url: '/bpm/task/update-assignee', data })
-}
 
 export const getTaskListByProcessInstanceId = async (processInstanceId) => {
   return await request.get({
@@ -37,11 +26,6 @@ export const getTaskListByProcessInstanceId = async (processInstanceId) => {
   })
 }
 
-// 导出任务
-export const exportTask = async (params) => {
-  return await request.download({ url: '/bpm/task/export', params })
-}
-
 // 获取所有可回退的节点
 export const getTaskListByReturn = async (id: string) => {
   return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
@@ -52,13 +36,16 @@ export const returnTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/return', data })
 }
 
-/**
- * 委派
- */
-export const delegateTask = async (data) => {
+// 委派
+export const delegateTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/delegate', data })
 }
 
+// 转派
+export const transferTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/transfer', data })
+}
+
 /**
  * 加签
  */
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index e20872db..e9b9d64d 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -44,9 +44,7 @@
               <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
                 {{ formatPast2(item?.durationInMillis) }}
               </label>
-              <p v-if="item.reason">
-                <el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
-              </p>
+              <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
             </el-card>
           </el-timeline-item>
         </el-timeline>
diff --git a/src/views/bpm/processInstance/detail/TaskDelegateForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
similarity index 92%
rename from src/views/bpm/processInstance/detail/TaskDelegateForm.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
index dc757a0c..178b1b97 100644
--- a/src/views/bpm/processInstance/detail/TaskDelegateForm.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskDelegateForm.vue
@@ -37,10 +37,12 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  delegateUserId: undefined
+  delegateUserId: undefined,
+  reason: ''
 })
 const formRules = ref({
-  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }]
+  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
@@ -79,7 +81,8 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    delegateUserId: undefined
+    delegateUserId: undefined,
+    reason: ''
   }
   formRef.value?.resetFields()
 }
diff --git a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
similarity index 97%
rename from src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
index 82a8f960..a1391697 100644
--- a/src/views/bpm/processInstance/detail/TaskReturnDialogForm.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskReturnForm.vue
@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="回退" width="500">
+  <Dialog v-model="dialogVisible" title="回退任务" width="500">
     <el-form
       ref="formRef"
       v-loading="formLoading"
diff --git a/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
similarity index 79%
rename from src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
index 6adf1de8..c1012ac9 100644
--- a/src/views/bpm/processInstance/detail/TaskUpdateAssigneeForm.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskTransferForm.vue
@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="转派审批人" width="500">
+  <Dialog v-model="dialogVisible" title="转派任务" width="500">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -17,6 +17,9 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="转派理由" prop="reason">
+        <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" />
+      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -28,16 +31,18 @@
 import * as TaskApi from '@/api/bpm/task'
 import * as UserApi from '@/api/system/user'
 
-defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
+defineOptions({ name: 'TaskTransferForm' })
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  assigneeUserId: undefined
+  assigneeUserId: undefined,
+  reason: ''
 })
 const formRules = ref({
-  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }]
+  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
+  reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
 })
 
 const formRef = ref() // 表单 Ref
@@ -63,7 +68,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    await TaskApi.updateTaskAssignee(formData.value)
+    await TaskApi.transferTask(formData.value)
     dialogVisible.value = false
     // 发送操作成功的事件
     emit('success')
@@ -76,7 +81,8 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    assigneeUserId: undefined
+    assigneeUserId: undefined,
+    reason: ''
   }
   formRef.value?.resetFields()
 }
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index a8c5f2a5..4f889b8f 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -104,12 +104,12 @@
     />
 
     <!-- 弹窗:转派审批人 -->
-    <TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
-    <!-- 弹窗,回退节点 -->
-    <TaskReturnDialog ref="taskReturnDialogRef" @success="getDetail" />
-    <!-- 委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
+    <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
+    <!-- 弹窗:回退节点 -->
+    <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
+    <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
     <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
-    <!-- 加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
+    <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
     <TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" />
   </ContentWrap>
 </template>
@@ -120,11 +120,11 @@ import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import * as TaskApi from '@/api/bpm/task'
-import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
 import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
 import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import TaskReturnDialog from './TaskReturnDialogForm.vue'
-import TaskDelegateForm from './TaskDelegateForm.vue'
+import TaskReturnForm from './dialog/TaskReturnForm.vue'
+import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
+import TaskTransferForm from './dialog/TaskTransferForm.vue'
 import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import { isEmpty } from '@/utils/is'
@@ -187,9 +187,9 @@ const handleAudit = async (task, pass) => {
 }
 
 /** 转派审批人 */
-const taskUpdateAssigneeFormRef = ref()
+const taskTransferFormRef = ref()
 const openTaskUpdateAssigneeForm = (id: string) => {
-  taskUpdateAssigneeFormRef.value.open(id)
+  taskTransferFormRef.value.open(id)
 }
 
 /** 处理审批退回的操作 */
@@ -199,9 +199,9 @@ const handleDelegate = async (task) => {
 }
 
 /** 处理审批退回的操作 */
-const taskReturnDialogRef = ref()
-const handleBack = async (task) => {
-  taskReturnDialogRef.value.open(task.id)
+const taskReturnFormRef = ref()
+const handleBack = async (task: any) => {
+  taskReturnFormRef.value.open(task.id)
 }
 
 /** 处理审批加签的操作 */

From 2d424fc9a65d6e34a6c28e3edcba9643ddc19829 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Tue, 19 Mar 2024 01:31:51 +0800
Subject: [PATCH 29/49] =?UTF-8?q?BPM=EF=BC=9A=E4=BC=98=E5=8C=96=20task=20?=
 =?UTF-8?q?=E5=8A=A0=E5=87=8F=E7=AD=BE=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/processInstance/index.ts          |  2 +-
 src/api/bpm/task/index.ts                     | 34 ++++------
 .../detail/ProcessInstanceTaskList.vue        | 23 ++++---
 .../TaskSignCreateForm.vue}                   | 22 ++++---
 .../TaskSignDeleteForm.vue}                   | 18 ++++--
 .../TaskSignList.vue}                         | 64 +++++++++++--------
 .../bpm/processInstance/detail/index.vue      | 14 ++--
 src/views/bpm/processInstance/index.vue       |  7 +-
 8 files changed, 102 insertions(+), 82 deletions(-)
 rename src/views/bpm/processInstance/detail/{TaskAddSignDialogForm.vue => dialog/TaskSignCreateForm.vue} (85%)
 rename src/views/bpm/processInstance/detail/{TaskSubSignDialogForm.vue => dialog/TaskSignDeleteForm.vue} (80%)
 rename src/views/bpm/processInstance/detail/{ProcessInstanceChildrenTaskList.vue => dialog/TaskSignList.vue} (51%)

diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts
index 1a5b5ecb..d5d0c05c 100644
--- a/src/api/bpm/processInstance/index.ts
+++ b/src/api/bpm/processInstance/index.ts
@@ -47,7 +47,7 @@ export const cancelProcessInstance = async (id: number, reason: string) => {
   return await request.delete({ url: '/bpm/process-instance/cancel', data: data })
 }
 
-export const getProcessInstance = async (id: number) => {
+export const getProcessInstance = async (id: string) => {
   return await request.get({ url: '/bpm/process-instance/get?id=' + id })
 }
 
diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts
index de575244..6592542d 100644
--- a/src/api/bpm/task/index.ts
+++ b/src/api/bpm/task/index.ts
@@ -4,23 +4,23 @@ export type TaskVO = {
   id: number
 }
 
-export const getTodoTaskPage = async (params) => {
+export const getTodoTaskPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/todo-page', params })
 }
 
-export const getDoneTaskPage = async (params) => {
+export const getDoneTaskPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
-export const approveTask = async (data) => {
+export const approveTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }
 
-export const rejectTask = async (data) => {
+export const rejectTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/reject', data })
 }
 
-export const getTaskListByProcessInstanceId = async (processInstanceId) => {
+export const getTaskListByProcessInstanceId = async (processInstanceId: string) => {
   return await request.get({
     url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId
   })
@@ -46,23 +46,17 @@ export const transferTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/transfer', data })
 }
 
-/**
- * 加签
- */
-export const taskAddSign = async (data) => {
+// 加签
+export const signCreateTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/create-sign', data })
 }
 
-/**
- * 获取减签任务列表
- */
-export const getChildrenTaskList = async (id: string) => {
-  return await request.get({ url: '/bpm/task/children-list?parentId=' + id })
-}
-
-/**
- * 减签
- */
-export const taskSubSign = async (data) => {
+// 减签
+export const signDeleteTask = async (data: any) => {
   return await request.delete({ url: '/bpm/task/delete-sign', data })
 }
+
+// 获取减签任务列表
+export const getChildrenTaskList = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
+}
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index e9b9d64d..ba573a2c 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -50,8 +50,9 @@
         </el-timeline>
       </div>
     </el-col>
-    <!-- 子任务  -->
-    <ProcessInstanceChildrenTaskList ref="processInstanceChildrenTaskList" />
+
+    <!-- 弹窗:子任务  -->
+    <TaskSignList ref="taskSignListRef" @success="refresh" />
   </el-card>
 </template>
 <script lang="ts" setup>
@@ -59,7 +60,7 @@ import { formatDate, formatPast2 } from '@/utils/formatTime'
 import { propTypes } from '@/utils/propTypes'
 import { DICT_TYPE } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
-import ProcessInstanceChildrenTaskList from './ProcessInstanceChildrenTaskList.vue'
+import TaskSignList from './dialog/TaskSignList.vue'
 
 defineOptions({ name: 'BpmProcessInstanceTaskList' })
 
@@ -69,6 +70,7 @@ defineProps({
 })
 
 /** 获得任务对应的 icon */
+// TODO @芋艿:对应的 icon 需要调整
 const getTimelineItemIcon = (item) => {
   if (item.status === 1) {
     return 'el-icon-time'
@@ -114,12 +116,15 @@ const getTimelineItemType = (item: any) => {
   return ''
 }
 
-/**
- * 子任务
- */
-const processInstanceChildrenTaskList = ref()
+/** 子任务 */
+const taskSignListRef = ref()
+const openChildrenTask = (item: any) => {
+  taskSignListRef.value.open(item)
+}
 
-const openChildrenTask = (item) => {
-  processInstanceChildrenTaskList.value.open(item)
+/** 刷新数据 */
+const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
+const refresh = () => {
+  emit('refresh')
 }
 </script>
diff --git a/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
similarity index 85%
rename from src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
index 40cd200e..9e4998c1 100644
--- a/src/views/bpm/processInstance/detail/TaskAddSignDialogForm.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskSignCreateForm.vue
@@ -7,8 +7,8 @@
       :rules="formRules"
       label-width="110px"
     >
-      <el-form-item label="加签处理人" prop="userIdList">
-        <el-select v-model="formData.userIdList" multiple clearable style="width: 100%">
+      <el-form-item label="加签处理人" prop="userIds">
+        <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
           <el-option
             v-for="item in userList"
             :key="item.id"
@@ -36,18 +36,19 @@
 import * as TaskApi from '@/api/bpm/task'
 import * as UserApi from '@/api/system/user'
 
-const message = useMessage() // 消息弹窗
-defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
+defineOptions({ name: 'TaskSignCreateForm' })
 
+const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   id: '',
-  userIdList: [],
-  type: ''
+  userIds: [],
+  type: '',
+  reason: ''
 })
 const formRules = ref({
-  userIdList: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
+  userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
   reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
 })
 
@@ -75,7 +76,7 @@ const submitForm = async (type: string) => {
   formLoading.value = true
   formData.value.type = type
   try {
-    await TaskApi.taskAddSign(formData.value)
+    await TaskApi.signCreateTask(formData.value)
     message.success('加签成功')
     dialogVisible.value = false
     // 发送操作成功的事件
@@ -89,8 +90,9 @@ const submitForm = async (type: string) => {
 const resetForm = () => {
   formData.value = {
     id: '',
-    userIdList: [],
-    type: ''
+    userIds: [],
+    type: '',
+    reason: ''
   }
   formRef.value?.resetFields()
 }
diff --git a/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
similarity index 80%
rename from src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
index 61f7d68c..19bb2dce 100644
--- a/src/views/bpm/processInstance/detail/TaskSubSignDialogForm.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
@@ -9,8 +9,10 @@
     >
       <el-form-item label="减签任务" prop="id">
         <el-radio-group v-model="formData.id">
-          <el-radio-button v-for="item in subTaskList" :key="item.id" :label="item.id">
-            {{ item.name }}({{ item.assigneeUser.deptName }}{{ item.assigneeUser.nickname }}--审批)
+          <el-radio-button v-for="item in childrenTaskList" :key="item.id" :label="item.id">
+            {{ item.name }}
+            ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
+            {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
           </el-radio-button>
         </el-radio-group>
       </el-form-item>
@@ -24,10 +26,12 @@
     </template>
   </Dialog>
 </template>
-<script lang="ts" name="TaskRollbackDialogForm" setup>
+<script lang="ts" setup>
 import * as TaskApi from '@/api/bpm/task'
 import { isEmpty } from '@/utils/is'
 
+defineOptions({ name: 'TaskSignDeleteForm' })
+
 const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
@@ -41,11 +45,11 @@ const formRules = ref({
 })
 
 const formRef = ref() // 表单 Ref
-const subTaskList = ref([])
+const childrenTaskList = ref([])
 /** 打开弹窗 */
 const open = async (id: string) => {
-  subTaskList.value = await TaskApi.getChildrenTaskList(id)
-  if (isEmpty(subTaskList.value)) {
+  childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
+  if (isEmpty(childrenTaskList.value)) {
     message.warning('当前没有可减签的任务')
     return false
   }
@@ -64,7 +68,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    await TaskApi.taskSubSign(formData.value)
+    await TaskApi.signDeleteTask(formData.value)
     message.success('减签成功')
     dialogVisible.value = false
     // 发送操作成功的事件
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
similarity index 51%
rename from src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
rename to src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
index 02aab321..564071bd 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceChildrenTaskList.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
@@ -1,23 +1,31 @@
 <template>
-  <el-drawer v-model="drawerVisible" title="子任务" size="70%">
+  <el-drawer v-model="drawerVisible" title="子任务" size="880px">
     <!-- 当前任务 -->
     <template #header>
-      <h4>【{{ baseTask.name }} 】审批人:{{ baseTask.assigneeUser?.nickname }}</h4>
+      <h4>【{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4>
       <el-button
         style="margin-left: 5px"
-        v-if="isSubSignButtonVisible(baseTask)"
+        v-if="isSignDeleteButtonVisible(parentTask)"
         type="danger"
         plain
-        @click="handleSubSign(baseTask)"
+        @click="handleSignDelete(parentTask)"
       >
         <Icon icon="ep:remove" /> 减签
       </el-button>
     </template>
     <!-- 子任务列表 -->
-    <el-table :data="baseTask.children" style="width: 100%" row-key="id" border>
-      <el-table-column prop="assigneeUser.nickname" label="审批人" />
-      <el-table-column prop="assigneeUser.deptName" label="所在部门" />
-      <el-table-column label="审批状态" prop="status">
+    <el-table :data="parentTask.children" style="width: 100%" row-key="id" border>
+      <el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100">
+        <template #default="scope">
+          {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100">
+        <template #default="scope">
+          {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="审批状态" prop="status" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
         </template>
@@ -36,61 +44,63 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column label="操作" prop="operation">
+      <el-table-column label="操作" prop="operation" width="90">
         <template #default="scope">
           <el-button
-            v-if="isSubSignButtonVisible(scope.row)"
+            v-if="isSignDeleteButtonVisible(scope.row)"
             type="danger"
             plain
-            @click="handleSubSign(scope.row)"
+            size="small"
+            @click="handleSignDelete(scope.row)"
           >
             <Icon icon="ep:remove" /> 减签
           </el-button>
         </template>
       </el-table-column>
     </el-table>
+
     <!-- 减签 -->
-    <TaskSubSignDialogForm ref="taskSubSignDialogForm" />
+    <TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" />
   </el-drawer>
 </template>
 <script lang="ts" setup>
 import { isEmpty } from '@/utils/is'
 import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import TaskSubSignDialogForm from './TaskSubSignDialogForm.vue'
+import TaskSignDeleteForm from './TaskSignDeleteForm.vue'
 
-defineOptions({ name: 'ProcessInstanceChildrenTaskList' })
+defineOptions({ name: 'TaskSignList' })
 
 const message = useMessage() // 消息弹窗
 const drawerVisible = ref(false) // 抽屉的是否展示
+const parentTask = ref({} as any)
 
-const baseTask = ref<object>({})
 /** 打开弹窗 */
 const open = async (task: any) => {
   if (isEmpty(task.children)) {
     message.warning('该任务没有子任务')
     return
   }
-  baseTask.value = task
+  parentTask.value = task
   // 展开抽屉
   drawerVisible.value = true
 }
 defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
 
 /** 发起减签 */
-const taskSubSignDialogForm = ref()
-const handleSubSign = (item) => {
-  taskSubSignDialogForm.value.open(item.id)
-  // TODO @海洋:减签后,需要刷新下界面哈
+const taskSignDeleteFormRef = ref()
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const handleSignDelete = (item: any) => {
+  taskSignDeleteFormRef.value.open(item.id)
+}
+const handleSignDeleteSuccess = () => {
+  emit('success')
+  // 关闭抽屉
+  drawerVisible.value = false
 }
 
 /** 是否显示减签按钮 */
-const isSubSignButtonVisible = (task: any) => {
-  if (task && task.children && !isEmpty(task.children)) {
-    // 有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮
-    const subTask = task.children.find((item) => item.status === 1 || item.status === 9)
-    return !isEmpty(subTask)
-  }
-  return false
+const isSignDeleteButtonVisible = (task: any) => {
+  return task && task.children && !isEmpty(task.children)
 }
 </script>
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 4f889b8f..2aac5fad 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -92,7 +92,7 @@
     </el-card>
 
     <!-- 审批记录 -->
-    <ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" />
+    <ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" @refresh="getTaskList" />
 
     <!-- 高亮流程图 -->
     <ProcessInstanceBpmnViewer
@@ -110,7 +110,7 @@
     <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
     <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
     <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
-    <TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" />
+    <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
   </ContentWrap>
 </template>
 <script lang="ts" setup>
@@ -125,7 +125,7 @@ import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
 import TaskReturnForm from './dialog/TaskReturnForm.vue'
 import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
 import TaskTransferForm from './dialog/TaskTransferForm.vue'
-import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue'
+import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import { isEmpty } from '@/utils/is'
 import * as UserApi from '@/api/system/user'
@@ -137,7 +137,7 @@ const message = useMessage() // 消息弹窗
 const { proxy } = getCurrentInstance() as any
 
 const userId = useUserStore().getUser.id // 当前登录的编号
-const id = query.id as unknown as number // 流程实例的编号
+const id = query.id as unknown as string // 流程实例的编号
 const processInstanceLoading = ref(false) // 流程实例的加载中
 const processInstance = ref<any>({}) // 流程实例
 const bpmnXML = ref('') // BPMN XML
@@ -205,9 +205,9 @@ const handleBack = async (task: any) => {
 }
 
 /** 处理审批加签的操作 */
-const taskAddSignDialogForm = ref()
-const handleSign = async (task) => {
-  taskAddSignDialogForm.value.open(task.id)
+const taskSignCreateFormRef = ref()
+const handleSign = async (task: any) => {
+  taskSignCreateFormRef.value.open(task.id)
 }
 
 /** 获得详情 */
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 5ef0edf8..c67a2baa 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -133,7 +133,7 @@
           <el-button
             link
             type="primary"
-            v-if="scope.row.result === 1"
+            v-if="scope.row.status === 1"
             v-hasPermi="['bpm:process-instance:query']"
             @click="handleCancel(scope.row)"
           >
@@ -234,6 +234,11 @@ const handleCancel = async (row) => {
   await getList()
 }
 
+/** 激活时 **/
+onActivated(() => {
+  getList()
+})
+
 /** 初始化 **/
 onMounted(() => {
   getList()

From a0f157c8b622907e1fb7bd09d2c1641869ef5ede Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Tue, 19 Mar 2024 12:13:54 +0800
Subject: [PATCH 30/49] =?UTF-8?q?BPM=EF=BC=9A=E7=AE=80=E5=8C=96=20userGrou?=
 =?UTF-8?q?p=20=E7=9A=84=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/userGroup/index.ts                         |  6 +++---
 .../package/penal/task/task-components/UserTask.vue    |  2 +-
 src/views/bpm/group/UserGroupForm.vue                  | 10 +++++-----
 src/views/bpm/group/index.vue                          |  2 +-
 4 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/api/bpm/userGroup/index.ts b/src/api/bpm/userGroup/index.ts
index 035762bf..7d12755e 100644
--- a/src/api/bpm/userGroup/index.ts
+++ b/src/api/bpm/userGroup/index.ts
@@ -4,7 +4,7 @@ export type UserGroupVO = {
   id: number
   name: string
   description: string
-  memberUserIds: number[]
+  userIds: number[]
   status: number
   remark: string
   createTime: string
@@ -42,6 +42,6 @@ export const getUserGroupPage = async (params) => {
 }
 
 // 获取用户组精简信息列表
-export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => {
-  return await request.get({ url: '/bpm/user-group/list-all-simple' })
+export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
+  return await request.get({ url: '/bpm/user-group/simple-list' })
 }
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index 013719ea..6431eca1 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -212,7 +212,7 @@ onMounted(async () => {
   // 获得用户列表
   userOptions.value = await UserApi.getSimpleUserList()
   // 获得用户组列表
-  userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
+  userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
 })
 
 onBeforeUnmount(() => {
diff --git a/src/views/bpm/group/UserGroupForm.vue b/src/views/bpm/group/UserGroupForm.vue
index 35d833ea..ac0cfcb3 100644
--- a/src/views/bpm/group/UserGroupForm.vue
+++ b/src/views/bpm/group/UserGroupForm.vue
@@ -13,8 +13,8 @@
       <el-form-item label="描述">
         <el-input v-model="formData.description" placeholder="请输入描述" type="textarea" />
       </el-form-item>
-      <el-form-item label="成员" prop="memberUserIds">
-        <el-select v-model="formData.memberUserIds" multiple placeholder="请选择成员">
+      <el-form-item label="成员" prop="userIds">
+        <el-select v-model="formData.userIds" multiple placeholder="请选择成员">
           <el-option
             v-for="user in userList"
             :key="user.id"
@@ -60,13 +60,13 @@ const formData = ref({
   id: undefined,
   name: undefined,
   description: undefined,
-  memberUserIds: undefined,
+  userIds: undefined,
   status: CommonStatusEnum.ENABLE
 })
 const formRules = reactive({
   name: [{ required: true, message: '组名不能为空', trigger: 'blur' }],
   description: [{ required: true, message: '描述不能为空', trigger: 'blur' }],
-  memberUserIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }],
+  userIds: [{ required: true, message: '成员不能为空', trigger: 'blur' }],
   status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
@@ -124,7 +124,7 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     description: undefined,
-    memberUserIds: undefined,
+    userIds: undefined,
     status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()
diff --git a/src/views/bpm/group/index.vue b/src/views/bpm/group/index.vue
index 98a445d6..62785a92 100644
--- a/src/views/bpm/group/index.vue
+++ b/src/views/bpm/group/index.vue
@@ -63,7 +63,7 @@
       <el-table-column label="描述" align="center" prop="description" />
       <el-table-column label="成员" align="center">
         <template #default="scope">
-          <span v-for="userId in scope.row.memberUserIds" :key="userId" class="pr-5px">
+          <span v-for="userId in scope.row.userIds" :key="userId" class="pr-5px">
             {{ userList.find((user) => user.id === userId)?.nickname }}
           </span>
         </template>

From 501a1c2f4d0636751a11ad30ca216cc7d7d0bdfd Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Tue, 19 Mar 2024 19:49:52 +0800
Subject: [PATCH 31/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=E6=B5=81?=
 =?UTF-8?q?=E7=A8=8B=E5=88=86=E7=B1=BB=E8=A1=A8=EF=BC=8C=E6=9B=BF=E4=BB=A3?=
 =?UTF-8?q?=E7=8E=B0=E6=9C=89=E7=9A=84=20`bpm=5Fcategory`=20=E6=95=B0?=
 =?UTF-8?q?=E6=8D=AE=E5=AD=97=E5=85=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/category/index.ts                 |  43 ++++
 src/router/modules/remaining.ts               |  12 --
 src/utils/dict.ts                             |   3 +-
 src/views/bpm/category/CategoryForm.vue       | 124 +++++++++++
 src/views/bpm/category/index.vue              | 198 ++++++++++++++++++
 src/views/bpm/definition/index.vue            |   7 +-
 src/views/bpm/model/ModelForm.vue             |  12 +-
 src/views/bpm/model/index.vue                 |  23 +-
 .../bpm/processInstance/create/index.vue      |  97 +++++----
 src/views/bpm/processInstance/index.vue       |  21 +-
 10 files changed, 457 insertions(+), 83 deletions(-)
 create mode 100644 src/api/bpm/category/index.ts
 create mode 100644 src/views/bpm/category/CategoryForm.vue
 create mode 100644 src/views/bpm/category/index.vue

diff --git a/src/api/bpm/category/index.ts b/src/api/bpm/category/index.ts
new file mode 100644
index 00000000..d1e109cb
--- /dev/null
+++ b/src/api/bpm/category/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// BPM 流程分类 VO
+export interface CategoryVO {
+  id: number // 分类编号
+  name: string // 分类名
+  code: string // 分类标志
+  status: number // 分类状态
+  sort: number // 分类排序
+}
+
+// BPM 流程分类 API
+export const CategoryApi = {
+  // 查询流程分类分页
+  getCategoryPage: async (params: any) => {
+    return await request.get({ url: `/bpm/category/page`, params })
+  },
+
+  // 查询流程分类列表
+  getCategorySimpleList: async () => {
+    return await request.get({ url: `/bpm/category/simple-list` })
+  },
+
+  // 查询流程分类详情
+  getCategory: async (id: number) => {
+    return await request.get({ url: `/bpm/category/get?id=` + id })
+  },
+
+  // 新增流程分类
+  createCategory: async (data: CategoryVO) => {
+    return await request.post({ url: `/bpm/category/create`, data })
+  },
+
+  // 修改流程分类
+  updateCategory: async (data: CategoryVO) => {
+    return await request.put({ url: `/bpm/category/update`, data })
+  },
+
+  // 删除流程分类
+  deleteCategory: async (id: number) => {
+    return await request.delete({ url: `/bpm/category/delete?id=` + id })
+  }
+}
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index b08035de..ec61e971 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -278,18 +278,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/bpm/manager/model'
         }
       },
-      {
-        path: '/process-instance/create',
-        component: () => import('@/views/bpm/processInstance/create/index.vue'),
-        name: 'BpmProcessInstanceCreate',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '发起流程',
-          activeMenu: 'bpm/processInstance/create'
-        }
-      },
       {
         path: '/process-instance/detail',
         component: () => import('@/views/bpm/processInstance/detail/index.vue'),
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index e6b82500..6d7d2e72 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -136,11 +136,10 @@ export enum DICT_TYPE {
   INFRA_FILE_STORAGE = 'infra_file_storage',
 
   // ========== BPM 模块 ==========
-  BPM_MODEL_CATEGORY = 'bpm_model_category',
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
   BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
-  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
+  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', // TODO @芋艿:改名
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
 
   // ========== PAY 模块 ==========
diff --git a/src/views/bpm/category/CategoryForm.vue b/src/views/bpm/category/CategoryForm.vue
new file mode 100644
index 00000000..5b771537
--- /dev/null
+++ b/src/views/bpm/category/CategoryForm.vue
@@ -0,0 +1,124 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="分类名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入分类名" />
+      </el-form-item>
+      <el-form-item label="分类标志" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入分类标志" />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="分类排序" prop="sort">
+        <el-input-number
+          v-model="formData.sort"
+          placeholder="请输入分类排序"
+          class="!w-1/1"
+          :precision="0"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+
+/** BPM 流程分类 表单 */
+defineOptions({ name: 'CategoryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  sort: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '分类名不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '分类标志不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '分类状态不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await CategoryApi.getCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as CategoryVO
+    if (formType.value === 'create') {
+      await CategoryApi.createCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CategoryApi.updateCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    code: undefined,
+    status: undefined,
+    sort: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/category/index.vue b/src/views/bpm/category/index.vue
new file mode 100644
index 00000000..0e11e819
--- /dev/null
+++ b/src/views/bpm/category/index.vue
@@ -0,0 +1,198 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="分类名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入分类名"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类标志" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输入分类标志"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择分类状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:category:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="分类编号" align="center" prop="id" />
+      <el-table-column label="分类名" align="center" prop="name" />
+      <el-table-column label="分类标志" align="center" prop="code" />
+      <el-table-column label="分类描述" align="center" prop="description" />
+      <el-table-column label="分类状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="分类排序" align="center" prop="sort" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:category:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:category:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <CategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import CategoryForm from './CategoryForm.vue'
+
+/** BPM 流程分类 列表 */
+defineOptions({ name: 'BpmCategory' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<CategoryVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CategoryApi.getCategoryPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await CategoryApi.deleteCategory(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue
index 923a5901..9ebd28b1 100644
--- a/src/views/bpm/definition/index.vue
+++ b/src/views/bpm/definition/index.vue
@@ -11,11 +11,7 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="定义分类" align="center" prop="category" width="100">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
+      <el-table-column label="定义分类" align="center" prop="categoryName" width="100" />
       <el-table-column label="表单信息" align="center" prop="formType" width="200">
         <template #default="scope">
           <el-button
@@ -85,7 +81,6 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
 import * as DefinitionApi from '@/api/bpm/definition'
diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue
index 15935e18..d41c2b87 100644
--- a/src/views/bpm/model/ModelForm.vue
+++ b/src/views/bpm/model/ModelForm.vue
@@ -43,10 +43,10 @@
           style="width: 100%"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
           />
         </el-select>
       </el-form-item>
@@ -126,6 +126,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { ElMessageBox } from 'element-plus'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'ModelForm' })
 
@@ -154,6 +155,7 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 const formList = ref([]) // 流程表单的下拉框的数据
+const categoryList = ref([]) // 流程分类列表
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -172,6 +174,8 @@ const open = async (type: string, id?: number) => {
   }
   // 获得流程表单的下拉框的数据
   formList.value = await FormApi.getSimpleFormList()
+  // 查询流程分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index dc47ff64..9b0eec5e 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -36,10 +36,10 @@
           class="!w-240px"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
           />
         </el-select>
       </el-form-item>
@@ -72,11 +72,7 @@
           </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="流程分类" align="center" prop="category" width="100">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
+      <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
       <el-table-column label="表单信息" align="center" prop="formType" width="200">
         <template #default="scope">
           <el-button
@@ -221,7 +217,6 @@
 </template>
 
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter, formatDate } from '@/utils/formatTime'
 import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
 import * as ModelApi from '@/api/bpm/model'
@@ -229,6 +224,7 @@ import * as FormApi from '@/api/bpm/form'
 import ModelForm from './ModelForm.vue'
 import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
 import { setConfAndFields2 } from '@/utils/formCreate'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmModel' })
 
@@ -247,6 +243,7 @@ const queryParams = reactive({
   category: undefined
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -382,7 +379,9 @@ const handleBpmnDetail = async (row) => {
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  // 查询流程分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
index a10e0208..2cbfe9c4 100644
--- a/src/views/bpm/processInstance/create/index.vue
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -1,35 +1,49 @@
 <template>
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap v-if="!selectProcessInstance">
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程名称" align="center" prop="name" />
-      <el-table-column label="流程分类" align="center" prop="category">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
-      <el-table-column label="流程版本" align="center" prop="version">
-        <template #default="scope">
-          <el-tag>v{{ scope.row.version }}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="流程描述" align="center" prop="description" />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button link type="primary" @click="handleSelect(scope.row)">
-            <Icon icon="ep:plus" /> 选择
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
+  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
+    <el-tabs tab-position="left" v-model="categoryActive">
+      <el-tab-pane
+        :label="category.name"
+        :name="category.code"
+        :key="category.code"
+        v-for="category in categoryList"
+      >
+        <el-row :gutter="20">
+          <el-col
+            :lg="6"
+            :sm="12"
+            :xs="24"
+            v-for="definition in categoryProcessDefinitionList"
+            :key="definition.id"
+          >
+            <el-card
+              shadow="hover"
+              class="mb-20px cursor-pointer"
+              @click="handleSelect(definition)"
+            >
+              <template #default>
+                <div class="flex">
+                  <!-- TODO 芋艿:流程图,增加 icon -->
+                  <el-image
+                    src="http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png"
+                    class="w-32px h-32px"
+                  />
+                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                </div>
+              </template>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+    </el-tabs>
   </ContentWrap>
 
   <!-- 第二步,填写表单,进行流程的提交 -->
   <ContentWrap v-else>
     <el-card class="box-card">
       <div class="clearfix">
-        <span class="el-icon-document">申请信息【{{ selectProcessInstance.name }}】</span>
-        <el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
+        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
+        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
           <Icon icon="ep:delete" /> 选择其它流程
         </el-button>
       </div>
@@ -47,35 +61,46 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE } from '@/utils/dict'
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { setConfAndFields2 } from '@/utils/formCreate'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
 const router = useRouter() // 路由
 const message = useMessage() // 消息
 
-// ========== 列表相关 ==========
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  suspensionState: 1
-})
+const loading = ref(true) // 加载中
+const categoryList = ref([]) // 分类的列表
+const categoryActive = ref('') // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    list.value = await DefinitionApi.getProcessDefinitionList(queryParams)
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+    if (categoryList.value.length > 0) {
+      categoryActive.value = categoryList.value[0].code
+    }
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
   } finally {
     loading.value = false
   }
 }
 
+/** 选中分类对应的流程定义列表 */
+const categoryProcessDefinitionList = computed(() => {
+  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+})
+
 // ========== 表单相关 ==========
 const bpmnXML = ref(null) // BPMN 数据
 const fApi = ref<ApiAttrs>()
@@ -84,12 +109,12 @@ const detailForm = ref({
   rule: [],
   option: {}
 })
-const selectProcessInstance = ref() // 选择的流程实例
+const selectProcessDefinition = ref() // 选择的流程定义
 
 /** 处理选择流程的按钮操作 **/
 const handleSelect = async (row) => {
   // 设置选择的流程
-  selectProcessInstance.value = row
+  selectProcessDefinition.value = row
 
   // 情况一:流程表单
   if (row.formType == 10) {
@@ -108,14 +133,14 @@ const handleSelect = async (row) => {
 
 /** 提交按钮 */
 const submitForm = async (formData) => {
-  if (!fApi.value || !selectProcessInstance.value) {
+  if (!fApi.value || !selectProcessDefinition.value) {
     return
   }
   // 提交请求
   fApi.value.btn.loading(true)
   try {
     await ProcessInstanceApi.createProcessInstance({
-      processDefinitionId: selectProcessInstance.value.id,
+      processDefinitionId: selectProcessDefinition.value.id,
       variables: formData
     })
     // 提示
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index c67a2baa..1b3e8484 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -36,10 +36,10 @@
           class="!w-240px"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
           />
         </el-select>
       </el-form-item>
@@ -89,11 +89,7 @@
     <el-table v-loading="loading" :data="list">
       <el-table-column label="流程编号" align="center" prop="id" width="300px" />
       <el-table-column label="流程名称" align="center" prop="name" />
-      <el-table-column label="流程分类" align="center" prop="category">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="scope.row.category" />
-        </template>
-      </el-table-column>
+      <el-table-column label="流程分类" align="center" prop="categoryName" />
       <el-table-column label="当前审批任务" align="center" prop="tasks">
         <template #default="scope">
           <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
@@ -156,6 +152,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi } from '@/api/bpm/category'
 
 defineOptions({ name: 'BpmProcessInstance' })
 
@@ -176,6 +173,7 @@ const queryParams = reactive({
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -240,7 +238,8 @@ onActivated(() => {
 })
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
 })
 </script>

From 08dd4ed072f1f1307c0e21cd85a6a18123d80d94 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 20 Mar 2024 12:50:59 +0800
Subject: [PATCH 32/49] =?UTF-8?q?BPM=EF=BC=9A=E6=94=AF=E6=8C=81=E5=A4=9A?=
 =?UTF-8?q?=E8=A1=A8=E5=8D=95=EF=BC=8C=E6=AF=8F=E4=B8=AA=E6=B5=81=E7=A8=8B?=
 =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E9=83=BD=E5=8F=AF=E4=BB=A5=E7=BB=91=E5=AE=9A?=
 =?UTF-8?q?=E6=B5=81=E7=A8=8B=E8=A1=A8=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../package/penal/PropertiesPanel.vue         |   7 +-
 .../package/penal/form/ElementForm.vue        | 425 +++++++++---------
 src/utils/formCreate.ts                       |  13 +-
 src/views/bpm/model/ModelForm.vue             |   5 +-
 .../bpm/processInstance/detail/index.vue      |  55 ++-
 5 files changed, 281 insertions(+), 224 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
index 1165568e..86a1cf74 100644
--- a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
@@ -24,12 +24,7 @@
       </el-collapse-item>
       <el-collapse-item name="condition" v-if="formVisible" key="form">
         <template #title><Icon icon="ep:list" />表单</template>
-        <!-- <element-form :id="elementId" :type="elementType" /> -->
-        友情提示:使用
-        <router-link :to="{ path: '/bpm/manager/form' }"
-          ><el-link type="danger">流程表单</el-link>
-        </router-link>
-        替代,提供更好的表单设计功能
+        <element-form :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task">
         <template #title><Icon icon="ep:checked" />任务(审批人)</template>
diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
index da1d1ae9..b12cf76f 100644
--- a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
@@ -1,228 +1,233 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="80px">
-      <el-form-item label="表单标识">
-        <el-input v-model="formKey" clearable @change="updateElementFormKey" />
-      </el-form-item>
-      <el-form-item label="业务标识">
-        <el-select v-model="businessKey" @change="updateElementBusinessKey">
-          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />
-          <el-option label="无" value="" />
+      <el-form-item label="流程表单">
+        <!--        <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
+        <el-select v-model="formKey" clearable @change="updateElementFormKey">
+          <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
         </el-select>
       </el-form-item>
+      <!--      <el-form-item label="业务标识">-->
+      <!--        <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
+      <!--          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
+      <!--          <el-option label="无" value="" />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
     </el-form>
 
     <!--字段列表-->
-    <div class="element-property list-property">
-      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>
-      <el-table :data="fieldList" max-height="240" fit border>
-        <el-table-column label="序号" type="index" width="50px" />
-        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />
-        <el-table-column
-          label="字段类型"
-          prop="type"
-          min-width="80px"
-          :formatter="(row) => fieldType[row.type] || row.type"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          label="默认值"
-          prop="defaultValue"
-          min-width="80px"
-          show-overflow-tooltip
-        />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeField(scope, scope.$index)"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-    <div class="element-drawer__button">
-      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />
-    </div>
+    <!--    <div class="element-property list-property">-->
+    <!--      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
+    <!--      <el-table :data="fieldList" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" type="index" width="50px" />-->
+    <!--        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
+    <!--        <el-table-column-->
+    <!--          label="字段类型"-->
+    <!--          prop="type"-->
+    <!--          min-width="80px"-->
+    <!--          :formatter="(row) => fieldType[row.type] || row.type"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column-->
+    <!--          label="默认值"-->
+    <!--          prop="defaultValue"-->
+    <!--          min-width="80px"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeField(scope, scope.$index)"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+    <!--    </div>-->
+    <!--    <div class="element-drawer__button">-->
+    <!--      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
+    <!--    </div>-->
 
     <!--字段配置侧边栏-->
-    <el-drawer
-      v-model="fieldModelVisible"
-      title="字段配置"
-      :size="`${width}px`"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="formFieldForm" label-width="90px">
-        <el-form-item label="字段ID">
-          <el-input v-model="formFieldForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="类型">
-          <el-select
-            v-model="formFieldForm.typeType"
-            placeholder="请选择字段类型"
-            clearable
-            @change="changeFieldTypeType"
-          >
-            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">
-          <el-input v-model="formFieldForm.type" clearable />
-        </el-form-item>
-        <el-form-item label="名称">
-          <el-input v-model="formFieldForm.label" clearable />
-        </el-form-item>
-        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">
-          <el-input v-model="formFieldForm.datePattern" clearable />
-        </el-form-item>
-        <el-form-item label="默认值">
-          <el-input v-model="formFieldForm.defaultValue" clearable />
-        </el-form-item>
-      </el-form>
+    <!--    <el-drawer-->
+    <!--      v-model="fieldModelVisible"-->
+    <!--      title="字段配置"-->
+    <!--      :size="`${width}px`"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="formFieldForm" label-width="90px">-->
+    <!--        <el-form-item label="字段ID">-->
+    <!--          <el-input v-model="formFieldForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型">-->
+    <!--          <el-select-->
+    <!--            v-model="formFieldForm.typeType"-->
+    <!--            placeholder="请选择字段类型"-->
+    <!--            clearable-->
+    <!--            @change="changeFieldTypeType"-->
+    <!--          >-->
+    <!--            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
+    <!--          </el-select>-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
+    <!--          <el-input v-model="formFieldForm.type" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称">-->
+    <!--          <el-input v-model="formFieldForm.label" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
+    <!--          <el-input v-model="formFieldForm.datePattern" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="默认值">-->
+    <!--          <el-input v-model="formFieldForm.defaultValue" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
 
-      <!-- 枚举值设置 -->
-      <template v-if="formFieldForm.type === 'enum'">
-        <el-divider key="enum-divider" />
-        <p class="listener-filed__title" key="enum-title">
-          <span><Icon icon="ep:menu" />枚举值列表:</span>
-          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"
-            >添加枚举值</el-button
-          >
-        </p>
-        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>
-          <el-table-column label="序号" width="50px" type="index" />
-          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="操作" width="90px">
-            <template #default="scope">
-              <el-button
-                type="primary"
-                link
-                @click="openFieldOptionForm(scope, scope.$index, 'enum')"
-                >编辑</el-button
-              >
-              <el-divider direction="vertical" />
-              <el-button
-                type="primary"
-                link
-                style="color: #ff4d4f"
-                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"
-                >移除</el-button
-              >
-            </template>
-          </el-table-column>
-        </el-table>
-      </template>
+    <!--      &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
+    <!--      <template v-if="formFieldForm.type === 'enum'">-->
+    <!--        <el-divider key="enum-divider" />-->
+    <!--        <p class="listener-filed__title" key="enum-title">-->
+    <!--          <span><Icon icon="ep:menu" />枚举值列表:</span>-->
+    <!--          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
+    <!--            >添加枚举值</el-button-->
+    <!--          >-->
+    <!--        </p>-->
+    <!--        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
+    <!--          <el-table-column label="序号" width="50px" type="index" />-->
+    <!--          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="操作" width="90px">-->
+    <!--            <template #default="scope">-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
+    <!--                >编辑</el-button-->
+    <!--              >-->
+    <!--              <el-divider direction="vertical" />-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                style="color: #ff4d4f"-->
+    <!--                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
+    <!--                >移除</el-button-->
+    <!--              >-->
+    <!--            </template>-->
+    <!--          </el-table-column>-->
+    <!--        </el-table>-->
+    <!--      </template>-->
 
-      <!-- 校验规则 -->
-      <el-divider key="validation-divider" />
-      <p class="listener-filed__title" key="validation-title">
-        <span><Icon icon="ep:menu" />约束条件列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"
-          >添加约束</el-button
-        >
-      </p>
-      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 校验规则 &ndash;&gt;-->
+    <!--      <el-divider key="validation-divider" />-->
+    <!--      <p class="listener-filed__title" key="validation-title">-->
+    <!--        <span><Icon icon="ep:menu" />约束条件列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
+    <!--          >添加约束</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 表单属性 -->
-      <el-divider key="property-divider" />
-      <p class="listener-filed__title" key="property-title">
-        <span><Icon icon="ep:menu" />字段属性列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"
-          >添加属性</el-button
-        >
-      </p>
-      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'property')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'property')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 表单属性 &ndash;&gt;-->
+    <!--      <el-divider key="property-divider" />-->
+    <!--      <p class="listener-filed__title" key="property-title">-->
+    <!--        <span><Icon icon="ep:menu" />字段属性列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
+    <!--          >添加属性</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 底部按钮 -->
-      <div class="element-drawer__button">
-        <el-button>取 消</el-button>
-        <el-button type="primary" @click="saveField">保 存</el-button>
-      </div>
-    </el-drawer>
+    <!--      &lt;!&ndash; 底部按钮 &ndash;&gt;-->
+    <!--      <div class="element-drawer__button">-->
+    <!--        <el-button>取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveField">保 存</el-button>-->
+    <!--      </div>-->
+    <!--    </el-drawer>-->
 
-    <el-dialog
-      v-model="fieldOptionModelVisible"
-      :title="optionModelTitle"
-      width="600px"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="fieldOptionForm" label-width="96px">
-        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">
-          <el-input v-model="fieldOptionForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">
-          <el-input v-model="fieldOptionForm.name" clearable />
-        </el-form-item>
-        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">
-          <el-input v-model="fieldOptionForm.config" clearable />
-        </el-form-item>
-        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">
-          <el-input v-model="fieldOptionForm.value" clearable />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>
-        <el-button type="primary" @click="saveFieldOption">确 定</el-button>
-      </template>
-    </el-dialog>
+    <!--    <el-dialog-->
+    <!--      v-model="fieldOptionModelVisible"-->
+    <!--      :title="optionModelTitle"-->
+    <!--      width="600px"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="fieldOptionForm" label-width="96px">-->
+    <!--        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
+    <!--          <el-input v-model="fieldOptionForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
+    <!--          <el-input v-model="fieldOptionForm.name" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
+    <!--          <el-input v-model="fieldOptionForm.config" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
+    <!--          <el-input v-model="fieldOptionForm.value" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
+    <!--      <template #footer>-->
+    <!--        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveFieldOption">确 定</el-button>-->
+    <!--      </template>-->
+    <!--    </el-dialog>-->
   </div>
 </template>
 
 <script lang="ts" setup>
+import * as FormApi from '@/api/bpm/form'
+
 defineOptions({ name: 'ElementForm' })
 
 const props = defineProps({
@@ -263,6 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
 const resetFormList = () => {
   bpmnELement.value = bpmnInstances().bpmnElement
   formKey.value = bpmnELement.value.businessObject.formKey
+  if (formKey.value?.length > 0) {
+    formKey.value = parseInt(formKey.value)
+  }
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnELement.value.businessObject.get('extensionElements') ||
@@ -451,6 +459,11 @@ const updateElementExtensions = () => {
   })
 }
 
+const formList = ref([]) // 流程表单的下拉框的数据
+onMounted(async () => {
+  formList.value = await FormApi.getSimpleFormList()
+})
+
 watch(
   () => props.id,
   (val) => {
diff --git a/src/utils/formCreate.ts b/src/utils/formCreate.ts
index 6d7dbc7f..b9644d6f 100644
--- a/src/utils/formCreate.ts
+++ b/src/utils/formCreate.ts
@@ -28,7 +28,7 @@ export const decodeFields = (fields: string[]) => {
   return rule
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景
 export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
   // @ts-ignore
   designerRef.value.setOption(JSON.parse(conf))
@@ -36,19 +36,22 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
   designerRef.value.setRule(decodeFields(fields))
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 form-create 场景
 export const setConfAndFields2 = (
   detailPreview: object,
   conf: string,
   fields: string,
   value?: object
 ) => {
+  if (isRef(detailPreview)) {
+    detailPreview = detailPreview.value
+  }
   // @ts-ignore
-  detailPreview.value.option = JSON.parse(conf)
+  detailPreview.option = JSON.parse(conf)
   // @ts-ignore
-  detailPreview.value.rule = decodeFields(fields)
+  detailPreview.rule = decodeFields(fields)
   if (value) {
     // @ts-ignore
-    detailPreview.value.value = value
+    detailPreview.value = value
   }
 }
diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue
index d41c2b87..5758954e 100644
--- a/src/views/bpm/model/ModelForm.vue
+++ b/src/views/bpm/model/ModelForm.vue
@@ -194,11 +194,10 @@ const submitForm = async () => {
       await ModelApi.createModel(data)
       // 提示,引导用户做后续的操作
       await ElMessageBox.alert(
-        '<strong>新建模型成功!</strong>后续需要执行如下 4 个步骤:' +
+        '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
           '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
           '<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
-          '<div>3. 点击【分配规则】按钮,设置每个用户任务的审批人</div>' +
-          '<div>4. 点击【发布流程】按钮,完成流程的最终发布</div>' +
+          '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
           '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
         '重要提示',
         {
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 2aac5fad..0a557e50 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -21,9 +21,22 @@
             {{ processInstance.name }}
           </el-form-item>
           <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
-            {{ processInstance.startUser.nickname }}
-            <el-tag size="small" type="info">{{ processInstance.startUser.deptName }}</el-tag>
+            {{ processInstance?.startUser.nickname }}
+            <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
           </el-form-item>
+          <el-card class="mb-15px !-mt-10px" v-if="runningTasks[index].formId > 0">
+            <template #header>
+              <span class="el-icon-picture-outline">
+                填写表单【{{ runningTasks[index]?.formName }}】
+              </span>
+            </template>
+            <form-create
+              v-model:api="approveFormFApis[index]"
+              v-model="approveForms[index].value"
+              :option="approveForms[index].option"
+              :rule="approveForms[index].rule"
+            />
+          </el-card>
           <el-form-item label="审批建议" prop="reason">
             <el-input
               v-model="auditForms[index].reason"
@@ -149,6 +162,9 @@ const auditForms = ref<any[]>([]) // 审批任务的表单
 const auditRule = reactive({
   reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
 })
+const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
+const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
+
 // ========== 申请信息 ==========
 const fApi = ref<ApiAttrs>() //
 const detailForm = ref({
@@ -158,6 +174,20 @@ const detailForm = ref({
   value: {}
 })
 
+/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
+watch(
+  () => approveFormFApis.value,
+  (value) => {
+    value?.forEach((api) => {
+      api.btn.show(false)
+      api.resetBtn.show(false)
+    })
+  },
+  {
+    deep: true
+  }
+)
+
 /** 处理审批通过和不通过的操作 */
 const handleAudit = async (task, pass) => {
   // 1.1 获得对应表单
@@ -176,6 +206,12 @@ const handleAudit = async (task, pass) => {
     copyUserIds: auditForms.value[index].copyUserIds
   }
   if (pass) {
+    // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
+    const formCreateApi = approveFormFApis.value[index]
+    if (formCreateApi) {
+      await formCreateApi.validate()
+      data.variables = approveForms.value[index].value
+    }
     await TaskApi.approveTask(data)
     message.success('审批通过成功')
   } else {
@@ -258,6 +294,10 @@ const getProcessInstance = async () => {
 
 /** 加载任务列表 */
 const getTaskList = async () => {
+  runningTasks.value = []
+  auditForms.value = []
+  approveForms.value = []
+  approveFormFApis.value = []
   try {
     // 获得未取消的任务
     tasksLoad.value = true
@@ -285,8 +325,6 @@ const getTaskList = async () => {
     })
 
     // 获得需要自己审批的任务
-    runningTasks.value = []
-    auditForms.value = []
     loadRunningTask(tasks.value)
   } finally {
     tasksLoad.value = false
@@ -315,6 +353,15 @@ const loadRunningTask = (tasks) => {
       reason: '',
       copyUserIds: []
     })
+
+    // 2.4 处理 approve 表单
+    if (task.formId && task.formConf) {
+      const approveForm = {}
+      setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariable)
+      approveForms.value.push(approveForm)
+    } else {
+      approveForms.value.push({}) // 占位,避免为空
+    }
   })
 }
 

From d0f73344bfd437d8fa7ded61793d20eb7c74f275 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 20 Mar 2024 18:51:49 +0800
Subject: [PATCH 33/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=BC=BA=20model=20?=
 =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=A8=A1=E5=9E=8B=E9=83=A8=E7=BD=B2=E6=97=B6?=
 =?UTF-8?q?=EF=BC=8C=E5=90=84=E7=A7=8D=E5=8F=82=E6=95=B0=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../package/designer/ProcessDesigner.vue      |  8 ++--
 .../package/designer/ProcessViewer.vue        |  2 +-
 .../package/penal/base/ElementBaseInfo.vue    | 33 ++++++-------
 .../package/penal/form/ElementForm.vue        |  2 +-
 src/views/bpm/model/editor/index.vue          |  2 +-
 .../detail/ProcessInstanceTaskList.vue        | 48 +++++++++++++++++--
 .../bpm/processInstance/detail/index.vue      |  3 +-
 7 files changed, 69 insertions(+), 29 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
index 3fe21944..6cbe11fa 100644
--- a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
@@ -436,7 +436,7 @@ const initBpmnModeler = () => {
 
   // bpmnModeler.createDiagram()
 
-  console.log(bpmnModeler, 'bpmnModeler111111')
+  // console.log(bpmnModeler, 'bpmnModeler111111')
   emit('init-finished', bpmnModeler)
   initModelListeners()
 }
@@ -666,10 +666,10 @@ const previewProcessJson = () => {
 }
 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
 const processSave = async () => {
-  console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
+  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
   const { err, xml } = await bpmnModeler.saveXML()
-  console.log(err, 'errerrerrerrerr')
-  console.log(xml, 'xmlxmlxmlxmlxml')
+  // console.log(err, 'errerrerrerrerr')
+  // console.log(xml, 'xmlxmlxmlxmlxml')
   // 读取异常时抛出异常
   if (err) {
     // this.$modal.msgError('保存模型失败,请重试!')
diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
index 27e6151a..f5646355 100644
--- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -368,7 +368,7 @@ const elementHover = (element) => {
         html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
       }
     }
-    console.log(html, 'html111111111111111')
+    // console.log(html, 'html111111111111111')
     elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
       position: { left: 0, bottom: 0 },
       html: `<div class="element-overlays">${html}</div>`
diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 03f82e76..5e77c948 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -68,13 +68,13 @@ const resetBaseInfo = () => {
   console.log(bpmnElement.value, 'bpmnElement')
 
   bpmnElement.value = bpmnInstances()?.bpmnElement
-  console.log(bpmnElement.value, 'resetBaseInfo11111111111')
+  // console.log(bpmnElement.value, 'resetBaseInfo11111111111')
   elementBaseInfo.value = bpmnElement.value.businessObject
   needProps.value['type'] = bpmnElement.value.businessObject.$type
   // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
 
   // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
-  console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
+  // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
 }
 const handleKeyUpdate = (value) => {
   // 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
@@ -121,11 +121,11 @@ const updateBaseInfo = (key) => {
   //   id: elementBaseInfo.value[key]
   //   // di: { id: `${elementBaseInfo.value[key]}_di` }
   // }
-  console.log(elementBaseInfo, 'elementBaseInfo11111111111')
+  // console.log(elementBaseInfo, 'elementBaseInfo11111111111')
   needProps.value = { ...elementBaseInfo.value, ...needProps.value }
 
   if (key === 'id') {
-    console.log('jinru')
+    // console.log('jinru')
     console.log(window, 'window')
     console.log(bpmnElement.value, 'bpmnElement')
     console.log(toRaw(bpmnElement.value), 'bpmnElement')
@@ -139,21 +139,10 @@ const updateBaseInfo = (key) => {
   }
 }
 
-// TODO 芋艿:这里延迟,可能存在覆盖 userTask 的问题。。例如说,打开的时候,立马选中某个 usertask,则它的 id 会被覆盖。。。
-onMounted(() => {
-  // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新
-  setTimeout(() => {
-    console.log(props.model, 'props.model')
-    handleKeyUpdate(props.model.key)
-    handleNameUpdate(props.model.name)
-    console.log(props, 'propsssssssssssssssssssss')
-  }, 1000)
-})
-
 watch(
   () => props.businessObject,
   (val) => {
-    console.log(val, 'val11111111111111111111')
+    // console.log(val, 'val11111111111111111111')
     if (val) {
       // nextTick(() => {
       resetBaseInfo()
@@ -161,6 +150,18 @@ watch(
     }
   }
 )
+
+watch(
+  () => props.model?.key,
+  (val) => {
+    // 针对上传的 bpmn 流程图时,保证 key 和 name 的更新
+    if (val) {
+      handleKeyUpdate(props.model.key)
+      handleNameUpdate(props.model.name)
+    }
+  }
+)
+
 // watch(
 //   () => ({ ...props }),
 //   (oldVal, newVal) => {
diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
index b12cf76f..60f374f4 100644
--- a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
@@ -429,7 +429,7 @@ const saveField = () => {
 
 // 移除某个 字段的 配置项
 const removeFieldOptionItem = (option, index, type) => {
-  console.log(option, 'option')
+  // console.log(option, 'option')
   if (type === 'property') {
     fieldPropertiesList.value.splice(index, 1)
     return
diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue
index f5c0ec6e..0dfabc75 100644
--- a/src/views/bpm/model/editor/index.vue
+++ b/src/views/bpm/model/editor/index.vue
@@ -89,11 +89,11 @@ onMounted(async () => {
   }
   // 查询模型
   const data = await ModelApi.getModel(modelId)
-  xmlString.value = data.bpmnXml
   model.value = {
     ...data,
     bpmnXml: undefined // 清空 bpmnXml 属性
   }
+  xmlString.value = data.bpmnXml
 })
 </script>
 <style lang="scss">
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index ba573a2c..f578cd98 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -16,12 +16,21 @@
               任务:{{ item.name }}
               <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.status" />
               <el-button
-                style="margin-left: 5px"
+                class="ml-10px"
                 v-if="!isEmpty(item.children)"
                 @click="openChildrenTask(item)"
+                size="small"
               >
                 <Icon icon="ep:memo" /> 子任务
               </el-button>
+              <el-button
+                class="ml-10px"
+                size="small"
+                v-if="item.formId > 0"
+                @click="handleFormDetail(item)"
+              >
+                <Icon icon="ep:document" /> 查看表单
+              </el-button>
             </p>
             <el-card :body-style="{ padding: '10px' }">
               <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal">
@@ -50,10 +59,19 @@
         </el-timeline>
       </div>
     </el-col>
-
-    <!-- 弹窗:子任务  -->
-    <TaskSignList ref="taskSignListRef" @success="refresh" />
   </el-card>
+
+  <!-- 弹窗:子任务  -->
+  <TaskSignList ref="taskSignListRef" @success="refresh" />
+  <!-- 弹窗:表单 -->
+  <Dialog title="表单详情" v-model="taskFormVisible" width="600">
+    <form-create
+      ref="fApi"
+      v-model="taskForm.value"
+      :option="taskForm.option"
+      :rule="taskForm.rule"
+    />
+  </Dialog>
 </template>
 <script lang="ts" setup>
 import { formatDate, formatPast2 } from '@/utils/formatTime'
@@ -61,6 +79,8 @@ import { propTypes } from '@/utils/propTypes'
 import { DICT_TYPE } from '@/utils/dict'
 import { isEmpty } from '@/utils/is'
 import TaskSignList from './dialog/TaskSignList.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { setConfAndFields2 } from '@/utils/formCreate'
 
 defineOptions({ name: 'BpmProcessInstanceTaskList' })
 
@@ -122,6 +142,26 @@ const openChildrenTask = (item: any) => {
   taskSignListRef.value.open(item)
 }
 
+/** 查看表单 */
+const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
+const taskForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程任务的表单详情
+const taskFormVisible = ref(false)
+const handleFormDetail = async (row) => {
+  // 设置表单
+  setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
+  // 弹窗打开
+  taskFormVisible.value = true
+  // 隐藏提交、重置按钮,设置禁用只读
+  await nextTick()
+  fApi.value.fapi.btn.show(false)
+  fApi.value?.fapi?.resetBtn.show(false)
+  fApi.value?.fapi?.disabled(true)
+}
+
 /** 刷新数据 */
 const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
 const refresh = () => {
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 0a557e50..1006f698 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -168,11 +168,10 @@ const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
 // ========== 申请信息 ==========
 const fApi = ref<ApiAttrs>() //
 const detailForm = ref({
-  // 流程表单详情
   rule: [],
   option: {},
   value: {}
-})
+}) // 流程实例的表单详情
 
 /** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
 watch(

From d16194b794994a8ee3f8e78c82ec540da9527bab Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 20 Mar 2024 21:33:48 +0800
Subject: [PATCH 34/49] =?UTF-8?q?BPM=EF=BC=9A=E4=BC=98=E5=8C=96=20task=20?=
 =?UTF-8?q?=E5=AE=A1=E6=89=B9=E8=AF=A6=E6=83=85=E7=95=8C=E9=9D=A2=EF=BC=8C?=
 =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BC=80=E5=A7=8B=E6=97=B6=E9=97=B4=E3=80=81?=
 =?UTF-8?q?=E7=BB=93=E6=9D=9F=E6=97=B6=E9=97=B4=E7=9A=84=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/form/index.ts                     |  4 +-
 src/api/bpm/leave/index.ts                    |  2 +-
 .../package/designer/ProcessViewer.vue        | 58 ++++++++++++-------
 .../package/penal/form/ElementForm.vue        |  2 +-
 src/utils/dict.ts                             |  2 +-
 src/views/bpm/model/ModelForm.vue             |  2 +-
 src/views/bpm/model/ModelImportForm.vue       |  1 +
 src/views/bpm/oa/leave/index.vue              | 15 +++--
 .../detail/ProcessInstanceTaskList.vue        | 57 +++++++++---------
 .../detail/dialog/TaskSignList.vue            |  2 +-
 .../bpm/processInstance/detail/index.vue      |  7 ++-
 src/views/bpm/task/done/index.vue             |  2 +-
 12 files changed, 93 insertions(+), 61 deletions(-)

diff --git a/src/api/bpm/form/index.ts b/src/api/bpm/form/index.ts
index 142ed24c..7fce11fc 100644
--- a/src/api/bpm/form/index.ts
+++ b/src/api/bpm/form/index.ts
@@ -49,8 +49,8 @@ export const getFormPage = async (params) => {
 }
 
 // 获得动态表单的精简列表
-export const getSimpleFormList = async () => {
+export const getFormSimpleList = async () => {
   return await request.get({
-    url: '/bpm/form/list-all-simple'
+    url: '/bpm/form/simple-list'
   })
 }
diff --git a/src/api/bpm/leave/index.ts b/src/api/bpm/leave/index.ts
index d4fe8d58..4f374b2f 100644
--- a/src/api/bpm/leave/index.ts
+++ b/src/api/bpm/leave/index.ts
@@ -2,7 +2,7 @@ import request from '@/config/axios'
 
 export type LeaveVO = {
   id: number
-  result: number
+  status: number
   type: number
   reason: string
   processInstanceId: string
diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
index f5646355..485b9795 100644
--- a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -115,7 +115,7 @@ const highlightDiagram = async () => {
       if (!task) {
         return
       }
-      //进行中的任务已经高亮过了,则不高亮后面的任务了
+      // 进行中的任务已经高亮过了,则不高亮后面的任务了
       if (findProcessTask) {
         removeTaskDefinitionKeyList.push(n.id)
         return
@@ -194,6 +194,7 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:StartEvent') {
       // 开始节点
+      canvas.addMarker(n.id, 'highlight')
       n.outgoing?.forEach((nn) => {
         // outgoing 例如说【bpmn:SequenceFlow】连线
         // 获得连线是否有指向目标。如果有,则进行高亮
@@ -223,40 +224,49 @@ const highlightDiagram = async () => {
           canvas.addMarker(out.id, getResultCss(2))
         })
       }
+    } else if (n.$type === 'bpmn:SequenceFlow') {
+      let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
+      if (targetActivity) {
+        canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
+      }
     }
   })
   if (!isEmpty(removeTaskDefinitionKeyList)) {
-    // TODO 芋艿:后面 .definitionKey 再看
     taskList.value = taskList.value.filter(
-      (item) => !removeTaskDefinitionKeyList.includes(item.definitionKey)
+      (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
     )
   }
 }
+
 const getActivityHighlightCss = (activity) => {
   return activity.endTime ? 'highlight' : 'highlight-todo'
 }
-const getResultCss = (result) => {
-  if (result === 1) {
+
+const getResultCss = (status) => {
+  if (status === 1) {
     // 审批中
     return 'highlight-todo'
-  } else if (result === 2) {
+  } else if (status === 2) {
     // 已通过
     return 'highlight'
-  } else if (result === 3) {
+  } else if (status === 3) {
     // 不通过
     return 'highlight-reject'
-  } else if (result === 4) {
+  } else if (status === 4) {
     // 已取消
     return 'highlight-cancel'
-  } else if (result === 5) {
+  } else if (status === 5) {
     // 退回
     return 'highlight-return'
-  } else if (result === 6) {
+  } else if (status === 6) {
     // 委派
-    return 'highlight-return'
-  } else if (result === 7 || result === 8 || result === 9) {
-    // 待后加签任务完成/待前加签任务完成/待前置任务完成
-    return 'highlight-return'
+    return 'highlight-todo'
+  } else if (status === 7) {
+    // 审批通过中
+    return 'highlight-todo'
+  } else if (status === 0) {
+    // 待审批
+    return 'highlight-todo'
   }
   return ''
 }
@@ -297,10 +307,10 @@ const elementHover = (element) => {
   !elementOverlayIds.value && (elementOverlayIds.value = {})
   !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
   // 展示信息
-  console.log(activityLists.value, 'activityLists.value')
-  console.log(element.value, 'element.value')
+  // console.log(activityLists.value, 'activityLists.value')
+  // console.log(element.value, 'element.value')
   const activity = activityLists.value.find((m) => m.key === element.value.id)
-  console.log(activity, 'activityactivityactivityactivity')
+  // console.log(activity, 'activityactivityactivityactivity')
   if (!activity) {
     return
   }
@@ -314,12 +324,11 @@ const elementHover = (element) => {
                   <p>部门:${processInstance.value.startUser.deptName}</p>
                   <p>创建时间:${formatDate(processInstance.value.createTime)}`
     } else if (element.value.type === 'bpmn:UserTask') {
-      // debugger
       let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
       if (!task) {
         return
       }
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
         if (element.value == task.status) {
@@ -352,7 +361,7 @@ const elementHover = (element) => {
       }
       console.log(html)
     } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
         if (element.value == processInstance.value.status) {
@@ -375,6 +384,7 @@ const elementHover = (element) => {
     })
   }
 }
+
 // 流程图的元素被 out
 const elementOut = (element) => {
   toRaw(overlays.value).remove({ element })
@@ -390,6 +400,7 @@ onMounted(() => {
   // 初始模型的监听器
   initModelListeners()
 })
+
 onBeforeUnmount(() => {
   // this.$once('hook:beforeDestroy', () => {
   // })
@@ -428,7 +439,7 @@ watch(
 )
 </script>
 
-<style>
+<style lang="scss">
 /** 处理中 */
 .highlight-todo.djs-connection > .djs-visual > path {
   stroke: #1890ff !important;
@@ -502,6 +513,10 @@ watch(
   stroke: green !important;
 }
 
+.djs-element.highlight > .djs-visual > path {
+  stroke: green !important;
+}
+
 /** 不通过 */
 .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
   fill: red !important;
@@ -521,6 +536,7 @@ watch(
 
 .highlight-reject.djs-connection > .djs-visual > path {
   stroke: red !important;
+  marker-end: url(#sequenceflow-end-white-success) !important;
 }
 
 .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
index 60f374f4..33f0bc09 100644
--- a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
@@ -461,7 +461,7 @@ const updateElementExtensions = () => {
 
 const formList = ref([]) // 流程表单的下拉框的数据
 onMounted(async () => {
-  formList.value = await FormApi.getSimpleFormList()
+  formList.value = await FormApi.getFormSimpleList()
 })
 
 watch(
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index 6d7d2e72..f7d337cb 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -139,7 +139,7 @@ export enum DICT_TYPE {
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
   BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
-  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', // TODO @芋艿:改名
+  BPM_TASK_STATUS = 'bpm_task_status',
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
 
   // ========== PAY 模块 ==========
diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue
index 5758954e..0e5b0521 100644
--- a/src/views/bpm/model/ModelForm.vue
+++ b/src/views/bpm/model/ModelForm.vue
@@ -173,7 +173,7 @@ const open = async (type: string, id?: number) => {
     }
   }
   // 获得流程表单的下拉框的数据
-  formList.value = await FormApi.getSimpleFormList()
+  formList.value = await FormApi.getFormSimpleList()
   // 查询流程分类列表
   categoryList.value = await CategoryApi.getCategorySimpleList()
 }
diff --git a/src/views/bpm/model/ModelImportForm.vue b/src/views/bpm/model/ModelImportForm.vue
index 74f10ffd..9a91e1d5 100644
--- a/src/views/bpm/model/ModelImportForm.vue
+++ b/src/views/bpm/model/ModelImportForm.vue
@@ -109,6 +109,7 @@ const submitFormSuccess = async (response: any) => {
   }
   // 提示成功
   message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
+  dialogVisible.value = false
   // 发送操作成功的事件
   emit('success')
 }
diff --git a/src/views/bpm/oa/leave/index.vue b/src/views/bpm/oa/leave/index.vue
index f6dac5bc..4af7ad3c 100644
--- a/src/views/bpm/oa/leave/index.vue
+++ b/src/views/bpm/oa/leave/index.vue
@@ -36,10 +36,15 @@
           value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
-      <el-form-item label="结果" prop="result">
-        <el-select v-model="queryParams.result" class="!w-240px" clearable placeholder="请选择结果">
+      <el-form-item label="审批结果" prop="result">
+        <el-select
+          v-model="queryParams.result"
+          class="!w-240px"
+          clearable
+          placeholder="请选择审批结果"
+        >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -78,7 +83,7 @@
       <el-table-column align="center" label="申请编号" prop="id" />
       <el-table-column align="center" label="状态" prop="result">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.result" />
         </template>
       </el-table-column>
       <el-table-column
@@ -166,7 +171,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   type: undefined,
-  result: undefined,
+  status: undefined,
   reason: undefined,
   createTime: []
 })
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
index f578cd98..f82e8003 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -7,14 +7,25 @@
       <div class="block">
         <el-timeline>
           <el-timeline-item
-            v-for="(item, index) in tasks"
-            :key="index"
-            :icon="getTimelineItemIcon(item)"
-            :type="getTimelineItemType(item)"
+            v-if="processInstance.endTime"
+            :type="getProcessInstanceTimelineItemType(processInstance)"
           >
             <p style="font-weight: 700">
-              任务:{{ item.name }}
-              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.status" />
+              结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
+              <dict-tag
+                :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+                :value="processInstance.status"
+              />
+            </p>
+          </el-timeline-item>
+          <el-timeline-item
+            v-for="(item, index) in tasks"
+            :key="index"
+            :type="getTaskTimelineItemType(item)"
+          >
+            <p style="font-weight: 700">
+              审批任务:{{ item.name }}
+              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
               <el-button
                 class="ml-10px"
                 v-if="!isEmpty(item.children)"
@@ -56,6 +67,12 @@
               <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
             </el-card>
           </el-timeline-item>
+          <el-timeline-item type="success">
+            <p style="font-weight: 700">
+              发起流程:【{{ processInstance.startUser?.nickname }}】在
+              {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
+            </p>
+          </el-timeline-item>
         </el-timeline>
       </div>
     </el-col>
@@ -86,33 +103,27 @@ defineOptions({ name: 'BpmProcessInstanceTaskList' })
 
 defineProps({
   loading: propTypes.bool, // 是否加载中
+  processInstance: propTypes.object, // 流程实例
   tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
 })
 
-/** 获得任务对应的 icon */
-// TODO @芋艿:对应的 icon 需要调整
-const getTimelineItemIcon = (item) => {
-  if (item.status === 1) {
-    return 'el-icon-time'
-  }
+/** 获得流程实例对应的颜色 */
+const getProcessInstanceTimelineItemType = (item: any) => {
   if (item.status === 2) {
-    return 'el-icon-check'
+    return 'success'
   }
   if (item.status === 3) {
-    return 'el-icon-close'
+    return 'danger'
   }
   if (item.status === 4) {
-    return 'el-icon-remove-outline'
-  }
-  if (item.status === 5) {
-    return 'el-icon-back'
+    return 'warning'
   }
   return ''
 }
 
 /** 获得任务对应的颜色 */
-const getTimelineItemType = (item: any) => {
-  if (item.status === 1) {
+const getTaskTimelineItemType = (item: any) => {
+  if ([0, 1, 6, 7].includes(item.status)) {
     return 'primary'
   }
   if (item.status === 2) {
@@ -127,12 +138,6 @@ const getTimelineItemType = (item: any) => {
   if (item.status === 5) {
     return 'warning'
   }
-  if (item.status === 6) {
-    return 'default'
-  }
-  if (item.status === 7 || item.status === 8) {
-    return 'warning'
-  }
   return ''
 }
 
diff --git a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
index 564071bd..648e86b5 100644
--- a/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
+++ b/src/views/bpm/processInstance/detail/dialog/TaskSignList.vue
@@ -27,7 +27,7 @@
       </el-table-column>
       <el-table-column label="审批状态" prop="status" width="120">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
+          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 1006f698..2297fae0 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -105,7 +105,12 @@
     </el-card>
 
     <!-- 审批记录 -->
-    <ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" @refresh="getTaskList" />
+    <ProcessInstanceTaskList
+      :loading="tasksLoad"
+      :process-instance="processInstance"
+      :tasks="tasks"
+      @refresh="getTaskList"
+    />
 
     <!-- 高亮流程图 -->
     <ProcessInstanceBpmnViewer
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index 7d8f905b..ed922397 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -77,7 +77,7 @@
       />
       <el-table-column align="center" label="审批状态" prop="status" width="120">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.status" />
+          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />

From 07dc7258574804479e744e112da96519fd629d20 Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Wed, 20 Mar 2024 22:19:18 +0800
Subject: [PATCH 35/49] =?UTF-8?q?=E6=95=B4=E5=90=88=E4=BB=BF=E9=92=89?=
 =?UTF-8?q?=E9=92=89=E6=B5=81=E7=A8=8B=E8=AE=BE=E8=AE=A1=E5=99=A8=20https:?=
 =?UTF-8?q?//github.com/StavinLi/Workflow-Vue3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../SimpleProcessDesigner/src/addNode.vue     |  237 +++
 .../src/drawer/approverDrawer.vue             |  283 ++++
 .../SimpleProcessDesigner/src/nodeWrap.vue    |  298 ++++
 .../SimpleProcessDesigner/src/util.ts         |  165 +++
 .../SimpleProcessDesigner/theme/workflow.css  | 1292 +++++++++++++++++
 src/store/modules/simpleWorkflow.ts           |   55 +
 6 files changed, 2330 insertions(+)
 create mode 100644 src/components/SimpleProcessDesigner/src/addNode.vue
 create mode 100644 src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue
 create mode 100644 src/components/SimpleProcessDesigner/src/nodeWrap.vue
 create mode 100644 src/components/SimpleProcessDesigner/src/util.ts
 create mode 100644 src/components/SimpleProcessDesigner/theme/workflow.css
 create mode 100644 src/store/modules/simpleWorkflow.ts

diff --git a/src/components/SimpleProcessDesigner/src/addNode.vue b/src/components/SimpleProcessDesigner/src/addNode.vue
new file mode 100644
index 00000000..6d09ae8a
--- /dev/null
+++ b/src/components/SimpleProcessDesigner/src/addNode.vue
@@ -0,0 +1,237 @@
+/* stylelint-disable order/properties-order */
+<template>
+  <div class="add-node-btn-box">
+    <div class="add-node-btn">
+      <el-popover placement="right-start" v-model="visible" width="auto">
+        <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(2)">
+            <div class="item-wrapper">
+              <span class="iconfont"></span>
+            </div>
+            <p>抄送人</p>
+          </a>
+          <a class="add-node-popover-item condition" @click="addType(4)">
+            <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: () => ({})
+  }
+})
+let emits = defineEmits(['update:childNodeP'])
+let visible = ref(false)
+const addType = (type) => {
+  visible.value = false
+  if (type != 4) {
+    var data
+    if (type == 1) {
+      data = {
+        nodeName: '审核人',
+        error: true,
+        type: 1,
+        settype: 1,
+        selectMode: 0,
+        selectRange: 0,
+        directorLevel: 1,
+        examineMode: 1,
+        noHanderAction: 1,
+        examineEndDirectorLevel: 0,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    } else if (type == 2) {
+      data = {
+        nodeName: '抄送人',
+        type: 2,
+        ccSelfSelectFlag: 1,
+        childNode: props.childNodeP,
+        nodeUserList: []
+      }
+    }
+    emits('update:childNodeP', data)
+  } else {
+    emits('update:childNodeP', {
+      nodeName: '路由',
+      type: 4,
+      childNode: null,
+      conditionNodes: [
+        {
+          nodeName: '条件1',
+          error: true,
+          type: 3,
+          priorityLevel: 1,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: props.childNodeP
+        },
+        {
+          nodeName: '条件2',
+          type: 3,
+          priorityLevel: 2,
+          conditionList: [],
+          nodeUserList: [],
+          childNode: null
+        }
+      ]
+    })
+  }
+}
+</script>
+<style scoped lang="scss">
+.add-node-btn-box {
+  width: 240px;
+  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);
+      }
+    }
+  }
+}
+
+.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>
diff --git a/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue b/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue
new file mode 100644
index 00000000..867c16ff
--- /dev/null
+++ b/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue
@@ -0,0 +1,283 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    title="审批人设置"
+    v-model="visible"
+    class="set_promoter"
+    :show-close="false"
+    :size="550"
+    :before-close="saveApprover"
+  >
+    <div class="demo-drawer__content">
+      <div class="drawer_content">
+        <div class="approver_content">
+          <el-radio-group v-model="approverConfig.settype" class="clear" @change="changeType">
+            <el-radio v-for="{ value, label } in setTypes" :key="value" :label="value">{{
+              label
+            }}</el-radio>
+          </el-radio-group>
+          <el-button type="primary" @click="addApprover" v-if="approverConfig.settype == 1"
+            >添加/修改成员</el-button
+          >
+          <p class="selected_list" v-if="approverConfig.settype == 1">
+            <span v-for="(item, index) in approverConfig.nodeUserList" :key="index"
+              >{{ item.name }}
+              <img
+                src="@/assets/images/add-close1.png"
+                @click="removeEle(approverConfig.nodeUserList, item, 'targetId')"
+              />
+            </span>
+            <a
+              v-if="approverConfig.nodeUserList.length != 0"
+              @click="approverConfig.nodeUserList = []"
+              >清除</a
+            >
+          </p>
+        </div>
+        <div class="approver_manager" v-if="approverConfig.settype == 2">
+          <p>
+            <span>发起人的:</span>
+            <select v-model="approverConfig.directorLevel">
+              <option v-for="item in directorMaxLevel" :value="item" :key="item"
+                >{{ item == 1 ? '直接' : '第' + item + '级' }}主管</option
+              >
+            </select>
+          </p>
+          <p class="tip">找不到主管时,由上级主管代审批</p>
+        </div>
+        <div class="approver_self" v-if="approverConfig.settype == 5">
+          <p>该审批节点设置“发起人自己”后,审批人默认为发起人</p>
+        </div>
+        <div class="approver_self_select" v-show="approverConfig.settype == 4">
+          <el-radio-group v-model="approverConfig.selectMode" style="width: 100%">
+            <el-radio v-for="{ value, label } in selectModes" :label="value" :key="value">{{
+              label
+            }}</el-radio>
+          </el-radio-group>
+          <h3>选择范围</h3>
+          <el-radio-group
+            v-model="approverConfig.selectRange"
+            style="width: 100%"
+            @change="changeRange"
+          >
+            <el-radio v-for="{ value, label } in selectRanges" :label="value" :key="value">{{
+              label
+            }}</el-radio>
+          </el-radio-group>
+          <template v-if="approverConfig.selectRange == 2 || approverConfig.selectRange == 3">
+            <el-button type="primary" @click="addApprover" v-if="approverConfig.selectRange == 2"
+              >添加/修改成员</el-button
+            >
+            <el-button type="primary" @click="addRoleApprover" v-else>添加/修改角色</el-button>
+            <p class="selected_list">
+              <span v-for="(item, index) in approverConfig.nodeUserList" :key="index"
+                >{{ item.name }}
+                <img
+                  src="@/assets/images/add-close1.png"
+                  @click="removeEle(approverConfig.nodeUserList, item, 'targetId')"
+                />
+              </span>
+              <a
+                v-if="approverConfig.nodeUserList.length != 0 && approverConfig.selectRange != 1"
+                @click="approverConfig.nodeUserList = []"
+                >清除</a
+              >
+            </p>
+          </template>
+        </div>
+        <div class="approver_manager" v-if="approverConfig.settype == 7">
+          <p>审批终点</p>
+          <p style="padding-bottom: 20px">
+            <span>发起人的:</span>
+            <select v-model="approverConfig.examineEndDirectorLevel">
+              <option v-for="item in directorMaxLevel" :value="item" :key="item"
+                >{{ item == 1 ? '最高' : '第' + item }}层级主管</option
+              >
+            </select>
+          </p>
+        </div>
+        <div
+          class="approver_some"
+          v-if="
+            (approverConfig.settype == 1 && approverConfig.nodeUserList.length > 1) ||
+            approverConfig.settype == 2 ||
+            (approverConfig.settype == 4 && approverConfig.selectMode == 2)
+          "
+        >
+          <p>多人审批时采用的审批方式</p>
+          <el-radio-group v-model="approverConfig.examineMode" class="clear">
+            <el-radio :label="1">依次审批</el-radio>
+            <br />
+            <el-radio :label="2" v-if="approverConfig.settype != 2"
+              >会签(须所有审批人同意)</el-radio
+            >
+          </el-radio-group>
+        </div>
+        <div
+          class="approver_some"
+          v-if="approverConfig.settype == 2 || approverConfig.settype == 7"
+        >
+          <p>审批人为空时</p>
+          <el-radio-group v-model="approverConfig.noHanderAction" class="clear">
+            <el-radio :label="1">自动审批通过/不允许发起</el-radio>
+            <br />
+            <el-radio :label="2">转交给审核管理员</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <div class="demo-drawer__footer clear">
+        <el-button type="primary" @click="saveApprover">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+import { ref, watch, computed } from 'vue'
+import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
+import { setTypes, selectModes, selectRanges } from '../util'
+import { removeEle, setApproverStr } from '../util'
+let props = defineProps({
+  directorMaxLevel: {
+    type: Number,
+    default: 0
+  }
+})
+let approverConfig = ref({})
+let approverVisible = ref(false)
+let approverRoleVisible = ref(false)
+let checkedRoleList = ref([])
+let checkedList = ref([])
+let store = useWorkFlowStoreWithOut()
+let { setApproverConfig, setApprover } = store
+let approverConfig1 = computed(() => store.approverConfig1)
+let approverDrawer = computed(() => store.approverDrawer)
+let visible = computed({
+  get() {
+    return approverDrawer.value
+  },
+  set() {
+    closeDrawer()
+  }
+})
+watch(approverConfig1, (val: any) => {
+  approverConfig.value = val.value
+})
+let changeRange = () => {
+  approverConfig.value.nodeUserList = []
+}
+const changeType = (val) => {
+  approverConfig.value.nodeUserList = []
+  approverConfig.value.examineMode = 1
+  approverConfig.value.noHanderAction = 2
+  if (val == 2) {
+    approverConfig.value.directorLevel = 1
+  } else if (val == 4) {
+    approverConfig.value.selectMode = 1
+    approverConfig.value.selectRange = 1
+  } else if (val == 7) {
+    approverConfig.value.examineEndDirectorLevel = 1
+  }
+}
+const addApprover = () => {
+  approverVisible.value = true
+  checkedList.value = approverConfig.value.nodeUserList
+}
+const addRoleApprover = () => {
+  approverRoleVisible.value = true
+  checkedRoleList.value = approverConfig.value.nodeUserList
+}
+const sureApprover = (data) => {
+  approverConfig.value.nodeUserList = data
+  approverVisible.value = false
+}
+const sureRoleApprover = (data) => {
+  approverConfig.value.nodeUserList = data
+  approverRoleVisible.value = false
+}
+const saveApprover = () => {
+  approverConfig.value.error = !setApproverStr(approverConfig.value)
+  setApproverConfig({
+    value: approverConfig.value,
+    flag: true,
+    id: approverConfig1.value.id
+  })
+  closeDrawer()
+}
+const closeDrawer = () => {
+  setApprover(false)
+}
+</script>
+<style lang="scss" scoped>
+.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;
+  }
+
+  .approver_self_select h3 {
+    margin: 5px 0 20px;
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 19px;
+  }
+}
+</style>
diff --git a/src/components/SimpleProcessDesigner/src/nodeWrap.vue b/src/components/SimpleProcessDesigner/src/nodeWrap.vue
new file mode 100644
index 00000000..9081becd
--- /dev/null
+++ b/src/components/SimpleProcessDesigner/src/nodeWrap.vue
@@ -0,0 +1,298 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<!--
+ * @Date: 2022-09-21 14:41:53
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2023-05-24 15:20:24
+ * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
+-->
+<template>
+     <div class="node-wrap" v-if="nodeConfig.type < 3">
+      <div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
+          <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
+            <span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
+            <template v-else>
+              <span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
+              <input
+                v-if="isInput"
+                type="text"
+                class="ant-input editable-title-input"
+                @blur="blurEvent()"
+                @focus="$event.currentTarget.select()"
+                v-focus
+                v-model="nodeConfig.nodeName"
+                :placeholder="defaultText"
+              />
+              <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
+              <i class="anticon anticon-close close" @click="delNode"></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" />
+    </div>
+    <div class="branch-wrap" v-if="nodeConfig.type == 4">
+    <div class="branch-box-wrap">
+      <div class="branch-box">
+        <button class="add-branch" @click="addTerm">添加条件</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" @click="arrTransfer(index, -1)">&lt;</div>
+                <div class="title-wrapper">
+                  <input
+                    v-if="isInputList[index]"
+                    type="text"
+                    class="ant-input editable-title-input"
+                    @blur="blurEvent(index)"
+                    @focus="$event.currentTarget.select()"
+                    v-focus
+                    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" @click="delTerm(index)"></i>
+                </div>
+                <div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">&gt;</div>
+                <div class="content" @click="setPerson(item.priorityLevel)">{{ 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" />
+            </div>
+          </div>
+          <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
+          <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" />
+    </div>
+  </div>
+    <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
+</template>
+<script  setup>
+import addNode from './addNode.vue'
+import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
+import {
+  arrToStr,
+  conditionStr,
+  setApproverStr,
+  copyerStr,
+  bgColors,
+  placeholderList
+} from './util'
+import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
+let _uid = getCurrentInstance().uid
+
+let props = defineProps({
+  nodeConfig: {
+    type: Object,
+    default: () => ({})
+  },
+  flowPermission: {
+    type: Object,
+    // eslint-disable-next-line vue/require-valid-default-prop
+    default: () => []
+  }
+})
+
+let defaultText = computed(() => {
+  return placeholderList[props.nodeConfig.type]
+})
+let showText = computed(() => {
+  if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
+  if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
+  return copyerStr(props.nodeConfig)
+})
+
+let isInputList = ref([])
+let isInput = ref(false)
+const resetConditionNodesErr = () => {
+  for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.conditionNodes[i].error =
+      conditionStr(props.nodeConfig, i) == '请设置条件' &&
+      i != props.nodeConfig.conditionNodes.length - 1
+  }
+}
+onMounted(() => {
+  if (props.nodeConfig.type == 1) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.error = !setApproverStr(props.nodeConfig)
+  } else if (props.nodeConfig.type == 2) {
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.error = !copyerStr(props.nodeConfig)
+  } else if (props.nodeConfig.type == 4) {
+    resetConditionNodesErr()
+  }
+})
+let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
+let store = useWorkFlowStoreWithOut()
+let {
+  setPromoter,
+  setApprover,
+  setCopyer,
+  setCondition,
+  setFlowPermission,
+  setApproverConfig,
+  setCopyerConfig,
+  setConditionsConfig
+} = store
+let isTried = computed(() => store.isTried)
+let flowPermission1 = computed(() => store.flowPermission1)
+let approverConfig1 = computed(() => store.approverConfig1)
+let copyerConfig1 = computed(() => store.copyerConfig1)
+let conditionsConfig1 = computed(() => store.conditionsConfig1)
+watch(flowPermission1, (flow) => {
+  if (flow.flag && flow.id === _uid) {
+    emits('update:flowPermission', flow.value)
+  }
+})
+watch(approverConfig1, (approver) => {
+  if (approver.flag && approver.id === _uid) {
+    emits('update:nodeConfig', approver.value)
+  }
+})
+watch(copyerConfig1, (copyer) => {
+  if (copyer.flag && copyer.id === _uid) {
+    emits('update:nodeConfig', copyer.value)
+  }
+})
+watch(conditionsConfig1, (condition) => {
+  if (condition.flag && condition.id === _uid) {
+    emits('update:nodeConfig', condition.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
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.conditionNodes[index].nodeName =
+      props.nodeConfig.conditionNodes[index].nodeName || '条件'
+  } else {
+    isInput.value = false
+    // eslint-disable-next-line vue/no-mutating-props
+    props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
+  }
+}
+const delNode = () => {
+  emits('update:nodeConfig', props.nodeConfig.childNode)
+}
+const addTerm = () => {
+  let len = props.nodeConfig.conditionNodes.length + 1
+  // eslint-disable-next-line vue/no-mutating-props
+  props.nodeConfig.conditionNodes.push({
+    nodeName: '条件' + len,
+    type: 3,
+    priorityLevel: len,
+    conditionList: [],
+    nodeUserList: [],
+    childNode: null
+  })
+  resetConditionNodesErr()
+  emits('update:nodeConfig', props.nodeConfig)
+}
+const delTerm = (index) => {
+  // eslint-disable-next-line vue/no-mutating-props
+  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 {
+        // eslint-disable-next-line vue/no-mutating-props
+        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 { type } = props.nodeConfig
+  if (type == 0) {
+    setPromoter(true)
+    setFlowPermission({
+      value: props.flowPermission,
+      flag: false,
+      id: _uid
+    })
+  } else if (type == 1) {
+    setApprover(true)
+    setApproverConfig({
+      value: {
+        ...JSON.parse(JSON.stringify(props.nodeConfig)),
+        ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
+      },
+      flag: false,
+      id: _uid
+    })
+  } else if (type == 2) {
+    setCopyer(true)
+    setCopyerConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      flag: false,
+      id: _uid
+    })
+  } else {
+    setCondition(true)
+    setConditionsConfig({
+      value: JSON.parse(JSON.stringify(props.nodeConfig)),
+      priorityLevel,
+      flag: false,
+      id: _uid
+    })
+  }
+}
+const arrTransfer = (index, type = 1) => {
+  //向左-1,向右1
+  // eslint-disable-next-line vue/no-mutating-props
+  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>
diff --git a/src/components/SimpleProcessDesigner/src/util.ts b/src/components/SimpleProcessDesigner/src/util.ts
new file mode 100644
index 00000000..f4acd76c
--- /dev/null
+++ b/src/components/SimpleProcessDesigner/src/util.ts
@@ -0,0 +1,165 @@
+/**
+ * todo
+ */
+export const arrToStr = (arr?: [{ name: string }]) => {
+  if (arr) {
+    return arr
+      .map((item) => {
+        return item.name
+      })
+      .toString()
+  }
+}
+
+export const setApproverStr = (nodeConfig: any) => {
+  if (nodeConfig.settype == 1) {
+    if (nodeConfig.nodeUserList.length == 1) {
+      return nodeConfig.nodeUserList[0].name
+    } else if (nodeConfig.nodeUserList.length > 1) {
+      if (nodeConfig.examineMode == 1) {
+        return arrToStr(nodeConfig.nodeUserList)
+      } else if (nodeConfig.examineMode == 2) {
+        return nodeConfig.nodeUserList.length + '人会签'
+      }
+    }
+  } else if (nodeConfig.settype == 2) {
+    const 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.nodeUserList.length > 0) {
+        if (nodeConfig.selectRange == 2) {
+          return '发起人自选'
+        } else {
+          return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
+        }
+      } else {
+        return ''
+      }
+    }
+  } else if (nodeConfig.settype == 5) {
+    return '发起人自己'
+  } else if (nodeConfig.settype == 7) {
+    return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
+  }
+}
+
+export const copyerStr = (nodeConfig: any) => {
+  if (nodeConfig.nodeUserList.length != 0) {
+    return arrToStr(nodeConfig.nodeUserList)
+  } else {
+    if (nodeConfig.ccSelfSelectFlag == 1) {
+      return '发起人自选'
+    }
+  }
+}
+export const conditionStr = (nodeConfig, index) => {
+  const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
+  if (conditionList.length == 0) {
+    return index == nodeConfig.conditionNodes.length - 1 &&
+      nodeConfig.conditionNodes[0].conditionList.length != 0
+      ? '其他条件进入此流程'
+      : '请设置条件'
+  } else {
+    let str = ''
+    for (let i = 0; i < conditionList.length; i++) {
+      const {
+        columnId,
+        columnType,
+        showType,
+        showName,
+        optType,
+        zdy1,
+        opt1,
+        zdy2,
+        opt2,
+        fixedDownBoxValue
+      } = conditionList[i]
+      if (columnId == 0) {
+        if (nodeUserList.length != 0) {
+          str += '发起人属于:'
+          str +=
+            nodeUserList
+              .map((item) => {
+                return item.name
+              })
+              .join('或') + ' 并且 '
+        }
+      }
+      if (columnType == 'String' && showType == '3') {
+        if (zdy1) {
+          str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
+        }
+      }
+      if (columnType == 'Double') {
+        if (optType != 6 && zdy1) {
+          const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
+          str += `${showName} ${optTypeStr} ${zdy1} 并且 `
+        } else if (optType == 6 && zdy1 && zdy2) {
+          str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
+        }
+      }
+    }
+    return str ? str.substring(0, str.length - 4) : '请设置条件'
+  }
+}
+
+export const dealStr = (str: string, obj) => {
+  const arr = []
+  const list = str.split(',')
+  for (const elem in obj) {
+    list.map((item) => {
+      if (item == elem) {
+        arr.push(obj[elem].value)
+      }
+    })
+  }
+  return arr.join('或')
+}
+
+export const removeEle = (arr, elem, key = 'id') => {
+  let includesIndex
+  arr.map((item, index) => {
+    if (item[key] == elem[key]) {
+      includesIndex = index
+    }
+  })
+  arr.splice(includesIndex, 1)
+}
+
+export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
+export const placeholderList = ['发起人', '审核人', '抄送人']
+export const setTypes = [
+  { value: 1, label: '指定成员' },
+  { value: 2, label: '主管' },
+  { value: 4, label: '发起人自选' },
+  { value: 5, label: '发起人自己' },
+  { value: 7, label: '连续多级主管' }
+]
+
+export const selectModes = [
+  { value: 1, label: '选一个人' },
+  { value: 2, label: '选多个人' }
+]
+
+export const selectRanges = [
+  { value: 1, label: '全公司' },
+  { value: 2, label: '指定成员' },
+  { value: 3, label: '指定角色' }
+]
+
+export const optTypes = [
+  { value: '1', label: '小于' },
+  { value: '2', label: '大于' },
+  { value: '3', label: '小于等于' },
+  { value: '4', label: '等于' },
+  { value: '5', label: '大于等于' },
+  { value: '6', label: '介于两个数之间' }
+]
diff --git a/src/components/SimpleProcessDesigner/theme/workflow.css b/src/components/SimpleProcessDesigner/theme/workflow.css
new file mode 100644
index 00000000..888b1a82
--- /dev/null
+++ b/src/components/SimpleProcessDesigner/theme/workflow.css
@@ -0,0 +1,1292 @@
+
+.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
+}
+
+.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: fixed;
+    -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 {
+    width: 100%;
+    background-color: #f5f5f7;
+    overflow: auto;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0
+}
+
+.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
+}
+
+
diff --git a/src/store/modules/simpleWorkflow.ts b/src/store/modules/simpleWorkflow.ts
new file mode 100644
index 00000000..cf98538d
--- /dev/null
+++ b/src/store/modules/simpleWorkflow.ts
@@ -0,0 +1,55 @@
+import { store } from '../index'
+import { defineStore } from 'pinia'
+
+export const useWorkFlowStore = defineStore('simpleWorkflow', {
+  state: () => ({
+    tableId: '',
+    isTried: false,
+    promoterDrawer: false,
+    flowPermission1: {},
+    approverDrawer: false,
+    approverConfig1: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    conditionDrawer: false,
+    conditionsConfig1: {
+      conditionNodes: []
+    }
+  }),
+  actions: {
+    setTableId(payload) {
+      this.tableId = payload
+    },
+    setIsTried(payload) {
+      this.isTried = payload
+    },
+    setPromoter(payload) {
+      this.promoterDrawer = payload
+    },
+    setFlowPermission(payload) {
+      this.flowPermission1 = payload
+    },
+    setApprover(payload) {
+      this.approverDrawer = payload
+    },
+    setApproverConfig(payload) {
+      this.approverConfig1 = payload
+    },
+    setCopyer(payload) {
+      this.copyerDrawer = payload
+    },
+    setCopyerConfig(payload) {
+      this.copyerConfig1 = payload
+    },
+    setCondition(payload) {
+      this.conditionDrawer = payload
+    },
+    setConditionsConfig(payload) {
+      this.conditionsConfig1 = payload
+    }
+  }
+})
+
+export const useWorkFlowStoreWithOut = () => {
+  return useWorkFlowStore(store)
+}

From 0d4b6f6344052b131b474c6212c8427f26d6a4e0 Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Wed, 20 Mar 2024 22:25:24 +0800
Subject: [PATCH 36/49] =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=A8=A1=E5=9E=8B?=
 =?UTF-8?q?=E4=B8=AD=E5=A2=9E=E5=8A=A0=E4=BB=BF=E9=92=89=E9=92=89=E8=AE=BE?=
 =?UTF-8?q?=E8=AE=A1=E5=99=A8=E5=85=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/router/modules/remaining.ts        | 12 ++++++++++
 src/views/bpm/model/index.vue          | 17 ++++++++++++++
 src/views/bpm/simpleWorkflow/index.vue | 31 ++++++++++++++++++++++++++
 3 files changed, 60 insertions(+)
 create mode 100644 src/views/bpm/simpleWorkflow/index.vue

diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index ec61e971..309a8e49 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -266,6 +266,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/bpm/manager/model'
         }
       },
+      {
+        path: '/manager/simple/workflow/model/edit',
+        component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
+        name: 'SimpleWorkflowDesignEditor',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '仿钉钉设计流程',
+          activeMenu: '/bpm/manager/model'
+        }
+      },
       {
         path: '/manager/definition',
         component: () => import('@/views/bpm/definition/index.vue'),
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index 9b0eec5e..47d24ea9 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -157,6 +157,14 @@
           >
             设计流程
           </el-button>
+          <el-button
+            link
+            type="primary"
+            @click="handleSimpleDesign(scope.row.id)"
+            v-hasPermi="['bpm:model:update']"
+          >
+            仿钉钉设计流程
+          </el-button>
           <el-button
             link
             type="primary"
@@ -323,6 +331,15 @@ const handleDesign = (row) => {
   })
 }
 
+const handleSimpleDesign = (row) => {
+  push({
+    name: 'SimpleWorkflowDesignEditor',
+    query: {
+      modelId: row.id
+    }
+  })
+}
+
 /** 发布流程 */
 const handleDeploy = async (row) => {
   try {
diff --git a/src/views/bpm/simpleWorkflow/index.vue b/src/views/bpm/simpleWorkflow/index.vue
new file mode 100644
index 00000000..7873da7f
--- /dev/null
+++ b/src/views/bpm/simpleWorkflow/index.vue
@@ -0,0 +1,31 @@
+<template>
+  <div>
+    <section class="dingflow-design">
+      <div class="box-scale">
+        <nodeWrap v-model:nodeConfig="nodeConfig" />
+        <div class="end-node">
+          <div class="end-node-circle"></div>
+          <div class="end-node-text">流程结束</div>
+        </div>
+      </div>
+    </section>
+  </div>
+  <approverDrawer :directorMaxLevel="directorMaxLevel" />
+</template>
+<script lang="ts" setup>
+import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
+import approverDrawer from '@/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue'
+defineOptions({ name: 'SimpleWorkflowDesignEditor' })
+let nodeConfig = ref({
+  nodeName: '发起人',
+  type: 0,
+  id: 'root',
+  formPerms: {},
+  nodeUserList: [],
+  childNode: {}
+})
+let directorMaxLevel = ref(0)
+</script>
+<style>
+@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
+</style>
\ No newline at end of file

From 05b408d10760e0d6d2a6e8e0ecdce012d32d9cbb Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 21 Mar 2024 00:34:10 +0800
Subject: [PATCH 37/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=E6=B5=81?=
 =?UTF-8?q?=E7=A8=8B=E7=9A=84=E9=87=8D=E6=96=B0=E5=8F=91=E8=B5=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/router/modules/remaining.ts               | 16 +++----
 .../bpm/processInstance/create/index.vue      | 42 +++++++++++++++----
 src/views/bpm/processInstance/index.vue       | 10 +++--
 3 files changed, 49 insertions(+), 19 deletions(-)

diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index 309a8e49..bc62a3c4 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -243,7 +243,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: '/manager/form/edit',
+        path: 'manager/form/edit',
         component: () => import('@/views/bpm/form/editor/index.vue'),
         name: 'BpmFormEditor',
         meta: {
@@ -255,7 +255,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/model/edit',
+        path: 'manager/model/edit',
         component: () => import('@/views/bpm/model/editor/index.vue'),
         name: 'BpmModelEditor',
         meta: {
@@ -267,7 +267,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/simple/workflow/model/edit',
+        path: 'manager/simple/workflow/model/edit',
         component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
         name: 'SimpleWorkflowDesignEditor',
         meta: {
@@ -279,7 +279,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/definition',
+        path: 'manager/definition',
         component: () => import('@/views/bpm/definition/index.vue'),
         name: 'BpmProcessDefinition',
         meta: {
@@ -291,7 +291,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/process-instance/detail',
+        path: 'process-instance/detail',
         component: () => import('@/views/bpm/processInstance/detail/index.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {
@@ -299,11 +299,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           title: '流程详情',
-          activeMenu: 'bpm/processInstance/detail'
+          activeMenu: '/bpm/task/my'
         }
       },
       {
-        path: '/bpm/oa/leave/create',
+        path: 'oa/leave/create',
         component: () => import('@/views/bpm/oa/leave/create.vue'),
         name: 'OALeaveCreate',
         meta: {
@@ -315,7 +315,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/bpm/oa/leave/detail',
+        path: 'oa/leave/detail',
         component: () => import('@/views/bpm/oa/leave/detail.vue'),
         name: 'OALeaveDetail',
         meta: {
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
index 2cbfe9c4..bd782fef 100644
--- a/src/views/bpm/processInstance/create/index.vue
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -51,6 +51,7 @@
         <form-create
           :rule="detailForm.rule"
           v-model:api="fApi"
+          v-model="detailForm.value"
           :option="detailForm.option"
           @submit="submitForm"
         />
@@ -67,12 +68,16 @@ import { setConfAndFields2 } from '@/utils/formCreate'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import { CategoryApi } from '@/api/bpm/category'
+import { useTagsViewStore } from '@/store/modules/tagsView'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
-const router = useRouter() // 路由
+const route = useRoute() // 路由
+const { push, currentRoute } = useRouter() // 路由
 const message = useMessage() // 消息
+const { delView } = useTagsViewStore() // 视图操作
 
+const processInstanceId = route.query.processInstanceId
 const loading = ref(true) // 加载中
 const categoryList = ref([]) // 分类的列表
 const categoryActive = ref('') // 选中的分类
@@ -91,6 +96,23 @@ const getList = async () => {
     processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
       suspensionState: 1
     })
+
+    // 如果 processInstanceId 非空,说明是重新发起
+    if (processInstanceId?.length > 0) {
+      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+      if (!processInstance) {
+        message.error('重新发起流程失败,原因:流程实例不存在')
+        return
+      }
+      const processDefinition = processDefinitionList.value.find(
+        (item) => item.key == processInstance.processDefinition?.key
+      )
+      if (!processDefinition) {
+        message.error('重新发起流程失败,原因:流程定义不存在')
+        return
+      }
+      await handleSelect(processDefinition, processInstance.formVariables)
+    }
   } finally {
     loading.value = false
   }
@@ -105,26 +127,26 @@ const categoryProcessDefinitionList = computed(() => {
 const bpmnXML = ref(null) // BPMN 数据
 const fApi = ref<ApiAttrs>()
 const detailForm = ref({
-  // 流程表单详情
   rule: [],
-  option: {}
-})
+  option: {},
+  value: {}
+}) // 流程表单详情
 const selectProcessDefinition = ref() // 选择的流程定义
 
 /** 处理选择流程的按钮操作 **/
-const handleSelect = async (row) => {
+const handleSelect = async (row, formVariables) => {
   // 设置选择的流程
   selectProcessDefinition.value = row
 
   // 情况一:流程表单
   if (row.formType == 10) {
     // 设置表单
-    setConfAndFields2(detailForm, row.formConf, row.formFields)
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
     // 加载流程图
     bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
     // 情况二:业务表单
   } else if (row.formCustomCreatePath) {
-    await router.push({
+    await push({
       path: row.formCustomCreatePath
     })
     // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
@@ -145,7 +167,11 @@ const submitForm = async (formData) => {
     })
     // 提示
     message.success('发起流程成功')
-    router.go(-1)
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstance'
+    })
   } finally {
     fApi.value.btn.loading(false)
   }
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 1b3e8484..504a4801 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -76,7 +76,7 @@
           type="primary"
           plain
           v-hasPermi="['bpm:process-instance:query']"
-          @click="handleCreate"
+          @click="handleCreate()"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 发起流程
         </el-button>
@@ -135,6 +135,9 @@
           >
             取消
           </el-button>
+          <el-button link type="primary" v-else @click="handleCreate(scope.row.id)">
+            重新发起
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -200,9 +203,10 @@ const resetQuery = () => {
 }
 
 /** 发起流程操作 **/
-const handleCreate = () => {
+const handleCreate = (id) => {
   router.push({
-    name: 'BpmProcessInstanceCreate'
+    name: 'BpmProcessInstanceCreate',
+    query: { processInstanceId: id }
   })
 }
 

From ee12e691be770dd125c114709ded94d6da1b0748 Mon Sep 17 00:00:00 2001
From: jason <2667446@qq.com>
Date: Thu, 21 Mar 2024 21:11:52 +0800
Subject: [PATCH 38/49] =?UTF-8?q?=E6=9A=82=E6=97=B6=E5=8E=BB=E6=8E=89?=
 =?UTF-8?q?=E5=8E=9F=E6=9C=89=E8=AE=BE=E8=AE=A1=E5=99=A8=E7=9A=84=E5=AE=A1?=
 =?UTF-8?q?=E6=89=B9=E4=BA=BA=E8=AE=BE=E7=BD=AE,=20=E9=81=BF=E5=85=8D?=
 =?UTF-8?q?=E7=BC=BA=E5=B0=91=E8=B5=84=E6=BA=90=E6=8A=A5=E9=94=99=E3=80=82?=
 =?UTF-8?q?=E5=90=8E=E7=BB=AD=E9=9C=80=E8=A6=81=E6=94=B9=E9=80=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/drawer/approverDrawer.vue             | 283 ------------------
 .../SimpleProcessDesigner/src/nodeWrap.vue    |   1 -
 src/views/bpm/simpleWorkflow/index.vue        |   3 -
 3 files changed, 287 deletions(-)
 delete mode 100644 src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue

diff --git a/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue b/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue
deleted file mode 100644
index 867c16ff..00000000
--- a/src/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue
+++ /dev/null
@@ -1,283 +0,0 @@
-<template>
-  <el-drawer
-    :append-to-body="true"
-    title="审批人设置"
-    v-model="visible"
-    class="set_promoter"
-    :show-close="false"
-    :size="550"
-    :before-close="saveApprover"
-  >
-    <div class="demo-drawer__content">
-      <div class="drawer_content">
-        <div class="approver_content">
-          <el-radio-group v-model="approverConfig.settype" class="clear" @change="changeType">
-            <el-radio v-for="{ value, label } in setTypes" :key="value" :label="value">{{
-              label
-            }}</el-radio>
-          </el-radio-group>
-          <el-button type="primary" @click="addApprover" v-if="approverConfig.settype == 1"
-            >添加/修改成员</el-button
-          >
-          <p class="selected_list" v-if="approverConfig.settype == 1">
-            <span v-for="(item, index) in approverConfig.nodeUserList" :key="index"
-              >{{ item.name }}
-              <img
-                src="@/assets/images/add-close1.png"
-                @click="removeEle(approverConfig.nodeUserList, item, 'targetId')"
-              />
-            </span>
-            <a
-              v-if="approverConfig.nodeUserList.length != 0"
-              @click="approverConfig.nodeUserList = []"
-              >清除</a
-            >
-          </p>
-        </div>
-        <div class="approver_manager" v-if="approverConfig.settype == 2">
-          <p>
-            <span>发起人的:</span>
-            <select v-model="approverConfig.directorLevel">
-              <option v-for="item in directorMaxLevel" :value="item" :key="item"
-                >{{ item == 1 ? '直接' : '第' + item + '级' }}主管</option
-              >
-            </select>
-          </p>
-          <p class="tip">找不到主管时,由上级主管代审批</p>
-        </div>
-        <div class="approver_self" v-if="approverConfig.settype == 5">
-          <p>该审批节点设置“发起人自己”后,审批人默认为发起人</p>
-        </div>
-        <div class="approver_self_select" v-show="approverConfig.settype == 4">
-          <el-radio-group v-model="approverConfig.selectMode" style="width: 100%">
-            <el-radio v-for="{ value, label } in selectModes" :label="value" :key="value">{{
-              label
-            }}</el-radio>
-          </el-radio-group>
-          <h3>选择范围</h3>
-          <el-radio-group
-            v-model="approverConfig.selectRange"
-            style="width: 100%"
-            @change="changeRange"
-          >
-            <el-radio v-for="{ value, label } in selectRanges" :label="value" :key="value">{{
-              label
-            }}</el-radio>
-          </el-radio-group>
-          <template v-if="approverConfig.selectRange == 2 || approverConfig.selectRange == 3">
-            <el-button type="primary" @click="addApprover" v-if="approverConfig.selectRange == 2"
-              >添加/修改成员</el-button
-            >
-            <el-button type="primary" @click="addRoleApprover" v-else>添加/修改角色</el-button>
-            <p class="selected_list">
-              <span v-for="(item, index) in approverConfig.nodeUserList" :key="index"
-                >{{ item.name }}
-                <img
-                  src="@/assets/images/add-close1.png"
-                  @click="removeEle(approverConfig.nodeUserList, item, 'targetId')"
-                />
-              </span>
-              <a
-                v-if="approverConfig.nodeUserList.length != 0 && approverConfig.selectRange != 1"
-                @click="approverConfig.nodeUserList = []"
-                >清除</a
-              >
-            </p>
-          </template>
-        </div>
-        <div class="approver_manager" v-if="approverConfig.settype == 7">
-          <p>审批终点</p>
-          <p style="padding-bottom: 20px">
-            <span>发起人的:</span>
-            <select v-model="approverConfig.examineEndDirectorLevel">
-              <option v-for="item in directorMaxLevel" :value="item" :key="item"
-                >{{ item == 1 ? '最高' : '第' + item }}层级主管</option
-              >
-            </select>
-          </p>
-        </div>
-        <div
-          class="approver_some"
-          v-if="
-            (approverConfig.settype == 1 && approverConfig.nodeUserList.length > 1) ||
-            approverConfig.settype == 2 ||
-            (approverConfig.settype == 4 && approverConfig.selectMode == 2)
-          "
-        >
-          <p>多人审批时采用的审批方式</p>
-          <el-radio-group v-model="approverConfig.examineMode" class="clear">
-            <el-radio :label="1">依次审批</el-radio>
-            <br />
-            <el-radio :label="2" v-if="approverConfig.settype != 2"
-              >会签(须所有审批人同意)</el-radio
-            >
-          </el-radio-group>
-        </div>
-        <div
-          class="approver_some"
-          v-if="approverConfig.settype == 2 || approverConfig.settype == 7"
-        >
-          <p>审批人为空时</p>
-          <el-radio-group v-model="approverConfig.noHanderAction" class="clear">
-            <el-radio :label="1">自动审批通过/不允许发起</el-radio>
-            <br />
-            <el-radio :label="2">转交给审核管理员</el-radio>
-          </el-radio-group>
-        </div>
-      </div>
-      <div class="demo-drawer__footer clear">
-        <el-button type="primary" @click="saveApprover">确 定</el-button>
-        <el-button @click="closeDrawer">取 消</el-button>
-      </div>
-    </div>
-  </el-drawer>
-</template>
-<script lang="ts" setup>
-import { ref, watch, computed } from 'vue'
-import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
-import { setTypes, selectModes, selectRanges } from '../util'
-import { removeEle, setApproverStr } from '../util'
-let props = defineProps({
-  directorMaxLevel: {
-    type: Number,
-    default: 0
-  }
-})
-let approverConfig = ref({})
-let approverVisible = ref(false)
-let approverRoleVisible = ref(false)
-let checkedRoleList = ref([])
-let checkedList = ref([])
-let store = useWorkFlowStoreWithOut()
-let { setApproverConfig, setApprover } = store
-let approverConfig1 = computed(() => store.approverConfig1)
-let approverDrawer = computed(() => store.approverDrawer)
-let visible = computed({
-  get() {
-    return approverDrawer.value
-  },
-  set() {
-    closeDrawer()
-  }
-})
-watch(approverConfig1, (val: any) => {
-  approverConfig.value = val.value
-})
-let changeRange = () => {
-  approverConfig.value.nodeUserList = []
-}
-const changeType = (val) => {
-  approverConfig.value.nodeUserList = []
-  approverConfig.value.examineMode = 1
-  approverConfig.value.noHanderAction = 2
-  if (val == 2) {
-    approverConfig.value.directorLevel = 1
-  } else if (val == 4) {
-    approverConfig.value.selectMode = 1
-    approverConfig.value.selectRange = 1
-  } else if (val == 7) {
-    approverConfig.value.examineEndDirectorLevel = 1
-  }
-}
-const addApprover = () => {
-  approverVisible.value = true
-  checkedList.value = approverConfig.value.nodeUserList
-}
-const addRoleApprover = () => {
-  approverRoleVisible.value = true
-  checkedRoleList.value = approverConfig.value.nodeUserList
-}
-const sureApprover = (data) => {
-  approverConfig.value.nodeUserList = data
-  approverVisible.value = false
-}
-const sureRoleApprover = (data) => {
-  approverConfig.value.nodeUserList = data
-  approverRoleVisible.value = false
-}
-const saveApprover = () => {
-  approverConfig.value.error = !setApproverStr(approverConfig.value)
-  setApproverConfig({
-    value: approverConfig.value,
-    flag: true,
-    id: approverConfig1.value.id
-  })
-  closeDrawer()
-}
-const closeDrawer = () => {
-  setApprover(false)
-}
-</script>
-<style lang="scss" scoped>
-.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;
-  }
-
-  .approver_self_select h3 {
-    margin: 5px 0 20px;
-    font-size: 14px;
-    font-weight: bold;
-    line-height: 19px;
-  }
-}
-</style>
diff --git a/src/components/SimpleProcessDesigner/src/nodeWrap.vue b/src/components/SimpleProcessDesigner/src/nodeWrap.vue
index 9081becd..3c9d5eb1 100644
--- a/src/components/SimpleProcessDesigner/src/nodeWrap.vue
+++ b/src/components/SimpleProcessDesigner/src/nodeWrap.vue
@@ -55,7 +55,6 @@
                     class="ant-input editable-title-input"
                     @blur="blurEvent(index)"
                     @focus="$event.currentTarget.select()"
-                    v-focus
                     v-model="item.nodeName"
                   />
                   <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
diff --git a/src/views/bpm/simpleWorkflow/index.vue b/src/views/bpm/simpleWorkflow/index.vue
index 7873da7f..144615e0 100644
--- a/src/views/bpm/simpleWorkflow/index.vue
+++ b/src/views/bpm/simpleWorkflow/index.vue
@@ -10,11 +10,9 @@
       </div>
     </section>
   </div>
-  <approverDrawer :directorMaxLevel="directorMaxLevel" />
 </template>
 <script lang="ts" setup>
 import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
-import approverDrawer from '@/components/SimpleProcessDesigner/src/drawer/approverDrawer.vue'
 defineOptions({ name: 'SimpleWorkflowDesignEditor' })
 let nodeConfig = ref({
   nodeName: '发起人',
@@ -24,7 +22,6 @@ let nodeConfig = ref({
   nodeUserList: [],
   childNode: {}
 })
-let directorMaxLevel = ref(0)
 </script>
 <style>
 @import url('@/components/SimpleProcessDesigner/theme/workflow.css');

From 5286ad1cd6945bd55ab01c21bf81e46da8e81108 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 22 Mar 2024 08:26:26 +0800
Subject: [PATCH 39/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=E3=80=90?=
 =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=AE=9E=E4=BE=8B=E3=80=91=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=85=A8=E9=83=A8=E6=B5=81=E7=A8=8B?=
 =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E7=9A=84=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/processInstance/index.ts          |  18 +-
 src/views/bpm/oa/leave/index.vue              |   2 +-
 src/views/bpm/processInstance/index.vue       |  47 ++--
 .../bpm/processInstance/manager/index.vue     | 255 ++++++++++++++++++
 4 files changed, 300 insertions(+), 22 deletions(-)
 create mode 100644 src/views/bpm/processInstance/manager/index.vue

diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts
index d5d0c05c..81640625 100644
--- a/src/api/bpm/processInstance/index.ts
+++ b/src/api/bpm/processInstance/index.ts
@@ -31,20 +31,32 @@ export type ProcessInstanceCopyVO = {
   reason: string
 }
 
-export const getMyProcessInstancePage = async (params) => {
+export const getProcessInstanceMyPage = async (params: any) => {
   return await request.get({ url: '/bpm/process-instance/my-page', params })
 }
 
+export const getProcessInstanceManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/manager-page', params })
+}
+
 export const createProcessInstance = async (data) => {
   return await request.post({ url: '/bpm/process-instance/create', data: data })
 }
 
-export const cancelProcessInstance = async (id: number, reason: string) => {
+export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => {
   const data = {
     id: id,
     reason: reason
   }
-  return await request.delete({ url: '/bpm/process-instance/cancel', data: data })
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data })
+}
+
+export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => {
+  const data = {
+    id: id,
+    reason: reason
+  }
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data })
 }
 
 export const getProcessInstance = async (id: string) => {
diff --git a/src/views/bpm/oa/leave/index.vue b/src/views/bpm/oa/leave/index.vue
index 4af7ad3c..fe96a498 100644
--- a/src/views/bpm/oa/leave/index.vue
+++ b/src/views/bpm/oa/leave/index.vue
@@ -226,7 +226,7 @@ const cancelLeave = async (row) => {
     inputErrorMessage: '取消原因不能为空'
   })
   // 发起取消
-  await ProcessInstanceApi.cancelProcessInstance(row.id, value)
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
   message.success('取消成功')
   // 刷新列表
   await getList()
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 504a4801..950f34f0 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -58,7 +58,7 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="提交时间" prop="createTime">
+      <el-form-item label="发起时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -87,23 +87,21 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程编号" align="center" prop="id" width="300px" />
-      <el-table-column label="流程名称" align="center" prop="name" />
-      <el-table-column label="流程分类" align="center" prop="categoryName" />
-      <el-table-column label="当前审批任务" align="center" prop="tasks">
-        <template #default="scope">
-          <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
-            <span>{{ task.name }}</span>
-          </el-button>
-        </template>
-      </el-table-column>
-      <el-table-column label="流程" prop="status">
+      <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" />
+      <el-table-column
+        label="流程分类"
+        align="center"
+        prop="categoryName"
+        min-width="100"
+        fixed="left"
+      />
+      <el-table-column label="流程状态" prop="status" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column
-        label="提交时间"
+        label="发起时间"
         align="center"
         prop="startTime"
         width="180"
@@ -116,7 +114,20 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+        <template #default="scope">
+          {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="当前审批任务" align="center" prop="tasks" min-width="120px">
+        <template #default="scope">
+          <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
+            <span>{{ task.name }}</span>
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
+      <el-table-column label="操作" align="center" fixed="right" width="180">
         <template #default="scope">
           <el-button
             link
@@ -152,12 +163,12 @@
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
 import { ElMessageBox } from 'element-plus'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { CategoryApi } from '@/api/bpm/category'
 
-defineOptions({ name: 'BpmProcessInstance' })
+defineOptions({ name: 'BpmProcessInstanceMy' })
 
 const router = useRouter() // 路由
 const message = useMessage() // 消息弹窗
@@ -182,7 +193,7 @@ const categoryList = ref([]) // 流程分类列表
 const getList = async () => {
   loading.value = true
   try {
-    const data = await ProcessInstanceApi.getMyProcessInstancePage(queryParams)
+    const data = await ProcessInstanceApi.getProcessInstanceMyPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -230,7 +241,7 @@ const handleCancel = async (row) => {
     inputErrorMessage: '取消原因不能为空'
   })
   // 发起取消
-  await ProcessInstanceApi.cancelProcessInstance(row.id, value)
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
   message.success('取消成功')
   // 刷新列表
   await getList()
diff --git a/src/views/bpm/processInstance/manager/index.vue b/src/views/bpm/processInstance/manager/index.vue
new file mode 100644
index 00000000..34cf6d17
--- /dev/null
+++ b/src/views/bpm/processInstance/manager/index.vue
@@ -0,0 +1,255 @@
+<template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="发起人" prop="startUserId">
+        <el-select v-model="queryParams.startUserId" placeholder="请选择发起人" class="!w-240px">
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入流程名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="所属流程" prop="processDefinitionId">
+        <el-input
+          v-model="queryParams.processDefinitionId"
+          placeholder="请输入流程定义的编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="流程分类" prop="category">
+        <el-select
+          v-model="queryParams.category"
+          placeholder="请选择流程分类"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="category in categoryList"
+            :key="category.code"
+            :label="category.name"
+            :value="category.code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择流程状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发起时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="流程名称" align="center" prop="name" min-width="200px" fixed="left" />
+      <el-table-column
+        label="流程分类"
+        align="center"
+        prop="categoryName"
+        min-width="100"
+        fixed="left"
+      />
+      <el-table-column label="流程发起人" align="center" prop="startUser.nickname" width="120" />
+      <el-table-column label="发起部门" align="center" prop="startUser.deptName" width="120" />
+      <el-table-column label="流程状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="发起时间"
+        align="center"
+        prop="startTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+        <template #default="scope">
+          {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="当前审批任务" align="center" prop="tasks" min-width="120px">
+        <template #default="scope">
+          <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
+            <span>{{ task.name }}</span>
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
+      <el-table-column label="操作" align="center" fixed="right" width="180">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            v-hasPermi="['bpm:process-instance:cancel']"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            link
+            type="primary"
+            v-if="scope.row.status === 1"
+            v-hasPermi="['bpm:process-instance:query']"
+            @click="handleCancel(scope.row)"
+          >
+            取消
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { ElMessageBox } from 'element-plus'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import { cancelProcessInstanceByAdmin } from '@/api/bpm/processInstance'
+
+// 它是【我的流程】的差异是,该菜单可以看全部的流程实例
+defineOptions({ name: 'BpmProcessInstanceManager' })
+
+const router = useRouter() // 路由
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  startUserId: undefined,
+  name: '',
+  processDefinitionId: undefined,
+  category: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
+const userList = ref<any[]>([]) // 用户列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessInstanceApi.getProcessInstanceManagerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 查看详情 */
+const handleDetail = (row) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消按钮操作 */
+const handleCancel = async (row) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 激活时 **/
+onActivated(() => {
+  getList()
+})
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  userList.value = await UserApi.getSimpleUserList()
+})
+</script>

From 48f66247374ed182ec3b08eefdb293a2bdda089b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 22 Mar 2024 09:07:04 +0800
Subject: [PATCH 40/49] =?UTF-8?q?BPM=EF=BC=9A=E6=96=B0=E5=A2=9E=E3=80=90?=
 =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=BB=BB=E5=8A=A1=E3=80=91=E8=8F=9C=E5=8D=95?=
 =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=85=A8=E9=83=A8=E6=B5=81=E7=A8=8B?=
 =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=9A=84=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/task/index.ts                     |   8 +-
 src/views/bpm/processInstance/index.vue       |   2 +-
 .../bpm/processInstance/manager/index.vue     |   4 +-
 src/views/bpm/task/done/index.vue             |   4 +-
 src/views/bpm/task/manager/index.vue          | 166 ++++++++++++++++++
 src/views/bpm/task/todo/index.vue             |   2 +-
 6 files changed, 178 insertions(+), 8 deletions(-)
 create mode 100644 src/views/bpm/task/manager/index.vue

diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts
index 6592542d..f3cda9f7 100644
--- a/src/api/bpm/task/index.ts
+++ b/src/api/bpm/task/index.ts
@@ -4,14 +4,18 @@ export type TaskVO = {
   id: number
 }
 
-export const getTodoTaskPage = async (params: any) => {
+export const getTaskTodoPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/todo-page', params })
 }
 
-export const getDoneTaskPage = async (params: any) => {
+export const getTaskDonePage = async (params: any) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
+export const getTaskManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/manager-page', params })
+}
+
 export const approveTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 950f34f0..5d72437f 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -114,7 +114,7 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
         <template #default="scope">
           {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
         </template>
diff --git a/src/views/bpm/processInstance/manager/index.vue b/src/views/bpm/processInstance/manager/index.vue
index 34cf6d17..ab8da9c9 100644
--- a/src/views/bpm/processInstance/manager/index.vue
+++ b/src/views/bpm/processInstance/manager/index.vue
@@ -114,7 +114,7 @@
         width="180"
         :formatter="dateFormatter"
       />
-      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="169">
         <template #default="scope">
           {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
         </template>
@@ -167,7 +167,7 @@ import { CategoryApi } from '@/api/bpm/category'
 import * as UserApi from '@/api/system/user'
 import { cancelProcessInstanceByAdmin } from '@/api/bpm/processInstance'
 
-// 它是【我的流程】的差异是,该菜单可以看全部的流程实例
+// 它和【我的流程】的差异是,该菜单可以看全部的流程实例
 defineOptions({ name: 'BpmProcessInstanceManager' })
 
 const router = useRouter() // 路由
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index ed922397..f73b47c3 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -81,7 +81,7 @@
         </template>
       </el-table-column>
       <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
-      <el-table-column align="center" label="耗时" prop="durationInMillis" width="120">
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
         <template #default="scope">
           {{ formatPast2(scope.row.durationInMillis) }}
         </template>
@@ -127,7 +127,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await TaskApi.getDoneTaskPage(queryParams)
+    const data = await TaskApi.getTaskDonePage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
diff --git a/src/views/bpm/task/manager/index.vue b/src/views/bpm/task/manager/index.vue
new file mode 100644
index 00000000..688e5150
--- /dev/null
+++ b/src/views/bpm/task/manager/index.vue
@@ -0,0 +1,166 @@
+<template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="任务名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入任务名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="流程" prop="processInstance.name" width="180" />
+      <el-table-column
+        align="center"
+        label="发起人"
+        prop="processInstance.startUser.nickname"
+        width="100"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="发起时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="当前任务" prop="name" width="180" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务开始时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="任务结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="审批人" prop="assigneeUser.nickname" width="100" />
+      <el-table-column align="center" label="审批状态" prop="status" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="审批建议" prop="reason" min-width="180" />
+      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+        <template #default="scope">
+          {{ formatPast2(scope.row.durationInMillis) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="流程编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="任务编号" prop="id" :show-overflow-tooltip="true" />
+      <el-table-column align="center" label="操作" fixed="right" width="80">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleAudit(scope.row)">历史</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+
+// 它和【待办任务】【已办任务】的差异是,该菜单可以看全部的流程任务
+defineOptions({ name: 'BpmManagerTask' })
+
+const { push } = useRouter() // 路由
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询任务列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await TaskApi.getTaskManagerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 处理审批按钮 */
+const handleAudit = (row: any) => {
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstance.id
+    }
+  })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue
index 43d29921..fc506815 100644
--- a/src/views/bpm/task/todo/index.vue
+++ b/src/views/bpm/task/todo/index.vue
@@ -109,7 +109,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    const data = await TaskApi.getTodoTaskPage(queryParams)
+    const data = await TaskApi.getTaskTodoPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {

From 728cf15c45174c9a6f980d1faab48d4f25564fa8 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 23 Mar 2024 00:54:33 +0800
Subject: [PATCH 41/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=8A=A0=E3=80=8C?=
 =?UTF-8?q?=E5=8F=91=E8=B5=B7=E4=BA=BA=E8=87=AA=E9=80=89=E3=80=8D=E7=9A=84?=
 =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=AE=A1=E6=89=B9=E4=BA=BA=E7=9A=84=E5=88=86?=
 =?UTF-8?q?=E9=85=8D=E7=AD=96=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/definition/index.ts               |  5 +-
 .../penal/task/task-components/UserTask.vue   |  6 +-
 src/views/bpm/definition/index.vue            |  8 +-
 src/views/bpm/oa/leave/create.vue             | 82 +++++++++++++++++-
 .../bpm/processInstance/create/index.vue      | 85 +++++++++++++++++--
 .../detail/ProcessInstanceBpmnViewer.vue      | 17 ++--
 .../bpm/processInstance/detail/index.vue      |  8 +-
 7 files changed, 182 insertions(+), 29 deletions(-)

diff --git a/src/api/bpm/definition/index.ts b/src/api/bpm/definition/index.ts
index c0e51fab..cb6d4271 100644
--- a/src/api/bpm/definition/index.ts
+++ b/src/api/bpm/definition/index.ts
@@ -1,8 +1,9 @@
 import request from '@/config/axios'
 
-export const getProcessDefinitionBpmnXML = async (id: number) => {
+export const getProcessDefinition = async (id: number, key: string) => {
   return await request.get({
-    url: '/bpm/process-definition/get-bpmn-xml?id=' + id
+    url: '/bpm/process-definition/get',
+    params: { id, key }
   })
 }
 
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index 6431eca1..166fb409 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -65,11 +65,7 @@
       </el-select>
     </el-form-item>
     <el-form-item
-      v-if="
-        userTaskForm.candidateStrategy == 30 ||
-        userTaskForm.candidateStrategy == 31 ||
-        userTaskForm.candidateStrategy == 32
-      "
+      v-if="userTaskForm.candidateStrategy == 30"
       label="指定用户"
       prop="candidateParam"
       span="24"
diff --git a/src/views/bpm/definition/index.vue b/src/views/bpm/definition/index.vue
index 9ebd28b1..1e7794b3 100644
--- a/src/views/bpm/definition/index.vue
+++ b/src/views/bpm/definition/index.vue
@@ -72,8 +72,8 @@
   <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
     <MyProcessViewer
       key="designer"
-      v-model="bpmnXML"
-      :value="bpmnXML as any"
+      v-model="bpmnXml"
+      :value="bpmnXml as any"
       v-bind="bpmnControlForm"
       :prefix="bpmnControlForm.prefix"
     />
@@ -133,12 +133,12 @@ const handleFormDetail = async (row) => {
 
 /** 流程图的详情按钮操作 */
 const bpmnDetailVisible = ref(false)
-const bpmnXML = ref(null)
+const bpmnXml = ref(null)
 const bpmnControlForm = ref({
   prefix: 'flowable'
 })
 const handleBpmnDetail = async (row) => {
-  bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
+  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
   bpmnDetailVisible.value = true
 }
 
diff --git a/src/views/bpm/oa/leave/create.vue b/src/views/bpm/oa/leave/create.vue
index a22392f9..28a15af7 100644
--- a/src/views/bpm/oa/leave/create.vue
+++ b/src/views/bpm/oa/leave/create.vue
@@ -37,6 +37,36 @@
     <el-form-item label="原因" prop="reason">
       <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
     </el-form-item>
+    <el-col v-if="startUserSelectTasks.length > 0">
+      <el-card class="mb-10px">
+        <template #header>指定审批人</template>
+        <el-form
+          :model="startUserSelectAssignees"
+          :rules="startUserSelectAssigneesFormRules"
+          ref="startUserSelectAssigneesFormRef"
+        >
+          <el-form-item
+            v-for="userTask in startUserSelectTasks"
+            :key="userTask.id"
+            :label="`任务【${userTask.name}】`"
+            :prop="userTask.id"
+          >
+            <el-select
+              v-model="startUserSelectAssignees[userTask.id]"
+              multiple
+              placeholder="请选择审批人"
+            >
+              <el-option
+                v-for="user in userList"
+                :key="user.id"
+                :label="user.nickname"
+                :value="user.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </el-col>
     <el-form-item>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
     </el-form-item>
@@ -46,10 +76,15 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as LeaveApi from '@/api/bpm/leave'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmOALeaveCreate' })
 
 const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { push, currentRoute } = useRouter() // 路由
+
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formData = ref({
   type: undefined,
@@ -64,18 +99,34 @@ const formRules = reactive({
   endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
-const { delView } = useTagsViewStore() // 视图操作
-const { push, currentRoute } = useRouter() // 路由
+
+// 指定审批人
+const processDefineKey = 'oa_leave' // 流程定义 Key
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
 /** 提交表单 */
 const submitForm = async () => {
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+  // 校验指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as LeaveApi.LeaveVO
+    const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
+    // 设置指定审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      data.startUserSelectAssignees = startUserSelectAssignees.value
+    }
     await LeaveApi.createLeave(data)
     message.success('发起成功')
     // 关闭当前 Tab
@@ -85,4 +136,29 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
+
+/** 初始化 */
+onMounted(async () => {
+  const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+    undefined,
+    processDefineKey
+  )
+  if (!processDefinitionDetail) {
+    message.error('OA 请假的流程模型未配置,请检查!')
+    return
+  }
+  startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+  // 设置指定审批人
+  if (startUserSelectTasks.value?.length > 0) {
+    // 设置校验规则
+    for (const userTask of startUserSelectTasks.value) {
+      startUserSelectAssignees.value[userTask.id] = []
+      startUserSelectAssigneesFormRules.value[userTask.id] = [
+        { required: true, message: '请选择审批人', trigger: 'blur' }
+      ]
+    }
+    // 加载用户列表
+    userList.value = await UserApi.getSimpleUserList()
+  }
+})
 </script>
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
index bd782fef..99bcdb06 100644
--- a/src/views/bpm/processInstance/create/index.vue
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -54,7 +54,40 @@
           v-model="detailForm.value"
           :option="detailForm.option"
           @submit="submitForm"
-        />
+        >
+          <template #type-startUserSelect>
+            <el-col :span="24">
+              <el-card class="mb-10px">
+                <template #header>指定审批人</template>
+                <el-form
+                  :model="startUserSelectAssignees"
+                  :rules="startUserSelectAssigneesFormRules"
+                  ref="startUserSelectAssigneesFormRef"
+                >
+                  <el-form-item
+                    v-for="userTask in startUserSelectTasks"
+                    :key="userTask.id"
+                    :label="`任务【${userTask.name}】`"
+                    :prop="userTask.id"
+                  >
+                    <el-select
+                      v-model="startUserSelectAssignees[userTask.id]"
+                      multiple
+                      placeholder="请选择审批人"
+                    >
+                      <el-option
+                        v-for="user in userList"
+                        :key="user.id"
+                        :label="user.nickname"
+                        :value="user.id"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+          </template>
+        </form-create>
       </el-col>
     </el-card>
     <!-- 流程图预览 -->
@@ -69,6 +102,7 @@ import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import { CategoryApi } from '@/api/bpm/category'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
@@ -124,7 +158,6 @@ const categoryProcessDefinitionList = computed(() => {
 })
 
 // ========== 表单相关 ==========
-const bpmnXML = ref(null) // BPMN 数据
 const fApi = ref<ApiAttrs>()
 const detailForm = ref({
   rule: [],
@@ -133,17 +166,53 @@ const detailForm = ref({
 }) // 流程表单详情
 const selectProcessDefinition = ref() // 选择的流程定义
 
+// 指定审批人
+const bpmnXML = ref(null) // BPMN 数据
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
 /** 处理选择流程的按钮操作 **/
 const handleSelect = async (row, formVariables) => {
   // 设置选择的流程
   selectProcessDefinition.value = row
 
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+  startUserSelectAssigneesFormRules.value = {}
+
   // 情况一:流程表单
   if (row.formType == 10) {
     // 设置表单
     setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
     // 加载流程图
-    bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+      // 设置指定审批人
+      if (startUserSelectTasks.value?.length > 0) {
+        detailForm.value.rule.push({
+          type: 'startUserSelect',
+          props: {
+            title: '指定审批人'
+          }
+        })
+        // 设置校验规则
+        for (const userTask of startUserSelectTasks.value) {
+          startUserSelectAssignees.value[userTask.id] = []
+          startUserSelectAssigneesFormRules.value[userTask.id] = [
+            { required: true, message: '请选择审批人', trigger: 'blur' }
+          ]
+        }
+        // 加载用户列表
+        userList.value = await UserApi.getSimpleUserList()
+      }
+    }
     // 情况二:业务表单
   } else if (row.formCustomCreatePath) {
     await push({
@@ -158,19 +227,25 @@ const submitForm = async (formData) => {
   if (!fApi.value || !selectProcessDefinition.value) {
     return
   }
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
   // 提交请求
   fApi.value.btn.loading(true)
   try {
     await ProcessInstanceApi.createProcessInstance({
       processDefinitionId: selectProcessDefinition.value.id,
-      variables: formData
+      variables: formData,
+      startUserSelectAssignees: startUserSelectAssignees.value
     })
     // 提示
     message.success('发起流程成功')
     // 跳转回去
     delView(unref(currentRoute))
     await push({
-      name: 'BpmProcessInstance'
+      name: 'BpmProcessInstanceMy'
     })
   } finally {
     fApi.value.btn.loading(false)
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
index dcf3bcc4..8912593a 100644
--- a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
@@ -34,14 +34,17 @@ const bpmnControlForm = ref({
 })
 const activityList = ref([]) // 任务列表
 
-/** 初始化 */
-onMounted(async () => {
-  if (props.id) {
-    activityList.value = await ActivityApi.getActivityList({
-      processInstanceId: props.id
-    })
+/** 只有 loading 完成时,才去加载流程列表 */
+watch(
+  () => props.loading,
+  async (value) => {
+    if (value && props.id) {
+      activityList.value = await ActivityApi.getActivityList({
+        processInstanceId: props.id
+      })
+    }
   }
-})
+)
 </script>
 <style>
 .box-card {
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index 2297fae0..ef260dee 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -115,7 +115,7 @@
     <!-- 高亮流程图 -->
     <ProcessInstanceBpmnViewer
       :id="`${id}`"
-      :bpmn-xml="bpmnXML"
+      :bpmn-xml="bpmnXml"
       :loading="processInstanceLoading"
       :process-instance="processInstance"
       :tasks="tasks"
@@ -158,7 +158,7 @@ const userId = useUserStore().getUser.id // 当前登录的编号
 const id = query.id as unknown as string // 流程实例的编号
 const processInstanceLoading = ref(false) // 流程实例的加载中
 const processInstance = ref<any>({}) // 流程实例
-const bpmnXML = ref('') // BPMN XML
+const bpmnXml = ref('') // BPMN XML
 const tasksLoad = ref(true) // 任务的加载中
 const tasks = ref<any[]>([]) // 任务列表
 // ========== 审批信息 ==========
@@ -290,7 +290,9 @@ const getProcessInstance = async () => {
     }
 
     // 加载流程图
-    bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number)
+    bpmnXml.value = (
+      await DefinitionApi.getProcessDefinition(processDefinition.id as number)
+    )?.bpmnXml
   } finally {
     processInstanceLoading.value = false
   }

From faf4557783a6ef3731893f843f48def258022d6b Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 23 Mar 2024 15:58:38 +0800
Subject: [PATCH 42/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=BB=BB?=
 =?UTF-8?q?=E5=8A=A1=E7=9B=91=E5=90=AC=E5=99=A8=E3=80=81=E6=89=A7=E8=A1=8C?=
 =?UTF-8?q?=E7=9B=91=E5=90=AC=E5=99=A8=E7=9A=84=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/processListener/index.ts          |  40 ++++
 .../package/penal/base/ElementBaseInfo.vue    |  30 +--
 src/utils/dict.ts                             |   4 +-
 .../processListener/ProcessListenerForm.vue   | 162 ++++++++++++++++
 src/views/bpm/processListener/index.vue       | 183 ++++++++++++++++++
 5 files changed, 407 insertions(+), 12 deletions(-)
 create mode 100644 src/api/bpm/processListener/index.ts
 create mode 100644 src/views/bpm/processListener/ProcessListenerForm.vue
 create mode 100644 src/views/bpm/processListener/index.vue

diff --git a/src/api/bpm/processListener/index.ts b/src/api/bpm/processListener/index.ts
new file mode 100644
index 00000000..dabaa476
--- /dev/null
+++ b/src/api/bpm/processListener/index.ts
@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+// BPM 流程监听器 VO
+export interface ProcessListenerVO {
+  id: number // 编号
+  name: string // 监听器名字
+  type: string // 监听器类型
+  status: number // 监听器状态
+  event: string // 监听事件
+  valueType: string // 监听器值类型
+  value: string // 监听器值
+}
+
+// BPM 流程监听器 API
+export const ProcessListenerApi = {
+  // 查询流程监听器分页
+  getProcessListenerPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-listener/page`, params })
+  },
+
+  // 查询流程监听器详情
+  getProcessListener: async (id: number) => {
+    return await request.get({ url: `/bpm/process-listener/get?id=` + id })
+  },
+
+  // 新增流程监听器
+  createProcessListener: async (data: ProcessListenerVO) => {
+    return await request.post({ url: `/bpm/process-listener/create`, data })
+  },
+
+  // 修改流程监听器
+  updateProcessListener: async (data: ProcessListenerVO) => {
+    return await request.put({ url: `/bpm/process-listener/update`, data })
+  },
+
+  // 删除流程监听器
+  deleteProcessListener: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-listener/delete?id=` + id })
+  }
+}
diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 5e77c948..60ee56ed 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -139,17 +139,25 @@ const updateBaseInfo = (key) => {
   }
 }
 
-watch(
-  () => props.businessObject,
-  (val) => {
-    // console.log(val, 'val11111111111111111111')
-    if (val) {
-      // nextTick(() => {
-      resetBaseInfo()
-      // })
-    }
-  }
-)
+onMounted(() => {
+  // 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
+  setTimeout(() => {
+    handleKeyUpdate(props.model.key)
+    handleNameUpdate(props.model.name)
+  }, 1)
+})
+
+// watch(
+//   () => props.businessObject,
+//   (val) => {
+//     // console.log(val, 'val11111111111111111111')
+//     if (val) {
+//       // nextTick(() => {
+//       resetBaseInfo()
+//       // })
+//     }
+//   }
+// )
 
 watch(
   () => props.model?.key,
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
index f7d337cb..2284ff13 100644
--- a/src/utils/dict.ts
+++ b/src/utils/dict.ts
@@ -141,6 +141,8 @@ export enum DICT_TYPE {
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
   BPM_TASK_STATUS = 'bpm_task_status',
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+  BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
+  BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
 
   // ========== PAY 模块 ==========
   PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
@@ -155,7 +157,7 @@ export enum DICT_TYPE {
   MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
   MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
 
-  // ========== MALL - 会员模块 ==========
+  // ========== Member 会员模块 ==========
   MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
   MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
 
diff --git a/src/views/bpm/processListener/ProcessListenerForm.vue b/src/views/bpm/processListener/ProcessListenerForm.vue
new file mode 100644
index 00000000..8d4e9796
--- /dev/null
+++ b/src/views/bpm/processListener/ProcessListenerForm.vue
@@ -0,0 +1,162 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="110px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="类型" prop="type">
+        <el-select
+          v-model="formData.type"
+          placeholder="请选择类型"
+          @change="formData.event = undefined"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="事件" prop="event">
+        <el-select v-model="formData.event" placeholder="请选择事件">
+          <el-option
+            v-for="event in formData.type == 'execution'
+              ? ['start', 'end']
+              : ['create', 'assignment', 'complete', 'delete', 'update', 'timeout']"
+            :label="event"
+            :value="event"
+            :key="event"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="值类型" prop="valueType">
+        <el-select v-model="formData.valueType" placeholder="请选择值类型">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="类路径" prop="value" v-if="formData.type == 'class'">
+        <el-input v-model="formData.value" placeholder="请输入类路径" />
+      </el-form-item>
+      <el-form-item label="表达式" prop="value" v-else>
+        <el-input v-model="formData.value" placeholder="请输入表达式" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessListenerForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  type: undefined,
+  status: undefined,
+  event: undefined,
+  valueType: undefined,
+  value: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  event: [{ required: true, message: '监听事件不能为空', trigger: 'blur' }],
+  valueType: [{ required: true, message: '值类型不能为空', trigger: 'change' }],
+  value: [{ required: true, message: '值不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProcessListenerApi.getProcessListener(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProcessListenerVO
+    if (formType.value === 'create') {
+      await ProcessListenerApi.createProcessListener(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProcessListenerApi.updateProcessListener(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    type: undefined,
+    status: CommonStatusEnum.ENABLE,
+    event: undefined,
+    valueType: undefined,
+    value: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/processListener/index.vue b/src/views/bpm/processListener/index.vue
new file mode 100644
index 00000000..83e998c9
--- /dev/null
+++ b/src/views/bpm/processListener/index.vue
@@ -0,0 +1,183 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:process-listener:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="类型" align="center" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="事件" align="center" prop="event" />
+      <el-table-column label="值类型" align="center" prop="valueType">
+        <template #default="scope">
+          <dict-tag
+            :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+            :value="scope.row.valueType"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="值" align="center" prop="value" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:process-listener:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:process-listener:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProcessListenerForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import ProcessListenerForm from './ProcessListenerForm.vue'
+
+/** BPM 流程 列表 */
+defineOptions({ name: 'BpmProcessListener' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessListenerVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  type: undefined,
+  event: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProcessListenerApi.deleteProcessListener(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

From 7218e718849834ff551ba50eea559cc974213f97 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 23 Mar 2024 19:23:19 +0800
Subject: [PATCH 43/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=BB=BB?=
 =?UTF-8?q?=E5=8A=A1=E7=9B=91=E5=90=AC=E5=99=A8=E3=80=81=E6=89=A7=E8=A1=8C?=
 =?UTF-8?q?=E7=9B=91=E5=90=AC=E5=99=A8=E7=9A=84=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../package/penal/base/ElementBaseInfo.vue    | 30 +++----
 .../penal/listeners/ElementListeners.vue      | 47 +++++++++-
 .../penal/listeners/ProcessListenerDialog.vue | 88 +++++++++++++++++++
 .../penal/listeners/UserTaskListeners.vue     | 42 ++++++++-
 .../package/penal/listeners/utilSelf.ts       | 27 ++++++
 .../bpmnProcessDesigner/package/utils.ts      |  1 +
 6 files changed, 214 insertions(+), 21 deletions(-)
 create mode 100644 src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue

diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 60ee56ed..5e77c948 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -139,25 +139,17 @@ const updateBaseInfo = (key) => {
   }
 }
 
-onMounted(() => {
-  // 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
-  setTimeout(() => {
-    handleKeyUpdate(props.model.key)
-    handleNameUpdate(props.model.name)
-  }, 1)
-})
-
-// watch(
-//   () => props.businessObject,
-//   (val) => {
-//     // console.log(val, 'val11111111111111111111')
-//     if (val) {
-//       // nextTick(() => {
-//       resetBaseInfo()
-//       // })
-//     }
-//   }
-// )
+watch(
+  () => props.businessObject,
+  (val) => {
+    // console.log(val, 'val11111111111111111111')
+    if (val) {
+      // nextTick(() => {
+      resetBaseInfo()
+      // })
+    }
+  }
+)
 
 watch(
   () => props.model?.key,
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
index 45ee8f93..410274b6 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
@@ -26,8 +26,16 @@
         type="primary"
         preIcon="ep:plus"
         title="添加监听器"
+        size="small"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -240,11 +248,21 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerType, initListenerForm, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerType,
+  initListenerForm,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from './ProcessListenerDialog.vue'
 
 defineOptions({ name: 'ElementListeners' })
 
@@ -284,6 +302,7 @@ const resetListenersList = () => {
 }
 // 打开 监听器详情 侧边栏
 const openListenerForm = (listener, index?) => {
+  // debugger
   if (listener) {
     listenerForm.value = initListenerForm(listener)
     editingListenerIndex.value = index
@@ -321,6 +340,7 @@ const openListenerFieldForm = (field, index?) => {
 }
 // 保存监听器注入字段
 const saveListenerFiled = async () => {
+  // debugger
   let validateStatus = await listenerFieldFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   if (editingListenerFieldIndex.value === -1) {
@@ -337,6 +357,7 @@ const saveListenerFiled = async () => {
 }
 // 移除监听器字段
 const removeListenerField = (index) => {
+  // debugger
   ElMessageBox.confirm('确认移除该字段吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -349,6 +370,7 @@ const removeListenerField = (index) => {
 }
 // 移除监听器
 const removeListener = (index) => {
+  debugger
   ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -365,6 +387,7 @@ const removeListener = (index) => {
 }
 // 保存监听器配置
 const saveListenerConfig = async () => {
+  // debugger
   let validateStatus = await listenerFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   const listenerObject = createListenerObject(listenerForm.value, false, prefix)
@@ -389,6 +412,28 @@ const saveListenerConfig = async () => {
   listenerForm.value = {}
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('execution')
+}
+const selectListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, false, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:ExecutionListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
new file mode 100644
index 00000000..7ebc4bb6
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
@@ -0,0 +1,88 @@
+<!-- 执行器选择 -->
+<template>
+  <Dialog title="请选择监听器" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="名字" align="center" prop="name" />
+        <el-table-column label="类型" align="center" prop="type">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" prop="status">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column label="事件" align="center" prop="event" />
+        <el-table-column label="值类型" align="center" prop="valueType">
+          <template #default="scope">
+            <dict-tag
+              :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+              :value="scope.row.valueType"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="值" align="center" prop="value" />
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { DICT_TYPE } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessListenerDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessListenerVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
index 9464883c..7a50032e 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
@@ -39,6 +39,13 @@
         title="添加监听器"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -286,11 +293,22 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerForm, initListenerType, eventType, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerForm,
+  initListenerType,
+  eventType,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue'
 
 defineOptions({ name: 'UserTaskListeners' })
 
@@ -437,6 +455,28 @@ const removeListenerField = (field, index) => {
     .catch(() => console.info('操作取消'))
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('task')
+}
+const selectListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, true, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:TaskListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
index 5f46abd0..b4eb1d27 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
@@ -40,6 +40,33 @@ export function initListenerType(listener) {
   }
 }
 
+/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
+export function initListenerForm2(processListener) {
+  if (processListener.valueType === 'class') {
+    return {
+      listenerType: 'classListener',
+      class: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'expression') {
+    return {
+      listenerType: 'expressionListener',
+      expression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'delegateExpression') {
+    return {
+      listenerType: 'delegateExpressionListener',
+      delegateExpression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  }
+  throw new Error('未知的监听器类型')
+}
+
 export const listenerType = {
   classListener: 'Java 类',
   expressionListener: '表达式',
diff --git a/src/components/bpmnProcessDesigner/package/utils.ts b/src/components/bpmnProcessDesigner/package/utils.ts
index bb6c5d52..8996788b 100644
--- a/src/components/bpmnProcessDesigner/package/utils.ts
+++ b/src/components/bpmnProcessDesigner/package/utils.ts
@@ -2,6 +2,7 @@ import { toRaw } from 'vue'
 const bpmnInstances = () => (window as any)?.bpmnInstances
 // 创建监听器实例
 export function createListenerObject(options, isTask, prefix) {
+  debugger
   const listenerObj = Object.create(null)
   listenerObj.event = options.event
   isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段

From cc14963bc8c9b53ef479dd9ccdd83cd3cb2e89e2 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sat, 23 Mar 2024 21:12:09 +0800
Subject: [PATCH 44/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A2=9E=E5=8A=A0=E6=B5=81?=
 =?UTF-8?q?=E7=A8=8B=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=9A=84=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/bpm/processExpression/index.ts        |  42 ++++
 .../penal/listeners/ElementListeners.vue      |   4 +-
 .../penal/listeners/ProcessListenerDialog.vue |   5 -
 .../penal/listeners/UserTaskListeners.vue     |   4 +-
 .../ProcessExpressionDialog.vue               |  68 +++++++
 .../penal/task/task-components/UserTask.vue   |  21 +-
 .../ProcessExpressionForm.vue                 | 114 +++++++++++
 src/views/bpm/processExpression/index.vue     | 180 ++++++++++++++++++
 8 files changed, 426 insertions(+), 12 deletions(-)
 create mode 100644 src/api/bpm/processExpression/index.ts
 create mode 100644 src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
 create mode 100644 src/views/bpm/processExpression/ProcessExpressionForm.vue
 create mode 100644 src/views/bpm/processExpression/index.vue

diff --git a/src/api/bpm/processExpression/index.ts b/src/api/bpm/processExpression/index.ts
new file mode 100644
index 00000000..af6a7372
--- /dev/null
+++ b/src/api/bpm/processExpression/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// BPM 流程表达式 VO
+export interface ProcessExpressionVO {
+  id: number // 编号
+  name: string // 表达式名字
+  status: number // 表达式状态
+  expression: string // 表达式
+}
+
+// BPM 流程表达式 API
+export const ProcessExpressionApi = {
+  // 查询BPM 流程表达式分页
+  getProcessExpressionPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-expression/page`, params })
+  },
+
+  // 查询BPM 流程表达式详情
+  getProcessExpression: async (id: number) => {
+    return await request.get({ url: `/bpm/process-expression/get?id=` + id })
+  },
+
+  // 新增BPM 流程表达式
+  createProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.post({ url: `/bpm/process-expression/create`, data })
+  },
+
+  // 修改BPM 流程表达式
+  updateProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.put({ url: `/bpm/process-expression/update`, data })
+  },
+
+  // 删除BPM 流程表达式
+  deleteProcessExpression: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-expression/delete?id=` + id })
+  },
+
+  // 导出BPM 流程表达式 Excel
+  exportProcessExpression: async (params) => {
+    return await request.download({ url: `/bpm/process-expression/export-excel`, params })
+  }
+}
\ No newline at end of file
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
index 410274b6..de5445c8 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
@@ -250,7 +250,7 @@
   </div>
 
   <!-- 选择弹窗 -->
-  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectListener" />
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
@@ -417,7 +417,7 @@ const processListenerDialogRef = ref()
 const openProcessListenerDialog = async () => {
   processListenerDialogRef.value.open('execution')
 }
-const selectListener = (listener) => {
+const selectProcessListener = (listener) => {
   const listenerForm = initListenerForm2(listener)
   const listenerObject = createListenerObject(listenerForm, false, prefix)
   bpmnElementListeners.value.push(listenerObject)
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
index 7ebc4bb6..01f81242 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
@@ -9,11 +9,6 @@
             <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
           </template>
         </el-table-column>
-        <el-table-column label="状态" align="center" prop="status">
-          <template #default="scope">
-            <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
-          </template>
-        </el-table-column>
         <el-table-column label="事件" align="center" prop="event" />
         <el-table-column label="值类型" align="center" prop="valueType">
           <template #default="scope">
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
index 7a50032e..76e0c809 100644
--- a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
@@ -295,7 +295,7 @@
   </div>
 
   <!-- 选择弹窗 -->
-  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectListener" />
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
@@ -460,7 +460,7 @@ const processListenerDialogRef = ref()
 const openProcessListenerDialog = async () => {
   processListenerDialogRef.value.open('task')
 }
-const selectListener = (listener) => {
+const selectProcessListener = (listener) => {
   const listenerForm = initListenerForm2(listener)
   const listenerObject = createListenerObject(listenerForm, true, prefix)
   bpmnElementListeners.value.push(listenerObject)
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
new file mode 100644
index 00000000..b478bb2f
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
@@ -0,0 +1,68 @@
+<!-- 表达式选择 -->
+<template>
+  <Dialog title="请选择表达式" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="名字" align="center" prop="name" />
+        <el-table-column label="表达式" align="center" prop="expression" />
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { CommonStatusEnum } from '@/utils/constants'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessExpressionDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
index 166fb409..0dffeb0f 100644
--- a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -5,7 +5,7 @@
         v-model="userTaskForm.candidateStrategy"
         clearable
         style="width: 100%"
-        @change="changecandidateStrategy"
+        @change="changeCandidateStrategy"
       >
         <el-option
           v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
@@ -114,9 +114,14 @@
         type="textarea"
         v-model="userTaskForm.candidateParam[0]"
         clearable
-        style="width: 100%"
+        style="width: 72%"
         @change="updateElementTask"
       />
+      <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog"
+        >选择表达式</el-button
+      >
+      <!-- 选择弹窗 -->
+      <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
     </el-form-item>
   </el-form>
 </template>
@@ -129,6 +134,7 @@ import * as DeptApi from '@/api/system/dept'
 import * as PostApi from '@/api/system/post'
 import * as UserApi from '@/api/system/user'
 import * as UserGroupApi from '@/api/bpm/userGroup'
+import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
 
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
@@ -173,7 +179,7 @@ const resetTaskForm = () => {
 }
 
 /** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
-const changecandidateStrategy = () => {
+const changeCandidateStrategy = () => {
   userTaskForm.value.candidateParam = []
   updateElementTask()
 }
@@ -186,6 +192,15 @@ const updateElementTask = () => {
   })
 }
 
+// 打开监听器弹窗
+const processExpressionDialogRef = ref()
+const openProcessExpressionDialog = async () => {
+  processExpressionDialogRef.value.open()
+}
+const selectProcessExpression = (expression) => {
+  userTaskForm.value.candidateParam = [expression.expression]
+}
+
 watch(
   () => props.id,
   () => {
diff --git a/src/views/bpm/processExpression/ProcessExpressionForm.vue b/src/views/bpm/processExpression/ProcessExpressionForm.vue
new file mode 100644
index 00000000..acf0667c
--- /dev/null
+++ b/src/views/bpm/processExpression/ProcessExpressionForm.vue
@@ -0,0 +1,114 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="表达式" prop="expression">
+        <el-input type="textarea" v-model="formData.expression" placeholder="请输入表达式" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessExpressionForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  status: undefined,
+  expression: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
+  expression: [{ required: true, message: '表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ProcessExpressionApi.getProcessExpression(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProcessExpressionVO
+    if (formType.value === 'create') {
+      await ProcessExpressionApi.createProcessExpression(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProcessExpressionApi.updateProcessExpression(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: CommonStatusEnum.ENABLE,
+    expression: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/processExpression/index.vue b/src/views/bpm/processExpression/index.vue
new file mode 100644
index 00000000..194a4d85
--- /dev/null
+++ b/src/views/bpm/processExpression/index.vue
@@ -0,0 +1,180 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['bpm:process-expression:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="表达式" align="center" prop="expression" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['bpm:process-expression:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['bpm:process-expression:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ProcessExpressionForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import ProcessExpressionForm from './ProcessExpressionForm.vue'
+
+/** BPM 流程表达式列表 */
+defineOptions({ name: 'BpmProcessExpression' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ProcessExpressionApi.deleteProcessExpression(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

From eed7bb2e1a34fab596521e8b5f25f6fac0d299b8 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 24 Mar 2024 10:08:59 +0800
Subject: [PATCH 45/49] =?UTF-8?q?BPM=EF=BC=9A=E6=B5=81=E7=A8=8B=E6=A8=A1?=
 =?UTF-8?q?=E5=9E=8B=E7=9A=84=20icon=20=E7=BB=B4=E6=8A=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/bpm/model/ModelForm.vue              | 8 +++++++-
 src/views/bpm/model/index.vue                  | 5 +++++
 src/views/bpm/processInstance/create/index.vue | 6 +-----
 3 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/src/views/bpm/model/ModelForm.vue b/src/views/bpm/model/ModelForm.vue
index 0e5b0521..ce60edca 100644
--- a/src/views/bpm/model/ModelForm.vue
+++ b/src/views/bpm/model/ModelForm.vue
@@ -50,6 +50,9 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item v-if="formData.id" label="流程图标" prop="icon">
+        <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" />
+      </el-form-item>
       <el-form-item label="流程描述" prop="description">
         <el-input v-model="formData.description" clearable type="textarea" />
       </el-form-item>
@@ -141,15 +144,17 @@ const formData = ref({
   formType: 10,
   name: '',
   category: undefined,
+  icon: undefined,
   description: '',
   formId: '',
   formCustomCreatePath: '',
   formCustomViewPath: ''
 })
 const formRules = reactive({
-  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
   key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
+  icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }],
   value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
   visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
 })
@@ -223,6 +228,7 @@ const resetForm = () => {
     formType: 10,
     name: '',
     category: undefined,
+    icon: '',
     description: '',
     formId: '',
     formCustomCreatePath: '',
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index 47d24ea9..d616f2c0 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -72,6 +72,11 @@
           </el-button>
         </template>
       </el-table-column>
+      <el-table-column label="流程图标" align="center" prop="icon" width="100">
+        <template #default="scope">
+          <el-image :src="scope.row.icon" class="w-32px h-32px" />
+        </template>
+      </el-table-column>
       <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
       <el-table-column label="表单信息" align="center" prop="formType" width="200">
         <template #default="scope">
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
index 99bcdb06..d9fee5d2 100644
--- a/src/views/bpm/processInstance/create/index.vue
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -23,11 +23,7 @@
             >
               <template #default>
                 <div class="flex">
-                  <!-- TODO 芋艿:流程图,增加 icon -->
-                  <el-image
-                    src="http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png"
-                    class="w-32px h-32px"
-                  />
+                  <el-image :src="definition.icon" class="w-32px h-32px" />
                   <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
                 </div>
               </template>

From 624594bb5c6f2ae3d576001c982e7f52497f8c0a Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 24 Mar 2024 10:46:37 +0800
Subject: [PATCH 46/49] =?UTF-8?q?BPM=EF=BC=9A=E5=A4=84=E7=90=86=E6=89=93?=
 =?UTF-8?q?=E5=BC=80=20bpmn=20=E8=AE=BE=E8=AE=A1=E5=99=A8=E6=97=B6?=
 =?UTF-8?q?=EF=BC=8Ckey=20=E5=92=8C=20name=20=E8=A6=86=E7=9B=96=E6=9B=B4?=
 =?UTF-8?q?=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../package/penal/base/ElementBaseInfo.vue                | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 5e77c948..5ad2ff4b 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -139,6 +139,14 @@ const updateBaseInfo = (key) => {
   }
 }
 
+onMounted(() => {
+  // 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
+  setTimeout(() => {
+    handleKeyUpdate(props.model.key)
+    handleNameUpdate(props.model.name)
+  }, 110)
+})
+
 watch(
   () => props.businessObject,
   (val) => {

From 607c2b6c4a77f568db36cca55e2eb68f05dc086e Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 27 Mar 2024 09:41:00 +0800
Subject: [PATCH 47/49] =?UTF-8?q?BPM=EF=BC=9A=E5=AE=8C=E5=96=84=E6=96=87?=
 =?UTF-8?q?=E6=A1=A3=E8=AF=B4=E6=98=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/bpm/category/index.vue               | 2 ++
 src/views/bpm/form/index.vue                   | 2 +-
 src/views/bpm/model/index.vue                  | 8 +++++++-
 src/views/bpm/oa/leave/index.vue               | 2 +-
 src/views/bpm/processExpression/index.vue      | 2 ++
 src/views/bpm/processInstance/create/index.vue | 2 ++
 src/views/bpm/processInstance/index.vue        | 2 +-
 src/views/bpm/processListener/index.vue        | 2 ++
 src/views/bpm/task/copy/index.vue              | 5 +++++
 src/views/bpm/task/done/index.vue              | 8 +++++++-
 src/views/bpm/task/todo/index.vue              | 8 +++++++-
 11 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/src/views/bpm/category/index.vue b/src/views/bpm/category/index.vue
index 0e11e819..46fa6cf1 100644
--- a/src/views/bpm/category/index.vue
+++ b/src/views/bpm/category/index.vue
@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue
index 4cf37777..3d542c80 100644
--- a/src/views/bpm/form/index.vue
+++ b/src/views/bpm/form/index.vue
@@ -1,5 +1,5 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index d616f2c0..e4ba6d4c 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -1,5 +1,11 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
+  <doc-alert
+    title="流程设计器(钉钉、飞书)"
+    url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
+  />
+  <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
+  <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->
diff --git a/src/views/bpm/oa/leave/index.vue b/src/views/bpm/oa/leave/index.vue
index fe96a498..bd41104a 100644
--- a/src/views/bpm/oa/leave/index.vue
+++ b/src/views/bpm/oa/leave/index.vue
@@ -1,5 +1,5 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->
diff --git a/src/views/bpm/processExpression/index.vue b/src/views/bpm/processExpression/index.vue
index 194a4d85..ec2de5ad 100644
--- a/src/views/bpm/processExpression/index.vue
+++ b/src/views/bpm/processExpression/index.vue
@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
index d9fee5d2..cc588881 100644
--- a/src/views/bpm/processInstance/create/index.vue
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
+
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
   <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
     <el-tabs tab-position="left" v-model="categoryActive">
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
index 5d72437f..7ca07f90 100644
--- a/src/views/bpm/processInstance/index.vue
+++ b/src/views/bpm/processInstance/index.vue
@@ -1,5 +1,5 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->
diff --git a/src/views/bpm/processListener/index.vue b/src/views/bpm/processListener/index.vue
index 83e998c9..8b5c36e7 100644
--- a/src/views/bpm/processListener/index.vue
+++ b/src/views/bpm/processListener/index.vue
@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
diff --git a/src/views/bpm/task/copy/index.vue b/src/views/bpm/task/copy/index.vue
index dd41b2e1..adc1fe32 100644
--- a/src/views/bpm/task/copy/index.vue
+++ b/src/views/bpm/task/copy/index.vue
@@ -1,5 +1,10 @@
 <!-- 工作流 - 抄送我的流程 -->
 <template>
+  <doc-alert
+    title="审批转办、委派、抄送"
+    url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+  />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px">
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index f73b47c3..a5137199 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -1,5 +1,11 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
+  <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
+  <doc-alert
+    title="审批转办、委派、抄送"
+    url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+  />
+  <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->
diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue
index fc506815..670fc683 100644
--- a/src/views/bpm/task/todo/index.vue
+++ b/src/views/bpm/task/todo/index.vue
@@ -1,5 +1,11 @@
 <template>
-  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+  <doc-alert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
+  <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
+  <doc-alert
+    title="审批转办、委派、抄送"
+    url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+  />
+  <doc-alert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
 
   <ContentWrap>
     <!-- 搜索工作栏 -->

From 9f3bc1ead8ece593018e226408667446a70a2c27 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 27 Mar 2024 18:51:06 +0800
Subject: [PATCH 48/49] =?UTF-8?q?BPM=EF=BC=9A=E4=BF=AE=E5=A4=8D=20bpmnxml?=
 =?UTF-8?q?=20=E4=B8=BA=E7=A9=BA=E6=97=B6=EF=BC=8C=E5=B1=95=E7=A4=BA?=
 =?UTF-8?q?=E7=9A=84=20key=20=E8=A2=AB=E8=A6=86=E7=9B=96=E7=9A=84=E9=97=AE?=
 =?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../package/penal/base/ElementBaseInfo.vue        | 15 ---------------
 src/views/bpm/model/editor/index.vue              | 10 ++++++++++
 2 files changed, 10 insertions(+), 15 deletions(-)

diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
index 5ad2ff4b..70ad4f8b 100644
--- a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -3,13 +3,6 @@
     <el-form label-width="90px" :model="needProps" :rules="rules">
       <div v-if="needProps.type == 'bpmn:Process'">
         <!-- 如果是 Process 信息的时候,使用自定义表单 -->
-        <el-link
-          href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B"
-          type="danger"
-          target="_blank"
-        >
-          如何实现实现会签、或签?
-        </el-link>
         <el-form-item label="流程标识" prop="id">
           <el-input
             v-model="needProps.id"
@@ -139,14 +132,6 @@ const updateBaseInfo = (key) => {
   }
 }
 
-onMounted(() => {
-  // 针对上传的 bpmn 流程图时,需要延迟 1 毫秒的时间,保证 key 和 name 的更新
-  setTimeout(() => {
-    handleKeyUpdate(props.model.key)
-    handleNameUpdate(props.model.name)
-  }, 110)
-})
-
 watch(
   () => props.businessObject,
   (val) => {
diff --git a/src/views/bpm/model/editor/index.vue b/src/views/bpm/model/editor/index.vue
index 0dfabc75..29bca71c 100644
--- a/src/views/bpm/model/editor/index.vue
+++ b/src/views/bpm/model/editor/index.vue
@@ -89,6 +89,16 @@ onMounted(async () => {
   }
   // 查询模型
   const data = await ModelApi.getModel(modelId)
+  if (!data.bpmnXml) {
+    // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的
+    data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
+  <process id="${data.key}" name="${data.name}" isExecutable="true" />
+  <bpmndi:BPMNDiagram id="BPMNDiagram">
+    <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" />
+  </bpmndi:BPMNDiagram>
+</definitions>`
+  }
   model.value = {
     ...data,
     bpmnXml: undefined // 清空 bpmnXml 属性

From b4677c4546280a0b422ce0d0fdede6aac84709b7 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 27 Mar 2024 21:23:21 +0800
Subject: [PATCH 49/49] =?UTF-8?q?README=EF=BC=9A=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=9B=BE=EF=BC=8C=E6=96=B9=E4=BE=BF=E7=90=86?=
 =?UTF-8?q?=E8=A7=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .image/common/bpm-feature.png    | Bin 0 -> 16260 bytes
 .image/common/infra-feature.png  | Bin 0 -> 16920 bytes
 .image/common/system-feature.png | Bin 0 -> 13584 bytes
 README.md                        |   6 ++++++
 4 files changed, 6 insertions(+)
 create mode 100644 .image/common/bpm-feature.png
 create mode 100644 .image/common/infra-feature.png
 create mode 100644 .image/common/system-feature.png

diff --git a/.image/common/bpm-feature.png b/.image/common/bpm-feature.png
new file mode 100644
index 0000000000000000000000000000000000000000..23787fb4f090154f2d914100ca91174c1e60cafc
GIT binary patch
literal 16260
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
K$W_Xkef=LKHG>cU

literal 0
HcmV?d00001

diff --git a/.image/common/infra-feature.png b/.image/common/infra-feature.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5cef50c56ff3a4acccbfbb4f5f3e453dda5e12b
GIT binary patch
literal 16920
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
W#wNR7sLindBg@OENLNXk2LB%-C!B`>

literal 0
HcmV?d00001

diff --git a/.image/common/system-feature.png b/.image/common/system-feature.png
new file mode 100644
index 0000000000000000000000000000000000000000..366087ce0c9061c34e60feaa30af099f99694cdd
GIT binary patch
literal 13584
zcmc(`Ra9I}6fW3UAOsRDK%jA#;O_43?gW>{-2#Ci4GAuR;O^GAcjE+iZCulMf=&K4
z^Dwh!-TN@>&dhl@Rj2CPXMd-5?UG&VeAQ5s$HpYV1ONcoiV8AX000Uc06^l$K>1g4
z>u7QL@4;0=MMw7j&;85Gi$;Ik)%6v8Z+Coh^6=<zdS>S2>|}XmWovuu0`dFo{0z{p
zw6?MS_wk{&t_})=<>cl<+S}(B7D~%X1A_wf_4T`Z`w|lq%PXo<Qd3-9Ts%BHY;A4r
z9qcVEEk;I1zC?csi-=IyHC9(w_wn_i?=k8h=+88I`J-=7mUO+Q(e)A~Y;EsS-_-Oq
zA*t(NDlIdsps2*v)33Ru*&LQ^YUA)ZI?lw@M9<i&a&vI)YA-e}7W_R;PEHPbyx7^@
z*|0kjHdFp>sWHDG-!~{cf4$ptBF|wg>*OBc<ri#Z@kwtG?BMG8>wNv|d=32OB;Z%k
z@4q+mzqhki+FnkNA{Vk|AuojvFE7y2zji+NCyzn{wnEvr+m-`G+f6UuHJ=*WGuF!+
z=O2I9ZL7lEUn&E0M*dn)gq@@<9Iw5+tSw&mAHK|WA6!01cKw+>c>c43FsZnEhK=v}
zjX&j-^ku!Y#=bnprp7Z6!2kemQ$-m`9lw>+#eD1m2?EsV+OuI=+UNolP1@vZ^Y~0*
zj+MIrVvhez^?$z-jUH0j4(_-$-~!r*FQkWc%=qfJ645`joPJ8J#%+SbF&%Em%aZg+
zN@a{3a_}6A5ph2PmqGZi1~|rF&S=c#7B~D*-i)>EevTACO_^6nW!61@+LW-)wp6fK
zDGx9p5Rcokj2YQ~AI+higF0EkWA;D3ZycyrJHip6%n3vc;l43ho88?eV!>U(xi4b}
zrpoXk#jU0B9#gZfpj^ZY(|;K0W1r=K`X$gP)v<0|mCKdXO^nLN-?jGVbyUUjHbv|S
zM0U|Mk0-Wp0S{?<H)Z6n_BLi;Mu&z{6Z~kQA=me$j>i;YE0HqaP<|O{ldw;o)MdD(
z+p+JLCKS@d+1miaiar>h0U5J?M>hp*1Dah-bR;dR(xJxqj7`Oeou)eekharc(oK>A
z6ckG2*$gQR)-}z5>P2edf?wV!Z~PX^fMS!=QqT-uJ+j#_+GM>|iB!Bj0ka&U+xyb7
zaCWzZ%CbT=Sri1M4!}dIamARl`?aqhPubcHQN!X8lv8%{cl4ZnzaFH<XUeQY9!re2
zTJW#^h<(=1E)TVZykCw(ylQeMY6ozB+faW!o#M&G#*!E!rb$akB5<CtB`OOqfDUdL
znv$@qy%9d%V?unH34i*69&mWI|G76!)0d~rK3Pv1_-0$=`-0$F0`wNyy@ncYlFi4d
z7iVkzEkucQ5=z0t)<Nv$OEjEcBJ4v}5?)o%gd*Shj|HL692;VFuN<);j%xrSF>|p?
zCqU(rW_@Sp2oNn|L>l!vO&rqYQ-`U_$|U1xo0R(`94xP&ND==K2iI0n|M0c{XHf^s
zX!C;F)xkW{yXzv9nRPz=Lg)HJzD8lWK1v6c(Za8+CbBc*$CwHuWyv!`Ow6_fG}$C>
z#Cd`=8#<$EGJheU9yXJn-x=kZ3H9O789$KG(x03e`kOSrTQnBS#w?QT>w0KSzMxz=
zOt$gII!6IL`e^yU4?C5glW-qT(GBgt5vJO7J1smg>aQ7N={NET>a+_`x{m=(__Fco
zfv!XTWIVZXB~#gho^auvdWabR^S-6eChoWEqrVwn=c?ng)u-;(=qqq}ZFVlCH}DZR
z*CCsJ5Ndw5owzICKlQ}h=Q942)dHNSWpoaIJ=%U}+}HA*4_O6dnX1d^ryu{Y{#gVM
zRA`Te8p}0;=^Fs%KAPV~Iw0gc8x1BQ>7?qa#>e^0iNmSTxGSM{a&sE}yGi}lM857G
za`Q~)^?+;#WFU4quUyq^HdM6QF9+-vf*!mCF&2Z##3b-*{v^+_g11|?|L^81<-N?f
zuP~Q((epBm4spq;zHLlAQp+mK05HT2d&&)N3K7awOkPKmxM{L$^Rn_z&aXG|Y^cj<
zw;kx^5c!O+zV{=yW#2DM$kw0eJ5_Z|OLnPMp~R0J?({YplD)lx2F?%jv1((#C!d@b
zL`YPTj@|fytd<=9><w2xReRnLE=yqcDya4ZU&tI+!@t}|liYpLml;f<YHPcpci{8o
z;#GU^D`EVfA4-S%q`iYvxO4`3SM$A}V&YLQ@;o*rzoxbmE!EF1wG4J#daeWEKe(p2
z%2Foml4!wM6%$q=i!IDk4TJ|#U3%RnK4PunZYCf3`^}>!PQwjoIwS_`o5*oSl$hr=
zaE_khuW%1z=!b+Cp6Woj5M0Q~f_mSV15fWnVfBXpn(Hm(eD+*roy--IP`Wo*K^~l-
z54EyIk0tpm!%=bd*Q@<wu#w=NouTp2&Zuip4<dT~RrsLh-5O-fjTwg6>2$YoCQsbW
z6LS)AxH*(l#H<9<Q`k4GtovWAm3i0J<EaVPKld(ixwckz-TLp9nZ_mlRt;dT(eueg
zuSkb>-H|!W(ZWD;_&5IzH15MAt9ujt=XKiCWS-ejbfi9(01dcC)w|FL$jlael8Iz+
zMu4H9GVEOn&)!Qf!9kR@%lcaPzV&^=;+bZ#mQu6J=AR%`zAI<*iJf&G`A1y27Rc<v
zLbIYY3!0p+F~wcsgz2C2DuKp}({<HYWKNv0X^pM$HOR3nG4d=H56aA)c$^=AXh*&2
zxJx*QHlOl+t8*cH;O5K2(oUP&fp_R5@!cI)&@o%lO+aOCce*5;0QbW8_{lde&;i(8
zjF|cfijAq@QNz_tg)&iB%m~QJ=#rGi9C23}g0Yo|HRoo2M%q3X1;kGOjIkaWo(jdT
zFpz8J|I_l!v$gj&`W0;nXjm1VlntfgPmp#ciC*J*+vWiDm=)OlpHx8VjM3Y$2RLwK
zZ52|^`s?_*UJN^!+ZClr6eBqio3CxG#J1M#fx`*V@c&DKAiq&4d*h_}$)%#kPWU?I
z;pi3RVVb!9`fVDxT6LZ&;9|BrJHzz1&WR|g8y%T(T!r-Y#{498i6OhAy$M>v>^%hu
z(K;~U!CoX1D$6eq7bAI>51O8sh9p9z?M=KqdCm(FIRD>4T5G?9-Qn4XU6p6DSP9}f
z?5it!wJhowK{`%1?fSXzeMP!kFJ|4Gy3FVWCa}f=9OG?cIY(KWV?t($$8-wkg!4`}
zJ@>@5w^R`%Y5t#N%^1>nWZa&d<~njuqq{s$K-m}rzdJ9P)C%IyK(u4NnsQcxf;BE9
zX;DFavTTlHYag3Tf2I&(e*E}x@A@JWG3Y&$CZ&ttP?y+Z?KVHKYbyQ_%nfR&O*jd~
z!7t<c{?-qhLHGo}T|lCFcDy~>KAoq*8tVO>(=ta%jK+_QLFi<$kZhT*;K!#0(UraU
zQwZpXjmj@JV||u<?n}J%b&!zoL{(sn4-<{Bs|mx0Sw=d|Z20iFx%kQ0Qnp3SKQZAL
zpAR&`9ww##D#7aRHuSYwH@0SA-^3%H#*KW3bk2qz@INxiK703-nGW|XH9+w{!)nur
z9^BbfxIXyhjeX-7@v=GWMm$C_+f*=w#QZA@tQL6D#tKfh^F0?4hOx9YfyINouJ)zl
zdm-RZkF#4?o&^@P$1iWKEA;NQoP`K%#4h4Ze6Js;=+pK_SE(N_!}0we_6zUFXU8%2
zQg~wbUyaLo)qeo6`NKZh2tNh<EZ=J_?(UrELb)6A@a2X3=>XVsf3ptW+?ex-MPdBF
zUP96RC~>(b+KGhvQUO0`-8gCc9G=Ni_w6r0<<Hn*J`RK&@Np#()KF)#|0~)3arIqb
zx2Sw#)@|?L-BZ*jUmO7KEI<dkVLjuuioq9G4}7`)k$X|o^UMqk$&=!o_WqHih#qe7
zA@TS=@#60Bnm91&K;txq^|anr1GOWEI+PYhvF6u$z>S3y-D|?=)f>-fW|8HvB!9qT
znt(rhM{)fa6ppkCFg>LNaH2DExX=Tvhz|3pSkJ5lYX~U(z8u{90~g=NzQ^J543n*o
zn4)AP4m11;h9oFW7Pc#BMdw7o`Ne_vX%>`aN$yc`sya5t{N`^+m|C(WH5g%^Ssex3
z8oc+SZWX|xPm{gAs3>w1{vWpJt_^mN^9x58+Hm=*e?AoQ)mFZBi;vT17E@5;z_}v7
zlZC13f45nelwUeb;#m*?n`6aND5$lP8?cuDA+bP1rB5b4{JCt&NY4-##4V+wg^;}L
z%7~xI86u;*3lBT+-x#JkAhCxkC}=_&n^dUsgFrH{)SaHnkSD?qKy&RKA251b&b!KA
zlEScN4>b%<W$K?CsxJGo+F{2G)0N^{J{Ny)vZKMtR??^mQL32iUho4eSUd)%KKR$Q
zpx_u86Uop&QrbS|51qnmP*2wLWj^77xbe$=Bgpn7;=L8kshA;YL%?1tJ4Tib7VLt#
z!}sZLFxKE(-6po-mcsG&&nNryo<`_A4o4YamZ8vxD&aNCz|pi<!)dB%9}9c4(rq&h
z6At`OB3OlEA;S0F^V=Q+R-`Xy)_*0JyG|A_EPE@NB9>Z?4TNy2itj!E`gxCae-7cv
zn(Dp5e7!vj7kpNE|2tZyEyQjLL~{;&cj+qReCBKL173w8Q<pR5_W{6iiScvMH~<sO
z(%Q40dR2?zQ=~dV>XxY#450^>PR~`NNPYs4@XuTC*tVf@Byy8dI#?&}-t$+4!F_xm
zbAB?6{8mD)k#s+V*mkT%l5=w7-F|43hq;@yJ>j2M@(M2d0x`D*nuR?U$UdEuAn$uG
z3jugxRq%e75fl%2vFm!q)YE*LU7DLT`+=EKOzKE4Cf*mz{sA(d`m-{Bdg#uXwU+Bk
ze4wLhHxd48i-)oA(cR*KeIRIZg?*c;4!wPiU}jir=Ahsgc#p5+3-X#~)FT~iT`D?q
z&ne0)M@DhcGR=jkpL+6#Ztu(|Y4v<?9!v9-q#5IGWR<m)&NFT#+~-ZJDJC__ZvjBo
z;)l;+k*c;ioD6Xc#4to+$_hm|<FdG1;-dYbd#gXGR@l7%7$E&25Dar*AyYqg0q9_5
zxHUHC++%>nH66u~G#fU9Q{n74$J}|ZcdCIOJ^6l29;y6-SW&v2-Mjr*>A7v#>=lzV
z*7esM66{CKML7jV_l_c{O(^I}N|nw^DW$0>hT)E6y1JypM=pHXw0NHfc-JjiLfa9s
zfDHF`chhvxK4!}iC$NVORh&TyyZq0ee0WG&JMx=f!Ct@Kqe-!mvGduEQW`X$Fq>ZW
z(=yc8J}Y26ADS3G{jL;+al)O5J}HLNrh?hEMK0!VzM~ktA8Pm#*0j2$Ji10lJc4kN
z`t7txMU{s!AugM^A;T8+6N3=RH@v#T9fJKmb>LYpm82QBg!?rT+RZHE*yZ4l@2x$4
z7o$(tu%sjgsjG+3eF*(m6G1$S$ypv#JFA$WI@UXTwygx>wN{_UFF21#;k)5;KvC>k
zT)*ChFKM6gbh1%k`fbykL!Q)u1h7z?h!^*Ks{m%67jj|ZgI6BddPsgV2u7>{0V3&9
zUV&VTueK?)hFD)G71PfyiNRwl!5bIdU8jD`qei4K2a+>*ni;YRX=9UdELc|4SfJ9b
zbnAYiMw4O^9cI`x@qrqpsJ=re*J<j+?-UfaU>7z0_mI-EiHep>&cmeTFpoU<E(5LB
z1whcdpbg7GqTQQbmaNT9cS++_xoY5glTKeU<}#Ap+U;i>_Y8Lr+49q2Q6l;|-TQ&V
z4d9F?=#*)An6O+qw(?}*c84u$LkDE{-BiXI1f>jtI0VRfj1|w3o?EQHCCr`=6FxoY
z=Y0OY((9UCodPBb>=NRFaq!{t40yZj#c4`u3CNejl{|{?^Wm;D)vzbzUX?{B`HN{j
zR-_$wJ*x?TM$agg(x*?jM+{q#GI-Z6|GU5&#;Vg#`vexeMerDYSE#@nD<aYa6ginU
zG-c)2lV7EznqR}6n}jyvf{lNiZ!fMPt1wLZp{WcM>{;4{4!yfu>jdx^h5ae2Rnu9N
zEBimRf31x%VLatN&G6g~Nr}q`ZX!sPF3k{qAcF^Q6aIKcUMyetHDw(OWs6USAZJ6P
zacBj+Yw7OGEci^4%nlI_&p#2uZDFfhlGt&kGIRsCkoTbUH4-ht$4`I^*x?q`DjFF8
z17!ZiNO(v&it=Rk_bbNL8s(Es-$%<<AVn@iXe5~$Q0LdrR7q<l*ysL)s6%&U(n#qz
z6M|PR(h>(IuDyFMU6Z0hU*LIOV<3g@liS7mcOzVfwtH*~?}N^(OU@=;5}lHC{B<y4
z3CJODk`#J91qrRIVLf5HrHj=t2iXk4k3?Z<8Wv{9?YU5^VW{t8Ye=c;$6+_1&<4Gt
z8LjSy1HtT*nW(lDkrE|4+LjLdpK#>vfotf~F9b7F0j&dyh4v>y!jb$`FasE_2s?u)
zG6Axpj@j#J1ZrAV$Rl##CNAAKhiW8j$lC(n*(wzq3L7)%W<7Y#Q?A;Gjxzo{$E$hb
zV2|J5k-_vUz<C!yB6-;|^i(5JT@7&Fd_PE7otlC&hp?W{9NjaIyVqzba)D6g5H0|o
z>M->VYU&``7kaBe6Df$+fT@f9iwiZXcT0#iYJMJs_s1LX>rc>RLmP0>`mFMW)?`yu
z^Z8yK*h<|bW}qywxGjDH;d($CMq``XUk!)XU)YWN7Q-tUmG6M9ZLbx{u)w4rC5{9h
zj>X?B`l_dx1BJRT`I|3bNL&81M=8x#b;a;-hnh&Z>L3Q~xofPUI~Hu@<I(*O<BKOQ
zR@nQd1fT+}G~p}Kim69ctQ7Z@e{M1)qK-DqJx6wdsR}k9@wnY;bx<uV{Z94((!xe#
zEYuAyEOrMvI7kWQelnuc_dho*3@31kUCH7f`<6?gANO0mJMbXeiGrf#`1LF=i+bHQ
z-}-M%Gwbgu(H8mDA6q^~(PsOA2N>UY;po;H@J7HLC7*X0>aTqCbGOxj>LN!1ymhay
zJ;DXk_bPA)YV;Y(IkVuqU%Vb0NiUvPZjJ-RK6IEpjbj&g7=PlEynx*E$jRHB0vAu;
zGr1fJ2Crt!Q9m9{{l|GgqU&THo8L1$!24I_qHO(pa5Ta2ewZm6Rel=?6{Z`<cB(f(
zhUJXOZO4cV!)O#8ss8Tcy>@=#S%~V2FChf}siMAZrbj(`cOG(cF4%;01bJU=_u3bO
zkK9<3-;5xx;_MhAc;Xg4ZLz?K#PudXFB>kPCKJcz<j)_{s1Hr2J6NOn<!DwSi`KYD
zHx@Z5c+kl~MO9pK4AF0yb;VnasBrSbf_=50_Pwi&ON0@}ut?*!{cK~W!6m>VC5^lZ
zbUjCvXitd!V*sv{`j4!gANT`r&3ix%1&pw6<aO0|1Y>L#^4!=#(qRxL#PU5T7xo*o
zeN-++vFWhmVSWgP7I4^Lk{oH#;9;c>g!pek-YwxS)I6s2eq^aI*_r*<|J%KX@^`AH
zE%}pUVE9J1P0H1mX^Q9hp1@I%V#ccbXIhlHnD6-Rz=Ik0D6Js(Afw+Y$W_kcsw;m|
zxs}pIP`4#61PBX)PU2JfBq~fuhd?GT`(*!+VQMNXWU9#CMaN}FsIfnP=~t6M4XXr;
z`ST)+<t>E&4Mo-hU|7G)KkYHAQkw06{FqiS+^_g`l!wVT-adn__V17LtqpSd<hC1J
zUO5~$IX|0<@AQN-Rrt`pDpUMw)lIb{Fwg^cP;w=UYy{8@dQoAg0~x`TT(cz#(wwEX
z$Esb(qlhs7988$1-g||XY*mxw4EFNQ-jy_>YXLBP`=jlBwcQd;>WG7li+4IDEv-pO
zxiXczG)5B^UNCYUbNxK6dLJk$yEaG>a}uxP7gZ+OA&_4Lxi2NmCW)31bq@Wy!YWCH
z6AY((B0I@5gON7;K8w`cN4*rGQgj}}XGdVg?+7S_!u}?r#|{X7rbf*7O!jlEg=W50
zj$WOjEI$JQDa=wl;M}K{fWfy*P+U2|FZsy7J^+M&#WDdJKxy$76x<tGg@bdO_0Lae
zujsWc8jee7DUS#uhVU$bc}N>x=TSmDEz79fk)~i;4CJ^SVld0t1qL}@=Ld$T6|Efc
zb|wKBWBi_g`_szVYO8oR827^nXbZ9aaCfm89FI#p1oFO%+d`6rIShIs21NVNHj*mu
z!rKBUGD2T=M0a_Bn+Ksd|F&<Xm#Gks$5BM?e>=99z{dmjjsJ2zD&h=#yJX^h6abG{
zaGr$<yz`E0NHhPXPl%0tR`FACzZ;N^TDT3O#!&(S=HUb1bG=j=ns@K-zPmgpZnY$S
zRmc%;uK7ByE`8&r3XR$CP=n^Pst7YX5v910B%d*cL-2luLfUwgP5XiQ#qd+JNYD}-
zdn$Ok=3UWEW}DLYVh^fJHoE}vKME-#{kBv50V_=VYBDF>Cx!He^hDnsCcUH!ed#pG
zk4@8(3ZjZxd3}+M^5BNo8k!AnYSb;VxYxh!cDXP$pMSS4I{au`-iH6{FR=DTL$~3j
zO)TP{xH`%9x%smnm7&&OQ^(DxhQ}WzH?5`bwJf=|3jIe=pW78?lt~gpXbnGoZ%JsK
zHFsJFlHl4YWOCTF;O?3qTd3rfe$_(%9jPrwg^nt{3w0?=;nglRS8AlmEuMuF^@pUm
zVI4e=W#zEyuQn2F)Y^)}w(Lm%PXwOa<<XJ<om60beypyYF4dq+ANG2k`6H}amMclw
zraf|8t#^U?!!R9}snjP13#V^byLg#wRE_v`%?htPzN(}rEx%qeqD5(wDN!CiMRor8
zJ=}zPmd?fpxrJ~<g6-Q89+xnZah%Qn;@3K?ts2K&3IJdIM*p5G_xgzH>k+4m!_kkQ
z*Zpt4ncYA2%(P7V|5zq7@F}l(c$Q!pz=~IYnWEJ@2>r0@7D&G#aN-+(iVY8dWjvcE
z?c!IRqJ-8{AYn1NVl;;(_`-Mc{Ij|1)$5j8P`8k_RtFfM#QAX(vC_)Fe}CKl0_r7#
zC<}!AJ{_h;-k%^2d-v9DFmVN&JXa*E9=)HHh!}<P`Zt$2Oa>!Cl8W(@K81NUjY!bw
z7Pdj3l4O9XZt7KaJA|_PBF?9qrY@%G2AeG^rGBJXq*QCP&_QLBv6p+fV9Nj%Td!_3
zQz4#d6+oO&PK|7#?ZMh#a1!tk<|(!Vw93D8j`r-T<dP8ntW`Q&UZ}#$e414<@<+*#
zLdn)g*D%cywWa%FzeCUou;_$?YD<Pn@O8nzrdezYed^Rt_LuoAkK@_JXP<~vF9zl9
z{ayI>NTHzysvXNKHANhip`2Mlc7&&j<;jtx6z+I{5V2s#yL5d#)(ki!!wguuE<#sm
z1Gx!VqkXHipPAd>11vsO5{{<dCs_B|gMU0}p5s=Yh$%Ga4pUD6-wYO^f{QWY=!uF$
z`Z-erzj(G9PDDQ6Rpx|#l*utPc?_hT`&RC|gmj3J%wddT(W3|B4?#;2qr8<!CDJ=U
zDp}x17A7;mG#3?Za4Di*D1CUUM{~D2A4<{Hb?`$YEJCs2?MRs)kGff#zSh;pT$}&6
z$WLwtKkyE!np#s$!J$@E^&-efSnEphWtN*gR=aD{!(uTghR@u6p1BK#N4znR{t{Ig
zLpI?Y@1+OsB>S+2+{~<F$k>5q!1#7d*PxG?OdrsOii4fx02nF)QfBmAE2GQj6)j-u
z8aAnhkcV-_x683I(TZkZ8F~5V<}DoiZDmG29}7g$iDIFB@1tHS?+*aMzFIrxz4I20
zmspo9wfwSN2(LAVwb_5b6t<KgwXPHCgia+F7i(#qNSEEfbS~#|KMt(p`D(_~hxgG^
zZkEC;7%yE7HJ)b>1_N3h9ap(^R@$L8@S12eYsiTHaA-65UsSxn`v(`2fjl*K5#GD?
zYIEbyi4OZQ^!u;QqiDVve7CqHv7YySq)Y{mHB`*Cg7If*R6a(m3)Y)0gDR=@m#-9V
zk#*CF@s1}xUFG?#ZMI<jc@-A!nVDpSVzztW{g{;ZwojIl^wi80fR}>Dz!wKbCr}kC
z+x~_1XVq)Xq5E{+*!2i2&iS2F`i+NPWc-lq;#CCE2mRji(dxK2ETr`jyh0&IS8FJP
zvZh9uG9;3%;bM>FaJndCD`&XC-C<KA6J(Tkj?f>O-WPig;g{m%4O|?3YSw2*YgI}d
zBOj;*AAGyH1gE>;Lml)kWLW3vCCmPH-)(u%90Rl_RCxdS=b(3|zQ50f9~;BvTe7LP
z(%E=^Mq&*U*=3$FI#PqGMX@kB^A4v~^NCfjPFE&XC8cr@)ns9+tpN|dt5M>30dbfy
z<+L`1DxLuD*7IUtd&xiR*TM!qgDmfk^-&ln{E29U;Q1ZvV$AVRH%0*o(Hd@*p244R
zDQJ6XnJfCWMaf5$4AV<vgMA(c<tr~cf6C(-p>{|au%8R)rW@HT`^9s07h?7vn2FlO
z6@A_-_Vc^6|E{X2Xf{D0#YxqK*Eu|;?`f!Z^eUDab&qaGUJ1MSIye9GGU@p|1C8yb
zB9kzYVM6V7Otv0hW03C&WNJz9Q~VOx*IX#~tUe<3_PD*xJIAyof3!h`Fk_Ce$15L$
z4u^pFUA$tgpw7$(?K6N*>bTFN2r-Ye&UCYxh2F@sXRYh+H)r*;gh??%&d2QwsOBP~
zTs!(U#IMLEe8bNC@KM8Gyld4w8K_fHJj6HEDO6`}%E%})0Gw2_am2NVE1Ms}%PC9^
z$C!CHI)gbZlOOmSXNShK+HsPz8a`Fby!SAoQ>YI;o%f8by+`7Ap=WLJT7yqqZWv0Z
zgN04+5B)AI&{(|J&iaett5s5@NI!|bbe7&>$rMk5I00V=4V20|_s4O#8@z&&SupEF
z#Qa}1jSVXq-kH}_Ha;7}3R5Ns9&?lqvB($iN4U)s6AQb&tbi9}?9B@|i{PuW1q`jw
zrPjJ!(JZrMPyqFz8=1Z{l>48%adN)bfcLl20s>alo;a`$UdK_ws0pP^q68+<W8YWd
z*yOJL3u-(5#^#1Nz->U}b;+h<?&iPoX-lu2WssdJG4$%RV14Q`CTu?*f1kaUr05g(
zT`#VaM3taD^N7*}BrFO@97zc)jK)+FSM{iIu)a~BR5I+?od!{L`vyg4N-^6b&2hL2
z?{t*<=EU$R#Ub@Bb_E=<9=~PmG(ssOJ^Wb3koc*mkR+O81au<sX*+Zkgc=pQmCkh+
zo}2phRg##HYm{JgXnVUsABp6GU>ex1;B&tq)YrvN{PwILtISENIHlA?Nt5D=3w2Gw
zmUJAfU*+;N)i#ZNzOwf2uig-`EDqCXoXSDiTT))6W`|7F2xYSYZiG?xRP2rE^c{5Y
z(?b`WKF=FROodRkdhpBE_R+WL>)n*VqI`<XiB@o>bG~qxMWt7XI@S5V=@0ZS#&${R
z!!r1Q`)boCXz^BfWDMz>2emgbesiZUA5s}jrX_9~S9o<4y+I$Qn+7dFA;Hh448hQJ
zS-->?ip#}%m@qYqn$`nj9e%yg5(1-;Le`3Y-HZG%F7~}wY<{UF5#owz(U4fL2NPV&
zR!xXLBex7&#^v6b^NzU4C13Wzk^m%H3c3SW2m7%thYXxWVEKN2$M#x8OOORZZx7|@
zr`E;!v)uVBK;)_b#?}G=`Bm6iDK?>GMD|2v=y~ho){EOi9*SGejj-J97f_eA%F7sK
z=qfh?YS?3K<3_wF486VsURwEy{gpJ^r?7qMmVEAWZ7T#|plUi}iZkv!B|16L5bn6%
ze#^1&qve>L>~MK?%GIlKF>Y*~_V<Uwde*et$!Fo^W$lUfKE9JG`5B6WDb`<!jvrYE
zN5UN6{;kv}m$7sakyIrul^|Gv;?FgKF#q2wc(lrUYyWTYhZA|@=7PPi_UgHg9-bOI
z!1ez}YQ4D;fZu||fff4QGm_B*2vduJ3rK$T(0@HB%Z9TP@$!=XDOziA;mqnYn2bDC
zn#qH1N8iW6kLW_c^2P0OlY{q#Gyw(-eP8|>u)kfQgrFP7@dYg8g}C?2K`13M1~3_t
zSbEt($t4bWetMVV(AVdByxnswK~|ACQ~msAYxkq_!!P$?&Wn)1KXYs75GHE?I+)@r
z819c=c@4r><9Q`SiAm@eQQDJKSq+qX>r~C;-ufyQQ%z%JTVabs@ZdhdB=5_+yMg50
zJKWk^Q3*6L-ct+zHmx%zu8tNIJP@zUR94+uMhp9PBt7bQgtaX_Ys{@mkv?m5Dp69u
z!}^oNsDJL>2kF}Kqla$GDaWAFVkYD2W)XlQ0a`_T0S0by3ijOB`dipKUAIX<6nd>|
zr-ss)1O@5aq8&#-Z?r!Zh*#e4_X>0f^Ik*AyElg(`6lwe50C>U!gK}!!@G64uEO!W
zf1w|=MEa7=hy5(d<)d*VT(Qn$|5hT-CBB!0$!15sL%4k*YoNsjX>JRtT+j%$Ip450
z^K}@*=Poaab+Ic7))!RoWFMNK162EMgmqv~v_Vc%c*d5MoXMa1?GIk21g0BtS_9M{
z5r%ttP5^6+1WsN+Ln&`J*hoTN0)f<2VTiRYG<MuJ&dh5|{uiqAqeqk<iM+ENbS$ZH
z`gh9!WqKZq(s?Th38&h7ruuzvz@ND)mbU8qy?A=rGL73ZrN@*DQugCsqjyp<bF*_5
zq)Jl?I<c}>;Dkips^B+21(hjcgaG*4T^S3qdt_X!e4?zPNKa_`rZJTX5j0Ei7&S8h
z9ZEZi!j6FJRDI=ylM|Uojg4ejyE)dBlL<0qJ77VY9|w^Aku~#&h(NKux>V31YdT2(
zuG^V)Uara|LmW8?b%junrFeXnBvE>xs}?HCDidSj>(N0f_$iXmZX>YLERmDE3xa@%
z^5sO!kpr&v2c+TRJ>V%&^8DUj4qxxL{6!^_VvLZ%yM#A_d=;auedwPVn6m@AWz_-h
zdnOwV2yHAU@7Scbk-A}S01wPdJ$=dimh|v`TvCIYZ##a<4`G$}gBSpz-9#Frwx(2w
zGIow?PZHI_Q;T)H?NbKbMrm5$lrs?BmL1t8#Ho{e>AZe8g@A?kvs-v)*sHh6l{0g=
zw7v2YtFXKd@I_>mNA;?n7*7aC6k9d;?!Ab%s;YF~khMy!s|ayrJm*4!@ykh=0{gu&
zDL+nSKi{MW|9~F%^K+NbG}#GrwZ?8tVkPTXj3CFaZ&qhXzRp43O0MONn25S=Ua&GY
z0m)D7su>)>w?Mx4?jnir&BA1tMjzWb4<L#6C*F@Hn<c!Q!U4MRv-RTSQ6fgC<q5th
zKRtf!FZh{TWNmAF%kUGa^bSF~JfnRutL(mObJn32s>A;2kJ7k#@lg6t18LCNytgEg
zkV&$Iea;(+hngBDBG&#wBK~tlIihT-6nQ6}^L7b=m?1#Y0$24A=xW6~hhN*4^g<b<
zPw4NfglOh!ac~orD6DC75&d&DeYCeBRpm)m_nujBu#%;JDMmwq*DwXCIC7>>*DwJ@
z3_O7gw*NHij9{YRbLH{DyrAhHJt*O9aA<Psyd<=a;dSG(Ge$0t#m&>&70q4ot4Yem
z9927NM(hqUih9>+T?qIi`TXG<@F8xy6_FE7#L)XCg5ReCz{bsTo%A3Stptg-9#tbU
z$q8LBQ2%b(Nc7qm-vsj>=ED1&NaeL|GiTj@;*)cI_-Qd#<Z~Y;3+elu&d0{G%(CiN
zM5EW>mqp)K;t)lF2!?mnIOYfW@Sz5`h!b+T=vGbZE)jJ)Z8U%mkr$%n=ug@SC-4-!
zoH~((T-ZHhqp-pqLpLio$vz3dj>QTFxTP-1s5((68x0_m5R9C5)Gbo>G)I=11HKwe
z4&BA(ZWGc{Xvi$kk}Tys3ds!{XFqE3z$>swoB0wsObZ=A66n~=$csoODU}@)cMbIZ
z-0|}x83kll=d>!*hLgM#0!s1T!Z<(A_NJGeMSmBX7&vNIbR+2%mQAr-X{}#iW2#AT
zY(M}M-+6#`kgeyuaUa2H!suYd7ZW8*gEGjtt@!b!oL3B_eXMkI?2&MRM?rW)ltr*L
z?l5fro<N@nEU+hW6|~pnn?^SpBn{CXm)@*<Isn|6POS?;34u`+(1duzAT-(C8N=q)
z7He%nD}jWck_9hX0OmMvSm*F<Kvu~BhMPmD8DU^@Wuqi@tCL7M0`c4}G=s|@Quyg;
z<I0HybLq#=8&$bl*EI;rjKW(=+x7~_v+aD)Xgel?K1-|x5*Q2@JO<LCY*^HW)bwYa
zeZO({>9V8fRNbM3%|Oa3`CWjSA70H;34sh~23&s;t@??KAKAf=ZFF(4G-+(X>Sn*2
zH7Q=D76^^xGnmr|Njkbz%v}zTTSrNwK><ZBSn3a`CYiw`iHv(b0ebM#*`It8d`Y$;
zJ~fDYf?h6kgV)Jl!V(%h6DWeuUXiQ^`>k>&r=zZ<*>^Ih>@||S8}33B^!`}10b~&`
zpsS*t3_|(~_4(DL_Ne$ce>I;$$Cx$zuW=nWk6!(J-GM2wSg7QFHPh9eRLI1hN-*8Q
zGs&K=+?XIsr+;`AlsIxC43gvHBOUesTatjH7=iveT$);hx(+!Arfh+m7{qTivUSSb
zTjZpOm$~3A3U1c8P4m3J84lx}2t5|zpS(t|{7ds1U5Ru-qQl$Ep_7=nj>NVg{IE!Q
z@a$PJ;d9fG01w<=#T73Iq1Ncpvx)XaSy%rc!~(eKX9)i)isNWp3EhBlmi#D?(6GU<
zJ<~yvNE)uF*~ijn&&wLU5;-=X1Z)(8!gRTuoK(0773`qQs>olYu56`^6e7e=D1x;(
zrnZWlC;=Ea$@gHuh9Hf2z~zk&UYG`;5;CR`%MIW{>O9dBASVv{-b=0SltT?M@=bgr
z0?b7|^zc<}c)J;}`~tfG6djz0bR(#v8{Y<R4y>!IOn?WJI`%?h^Zw1p^%t^qfVV7%
z?MZ8iTYueS5T%cB(igIqop%P0FPSN!19OjVLG*ihTVxEUF9|QCCHpT4J#pqs)hGm!
zmW-GYXi<kx?5DU*WiAar1pDnMM&kQc{HP{m`NbCMSe8IJrlc?Buv@4<Ltt*^{;m8S
zqIvc9Yh~w0lhsIv$@_J~%OnnmEWWpAJTAZ$hZTXQ*(Q*EsRR6%+AK8xM@fRTuqtV^
z&;M!wg6P4gp^^{li{*Xt>0lA%n9u%FW-4QcQnf){3{KqI%SYa~`Ndx3hZ*uh3(J(C
zf)~+O_<Y%+fjlqEP^ND3@V#zRU%w`pFuF6}CRY})ao|mr39`uKN}(P9`AQzjnrKy?
z*6bDgZb+*UJ}YgN5SH|IQ6~{Vv$Az@DO}gdD4NYd0C0!ei|Y3iwU=xx3IW(RK$ZBd
z|D3!g@@4>i_$zV}lb(MV+SfY??k(%wb-#KP9b{9KXJx-><jAR_EJYK4pq?5+r~yD&
z6J+pxOORNM16p}WQb>|cO~y0}v^BgU!KjHPs|onB-1n>dEid~W>afmgOR>Z1t2<`0
z*un++MR4HT0Z9k4))lm^4-pglo(~X(4ZcacPZ{YbNk!JHSyto%MDKFf1ri!*$5uyp
zKCJn<LkCVHDPvW49hkf!VomOxG=lPDNGYhfJr$N@+9K!!2f*1To}-AafU_asc$pG%
zve}>$ToHwZB<aDi%6gWQfc+5D_c(;u5s(i^A^!r9ps+wFSv3B+wx{2Vm-H#sj%151
zU`s4Wekb$RbbGpyRG-<hDl>=AChS1y8+RNMK_F0xQrIfr+-m>!2d;~2o={)m%z!pW
zP+ujDo@S1mCXY|cFi{X_73PV)uvL5{pl7WlZ!)Pc-rq--j9JgueL_!lZwl}1{dAQ%
zT@>cI3KItSBx;H+WpXt_wx*Lw8Rn<&mWUhlIM|#7_IOSJ3jh&hGev{l1uwPfzuu<$
zyAqoomR)lOqEAwI-7D67`$sC4wU}yeqfw5TYiD)W%I==SV(Mu>6hQq|E8bGj*algi
zh_Oj~`hyk)tcbEFO640P(l>d}b88c_!8^&hs{#f!PTq97SVzV%K(%6U8nO$Z@*I>}
zS(w$pdP4QrFr=k2<d_`x8_BGS_EX!xGc1E09J7kGSQI(rzBUoi`5yxAfc_m1Xp(J2
z6MHb9F0oicqBgl_H{WD*?(73pHEd#kws0BsnZTxeq~V8)i9A~LrNhcgOE9n#;gDoV
zjzfc6FlXhupYx}^Cbl3Jef*Jt@U<}A+<SLF%UO`A$+x%aeZSGxkpL%1w2obqVXOSY
z7ddEAUxru=iWpCi0>gMpDc@t5;;c=pF7iASBTVs!5>`kygKq3GzX=&;(JGEiTQYye
z=id8#P0(r>c)bf9F}RVkXWBbN;d~uId3QkPfLCH!e0eUBQ1SgzgZ>NWtHTk#^Sho_
z#LeqrD4$u$g2QV~6SFL?PX?#LtYzVZ%+_C#G$i#VSVJ)GP^%sUItd|eA{%0&t3kJP
z`^A)FRC<Hcyx-o`DLqK2cR@1Wzs?=ydsbM)nHWoFr&)zmdIzvIEe!8N%_n+P3O{GY
zyQgaB_;g|pkeDet!-*MK&2%7tQWH8rPq#e1v$3AFx&&pj5JaF6A35QkQMu%<8FS!9
zH%|?x6N=4lH=>K$jWiVTw+0l@|LN*znl}7UCBSJdN_MUpQA~9sOhTkoG{C{r0_yu&
z(vOk`U1p6z9`{*ag&rAt3@p9TdbOCjsn5$QKJ?ixM4~>+<O(xsoGG~1+d&LsschN`
zW;;KhiHJmCD0lYvcQS6ND{9LR@@-Jq*@o%=dCOl^DYb(bM%RE1)xkGh9zvc-{_9~^
z#8DpUyoPDIKr&XfRU3y+fysi+Uw&RYR@cEjH@G?4wGOz0R*6-NJnma&r~QuTtXM7s
zJ~_$<TH)yZ01L6IwLWbQ5ux9$YdT23f@?5*&V&Oc^q_st=y55)v~4Vj9@t<y^eOXo
z7*ij%3zLFj$!)k6e%*|U!7r<hR`08!^o-U!ID<in{jD;*Sp0!3fhE=$F7qmNA{8u%
z7yULY{Q!9(AetzFW&wZL!I=cmedM$@jFI?UM*8w6Og=CS;PXexLm0Fgv}yl+wd!ey
zyY)Sl&!1@oq43({w*)u7mp=RF#!cZ>=Z!iyc?Li~2Z8LHFJ)WLSM3qMG?)eLzO40h
zz7xvXS0_#E$}Dg4hE}n<J%YXEQrB{ztL+{O+_7YB;2zii(?2m`t)c>yNdHA2u3x;x
drO^K6mM}P0xV$DFHT(a=DaxwJ)JR!G{BJSciA?|i

literal 0
HcmV?d00001

diff --git a/README.md b/README.md
index f78b0cba..7bac2225 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,8 @@
 | 🚀  | 应用管理  | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
 | 🚀  | 地区管理  | 展示省份、城市、区镇等城市信息,支持 IP 对应城市      |
 
+![功能图](/.image/common/system-feature.png)
+
 ### 工作流程
 
 |     | 功能    | 描述                                     |
@@ -129,6 +131,8 @@
 | 🚀  | 已办任务  | 查看自己【已】审批的工作任务,未来会支持回退操作               |
 | 🚀  | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
 
+![功能图](/.image/common/bpm-feature.png)
+
 ### 支付系统
 
 |     | 功能   | 描述                        |
@@ -164,6 +168,8 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | 🚀  | 日志服务     | 轻量级日志中心,查看远程服务器的日志                           |
 | 🚀  | 单元测试     | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
 
+![功能图](/.image/common/infra-feature.png)
+
 ### 数据报表
 
 |     | 功能    | 描述                 |