Commit 7639f34e by YunaiV

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/iot

parents 77e0a763 49fc9b54
...@@ -32,3 +32,6 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' ...@@ -32,3 +32,6 @@ VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# 验证码的开关 # 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true VITE_APP_CAPTCHA_ENABLE=true
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
...@@ -29,3 +29,6 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000' ...@@ -29,3 +29,6 @@ VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 验证码的开关 # 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false VITE_APP_CAPTCHA_ENABLE=false
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-prod ...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-prod
# 商城H5会员端域名 # 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-stage ...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-stage
# 商城H5会员端域名 # 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-test ...@@ -29,3 +29,6 @@ VITE_OUT_DIR=dist-test
# 商城H5会员端域名 # 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.3.0-snapshot", "version": "2.4.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.8.4", "element-plus": "2.9.1",
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
...@@ -96,8 +96,8 @@ ...@@ -96,8 +96,8 @@
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"bpmn-js": "8.10.0", "bpmn-js": "^17.9.2",
"bpmn-js-properties-panel": "0.46.0", "bpmn-js-properties-panel": "5.23.0",
"consola": "^3.2.3", "consola": "^3.2.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
......
...@@ -22,11 +22,6 @@ export const register = (data: RegisterVO) => { ...@@ -22,11 +22,6 @@ export const register = (data: RegisterVO) => {
return request.post({ url: '/system/auth/register', data }) return request.post({ url: '/system/auth/register', data })
} }
// 刷新访问令牌
export const refreshToken = () => {
return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
}
// 使用租户名,获得租户编号 // 使用租户名,获得租户编号
export const getTenantIdByName = (name: string) => { export const getTenantIdByName = (name: string) => {
return request.get({ url: '/system/tenant/get-id-by-name?name=' + name }) return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
...@@ -76,11 +71,17 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => { ...@@ -76,11 +71,17 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => {
}) })
} }
// 获取验证图片以及 token // 获取验证图片以及 token
export const getCode = (data) => { export const getCode = (data: any) => {
debugger
return request.postOriginal({ url: 'system/captcha/get', data }) return request.postOriginal({ url: 'system/captcha/get', data })
} }
// 滑动或者点选验证 // 滑动或者点选验证
export const reqCheck = (data) => { export const reqCheck = (data: any) => {
return request.postOriginal({ url: 'system/captcha/check', data }) return request.postOriginal({ url: 'system/captcha/check', data })
} }
// 通过短信重置密码
export const smsResetPassword = (data: any) => {
return request.post({ url: '/system/auth/sms-reset-password', data })
}
...@@ -13,6 +13,11 @@ export interface BrokerageUserVO { ...@@ -13,6 +13,11 @@ export interface BrokerageUserVO {
avatar: string avatar: string
} }
// 创建分销用户
export const createBrokerageUser = (data: any) => {
return request.post({ url: '/trade/brokerage-user/create', data })
}
// 查询分销用户列表 // 查询分销用户列表
export const getBrokerageUserPage = async (params: any) => { export const getBrokerageUserPage = async (params: any) => {
return await request.get({ url: `/trade/brokerage-user/page`, params }) return await request.get({ url: `/trade/brokerage-user/page`, params })
......
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg>
\ No newline at end of file
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
<div class="h-40px flex items-center justify-center"> <div class="h-40px flex items-center justify-center">
<MagicCubeEditor <MagicCubeEditor
v-model="cellList" v-model="cellList"
class="m-b-16px"
:rows="1"
:cols="cellCount" :cols="cellCount"
:cube-size="38" :cube-size="38"
:rows="1"
class="m-b-16px"
@hot-area-selected="handleHotAreaSelected" @hot-area-selected="handleHotAreaSelected"
/> />
<img src="@/assets/imgs/diy/app-nav-bar-mp.png" alt="" class="h-30px w-76px" v-if="isMp" /> <img v-if="isMp" alt="" class="h-30px w-76px" src="@/assets/imgs/diy/app-nav-bar-mp.png" />
</div> </div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex"> <template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex"> <template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`"> <el-form-item :prop="`cell[${cellIndex}].type`" label="类型">
<el-radio-group v-model="cell.type"> <el-radio-group v-model="cell.type">
<el-radio value="text">文字</el-radio> <el-radio value="text">文字</el-radio>
<el-radio value="image">图片</el-radio> <el-radio value="image">图片</el-radio>
...@@ -21,37 +21,40 @@ ...@@ -21,37 +21,40 @@
</el-form-item> </el-form-item>
<!-- 1. 文字 --> <!-- 1. 文字 -->
<template v-if="cell.type === 'text'"> <template v-if="cell.type === 'text'">
<el-form-item label="内容" :prop="`cell[${cellIndex}].text`"> <el-form-item :prop="`cell[${cellIndex}].text`" label="内容">
<el-input v-model="cell!.text" maxlength="10" show-word-limit /> <el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item> </el-form-item>
<el-form-item label="颜色" :prop="`cell[${cellIndex}].text`"> <el-form-item :prop="`cell[${cellIndex}].text`" label="颜色">
<ColorInput v-model="cell!.textColor" /> <ColorInput v-model="cell!.textColor" />
</el-form-item> </el-form-item>
<el-form-item :prop="`cell[${cellIndex}].url`" label="链接">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</template> </template>
<!-- 2. 图片 --> <!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'"> <template v-else-if="cell.type === 'image'">
<el-form-item label="图片" :prop="`cell[${cellIndex}].imgUrl`"> <el-form-item :prop="`cell[${cellIndex}].imgUrl`" label="图片">
<UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px"> <UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
<template #tip>建议尺寸 56*56</template> <template #tip>建议尺寸 56*56</template>
</UploadImg> </UploadImg>
</el-form-item> </el-form-item>
<el-form-item label="链接" :prop="`cell[${cellIndex}].url`"> <el-form-item :prop="`cell[${cellIndex}].url`" label="链接">
<AppLinkInput v-model="cell.url" /> <AppLinkInput v-model="cell.url" />
</el-form-item> </el-form-item>
</template> </template>
<!-- 3. 搜索框 --> <!-- 3. 搜索框 -->
<template v-else> <template v-else>
<el-form-item label="提示文字" :prop="`cell[${cellIndex}].placeholder`"> <el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit /> <el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item> </el-form-item>
<el-form-item label="圆角" :prop="`cell[${cellIndex}].borderRadius`"> <el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
<el-slider <el-slider
v-model="cell.borderRadius" v-model="cell.borderRadius"
:max="100" :max="100"
:min="0" :min="0"
show-input
input-size="small"
:show-input-controls="false" :show-input-controls="false"
input-size="small"
show-input
/> />
</el-form-item> </el-form-item>
</template> </template>
...@@ -59,7 +62,7 @@ ...@@ -59,7 +62,7 @@
</template> </template>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { NavigationBarCellProperty } from '../config' import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util' import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板 // 导航栏属性面板
...@@ -87,4 +90,4 @@ const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: numb ...@@ -87,4 +90,4 @@ const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: numb
} }
</script> </script>
<style scoped lang="scss"></style> <style lang="scss" scoped></style>
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
/** 标题栏属性 */ /** 标题栏属性 */
export interface TitleBarProperty { export interface TitleBarProperty {
// 背景图
bgImgUrl: string
// 偏移 // 偏移
marginLeft: number marginLeft: number
// 显示位置 // 显示位置
......
<template> <template>
<div class="title-bar"> <div
<el-image v-if="property.bgImgUrl" :src="property.bgImgUrl" fit="cover" class="w-full" /> :style="{
<div class="absolute left-0 top-0 w-full"> background:
property.style.bgType === 'color' ? property.style.bgColor : `url(${property.style.bgImg})`,
backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat'
}"
class="title-bar"
>
<!-- 内容 -->
<div>
<!-- 标题 --> <!-- 标题 -->
<div <div
v-if="property.title"
:style="{ :style="{
fontSize: `${property.titleSize}px`, fontSize: `${property.titleSize}px`,
fontWeight: property.titleWeight, fontWeight: property.titleWeight,
color: property.titleColor, color: property.titleColor,
textAlign: property.textAlign textAlign: property.textAlign
}" }"
v-if="property.title"
> >
{{ property.title }} {{ property.title }}
</div> </div>
<!-- 副标题 --> <!-- 副标题 -->
<div <div
v-if="property.description"
:style="{ :style="{
fontSize: `${property.descriptionSize}px`, fontSize: `${property.descriptionSize}px`,
fontWeight: property.descriptionWeight, fontWeight: property.descriptionWeight,
...@@ -23,25 +32,24 @@ ...@@ -23,25 +32,24 @@
textAlign: property.textAlign textAlign: property.textAlign
}" }"
class="m-t-8px" class="m-t-8px"
v-if="property.description"
> >
{{ property.description }} {{ property.description }}
</div> </div>
</div> </div>
<!-- 更多 --> <!-- 更多 -->
<div <div
class="more"
v-show="property.more.show" v-show="property.more.show"
:style="{ :style="{
color: property.descriptionColor color: property.descriptionColor
}" }"
class="more"
> >
<span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span> <span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span>
<Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" /> <Icon v-if="property.more.type !== 'text'" icon="ep:arrow-right" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { TitleBarProperty } from './config' import { TitleBarProperty } from './config'
/** 标题栏 */ /** 标题栏 */
...@@ -49,7 +57,7 @@ defineOptions({ name: 'TitleBar' }) ...@@ -49,7 +57,7 @@ defineOptions({ name: 'TitleBar' })
defineProps<{ property: TitleBarProperty }>() defineProps<{ property: TitleBarProperty }>()
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
.title-bar { .title-bar {
position: relative; position: relative;
width: 100%; width: 100%;
......
<template> <template>
<ComponentContainerProperty v-model="formData.style"> <ComponentContainerProperty v-model="formData.style">
<el-form label-width="85px" :model="formData" :rules="rules"> <el-form :model="formData" :rules="rules" label-width="85px">
<el-card header="风格" class="property-group" shadow="never"> <el-card class="property-group" header="风格" shadow="never">
<el-form-item label="背景图片" prop="bgImgUrl">
<UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
<template #tip>建议尺寸 750*80</template>
</UploadImg>
</el-form-item>
<el-form-item label="标题位置" prop="textAlign"> <el-form-item label="标题位置" prop="textAlign">
<el-radio-group v-model="formData!.textAlign"> <el-radio-group v-model="formData!.textAlign">
<el-tooltip content="居左" placement="top"> <el-tooltip content="居左" placement="top">
...@@ -22,65 +17,65 @@ ...@@ -22,65 +17,65 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-card> </el-card>
<el-card header="主标题" class="property-group" shadow="never"> <el-card class="property-group" header="主标题" shadow="never">
<el-form-item label="文字" prop="title" label-width="40px"> <el-form-item label="文字" label-width="40px" prop="title">
<InputWithColor <InputWithColor
v-model="formData.title" v-model="formData.title"
v-model:color="formData.titleColor" v-model:color="formData.titleColor"
show-word-limit
maxlength="20" maxlength="20"
show-word-limit
/> />
</el-form-item> </el-form-item>
<el-form-item label="大小" prop="titleSize" label-width="40px"> <el-form-item label="大小" label-width="40px" prop="titleSize">
<el-slider <el-slider
v-model="formData.titleSize" v-model="formData.titleSize"
:max="60" :max="60"
:min="10" :min="10"
show-input
input-size="small" input-size="small"
show-input
/> />
</el-form-item> </el-form-item>
<el-form-item label="粗细" prop="titleWeight" label-width="40px"> <el-form-item label="粗细" label-width="40px" prop="titleWeight">
<el-slider <el-slider
v-model="formData.titleWeight" v-model="formData.titleWeight"
:min="100"
:max="900" :max="900"
:min="100"
:step="100" :step="100"
show-input
input-size="small" input-size="small"
show-input
/> />
</el-form-item> </el-form-item>
</el-card> </el-card>
<el-card header="副标题" class="property-group" shadow="never"> <el-card class="property-group" header="副标题" shadow="never">
<el-form-item label="文字" prop="description" label-width="40px"> <el-form-item label="文字" label-width="40px" prop="description">
<InputWithColor <InputWithColor
v-model="formData.description" v-model="formData.description"
v-model:color="formData.descriptionColor" v-model:color="formData.descriptionColor"
show-word-limit
maxlength="50" maxlength="50"
show-word-limit
/> />
</el-form-item> </el-form-item>
<el-form-item label="大小" prop="descriptionSize" label-width="40px"> <el-form-item label="大小" label-width="40px" prop="descriptionSize">
<el-slider <el-slider
v-model="formData.descriptionSize" v-model="formData.descriptionSize"
:max="60" :max="60"
:min="10" :min="10"
show-input
input-size="small" input-size="small"
show-input
/> />
</el-form-item> </el-form-item>
<el-form-item label="粗细" prop="descriptionWeight" label-width="40px"> <el-form-item label="粗细" label-width="40px" prop="descriptionWeight">
<el-slider <el-slider
v-model="formData.descriptionWeight" v-model="formData.descriptionWeight"
:min="100"
:max="900" :max="900"
:min="100"
:step="100" :step="100"
show-input
input-size="small" input-size="small"
show-input
/> />
</el-form-item> </el-form-item>
</el-card> </el-card>
<el-card header="查看更多" class="property-group" shadow="never"> <el-card class="property-group" header="查看更多" shadow="never">
<el-form-item label="是否显示" prop="more.show"> <el-form-item label="是否显示" prop="more.show">
<el-checkbox v-model="formData.more.show" /> <el-checkbox v-model="formData.more.show" />
</el-form-item> </el-form-item>
...@@ -93,7 +88,7 @@ ...@@ -93,7 +88,7 @@
<el-radio value="all">文字+图标</el-radio> <el-radio value="all">文字+图标</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'"> <el-form-item v-show="formData.more.type !== 'icon'" label="更多文字" prop="more.text">
<el-input v-model="formData.more.text" /> <el-input v-model="formData.more.text" />
</el-form-item> </el-form-item>
<el-form-item label="跳转链接" prop="more.url"> <el-form-item label="跳转链接" prop="more.url">
...@@ -104,7 +99,7 @@ ...@@ -104,7 +99,7 @@
</el-form> </el-form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { TitleBarProperty } from './config' import { TitleBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util' import { usePropertyForm } from '@/components/DiyEditor/util'
// 导航栏属性面板 // 导航栏属性面板
...@@ -118,4 +113,4 @@ const { formData } = usePropertyForm(props.modelValue, emit) ...@@ -118,4 +113,4 @@ const { formData } = usePropertyForm(props.modelValue, emit)
const rules = {} const rules = {}
</script> </script>
<style scoped lang="scss"></style> <style lang="scss" scoped></style>
...@@ -12,17 +12,17 @@ ...@@ -12,17 +12,17 @@
<el-button-group class="header-right"> <el-button-group class="header-right">
<el-tooltip content="重置"> <el-tooltip content="重置">
<el-button @click="handleReset"> <el-button @click="handleReset">
<Icon icon="system-uicons:reset-alt" :size="24" /> <Icon :size="24" icon="system-uicons:reset-alt" />
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="预览" v-if="previewUrl"> <el-tooltip v-if="previewUrl" content="预览">
<el-button @click="handlePreview"> <el-button @click="handlePreview">
<Icon icon="ep:view" :size="24" /> <Icon :size="24" icon="ep:view" />
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="保存"> <el-tooltip content="保存">
<el-button @click="handleSave"> <el-button @click="handleSave">
<Icon icon="ep:check" :size="24" /> <Icon :size="24" icon="ep:check" />
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</el-button-group> </el-button-group>
...@@ -31,21 +31,21 @@ ...@@ -31,21 +31,21 @@
<!-- 中心区域 --> <!-- 中心区域 -->
<el-container class="editor-container"> <el-container class="editor-container">
<!-- 左侧:组件库(ComponentLibrary) --> <!-- 左侧:组件库(ComponentLibrary) -->
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" /> <ComponentLibrary v-if="libs && libs.length > 0" ref="componentLibrary" :list="libs" />
<!-- 中心:设计区域(ComponentContainer) --> <!-- 中心:设计区域(ComponentContainer) -->
<div class="editor-center page-prop-area" @click="handlePageSelected"> <div class="editor-center page-prop-area" @click="handlePageSelected">
<!-- 手机顶部 --> <!-- 手机顶部 -->
<div class="editor-design-top"> <div class="editor-design-top">
<!-- 手机顶部状态栏 --> <!-- 手机顶部状态栏 -->
<img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" /> <img alt="" class="status-bar" src="@/assets/imgs/diy/statusBar.png" />
<!-- 手机顶部导航栏 --> <!-- 手机顶部导航栏 -->
<ComponentContainer <ComponentContainer
v-if="showNavigationBar" v-if="showNavigationBar"
:active="selectedComponent?.id === navigationBarComponent.id"
:component="navigationBarComponent" :component="navigationBarComponent"
:show-toolbar="false" :show-toolbar="false"
:active="selectedComponent?.id === navigationBarComponent.id"
@click="handleNavigationBarSelected"
class="cursor-pointer!" class="cursor-pointer!"
@click="handleNavigationBarSelected"
/> />
</div> </div>
<!-- 绝对定位的组件:例如 弹窗、浮动按钮等 --> <!-- 绝对定位的组件:例如 弹窗、浮动按钮等 -->
...@@ -55,43 +55,43 @@ ...@@ -55,43 +55,43 @@
@click="handleComponentSelected(component, index)" @click="handleComponentSelected(component, index)"
> >
<component <component
v-if="component.position === 'fixed' && selectedComponent?.uid === component.uid"
:is="component.id" :is="component.id"
v-if="component.position === 'fixed' && selectedComponent?.uid === component.uid"
:property="component.property" :property="component.property"
/> />
</div> </div>
<!-- 手机页面编辑区域 --> <!-- 手机页面编辑区域 -->
<el-scrollbar <el-scrollbar
height="100%"
wrap-class="editor-design-center page-prop-area"
view-class="phone-container"
:view-style="{ :view-style="{
backgroundColor: pageConfigComponent.property.backgroundColor, backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})` backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
}" }"
height="100%"
view-class="phone-container"
wrap-class="editor-design-center page-prop-area"
> >
<draggable <draggable
class="page-prop-area drag-area"
v-model="pageComponents" v-model="pageComponents"
item-key="index"
:animation="200" :animation="200"
:force-fallback="true"
class="page-prop-area drag-area"
filter=".component-toolbar" filter=".component-toolbar"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
:force-fallback="true"
group="component" group="component"
item-key="index"
@change="handleComponentChange" @change="handleComponentChange"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<ComponentContainer <ComponentContainer
v-if="!element.position || element.position === 'center'" v-if="!element.position || element.position === 'center'"
:component="element"
:active="selectedComponentIndex === index" :active="selectedComponentIndex === index"
:can-move-up="index > 0"
:can-move-down="index < pageComponents.length - 1" :can-move-down="index < pageComponents.length - 1"
@move="(direction) => handleMoveComponent(index, direction)" :can-move-up="index > 0"
:component="element"
@click="handleComponentSelected(element, index)"
@copy="handleCopyComponent(index)" @copy="handleCopyComponent(index)"
@delete="handleDeleteComponent(index)" @delete="handleDeleteComponent(index)"
@click="handleComponentSelected(element, index)" @move="(direction) => handleMoveComponent(index, direction)"
/> />
</template> </template>
</draggable> </draggable>
...@@ -99,9 +99,9 @@ ...@@ -99,9 +99,9 @@
<!-- 手机底部导航 --> <!-- 手机底部导航 -->
<div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']"> <div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
<ComponentContainer <ComponentContainer
:active="selectedComponent?.id === tabBarComponent.id"
:component="tabBarComponent" :component="tabBarComponent"
:show-toolbar="false" :show-toolbar="false"
:active="selectedComponent?.id === tabBarComponent.id"
@click="handleTabBarSelected" @click="handleTabBarSelected"
/> />
</div> </div>
...@@ -109,9 +109,9 @@ ...@@ -109,9 +109,9 @@
<div class="fixed-component-action-group"> <div class="fixed-component-action-group">
<el-tag <el-tag
v-if="showPageConfig" v-if="showPageConfig"
size="large"
:effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'" :effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'"
:type="selectedComponent?.uid === pageConfigComponent.uid ? '' : 'info'" :type="selectedComponent?.uid === pageConfigComponent.uid ? '' : 'info'"
size="large"
@click="handleComponentSelected(pageConfigComponent)" @click="handleComponentSelected(pageConfigComponent)"
> >
<Icon :icon="pageConfigComponent.icon" :size="12" /> <Icon :icon="pageConfigComponent.icon" :size="12" />
...@@ -120,10 +120,10 @@ ...@@ -120,10 +120,10 @@
<template v-for="(component, index) in pageComponents" :key="index"> <template v-for="(component, index) in pageComponents" :key="index">
<el-tag <el-tag
v-if="component.position === 'fixed'" v-if="component.position === 'fixed'"
size="large"
closable
:effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'" :effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'"
:type="selectedComponent?.uid === component.uid ? '' : 'info'" :type="selectedComponent?.uid === component.uid ? '' : 'info'"
closable
size="large"
@click="handleComponentSelected(component)" @click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)" @close="handleDeleteComponent(index)"
> >
...@@ -134,11 +134,11 @@ ...@@ -134,11 +134,11 @@
</div> </div>
</div> </div>
<!-- 右侧:属性面板(ComponentContainerProperty) --> <!-- 右侧:属性面板(ComponentContainerProperty) -->
<el-aside class="editor-right" width="350px" v-if="selectedComponent?.property"> <el-aside v-if="selectedComponent?.property" class="editor-right" width="350px">
<el-card <el-card
shadow="never"
body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]" body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
class="h-full" class="h-full"
shadow="never"
> >
<!-- 组件名称 --> <!-- 组件名称 -->
<template #header> <template #header>
...@@ -152,8 +152,8 @@ ...@@ -152,8 +152,8 @@
view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property" view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
> >
<component <component
:key="selectedComponent?.uid || selectedComponent?.id"
:is="selectedComponent?.id + 'Property'" :is="selectedComponent?.id + 'Property'"
:key="selectedComponent?.uid || selectedComponent?.id"
v-model="selectedComponent.property" v-model="selectedComponent.property"
/> />
</el-scrollbar> </el-scrollbar>
...@@ -166,8 +166,8 @@ ...@@ -166,8 +166,8 @@
<Dialog v-model="previewDialogVisible" title="预览" width="700"> <Dialog v-model="previewDialogVisible" title="预览" width="700">
<div class="flex justify-around"> <div class="flex justify-around">
<IFrame <IFrame
class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!"
:src="previewUrl" :src="previewUrl"
class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!"
/> />
<div class="flex flex-col"> <div class="flex flex-col">
<el-text>手机扫码预览</el-text> <el-text>手机扫码预览</el-text>
...@@ -179,6 +179,7 @@ ...@@ -179,6 +179,7 @@
<script lang="ts"> <script lang="ts">
// 注册所有的组件 // 注册所有的组件
import { components } from './components/mobile/index' import { components } from './components/mobile/index'
export default { export default {
components: { ...components } components: { ...components }
} }
...@@ -257,6 +258,11 @@ watch( ...@@ -257,6 +258,11 @@ watch(
// 保存 // 保存
const handleSave = () => { const handleSave = () => {
// 发送保存通知
emits('save')
}
// 监听配置修改
const pageConfigChange = () => {
const pageConfig = { const pageConfig = {
page: pageConfigComponent.value.property, page: pageConfigComponent.value.property,
navigationBar: navigationBarComponent.value.property, navigationBar: navigationBarComponent.value.property,
...@@ -272,10 +278,19 @@ const handleSave = () => { ...@@ -272,10 +278,19 @@ const handleSave = () => {
// 发送数据更新通知 // 发送数据更新通知
const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
emits('update:modelValue', modelValue) emits('update:modelValue', modelValue)
// 发送保存通知
emits('save', pageConfig)
} }
watch(
() => [
pageConfigComponent.value.property,
navigationBarComponent.value.property,
tabBarComponent.value.property,
pageComponents.value
],
() => {
pageConfigChange()
},
{ deep: true }
)
// 处理页面选中:显示属性表单 // 处理页面选中:显示属性表单
const handlePageSelected = (event: any) => { const handlePageSelected = (event: any) => {
if (!props.showPageConfig) return if (!props.showPageConfig) return
...@@ -547,6 +562,7 @@ $toolbar-height: 42px; ...@@ -547,6 +562,7 @@ $toolbar-height: 42px;
:deep(.el-tag) { :deep(.el-tag) {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
border: none; border: none;
.el-tag__content { .el-tag__content {
width: 100%; width: 100%;
display: flex; display: flex;
......
...@@ -9,6 +9,10 @@ import { useAppStore } from '@/store/modules/app' ...@@ -9,6 +9,10 @@ import { useAppStore } from '@/store/modules/app'
import { isString } from '@/utils/is' import { isString } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import 'echarts/lib/component/markPoint'
import 'echarts/lib/component/markLine'
import 'echarts/lib/component/markArea'
defineOptions({ name: 'EChart' }) defineOptions({ name: 'EChart' })
const { getPrefixCls, variables } = useDesign() const { getPrefixCls, variables } = useDesign()
......
...@@ -6,7 +6,7 @@ import { propTypes } from '@/utils/propTypes' ...@@ -6,7 +6,7 @@ import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is' import { isNumber } from '@/utils/is'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale' import { useLocaleStore } from '@/store/modules/locale'
import { getAccessToken, getTenantId } from '@/utils/auth' import { getRefreshToken, getTenantId } from '@/utils/auth'
import { getUploadUrl } from '@/components/UploadFile/src/useUpload' import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'Editor' }) defineOptions({ name: 'Editor' })
...@@ -100,7 +100,7 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -100,7 +100,7 @@ const editorConfig = computed((): IEditorConfig => {
// 自定义增加 http header // 自定义增加 http header
headers: { headers: {
Accept: '*', Accept: '*',
Authorization: 'Bearer ' + getAccessToken(), Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
'tenant-id': getTenantId() 'tenant-id': getTenantId()
}, },
...@@ -148,7 +148,7 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -148,7 +148,7 @@ const editorConfig = computed((): IEditorConfig => {
// 自定义增加 http header // 自定义增加 http header
headers: { headers: {
Accept: '*', Accept: '*',
Authorization: 'Bearer ' + getAccessToken(), Authorization: 'Bearer ' + getRefreshToken(), // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:Editor 无法方便的刷新访问令牌
'tenant-id': getTenantId() 'tenant-id': getTenantId()
}, },
......
...@@ -79,9 +79,14 @@ function remoteMethod(data) { ...@@ -79,9 +79,14 @@ function remoteMethod(data) {
function handleChange(path) { function handleChange(path) {
router.push({ path }) router.push({ path })
hiddenSearch()
hiddenTopSearch() hiddenTopSearch()
} }
function hiddenSearch() {
showSearch.value = false
}
function hiddenTopSearch() { function hiddenTopSearch() {
showTopSearch.value = false showTopSearch.value = false
} }
...@@ -99,6 +104,8 @@ onUnmounted(() => { ...@@ -99,6 +104,8 @@ onUnmounted(() => {
// 监听 ctrl + k // 监听 ctrl + k
function listenKey(event) { function listenKey(event) {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') { if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
// 阻止触发浏览器默认事件
event.preventDefault()
showSearch.value = !showSearch.value showSearch.value = !showSearch.value
// 这里可以执行相应的操作(例如打开搜索框等) // 这里可以执行相应的操作(例如打开搜索框等)
} }
......
...@@ -39,6 +39,13 @@ ...@@ -39,6 +39,13 @@
</div> </div>
<div class="handler-item-text">包容分支</div> <div class="handler-item-text">包容分支</div>
</div> </div>
<div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
<!-- TODO @芋艿 需要更换一下iconfont的图标 -->
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">延迟器</div>
</div>
</div> </div>
<template #reference> <template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div> <div class="add-icon"><Icon icon="ep:plus" /></div>
...@@ -208,6 +215,16 @@ const addNode = (type: number) => { ...@@ -208,6 +215,16 @@ const addNode = (type: number) => {
} }
emits('update:childNode', data) emits('update:childNode', data)
} }
if (type === NodeType.DELAY_TIMER_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string,
showText: '',
type: NodeType.DELAY_TIMER_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
} }
</script> </script>
......
...@@ -38,6 +38,12 @@ ...@@ -38,6 +38,12 @@
@update:model-value="handleModelValueUpdate" @update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode" @find:parent-node="findFromParentNode"
/> />
<!-- 延迟器节点 -->
<DelayTimerNode
v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 递归显示孩子节点 --> <!-- 递归显示孩子节点 -->
<ProcessNodeTree <ProcessNodeTree
v-if="currentNode && currentNode.childNode" v-if="currentNode && currentNode.childNode"
...@@ -60,6 +66,7 @@ import CopyTaskNode from './nodes/CopyTaskNode.vue' ...@@ -60,6 +66,7 @@ import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue' import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue' import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue' import InclusiveNode from './nodes/InclusiveNode.vue'
import DelayTimerNode from './nodes/DelayTimerNode.vue'
import { SimpleFlowNode, NodeType } from './consts' import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node' import { useWatchNode } from './node'
defineOptions({ defineOptions({
......
<template> <template>
<div v-loading="loading" class="overflow-auto"> <div v-loading="loading" class="overflow-auto">
<SimpleProcessModel <SimpleProcessModel
ref="simpleProcessModelRef"
v-if="processNodeTree" v-if="processNodeTree"
:flow-node="processNodeTree" :flow-node="processNodeTree"
:readonly="false" :readonly="false"
...@@ -38,12 +39,30 @@ import * as UserGroupApi from '@/api/bpm/userGroup' ...@@ -38,12 +39,30 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({ defineOptions({
name: 'SimpleProcessDesigner' name: 'SimpleProcessDesigner'
}) })
const emits = defineEmits(['success']) // 保存成功事件
const emits = defineEmits(['success', 'init-finished']) // 保存成功事件
const props = defineProps({ const props = defineProps({
modelId: { modelId: {
type: String, type: String,
required: true required: false
},
modelKey: {
type: String,
required: false
},
modelName: {
type: String,
required: false
},
// 可发起流程的人员编号
startUserIds : {
type: Array,
required: false
},
value: {
type: [String, Object],
required: false
} }
}) })
...@@ -56,6 +75,10 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表 ...@@ -56,6 +75,10 @@ const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表 const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref() const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表 const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
// 添加当前值的引用
const currentValue = ref<SimpleFlowNode | undefined>()
provide('formFields', formFields) provide('formFields', formFields)
provide('formType', formType) provide('formType', formType)
provide('roleList', roleOptions) provide('roleList', roleOptions)
...@@ -64,33 +87,101 @@ provide('userList', userOptions) ...@@ -64,33 +87,101 @@ provide('userList', userOptions)
provide('deptList', deptOptions) provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions) provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions) provide('deptTree', deptTreeOptions)
provide('startUserIds', props.startUserIds)
const message = useMessage() // 国际化 const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>() const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false) const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = [] let errorNodes: SimpleFlowNode[] = []
// 添加更新模型的方法
const updateModel = () => {
if (!processNodeTree.value) {
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
// 初始化时也触发一次保存
saveSimpleFlowModel(processNodeTree.value)
}
}
// 加载流程数据
const loadProcessData = async (data: any) => {
try {
if (data) {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
processNodeTree.value = parsedData
currentValue.value = parsedData
// 确保数据加载后刷新视图
await nextTick()
if (simpleProcessModelRef.value?.refresh) {
await simpleProcessModelRef.value.refresh()
}
}
} catch (error) {
console.error('加载流程数据失败:', error)
}
}
// 监听属性变化
watch(
() => props.value,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await loadProcessData(newValue)
}
},
{ immediate: true, deep: true }
)
// 监听流程节点树变化,自动保存
watch(
() => processNodeTree.value,
async (newValue, oldValue) => {
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
await saveSimpleFlowModel(newValue)
}
},
{ deep: true }
)
const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => { const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
if (!simpleModelNode) { if (!simpleModelNode) {
message.error('模型数据为空')
return return
} }
// 校验节点
errorNodes = []
validateNode(simpleModelNode, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
try { try {
loading.value = true if (props.modelId) {
const data = { // 编辑模式
id: props.modelId, const data = {
simpleModel: simpleModelNode id: props.modelId,
} simpleModel: simpleModelNode
const result = await updateBpmSimpleModel(data) }
if (result) { await updateBpmSimpleModel(data)
message.success('修改成功')
emits('success')
} else {
message.alert('修改失败')
} }
} finally { // 无论是编辑还是新建模式,都更新当前值并触发事件
loading.value = false currentValue.value = simpleModelNode
emits('success', simpleModelNode)
} catch (error) {
console.error('保存失败:', error)
} }
} }
// 校验节点设置。 暂时以 showText 为空 未节点错误配置 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) { if (node) {
...@@ -134,12 +225,14 @@ onMounted(async () => { ...@@ -134,12 +225,14 @@ onMounted(async () => {
try { try {
loading.value = true loading.value = true
// 获取表单字段 // 获取表单字段
const bpmnModel = await getModel(props.modelId) if (props.modelId) {
if (bpmnModel) { const bpmnModel = await getModel(props.modelId)
formType.value = bpmnModel.formType if (bpmnModel) {
if (formType.value === 10) { formType.value = bpmnModel.formType
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO if (formType.value === 10) {
formFields.value = bpmnForm?.fields const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
} }
} }
// 获得角色列表 // 获得角色列表
...@@ -150,30 +243,64 @@ onMounted(async () => { ...@@ -150,30 +243,64 @@ onMounted(async () => {
userOptions.value = await UserApi.getSimpleUserList() userOptions.value = await UserApi.getSimpleUserList()
// 获得部门列表 // 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList() deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id') deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表 // 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList() userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
//获取 SIMPLE 设计器模型 // 加载流程数据
const result = await getBpmSimpleModel(props.modelId) if (props.modelId) {
if (result) { // 获取 SIMPLE 设计器模型
processNodeTree.value = result const result = await getBpmSimpleModel(props.modelId)
} else { if (result) {
// 初始值 await loadProcessData(result)
processNodeTree.value = { } else {
name: '发起人', updateModel()
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
} }
} else if (props.value) {
await loadProcessData(props.value)
} else {
updateModel()
} }
} finally { } finally {
loading.value = false loading.value = false
emits('init-finished')
} }
}) })
const simpleProcessModelRef = ref()
/** 获取当前流程数据 */
const getCurrentFlowData = async () => {
try {
if (simpleProcessModelRef.value) {
const data = await simpleProcessModelRef.value.getCurrentFlowData()
if (data) {
currentValue.value = data
return data
}
}
return currentValue.value
} catch (error) {
console.error('获取流程数据失败:', error)
return currentValue.value
}
}
// 刷新方法
const refresh = async () => {
try {
if (currentValue.value) {
await loadProcessData(currentValue.value)
}
} catch (error) {
console.error('刷新失败:', error)
}
}
defineExpose({
getCurrentFlowData,
updateModel,
loadProcessData,
refresh
})
</script> </script>
...@@ -8,15 +8,6 @@ ...@@ -8,15 +8,6 @@
<el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button> <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
<el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" /> <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
</el-button-group> </el-button-group>
<el-button
v-if="!readonly"
size="default"
class="ml-4px"
type="primary"
:icon="Select"
@click="saveSimpleFlowModel"
>保存模型</el-button
>
</el-row> </el-row>
</div> </div>
<div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`"> <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
...@@ -42,7 +33,8 @@ ...@@ -42,7 +33,8 @@
import ProcessNodeTree from './ProcessNodeTree.vue' import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts' import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node' import { useWatchNode } from './node'
import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
defineOptions({ defineOptions({
name: 'SimpleProcessModel' name: 'SimpleProcessModel'
}) })
...@@ -58,6 +50,7 @@ const props = defineProps({ ...@@ -58,6 +50,7 @@ const props = defineProps({
default: true default: true
} }
}) })
const emits = defineEmits<{ const emits = defineEmits<{
'save': [node: SimpleFlowNode | undefined] 'save': [node: SimpleFlowNode | undefined]
}>() }>()
...@@ -68,6 +61,7 @@ provide('readonly', props.readonly) ...@@ -68,6 +61,7 @@ provide('readonly', props.readonly)
let scaleValue = ref(100) let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200 const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50 const MIN_SCALE_VALUE = 50
// 放大 // 放大
const zoomIn = () => { const zoomIn = () => {
if (scaleValue.value == MAX_SCALE_VALUE) { if (scaleValue.value == MAX_SCALE_VALUE) {
...@@ -75,6 +69,7 @@ const zoomIn = () => { ...@@ -75,6 +69,7 @@ const zoomIn = () => {
} }
scaleValue.value += 10 scaleValue.value += 10
} }
// 缩小 // 缩小
const zoomOut = () => { const zoomOut = () => {
if (scaleValue.value == MIN_SCALE_VALUE) { if (scaleValue.value == MIN_SCALE_VALUE) {
...@@ -82,21 +77,14 @@ const zoomOut = () => { ...@@ -82,21 +77,14 @@ const zoomOut = () => {
} }
scaleValue.value -= 10 scaleValue.value -= 10
} }
const processReZoom = () => { const processReZoom = () => {
scaleValue.value = 100 scaleValue.value = 100
} }
const errorDialogVisible = ref(false) const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = [] let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => { const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) { if (node) {
...@@ -135,6 +123,26 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo ...@@ -135,6 +123,26 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
} }
} }
} }
/** 获取当前流程数据 */
const getCurrentFlowData = async () => {
try {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return undefined
}
return processNodeTree.value
} catch (error) {
console.error('获取流程数据失败:', error)
return undefined
}
}
defineExpose({
getCurrentFlowData
})
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>
...@@ -24,6 +24,11 @@ export enum NodeType { ...@@ -24,6 +24,11 @@ export enum NodeType {
COPY_TASK_NODE = 12, COPY_TASK_NODE = 12,
/** /**
* 延迟器节点
*/
DELAY_TIMER_NODE = 14,
/**
* 条件节点 * 条件节点
*/ */
CONDITION_NODE = 50, CONDITION_NODE = 50,
...@@ -98,6 +103,8 @@ export interface SimpleFlowNode { ...@@ -98,6 +103,8 @@ export interface SimpleFlowNode {
defaultFlow?: boolean defaultFlow?: boolean
// 活动的状态,用于前端节点状态展示 // 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum activityStatus?: TaskStatusEnum
// 延迟设置
delaySetting?: DelaySetting
} }
// 候选人策略枚举 ( 用于审批节点。抄送节点 ) // 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy { export enum CandidateStrategy {
...@@ -413,12 +420,14 @@ NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人') ...@@ -413,12 +420,14 @@ NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人') NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件') NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人') NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
export const NODE_DEFAULT_NAME = new Map<number, string>() export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人') NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人') NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件') NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人') NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序 // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [ export const CANDIDATE_STRATEGY: DictDataVO[] = [
...@@ -568,3 +577,30 @@ export enum ProcessVariableEnum { ...@@ -568,3 +577,30 @@ export enum ProcessVariableEnum {
*/ */
START_USER_ID = 'PROCESS_START_USER_ID' START_USER_ID = 'PROCESS_START_USER_ID'
} }
/**
* 延迟设置
*/
export type DelaySetting = {
// 延迟类型
delayType: number
// 延迟时间表达式
delayTime: string
}
/**
* 延迟类型
*/
export enum DelayTypeEnum {
/**
* 固定时长
*/
FIXED_TIME_DURATION = 1,
/**
* 固定日期时间
*/
FIXED_DATE_TIME = 2
}
export const DELAY_TYPE = [
{ label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
{ label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
]
...@@ -14,8 +14,7 @@ import { ...@@ -14,8 +14,7 @@ import {
NODE_DEFAULT_NAME, NODE_DEFAULT_NAME,
AssignStartUserHandlerType, AssignStartUserHandlerType,
AssignEmptyHandlerType, AssignEmptyHandlerType,
FieldPermissionType, FieldPermissionType
ProcessVariableEnum
} from './consts' } from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index' import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> { export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
...@@ -37,13 +36,6 @@ const parseFormCreateFields = (formFields?: string[]) => { ...@@ -37,13 +36,6 @@ const parseFormCreateFields = (formFields?: string[]) => {
parseFormFields(JSON.parse(fieldStr), result) parseFormFields(JSON.parse(fieldStr), result)
}) })
} }
// 固定添加发起人 ID 字段
result.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
type: 'UserSelect',
required: true
})
return result return result
} }
...@@ -60,9 +52,33 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType) ...@@ -60,9 +52,33 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => { const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
nodeFormFields = toRaw(nodeFormFields) nodeFormFields = toRaw(nodeFormFields)
fieldsPermissionConfig.value = if (!nodeFormFields || nodeFormFields.length === 0) {
cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields)) fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields))
} else {
fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields))
}
} }
// 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段)
const mergeFieldsPermission = (
formFieldsPermisson: Array<Record<string, string>>,
formFields?: string[]
) => {
let mergedFieldsPermission: Array<Record<string, any>> = []
if (formFields) {
mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => {
const found = formFieldsPermisson.find(
(fieldPermission) => fieldPermission.field == item.field
)
return {
field: item.field,
title: item.title,
permission: found ? found.permission : defaultPermission
}
})
}
return mergedFieldsPermission
}
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读 // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => { const getDefaultFieldsPermission = (formFields?: string[]) => {
let defaultFieldsPermission: Array<Record<string, any>> = [] let defaultFieldsPermission: Array<Record<string, any>> = []
......
...@@ -26,19 +26,13 @@ ...@@ -26,19 +26,13 @@
</div> </div>
</template> </template>
<div> <div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div> <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"
>未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
>
<div v-else> <div v-else>
<el-form <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top">
ref="formRef"
:model="currentNode"
:rules="formRules"
label-position="top"
>
<el-form-item label="配置方式" prop="conditionType"> <el-form-item label="配置方式" prop="conditionType">
<el-radio-group <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType">
v-model="currentNode.conditionType"
@change="changeConditionType"
>
<el-radio <el-radio
v-for="(dict, index) in conditionConfigTypes" v-for="(dict, index) in conditionConfigTypes"
:key="index" :key="index"
...@@ -108,10 +102,11 @@ ...@@ -108,10 +102,11 @@
<div class="mr-2"> <div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide"> <el-select style="width: 160px" v-model="rule.leftSide">
<el-option <el-option
v-for="(item, index) in fieldsInfo" v-for="(item, index) in fieldOptions"
:key="index" :key="index"
:label="item.title" :label="item.title"
:value="item.field" :value="item.field"
:disabled="!item.required"
/> />
</el-select> </el-select>
</div> </div>
...@@ -165,10 +160,12 @@ import { ...@@ -165,10 +160,12 @@ import {
COMPARISON_OPERATORS, COMPARISON_OPERATORS,
ConditionGroup, ConditionGroup,
Condition, Condition,
ConditionRule ConditionRule,
ProcessVariableEnum
} from '../consts' } from '../consts'
import { getDefaultConditionNodeName } from '../utils' import { getDefaultConditionNodeName } from '../utils'
import { useFormFields } from '../node' import { useFormFields } from '../node'
import { BpmModelFormType } from '@/utils/constants'
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
defineOptions({ defineOptions({
name: 'ConditionNodeConfig' name: 'ConditionNodeConfig'
...@@ -177,8 +174,8 @@ const formType = inject<Ref<number>>('formType') // 表单类型 ...@@ -177,8 +174,8 @@ const formType = inject<Ref<number>>('formType') // 表单类型
const conditionConfigTypes = computed(() => { const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => { return CONDITION_CONFIG_TYPES.filter((item) => {
// 业务表单暂时去掉条件规则选项 // 业务表单暂时去掉条件规则选项
if (formType?.value !== 10) { if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return item.value === ConditionType.RULE return false
} else { } else {
return true return true
} }
...@@ -368,16 +365,29 @@ const addConditionRule = (condition: Condition, idx: number) => { ...@@ -368,16 +365,29 @@ const addConditionRule = (condition: Condition, idx: number) => {
const deleteConditionRule = (condition: Condition, idx: number) => { const deleteConditionRule = (condition: Condition, idx: number) => {
condition.rules.splice(idx, 1) condition.rules.splice(idx, 1)
} }
const fieldsInfo = useFormFields() const fieldsInfo = useFormFields()
/** 条件规则可选择的表单字段 */
const fieldOptions = computed(() => {
const fieldsCopy = fieldsInfo.slice()
// 固定添加发起人 ID 字段
fieldsCopy.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return fieldsCopy
})
/** 获取字段名称 */
const getFieldTitle = (field: string) => { const getFieldTitle = (field: string) => {
const item = fieldsInfo.find((item) => item.field === field) const item = fieldOptions.value.find((item) => item.field === field)
return item?.title return item?.title
} }
/** 获取操作符名称 */
const getOpName = (opCode: string): string => { const getOpName = (opCode: string): string => {
const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode) const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode)
return opName?.label return opName?.label
} }
</script> </script>
......
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="延迟时间" prop="delayType">
<el-radio-group v-model="configForm.delayType">
<el-radio-button
v-for="item in DELAY_TYPE"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-radio-group>
</el-form-item>
<el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION">
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</el-form-item>
<el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-text>后进入下一节点</el-text>
</el-form-item>
<el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime">
<el-date-picker
class="mr-2"
v-model="configForm.dateTime"
type="datetime"
placeholder="请选择日期和时间"
value-format="YYYY-MM-DDTHH:mm:ss"
/>
<el-text>后进入下一节点</el-text>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
NodeType,
TIME_UNIT_TYPES,
TimeUnitType,
DelayTypeEnum,
DELAY_TYPE
} from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import { convertTimeUnit } from '../utils'
defineOptions({
name: 'DelayTimerNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE)
// 抄送人表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
const formRules = reactive({
delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }]
})
// 配置表单数据
const configForm = ref({
delayType: DelayTypeEnum.FIXED_TIME_DURATION,
timeDuration: 1,
timeUnit: TimeUnitType.HOUR,
dateTime: ''
})
// 保存配置
const saveConfig = async () => {
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.showText = showText
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
currentNode.value.delaySetting = {
delayType: configForm.value.delayType,
delayTime: getIsoTimeDuration()
}
}
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
currentNode.value.delaySetting = {
delayType: configForm.value.delayType,
delayTime: configForm.value.dateTime
}
}
settingVisible.value = false
return true
}
const getShowText = (): string => {
let showText = ''
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}`
}
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}`
}
return showText
}
const getIsoTimeDuration = () => {
let strTimeDuration = 'PT'
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
strTimeDuration += configForm.value.timeDuration + 'M'
}
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
strTimeDuration += configForm.value.timeDuration + 'H'
}
if (configForm.value.timeUnit === TimeUnitType.DAY) {
strTimeDuration += configForm.value.timeDuration + 'D'
}
return strTimeDuration
}
// 显示延迟器节点配置, 由父组件传过来
const showDelayTimerNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
if (node.delaySetting) {
configForm.value.delayType = node.delaySetting.delayType
// 固定时长
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
const strTimeDuration = node.delaySetting.delayTime
let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
configForm.value.timeDuration = parseInt(parseTime)
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
}
// 固定日期时间
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
configForm.value.dateTime = node.delaySetting.delayTime
}
}
}
defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
...@@ -25,7 +25,20 @@ ...@@ -25,7 +25,20 @@
</template> </template>
<el-tabs type="border-card" v-model="activeTabName"> <el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user"> <el-tab-pane label="权限" name="user">
<div> 待实现 </div> <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text>
<el-text v-else-if="startUserIds.length == 1">
{{ getUserNicknames(startUserIds) }} 可发起流程
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="getUserNicknames(startUserIds)"
>
{{ getUserNicknames(startUserIds.slice(0,2)) }} 等 {{ startUserIds.length }} 人可发起流程
</el-tooltip>
</el-text>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10"> <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane"> <div class="field-setting-pane">
...@@ -86,7 +99,7 @@ ...@@ -86,7 +99,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts' import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node' import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
import * as UserApi from '@/api/system/user'
defineOptions({ defineOptions({
name: 'StartUserNodeConfig' name: 'StartUserNodeConfig'
}) })
...@@ -96,6 +109,10 @@ const props = defineProps({ ...@@ -96,6 +109,10 @@ const props = defineProps({
required: true required: true
} }
}) })
// 可发起流程的用户编号
const startUserIds = inject<Ref<any[]>>('startUserIds')
// 用户列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
// 抽屉配置 // 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer() const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点 // 当前节点
...@@ -108,12 +125,23 @@ const activeTabName = ref('user') ...@@ -108,12 +125,23 @@ const activeTabName = ref('user')
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission( const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.WRITE FieldPermissionType.WRITE
) )
const getUserNicknames = (userIds: number[]): string => {
if (!userIds || userIds.length === 0) {
return ''
}
const nicknames: string[] = []
userIds.forEach((userId) => {
const found = userOptions?.value.find((item) => item.id === userId)
if (found && found.nickname) {
nicknames.push(found.nickname)
}
})
return nicknames.join(',')
}
// 保存配置 // 保存配置
const saveConfig = async () => { const saveConfig = async () => {
activeTabName.value = 'user' activeTabName.value = 'user'
currentNode.value.name = nodeName.value! currentNode.value.name = nodeName.value!
// TODO 暂时写死。后续可以显示谁有权限可以发起
currentNode.value.showText = '已设置' currentNode.value.showText = '已设置'
// 设置表单权限 // 设置表单权限
currentNode.value.fieldsPermission = fieldsPermissionConfig.value currentNode.value.fieldsPermission = fieldsPermissionConfig.value
......
...@@ -469,7 +469,8 @@ import { ...@@ -469,7 +469,8 @@ import {
TimeoutHandlerType, TimeoutHandlerType,
ASSIGN_EMPTY_HANDLER_TYPES, ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType, AssignEmptyHandlerType,
FieldPermissionType FieldPermissionType,
ProcessVariableEnum
} from '../consts' } from '../consts'
import { import {
...@@ -519,6 +520,13 @@ const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFie ...@@ -519,6 +520,13 @@ const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFie
useFormFieldsPermission(FieldPermissionType.READ) useFormFieldsPermission(FieldPermissionType.READ)
// 表单内用户字段选项, 必须是必填和用户选择器 // 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => { const userFieldOnFormOptions = computed(() => {
// 固定添加发起人 ID 字段
formFieldOptions.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
type: 'UserSelect',
required: true
})
return formFieldOptions.filter((item) => item.type === 'UserSelect') return formFieldOptions.filter((item) => item.type === 'UserSelect')
}) })
// 表单内部门字段选项, 必须是必填和部门选择器 // 表单内部门字段选项, 必须是必填和部门选择器
......
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<!-- TODO @芋艿 需要更换图标 -->
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<DelayTimerNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue'
defineOptions({
name: 'DelayTimerNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showDelayTimerNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
...@@ -173,13 +173,16 @@ ...@@ -173,13 +173,16 @@
height: 100%; height: 100%;
padding-top: 32px; padding-top: 32px;
background-color: #fafafa; background-color: #fafafa;
overflow-x: auto;
width: 100%;
.simple-process-model { .simple-process-model {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform-origin: 50% 0 0; transform-origin: 50% 0 0;
overflow: auto; min-width: fit-content;
transform: scale(1); transform: scale(1);
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat; background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
...@@ -473,6 +476,7 @@ ...@@ -473,6 +476,7 @@
.branch-node-container { .branch-node-container {
position: relative; position: relative;
display: flex; display: flex;
min-width: fit-content;
&::before { &::before {
position: absolute; position: absolute;
...@@ -548,6 +552,7 @@ ...@@ -548,6 +552,7 @@
background: transparent; background: transparent;
border-top: 2px solid #dedede; border-top: 2px solid #dedede;
border-bottom: 2px solid #dedede; border-bottom: 2px solid #dedede;
flex-shrink: 0;
&::before { &::before {
position: absolute; position: absolute;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defaultProps, findTreeNode, handleTree } from '@/utils/tree' import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept' import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
...@@ -50,6 +50,7 @@ const emit = defineEmits<{ ...@@ -50,6 +50,7 @@ const emit = defineEmits<{
const { t } = useI18n() // 国际 const { t } = useI18n() // 国际
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const deptTree = ref<Tree[]>([]) // 部门树形结构化 const deptTree = ref<Tree[]>([]) // 部门树形结构化
const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据
const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表 const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表 const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
const selectedUserIdList: any = ref([]) // 选中的用户列表 const selectedUserIdList: any = ref([]) // 选中的用户列表
...@@ -79,7 +80,9 @@ const open = async (id: number, selectedList?: any[]) => { ...@@ -79,7 +80,9 @@ const open = async (id: number, selectedList?: any[]) => {
resetForm() resetForm()
// 加载部门、用户列表 // 加载部门、用户列表
deptTree.value = handleTree(await DeptApi.getSimpleDeptList()) const deptData = await DeptApi.getSimpleDeptList()
deptList.value = deptData // 保存扁平结构的部门数据
deptTree.value = handleTree(deptData) // 转换成树形结构
userList.value = await UserApi.getSimpleUserList() userList.value = await UserApi.getSimpleUserList()
// 初始状态下,过滤列表等于所有用户列表 // 初始状态下,过滤列表等于所有用户列表
...@@ -88,16 +91,31 @@ const open = async (id: number, selectedList?: any[]) => { ...@@ -88,16 +91,31 @@ const open = async (id: number, selectedList?: any[]) => {
dialogVisible.value = true dialogVisible.value = true
} }
/** 获取指定部门及其所有子部门的ID列表 */
const getChildDeptIds = (deptId: number, deptList: any[]): number[] => {
const ids = [deptId]
const children = deptList.filter((dept) => dept.parentId === deptId)
children.forEach((child) => {
ids.push(...getChildDeptIds(child.id, deptList))
})
return ids
}
/** 获取部门过滤后的用户列表 */ /** 获取部门过滤后的用户列表 */
const getUserList = async (deptId?: number) => { const filterUserList = async (deptId?: number) => {
formLoading.value = true formLoading.value = true
try { try {
// @ts-ignore if (!deptId) {
// TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤 // 如果没有选择部门,显示所有用户
// TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList filteredUserList.value = [...userList.value]
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId }) return
// 更新过滤后的用户列表 }
filteredUserList.value = data.list
// 直接使用已保存的部门列表数据进行过滤
const deptIds = getChildDeptIds(deptId, deptList.value)
// 过滤出这些部门下的用户
filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId))
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
...@@ -121,6 +139,7 @@ const submitForm = async () => { ...@@ -121,6 +139,7 @@ const submitForm = async () => {
/** 重置表单 */ /** 重置表单 */
const resetForm = () => { const resetForm = () => {
deptTree.value = [] deptTree.value = []
deptList.value = []
userList.value = [] userList.value = []
filteredUserList.value = [] filteredUserList.value = []
selectedUserIdList.value = [] selectedUserIdList.value = []
...@@ -128,7 +147,7 @@ const resetForm = () => { ...@@ -128,7 +147,7 @@ const resetForm = () => {
/** 处理部门被点击 */ /** 处理部门被点击 */
const handleNodeClick = (row: { [key: string]: any }) => { const handleNodeClick = (row: { [key: string]: any }) => {
getUserList(row.id) filterUserList(row.id)
} }
defineExpose({ open }) // 提供 open 方法,用于打开弹窗 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
......
...@@ -160,13 +160,6 @@ ...@@ -160,13 +160,6 @@
<XButton preIcon="ep:refresh" @click="processRestart()" /> <XButton preIcon="ep:refresh" @click="processRestart()" />
</el-tooltip> </el-tooltip>
</ElButtonGroup> </ElButtonGroup>
<XButton
preIcon="ep:plus"
title="保存模型"
@click="processSave"
:type="props.headerButtonType"
:disabled="simulationStatus"
/>
</template> </template>
<!-- 用于打开本地文件--> <!-- 用于打开本地文件-->
<input <input
...@@ -315,6 +308,28 @@ const props = defineProps({ ...@@ -315,6 +308,28 @@ const props = defineProps({
} }
}) })
// 监听value变化,重新加载流程图
watch(
() => props.value,
(newValue) => {
if (newValue && bpmnModeler) {
createNewDiagram(newValue)
}
},
{ immediate: true }
)
// 监听processId和processName变化
watch(
[() => props.processId, () => props.processName],
([newId, newName]) => {
if (newId && newName && !props.value) {
createNewDiagram(null)
}
},
{ immediate: true }
)
provide('configGlobal', props) provide('configGlobal', props)
let bpmnModeler: any = null let bpmnModeler: any = null
const defaultZoom = ref(1) const defaultZoom = ref(1)
...@@ -592,16 +607,6 @@ const processZoomOut = (zoomStep = 0.1) => { ...@@ -592,16 +607,6 @@ const processZoomOut = (zoomStep = 0.1) => {
defaultZoom.value = newZoom defaultZoom.value = newZoom
bpmnModeler.get('canvas').zoom(defaultZoom.value) bpmnModeler.get('canvas').zoom(defaultZoom.value)
} }
// const processZoomTo = (newZoom = 1) => {
// if (newZoom < 0.2) {
// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
// }
// if (newZoom > 4) {
// throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
// }
// defaultZoom = newZoom
// bpmnModeler.get('canvas').zoom(newZoom)
// }
const processReZoom = () => { const processReZoom = () => {
defaultZoom.value = 1 defaultZoom.value = 1
bpmnModeler.get('canvas').zoom('fit-viewport', 'auto') bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
...@@ -640,63 +645,19 @@ const previewProcessXML = () => { ...@@ -640,63 +645,19 @@ const previewProcessXML = () => {
} }
const previewProcessJson = () => { const previewProcessJson = () => {
bpmnModeler.saveXML({ format: true }).then(({ xml }) => { bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
// console.log(xml, 'xml')
// const rootNode = parseXmlString(xml)
// console.log(rootNode, 'rootNoderootNode')
const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml)) const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml))
// console.log(rootNodes, 'rootNodesrootNodesrootNodes')
// console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()')
// console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()')
// console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()')
// const parser = new xml2js.XMLParser()
// let jObj = parser.parse(xml)
// console.log(jObj, 'jObjjObjjObjjObjjObj')
// const builder = new xml2js.XMLBuilder(xml)
// const xmlContent = builder
// console.log(xmlContent, 'xmlContent')
// console.log(xml2js, 'convertconvertconvert')
previewResult.value = rootNodes.parent?.toJSON() as unknown as string previewResult.value = rootNodes.parent?.toJSON() as unknown as string
// previewResult.value = jObj
// previewResult.value = convert.xml2json(xml, {explicitArray : false},{ spaces: 2 })
previewType.value = 'json' previewType.value = 'json'
previewModelVisible.value = true previewModelVisible.value = true
}) })
} }
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
const processSave = async () => {
// console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
const { err, xml } = await bpmnModeler.saveXML()
// console.log(err, 'errerrerrerrerr')
// console.log(xml, 'xmlxmlxmlxmlxml')
// 读取异常时抛出异常
if (err) {
// this.$modal.msgError('保存模型失败,请重试!')
alert('保存模型失败,请重试!')
return
}
// 触发 save 事件
emit('save', xml)
}
/** 高亮显示 */
// const highlightedCode = (previewType, previewResult) => {
// console.log(previewType, 'previewType, previewResult')
// console.log(previewResult, 'previewType, previewResult')
// console.log(hljs.highlight, 'hljs.highlight')
// const result = hljs.highlight(previewType, previewResult.value || '', true)
// return result.value || '&nbsp;'
// }
onBeforeMount(() => {
console.log(props, 'propspropspropsprops')
})
onMounted(() => { onMounted(() => {
initBpmnModeler() initBpmnModeler()
createNewDiagram(props.value) createNewDiagram(props.value)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
// this.$once('hook:beforeDestroy', () => {
// })
if (bpmnModeler) bpmnModeler.destroy() if (bpmnModeler) bpmnModeler.destroy()
emit('destroy', bpmnModeler) emit('destroy', bpmnModeler)
bpmnModeler = null bpmnModeler = null
......
...@@ -406,6 +406,31 @@ ...@@ -406,6 +406,31 @@
"name": "variableMappingDelegateExpression", "name": "variableMappingDelegateExpression",
"isAttr": true, "isAttr": true,
"type": "String" "type": "String"
},
{
"name": "calledElementType",
"isAttr": true,
"type": "String"
},
{
"name": "processInstanceName",
"isAttr": true,
"type": "String"
},
{
"name": "inheritBusinessKey",
"isAttr": true,
"type": "Boolean"
},
{
"name": "businessKey",
"isAttr": true,
"type": "String"
},
{
"name": "inheritVariables",
"isAttr": true,
"type": "Boolean"
} }
] ]
}, },
...@@ -1281,6 +1306,138 @@ ...@@ -1281,6 +1306,138 @@
"isBody": true "isBody": true
} }
] ]
},
{
"name": "ButtonsSetting",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "flowable:id",
"type": "Integer",
"isAttr": true
},
{
"name": "flowable:enable",
"type": "Boolean",
"isAttr": true
},
{
"name": "flowable:displayName",
"type": "String",
"isAttr": true
}
]
},
{
"name": "FieldsPermission",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "flowable:field",
"type": "String",
"isAttr": true
},
{
"name": "flowable:title",
"type": "String",
"isAttr": true
},
{
"name": "flowable:permission",
"type": "String",
"isAttr": true
}
]
},
{
"name": "BoundaryEventType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:BoundaryEvent"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "TimeoutHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:BoundaryEvent"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "ApproveType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "ApproveMethod",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "CandidateStrategy",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "CandidateParam",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "String",
"isBody": true
}
]
} }
], ],
"emumerations": [] "emumerations": []
......
...@@ -165,6 +165,18 @@ F.prototype.getPaletteEntries = function () { ...@@ -165,6 +165,18 @@ F.prototype.getPaletteEntries = function () {
'bpmn-icon-user-task', 'bpmn-icon-user-task',
translate('Create User Task') translate('Create User Task')
), ),
'create.call-activity': createAction(
'bpmn:CallActivity',
'activity',
'bpmn-icon-call-activity',
translate('Create Call Activity')
),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task')
),
'create.data-object': createAction( 'create.data-object': createAction(
'bpmn:DataObjectReference', 'bpmn:DataObjectReference',
'data-object', 'data-object',
......
...@@ -171,6 +171,12 @@ PaletteProvider.prototype.getPaletteEntries = function () { ...@@ -171,6 +171,12 @@ PaletteProvider.prototype.getPaletteEntries = function () {
'bpmn-icon-user-task', 'bpmn-icon-user-task',
translate('Create User Task') translate('Create User Task')
), ),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task')
),
'create.data-object': createAction( 'create.data-object': createAction(
'bpmn:DataObjectReference', 'bpmn:DataObjectReference',
'data-object', 'data-object',
......
...@@ -56,6 +56,8 @@ export default { ...@@ -56,6 +56,8 @@ export default {
'Create EndEvent': '创建结束事件', 'Create EndEvent': '创建结束事件',
'Create Task': '创建任务', 'Create Task': '创建任务',
'Create User Task': '创建用户任务', 'Create User Task': '创建用户任务',
'Create Call Activity': '创建调用活动',
'Create Service Task': '创建服务任务',
'Create Gateway': '创建网关', 'Create Gateway': '创建网关',
'Create DataObjectReference': '创建数据对象', 'Create DataObjectReference': '创建数据对象',
'Create DataStoreReference': '创建数据存储', 'Create DataStoreReference': '创建数据存储',
......
<template> <template>
<div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }"> <div class="process-panel__container" :style="{ width: `${width}px` }">
<el-collapse v-model="activeTab"> <el-collapse v-model="activeTab" v-if="isReady">
<el-collapse-item name="base"> <el-collapse-item name="base">
<!-- class="panel-tab__title" --> <!-- class="panel-tab__title" -->
<template #title> <template #title>
...@@ -26,8 +26,10 @@ ...@@ -26,8 +26,10 @@
<template #title><Icon icon="ep:list" />表单</template> <template #title><Icon icon="ep:list" />表单</template>
<element-form :id="elementId" :type="elementType" /> <element-form :id="elementId" :type="elementType" />
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task"> <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task">
<template #title><Icon icon="ep:checked" />任务(审批人)</template> <template #title
><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template
>
<element-task :id="elementId" :type="elementType" /> <element-task :id="elementId" :type="elementType" />
</el-collapse-item> </el-collapse-item>
<el-collapse-item <el-collapse-item
...@@ -35,8 +37,12 @@ ...@@ -35,8 +37,12 @@
v-if="elementType.indexOf('Task') !== -1" v-if="elementType.indexOf('Task') !== -1"
key="multiInstance" key="multiInstance"
> >
<template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template> <template #title><Icon icon="ep:help-filled" />多人审批方式</template>
<element-multi-instance :business-object="elementBusinessObject" :type="elementType" /> <element-multi-instance
:id="elementId"
:business-object="elementBusinessObject"
:type="elementType"
/>
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="listeners" key="listeners"> <el-collapse-item name="listeners" key="listeners">
<template #title><Icon icon="ep:bell-filled" />执行监听器</template> <template #title><Icon icon="ep:bell-filled" />执行监听器</template>
...@@ -54,9 +60,13 @@ ...@@ -54,9 +60,13 @@
<template #title><Icon icon="ep:promotion" />其他</template> <template #title><Icon icon="ep:promotion" />其他</template>
<element-other-config :id="elementId" /> <element-other-config :id="elementId" />
</el-collapse-item> </el-collapse-item>
<el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig"> <el-collapse-item name="customConfig" key="customConfig">
<template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template> <template #title><Icon icon="ep:tools" />自定义配置</template>
<element-custom-config :id="elementId" :type="elementType" /> <element-custom-config
:id="elementId"
:type="elementType"
:business-object="elementBusinessObject"
/>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
...@@ -72,6 +82,7 @@ import ElementListeners from './listeners/ElementListeners.vue' ...@@ -72,6 +82,7 @@ import ElementListeners from './listeners/ElementListeners.vue'
import ElementProperties from './properties/ElementProperties.vue' import ElementProperties from './properties/ElementProperties.vue'
// import ElementForm from './form/ElementForm.vue' // import ElementForm from './form/ElementForm.vue'
import UserTaskListeners from './listeners/UserTaskListeners.vue' import UserTaskListeners from './listeners/UserTaskListeners.vue'
import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data'
defineOptions({ name: 'MyPropertiesPanel' }) defineOptions({ name: 'MyPropertiesPanel' })
...@@ -108,24 +119,16 @@ const elementBusinessObject = ref<any>({}) // 元素 businessObject 镜像,提 ...@@ -108,24 +119,16 @@ const elementBusinessObject = ref<any>({}) // 元素 businessObject 镜像,提
const conditionFormVisible = ref(false) // 流转条件设置 const conditionFormVisible = ref(false) // 流转条件设置
const formVisible = ref(false) // 表单配置 const formVisible = ref(false) // 表单配置
const bpmnElement = ref() const bpmnElement = ref()
const isReady = ref(false)
provide('prefix', props.prefix) provide('prefix', props.prefix)
provide('width', props.width) provide('width', props.width)
const bpmnInstances = () => (window as any)?.bpmnInstances
// 监听 props.bpmnModeler 然后 initModels
const unwatchBpmn = watch(
() => props.bpmnModeler,
() => {
// 避免加载时 流程图 并未加载完成
if (!props.bpmnModeler) {
console.log('缺少props.bpmnModeler')
return
}
console.log('props.bpmnModeler 有值了!!!') // 初始化 bpmnInstances
const w = window as any const initBpmnInstances = () => {
w.bpmnInstances = { if (!props.bpmnModeler) return false
try {
const instances = {
modeler: props.bpmnModeler, modeler: props.bpmnModeler,
modeling: props.bpmnModeler.get('modeling'), modeling: props.bpmnModeler.get('modeling'),
moddle: props.bpmnModeler.get('moddle'), moddle: props.bpmnModeler.get('moddle'),
...@@ -137,9 +140,45 @@ const unwatchBpmn = watch( ...@@ -137,9 +140,45 @@ const unwatchBpmn = watch(
selection: props.bpmnModeler.get('selection') selection: props.bpmnModeler.get('selection')
} }
console.log(bpmnInstances(), 'window.bpmnInstances') // 检查所有实例是否都存在
getActiveElement() const allInstancesExist = Object.values(instances).every(instance => instance)
unwatchBpmn() if (allInstancesExist) {
const w = window as any
w.bpmnInstances = instances
return true
}
return false
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error)
return false
}
}
const bpmnInstances = () => (window as any)?.bpmnInstances
// 监听 props.bpmnModeler 然后 initModels
const unwatchBpmn = watch(
() => props.bpmnModeler,
async () => {
// 避免加载时 流程图 并未加载完成
if (!props.bpmnModeler) {
console.log('缺少props.bpmnModeler')
return
}
try {
// 等待 modeler 初始化完成
await nextTick()
if (initBpmnInstances()) {
isReady.value = true
await nextTick()
getActiveElement()
} else {
console.error('modeler 实例未完全初始化')
}
} catch (error) {
console.error('初始化失败:', error)
}
}, },
{ {
immediate: true immediate: true
...@@ -147,6 +186,8 @@ const unwatchBpmn = watch( ...@@ -147,6 +186,8 @@ const unwatchBpmn = watch(
) )
const getActiveElement = () => { const getActiveElement = () => {
if (!isReady.value || !props.bpmnModeler) return
// 初始第一个选中元素 bpmn:Process // 初始第一个选中元素 bpmn:Process
initFormOnChanged(null) initFormOnChanged(null)
props.bpmnModeler.on('import.done', (e) => { props.bpmnModeler.on('import.done', (e) => {
...@@ -164,8 +205,11 @@ const getActiveElement = () => { ...@@ -164,8 +205,11 @@ const getActiveElement = () => {
} }
}) })
} }
// 初始化数据 // 初始化数据
const initFormOnChanged = (element) => { const initFormOnChanged = (element) => {
if (!isReady.value || !bpmnInstances()) return
let activatedElement = element let activatedElement = element
if (!activatedElement) { if (!activatedElement) {
activatedElement = activatedElement =
...@@ -173,32 +217,36 @@ const initFormOnChanged = (element) => { ...@@ -173,32 +217,36 @@ const initFormOnChanged = (element) => {
bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration') bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration')
} }
if (!activatedElement) return if (!activatedElement) return
console.log(`
---------- try {
select element changed: console.log(`
id: ${activatedElement.id} ----------
type: ${activatedElement.businessObject.$type} select element changed:
---------- id: ${activatedElement.id}
`) type: ${activatedElement.businessObject.$type}
console.log('businessObject: ', activatedElement.businessObject) ----------
bpmnInstances().bpmnElement = activatedElement `)
bpmnElement.value = activatedElement console.log('businessObject: ', activatedElement.businessObject)
elementId.value = activatedElement.id bpmnInstances().bpmnElement = activatedElement
elementType.value = activatedElement.type.split(':')[1] || '' bpmnElement.value = activatedElement
elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject)) elementId.value = activatedElement.id
conditionFormVisible.value = !!( elementType.value = activatedElement.type.split(':')[1] || ''
elementType.value === 'SequenceFlow' && elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
activatedElement.source && conditionFormVisible.value = !!(
activatedElement.source.type.indexOf('StartEvent') === -1 elementType.value === 'SequenceFlow' &&
) activatedElement.source &&
formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent' activatedElement.source.type.indexOf('StartEvent') === -1
)
formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
} catch (error) {
console.error('初始化表单数据失败:', error)
}
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
const w = window as any const w = window as any
w.bpmnInstances = null w.bpmnInstances = null
console.log(props, 'props1') isReady.value = false
console.log(props.bpmnModeler, 'props.bpmnModeler1')
}) })
watch( watch(
......
<template>
<div>
<el-divider content-position="left">审批人超时未处理时</el-divider>
<el-form-item label="启用开关" prop="timeoutHandlerEnable">
<el-switch
v-model="timeoutHandlerEnable"
active-text="开启"
inactive-text="关闭"
@change="timeoutHandlerChange"
/>
</el-form-item>
<el-form-item label="执行动作" prop="timeoutHandlerType" v-if="timeoutHandlerEnable">
<el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged">
<el-radio-button
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="超时时间设置" v-if="timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="timeDuration"
:min="1"
controls-position="right"
@change="() => updateTimeModdle()"
/>
</el-form-item>
<el-select
v-model="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="onTimeUnitChange"
>
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
未处理
</el-form-item>
<el-form-item
label="最大提醒次数"
prop="maxRemindCount"
v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
>
<el-input-number
v-model="maxRemindCount"
:min="1"
:max="10"
@change="() => updateTimeModdle()"
/>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
import {
TimeUnitType,
TIME_UNIT_TYPES,
TIMEOUT_HANDLER_TYPES,
} from '@/components/SimpleProcessDesignerV2/src/consts'
import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils'
defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' })
const props = defineProps({
id: String,
type: String
})
const prefix = inject('prefix')
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
const timeoutHandlerEnable = ref(false)
const boundaryEventType = ref()
const timeoutHandlerType = ref({
value: undefined
})
const timeModdle = ref()
const timeDuration = ref(6)
const timeUnit = ref(TimeUnitType.HOUR)
const maxRemindCount = ref(1)
const elExtensionElements = ref()
const otherExtensions = ref()
const configExtensions = ref([])
const eventDefinition = ref()
const resetElement = () => {
bpmnElement.value = bpmnInstances().bpmnElement
eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0]
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
// 是否开启自定义用户任务超时处理
boundaryEventType.value = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:BoundaryEventType`
)?.[0]
if (boundaryEventType.value && boundaryEventType.value.value === 1) {
timeoutHandlerEnable.value = true
configExtensions.value.push(boundaryEventType.value)
}
// 执行动作
timeoutHandlerType.value = elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:TimeoutHandlerType`
)?.[0]
if (timeoutHandlerType.value) {
configExtensions.value.push(timeoutHandlerType.value)
if (eventDefinition.value.timeCycle) {
const timeStr = eventDefinition.value.timeCycle.body
const maxRemindCountStr = timeStr.split('/')[0]
const timeDurationStr = timeStr.split('/')[1]
console.log(maxRemindCountStr)
maxRemindCount.value = parseInt(maxRemindCountStr.slice(1))
timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
timeModdle.value = eventDefinition.value.timeCycle
}
if (eventDefinition.value.timeDuration) {
const timeDurationStr = eventDefinition.value.timeDuration.body
timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
timeModdle.value = eventDefinition.value.timeDuration
}
}
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex) =>
ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType`
) ?? []
}
const timeoutHandlerChange = (val) => {
timeoutHandlerEnable.value = val
if (val) {
// 启用自定义用户任务超时处理
// 边界事件类型 --- 超时
boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, {
value: 1
})
configExtensions.value.push(boundaryEventType.value)
// 超时处理类型
timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, {
value: 1
})
configExtensions.value.push(timeoutHandlerType.value)
// 超时时间表达式
timeDuration.value = 6
timeUnit.value = 2
maxRemindCount.value = 1
timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, {
body: 'PT6H'
})
eventDefinition.value.timeDuration = timeModdle.value
} else {
// 关闭自定义用户任务超时处理
configExtensions.value = []
delete eventDefinition.value.timeDuration
delete eventDefinition.value.timeCycle
}
updateElementExtensions()
}
const onTimeoutHandlerTypeChanged = () => {
maxRemindCount.value = 1
updateElementExtensions()
updateTimeModdle()
}
const onTimeUnitChange = () => {
// 分钟,默认是 60 分钟
if (timeUnit.value === TimeUnitType.MINUTE) {
timeDuration.value = 60
}
// 小时,默认是 6 个小时
if (timeUnit.value === TimeUnitType.HOUR) {
timeDuration.value = 6
}
// 天, 默认 1天
if (timeUnit.value === TimeUnitType.DAY) {
timeDuration.value = 1
}
updateTimeModdle()
}
const updateTimeModdle = () => {
if (maxRemindCount.value > 1) {
timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration()
if (!eventDefinition.value.timeCycle) {
delete eventDefinition.value.timeDuration
eventDefinition.value.timeCycle = timeModdle.value
}
} else {
timeModdle.value.body = isoTimeDuration()
if (!eventDefinition.value.timeDuration) {
delete eventDefinition.value.timeCycle
eventDefinition.value.timeDuration = timeModdle.value
}
}
}
const isoTimeDuration = () => {
let strTimeDuration = 'PT'
if (timeUnit.value === TimeUnitType.MINUTE) {
strTimeDuration += timeDuration.value + 'M'
}
if (timeUnit.value === TimeUnitType.HOUR) {
strTimeDuration += timeDuration.value + 'H'
}
if (timeUnit.value === TimeUnitType.DAY) {
strTimeDuration += timeDuration.value + 'D'
}
return strTimeDuration
}
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...otherExtensions.value, ...configExtensions.value]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions
})
}
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetElement()
})
},
{ immediate: true }
)
</script>
<style lang="scss" scoped></style>
import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue'
import BoundaryEventTimer from './components/BoundaryEventTimer.vue'
export const CustomConfigMap = {
UserTask: {
name: '用户任务',
componet: UserTaskCustomConfig
},
BoundaryEventTimerEventDefinition: {
name: '定时边界事件(非中断)',
componet: BoundaryEventTimer
}
}
<template> <template>
<div class="panel-tab__content"> <div class="panel-tab__content">
<el-form label-width="90px"> <el-radio-group v-model="approveMethod" @change="onApproveMethodChange">
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<el-radio :value="item.value" :label="item.value">
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
<el-input-number
v-model="approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
@change="onApproveRatioChange"
/>
</el-form-item>
</div>
</div>
</el-radio-group>
<!-- 与Simple设计器配置合并,保留以前的代码 -->
<el-form label-width="90px" style="display: none">
<el-form-item label="快捷配置"> <el-form-item label="快捷配置">
<el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button> <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
<el-button size="small" @click="changeConfig('会签')">会签</el-button> <el-button size="small" @click="changeConfig('会签')">会签</el-button>
...@@ -76,11 +100,14 @@ ...@@ -76,11 +100,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ElementMultiInstance' }) defineOptions({ name: 'ElementMultiInstance' })
const props = defineProps({ const props = defineProps({
businessObject: Object, businessObject: Object,
type: String type: String,
id: String
}) })
const prefix = inject('prefix') const prefix = inject('prefix')
const loopCharacteristics = ref('') const loopCharacteristics = ref('')
...@@ -267,16 +294,118 @@ const changeConfig = (config) => { ...@@ -267,16 +294,118 @@ const changeConfig = (config) => {
} }
} }
/**
* -----新版本多实例-----
*/
const approveMethod = ref()
const approveRatio = ref(100)
const otherExtensions = ref()
const getElementLoopNew = () => {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`
)?.[0]?.value
otherExtensions.value =
extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
updateLoopCharacteristics()
}
}
const onApproveMethodChange = () => {
approveRatio.value = 100
updateLoopCharacteristics()
}
const onApproveRatioChange = () => {
updateLoopCharacteristics()
}
const updateLoopCharacteristics = () => {
// 根据ApproveMethod生成multiInstanceLoopCharacteristics节点
if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null
})
} else {
if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: false, collection: '${coll_userList}' }
)
multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}'
}
)
}
if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: false, collection: '${coll_userList}' }
)
multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '${ nrOfCompletedInstances > 0 }'
}
)
}
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
{ isSequential: true, collection: '${coll_userList}' }
)
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '1'
}
)
multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '${ nrOfCompletedInstances >= nrOfInstances }'
}
)
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value)
})
}
// 添加ApproveMethod到ExtensionElements
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, {
value: approveMethod.value
})
]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions
})
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
multiLoopInstance.value = null multiLoopInstance.value = null
bpmnElement.value = null bpmnElement.value = null
}) })
watch( watch(
() => props.businessObject, () => props.id,
(val) => { (val) => {
bpmnElement.value = bpmnInstances().bpmnElement if (val) {
getElementLoop(val) nextTick(() => {
bpmnElement.value = bpmnInstances().bpmnElement
// getElementLoop(val)
getElementLoopNew()
})
}
}, },
{ immediate: true } { immediate: true }
) )
......
...@@ -75,7 +75,6 @@ const attributeFormRef = ref() ...@@ -75,7 +75,6 @@ const attributeFormRef = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances const bpmnInstances = () => (window as any)?.bpmnInstances
const resetAttributesList = () => { const resetAttributesList = () => {
console.log(window, 'windowwindowwindowwindowwindowwindowwindow')
bpmnElement.value = bpmnInstances().bpmnElement bpmnElement.value = bpmnInstances().bpmnElement
otherExtensionList.value = [] // 其他扩展配置 otherExtensionList.value = [] // 其他扩展配置
bpmnElementProperties.value = bpmnElementProperties.value =
...@@ -85,7 +84,7 @@ const resetAttributesList = () => { ...@@ -85,7 +84,7 @@ const resetAttributesList = () => {
otherExtensionList.value.push(ex) otherExtensionList.value.push(ex)
} }
return ex.$type === `${prefix}:Properties` return ex.$type === `${prefix}:Properties`
}) ?? [] }) ?? [];
// 保存所有的 扩展属性字段 // 保存所有的 扩展属性字段
bpmnElementPropertyList.value = bpmnElementProperties.value.reduce( bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
......
...@@ -29,9 +29,7 @@ ...@@ -29,9 +29,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import UserTask from './task-components/UserTask.vue' import { installedComponent } from './data'
import ScriptTask from './task-components/ScriptTask.vue'
import ReceiveTask from './task-components/ReceiveTask.vue'
defineOptions({ name: 'ElementTaskConfig' }) defineOptions({ name: 'ElementTaskConfig' })
...@@ -45,14 +43,7 @@ const taskConfigForm = ref({ ...@@ -45,14 +43,7 @@ const taskConfigForm = ref({
exclusive: false exclusive: false
}) })
const witchTaskComponent = ref() const witchTaskComponent = ref()
const installedComponent = ref({
// 手工任务与普通任务一致,不需要其他配置
// 接收消息任务,需要在全局下插入新的消息实例,并在该节点下的 messageRef 属性绑定该实例
// 发送任务、服务任务、业务规则任务共用一个相同配置
UserTask: 'UserTask', // 用户任务配置
ScriptTask: 'ScriptTask', // 脚本任务配置
ReceiveTask: 'ReceiveTask' // 消息接收任务
})
const bpmnElement = ref() const bpmnElement = ref()
const bpmnInstances = () => (window as any).bpmnInstances const bpmnInstances = () => (window as any).bpmnInstances
...@@ -78,15 +69,8 @@ watch( ...@@ -78,15 +69,8 @@ watch(
watch( watch(
() => props.type, () => props.type,
() => { () => {
// witchTaskComponent.value = installedComponent.value[props.type] if (props.type) {
if (props.type == installedComponent.value.UserTask) { witchTaskComponent.value = installedComponent[props.type].component
witchTaskComponent.value = UserTask
}
if (props.type == installedComponent.value.ScriptTask) {
witchTaskComponent.value = ScriptTask
}
if (props.type == installedComponent.value.ReceiveTask) {
witchTaskComponent.value = ReceiveTask
} }
}, },
{ immediate: true } { immediate: true }
......
import UserTask from './task-components/UserTask.vue'
import ServiceTask from './task-components/ServiceTask.vue'
import ScriptTask from './task-components/ScriptTask.vue'
import ReceiveTask from './task-components/ReceiveTask.vue'
import CallActivity from './task-components/CallActivity.vue'
export const installedComponent = {
UserTask: {
name: '用户任务',
component: UserTask
},
ServiceTask: {
name: '服务任务',
component: ServiceTask
},
ScriptTask: {
name: '脚本任务',
component: ScriptTask
},
ReceiveTask: {
name: '接收任务',
component: ReceiveTask
},
CallActivity: {
name: '调用活动',
component: CallActivity
}
}
export const getTaskCollapseItemName = (elementType) => {
return installedComponent[elementType].name
}
export const isTaskCollapseItemShow = (elementType) => {
return installedComponent[elementType]
}
<template>
<div>
<el-form label-width="100px">
<el-form-item label="实例名称" prop="processInstanceName">
<el-input
v-model="formData.processInstanceName"
clearable
placeholder="请输入实例名称"
@change="updateCallActivityAttr('processInstanceName')"
/>
</el-form-item>
<!-- TODO 需要可选择已存在的流程 -->
<el-form-item label="被调用流程" prop="calledElement">
<el-input
v-model="formData.calledElement"
clearable
placeholder="请输入被调用流程"
@change="updateCallActivityAttr('calledElement')"
/>
</el-form-item>
<el-form-item label="继承变量" prop="inheritVariables">
<el-switch
v-model="formData.inheritVariables"
@change="updateCallActivityAttr('inheritVariables')"
/>
</el-form-item>
<el-form-item label="继承业务键" prop="inheritBusinessKey">
<el-switch
v-model="formData.inheritBusinessKey"
@change="updateCallActivityAttr('inheritBusinessKey')"
/>
</el-form-item>
<el-form-item v-if="!formData.inheritBusinessKey" label="业务键表达式" prop="businessKey">
<el-input
v-model="formData.businessKey"
clearable
placeholder="请输入业务键表达式"
@change="updateCallActivityAttr('businessKey')"
/>
</el-form-item>
<el-divider />
<div>
<div class="flex mb-10px">
<el-text>输入参数</el-text>
<XButton
class="ml-auto"
type="primary"
preIcon="ep:plus"
title="添加参数"
size="small"
@click="openVariableForm('in', null, -1)"
/>
</div>
<el-table :data="inVariableList" max-height="240" fit border>
<el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip />
<el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip />
<el-table-column label="操作" width="110px">
<template #default="scope">
<el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small">
编辑
</el-button>
<el-divider direction="vertical" />
<el-button
link
size="small"
style="color: #ff4d4f"
@click="removeVariable('in', scope.$index)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-divider />
<div>
<div class="flex mb-10px">
<el-text>输出参数</el-text>
<XButton
class="ml-auto"
type="primary"
preIcon="ep:plus"
title="添加参数"
size="small"
@click="openVariableForm('out', null, -1)"
/>
</div>
<el-table :data="outVariableList" max-height="240" fit border>
<el-table-column label="源" prop="source" min-width="100px" show-overflow-tooltip />
<el-table-column label="目标" prop="target" min-width="100px" show-overflow-tooltip />
<el-table-column label="操作" width="110px">
<template #default="scope">
<el-button
link
@click="openVariableForm('out', scope.row, scope.$index)"
size="small"
>
编辑
</el-button>
<el-divider direction="vertical" />
<el-button
link
size="small"
style="color: #ff4d4f"
@click="removeVariable('out', scope.$index)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<!-- 添加或修改参数 -->
<el-dialog
v-model="variableDialogVisible"
title="参数配置"
width="600px"
append-to-body
destroy-on-close
>
<el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef">
<el-form-item label="源:" prop="source">
<el-input v-model="varialbeFormData.source" clearable />
</el-form-item>
<el-form-item label="目标:" prop="target">
<el-input v-model="varialbeFormData.target" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="variableDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveVariable">确 定</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'CallActivity' })
const props = defineProps({
id: String,
type: String
})
const prefix = inject('prefix')
const message = useMessage()
const formData = ref({
processInstanceName: '',
calledElement: '',
inheritVariables: false,
businessKey: '',
inheritBusinessKey: false,
calledElementType: 'key'
})
const inVariableList = ref()
const outVariableList = ref()
const variableType = ref() // 参数类型
const editingVariableIndex = ref(-1) // 编辑参数下标
const variableDialogVisible = ref(false)
const varialbeFormRef = ref()
const varialbeFormData = ref({
source: '',
target: ''
})
const bpmnInstances = () => (window as any)?.bpmnInstances
const bpmnElement = ref()
const otherExtensionList = ref()
const initCallActivity = () => {
bpmnElement.value = bpmnInstances().bpmnElement
console.log(bpmnElement.value.businessObject, 'callActivity')
// 初始化所有配置项
Object.keys(formData.value).forEach((key) => {
formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key]
})
otherExtensionList.value = [] // 其他扩展配置
inVariableList.value = []
outVariableList.value = []
// 初始化输入参数
bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => {
if (ex.$type === `${prefix}:In`) {
inVariableList.value.push(ex)
} else if (ex.$type === `${prefix}:Out`) {
outVariableList.value.push(ex)
} else {
otherExtensionList.value.push(ex)
}
})
// 默认添加
// bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
// calledElementType: 'key'
// })
}
const updateCallActivityAttr = (attr) => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
[attr]: formData.value[attr]
})
}
const openVariableForm = (type, data, index) => {
editingVariableIndex.value = index
variableType.value = type
varialbeFormData.value = index === -1 ? {} : { ...data }
variableDialogVisible.value = true
}
const removeVariable = async (type, index) => {
try {
await message.delConfirm()
if (type === 'in') {
inVariableList.value.splice(index, 1)
}
if (type === 'out') {
outVariableList.value.splice(index, 1)
}
updateElementExtensions()
} catch {}
}
const saveVariable = () => {
if (editingVariableIndex.value === -1) {
if (variableType.value === 'in') {
inVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value })
)
}
if (variableType.value === 'out') {
outVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value })
)
}
updateElementExtensions()
} else {
if (variableType.value === 'in') {
inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
}
if (variableType.value === 'out') {
outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
}
}
variableDialogVisible.value = false
}
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions
})
}
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
initCallActivity()
})
},
{ immediate: true }
)
</script>
<style lang="scss" scoped></style>
<template>
<div>
<el-form-item label="执行类型" key="executeType">
<el-select v-model="serviceTaskForm.executeType">
<el-option label="Java类" value="class" />
<el-option label="表达式" value="expression" />
<el-option label="代理表达式" value="delegateExpression" />
</el-select>
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
prop="class"
key="execute-class"
>
<el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" />
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
prop="expression"
key="execute-expression"
>
<el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" />
</el-form-item>
<el-form-item
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
prop="delegateExpression"
key="execute-delegate"
>
<el-input v-model="serviceTaskForm.delegateExpression" clearable @change="updateElementTask" />
</el-form-item>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ServiceTask' })
const props = defineProps({
id: String,
type: String
})
const defaultTaskForm = ref({
executeType: '',
class: '',
expression: '',
delegateExpression: ''
})
const serviceTaskForm = ref<any>({})
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
const resetTaskForm = () => {
for (let key in defaultTaskForm.value) {
let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
serviceTaskForm.value[key] = value
if (value) {
serviceTaskForm.value.executeType = key
}
}
}
const updateElementTask = () => {
let taskAttr = Object.create(null);
const type = serviceTaskForm.value.executeType;
for (let key in serviceTaskForm.value) {
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
}
taskAttr[type] = serviceTaskForm.value[type] || "";
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
}
onBeforeUnmount(() => {
bpmnElement.value = null
})
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => {
resetTaskForm()
})
},
{ immediate: true }
)
</script>
...@@ -5,18 +5,10 @@ const { t } = useI18n() // 国际化 ...@@ -5,18 +5,10 @@ const { t } = useI18n() // 国际化
export function hasPermi(app: App<Element>) { export function hasPermi(app: App<Element>) {
app.directive('hasPermi', (el, binding) => { app.directive('hasPermi', (el, binding) => {
const { wsCache } = useCache()
const { value } = binding const { value } = binding
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
if (value && value instanceof Array && value.length > 0) { if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value const hasPermissions = hasPermission(value)
const hasPermissions = permissions.some((permission: string) => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) { if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el) el.parentNode && el.parentNode.removeChild(el)
...@@ -26,3 +18,14 @@ export function hasPermi(app: App<Element>) { ...@@ -26,3 +18,14 @@ export function hasPermi(app: App<Element>) {
} }
}) })
} }
export const hasPermission = (permission: string[]) => {
const { wsCache } = useCache()
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
return permissions.some((p: string) => {
return all_permission === p || permission.includes(p)
})
}
\ No newline at end of file
...@@ -12,6 +12,9 @@ const prefixCls = getPrefixCls('footer') ...@@ -12,6 +12,9 @@ const prefixCls = getPrefixCls('footer')
const appStore = useAppStore() const appStore = useAppStore()
const title = computed(() => appStore.getTitle) const title = computed(() => appStore.getTitle)
// 添加当前年份计算属性
const currentYear = computed(() => new Date().getFullYear())
</script> </script>
<template> <template>
...@@ -19,6 +22,6 @@ const title = computed(() => appStore.getTitle) ...@@ -19,6 +22,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls" :class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden" class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
> >
<span class="text-14px">Copyright ©2022-{{ title }}</span> <span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span>
</div> </div>
</template> </template>
...@@ -140,7 +140,10 @@ export default { ...@@ -140,7 +140,10 @@ export default {
btnQRCode: 'QR code sign in', btnQRCode: 'QR code sign in',
qrcode: 'Scan the QR code to log in', qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up', btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent' SmsSendMsg: 'code has been sent',
resetPassword: "Reset Password",
resetPasswordSuccess: "Reset Password Success",
invalidTenantName:"Invalid Tenant Name"
}, },
captcha: { captcha: {
verification: 'Please complete security verification', verification: 'Please complete security verification',
......
...@@ -141,7 +141,10 @@ export default { ...@@ -141,7 +141,10 @@ export default {
btnQRCode: '二维码登录', btnQRCode: '二维码登录',
qrcode: '扫描二维码登录', qrcode: '扫描二维码登录',
btnRegister: '注册', btnRegister: '注册',
SmsSendMsg: '验证码已发送' SmsSendMsg: '验证码已发送',
resetPassword: "重置密码",
resetPasswordSuccess: "重置密码成功",
invalidTenantName: "无效的租户名称"
}, },
captcha: { captcha: {
verification: '请完成安全验证', verification: '请完成安全验证',
......
...@@ -330,6 +330,30 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -330,6 +330,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
title: '查看 OA 请假', title: '查看 OA 请假',
activeMenu: '/bpm/oa/leave' activeMenu: '/bpm/oa/leave'
} }
},
{
path: 'manager/model/create',
component: () => import('@/views/bpm/model/form/index.vue'),
name: 'BpmModelCreate',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '创建流程',
activeMenu: '/bpm/manager/model'
}
},
{
path: 'manager/model/update/:id',
component: () => import('@/views/bpm/model/form/index.vue'),
name: 'BpmModelUpdate',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '修改流程',
activeMenu: '/bpm/manager/model'
}
} }
] ]
}, },
......
...@@ -73,7 +73,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord ...@@ -73,7 +73,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
noCache: !route.keepAlive, noCache: !route.keepAlive,
alwaysShow: alwaysShow:
route.children && route.children &&
route.children.length === 1 && route.children.length > 0 &&
(route.alwaysShow !== undefined ? route.alwaysShow : true) (route.alwaysShow !== undefined ? route.alwaysShow : true)
} as any } as any
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数 // 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
...@@ -100,7 +100,6 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord ...@@ -100,7 +100,6 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
//处理顶级非目录路由 //处理顶级非目录路由
if (!route.children && route.parentId == 0 && route.component) { if (!route.children && route.parentId == 0 && route.component) {
data.component = Layout data.component = Layout
data.meta = {}
data.name = toCamelCase(route.path, true) + 'Parent' data.name = toCamelCase(route.path, true) + 'Parent'
data.redirect = '' data.redirect = ''
meta.alwaysShow = true meta.alwaysShow = true
......
...@@ -376,6 +376,9 @@ export const treeToString = (tree: any[], nodeId) => { ...@@ -376,6 +376,9 @@ export const treeToString = (tree: any[], nodeId) => {
let str = '' let str = ''
function performAThoroughValidation(arr) { function performAThoroughValidation(arr) {
if (typeof arr === 'undefined' || !Array.isArray(arr) || arr.length === 0) {
return false
}
for (const item of arr) { for (const item of arr) {
if (item.id === nodeId) { if (item.id === nodeId) {
str += ` / ${item.name}` str += ` / ${item.name}`
......
...@@ -59,6 +59,8 @@ ...@@ -59,6 +59,8 @@
<RegisterForm 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)" /> <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 忘记密码 -->
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div> </div>
</Transition> </Transition>
</div> </div>
...@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app' ...@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch' import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown' import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components' import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
defineOptions({ name: 'Login' }) defineOptions({ name: 'Login' })
......
...@@ -133,6 +133,7 @@ ...@@ -133,6 +133,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<Verify <Verify
v-if="loginData.captchaEnable === 'true'"
ref="verify" ref="verify"
:captchaType="captchaType" :captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }" :imgSize="{ width: '400px', height: '200px' }"
......
<template>
<el-form
v-show="getShow"
ref="formSmsResetPassword"
:model="resetPasswordData"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<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="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="resetPasswordData.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item>
</el-col>
<!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mobile">
<el-input
v-model="resetPasswordData.mobile"
:placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone"
/>
</el-form-item>
</el-col>
<Verify
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="getSmsCode"
/>
<!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="resetPasswordData.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<template #append>
<span
v-if="mobileCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getCode"
>
{{ t('login.getSmsCode') }}
</span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
{{ mobileCodeTimer }}秒后可重新获取
</span>
</template>
</el-input>
<!-- </el-button> -->
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<InputPassword
v-model="resetPasswordData.password"
:placeholder="t('login.passwordPlaceholder')"
style="width: 100%"
strength="true"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="check_password">
<InputPassword
v-model="resetPasswordData.check_password"
:placeholder="t('login.checkPassword')"
style="width: 100%"
strength="true"
/>
</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.resetPassword')"
class="w-[100%]"
type="primary"
@click="resetPassword()"
/>
</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.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import { sendSmsCode, smsResetPassword } from '@/api/login'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
import { ElLoading } from 'element-plus'
import * as authUtil from '@/utils/auth'
import * as LoginApi from '@/api/login'
defineOptions({ name: 'ForgetPasswordForm' })
const verify = ref()
const { t } = useI18n()
const message = useMessage()
const { currentRoute, push } = useRouter()
const formSmsResetPassword = ref()
const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsResetPassword)
const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== resetPasswordData.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const rules = {
tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
password: [
{
required: true,
min: 4,
max: 16,
validator: validatePass2,
trigger: 'blur',
message: '密码长度为4到16位'
}
],
check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
code: [required]
}
const resetPasswordData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
tenantName: '',
username: '',
password: '',
check_password: '',
mobile: '',
code: ''
})
const smsVO = reactive({
tenantName: '',
mobile: '',
captchaVerification: '',
scene: 23
})
const mobileCodeTimer = ref(0)
const redirect = ref<string>('')
// 获取验证码
const getCode = async () => {
// 情况一,未开启:则直接发送验证码
if (resetPasswordData.captchaEnable === 'false') {
await getSmsCode({})
} else {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行发送验证码
// 弹出验证码
verify.value.show()
}
}
const getSmsCode = async (params) => {
if (resetPasswordData.tenantEnable === 'true') {
await getTenantId()
}
smsVO.captchaVerification = params.captchaVerification
smsVO.mobile = resetPasswordData.mobile
await sendSmsCode(smsVO).then(async () => {
message.success(t('login.SmsSendMsg'))
// 设置倒计时
mobileCodeTimer.value = 60
let msgTimer = setInterval(() => {
mobileCodeTimer.value = mobileCodeTimer.value - 1
if (mobileCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
const getTenantId = async () => {
if (resetPasswordData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
if (res == null) {
message.error(t('login.invalidTenantName'))
throw t('login.invalidTenantName')
}
authUtil.setTenantId(res)
}
}
// 重置密码
const resetPassword = async () => {
const data = await validForm()
if (!data) return
await getTenantId()
loginLoading.value = true
await smsResetPassword(resetPasswordData)
.then(async () => {
message.success(t('login.resetPasswordSuccess'))
setLoginState(LoginStateEnum.LOGIN)
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>
...@@ -59,7 +59,13 @@ ...@@ -59,7 +59,13 @@
</el-checkbox> </el-checkbox>
</el-col> </el-col>
<el-col :offset="6" :span="12"> <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"
@click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
>
{{ t('login.forgetPassword') }}
</el-link>
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
...@@ -76,6 +82,7 @@ ...@@ -76,6 +82,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<Verify <Verify
v-if="loginData.captchaEnable === 'true'"
ref="verify" ref="verify"
:captchaType="captchaType" :captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }" :imgSize="{ width: '400px', height: '200px' }"
...@@ -241,7 +248,7 @@ const getTenantByWebsite = async () => { ...@@ -241,7 +248,7 @@ const getTenantByWebsite = async () => {
} }
const loading = ref() // ElLoading.service 返回的实例 const loading = ref() // ElLoading.service 返回的实例
// 登录 // 登录
const handleLogin = async (params) => { const handleLogin = async (params: any) => {
loginLoading.value = true loginLoading.value = true
try { try {
await getTenantId() await getTenantId()
...@@ -273,7 +280,7 @@ const handleLogin = async (params) => { ...@@ -273,7 +280,7 @@ const handleLogin = async (params) => {
if (redirect.value.indexOf('sso') !== -1) { if (redirect.value.indexOf('sso') !== -1) {
window.location.href = window.location.href.replace('/login?redirect=', '') window.location.href = window.location.href.replace('/login?redirect=', '')
} else { } else {
push({ path: redirect.value || permissionStore.addRouters[0].path }) await push({ path: redirect.value || permissionStore.addRouters[0].path })
} }
} finally { } finally {
loginLoading.value = false loginLoading.value = false
...@@ -313,8 +320,7 @@ const doSocialLogin = async (type: number) => { ...@@ -313,8 +320,7 @@ const doSocialLogin = async (type: number) => {
encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`) encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
// 进行跳转 // 进行跳转
const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri)) window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
window.location.href = res
} }
} }
watch( watch(
......
...@@ -85,6 +85,7 @@ ...@@ -85,6 +85,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<Verify <Verify
v-if="registerData.captchaEnable === 'true'"
ref="verify" ref="verify"
:captchaType="captchaType" :captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }" :imgSize="{ width: '400px', height: '200px' }"
......
...@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue' ...@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue'
import RegisterForm from './RegisterForm.vue' import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue' import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue' import SSOLoginVue from './SSOLogin.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue } export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 模型列表 --> <!-- 模型列表 -->
<el-collapse-transition> <el-collapse-transition>
<div v-show="isExpand"> <div v-show="isExpand">
...@@ -90,7 +91,7 @@ ...@@ -90,7 +91,7 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="可见范围" prop="startUserIds" min-width="100"> <el-table-column label="可见范围" prop="startUserIds" min-width="150">
<template #default="scope"> <template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0"> <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见 全部可见
...@@ -110,7 +111,7 @@ ...@@ -110,7 +111,7 @@
</el-text> </el-text>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="表单信息" prop="formType" min-width="200"> <el-table-column label="表单信息" prop="formType" min-width="150">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="scope.row.formType === BpmModelFormType.NORMAL" v-if="scope.row.formType === BpmModelFormType.NORMAL"
...@@ -166,16 +167,6 @@ ...@@ -166,16 +167,6 @@
link link
class="!ml-5px" class="!ml-5px"
type="primary" 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)" @click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']" v-hasPermi="['bpm:model:deploy']"
:disabled="!isManagerUser(scope.row)" :disabled="!isManagerUser(scope.row)"
...@@ -249,7 +240,7 @@ import { formatDate } from '@/utils/formatTime' ...@@ -249,7 +240,7 @@ import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model' import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form' import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate' import { setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelFormType, BpmModelType } from '@/utils/constants' import { BpmModelFormType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission' import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user' import { useUserStoreWithOut } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
...@@ -337,25 +328,6 @@ const handleChangeState = async (row: any) => { ...@@ -337,25 +328,6 @@ const handleChangeState = async (row: any) => {
} catch {} } 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) => { const handleDeploy = async (row: any) => {
try { try {
...@@ -496,7 +468,14 @@ const handleDeleteCategory = async () => { ...@@ -496,7 +468,14 @@ const handleDeleteCategory = async () => {
/** 添加流程模型弹窗 */ /** 添加流程模型弹窗 */
const modelFormRef = ref() const modelFormRef = ref()
const openModelForm = (type: string, id?: number) => { const openModelForm = (type: string, id?: number) => {
modelFormRef.value.open(type, id) if (type === 'create') {
push({ name: 'BpmModelCreate' })
} else {
push({
name: 'BpmModelUpdate',
params: { id }
})
}
} }
watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true }) watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
......
...@@ -123,29 +123,69 @@ ...@@ -123,29 +123,69 @@
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="谁可以发起" prop="startUserIds"> <el-form-item label="谁可以发起" prop="startUserType">
<el-select <el-select
v-model="formData.startUserIds" v-model="formData.startUserType"
multiple placeholder="请选择谁可以发起"
placeholder="请选择可发起人,默认(不选择)则所有人都可以发起" @change="handleStartUserTypeChange"
> >
<el-option <el-option label="全员" :value="0" />
v-for="user in userList" <el-option label="指定人员" :value="1" />
:key="user.id" <el-option label="均不可提交" :value="2" />
:label="user.nickname"
:value="user.id"
/>
</el-select> </el-select>
</el-form-item> <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
<el-form-item label="流程管理员" prop="managerUserIds"> <div
<el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员"> v-for="user in selectedStartUsers"
<el-option
v-for="user in userList"
:key="user.id" :key="user.id"
:label="user.nickname" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
:value="user.id" >
/> <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
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveStartUser(user)"
/>
</div>
<el-button type="primary" link @click="openStartUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserType">
<el-select
v-model="formData.managerUserType"
placeholder="请选择流程管理员"
@change="handleManagerUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select> </el-select>
<div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
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
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveManagerUser(user)"
/>
</div>
<el-button type="primary" link @click="openManagerUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
...@@ -153,6 +193,7 @@ ...@@ -153,6 +193,7 @@
<el-button @click="dialogVisible = false">取 消</el-button> <el-button @click="dialogVisible = false">取 消</el-button>
</template> </template>
</Dialog> </Dialog>
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
...@@ -160,11 +201,12 @@ import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict' ...@@ -160,11 +201,12 @@ import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model' import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form' import * as FormApi from '@/api/bpm/form'
import { CategoryApi } from '@/api/bpm/category' import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { BpmModelFormType, BpmModelType } from '@/utils/constants' import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { UserVO } from '@/api/system/user' import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user' import { useUserStoreWithOut } from '@/store/modules/user'
import { FormVO } from '@/api/bpm/form'
defineOptions({ name: 'ModelForm' }) defineOptions({ name: 'ModelForm' })
...@@ -178,7 +220,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示 ...@@ -178,7 +220,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题 const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改 const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({ const formData: any = ref({
id: undefined, id: undefined,
name: '', name: '',
key: '', key: '',
...@@ -191,6 +233,8 @@ const formData = ref({ ...@@ -191,6 +233,8 @@ const formData = ref({
formCustomCreatePath: '', formCustomCreatePath: '',
formCustomViewPath: '', formCustomViewPath: '',
visible: true, visible: true,
startUserType: undefined,
managerUserType: undefined,
startUserIds: [], startUserIds: [],
managerUserIds: [] managerUserIds: []
}) })
...@@ -208,9 +252,13 @@ const formRules = reactive({ ...@@ -208,9 +252,13 @@ const formRules = reactive({
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }] managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
const formList = ref([]) // 流程表单的下拉框的数据 const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据
const categoryList = ref([]) // 流程分类列表 const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
const userList = ref<UserVO[]>([]) // 用户列表 const userList = ref<UserVO[]>([]) // 用户列表
const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表
const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表
const userSelectFormRef = ref() // 用户选择弹窗 ref
const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: string) => { const open = async (type: string, id?: string) => {
...@@ -226,6 +274,19 @@ const open = async (type: string, id?: string) => { ...@@ -226,6 +274,19 @@ const open = async (type: string, id?: string) => {
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
// 加载数据时,根据已有的用户ID列表初始化已选用户
if (formData.value.startUserIds?.length) {
formData.value.startUserType = 1
selectedStartUsers.value = userList.value.filter((user) =>
formData.value.startUserIds.includes(user.id)
)
}
if (formData.value.managerUserIds?.length) {
formData.value.managerUserType = 1
selectedManagerUsers.value = userList.value.filter((user) =>
formData.value.managerUserIds.includes(user.id)
)
}
} else { } else {
formData.value.managerUserIds.push(userStore.getUser.id) formData.value.managerUserIds.push(userStore.getUser.id)
} }
...@@ -293,9 +354,87 @@ const resetForm = () => { ...@@ -293,9 +354,87 @@ const resetForm = () => {
formCustomCreatePath: '', formCustomCreatePath: '',
formCustomViewPath: '', formCustomViewPath: '',
visible: true, visible: true,
startUserType: undefined,
managerUserType: undefined,
startUserIds: [], startUserIds: [],
managerUserIds: [] managerUserIds: []
} }
formRef.value?.resetFields() formRef.value?.resetFields()
selectedStartUsers.value = []
selectedManagerUsers.value = []
}
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: number) => {
if (value !== 1) {
selectedStartUsers.value = []
formData.value.startUserIds = []
}
}
/** 处理管理员类型变化 */
const handleManagerUserTypeChange = (value: number) => {
if (value !== 1) {
selectedManagerUsers.value = []
formData.value.managerUserIds = []
}
}
/** 打开发起人选择 */
const openStartUserSelect = () => {
currentSelectType.value = 'start'
userSelectFormRef.value.open(0, selectedStartUsers.value)
}
/** 打开管理员选择 */
const openManagerUserSelect = () => {
currentSelectType.value = 'manager'
userSelectFormRef.value.open(0, selectedManagerUsers.value)
}
/** 处理用户选择确认 */
const handleUserSelectConfirm = (_, users: UserVO[]) => {
if (currentSelectType.value === 'start') {
selectedStartUsers.value = users
formData.value.startUserIds = users.map((u) => u.id)
} else {
selectedManagerUsers.value = users
formData.value.managerUserIds = users.map((u) => u.id)
}
}
/** 移除发起人 */
const handleRemoveStartUser = (user: UserVO) => {
selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id)
}
/** 移除管理员 */
const handleRemoveManagerUser = (user: UserVO) => {
selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
formData.value.managerUserIds = formData.value.managerUserIds.filter(
(id: number) => id !== user.id
)
} }
</script> </script>
<style lang="scss" scoped>
.bg-gray-100 {
background-color: #f5f7fa;
transition: all 0.3s;
&:hover {
background-color: #e6e8eb;
}
.ep-close {
font-size: 14px;
color: #909399;
transition: color 0.3s;
&:hover {
color: #f56c6c;
}
}
}
</style>
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
<!-- 流程设计器,负责绘制流程等 --> <!-- 流程设计器,负责绘制流程等 -->
<MyProcessDesigner <MyProcessDesigner
key="designer" key="designer"
v-if="xmlString !== undefined"
v-model="xmlString" v-model="xmlString"
:value="xmlString" :value="xmlString"
v-bind="controlForm" v-bind="controlForm"
...@@ -11,12 +10,14 @@ ...@@ -11,12 +10,14 @@
ref="processDesigner" ref="processDesigner"
@init-finished="initModeler" @init-finished="initModeler"
:additionalModel="controlForm.additionalModel" :additionalModel="controlForm.additionalModel"
:model="model"
@save="save" @save="save"
/> />
<!-- 流程属性器,负责编辑每个流程节点的属性 --> <!-- 流程属性器,负责编辑每个流程节点的属性 -->
<MyProcessPenal <MyProcessPenal
v-if="isModelerReady && modeler"
key="penal" key="penal"
:bpmnModeler="modeler as any" :bpmnModeler="modeler"
:prefix="controlForm.prefix" :prefix="controlForm.prefix"
class="process-panel" class="process-panel"
:model="model" :model="model"
...@@ -34,12 +35,26 @@ import * as ModelApi from '@/api/bpm/model' ...@@ -34,12 +35,26 @@ import * as ModelApi from '@/api/bpm/model'
defineOptions({ name: 'BpmModelEditor' }) defineOptions({ name: 'BpmModelEditor' })
const router = useRouter() // 路由 const props = defineProps<{
const { query } = useRoute() // 路由的查询 modelId?: string
modelKey?: string
modelName?: string
value?: string
}>()
const emit = defineEmits(['success', 'init-finished'])
const message = useMessage() // 国际化 const message = useMessage() // 国际化
const xmlString = ref(undefined) // BPMN XML // 表单信息
const modeler = ref(null) // BPMN Modeler const formFields = ref<string[]>([])
const formType = ref(20)
provide('formFields', formFields)
provide('formType', formType)
const xmlString = ref<string>('') // BPMN XML
const modeler = shallowRef() // BPMN Modeler
const processDesigner = ref()
const isModelerReady = ref(false)
const controlForm = ref({ const controlForm = ref({
simulation: true, simulation: true,
labelEditing: false, labelEditing: false,
...@@ -50,66 +65,215 @@ const controlForm = ref({ ...@@ -50,66 +65,215 @@ const controlForm = ref({
}) })
const model = ref<ModelApi.ModelVO>() // 流程模型的信息 const model = ref<ModelApi.ModelVO>() // 流程模型的信息
// 初始化 bpmnInstances
const initBpmnInstances = () => {
if (!modeler.value) return false
try {
const instances = {
modeler: modeler.value,
modeling: modeler.value.get('modeling'),
moddle: modeler.value.get('moddle'),
eventBus: modeler.value.get('eventBus'),
bpmnFactory: modeler.value.get('bpmnFactory'),
elementFactory: modeler.value.get('elementFactory'),
elementRegistry: modeler.value.get('elementRegistry'),
replace: modeler.value.get('replace'),
selection: modeler.value.get('selection')
}
// 检查所有实例是否都存在
return Object.values(instances).every((instance) => instance)
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error)
return false
}
}
/** 初始化 modeler */ /** 初始化 modeler */
const initModeler = (item) => { const initModeler = async (item) => {
setTimeout(() => { try {
modeler.value = item modeler.value = item
}, 10) // 等待 modeler 初始化完成
await nextTick()
// 确保 modeler 的所有实例都已经准备好
if (initBpmnInstances()) {
isModelerReady.value = true
emit('init-finished')
// 初始化完成后,设置初始值
if (props.modelId) {
// 编辑模式
const data = await ModelApi.getModel(props.modelId)
model.value = {
...data,
bpmnXml: undefined // 清空 bpmnXml 属性
}
xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name)
} else if (props.modelKey && props.modelName) {
// 新建模式
xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName)
model.value = {
key: props.modelKey,
name: props.modelName
} as ModelApi.ModelVO
}
// 导入XML并刷新视图
await nextTick()
try {
await modeler.value.importXML(xmlString.value)
if (processDesigner.value?.refresh) {
processDesigner.value.refresh()
}
} catch (error) {
console.error('导入XML失败:', error)
}
} else {
console.error('modeler 实例未完全初始化')
}
} catch (error) {
console.error('初始化 modeler 失败:', error)
}
}
/** 获取默认的BPMN XML */
const getDefaultBpmnXml = (key: string, name: string) => {
return `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
<process id="${key}" name="${name}" isExecutable="true" />
<bpmndi:BPMNDiagram id="BPMNDiagram">
<bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" />
</bpmndi:BPMNDiagram>
</definitions>`
} }
/** 添加/修改模型 */ /** 添加/修改模型 */
const save = async (bpmnXml: string) => { const save = async (bpmnXml: string) => {
const data = { try {
...model.value, xmlString.value = bpmnXml
bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得 if (props.modelId) {
} as unknown as ModelApi.ModelVO // 编辑模式
// 提交 const data = {
if (data.id) { ...model.value,
await ModelApi.updateModelBpmn(data) bpmnXml: bpmnXml
message.success('修改成功') } as unknown as ModelApi.ModelVO
} else { await ModelApi.updateModelBpmn(data)
await ModelApi.updateModelBpmn(data) emit('success')
message.success('新增成功') } else {
// 新建模式,直接返回XML
emit('success', bpmnXml)
}
} catch (error) {
console.error('保存失败:', error)
message.error('保存失败')
} }
// 跳转回去
close()
} }
/** 关闭按钮 */ // 监听 key、name 和 value 的变化
const close = () => { watch(
router.push({ path: '/bpm/manager/model' }) [() => props.modelKey, () => props.modelName, () => props.value],
async ([newKey, newName, newValue]) => {
if (!props.modelId && isModelerReady.value) {
let shouldRefresh = false
if (newKey && newName) {
const newXml = newValue || getDefaultBpmnXml(newKey, newName)
if (newXml !== xmlString.value) {
xmlString.value = newXml
shouldRefresh = true
}
model.value = {
...model.value,
key: newKey,
name: newName
} as ModelApi.ModelVO
} else if (newValue && newValue !== xmlString.value) {
xmlString.value = newValue
shouldRefresh = true
}
if (shouldRefresh) {
// 确保更新后重新渲染
await nextTick()
if (processDesigner.value?.refresh) {
try {
await modeler.value?.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('导入XML失败:', error)
}
}
}
}
},
{ deep: true }
)
// 在组件卸载时清理
onBeforeUnmount(() => {
isModelerReady.value = false
modeler.value = null
// 清理全局实例
const w = window as any
if (w.bpmnInstances) {
w.bpmnInstances = null
}
})
/** 获取 XML 字符串 */
const saveXML = async () => {
if (!modeler.value) {
return { xml: xmlString.value }
}
try {
const result = await modeler.value.saveXML({ format: true })
xmlString.value = result.xml
return result
} catch (error) {
console.error('获取XML失败:', error)
return { xml: xmlString.value }
}
} }
/** 初始化 */ /** 获取SVG字符串 */
onMounted(async () => { const saveSVG = async () => {
const modelId = query.modelId as unknown as number if (!modeler.value) {
if (!modelId) { return { svg: undefined }
message.error('缺少模型 modelId 编号')
return
} }
// 查询模型 try {
const data = await ModelApi.getModel(modelId) return await modeler.value.saveSVG()
if (!data.bpmnXml) { } catch (error) {
// 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的 console.error('获取SVG失败:', error)
data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?> return { svg: undefined }
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
<process id="${data.key}" name="${data.name}" isExecutable="true" />
<bpmndi:BPMNDiagram id="BPMNDiagram">
<bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" />
</bpmndi:BPMNDiagram>
</definitions>`
} }
model.value = { }
...data,
bpmnXml: undefined // 清空 bpmnXml 属性 /** 刷新视图 */
const refresh = async () => {
if (processDesigner.value?.refresh && modeler.value) {
try {
await modeler.value.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('刷新视图失败:', error)
}
} }
xmlString.value = data.bpmnXml }
// 暴露必要的属性和方法给父组件
defineExpose({
modeler,
isModelerReady,
saveXML,
saveSVG,
refresh
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
.process-panel__container { .process-panel__container {
position: absolute; position: absolute;
top: 90px; top: 172px;
right: 60px; right: 70px;
} }
</style> </style>
<template>
<el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
<el-form-item label="流程标识" prop="key" class="mb-20px">
<div class="flex items-center">
<el-input
class="!w-440px"
v-model="modelData.key"
:disabled="!!modelData.id"
placeholder="请输入流标标识"
/>
<el-tooltip
class="item"
:content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'"
effect="light"
placement="top"
>
<Icon icon="ep:question-filled" class="ml-5px" />
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="流程名称" prop="name" class="mb-20px">
<el-input
v-model="modelData.name"
:disabled="!!modelData.id"
clearable
placeholder="请输入流程名称"
/>
</el-form-item>
<el-form-item label="流程分类" prop="category" class="mb-20px">
<el-select
class="!w-full"
v-model="modelData.category"
clearable
placeholder="请选择流程分类"
>
<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="icon" class="mb-20px">
<UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" />
</el-form-item>
<el-form-item label="流程描述" prop="description" class="mb-20px">
<el-input v-model="modelData.description" clearable type="textarea" />
</el-form-item>
<el-form-item label="流程类型" prop="type" class="mb-20px">
<el-radio-group v-model="modelData.type">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否可见" prop="visible" class="mb-20px">
<el-radio-group v-model="modelData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="谁可以发起" prop="startUserType" class="mb-20px">
<el-select
v-model="modelData.startUserType"
placeholder="请选择谁可以发起"
@change="handleStartUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
v-for="user in selectedStartUsers"
:key="user.id"
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
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveStartUser(user)"
/>
</div>
<el-button type="primary" link @click="openStartUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserType" class="mb-20px">
<el-select
v-model="modelData.managerUserType"
placeholder="请选择流程管理员"
@change="handleManagerUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
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
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveManagerUser(user)"
/>
</div>
<el-button type="primary" link @click="openManagerUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item>
</el-form>
<!-- 用户选择弹窗 -->
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { UserVO } from '@/api/system/user'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
categoryList: {
type: Array,
required: true
},
userList: {
type: Array,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref()
const selectedStartUsers = ref<UserVO[]>([])
const selectedManagerUsers = ref<UserVO[]>([])
const userSelectFormRef = ref()
const currentSelectType = ref<'start' | 'manager'>('start')
const rules = {
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' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
}
// 创建本地数据副本
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// 初始化选中的用户
watch(
() => props.modelValue,
(newVal) => {
if (newVal.startUserIds?.length) {
selectedStartUsers.value = props.userList.filter((user: UserVO) =>
newVal.startUserIds.includes(user.id)
) as UserVO[]
}
if (newVal.managerUserIds?.length) {
selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
newVal.managerUserIds.includes(user.id)
) as UserVO[]
}
},
{ immediate: true }
)
/** 打开发起人选择 */
const openStartUserSelect = () => {
currentSelectType.value = 'start'
userSelectFormRef.value.open(0, selectedStartUsers.value)
}
/** 打开管理员选择 */
const openManagerUserSelect = () => {
currentSelectType.value = 'manager'
userSelectFormRef.value.open(0, selectedManagerUsers.value)
}
/** 处理用户选择确认 */
const handleUserSelectConfirm = (_, users: UserVO[]) => {
if (currentSelectType.value === 'start') {
selectedStartUsers.value = users
emit('update:modelValue', {
...modelData.value,
startUserIds: users.map((u) => u.id)
})
} else {
selectedManagerUsers.value = users
emit('update:modelValue', {
...modelData.value,
managerUserIds: users.map((u) => u.id)
})
}
}
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: number) => {
if (value !== 1) {
selectedStartUsers.value = []
emit('update:modelValue', {
...modelData.value,
startUserIds: []
})
}
}
/** 处理管理员类型变化 */
const handleManagerUserTypeChange = (value: number) => {
if (value !== 1) {
selectedManagerUsers.value = []
emit('update:modelValue', {
...modelData.value,
managerUserIds: []
})
}
}
/** 移除发起人 */
const handleRemoveStartUser = (user: UserVO) => {
selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
...modelData.value,
startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
})
}
/** 移除管理员 */
const handleRemoveManagerUser = (user: UserVO) => {
selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
...modelData.value,
managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
})
}
/** 表单校验 */
const validate = async () => {
await formRef.value?.validate()
}
defineExpose({
validate
})
</script>
<style lang="scss" scoped>
.bg-gray-100 {
background-color: #f5f7fa;
transition: all 0.3s;
&:hover {
background-color: #e6e8eb;
}
.ep-close {
font-size: 14px;
color: #909399;
transition: color 0.3s;
&:hover {
color: #f56c6c;
}
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment