main
chenyuan 1 year ago
parent deb3028101
commit 8128b2dd06

@ -579,26 +579,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
]
},
{
path: '/ai',
component: Layout,
name: 'Ai',
meta: {
hidden: true
},
children: [
{
path: 'image/square',
component: () => import('@/views/ai/image/square/index.vue'),
name: 'AiImageSquare',
meta: {
title: '绘图作品',
icon: 'ep:home-filled',
noCache: false
}
}
]
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/Error/404.vue'),
@ -609,38 +590,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
breadcrumb: false
}
},
{
path: '/iot',
component: Layout,
name: 'IOT',
meta: {
hidden: true
},
children: [
{
path: 'product/detail/:id',
name: 'IoTProductDetail',
meta: {
title: '产品详情',
noCache: true,
hidden: true,
activeMenu: '/iot/product'
},
component: () => import('@/views/iot/product/detail/index.vue')
},
{
path: 'device/detail/:id',
name: 'IoTDeviceDetail',
meta: {
title: '设备详情',
noCache: true,
hidden: true,
activeMenu: '/iot/device'
},
component: () => import('@/views/iot/device/detail/index.vue')
}
]
},
{
path: '/mymes',
component: Layout,

@ -5,11 +5,11 @@
>
<div class="relative mx-auto h-full flex">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
>
<!-- 左上角的 logo + 系统标题 -->
<div class="relative flex items-center text-white">
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" width="32"/>
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<!-- 左边的背景图 + 欢迎语 -->
@ -19,43 +19,46 @@
enter-active-class="animate__animated animate__bounceInLeft"
tag="div"
>
<div class="flex items-center justify-center"><img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg"/></div>
<div key="2" class="text-3xl text-white flex items-center justify-center">{{ t('login.welcome') }}</div>
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
<div key="3" class="mt-5 text-14px font-normal text-white">
{{ t('login.message') }}
</div>
</TransitionGroup>
</div>
</div>
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
<div
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
>
<!-- 右上角的主题语言选择 -->
<div
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
class="flex items-center justify-between at-2xl:justify-end at-xl:justify-end"
style="color: var(--el-text-color-primary);"
>
<div class="flex items-center at-2xl:hidden at-xl:hidden">
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
<span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<div class="flex items-center justify-end space-x-10px">
<div class="flex items-center justify-end space-x-10px h-48px">
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
<LocaleDropdown />
</div>
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
>
<!-- 账号登录 -->
<LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- &lt;!&ndash; 手机登录 &ndash;&gt;-->
<!-- <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />-->
<!-- &lt;!&ndash; 二维码登录 &ndash;&gt;-->
<!-- <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />-->
<!-- &lt;!&ndash; 注册 &ndash;&gt;-->
<!-- <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />-->
<!-- &lt;!&ndash; 三方登录 &ndash;&gt;-->
<!-- <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />-->
<!-- 手机登录 -->
<MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 二维码登录 -->
<QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 注册 -->
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 三方登录 -->
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div>
</Transition>
</div>
@ -64,10 +67,12 @@
</template>
<script lang="ts" setup>
import { underlineToHump } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
defineOptions({ name: 'Login' })
@ -100,3 +105,15 @@ $prefix-cls: #{$namespace}-login;
}
}
</style>
<style lang="scss">
.dark .login-form {
.el-divider__text {
background-color: var(--login-bg-color);
}
.el-card {
background-color: var(--login-bg-color);
}
}
</style>

