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> + <!-- <!– 枚举值设置 –>--> + <!-- <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> + <!-- <!– 校验规则 –>--> + <!-- <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> + <!-- <!– 表单属性 –>--> + <!-- <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> + <!-- <!– 底部按钮 –>--> + <!-- <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)"><</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)">></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 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 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 对应城市 | + + ### 工作流程 | | 功能 | 描述 | @@ -129,6 +131,8 @@ | 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | | 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | + + ### 支付系统 | | 功能 | 描述 | @@ -164,6 +168,8 @@ ps:核心功能已经实现,正在对接微信小程序中... | 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | | 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | + + ### 数据报表 | | 功能 | 描述 |