@ -1,11 +1,11 @@
<template>
<div
:class="prefixCls"
class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
>
<div class="relative mx-auto h-full flex">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`"
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
>
<!-- 左上角的 logo + 系统标题 -->
<div class="relative flex items-center text-white">
@ -27,7 +27,9 @@
</TransitionGroup>
</div>
</div>
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
<div
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
>
<!-- 右上角的主题语言选择 -->
<div
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
@ -36,7 +38,7 @@
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<div class="flex items-center justify-end space-x-10px">
<div class="flex items-center justify-end space-x-10px h-48px">
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
@ -44,7 +46,7 @@
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
>
<!-- 账号登录 -->
<el-form
@ -64,7 +66,7 @@
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
@ -112,9 +114,9 @@
</el-checkbox>
</el-col>
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{
t('login.forgetPassword')
}}</el-link>
<el-link style="float: right" type="primary"
>{{ t('login.forgetPassword') }}
</el-link>
</el-col>
</el-row>
</el-form-item>
@ -196,7 +198,7 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
loginForm: {
tenantName: '内蒙必硕',
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
captchaVerification: '',
@ -207,7 +209,7 @@ const loginData = reactive({
//
const getCode = async () => {
//
if (loginData.captchaEnable) {
if (!loginData.captchaEnable) {
await handleLogin({})
} else {
//
@ -274,10 +276,11 @@ const handleLogin = async (params) => {
const code = route?.query?.code as string
const state = route?.query?.state as string
const loginDataLoginForm = { ...loginData.loginForm }
const res = await LoginApi.login({
//
username: loginData.loginForm.username,
password: loginData.loginForm.password,
username: loginDataLoginForm.username,
password: loginDataLoginForm.password,
captchaVerification: params.captchaVerification,
//
socialCode: code,
@ -292,8 +295,8 @@ const handleLogin = async (params) => {
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
if (loginData.loginForm.rememberMe) {
authUtil.setLoginForm(loginData.loginForm)
if (loginDataLoginForm.rememberMe) {
authUtil.setLoginForm(loginDataLoginForm)
} else {
authUtil.removeLoginForm()
}

@ -18,7 +18,6 @@
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
disabled
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
@ -83,34 +82,34 @@
mode="pop"
@success="handleLogin"
/>
<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">-->
<!-- <el-form-item>-->
<!-- <el-row :gutter="5" justify="space-between" style="width: 100%">-->
<!-- <el-col :span="8">-->
<!-- <XButton-->
<!-- :title="t('login.btnMobile')"-->
<!-- class="w-[100%]"-->
<!-- @click="setLoginState(LoginStateEnum.MOBILE)"-->
<!-- />-->
<!-- </el-col>-->
<!-- <el-col :span="8">-->
<!-- <XButton-->
<!-- :title="t('login.btnQRCode')"-->
<!-- class="w-[100%]"-->
<!-- @click="setLoginState(LoginStateEnum.QR_CODE)"-->
<!-- />-->
<!-- </el-col>-->
<!-- <el-col :span="8">-->
<!-- <XButton-->
<!-- :title="t('login.btnRegister')"-->
<!-- class="w-[100%]"-->
<!-- @click="setLoginState(LoginStateEnum.REGISTER)"-->
<!-- />-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="8">
<XButton
:title="t('login.btnMobile')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.MOBILE)"
/>
</el-col>
<el-col :span="8">
<XButton
:title="t('login.btnQRCode')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.QR_CODE)"
/>
</el-col>
<el-col :span="8">
<XButton
:title="t('login.btnRegister')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.REGISTER)"
/>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<div class="w-[100%] flex justify-between">
@ -125,8 +124,22 @@
/>
</div>
</el-form-item>
</el-col> -->
</el-col>
<el-divider content-position="center">萌新必读</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<div class="w-[100%] flex justify-between">
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
面试手册
</el-link>
<el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
🤝外包咨询
</el-link>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
@ -171,9 +184,9 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: {
tenantName: '内蒙必硕',
username: '',
password: '',
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
captchaVerification: '',
rememberMe: true //
}
@ -236,8 +249,9 @@ const handleLogin = async (params) => {
if (!data) {
return
}
loginData.loginForm.captchaVerification = params.captchaVerification
const res = await LoginApi.login(loginData.loginForm)
const loginDataLoginForm = { ...loginData.loginForm }
loginDataLoginForm.captchaVerification = params.captchaVerification
const res = await LoginApi.login(loginDataLoginForm)
if (!res) {
return
}
@ -246,8 +260,8 @@ const handleLogin = async (params) => {
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
if (loginData.loginForm.rememberMe) {
authUtil.setLoginForm(loginData.loginForm)
if (loginDataLoginForm.rememberMe) {
authUtil.setLoginForm(loginDataLoginForm)
} else {
authUtil.removeLoginForm()
}

@ -133,7 +133,7 @@ const loginData = reactive({
},
loginForm: {
uuid: '',
tenantName: '内蒙必硕',
tenantName: '芋道源码',
mobileNumber: '',
code: ''
}

@ -1,5 +1,5 @@
<template>
<el-row v-show="getShow" style="margin-right: -10px; margin-left: -10px">
<el-row v-show="getShow" class="login-form" style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<LoginFormTitle style="width: 100%" />
</el-col>

@ -1,142 +1,279 @@
<template>
<Form
<el-form
v-show="getShow"
:rules="rules"
:schema="schema"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
hide-required-asterisk
ref="formLogin"
:model="registerData.registerForm"
:rules="registerRules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
@register="register"
>
<template #title>
<LoginFormTitle style="width: 100%" />
</template>
<template #code="form">
<div class="w-[100%] flex">
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
</div>
</template>
<template #register>
<div class="w-[100%]">
<XButton
:loading="loading"
:title="t('login.register')"
class="w-[100%]"
type="primary"
@click="loginRegister()"
/>
</div>
<div class="mt-15px w-[100%]">
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
</div>
</template>
</Form>
<el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="registerData.registerForm.tenantName"
:placeholder="t('login.tenantname')"
:prefix-icon="iconHouse"
link
type="primary"
size="large"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input
v-model="registerData.registerForm.username"
:placeholder="t('login.username')"
size="large"
:prefix-icon="iconAvatar"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input
v-model="registerData.registerForm.nickname"
placeholder="昵称"
size="large"
:prefix-icon="iconAvatar"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
v-model="registerData.registerForm.password"
type="password"
auto-complete="off"
:placeholder="t('login.password')"
size="large"
:prefix-icon="iconLock"
show-password
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="confirmPassword">
<el-input
v-model="registerData.registerForm.confirmPassword"
type="password"
size="large"
auto-complete="off"
:placeholder="t('login.checkPassword')"
:prefix-icon="iconLock"
show-password
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.register')"
class="w-[100%]"
type="primary"
@click="getCode()"
/>
</el-form-item>
</el-col>
<Verify
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="handleRegister"
/>
</el-row>
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
</el-form>
</template>
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { useValidator } from '@/hooks/web/useValidator'
import { ElLoading } from 'element-plus'
import LoginFormTitle from './LoginFormTitle.vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import * as authUtil from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import * as LoginApi from '@/api/login'
import { LoginStateEnum, useLoginState } from './useLogin'
import { FormSchema } from '@/types/form'
defineOptions({ name: 'RegisterForm' })
const { t } = useI18n()
const { required } = useValidator()
const { register, elFormRef } = useForm()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const formLogin = ref()
const { handleBackLogin, getLoginState } = useLoginState()
const { currentRoute, push } = useRouter()
const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
const equalToPassword = (rule, value, callback) => {
if (registerData.registerForm.password !== value) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const registerRules = {
tenantName: [
{ required: true, trigger: 'blur', message: '请输入您所属的租户' },
{ min: 2, max: 20, message: '租户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
],
username: [
{ required: true, trigger: 'blur', message: '请输入您的账号' },
{ min: 4, max: 30, message: '用户账号长度必须介于 4 和 30 之间', trigger: 'blur' }
],
nickname: [
{ required: true, trigger: 'blur', message: '请输入您的昵称' },
{ min: 0, max: 30, message: '昵称长度必须介于 0 和 30 之间', trigger: 'blur' }
],
password: [
{ required: true, trigger: 'blur', message: '请输入您的密码' },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
],
confirmPassword: [
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
}
const registerData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
registerForm: {
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
nickname: '',
tenantId: 0,
username: '',
password: '',
confirmPassword: '',
captchaVerification: ''
}
})
//
const handleRegister = async (params: any) => {
loading.value = true
try {
if (registerData.tenantEnable) {
await getTenantId()
registerData.registerForm.tenantId = authUtil.getTenantId()
}
},
{
field: 'username',
label: t('login.username'),
value: '',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: t('login.usernamePlaceholder')
if (registerData.captchaEnable) {
registerData.registerForm.captchaVerification = params.captchaVerification
}
},
{
field: 'password',
label: t('login.password'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
const res = await LoginApi.register(registerData.registerForm)
if (!res) {
return
}
},
{
field: 'check_password',
label: t('login.checkPassword'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
loading.value = ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
authUtil.removeLoginForm()
authUtil.setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
},
{
field: 'code',
label: t('login.code'),
colProps: {
span: 24
// SSO
if (redirect.value.indexOf('sso') !== -1) {
window.location.href = window.location.href.replace('/login?redirect=', '')
} else {
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
} finally {
loginLoading.value = false
loading.value.close()
}
}
//
const getCode = async () => {
//
if (registerData.captchaEnable === 'false') {
await handleRegister({})
} else {
//
//
verify.value.show()
}
}
// ID
const getTenantId = async () => {
if (registerData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(registerData.registerForm.tenantName)
authUtil.setTenantId(res)
}
}
//
const getTenantByWebsite = async () => {
const website = location.host
const res = await LoginApi.getTenantByWebsite(website)
if (res) {
registerData.registerForm.tenantName = res.name
authUtil.setTenantId(res.id)
}
}
const loading = ref() // ElLoading.service
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
field: 'register',
colProps: {
span: 24
}
immediate: true
}
])
)
onMounted(() => {
// getCookie()
getTenantByWebsite()
})
</script>
const rules: FormRules = {
username: [required()],
password: [required()],
check_password: [required()],
code: [required()]
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
const loading = ref(false)
const loginRegister = async () => {
const formRef = unref(elFormRef)
formRef?.validate(async (valid) => {
if (valid) {
try {
loading.value = true
} finally {
loading.value = false
}
}
})
.login-code {
float: right;
width: 100%;
height: 38px;
img {
width: 100%;
height: auto;
max-width: 100px;
vertical-align: middle;
cursor: pointer;
}
}
</script>
</style>

@ -14,7 +14,7 @@
<el-checkbox
v-for="scope in queryParams.scopes"
:key="scope"
:label="scope"
:value="scope"
style="display: block; margin-bottom: -10px"
>
{{ formatScope(scope) }}

@ -2,8 +2,8 @@
<Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema">
<template #sex="form">
<el-radio-group v-model="form['sex']">
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio>
<el-radio :label="2">{{ t('profile.user.woman') }}</el-radio>
<el-radio :value="1">{{ t('profile.user.man') }}</el-radio>
<el-radio :value="2">{{ t('profile.user.woman') }}</el-radio>
</el-radio-group>
</template>
</Form>
@ -27,7 +27,7 @@ defineOptions({ name: 'BasicInfo' })
const { t } = useI18n()
const message = useMessage() //
const userStore = useUserStore()
const userStore = useUserStore()
//
const rules = reactive<FormRules>({
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],

@ -1,166 +0,0 @@
<template>
<el-container>
<!-- 左侧会话列表 -->
<el-aside width="260px" class="conversation-container">
<!-- 左顶部新建对话 -->
<el-button class="w-1/1" type="primary">
<Icon icon="ep:plus" class="mr-5px" />
新建对话
</el-button>
<!-- 左顶部搜索对话 -->
<el-input
v-model="searchName"
class="mt-10px"
placeholder="搜索历史记录"
@keyup="searchConversation"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<!-- 左中间对话列表 -->
<div class="conversation-list" :style="{ height: leftHeight + 'px' }">
<el-row v-for="conversation in conversationList" :key="conversation.id">
<div
:class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
@click="changeConversation(conversation)"
>
<el-image :src="conversation.avatar" class="avatar" />
<span class="title">{{ conversation.title }}</span>
<span class="button">
<!-- TODO 芋艿缺置顶按钮 -->
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
<Icon icon="ep:edit" />
</el-icon>
<el-icon title="删除会话" @click="deleteConversationTitle(conversation)">
<Icon icon="ep:delete" />
</el-icon>
</span>
</div>
</el-row>
</div>
<!-- 左底部工具栏 TODO 芋艿50% 不太对 -->
<div class="tool-box">
<sapn class="w-1/2"> <Icon icon="ep:user" /> 角色仓库 </sapn>
<sapn class="w-1/2"> <Icon icon="ep:delete" />清空未置顶对话</sapn>
</div>
</el-aside>
<!-- 右侧会话详情 -->
<el-container class="detail-container">
<!-- 右顶部 TODO 芋艿右对齐 -->
<el-header class="header">
<el-button>3.5-turbo-0125 <Icon icon="ep:setting" /></el-button>
<el-button>
<Icon icon="ep:user" />
</el-button>
<el-button>
<Icon icon="ep:download" />
</el-button>
<el-button>
<Icon icon="ep:arrow-up" />
</el-button>
</el-header>
<el-main>对话列表</el-main>
<el-footer>发送消息框</el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
const conversationList = [
{
id: 1,
title: '测试标题',
avatar:
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
},
{
id: 2,
title: '测试对话',
avatar:
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
}
]
const conversationId = ref(1)
const searchName = ref('')
const leftHeight = window.innerHeight - 240 // TODO
const changeConversation = (conversation) => {
console.log(conversation)
conversationId.value = conversation.id
// TODO
}
const updateConversationTitle = (conversation) => {
console.log(conversation)
// TODO
}
const deleteConversationTitle = (conversation) => {
console.log(conversation)
// TODO
}
const searchConversation = () => {
// TODO
}
</script>
<style lang="scss" scoped>
.conversation-container {
.conversation-list {
.conversation {
display: flex;
justify-content: flex-start;
width: 100%;
padding: 5px 5px 0 0;
cursor: pointer;
&.active {
// TODO
background-color: #343540;
.button {
display: inline;
}
}
.title {
padding: 5px 10px;
max-width: 220px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
}
.button {
position: absolute;
right: 2px;
top: 16px;
.el-icon {
margin-right: 5px;
}
}
}
.tool-box {
display: flex;
justify-content: flex-start;
padding: 0 20px 10px 20px;
border-top: 1px solid black;
}
}
}
.detail-container {
.header {
width: 100%;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: 10px;
}
}
</style>

@ -18,7 +18,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@ -42,6 +42,7 @@
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { CommonStatusEnum } from '@/utils/constants'
/** BPM 流程分类 表单 */
defineOptions({ name: 'CategoryForm' })
@ -57,7 +58,7 @@ const formData = ref({
id: undefined,
name: undefined,
code: undefined,
status: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
})
const formRules = reactive({
@ -116,7 +117,7 @@ const resetForm = () => {
id: undefined,
name: undefined,
code: undefined,
status: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
}
formRef.value?.resetFields()

@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->
@ -126,7 +126,6 @@
<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'

@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<el-table v-loading="loading" :data="list">
@ -70,13 +70,7 @@
<!-- 弹窗流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer
key="designer"
v-model="bpmnXml"
:value="bpmnXml as any"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
<MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
</Dialog>
</template>
@ -118,7 +112,7 @@ const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row) => {
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
//
setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
@ -133,13 +127,13 @@ const handleFormDetail = async (row) => {
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXml = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
const handleBpmnDetail = async (row) => {
bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
const bpmnXml = ref('')
const handleBpmnDetail = async (row: any) => {
//
bpmnXml.value = ''
bpmnDetailVisible.value = true
// BPMN XML
bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
}
/** 初始化 **/

@ -1,14 +1,18 @@
<template>
<ContentWrap>
<ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px">
<template #handle>
<el-button round size="small" type="primary" @click="handleSave">
<Icon class="mr-5px" icon="ep:plus" />
保存
</el-button>
</template>
</FcDesigner>
<div
class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
>
<fc-designer class="my-designer" ref="designer" :config="designerConfig">
<template #handle>
<el-button size="small" type="success" plain @click="handleSave">
<Icon class="mr-5px" icon="ep:plus" />
保存
</el-button>
</template>
</fc-designer>
</div>
</ContentWrap>
<!-- 表单保存的弹窗 -->
@ -22,7 +26,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@ -55,6 +59,35 @@ const { push, currentRoute } = useRouter() // 路由
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
//
const designerConfig = ref({
switchType: [], // ,
autoActive: true, //
useTemplate: false, // vue2
formOptions: {
form: {
labelWidth: '100px' // label 100px
}
}, //
fieldReadonly: false, // field
hiddenDragMenu: false, //
hiddenDragBtn: false, //
hiddenMenu: [], //
hiddenItem: [], //
hiddenItemConfig: {}, //
disabledItemConfig: {}, //
showSaveBtn: false, //
showConfig: true, //
showBaseForm: true, //
showControl: true, //
showPropsForm: true, //
showEventForm: true, //
showValidateForm: true, //
showFormConfig: true, //
showInputData: true, //
showDevice: true, //
appendConfigData: [] // formData
})
const designer = ref() //
useFormCreateDesigner(designer) //
const dialogVisible = ref(false) //
@ -119,3 +152,13 @@ onMounted(async () => {
setConfAndFields(designer, data.conf, data.fields)
})
</script>
<style>
.my-designer {
._fc-l,
._fc-m,
._fc-r {
border-top: none;
}
}
</style>

@ -1,5 +1,5 @@
<template>
<doc-alert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
@ -143,8 +143,9 @@ const openForm = (id?: number) => {
const toRouter: { name: string; query?: { id: number } } = {
name: 'BpmFormEditor'
}
console.log(typeof id)
// idevent
if (typeof id === 'number') {
if (typeof id === 'number' || typeof id === 'string') {
toRouter.query = {
id
}

@ -28,7 +28,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -0,0 +1,532 @@
<template>
<div class="flex items-center h-50px">
<!-- 头部分类名 -->
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isCategorySorting">
<Icon
:size="22"
icon="ic:round-drag-indicator"
class="ml-10px category-drag-icon cursor-move text-#8a909c"
/>
</el-tooltip>
<h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
<div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
</div>
<!-- 头部操作 -->
<div class="flex-1 flex" v-if="!isCategorySorting">
<div
v-if="categoryInfo.modelList.length > 0"
class="ml-20px flex items-center"
:class="[
'transition-transform duration-300 cursor-pointer',
isExpand ? 'rotate-180' : 'rotate-0'
]"
@click="isExpand = !isExpand"
>
<Icon icon="ep:arrow-down-bold" color="#999" />
</div>
<div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
<template v-if="!isModelSorting">
<el-button
v-if="categoryInfo.modelList.length > 0"
link
type="info"
class="mr-20px"
@click.stop="handleModelSort"
>
<Icon icon="fa:sort-amount-desc" class="mr-5px" />
排序
</el-button>
<el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
<Icon icon="fa:plus" class="mr-5px" />
新建
</el-button>
<el-dropdown
@command="(command) => handleCategoryCommand(command, categoryInfo)"
placement="bottom"
>
<el-button link type="info">
<Icon icon="ep:setting" class="mr-5px" />
分类
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
<el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button @click.stop="handleModelSortCancel"> </el-button>
<el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
</template>
</div>
</div>
</div>
<!-- 模型列表 -->
<el-collapse-transition>
<div v-show="isExpand">
<el-table
:class="categoryInfo.name"
ref="tableRef"
:header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }"
:cell-style="{ paddingLeft: '10px' }"
:row-style="{ height: '68px' }"
:data="modelList"
row-key="id"
>
<el-table-column label="流程名" prop="name" min-width="150">
<template #default="scope">
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isModelSorting">
<Icon
icon="ic:round-drag-indicator"
class="drag-icon cursor-move text-#8a909c mr-10px"
/>
</el-tooltip>
<el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
{{ scope.row.name }}
</div>
</template>
</el-table-column>
<el-table-column label="可见范围" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }} {{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="表单信息" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === BpmModelFormType.NORMAL"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else></label>
</template>
</el-table-column>
<el-table-column label="最后发布" prop="deploymentTime" min-width="250">
<template #default="scope">
<div class="flex items-center">
<span v-if="scope.row.processDefinition" class="w-150px">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openModelForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleModelCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
</el-collapse-transition>
<!-- 弹窗重命名分类 -->
<Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
<template #title>
<div class="pl-10px font-bold text-18px"> 重命名分类 </div>
</template>
<div class="px-30px">
<el-input v-model="renameCategoryForm.name" />
</div>
<template #footer>
<div class="pr-25px pb-25px">
<el-button @click="renameCategoryVisible = false"> </el-button>
<el-button type="primary" @click="handleRenameConfirm"> </el-button>
</div>
</template>
</Dialog>
<!-- 表单弹窗添加流程模型 -->
<ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
</template>
<script lang="ts" setup>
import ModelForm from './ModelForm.vue'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import Sortable from 'sortablejs'
import { propTypes } from '@/utils/propTypes'
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'BpmModel' })
const props = defineProps({
categoryInfo: propTypes.object.def([]), //
isCategorySorting: propTypes.bool.def(false) //
})
const emit = defineEmits(['success'])
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const userStore = useUserStoreWithOut() //
const isDark = computed(() => useAppStore().getIsDark) //
const isModelSorting = ref(false) //
const originalData: any = ref([]) //
const modelList: any = ref([]) //
const isExpand = ref(false) //
/** '更多'操作按钮 */
const handleModelCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** '分类'操作按钮 */
const handleCategoryCommand = async (command: string, row: any) => {
switch (command) {
case 'handleRename':
renameCategoryForm.value = await CategoryApi.getCategory(row.id)
renameCategoryVisible.value = true
break
case 'handleDeleteCategory':
await handleDeleteCategory()
break
default:
break
}
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
try {
//
await message.delConfirm()
//
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
//
emit('success')
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
//
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
//
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
//
emit('success')
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleModelDesign',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
//
await message.confirm('是否部署该流程!!')
//
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
//
emit('success')
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row: any) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
//
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
//
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 处理模型的排序 **/
const handleModelSort = () => {
//
originalData.value = cloneDeep(props.categoryInfo.modelList)
isModelSorting.value = true
initSort()
}
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
//
const ids = modelList.value.map((item: any) => item.id)
await ModelApi.updateModelSortBatch(ids)
//
isModelSorting.value = false
message.success('排序模型成功')
emit('success')
}
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
//
modelList.value = cloneDeep(originalData.value)
isModelSorting.value = false
}
/** 创建拖拽实例 */
const tableRef = ref()
const initSort = () => {
const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
Sortable.create(table, {
group: 'shared',
animation: 150,
draggable: '.el-table__row',
handle: '.drag-icon',
//
onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
if (oldDraggableIndex !== newDraggableIndex) {
modelList.value.splice(
newDraggableIndex,
0,
modelList.value.splice(oldDraggableIndex, 1)[0]
)
}
}
})
}
/** 更新 modelList 模型列表 */
const updateModeList = () => {
modelList.value = cloneDeep(props.categoryInfo.modelList)
if (props.categoryInfo.modelList.length > 0) {
isExpand.value = true
}
}
/** 重命名弹窗确定 */
const renameCategoryVisible = ref(false)
const renameCategoryForm = ref({
name: ''
})
const handleRenameConfirm = async () => {
if (renameCategoryForm.value?.name.length === 0) {
return message.warning('请输入名称')
}
//
await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
message.success('重命名成功')
//
renameCategoryVisible.value = false
emit('success')
}
/** 删除分类 */
const handleDeleteCategory = async () => {
try {
if (props.categoryInfo.modelList.length > 0) {
return message.warning('该分类下仍有流程定义,不允许删除')
}
await message.confirm('确认删除分类吗?')
//
await CategoryApi.deleteCategory(props.categoryInfo.id)
message.success(t('common.delSuccess'))
//
emit('success')
} catch {}
}
/** 添加流程模型弹窗 */
const modelFormRef = ref()
const openModelForm = (type: string, id?: number) => {
modelFormRef.value.open(type, id)
}
watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
watch(
() => props.isCategorySorting,
(val) => {
if (val) isExpand.value = false
},
{ immediate: true }
)
</script>
<style lang="scss">
.rename-dialog.el-dialog {
padding: 0 !important;
.el-dialog__header {
border-bottom: none;
}
.el-dialog__footer {
border-top: none !important;
}
}
</style>
<style lang="scss" scoped>
:deep() {
.el-table__cell {
overflow: hidden;
border-bottom: none !important;
}
}
</style>

@ -8,12 +8,7 @@
label-width="110px"
>
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
:disabled="!!formData.id"
placeholder="请输入流标标识"
style="width: 330px"
/>
<el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
<el-tooltip
v-if="!formData.id"
class="item"
@ -35,7 +30,7 @@
placeholder="请输入流程名称"
/>
</el-form-item>
<el-form-item v-if="formData.id" label="流程分类" prop="category">
<el-form-item label="流程分类" prop="category">
<el-select
v-model="formData.category"
clearable
@ -50,73 +45,108 @@
/>
</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 label="流程图标" prop="icon">
<UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
<div v-if="formData.id">
<el-form-item label="表单类型" prop="formType">
<el-radio-group v-model="formData.formType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
<el-select v-model="formData.formId" clearable style="width: 100%">
<el-option
v-for="form in formList"
:key="form.id"
:label="form.name"
:value="form.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单提交路由"
prop="formCustomCreatePath"
<el-form-item label="流程类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="表单类型" prop="formType">
<el-radio-group v-model="formData.formType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
<el-select v-model="formData.formId" clearable style="width: 100%">
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单提交路由"
prop="formCustomCreatePath"
>
<el-input
v-model="formData.formCustomCreatePath"
placeholder="请输入表单提交路由"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的提交路径,使用 Vue 的路由地址例如说bpm/oa/leave/create.vue"
effect="light"
placement="top"
>
<el-input
v-model="formData.formCustomCreatePath"
placeholder="请输入表单提交路由"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的提交路径,使用 Vue 的路由地址例如说bpm/oa/leave/create"
effect="light"
placement="top"
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
<el-input
v-model="formData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的查看组件地址,使用 Vue 的组件地址例如说bpm/oa/leave/detail.vue"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item label="是否可见" prop="visible">
<el-radio-group v-model="formData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:label="dict.value"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单查看地址"
prop="formCustomViewPath"
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="谁可以发起" prop="startUserIds">
<el-select
v-model="formData.startUserIds"
multiple
placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
>
<el-input
v-model="formData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
style="width: 330px"
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
<el-tooltip
class="item"
content="自定义表单的查看组件地址,使用 Vue 的组件地址例如说bpm/oa/leave/detail"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
</div>
</el-select>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserIds">
<el-select v-model="formData.managerUserIds" 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>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
@ -125,45 +155,65 @@
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE, getBoolDictOptions, 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'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'ModelForm' })
const { t } = useI18n() //
const message = useMessage() //
const userStore = useUserStoreWithOut() //
const props = defineProps({
categoryId: propTypes.number
})
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
formType: 10,
id: undefined,
name: '',
key: '',
category: undefined,
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: ''
formCustomViewPath: '',
visible: true,
startUserIds: [],
managerUserIds: []
})
const formRules = reactive({
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' }]
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const formList = ref([]) //
const categoryList = ref([]) //
const userList = ref<UserVO[]>([]) //
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
const open = async (type: string, id?: string) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
@ -176,11 +226,18 @@ const open = async (type: string, id?: number) => {
} finally {
formLoading.value = false
}
} else {
formData.value.managerUserIds.push(userStore.getUser.id)
}
//
formList.value = await FormApi.getFormSimpleList()
//
categoryList.value = await CategoryApi.getCategorySimpleList()
//
userList.value = await UserApi.getSimpleUserList()
if (props.categoryId) {
formData.value.category = props.categoryId
}
}
defineExpose({ open }) // open
@ -199,10 +256,9 @@ const submitForm = async () => {
await ModelApi.createModel(data)
//
await ElMessageBox.alert(
'<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
'<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
'<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
'<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
'重要提示',
{
@ -225,14 +281,20 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
formType: 10,
id: undefined,
name: '',
key: '',
category: undefined,
icon: '',
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: ''
formCustomViewPath: '',
visible: true,
startUserIds: [],
managerUserIds: []
}
formRef.value?.resetFields()
}

@ -1,141 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="导入流程" width="400">
<div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:data="formData"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".bpmn, .xml"
drag
name="bpmnFile"
>
<Icon class="el-icon--upload" icon="ep:upload-filled" />
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入bpmxml格式文件
</div>
<div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
placeholder="请输入流标标识"
style="width: 250px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
</el-form>
</div>
</template>
</el-upload>
</div>
<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 { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'ModelImportForm' })
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
key: '',
name: '',
description: ''
})
const formRules = reactive({
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const uploadRef = ref() // Ref
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
const uploadHeaders = ref() // Header
const fileList = ref([]) //
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // open
/** 提交表单 */
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenantId': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emit = defineEmits(['success']) // success
const submitFormSuccess = async (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
//
message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
dialogVisible.value = false
//
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('导入流程失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
//
formLoading.value = false
uploadRef.value?.clearFiles()
//
formData.value = {
key: '',
name: '',
description: ''
}
formRef.value?.resetFields()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

@ -31,6 +31,7 @@ import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/d
//
import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette'
import * as ModelApi from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
defineOptions({ name: 'BpmModelEditor' })
@ -38,6 +39,12 @@ const router = useRouter() // 路由
const { query } = useRoute() //
const message = useMessage() //
//
const formFields = ref<string[]>([])
const formType = ref(20)
provide('formFields', formFields)
provide('formType', formType)
const xmlString = ref(undefined) // BPMN XML
const modeler = ref(null) // BPMN Modeler
const controlForm = ref({
@ -58,17 +65,17 @@ const initModeler = (item) => {
}
/** 添加/修改模型 */
const save = async (bpmnXml) => {
const save = async (bpmnXml: string) => {
const data = {
...model.value,
bpmnXml: bpmnXml // bpmnXml
} as unknown as ModelApi.ModelVO
//
if (data.id) {
await ModelApi.updateModel(data)
await ModelApi.updateModelBpmn(data)
message.success('修改成功')
} else {
await ModelApi.createModel(data)
await ModelApi.updateModelBpmn(data)
message.success('新增成功')
}
//
@ -99,6 +106,13 @@ onMounted(async () => {
</bpmndi:BPMNDiagram>
</definitions>`
}
formType.value = data.formType
if (data.formType === 10) {
const bpmnForm = (await getForm(data.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
model.value = {
...data,
bpmnXml: undefined // bpmnXml

@ -1,409 +1,221 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="流程标识" prop="key">
<el-input
v-model="queryParams.key"
placeholder="请输入流程标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</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="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>
<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:model:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新建流程
</el-button>
<el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
<Icon icon="ep:upload" class="mr-5px" /> 导入流程
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="流程标识" align="center" prop="key" width="200" />
<el-table-column label="流程名称" align="center" prop="name" width="200">
<template #default="scope">
<el-button type="primary" link @click="handleBpmnDetail(scope.row)">
<span>{{ scope.row.name }}</span>
</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">
<el-button
v-if="scope.row.formType === 10"
type="primary"
link
@click="handleFormDetail(scope.row)"
<div class="flex justify-between pl-20px items-center">
<h3 class="font-extrabold">流程模型</h3>
<!-- 搜索工作栏 -->
<el-form
v-if="!isCategorySorting"
class="-mb-15px flex mr-10px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
@submit.prevent
>
<el-form-item prop="name" class="ml-auto">
<el-input
v-model="queryParams.name"
placeholder="搜索流程"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
>
<span>{{ scope.row.formName }}</span>
<template #prefix>
<Icon icon="ep:search" class="mx-10px" />
</template>
</el-input>
</el-form-item>
<!-- 右上角新建模型更多操作 -->
<el-form-item>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新建模型
</el-button>
<el-button
v-else-if="scope.row.formType === 20"
type="primary"
link
@click="handleFormDetail(scope.row)"
</el-form-item>
<el-form-item>
<el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
<el-button class="w-30px" plain>
<Icon icon="ep:setting" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleCategoryAdd">
<Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
新建分类
</el-dropdown-item>
<el-dropdown-item command="handleCategorySort">
<Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
分类排序
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
</el-form>
<div class="mr-20px" v-else>
<el-button @click="handleCategorySortCancel"> </el-button>
<el-button type="primary" @click="handleCategorySortSubmit"> </el-button>
</div>
</div>
<el-divider />
<!-- 按照分类展示其所属的模型列表 -->
<div class="px-15px">
<draggable
:disabled="!isCategorySorting"
v-model="categoryGroup"
item-key="id"
:animation="400"
>
<template #item="{ element }">
<ContentWrap
class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
v-loading="loading"
:body-style="{ padding: 0 }"
:key="element.id"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else></label>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="最新部署的流程定义" align="center">
<el-table-column
label="流程版本"
align="center"
prop="processDefinition.version"
width="100"
>
<template #default="scope">
<el-tag v-if="scope.row.processDefinition">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
</template>
</el-table-column>
<el-table-column
label="激活状态"
align="center"
prop="processDefinition.version"
width="85"
>
<template #default="scope">
<el-switch
v-if="scope.row.processDefinition"
v-model="scope.row.processDefinition.suspensionState"
:active-value="1"
:inactive-value="2"
@change="handleChangeState(scope.row)"
<CategoryDraggableModel
:isCategorySorting="isCategorySorting"
:categoryInfo="element"
@success="getList"
/>
</template>
</el-table-column>
<el-table-column label="部署时间" align="center" prop="deploymentTime" width="180">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="操作" align="center" width="240" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
>
修改流程
</el-button>
<el-button
link
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
>
设计流程
</el-button>
<el-button
link
type="primary"
@click="handleSimpleDesign(scope.row.id)"
v-hasPermi="['bpm:model:update']"
>
仿钉钉设计流程
</el-button>
<el-button
link
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
>
发布流程
</el-button>
<el-button
link
type="primary"
v-hasPermi="['bpm:process-definition:query']"
@click="handleDefinitionList(scope.row)"
>
流程定义
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['bpm:model:delete']"
>
删除
</el-button>
</ContentWrap>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</draggable>
</div>
</ContentWrap>
<!-- 表单弹窗添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 表单弹窗导入流程 -->
<ModelImportForm ref="importFormRef" @success="getList" />
<!-- 表单弹窗添加分类 -->
<CategoryForm ref="categoryFormRef" @success="getList" />
<!-- 弹窗表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</Dialog>
<!-- 弹窗流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML as any"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog>
</template>
<script lang="ts" setup>
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import draggable from 'vuedraggable'
import { CategoryApi } from '@/api/bpm/category'
import * as ModelApi from '@/api/bpm/model'
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'
import CategoryForm from '../category/CategoryForm.vue'
import { cloneDeep } from 'lodash-es'
import CategoryDraggableModel from './CategoryDraggableModel.vue'
defineOptions({ name: 'BpmModel' })
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const isCategorySorting = ref(false) // category
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
key: undefined,
name: undefined,
category: undefined
name: undefined
})
const queryFormRef = ref() //
const categoryList = ref([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ModelApi.getModelPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const categoryGroup: any = ref([]) // category
const originalData: any = ref([]) //
/** 搜索按钮操作 */
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 importFormRef = ref()
const openImportForm = () => {
importFormRef.value.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await ModelApi.deleteModel(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
/** 更新状态操作 */
const handleChangeState = async (row) => {
const state = row.processDefinition.suspensionState
try {
//
const id = row.id
const statusState = state === 1 ? '激活' : '挂起'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
//
await ModelApi.updateModelState(id, state)
//
await getList()
} catch {
//
row.processDefinition.suspensionState = state === 1 ? 2 : 1
/** 右上角设置按钮 */
const handleCommand = (command: string) => {
switch (command) {
case 'handleCategoryAdd':
handleCategoryAdd()
break
case 'handleCategorySort':
handleCategorySort()
break
default:
break
}
}
/** 设计流程 */
const handleDesign = (row) => {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
/** 新建分类 */
const categoryFormRef = ref()
const handleCategoryAdd = () => {
categoryFormRef.value.open('create')
}
const handleSimpleDesign = (row) => {
push({
name: 'SimpleWorkflowDesignEditor',
query: {
modelId: row.id
}
})
/** 分类排序的提交 */
const handleCategorySort = () => {
//
originalData.value = cloneDeep(categoryGroup.value)
isCategorySorting.value = true
}
/** 发布流程 */
const handleDeploy = async (row) => {
try {
//
await message.confirm('是否部署该流程!!')
//
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
//
await getList()
} catch {}
/** 分类排序的取消 */
const handleCategorySortCancel = () => {
//
categoryGroup.value = cloneDeep(originalData.value)
isCategorySorting.value = false
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
/** 分类排序的保存 */
const handleCategorySortSubmit = async () => {
//
const ids = categoryGroup.value.map((item: any) => item.id)
await CategoryApi.updateCategorySortBatch(ids)
//
isCategorySorting.value = false
message.success('排序分类成功')
await getList()
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row) => {
if (row.formType == 10) {
//
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
//
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
/** 加载数据 */
const getList = async () => {
loading.value = true
try {
// +
const modelList = await ModelApi.getModelList(queryParams.name)
const categoryList = await CategoryApi.getCategorySimpleList()
// category
// categoryGroup
categoryGroup.value = categoryList.map((category: any) => ({
...category,
modelList: modelList.filter((model: any) => model.categoryName == category.name)
}))
} finally {
loading.value = false
}
}
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
const handleBpmnDetail = async (row) => {
const data = await ModelApi.getModel(row.id)
bpmnXML.value = data.bpmnXml || ''
bpmnDetailVisible.value = true
}
/** 初始化 **/
onMounted(async () => {
await getList()
//
categoryList.value = await CategoryApi.getCategorySimpleList()
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
:deep() {
.el-table--fit .el-table__inner-wrapper:before {
height: 0;
}
.el-card {
border-radius: 8px;
}
.el-form--inline .el-form-item {
margin-right: 10px;
}
.el-divider--horizontal {
margin-top: 6px;
}
}
</style>

@ -0,0 +1,404 @@
<template>
<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>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="流程标识" prop="key">
<el-input
v-model="queryParams.key"
placeholder="请输入流程标识"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</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="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>
<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:model:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新建
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="流程名称" align="center" prop="name" min-width="200" />
<el-table-column label="流程图标" align="center" prop="icon" min-width="100">
<template #default="scope">
<el-image :src="scope.row.icon" class="h-32px w-32px" />
</template>
</el-table-column>
<el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }} {{ scope.row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
<el-table-column label="表单信息" align="center" prop="formType" min-width="200">
<template #default="scope">
<el-button
v-if="scope.row.formType === 10"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formName }}</span>
</el-button>
<el-button
v-else-if="scope.row.formType === 20"
type="primary"
link
@click="handleFormDetail(scope.row)"
>
<span>{{ scope.row.formCustomCreatePath }}</span>
</el-button>
<label v-else></label>
</template>
</el-table-column>
<el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
<template #default="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
<el-tag v-if="scope.row.processDefinition" class="ml-10px">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
<el-tag
v-if="scope.row.processDefinition?.suspensionState === 2"
type="warning"
class="ml-10px"
>
已停用
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
:disabled="!isManagerUser(scope.row)"
>
设计
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)"
>
发布
</el-button>
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
历史
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
:disabled="!isManagerUser(scope.row)"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 弹窗表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
</Dialog>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue'
import { setConfAndFields2 } from '@/utils/formCreate'
import { CategoryApi } from '@/api/bpm/category'
import { BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmModel' })
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const userStore = useUserStoreWithOut() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
key: undefined,
name: undefined,
category: undefined
})
const queryFormRef = ref() //
const categoryList = ref([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ModelApi.getModelList(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 handleCommand = (command: string, row: any) => {
switch (command) {
case 'handleDefinitionList':
handleDefinitionList(row)
break
case 'handleDelete':
handleDelete(row)
break
case 'handleChangeState':
handleChangeState(row)
break
default:
break
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
try {
//
await message.delConfirm()
//
await ModelApi.deleteModel(row.id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
const newState = state === 1 ? 2 : 1
try {
//
const id = row.id
debugger
const statusState = state === 1 ? '停用' : '启用'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
//
await ModelApi.updateModelState(id, newState)
message.success(statusState + '成功')
//
await getList()
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
if (row.type == BpmModelType.BPMN) {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
} else {
push({
name: 'SimpleModelDesign',
query: {
modelId: row.id
}
})
}
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
//
await message.confirm('是否部署该流程!!')
//
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
//
await getList()
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
rule: [],
option: {}
})
const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
//
const data = await FormApi.getForm(row.formId)
setConfAndFields2(formDetailPreview, data.conf, data.fields)
//
formDetailVisible.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
const userId = userStore.getUser.id
return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 初始化 **/
onMounted(async () => {
await getList()
//
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>

@ -1,5 +1,5 @@
<template>
<doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
<ContentWrap>
<!-- 搜索工作栏 -->
@ -36,9 +36,9 @@
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="审批结果" prop="result">
<el-form-item label="审批结果" prop="status">
<el-select
v-model="queryParams.result"
v-model="queryParams.status"
class="!w-240px"
clearable
placeholder="请选择审批结果"
@ -81,7 +81,7 @@
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="申请编号" prop="id" />
<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_STATUS" :value="scope.row.status" />
</template>

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,5 +1,5 @@
<template>
<doc-alert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -0,0 +1,261 @@
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
<div class="processInstance-wrap-main">
<el-scrollbar>
<div class="text-#878c93 h-15px">流程{{ selectProcessDefinition.name }}</div>
<el-divider class="!my-8px" />
<!-- 中间主要内容 tab -->
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="表单填写" name="form" >
<div class="form-scroll-area" v-loading="processInstanceStartLoading">
<el-scrollbar>
<el-row>
<el-col :span="17">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
v-model="detailForm.value"
:option="detailForm.option"
@submit="submitForm"
/>
</el-col>
<el-col :span="6" :offset="1">
<!-- 流程时间线 -->
<ProcessInstanceTimeline
ref="timelineRef"
:activity-nodes="activityNodes"
:show-status-icon="false"
@select-user-confirm="selectUserConfirm"
/>
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<div class="form-scroll-area">
<!-- BPMN 流程图预览 -->
<ProcessInstanceBpmnViewer
:bpmn-xml="bpmnXML"
v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
/>
<!-- Simple 流程图预览 -->
<ProcessInstanceSimpleViewer
:simple-json="simpleJson"
v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
/>
</div>
</el-tab-pane>
</el-tabs>
<!-- 底部操作栏 -->
<div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
<!-- 操作栏按钮 -->
<div
v-if="activeTab === 'form'"
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
>
<el-button plain type="success" @click="submitForm">
<Icon icon="ep:select" />&nbsp; 发起
</el-button>
<el-button plain type="danger" @click="handleCancel">
<Icon icon="ep:close" />&nbsp; 取消
</el-button>
</div>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelType } from '@/utils/constants'
import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
defineOptions({ name: 'ProcessDefinitionDetail' })
const props = defineProps<{
selectProcessDefinition: any
}>()
const emit = defineEmits(['cancel'])
const processInstanceStartLoading = ref(false) //
const { push, currentRoute } = useRouter() //
const message = useMessage() //
const { delView } = useTagsViewStore() //
const detailForm: any = ref({
rule: [],
option: {},
value: {}
}) //
const fApi = ref<ApiAttrs>()
//
const startUserSelectTasks: any = ref([]) //
const startUserSelectAssignees = ref({}) //
const bpmnXML: any = ref(null) // BPMN
const simpleJson = ref<string | undefined>() // Simple json
const activeTab = ref('form') // Tab
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) //
/** 设置表单信息、获取流程图数据 **/
const initProcessInfo = async (row: any, formVariables?: any) => {
//
startUserSelectTasks.value = []
startUserSelectAssignees.value = {}
//
if (row.formType == 10) {
//
// formVariables row.formFields
// formVariables
//
const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
for (const key in formVariables) {
if (!allowedFields.includes(key)) {
delete formVariables[key]
}
}
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
await nextTick()
fApi.value?.btn.show(false) //
//
await getApprovalDetail(row)
//
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
simpleJson.value = processDefinitionDetail.simpleModel
}
//
} else if (row.formCustomCreatePath) {
await push({
path: row.formCustomCreatePath
})
// Tab
}
}
/** 获取审批详情 */
const getApprovalDetail = async (row: any) => {
try {
const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
if (!data) {
message.error('查询不到审批详情信息!')
return
}
//
startUserSelectTasks.value = data.activityNodes?.filter(
(node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
)
if (startUserSelectTasks.value?.length > 0) {
for (const node of startUserSelectTasks.value) {
startUserSelectAssignees.value[node.id] = []
}
}
// Timeline
activityNodes.value = data.activityNodes
} finally {
}
}
/** 提交按钮 */
const submitForm = async () => {
if (!fApi.value || !props.selectProcessDefinition) {
return
}
//
await fApi.value.validate()
//
if (startUserSelectTasks.value?.length > 0) {
for (const userTask of startUserSelectTasks.value) {
if (
Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
startUserSelectAssignees.value[userTask.id].length === 0
)
return message.warning(`请选择${userTask.name}的候选人`)
}
}
//
processInstanceStartLoading.value = true
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: props.selectProcessDefinition.id,
variables: detailForm.value.value,
startUserSelectAssignees: startUserSelectAssignees.value
})
//
message.success('发起流程成功')
//
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
processInstanceStartLoading.value = false
}
}
/** 取消发起审批 */
const handleCancel = () => {
emit('cancel')
}
/** 选择发起人 */
const selectUserConfirm = (id: string, userList: any[]) => {
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
}
defineExpose({ initProcessInfo })
</script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 105px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>

@ -1,133 +1,115 @@
<template>
<!-- 第一步通过流程定义的列表选择对应的流程 -->
<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 v-if="!selectProcessDefinition">
<el-input
v-model="searchName"
class="!w-50% mb-15px"
placeholder="请输入流程名称"
clearable
@input="handleQuery"
@clear="handleQuery"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<ContentWrap
:class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
class="position-relative pb-20px h-700px"
v-loading="loading"
>
<el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
<el-col :span="5">
<div class="flex flex-col">
<div
v-for="category in availableCategories"
:key="category.code"
class="flex items-center p-10px cursor-pointer text-14px rounded-md"
:class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
@click="handleCategoryClick(category)"
>
<template #default>
<div class="flex">
<el-image :src="definition.icon" 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">申请信息{{ selectProcessDefinition.name }}</span>
<el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
<Icon icon="ep:delete" /> 选择其它流程
</el-button>
</div>
<el-col :span="16" :offset="6" style="margin-top: 20px">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
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"
{{ category.name }}
</div>
</div>
</el-col>
<el-col :span="19">
<el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
<div
class="mb-20px pl-10px"
v-for="(definitions, categoryCode) in processDefinitionGroup"
:key="categoryCode"
:ref="`category-${categoryCode}`"
>
<h3 class="text-18px font-bold mb-10px mt-5px">
{{ getCategoryName(categoryCode as any) }}
</h3>
<div class="grid grid-cols-3 gap3">
<el-tooltip
v-for="definition in definitions"
:key="definition.id"
:content="definition.description"
:disabled="!definition.description || definition.description.trim().length === 0"
placement="top"
>
<el-form-item
v-for="userTask in startUserSelectTasks"
:key="userTask.id"
:label="`任务【${userTask.name}】`"
:prop="userTask.id"
<el-card
shadow="hover"
class="cursor-pointer definition-item-card"
@click="handleSelect(definition)"
>
<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>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
</ContentWrap>
<template #default>
<div class="flex">
<el-image :src="definition.icon" class="w-32px h-32px" />
<el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
</div>
</template>
</el-card>
</el-tooltip>
</div>
</div>
</el-scrollbar>
</el-col>
</el-row>
<el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
</ContentWrap>
</template>
<!-- 第二步填写表单进行流程的提交 -->
<ProcessDefinitionDetail
v-else
ref="processDefinitionDetailRef"
:selectProcessDefinition="selectProcessDefinition"
@cancel="selectProcessDefinition = undefined"
/>
</template>
<script lang="ts" setup>
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'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as UserApi from '@/api/system/user'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
import { groupBy } from 'lodash-es'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const { proxy } = getCurrentInstance() as any
const route = useRoute() //
const { push, currentRoute } = useRouter() //
const message = useMessage() //
const { delView } = useTagsViewStore() //
const processInstanceId = route.query.processInstanceId
const searchName = ref('') //
const processInstanceId: any = route.query.processInstanceId //
const loading = ref(true) //
const categoryList = ref([]) //
const categoryActive = ref('') //
const categoryList: any = ref([]) //
const categoryActive: any = ref({}) //
const processDefinitionList = ref([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
//
categoryList.value = await CategoryApi.getCategorySimpleList()
if (categoryList.value.length > 0) {
categoryActive.value = categoryList.value[0].code
}
//
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
//
await getCategoryList()
//
await getProcessDefinitionList()
// processInstanceId
if (processInstanceId?.length > 0) {
@ -137,7 +119,7 @@ const getList = async () => {
return
}
const processDefinition = processDefinitionList.value.find(
(item) => item.key == processInstance.processDefinition?.key
(item: any) => item.key == processInstance.processDefinition?.key
)
if (!processDefinition) {
message.error('重新发起流程失败,原因:流程定义不存在')
@ -150,108 +132,168 @@ const getList = async () => {
}
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList = computed(() => {
return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
/** 获取所有流程分类数据 */
const getCategoryList = async () => {
try {
//
categoryList.value = await CategoryApi.getCategorySimpleList()
} finally {
}
}
/** 获取所有流程定义数据 */
const getProcessDefinitionList = async () => {
try {
//
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
//
filteredProcessDefinitionList.value = processDefinitionList.value
//
if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
categoryActive.value = availableCategories.value[0]
}
} finally {
}
}
/** 搜索流程 */
const filteredProcessDefinitionList = ref([]) //
const handleQuery = () => {
if (searchName.value.trim()) {
//
filteredProcessDefinitionList.value = processDefinitionList.value.filter(
(definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) //
)
} else {
//
filteredProcessDefinitionList.value = processDefinitionList.value
}
}
/** 流程定义的分组 */
const processDefinitionGroup: any = computed(() => {
if (!processDefinitionList.value?.length) {
return {}
}
const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
// categoryList
const orderedGroup = {}
categoryList.value.forEach((category: any) => {
if (grouped[category.code]) {
orderedGroup[category.code] = grouped[category.code]
}
})
return orderedGroup
})
/** 左侧分类切换 */
const handleCategoryClick = (category: any) => {
categoryActive.value = category
const categoryRef = proxy.$refs[`category-${category.code}`] // DOM
if (categoryRef?.length) {
const scrollWrapper = proxy.$refs.scrollWrapper //
const categoryOffsetTop = categoryRef[0].offsetTop
//
scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
}
}
/** 通过分类 code 获取对应的名称 */
const getCategoryName = (categoryCode: string) => {
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
}
// ========== ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
rule: [],
option: {},
value: {}
}) //
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 processDefinitionDetailRef = ref()
/** 处理选择流程的按钮操作 **/
const handleSelect = async (row, formVariables) => {
const handleSelect = async (row, formVariables?) => {
//
selectProcessDefinition.value = row
//
await nextTick()
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
}
//
startUserSelectTasks.value = []
startUserSelectAssignees.value = {}
startUserSelectAssigneesFormRules.value = {}
//
if (row.formType == 10) {
//
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
//
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
/** 处理滚动事件,和左侧分类联动 */
const handleScroll = (e: any) => {
// 使
const scrollTop = e.scrollTop
//
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' }
]
//
const categoryPositions = categoryList.value
.map((category: CategoryVO) => {
const categoryRef = proxy.$refs[`category-${category.code}`]
if (categoryRef?.[0]) {
return {
code: category.code,
offsetTop: categoryRef[0].offsetTop,
height: categoryRef[0].offsetHeight
}
//
userList.value = await UserApi.getSimpleUserList()
}
}
//
} else if (row.formCustomCreatePath) {
await push({
path: row.formCustomCreatePath
return null
})
// Tab
}
}
.filter(Boolean)
/** 提交按钮 */
const submitForm = async (formData) => {
if (!fApi.value || !selectProcessDefinition.value) {
return
}
//
if (startUserSelectTasks.value?.length > 0) {
await startUserSelectAssigneesFormRef.value.validate()
//
let currentCategory = categoryPositions[0]
for (const position of categoryPositions) {
// 50px
if (scrollTop >= position.offsetTop - 50) {
currentCategory = position
} else {
break
}
}
//
fApi.value.btn.loading(true)
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: selectProcessDefinition.value.id,
variables: formData,
startUserSelectAssignees: startUserSelectAssignees.value
})
//
message.success('发起流程成功')
//
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
fApi.value.btn.loading(false)
// active
if (currentCategory && categoryActive.value.code !== currentCategory.code) {
categoryActive.value = categoryList.value.find(
(c: CategoryVO) => c.code === currentCategory.code
)
}
}
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
const availableCategories = computed(() => {
if (!categoryList.value?.length || !processDefinitionGroup.value) {
return []
}
//
const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
//
return categoryList.value.filter((category: CategoryVO) =>
availableCategoryCodes.includes(category.code)
)
})
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.process-definition-container::before {
content: '';
border-left: 1px solid #e6e6e6;
position: absolute;
left: 20.8%;
height: 100%;
}
:deep() {
.definition-item-card {
.el-card__body {
padding: 14px;
}
}
}
</style>

@ -0,0 +1,267 @@
<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">
<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">
<el-image :src="definition.icon" 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">申请信息{{ selectProcessDefinition.name }}</span>
<el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
<Icon icon="ep:delete" /> 选择其它流程
</el-button>
</div>
<el-col :span="16" :offset="6" style="margin-top: 20px">
<form-create
:rule="detailForm.rule"
v-model:api="fApi"
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>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { decodeFields, 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'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmProcessInstanceCreate' })
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('') //
const processDefinitionList = ref([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
//
categoryList.value = await CategoryApi.getCategorySimpleList()
if (categoryList.value.length > 0) {
categoryActive.value = categoryList.value[0].code
}
//
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
}
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList = computed(() => {
return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
})
// ========== ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
rule: [],
option: {},
value: {}
}) //
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) {
//
// formVariables row.formFields
// formVariables
//
const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
for (const key in formVariables) {
if (!allowedFields.includes(key)) {
delete formVariables[key]
}
}
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
//
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({
path: row.formCustomCreatePath
})
// Tab
}
}
/** 提交按钮 */
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,
startUserSelectAssignees: startUserSelectAssignees.value
})
//
message.success('发起流程成功')
//
delView(unref(currentRoute))
await push({
name: 'BpmProcessInstanceMy'
})
} finally {
fApi.value.btn.loading(false)
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

@ -1,54 +1,61 @@
<template>
<el-card v-loading="loading" class="box-card">
<template #header>
<span class="el-icon-picture-outline">流程图</span>
</template>
<MyProcessViewer
key="designer"
:activityData="activityList"
:prefix="bpmnControlForm.prefix"
:processInstanceData="processInstance"
:taskData="tasks"
:value="bpmnXml"
v-bind="bpmnControlForm"
/>
<MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
</el-card>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as ActivityApi from '@/api/bpm/activity'
defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
const props = defineProps({
loading: propTypes.bool, //
id: propTypes.string, //
processInstance: propTypes.any, //
tasks: propTypes.array, //
bpmnXml: propTypes.string // BPMN XML
loading: propTypes.bool.def(false), //
bpmnXml: propTypes.string, // BPMN XML
modelView: propTypes.object
})
const bpmnControlForm = ref({
prefix: 'flowable'
})
const activityList = ref([]) //
const view = ref({
bpmnXml: ''
}) // BPMN
/** 只有 loading 完成时,才去加载流程列表 */
watch(
() => props.loading,
async (value) => {
if (value && props.id) {
activityList.value = await ActivityApi.getActivityList({
processInstanceId: props.id
})
() => props.modelView,
async (newModelView) => {
//
if (newModelView) {
//@ts-ignore
view.value = newModelView
}
}
)
/** 监听 bpmnXml */
watch(
() => props.bpmnXml,
(value) => {
view.value.bpmnXml = value
}
)
</script>
<style>
<style lang="scss" scoped>
.box-card {
height: 100%;
width: 100%;
margin-bottom: 20px;
margin-bottom: 0;
:deep(.el-card__body) {
height: 100%;
padding: 0;
}
:deep(.process-viewer) {
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
}
</style>

@ -0,0 +1,989 @@
<template>
<div
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
>
<!-- 通过按钮 -->
<el-popover
:visible="popOverVisible.approve"
placement="top-end"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)"
>
<template #reference>
<el-button plain type="success" @click="openPopover('approve')">
<Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="approveFormRef"
:model="approveReasonForm"
:rules="approveReasonRule"
label-width="100px"
>
<el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
<template #header>
<span class="el-icon-picture-outline"> 填写表单{{ runningTask?.formName }} </span>
</template>
<form-create
v-model="approveForm.value"
v-model:api="approveFormFApi"
:option="approveForm.option"
:rule="approveForm.rule"
/>
</el-card>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="approveReasonForm.reason"
placeholder="请输入审批意见"
type="textarea"
:rows="4"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)">
{{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
<el-button @click="closePropover('approve', approveFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 拒绝按钮 -->
<el-popover
:visible="popOverVisible.reject"
placement="top-end"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)"
>
<template #reference>
<el-button class="mr-20px" plain type="danger" @click="openPopover('reject')">
<Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="rejectFormRef"
:model="rejectReasonForm"
:rules="rejectReasonRule"
label-width="100px"
>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="rejectReasonForm.reason"
placeholder="请输入审批意见"
type="textarea"
:rows="4"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false,rejectFormRef)">
{{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
<el-button @click="closePropover('reject', rejectFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 抄送按钮 -->
<el-popover
:visible="popOverVisible.copy"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)"
>
<template #reference>
<div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="svg-icon:send" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.COPY) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="copyFormRef"
:model="copyForm"
:rules="copyFormRule"
label-width="100px"
>
<el-form-item label="抄送人" prop="copyUserIds">
<el-select
v-model="copyForm.copyUserIds"
clearable
style="width: 100%"
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-item label="抄送意见" prop="copyReason">
<el-input
v-model="copyForm.copyReason"
clearable
placeholder="请输入抄送意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleCopy">
{{ getButtonDisplayName(OperationButtonType.COPY) }}
</el-button>
<el-button @click="closePropover('copy', copyFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 转办按钮 -->
<el-popover
:visible="popOverVisible.transfer"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)"
>
<template #reference>
<div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="transferFormRef"
:model="transferForm"
:rules="transferFormRule"
label-width="100px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="transferForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</el-button>
<el-button @click="closePropover('transfer', transferFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 委派按钮 -->
<el-popover
:visible="popOverVisible.delegate"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)"
>
<template #reference>
<div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:position" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="delegateFormRef"
:model="delegateForm"
:rules="delegateFormRule"
label-width="100px"
>
<el-form-item label="接收人" prop="delegateUserId">
<el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="delegateForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</el-button>
<el-button @click="closePropover('delegate', delegateFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 加签按钮 当前任务审批人为A向前加签选了一个C则需要C先审批然后再是A审批向后加签BA审批完需要B再审批完才算完成这个任务节点 -->
<el-popover
:visible="popOverVisible.addSign"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)"
>
<template #reference>
<div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:plus" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="addSignFormRef"
:model="addSignForm"
:rules="addSignFormRule"
label-width="100px"
>
<el-form-item label="加签处理人" prop="addSignUserIds">
<el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="addSignForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')">
向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</el-button>
<el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</el-button>
<el-button @click="closePropover('addSign', addSignFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 减签按钮 -->
<el-popover
:visible="popOverVisible.deleteSign"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask?.children.length > 0"
>
<template #reference>
<div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:semi-select" />&nbsp; 减签
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="deleteSignFormRef"
:model="deleteSignForm"
:rules="deleteSignFormRule"
label-width="100px"
>
<el-form-item label="减签人员" prop="deleteSignTaskId">
<el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%">
<el-option
v-for="item in runningTask.children"
:key="item.id"
:label="getDeleteSignUserLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="审批意见" prop="reason">
<el-input
v-model="deleteSignForm.reason"
clearable
placeholder="请输入审批意见"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
减签
</el-button>
<el-button @click="closePropover('deleteSign', deleteSignFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 退回按钮 -->
<el-popover
:visible="popOverVisible.return"
placement="top-start"
:width="420"
trigger="click"
v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
>
<template #reference>
<div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="ep:back" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="returnFormRef"
:model="returnForm"
:rules="returnFormRule"
label-width="100px"
>
<el-form-item label="退回节点" prop="targetTaskDefinitionKey">
<el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%">
<el-option
v-for="item in returnList"
:key="item.taskDefinitionKey"
:label="item.name"
:value="item.taskDefinitionKey"
/>
</el-select>
</el-form-item>
<el-form-item label="退回理由" prop="returnReason">
<el-input
v-model="returnForm.returnReason"
clearable
placeholder="请输入退回理由"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleReturn()">
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</el-button>
<el-button @click="closePropover('return', returnFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!--取消按钮 这个对应发起人的取消, 只有发起人可以取消 -->
<el-popover
:visible="popOverVisible.cancel"
placement="top-start"
:width="420"
trigger="click"
v-if="
userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status)
"
>
<template #reference>
<div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px">
<Icon :size="14" icon="fa:mail-reply" />&nbsp; 取消
</div>
</template>
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
class="mb-auto"
ref="cancelFormRef"
:model="cancelForm"
:rules="cancelFormRule"
label-width="100px"
>
<el-form-item label="取消理由" prop="cancelReason">
<span class="text-#878c93 text-12px">&nbsp; 取消后该审批流程将自动结束</span>
<el-input
v-model="cancelForm.cancelReason"
clearable
placeholder="请输入取消理由"
type="textarea"
:rows="3"
/>
</el-form-item>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="handleCancel()">
确认
</el-button>
<el-button @click="closePropover('cancel', cancelFormRef)"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 再次提交 按钮-->
<div
@click="handleReCreate()"
class="hover-bg-gray-100 rounded-xl p-6px"
v-if="
userId === processInstance?.startUser?.id &&
isEndProcessStatus(processInstance?.status) &&
processDefinition?.formType === 10
"
>
<Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
</div>
</div>
</template>
<script lang="ts" setup>
import { useUserStoreWithOut } from '@/store/modules/user'
import { setConfAndFields2 } from '@/utils/formCreate'
import * as TaskApi from '@/api/bpm/task'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as UserApi from '@/api/system/user'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'ProcessInstanceBtnContainer' })
const router = useRouter() //
const message = useMessage() //
const userId = useUserStoreWithOut().getUser.id //
const emit = defineEmits(['success']) // success
const props = defineProps< {
processInstance: any, //
processDefinition: any, //
userOptions: UserApi.UserVO[],
normalForm: any, // formCreate
normalFormApi: any, // formCreate Api
writableFields: string[] //
}>()
const formLoading = ref(false) //
const popOverVisible = ref({
approve: false,
reject: false,
transfer: false,
delegate: false,
addSign: false,
return: false,
copy: false,
cancel: false,
deleteSign: false
}) //
const returnList = ref([] as any) // 退
// ========== ==========
const runningTask = ref<any>() //
const approveForm = ref<any>({}) //
const approveFormFApi = ref<any>({}) // approveForms fAPi
//
const approveFormRef = ref<FormInstance>()
const approveReasonForm = reactive({
reason: ''
})
const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
//
const rejectFormRef = ref<FormInstance>()
const rejectReasonForm = reactive({
reason: ''
})
const rejectReasonRule = reactive<FormRules<typeof rejectReasonForm>>({
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
//
const copyFormRef = ref<FormInstance>()
const copyForm = reactive({
copyUserIds: [],
copyReason: ''
})
const copyFormRule = reactive<FormRules<typeof copyForm>>({
copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }]
})
//
const transferFormRef = ref<FormInstance>()
const transferForm = reactive({
assigneeUserId: undefined,
reason: ''
})
const transferFormRule = reactive<FormRules<typeof transferForm>>({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
//
const delegateFormRef = ref<FormInstance>()
const delegateForm = reactive({
delegateUserId: undefined,
reason: ''
})
const delegateFormRule = reactive<FormRules<typeof delegateForm>>({
delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
//
const addSignFormRef = ref<FormInstance>()
const addSignForm = reactive({
addSignUserIds: undefined,
reason: ''
})
const addSignFormRule = reactive<FormRules<typeof addSignForm>>({
addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
//
const deleteSignFormRef = ref<FormInstance>()
const deleteSignForm = reactive({
deleteSignTaskId: undefined,
reason: ''
})
const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({
deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
})
// 退
const returnFormRef = ref<FormInstance>()
const returnForm = reactive({
targetTaskDefinitionKey: undefined,
returnReason: ''
})
const returnFormRule = reactive<FormRules<typeof returnForm>>({
targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }],
returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }]
})
//
const cancelFormRef = ref<FormInstance>()
const cancelForm = reactive({
cancelReason: ''
})
const cancelFormRule = reactive<FormRules<typeof cancelForm>>({
cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
})
/** 监听 approveFormFApis实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
watch(
() => approveFormFApi.value,
(val) => {
val?.btn?.show(false)
val?.resetBtn?.show(false)
},
{
deep: true
}
)
/** 弹出气泡卡 */
const openPopover = async (type: string) => {
if (type === 'approve') {
//
const valid = await validateNormalForm();
if (!valid) {
message.warning('表单校验不通过,请先完善表单!!')
return;
}
}
if (type === 'return') {
// 退
returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
if (returnList.value.length === 0) {
message.warning('当前没有可退回的节点')
return
}
}
Object.keys(popOverVisible.value).forEach((item) => {
popOverVisible.value[item] = item === type
})
// await nextTick()
// formRef.value.resetFields()
}
/** 关闭气泡卡 */
const closePropover = (type: string, formRef: FormInstance | undefined) => {
if (formRef) {
formRef.resetFields()
}
popOverVisible.value[type] = false
}
/** 处理审批通过和不通过的操作 */
const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => {
formLoading.value = true
try {
//
if (!formRef) return
await formRef.validate()
if (pass) {
// ,
const variables = getUpdatedProcessInstanceVaiables();
//
const data = {
id: runningTask.value.id,
reason: approveReasonForm.reason,
variables // ,
}
// approveForm + data
// TODO
const formCreateApi = approveFormFApi.value
if (Object.keys(formCreateApi)?.length > 0) {
await formCreateApi.validate()
// @ts-ignore
data.variables = approveForm.value.value
}
await TaskApi.approveTask(data)
popOverVisible.value.approve = false
message.success('审批通过成功')
} else {
//
const data = {
id: runningTask.value.id,
reason: rejectReasonForm.reason,
}
await TaskApi.rejectTask(data)
popOverVisible.value.reject = false
message.success('审批不通过成功')
}
//
formRef.resetFields()
//
reload()
} finally {
formLoading.value = false
}
}
/** 处理抄送 */
const handleCopy = async () => {
formLoading.value = true
try {
// 1.
if (!copyFormRef.value) return
await copyFormRef.value.validate()
// 2.
const data = {
id: runningTask.value.id,
reason: copyForm.copyReason,
copyUserIds:copyForm.copyUserIds
}
await TaskApi.copyTask(data)
copyFormRef.value.resetFields()
popOverVisible.value.copy = false
message.success('操作成功')
} finally {
formLoading.value = false
}
}
/** 处理转交 */
const handleTransfer = async () => {
formLoading.value = true
try {
// 1.1
if (!transferFormRef.value) return
await transferFormRef.value.validate()
// 1.2
const data = {
id: runningTask.value.id,
reason: transferForm.reason,
assigneeUserId: transferForm.assigneeUserId
}
await TaskApi.transferTask(data)
transferFormRef.value.resetFields()
popOverVisible.value.transfer = false
message.success('操作成功')
// 2.
reload()
} finally {
formLoading.value = false
}
}
/** 处理委派 */
const handleDelegate = async () => {
formLoading.value = true
try {
// 1.1
if (!delegateFormRef.value) return
await delegateFormRef.value.validate()
// 1.2
const data = {
id: runningTask.value.id,
reason: delegateForm.reason,
delegateUserId: delegateForm.delegateUserId
}
await TaskApi.delegateTask(data)
popOverVisible.value.delegate = false
delegateFormRef.value.resetFields()
message.success('操作成功')
// 2.
reload()
} finally {
formLoading.value = false
}
}
/** 处理加签 */
const handlerAddSign = async (type: string) => {
formLoading.value = true
try {
// 1.1
if (!addSignFormRef.value) return
await addSignFormRef.value.validate()
// 1.2
const data = {
id: runningTask.value.id,
type,
reason: addSignForm.reason,
userIds: addSignForm.addSignUserIds
}
await TaskApi.signCreateTask(data)
message.success('操作成功')
addSignFormRef.value.resetFields()
popOverVisible.value.addSign = false
// 2
reload()
} finally {
formLoading.value = false
}
}
/** 处理退回 */
const handleReturn = async () => {
formLoading.value = true
try {
// 1.1
if (!returnFormRef.value) return
await returnFormRef.value.validate()
// 1.2 退
const data = {
id: runningTask.value.id,
reason: returnForm.returnReason,
targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey
}
await TaskApi.returnTask(data)
popOverVisible.value.return = false
returnFormRef.value.resetFields()
message.success('操作成功')
// 2
reload()
} finally {
formLoading.value = false
}
}
/** 处理取消 */
const handleCancel = async () => {
formLoading.value = true
try {
// 1.1
if (!cancelFormRef.value) return
await cancelFormRef.value.validate()
// 1.2
await ProcessInstanceApi.cancelProcessInstanceByStartUser(
props.processInstance.id,
cancelForm.cancelReason
)
popOverVisible.value.return = false
message.success('操作成功')
cancelFormRef.value.resetFields()
// 2
reload()
} finally {
formLoading.value = false
}
}
/** 处理再次提交 */
const handleReCreate = async () => {
//
await router.push({
name: 'BpmProcessInstanceCreate',
query: { processInstanceId: props.processInstance?.id }
})
}
/** 获取减签人员标签 */
const getDeleteSignUserLabel = (task: any): string => {
const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
return `${nickname} ( 所属部门:${deptName} )`
}
/** 处理减签 */
const handlerDeleteSign = async () => {
formLoading.value = true
try {
// 1.1
if (!deleteSignFormRef.value) return
await deleteSignFormRef.value.validate()
// 1.2
const data = {
id: deleteSignForm.deleteSignTaskId,
reason: deleteSignForm.reason
}
await TaskApi.signDeleteTask(data)
message.success('减签成功')
deleteSignFormRef.value.resetFields()
popOverVisible.value.deleteSign = false
// 2
reload()
} finally {
formLoading.value = false
}
}
/** 重新加载数据 */
const reload = () => {
emit('success')
}
/** 任务是否为处理中状态 */
const isHandleTaskStatus = () => {
let canHandle = false
if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
canHandle = true
}
return canHandle
}
/** 流程状态是否为结束状态 */
const isEndProcessStatus = (status: number) => {
let isEndStatus = false
if (
BpmProcessInstanceStatus.APPROVE === status ||
BpmProcessInstanceStatus.REJECT === status ||
BpmProcessInstanceStatus.CANCEL === status
) {
isEndStatus = true
}
return isEndStatus
}
/** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => {
let isShow = true
if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
isShow = runningTask.value.buttonsSetting[btnType].enable
}
return isShow
}
/** 获取按钮的显示名称 */
const getButtonDisplayName = (btnType: OperationButtonType) => {
let displayName = OPERATION_BUTTON_NAME.get(btnType)
if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
displayName = runningTask.value.buttonsSetting[btnType].displayName
}
return displayName
}
const loadTodoTask = (task: any) => {
approveForm.value = {}
approveFormFApi.value = {}
runningTask.value = task
// approve .
if (task && task.formId && task.formConf) {
const tempApproveForm = {}
setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
approveForm.value = tempApproveForm
} else {
approveForm.value = {} //
}
}
/** 校验流程表单 */
const validateNormalForm = async () => {
if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
let valid = true
try {
await props.normalFormApi?.validate()
} catch {
valid = false;
}
return valid;
} else {
return true;
}
}
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
const getUpdatedProcessInstanceVaiables = ()=> {
const variables = {}
props.writableFields.forEach( (field) => {
const fieldValue = props.normalFormApi.getValue(field)
variables[field] = fieldValue;
})
return variables
}
defineExpose({ loadTodoTask })
</script>
<style lang="scss" scoped>
:deep(.el-affix--fixed) {
background-color: var(--el-bg-color);
}
.btn-container {
> div {
display: flex;
margin: 0 8px;
cursor: pointer;
align-items: center;
&:hover {
color: #6db5ff;
}
}
}
</style>

@ -0,0 +1,168 @@
<template>
<div v-loading="loading" class="process-viewer-container">
<SimpleProcessViewer
:flow-node="simpleModel"
:tasks="tasks"
:process-instance="processInstance"
class="process-viewer"
/>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { TaskStatusEnum } from '@/api/bpm/task'
import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
const props = defineProps({
loading: propTypes.bool.def(false), //
modelView: propTypes.object,
simpleJson: propTypes.string // Simple (json )
})
const simpleModel = ref()
//
const tasks = ref([])
//
const processInstance = ref()
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks
processInstance.value = newModelView.processInstance
// UserTask
const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
// UserTask
const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
// UserTaskGateway
const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
// 线 SequenceFlow
const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
simpleModel.value = newModelView.simpleModel
}
}
)
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value)
}
}
)
const setSimpleModelNodeTaskStatus = (
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[]
) => {
if (!simpleModel) {
return
}
//
if (simpleModel.type === NodeType.END_EVENT_NODE) {
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = processStatus
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
return
}
//
if (
simpleModel.type === NodeType.START_USER_NODE ||
simpleModel.type === NodeType.USER_TASK_NODE
) {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
if (rejectedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.REJECT
} else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.RUNNING
} else if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
}
// TODO cancel
}
//
if (simpleModel.type === NodeType.COPY_TASK_NODE) {
//
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
}
// SequenceFlow
if (simpleModel.type === NodeType.CONDITION_NODE) {
//
if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
}
//
if (
simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
) {
//
if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = TaskStatusEnum.APPROVE
} else {
simpleModel.activityStatus = TaskStatusEnum.NOT_START
}
simpleModel.conditionNodes?.forEach((node) => {
setSimpleModelNodeTaskStatus(
node,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
})
}
setSimpleModelNodeTaskStatus(
simpleModel.childNode,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds
)
}
</script>
<style lang="scss" scoped>
.process-viewer-container {
height: 100%;
width: 100%;
:deep(.process-viewer) {
height: 100% !important;
min-height: 100%;
width: 100%;
overflow: auto;
}
}
</style>

@ -1,85 +1,50 @@
<template>
<el-card v-loading="loading" class="box-card">
<template #header>
<span class="el-icon-picture-outline">审批记录</span>
</template>
<el-col :offset="3" :span="17">
<div class="block">
<el-timeline>
<el-timeline-item
v-if="processInstance.endTime"
:type="getProcessInstanceTimelineItemType(processInstance)"
>
<p style="font-weight: 700">
结束流程 {{ 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)"
@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">
审批人{{ item.assigneeUser.nickname }}
<el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
</label>
<label v-if="item.createTime" style="font-weight: normal"></label>
<label style="font-weight: normal; color: #8a909c">
{{ formatDate(item?.createTime) }}
</label>
<label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
审批时间
</label>
<label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
{{ formatDate(item?.endTime) }}
</label>
<label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
耗时
</label>
<label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
{{ formatPast2(item?.durationInMillis) }}
</label>
<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>
</el-card>
<el-table :data="tasks" border header-cell-class-name="table-header-gray">
<el-table-column label="审批节点" prop="name" min-width="120" align="center" />
<el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<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="200">
<template #default="scope">
{{ scope.row.reason }}
<el-button
class="ml-10px"
size="small"
v-if="scope.row.formId > 0"
@click="handleFormDetail(scope.row)"
>
<Icon icon="ep:document" /> 查看表单
</el-button>
</template>
</el-table-column>
<el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
<!-- 弹窗子任务 -->
<TaskSignList ref="taskSignListRef" @success="refresh" />
<!-- 弹窗表单 -->
<Dialog title="表单详情" v-model="taskFormVisible" width="600">
<form-create
@ -91,61 +56,20 @@
</Dialog>
</template>
<script lang="ts" setup>
import { formatDate, formatPast2 } from '@/utils/formatTime'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
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'
import * as TaskApi from '@/api/bpm/task'
defineOptions({ name: 'BpmProcessInstanceTaskList' })
defineProps({
loading: propTypes.bool, //
processInstance: propTypes.object, //
tasks: propTypes.arrayOf(propTypes.object) //
const props = defineProps({
loading: propTypes.bool.def(false), //
id: propTypes.string //
})
/** 获得流程实例对应的颜色 */
const getProcessInstanceTimelineItemType = (item: any) => {
if (item.status === 2) {
return 'success'
}
if (item.status === 3) {
return 'danger'
}
if (item.status === 4) {
return 'warning'
}
return ''
}
/** 获得任务对应的颜色 */
const getTaskTimelineItemType = (item: any) => {
if ([0, 1, 6, 7].includes(item.status)) {
return 'primary'
}
if (item.status === 2) {
return 'success'
}
if (item.status === 3) {
return 'danger'
}
if (item.status === 4) {
return 'info'
}
if (item.status === 5) {
return 'warning'
}
return ''
}
/** 子任务 */
const taskSignListRef = ref()
const openChildrenTask = (item: any) => {
taskSignListRef.value.open(item)
}
const tasks = ref([]) //
/** 查看表单 */
const fApi = ref<ApiAttrs>() // form-create API
@ -155,7 +79,7 @@ const taskForm = ref({
value: {}
}) //
const taskFormVisible = ref(false)
const handleFormDetail = async (row) => {
const handleFormDetail = async (row: any) => {
//
setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
//
@ -167,9 +91,13 @@ const handleFormDetail = async (row) => {
fApi.value?.fapi?.disabled(true)
}
/** 刷新数据 */
const emit = defineEmits(['refresh']) // success
const refresh = () => {
emit('refresh')
}
/** 只有 loading 完成时,才去加载流程列表 */
watch(
() => props.loading,
async (value) => {
if (value) {
tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
}
}
)
</script>

@ -0,0 +1,292 @@
<!-- 审批详情的右侧审批流 -->
<template>
<el-timeline class="pt-20px">
<!-- 遍历每个审批节点 -->
<el-timeline-item
v-for="(activity, index) in activityNodes"
:key="index"
size="large"
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
:color="getApprovalNodeColor(activity.status)"
>
<template #dot>
<div
class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
>
<img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
<div
v-if="showStatusIcon"
class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
>
<el-icon :size="11" color="#fff">
<component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
</el-icon>
</div>
</div>
</template>
<div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
<!-- 第一行节点名称时间 -->
<div class="flex w-full">
<div class="font-bold"> {{ activity.name }}</div>
<!-- 信息时间 -->
<div
v-if="activity.status !== TaskStatusEnum.NOT_START"
class="text-#a5a5a5 text-13px mt-1 ml-auto"
>
{{ getApprovalNodeTime(activity) }}
</div>
</div>
<!-- 需要自定义选择审批人 -->
<div
class="flex flex-wrap gap2 items-center"
v-if="
isEmpty(activity.tasks) &&
isEmpty(activity.candidateUsers) &&
CandidateStrategy.START_USER_SELECT === activity.candidateStrategy
"
>
<!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
<el-tooltip content="添加用户" placement="left">
<el-button
class="!px-6px"
@click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
>
<img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
</el-button>
</el-tooltip>
<div
v-for="(user, idx1) in customApproveUsers[activity.id]"
:key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
</div>
</div>
<div v-else class="flex items-center flex-wrap mt-1 gap2">
<!-- 情况一遍历每个审批节点下的进行中task 任务 -->
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
<div
class="position-relative flex flex-wrap gap2"
v-if="task.assigneeUser || task.ownerUser"
>
<!-- 信息头像昵称 -->
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
<el-avatar
class="!m-5px"
:size="28"
v-if="task.assigneeUser?.avatar"
:src="task.assigneeUser?.avatar"
/>
<el-avatar class="!m-5px" :size="28" v-else>
{{ task.assigneeUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.assigneeUser?.nickname }}
</template>
<template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
<el-avatar
class="!m-5px"
:size="28"
v-if="task.ownerUser?.avatar"
:src="task.ownerUser?.avatar"
/>
<el-avatar class="!m-5px" :size="28" v-else>
{{ task.ownerUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.ownerUser?.nickname }}
</template>
<!-- 信息任务 ICON -->
<div
v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2[task.status]?.color }"
>
<Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
</div>
</div>
</div>
<teleport defer :to="`#activity-task-${activity.id}`">
<div
v-if="
task.reason &&
[NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
"
class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
>
审批意见{{ task.reason }}
</div>
</teleport>
</div>
<!-- 情况二遍历每个审批节点下的候选的task 任务例如说1依次审批2未来的审批任务等 -->
<div
v-for="(user, idx1) in activity.candidateUsers"
:key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
<!-- 信息任务 ICON -->
<div
v-if="showStatusIcon"
class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
:style="{ backgroundColor: statusIconMap2['-1']?.color }"
>
<Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
</div>
</div>
</div>
</div>
</el-timeline-item>
</el-timeline>
<!-- 用户选择弹窗 -->
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import { isEmpty } from '@/utils/is'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
import starterSvg from '@/assets/svgs/bpm/starter.svg'
import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
import copySvg from '@/assets/svgs/bpm/copy.svg'
import conditionSvg from '@/assets/svgs/bpm/condition.svg'
import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
import finishSvg from '@/assets/svgs/bpm/finish.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
withDefaults(
defineProps<{
activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] //
showStatusIcon?: boolean //
}>(),
{
showStatusIcon: true // true
}
)
//
const statusIconMap2 = {
//
'-1': { color: '#909398', icon: 'ep-clock' },
//
'0': { color: '#00b32a', icon: 'ep:loading' },
//
'1': { color: '#448ef7', icon: 'ep:loading' },
//
'2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
//
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
//
'4': { color: '#cccccc', icon: 'ep:delete-filled' },
// 退
'5': { color: '#f46b6c', icon: 'ep:remove-filled' },
//
'6': { color: '#448ef7', icon: 'ep:loading' },
//
'7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
}
const statusIconMap = {
//
'-1': { color: '#909398', icon: Clock },
'0': { color: '#00b32a', icon: Clock },
//
'1': { color: '#448ef7', icon: Loading },
//
'2': { color: '#00b32a', icon: Check },
//
'3': { color: '#f46b6c', icon: Close },
//
'4': { color: '#cccccc', icon: Delete },
// 退
'5': { color: '#f46b6c', icon: Minus },
//
'6': { color: '#448ef7', icon: Loading },
//
'7': { color: '#00b32a', icon: Check }
}
const nodeTypeSvgMap = {
//
[NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
//
[NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
//
[NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
//
[NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
//
[NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
//
[NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
}
// -101 icon
const onlyStatusIconShow = [-1, 0, 1]
// timeline线icon
const getApprovalNodeImg = (nodeType: NodeType) => {
return nodeTypeSvgMap[nodeType]?.svg
}
const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
if (taskStatus == TaskStatusEnum.NOT_START) {
return statusIconMap[taskStatus]?.icon
}
if (
nodeType === NodeType.START_USER_NODE ||
nodeType === NodeType.USER_TASK_NODE ||
nodeType === NodeType.END_EVENT_NODE
) {
return statusIconMap[taskStatus]?.icon
}
}
const getApprovalNodeColor = (taskStatus: number) => {
return statusIconMap[taskStatus]?.color
}
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
return `${formatDate(node.startTime)}`
}
if (node.endTime) {
return `${formatDate(node.endTime)}`
}
if (node.startTime) {
return `${formatDate(node.startTime)}`
}
}
//
const userSelectFormRef = ref()
const handleSelectUser = (activityId, selectedList) => {
userSelectFormRef.value.open(activityId, selectedList)
}
const emit = defineEmits<{
selectUserConfirm: [id: any, userList: any[]]
}>()
const customApproveUsers: any = ref({}) // keyactivityIdvalue
//
const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
customApproveUsers.value[activityId] = userList || []
emit('selectUserConfirm', activityId, userList)
}
</script>

@ -1,89 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="委派任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="接收人" prop="delegateUserId">
<el-select v-model="formData.delegateUserId" clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</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>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmTaskDelegateForm' })
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
id: '',
delegateUserId: undefined,
reason: ''
})
const formRules = ref({
delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const userList = ref<any[]>([]) //
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
//
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // openModal
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
await TaskApi.delegateTask(formData.value)
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
delegateUserId: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>

@ -1,90 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="回退任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<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.taskDefinitionKey"
:label="item.name"
:value="item.taskDefinitionKey"
/>
</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>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="TaskRollbackDialogForm" setup>
import * as TaskApi from '@/api/bpm/task'
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
id: '',
targetTaskDefinitionKey: undefined,
reason: ''
})
const formRules = ref({
targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const returnList = ref([] as any)
/** 打开弹窗 */
const open = async (id: string) => {
returnList.value = await TaskApi.getTaskListByReturn(id)
if (returnList.value.length === 0) {
message.warning('当前没有可回退的节点')
return false
}
dialogVisible.value = true
resetForm()
formData.value.id = id
}
defineExpose({ open }) // openModal
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
await TaskApi.returnTask(formData.value)
message.success('回退成功')
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
targetTaskDefinitionKey: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>

@ -1,99 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="加签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<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"
:label="item.nickname"
:value="item.id"
/>
</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('before')">
向前加签
</el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm('after')">
向后加签
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'TaskSignCreateForm' })
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
id: '',
userIds: [],
type: '',
reason: ''
})
const formRules = ref({
userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
const userList = ref<any[]>([]) //
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
//
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // openModal
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async (type: string) => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
formData.value.type = type
try {
await TaskApi.signCreateTask(formData.value)
message.success('加签成功')
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
userIds: [],
type: '',
reason: ''
}
formRef.value?.resetFields()
}
</script>

@ -1,89 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="减签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="减签任务" prop="id">
<el-radio-group v-model="formData.id">
<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>
<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>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<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) //
const formData = ref({
id: '',
reason: ''
})
const formRules = ref({
id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }],
reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const childrenTaskList = ref([])
/** 打开弹窗 */
const open = async (id: string) => {
childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
if (isEmpty(childrenTaskList.value)) {
message.warning('当前没有可减签的任务')
return false
}
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // openModal
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
await TaskApi.signDeleteTask(formData.value)
message.success('减签成功')
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
reason: ''
}
formRef.value?.resetFields()
}
</script>

@ -1,106 +0,0 @@
<template>
<el-drawer v-model="drawerVisible" title="子任务" size="880px">
<!-- 当前任务 -->
<template #header>
<h4>{{ parentTask.name }} 审批人{{ parentTask?.assigneeUser?.nickname }}</h4>
<el-button
style="margin-left: 5px"
v-if="isSignDeleteButtonVisible(parentTask)"
type="danger"
plain
@click="handleSignDelete(parentTask)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
<!-- 子任务列表 -->
<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_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="提交时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="结束时间"
align="center"
prop="endTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" prop="operation" width="90">
<template #default="scope">
<el-button
v-if="isSignDeleteButtonVisible(scope.row)"
type="danger"
plain
size="small"
@click="handleSignDelete(scope.row)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 减签 -->
<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 TaskSignDeleteForm from './TaskSignDeleteForm.vue'
defineOptions({ name: 'TaskSignList' })
const message = useMessage() //
const drawerVisible = ref(false) //
const parentTask = ref({} as any)
/** 打开弹窗 */
const open = async (task: any) => {
if (isEmpty(task.children)) {
message.warning('该任务没有子任务')
return
}
parentTask.value = task
//
drawerVisible.value = true
}
defineExpose({ open }) // openModal
/** 发起减签 */
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 isSignDeleteButtonVisible = (task: any) => {
return task && task.children && !isEmpty(task.children)
}
</script>

@ -1,89 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="转派任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</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>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'TaskTransferForm' })
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
id: '',
assigneeUserId: undefined,
reason: ''
})
const formRules = ref({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const userList = ref<any[]>([]) //
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
//
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // openModal
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
await TaskApi.transferTask(formData.value)
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
assigneeUserId: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>

@ -1,174 +1,170 @@
<template>
<ContentWrap>
<!-- 审批信息 -->
<el-card
v-for="(item, index) in runningTasks"
:key="index"
v-loading="processInstanceLoading"
class="box-card"
>
<template #header>
<span class="el-icon-picture-outline">审批任务{{ item.name }}</span>
</template>
<el-col :offset="6" :span="16">
<el-form
:ref="'form' + index"
:model="auditForms[index]"
:rules="auditRule"
label-width="100px"
>
<el-form-item v-if="processInstance && processInstance.name" label="流程名">
{{ 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>
</el-form-item>
<el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
<template #header>
<span class="el-icon-picture-outline">
填写表单{{ runningTasks[index]?.formName }}
</span>
</template>
<form-create
v-model="approveForms[index].value"
v-model:api="approveFormFApis[index]"
:option="approveForms[index].option"
:rule="approveForms[index].rule"
/>
</el-card>
<el-form-item label="审批建议" prop="reason">
<el-input
v-model="auditForms[index].reason"
placeholder="请输入审批建议"
type="textarea"
<ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
<div class="processInstance-wrap-main">
<el-scrollbar>
<img
class="position-absolute right-20px"
width="150"
:src="auditIconsMap[processInstance.status]"
alt=""
/>
<div class="text-#878c93 h-15px">编号{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag
v-if="processInstance.status"
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
:value="processInstance.status"
/>
</div>
<div class="flex items-center gap-5 mb-10px text-13px h-35px">
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
>
<el-avatar
:size="28"
v-if="processInstance?.startUser?.avatar"
:src="processInstance?.startUser?.avatar"
/>
</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)">
<Icon icon="ep:select" />
通过
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<Icon icon="ep:close" />
不通过
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<Icon icon="ep:edit" />
转办
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<Icon icon="ep:position" />
委派
</el-button>
<el-button type="primary" @click="handleSign(item)">
<Icon icon="ep:plus" />
加签
</el-button>
<el-button type="warning" @click="handleBack(item)">
<Icon icon="ep:back" />
回退
</el-button>
<el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
{{ processInstance?.startUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
</el-col>
</el-card>
<!-- 申请信息 -->
<el-card v-loading="processInstanceLoading" class="box-card">
<template #header>
<span class="el-icon-document">申请信息{{ processInstance.name }}</span>
</template>
<!-- 情况一流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</el-card>
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="审批详情" name="form">
<div class="form-scroll-area">
<el-scrollbar>
<el-row>
<el-col :span="17" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一流程表单 -->
<el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
<form-create
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</div>
</el-col>
<el-col :span="7">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 审批记录 -->
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
:tasks="tasks"
@refresh="getTaskList"
/>
<!-- 流程图 -->
<el-tab-pane label="流程图" name="diagram">
<div class="form-scroll-area">
<ProcessInstanceSimpleViewer
v-show="
processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
"
:loading="processInstanceLoading"
:model-view="processModelView"
/>
<ProcessInstanceBpmnViewer
v-show="
processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
"
:loading="processInstanceLoading"
:model-view="processModelView"
/>
</div>
</el-tab-pane>
<!-- 高亮流程图 -->
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
<!-- 流转记录 -->
<el-tab-pane label="流转记录" name="record">
<div class="form-scroll-area">
<el-scrollbar>
<ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 弹窗转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗回退节点 -->
<TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
<!-- 弹窗委派将任务委派给别人处理处理完成后会重新回到原审批人手中-->
<TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
<!-- 弹窗加签当前任务审批人为A向前加签选了一个C则需要C先审批然后再是A审批向后加签BA审批完需要B再审批完才算完成这个任务节点 -->
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
<!-- 流转评论 TODO 待开发 -->
<el-tab-pane label="流转评论" name="comment" v-if="false">
<div class="form-scroll-area">
<el-scrollbar> 流转评论 </el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
<div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:process-instance="processInstance"
:process-definition="processDefinition"
:userOptions="userOptions"
:normal-form="detailForm"
:normal-form-api="fApi"
:writable-fields="writableFields"
@success="refresh"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { BpmModelType, BpmModelFormType } from '@/utils/constants'
import { setConfAndFields2 } from '@/utils/formCreate'
import { registerComponent } from '@/utils/routerHelper'
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 * as UserApi from '@/api/system/user'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import TaskReturnForm from './dialog/TaskReturnForm.vue'
import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is'
import * as UserApi from '@/api/system/user'
import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { TaskStatusEnum } from '@/api/bpm/task'
import runningSvg from '@/assets/svgs/bpm/running.svg'
import approveSvg from '@/assets/svgs/bpm/approve.svg'
import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const { query } = useRoute() //
const props = defineProps<{
id: string //
taskId?: string //
activityId?: string //
}>()
const message = useMessage() //
const { proxy } = getCurrentInstance() as any
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 tasksLoad = ref(true) //
const tasks = ref<any[]>([]) //
// ========== ==========
const runningTasks = ref<any[]>([]) //
const auditForms = ref<any[]>([]) //
const auditRule = reactive({
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
})
const approveForms = ref<any[]>([]) //
const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms fAPi
const processDefinition = ref<any>({}) //
const processModelView = ref<any>({}) //
const operationButtonRef = ref() // ref
const auditIconsMap = {
[TaskStatusEnum.RUNNING]: runningSvg,
[TaskStatusEnum.APPROVE]: approveSvg,
[TaskStatusEnum.REJECT]: rejectSvg,
[TaskStatusEnum.CANCEL]: cancelSvg
}
// ========== ==========
const fApi = ref<ApiAttrs>() //
@ -178,199 +174,129 @@ 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
const index = runningTasks.value.indexOf(task)
const auditFormRef = proxy.$refs['form' + index][0]
// 1.2
const elForm = unref(auditFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 2.1
const data = {
id: task.id,
reason: auditForms.value[index].reason,
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 {
await TaskApi.rejectTask(data)
message.success('审批不通过成功')
}
// 2.2
getDetail()
}
/** 转派审批人 */
const taskTransferFormRef = ref()
const openTaskUpdateAssigneeForm = (id: string) => {
taskTransferFormRef.value.open(id)
}
/** 处理审批退回的操作 */
const taskDelegateForm = ref()
const handleDelegate = async (task) => {
taskDelegateForm.value.open(task.id)
}
/** 处理审批退回的操作 */
const taskReturnFormRef = ref()
const handleBack = async (task: any) => {
taskReturnFormRef.value.open(task.id)
}
/** 处理审批加签的操作 */
const taskSignCreateFormRef = ref()
const handleSign = async (task: any) => {
taskSignCreateFormRef.value.open(task.id)
}
const writableFields: Array<string> = [] //
/** 获得详情 */
const getDetail = () => {
// 1.
getProcessInstance()
// 2.
getTaskList()
getApprovalDetail()
getProcessModelView()
}
/** 加载流程实例 */
const BusinessFormComponent = ref(null) //
const getProcessInstance = async () => {
const BusinessFormComponent = ref<any>(null) //
/** 获取审批详情 */
const getApprovalDetail = async () => {
processInstanceLoading.value = true
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(id)
const param = {
processInstanceId: props.id,
activityId: props.activityId,
taskId: props.taskId
}
const data = await ProcessInstanceApi.getApprovalDetail(param)
if (!data) {
message.error('查询不到审批详情信息!')
return
}
if (!data.processDefinition || !data.processInstance) {
message.error('查询不到流程信息!')
return
}
processInstance.value = data
processInstance.value = data.processInstance
processDefinition.value = data.processDefinition
//
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
if (processDefinition.value.formType === BpmModelFormType.NORMAL) {
//
const formFieldsPermission = data.formFieldsPermission
//
writableFields.splice(0)
if (detailForm.value.rule?.length > 0) {
// form-create
detailForm.value.value = processInstance.value.formVariables
} else {
setConfAndFields2(
detailForm,
processDefinition.value.formConf,
processDefinition.value.formFields,
processInstance.value.formVariables
)
}
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
//@ts-ignore
fApi.value?.disabled(true)
//
if (formFieldsPermission) {
Object.keys(data.formFieldsPermission).forEach((item) => {
setFieldPermission(item, formFieldsPermission[item])
})
}
})
} else {
// data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
}
//
bpmnXml.value = (
await DefinitionApi.getProcessDefinition(processDefinition.id as number)
)?.bpmnXml
// Timeline
activityNodes.value = data.activityNodes
//
operationButtonRef.value?.loadTodoTask(data.todoTask)
} finally {
processInstanceLoading.value = false
}
}
/** 加载任务列表 */
const getTaskList = async () => {
runningTasks.value = []
auditForms.value = []
approveForms.value = []
approveFormFApis.value = []
try {
//
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
tasks.value = []
// 1.1
data.forEach((task) => {
if (task.status !== 4) {
tasks.value.push(task)
}
})
// 1.2
tasks.value.sort((a, b) => {
//
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
//
} else {
return b.createTime - a.createTime
}
})
//
loadRunningTask(tasks.value)
} finally {
tasksLoad.value = false
/** 获取流程模型视图*/
const getProcessModelView = async () => {
if (BpmModelType.BPMN === processDefinition.value?.modelType) {
// BPMN
processModelView.value = {
bpmnXml: ''
}
}
const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
if (data) {
processModelView.value = data
}
}
//
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/**
* 设置 runningTasks 中的任务
* 设置表单权限
*/
const loadRunningTask = (tasks) => {
tasks.forEach((task) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
}
// 2.1
if (task.status !== 1 && task.status !== 6) {
return
}
// 2.2
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2.3
runningTasks.value.push({ ...task })
auditForms.value.push({
reason: '',
copyUserIds: []
})
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
//@ts-ignore
fApi.value?.disabled(true, field)
}
if (permission === FieldPermissionType.WRITE) {
//@ts-ignore
fApi.value?.disabled(false, field)
//
writableFields.push(field)
}
if (permission === FieldPermissionType.NONE) {
//@ts-ignore
fApi.value?.hidden(true, field)
}
}
// 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({}) //
}
})
/**
* 操作成功后刷新
*/
const refresh = () => {
//
getDetail()
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) //
onMounted(async () => {
@ -379,3 +305,50 @@ onMounted(async () => {
userOptions.value = await UserApi.getSimpleUserList()
})
</script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
);
overflow: auto;
.form-scroll-area {
display: flex;
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
$process-header-height - 40px
);
overflow: auto;
flex-direction: column;
:deep(.box-card) {
height: 100%;
flex: 1;
.el-card__body {
height: 100%;
padding: 0;
}
}
}
}
.form-box {
:deep(.el-card) {
border: none;
}
}
</style>

@ -1,5 +1,5 @@
<template>
<doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<ContentWrap>
<!-- 搜索工作栏 -->
@ -10,7 +10,7 @@
:inline="true"
label-width="68px"
>
<el-form-item label="流程名称" prop="name">
<el-form-item label="" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入流程名称"
@ -19,21 +19,20 @@
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-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
</el-form-item>
<el-form-item label="流程分类" prop="category">
<!-- TODO @ tuitujistyle 可以使用 unocss -->
<el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
<!-- TODO @tuituji应该选择好分类就触发搜索啦 RE:done & to check-->
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
class="!w-240px"
class="!w-155px"
@change="handleQuery"
>
<el-option
v-for="category in categoryList"
@ -43,12 +42,14 @@
/>
</el-select>
</el-form-item>
<el-form-item label="流程状态" prop="status">
<el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }">
<el-select
v-model="queryParams.status"
placeholder="请选择流程状态"
clearable
class="!w-240px"
class="!w-155px"
@change="handleQuery"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
@ -58,28 +59,69 @@
/>
</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
v-hasPermi="['bpm:process-instance:query']"
@click="handleCreate()"
<!-- 高级筛选 -->
<!-- TODO @ tuitujistyle 可以使用 unocss -->
<el-form-item :style="{ position: 'absolute', right: '0px' }">
<el-popover
:visible="showPopover"
persistent
:width="400"
:show-arrow="false"
placement="bottom-end"
>
<Icon icon="ep:plus" class="mr-5px" /> 发起流程
</el-button>
<template #reference>
<el-button @click="showPopover = !showPopover">
<Icon icon="ep:plus" class="mr-5px" />高级筛选
</el-button>
</template>
<el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择流程发起人"
clearable
class="!w-390px"
>
<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="所属流程"
class="bold-label"
label-position="top"
prop="processDefinitionKey"
>
<el-input
v-model="queryParams.processDefinitionKey"
placeholder="请输入流程定义的标识"
clearable
@keyup.enter="handleQuery"
class="!w-390px"
/>
</el-form-item>
<el-form-item label="发起时间" class="bold-label" label-position="top" 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>
<!-- TODO tuituiji参考钉钉1按照清空取消确认排序2右对齐3确认增加 primary -->
<el-form-item class="bold-label" label-position="top">
<el-button @click="handleQuery"> </el-button>
<el-button @click="showPopover = false"> 取消</el-button>
<el-button @click="resetQuery"> </el-button>
</el-form-item>
</el-popover>
</el-form-item>
</el-form>
</ContentWrap>
@ -95,6 +137,8 @@
min-width="100"
fixed="left"
/>
<!-- TODO @芋艿摘要 -->
<!-- TODO tuituiji参考钉钉1审批中时展示审批任务2非审批中展示状态 -->
<el-table-column label="流程状态" prop="status" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
@ -114,7 +158,7 @@
width="180"
:formatter="dateFormatter"
/>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
<!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
<template #default="scope">
{{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
</template>
@ -126,7 +170,7 @@
</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
@ -146,7 +190,7 @@
>
取消
</el-button>
<el-button link type="primary" v-else @click="handleCreate(scope.row.id)">
<el-button link type="primary" v-else @click="handleCreate(scope.row)">
重新发起
</el-button>
</template>
@ -162,11 +206,14 @@
</ContentWrap>
</template>
<script lang="ts" setup>
// TODO @tuitujiList <Icon icon="ep:plus" class="mr-5px" /> RE:done & to check
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { dateFormatter } from '@/utils/formatTime'
import { ElMessageBox } from 'element-plus'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { CategoryApi } from '@/api/bpm/category'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { ProcessInstanceVO } from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
defineOptions({ name: 'BpmProcessInstanceMy' })
@ -181,13 +228,13 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: '',
processDefinitionId: undefined,
processDefinitionKey: undefined,
category: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() //
const categoryList = ref([]) //
const categoryList = ref<CategoryVO[]>([]) //
/** 查询列表 */
const getList = async () => {
@ -201,6 +248,8 @@ const getList = async () => {
}
}
const showPopover = ref(false)
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@ -214,15 +263,26 @@ const resetQuery = () => {
}
/** 发起流程操作 **/
const handleCreate = (id) => {
router.push({
const handleCreate = async (row?: ProcessInstanceVO) => {
//
if (row?.id) {
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
row.processDefinitionId
)
if (processDefinitionDetail.formType === 20) {
message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
return
}
}
//
await router.push({
name: 'BpmProcessInstanceCreate',
query: { processInstanceId: id }
query: { processInstanceId: row?.id }
})
}
/** 查看详情 */
const handleDetail = (row) => {
const handleDetail = (row: ProcessInstanceVO) => {
router.push({
name: 'BpmProcessInstanceDetail',
query: {
@ -232,7 +292,7 @@ const handleDetail = (row) => {
}
/** 取消按钮操作 */
const handleCancel = async (row) => {
const handleCancel = async (row: ProcessInstanceVO) => {
//
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
confirmButtonText: t('common.ok'),
@ -258,3 +318,8 @@ onMounted(async () => {
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>
<style>
.bold-label .el-form-item__label {
font-weight: bold; /* 将字体加粗 */
}
</style>

@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->
@ -76,9 +76,13 @@
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
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-form-item>
</el-form>
</ContentWrap>

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,5 +1,5 @@
<template>
<doc-alert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -0,0 +1,19 @@
<template>
<ContentWrap :bodyStyle="{ padding: '20px 16px' }">
<SimpleProcessDesigner :model-id="modelId" @success="close" />
</ContentWrap>
</template>
<script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({
name: 'SimpleModelDesign'
})
const router = useRouter() //
const { query } = useRoute() //
const modelId = query.modelId as string
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
</script>
<style lang="scss" scoped></style>

@ -1,28 +0,0 @@
<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>
</template>
<script lang="ts" setup>
import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
defineOptions({ name: 'SimpleWorkflowDesignEditor' })
let nodeConfig = ref({
nodeName: '发起人',
type: 0,
id: 'root',
formPerms: {},
nodeUserList: [],
childNode: {}
})
</script>
<style>
@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
</style>

@ -1,5 +1,9 @@
<!-- 工作流 - 抄送我的流程 -->
<template>
<doc-alert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<ContentWrap>
<!-- 搜索工作栏 -->
@ -7,6 +11,7 @@
<el-form-item label="流程名称" prop="name">
<el-input
v-model="queryParams.processInstanceName"
@keyup.enter="handleQuery"
class="!w-240px"
clearable
placeholder="请输入流程名称"
@ -40,7 +45,12 @@
<ContentWrap>
<el-table v-loading="loading" :data="list">
<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
align="center"
label="流程发起人"
prop="startUser.nickname"
min-width="100"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
@ -48,8 +58,11 @@
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="抄送节点" prop="activityName" min-width="180" />
<el-table-column align="center" label="抄送人" min-width="100">
<template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
</el-table-column>
<el-table-column align="center" label="抄送意见" prop="reason" width="150" />
<el-table-column
align="center"
label="抄送时间"
@ -106,11 +119,16 @@ const getList = async () => {
/** 处理审批按钮 */
const handleAudit = (row: any) => {
const query = {
id: row.processInstanceId,
activityId: undefined
}
if (row.activityId) {
query.activityId = row.activityId
}
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstanceId
}
query: query
})
}

@ -1,5 +1,11 @@
<template>
<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>
<!-- 搜索工作栏 -->
@ -10,7 +16,7 @@
class="-mb-15px"
label-width="68px"
>
<el-form-item label="任务名称" prop="name">
<el-form-item label="" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
@ -19,27 +25,96 @@
@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-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
class="!w-155px"
@change="handleQuery"
>
<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" :style="{ position: 'absolute', right: '130px' }">
<el-select
v-model="queryParams.status"
placeholder="请选择流程状态"
clearable
class="!w-155px"
@change="handleQuery"
>
<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 :style="{ position: 'absolute', right: '0px' }">
<el-popover
:visible="showPopover"
persistent
:width="400"
:show-arrow="false"
placement="bottom-end"
>
<template #reference>
<el-button @click="showPopover = !showPopover" >
<Icon icon="ep:plus" class="mr-5px" />高级筛选
</el-button>
</template>
<el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择流程发起人"
clearable
class="!w-390px"
>
<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="发起时间" class="bold-label" label-position="top" 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 class="bold-label" label-position="top">
<el-button @click="handleQuery"> </el-button>
<el-button @click="showPopover = false"> 取消</el-button>
<el-button @click="resetQuery"> </el-button>
</el-form-item>
</el-popover>
</el-form-item>
</el-form>
</ContentWrap>
@ -104,9 +179,10 @@
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import * as TaskApi from '@/api/bpm/task'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
defineOptions({ name: 'BpmTodoTask' })
@ -119,9 +195,13 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: '',
category: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() //
const categoryList = ref<CategoryVO[]>([]) //
const showPopover = ref(false)
/** 查询任务列表 */
const getList = async () => {
@ -152,13 +232,15 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}
/** 初始化 **/
onMounted(() => {
getList()
onMounted(async () => {
await getList()
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>

@ -1,5 +1,5 @@
<template>
<doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,11 @@
<template>
<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>
<!-- 搜索工作栏 -->
@ -10,7 +16,7 @@
class="-mb-15px"
label-width="68px"
>
<el-form-item label="任务名称" prop="name">
<el-form-item label="" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
@ -19,27 +25,79 @@
@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-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
<el-select
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
class="!w-155px"
@change="handleQuery"
>
<el-option
v-for="category in categoryList"
:key="category.code"
:label="category.name"
:value="category.code"
/>
</el-select>
</el-form-item>
<!-- 高级筛选 -->
<el-form-item :style="{ position: 'absolute', right: '0px' }">
<el-popover
:visible="showPopover"
persistent
:width="400"
:show-arrow="false"
placement="bottom-end"
>
<template #reference>
<el-button @click="showPopover = !showPopover" >
<Icon icon="ep:plus" class="mr-5px" />高级筛选
</el-button>
</template>
<el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择流程发起人"
clearable
class="!w-390px"
>
<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="发起时间" class="bold-label" label-position="top" 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 class="bold-label" label-position="top">
<el-button @click="handleQuery"> </el-button>
<el-button @click="showPopover = false"> 取消</el-button>
<el-button @click="resetQuery"> </el-button>
</el-form-item>
</el-popover>
</el-form-item>
</el-form>
</ContentWrap>
@ -89,6 +147,7 @@
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as TaskApi from '@/api/bpm/task'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
defineOptions({ name: 'BpmTodoTask' })
@ -101,9 +160,11 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: '',
category: undefined,
createTime: []
})
const queryFormRef = ref() //
const categoryList = ref<CategoryVO[]>([]) //
/** 查询任务列表 */
const getList = async () => {
@ -117,6 +178,8 @@ const getList = async () => {
}
}
const showPopover = ref(false)
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
@ -134,13 +197,15 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}
/** 初始化 **/
onMounted(() => {
getList()
onMounted(async () => {
await getList()
categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>

@ -1,5 +1,5 @@
<template>
<doc-alert title="【通用】跟进记录、待办事项" url="https://doc.iocoder.cn/crm/follow-up/" />
<el-row :gutter="20">
<el-col :span="4" class="min-w-[200px]">

@ -1,5 +1,6 @@
<template>
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,6 @@
<template>
<doc-alert title="【商机】商机管理、商机状态" url="https://doc.iocoder.cn/crm/business/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -127,6 +127,7 @@
placeholder="选择下次联系时间"
type="datetime"
value-format="x"
class="!w-1/1"
/>
</el-form-item>
</el-col>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【线索】线索管理" url="https://doc.iocoder.cn/crm/clue/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -90,7 +90,7 @@
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@ -153,6 +153,7 @@
placeholder="选择下次联系时间"
type="datetime"
value-format="x"
class="!w-1/1"
/>
</el-form-item>
</el-col>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,6 @@
<template>
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<el-form
@ -26,8 +27,8 @@
@change="changeNotifyEnable"
class="ml-4"
>
<el-radio :label="false" size="large">不提醒</el-radio>
<el-radio :label="true" size="large">提醒</el-radio>
<el-radio :value="false" size="large">不提醒</el-radio>
<el-radio :value="true" size="large">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.notifyEnabled">

@ -36,7 +36,7 @@
ref="permissionListRef"
:biz-id="contract.id!"
:biz-type="BizTypeEnum.CRM_CONTRACT"
:show-action="false"
:show-action="true"
@quit-team="close"
/>
</el-tab-pane>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【合同】合同管理、合同提醒" url="https://doc.iocoder.cn/crm/contract/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -127,6 +127,7 @@
placeholder="选择下次联系时间"
type="datetime"
value-format="x"
class="!w-1/1"
/>
</el-form-item>
</el-col>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,6 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<!-- 列表 -->
<ContentWrap>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,6 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<el-form
@ -26,8 +27,8 @@
<!-- 表单 -->
<el-form-item label="客户公海规则设置" prop="enabled">
<el-radio-group v-model="formData.enabled" @change="changeEnable" class="ml-4">
<el-radio :label="false" size="large">不启用</el-radio>
<el-radio :label="true" size="large">启用</el-radio>
<el-radio :value="false" size="large">不启用</el-radio>
<el-radio :value="true" size="large">启用</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.enabled">
@ -43,8 +44,8 @@
@change="changeNotifyEnable"
class="ml-4"
>
<el-radio :label="false" size="large">不提醒</el-radio>
<el-radio :label="true" size="large">提醒</el-radio>
<el-radio :value="false" size="large">不提醒</el-radio>
<el-radio :value="true" size="large">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.notifyEnabled">

@ -23,7 +23,7 @@
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)"
:key="dict.value"
>
<el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :label="dict.value">
<el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :value="dict.value">
{{ dict.label }}
</el-radio>
</template>
@ -34,9 +34,9 @@
label="同时添加至"
>
<el-checkbox-group v-model="formData.toBizTypes">
<el-checkbox :label="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
<el-checkbox :label="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
<el-checkbox :label="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>

@ -20,8 +20,8 @@
</el-form-item>
<el-form-item label="老负责人">
<el-radio-group v-model="oldOwnerHandler" @change="handleOwnerChange">
<el-radio :label="false" size="large">移除</el-radio>
<el-radio :label="true" size="large">加入团队</el-radio>
<el-radio :value="false" size="large">移除</el-radio>
<el-radio :value="true" size="large">加入团队</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="oldOwnerHandler" label="老负责人权限级别" prop="oldOwnerPermissionLevel">
@ -30,7 +30,7 @@
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)"
:key="dict.value"
>
<el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :label="dict.value">
<el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :value="dict.value">
{{ dict.label }}
</el-radio>
</template>
@ -38,9 +38,9 @@
</el-form-item>
<el-form-item v-if="bizType === BizTypeEnum.CRM_CUSTOMER" label="同时转移">
<el-checkbox-group v-model="formData.toBizTypes">
<el-checkbox :label="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
<el-checkbox :label="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
<el-checkbox :label="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_CONTACT">联系人</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_BUSINESS">商机</el-checkbox>
<el-checkbox :value="BizTypeEnum.CRM_CONTRACT">合同</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>

@ -1,5 +1,5 @@
<template>
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -24,7 +24,7 @@ defineOptions({ name: 'CrmProductDetail' })
const route = useRoute()
const message = useMessage()
const id = Number(route.params.id) //
const id = route.params.id //
const loading = ref(true) //
const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) //

@ -1,5 +1,5 @@
<template>
<doc-alert title="【产品】产品管理、产品分类" url="https://doc.iocoder.cn/crm/product/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -124,6 +124,7 @@
placeholder="选择回款日期"
type="date"
value-format="x"
class="!w-100%"
/>
</el-form-item>
</el-col>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -89,6 +89,7 @@
placeholder="选择计划回款日期"
type="date"
value-format="x"
class="!w-100%"
/>
</el-form-item>
</el-col>

@ -1,5 +1,6 @@
<template>
<doc-alert title="【回款】回款管理、回款计划" url="https://doc.iocoder.cn/crm/receivable/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,5 +1,4 @@
<!-- 员工业绩统计 -->
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
@ -173,7 +172,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
s.lastMonthCount !== 0
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
: 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
@ -181,7 +182,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
s.lastYearCount !== 0
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
: 'NULL'
)
}
@ -197,8 +200,8 @@ const tableData = reactive([
{ title: '当月合同数量统计(个)' },
{ title: '上月合同数量统计(个)' },
{ title: '去年当月合同数量统计(个)' },
{ title: '比增长率(%' },
{ title: '比增长率(%' }
{ title: '比增长率(%' },
{ title: '比增长率(%' }
])
// convertListData
@ -214,9 +217,13 @@ const convertListData = () => {
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
item.lastMonthCount !== 0
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
: 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
item.lastYearCount !== 0
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
: 'NULL'
})
}

@ -1,5 +1,4 @@
<!-- 员工业绩统计 -->
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
@ -173,7 +172,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
s.lastMonthCount !== 0
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
: 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
@ -181,7 +182,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
s.lastYearCount !== 0
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
: 'NULL'
)
}
@ -197,8 +200,8 @@ const tableData = reactive([
{ title: '当月合同金额统计(元)' },
{ title: '上月合同金额统计(元)' },
{ title: '去年当月合同金额统计(元)' },
{ title: '比增长率(%' },
{ title: '比增长率(%' }
{ title: '比增长率(%' },
{ title: '比增长率(%' }
])
// init
@ -214,9 +217,13 @@ const convertListData = () => {
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
item.lastMonthCount !== 0
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
: 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
item.lastYearCount !== 0
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
: 'NULL'
})
}

@ -17,7 +17,6 @@
:prop="item.prop"
align="center"
>
<!-- TODO @scholarIDEA 爆红的处理 -->
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '比增长率(%',
name: '比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
@ -121,7 +120,6 @@ const echartsOption = reactive<EChartsOption>({
type: 'value',
name: '',
axisTick: {
// TODO @scholarIDEA
alignWithLabel: true,
lineStyle: {
width: 0
@ -174,7 +172,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
s.lastMonthCount !== 0
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
: 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[1]['data']) {
@ -182,7 +182,9 @@ const loadData = async () => {
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
s.lastYearCount !== 0
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
: 'NULL'
)
}
@ -193,14 +195,13 @@ const loadData = async () => {
}
//
// TODO @scholar as any[] idea
const columnsData = reactive([] as any[])
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月回款金额统计(元)' },
{ title: '上月回款金额统计(元)' },
{ title: '去年当月回款金额统计(元)' },
{ title: '比增长率(%' },
{ title: '比增长率(%' }
{ title: '比增长率(%' },
{ title: '比增长率(%' }
])
// init
@ -215,11 +216,14 @@ const convertListData = () => {
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
// TODO @scholar使 erpCalculatePercentage 0 0
tableData[3]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
item.lastMonthCount !== 0
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
: 'NULL'
tableData[4]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
item.lastYearCount !== 0
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
: 'NULL'
})
}

@ -73,7 +73,7 @@
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { beginOfDay, formatDate } from '@/utils/formatTime'
import { beginOfDay, endOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import ContractCountPerformance from './components/ContractCountPerformance.vue'
import ContractPricePerformance from './components/ContractPricePerformance.vue'
@ -85,8 +85,8 @@ const queryParams = reactive({
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
//
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)))
formatDate(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
formatDate(endOfDay(new Date(new Date().getFullYear(), 11, 31)))
]
})
@ -100,31 +100,20 @@ const userListByDeptId = computed(() =>
: []
)
// TODO @scholar vue
//
const activeTab = ref('ContractCountPerformance')
// 1.
const ContractCountPerformanceRef = ref()
// 2.
const ContractPricePerformanceRef = ref()
// 3.
const ReceivablePricePerformanceRef = ref()
const ContractCountPerformanceRef = ref() //
const ContractPricePerformanceRef = ref() //
const ReceivablePricePerformanceRef = ref() //
/** 搜索按钮操作 */
const handleQuery = () => {
// queryParams.times[0]
const selectYear = parseInt(queryParams.times[0])
queryParams.times[0] = formatDate(beginOfDay(new Date(selectYear, 0, 1)))
queryParams.times[1] = formatDate(endOfDay(new Date(selectYear, 11, 31)))
// Date
const fullDate = new Date(selectYear, 0, 1, 0, 0, 0)
// 2004-01-01 00:00:00
// TODO @scholar使 year
queryParams.times[0] = `${fullDate.getFullYear()}-${String(fullDate.getMonth() + 1).padStart(
2,
'0'
)}-${String(fullDate.getDate()).padStart(2, '0')} ${String(fullDate.getHours()).padStart(2, '0')}:${String(fullDate.getMinutes()).padStart(2, '0')}:${String(fullDate.getSeconds()).padStart(2, '0')}`
//
switch (activeTab.value) {
case 'ContractCountPerformance':
ContractCountPerformanceRef.value?.loadData?.()

@ -1,4 +1,5 @@
<template>
<doc-alert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,4 +1,5 @@
<template>
<doc-alert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,16 +1,17 @@
<template>
<ContentWrap>
<el-row>
<el-col>
<div class="float-right mb-2">
<el-button size="small" type="primary" @click="showJson"> JSON</el-button>
<el-button size="small" type="success" @click="showOption"> Options</el-button>
<el-button size="small" type="danger" @click="showTemplate"></el-button>
</div>
</el-col>
</el-row>
<ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
<!-- 表单设计器 -->
<FcDesigner ref="designer" height="780px" />
<div
class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
>
<fc-designer class="my-designer" ref="designer" :config="designerConfig">
<template #handle>
<el-button size="small" type="primary" plain @click="showJson">JSON</el-button>
<el-button size="small" type="success" plain @click="showOption">Options</el-button>
<el-button size="small" type="danger" plain @click="showTemplate"></el-button>
</template>
</fc-designer>
</div>
</ContentWrap>
<!-- 弹窗表单预览 -->
@ -43,6 +44,35 @@ defineOptions({ name: 'InfraBuild' })
const { t } = useI18n() //
const message = useMessage() //
//
const designerConfig = ref({
switchType: [], // ,
autoActive: true, //
useTemplate: false, // vue2
formOptions: {
form: {
labelWidth: '100px' // label 100px
}
}, //
fieldReadonly: false, // field
hiddenDragMenu: false, //
hiddenDragBtn: false, //
hiddenMenu: [], //
hiddenItem: [], //
hiddenItemConfig: {}, //
disabledItemConfig: {}, //
showSaveBtn: false, //
showConfig: true, //
showBaseForm: true, //
showControl: true, //
showPropsForm: true, //
showEventForm: true, //
showValidateForm: true, //
showFormConfig: true, //
showInputData: true, //
showDevice: true, //
appendConfigData: [] // formData
})
const designer = ref() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
@ -140,3 +170,13 @@ onMounted(async () => {
hljs.registerLanguage('json', json)
})
</script>
<style>
.my-designer {
._fc-l,
._fc-m,
._fc-r {
border-top: none;
}
}
</style>

@ -37,26 +37,26 @@
</el-table-column>
<el-table-column label="插入" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.createOperation" false-label="false" true-label="true" />
<el-checkbox v-model="scope.row.createOperation" false-value="false" true-value="true" />
</template>
</el-table-column>
<el-table-column label="编辑" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.updateOperation" false-label="false" true-label="true" />
<el-checkbox v-model="scope.row.updateOperation" false-value="false" true-value="true" />
</template>
</el-table-column>
<el-table-column label="列表" min-width="4%">
<template #default="scope">
<el-checkbox
v-model="scope.row.listOperationResult"
false-label="false"
true-label="true"
false-value="false"
true-value="true"
/>
</template>
</el-table-column>
<el-table-column label="查询" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.listOperation" false-label="false" true-label="true" />
<el-checkbox v-model="scope.row.listOperation" false-value="false" true-value="true" />
</template>
</el-table-column>
<el-table-column label="查询方式" min-width="10%">
@ -75,7 +75,7 @@
</el-table-column>
<el-table-column label="允许空" min-width="5%">
<template #default="scope">
<el-checkbox v-model="scope.row.nullable" false-label="false" true-label="true" />
<el-checkbox v-model="scope.row.nullable" false-value="false" true-value="true" />
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="12%">

@ -285,8 +285,8 @@
</span>
</template>
<el-radio-group v-model="formData.subJoinMany" placeholder="请选择">
<el-radio :label="true">一对多</el-radio>
<el-radio :label="false">一对一</el-radio>
<el-radio :value="true">一对多</el-radio>
<el-radio :value="false">一对一</el-radio>
</el-radio-group>
</el-form-item>
</el-col>

@ -1,4 +1,8 @@
<template>
<doc-alert title="代码生成(单表)" url="https://doc.iocoder.cn/new-feature/" />
<doc-alert title="代码生成(树表)" url="https://doc.iocoder.cn/new-feature/tree/" />
<doc-alert title="代码生成(主子表)" url="https://doc.iocoder.cn/new-feature/master-sub/" />
<doc-alert title="单元测试" url="https://doc.iocoder.cn/unit-test/" />
<!-- 搜索 -->
<ContentWrap>

@ -24,7 +24,7 @@
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,4 +1,5 @@
<template>
<doc-alert title="配置中心" url="https://doc.iocoder.cn/config-center/" />
<!-- 搜索 -->
<ContentWrap>

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,4 +1,5 @@
<template>
<doc-alert title="代码生成(单表)" url="https://doc.iocoder.cn/new-feature/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,4 +1,5 @@
<template>
<doc-alert title="代码生成(树表)" url="https://doc.iocoder.cn/new-feature/tree/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,4 +1,5 @@
<template>
<doc-alert title="代码生成(主子表)" url="https://doc.iocoder.cn/new-feature/master-sub/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,4 +1,5 @@
<template>
<doc-alert title="代码生成(主子表)" url="https://doc.iocoder.cn/new-feature/master-sub/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -15,7 +15,7 @@
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>

@ -1,4 +1,5 @@
<template>
<doc-alert title="代码生成(主子表)" url="https://doc.iocoder.cn/new-feature/master-sub/" />
<ContentWrap>
<!-- 搜索工作栏 -->

@ -1,7 +1,9 @@
<template>
<doc-alert title="数据库 MyBatis" url="https://doc.iocoder.cn/mybatis/" />
<doc-alert title="多数据源(读写分离)" url="https://doc.iocoder.cn/dynamic-datasource/" />
<ContentWrap>
<IFrame v-if="!loading" :src="url" />
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="url" />
</ContentWrap>
</template>
<script lang="ts" setup>

@ -1,4 +1,5 @@
<template>
<doc-alert title="上传下载" url="https://doc.iocoder.cn/file/" />
<!-- 搜索 -->
<ContentWrap>
<el-form
@ -14,6 +15,7 @@
placeholder="请输入文件路径"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="文件类型" prop="type" width="80">
@ -22,6 +24,7 @@
placeholder="请输入文件类型"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
@ -32,6 +35,7 @@
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>

@ -66,8 +66,8 @@
</el-form-item>
<el-form-item v-if="formData.storage === 11" label="连接模式" prop="config.mode">
<el-radio-group v-model="formData.config.mode">
<el-radio key="Active" label="Active">主动模式</el-radio>
<el-radio key="Passive" label="Passive">被动模式</el-radio>
<el-radio key="Active" value="Active">主动模式</el-radio>
<el-radio key="Passive" value="Passive">被动模式</el-radio>
</el-radio-group>
</el-form-item>
<!-- S3 -->

@ -1,5 +1,5 @@
<template>
<doc-alert title="上传下载" url="https://doc.iocoder.cn/file/" />
<!-- 搜索 -->
<ContentWrap>
@ -206,7 +206,8 @@ const handleMaster = async (id) => {
const handleTest = async (id) => {
try {
const response = await FileConfigApi.testFileConfig(id)
message.alert('测试通过,上传文件成功!访问地址:' + response)
await message.confirm('是否要访问该文件?', '测试上传成功')
window.open(response, '_blank')
} catch {}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save